| 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 |