返回 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 observeEngine(metadataJSON, timeMs) { this.engine.observe(JSON.parse(metadataJSON), timeMs); }
94 observeEngineBatch(metadataJSON, timeMs) {
95 const events = JSON.parse(metadataJSON);
96 if (!Array.isArray(events) || events.length > 64)
97 throw new Error('Invalid Engine batch.');
98 const next = this.engine.clone();
99 for (const event of events)
100 next.observe(event, timeMs);
101 this.engine = next;
102 }
103 advanceEngine(timeMs, motion, waiting) {
104 const target = Math.floor(timeMs * 30 / 1000 + 1e-8);
105 if (!Number.isFinite(timeMs) || target < this.engineTick || target - this.engineTick > 300)
106 throw new Error('Engine pet clock jump.');
107 this.engine.confirmWaiting(timeMs, waiting);
108 const voices = [];
109 while (this.engineTick < target) {
110 if ((this.engineTick + 1) % 12 === 0)
111 this.world.acceptTelemetry(this.engine.bucket(Math.floor(this.engineTick / 12)));
112 this.world.step(1 / 30, { motion, sensitivity: 1 });
113 voices.push(...this.world.voices);
114 this.engineTick++;
115 }
116 this.world.voices = voices;
117 }
118 terminal(width, height) {
119 if (![width, height].every(n => Number.isSafeInteger(n) && n >= 1 && n <= 512))
120 throw new Error('Invalid pet raster size.');
121 const { sim, frame } = this.world, l = (0, pet_sim_js_1.layout)(width * 2, height * 4, frame.state);
122 const cells = Array(width * height).fill(0), bits = [[1, 8], [2, 16], [4, 32], [64, 128]];
123 sim.p.forEach((p, i) => {
124 if (frame.state.observed < .92 && i % 2 === 1)
125 return;
126 const x = Math.round(l.ox + p.x * l.scale * l.flipX), y = Math.round(l.oy + p.y * l.scale);
127 if (x >= 0 && x < width * 2 && y >= 0 && y < height * 4)
128 cells[Math.floor(y / 4) * width + Math.floor(x / 2)] |= bits[y % 4][x % 2];
129 });
130 return JSON.stringify({ width, height, cells, timeMs: frame.timeMs, channel: frame.state.channel, arch: pet_sim_js_1.ARCH_OF[frame.state.channel],
131 hollow: frame.state.observed < .92, dozing: frame.behaviour === 'doze', lit: frame.state.lit });
132 }
133 pcmChannels(voicesJSON, startSample, length, rate) {
134 const p = (0, pet_audio_js_1.renderPetPCM)(JSON.parse(voicesJSON), startSample, length, rate);
135 return [p.left, p.right];
136 }
137 pcm(voicesJSON, startSample, length, rate) {
138 return JSON.stringify(this.pcmChannels(voicesJSON, startSample, length, rate).map(channel => Array.from(channel)));
139 }
140 }
141 exports.PetNative = PetNative;
142
143 };
144 factories["pet-world"]=function(exports,require){
145 "use strict";
146 Object.defineProperty(exports, "__esModule", { value: true });
147 exports.PetWorld = exports.PET_MAX_SECONDS = void 0;
148 const model_js_1 = require("./model.js");
149 const pet_sim_js_1 = require("./pet-sim.js");
150 const pet_telemetry_js_1 = require("./pet-telemetry.js");
151 const pet_audio_js_1 = require("./pet-audio.js");
152 const HZ = 30;
153 var pet_sim_js_2 = require("./pet-sim.js");
154 Object.defineProperty(exports, "PET_MAX_SECONDS", { enumerable: true, get: function () { return pet_sim_js_2.PET_MAX_SECONDS; } });
155 const seedFor = (name) => (0xC0FFEE ^ (0, model_js_1.stableHash)(name)) >>> 0;
156 /** Fixed-tick creature controller. Wall clocks and pointer APIs belong to drivers.
157 * Reconstructing with the same tape and interactions is also the seek operation.
158 * Particle, behaviour and identity randomness never consume one another's stream. */
159 class PetWorld {
160 sim;
161 tapeLog;
162 tapeHashes = [];
163 get tape() { return this.tapeLog; }
164 interactionLog;
165 get interactions() { return this.interactionLog.map(e => ({ ...e })); }
166 frame;
167 voices = [];
168 score = new pet_audio_js_1.PetScore();
169 accumulator = 0;
170 tick = 0;
171 bucketIndex = -1;
172 interactionIndex = 0;
173 branchTick = -1;
174 random = (0, pet_sim_js_1.mulberry32)(seedFor('behaviour'));
175 behaviour = 'swim';
176 until = 6;
177 targetX = .35;
178 targetY = -.08;
179 x = 0;
180 y = 0;
181 flip = 1;
182 lit = 1;
183 lastActivity = 0;
184 addressedAt = -Infinity;
185 waitSince = -1;
186 food = null;
187 members = new Map();
188 lastStill = '';
189 origin;
190 segmented = false;
191 hasTelemetry = false;
192 get startTimeMs() { return this.origin?.frame.timeMs ?? 0; }
193 get endTimeMs() { return Math.max(this.frame.timeMs, (this.tapeLog.at(-1)?.simTimeMs ?? 0) + 400); }
194 get needsSegment() {
195 return !this.segmented && this.tapeLog.length < 1024 && this.interactionLog.length < 4096
196 || this.bucketIndex >= 1024 || this.interactionIndex >= 4096;
197 }
198 constructor(points, tape = [], interactions = [], expressionVersion = 2, segmented = false) {
199 if (tape.length > 216_000 || interactions.length > 100_000)
200 throw new Error('Pet recording exceeds its input limit.');
201 this.sim = new pet_sim_js_1.PetSim(points, 0xC0FFEE, expressionVersion);
202 this.segmented = segmented;
203 this.hasTelemetry = tape.length > 0;
204 this.tapeLog = structuredClone([...tape]);
205 this.interactionLog = structuredClone([...interactions]);
206 for (let i = 0; i < this.tape.length; i++) {
207 const b = this.tape[i];
208 (0, pet_telemetry_js_1.validatePetBucket)(b);
209 if (segmented ? i > 0 && b.sequence <= this.tape[i - 1].sequence : b.sequence !== i)
210 throw new Error('World requires contiguous version 1 pet buckets.');
211 }
212 for (let i = 0; i < this.interactionLog.length; i++) {
213 const e = this.interactionLog[i];
214 if (!Number.isFinite(e.timeMs) || e.timeMs < 0 || i > 0 && e.timeMs < this.interactionLog[i - 1].timeMs
215 || !['attention', 'food'].includes(e.kind) || !Number.isFinite(e.x) || !Number.isFinite(e.y)
216 || Math.abs(e.x) > 1 || Math.abs(e.y) > 1)
217 throw new Error('Invalid pet interaction.');
218 }
219 this.hashTape(0);
220 this.frame = this.makeFrame(0);
221 this.voices = this.score.voices(this.frame);
222 if (segmented)
223 this.origin = this.checkpoint();
224 }
225 checkpoint() {
226 return structuredClone({ petCheckpointVersion: this.segmented ? 2 : 1,
227 ...(this.segmented ? { historyStart: (this.origin?.tick ?? this.tick), hasTelemetry: this.hasTelemetry } : {}), history: this.historyDigest(),
228 sim: this.sim.checkpoint(), score: this.score.checkpoint(), accumulator: this.accumulator,
229 tick: this.tick, bucketIndex: this.bucketIndex, interactionIndex: this.interactionIndex, branchTick: this.branchTick,
230 random: this.random.state(), behaviour: this.behaviour, until: this.until,
231 targetX: this.targetX, targetY: this.targetY, x: this.x, y: this.y, flip: this.flip, lit: this.lit,
232 lastActivity: this.lastActivity, addressedAt: Number.isFinite(this.addressedAt) ? this.addressedAt : null,
233 waitSince: this.waitSince, food: this.food, members: [...this.members.values()], lastStill: this.lastStill,
234 frame: this.frame, voices: this.voices });
235 }
236 recording(withCheckpoint = true, completed = false) {
237 const tapeEnd = completed ? this.bucketIndex + 1 : this.tapeLog.length;
238 const inputEnd = completed ? this.interactionIndex : this.interactionLog.length;
239 const history = this.historyDigest(tapeEnd, inputEnd);
240 return { petReplayVersion: this.segmented ? 2 : 1, expressionVersion: this.sim.expressionVersion,
241 tape: this.tapeLog.slice(0, tapeEnd), interactions: this.interactionLog.slice(0, inputEnd).map(e => ({ ...e })),
242 ...(this.origin ? { start: { ...structuredClone(this.origin), hasTelemetry: this.hasTelemetry, history } } : {}),
243 ...(withCheckpoint ? { checkpoint: { ...this.checkpoint(), history } } : {}) };
244 }
245 static fromRecording(points, value) {
246 const r = value;
247 if (!r || ![1, 2].includes(r.petReplayVersion) || !Array.isArray(r.tape) || !Array.isArray(r.interactions))
248 throw new Error('Invalid pet recording.');
249 const version = r.expressionVersion === undefined ? 1 : r.expressionVersion;
250 if (![1, 2].includes(version))
251 throw new Error('Unsupported pet expression version.');
252 if (r.checkpoint !== undefined && !r.checkpoint || r.start !== undefined && !r.start)
253 throw new Error('Invalid pet checkpoint.');
254 for (const c of [r.start, r.checkpoint])
255 if (c && (c.sim?.expressionVersion ?? 1) !== version)
256 throw new Error('Pet expression version does not match its checkpoint.');
257 if (r.petReplayVersion === 1 && (r.start || r.checkpoint && r.checkpoint.petCheckpointVersion !== 1))
258 throw new Error('Invalid legacy pet recording.');
259 if (r.petReplayVersion === 2 && (!r.start || r.start.petCheckpointVersion !== 2 || r.start.historyStart !== r.start.tick))
260 throw new Error('The recording segment is missing its starting checkpoint.');
261 const first = r.start && PetWorld.restore(points, r.tape, r.interactions, r.start);
262 const world = r.checkpoint ? PetWorld.restore(points, r.tape, r.interactions, r.checkpoint) : first ?? new PetWorld(points, r.tape, r.interactions, version);
263 if (first) {
264 if (!world.segmented || world.tick < first.tick || r.checkpoint && r.checkpoint.historyStart !== first.tick)
265 throw new Error('Pet segment checkpoints do not agree.');
266 world.origin = first.checkpoint();
267 }
268 return world;
269 }
270 /** Retire only consumed input. The exact origin makes each archived segment
271 * independently replayable; no particle, random stream or score is reset. */
272 prepareSegment() {
273 const dropTape = Math.max(0, this.bucketIndex), dropInputs = this.interactionIndex, previous = this.origin;
274 const tape = this.tapeLog.slice(dropTape), interactions = this.interactionLog.slice(dropInputs);
275 const points = this.sim.p.map(p => [p.hx, p.hy]);
276 const c = this.checkpoint();
277 c.petCheckpointVersion = 2;
278 c.historyStart = this.tick;
279 c.hasTelemetry = this.hasTelemetry;
280 c.bucketIndex -= dropTape;
281 c.interactionIndex = 0;
282 c.history = new PetWorld(points, tape, interactions, this.sim.expressionVersion, true).historyDigest();
283 const next = PetWorld.restore(points, tape, interactions, c);
284 next.origin = next.checkpoint();
285 let committed = false;
286 return { recording: next.recording(), archive: this.recording(true, true), commit: () => {
287 if (committed || this.origin !== previous || this.tick < c.tick)
288 throw new Error('The pet recording segment has changed.');
289 this.tapeLog.splice(0, dropTape);
290 this.interactionLog.splice(0, dropInputs);
291 this.bucketIndex -= dropTape;
292 this.interactionIndex -= dropInputs;
293 this.tapeHashes = [];
294 this.hashTape(0);
295 this.segmented = true;
296 this.origin = next.origin;
297 committed = true;
298 } };
299 }
300 /** Lossless version 1 export, including the current pose and score. Drivers
301 * consume all chunks synchronously on the world's owner before another tick.
302 * Only a small slice is serialized inside an embedded runtime at a time. */
303 recordingChunk(index, completed = false) {
304 if (!Number.isSafeInteger(index) || index < 0)
305 throw new Error('Invalid pet export cursor.');
306 const size = 16, tapeEnd = completed ? this.bucketIndex + 1 : this.tapeLog.length;
307 const inputEnd = completed ? this.interactionIndex : this.interactionLog.length;
308 const tapes = Math.ceil(tapeEnd / size), inputs = Math.ceil(inputEnd / size);
309 if (index === 0)
310 return `{"petReplayVersion":${this.segmented ? 2 : 1},"expressionVersion":${this.sim.expressionVersion},"tape":[`;
311 if (index <= tapes)
312 return (index === 1 ? '' : ',') + JSON.stringify(this.tapeLog.slice((index - 1) * size, Math.min(tapeEnd, index * size))).slice(1, -1);
313 if (index === tapes + 1)
314 return '],"interactions":[';
315 const part = index - tapes - 2;
316 if (part < inputs)
317 return (part === 0 ? '' : ',') + JSON.stringify(this.interactionLog.slice(part * size, Math.min(inputEnd, (part + 1) * size))).slice(1, -1);
318 if (part === inputs) {
319 const history = this.historyDigest(tapeEnd, inputEnd);
320 return `]${this.origin ? ',"start":' + JSON.stringify({ ...this.origin, hasTelemetry: this.hasTelemetry, history }) : ''},"checkpoint":${JSON.stringify({ ...this.checkpoint(), history })}}`;
321 }
322 return null;
323 }
324 /** Prefix states preserve the original FNV checksum byte for byte. Live
325 * appends and replacements hash only the changed suffix, so checkpointing
326 * does not rescan hours of accepted telemetry on the world worker. */
327 hashTape(from) {
328 for (let i = from; i < this.tapeLog.length; i++)
329 this.tapeHashes[i] = (0, model_js_1.stableHash)((i ? ',' : '') + JSON.stringify(this.tapeLog[i]), this.tapeHashes[i - 1] ?? (0, model_js_1.stableHash)('[['));
330 }
331 historyDigest(tapeEnd = this.tapeLog.length, inputEnd = this.interactionLog.length) {
332 let hash = (0, model_js_1.stableHash)('],[', this.tapeHashes[tapeEnd - 1] ?? (0, model_js_1.stableHash)('[['));
333 for (let i = 0; i < inputEnd; i++)
334 hash = (0, model_js_1.stableHash)((i ? ',' : '') + JSON.stringify(this.interactionLog[i]), hash);
335 return (0, model_js_1.stableHash)(']]', hash);
336 }
337 /** Hydrate a new world without stepping history. Validation finishes before
338 * the caller receives it, so a corrupt checkpoint never mutates a live pet. */
339 static restore(points, tape, interactions, value) {
340 const c = value;
341 const range = (n, low, high) => Number.isFinite(n) && n >= low && n <= high;
342 const integer = (n, low, high) => Number.isSafeInteger(n) && range(n, low, high);
343 if (!c || ![1, 2].includes(c.petCheckpointVersion) || JSON.stringify(c).length > 512 * 1024
344 || c.petCheckpointVersion === 2 && (!integer(c.historyStart, 0, c.tick) || typeof c.hasTelemetry !== 'boolean')
345 || !integer(c.tick, 0, pet_sim_js_1.PET_MAX_SECONDS * HZ) || !range(c.accumulator, -1e-8, 1 / HZ + 1e-8)
346 || !integer(c.bucketIndex, -1, tape.length - 1) || !integer(c.interactionIndex, 0, interactions.length)
347 || !integer(c.branchTick, -1, c.tick) || !integer(c.random, 0, 0xffffffff)
348 || !['swim', 'dive', 'roll', 'breathe', 'drift', 'doze', 'wake'].includes(c.behaviour)
349 || !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))
350 || !range(c.lit, 0, 1) || !range(c.lastActivity, 0, c.tick / HZ)
351 || c.addressedAt !== null && !range(c.addressedAt, 0, c.tick / HZ)
352 || !range(c.waitSince, -1, c.tick / HZ) || typeof c.lastStill !== 'string' || c.lastStill.length > 2048
353 || !Array.isArray(c.members) || c.members.length > 6
354 || c.members.some(m => !m || typeof m.id !== 'string' || !m.id || m.id.length > 4096
355 || !integer(m.slot, 0, 5) || !range(m.phase, 0, Math.PI * 2) || typeof m.present !== 'boolean')
356 || new Set(c.members.map(m => m.id)).size !== c.members.length || new Set(c.members.map(m => m.slot)).size !== c.members.length
357 || 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)))
358 || !c.frame || c.frame.timeMs !== c.tick * 1000 / HZ || c.frame.behaviour !== c.behaviour
359 || !['none', 'orient', 'approach', 'call'].includes(c.frame.needs)
360 || !range(c.frame.surface, -.9, -.8) || !range(c.frame.caustic, 0, 1)
361 || 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))
362 || JSON.stringify(c.frame.pod) !== JSON.stringify(c.members)
363 || !Array.isArray(c.voices) || c.voices.length > 1024
364 || c.voices.some(v => !v || typeof v.id !== 'string' || v.id.length > 256))
365 throw new Error('Invalid pet world checkpoint.');
366 (0, pet_sim_js_1.validatePetState)(c.frame.state);
367 (0, pet_audio_js_1.renderPetPCM)(c.voices, 0, 0);
368 const world = new PetWorld(points, tape, interactions, c.sim?.expressionVersion ?? 1, c.petCheckpointVersion === 2);
369 if (c.petCheckpointVersion === 2) {
370 world.hasTelemetry = c.hasTelemetry;
371 world.origin = structuredClone(c);
372 }
373 if (c.history !== world.historyDigest()
374 || c.bucketIndex >= 0 && world.tape[c.bucketIndex].simTimeMs > c.frame.timeMs + 1e-7
375 // acceptTelemetry may fill past gaps after the most recent fixed tick.
376 // Preserve that pending cursor exactly; it may lag only over empty gaps.
377 || world.tape.some((b, i) => i > c.bucketIndex && b.simTimeMs <= c.frame.timeMs + 1e-7
378 && (b.observed !== 0 || b.channel !== 'other' || b.errors || b.waiting || b.agentIds.length
379 || b.onsets.some(Boolean) || b.activeMs.some(Boolean)))
380 || world.interactionLog.slice(0, c.interactionIndex).some(e => e.timeMs > c.frame.timeMs + 1e-7)
381 || world.interactionLog[c.interactionIndex]?.timeMs <= c.frame.timeMs + 1e-7)
382 throw new Error('Pet checkpoint does not match its recording.');
383 const candidate = world.tape[c.bucketIndex];
384 const telemetry = candidate && c.frame.timeMs < candidate.simTimeMs + candidate.durationMs ? candidate : undefined;
385 if (JSON.stringify(c.frame.telemetry) !== JSON.stringify(telemetry))
386 throw new Error('Pet checkpoint telemetry does not match its clock.');
387 world.sim.restore(c.sim);
388 world.score.restore(c.score);
389 world.random.restore(c.random);
390 world.accumulator = c.accumulator;
391 world.tick = c.tick;
392 world.bucketIndex = c.bucketIndex;
393 world.interactionIndex = c.interactionIndex;
394 world.branchTick = c.branchTick;
395 world.behaviour = c.behaviour;
396 world.until = c.until;
397 world.targetX = c.targetX;
398 world.targetY = c.targetY;
399 world.x = c.x;
400 world.y = c.y;
401 world.flip = c.flip;
402 world.lit = c.lit;
403 world.lastActivity = c.lastActivity;
404 world.addressedAt = c.addressedAt ?? -Infinity;
405 world.waitSince = c.waitSince;
406 world.food = structuredClone(c.food);
407 world.members = new Map(c.members.map(m => [m.id, { ...m }]));
408 world.lastStill = c.lastStill;
409 // JSON omits undefined properties; keep the same frame shape as makeFrame.
410 world.frame = { ...structuredClone(c.frame), telemetry: telemetry ? structuredClone(telemetry) : undefined };
411 world.voices = structuredClone(c.voices);
412 return world;
413 }
414 /** Resume observation beyond all already accepted live packets, without
415 * replaying their sound or exposing a stale request as current work. */
416 resumeObservation() {
417 const last = this.tapeLog.at(-1), end = last ? (last.sequence + 1) * 12 : 0;
418 if (!end || end - this.tick > 24)
419 throw new Error('Only a live recording can resume observation.');
420 while (this.tick < end)
421 this.step(1 / 30, { motion: false, sensitivity: 1 });
422 this.voices = [];
423 }
424 /** Branch at the current playhead; input is journalled for the next fixed tick.
425 * A live touch never needs to re-simulate the creature's entire lifetime. */
426 interact(kind, x, y) {
427 if (!['attention', 'food'].includes(kind) || !Number.isFinite(x) || !Number.isFinite(y) || Math.abs(x) > 1 || Math.abs(y) > 1)
428 throw new Error('Invalid pet interaction.');
429 if (this.branchTick !== this.tick)
430 this.interactionLog.splice(this.interactionIndex);
431 this.branchTick = this.tick;
432 this.interactionLog.push({ timeMs: (this.tick + 1) * 1000 / HZ, kind, x, y });
433 }
434 /** Accept a live source packet at the next 400ms boundary. The accepted tape,
435 * including any missing intervals, is the exact replay authority for this host. */
436 acceptTelemetry(input) {
437 (0, pet_telemetry_js_1.validatePetBucket)(input);
438 const sequence = Math.floor(this.tick / 12) + 1;
439 if (this.tapeLog.length >= 216_000)
440 throw new Error('Archive this pet recording before accepting more telemetry.');
441 this.hasTelemetry = true;
442 if (this.segmented) {
443 const at = this.tapeLog.findIndex(b => b.sequence >= sequence);
444 const index = at < 0 ? this.tapeLog.length : at;
445 this.tapeLog.splice(index, at >= 0 && this.tapeLog[at].sequence === sequence ? 1 : 0, { ...structuredClone(input), sequence, simTimeMs: sequence * 400 });
446 this.hashTape(index);
447 return;
448 }
449 if (sequence >= 216_000)
450 throw new Error('Archive the legacy recording before accepting more telemetry.');
451 const changedFrom = Math.min(sequence, this.tapeLog.length);
452 while (this.tapeLog.length <= sequence) {
453 const at = this.tapeLog.length;
454 this.tapeLog.push({ version: 1, sequence: at, simTimeMs: at * 400, durationMs: 400,
455 activity: .12, coherence: .25, attention: 0, channel: 'other', observed: 0, roamX: 0, roamY: 0, flip: 1, lit: 1,
456 onsets: Array(13).fill(0), activeMs: Array(13).fill(0), errors: 0, agentIds: [], waiting: false });
457 }
458 this.tapeLog[sequence] = { ...structuredClone(input), sequence, simTimeMs: sequence * 400 };
459 this.hashTape(changedFrom);
460 }
461 /** dt is bounded so suspending a surface cannot cause an unbounded catch-up. */
462 step(dt, opts = { motion: true, sensitivity: 1 }) {
463 if (!Number.isFinite(dt) || dt < 0 || dt > 10)
464 throw new Error('World dt must be in [0, 10] seconds.');
465 this.accumulator += dt;
466 this.voices = [];
467 while (this.accumulator + 1e-10 >= 1 / HZ) {
468 this.accumulator -= 1 / HZ;
469 const time = ++this.tick / HZ;
470 this.frame = this.makeFrame(time);
471 this.voices.push(...this.score.voices(this.frame));
472 if (!opts.motion) {
473 this.frame.state = { ...this.frame.state, roamX: 0, roamY: 0, flip: 1, lit: this.behaviour === 'doze' ? .18 : 1 };
474 this.frame.surface = -.86;
475 this.frame.caustic = .5;
476 if (this.frame.food)
477 this.frame.food.y = this.food.y;
478 }
479 const signature = JSON.stringify(this.frame.state);
480 const peers = this.frame.pod.filter(p => p.present);
481 const podSlots = peers.length >= 3 ? peers.map(p => [[0, 2, 4, 1, 3, 5][p.slot], p.phase]) : undefined;
482 if (opts.motion || signature !== this.lastStill || podSlots)
483 this.sim.step(1 / HZ, this.frame.state, { ...opts, podSlots });
484 this.lastStill = signature;
485 }
486 return this.frame;
487 }
488 makeFrame(time) {
489 const timeMs = this.tick * 1000 / HZ;
490 while (this.bucketIndex + 1 < this.tape.length && this.tape[this.bucketIndex + 1].simTimeMs <= timeMs + 1e-7)
491 this.bucketIndex++;
492 const candidate = this.tape[this.bucketIndex];
493 const telemetry = candidate && timeMs < candidate.simTimeMs + candidate.durationMs ? candidate : undefined;
494 while (this.interactionIndex < this.interactionLog.length && this.interactionLog[this.interactionIndex].timeMs <= timeMs + 1e-7) {
495 const e = this.interactionLog[this.interactionIndex++];
496 this.addressedAt = time;
497 this.lastActivity = time;
498 this.targetX = e.x * .65;
499 this.targetY = e.y * .65;
500 if (e.kind === 'food')
501 this.food = { time, x: e.x, y: e.y };
502 }
503 const busy = telemetry && telemetry.observed >= .92 && telemetry.activity > .2;
504 if (busy)
505 this.lastActivity = time;
506 if (telemetry?.waiting) {
507 if (this.waitSince < 0)
508 this.waitSince = time;
509 }
510 else
511 this.waitSince = -1;
512 const waitingFor = this.waitSince < 0 ? 0 : time - this.waitSince;
513 const needs = this.waitSince < 0 ? 'none' : waitingFor < 8 ? 'orient' : waitingFor < 25 ? 'approach' : 'call';
514 const addressed = time - this.addressedAt < 2;
515 if (this.behaviour === 'doze' && (busy || addressed)) {
516 this.behaviour = 'wake';
517 this.until = time + 2;
518 }
519 else if (!busy && !addressed && time - this.lastActivity >= 75)
520 this.behaviour = 'doze';
521 else if (time >= this.until) {
522 const choices = ['swim', 'swim', 'dive', 'roll', 'breathe', 'drift'];
523 this.behaviour = choices[Math.floor(this.random() * choices.length)];
524 this.until = time + 3 + this.random() * 7;
525 this.targetX = (this.random() * 2 - 1) * .75;
526 this.targetY = this.behaviour === 'dive' ? .6 : this.behaviour === 'breathe' ? -.65 : (this.random() * 2 - 1) * .4;
527 }
528 const sleeping = this.behaviour === 'doze';
529 if (sleeping) {
530 this.targetY = .65;
531 this.targetX = .15;
532 }
533 if (this.sim.expressionVersion === 1 && (needs === 'approach' || needs === 'call')) {
534 this.targetX = 0;
535 this.targetY = .3;
536 }
537 const move = sleeping ? .004 : this.behaviour === 'drift' ? .006 : .018;
538 const dx = this.targetX - this.x;
539 this.x += dx * move;
540 this.y += (this.targetY - this.y) * move;
541 this.flip += ((Math.abs(dx) < .015 ? this.flip < 0 ? -1 : 1 : dx < 0 ? -1 : 1) - this.flip) * .035;
542 this.lit += ((sleeping ? .18 : 1) - this.lit) * .03;
543 const wild = !this.hasTelemetry;
544 const state = {
545 activity: telemetry?.activity ?? (wild ? sleeping ? .05 : .18 : .12),
546 coherence: telemetry?.coherence ?? (wild ? .94 : .25),
547 attention: Math.max(telemetry?.attention ?? 0, addressed ? .85 : 0, this.sim.expressionVersion === 1 && needs === 'call' ? 1 : 0),
548 // A touch changes orientation, never hides an instrumentation gap.
549 channel: addressed ? 'human' : telemetry?.channel ?? 'other',
550 observed: telemetry?.observed ?? (wild ? 1 : 0),
551 roamX: this.x, roamY: this.y, flip: this.flip, lit: this.lit,
552 };
553 const activeIds = new Set(telemetry?.agentIds ?? []);
554 for (const member of this.members.values())
555 member.present = activeIds.has(member.id);
556 for (const id of activeIds) {
557 if (!this.members.has(id)) {
558 const vacant = [...this.members.values()].find(m => !m.present);
559 const slot = this.members.size < 6 ? this.members.size : vacant?.slot;
560 if (slot !== undefined) {
561 if (this.members.size >= 6 && vacant)
562 this.members.delete(vacant.id);
563 this.members.set(id, { id, slot, phase: (0, pet_sim_js_1.mulberry32)(seedFor(`pod:${id}`))() * Math.PI * 2, present: true });
564 }
565 }
566 const member = this.members.get(id);
567 if (member)
568 member.present = true;
569 }
570 return { timeMs, behaviour: this.behaviour, state, telemetry, needs,
571 pod: [...this.members.values()].map(m => ({ ...m })),
572 surface: -.86 + .012 * Math.sin(time * .8), caustic: .5 + .5 * Math.sin(time * .31),
573 food: this.food && time - this.food.time < 5
574 ? { 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 };
575 }
576 }
577 exports.PetWorld = PetWorld;
578
579 };
580 factories["model"]=function(exports,require){
581 "use strict";
582 Object.defineProperty(exports, "__esModule", { value: true });
583 exports.durationOf = exports.EMPTY_FILTERS = exports.CATEGORIES = void 0;
584 exports.eventMatches = eventMatches;
585 exports.errorOnsetOf = errorOnsetOf;
586 exports.stableHash = stableHash;
587 exports.quantile = quantile;
588 exports.clamp = clamp;
589 exports.formatTime = formatTime;
590 /** Versioned, vendor-neutral trace and signal contracts. All internal time is ms. */
591 exports.CATEGORIES = [
592 'reasoning', 'tool', 'memory', 'code', 'filesystem', 'network', 'browser',
593 'communication', 'agent', 'orchestration', 'error', 'human', 'other',
594 ];
595 exports.EMPTY_FILTERS = { query: '', category: '', agent: '', model: '', tool: '', status: '' };
596 function eventMatches(e, f) {
597 if (f.category && e.category !== f.category)
598 return false;
599 if (f.agent && e.agentId !== f.agent)
600 return false;
601 if (f.model && e.model !== f.model)
602 return false;
603 if (f.tool && e.tool !== f.tool)
604 return false;
605 if (f.status && e.status !== f.status)
606 return false;
607 if (f.query) {
608 const q = f.query.toLowerCase();
609 // Search is deliberately content-aware but runs only over locally retained fields.
610 if (![e.name, e.id, e.agentId, e.model, e.tool, e.provider, e.category,
611 JSON.stringify(e.attributes), JSON.stringify(e.payload), JSON.stringify(e.raw)].filter(Boolean).join(' ').toLowerCase().includes(q))
612 return false;
613 }
614 return true;
615 }
616 const durationOf = (e) => Math.max(0, e.endTime - e.startTime);
617 exports.durationOf = durationOf;
618 /** An explicit failure receipt can arrive after a span began or ended. Keep
619 * its timestamp distinct from the operation onset in every signal view. */
620 function errorOnsetOf(e) {
621 const time = e.attributes['whalesong.error_onset_ms'];
622 if (time === undefined)
623 return e.startTime;
624 if (typeof time !== 'number' || !Number.isFinite(time) || time < e.startTime)
625 throw new Error('Invalid failure observation time.');
626 return time;
627 }
628 function stableHash(text, seed = 2166136261) {
629 let h = seed;
630 for (let i = 0; i < text.length; i++) {
631 h ^= text.charCodeAt(i);
632 h = Math.imul(h, 16777619);
633 }
634 return h >>> 0;
635 }
636 function quantile(a, q) {
637 if (!a.length)
638 return 0;
639 const s = [...a].sort((a, b) => a - b), x = Math.min(1, Math.max(0, q)) * (s.length - 1);
640 return s[Math.floor(x)] + (s[Math.ceil(x)] - s[Math.floor(x)]) * (x % 1);
641 }
642 function clamp(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); }
643 function formatTime(ms, precise = false) {
644 const m = Math.floor(Math.max(0, ms) / 60000), s = Math.floor(Math.max(0, ms) / 1000) % 60;
645 return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}${precise ? '.' + String(Math.floor(ms % 1000)).padStart(3, '0') : ''}`;
646 }
647
648 };
649 factories["pet-sim"]=function(exports,require){
650 "use strict";
651 // PetSim — the portable core of the Codewhale pet.
652 //
653 // This is a faithful, DOM-free port of the consort study's particle engine
654 // (grammar.js). The same code — same constants, same order of operations — is
655 // what the Rust, Swift and Kotlin cores implement. Four rules keep the view
656 // identical on every surface:
657 //
658 // 1. The body is the same 980 points, loaded from whale-points.tsv — never
659 // re-sampled from the image on each platform.
660 // 2. Per-particle jitter phases come from mulberry32(0xC0FFEE), not from the
661 // platform's RNG.
662 // 3. All motion is a pure function of (sim clock, state): nothing accumulates
663 // noise. Two runs fed the same tape produce the same frame.
664 // 4. Colour, hollowness and brightness are computed here, once — renderers
665 // only place and stamp dots.
666 //
667 // Positions stay normalized in body space (roughly [-0.5, 0.5]²). A renderer
668 // maps them through layout() to its own medium.
669 Object.defineProperty(exports, "__esModule", { value: true });
670 exports.PetSim = exports.CHANNEL_INDEX = exports.CHANNELS = exports.ARCH_OF = exports.REST_STATE = exports.PET_MAX_SECONDS = void 0;
671 exports.validatePetState = validatePetState;
672 exports.mulberry32 = mulberry32;
673 exports.layout = layout;
674 exports.digest = digest;
675 exports.runTape = runTape;
676 exports.PET_MAX_SECONDS = 100 * 365 * 86_400;
677 exports.REST_STATE = {
678 activity: 0.35, coherence: 0.8, attention: 0, channel: 'reasoning',
679 observed: 1, roamX: 0, roamY: 0, flip: 1, lit: 1,
680 };
681 const lerp = (a, b, t) => a + (b - a) * t;
682 const clamp = (v, a = 0, b = 1) => Math.min(b, Math.max(a, v));
683 exports.ARCH_OF = {
684 reasoning: 'gyre', memory: 'gyre',
685 tool: 'strike', code: 'strike', filesystem: 'strike',
686 network: 'cross', communication: 'cross', browser: 'cross',
687 agent: 'pod', orchestration: 'pod',
688 error: 'tear', human: 'address', other: 'drift',
689 };
690 exports.CHANNELS = [
691 { key: 'reasoning', label: 'Model / reasoning', color: '#73c9b5', freq: 130.81, sustained: true, arch: 'gyre', form: 'gyre · rolling' },
692 { key: 'tool', label: 'Tool calls', color: '#74aadd', freq: 261.63, sustained: false, arch: 'strike', form: 'strike · reaching' },
693 { key: 'memory', label: 'Memory / RAG', color: '#b6a77f', freq: 195.99, sustained: true, arch: 'gyre', form: 'gyre · scanning' },
694 { key: 'code', label: 'Code execution', color: '#9b9ed7', freq: 164.81, sustained: false, arch: 'strike', form: 'strike · along the body' },
695 { key: 'filesystem', label: 'Filesystem', color: '#92b9c9', freq: 440.00, sustained: false, arch: 'strike', form: 'strike · fanning' },
696 { key: 'network', label: 'Network / API', color: '#d3ac74', freq: 523.25, sustained: false, arch: 'cross', form: 'crossing · one way' },
697 { key: 'browser', label: 'Browser / computer', color: '#9ea9df', freq: 349.23, sustained: false, arch: 'cross', form: 'crossing · a sweep' },
698 { key: 'communication', label: 'Agent messages', color: '#83c5c9', freq: 293.66, sustained: false, arch: 'cross', form: 'crossing · two ways' },
699 { key: 'agent', label: 'Subagent activity', color: '#b09acb', freq: 220.00, sustained: true, arch: 'pod', form: 'pod · peers' },
700 { key: 'orchestration', label: 'Orchestration', color: '#6c8798', freq: 98.00, sustained: true, arch: 'pod', form: 'pod · hub' },
701 { key: 'error', label: 'Errors / exceptions', color: '#e79186', freq: 185.00, sustained: false, arch: 'tear', form: 'torn · irregular' },
702 { key: 'human', label: 'Human interaction', color: '#c2b787', freq: 391.99, sustained: false, arch: 'address', form: 'decision · junction' },
703 { key: 'other', label: 'Unclassified', color: '#738492', freq: 146.83, sustained: false, arch: 'drift', form: 'drifting · unformed' },
704 ];
705 exports.CHANNEL_INDEX = Object.fromEntries(exports.CHANNELS.map((c, i) => [c.key, i]));
706 function validatePetState(value) {
707 const s = value;
708 if (!s || typeof s !== 'object' || !Object.hasOwn(exports.CHANNEL_INDEX, s.channel)
709 || ![s.activity, s.coherence, s.attention, s.observed, s.lit].every(n => Number.isFinite(n) && n >= 0 && n <= 1)
710 || ![s.roamX, s.roamY, s.flip].every(n => Number.isFinite(n) && Math.abs(n) <= 1))
711 throw new Error('Invalid pet state.');
712 }
713 const hex2rgb = (h) => [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)];
714 const RGB = exports.CHANNELS.map(c => hex2rgb(c.color));
715 const UNKNOWN_RGB = hex2rgb('#738492');
716 const REST_RGB = [122, 214, 240];
717 // mulberry32 — a 32-bit seeded PRNG tiny enough to port by hand correctly.
718 function mulberry32(seed) {
719 let a = seed >>> 0;
720 return Object.assign(() => {
721 a = (a + 0x6D2B79F5) >>> 0;
722 let t = a;
723 t = Math.imul(t ^ (t >>> 15), t | 1);
724 t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
725 return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
726 }, { state: () => a, restore: (state) => {
727 if (!Number.isSafeInteger(state) || state < 0 || state > 0xffffffff)
728 throw new Error('Invalid pet random stream.');
729 a = state;
730 } });
731 }
732 /** Work reorganizes the same particles; no new random draws or invented facts.
733 * These are expressive fields, not diagrams of unobserved network/file topology. */
734 function fieldTarget(q, t, act, att, key) {
735 const u = q.s * 2 - 1, lane = q.pod - 2.5, a = q.s * Math.PI * 2;
736 const flow = t * (.35 + act * .65);
737 if (key === 'reasoning') {
738 const ring = .34 + .105 * Math.cos(a * 3 + flow + lane * .18);
739 return [ring * Math.cos(a * 2 + flow * .3), ring * Math.sin(a * 2 + flow * .3) * .7 + .10 * Math.sin(a * 3 + flow)];
740 }
741 if (key === 'memory')
742 return [.46 * Math.cos(a + lane * .1 + flow * .25), lane * .082 + .052 * Math.sin(a * 2 + flow)];
743 if (key === 'code')
744 return [u * .57, lane * .066 + .12 * Math.sin(u * 7 + flow * 2 + q.pod * Math.PI / 3)];
745 if (key === 'filesystem') {
746 const branch = Math.max(0, (u + .3) / 1.3);
747 return [u * .56, lane * .13 * branch + .025 * Math.sin(u * 8 - flow)];
748 }
749 if (key === 'tool') {
750 const reach = .14 + (u + 1) * .20 + .04 * Math.sin(flow * 3 - u * 4);
751 return [Math.cos(q.pod * Math.PI / 3) * reach, Math.sin(q.pod * Math.PI / 3) * reach * .8 + q.hy * .06];
752 }
753 if (key === 'browser')
754 return [u * .56, lane * .083 + .035 * Math.sin(u * 5 - flow * 2)];
755 if (key === 'network' || key === 'communication') {
756 const direction = key === 'communication' && q.pod % 2 === 1 ? -1 : 1;
757 const phase = a + flow * direction;
758 return [.54 * Math.cos(phase), Math.sin(phase) * (.12 + q.pod * .035) + lane * .024];
759 }
760 if (key === 'human') {
761 const gap = u < 0 ? -.075 : .075;
762 return [u * .47 + gap, lane * .10 * Math.abs(u) + .012 * Math.sin(flow + a) * (1 - att)];
763 }
764 return undefined;
765 }
766 // Version 1 retains the original authored gait for existing recordings.
767 function gaitTarget(q, t, act, coh, att, key, work, podSlots, expressionVersion = 1) {
768 const { hx, hy, ang, rad, tail, s, pod, jx, jy } = q;
769 const omega = lerp(4.6, 5.2 + act * 2.8, work);
770 const breath = 1 + Math.sin(t * 1.85) * lerp(0.048, 0.018, work);
771 const flex = Math.sin(ang * 2.05 + t * omega) * lerp(0.042, 0.016 + act * 0.028, work) * (0.18 + 0.82 * tail);
772 let px = Math.cos(ang + flex) * rad * breath;
773 let py = Math.sin(ang + flex) * rad * breath;
774 px += Math.sin(t * 0.33) * lerp(0.030, 0.014, work);
775 py += Math.cos(t * 0.21) * lerp(0.018, 0.010, work);
776 if (work < 0.02)
777 return [px, py];
778 const arch = exports.ARCH_OF[key] || 'drift';
779 let gx = px, gy = py;
780 if (arch === 'gyre') {
781 if (key === 'memory') {
782 const pulse = 1 + Math.sin(t * (2.4 + act * 1.6) - rad * 11) * (0.15 + act * 0.10);
783 gx *= pulse;
784 gy *= pulse;
785 }
786 else {
787 const roll = Math.sin(t * (1.05 + act * 0.35)) * (0.48 + act * 0.32);
788 const c = Math.cos(roll), sn = Math.sin(roll);
789 gx = px * c - py * sn * 0.88;
790 gy = px * sn * 0.88 + py * c;
791 }
792 }
793 else if (arch === 'strike') {
794 if (key === 'tool') {
795 const rate = 2.7 + act * 2.1;
796 const lunge = Math.pow(Math.max(0, Math.sin(t * rate)), 2);
797 gx += lunge * 0.11;
798 if (s > 0.60) {
799 const reach = Math.pow(Math.max(0, Math.sin(t * rate + pod * 0.92)), 4) * (0.30 + act * 0.24);
800 gx += Math.cos(ang) * reach;
801 gy += Math.sin(ang) * reach;
802 }
803 }
804 else if (key === 'code') {
805 const rate = 3.2 + act * 1.8;
806 const wave = Math.sin(t * rate - tail * 7.5);
807 const bump = 0.11 + act * 0.08;
808 gx += Math.cos(ang) * wave * bump;
809 gy += Math.sin(ang) * wave * bump * 1.2;
810 gx += Math.max(0, wave) * 0.07;
811 }
812 else {
813 const rate = 2.15 + act * 1.5;
814 const side = (pod % 2) * 2 - 1;
815 const w = Math.pow(Math.max(0, Math.sin(t * rate + pod * 0.72)), 2);
816 gx += w * 0.055;
817 gy += side * w * (0.17 + act * 0.13);
818 }
819 }
820 else if (arch === 'cross') {
821 if (key === 'browser') {
822 const band = ((t * (0.55 + act * 0.35)) % 1) * 1.28 - 0.64;
823 const inBand = Math.max(0, 1 - Math.abs(hy - band) / 0.08);
824 gx += inBand * (0.24 + act * 0.10);
825 gy += inBand * 0.02;
826 }
827 else {
828 const two = key === 'communication';
829 const courier = s < (two ? 0.44 : 0.32);
830 if (courier) {
831 const dir = two ? (s < 0.22 ? 1 : -1) : 1;
832 const u = (t * (0.38 + act * 0.36) + s * 5.2) % 1;
833 const going = u < 0.5 ? u * 2 : 2 - u * 2;
834 const e = going * going * (3 - 2 * going);
835 gx = lerp(hx, dir * 0.80, e);
836 gy = hy * (1 - e * 0.38) + Math.sin(going * Math.PI) * 0.11 * dir;
837 }
838 }
839 }
840 else if (arch === 'pod') {
841 const n = 6, member = podSlots?.length ? podSlots[pod % podSlots.length] : undefined;
842 const k = member ? member[0] : pod % n;
843 const hub = key === 'orchestration' && k === 0;
844 const spread = 0.30 + act * 0.11;
845 const orbit = t * (0.55 + act * 0.28);
846 if (hub) {
847 gx = px * 0.70;
848 gy = py * 0.70;
849 }
850 else {
851 const slots = key === 'orchestration' ? n - 1 : n;
852 const a = (key === 'orchestration' ? k - 1 : k) * (Math.PI * 2 / slots) + orbit + (member ? member[1] * .04 : 0);
853 const sc = 0.34;
854 gx = hx * sc + Math.cos(a) * spread * 1.28;
855 gy = hy * sc + Math.sin(a) * spread * 0.80;
856 }
857 }
858 else if (arch === 'tear') {
859 const side = hx + hy < 0 ? -1 : 1;
860 gx += side * (0.24 + (1 - coh) * 0.16);
861 gy += side * 0.15;
862 gx += Math.sin(t * 11.4 + s * 40) * (0.045 + act * 0.05);
863 gy += Math.cos(t * 9.2 + s * 31) * (0.040 + act * 0.045);
864 }
865 else if (arch === 'address') {
866 const face = 0.90 + att * 0.08;
867 const th = 0.70;
868 const z = (s - 0.5) * 0.42;
869 let ax = hx * Math.cos(th) + z * Math.sin(th);
870 let ay = hy;
871 const disc = 0.48 * face;
872 ax = lerp(ax, Math.cos(ang) * Math.min(0.36, rad + 0.06) * 0.95, disc);
873 ay = lerp(ay, Math.sin(ang) * Math.min(0.36, rad + 0.06) * 1.08, disc);
874 const grow = 1.20 + Math.sin(t * 1.65) * 0.055;
875 gx = ax * grow;
876 gy = ay * grow;
877 }
878 else {
879 const mill = 0.13 + (1 - coh) * 0.10;
880 gx = hx * 0.52 + Math.sin(t * 0.72 + jx) * mill;
881 gy = hy * 0.52 + Math.cos(t * 0.54 + jy) * mill;
882 }
883 if (expressionVersion === 2) {
884 const field = fieldTarget(q, t, act, att, key);
885 if (field)
886 [gx, gy] = field;
887 }
888 return [lerp(px, gx, work), lerp(py, gy, work)];
889 }
890 // Fixed reduced-motion clock per channel, so ticks still point along the gait.
891 const STILL_T = {
892 reasoning: 1.15, memory: 0.42, tool: 0.30, code: 0.18, filesystem: 0.48,
893 network: 0.72, browser: 0.95, communication: 0.58, agent: 1.25,
894 orchestration: 0.85, error: 0.35, human: 0.05, other: 0.90,
895 };
896 class PetSim {
897 expressionVersion;
898 p;
899 phase = 0;
900 clock = 0;
901 tear = 0;
902 prev;
903 col = [...REST_RGB];
904 cur;
905 frame = { r: REST_RGB[0], g: REST_RGB[1], b: REST_RGB[2], alpha: 0.3, hollow: false, channel: 'reasoning', arch: 'gyre', work: 0 };
906 constructor(points, seed = 0xC0FFEE, expressionVersion = 2) {
907 this.expressionVersion = expressionVersion;
908 if (expressionVersion !== 1 && expressionVersion !== 2)
909 throw new Error('Unsupported pet expression version.');
910 const rand = mulberry32(seed);
911 this.p = points.map(([hx, hy], i) => {
912 const q = {
913 x: hx, y: hy, vx: 0, vy: 0,
914 s: rand(), jx: rand() * 6.283, jy: rand() * 6.283, pod: i % 6,
915 hx, hy, ang: 0, rad: 0, tail: 0, tx: hx, ty: hy,
916 };
917 q.ang = Math.atan2(hy, hx);
918 q.rad = Math.hypot(hx, hy);
919 q.tail = clamp(((-hx - hy) * 0.5 + 0.22) / 0.62);
920 return q;
921 });
922 this.cur = this.prev = exports.CHANNEL_INDEX['reasoning'];
923 }
924 checkpoint() {
925 return { version: 1, expressionVersion: this.expressionVersion, body: this.p.map(p => [p.hx, p.hy, p.s]),
926 particles: this.p.map(p => [p.x, p.y, p.vx, p.vy, p.jx, p.jy, p.tx, p.ty]),
927 phase: this.phase, clock: this.clock, tear: this.tear, previous: this.prev, current: this.cur,
928 color: [...this.col], frame: { ...this.frame } };
929 }
930 /** Restore into a newly constructed sim. Authored body and seeded particle
931 * identity must match exactly; a checkpoint cannot replace the whale. */
932 restore(value) {
933 const c = value;
934 const inRange = (n, low, high) => Number.isFinite(n) && n >= low && n <= high;
935 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
936 || 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)
937 || !Array.isArray(c.particles) || c.particles.length !== this.p.length
938 || 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)))
939 || !inRange(c.phase, 0, exports.PET_MAX_SECONDS) || !inRange(c.clock, 0, exports.PET_MAX_SECONDS) || !inRange(c.tear, 0, 1)
940 || ![c.previous, c.current].every(n => Number.isInteger(n) && n >= 0 && n < exports.CHANNELS.length)
941 || !Array.isArray(c.color) || c.color.length !== 3 || c.color.some(n => !inRange(n, 0, 255))
942 || !c.frame || ![c.frame.r, c.frame.g, c.frame.b].every(n => inRange(n, 0, 255))
943 || !inRange(c.frame.alpha, 0, 1) || !inRange(c.frame.work, 0, 1) || typeof c.frame.hollow !== 'boolean'
944 || c.frame.channel !== exports.CHANNELS[c.current].key || c.frame.arch !== exports.CHANNELS[c.current].arch)
945 throw new Error('Invalid pet particle checkpoint.');
946 this.phase = c.phase;
947 this.clock = c.clock;
948 this.tear = c.tear;
949 this.prev = c.previous;
950 this.cur = c.current;
951 this.col = [...c.color];
952 this.frame = { ...c.frame };
953 this.p.forEach((p, i) => { [p.x, p.y, p.vx, p.vy, p.jx, p.jy, p.tx, p.ty] = c.particles[i]; });
954 }
955 /** Advance the sim by dt seconds under `state`. Identical math on every port. */
956 step(dt, state, opts) {
957 const S = (v) => lerp(0.5, v, opts.sensitivity);
958 const act = S(state.activity), coh = S(state.coherence), att = S(state.attention);
959 const seen = S(state.observed === undefined ? 1 : state.observed);
960 const motion = opts.motion ? 1 : 0;
961 this.phase += dt * (0.18 + act * 0.55) * motion;
962 this.clock += dt * (opts.motion ? 1 : 0);
963 if (exports.CHANNEL_INDEX[state.channel] !== undefined)
964 this.cur = exports.CHANNEL_INDEX[state.channel];
965 const shown = this.cur;
966 const ch = exports.CHANNELS[shown];
967 const work = clamp((act - 0.16) / 0.18);
968 const wander = lerp(0.32, 1, Math.pow(1 - coh, 1.15));
969 if (shown !== this.prev) {
970 if (shown === exports.CHANNEL_INDEX['error'])
971 this.tear = 1;
972 this.prev = shown;
973 }
974 this.tear = opts.motion ? Math.max(0, this.tear - dt * 1.6) : 0;
975 const split = Math.pow(1 - coh, 1.6) * 0.16 + this.tear * 0.10;
976 const blur = Math.pow(1 - coh, 1.45) * 0.22 + this.tear * 0.18;
977 const pull = opts.motion ? (2.2 + coh * 5.2) : 18;
978 const tGait = opts.motion ? this.clock : (STILL_T[ch.key] ?? 0.4);
979 for (const q of this.p) {
980 if (opts.motion) {
981 q.jx += dt * (0.40 + act * 1.1);
982 q.jy += dt * (0.34 + act * 0.9);
983 }
984 const [gx, gy] = gaitTarget(q, tGait, act, coh, att, ch.key, work, opts.podSlots, this.expressionVersion);
985 const podAng = q.pod * 1.047 + this.phase * 0.22;
986 const tx = gx + Math.sin(q.jx + q.s * 9) * blur * wander + Math.cos(podAng) * split;
987 const ty = gy + Math.cos(q.jy + q.s * 7) * blur * wander + Math.sin(podAng) * split * 0.55;
988 q.tx = tx;
989 q.ty = ty;
990 if (!opts.motion) {
991 q.x = tx;
992 q.y = ty;
993 q.vx = 0;
994 q.vy = 0;
995 continue;
996 }
997 q.vx += (tx - q.x) * pull * dt;
998 q.vy += (ty - q.y) * pull * dt;
999 q.vx *= 0.90;
1000 q.vy *= 0.90;
1001 q.x += q.vx * dt * (opts.motion ? 2.6 : 8);
1002 q.y += q.vy * dt * (opts.motion ? 2.6 : 8);
1003 }
1004 // ---- visual encoding: the parts of "the same view" that are not motion
1005 const want = work > 0.35 ? RGB[shown] : REST_RGB;
1006 const k = opts.motion ? Math.min(1, dt * 2.6) : 1;
1007 for (let c = 0; c < 3; c++)
1008 this.col[c] += (lerp(UNKNOWN_RGB[c], want[c], seen) - this.col[c]) * k;
1009 const lit = clamp(state.lit);
1010 const alpha = (0.22 + act * 0.10) * lerp(0.50, 1, coh) * lerp(0.55, 1, seen) * lerp(0.35, 1, lit);
1011 this.frame = {
1012 r: this.col[0], g: this.col[1], b: this.col[2],
1013 alpha: Math.min(0.92, alpha * 1.85),
1014 hollow: seen < 0.92,
1015 channel: ch.key, arch: ch.arch, work,
1016 };
1017 }
1018 }
1019 exports.PetSim = PetSim;
1020 /** Body-space → renderer-space. Renderers place each dot at (lx,ly) in pixels/cells. */
1021 function layout(w, h, state) {
1022 const att = state.attention;
1023 const scale = Math.min(w * 0.52, h * 0.92) * (1 + att * 0.07);
1024 return {
1025 scale,
1026 flipX: state.flip,
1027 ox: w / 2 + state.roamX * w * 0.30,
1028 oy: h / 2 + state.roamY * h * 0.30 + h * att * 0.05,
1029 dot: Math.max(1.6, Math.min(w, h) * 0.0092) * (1 + att * 0.18),
1030 };
1031 }
1032 // ---------------------------------------------------------------------------
1033 // Conformance. A tape is a list of [dt, state] rows; run it and digest the
1034 // quantized field every `every` frames. 64×32 cells over [-0.66, 0.66]².
1035 // Two implementations that produce the same digest lines drew the same whale.
1036 function digest(sim) {
1037 const W = 64, H = 32;
1038 const grid = new Uint8Array(W * H);
1039 for (const q of sim.p) {
1040 const cx = Math.floor((q.x + 0.66) / 1.32 * W);
1041 const cy = Math.floor((q.y + 0.66) / 1.32 * H);
1042 if (cx >= 0 && cx < W && cy >= 0 && cy < H)
1043 grid[cy * W + cx] = Math.min(255, grid[cy * W + cx] + 1);
1044 }
1045 // FNV-1a 64 over the grid plus the frame encoding (rgb, hollow, alpha byte)
1046 // Two unsigned halves also work in embedded engines without BigInt.
1047 // FNV's prime is (256 << 32) + 435; these products stay below 2^42,
1048 // so every intermediate integer is exactly representable by a JS number.
1049 let hi = 0xcbf29ce4, lo = 0x84222325;
1050 const mix = (b) => {
1051 lo = (lo ^ (b & 0xff)) >>> 0;
1052 const product = lo * 435;
1053 hi = (hi * 435 + lo * 256 + Math.floor(product / 4294967296)) >>> 0;
1054 lo = product >>> 0;
1055 };
1056 for (const v of grid)
1057 mix(v);
1058 mix(Math.round(sim.frame.r));
1059 mix(Math.round(sim.frame.g));
1060 mix(Math.round(sim.frame.b));
1061 mix(Math.round(sim.frame.alpha * 255));
1062 mix(sim.frame.hollow ? 1 : 0);
1063 return hi.toString(16).padStart(8, '0') + lo.toString(16).padStart(8, '0');
1064 }
1065 /** Shared tape runner. `rows` are parsed tape.tsv lines. */
1066 function runTape(sim, rows, opts) {
1067 const out = [];
1068 let f = 0;
1069 for (const row of rows) {
1070 const c = row.split('\t');
1071 if (c.length < 10 || c[0] === 'dt')
1072 continue;
1073 const st = {
1074 activity: +c[1], coherence: +c[2], attention: +c[3], channel: c[4],
1075 observed: +c[5], roamX: +c[6], roamY: +c[7], flip: +c[8], lit: +c[9],
1076 };
1077 sim.step(+c[0], st, opts);
1078 if (f++ % 30 === 0)
1079 out.push(`f${String(f - 1).padStart(4, '0')} ${digest(sim)} ${st.channel}`);
1080 }
1081 out.push(`final ${digest(sim)}`);
1082 return out;
1083 }
1084
1085 };
1086 factories["pet-telemetry"]=function(exports,require){
1087 "use strict";
1088 Object.defineProperty(exports, "__esModule", { value: true });
1089 exports.PetLiveTape = exports.PET_BIN_MS = void 0;
1090 exports.validatePetBucket = validatePetBucket;
1091 exports.decodePetJSONL = decodePetJSONL;
1092 exports.compilePetTelemetry = compilePetTelemetry;
1093 exports.encodePetJSONL = encodePetJSONL;
1094 exports.encodePetTSV = encodePetTSV;
1095 const model_js_1 = require("./model.js");
1096 const signal_js_1 = require("./signal.js");
1097 const pet_sim_js_1 = require("./pet-sim.js");
1098 /** One projection for imports, demos and recorded live snapshots. Times are ms.
1099 * These are aesthetic encodings of measured events, never model confidence. */
1100 exports.PET_BIN_MS = 400;
1101 function validatePetBucket(value) {
1102 (0, pet_sim_js_1.validatePetState)(value);
1103 const b = value;
1104 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
1105 || b.simTimeMs !== b.sequence * exports.PET_BIN_MS || b.durationMs !== exports.PET_BIN_MS
1106 || !model_js_1.CATEGORIES.includes(b.channel) || typeof b.waiting !== 'boolean'
1107 || !Array.isArray(b.agentIds) || b.agentIds.length > 250_000 || b.agentIds.some(id => typeof id !== 'string' || !id || id.length > 4096)
1108 || !Number.isSafeInteger(b.errors) || b.errors < 0 || b.errors > 250_000
1109 || !Array.isArray(b.onsets) || b.onsets.length !== 13 || b.onsets.some(n => !Number.isSafeInteger(n) || n < 0 || n > 250_000)
1110 || !Array.isArray(b.activeMs) || b.activeMs.length !== 13 || b.activeMs.some(n => !Number.isFinite(n) || n < 0 || n > 100_000_000))
1111 throw new Error('Invalid version 1 pet bucket.');
1112 }
1113 function decodePetJSONL(text) {
1114 if (text.length > 64 * 1024 * 1024)
1115 throw new Error('Pet tape exceeds 64 MiB.');
1116 const rows = text.split(/\r?\n/).filter(line => line.trim()).map(line => JSON.parse(line));
1117 if (rows.length > 216_000)
1118 throw new Error('Pet tape exceeds 24 hours.');
1119 return rows.map((row, i) => { validatePetBucket(row); if (row.sequence !== i)
1120 throw new Error('Non-contiguous pet tape.'); return row; });
1121 }
1122 /** A live file must advance before its contents count as a new observation.
1123 * Existing bytes, duplicate samples and a restarted sequence establish a
1124 * baseline; they never replay an old onset or human request. Drivers supply a
1125 * bounded tail and reset this cursor after suspension or a new attachment. */
1126 class PetLiveTape {
1127 sequence;
1128 reset() { this.sequence = undefined; }
1129 readTail(text) {
1130 if (!text) {
1131 this.reset();
1132 return;
1133 }
1134 if (text.length > 262_144) {
1135 this.reset();
1136 throw new Error('Live pet input exceeds its tail limit.');
1137 }
1138 if (!text.endsWith('\n'))
1139 return;
1140 const line = text.trimEnd().split('\n').at(-1);
1141 if (!line)
1142 return;
1143 let packet;
1144 try {
1145 packet = JSON.parse(line);
1146 validatePetBucket(packet);
1147 }
1148 catch (error) {
1149 this.reset();
1150 throw error;
1151 }
1152 const previous = this.sequence;
1153 this.sequence = packet.sequence;
1154 if (previous === undefined || packet.sequence <= previous)
1155 return;
1156 return packet;
1157 }
1158 }
1159 exports.PetLiveTape = PetLiveTape;
1160 const order = (a, b) => a < b ? -1 : a > b ? 1 : 0;
1161 const keyOf = (e) => JSON.stringify([e.traceId, e.id]);
1162 const isContainer = (e) => e.attributes['whalesong.container'] === true
1163 || e.attributes['codewhale.container'] === true;
1164 /** Compile a single trace. Unknown-duration spans provide onsets, not occupancy.
1165 * Updates of the same trace/id replace earlier snapshots rather than double count.
1166 * An endpoint onset gets its own bucket; intervals use [start, end). */
1167 function compilePetTelemetry(input, durationMs = 0, firstSequence = 0, originMs = 0) {
1168 if (input.length > 250_000)
1169 throw new Error('Pet input exceeds 250000 events.');
1170 if (!Number.isFinite(durationMs) || durationMs < 0)
1171 throw new Error('Invalid pet duration.');
1172 if (!Number.isSafeInteger(firstSequence) || firstSequence < 0)
1173 throw new Error('Invalid first pet bucket.');
1174 if (!Number.isFinite(originMs))
1175 throw new Error('Invalid pet clock origin.');
1176 durationMs = Math.max(0, durationMs - originMs);
1177 const unique = new Map();
1178 const traces = new Set();
1179 for (const e of input) {
1180 if (e.schemaVersion !== 1 || !e.id || !e.traceId || !model_js_1.CATEGORIES.includes(e.category)
1181 || !Number.isFinite(e.startTime) || !Number.isFinite(e.endTime)
1182 || e.startTime < 0 || e.endTime < e.startTime || !e.attributes)
1183 throw new Error('Invalid event-v1 pet input. Import through importTrace first.');
1184 traces.add(e.traceId);
1185 unique.set(keyOf(e), e);
1186 }
1187 if (traces.size > 1)
1188 throw new Error('Select one trace for the pet.');
1189 const parents = new Set([...unique.values()].filter(e => e.parentId).map(e => e.parentId));
1190 const events = [...unique.values()].filter(e => !isContainer(e)
1191 && !(e.category === 'orchestration' && parents.has(e.id)))
1192 .map(e => ({ ...e, startTime: e.startTime - originMs, endTime: (e.openEnded ? e.startTime : e.endTime) - originMs,
1193 attributes: e.attributes['whalesong.error_onset_ms'] === undefined ? e.attributes
1194 : { ...e.attributes, 'whalesong.error_onset_ms': (0, model_js_1.errorOnsetOf)(e) - originMs } }))
1195 .sort((a, b) => a.startTime - b.startTime || order(a.id, b.id));
1196 let lastOnset = 0;
1197 for (const e of events) {
1198 durationMs = Math.max(durationMs, e.endTime);
1199 lastOnset = Math.max(lastOnset, e.startTime);
1200 }
1201 const failures = events.filter(e => e.category === 'error' || e.status === 'error').map(model_js_1.errorOnsetOf).sort((a, b) => a - b);
1202 if (failures.length)
1203 lastOnset = Math.max(lastOnset, failures[failures.length - 1]);
1204 const count = Math.max(1, Math.ceil(durationMs / exports.PET_BIN_MS), Math.floor(lastOnset / exports.PET_BIN_MS) + 1);
1205 if (count - firstSequence > 216_000 || count > pet_sim_js_1.PET_MAX_SECONDS * 2.5)
1206 throw new Error('Pet replay exceeds 24 hours; select a shorter trace.');
1207 const index = new signal_js_1.IntervalIndex(events), result = [];
1208 const recent = [], names = new Map();
1209 let next = 0, expired = 0, nextFailure = 0;
1210 for (let sequence = firstSequence; sequence < count; sequence++) {
1211 const start = sequence * exports.PET_BIN_MS, end = start + exports.PET_BIN_MS;
1212 // A trailing window only: appending future events cannot rewrite earlier bins.
1213 while (next < events.length && events[next].startTime < end) {
1214 const e = events[next++];
1215 // A liveness pulse continues an operation; it is not a repeated tool call.
1216 if (e.attributes['whalesong.continuation'] === true)
1217 continue;
1218 recent.push(e);
1219 names.set(e.name, (names.get(e.name) ?? 0) + 1);
1220 }
1221 while (expired < recent.length && recent[expired].startTime < end - 12_000) {
1222 const name = recent[expired++].name, n = names.get(name) - 1;
1223 if (n)
1224 names.set(name, n);
1225 else
1226 names.delete(name);
1227 }
1228 const onsets = model_js_1.CATEGORIES.map(() => 0), activeMs = model_js_1.CATEGORIES.map(() => 0);
1229 const agents = new Set();
1230 while (nextFailure < failures.length && failures[nextFailure] < start)
1231 nextFailure++;
1232 let errors = 0, waiting = false, human = false;
1233 while (nextFailure < failures.length && failures[nextFailure] < end) {
1234 errors++;
1235 nextFailure++;
1236 }
1237 for (const e of index.query(start, end)) {
1238 if (e.startTime >= end)
1239 continue;
1240 const onset = e.startTime >= start, c = model_js_1.CATEGORIES.indexOf(e.category);
1241 const overlap = Math.max(0, Math.min(end, e.endTime) - Math.max(start, e.startTime));
1242 if (!onset && !overlap)
1243 continue;
1244 activeMs[c] += overlap;
1245 if (onset)
1246 onsets[c]++;
1247 if (e.agentId && !['unknown', 'unattributed'].includes(e.agentId))
1248 agents.add(e.agentId);
1249 if (e.category === 'human') {
1250 human = true;
1251 waiting ||= e.status === 'pending' || e.status === 'running' || e.attributes['whalesong.waiting'] === true;
1252 }
1253 }
1254 const total = activeMs.reduce((a, b) => a + b, 0), hits = onsets.reduce((a, b) => a + b, 0);
1255 const observed = total > 0 || hits > 0 || errors > 0;
1256 let dominant = model_js_1.CATEGORIES.indexOf('other');
1257 for (let c = 0; c < model_js_1.CATEGORIES.length; c++) {
1258 if (activeMs[c] > activeMs[dominant]
1259 || activeMs[c] === activeMs[dominant] && onsets[c] > onsets[dominant])
1260 dominant = c;
1261 }
1262 let repeated = 0;
1263 for (const n of names.values())
1264 if (n >= 4)
1265 repeated += n;
1266 const repeatDensity = repeated / Math.max(1, recent.length - expired);
1267 const channel = errors ? 'error' : human ? 'human' : agents.size >= 3 ? 'agent' : model_js_1.CATEGORIES[dominant];
1268 result.push({ version: 1, sequence, simTimeMs: start, durationMs: exports.PET_BIN_MS,
1269 activity: observed ? (0, model_js_1.clamp)(.28 + .38 * total / exports.PET_BIN_MS + .08 * hits, 0, 1) : .12,
1270 coherence: observed ? (0, model_js_1.clamp)(.92 - repeatDensity * .58 - Math.min(.55, errors * .18), .08, 1) : .25,
1271 attention: waiting ? .8 : human ? .65 : errors ? .45 : 0,
1272 channel, observed: observed ? 1 : 0, roamX: 0, roamY: 0, flip: 1, lit: 1,
1273 onsets, activeMs, errors, agentIds: [...agents].sort(order), waiting });
1274 }
1275 return result;
1276 }
1277 /** Flat state remains compatible with native readers; metadata preserves audio
1278 * onsets and peer identities. No prompts, tool arguments, or event names escape. */
1279 function encodePetJSONL(buckets) {
1280 return buckets.map(b => JSON.stringify(b)).join('\n') + (buckets.length ? '\n' : '');
1281 }
1282 /** Legacy visual conformance interchange. Use JSONL to preserve audio metadata. */
1283 function encodePetTSV(buckets) {
1284 return 'dt\tactivity\tcoherence\tattention\tchannel\tobserved\troamX\troamY\tflip\tlit\n'
1285 + buckets.map(b => [b.durationMs / 1000, b.activity, b.coherence, b.attention, b.channel,
1286 b.observed, b.roamX, b.roamY, b.flip, b.lit].join('\t')).join('\n') + '\n';
1287 }
1288
1289 };
1290 factories["signal"]=function(exports,require){
1291 "use strict";
1292 Object.defineProperty(exports, "__esModule", { value: true });
1293 exports.IntervalIndex = void 0;
1294 exports.buildPyramid = buildPyramid;
1295 exports.chooseLevel = chooseLevel;
1296 exports.binValue = binValue;
1297 exports.intensity = intensity;
1298 exports.totals = totals;
1299 exports.onsetSeries = onsetSeries;
1300 exports.autocorrelation = autocorrelation;
1301 exports.periodogram = periodogram;
1302 exports.unionDuration = unionDuration;
1303 const model_js_1 = require("./model.js");
1304 function emptyLevel(length, binMs) {
1305 const n = model_js_1.CATEGORIES.length * length;
1306 return { binMs, length, onsets: new Float64Array(n), activeMs: new Float64Array(n),
1307 outputTokens: new Float64Array(n), cost: new Float64Array(n), errors: new Float64Array(n), peak: new Float64Array(n) };
1308 }
1309 const FIELDS = ['onsets', 'activeMs', 'outputTokens', 'cost', 'errors'];
1310 /** O(events + channels × bins), including long intervals. No span-length inner loop. */
1311 function buildPyramid(events, requestedDuration, maxBins = 16_384) {
1312 if (!Number.isInteger(maxBins) || maxBins < 16 || maxBins > 1_048_576)
1313 throw new Error('maxBins must be an integer in [16, 1048576].');
1314 let duration = requestedDuration ?? 1;
1315 for (const e of events)
1316 duration = Math.max(duration, e.endTime, e.startTime, e.status === 'error' ? (0, model_js_1.errorOnsetOf)(e) : 0);
1317 if (!Number.isFinite(duration) || duration < 0)
1318 throw new Error('Signal duration must be finite and nonnegative.');
1319 duration = Math.max(1, duration);
1320 const binMs = 2 ** Math.ceil(Math.log2(Math.max(1, duration / (maxBins - 1))));
1321 const length = Math.floor(duration / binMs) + 1, fine = emptyLevel(length, binMs);
1322 const stride = length + 1;
1323 const activeDiff = new Float64Array(model_js_1.CATEGORIES.length * stride), tokenDiff = new Float64Array(model_js_1.CATEGORIES.length * stride);
1324 for (const e of events) {
1325 if (!Number.isFinite(e.startTime) || !Number.isFinite(e.endTime) || e.startTime < 0 || e.endTime < e.startTime)
1326 throw new Error(`Invalid interval for ${e.id}.`);
1327 const channel = model_js_1.CATEGORIES.indexOf(e.category);
1328 if (channel < 0)
1329 throw new Error(`Unknown category for ${e.id}.`);
1330 const a = Math.floor(e.startTime / binMs), b = Math.floor(e.endTime / binMs);
1331 const at = channel * length, diff = channel * stride;
1332 fine.onsets[at + a]++;
1333 fine.cost[at + a] += e.cost ?? 0;
1334 if (e.status === 'error')
1335 fine.errors[at + Math.floor((0, model_js_1.errorOnsetOf)(e) / binMs)]++;
1336 const d = e.endTime - e.startTime, tokens = e.outputTokens ?? 0;
1337 if (d === 0) {
1338 fine.outputTokens[at + a] += tokens;
1339 continue;
1340 }
1341 if (a === b) {
1342 fine.activeMs[at + a] += d;
1343 fine.outputTokens[at + a] += tokens;
1344 }
1345 else {
1346 const left = (a + 1) * binMs - e.startTime, right = e.endTime - b * binMs, tokenRate = tokens / d;
1347 fine.activeMs[at + a] += left;
1348 fine.activeMs[at + b] += right;
1349 fine.outputTokens[at + a] += left * tokenRate;
1350 fine.outputTokens[at + b] += right * tokenRate;
1351 if (b > a + 1) {
1352 activeDiff[diff + a + 1] += binMs;
1353 activeDiff[diff + b] -= binMs;
1354 tokenDiff[diff + a + 1] += binMs * tokenRate;
1355 tokenDiff[diff + b] -= binMs * tokenRate;
1356 }
1357 }
1358 }
1359 for (let c = 0; c < model_js_1.CATEGORIES.length; c++) {
1360 let active = 0, tokens = 0;
1361 for (let i = 0; i < length; i++) {
1362 active += activeDiff[c * stride + i];
1363 tokens += tokenDiff[c * stride + i];
1364 const at = c * length + i;
1365 fine.activeMs[at] = Math.max(0, fine.activeMs[at] + active);
1366 fine.outputTokens[at] = Math.max(0, fine.outputTokens[at] + tokens);
1367 fine.peak[at] = fine.activeMs[at] / binMs;
1368 }
1369 }
1370 const levels = [fine];
1371 while (levels.at(-1).length > 1) {
1372 const child = levels.at(-1), parent = emptyLevel(Math.ceil(child.length / 2), child.binMs * 2);
1373 for (let c = 0; c < model_js_1.CATEGORIES.length; c++)
1374 for (let i = 0; i < parent.length; i++) {
1375 const a = c * child.length + i * 2, b = a + 1, dst = c * parent.length + i, hasB = i * 2 + 1 < child.length;
1376 for (const key of FIELDS)
1377 parent[key][dst] = child[key][a] + (hasB ? child[key][b] : 0);
1378 parent.peak[dst] = Math.max(child.peak[a], hasB ? child.peak[b] : 0);
1379 }
1380 levels.push(parent);
1381 }
1382 const p = { duration, channels: model_js_1.CATEGORIES, levels, calibration: { activity: 1, onsets: 1, tokens: 1, cost: 1 } };
1383 const reference = chooseLevel(p, duration / 1200);
1384 for (const metric of ['activity', 'onsets', 'tokens', 'cost']) {
1385 const positives = [];
1386 for (let c = 0; c < model_js_1.CATEGORIES.length; c++)
1387 for (let i = 0; i < reference.length; i++) {
1388 const v = binValue(reference, c, i, metric);
1389 if (v > 0)
1390 positives.push(v);
1391 }
1392 p.calibration[metric] = Math.max(1e-9, (0, model_js_1.quantile)(positives, .95));
1393 }
1394 return p;
1395 }
1396 /** Choose the coarsest stored level no wider than one requested pixel interval. */
1397 function chooseLevel(p, targetBinMs) {
1398 let result = p.levels[0];
1399 for (const level of p.levels) {
1400 if (level.binMs > targetBinMs)
1401 break;
1402 result = level;
1403 }
1404 return result;
1405 }
1406 function binValue(level, channel, bin, metric) {
1407 if (bin < 0 || bin >= level.length)
1408 return 0;
1409 const at = channel * level.length + bin;
1410 if (metric === 'activity')
1411 return level.activeMs[at] / level.binMs;
1412 if (metric === 'onsets')
1413 return level.onsets[at] * 1000 / level.binMs;
1414 if (metric === 'tokens')
1415 return level.outputTokens[at] * 1000 / level.binMs;
1416 return level.cost[at] * 1000 / level.binMs;
1417 }
1418 function intensity(value, reference) {
1419 return (0, model_js_1.clamp)(Math.log1p(value / Math.max(1e-9, reference) * 8) / Math.log(9), 0, 1);
1420 }
1421 /** Conserved totals: useful for tests and alternate native backends. */
1422 function totals(level) {
1423 return Object.fromEntries(FIELDS.map(k => [k, level[k].reduce((s, x) => s + x, 0)]));
1424 }
1425 /** Sorted-start, max-end segment tree. Long root spans do not force a reverse
1426 * scan through every earlier event. Results are chronological and honor limits. */
1427 class IntervalIndex {
1428 events;
1429 maxEnd;
1430 leafCount;
1431 constructor(events) {
1432 this.events = [...events].sort((a, b) => a.startTime - b.startTime || a.id.localeCompare(b.id));
1433 this.leafCount = 2 ** Math.ceil(Math.log2(Math.max(1, this.events.length)));
1434 this.maxEnd = new Float64Array(this.leafCount * 2).fill(-Infinity);
1435 for (let i = 0; i < this.events.length; i++)
1436 this.maxEnd[this.leafCount + i] = this.events[i].endTime;
1437 for (let i = this.leafCount - 1; i; i--)
1438 this.maxEnd[i] = Math.max(this.maxEnd[i * 2], this.maxEnd[i * 2 + 1]);
1439 }
1440 query(start, end, limit = Infinity) {
1441 if (!Number.isFinite(start) || !Number.isFinite(end) || end < start || limit <= 0)
1442 return [];
1443 let lo = 0, hi = this.events.length;
1444 while (lo < hi) {
1445 const mid = (lo + hi) >>> 1;
1446 if (this.events[mid].startTime <= end)
1447 lo = mid + 1;
1448 else
1449 hi = mid;
1450 }
1451 const bound = lo, found = [];
1452 const visit = (node, left, right) => {
1453 if (left >= bound || this.maxEnd[node] < start || found.length >= limit)
1454 return;
1455 if (right - left === 1) {
1456 const e = this.events[left];
1457 if (e && (e.endTime > start || e.startTime === e.endTime && e.startTime >= start))
1458 found.push(e);
1459 return;
1460 }
1461 const mid = (left + right) >>> 1;
1462 visit(node * 2, left, mid);
1463 visit(node * 2 + 1, mid, right);
1464 };
1465 visit(1, 0, this.leafCount);
1466 return found;
1467 }
1468 }
1469 exports.IntervalIndex = IntervalIndex;
1470 /** Sample an onset train into equal-width bins; nothing is inferred between impulses. */
1471 function onsetSeries(events, start, end, n = 256, category) {
1472 const out = new Float64Array(n), span = Math.max(1e-6, end - start);
1473 for (const e of events)
1474 if ((!category || e.category === category) && e.startTime >= start && e.startTime < end) {
1475 const at = Math.floor((e.startTime - start) / span * n);
1476 if (at >= 0 && at < n)
1477 out[at]++;
1478 }
1479 return out;
1480 }
1481 /** Centered, variance-normalized autocorrelation; lag-zero is one unless constant. */
1482 function autocorrelation(input, maxLag = 64) {
1483 const n = input.length;
1484 if (!n)
1485 return [];
1486 let mean = 0;
1487 for (let i = 0; i < n; i++)
1488 mean += input[i];
1489 mean /= n;
1490 const centered = Array.from(input, x => x - mean), energy = centered.reduce((s, v) => s + v * v, 0);
1491 const result = [];
1492 for (let lag = 0; lag <= Math.min(maxLag, n - 1); lag++) {
1493 let sum = 0;
1494 for (let i = 0; i < n - lag; i++)
1495 sum += centered[i] * centered[i + lag];
1496 result.push(energy > 1e-12 ? sum / energy : 0);
1497 }
1498 return result;
1499 }
1500 /** Hann-window periodogram of an actual uniformly sampled onset signal, DC removed. */
1501 function periodogram(input, sampleHz) {
1502 const n = input.length, frequencies = [], power = [];
1503 if (n < 4 || sampleHz <= 0)
1504 return { frequencies, power, entropy: 0, peakHz: 0, sampleHz, resolutionHz: 0 };
1505 let mean = 0;
1506 for (let i = 0; i < n; i++)
1507 mean += input[i];
1508 mean /= n;
1509 const windowed = Array.from(input, (x, i) => (x - mean) * (.5 - .5 * Math.cos(2 * Math.PI * i / (n - 1))));
1510 const windowEnergy = Array.from({ length: n }, (_, i) => (.5 - .5 * Math.cos(2 * Math.PI * i / (n - 1))) ** 2).reduce((a, b) => a + b, 0);
1511 for (let k = 1; k <= Math.floor(n / 2); k++) {
1512 let re = 0, im = 0;
1513 for (let t = 0; t < n; t++) {
1514 const angle = 2 * Math.PI * k * t / n;
1515 re += windowed[t] * Math.cos(angle);
1516 im -= windowed[t] * Math.sin(angle);
1517 }
1518 frequencies.push(k * sampleHz / n);
1519 power.push((re * re + im * im) / (windowEnergy * sampleHz) * (n % 2 === 0 && k === n / 2 ? 1 : 2));
1520 }
1521 const sum = power.reduce((s, x) => s + x, 0);
1522 let entropy = 0;
1523 for (const x of power)
1524 if (x > 0 && sum > 0) {
1525 const p = x / sum;
1526 entropy -= p * Math.log2(p);
1527 }
1528 entropy = power.length > 1 ? entropy / Math.log2(power.length) : 0;
1529 const max = Math.max(...power);
1530 return { frequencies, power, entropy, peakHz: max > 1e-12 ? frequencies[power.indexOf(max)] : 0, sampleHz, resolutionHz: sampleHz / n };
1531 }
1532 function unionDuration(events, start = 0, end = Infinity) {
1533 const intervals = events.filter(e => e.endTime > e.startTime && e.endTime > start && e.startTime < end)
1534 .map(e => [Math.max(start, e.startTime), Math.min(end, e.endTime)]).sort((a, b) => a[0] - b[0]);
1535 let total = 0, left = 0, right = 0;
1536 for (const [a, b] of intervals) {
1537 if (a > right) {
1538 total += right - left;
1539 left = a;
1540 right = b;
1541 }
1542 else
1543 right = Math.max(right, b);
1544 }
1545 return total + right - left;
1546 }
1547
1548 };
1549 factories["pet-audio"]=function(exports,require){
1550 "use strict";
1551 Object.defineProperty(exports, "__esModule", { value: true });
1552 exports.PetScore = void 0;
1553 exports.renderPetPCM = renderPetPCM;
1554 const pet_sim_js_1 = require("./pet-sim.js");
1555 const model_js_1 = require("./model.js");
1556 /** Core emits score events; WebAudio / AVAudioEngine only present their PCM.
1557 * Calling at any display cadence produces the same score when all world ticks
1558 * are supplied. Calling twice for a world tick cannot retrigger a voice. */
1559 class PetScore {
1560 lastWindow = -1;
1561 lastSequence = -1;
1562 lastAddress = false;
1563 checkpoint() { return [this.lastWindow, this.lastSequence, this.lastAddress]; }
1564 restore(value) {
1565 if (!Array.isArray(value) || value.length !== 3
1566 || !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')
1567 throw new Error('Invalid pet score checkpoint.');
1568 [this.lastWindow, this.lastSequence, this.lastAddress] = value;
1569 }
1570 voices(frame) {
1571 const time = frame.timeMs / 1000, window = Math.floor((frame.timeMs + 1e-7) / 400);
1572 const out = [];
1573 const add = (id, frequency, duration, gain, pan = 0, delay = 0, kind = 'tone') => out.push({ id, start: time + delay, duration, frequency, gain, pan, kind });
1574 const t = frame.telemetry, fresh = t !== undefined && t.sequence !== this.lastSequence;
1575 if (fresh) {
1576 this.lastSequence = t.sequence;
1577 for (let c = 0; c < pet_sim_js_1.CHANNELS.length; c++) {
1578 const channel = pet_sim_js_1.CHANNELS[c], n = t.onsets[c];
1579 if (!n || channel.sustained || ['human', 'error'].includes(channel.key))
1580 continue;
1581 add(`onset:${t.sequence}:${c}`, channel.freq, .24, .035 * Math.min(2, Math.sqrt(n)), (c / 12 - .5) * .7);
1582 }
1583 if (t.errors)
1584 add(`tear:${t.sequence}`, pet_sim_js_1.CHANNELS.find(c => c.key === 'error').freq, .22, .05, 0, 0, 'noise');
1585 }
1586 const address = frame.state.channel === 'human' && frame.state.attention > .5;
1587 if (address && (!this.lastAddress || fresh && t.onsets[11] > 0)) {
1588 const frequency = pet_sim_js_1.CHANNELS[11].freq;
1589 for (let i = 0; i < 3; i++)
1590 add(`address:${frame.timeMs}:${i}`, frequency * [1, 1.25, 1.5][i], .23, .032, 0, i * .14);
1591 }
1592 this.lastAddress = address;
1593 if (window !== this.lastWindow) {
1594 this.lastWindow = window;
1595 if (frame.state.observed >= .92) {
1596 const channel = pet_sim_js_1.CHANNELS.find(c => c.key === frame.state.channel);
1597 if (channel.sustained && frame.behaviour !== 'doze') {
1598 const peers = frame.state.channel === 'agent' ? Math.max(1, frame.pod.filter(p => p.present).length) : 1;
1599 for (let i = 0; i < peers; i++)
1600 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);
1601 }
1602 if (frame.behaviour === 'doze' && window % 5 === 0) {
1603 add(`heart:${window}:0`, 49, .3, .035);
1604 add(`heart:${window}:1`, 49, .24, .023, 0, .33);
1605 }
1606 if (frame.needs === 'call' && window % 10 === 0)
1607 add(`call:${window}`, pet_sim_js_1.CHANNELS[11].freq * 1.5, .38, .03);
1608 }
1609 }
1610 return out;
1611 }
1612 }
1613 exports.PetScore = PetScore;
1614 /** Sample-addressed noise: no global RNG, identical samples when chunked/seeking. */
1615 function noise(seed, sample) {
1616 let x = (seed + Math.imul(sample, 0x6D2B79F5)) >>> 0;
1617 x = Math.imul(x ^ x >>> 15, x | 1);
1618 x ^= x + Math.imul(x ^ x >>> 7, x | 61);
1619 return ((x ^ x >>> 14) >>> 0) / 2147483648 - 1;
1620 }
1621 function renderPetPCM(voices, startSample, length, sampleRate = 48_000) {
1622 if (!Number.isInteger(sampleRate) || sampleRate < 8000 || sampleRate > 96000
1623 || !Number.isSafeInteger(startSample) || startSample < 0 || !Number.isInteger(length) || length < 0 || length > sampleRate * 120)
1624 throw new Error('Invalid pet PCM range.');
1625 const left = new Float32Array(length), right = new Float32Array(length);
1626 for (const v of voices) {
1627 if (![v.start, v.duration, v.frequency, v.gain, v.pan].every(Number.isFinite)
1628 || v.start < 0 || v.duration <= 0 || v.duration > 10 || v.frequency <= 0 || v.frequency > sampleRate / 2
1629 || v.gain < 0 || v.gain > 1 || Math.abs(v.pan) > 1 || !['tone', 'noise'].includes(v.kind))
1630 throw new Error('Invalid pet voice.');
1631 const first = Math.max(startSample, Math.ceil(v.start * sampleRate));
1632 const last = Math.min(startSample + length, Math.ceil((v.start + v.duration) * sampleRate));
1633 const pan = (v.pan + 1) * Math.PI / 4, seed = (0xC0FFEE ^ (0, model_js_1.stableHash)(v.id)) >>> 0;
1634 for (let absolute = first; absolute < last; absolute++) {
1635 const age = absolute / sampleRate - v.start;
1636 const envelope = Math.min(1, age / .015, (v.duration - age) / .045);
1637 const sample = v.kind === 'noise' ? noise(seed, absolute) * Math.exp(-age * 14)
1638 : Math.sin(2 * Math.PI * v.frequency * age) * .88 + Math.sin(4 * Math.PI * v.frequency * age) * .12;
1639 const value = sample * Math.max(0, envelope) * v.gain, at = absolute - startSample;
1640 left[at] += value * Math.cos(pan);
1641 right[at] += value * Math.sin(pan);
1642 }
1643 }
1644 // Limiting is a presentation operation and cannot perturb voice scheduling.
1645 for (let i = 0; i < length; i++) {
1646 left[i] = (0, model_js_1.clamp)(left[i], -1, 1);
1647 right[i] = (0, model_js_1.clamp)(right[i], -1, 1);
1648 }
1649 return { left, right };
1650 }
1651
1652 };
1653 factories["pet-engine"]=function(exports,require){
1654 "use strict";
1655 Object.defineProperty(exports, "__esModule", { value: true });
1656 exports.PetEngineTelemetry = void 0;
1657 const pet_sim_js_1 = require("./pet-sim.js");
1658 const codewhale_js_1 = require("./codewhale.js");
1659 const pet_telemetry_js_1 = require("./pet-telemetry.js");
1660 /** Read-only adapter for codewhale_protocol::EventMsg metadata. The foreground
1661 * Engine is the event owner. This replaces no turn loop: it only translates
1662 * lifecycle observations to event-v1 for the same pet bucketer used by imports.
1663 * Text, inputs and results are neither accepted nor retained. */
1664 class PetEngineTelemetry {
1665 events = [];
1666 active = new Map();
1667 waiting;
1668 sequence = 0;
1669 lastTime = 0;
1670 /** Ephemeral receipts. Replay tapes retain measured categories, not tool
1671 * names. Restoring/disconnecting clears these captions. Never mutates world. */
1672 activity(at) {
1673 const fresh = (e) => at >= e.startTime && at - e.endTime <= pet_telemetry_js_1.PET_BIN_MS * 2;
1674 const spans = [...this.active].filter(([, e]) => fresh(e));
1675 const parallel = spans.filter(([key]) => key.startsWith('agent:')).length;
1676 const cue = ([key, e]) => ({
1677 ...(key.startsWith('tool:') ? (0, codewhale_js_1.toolActivity)(e.name) : key.startsWith('thinking:')
1678 ? { kind: 'thinking', label: 'Thinking' } : key.startsWith('agent:')
1679 ? { kind: 'delegating', label: 'Coordinating agents' } : { kind: 'responding', label: 'Writing the response' }),
1680 tool: key.startsWith('tool:') ? e.name.replace(/[^a-zA-Z0-9_.:-]/g, '').slice(0, 96) : null,
1681 sinceMs: e.startTime,
1682 });
1683 const active = spans.filter(([key]) => !key.startsWith('agent:')).slice(-4).reverse().map(cue);
1684 const error = [...this.events].reverse().find(e => e.category === 'error' && fresh(e));
1685 const primary = this.waiting && fresh(this.waiting)
1686 ? { kind: 'waiting', label: 'Waiting for you', tool: null, sinceMs: this.waiting.startTime }
1687 : error ? { kind: 'error', label: 'An operation failed', tool: null, sinceMs: error.startTime }
1688 : active[0] ?? (parallel ? cue(spans.find(([key]) => key.startsWith('agent:')))
1689 : { kind: 'unknown', label: 'Activity unobserved', tool: null, sinceMs: at });
1690 return { ...primary, observed: primary.kind !== 'unknown', parallel, active };
1691 }
1692 add(name, category, at, agentId = 'parent', continuation = false) {
1693 if (this.events.length >= 8192)
1694 throw new Error('Pet Engine observation window is full.');
1695 const e = { schemaVersion: 1, id: `engine:${this.sequence++}`, traceId: 'foreground',
1696 startTime: at, endTime: at, name, category, agentId, status: 'running',
1697 attributes: continuation ? { 'whalesong.continuation': true } : {} };
1698 this.events.push(e);
1699 return e;
1700 }
1701 pulse(key, at) {
1702 const e = this.active.get(key);
1703 if (!e)
1704 return;
1705 // A resumed stream does not assert coverage across its silent interval.
1706 if (at - e.endTime > pet_telemetry_js_1.PET_BIN_MS * 2) {
1707 this.active.set(key, this.add(e.name, e.category, at, e.agentId, true));
1708 }
1709 else
1710 e.endTime = at;
1711 }
1712 /** Transactional batch copy; failed validation cannot accept half a packet. */
1713 clone() {
1714 const next = new PetEngineTelemetry();
1715 const copy = (value) => JSON.parse(JSON.stringify(value));
1716 next.events = copy(this.events);
1717 const spans = new Map(next.events.map(event => [event.id, event]));
1718 // Active and waiting spans must still reference their journal entry so a
1719 // later heartbeat extends the coverage consumed by bucket().
1720 const span = (event) => spans.get(event.id) ?? copy(event);
1721 next.active = new Map(Array.from(this.active, ([key, event]) => [key, span(event)]));
1722 next.waiting = this.waiting ? span(this.waiting) : undefined;
1723 next.sequence = this.sequence;
1724 next.lastTime = this.lastTime;
1725 return next;
1726 }
1727 observe(value, at) {
1728 if (!Number.isFinite(at) || at < this.lastTime || at > pet_sim_js_1.PET_MAX_SECONDS * 1000)
1729 throw new Error('Invalid Engine pet clock.');
1730 if (!value || typeof value !== 'object' || Array.isArray(value))
1731 throw new Error('Invalid Engine pet metadata.');
1732 const e = value;
1733 const allowed = ['event', 'index', 'channel', 'tool_call_id', 'tool_name', 'id', 'worker_status', 'failed'];
1734 if (Object.keys(e).some(k => !allowed.includes(k)) || typeof e.event !== 'string'
1735 || Object.values(e).some(v => typeof v === 'string' && v.length > 4096)
1736 || e.channel !== undefined && !['text', 'reasoning'].includes(e.channel)
1737 || ['tool_call_id', 'tool_name', 'id', 'worker_status'].some(k => e[k] !== undefined && typeof e[k] !== 'string')
1738 || e.failed !== undefined && typeof e.failed !== 'boolean'
1739 || e.index !== undefined && (!Number.isSafeInteger(e.index) || e.index < 0))
1740 throw new Error('Invalid Engine pet metadata fields.');
1741 this.lastTime = at;
1742 this.events = this.events.filter(span => span.endTime >= at - 12_800);
1743 const id = (field) => { const s = e[field]; if (typeof s !== 'string' || !s)
1744 throw new Error(`Missing Engine ${field}.`); return s; };
1745 const index = () => { if (!Number.isSafeInteger(e.index))
1746 throw new Error('Missing Engine index.'); return String(e.index); };
1747 const start = (key, name, category, agentId) => {
1748 if (this.active.size >= 256 && !this.active.has(key))
1749 throw new Error('Too many active Engine pet spans.');
1750 this.active.set(key, this.add(name, category, at, agentId));
1751 };
1752 const finish = (key) => { this.pulse(key, at); this.active.delete(key); };
1753 switch (e.event) {
1754 case 'turn_started':
1755 this.active.clear();
1756 this.waiting = undefined;
1757 break;
1758 case 'message_started':
1759 start(`message:${index()}`, 'assistant_message', 'communication');
1760 this.waiting = undefined;
1761 break;
1762 case 'thinking_started':
1763 start(`thinking:${index()}`, 'thinking', 'reasoning');
1764 this.waiting = undefined;
1765 break;
1766 case 'response_delta': {
1767 const reasoning = e.channel === 'reasoning';
1768 const key = `${reasoning ? 'thinking' : 'message'}:${index()}`;
1769 if (!this.active.has(key))
1770 start(key, reasoning ? 'thinking' : 'assistant_message', reasoning ? 'reasoning' : 'communication');
1771 else
1772 this.pulse(key, at);
1773 this.waiting = undefined;
1774 break;
1775 }
1776 case 'message_complete':
1777 finish(`message:${index()}`);
1778 break;
1779 case 'thinking_complete':
1780 finish(`thinking:${index()}`);
1781 break;
1782 case 'tool_call_started':
1783 start(`tool:${id('tool_call_id')}`, id('tool_name'), (0, codewhale_js_1.toolCategory)(id('tool_name')));
1784 this.waiting = undefined;
1785 break;
1786 case 'tool_call_heartbeat':
1787 for (const key of this.active.keys())
1788 if (key.startsWith('tool:'))
1789 this.pulse(key, at);
1790 break;
1791 case 'tool_call_complete':
1792 finish(`tool:${id('tool_call_id')}`);
1793 this.waiting = undefined;
1794 break;
1795 case 'agent_spawned':
1796 start(`agent:${id('id')}`, 'agent', 'agent', id('id'));
1797 break;
1798 case 'agent_progress': {
1799 const key = `agent:${id('id')}`;
1800 if (['completed', 'failed', 'cancelled', 'interrupted', 'budget_exhausted'].includes(e.worker_status)) {
1801 finish(key);
1802 break;
1803 }
1804 if (!this.active.has(key))
1805 start(key, 'agent', 'agent', id('id'));
1806 else
1807 this.pulse(key, at);
1808 break;
1809 }
1810 case 'agent_complete':
1811 finish(`agent:${id('id')}`);
1812 break;
1813 case 'approval_required':
1814 case 'user_input_required':
1815 this.waiting = this.add('human', 'human', at);
1816 this.waiting.status = 'pending';
1817 break;
1818 case 'turn_complete':
1819 this.active.clear();
1820 this.waiting = undefined;
1821 break;
1822 case 'error': break;
1823 default: throw new Error('Unsupported Engine pet event.');
1824 }
1825 // Receipt time is the error onset; never rewrite the operation's old start.
1826 if (e.event === 'error' || e.failed === true)
1827 this.add('error', 'error', at).status = 'error';
1828 }
1829 /** Waiting coverage comes from the existing typed shell's current request.
1830 * It can extend a witnessed request, never invent one on a mid-turn attach. */
1831 confirmWaiting(at, waiting) {
1832 if (!waiting) {
1833 this.waiting = undefined;
1834 return;
1835 }
1836 if (this.waiting && at >= this.waiting.endTime)
1837 this.waiting.endTime = at;
1838 }
1839 bucket(sequence) {
1840 const end = (sequence + 1) * pet_telemetry_js_1.PET_BIN_MS;
1841 const input = this.events.filter(e => e.startTime < end && e.endTime >= end - 12_400)
1842 .map(e => ({ ...e, endTime: Math.min(e.endTime, end) }));
1843 return (0, pet_telemetry_js_1.compilePetTelemetry)(input, end, sequence)[0];
1844 }
1845 }
1846 exports.PetEngineTelemetry = PetEngineTelemetry;
1847
1848 };
1849 factories["codewhale"]=function(exports,require){
1850 "use strict";
1851 Object.defineProperty(exports, "__esModule", { value: true });
1852 exports.CodewhaleRuntimeTrace = void 0;
1853 exports.isCodewhaleSession = isCodewhaleSession;
1854 exports.isCodewhaleRuntimeRecord = isCodewhaleRuntimeRecord;
1855 exports.isCodewhaleRuntimeDocument = isCodewhaleRuntimeDocument;
1856 exports.toolActivity = toolActivity;
1857 exports.toolCategory = toolCategory;
1858 exports.fromCodewhaleSession = fromCodewhaleSession;
1859 exports.fromCodewhaleRuntime = fromCodewhaleRuntime;
1860 exports.observeRuntimeRequests = observeRuntimeRequests;
1861 /** Read-only Codewhale session/runtime adapter. Does not record, mutate, or own receipts. */
1862 const model_js_1 = require("./model.js");
1863 const PAYLOAD_LIMIT = 2000;
1864 const COLLAPSED_SPAN_MS = 1000;
1865 const ENVELOPE_MIN_MS = 60_000;
1866 const obj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) ? v : {};
1867 const str = (v) => typeof v === 'string' && v.length ? v : undefined;
1868 const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
1869 function isCodewhaleSession(value) {
1870 const root = obj(value);
1871 const metadata = obj(root.metadata);
1872 if (!str(metadata.id))
1873 return false;
1874 if (root.format === 'whalesong.evidence/v1' || Array.isArray(root.resourceSpans) || root.schemaVersion === 1)
1875 return false;
1876 const journal = obj(root.journal);
1877 return Array.isArray(root.messages) || Array.isArray(journal.entries);
1878 }
1879 function isCodewhaleRuntimeRecord(value) {
1880 const rec = obj(value);
1881 return Number.isSafeInteger(rec.seq) && rec.seq >= 0 && typeof rec.event === 'string' && !!rec.event
1882 && typeof rec.thread_id === 'string' && !!rec.thread_id && rec.timestamp != null;
1883 }
1884 function isCodewhaleRuntimeDocument(value) {
1885 if (!Array.isArray(value) || !value.length)
1886 return false;
1887 const n = Math.min(value.length, 8);
1888 let hits = 0;
1889 for (let i = 0; i < n; i++)
1890 if (isCodewhaleRuntimeRecord(value[i]))
1891 hits++;
1892 return hits === n;
1893 }
1894 function clip(value) {
1895 if (value == null)
1896 return value;
1897 const text = typeof value === 'string' ? value : JSON.stringify(value);
1898 if (text.length <= PAYLOAD_LIMIT)
1899 return typeof value === 'string' ? value : JSON.parse(text);
1900 return `${text.slice(0, PAYLOAD_LIMIT)}…[truncated ${text.length - PAYLOAD_LIMIT} source bytes]`;
1901 }
1902 function parseTime(value) {
1903 if (typeof value === 'number' && Number.isFinite(value))
1904 return value;
1905 if (typeof value !== 'string' || !value)
1906 return undefined;
1907 const ms = Date.parse(value);
1908 return Number.isFinite(ms) ? ms : undefined;
1909 }
1910 function statusOf(value, isError) {
1911 if (isError === true)
1912 return 'error';
1913 if (isError === false)
1914 return 'success';
1915 const s = String(value ?? '').toLowerCase();
1916 if (s === 'completed' || s === 'success' || s === 'ok')
1917 return 'success';
1918 if (s === 'failed' || s === 'error' || s === 'errored')
1919 return 'error';
1920 if (s === 'canceled' || s === 'cancelled' || s === 'interrupted')
1921 return 'error';
1922 if (s === 'in_progress' || s === 'running')
1923 return 'running';
1924 if (s === 'pending')
1925 return 'pending';
1926 return 'unknown';
1927 }
1928 function classify(name) {
1929 const n = name.toLowerCase();
1930 if (/exception|^error\b/.test(n))
1931 return 'error';
1932 if (/spawn|fork|subagent|^agent$/.test(n))
1933 return 'agent';
1934 if (/message\.send|handoff|agent\.message|assistant_message/.test(n))
1935 return 'communication';
1936 if (/retrieve|retrieval|context|embedding|vector|memory|rag/.test(n))
1937 return 'memory';
1938 if (/browser|navigate|screenshot|click|playwright/.test(n))
1939 return 'browser';
1940 if (/read_file|write_file|list_dir|^read$|^write$|^edit$|glob|grep|file\.|filesystem/.test(n))
1941 return 'filesystem';
1942 if (/bash|exec|shell|run_test|cargo|pytest|compile/.test(n))
1943 return 'code';
1944 if (/reason|thinking|completion|generate|chat|llm/.test(n))
1945 return 'reasoning';
1946 if (/http|request|api|fetch|network|mcp_/.test(n))
1947 return 'network';
1948 if (/user_message|human|approval/.test(n))
1949 return 'human';
1950 if (/orchestrat|workflow|phase|join|session|thread|turn|todo|plan|operate_contract|status/.test(n))
1951 return 'orchestration';
1952 if (/tool/.test(n))
1953 return 'tool';
1954 return model_js_1.CATEGORIES.includes(n) ? n : 'other';
1955 }
1956 /** Presentation vocabulary beside the canonical category classifier. Only the
1957 * witnessed tool name is used; command contents are never inferred. */
1958 function toolActivity(name) {
1959 const n = name.toLowerCase().replace(/-/g, '_');
1960 if (/search|grep|glob|find_file/.test(n))
1961 return { kind: 'searching', label: 'Searching' };
1962 if (/read_file|list_dir|read_text|open_file/.test(n))
1963 return { kind: 'reading', label: 'Reading files' };
1964 if (/apply_patch|write_file|edit_file|replace_text/.test(n))
1965 return { kind: 'editing', label: 'Editing files' };
1966 if (/run_test|pytest|test_suite/.test(n))
1967 return { kind: 'testing', label: 'Running tests' };
1968 const category = toolCategory(name);
1969 return { browser: { kind: 'browsing', label: 'Using the browser' },
1970 filesystem: { kind: 'files', label: 'Working with files' },
1971 code: { kind: 'executing', label: 'Running a command' },
1972 network: { kind: 'network', label: 'Calling a service' },
1973 agent: { kind: 'delegating', label: 'Coordinating agents' },
1974 memory: { kind: 'memory', label: 'Retrieving context' },
1975 reasoning: { kind: 'thinking', label: 'Thinking' },
1976 communication: { kind: 'communicating', label: 'Communicating' },
1977 }[category] ?? { kind: 'tool', label: 'Using a tool' };
1978 }
1979 function toolCategory(name) {
1980 const category = classify(name);
1981 return category === 'other' ? 'tool' : category;
1982 }
1983 function pointer(source, ids) {
1984 return { format: source, ...ids };
1985 }
1986 function titleOfSession(metadata, filename) {
1987 const title = str(metadata.title)?.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
1988 if (title && !title.startsWith('codewhale:runtime_event'))
1989 return title.slice(0, 120);
1990 return `Codewhale session · ${(str(metadata.id) ?? filename).slice(0, 8)}`;
1991 }
1992 function activeJournalEntries(journal) {
1993 const entries = Array.isArray(journal.entries) ? journal.entries.map(obj) : [];
1994 const leaf = str(journal.leaf_id);
1995 if (!leaf || !entries.length)
1996 return { entries, warnings: [] };
1997 const byId = new Map(entries.filter(e => str(e.id)).map(e => [e.id, e]));
1998 const chain = [];
1999 const seen = new Set();
2000 let id = leaf;
2001 while (id && !seen.has(id)) {
2002 seen.add(id);
2003 const entry = byId.get(id);
2004 if (!entry)
2005 break;
2006 chain.push(entry);
2007 id = str(entry.parent_id);
2008 }
2009 if (!chain.length)
2010 return { entries, warnings: ['Journal leaf_id did not resolve; using append order instead of the active branch.'] };
2011 if (chain.length < entries.length) {
2012 return {
2013 entries: chain.reverse(),
2014 warnings: [`Active journal branch has ${chain.length} of ${entries.length} entries. Forked history was not invented into the timeline.`],
2015 };
2016 }
2017 return { entries: chain.reverse(), warnings: [] };
2018 }
2019 function collapsedTimestamps(entries, created, updated) {
2020 const times = entries.map(e => parseTime(e.created_at)).filter((n) => n !== undefined);
2021 if (times.length < 2)
2022 return false;
2023 const span = Math.max(...times) - Math.min(...times);
2024 const envelope = created !== undefined && updated !== undefined ? updated - created : 0;
2025 return envelope >= ENVELOPE_MIN_MS && span < COLLAPSED_SPAN_MS;
2026 }
2027 function pushEvent(events, event) {
2028 events.push(event);
2029 }
2030 function fromCodewhaleSession(document, filename = 'Codewhale session', maxEvents = 250_000) {
2031 const root = obj(document);
2032 const metadata = obj(root.metadata);
2033 const sessionId = str(metadata.id) ?? filename;
2034 const journal = obj(root.journal);
2035 const { entries, warnings } = activeJournalEntries(journal);
2036 const sourceEntries = entries.length ? entries : (Array.isArray(root.messages) ? root.messages.map((message, i) => ({ id: `${sessionId}/message/${i}`, kind: 'message', message })) : []);
2037 if (!sourceEntries.length)
2038 throw new Error('Codewhale session contains no journal entries or messages.');
2039 const created = parseTime(metadata.created_at);
2040 const updated = parseTime(metadata.updated_at);
2041 const orderOnly = collapsedTimestamps(sourceEntries, created, updated);
2042 if (orderOnly) {
2043 warnings.push('Journal created_at values are collapsed to last-save time, not execution time. The time axis is journal order (1 ms per emitted event), not wall-clock duration. Gap, burst, and cycle-period findings are not execution-time claims.');
2044 }
2045 else {
2046 const times = sourceEntries.map(e => parseTime(e.created_at)).filter((n) => n !== undefined);
2047 if (!times.length)
2048 warnings.push('Journal entries have no usable timestamps. The time axis is journal order.');
2049 }
2050 const events = [];
2051 const pending = new Map();
2052 let seq = 0;
2053 const originWall = orderOnly ? undefined : sourceEntries.map(e => parseTime(e.created_at)).find((n) => n !== undefined);
2054 const agentId = 'parent';
2055 const model = str(metadata.model);
2056 const provider = str(metadata.model_provider);
2057 const when = (entry, fallback) => {
2058 if (orderOnly || originWall === undefined)
2059 return { start: fallback, open: false };
2060 const t = parseTime(entry.created_at);
2061 if (t === undefined)
2062 return { start: fallback, open: true };
2063 return { start: t - originWall, open: false };
2064 };
2065 for (const entry of sourceEntries) {
2066 if (events.length >= maxEvents)
2067 throw new Error(`Import exceeds the ${maxEvents.toLocaleString()} event limit.`);
2068 const entryId = str(entry.id) ?? `${sessionId}/entry/${seq}`;
2069 const message = obj(entry.message ?? (entry.kind === 'message' ? entry : {}));
2070 const role = str(message.role) ?? (str(entry.kind) === 'user' ? 'user' : str(entry.kind) === 'assistant' ? 'assistant' : undefined);
2071 const blocks = Array.isArray(message.content) ? message.content.map(obj) : [];
2072 if (!blocks.length) {
2073 const text = str(entry.text) ?? str(message.text);
2074 if (text)
2075 blocks.push({ type: role === 'user' ? 'text' : 'text', text });
2076 }
2077 if (!blocks.length)
2078 continue;
2079 const parentEventId = events.length ? events[events.length - 1].id : undefined;
2080 for (const block of blocks) {
2081 const t = when(entry, seq);
2082 const idBase = `${entryId}/${seq}`;
2083 const type = str(block.type) ?? 'text';
2084 const raw = pointer('codewhale.session/v1', { sessionId, entryId, seq, blockType: type, toolUseId: block.id ?? block.tool_use_id });
2085 if (type === 'tool_use' || type === 'server_tool_use') {
2086 const tool = str(block.name) ?? 'tool';
2087 const callId = str(block.id) ?? idBase;
2088 const started = tool === 'agent' && obj(block.input).action === 'start';
2089 const event = {
2090 schemaVersion: 1, id: callId, traceId: sessionId, parentId: parentEventId,
2091 startTime: t.start, endTime: t.start, openEnded: true,
2092 agentId, name: started ? 'agent.spawn' : tool, tool, category: toolCategory(tool),
2093 subtype: started ? 'fork' : undefined, model, provider,
2094 status: 'running', attributes: { 'codewhale.entry_id': entryId, 'codewhale.seq': seq, 'tool.name': tool },
2095 payload: { arguments: clip(block.input) }, raw,
2096 };
2097 pending.set(callId, events.length);
2098 pushEvent(events, event);
2099 }
2100 else if (type === 'tool_result') {
2101 const callId = str(block.tool_use_id);
2102 const isError = block.is_error === true;
2103 const target = callId !== undefined ? pending.get(callId) : undefined;
2104 if (target !== undefined) {
2105 const prior = events[target];
2106 prior.endTime = t.start;
2107 prior.openEnded = false;
2108 prior.status = statusOf('completed', isError);
2109 prior.payload = { ...(obj(prior.payload)), result: clip(block.content) };
2110 prior.attributes = { ...prior.attributes, 'codewhale.result_entry_id': entryId };
2111 pending.delete(callId);
2112 }
2113 else {
2114 pushEvent(events, {
2115 schemaVersion: 1, id: idBase, traceId: sessionId, parentId: callId ?? parentEventId,
2116 startTime: t.start, endTime: t.start, agentId,
2117 name: 'tool_result', category: 'tool', model, provider,
2118 status: statusOf(undefined, isError),
2119 attributes: { 'codewhale.entry_id': entryId, 'codewhale.seq': seq, tool_use_id: callId },
2120 payload: { result: clip(block.content) }, raw,
2121 });
2122 }
2123 }
2124 else if (type === 'thinking') {
2125 pushEvent(events, {
2126 schemaVersion: 1, id: idBase, traceId: sessionId, parentId: parentEventId,
2127 startTime: t.start, endTime: t.start, agentId, name: 'thinking', category: 'reasoning',
2128 model, provider, status: 'success',
2129 attributes: { 'codewhale.entry_id': entryId, 'codewhale.seq': seq },
2130 payload: { thinking: clip(block.thinking ?? block.text) }, raw,
2131 });
2132 }
2133 else {
2134 const text = str(block.text) ?? '';
2135 const operate = text.includes('codewhale:runtime_event');
2136 const user = role === 'user' || role === 'User';
2137 pushEvent(events, {
2138 schemaVersion: 1, id: idBase, traceId: sessionId, parentId: parentEventId,
2139 startTime: t.start, endTime: t.start, agentId,
2140 name: operate ? 'operate_contract' : user ? 'user_message' : 'assistant_message',
2141 category: operate ? 'orchestration' : user ? 'human' : 'communication',
2142 model, provider, status: 'success',
2143 attributes: { 'codewhale.entry_id': entryId, 'codewhale.seq': seq, role: role ?? 'unknown' },
2144 payload: { text: clip(text) }, raw,
2145 });
2146 }
2147 seq += 1;
2148 }
2149 }
2150 if (!events.length)
2151 throw new Error('Codewhale session produced no inspectable events.');
2152 for (const event of events) {
2153 if (event.openEnded && event.tool)
2154 warnings.push(`Tool ${event.id} has no matching tool_result in this snapshot; duration remains unknown.`);
2155 }
2156 const base = events.reduce((m, e) => Math.min(m, e.startTime), events[0].startTime);
2157 for (const event of events) {
2158 event.startTime -= base;
2159 event.endTime -= base;
2160 }
2161 const cost = obj(metadata.cost);
2162 const sessionCost = num(cost.session_cost_usd);
2163 const duration = Math.max(1, events.reduce((m, e) => Math.max(m, e.endTime, e.startTime), 0));
2164 const uniqueWarnings = [...new Set(warnings)];
2165 return {
2166 id: sessionId,
2167 name: titleOfSession(metadata, filename),
2168 events,
2169 duration,
2170 originTime: orderOnly ? 'journal-order' : (str(metadata.created_at) ?? `${base} ms`),
2171 source: 'codewhale',
2172 privacy: 'redact',
2173 warnings: uniqueWarnings,
2174 metadata: {
2175 sourceFormat: 'codewhale.session/v1',
2176 timeBasis: orderOnly || originWall === undefined ? 'journal-order' : 'wall-clock',
2177 sourceFilename: filename,
2178 sessionId,
2179 model,
2180 provider,
2181 workspace: metadata.workspace,
2182 mode: metadata.mode,
2183 envelopeCreatedAt: metadata.created_at,
2184 envelopeUpdatedAt: metadata.updated_at,
2185 cumulativeTurnSecs: metadata.cumulative_turn_secs,
2186 messageCount: metadata.message_count,
2187 journalEntries: sourceEntries.length,
2188 totalTokens: metadata.total_tokens,
2189 sessionCostUsd: sessionCost,
2190 pricedTurns: cost.priced_turns,
2191 unpricedTurns: cost.unpriced_turns,
2192 runtimeStore: metadata.runtime_store,
2193 timeUnit: 'ms',
2194 },
2195 };
2196 }
2197 function itemToolName(item, payload) {
2198 const named = str(payload.tool) ?? str(item.tool) ?? str(item.name);
2199 if (named)
2200 return named;
2201 if (str(item.kind) !== 'tool_call')
2202 return undefined;
2203 const head = str(item.summary)?.split(':')[0]?.trim();
2204 if (head && head.length < 80 && !/\s/.test(head))
2205 return head;
2206 return undefined;
2207 }
2208 function itemCategory(kind, tool) {
2209 if (kind === 'user_message')
2210 return 'human';
2211 if (kind === 'agent_reasoning')
2212 return 'reasoning';
2213 if (kind === 'agent_message')
2214 return 'communication';
2215 if (kind === 'status')
2216 return 'orchestration';
2217 if (kind === 'tool_call' && tool)
2218 return toolCategory(tool);
2219 if (kind === 'tool_call')
2220 return 'tool';
2221 return classify(kind);
2222 }
2223 /** Incremental form of the existing Runtime importer. File imports and live
2224 * recording share this exact lifecycle parser; only a live driver retires old
2225 * completed events after it has recorded their projection. */
2226 class CodewhaleRuntimeTrace {
2227 filename;
2228 maxEvents;
2229 project;
2230 maxBytes;
2231 events = [];
2232 open = new Map();
2233 requests = new Map();
2234 sizes = new Map();
2235 bytes = 0;
2236 recordCount = 0;
2237 skippedDeltas = 0;
2238 origin;
2239 model;
2240 threadId;
2241 threadName;
2242 constructor(filename = 'Codewhale runtime', maxEvents = 250_000, project = event => event, maxBytes = Infinity) {
2243 this.filename = filename;
2244 this.maxEvents = maxEvents;
2245 this.project = project;
2246 this.maxBytes = maxBytes;
2247 }
2248 get retainedEvents() { return this.events.length; }
2249 get retainedBytes() { return this.bytes; }
2250 measure(event, proposed = event) {
2251 const safe = this.project(proposed);
2252 if (this.maxBytes !== Infinity) {
2253 const size = new TextEncoder().encode(JSON.stringify(safe)).length;
2254 const total = this.bytes - (this.sizes.get(event) ?? 0) + size;
2255 if (total > this.maxBytes)
2256 throw new Error('Runtime observation exceeds its retained input limit.');
2257 this.bytes = total;
2258 this.sizes.set(event, size);
2259 }
2260 for (const key of Object.keys(event))
2261 if (!Object.hasOwn(safe, key))
2262 delete event[key];
2263 Object.assign(event, safe);
2264 }
2265 push(event) {
2266 if (this.events.length >= this.maxEvents)
2267 throw new Error(`Import exceeds the ${this.maxEvents.toLocaleString()} event limit.`);
2268 this.measure(event);
2269 pushEvent(this.events, event);
2270 }
2271 /** Keep unfinished lifetimes plus the recent window needed by the bucketer's
2272 * 12-second recurrence measure. A completion may still arrive for any open item. */
2273 prune(beforeWall) {
2274 if (!Number.isFinite(beforeWall))
2275 throw new Error('Invalid Runtime retention horizon.');
2276 if (this.origin === undefined)
2277 return;
2278 const cutoff = beforeWall - this.origin;
2279 let keep = 0;
2280 for (const event of this.events) {
2281 if (event.openEnded || Math.max(event.endTime, (0, model_js_1.errorOnsetOf)(event)) >= cutoff)
2282 this.events[keep++] = event;
2283 else {
2284 this.bytes -= this.sizes.get(event) ?? 0;
2285 this.sizes.delete(event);
2286 if (this.open.get(event.id) === event)
2287 this.open.delete(event.id);
2288 }
2289 }
2290 this.events.length = keep;
2291 }
2292 append(records) {
2293 if (!records.length)
2294 return;
2295 const { events, open, requests } = this;
2296 const threadId = this.threadId ?? str(obj(records[0]).thread_id) ?? this.filename;
2297 this.threadId = threadId;
2298 let { origin, model, skippedDeltas } = this;
2299 let threadName = this.threadName ?? threadId;
2300 const stamp = (rec) => {
2301 const t = parseTime(rec.timestamp);
2302 if (t === undefined)
2303 throw new Error(`Runtime event seq ${rec.seq} is missing a usable timestamp.`);
2304 if (origin === undefined)
2305 origin = t;
2306 return t - origin;
2307 };
2308 for (const raw of records) {
2309 if (!isCodewhaleRuntimeRecord(raw))
2310 throw new Error('Runtime import cancelled: a line is not a Codewhale runtime event record. No rows were skipped.');
2311 this.recordCount++;
2312 const rec = obj(raw);
2313 if (rec.thread_id !== threadId)
2314 throw new Error('Runtime import contains multiple threads. Export one thread before importing.');
2315 const eventName = rec.event;
2316 if (eventName === 'item.delta') {
2317 skippedDeltas++;
2318 continue;
2319 }
2320 const payload = obj(rec.payload);
2321 const item = obj(payload.item);
2322 const turn = obj(payload.turn);
2323 const thread = obj(payload.thread);
2324 const relative = stamp(rec);
2325 const turnId = str(rec.turn_id) ?? str(payload.turn_id);
2326 const itemId = str(rec.item_id) ?? str(item.id);
2327 const agentId = 'parent';
2328 if (str(thread.model))
2329 model = str(thread.model);
2330 if (str(turn.model))
2331 model = str(turn.model) ?? model;
2332 if (eventName === 'thread.started') {
2333 model = str(thread.model) ?? model;
2334 threadName = str(thread.id) ?? threadId;
2335 this.push({
2336 schemaVersion: 1, id: `thread:${threadId}`, traceId: threadId,
2337 startTime: relative, endTime: relative, openEnded: true,
2338 agentId, name: 'thread', category: 'orchestration', model, status: 'running',
2339 attributes: { 'codewhale.seq': rec.seq, 'whalesong.container': true }, raw: rec,
2340 });
2341 continue;
2342 }
2343 if (eventName === 'turn.started' || eventName === 'turn.completed') {
2344 const id = `turn:${turnId ?? rec.seq}`;
2345 if (eventName === 'turn.completed')
2346 for (const [key, request] of requests) {
2347 if (request.parentId !== id)
2348 continue;
2349 this.measure(request, { ...request, endTime: Math.max(request.startTime, relative), openEnded: false, status: 'unknown' });
2350 requests.delete(key);
2351 }
2352 const startWall = parseTime(turn.started_at) ?? parseTime(turn.created_at);
2353 const endWall = parseTime(turn.ended_at);
2354 const start = startWall !== undefined && origin !== undefined ? startWall - origin : relative;
2355 const end = eventName === 'turn.completed' && endWall !== undefined && origin !== undefined ? endWall - origin : relative;
2356 const usage = obj(turn.usage);
2357 const existing = events.findIndex(e => e.id === id);
2358 const next = {
2359 schemaVersion: 1, id, traceId: threadId, parentId: `thread:${threadId}`,
2360 startTime: start, endTime: Math.max(start, end), openEnded: eventName !== 'turn.completed',
2361 agentId, name: 'turn', category: 'orchestration', model,
2362 inputTokens: num(usage.input_tokens), outputTokens: num(usage.output_tokens),
2363 status: statusOf(turn.status ?? payload.status), latency: num(turn.duration_ms),
2364 attributes: { 'codewhale.seq': rec.seq, 'codewhale.turn_id': turnId, 'whalesong.container': true,
2365 ...(statusOf(turn.status ?? payload.status) === 'error' ? { 'whalesong.error_onset_ms': relative } : {}) },
2366 payload: { input_summary: clip(turn.input_summary) }, raw: rec,
2367 };
2368 if (existing >= 0) {
2369 const prior = events[existing];
2370 this.measure(prior, { ...next, startTime: prior.startTime });
2371 }
2372 else
2373 this.push(next);
2374 continue;
2375 }
2376 if (eventName === 'turn.lifecycle')
2377 continue;
2378 if (['approval.required', 'approval.decided', 'approval.timeout', 'user_input.required', 'user_input.answered', 'user_input.canceled'].includes(eventName)) {
2379 const kind = eventName.startsWith('approval.') ? 'approval' : 'user_input';
2380 const requestId = str(payload[kind === 'approval' ? 'approval_id' : 'input_id']) ?? str(payload.id);
2381 if (!requestId)
2382 throw new Error(`Runtime ${eventName} is missing its request identity.`);
2383 const key = JSON.stringify([turnId ?? '', kind, requestId]);
2384 const prior = requests.get(key), required = eventName.endsWith('.required');
2385 if (required && prior)
2386 continue;
2387 if (!required && prior) {
2388 const next = { ...prior, attributes: { ...prior.attributes },
2389 endTime: Math.max(prior.startTime, relative), openEnded: false,
2390 status: eventName === 'approval.decided' || eventName === 'user_input.answered' ? 'success' : 'unknown' };
2391 if (payload.auto === true) {
2392 // Automatic consent has a receipt, but never asked the human to wait.
2393 next.category = 'orchestration';
2394 delete next.attributes['whalesong.waiting'];
2395 next.attributes['whalesong.container'] = true;
2396 }
2397 this.measure(prior, next);
2398 requests.delete(key);
2399 continue;
2400 }
2401 const automatic = payload.auto === true;
2402 const event = {
2403 schemaVersion: 1, id: `request:${key}:${rec.seq}`, traceId: threadId,
2404 parentId: turnId ? `turn:${turnId}` : undefined, startTime: relative, endTime: relative,
2405 openEnded: required, agentId, name: eventName, category: automatic ? 'orchestration' : 'human',
2406 status: required ? 'pending' : 'success', model,
2407 attributes: { 'codewhale.seq': rec.seq, 'whalesong.waiting': required, 'whalesong.container': automatic }, raw: rec,
2408 };
2409 this.push(event);
2410 if (required)
2411 requests.set(key, event);
2412 continue;
2413 }
2414 if (eventName === 'tool_call.requested' || eventName === 'tool_call.canceled') {
2415 const callId = str(payload.call_id) ?? `call:${rec.seq}`;
2416 const tool = str(payload.tool);
2417 const canceled = eventName === 'tool_call.canceled';
2418 this.push({
2419 schemaVersion: 1, id: `${eventName}:${callId}`, traceId: threadId, parentId: turnId ? `turn:${turnId}` : undefined,
2420 startTime: relative, endTime: relative, agentId, name: tool ?? eventName, tool,
2421 category: tool ? toolCategory(tool) : 'tool', model, status: canceled ? 'error' : 'pending',
2422 attributes: { 'codewhale.seq': rec.seq, 'codewhale.turn_id': turnId, 'codewhale.call_id': callId, reason: payload.reason },
2423 payload: { arguments: clip(payload.arguments) }, raw: rec,
2424 });
2425 continue;
2426 }
2427 if (eventName === 'item.started' || eventName === 'item.completed') {
2428 const kind = str(item.kind) ?? 'item';
2429 const tool = itemToolName(item, payload);
2430 const id = itemId ?? `item:${rec.seq}`;
2431 const startWall = parseTime(item.started_at);
2432 const endWall = parseTime(item.ended_at);
2433 const start = startWall !== undefined && origin !== undefined ? startWall - origin : relative;
2434 const end = eventName === 'item.completed' && endWall !== undefined && origin !== undefined ? endWall - origin : relative;
2435 const openEnded = eventName === 'item.started' && endWall === undefined;
2436 const existing = open.get(id);
2437 if (existing && eventName === 'item.completed') {
2438 const prior = existing;
2439 const status = statusOf(item.status);
2440 this.measure(prior, { ...prior, endTime: Math.max(prior.startTime, end), openEnded: false, status,
2441 attributes: { ...prior.attributes, ...(status === 'error' ? { 'whalesong.error_onset_ms': relative } : {}) },
2442 payload: { summary: clip(item.summary), detail: clip(item.detail) } });
2443 open.delete(id);
2444 continue;
2445 }
2446 const event = {
2447 schemaVersion: 1, id, traceId: threadId, parentId: turnId ? `turn:${turnId}` : undefined,
2448 startTime: start, endTime: Math.max(start, end), openEnded,
2449 agentId, name: tool ?? kind, tool, category: itemCategory(kind, tool), model,
2450 status: statusOf(item.status ?? (eventName === 'item.started' ? 'running' : undefined)),
2451 attributes: { 'codewhale.seq': rec.seq, 'codewhale.turn_id': turnId, 'codewhale.item_kind': kind,
2452 ...(statusOf(item.status) === 'error' ? { 'whalesong.error_onset_ms': relative } : {}) },
2453 payload: { summary: clip(item.summary), detail: clip(item.detail) }, raw: rec,
2454 };
2455 this.push(event);
2456 if (eventName === 'item.started')
2457 open.set(id, event);
2458 continue;
2459 }
2460 this.push({
2461 schemaVersion: 1, id: `${eventName}:${rec.seq}`, traceId: threadId, parentId: turnId ? `turn:${turnId}` : undefined,
2462 startTime: relative, endTime: relative, agentId, name: eventName, category: classify(eventName),
2463 model, status: 'unknown', attributes: { 'codewhale.seq': rec.seq }, raw: rec,
2464 });
2465 }
2466 this.origin = origin;
2467 this.model = model;
2468 this.threadName = threadName;
2469 this.skippedDeltas = skippedDeltas;
2470 }
2471 snapshot() {
2472 const { events, open, requests, origin, model, skippedDeltas, filename } = this;
2473 const threadId = this.threadId ?? filename, threadName = this.threadName ?? threadId;
2474 const warnings = [];
2475 if (skippedDeltas)
2476 warnings.push(`Dropped ${skippedDeltas.toLocaleString()} item.delta records; they are token stream fragments, not spans. Item start/end remain the source of duration.`);
2477 for (const [id] of open)
2478 warnings.push(`Item ${id} started and never completed in this file; duration remains unknown.`);
2479 for (const request of requests.values())
2480 warnings.push(`Request ${request.id} has no terminal receipt; its duration remains unknown in this file.`);
2481 if (!events.length)
2482 throw new Error('Codewhale runtime file contained only stream deltas or unreadable records.');
2483 const base = events.reduce((m, e) => Math.min(m, e.startTime), events[0].startTime);
2484 const normalized = events.map(event => ({ ...event, startTime: event.startTime - base, endTime: event.endTime - base,
2485 attributes: { ...event.attributes, ...(event.attributes['whalesong.error_onset_ms'] !== undefined
2486 ? { 'whalesong.error_onset_ms': (0, model_js_1.errorOnsetOf)(event) - base } : {}) } }));
2487 return {
2488 id: threadId,
2489 name: `Codewhale runtime · ${threadName}`,
2490 events: normalized,
2491 duration: Math.max(1, normalized.reduce((m, e) => Math.max(m, e.endTime, e.startTime, e.status === 'error' ? (0, model_js_1.errorOnsetOf)(e) : 0), 0)),
2492 originTime: origin !== undefined ? new Date(origin + base).toISOString() : '0 ms',
2493 source: 'codewhale',
2494 privacy: 'redact',
2495 warnings: [...new Set(warnings)],
2496 metadata: {
2497 sourceFormat: 'codewhale.runtime-events/v2',
2498 timeBasis: 'wall-clock',
2499 sourceFilename: filename,
2500 threadId,
2501 model,
2502 skippedDeltas,
2503 recordCount: this.recordCount,
2504 timeUnit: 'ms',
2505 },
2506 };
2507 }
2508 }
2509 exports.CodewhaleRuntimeTrace = CodewhaleRuntimeTrace;
2510 function fromCodewhaleRuntime(records, filename = 'Codewhale runtime', maxEvents = 250_000) {
2511 if (!records.length)
2512 throw new Error('Codewhale runtime event file is empty.');
2513 const trace = new CodewhaleRuntimeTrace(filename, maxEvents);
2514 trace.append(records);
2515 return trace.snapshot();
2516 }
2517 /** The journal owns request state until a matching terminal receipt. A live
2518 * driver may confirm that state only while its cursor-checked stream is healthy.
2519 * Ordinary open tool spans remain unknown-duration; no execution is inferred. */
2520 function observeRuntimeRequests(trace, observedThrough) {
2521 const origin = Date.parse(trace.originTime ?? '');
2522 if (trace.metadata.sourceFormat !== 'codewhale.runtime-events/v2' || !Number.isFinite(origin)
2523 || !Number.isFinite(observedThrough))
2524 throw new Error('Invalid Runtime observation horizon.');
2525 const at = observedThrough - origin;
2526 const events = trace.events.map(e => e.openEnded && e.attributes['whalesong.waiting'] === true && at >= e.startTime
2527 ? { ...e, endTime: at, openEnded: false } : e);
2528 return { ...trace, events, duration: Math.max(trace.duration, at) };
2529 }
2530
2531 };
2532 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;}
2533 global.PetNative=load('pet-native').PetNative;
2534 })(globalThis);
2535
2535 lines JAVASCRIPT