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