返回 CodeWhale
pet-native.js
根目录 / crates / tui / src / tui / pet_watch / pet-native.js
1 /* Generated from pet/src/core. Run npm --prefix pet run sync. */
2 (function(global){
3 'use strict';
4 if(!global.structuredClone)global.structuredClone=value=>JSON.parse(JSON.stringify(value));
5 const factories={},cache={};
6 factories["pet-native"]=function(exports,require){
7 "use strict";
8 Object.defineProperty(exports, "__esModule", { value: true });
9 exports.PetNative = void 0;
10 const pet_world_js_1 = require("./pet-world.js");
11 const pet_telemetry_js_1 = require("./pet-telemetry.js");
12 const pet_sim_js_1 = require("./pet-sim.js");
13 const pet_audio_js_1 = require("./pet-audio.js");
14 const pet_engine_js_1 = require("./pet-engine.js");
15 /** Synchronous native boundary: JSON state and directly transferable PCM.
16 * Native hosts share the actual world / score implementation, not a rewrite. */
17 class PetNative {
18 world;
19 engine = new pet_engine_js_1.PetEngineTelemetry();
20 engineTick = 0;
21 segment;
22 liveTape = new pet_telemetry_js_1.PetLiveTape();
23 stillProjection;
24 constructor(pointsJSON, tapeJSONL = '', interactionsJSON = '[]', live = false, expressionVersion = 2) {
25 const points = JSON.parse(pointsJSON);
26 if (!Array.isArray(points) || points.length !== 980 || points.some(p => !Array.isArray(p) || p.length !== 2 || !p.every(n => Number.isFinite(n) && Math.abs(n) <= 1)))
27 throw new Error('Invalid native whale body.');
28 this.world = new pet_world_js_1.PetWorld(points, live ? (0, pet_telemetry_js_1.compilePetTelemetry)([]) : (0, pet_telemetry_js_1.decodePetJSONL)(tapeJSONL), JSON.parse(interactionsJSON), expressionVersion, true);
29 }
30 step(dt, motion) { this.world.step(dt, { motion, sensitivity: 1 }); return this.snapshot(); }
31 snapshot() { return JSON.stringify({ ...this.world.frame, voices: this.world.voices, digest: (0, pet_sim_js_1.digest)(this.world.sim) }); }
32 /** View-only projection. Display cadence and accessibility preferences never
33 * advance the owner, consume randomness, or change its score/checkpoint. */
34 presentation() {
35 const { sim, frame } = this.world;
36 const state = { ...frame.state, roamX: 0, roamY: 0, flip: 1, lit: frame.behaviour === 'doze' ? .18 : 1 };
37 const key = JSON.stringify([state, frame.pod]);
38 if (this.stillProjection?.key !== key) {
39 const still = new pet_sim_js_1.PetSim(sim.p.map(p => [p.hx, p.hy]), 0xC0FFEE, sim.expressionVersion);
40 const peers = frame.pod.filter(p => p.present);
41 still.step(1 / 30, state, { motion: false, sensitivity: 1,
42 podSlots: peers.length >= 3 ? peers.map(p => [[0, 2, 4, 1, 3, 5][p.slot], p.phase]) : undefined });
43 this.stillProjection = { key, points: still.p.map(p => [p.x, p.y]), style: still.frame };
44 }
45 return JSON.stringify({ ...frame, digest: (0, pet_sim_js_1.digest)(sim), style: sim.frame, activity: this.engine.activity(frame.timeMs),
46 points: sim.p.map(p => [p.x, p.y]),
47 still: { points: this.stillProjection.points, style: this.stillProjection.style, state } });
48 }
49 /** Losing a producer invalidates outstanding coverage, never the creature. */
50 disconnectEngine() { this.engine = new pet_engine_js_1.PetEngineTelemetry(); this.world.voices = []; }
51 interact(kind, x, y) { this.world.interact(kind, x, y); }
52 interactions() { return JSON.stringify(this.world.interactions); }
53 accept(packet) { this.world.acceptTelemetry(JSON.parse(packet)); }
54 acceptLiveTail(text) {
55 const packet = this.liveTape.readTail(text);
56 if (!packet)
57 return false;
58 this.world.acceptTelemetry(packet);
59 return true;
60 }
61 resetLiveInput() { this.liveTape.reset(); return this.resumeEngine(); }
62 recording(withCheckpoint = false) { return JSON.stringify(this.world.recording(withCheckpoint)); }
63 needsSegment() { return this.world.needsSegment; }
64 prepareSegment() { this.segment = this.world.prepareSegment(); return JSON.stringify(this.segment.recording); }
65 commitSegment() { if (!this.segment)
66 throw new Error('No pet segment was prepared.'); this.segment.commit(); this.segment = undefined; }
67 checkpoint() { return JSON.stringify(this.world.checkpoint()); }
68 recordingChunk(index, completed = false) { return this.world.recordingChunk(index, completed); }
69 restoreCheckpoint(text) {
70 if (text.length > 512 * 1024)
71 throw new Error('Pet checkpoint exceeds its size limit.');
72 this.restoreRecording(JSON.stringify({ ...this.world.recording(false), checkpoint: JSON.parse(text) }));
73 }
74 restoreRecording(text) {
75 if (text.length > 8 * 1024 * 1024)
76 throw new Error('Native habitat exceeds 8 MiB.');
77 const points = this.world.sim.p.map(p => [p.hx, p.hy]);
78 this.world = pet_world_js_1.PetWorld.fromRecording(points, JSON.parse(text));
79 this.liveTape.reset();
80 this.segment = undefined;
81 this.engineTick = Math.round(this.world.frame.timeMs * 30 / 1000);
82 this.engine = new pet_engine_js_1.PetEngineTelemetry();
83 }
84 /** Resume a live host at the first unrecorded bucket. Keep every accepted
85 * interval, but never present its last observed frame as current evidence. */
86 resumeEngine() {
87 this.world.resumeObservation();
88 this.engineTick = Math.round(this.world.frame.timeMs * 30 / 1000);
89 this.engine = new pet_engine_js_1.PetEngineTelemetry();
90 this.world.voices = [];
91 return this.world.frame.timeMs;
92 }
93 /** A rejected observation leaves the reducer exactly as it was, like a batch. */
94 observeEngine(metadataJSON, timeMs) {
95 const next = this.engine.clone();
96 next.observe(JSON.parse(metadataJSON), timeMs);
97 this.engine = next;
98 }
99 observeEngineBatch(metadataJSON, timeMs) {
100 const events = JSON.parse(metadataJSON);
101 if (!Array.isArray(events) || events.length > 64)
102 throw new Error('Invalid Engine batch.');
103 const next = this.engine.clone();
104 for (const event of events)
105 next.observe(event, timeMs);
106 this.engine = next;
107 }
108 advanceEngine(timeMs, motion, waiting) {
109 const target = Math.floor(timeMs * 30 / 1000 + 1e-8);
110 if (!Number.isFinite(timeMs) || target < this.engineTick || target - this.engineTick > 300)
111 throw new Error('Engine pet clock jump.');
112 this.engine.confirmWaiting(timeMs, waiting);
113 const voices = [];
114 while (this.engineTick < target) {
115 if ((this.engineTick + 1) % 12 === 0)
116 this.world.acceptTelemetry(this.engine.bucket(Math.floor(this.engineTick / 12)));
117 this.world.step(1 / 30, { motion, sensitivity: 1 });
118 voices.push(...this.world.voices);
119 this.engineTick++;
120 }
121 this.world.voices = voices;
122 }
123 terminal(width, height) {
124 if (![width, height].every(n => Number.isSafeInteger(n) && n >= 1 && n <= 512))
125 throw new Error('Invalid pet raster size.');
126 const { sim, frame } = this.world, l = (0, pet_sim_js_1.layout)(width * 2, height * 4, frame.state);
127 const cells = Array(width * height).fill(0), bits = [[1, 8], [2, 16], [4, 32], [64, 128]];
128 sim.p.forEach((p, i) => {
129 if (frame.state.observed < .92 && i % 2 === 1)
130 return;
131 const x = Math.round(l.ox + p.x * l.scale * l.flipX), y = Math.round(l.oy + p.y * l.scale);
132 if (x >= 0 && x < width * 2 && y >= 0 && y < height * 4)
133 cells[Math.floor(y / 4) * width + Math.floor(x / 2)] |= bits[y % 4][x % 2];
134 });
135 return JSON.stringify({ width, height, cells, timeMs: frame.timeMs, channel: frame.state.channel, arch: pet_sim_js_1.ARCH_OF[frame.state.channel],
136 hollow: frame.state.observed < .92, dozing: frame.behaviour === 'doze', lit: frame.state.lit });
137 }
138 pcmChannels(voicesJSON, startSample, length, rate) {
139 const p = (0, pet_audio_js_1.renderPetPCM)(JSON.parse(voicesJSON), startSample, length, rate);
140 return [p.left, p.right];
141 }
142 pcm(voicesJSON, startSample, length, rate) {
143 return JSON.stringify(this.pcmChannels(voicesJSON, startSample, length, rate).map(channel => Array.from(channel)));
144 }
145 }
146 exports.PetNative = PetNative;
147
148 };
149 factories["pet-world"]=function(exports,require){
150 "use strict";
151 Object.defineProperty(exports, "__esModule", { value: true });
152 exports.PetWorld = exports.PET_MAX_SECONDS = void 0;
153 const model_js_1 = require("./model.js");
154 const pet_sim_js_1 = require("./pet-sim.js");
155 const pet_telemetry_js_1 = require("./pet-telemetry.js");
156 const pet_audio_js_1 = require("./pet-audio.js");
157 const HZ = 30;
158 var pet_sim_js_2 = require("./pet-sim.js");
159 Object.defineProperty(exports, "PET_MAX_SECONDS", { enumerable: true, get: function () { return pet_sim_js_2.PET_MAX_SECONDS; } });
160 const seedFor = (name) => (0xC0FFEE ^ (0, model_js_1.stableHash)(name)) >>> 0;
161 /** Fixed-tick creature controller. Wall clocks and pointer APIs belong to drivers.
162 * Reconstructing with the same tape and interactions is also the seek operation.
163 * Particle, behaviour and identity randomness never consume one another's stream. */
164 class PetWorld {
165 sim;
166 tapeLog;
167 tapeHashes = [];
168 get tape() { return this.tapeLog; }
169 interactionLog;
170 get interactions() { return this.interactionLog.map(e => ({ ...e })); }
171 frame;
172 voices = [];
173 score = new pet_audio_js_1.PetScore();
174 accumulator = 0;
175 tick = 0;
176 bucketIndex = -1;
177 interactionIndex = 0;
178 branchTick = -1;
179 random = (0, pet_sim_js_1.mulberry32)(seedFor('behaviour'));
180 behaviour = 'swim';
181 until = 6;
182 targetX = .35;
183 targetY = -.08;
184 x = 0;
185 y = 0;
186 flip = 1;
187 lit = 1;
188 lastActivity = 0;
189 addressedAt = -Infinity;
190 waitSince = -1;
191 food = null;
192 members = new Map();
193 lastStill = '';
194 origin;
195 segmented = false;
196 hasTelemetry = false;
197 get startTimeMs() { return this.origin?.frame.timeMs ?? 0; }
198 get endTimeMs() { return Math.max(this.frame.timeMs, (this.tapeLog.at(-1)?.simTimeMs ?? 0) + 400); }
199 get needsSegment() {
200 return !this.segmented && this.tapeLog.length < 1024 && this.interactionLog.length < 4096
201 || this.bucketIndex >= 1024 || this.interactionIndex >= 4096;
202 }
203 constructor(points, tape = [], interactions = [], expressionVersion = 2, segmented = false) {
204 if (tape.length > 216_000 || interactions.length > 100_000)
205 throw new Error('Pet recording exceeds its input limit.');
206 this.sim = new pet_sim_js_1.PetSim(points, 0xC0FFEE, expressionVersion);
207 this.segmented = segmented;
208 this.hasTelemetry = tape.length > 0;
209 this.tapeLog = structuredClone([...tape]);
210 this.interactionLog = structuredClone([...interactions]);
211 for (let i = 0; i < this.tape.length; i++) {
212 const b = this.tape[i];
213 (0, pet_telemetry_js_1.validatePetBucket)(b);
214 if (segmented ? i > 0 && b.sequence <= this.tape[i - 1].sequence : b.sequence !== i)
215 throw new Error('World requires contiguous version 1 pet buckets.');
216 }
217 for (let i = 0; i < this.interactionLog.length; i++) {
218 const e = this.interactionLog[i];
219 if (!Number.isFinite(e.timeMs) || e.timeMs < 0 || i > 0 && e.timeMs < this.interactionLog[i - 1].timeMs
220 || !['attention', 'food'].includes(e.kind) || !Number.isFinite(e.x) || !Number.isFinite(e.y)
221 || Math.abs(e.x) > 1 || Math.abs(e.y) > 1)
222 throw new Error('Invalid pet interaction.');
223 }
224 this.hashTape(0);
225 this.frame = this.makeFrame(0);
226 this.voices = this.score.voices(this.frame);
227 if (segmented)
228 this.origin = this.checkpoint();
229 }
230 checkpoint() {
231 return structuredClone({ petCheckpointVersion: this.segmented ? 2 : 1,
232 ...(this.segmented ? { historyStart: (this.origin?.tick ?? this.tick), hasTelemetry: this.hasTelemetry } : {}), history: this.historyDigest(),
233 sim: this.sim.checkpoint(), score: this.score.checkpoint(), accumulator: this.accumulator,
234 tick: this.tick, bucketIndex: this.bucketIndex, interactionIndex: this.interactionIndex, branchTick: this.branchTick,
235 random: this.random.state(), behaviour: this.behaviour, until: this.until,
236 targetX: this.targetX, targetY: this.targetY, x: this.x, y: this.y, flip: this.flip, lit: this.lit,
237 lastActivity: this.lastActivity, addressedAt: Number.isFinite(this.addressedAt) ? this.addressedAt : null,
238 waitSince: this.waitSince, food: this.food, members: [...this.members.values()], lastStill: this.lastStill,
239 frame: this.frame, voices: this.voices });
240 }
241 recording(withCheckpoint = true, completed = false) {
242 const tapeEnd = completed ? this.bucketIndex + 1 : this.tapeLog.length;
243 const inputEnd = completed ? this.interactionIndex : this.interactionLog.length;
244 const history = this.historyDigest(tapeEnd, inputEnd);
245 return { petReplayVersion: this.segmented ? 2 : 1, expressionVersion: this.sim.expressionVersion,
246 tape: this.tapeLog.slice(0, tapeEnd), interactions: this.interactionLog.slice(0, inputEnd).map(e => ({ ...e })),
247 ...(this.origin ? { start: { ...structuredClone(this.origin), hasTelemetry: this.hasTelemetry, history } } : {}),
248 ...(withCheckpoint ? { checkpoint: { ...this.checkpoint(), history } } : {}) };
249 }
250 static fromRecording(points, value) {
251 const r = value;
252 if (!r || ![1, 2].includes(r.petReplayVersion) || !Array.isArray(r.tape) || !Array.isArray(r.interactions))
253 throw new Error('Invalid pet recording.');
254 const version = r.expressionVersion === undefined ? 1 : r.expressionVersion;
255 if (![1, 2].includes(version))
256 throw new Error('Unsupported pet expression version.');
257 if (r.checkpoint !== undefined && !r.checkpoint || r.start !== undefined && !r.start)
258 throw new Error('Invalid pet checkpoint.');
259 for (const c of [r.start, r.checkpoint])
260 if (c && (c.sim?.expressionVersion ?? 1) !== version)
261 throw new Error('Pet expression version does not match its checkpoint.');
262 if (r.petReplayVersion === 1 && (r.start || r.checkpoint && r.checkpoint.petCheckpointVersion !== 1))
263 throw new Error('Invalid legacy pet recording.');
264 if (r.petReplayVersion === 2 && (!r.start || r.start.petCheckpointVersion !== 2 || r.start.historyStart !== r.start.tick))
265 throw new Error('The recording segment is missing its starting checkpoint.');
266 const first = r.start && PetWorld.restore(points, r.tape, r.interactions, r.start);
267 const world = r.checkpoint ? PetWorld.restore(points, r.tape, r.interactions, r.checkpoint) : first ?? new PetWorld(points, r.tape, r.interactions, version);
268 if (first) {
269 if (!world.segmented || world.tick < first.tick || r.checkpoint && r.checkpoint.historyStart !== first.tick)
270 throw new Error('Pet segment checkpoints do not agree.');
271 world.origin = first.checkpoint();
272 }
273 return world;
274 }
275 /** Retire only consumed input. The exact origin makes each archived segment
276 * independently replayable; no particle, random stream or score is reset. */
277 prepareSegment() {
278 const dropTape = Math.max(0, this.bucketIndex), dropInputs = this.interactionIndex, previous = this.origin;
279 const tape = this.tapeLog.slice(dropTape), interactions = this.interactionLog.slice(dropInputs);
280 const points = this.sim.p.map(p => [p.hx, p.hy]);
281 const c = this.checkpoint();
282 c.petCheckpointVersion = 2;
283 c.historyStart = this.tick;
284 c.hasTelemetry = this.hasTelemetry;
285 c.bucketIndex -= dropTape;
286 c.interactionIndex = 0;
287 c.history = new PetWorld(points, tape, interactions, this.sim.expressionVersion, true).historyDigest();
288 const next = PetWorld.restore(points, tape, interactions, c);
289 next.origin = next.checkpoint();
290 let committed = false;
291 return { recording: next.recording(), archive: this.recording(true, true), commit: () => {
292 if (committed || this.origin !== previous || this.tick < c.tick)
293 throw new Error('The pet recording segment has changed.');
294 this.tapeLog.splice(0, dropTape);
295 this.interactionLog.splice(0, dropInputs);
296 this.bucketIndex -= dropTape;
297 this.interactionIndex -= dropInputs;
298 this.tapeHashes = [];
299 this.hashTape(0);
300 this.segmented = true;
301 this.origin = next.origin;
302 committed = true;
303 } };
304 }
305 /** Lossless version 1 export, including the current pose and score. Drivers
306 * consume all chunks synchronously on the world's owner before another tick.
307 * Only a small slice is serialized inside an embedded runtime at a time. */
308 recordingChunk(index, completed = false) {
309 if (!Number.isSafeInteger(index) || index < 0)
310 throw new Error('Invalid pet export cursor.');
311 const size = 16, tapeEnd = completed ? this.bucketIndex + 1 : this.tapeLog.length;
312 const inputEnd = completed ? this.interactionIndex : this.interactionLog.length;
313 const tapes = Math.ceil(tapeEnd / size), inputs = Math.ceil(inputEnd / size);
314 if (index === 0)
315 return `{"petReplayVersion":${this.segmented ? 2 : 1},"expressionVersion":${this.sim.expressionVersion},"tape":[`;
316 if (index <= tapes)
317 return (index === 1 ? '' : ',') + JSON.stringify(this.tapeLog.slice((index - 1) * size, Math.min(tapeEnd, index * size))).slice(1, -1);
318 if (index === tapes + 1)
319 return '],"interactions":[';
320 const part = index - tapes - 2;
321 if (part < inputs)
322 return (part === 0 ? '' : ',') + JSON.stringify(this.interactionLog.slice(part * size, Math.min(inputEnd, (part + 1) * size))).slice(1, -1);
323 if (part === inputs) {
324 const history = this.historyDigest(tapeEnd, inputEnd);
325 return `]${this.origin ? ',"start":' + JSON.stringify({ ...this.origin, hasTelemetry: this.hasTelemetry, history }) : ''},"checkpoint":${JSON.stringify({ ...this.checkpoint(), history })}}`;
326 }
327 return null;
328 }
329 /** Prefix states preserve the original FNV checksum byte for byte. Live
330 * appends and replacements hash only the changed suffix, so checkpointing
331 * does not rescan hours of accepted telemetry on the world worker. */
332 hashTape(from) {
333 for (let i = from; i < this.tapeLog.length; i++)
334 this.tapeHashes[i] = (0, model_js_1.stableHash)((i ? ',' : '') + JSON.stringify(this.tapeLog[i]), this.tapeHashes[i - 1] ?? (0, model_js_1.stableHash)('[['));
335 }
336 historyDigest(tapeEnd = this.tapeLog.length, inputEnd = this.interactionLog.length) {
337 let hash = (0, model_js_1.stableHash)('],[', this.tapeHashes[tapeEnd - 1] ?? (0, model_js_1.stableHash)('[['));
338 for (let i = 0; i < inputEnd; i++)
339 hash = (0, model_js_1.stableHash)((i ? ',' : '') + JSON.stringify(this.interactionLog[i]), hash);
340 return (0, model_js_1.stableHash)(']]', hash);
341 }
342 /** Hydrate a new world without stepping history. Validation finishes before
343 * the caller receives it, so a corrupt checkpoint never mutates a live pet. */
344 static restore(points, tape, interactions, value) {
345 const c = value;
346 const range = (n, low, high) => Number.isFinite(n) && n >= low && n <= high;
347 const integer = (n, low, high) => Number.isSafeInteger(n) && range(n, low, high);
348 if (!c || ![1, 2].includes(c.petCheckpointVersion) || JSON.stringify(c).length > 512 * 1024
349 || c.petCheckpointVersion === 2 && (!integer(c.historyStart, 0, c.tick) || typeof c.hasTelemetry !== 'boolean')
350 || !integer(c.tick, 0, pet_sim_js_1.PET_MAX_SECONDS * HZ) || !range(c.accumulator, -1e-8, 1 / HZ + 1e-8)
351 || !integer(c.bucketIndex, -1, tape.length - 1) || !integer(c.interactionIndex, 0, interactions.length)
352 || !integer(c.branchTick, -1, c.tick) || !integer(c.random, 0, 0xffffffff)
353 || !['swim', 'dive', 'roll', 'breathe', 'drift', 'doze', 'wake'].includes(c.behaviour)
354 || !range(c.until, 0, pet_sim_js_1.PET_MAX_SECONDS + 10) || ![c.targetX, c.targetY, c.x, c.y, c.flip].every(n => range(n, -1, 1))
355 || !range(c.lit, 0, 1) || !range(c.lastActivity, 0, c.tick / HZ)
356 || c.addressedAt !== null && !range(c.addressedAt, 0, c.tick / HZ)
357 || !range(c.waitSince, -1, c.tick / HZ) || typeof c.lastStill !== 'string' || c.lastStill.length > 2048
358 || !Array.isArray(c.members) || c.members.length > 6
359 || c.members.some(m => !m || typeof m.id !== 'string' || !m.id || m.id.length > 4096
360 || !integer(m.slot, 0, 5) || !range(m.phase, 0, Math.PI * 2) || typeof m.present !== 'boolean')
361 || new Set(c.members.map(m => m.id)).size !== c.members.length || new Set(c.members.map(m => m.slot)).size !== c.members.length
362 || c.food !== null && (!c.food || !range(c.food.time, 0, c.tick / HZ) || ![c.food.x, c.food.y].every(n => range(n, -1, 1)))
363 || !c.frame || c.frame.timeMs !== c.tick * 1000 / HZ || c.frame.behaviour !== c.behaviour
364 || !['none', 'orient', 'approach', 'call'].includes(c.frame.needs)
365 || !range(c.frame.surface, -.9, -.8) || !range(c.frame.caustic, 0, 1)
366 || c.frame.food !== null && (!c.frame.food || !range(c.frame.food.x, -1, 1) || !range(c.frame.food.y, -1, 1.21) || !range(c.frame.food.life, 0, 1))
367 || JSON.stringify(c.frame.pod) !== JSON.stringify(c.members)
368 || !Array.isArray(c.voices) || c.voices.length > 1024
369 || c.voices.some(v => !v || typeof v.id !== 'string' || v.id.length > 256))
370 throw new Error('Invalid pet world checkpoint.');
371 (0, pet_sim_js_1.validatePetState)(c.frame.state);
372 (0, pet_audio_js_1.renderPetPCM)(c.voices, 0, 0);
373 const world = new PetWorld(points, tape, interactions, c.sim?.expressionVersion ?? 1, c.petCheckpointVersion === 2);
374 if (c.petCheckpointVersion === 2) {
375 world.hasTelemetry = c.hasTelemetry;
376 world.origin = structuredClone(c);
377 }
378 if (c.history !== world.historyDigest()
379 || c.bucketIndex >= 0 && world.tape[c.bucketIndex].simTimeMs > c.frame.timeMs + 1e-7
380 // acceptTelemetry may fill past gaps after the most recent fixed tick.
381 // Preserve that pending cursor exactly; it may lag only over empty gaps.
382 || world.tape.some((b, i) => i > c.bucketIndex && b.simTimeMs <= c.frame.timeMs + 1e-7
383 && (b.observed !== 0 || b.channel !== 'other' || b.errors || b.waiting || b.agentIds.length
384 || b.onsets.some(Boolean) || b.activeMs.some(Boolean)))
385 || world.interactionLog.slice(0, c.interactionIndex).some(e => e.timeMs > c.frame.timeMs + 1e-7)
386 || world.interactionLog[c.interactionIndex]?.timeMs <= c.frame.timeMs + 1e-7)
387 throw new Error('Pet checkpoint does not match its recording.');
388 const candidate = world.tape[c.bucketIndex];
389 const telemetry = candidate && c.frame.timeMs < candidate.simTimeMs + candidate.durationMs ? candidate : undefined;
390 if (JSON.stringify(c.frame.telemetry) !== JSON.stringify(telemetry))
391 throw new Error('Pet checkpoint telemetry does not match its clock.');
392 world.sim.restore(c.sim);
393 world.score.restore(c.score);
394 world.random.restore(c.random);
395 world.accumulator = c.accumulator;
396 world.tick = c.tick;
397 world.bucketIndex = c.bucketIndex;
398 world.interactionIndex = c.interactionIndex;
399 world.branchTick = c.branchTick;
400 world.behaviour = c.behaviour;
401 world.until = c.until;
402 world.targetX = c.targetX;
403 world.targetY = c.targetY;
404 world.x = c.x;
405 world.y = c.y;
406 world.flip = c.flip;
407 world.lit = c.lit;
408 world.lastActivity = c.lastActivity;
409 world.addressedAt = c.addressedAt ?? -Infinity;
410 world.waitSince = c.waitSince;
411 world.food = structuredClone(c.food);
412 world.members = new Map(c.members.map(m => [m.id, { ...m }]));
413 world.lastStill = c.lastStill;
414 // JSON omits undefined properties; keep the same frame shape as makeFrame.
415 world.frame = { ...structuredClone(c.frame), telemetry: telemetry ? structuredClone(telemetry) : undefined };
416 world.voices = structuredClone(c.voices);
417 return world;
418 }
419 /** Resume observation beyond all already accepted live packets, without
420 * replaying their sound or exposing a stale request as current work. */
421 resumeObservation() {
422 const last = this.tapeLog.at(-1), end = last ? (last.sequence + 1) * 12 : 0;
423 if (!end || end - this.tick > 24)
424 throw new Error('Only a live recording can resume observation.');
425 while (this.tick < end)
426 this.step(1 / 30, { motion: false, sensitivity: 1 });
427 this.voices = [];
428 }
429 /** Branch at the current playhead; input is journalled for the next fixed tick.
430 * A live touch never needs to re-simulate the creature's entire lifetime. */
431 interact(kind, x, y) {
432 if (!['attention', 'food'].includes(kind) || !Number.isFinite(x) || !Number.isFinite(y) || Math.abs(x) > 1 || Math.abs(y) > 1)
433 throw new Error('Invalid pet interaction.');
434 if (this.branchTick !== this.tick)
435 this.interactionLog.splice(this.interactionIndex);
436 this.branchTick = this.tick;
437 this.interactionLog.push({ timeMs: (this.tick + 1) * 1000 / HZ, kind, x, y });
438 }
439 /** Accept a live source packet at the next 400ms boundary. The accepted tape,
440 * including any missing intervals, is the exact replay authority for this host. */
441 acceptTelemetry(input) {
442 (0, pet_telemetry_js_1.validatePetBucket)(input);
443 const sequence = Math.floor(this.tick / 12) + 1;
444 if (this.tapeLog.length >= 216_000)
445 throw new Error('Archive this pet recording before accepting more telemetry.');
446 this.hasTelemetry = true;
447 if (this.segmented) {
448 const at = this.tapeLog.findIndex(b => b.sequence >= sequence);
449 const index = at < 0 ? this.tapeLog.length : at;
450 this.tapeLog.splice(index, at >= 0 && this.tapeLog[at].sequence === sequence ? 1 : 0, { ...structuredClone(input), sequence, simTimeMs: sequence * 400 });
451 this.hashTape(index);
452 return;
453 }
454 if (sequence >= 216_000)
455 throw new Error('Archive the legacy recording before accepting more telemetry.');
456 const changedFrom = Math.min(sequence, this.tapeLog.length);
457 while (this.tapeLog.length <= sequence) {
458 const at = this.tapeLog.length;
459 this.tapeLog.push({ version: 1, sequence: at, simTimeMs: at * 400, durationMs: 400,
460 activity: .12, coherence: .25, attention: 0, channel: 'other', observed: 0, roamX: 0, roamY: 0, flip: 1, lit: 1,
461 onsets: Array(13).fill(0), activeMs: Array(13).fill(0), errors: 0, agentIds: [], waiting: false });
462 }
463 this.tapeLog[sequence] = { ...structuredClone(input), sequence, simTimeMs: sequence * 400 };
464 this.hashTape(changedFrom);
465 }
466 /** dt is bounded so suspending a surface cannot cause an unbounded catch-up. */
467 step(dt, opts = { motion: true, sensitivity: 1 }) {
468 if (!Number.isFinite(dt) || dt < 0 || dt > 10)
469 throw new Error('World dt must be in [0, 10] seconds.');
470 this.accumulator += dt;
471 this.voices = [];
472 while (this.accumulator + 1e-10 >= 1 / HZ) {
473 this.accumulator -= 1 / HZ;
474 const time = ++this.tick / HZ;
475 this.frame = this.makeFrame(time);
476 this.voices.push(...this.score.voices(this.frame));
477 if (!opts.motion) {
478 this.frame.state = { ...this.frame.state, roamX: 0, roamY: 0, flip: 1, lit: this.behaviour === 'doze' ? .18 : 1 };
479 this.frame.surface = -.86;
480 this.frame.caustic = .5;
481 if (this.frame.food)
482 this.frame.food.y = this.food.y;
483 }
484 const signature = JSON.stringify(this.frame.state);
485 const peers = this.frame.pod.filter(p => p.present);
486 const podSlots = peers.length >= 3 ? peers.map(p => [[0, 2, 4, 1, 3, 5][p.slot], p.phase]) : undefined;
487 if (opts.motion || signature !== this.lastStill || podSlots)
488 this.sim.step(1 / HZ, this.frame.state, { ...opts, podSlots });
489 this.lastStill = signature;
490 }
491 return this.frame;
492 }
493 makeFrame(time) {
494 const timeMs = this.tick * 1000 / HZ;
495 while (this.bucketIndex + 1 < this.tape.length && this.tape[this.bucketIndex + 1].simTimeMs <= timeMs + 1e-7)
496 this.bucketIndex++;
497 const candidate = this.tape[this.bucketIndex];
498 const telemetry = candidate && timeMs < candidate.simTimeMs + candidate.durationMs ? candidate : undefined;
499 while (this.interactionIndex < this.interactionLog.length && this.interactionLog[this.interactionIndex].timeMs <= timeMs + 1e-7) {
500 const e = this.interactionLog[this.interactionIndex++];
501 this.addressedAt = time;
502 this.lastActivity = time;
503 this.targetX = e.x * .65;
504 this.targetY = e.y * .65;
505 if (e.kind === 'food')
506 this.food = { time, x: e.x, y: e.y };
507 }
508 const busy = telemetry && telemetry.observed >= .92 && telemetry.activity > .2;
509 if (busy)
510 this.lastActivity = time;
511 if (telemetry?.waiting) {
512 if (this.waitSince < 0)
513 this.waitSince = time;
514 }
515 else
516 this.waitSince = -1;
517 const waitingFor = this.waitSince < 0 ? 0 : time - this.waitSince;
518 const needs = this.waitSince < 0 ? 'none' : waitingFor < 8 ? 'orient' : waitingFor < 25 ? 'approach' : 'call';
519 const addressed = time - this.addressedAt < 2;
520 if (this.behaviour === 'doze' && (busy || addressed)) {
521 this.behaviour = 'wake';
522 this.until = time + 2;
523 }
524 else if (!busy && !addressed && time - this.lastActivity >= 75)
525 this.behaviour = 'doze';
526 else if (time >= this.until) {
527 const choices = ['swim', 'swim', 'dive', 'roll', 'breathe', 'drift'];
528 this.behaviour = choices[Math.floor(this.random() * choices.length)];
529 this.until = time + 3 + this.random() * 7;
530 this.targetX = (this.random() * 2 - 1) * .75;
531 this.targetY = this.behaviour === 'dive' ? .6 : this.behaviour === 'breathe' ? -.65 : (this.random() * 2 - 1) * .4;
532 }
533 const sleeping = this.behaviour === 'doze';
534 if (sleeping) {
535 this.targetY = .65;
536 this.targetX = .15;
537 }
538 if (this.sim.expressionVersion === 1 && (needs === 'approach' || needs === 'call')) {
539 this.targetX = 0;
540 this.targetY = .3;
541 }
542 const move = sleeping ? .004 : this.behaviour === 'drift' ? .006 : .018;
543 const dx = this.targetX - this.x;
544 this.x += dx * move;
545 this.y += (this.targetY - this.y) * move;
546 this.flip += ((Math.abs(dx) < .015 ? this.flip < 0 ? -1 : 1 : dx < 0 ? -1 : 1) - this.flip) * .035;
547 this.lit += ((sleeping ? .18 : 1) - this.lit) * .03;
548 const wild = !this.hasTelemetry;
549 const state = {
550 activity: telemetry?.activity ?? (wild ? sleeping ? .05 : .18 : .12),
551 coherence: telemetry?.coherence ?? (wild ? .94 : .25),
552 attention: Math.max(telemetry?.attention ?? 0, addressed ? .85 : 0, this.sim.expressionVersion === 1 && needs === 'call' ? 1 : 0),
553 // A touch changes orientation, never hides an instrumentation gap.
554 channel: addressed ? 'human' : telemetry?.channel ?? 'other',
555 observed: telemetry?.observed ?? (wild ? 1 : 0),
556 roamX: this.x, roamY: this.y, flip: this.flip, lit: this.lit,
557 };
558 const activeIds = new Set(telemetry?.agentIds ?? []);
559 for (const member of this.members.values())
560 member.present = activeIds.has(member.id);
561 for (const id of activeIds) {
562 if (!this.members.has(id)) {
563 const vacant = [...this.members.values()].find(m => !m.present);
564 const slot = this.members.size < 6 ? this.members.size : vacant?.slot;
565 if (slot !== undefined) {
566 if (this.members.size >= 6 && vacant)
567 this.members.delete(vacant.id);
568 this.members.set(id, { id, slot, phase: (0, pet_sim_js_1.mulberry32)(seedFor(`pod:${id}`))() * Math.PI * 2, present: true });
569 }
570 }
571 const member = this.members.get(id);
572 if (member)
573 member.present = true;
574 }
575 return { timeMs, behaviour: this.behaviour, state, telemetry, needs,
576 pod: [...this.members.values()].map(m => ({ ...m })),
577 surface: -.86 + .012 * Math.sin(time * .8), caustic: .5 + .5 * Math.sin(time * .31),
578 food: this.food && time - this.food.time < 5
579 ? { x: this.food.x, y: this.food.y + (time - this.food.time) * .04, life: (0, model_js_1.clamp)(1 - (time - this.food.time) / 5, 0, 1) } : null };
580 }
581 }
582 exports.PetWorld = PetWorld;
583
584 };
585 factories["model"]=function(exports,require){
586 "use strict";
587 Object.defineProperty(exports, "__esModule", { value: true });
588 exports.durationOf = exports.EMPTY_FILTERS = exports.CATEGORIES = void 0;
589 exports.eventMatches = eventMatches;
590 exports.errorOnsetOf = errorOnsetOf;
591 exports.stableHash = stableHash;
592 exports.quantile = quantile;
593 exports.clamp = clamp;
594 exports.formatTime = formatTime;
595 /** Versioned, vendor-neutral trace and signal contracts. All internal time is ms. */
596 exports.CATEGORIES = [
597 'reasoning', 'tool', 'memory', 'code', 'filesystem', 'network', 'browser',
598 'communication', 'agent', 'orchestration', 'error', 'human', 'other',
599 ];
600 exports.EMPTY_FILTERS = { query: '', category: '', agent: '', model: '', tool: '', status: '' };
601 function eventMatches(e, f) {
602 if (f.category && e.category !== f.category)
603 return false;
604 if (f.agent && e.agentId !== f.agent)
605 return false;
606 if (f.model && e.model !== f.model)
607 return false;
608 if (f.tool && e.tool !== f.tool)
609 return false;
610 if (f.status && e.status !== f.status)
611 return false;
612 if (f.query) {
613 const q = f.query.toLowerCase();
614 // Search is deliberately content-aware but runs only over locally retained fields.
615 if (![e.name, e.id, e.agentId, e.model, e.tool, e.provider, e.category,
616 JSON.stringify(e.attributes), JSON.stringify(e.payload), JSON.stringify(e.raw)].filter(Boolean).join(' ').toLowerCase().includes(q))
617 return false;
618 }
619 return true;
620 }
621 const durationOf = (e) => Math.max(0, e.endTime - e.startTime);
622 exports.durationOf = durationOf;
623 /** An explicit failure receipt can arrive after a span began or ended. Keep
624 * its timestamp distinct from the operation onset in every signal view. */
625 function errorOnsetOf(e) {
626 const time = e.attributes['whalesong.error_onset_ms'];
627 if (time === undefined)
628 return e.startTime;
629 if (typeof time !== 'number' || !Number.isFinite(time) || time < e.startTime)
630 throw new Error('Invalid failure observation time.');
631 return time;
632 }
633 function stableHash(text, seed = 2166136261) {
634 let h = seed;
635 for (let i = 0; i < text.length; i++) {
636 h ^= text.charCodeAt(i);
637 h = Math.imul(h, 16777619);
638 }
639 return h >>> 0;
640 }
641 function quantile(a, q) {
642 if (!a.length)
643 return 0;
644 const s = [...a].sort((a, b) => a - b), x = Math.min(1, Math.max(0, q)) * (s.length - 1);
645 return s[Math.floor(x)] + (s[Math.ceil(x)] - s[Math.floor(x)]) * (x % 1);
646 }
647 function clamp(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); }
648 function formatTime(ms, precise = false) {
649 const m = Math.floor(Math.max(0, ms) / 60000), s = Math.floor(Math.max(0, ms) / 1000) % 60;
650 return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}${precise ? '.' + String(Math.floor(ms % 1000)).padStart(3, '0') : ''}`;
651 }
652
653 };
654 factories["pet-sim"]=function(exports,require){
655 "use strict";
656 // PetSim — the portable core of the Codewhale pet.
657 //
658 // This is a faithful, DOM-free port of the consort study's particle engine
659 // (grammar.js). The same code — same constants, same order of operations — is
660 // what the Rust, Swift and Kotlin cores implement. Four rules keep the view
661 // identical on every surface:
662 //
663 // 1. The body is the same 980 points, loaded from whale-points.tsv — never
664 // re-sampled from the image on each platform.
665 // 2. Per-particle jitter phases come from mulberry32(0xC0FFEE), not from the
666 // platform's RNG.
667 // 3. All motion is a pure function of (sim clock, state): nothing accumulates
668 // noise. Two runs fed the same tape produce the same frame.
669 // 4. Colour, hollowness and brightness are computed here, once — renderers
670 // only place and stamp dots.
671 //
672 // Positions stay normalized in body space (roughly [-0.5, 0.5]²). A renderer
673 // maps them through layout() to its own medium.
674 Object.defineProperty(exports, "__esModule", { value: true });
675 exports.PetSim = exports.CHANNEL_INDEX = exports.CHANNELS = exports.ARCH_OF = exports.REST_STATE = exports.PET_MAX_SECONDS = void 0;
676 exports.validatePetState = validatePetState;
677 exports.mulberry32 = mulberry32;
678 exports.layout = layout;
679 exports.digest = digest;
680 exports.runTape = runTape;
681 exports.PET_MAX_SECONDS = 100 * 365 * 86_400;
682 exports.REST_STATE = {
683 activity: 0.35, coherence: 0.8, attention: 0, channel: 'reasoning',
684 observed: 1, roamX: 0, roamY: 0, flip: 1, lit: 1,
685 };
686 const lerp = (a, b, t) => a + (b - a) * t;
687 const clamp = (v, a = 0, b = 1) => Math.min(b, Math.max(a, v));
688 exports.ARCH_OF = {
689 reasoning: 'gyre', memory: 'gyre',
690 tool: 'strike', code: 'strike', filesystem: 'strike',
691 network: 'cross', communication: 'cross', browser: 'cross',
692 agent: 'pod', orchestration: 'pod',
693 error: 'tear', human: 'address', other: 'drift',
694 };
695 exports.CHANNELS = [
696 { key: 'reasoning', label: 'Model / reasoning', color: '#73c9b5', freq: 130.81, sustained: true, arch: 'gyre', form: 'gyre · rolling' },
697 { key: 'tool', label: 'Tool calls', color: '#74aadd', freq: 261.63, sustained: false, arch: 'strike', form: 'strike · reaching' },
698 { key: 'memory', label: 'Memory / RAG', color: '#b6a77f', freq: 195.99, sustained: true, arch: 'gyre', form: 'gyre · scanning' },
699 { key: 'code', label: 'Code execution', color: '#9b9ed7', freq: 164.81, sustained: false, arch: 'strike', form: 'strike · along the body' },
700 { key: 'filesystem', label: 'Filesystem', color: '#92b9c9', freq: 440.00, sustained: false, arch: 'strike', form: 'strike · fanning' },
701 { key: 'network', label: 'Network / API', color: '#d3ac74', freq: 523.25, sustained: false, arch: 'cross', form: 'crossing · one way' },
702 { key: 'browser', label: 'Browser / computer', color: '#9ea9df', freq: 349.23, sustained: false, arch: 'cross', form: 'crossing · a sweep' },
703 { key: 'communication', label: 'Agent messages', color: '#83c5c9', freq: 293.66, sustained: false, arch: 'cross', form: 'crossing · two ways' },
704 { key: 'agent', label: 'Subagent activity', color: '#b09acb', freq: 220.00, sustained: true, arch: 'pod', form: 'pod · peers' },
705 { key: 'orchestration', label: 'Orchestration', color: '#6c8798', freq: 98.00, sustained: true, arch: 'pod', form: 'pod · hub' },
706 { key: 'error', label: 'Errors / exceptions', color: '#e79186', freq: 185.00, sustained: false, arch: 'tear', form: 'torn · irregular' },
707 { key: 'human', label: 'Human interaction', color: '#c2b787', freq: 391.99, sustained: false, arch: 'address', form: 'decision · junction' },
708 { key: 'other', label: 'Unclassified', color: '#738492', freq: 146.83, sustained: false, arch: 'drift', form: 'drifting · unformed' },
709 ];
710 exports.CHANNEL_INDEX = Object.fromEntries(exports.CHANNELS.map((c, i) => [c.key, i]));
711 function validatePetState(value) {
712 const s = value;
713 if (!s || typeof s !== 'object' || !Object.hasOwn(exports.CHANNEL_INDEX, s.channel)
714 || ![s.activity, s.coherence, s.attention, s.observed, s.lit].every(n => Number.isFinite(n) && n >= 0 && n <= 1)
715 || ![s.roamX, s.roamY, s.flip].every(n => Number.isFinite(n) && Math.abs(n) <= 1))
716 throw new Error('Invalid pet state.');
717 }
718 const hex2rgb = (h) => [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)];
719 const RGB = exports.CHANNELS.map(c => hex2rgb(c.color));
720 const UNKNOWN_RGB = hex2rgb('#738492');
721 const REST_RGB = [122, 214, 240];
722 // mulberry32 — a 32-bit seeded PRNG tiny enough to port by hand correctly.
723 function mulberry32(seed) {
724 let a = seed >>> 0;
725 return Object.assign(() => {
726 a = (a + 0x6D2B79F5) >>> 0;
727 let t = a;
728 t = Math.imul(t ^ (t >>> 15), t | 1);
729 t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
730 return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
731 }, { state: () => a, restore: (state) => {
732 if (!Number.isSafeInteger(state) || state < 0 || state > 0xffffffff)
733 throw new Error('Invalid pet random stream.');
734 a = state;
735 } });
736 }
737 /** Work reorganizes the same particles; no new random draws or invented facts.
738 * These are expressive fields, not diagrams of unobserved network/file topology. */
739 function fieldTarget(q, t, act, att, key) {
740 const u = q.s * 2 - 1, lane = q.pod - 2.5, a = q.s * Math.PI * 2;
741 const flow = t * (.35 + act * .65);
742 if (key === 'reasoning') {
743 const ring = .34 + .105 * Math.cos(a * 3 + flow + lane * .18);
744 return [ring * Math.cos(a * 2 + flow * .3), ring * Math.sin(a * 2 + flow * .3) * .7 + .10 * Math.sin(a * 3 + flow)];
745 }
746 if (key === 'memory')
747 return [.46 * Math.cos(a + lane * .1 + flow * .25), lane * .082 + .052 * Math.sin(a * 2 + flow)];
748 if (key === 'code')
749 return [u * .57, lane * .066 + .12 * Math.sin(u * 7 + flow * 2 + q.pod * Math.PI / 3)];
750 if (key === 'filesystem') {
751 const branch = Math.max(0, (u + .3) / 1.3);
752 return [u * .56, lane * .13 * branch + .025 * Math.sin(u * 8 - flow)];
753 }
754 if (key === 'tool') {
755 const reach = .14 + (u + 1) * .20 + .04 * Math.sin(flow * 3 - u * 4);
756 return [Math.cos(q.pod * Math.PI / 3) * reach, Math.sin(q.pod * Math.PI / 3) * reach * .8 + q.hy * .06];
757 }
758 if (key === 'browser')
759 return [u * .56, lane * .083 + .035 * Math.sin(u * 5 - flow * 2)];
760 if (key === 'network' || key === 'communication') {
761 const direction = key === 'communication' && q.pod % 2 === 1 ? -1 : 1;
762 const phase = a + flow * direction;
763 return [.54 * Math.cos(phase), Math.sin(phase) * (.12 + q.pod * .035) + lane * .024];
764 }
765 if (key === 'human') {
766 const gap = u < 0 ? -.075 : .075;
767 return [u * .47 + gap, lane * .10 * Math.abs(u) + .012 * Math.sin(flow + a) * (1 - att)];
768 }
769 return undefined;
770 }
771 // Version 1 retains the original authored gait for existing recordings.
772 function gaitTarget(q, t, act, coh, att, key, work, podSlots, expressionVersion = 1) {
773 const { hx, hy, ang, rad, tail, s, pod, jx, jy } = q;
774 const omega = lerp(4.6, 5.2 + act * 2.8, work);
775 const breath = 1 + Math.sin(t * 1.85) * lerp(0.048, 0.018, work);
776 const flex = Math.sin(ang * 2.05 + t * omega) * lerp(0.042, 0.016 + act * 0.028, work) * (0.18 + 0.82 * tail);
777 let px = Math.cos(ang + flex) * rad * breath;
778 let py = Math.sin(ang + flex) * rad * breath;
779 px += Math.sin(t * 0.33) * lerp(0.030, 0.014, work);
780 py += Math.cos(t * 0.21) * lerp(0.018, 0.010, work);
781 if (work < 0.02)
782 return [px, py];
783 const arch = exports.ARCH_OF[key] || 'drift';
784 let gx = px, gy = py;
785 if (arch === 'gyre') {
786 if (key === 'memory') {
787 const pulse = 1 + Math.sin(t * (2.4 + act * 1.6) - rad * 11) * (0.15 + act * 0.10);
788 gx *= pulse;
789 gy *= pulse;
790 }
791 else {
792 const roll = Math.sin(t * (1.05 + act * 0.35)) * (0.48 + act * 0.32);
793 const c = Math.cos(roll), sn = Math.sin(roll);
794 gx = px * c - py * sn * 0.88;
795 gy = px * sn * 0.88 + py * c;
796 }
797 }
798 else if (arch === 'strike') {
799 if (key === 'tool') {
800 const rate = 2.7 + act * 2.1;
801 const lunge = Math.pow(Math.max(0, Math.sin(t * rate)), 2);
802 gx += lunge * 0.11;
803 if (s > 0.60) {
804 const reach = Math.pow(Math.max(0, Math.sin(t * rate + pod * 0.92)), 4) * (0.30 + act * 0.24);
805 gx += Math.cos(ang) * reach;
806 gy += Math.sin(ang) * reach;
807 }
808 }
809 else if (key === 'code') {
810 const rate = 3.2 + act * 1.8;
811 const wave = Math.sin(t * rate - tail * 7.5);
812 const bump = 0.11 + act * 0.08;
813 gx += Math.cos(ang) * wave * bump;
814 gy += Math.sin(ang) * wave * bump * 1.2;
815 gx += Math.max(0, wave) * 0.07;
816 }
817 else {
818 const rate = 2.15 + act * 1.5;
819 const side = (pod % 2) * 2 - 1;
820 const w = Math.pow(Math.max(0, Math.sin(t * rate + pod * 0.72)), 2);
821 gx += w * 0.055;
822 gy += side * w * (0.17 + act * 0.13);
823 }
824 }
825 else if (arch === 'cross') {
826 if (key === 'browser') {
827 const band = ((t * (0.55 + act * 0.35)) % 1) * 1.28 - 0.64;
828 const inBand = Math.max(0, 1 - Math.abs(hy - band) / 0.08);
829 gx += inBand * (0.24 + act * 0.10);
830 gy += inBand * 0.02;
831 }
832 else {
833 const two = key === 'communication';
834 const courier = s < (two ? 0.44 : 0.32);
835 if (courier) {
836 const dir = two ? (s < 0.22 ? 1 : -1) : 1;
837 const u = (t * (0.38 + act * 0.36) + s * 5.2) % 1;
838 const going = u < 0.5 ? u * 2 : 2 - u * 2;
839 const e = going * going * (3 - 2 * going);
840 gx = lerp(hx, dir * 0.80, e);
841 gy = hy * (1 - e * 0.38) + Math.sin(going * Math.PI) * 0.11 * dir;
842 }
843 }
844 }
845 else if (arch === 'pod') {
846 const n = 6, member = podSlots?.length ? podSlots[pod % podSlots.length] : undefined;
847 const k = member ? member[0] : pod % n;
848 const hub = key === 'orchestration' && k === 0;
849 const spread = 0.30 + act * 0.11;
850 const orbit = t * (0.55 + act * 0.28);
851 if (hub) {
852 gx = px * 0.70;
853 gy = py * 0.70;
854 }
855 else {
856 const slots = key === 'orchestration' ? n - 1 : n;
857 const a = (key === 'orchestration' ? k - 1 : k) * (Math.PI * 2 / slots) + orbit + (member ? member[1] * .04 : 0);
858 const sc = 0.34;
859 gx = hx * sc + Math.cos(a) * spread * 1.28;
860 gy = hy * sc + Math.sin(a) * spread * 0.80;
861 }
862 }
863 else if (arch === 'tear') {
864 const side = hx + hy < 0 ? -1 : 1;
865 gx += side * (0.24 + (1 - coh) * 0.16);
866 gy += side * 0.15;
867 gx += Math.sin(t * 11.4 + s * 40) * (0.045 + act * 0.05);
868 gy += Math.cos(t * 9.2 + s * 31) * (0.040 + act * 0.045);
869 }
870 else if (arch === 'address') {
871 const face = 0.90 + att * 0.08;
872 const th = 0.70;
873 const z = (s - 0.5) * 0.42;
874 let ax = hx * Math.cos(th) + z * Math.sin(th);
875 let ay = hy;
876 const disc = 0.48 * face;
877 ax = lerp(ax, Math.cos(ang) * Math.min(0.36, rad + 0.06) * 0.95, disc);
878 ay = lerp(ay, Math.sin(ang) * Math.min(0.36, rad + 0.06) * 1.08, disc);
879 const grow = 1.20 + Math.sin(t * 1.65) * 0.055;
880 gx = ax * grow;
881 gy = ay * grow;
882 }
883 else {
884 const mill = 0.13 + (1 - coh) * 0.10;
885 gx = hx * 0.52 + Math.sin(t * 0.72 + jx) * mill;
886 gy = hy * 0.52 + Math.cos(t * 0.54 + jy) * mill;
887 }
888 if (expressionVersion === 2) {
889 const field = fieldTarget(q, t, act, att, key);
890 if (field)
891 [gx, gy] = field;
892 }
893 return [lerp(px, gx, work), lerp(py, gy, work)];
894 }
895 // Fixed reduced-motion clock per channel, so ticks still point along the gait.
896 const STILL_T = {
897 reasoning: 1.15, memory: 0.42, tool: 0.30, code: 0.18, filesystem: 0.48,
898 network: 0.72, browser: 0.95, communication: 0.58, agent: 1.25,
899 orchestration: 0.85, error: 0.35, human: 0.05, other: 0.90,
900 };
901 class PetSim {
902 expressionVersion;
903 p;
904 phase = 0;
905 clock = 0;
906 tear = 0;
907 prev;
908 col = [...REST_RGB];
909 cur;
910 frame = { r: REST_RGB[0], g: REST_RGB[1], b: REST_RGB[2], alpha: 0.3, hollow: false, channel: 'reasoning', arch: 'gyre', work: 0 };
911 constructor(points, seed = 0xC0FFEE, expressionVersion = 2) {
912 this.expressionVersion = expressionVersion;
913 if (expressionVersion !== 1 && expressionVersion !== 2)
914 throw new Error('Unsupported pet expression version.');
915 const rand = mulberry32(seed);
916 this.p = points.map(([hx, hy], i) => {
917 const q = {
918 x: hx, y: hy, vx: 0, vy: 0,
919 s: rand(), jx: rand() * 6.283, jy: rand() * 6.283, pod: i % 6,
920 hx, hy, ang: 0, rad: 0, tail: 0, tx: hx, ty: hy,
921 };
922 q.ang = Math.atan2(hy, hx);
923 q.rad = Math.hypot(hx, hy);
924 q.tail = clamp(((-hx - hy) * 0.5 + 0.22) / 0.62);
925 return q;
926 });
927 this.cur = this.prev = exports.CHANNEL_INDEX['reasoning'];
928 }
929 checkpoint() {
930 return { version: 1, expressionVersion: this.expressionVersion, body: this.p.map(p => [p.hx, p.hy, p.s]),
931 particles: this.p.map(p => [p.x, p.y, p.vx, p.vy, p.jx, p.jy, p.tx, p.ty]),
932 phase: this.phase, clock: this.clock, tear: this.tear, previous: this.prev, current: this.cur,
933 color: [...this.col], frame: { ...this.frame } };
934 }
935 /** Restore into a newly constructed sim. Authored body and seeded particle
936 * identity must match exactly; a checkpoint cannot replace the whale. */
937 restore(value) {
938 const c = value;
939 const inRange = (n, low, high) => Number.isFinite(n) && n >= low && n <= high;
940 if (!c || c.version !== 1 || c.expressionVersion !== undefined && ![1, 2].includes(c.expressionVersion) || (c.expressionVersion ?? 1) !== this.expressionVersion || !Array.isArray(c.body) || c.body.length !== this.p.length
941 || c.body.some((v, i) => !Array.isArray(v) || v.length !== 3 || v[0] !== this.p[i].hx || v[1] !== this.p[i].hy || v[2] !== this.p[i].s)
942 || !Array.isArray(c.particles) || c.particles.length !== this.p.length
943 || c.particles.some(v => !Array.isArray(v) || v.length !== 8 || v.some((n, i) => !inRange(n, i === 4 || i === 5 ? 0 : -8, i === 4 || i === 5 ? 2 * exports.PET_MAX_SECONDS : 8)))
944 || !inRange(c.phase, 0, exports.PET_MAX_SECONDS) || !inRange(c.clock, 0, exports.PET_MAX_SECONDS) || !inRange(c.tear, 0, 1)
945 || ![c.previous, c.current].every(n => Number.isInteger(n) && n >= 0 && n < exports.CHANNELS.length)
946 || !Array.isArray(c.color) || c.color.length !== 3 || c.color.some(n => !inRange(n, 0, 255))
947 || !c.frame || ![c.frame.r, c.frame.g, c.frame.b].every(n => inRange(n, 0, 255))
948 || !inRange(c.frame.alpha, 0, 1) || !inRange(c.frame.work, 0, 1) || typeof c.frame.hollow !== 'boolean'
949 || c.frame.channel !== exports.CHANNELS[c.current].key || c.frame.arch !== exports.CHANNELS[c.current].arch)
950 throw new Error('Invalid pet particle checkpoint.');
951 this.phase = c.phase;
952 this.clock = c.clock;
953 this.tear = c.tear;
954 this.prev = c.previous;
955 this.cur = c.current;
956 this.col = [...c.color];
957 this.frame = { ...c.frame };
958 this.p.forEach((p, i) => { [p.x, p.y, p.vx, p.vy, p.jx, p.jy, p.tx, p.ty] = c.particles[i]; });
959 }
960 /** Advance the sim by dt seconds under `state`. Identical math on every port. */
961 step(dt, state, opts) {
962 const S = (v) => lerp(0.5, v, opts.sensitivity);
963 const act = S(state.activity), coh = S(state.coherence), att = S(state.attention);
964 const seen = S(state.observed === undefined ? 1 : state.observed);
965 const motion = opts.motion ? 1 : 0;
966 this.phase += dt * (0.18 + act * 0.55) * motion;
967 this.clock += dt * (opts.motion ? 1 : 0);
968 if (exports.CHANNEL_INDEX[state.channel] !== undefined)
969 this.cur = exports.CHANNEL_INDEX[state.channel];
970 const shown = this.cur;
971 const ch = exports.CHANNELS[shown];
972 const work = clamp((act - 0.16) / 0.18);
973 const wander = lerp(0.32, 1, Math.pow(1 - coh, 1.15));
974 if (shown !== this.prev) {
975 if (shown === exports.CHANNEL_INDEX['error'])
976 this.tear = 1;
977 this.prev = shown;
978 }
979 this.tear = opts.motion ? Math.max(0, this.tear - dt * 1.6) : 0;
980 const split = Math.pow(1 - coh, 1.6) * 0.16 + this.tear * 0.10;
981 const blur = Math.pow(1 - coh, 1.45) * 0.22 + this.tear * 0.18;
982 const pull = opts.motion ? (2.2 + coh * 5.2) : 18;
983 const tGait = opts.motion ? this.clock : (STILL_T[ch.key] ?? 0.4);
984 for (const q of this.p) {
985 if (opts.motion) {
986 q.jx += dt * (0.40 + act * 1.1);
987 q.jy += dt * (0.34 + act * 0.9);
988 }
989 const [gx, gy] = gaitTarget(q, tGait, act, coh, att, ch.key, work, opts.podSlots, this.expressionVersion);
990 const podAng = q.pod * 1.047 + this.phase * 0.22;
991 const tx = gx + Math.sin(q.jx + q.s * 9) * blur * wander + Math.cos(podAng) * split;
992 const ty = gy + Math.cos(q.jy + q.s * 7) * blur * wander + Math.sin(podAng) * split * 0.55;
993 q.tx = tx;
994 q.ty = ty;
995 if (!opts.motion) {
996 q.x = tx;
997 q.y = ty;
998 q.vx = 0;
999 q.vy = 0;
1000 continue;
1001 }
1002 q.vx += (tx - q.x) * pull * dt;
1003 q.vy += (ty - q.y) * pull * dt;
1004 q.vx *= 0.90;
1005 q.vy *= 0.90;
1006 q.x += q.vx * dt * (opts.motion ? 2.6 : 8);
1007 q.y += q.vy * dt * (opts.motion ? 2.6 : 8);
1008 }
1009 // ---- visual encoding: the parts of "the same view" that are not motion
1010 const want = work > 0.35 ? RGB[shown] : REST_RGB;
1011 const k = opts.motion ? Math.min(1, dt * 2.6) : 1;
1012 for (let c = 0; c < 3; c++)
1013 this.col[c] += (lerp(UNKNOWN_RGB[c], want[c], seen) - this.col[c]) * k;
1014 const lit = clamp(state.lit);
1015 const alpha = (0.22 + act * 0.10) * lerp(0.50, 1, coh) * lerp(0.55, 1, seen) * lerp(0.35, 1, lit);
1016 this.frame = {
1017 r: this.col[0], g: this.col[1], b: this.col[2],
1018 alpha: Math.min(0.92, alpha * 1.85),
1019 hollow: seen < 0.92,
1020 channel: ch.key, arch: ch.arch, work,
1021 };
1022 }
1023 }
1024 exports.PetSim = PetSim;
1025 /** Body-space → renderer-space. Renderers place each dot at (lx,ly) in pixels/cells. */
1026 function layout(w, h, state) {
1027 const att = state.attention;
1028 const scale = Math.min(w * 0.52, h * 0.92) * (1 + att * 0.07);
1029 return {
1030 scale,
1031 flipX: state.flip,
1032 ox: w / 2 + state.roamX * w * 0.30,
1033 oy: h / 2 + state.roamY * h * 0.30 + h * att * 0.05,
1034 dot: Math.max(1.6, Math.min(w, h) * 0.0092) * (1 + att * 0.18),
1035 };
1036 }
1037 // ---------------------------------------------------------------------------
1038 // Conformance. A tape is a list of [dt, state] rows; run it and digest the
1039 // quantized field every `every` frames. 64×32 cells over [-0.66, 0.66]².
1040 // Two implementations that produce the same digest lines drew the same whale.
1041 function digest(sim) {
1042 const W = 64, H = 32;
1043 const grid = new Uint8Array(W * H);
1044 for (const q of sim.p) {
1045 const cx = Math.floor((q.x + 0.66) / 1.32 * W);
1046 const cy = Math.floor((q.y + 0.66) / 1.32 * H);
1047 if (cx >= 0 && cx < W && cy >= 0 && cy < H)
1048 grid[cy * W + cx] = Math.min(255, grid[cy * W + cx] + 1);
1049 }
1050 // FNV-1a 64 over the grid plus the frame encoding (rgb, hollow, alpha byte)
1051 // Two unsigned halves also work in embedded engines without BigInt.
1052 // FNV's prime is (256 << 32) + 435; these products stay below 2^42,
1053 // so every intermediate integer is exactly representable by a JS number.
1054 let hi = 0xcbf29ce4, lo = 0x84222325;
1055 const mix = (b) => {
1056 lo = (lo ^ (b & 0xff)) >>> 0;
1057 const product = lo * 435;
1058 hi = (hi * 435 + lo * 256 + Math.floor(product / 4294967296)) >>> 0;
1059 lo = product >>> 0;
1060 };
1061 for (const v of grid)
1062 mix(v);
1063 mix(Math.round(sim.frame.r));
1064 mix(Math.round(sim.frame.g));
1065 mix(Math.round(sim.frame.b));
1066 mix(Math.round(sim.frame.alpha * 255));
1067 mix(sim.frame.hollow ? 1 : 0);
1068 return hi.toString(16).padStart(8, '0') + lo.toString(16).padStart(8, '0');
1069 }
1070 /** Shared tape runner. `rows` are parsed tape.tsv lines. */
1071 function runTape(sim, rows, opts) {
1072 const out = [];
1073 let f = 0;
1074 for (const row of rows) {
1075 const c = row.split('\t');
1076 if (c.length < 10 || c[0] === 'dt')
1077 continue;
1078 const st = {
1079 activity: +c[1], coherence: +c[2], attention: +c[3], channel: c[4],
1080 observed: +c[5], roamX: +c[6], roamY: +c[7], flip: +c[8], lit: +c[9],
1081 };
1082 sim.step(+c[0], st, opts);
1083 if (f++ % 30 === 0)
1084 out.push(`f${String(f - 1).padStart(4, '0')} ${digest(sim)} ${st.channel}`);
1085 }
1086 out.push(`final ${digest(sim)}`);
1087 return out;
1088 }
1089
1090 };
1091 factories["pet-telemetry"]=function(exports,require){
1092 "use strict";
1093 Object.defineProperty(exports, "__esModule", { value: true });
1094 exports.PetLiveTape = exports.PET_BIN_MS = void 0;
1095 exports.validatePetBucket = validatePetBucket;
1096 exports.decodePetJSONL = decodePetJSONL;
1097 exports.compilePetTelemetry = compilePetTelemetry;
1098 exports.encodePetJSONL = encodePetJSONL;
1099 exports.encodePetTSV = encodePetTSV;
1100 const model_js_1 = require("./model.js");
1101 const signal_js_1 = require("./signal.js");
1102 const pet_sim_js_1 = require("./pet-sim.js");
1103 /** One projection for imports, demos and recorded live snapshots. Times are ms.
1104 * These are aesthetic encodings of measured events, never model confidence. */
1105 exports.PET_BIN_MS = 400;
1106 function validatePetBucket(value) {
1107 (0, pet_sim_js_1.validatePetState)(value);
1108 const b = value;
1109 if (!b || typeof b !== 'object' || b.version !== 1 || !Number.isSafeInteger(b.sequence) || b.sequence < 0 || b.sequence > pet_sim_js_1.PET_MAX_SECONDS * 2.5
1110 || b.simTimeMs !== b.sequence * exports.PET_BIN_MS || b.durationMs !== exports.PET_BIN_MS
1111 || !model_js_1.CATEGORIES.includes(b.channel) || typeof b.waiting !== 'boolean'
1112 || !Array.isArray(b.agentIds) || b.agentIds.length > 250_000 || b.agentIds.some(id => typeof id !== 'string' || !id || id.length > 4096)
1113 || !Number.isSafeInteger(b.errors) || b.errors < 0 || b.errors > 250_000
1114 || !Array.isArray(b.onsets) || b.onsets.length !== 13 || b.onsets.some(n => !Number.isSafeInteger(n) || n < 0 || n > 250_000)
1115 || !Array.isArray(b.activeMs) || b.activeMs.length !== 13 || b.activeMs.some(n => !Number.isFinite(n) || n < 0 || n > 100_000_000))
1116 throw new Error('Invalid version 1 pet bucket.');
1117 }
1118 function decodePetJSONL(text) {
1119 if (text.length > 64 * 1024 * 1024)
1120 throw new Error('Pet tape exceeds 64 MiB.');
1121 const rows = text.split(/\r?\n/).filter(line => line.trim()).map(line => JSON.parse(line));
1122 if (rows.length > 216_000)
1123 throw new Error('Pet tape exceeds 24 hours.');
1124 return rows.map((row, i) => { validatePetBucket(row); if (row.sequence !== i)
1125 throw new Error('Non-contiguous pet tape.'); return row; });
1126 }
1127 /** A live file must advance before its contents count as a new observation.
1128 * Existing bytes, duplicate samples and a restarted sequence establish a
1129 * baseline; they never replay an old onset or human request. Drivers supply a
1130 * bounded tail and reset this cursor after suspension or a new attachment. */
1131 class PetLiveTape {
1132 sequence;
1133 reset() { this.sequence = undefined; }
1134 readTail(text) {
1135 if (!text) {
1136 this.reset();
1137 return;
1138 }
1139 if (text.length > 262_144) {
1140 this.reset();
1141 throw new Error('Live pet input exceeds its tail limit.');
1142 }
1143 if (!text.endsWith('\n'))
1144 return;
1145 const line = text.trimEnd().split('\n').at(-1);
1146 if (!line)
1147 return;
1148 let packet;
1149 try {
1150 packet = JSON.parse(line);
1151 validatePetBucket(packet);
1152 }
1153 catch (error) {
1154 this.reset();
1155 throw error;
1156 }
1157 const previous = this.sequence;
1158 this.sequence = packet.sequence;
1159 if (previous === undefined || packet.sequence <= previous)
1160 return;
1161 return packet;
1162 }
1163 }
1164 exports.PetLiveTape = PetLiveTape;
1165 const order = (a, b) => a < b ? -1 : a > b ? 1 : 0;
1166 const keyOf = (e) => JSON.stringify([e.traceId, e.id]);
1167 const isContainer = (e) => e.attributes['whalesong.container'] === true
1168 || e.attributes['codewhale.container'] === true;
1169 /** Compile a single trace. Unknown-duration spans provide onsets, not occupancy.
1170 * Updates of the same trace/id replace earlier snapshots rather than double count.
1171 * An endpoint onset gets its own bucket; intervals use [start, end). */
1172 function compilePetTelemetry(input, durationMs = 0, firstSequence = 0, originMs = 0) {
1173 if (input.length > 250_000)
1174 throw new Error('Pet input exceeds 250000 events.');
1175 if (!Number.isFinite(durationMs) || durationMs < 0)
1176 throw new Error('Invalid pet duration.');
1177 if (!Number.isSafeInteger(firstSequence) || firstSequence < 0)
1178 throw new Error('Invalid first pet bucket.');
1179 if (!Number.isFinite(originMs))
1180 throw new Error('Invalid pet clock origin.');
1181 durationMs = Math.max(0, durationMs - originMs);
1182 const unique = new Map();
1183 const traces = new Set();
1184 for (const e of input) {
1185 if (e.schemaVersion !== 1 || !e.id || !e.traceId || !model_js_1.CATEGORIES.includes(e.category)
1186 || !Number.isFinite(e.startTime) || !Number.isFinite(e.endTime)
1187 || e.startTime < 0 || e.endTime < e.startTime || !e.attributes)
1188 throw new Error('Invalid event-v1 pet input. Import through importTrace first.');
1189 traces.add(e.traceId);
1190 unique.set(keyOf(e), e);
1191 }
1192 if (traces.size > 1)
1193 throw new Error('Select one trace for the pet.');
1194 const parents = new Set([...unique.values()].filter(e => e.parentId).map(e => e.parentId));
1195 const events = [...unique.values()].filter(e => !isContainer(e)
1196 && !(e.category === 'orchestration' && parents.has(e.id)))
1197 .map(e => ({ ...e, startTime: e.startTime - originMs, endTime: (e.openEnded ? e.startTime : e.endTime) - originMs,
1198 attributes: e.attributes['whalesong.error_onset_ms'] === undefined ? e.attributes
1199 : { ...e.attributes, 'whalesong.error_onset_ms': (0, model_js_1.errorOnsetOf)(e) - originMs } }))
1200 .sort((a, b) => a.startTime - b.startTime || order(a.id, b.id));
1201 let lastOnset = 0;
1202 for (const e of events) {
1203 durationMs = Math.max(durationMs, e.endTime);
1204 lastOnset = Math.max(lastOnset, e.startTime);
1205 }
1206 const failures = events.filter(e => e.category === 'error' || e.status === 'error').map(model_js_1.errorOnsetOf).sort((a, b) => a - b);
1207 if (failures.length)
1208 lastOnset = Math.max(lastOnset, failures[failures.length - 1]);
1209 const count = Math.max(1, Math.ceil(durationMs / exports.PET_BIN_MS), Math.floor(lastOnset / exports.PET_BIN_MS) + 1);
1210 if (count - firstSequence > 216_000 || count > pet_sim_js_1.PET_MAX_SECONDS * 2.5)
1211 throw new Error('Pet replay exceeds 24 hours; select a shorter trace.');
1212 const index = new signal_js_1.IntervalIndex(events), result = [];
1213 const recent = [], names = new Map();
1214 let next = 0, expired = 0, nextFailure = 0;
1215 for (let sequence = firstSequence; sequence < count; sequence++) {
1216 const start = sequence * exports.PET_BIN_MS, end = start + exports.PET_BIN_MS;
1217 // A trailing window only: appending future events cannot rewrite earlier bins.
1218 while (next < events.length && events[next].startTime < end) {
1219 const e = events[next++];
1220 // A liveness pulse continues an operation; it is not a repeated tool call.
1221 if (e.attributes['whalesong.continuation'] === true)
1222 continue;
1223 recent.push(e);
1224 names.set(e.name, (names.get(e.name) ?? 0) + 1);
1225 }
1226 while (expired < recent.length && recent[expired].startTime < end - 12_000) {
1227 const name = recent[expired++].name, n = names.get(name) - 1;
1228 if (n)
1229 names.set(name, n);
1230 else
1231 names.delete(name);
1232 }
1233 const onsets = model_js_1.CATEGORIES.map(() => 0), activeMs = model_js_1.CATEGORIES.map(() => 0);
1234 const agents = new Set();
1235 while (nextFailure < failures.length && failures[nextFailure] < start)
1236 nextFailure++;
1237 let errors = 0, waiting = false, human = false;
1238 while (nextFailure < failures.length && failures[nextFailure] < end) {
1239 errors++;
1240 nextFailure++;
1241 }
1242 for (const e of index.query(start, end)) {
1243 if (e.startTime >= end)
1244 continue;
1245 const onset = e.startTime >= start, c = model_js_1.CATEGORIES.indexOf(e.category);
1246 const overlap = Math.max(0, Math.min(end, e.endTime) - Math.max(start, e.startTime));
1247 if (!onset && !overlap)
1248 continue;
1249 activeMs[c] += overlap;
1250 if (onset)
1251 onsets[c]++;
1252 if (e.agentId && !['unknown', 'unattributed'].includes(e.agentId))
1253 agents.add(e.agentId);
1254 if (e.category === 'human') {
1255 human = true;
1256 waiting ||= e.status === 'pending' || e.status === 'running' || e.attributes['whalesong.waiting'] === true;
1257 }
1258 }
1259 const total = activeMs.reduce((a, b) => a + b, 0), hits = onsets.reduce((a, b) => a + b, 0);
1260 const observed = total > 0 || hits > 0 || errors > 0;
1261 let dominant = model_js_1.CATEGORIES.indexOf('other');
1262 for (let c = 0; c < model_js_1.CATEGORIES.length; c++) {
1263 if (activeMs[c] > activeMs[dominant]
1264 || activeMs[c] === activeMs[dominant] && onsets[c] > onsets[dominant])
1265 dominant = c;
1266 }
1267 let repeated = 0;
1268 for (const n of names.values())
1269 if (n >= 4)
1270 repeated += n;
1271 const repeatDensity = repeated / Math.max(1, recent.length - expired);
1272 const channel = errors ? 'error' : human ? 'human' : agents.size >= 3 ? 'agent' : model_js_1.CATEGORIES[dominant];
1273 result.push({ version: 1, sequence, simTimeMs: start, durationMs: exports.PET_BIN_MS,
1274 activity: observed ? (0, model_js_1.clamp)(.28 + .38 * total / exports.PET_BIN_MS + .08 * hits, 0, 1) : .12,
1275 coherence: observed ? (0, model_js_1.clamp)(.92 - repeatDensity * .58 - Math.min(.55, errors * .18), .08, 1) : .25,
1276 attention: waiting ? .8 : human ? .65 : errors ? .45 : 0,
1277 channel, observed: observed ? 1 : 0, roamX: 0, roamY: 0, flip: 1, lit: 1,
1278 onsets, activeMs, errors, agentIds: [...agents].sort(order), waiting });
1279 }
1280 return result;
1281 }
1282 /** Flat state remains compatible with native readers; metadata preserves audio
1283 * onsets and peer identities. No prompts, tool arguments, or event names escape. */
1284 function encodePetJSONL(buckets) {
1285 return buckets.map(b => JSON.stringify(b)).join('\n') + (buckets.length ? '\n' : '');
1286 }
1287 /** Legacy visual conformance interchange. Use JSONL to preserve audio metadata. */
1288 function encodePetTSV(buckets) {
1289 return 'dt\tactivity\tcoherence\tattention\tchannel\tobserved\troamX\troamY\tflip\tlit\n'
1290 + buckets.map(b => [b.durationMs / 1000, b.activity, b.coherence, b.attention, b.channel,
1291 b.observed, b.roamX, b.roamY, b.flip, b.lit].join('\t')).join('\n') + '\n';
1292 }
1293
1294 };
1295 factories["signal"]=function(exports,require){
1296 "use strict";
1297 Object.defineProperty(exports, "__esModule", { value: true });
1298 exports.IntervalIndex = void 0;
1299 exports.buildPyramid = buildPyramid;
1300 exports.chooseLevel = chooseLevel;
1301 exports.binValue = binValue;
1302 exports.intensity = intensity;
1303 exports.totals = totals;
1304 exports.onsetSeries = onsetSeries;
1305 exports.autocorrelation = autocorrelation;
1306 exports.periodogram = periodogram;
1307 exports.unionDuration = unionDuration;
1308 const model_js_1 = require("./model.js");
1309 function emptyLevel(length, binMs) {
1310 const n = model_js_1.CATEGORIES.length * length;
1311 return { binMs, length, onsets: new Float64Array(n), activeMs: new Float64Array(n),
1312 outputTokens: new Float64Array(n), cost: new Float64Array(n), errors: new Float64Array(n), peak: new Float64Array(n) };
1313 }
1314 const FIELDS = ['onsets', 'activeMs', 'outputTokens', 'cost', 'errors'];
1315 /** O(events + channels × bins), including long intervals. No span-length inner loop. */
1316 function buildPyramid(events, requestedDuration, maxBins = 16_384) {
1317 if (!Number.isInteger(maxBins) || maxBins < 16 || maxBins > 1_048_576)
1318 throw new Error('maxBins must be an integer in [16, 1048576].');
1319 let duration = requestedDuration ?? 1;
1320 for (const e of events)
1321 duration = Math.max(duration, e.endTime, e.startTime, e.status === 'error' ? (0, model_js_1.errorOnsetOf)(e) : 0);
1322 if (!Number.isFinite(duration) || duration < 0)
1323 throw new Error('Signal duration must be finite and nonnegative.');
1324 duration = Math.max(1, duration);
1325 const binMs = 2 ** Math.ceil(Math.log2(Math.max(1, duration / (maxBins - 1))));
1326 const length = Math.floor(duration / binMs) + 1, fine = emptyLevel(length, binMs);
1327 const stride = length + 1;
1328 const activeDiff = new Float64Array(model_js_1.CATEGORIES.length * stride), tokenDiff = new Float64Array(model_js_1.CATEGORIES.length * stride);
1329 for (const e of events) {
1330 if (!Number.isFinite(e.startTime) || !Number.isFinite(e.endTime) || e.startTime < 0 || e.endTime < e.startTime)
1331 throw new Error(`Invalid interval for ${e.id}.`);
1332 const channel = model_js_1.CATEGORIES.indexOf(e.category);
1333 if (channel < 0)
1334 throw new Error(`Unknown category for ${e.id}.`);
1335 const a = Math.floor(e.startTime / binMs), b = Math.floor(e.endTime / binMs);
1336 const at = channel * length, diff = channel * stride;
1337 fine.onsets[at + a]++;
1338 fine.cost[at + a] += e.cost ?? 0;
1339 if (e.status === 'error')
1340 fine.errors[at + Math.floor((0, model_js_1.errorOnsetOf)(e) / binMs)]++;
1341 const d = e.endTime - e.startTime, tokens = e.outputTokens ?? 0;
1342 if (d === 0) {
1343 fine.outputTokens[at + a] += tokens;
1344 continue;
1345 }
1346 if (a === b) {
1347 fine.activeMs[at + a] += d;
1348 fine.outputTokens[at + a] += tokens;
1349 }
1350 else {
1351 const left = (a + 1) * binMs - e.startTime, right = e.endTime - b * binMs, tokenRate = tokens / d;
1352 fine.activeMs[at + a] += left;
1353 fine.activeMs[at + b] += right;
1354 fine.outputTokens[at + a] += left * tokenRate;
1355 fine.outputTokens[at + b] += right * tokenRate;
1356 if (b > a + 1) {
1357 activeDiff[diff + a + 1] += binMs;
1358 activeDiff[diff + b] -= binMs;
1359 tokenDiff[diff + a + 1] += binMs * tokenRate;
1360 tokenDiff[diff + b] -= binMs * tokenRate;
1361 }
1362 }
1363 }
1364 for (let c = 0; c < model_js_1.CATEGORIES.length; c++) {
1365 let active = 0, tokens = 0;
1366 for (let i = 0; i < length; i++) {
1367 active += activeDiff[c * stride + i];
1368 tokens += tokenDiff[c * stride + i];
1369 const at = c * length + i;
1370 fine.activeMs[at] = Math.max(0, fine.activeMs[at] + active);
1371 fine.outputTokens[at] = Math.max(0, fine.outputTokens[at] + tokens);
1372 fine.peak[at] = fine.activeMs[at] / binMs;
1373 }
1374 }
1375 const levels = [fine];
1376 while (levels.at(-1).length > 1) {
1377 const child = levels.at(-1), parent = emptyLevel(Math.ceil(child.length / 2), child.binMs * 2);
1378 for (let c = 0; c < model_js_1.CATEGORIES.length; c++)
1379 for (let i = 0; i < parent.length; i++) {
1380 const a = c * child.length + i * 2, b = a + 1, dst = c * parent.length + i, hasB = i * 2 + 1 < child.length;
1381 for (const key of FIELDS)
1382 parent[key][dst] = child[key][a] + (hasB ? child[key][b] : 0);
1383 parent.peak[dst] = Math.max(child.peak[a], hasB ? child.peak[b] : 0);
1384 }
1385 levels.push(parent);
1386 }
1387 const p = { duration, channels: model_js_1.CATEGORIES, levels, calibration: { activity: 1, onsets: 1, tokens: 1, cost: 1 } };
1388 const reference = chooseLevel(p, duration / 1200);
1389 for (const metric of ['activity', 'onsets', 'tokens', 'cost']) {
1390 const positives = [];
1391 for (let c = 0; c < model_js_1.CATEGORIES.length; c++)
1392 for (let i = 0; i < reference.length; i++) {
1393 const v = binValue(reference, c, i, metric);
1394 if (v > 0)
1395 positives.push(v);
1396 }
1397 p.calibration[metric] = Math.max(1e-9, (0, model_js_1.quantile)(positives, .95));
1398 }
1399 return p;
1400 }
1401 /** Choose the coarsest stored level no wider than one requested pixel interval. */
1402 function chooseLevel(p, targetBinMs) {
1403 let result = p.levels[0];
1404 for (const level of p.levels) {
1405 if (level.binMs > targetBinMs)
1406 break;
1407 result = level;
1408 }
1409 return result;
1410 }
1411 function binValue(level, channel, bin, metric) {
1412 if (bin < 0 || bin >= level.length)
1413 return 0;
1414 const at = channel * level.length + bin;
1415 if (metric === 'activity')
1416 return level.activeMs[at] / level.binMs;
1417 if (metric === 'onsets')
1418 return level.onsets[at] * 1000 / level.binMs;
1419 if (metric === 'tokens')
1420 return level.outputTokens[at] * 1000 / level.binMs;
1421 return level.cost[at] * 1000 / level.binMs;
1422 }
1423 function intensity(value, reference) {
1424 return (0, model_js_1.clamp)(Math.log1p(value / Math.max(1e-9, reference) * 8) / Math.log(9), 0, 1);
1425 }
1426 /** Conserved totals: useful for tests and alternate native backends. */
1427 function totals(level) {
1428 return Object.fromEntries(FIELDS.map(k => [k, level[k].reduce((s, x) => s + x, 0)]));
1429 }
1430 /** Sorted-start, max-end segment tree. Long root spans do not force a reverse
1431 * scan through every earlier event. Results are chronological and honor limits. */
1432 class IntervalIndex {
1433 events;
1434 maxEnd;
1435 leafCount;
1436 constructor(events) {
1437 this.events = [...events].sort((a, b) => a.startTime - b.startTime || a.id.localeCompare(b.id));
1438 this.leafCount = 2 ** Math.ceil(Math.log2(Math.max(1, this.events.length)));
1439 this.maxEnd = new Float64Array(this.leafCount * 2).fill(-Infinity);
1440 for (let i = 0; i < this.events.length; i++)
1441 this.maxEnd[this.leafCount + i] = this.events[i].endTime;
1442 for (let i = this.leafCount - 1; i; i--)
1443 this.maxEnd[i] = Math.max(this.maxEnd[i * 2], this.maxEnd[i * 2 + 1]);
1444 }
1445 query(start, end, limit = Infinity) {
1446 if (!Number.isFinite(start) || !Number.isFinite(end) || end < start || limit <= 0)
1447 return [];
1448 let lo = 0, hi = this.events.length;
1449 while (lo < hi) {
1450 const mid = (lo + hi) >>> 1;
1451 if (this.events[mid].startTime <= end)
1452 lo = mid + 1;
1453 else
1454 hi = mid;
1455 }
1456 const bound = lo, found = [];
1457 const visit = (node, left, right) => {
1458 if (left >= bound || this.maxEnd[node] < start || found.length >= limit)
1459 return;
1460 if (right - left === 1) {
1461 const e = this.events[left];
1462 if (e && (e.endTime > start || e.startTime === e.endTime && e.startTime >= start))
1463 found.push(e);
1464 return;
1465 }
1466 const mid = (left + right) >>> 1;
1467 visit(node * 2, left, mid);
1468 visit(node * 2 + 1, mid, right);
1469 };
1470 visit(1, 0, this.leafCount);
1471 return found;
1472 }
1473 }
1474 exports.IntervalIndex = IntervalIndex;
1475 /** Sample an onset train into equal-width bins; nothing is inferred between impulses. */
1476 function onsetSeries(events, start, end, n = 256, category) {
1477 const out = new Float64Array(n), span = Math.max(1e-6, end - start);
1478 for (const e of events)
1479 if ((!category || e.category === category) && e.startTime >= start && e.startTime < end) {
1480 const at = Math.floor((e.startTime - start) / span * n);
1481 if (at >= 0 && at < n)
1482 out[at]++;
1483 }
1484 return out;
1485 }
1486 /** Centered, variance-normalized autocorrelation; lag-zero is one unless constant. */
1487 function autocorrelation(input, maxLag = 64) {
1488 const n = input.length;
1489 if (!n)
1490 return [];
1491 let mean = 0;
1492 for (let i = 0; i < n; i++)
1493 mean += input[i];
1494 mean /= n;
1495 const centered = Array.from(input, x => x - mean), energy = centered.reduce((s, v) => s + v * v, 0);
1496 const result = [];
1497 for (let lag = 0; lag <= Math.min(maxLag, n - 1); lag++) {
1498 let sum = 0;
1499 for (let i = 0; i < n - lag; i++)
1500 sum += centered[i] * centered[i + lag];
1501 result.push(energy > 1e-12 ? sum / energy : 0);
1502 }
1503 return result;
1504 }
1505 /** Hann-window periodogram of an actual uniformly sampled onset signal, DC removed. */
1506 function periodogram(input, sampleHz) {
1507 const n = input.length, frequencies = [], power = [];
1508 if (n < 4 || sampleHz <= 0)
1509 return { frequencies, power, entropy: 0, peakHz: 0, sampleHz, resolutionHz: 0 };
1510 let mean = 0;
1511 for (let i = 0; i < n; i++)
1512 mean += input[i];
1513 mean /= n;
1514 const windowed = Array.from(input, (x, i) => (x - mean) * (.5 - .5 * Math.cos(2 * Math.PI * i / (n - 1))));
1515 const windowEnergy = Array.from({ length: n }, (_, i) => (.5 - .5 * Math.cos(2 * Math.PI * i / (n - 1))) ** 2).reduce((a, b) => a + b, 0);
1516 for (let k = 1; k <= Math.floor(n / 2); k++) {
1517 let re = 0, im = 0;
1518 for (let t = 0; t < n; t++) {
1519 const angle = 2 * Math.PI * k * t / n;
1520 re += windowed[t] * Math.cos(angle);
1521 im -= windowed[t] * Math.sin(angle);
1522 }
1523 frequencies.push(k * sampleHz / n);
1524 power.push((re * re + im * im) / (windowEnergy * sampleHz) * (n % 2 === 0 && k === n / 2 ? 1 : 2));
1525 }
1526 const sum = power.reduce((s, x) => s + x, 0);
1527 let entropy = 0;
1528 for (const x of power)
1529 if (x > 0 && sum > 0) {
1530 const p = x / sum;
1531 entropy -= p * Math.log2(p);
1532 }
1533 entropy = power.length > 1 ? entropy / Math.log2(power.length) : 0;
1534 const max = Math.max(...power);
1535 return { frequencies, power, entropy, peakHz: max > 1e-12 ? frequencies[power.indexOf(max)] : 0, sampleHz, resolutionHz: sampleHz / n };
1536 }
1537 function unionDuration(events, start = 0, end = Infinity) {
1538 const intervals = events.filter(e => e.endTime > e.startTime && e.endTime > start && e.startTime < end)
1539 .map(e => [Math.max(start, e.startTime), Math.min(end, e.endTime)]).sort((a, b) => a[0] - b[0]);
1540 let total = 0, left = 0, right = 0;
1541 for (const [a, b] of intervals) {
1542 if (a > right) {
1543 total += right - left;
1544 left = a;
1545 right = b;
1546 }
1547 else
1548 right = Math.max(right, b);
1549 }
1550 return total + right - left;
1551 }
1552
1553 };
1554 factories["pet-audio"]=function(exports,require){
1555 "use strict";
1556 Object.defineProperty(exports, "__esModule", { value: true });
1557 exports.PetScore = void 0;
1558 exports.renderPetPCM = renderPetPCM;
1559 const pet_sim_js_1 = require("./pet-sim.js");
1560 const model_js_1 = require("./model.js");
1561 /** Core emits score events; WebAudio / AVAudioEngine only present their PCM.
1562 * Calling at any display cadence produces the same score when all world ticks
1563 * are supplied. Calling twice for a world tick cannot retrigger a voice. */
1564 class PetScore {
1565 lastWindow = -1;
1566 lastSequence = -1;
1567 lastAddress = false;
1568 checkpoint() { return [this.lastWindow, this.lastSequence, this.lastAddress]; }
1569 restore(value) {
1570 if (!Array.isArray(value) || value.length !== 3
1571 || !value.slice(0, 2).every(n => Number.isSafeInteger(n) && n >= -1 && n <= pet_sim_js_1.PET_MAX_SECONDS * 2.5) || typeof value[2] !== 'boolean')
1572 throw new Error('Invalid pet score checkpoint.');
1573 [this.lastWindow, this.lastSequence, this.lastAddress] = value;
1574 }
1575 voices(frame) {
1576 const time = frame.timeMs / 1000, window = Math.floor((frame.timeMs + 1e-7) / 400);
1577 const out = [];
1578 const add = (id, frequency, duration, gain, pan = 0, delay = 0, kind = 'tone') => out.push({ id, start: time + delay, duration, frequency, gain, pan, kind });
1579 const t = frame.telemetry, fresh = t !== undefined && t.sequence !== this.lastSequence;
1580 if (fresh) {
1581 this.lastSequence = t.sequence;
1582 for (let c = 0; c < pet_sim_js_1.CHANNELS.length; c++) {
1583 const channel = pet_sim_js_1.CHANNELS[c], n = t.onsets[c];
1584 if (!n || channel.sustained || ['human', 'error'].includes(channel.key))
1585 continue;
1586 add(`onset:${t.sequence}:${c}`, channel.freq, .24, .035 * Math.min(2, Math.sqrt(n)), (c / 12 - .5) * .7);
1587 }
1588 if (t.errors)
1589 add(`tear:${t.sequence}`, pet_sim_js_1.CHANNELS.find(c => c.key === 'error').freq, .22, .05, 0, 0, 'noise');
1590 }
1591 const address = frame.state.channel === 'human' && frame.state.attention > .5;
1592 if (address && (!this.lastAddress || fresh && t.onsets[11] > 0)) {
1593 const frequency = pet_sim_js_1.CHANNELS[11].freq;
1594 for (let i = 0; i < 3; i++)
1595 add(`address:${frame.timeMs}:${i}`, frequency * [1, 1.25, 1.5][i], .23, .032, 0, i * .14);
1596 }
1597 this.lastAddress = address;
1598 if (window !== this.lastWindow) {
1599 this.lastWindow = window;
1600 if (frame.state.observed >= .92) {
1601 const channel = pet_sim_js_1.CHANNELS.find(c => c.key === frame.state.channel);
1602 if (channel.sustained && frame.behaviour !== 'doze') {
1603 const peers = frame.state.channel === 'agent' ? Math.max(1, frame.pod.filter(p => p.present).length) : 1;
1604 for (let i = 0; i < peers; i++)
1605 add(`sustain:${window}:${i}`, channel.freq * (1 + (i - (peers - 1) / 2) * .004), .44, (.02 + frame.state.activity * .02) / Math.sqrt(peers), peers === 1 ? 0 : i / (peers - 1) - .5);
1606 }
1607 if (frame.behaviour === 'doze' && window % 5 === 0) {
1608 add(`heart:${window}:0`, 49, .3, .035);
1609 add(`heart:${window}:1`, 49, .24, .023, 0, .33);
1610 }
1611 if (frame.needs === 'call' && window % 10 === 0)
1612 add(`call:${window}`, pet_sim_js_1.CHANNELS[11].freq * 1.5, .38, .03);
1613 }
1614 }
1615 return out;
1616 }
1617 }
1618 exports.PetScore = PetScore;
1619 /** Sample-addressed noise: no global RNG, identical samples when chunked/seeking. */
1620 function noise(seed, sample) {
1621 let x = (seed + Math.imul(sample, 0x6D2B79F5)) >>> 0;
1622 x = Math.imul(x ^ x >>> 15, x | 1);
1623 x ^= x + Math.imul(x ^ x >>> 7, x | 61);
1624 return ((x ^ x >>> 14) >>> 0) / 2147483648 - 1;
1625 }
1626 function renderPetPCM(voices, startSample, length, sampleRate = 48_000) {
1627 if (!Number.isInteger(sampleRate) || sampleRate < 8000 || sampleRate > 96000
1628 || !Number.isSafeInteger(startSample) || startSample < 0 || !Number.isInteger(length) || length < 0 || length > sampleRate * 120)
1629 throw new Error('Invalid pet PCM range.');
1630 const left = new Float32Array(length), right = new Float32Array(length);
1631 for (const v of voices) {
1632 if (![v.start, v.duration, v.frequency, v.gain, v.pan].every(Number.isFinite)
1633 || v.start < 0 || v.duration <= 0 || v.duration > 10 || v.frequency <= 0 || v.frequency > sampleRate / 2
1634 || v.gain < 0 || v.gain > 1 || Math.abs(v.pan) > 1 || !['tone', 'noise'].includes(v.kind))
1635 throw new Error('Invalid pet voice.');
1636 const first = Math.max(startSample, Math.ceil(v.start * sampleRate));
1637 const last = Math.min(startSample + length, Math.ceil((v.start + v.duration) * sampleRate));
1638 const pan = (v.pan + 1) * Math.PI / 4, seed = (0xC0FFEE ^ (0, model_js_1.stableHash)(v.id)) >>> 0;
1639 for (let absolute = first; absolute < last; absolute++) {
1640 const age = absolute / sampleRate - v.start;
1641 const envelope = Math.min(1, age / .015, (v.duration - age) / .045);
1642 const sample = v.kind === 'noise' ? noise(seed, absolute) * Math.exp(-age * 14)
1643 : Math.sin(2 * Math.PI * v.frequency * age) * .88 + Math.sin(4 * Math.PI * v.frequency * age) * .12;
1644 const value = sample * Math.max(0, envelope) * v.gain, at = absolute - startSample;
1645 left[at] += value * Math.cos(pan);
1646 right[at] += value * Math.sin(pan);
1647 }
1648 }
1649 // Limiting is a presentation operation and cannot perturb voice scheduling.
1650 for (let i = 0; i < length; i++) {
1651 left[i] = (0, model_js_1.clamp)(left[i], -1, 1);
1652 right[i] = (0, model_js_1.clamp)(right[i], -1, 1);
1653 }
1654 return { left, right };
1655 }
1656
1657 };
1658 factories["pet-engine"]=function(exports,require){
1659 "use strict";
1660 Object.defineProperty(exports, "__esModule", { value: true });
1661 exports.PetEngineTelemetry = exports.ENGINE_OWNER_STALE_MS = void 0;
1662 const pet_sim_js_1 = require("./pet-sim.js");
1663 const pet_telemetry_js_1 = require("./pet-telemetry.js");
1664 // Engine emits a liveness pulse every 10 seconds for a running operation.
1665 // Keep a small scheduling margin so one delayed pulse does not erase a valid
1666 // long operation from the owner projection; after that, it becomes generic.
1667 // The physics tape keeps its own, shorter coverage rule (see `pulse`).
1668 exports.ENGINE_OWNER_STALE_MS = 12_000;
1669 const ACTIVITY_KINDS = [
1670 'reading', 'editing', 'searching', 'testing', 'executing', 'browsing', 'computer',
1671 'memory', 'tool', 'thinking', 'responding', 'delegating',
1672 ];
1673 const TURN_OUTCOMES = ['completed', 'interrupted', 'failed'];
1674 const OPERATION_OUTCOMES = ['succeeded', 'failed', 'cancelled', 'denied'];
1675 const APPROVAL_OUTCOMES = ['approved', 'denied', 'cancelled'];
1676 function categoryFor(kind) {
1677 switch (kind) {
1678 case 'reading':
1679 case 'editing':
1680 case 'searching': return 'filesystem';
1681 case 'testing':
1682 case 'executing': return 'code';
1683 case 'browsing': return 'browser';
1684 case 'computer':
1685 case 'tool': return 'tool';
1686 case 'memory': return 'memory';
1687 case 'thinking': return 'reasoning';
1688 case 'responding': return 'communication';
1689 case 'delegating': return 'agent';
1690 }
1691 }
1692 /** Read-only reducer for the Engine owner's typed metadata. It keeps using the
1693 * existing pet telemetry tape and physics owner; it neither recognizes tool
1694 * names nor accepts transcript text, arguments, commands, or results. */
1695 class PetEngineTelemetry {
1696 events = [];
1697 active = new Map();
1698 waiting;
1699 sequence = 0;
1700 lastTime = 0;
1701 lastObservedAt;
1702 turnId;
1703 turnOutcome;
1704 terminalAt;
1705 lastFailedTool;
1706 completedSpans = new Set();
1707 completedTurns = new Set();
1708 /** Ephemeral safe read projection. Span ids are retained only in this
1709 * reducer to correlate trusted lifecycle events and never leave the owner. */
1710 activity(at) {
1711 const observed = this.lastObservedAt !== undefined;
1712 const freshness = !observed ? 'missing'
1713 : at - this.lastObservedAt <= exports.ENGINE_OWNER_STALE_MS ? 'fresh' : 'stale';
1714 const fresh = freshness === 'fresh';
1715 const active = fresh
1716 ? [...this.active.values()]
1717 .filter(span => at >= span.startedAtMs && at - span.event.endTime <= exports.ENGINE_OWNER_STALE_MS)
1718 // The parent's own work leads; delegated agents are counted in
1719 // `parallelAgentCount` and only lead when nothing else is active.
1720 .sort((a, b) => Number(a.group === 'agent') - Number(b.group === 'agent')
1721 || b.startedAtMs - a.startedAtMs || a.key.localeCompare(b.key))
1722 : [];
1723 const agents = active.filter(span => span.group === 'agent').length;
1724 const terminalFresh = fresh && this.terminalAt !== undefined
1725 && at >= this.terminalAt && at - this.terminalAt <= exports.ENGINE_OWNER_STALE_MS;
1726 let authoritativePresence = 'unknown';
1727 if (fresh) {
1728 if (this.waiting)
1729 authoritativePresence = 'needs_you';
1730 else if (terminalFresh && this.turnOutcome === 'completed' && this.turnId)
1731 authoritativePresence = 'done';
1732 else if (terminalFresh)
1733 authoritativePresence = 'idle';
1734 else if (active.length > 0 || this.turnId)
1735 authoritativePresence = 'working';
1736 }
1737 const activityKind = fresh && authoritativePresence !== 'needs_you'
1738 && authoritativePresence !== 'done' && authoritativePresence !== 'idle'
1739 ? active[0]?.activityKind
1740 ?? (this.lastFailedTool && at >= this.lastFailedTool.failedAtMs
1741 && at - this.lastFailedTool.failedAtMs <= exports.ENGINE_OWNER_STALE_MS
1742 ? this.lastFailedTool.activityKind : null)
1743 : null;
1744 const failedAge = fresh && this.lastFailedTool
1745 && at >= this.lastFailedTool.failedAtMs
1746 && at - this.lastFailedTool.failedAtMs <= exports.ENGINE_OWNER_STALE_MS
1747 ? { activityKind: this.lastFailedTool.activityKind, ageMs: at - this.lastFailedTool.failedAtMs }
1748 : null;
1749 const doneEffectId = authoritativePresence === 'done' && terminalFresh
1750 && this.turnOutcome === 'completed' && this.turnId ? this.turnId : null;
1751 return {
1752 schemaVersion: 1,
1753 sessionId: null,
1754 cursor: 0,
1755 observed,
1756 freshness,
1757 authoritativePresence,
1758 activityKind: activityKind ?? null,
1759 observedAtMs: this.lastObservedAt ?? null,
1760 parallelAgentCount: fresh ? agents : 0,
1761 activeSpans: active.slice(0, 4).map(span => ({
1762 activityKind: span.activityKind,
1763 startedAtMs: span.startedAtMs,
1764 })),
1765 turnId: this.turnId ?? null,
1766 turnOutcome: this.turnOutcome ?? null,
1767 doneEffectId,
1768 failedToolAge: failedAge,
1769 };
1770 }
1771 add(name, category, at, agentId = 'parent', continuation = false) {
1772 if (this.events.length >= 8192)
1773 throw new Error('Pet Engine observation window is full.');
1774 const event = {
1775 schemaVersion: 1,
1776 id: `engine:${this.sequence++}`,
1777 traceId: 'foreground',
1778 startTime: at,
1779 endTime: at,
1780 name,
1781 category,
1782 agentId,
1783 status: 'running',
1784 attributes: continuation ? { 'whalesong.continuation': true } : {},
1785 };
1786 this.events.push(event);
1787 return event;
1788 }
1789 start(key, kind, group, at, agentId = 'parent') {
1790 if (this.active.has(key))
1791 return;
1792 if (this.active.size >= 256)
1793 throw new Error('Too many active Engine pet spans.');
1794 const event = this.add(group === 'agent' ? 'agent' : group === 'thinking' ? 'thinking'
1795 : group === 'responding' ? 'assistant_message' : 'operation', categoryFor(kind), at, agentId);
1796 this.active.set(key, { key, activityKind: kind, startedAtMs: at, event, group });
1797 }
1798 pulse(key, at) {
1799 const span = this.active.get(key);
1800 if (!span)
1801 return;
1802 // A resumed stream does not assert tape coverage across its silent
1803 // interval; the span itself (and its start) stays active.
1804 if (at - span.event.endTime > pet_telemetry_js_1.PET_BIN_MS * 2) {
1805 const event = this.add(span.event.name, span.event.category, at, span.event.agentId, true);
1806 this.active.set(key, { ...span, event });
1807 }
1808 else {
1809 span.event.endTime = at;
1810 }
1811 }
1812 finish(key, at, status = 'success') {
1813 if (!this.active.has(key))
1814 return undefined;
1815 this.pulse(key, at);
1816 const span = this.active.get(key);
1817 if (!span)
1818 return undefined;
1819 span.event.endTime = at;
1820 span.event.status = status;
1821 this.active.delete(key);
1822 return span;
1823 }
1824 addOnce(set, id, maximum) {
1825 if (set.has(id))
1826 return false;
1827 set.add(id);
1828 while (set.size > maximum)
1829 set.delete(set.values().next().value);
1830 return true;
1831 }
1832 /** Transactional batch copy; validation errors cannot accept half a batch. */
1833 clone() {
1834 const next = new PetEngineTelemetry();
1835 const copy = (value) => JSON.parse(JSON.stringify(value));
1836 next.events = copy(this.events);
1837 const spans = new Map(next.events.map(event => [event.id, event]));
1838 next.active = new Map(Array.from(this.active, ([key, span]) => [key, {
1839 ...span,
1840 event: spans.get(span.event.id) ?? copy(span.event),
1841 }]));
1842 next.waiting = this.waiting ? spans.get(this.waiting.id) ?? copy(this.waiting) : undefined;
1843 next.sequence = this.sequence;
1844 next.lastTime = this.lastTime;
1845 next.lastObservedAt = this.lastObservedAt;
1846 next.turnId = this.turnId;
1847 next.turnOutcome = this.turnOutcome;
1848 next.terminalAt = this.terminalAt;
1849 next.lastFailedTool = this.lastFailedTool ? { ...this.lastFailedTool } : undefined;
1850 next.completedSpans = new Set(this.completedSpans);
1851 next.completedTurns = new Set(this.completedTurns);
1852 return next;
1853 }
1854 /** Mutates in place: clock, freshness and the event window change before
1855 * type-specific fields are checked. Callers that continue after a throw must
1856 * observe on a clone() and swap it in on success, as PetNative does. */
1857 observe(value, at) {
1858 if (!Number.isFinite(at) || at < this.lastTime || at > pet_sim_js_1.PET_MAX_SECONDS * 1000)
1859 throw new Error('Invalid Engine pet clock.');
1860 if (!value || typeof value !== 'object' || Array.isArray(value))
1861 throw new Error('Invalid Engine pet metadata.');
1862 const event = value;
1863 const allowed = ['event', 'index', 'channel', 'span_id', 'activity_kind', 'outcome', 'id', 'worker_status', 'turn_id', 'turn_outcome'];
1864 const stringFields = ['event', 'span_id', 'id', 'worker_status', 'turn_id', 'turn_outcome'];
1865 if (Object.keys(event).some(key => !allowed.includes(key)) || typeof event.event !== 'string'
1866 || Object.values(event).some(v => typeof v === 'string' && v.length > 256)
1867 || stringFields.some(key => event[key] !== undefined && typeof event[key] !== 'string')
1868 || event.channel !== undefined && !['text', 'reasoning'].includes(event.channel)
1869 || event.activity_kind !== undefined && !ACTIVITY_KINDS.includes(event.activity_kind)
1870 || event.outcome !== undefined && (typeof event.outcome !== 'string'
1871 || (event.event === 'approval_resolved'
1872 ? !APPROVAL_OUTCOMES.includes(event.outcome)
1873 : event.event === 'operation_activity_completed'
1874 ? !OPERATION_OUTCOMES.includes(event.outcome)
1875 : true))
1876 || event.turn_outcome !== undefined && !TURN_OUTCOMES.includes(event.turn_outcome)
1877 || event.index !== undefined && (!Number.isSafeInteger(event.index) || event.index < 0))
1878 throw new Error('Invalid Engine pet metadata fields.');
1879 this.lastTime = at;
1880 this.lastObservedAt = at;
1881 this.events = this.events.filter(item => item.endTime >= at - 12_800);
1882 const required = (key) => {
1883 const value = event[key];
1884 if (typeof value !== 'string' || !value)
1885 throw new Error(`Missing Engine ${key}.`);
1886 return value;
1887 };
1888 const index = () => {
1889 if (!Number.isSafeInteger(event.index))
1890 throw new Error('Missing Engine index.');
1891 return String(event.index);
1892 };
1893 const activityKind = () => {
1894 if (!ACTIVITY_KINDS.includes(event.activity_kind))
1895 throw new Error('Missing Engine activity kind.');
1896 return event.activity_kind;
1897 };
1898 const startMessage = (key, kind, group) => {
1899 this.start(key, kind, group, at);
1900 this.waiting = undefined;
1901 };
1902 switch (event.event) {
1903 case 'turn_started': {
1904 const id = required('turn_id');
1905 if (this.turnId !== id) {
1906 this.active.clear();
1907 this.waiting = undefined;
1908 this.lastFailedTool = undefined;
1909 this.turnId = id;
1910 this.turnOutcome = undefined;
1911 this.terminalAt = undefined;
1912 }
1913 break;
1914 }
1915 case 'message_started':
1916 startMessage(`message:${index()}`, 'responding', 'responding');
1917 break;
1918 case 'thinking_started':
1919 startMessage(`thinking:${index()}`, 'thinking', 'thinking');
1920 break;
1921 case 'response_delta': {
1922 const reasoning = event.channel === 'reasoning';
1923 const key = `${reasoning ? 'thinking' : 'message'}:${index()}`;
1924 if (!this.active.has(key))
1925 this.start(key, reasoning ? 'thinking' : 'responding', reasoning ? 'thinking' : 'responding', at);
1926 else
1927 this.pulse(key, at);
1928 this.waiting = undefined;
1929 break;
1930 }
1931 case 'message_complete':
1932 this.finish(`message:${index()}`, at);
1933 break;
1934 case 'thinking_complete':
1935 this.finish(`thinking:${index()}`, at);
1936 break;
1937 case 'operation_activity_started': {
1938 const spanId = required('span_id');
1939 const kind = activityKind();
1940 if (!this.completedSpans.has(spanId))
1941 this.start(`operation:${spanId}`, kind, 'operation', at);
1942 this.waiting = undefined;
1943 break;
1944 }
1945 case 'operation_activity_completed': {
1946 const spanId = required('span_id');
1947 const kind = activityKind();
1948 if (!OPERATION_OUTCOMES.includes(event.outcome))
1949 throw new Error('Missing Engine operation outcome.');
1950 const outcome = event.outcome;
1951 // A failure is recorded once, as its own onset event below; marking
1952 // the span `error` too would count it twice on the tape.
1953 const completed = this.finish(`operation:${spanId}`, at, outcome === 'succeeded' ? 'success' : 'unknown');
1954 if (completed && this.addOnce(this.completedSpans, spanId, 4096)) {
1955 if (outcome === 'failed') {
1956 this.add('operation_failed', 'error', at).status = 'error';
1957 this.lastFailedTool = { activityKind: kind, failedAtMs: at };
1958 }
1959 if (outcome === 'denied' || outcome === 'cancelled')
1960 this.waiting = undefined;
1961 }
1962 break;
1963 }
1964 case 'tool_call_heartbeat':
1965 for (const [key, span] of this.active)
1966 if (span.group === 'operation')
1967 this.pulse(key, at);
1968 break;
1969 case 'approval_resolved': {
1970 required('id');
1971 if (!APPROVAL_OUTCOMES.includes(event.outcome))
1972 throw new Error('Invalid Engine approval outcome.');
1973 this.waiting = undefined;
1974 break;
1975 }
1976 case 'agent_spawned': {
1977 const id = required('id');
1978 this.start(`agent:${id}`, 'delegating', 'agent', at, id);
1979 break;
1980 }
1981 case 'agent_progress': {
1982 const id = required('id');
1983 const key = `agent:${id}`;
1984 if (['completed', 'failed', 'cancelled', 'interrupted', 'budget_exhausted'].includes(event.worker_status)) {
1985 this.finish(key, at);
1986 break;
1987 }
1988 if (!this.active.has(key))
1989 this.start(key, 'delegating', 'agent', at, id);
1990 else
1991 this.pulse(key, at);
1992 break;
1993 }
1994 case 'agent_complete':
1995 this.finish(`agent:${required('id')}`, at);
1996 break;
1997 case 'approval_required':
1998 case 'user_input_required': {
1999 required('id');
2000 if (!this.waiting) {
2001 this.waiting = this.add('human_request', 'human', at);
2002 this.waiting.status = 'pending';
2003 }
2004 break;
2005 }
2006 case 'turn_complete': {
2007 const outcome = event.turn_outcome;
2008 if (!TURN_OUTCOMES.includes(outcome))
2009 throw new Error('Missing Engine turn outcome.');
2010 const id = typeof event.turn_id === 'string' && event.turn_id.length ? event.turn_id : undefined;
2011 this.active.clear();
2012 this.waiting = undefined;
2013 this.turnId = id;
2014 // An outcome belongs to a turn. `/purge`, an edit rejection or a
2015 // session switch mid-turn completes with no turn id; recording the
2016 // outcome alone would break the projection invariant the Rust
2017 // contract checks (`turn_outcome` requires `turn_id`).
2018 this.turnOutcome = id ? outcome : undefined;
2019 this.terminalAt = at;
2020 if (outcome === 'completed' && id && this.addOnce(this.completedTurns, id, 256))
2021 this.add('turn_completed', 'communication', at).status = 'success';
2022 break;
2023 }
2024 default: throw new Error('Unsupported Engine pet event.');
2025 }
2026 }
2027 /** The typed shell may extend a request already witnessed in Engine events,
2028 * but a mid-turn attach cannot invent a NeedsYou state. */
2029 confirmWaiting(at, waiting) {
2030 if (!waiting) {
2031 this.waiting = undefined;
2032 return;
2033 }
2034 if (this.waiting && at >= this.waiting.endTime) {
2035 this.waiting.endTime = at;
2036 this.lastObservedAt = at;
2037 this.lastTime = Math.max(this.lastTime, at);
2038 }
2039 }
2040 bucket(sequence) {
2041 const end = (sequence + 1) * pet_telemetry_js_1.PET_BIN_MS;
2042 const input = this.events.filter(event => event.startTime < end && event.endTime >= end - 12_400)
2043 .map(event => ({ ...event, endTime: Math.min(event.endTime, end) }));
2044 return (0, pet_telemetry_js_1.compilePetTelemetry)(input, end, sequence)[0];
2045 }
2046 }
2047 exports.PetEngineTelemetry = PetEngineTelemetry;
2048
2049 };
2050 function load(id){id=id.replace(/^\.\//,'').replace(/\.js$/,'');if(cache[id])return cache[id];if(!factories[id])throw Error('Missing core module');const e=cache[id]={};factories[id](e,load);return e;}
2051 global.PetNative=load('pet-native').PetNative;
2052 })(globalThis);
2053
2053 lines JAVASCRIPT