From 3988f446ca28fff200b8e3d2b6f61fc7f208e4cf Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:05:20 -0700 Subject: [PATCH 01/33] feat(runtime): carry stable pursuit identity through observer hooks --- src/runtime-hooks.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/runtime-hooks.ts b/src/runtime-hooks.ts index 6cb9d5ba..5ed8c0fe 100644 --- a/src/runtime-hooks.ts +++ b/src/runtime-hooks.ts @@ -4,6 +4,11 @@ * `AgentProfile`: profiles stay portable agent recipes; hooks attach to the * loop or product harness that is running the profile. * + * A `pursuitId` is deliberately orthogonal to `runId`: a pursuit can span many + * resumed/retried/forked runs while every event remains attributable to the + * durable objective that caused it. The observer plane is outside the agent + * environment and must never be required for agent correctness. + * * @experimental */ @@ -35,6 +40,8 @@ export type RuntimeDecisionKind = export interface RuntimeHookEvent { id: string + /** Stable identity for the long-lived objective. One pursuit may contain many runs. */ + pursuitId?: string runId: string scenarioId?: string target: RuntimeHookTarget @@ -59,6 +66,8 @@ export interface RuntimeDecisionEvidenceRef { export interface RuntimeDecisionPoint { id: string + /** Stable identity for the long-lived objective. One pursuit may contain many runs. */ + pursuitId?: string runId: string scenarioId?: string stepIndex: number @@ -108,6 +117,40 @@ export function defineRuntimeHooks(hooks: RuntimeHooks): RuntimeHooks { return hooks } +/** + * Attach a stable pursuit identity to the entire observer stream without changing + * agent code or teaching individual runtimes about pursuits. Because recursive Scope + * execution already inherits one RuntimeHooks instance, this wrapper automatically + * covers descendants, nested drivers, and resumed execution that reuses the wrapper. + * + * Existing matching pursuit ids are preserved. A conflicting id fails closed: silently + * rewriting attribution would make the meta-observer untrustworthy. + */ +export function withPursuitContext(pursuitId: string, hooks: RuntimeHooks): RuntimeHooks { + const stableId = pursuitId.trim() + if (stableId.length === 0) throw new TypeError('withPursuitContext: pursuitId must be non-empty') + + const assertAndStamp = (value: T): T => { + if (value.pursuitId !== undefined && value.pursuitId !== stableId) { + throw new Error( + `withPursuitContext: observer identity conflict (${value.pursuitId} !== ${stableId})`, + ) + } + if (value.pursuitId === stableId) return value + return { ...value, pursuitId: stableId } + } + + return { + onEvent: hooks.onEvent + ? (event, context) => hooks.onEvent?.(assertAndStamp(event), context) + : undefined, + onDecisionPoint: hooks.onDecisionPoint + ? (point, context) => hooks.onDecisionPoint?.(assertAndStamp(point), context) + : undefined, + onHookError: hooks.onHookError, + } +} + /** * Merge several {@link RuntimeHooks} into one. Falsy entries are dropped (so you can * pass `flag && hooks`), and every observer's `onEvent`/`onDecisionPoint` fires for each From 4f82407a160db7df994c144cd9174995e0958c70 Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:05:52 -0700 Subject: [PATCH 02/33] feat(durable): add tamper-evident pursuit observer journal --- src/durable/observer-journal.ts | 223 ++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 src/durable/observer-journal.ts diff --git a/src/durable/observer-journal.ts b/src/durable/observer-journal.ts new file mode 100644 index 00000000..08fb8c46 --- /dev/null +++ b/src/durable/observer-journal.ts @@ -0,0 +1,223 @@ +import { createHash } from 'node:crypto' +import { mkdir, readFile, appendFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { + type RuntimeDecisionPoint, + type RuntimeHookEvent, + type RuntimeHooks, + withPursuitContext, +} from '../runtime-hooks' + +export type ObserverRecordKind = 'event' | 'decision' + +/** + * One immutable record in the observer plane. `sequence` is observer order, not + * execution order; causal/runtime order remains available on the underlying event. + * `previousDigest` + `digest` make deletion, reordering, or mutation detectable. + */ +export interface ObserverRecord { + readonly schemaVersion: 1 + readonly pursuitId: string + readonly sequence: number + readonly kind: ObserverRecordKind + readonly observedAt: number + readonly previousDigest?: string + readonly event?: RuntimeHookEvent + readonly decision?: RuntimeDecisionPoint + readonly digest: string +} + +export interface ObserverJournal { + appendEvent(event: RuntimeHookEvent): Promise + appendDecision(point: RuntimeDecisionPoint): Promise + read(): Promise + hooks(): RuntimeHooks +} + +type UnsignedObserverRecord = Omit + +/** + * Durable, append-only third-person history for one pursuit. It consumes Runtime's + * existing hook stream and does not participate in execution decisions. A broken + * observer therefore cannot change what an agent is allowed to do. + */ +export class FileObserverJournal implements ObserverJournal { + readonly path: string + readonly pursuitId: string + private tail: Promise = Promise.resolve() + private initialized = false + private sequence = 0 + private previousDigest: string | undefined + + constructor(path: string, pursuitId: string) { + const stableId = pursuitId.trim() + if (stableId.length === 0) throw new TypeError('FileObserverJournal: pursuitId must be non-empty') + this.path = resolve(path) + this.pursuitId = stableId + } + + hooks(): RuntimeHooks { + return withPursuitContext(this.pursuitId, { + onEvent: (event) => this.appendEvent(event).then(() => undefined), + onDecisionPoint: (point) => this.appendDecision(point).then(() => undefined), + }) + } + + appendEvent(event: RuntimeHookEvent): Promise { + return this.enqueue('event', event) + } + + appendDecision(point: RuntimeDecisionPoint): Promise { + return this.enqueue('decision', point) + } + + async read(): Promise { + await this.tail + let text: string + try { + text = await readFile(this.path, 'utf8') + } catch (error) { + if (isNoEnt(error)) return [] + throw error + } + return verifyObserverRecords(parseCommittedLines(text, this.path), this.pursuitId) + } + + private enqueue( + kind: ObserverRecordKind, + value: RuntimeHookEvent | RuntimeDecisionPoint, + ): Promise { + let result: ObserverRecord | undefined + const operation = this.tail.then(async () => { + await this.initialize() + if (value.pursuitId !== this.pursuitId) { + throw new Error( + `FileObserverJournal: ${kind} pursuitId ${String(value.pursuitId)} does not match ${this.pursuitId}`, + ) + } + + const unsigned: UnsignedObserverRecord = { + schemaVersion: 1, + pursuitId: this.pursuitId, + sequence: this.sequence + 1, + kind, + observedAt: Date.now(), + ...(this.previousDigest ? { previousDigest: this.previousDigest } : {}), + ...(kind === 'event' + ? { event: value as RuntimeHookEvent } + : { decision: value as RuntimeDecisionPoint }), + } + const record: ObserverRecord = Object.freeze({ + ...unsigned, + digest: observerRecordDigest(unsigned), + }) + await mkdir(dirname(this.path), { recursive: true }) + await appendFile(this.path, `${JSON.stringify(record)}\n`, 'utf8') + this.sequence = record.sequence + this.previousDigest = record.digest + result = record + }) + this.tail = operation.catch(() => undefined) + return operation.then(() => { + if (!result) throw new Error('FileObserverJournal: append completed without a record') + return result + }) + } + + private async initialize(): Promise { + if (this.initialized) return + this.initialized = true + const records = await this.readExistingUnsafe() + const verified = verifyObserverRecords(records, this.pursuitId) + const tail = verified.at(-1) + this.sequence = tail?.sequence ?? 0 + this.previousDigest = tail?.digest + } + + private async readExistingUnsafe(): Promise { + let text: string + try { + text = await readFile(this.path, 'utf8') + } catch (error) { + if (isNoEnt(error)) return [] + throw error + } + return parseCommittedLines(text, this.path) + } +} + +/** Verify identity, monotonic sequence, and the complete digest chain. */ +export function verifyObserverRecords( + records: readonly ObserverRecord[], + pursuitId?: string, +): readonly ObserverRecord[] { + let previousDigest: string | undefined + let expectedSequence = 1 + for (const record of records) { + if (record.schemaVersion !== 1) throw new Error('observer journal: unsupported schemaVersion') + if (pursuitId !== undefined && record.pursuitId !== pursuitId) { + throw new Error(`observer journal: pursuit identity mismatch at sequence ${record.sequence}`) + } + if (record.sequence !== expectedSequence) { + throw new Error( + `observer journal: non-contiguous sequence ${record.sequence}; expected ${expectedSequence}`, + ) + } + if (record.previousDigest !== previousDigest) { + throw new Error(`observer journal: digest-chain break at sequence ${record.sequence}`) + } + if ((record.kind === 'event') === (record.event === undefined)) { + throw new Error(`observer journal: invalid event payload at sequence ${record.sequence}`) + } + if ((record.kind === 'decision') === (record.decision === undefined)) { + throw new Error(`observer journal: invalid decision payload at sequence ${record.sequence}`) + } + const { digest, ...unsigned } = record + const expected = observerRecordDigest(unsigned) + if (digest !== expected) throw new Error(`observer journal: digest mismatch at sequence ${record.sequence}`) + previousDigest = digest + expectedSequence += 1 + } + return Object.freeze([...records]) +} + +export function observerRecordDigest(record: UnsignedObserverRecord): string { + return createHash('sha256').update(JSON.stringify(record)).digest('hex') +} + +/** Build the canonical durable observer hook in one call. */ +export function createFileObserverHooks(path: string, pursuitId: string): { + readonly journal: FileObserverJournal + readonly hooks: RuntimeHooks +} { + const journal = new FileObserverJournal(path, pursuitId) + return { journal, hooks: journal.hooks() } +} + +function parseCommittedLines(text: string, path: string): ObserverRecord[] { + const lines = text.split('\n') + const out: ObserverRecord[] = [] + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index] + if (!line?.trim()) continue + try { + out.push(JSON.parse(line) as ObserverRecord) + } catch (error) { + // A torn final append was never a committed observer record; older malformed + // records are corruption and must fail loud. + const isLastNonEmpty = lines.slice(index + 1).every((entry) => !entry.trim()) + if (isLastNonEmpty) break + throw new Error(`${path}: malformed observer record ${index + 1}`, { cause: error }) + } + } + return out +} + +function isNoEnt(error: unknown): boolean { + return ( + typeof error === 'object' && + error !== null && + 'code' in error && + (error as { code?: unknown }).code === 'ENOENT' + ) +} From 070ab826b78b8caf94da51087d8f9f77195c5654 Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:06:14 -0700 Subject: [PATCH 03/33] feat(durable): export pursuit observer journal --- src/durable/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/durable/index.ts b/src/durable/index.ts index b00f874c..25445917 100644 --- a/src/durable/index.ts +++ b/src/durable/index.ts @@ -12,6 +12,8 @@ * persist and pass as both execution and turn identity on dispatch. * - `discoverDurableSupervisionRun`: inspect a durable supervision directory * without already knowing the root/run identities written inside it. + * - `FileObserverJournal`: tamper-evident, append-only third-person history + * for a pursuit, fed by Runtime's existing hook stream. */ export type { @@ -24,6 +26,15 @@ export type { } from './chat-engine' export { handleChatTurn } from './chat-engine' export { deriveExecutionId } from './execution-handle' +export { + createFileObserverHooks, + FileObserverJournal, + type ObserverJournal, + type ObserverRecord, + type ObserverRecordKind, + observerRecordDigest, + verifyObserverRecords, +} from './observer-journal' export { type DurableCoordinationStreamIdentity, type DurableSupervisionDiscovery, From c502c47659b22a2d101e46160d9c5b78854175bd Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:06:32 -0700 Subject: [PATCH 04/33] test(durable): verify pursuit observer identity and hash chain --- src/durable/tests/observer-journal.test.ts | 84 ++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/durable/tests/observer-journal.test.ts diff --git a/src/durable/tests/observer-journal.test.ts b/src/durable/tests/observer-journal.test.ts new file mode 100644 index 00000000..a68e97a0 --- /dev/null +++ b/src/durable/tests/observer-journal.test.ts @@ -0,0 +1,84 @@ +import { mkdtemp, readFile, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { FileObserverJournal } from '../observer-journal' + +function event(id: string, pursuitId = 'pursuit:test') { + return { + id, + pursuitId, + runId: 'run:1', + target: 'agent.spawn' as const, + phase: 'event' as const, + timestamp: 1, + payload: { child: id }, + } +} + +describe('FileObserverJournal', () => { + it('persists one pursuit across multiple run events with a verified chain', async () => { + const dir = await mkdtemp(join(tmpdir(), 'runtime-observer-')) + const path = join(dir, 'observer.jsonl') + const journal = new FileObserverJournal(path, 'pursuit:test') + + await journal.appendEvent(event('e1')) + await journal.appendEvent({ ...event('e2'), runId: 'run:2' }) + + const records = await journal.read() + expect(records.map((record) => record.sequence)).toEqual([1, 2]) + expect(records.map((record) => record.pursuitId)).toEqual(['pursuit:test', 'pursuit:test']) + expect(records[1]?.previousDigest).toBe(records[0]?.digest) + expect(records[1]?.event?.runId).toBe('run:2') + }) + + it('stamps every hook event and decision with the pursuit identity', async () => { + const dir = await mkdtemp(join(tmpdir(), 'runtime-observer-hooks-')) + const journal = new FileObserverJournal(join(dir, 'observer.jsonl'), 'pursuit:hooks') + const hooks = journal.hooks() + + await hooks.onEvent?.( + { + id: 'event', + runId: 'run:1', + target: 'agent.child', + phase: 'event', + timestamp: 1, + }, + {}, + ) + await hooks.onDecisionPoint?.( + { + id: 'decision', + runId: 'run:1', + stepIndex: 0, + kind: 'continue', + candidateActions: ['continue'], + evidence: [], + }, + {}, + ) + + const records = await journal.read() + expect(records).toHaveLength(2) + expect(records[0]?.event?.pursuitId).toBe('pursuit:hooks') + expect(records[1]?.decision?.pursuitId).toBe('pursuit:hooks') + }) + + it('fails closed on identity conflicts and detects mutation', async () => { + const dir = await mkdtemp(join(tmpdir(), 'runtime-observer-corrupt-')) + const path = join(dir, 'observer.jsonl') + const journal = new FileObserverJournal(path, 'pursuit:one') + + await expect(journal.appendEvent(event('wrong', 'pursuit:two'))).rejects.toThrow( + /does not match/, + ) + await journal.appendEvent(event('right', 'pursuit:one')) + + const text = await readFile(path, 'utf8') + await writeFile(path, text.replace('agent.spawn', 'agent.child'), 'utf8') + await expect(new FileObserverJournal(path, 'pursuit:one').read()).rejects.toThrow( + /digest mismatch/, + ) + }) +}) From 3d5f1cf8f6aa657dda1090efc9739cbf87d07401 Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:08:59 -0700 Subject: [PATCH 05/33] fix(durable): use fsynced observer appends and safe recovery --- src/durable/observer-journal.ts | 63 +++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/src/durable/observer-journal.ts b/src/durable/observer-journal.ts index 08fb8c46..521a8fa5 100644 --- a/src/durable/observer-journal.ts +++ b/src/durable/observer-journal.ts @@ -1,12 +1,13 @@ import { createHash } from 'node:crypto' -import { mkdir, readFile, appendFile } from 'node:fs/promises' -import { dirname, resolve } from 'node:path' +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' import { type RuntimeDecisionPoint, type RuntimeHookEvent, type RuntimeHooks, withPursuitContext, } from '../runtime-hooks' +import { parseCommittedJsonLines, prepareJsonlAppend, writeAllBytes } from './jsonl-file' export type ObserverRecordKind = 'event' | 'decision' @@ -40,6 +41,9 @@ type UnsignedObserverRecord = Omit * Durable, append-only third-person history for one pursuit. It consumes Runtime's * existing hook stream and does not participate in execution decisions. A broken * observer therefore cannot change what an agent is allowed to do. + * + * The write discipline deliberately matches `FileSpawnJournal`: serialized appends, + * torn-tail recovery, short-write handling, and fsync before acknowledgement. */ export class FileObserverJournal implements ObserverJournal { readonly path: string @@ -80,7 +84,10 @@ export class FileObserverJournal implements ObserverJournal { if (isNoEnt(error)) return [] throw error } - return verifyObserverRecords(parseCommittedLines(text, this.path), this.pursuitId) + return verifyObserverRecords( + parseCommittedJsonLines(text, this.path), + this.pursuitId, + ) } private enqueue( @@ -111,8 +118,7 @@ export class FileObserverJournal implements ObserverJournal { ...unsigned, digest: observerRecordDigest(unsigned), }) - await mkdir(dirname(this.path), { recursive: true }) - await appendFile(this.path, `${JSON.stringify(record)}\n`, 'utf8') + await this.writeRecord(record) this.sequence = record.sequence this.previousDigest = record.digest result = record @@ -126,12 +132,14 @@ export class FileObserverJournal implements ObserverJournal { private async initialize(): Promise { if (this.initialized) return - this.initialized = true const records = await this.readExistingUnsafe() const verified = verifyObserverRecords(records, this.pursuitId) const tail = verified.at(-1) this.sequence = tail?.sequence ?? 0 this.previousDigest = tail?.digest + // Only latch after recovery + verification succeed. A transient read error or + // corruption must never leave an instance pretending it initialized cleanly. + this.initialized = true } private async readExistingUnsafe(): Promise { @@ -142,11 +150,25 @@ export class FileObserverJournal implements ObserverJournal { if (isNoEnt(error)) return [] throw error } - return parseCommittedLines(text, this.path) + return parseCommittedJsonLines(text, this.path) + } + + private async writeRecord(record: ObserverRecord): Promise { + const fs = await import('node:fs/promises') + const path = await import('node:path') + await fs.mkdir(path.dirname(this.path), { recursive: true }) + const needsSeparator = await prepareJsonlAppend(this.path) + const handle = await fs.open(this.path, 'a') + try { + await writeAllBytes(handle, `${needsSeparator ? '\n' : ''}${JSON.stringify(record)}\n`) + await handle.sync() + } finally { + await handle.close() + } } } -/** Verify identity, monotonic sequence, and the complete digest chain. */ +/** Verify identity, monotonic sequence, payload shape, and the complete digest chain. */ export function verifyObserverRecords( records: readonly ObserverRecord[], pursuitId?: string, @@ -172,6 +194,12 @@ export function verifyObserverRecords( if ((record.kind === 'decision') === (record.decision === undefined)) { throw new Error(`observer journal: invalid decision payload at sequence ${record.sequence}`) } + if (record.event !== undefined && record.event.pursuitId !== record.pursuitId) { + throw new Error(`observer journal: nested event pursuit mismatch at sequence ${record.sequence}`) + } + if (record.decision !== undefined && record.decision.pursuitId !== record.pursuitId) { + throw new Error(`observer journal: nested decision pursuit mismatch at sequence ${record.sequence}`) + } const { digest, ...unsigned } = record const expected = observerRecordDigest(unsigned) if (digest !== expected) throw new Error(`observer journal: digest mismatch at sequence ${record.sequence}`) @@ -194,25 +222,6 @@ export function createFileObserverHooks(path: string, pursuitId: string): { return { journal, hooks: journal.hooks() } } -function parseCommittedLines(text: string, path: string): ObserverRecord[] { - const lines = text.split('\n') - const out: ObserverRecord[] = [] - for (let index = 0; index < lines.length; index += 1) { - const line = lines[index] - if (!line?.trim()) continue - try { - out.push(JSON.parse(line) as ObserverRecord) - } catch (error) { - // A torn final append was never a committed observer record; older malformed - // records are corruption and must fail loud. - const isLastNonEmpty = lines.slice(index + 1).every((entry) => !entry.trim()) - if (isLastNonEmpty) break - throw new Error(`${path}: malformed observer record ${index + 1}`, { cause: error }) - } - } - return out -} - function isNoEnt(error: unknown): boolean { return ( typeof error === 'object' && From 047d36be241a74c0509b48201fda40d71a112de0 Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:14:24 -0700 Subject: [PATCH 06/33] feat(durable): project pursuit topology from observer facts --- src/durable/observer-projection.ts | 245 +++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 src/durable/observer-projection.ts diff --git a/src/durable/observer-projection.ts b/src/durable/observer-projection.ts new file mode 100644 index 00000000..e72c7d3b --- /dev/null +++ b/src/durable/observer-projection.ts @@ -0,0 +1,245 @@ +import type { RuntimeDecisionKind, RuntimeHookTarget } from '../runtime-hooks' +import type { ObserverRecord } from './observer-journal' + +export interface PursuitRunProjection { + readonly runId: string + readonly firstSequence: number + readonly lastSequence: number + readonly firstObservedAt: number + readonly lastObservedAt: number + readonly eventCount: number + readonly decisionCount: number + readonly targets: Readonly> + readonly decisions: Readonly> +} + +export interface PursuitNodeProjection { + readonly id: string + readonly parentId?: string + readonly runId: string + readonly label?: string + readonly runtime?: string + readonly depth?: number + readonly assignmentId?: string + readonly identity?: unknown + readonly budget?: unknown + readonly firstSequence: number + readonly lastSequence: number + readonly firstObservedAt: number + readonly lastObservedAt: number + readonly eventCount: number +} + +export interface PursuitProjection { + readonly pursuitId: string + readonly sequence: number + readonly chainTip?: string + readonly firstObservedAt?: number + readonly lastObservedAt?: number + readonly runs: readonly PursuitRunProjection[] + readonly nodes: readonly PursuitNodeProjection[] + readonly eventCount: number + readonly decisionCount: number +} + +type MutableRun = { + runId: string + firstSequence: number + lastSequence: number + firstObservedAt: number + lastObservedAt: number + eventCount: number + decisionCount: number + targets: Record + decisions: Record +} + +type MutableNode = { + id: string + parentId?: string + runId: string + label?: string + runtime?: string + depth?: number + assignmentId?: string + identity?: unknown + budget?: unknown + firstSequence: number + lastSequence: number + firstObservedAt: number + lastObservedAt: number + eventCount: number +} + +/** + * Fold the append-only observer history into a deterministic operator projection. + * + * This is intentionally a READ model, not another state machine: it does not own + * execution, cannot steer agents, and can be rebuilt from the journal at any time. + * The only topology fact it special-cases is Runtime's canonical `agent.spawn` + * payload (`childId` + `parentId`); every other event remains visible through the + * per-run target/decision counters even when a future Runtime adds new event kinds. + */ +export function projectPursuit(records: readonly ObserverRecord[]): PursuitProjection { + if (records.length === 0) { + throw new TypeError('projectPursuit: at least one observer record is required') + } + const pursuitId = records[0]!.pursuitId + const runs = new Map() + const nodes = new Map() + let eventCount = 0 + let decisionCount = 0 + + for (const record of records) { + if (record.pursuitId !== pursuitId) { + throw new Error( + `projectPursuit: mixed pursuit journals (${record.pursuitId} !== ${pursuitId})`, + ) + } + const observed = record.event ?? record.decision + if (!observed) throw new Error(`projectPursuit: record ${record.sequence} has no observation`) + const run = getRun(runs, observed.runId, record) + run.lastSequence = record.sequence + run.lastObservedAt = record.observedAt + + if (record.event) { + eventCount += 1 + run.eventCount += 1 + increment(run.targets, record.event.target) + projectSpawnNode(nodes, record) + projectNodeActivity(nodes, record) + } else if (record.decision) { + decisionCount += 1 + run.decisionCount += 1 + increment(run.decisions, record.decision.kind) + } + } + + const first = records[0]! + const last = records.at(-1)! + return Object.freeze({ + pursuitId, + sequence: last.sequence, + chainTip: last.digest, + firstObservedAt: first.observedAt, + lastObservedAt: last.observedAt, + runs: Object.freeze( + [...runs.values()] + .sort((a, b) => a.firstSequence - b.firstSequence || a.runId.localeCompare(b.runId)) + .map(freezeRun), + ), + nodes: Object.freeze( + [...nodes.values()] + .sort((a, b) => a.firstSequence - b.firstSequence || a.id.localeCompare(b.id)) + .map(freezeNode), + ), + eventCount, + decisionCount, + }) +} + +function getRun( + runs: Map, + runId: string, + record: ObserverRecord, +): MutableRun { + const existing = runs.get(runId) + if (existing) return existing + const created: MutableRun = { + runId, + firstSequence: record.sequence, + lastSequence: record.sequence, + firstObservedAt: record.observedAt, + lastObservedAt: record.observedAt, + eventCount: 0, + decisionCount: 0, + targets: {}, + decisions: {}, + } + runs.set(runId, created) + return created +} + +function projectSpawnNode(nodes: Map, record: ObserverRecord): void { + const event = record.event + if (!event || event.target !== 'agent.spawn') return + const payload = objectRecord(event.payload) + const childId = stringField(payload, 'childId') + if (!childId) return + const existing = nodes.get(childId) + if (existing) { + existing.lastSequence = record.sequence + existing.lastObservedAt = record.observedAt + existing.eventCount += 1 + return + } + nodes.set(childId, { + id: childId, + ...(event.parentId ? { parentId: event.parentId } : {}), + runId: event.runId, + ...(stringField(payload, 'label') ? { label: stringField(payload, 'label') } : {}), + ...(stringField(payload, 'runtime') ? { runtime: stringField(payload, 'runtime') } : {}), + ...(numberField(payload, 'depth') !== undefined ? { depth: numberField(payload, 'depth') } : {}), + ...(stringField(payload, 'assignmentId') + ? { assignmentId: stringField(payload, 'assignmentId') } + : {}), + ...(payload && Object.hasOwn(payload, 'identity') ? { identity: payload.identity } : {}), + ...(payload && Object.hasOwn(payload, 'budget') ? { budget: payload.budget } : {}), + firstSequence: record.sequence, + lastSequence: record.sequence, + firstObservedAt: record.observedAt, + lastObservedAt: record.observedAt, + eventCount: 1, + }) +} + +function projectNodeActivity(nodes: Map, record: ObserverRecord): void { + const event = record.event + if (!event || event.target === 'agent.spawn') return + const payload = objectRecord(event.payload) + const nodeId = + stringField(payload, 'childId') ?? + stringField(payload, 'nodeId') ?? + stringField(payload, 'workerId') + if (!nodeId) return + const node = nodes.get(nodeId) + if (!node) return + node.lastSequence = record.sequence + node.lastObservedAt = record.observedAt + node.eventCount += 1 +} + +function freezeRun(run: MutableRun): PursuitRunProjection { + return Object.freeze({ + ...run, + targets: Object.freeze({ ...run.targets }), + decisions: Object.freeze({ ...run.decisions }), + }) +} + +function freezeNode(node: MutableNode): PursuitNodeProjection { + return Object.freeze({ ...node }) +} + +function increment( + target: Record, + key: RuntimeHookTarget | RuntimeDecisionKind, +): void { + target[key] = (target[key] ?? 0) + 1 +} + +function objectRecord(value: unknown): Record | undefined { + return typeof value === 'object' && value !== null && !Array.isArray(value) + ? (value as Record) + : undefined +} + +function stringField(value: Record | undefined, key: string): string | undefined { + const field = value?.[key] + return typeof field === 'string' && field.length > 0 ? field : undefined +} + +function numberField(value: Record | undefined, key: string): number | undefined { + const field = value?.[key] + return typeof field === 'number' && Number.isFinite(field) ? field : undefined +} From 1163a12f3739772ce5f9a565011f26cb1b141e8c Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:14:40 -0700 Subject: [PATCH 07/33] feat(durable): export pursuit observer projection --- src/durable/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/durable/index.ts b/src/durable/index.ts index 25445917..4877d24f 100644 --- a/src/durable/index.ts +++ b/src/durable/index.ts @@ -14,6 +14,8 @@ * without already knowing the root/run identities written inside it. * - `FileObserverJournal`: tamper-evident, append-only third-person history * for a pursuit, fed by Runtime's existing hook stream. + * - `projectPursuit`: a rebuildable operator read model over that history; + * it owns no execution or coordination semantics. */ export type { @@ -35,6 +37,12 @@ export { observerRecordDigest, verifyObserverRecords, } from './observer-journal' +export { + type PursuitNodeProjection, + type PursuitProjection, + type PursuitRunProjection, + projectPursuit, +} from './observer-projection' export { type DurableCoordinationStreamIdentity, type DurableSupervisionDiscovery, From e9b7fab1f9aebaca387a350309631b9d7426c72a Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:14:55 -0700 Subject: [PATCH 08/33] test(durable): project recursive pursuit topology --- src/durable/tests/observer-projection.test.ts | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/durable/tests/observer-projection.test.ts diff --git a/src/durable/tests/observer-projection.test.ts b/src/durable/tests/observer-projection.test.ts new file mode 100644 index 00000000..eb3c7c21 --- /dev/null +++ b/src/durable/tests/observer-projection.test.ts @@ -0,0 +1,127 @@ +import { describe, expect, it } from 'vitest' +import { type ObserverRecord, observerRecordDigest } from '../observer-journal' +import { projectPursuit } from '../observer-projection' + +function record( + sequence: number, + input: Omit, + previousDigest?: string, +): ObserverRecord { + const unsigned = { + schemaVersion: 1 as const, + pursuitId: 'pursuit:test', + sequence, + observedAt: sequence * 10, + ...(previousDigest ? { previousDigest } : {}), + ...input, + } + return { ...unsigned, digest: observerRecordDigest(unsigned) } +} + +describe('projectPursuit', () => { + it('builds a multi-run recursive topology from substrate spawn facts', () => { + const first = record(1, { + kind: 'event', + event: { + id: 'spawn-a', + pursuitId: 'pursuit:test', + runId: 'run:1', + target: 'agent.spawn', + phase: 'after', + timestamp: 1, + parentId: 'root', + payload: { + childId: 'root:s0', + label: 'researcher', + runtime: 'sandbox', + depth: 0, + identity: { candidateDigest: 'sha256:a' }, + }, + }, + }) + const second = record( + 2, + { + kind: 'decision', + decision: { + id: 'decision-a', + pursuitId: 'pursuit:test', + runId: 'run:1', + stepIndex: 0, + kind: 'continue', + candidateActions: ['continue'], + evidence: [], + }, + }, + first.digest, + ) + const third = record( + 3, + { + kind: 'event', + event: { + id: 'spawn-b', + pursuitId: 'pursuit:test', + runId: 'run:2', + target: 'agent.spawn', + phase: 'after', + timestamp: 3, + parentId: 'root:s0', + payload: { + childId: 'root:s0:s0', + label: 'critic', + runtime: 'bridge', + depth: 1, + }, + }, + }, + second.digest, + ) + + const view = projectPursuit([first, second, third]) + expect(view.pursuitId).toBe('pursuit:test') + expect(view.sequence).toBe(3) + expect(view.chainTip).toBe(third.digest) + expect(view.runs.map((run) => run.runId)).toEqual(['run:1', 'run:2']) + expect(view.runs[0]?.decisions.continue).toBe(1) + expect(view.nodes.map((node) => [node.id, node.parentId])).toEqual([ + ['root:s0', 'root'], + ['root:s0:s0', 'root:s0'], + ]) + }) + + it('refuses records from different pursuits', () => { + const first = record(1, { + kind: 'event', + event: { + id: 'a', + pursuitId: 'pursuit:test', + runId: 'run:1', + target: 'agent.run', + phase: 'event', + timestamp: 1, + }, + }) + const secondUnsigned = { + schemaVersion: 1 as const, + pursuitId: 'pursuit:other', + sequence: 2, + observedAt: 20, + previousDigest: first.digest, + kind: 'event' as const, + event: { + id: 'b', + pursuitId: 'pursuit:other', + runId: 'run:2', + target: 'agent.run' as const, + phase: 'event' as const, + timestamp: 2, + }, + } + const second: ObserverRecord = { + ...secondUnsigned, + digest: observerRecordDigest(secondUnsigned), + } + expect(() => projectPursuit([first, second])).toThrow(/mixed pursuit journals/) + }) +}) From b8c6dab0f9e0722d1fd489bf7ff5628bb7b1b411 Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:17:59 -0700 Subject: [PATCH 09/33] feat(durable): project verified node terminal state and spend --- src/durable/observer-projection.ts | 81 +++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 18 deletions(-) diff --git a/src/durable/observer-projection.ts b/src/durable/observer-projection.ts index e72c7d3b..b7692ffd 100644 --- a/src/durable/observer-projection.ts +++ b/src/durable/observer-projection.ts @@ -1,5 +1,5 @@ import type { RuntimeDecisionKind, RuntimeHookTarget } from '../runtime-hooks' -import type { ObserverRecord } from './observer-journal' +import { type ObserverRecord, verifyObserverRecords } from './observer-journal' export interface PursuitRunProjection { readonly runId: string @@ -13,6 +13,8 @@ export interface PursuitRunProjection { readonly decisions: Readonly> } +export type PursuitNodeStatus = 'running' | 'done' | 'down' + export interface PursuitNodeProjection { readonly id: string readonly parentId?: string @@ -23,6 +25,15 @@ export interface PursuitNodeProjection { readonly assignmentId?: string readonly identity?: unknown readonly budget?: unknown + readonly status: PursuitNodeStatus + readonly settledAt?: number + readonly spent?: unknown + readonly outRef?: string + readonly score?: number + readonly valid?: boolean + readonly reason?: string + readonly infra?: boolean + readonly wait?: unknown readonly firstSequence: number readonly lastSequence: number readonly firstObservedAt: number @@ -64,6 +75,15 @@ type MutableNode = { assignmentId?: string identity?: unknown budget?: unknown + status: PursuitNodeStatus + settledAt?: number + spent?: unknown + outRef?: string + score?: number + valid?: boolean + reason?: string + infra?: boolean + wait?: unknown firstSequence: number lastSequence: number firstObservedAt: number @@ -76,26 +96,25 @@ type MutableNode = { * * This is intentionally a READ model, not another state machine: it does not own * execution, cannot steer agents, and can be rebuilt from the journal at any time. - * The only topology fact it special-cases is Runtime's canonical `agent.spawn` - * payload (`childId` + `parentId`); every other event remains visible through the - * per-run target/decision counters even when a future Runtime adds new event kinds. + * Projection verifies the complete hash chain first, so an operator view can never + * silently render a mutated or reordered observer history as trustworthy state. + * + * Topology comes only from Runtime's canonical `agent.spawn` facts. Terminal state + * comes only from Runtime's canonical `agent.child` facts. New event kinds remain + * visible through per-run counters without teaching this layer intellectual policy. */ export function projectPursuit(records: readonly ObserverRecord[]): PursuitProjection { if (records.length === 0) { throw new TypeError('projectPursuit: at least one observer record is required') } const pursuitId = records[0]!.pursuitId + const verified = verifyObserverRecords(records, pursuitId) const runs = new Map() const nodes = new Map() let eventCount = 0 let decisionCount = 0 - for (const record of records) { - if (record.pursuitId !== pursuitId) { - throw new Error( - `projectPursuit: mixed pursuit journals (${record.pursuitId} !== ${pursuitId})`, - ) - } + for (const record of verified) { const observed = record.event ?? record.decision if (!observed) throw new Error(`projectPursuit: record ${record.sequence} has no observation`) const run = getRun(runs, observed.runId, record) @@ -115,8 +134,8 @@ export function projectPursuit(records: readonly ObserverRecord[]): PursuitProje } } - const first = records[0]! - const last = records.at(-1)! + const first = verified[0]! + const last = verified.at(-1)! return Object.freeze({ pursuitId, sequence: last.sequence, @@ -173,18 +192,21 @@ function projectSpawnNode(nodes: Map, record: ObserverRecor existing.eventCount += 1 return } + const label = stringField(payload, 'label') + const runtime = stringField(payload, 'runtime') + const depth = numberField(payload, 'depth') + const assignmentId = stringField(payload, 'assignmentId') nodes.set(childId, { id: childId, ...(event.parentId ? { parentId: event.parentId } : {}), runId: event.runId, - ...(stringField(payload, 'label') ? { label: stringField(payload, 'label') } : {}), - ...(stringField(payload, 'runtime') ? { runtime: stringField(payload, 'runtime') } : {}), - ...(numberField(payload, 'depth') !== undefined ? { depth: numberField(payload, 'depth') } : {}), - ...(stringField(payload, 'assignmentId') - ? { assignmentId: stringField(payload, 'assignmentId') } - : {}), + ...(label ? { label } : {}), + ...(runtime ? { runtime } : {}), + ...(depth !== undefined ? { depth } : {}), + ...(assignmentId ? { assignmentId } : {}), ...(payload && Object.hasOwn(payload, 'identity') ? { identity: payload.identity } : {}), ...(payload && Object.hasOwn(payload, 'budget') ? { budget: payload.budget } : {}), + status: 'running', firstSequence: record.sequence, lastSequence: record.sequence, firstObservedAt: record.observedAt, @@ -207,6 +229,24 @@ function projectNodeActivity(nodes: Map, record: ObserverRe node.lastSequence = record.sequence node.lastObservedAt = record.observedAt node.eventCount += 1 + + if (event.target !== 'agent.child') return + const status = stringField(payload, 'status') + if (status !== 'done' && status !== 'down') return + node.status = status + node.settledAt = record.observedAt + if (payload && Object.hasOwn(payload, 'spent')) node.spent = payload.spent + const outRef = stringField(payload, 'outRef') + if (outRef) node.outRef = outRef + const score = numberField(payload, 'score') + if (score !== undefined) node.score = score + const valid = booleanField(payload, 'valid') + if (valid !== undefined) node.valid = valid + const reason = stringField(payload, 'reason') + if (reason) node.reason = reason + const infra = booleanField(payload, 'infra') + if (infra !== undefined) node.infra = infra + if (payload && Object.hasOwn(payload, 'wait')) node.wait = payload.wait } function freezeRun(run: MutableRun): PursuitRunProjection { @@ -243,3 +283,8 @@ function numberField(value: Record | undefined, key: string): n const field = value?.[key] return typeof field === 'number' && Number.isFinite(field) ? field : undefined } + +function booleanField(value: Record | undefined, key: string): boolean | undefined { + const field = value?.[key] + return typeof field === 'boolean' ? field : undefined +} From c6bb7f892b0351a4c46d72e76c4766f3bf50b6af Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:18:22 -0700 Subject: [PATCH 10/33] test(durable): cover terminal observer projection and tamper refusal --- src/durable/tests/observer-projection.test.ts | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/src/durable/tests/observer-projection.test.ts b/src/durable/tests/observer-projection.test.ts index eb3c7c21..62cf6505 100644 --- a/src/durable/tests/observer-projection.test.ts +++ b/src/durable/tests/observer-projection.test.ts @@ -19,7 +19,7 @@ function record( } describe('projectPursuit', () => { - it('builds a multi-run recursive topology from substrate spawn facts', () => { + it('builds a multi-run recursive topology and terminal truth from substrate facts', () => { const first = record(1, { kind: 'event', event: { @@ -77,20 +77,52 @@ describe('projectPursuit', () => { }, second.digest, ) + const fourth = record( + 4, + { + kind: 'event', + event: { + id: 'settle-b', + pursuitId: 'pursuit:test', + runId: 'run:2', + target: 'agent.child', + phase: 'after', + timestamp: 4, + parentId: 'root:s0', + payload: { + childId: 'root:s0:s0', + status: 'done', + outRef: 'sha256:out', + score: 0.9, + valid: true, + spent: { tokens: 123 }, + }, + }, + }, + third.digest, + ) - const view = projectPursuit([first, second, third]) + const view = projectPursuit([first, second, third, fourth]) expect(view.pursuitId).toBe('pursuit:test') - expect(view.sequence).toBe(3) - expect(view.chainTip).toBe(third.digest) + expect(view.sequence).toBe(4) + expect(view.chainTip).toBe(fourth.digest) expect(view.runs.map((run) => run.runId)).toEqual(['run:1', 'run:2']) expect(view.runs[0]?.decisions.continue).toBe(1) expect(view.nodes.map((node) => [node.id, node.parentId])).toEqual([ ['root:s0', 'root'], ['root:s0:s0', 'root:s0'], ]) + expect(view.nodes[1]).toMatchObject({ + status: 'done', + settledAt: 40, + outRef: 'sha256:out', + score: 0.9, + valid: true, + spent: { tokens: 123 }, + }) }) - it('refuses records from different pursuits', () => { + it('refuses mixed or tampered observer history before projecting it', () => { const first = record(1, { kind: 'event', event: { @@ -122,6 +154,12 @@ describe('projectPursuit', () => { ...secondUnsigned, digest: observerRecordDigest(secondUnsigned), } - expect(() => projectPursuit([first, second])).toThrow(/mixed pursuit journals/) + expect(() => projectPursuit([first, second])).toThrow(/pursuit/i) + + const tampered: ObserverRecord = { + ...first, + event: first.event ? { ...first.event, runId: 'run:forged' } : undefined, + } + expect(() => projectPursuit([tampered])).toThrow(/digest mismatch/) }) }) From a2891bb030a6ac80e8c52f800d94ee4fe214213a Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:22:14 -0700 Subject: [PATCH 11/33] fix(durable): scope observer node identity to Runtime run --- src/durable/observer-projection.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/durable/observer-projection.ts b/src/durable/observer-projection.ts index b7692ffd..87423c0c 100644 --- a/src/durable/observer-projection.ts +++ b/src/durable/observer-projection.ts @@ -18,6 +18,7 @@ export type PursuitNodeStatus = 'running' | 'done' | 'down' export interface PursuitNodeProjection { readonly id: string readonly parentId?: string + /** Node ids are scoped to this concrete Runtime tree; `(runId,id)` is identity. */ readonly runId: string readonly label?: string readonly runtime?: string @@ -100,8 +101,9 @@ type MutableNode = { * silently render a mutated or reordered observer history as trustworthy state. * * Topology comes only from Runtime's canonical `agent.spawn` facts. Terminal state - * comes only from Runtime's canonical `agent.child` facts. New event kinds remain - * visible through per-run counters without teaching this layer intellectual policy. + * comes only from Runtime's canonical `agent.child` facts. Node identity is scoped + * to the concrete Runtime run so two independent trees may both contain `root:s0` + * without aliasing in a long-lived pursuit. */ export function projectPursuit(records: readonly ObserverRecord[]): PursuitProjection { if (records.length === 0) { @@ -149,7 +151,12 @@ export function projectPursuit(records: readonly ObserverRecord[]): PursuitProje ), nodes: Object.freeze( [...nodes.values()] - .sort((a, b) => a.firstSequence - b.firstSequence || a.id.localeCompare(b.id)) + .sort( + (a, b) => + a.firstSequence - b.firstSequence || + a.runId.localeCompare(b.runId) || + a.id.localeCompare(b.id), + ) .map(freezeNode), ), eventCount, @@ -179,13 +186,18 @@ function getRun( return created } +function nodeKey(runId: string, nodeId: string): string { + return `${runId}\u0000${nodeId}` +} + function projectSpawnNode(nodes: Map, record: ObserverRecord): void { const event = record.event if (!event || event.target !== 'agent.spawn') return const payload = objectRecord(event.payload) const childId = stringField(payload, 'childId') if (!childId) return - const existing = nodes.get(childId) + const key = nodeKey(event.runId, childId) + const existing = nodes.get(key) if (existing) { existing.lastSequence = record.sequence existing.lastObservedAt = record.observedAt @@ -196,7 +208,7 @@ function projectSpawnNode(nodes: Map, record: ObserverRecor const runtime = stringField(payload, 'runtime') const depth = numberField(payload, 'depth') const assignmentId = stringField(payload, 'assignmentId') - nodes.set(childId, { + nodes.set(key, { id: childId, ...(event.parentId ? { parentId: event.parentId } : {}), runId: event.runId, @@ -224,7 +236,7 @@ function projectNodeActivity(nodes: Map, record: ObserverRe stringField(payload, 'nodeId') ?? stringField(payload, 'workerId') if (!nodeId) return - const node = nodes.get(nodeId) + const node = nodes.get(nodeKey(event.runId, nodeId)) if (!node) return node.lastSequence = record.sequence node.lastObservedAt = record.observedAt From 15345a10bf76d2bae99a8ca2470d283e2db701aa Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:22:32 -0700 Subject: [PATCH 12/33] test(durable): prove node ids may repeat across Runtime runs --- src/durable/tests/observer-projection.test.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/durable/tests/observer-projection.test.ts b/src/durable/tests/observer-projection.test.ts index 62cf6505..71995756 100644 --- a/src/durable/tests/observer-projection.test.ts +++ b/src/durable/tests/observer-projection.test.ts @@ -19,7 +19,7 @@ function record( } describe('projectPursuit', () => { - it('builds a multi-run recursive topology and terminal truth from substrate facts', () => { + it('keeps recursive topology and terminal truth isolated per concrete Runtime run', () => { const first = record(1, { kind: 'event', event: { @@ -55,6 +55,7 @@ describe('projectPursuit', () => { }, first.digest, ) + // A different top-level Runtime run may legitimately mint the same local node id. const third = record( 3, { @@ -66,12 +67,12 @@ describe('projectPursuit', () => { target: 'agent.spawn', phase: 'after', timestamp: 3, - parentId: 'root:s0', + parentId: 'root', payload: { - childId: 'root:s0:s0', + childId: 'root:s0', label: 'critic', runtime: 'bridge', - depth: 1, + depth: 0, }, }, }, @@ -88,9 +89,9 @@ describe('projectPursuit', () => { target: 'agent.child', phase: 'after', timestamp: 4, - parentId: 'root:s0', + parentId: 'root', payload: { - childId: 'root:s0:s0', + childId: 'root:s0', status: 'done', outRef: 'sha256:out', score: 0.9, @@ -108,10 +109,11 @@ describe('projectPursuit', () => { expect(view.chainTip).toBe(fourth.digest) expect(view.runs.map((run) => run.runId)).toEqual(['run:1', 'run:2']) expect(view.runs[0]?.decisions.continue).toBe(1) - expect(view.nodes.map((node) => [node.id, node.parentId])).toEqual([ - ['root:s0', 'root'], - ['root:s0:s0', 'root:s0'], + expect(view.nodes.map((node) => [node.runId, node.id, node.parentId])).toEqual([ + ['run:1', 'root:s0', 'root'], + ['run:2', 'root:s0', 'root'], ]) + expect(view.nodes[0]?.status).toBe('running') expect(view.nodes[1]).toMatchObject({ status: 'done', settledAt: 40, From c45814a15e1a97cd193462871b41bbb2e8cdb745 Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:24:35 -0700 Subject: [PATCH 13/33] feat(durable): add one-call supervised pursuit observer adapter --- src/durable/supervise-pursuit.ts | 82 ++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/durable/supervise-pursuit.ts diff --git a/src/durable/supervise-pursuit.ts b/src/durable/supervise-pursuit.ts new file mode 100644 index 00000000..95127363 --- /dev/null +++ b/src/durable/supervise-pursuit.ts @@ -0,0 +1,82 @@ +import { resolve } from 'node:path' +import type { SupervisorProfile } from '../runtime/supervise/supervisor-agent' +import { + supervise, + type SuperviseOptions, +} from '../runtime/supervise/supervise' +import { composeRuntimeHooks } from '../runtime-hooks' +import { createFileObserverHooks } from './observer-journal' +import { projectPursuit, type PursuitProjection } from './observer-projection' + +export interface SupervisePursuitOptions extends SuperviseOptions { + /** Stable objective identity spanning concrete Runtime runs. */ + readonly pursuitId: string + /** + * Shared durable observer file. Defaults under `runDir`. Supply an explicit + * path when one pursuit intentionally spans multiple run directories. + */ + readonly observerPath?: string +} + +export interface SupervisedPursuitResult { + readonly result: Result + readonly pursuit: PursuitProjection + readonly observerPath: string +} + +/** + * One-call durable pursuit execution over the canonical `supervise()` kernel. + * + * This is an adapter, not a second executor: it composes a durable third-person + * observer into Runtime's existing recursive hook stream and then rebuilds the + * operator projection after the same `supervise()` call settles. The agents never + * receive the observer path or projection and their behavior does not depend on it. + * + * A pursuit must have durable observer storage. `runDir` is the normal path; an + * explicit `observerPath` supports a pursuit that spans several concrete run dirs. + */ +export async function supervisePursuit( + profile: SupervisorProfile, + task: unknown, + opts: SupervisePursuitOptions, +): Promise>>> { + const pursuitId = opts.pursuitId.trim() + if (pursuitId.length === 0) { + throw new TypeError('supervisePursuit: pursuitId must be non-empty') + } + const observerPath = resolveObserverPath(opts) + const { pursuitId: _pursuitId, observerPath: _observerPath, hooks, ...superviseOptions } = opts + const observer = createFileObserverHooks(observerPath, pursuitId) + + const result = await supervise(profile, task, { + ...superviseOptions, + hooks: composeRuntimeHooks(hooks, observer.hooks), + }) + + // Runtime hook notifications are deliberately non-blocking for execution. `read()` + // joins the journal's serialized append tail here, so the returned projection covers + // every observer append that was scheduled by this execution before settlement. + const records = await observer.journal.read() + if (records.length === 0) { + throw new Error('supervisePursuit: execution produced no observer records') + } + return Object.freeze({ + result, + pursuit: projectPursuit(records), + observerPath, + }) +} + +function resolveObserverPath(opts: SupervisePursuitOptions): string { + if (opts.observerPath !== undefined) { + const path = opts.observerPath.trim() + if (path.length === 0) throw new TypeError('supervisePursuit: observerPath must be non-empty') + return resolve(path) + } + if (opts.runDir === undefined) { + throw new TypeError( + 'supervisePursuit: provide runDir or observerPath; pursuit observation must be durable', + ) + } + return resolve(opts.runDir, 'observer.jsonl') +} From 068f4052bebc9275ef6253c41006439ef2ef2fce Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:25:14 -0700 Subject: [PATCH 14/33] fix(durable): journal pursuit root lifecycle outside the agent environment --- src/durable/supervise-pursuit.ts | 65 ++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/src/durable/supervise-pursuit.ts b/src/durable/supervise-pursuit.ts index 95127363..cbc49b01 100644 --- a/src/durable/supervise-pursuit.ts +++ b/src/durable/supervise-pursuit.ts @@ -1,10 +1,7 @@ import { resolve } from 'node:path' import type { SupervisorProfile } from '../runtime/supervise/supervisor-agent' -import { - supervise, - type SuperviseOptions, -} from '../runtime/supervise/supervise' -import { composeRuntimeHooks } from '../runtime-hooks' +import { supervise, type SuperviseOptions } from '../runtime/supervise/supervise' +import { composeRuntimeHooks, type RuntimeHookEvent } from '../runtime-hooks' import { createFileObserverHooks } from './observer-journal' import { projectPursuit, type PursuitProjection } from './observer-projection' @@ -47,19 +44,45 @@ export async function supervisePursuit( const observerPath = resolveObserverPath(opts) const { pursuitId: _pursuitId, observerPath: _observerPath, hooks, ...superviseOptions } = opts const observer = createFileObserverHooks(observerPath, pursuitId) + const runId = superviseOptions.runId ?? 'supervise' + const now = superviseOptions.now ?? Date.now - const result = await supervise(profile, task, { - ...superviseOptions, - hooks: composeRuntimeHooks(hooks, observer.hooks), - }) + // Root lifecycle is an observer-plane fact, not something the manager has to + // narrate about itself. This also makes a zero-spawn/single-agent run observable. + await observer.journal.appendEvent(rootEvent(pursuitId, runId, 'before', now())) + + let result: Awaited> + try { + result = await supervise(profile, task, { + ...superviseOptions, + hooks: composeRuntimeHooks(hooks, observer.hooks), + }) + await observer.journal.appendEvent( + rootEvent(pursuitId, runId, 'after', now(), { status: 'done' }), + ) + } catch (error) { + // Best effort to preserve the failure fact. If the durable observer itself + // cannot record it, surface that observer failure as cause rather than hiding + // an integrity break behind the original execution error. + try { + await observer.journal.appendEvent( + rootEvent(pursuitId, runId, 'error', now(), { + status: 'failed', + error: error instanceof Error ? error.message : String(error), + }), + ) + } catch (observerError) { + throw new Error('supervisePursuit: execution failed and observer could not record failure', { + cause: observerError, + }) + } + throw error + } // Runtime hook notifications are deliberately non-blocking for execution. `read()` // joins the journal's serialized append tail here, so the returned projection covers // every observer append that was scheduled by this execution before settlement. const records = await observer.journal.read() - if (records.length === 0) { - throw new Error('supervisePursuit: execution produced no observer records') - } return Object.freeze({ result, pursuit: projectPursuit(records), @@ -67,6 +90,24 @@ export async function supervisePursuit( }) } +function rootEvent( + pursuitId: string, + runId: string, + phase: 'before' | 'after' | 'error', + timestamp: number, + payload?: Record, +): RuntimeHookEvent { + return Object.freeze({ + id: `${runId}:pursuit:${phase}:${timestamp}`, + pursuitId, + runId, + target: 'agent.run', + phase, + timestamp, + ...(payload ? { payload: Object.freeze({ ...payload }) } : {}), + }) +} + function resolveObserverPath(opts: SupervisePursuitOptions): string { if (opts.observerPath !== undefined) { const path = opts.observerPath.trim() From 2c392663c777c4327ddcbcabcc63ae1bbf2f1156 Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:25:32 -0700 Subject: [PATCH 15/33] feat(durable): export one-call pursuit supervision --- src/durable/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/durable/index.ts b/src/durable/index.ts index 4877d24f..049454c1 100644 --- a/src/durable/index.ts +++ b/src/durable/index.ts @@ -16,6 +16,8 @@ * for a pursuit, fed by Runtime's existing hook stream. * - `projectPursuit`: a rebuildable operator read model over that history; * it owns no execution or coordination semantics. + * - `supervisePursuit`: one-call adapter over the canonical `supervise()` + * executor that makes a stable pursuit durably observable. */ export type { @@ -39,10 +41,16 @@ export { } from './observer-journal' export { type PursuitNodeProjection, + type PursuitNodeStatus, type PursuitProjection, type PursuitRunProjection, projectPursuit, } from './observer-projection' +export { + supervisePursuit, + type SupervisedPursuitResult, + type SupervisePursuitOptions, +} from './supervise-pursuit' export { type DurableCoordinationStreamIdentity, type DurableSupervisionDiscovery, From f252c2d34f420fff1619a6f9c063982f34a8a21c Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:53:12 -0700 Subject: [PATCH 16/33] fix(durable): fail closed on incomplete observer journals --- src/durable/observer-journal.ts | 76 +++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 17 deletions(-) diff --git a/src/durable/observer-journal.ts b/src/durable/observer-journal.ts index 521a8fa5..f235bc64 100644 --- a/src/durable/observer-journal.ts +++ b/src/durable/observer-journal.ts @@ -12,7 +12,7 @@ import { parseCommittedJsonLines, prepareJsonlAppend, writeAllBytes } from './js export type ObserverRecordKind = 'event' | 'decision' /** - * One immutable record in the observer plane. `sequence` is observer order, not + * One immutable record in the observer plane. `sequence` is journal order, not * execution order; causal/runtime order remains available on the underlying event. * `previousDigest` + `digest` make deletion, reordering, or mutation detectable. */ @@ -38,12 +38,14 @@ export interface ObserverJournal { type UnsignedObserverRecord = Omit /** - * Durable, append-only third-person history for one pursuit. It consumes Runtime's - * existing hook stream and does not participate in execution decisions. A broken - * observer therefore cannot change what an agent is allowed to do. + * Durable, append-only third-person history for one concrete Runtime execution. + * It consumes Runtime's existing hook stream and does not participate in execution + * decisions. A broken observer therefore cannot change what an agent is allowed to do. * * The write discipline deliberately matches `FileSpawnJournal`: serialized appends, - * torn-tail recovery, short-write handling, and fsync before acknowledgement. + * torn-tail recovery, short-write handling, and fsync before acknowledgement. One + * execution owns one journal file; higher-level pursuit aggregation joins isolated + * journals by `pursuitId` instead of making independent processes share a write head. */ export class FileObserverJournal implements ObserverJournal { readonly path: string @@ -52,10 +54,12 @@ export class FileObserverJournal implements ObserverJournal { private initialized = false private sequence = 0 private previousDigest: string | undefined + private appendFailure: Error | undefined constructor(path: string, pursuitId: string) { const stableId = pursuitId.trim() - if (stableId.length === 0) throw new TypeError('FileObserverJournal: pursuitId must be non-empty') + if (stableId.length === 0) + throw new TypeError('FileObserverJournal: pursuitId must be non-empty') this.path = resolve(path) this.pursuitId = stableId } @@ -77,6 +81,7 @@ export class FileObserverJournal implements ObserverJournal { async read(): Promise { await this.tail + this.assertComplete() let text: string try { text = await readFile(this.path, 'utf8') @@ -94,13 +99,22 @@ export class FileObserverJournal implements ObserverJournal { kind: ObserverRecordKind, value: RuntimeHookEvent | RuntimeDecisionPoint, ): Promise { + if (value.pursuitId !== this.pursuitId) { + return Promise.reject( + new Error( + `FileObserverJournal: ${kind} pursuitId ${String(value.pursuitId)} does not match ${this.pursuitId}`, + ), + ) + } + let result: ObserverRecord | undefined const operation = this.tail.then(async () => { - await this.initialize() - if (value.pursuitId !== this.pursuitId) { - throw new Error( - `FileObserverJournal: ${kind} pursuitId ${String(value.pursuitId)} does not match ${this.pursuitId}`, - ) + this.assertComplete() + try { + await this.initialize() + } catch (error) { + this.appendFailure ??= toError(error) + throw error } const unsigned: UnsignedObserverRecord = { @@ -118,12 +132,20 @@ export class FileObserverJournal implements ObserverJournal { ...unsigned, digest: observerRecordDigest(unsigned), }) - await this.writeRecord(record) + try { + await this.writeRecord(record) + } catch (error) { + this.appendFailure ??= toError(error) + throw error + } this.sequence = record.sequence this.previousDigest = record.digest result = record }) - this.tail = operation.catch(() => undefined) + this.tail = operation.then( + () => undefined, + () => undefined, + ) return operation.then(() => { if (!result) throw new Error('FileObserverJournal: append completed without a record') return result @@ -166,6 +188,14 @@ export class FileObserverJournal implements ObserverJournal { await handle.close() } } + + private assertComplete(): void { + if (!this.appendFailure) return + throw new Error( + 'FileObserverJournal: a prior durable append failed; observer completeness is unknown', + { cause: this.appendFailure }, + ) + } } /** Verify identity, monotonic sequence, payload shape, and the complete digest chain. */ @@ -195,14 +225,19 @@ export function verifyObserverRecords( throw new Error(`observer journal: invalid decision payload at sequence ${record.sequence}`) } if (record.event !== undefined && record.event.pursuitId !== record.pursuitId) { - throw new Error(`observer journal: nested event pursuit mismatch at sequence ${record.sequence}`) + throw new Error( + `observer journal: nested event pursuit mismatch at sequence ${record.sequence}`, + ) } if (record.decision !== undefined && record.decision.pursuitId !== record.pursuitId) { - throw new Error(`observer journal: nested decision pursuit mismatch at sequence ${record.sequence}`) + throw new Error( + `observer journal: nested decision pursuit mismatch at sequence ${record.sequence}`, + ) } const { digest, ...unsigned } = record const expected = observerRecordDigest(unsigned) - if (digest !== expected) throw new Error(`observer journal: digest mismatch at sequence ${record.sequence}`) + if (digest !== expected) + throw new Error(`observer journal: digest mismatch at sequence ${record.sequence}`) previousDigest = digest expectedSequence += 1 } @@ -214,7 +249,10 @@ export function observerRecordDigest(record: UnsignedObserverRecord): string { } /** Build the canonical durable observer hook in one call. */ -export function createFileObserverHooks(path: string, pursuitId: string): { +export function createFileObserverHooks( + path: string, + pursuitId: string, +): { readonly journal: FileObserverJournal readonly hooks: RuntimeHooks } { @@ -230,3 +268,7 @@ function isNoEnt(error: unknown): boolean { (error as { code?: unknown }).code === 'ENOENT' ) } + +function toError(error: unknown): Error { + return error instanceof Error ? error : new Error(String(error)) +} From e7cfe6b63621c693dd27071f57071f3218288477 Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:54:08 -0700 Subject: [PATCH 17/33] feat(durable): project authoritative run lifecycle --- src/durable/observer-projection.ts | 61 ++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/src/durable/observer-projection.ts b/src/durable/observer-projection.ts index 87423c0c..376686de 100644 --- a/src/durable/observer-projection.ts +++ b/src/durable/observer-projection.ts @@ -1,8 +1,13 @@ import type { RuntimeDecisionKind, RuntimeHookTarget } from '../runtime-hooks' import { type ObserverRecord, verifyObserverRecords } from './observer-journal' +export type PursuitRunStatus = 'running' | 'done' | 'failed' + export interface PursuitRunProjection { readonly runId: string + readonly status: PursuitRunStatus + readonly settledAt?: number + readonly error?: string readonly firstSequence: number readonly lastSequence: number readonly firstObservedAt: number @@ -44,10 +49,12 @@ export interface PursuitNodeProjection { export interface PursuitProjection { readonly pursuitId: string + /** Number of records in this concrete execution journal. */ readonly sequence: number - readonly chainTip?: string - readonly firstObservedAt?: number - readonly lastObservedAt?: number + /** Digest-chain tip for this concrete execution journal. */ + readonly chainTip: string + readonly firstObservedAt: number + readonly lastObservedAt: number readonly runs: readonly PursuitRunProjection[] readonly nodes: readonly PursuitNodeProjection[] readonly eventCount: number @@ -56,6 +63,9 @@ export interface PursuitProjection { type MutableRun = { runId: string + status: PursuitRunStatus + settledAt?: number + error?: string firstSequence: number lastSequence: number firstObservedAt: number @@ -93,17 +103,17 @@ type MutableNode = { } /** - * Fold the append-only observer history into a deterministic operator projection. + * Fold one append-only execution journal into a deterministic operator projection. * * This is intentionally a READ model, not another state machine: it does not own * execution, cannot steer agents, and can be rebuilt from the journal at any time. * Projection verifies the complete hash chain first, so an operator view can never * silently render a mutated or reordered observer history as trustworthy state. * - * Topology comes only from Runtime's canonical `agent.spawn` facts. Terminal state - * comes only from Runtime's canonical `agent.child` facts. Node identity is scoped - * to the concrete Runtime run so two independent trees may both contain `root:s0` - * without aliasing in a long-lived pursuit. + * Topology comes only from Runtime's canonical `agent.spawn` facts. Terminal node + * state comes only from `agent.child`; concrete run state comes only from the root + * `agent.run` lifecycle emitted by `supervisePursuit`. Node identity is scoped to the + * concrete Runtime run so independent trees may both contain `root:s0` without aliasing. */ export function projectPursuit(records: readonly ObserverRecord[]): PursuitProjection { if (records.length === 0) { @@ -127,6 +137,7 @@ export function projectPursuit(records: readonly ObserverRecord[]): PursuitProje eventCount += 1 run.eventCount += 1 increment(run.targets, record.event.target) + projectRunActivity(run, record) projectSpawnNode(nodes, record) projectNodeActivity(nodes, record) } else if (record.decision) { @@ -164,15 +175,12 @@ export function projectPursuit(records: readonly ObserverRecord[]): PursuitProje }) } -function getRun( - runs: Map, - runId: string, - record: ObserverRecord, -): MutableRun { +function getRun(runs: Map, runId: string, record: ObserverRecord): MutableRun { const existing = runs.get(runId) if (existing) return existing const created: MutableRun = { runId, + status: 'running', firstSequence: record.sequence, lastSequence: record.sequence, firstObservedAt: record.observedAt, @@ -186,13 +194,30 @@ function getRun( return created } +function projectRunActivity(run: MutableRun, record: ObserverRecord): void { + const event = record.event + if (event?.target !== 'agent.run') return + const payload = objectRecord(event.payload) + const status = stringField(payload, 'status') + if (event.phase === 'after' || status === 'done') { + run.status = 'done' + run.settledAt = record.observedAt + return + } + if (event.phase !== 'error' && status !== 'failed') return + run.status = 'failed' + run.settledAt = record.observedAt + const error = stringField(payload, 'error') + if (error) run.error = error +} + function nodeKey(runId: string, nodeId: string): string { return `${runId}\u0000${nodeId}` } function projectSpawnNode(nodes: Map, record: ObserverRecord): void { const event = record.event - if (!event || event.target !== 'agent.spawn') return + if (event?.target !== 'agent.spawn') return const payload = objectRecord(event.payload) const childId = stringField(payload, 'childId') if (!childId) return @@ -229,7 +254,8 @@ function projectSpawnNode(nodes: Map, record: ObserverRecor function projectNodeActivity(nodes: Map, record: ObserverRecord): void { const event = record.event - if (!event || event.target === 'agent.spawn') return + if (event === undefined) return + if (event.target === 'agent.spawn') return const payload = objectRecord(event.payload) const nodeId = stringField(payload, 'childId') ?? @@ -296,7 +322,10 @@ function numberField(value: Record | undefined, key: string): n return typeof field === 'number' && Number.isFinite(field) ? field : undefined } -function booleanField(value: Record | undefined, key: string): boolean | undefined { +function booleanField( + value: Record | undefined, + key: string, +): boolean | undefined { const field = value?.[key] return typeof field === 'boolean' ? field : undefined } From b1b5dfe8786296d7e70bcab173d10ce6f1816b5f Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:54:53 -0700 Subject: [PATCH 18/33] fix(durable): isolate observer journals per runtime run --- src/durable/supervise-pursuit.ts | 107 ++++++++++++++++++------------- 1 file changed, 62 insertions(+), 45 deletions(-) diff --git a/src/durable/supervise-pursuit.ts b/src/durable/supervise-pursuit.ts index cbc49b01..776928c6 100644 --- a/src/durable/supervise-pursuit.ts +++ b/src/durable/supervise-pursuit.ts @@ -1,18 +1,23 @@ import { resolve } from 'node:path' +import { type SuperviseOptions, supervise } from '../runtime/supervise/supervise' import type { SupervisorProfile } from '../runtime/supervise/supervisor-agent' -import { supervise, type SuperviseOptions } from '../runtime/supervise/supervise' -import { composeRuntimeHooks, type RuntimeHookEvent } from '../runtime-hooks' +import { + composeRuntimeHooks, + type RuntimeHookEvent, + withPursuitContext, +} from '../runtime-hooks' import { createFileObserverHooks } from './observer-journal' -import { projectPursuit, type PursuitProjection } from './observer-projection' +import { type PursuitProjection, projectPursuit } from './observer-projection' export interface SupervisePursuitOptions extends SuperviseOptions { /** Stable objective identity spanning concrete Runtime runs. */ readonly pursuitId: string /** - * Shared durable observer file. Defaults under `runDir`. Supply an explicit - * path when one pursuit intentionally spans multiple run directories. + * One concrete Runtime execution owns one durable directory and observer journal. + * A pursuit spanning several runs reuses `pursuitId` across distinct `runDir`s; + * Intelligence joins those isolated projections without a shared write head. */ - readonly observerPath?: string + readonly runDir: string } export interface SupervisedPursuitResult { @@ -21,16 +26,30 @@ export interface SupervisedPursuitResult { readonly observerPath: string } +/** A failed Runtime execution whose complete third-person projection was retained. */ +export class SupervisePursuitError extends Error { + readonly pursuit: PursuitProjection + readonly observerPath: string + + constructor(cause: unknown, pursuit: PursuitProjection, observerPath: string) { + super(`supervisePursuit: ${errorMessage(cause)}`, { cause }) + this.name = 'SupervisePursuitError' + this.pursuit = pursuit + this.observerPath = observerPath + } +} + /** * One-call durable pursuit execution over the canonical `supervise()` kernel. * * This is an adapter, not a second executor: it composes a durable third-person * observer into Runtime's existing recursive hook stream and then rebuilds the - * operator projection after the same `supervise()` call settles. The agents never + * operator projection after the same `supervise()` call settles. Agents never * receive the observer path or projection and their behavior does not depend on it. * - * A pursuit must have durable observer storage. `runDir` is the normal path; an - * explicit `observerPath` supports a pursuit that spans several concrete run dirs. + * Every concrete execution writes only inside its own `runDir`. Cross-run pursuit + * aggregation is therefore lock-free at the observer layer: reuse `pursuitId` across + * run directories and let Intelligence join the independently verified projections. */ export async function supervisePursuit( profile: SupervisorProfile, @@ -41,8 +60,13 @@ export async function supervisePursuit( if (pursuitId.length === 0) { throw new TypeError('supervisePursuit: pursuitId must be non-empty') } - const observerPath = resolveObserverPath(opts) - const { pursuitId: _pursuitId, observerPath: _observerPath, hooks, ...superviseOptions } = opts + const runDir = opts.runDir.trim() + if (runDir.length === 0) { + throw new TypeError('supervisePursuit: runDir must be non-empty') + } + + const observerPath = resolve(runDir, 'observer.jsonl') + const { pursuitId: _pursuitId, hooks, ...superviseOptions } = opts const observer = createFileObserverHooks(observerPath, pursuitId) const runId = superviseOptions.runId ?? 'supervise' const now = superviseOptions.now ?? Date.now @@ -51,43 +75,46 @@ export async function supervisePursuit( // narrate about itself. This also makes a zero-spawn/single-agent run observable. await observer.journal.appendEvent(rootEvent(pursuitId, runId, 'before', now())) - let result: Awaited> try { - result = await supervise(profile, task, { + const result = await supervise(profile, task, { ...superviseOptions, - hooks: composeRuntimeHooks(hooks, observer.hooks), + // The observer runs first so a caller hook that throws cannot prevent the + // canonical lifecycle fact from entering the durable journal. + hooks: withPursuitContext( + pursuitId, + composeRuntimeHooks(observer.hooks, hooks), + ), }) await observer.journal.appendEvent( rootEvent(pursuitId, runId, 'after', now(), { status: 'done' }), ) + return Object.freeze({ + result, + pursuit: projectPursuit(await observer.journal.read()), + observerPath, + }) } catch (error) { - // Best effort to preserve the failure fact. If the durable observer itself - // cannot record it, surface that observer failure as cause rather than hiding - // an integrity break behind the original execution error. + let pursuit: PursuitProjection | undefined + let observerError: unknown try { await observer.journal.appendEvent( rootEvent(pursuitId, runId, 'error', now(), { status: 'failed', - error: error instanceof Error ? error.message : String(error), + error: errorMessage(error), }), ) - } catch (observerError) { - throw new Error('supervisePursuit: execution failed and observer could not record failure', { - cause: observerError, - }) + pursuit = projectPursuit(await observer.journal.read()) + } catch (failure) { + observerError = failure } - throw error + if (observerError !== undefined || pursuit === undefined) { + throw new Error( + 'supervisePursuit: Runtime failed and durable observer completeness could not be proven', + { cause: new AggregateError([error, observerError]) }, + ) + } + throw new SupervisePursuitError(error, pursuit, observerPath) } - - // Runtime hook notifications are deliberately non-blocking for execution. `read()` - // joins the journal's serialized append tail here, so the returned projection covers - // every observer append that was scheduled by this execution before settlement. - const records = await observer.journal.read() - return Object.freeze({ - result, - pursuit: projectPursuit(records), - observerPath, - }) } function rootEvent( @@ -108,16 +135,6 @@ function rootEvent( }) } -function resolveObserverPath(opts: SupervisePursuitOptions): string { - if (opts.observerPath !== undefined) { - const path = opts.observerPath.trim() - if (path.length === 0) throw new TypeError('supervisePursuit: observerPath must be non-empty') - return resolve(path) - } - if (opts.runDir === undefined) { - throw new TypeError( - 'supervisePursuit: provide runDir or observerPath; pursuit observation must be durable', - ) - } - return resolve(opts.runDir, 'observer.jsonl') +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error) } From a06bf7b951d7907b3e5184a245b0fa56749d67fb Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:55:18 -0700 Subject: [PATCH 19/33] chore(durable): expose verified pursuit lifecycle types --- src/durable/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/durable/index.ts b/src/durable/index.ts index 049454c1..3ff0f4d3 100644 --- a/src/durable/index.ts +++ b/src/durable/index.ts @@ -13,11 +13,11 @@ * - `discoverDurableSupervisionRun`: inspect a durable supervision directory * without already knowing the root/run identities written inside it. * - `FileObserverJournal`: tamper-evident, append-only third-person history - * for a pursuit, fed by Runtime's existing hook stream. + * for one concrete Runtime execution. * - `projectPursuit`: a rebuildable operator read model over that history; * it owns no execution or coordination semantics. - * - `supervisePursuit`: one-call adapter over the canonical `supervise()` - * executor that makes a stable pursuit durably observable. + * - `supervisePursuit`: one-call adapter over canonical `supervise()` that + * gives each isolated run a stable cross-run pursuit identity. */ export type { @@ -44,12 +44,14 @@ export { type PursuitNodeStatus, type PursuitProjection, type PursuitRunProjection, + type PursuitRunStatus, projectPursuit, } from './observer-projection' export { - supervisePursuit, type SupervisedPursuitResult, + SupervisePursuitError, type SupervisePursuitOptions, + supervisePursuit, } from './supervise-pursuit' export { type DurableCoordinationStreamIdentity, From c746b43d5496d617f0f023beae4bbb5aa47a1043 Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:56:00 -0700 Subject: [PATCH 20/33] test(durable): cover run lifecycle and scoped topology --- src/durable/tests/observer-projection.test.ts | 141 ++++++++++++++---- 1 file changed, 114 insertions(+), 27 deletions(-) diff --git a/src/durable/tests/observer-projection.test.ts b/src/durable/tests/observer-projection.test.ts index 71995756..135dd50d 100644 --- a/src/durable/tests/observer-projection.test.ts +++ b/src/durable/tests/observer-projection.test.ts @@ -19,28 +19,43 @@ function record( } describe('projectPursuit', () => { - it('keeps recursive topology and terminal truth isolated per concrete Runtime run', () => { + it('keeps run lifecycle and recursive terminal truth isolated per Runtime run', () => { const first = record(1, { kind: 'event', event: { - id: 'spawn-a', + id: 'run-a-before', pursuitId: 'pursuit:test', runId: 'run:1', - target: 'agent.spawn', - phase: 'after', + target: 'agent.run', + phase: 'before', timestamp: 1, - parentId: 'root', - payload: { - childId: 'root:s0', - label: 'researcher', - runtime: 'sandbox', - depth: 0, - identity: { candidateDigest: 'sha256:a' }, - }, }, }) const second = record( 2, + { + kind: 'event', + event: { + id: 'spawn-a', + pursuitId: 'pursuit:test', + runId: 'run:1', + target: 'agent.spawn', + phase: 'after', + timestamp: 2, + parentId: 'root', + payload: { + childId: 'root:s0', + label: 'researcher', + runtime: 'sandbox', + depth: 0, + identity: { candidateDigest: 'sha256:a' }, + }, + }, + }, + first.digest, + ) + const third = record( + 3, { kind: 'decision', decision: { @@ -53,11 +68,26 @@ describe('projectPursuit', () => { evidence: [], }, }, - first.digest, + second.digest, + ) + const fourth = record( + 4, + { + kind: 'event', + event: { + id: 'run-b-before', + pursuitId: 'pursuit:test', + runId: 'run:2', + target: 'agent.run', + phase: 'before', + timestamp: 4, + }, + }, + third.digest, ) // A different top-level Runtime run may legitimately mint the same local node id. - const third = record( - 3, + const fifth = record( + 5, { kind: 'event', event: { @@ -66,7 +96,7 @@ describe('projectPursuit', () => { runId: 'run:2', target: 'agent.spawn', phase: 'after', - timestamp: 3, + timestamp: 5, parentId: 'root', payload: { childId: 'root:s0', @@ -76,10 +106,10 @@ describe('projectPursuit', () => { }, }, }, - second.digest, + fourth.digest, ) - const fourth = record( - 4, + const sixth = record( + 6, { kind: 'event', event: { @@ -88,7 +118,7 @@ describe('projectPursuit', () => { runId: 'run:2', target: 'agent.child', phase: 'after', - timestamp: 4, + timestamp: 6, parentId: 'root', payload: { childId: 'root:s0', @@ -100,15 +130,35 @@ describe('projectPursuit', () => { }, }, }, - third.digest, + fifth.digest, + ) + const seventh = record( + 7, + { + kind: 'event', + event: { + id: 'run-b-after', + pursuitId: 'pursuit:test', + runId: 'run:2', + target: 'agent.run', + phase: 'after', + timestamp: 7, + payload: { status: 'done' }, + }, + }, + sixth.digest, ) - const view = projectPursuit([first, second, third, fourth]) + const view = projectPursuit([first, second, third, fourth, fifth, sixth, seventh]) expect(view.pursuitId).toBe('pursuit:test') - expect(view.sequence).toBe(4) - expect(view.chainTip).toBe(fourth.digest) - expect(view.runs.map((run) => run.runId)).toEqual(['run:1', 'run:2']) + expect(view.sequence).toBe(7) + expect(view.chainTip).toBe(seventh.digest) + expect(view.runs.map((run) => [run.runId, run.status])).toEqual([ + ['run:1', 'running'], + ['run:2', 'done'], + ]) expect(view.runs[0]?.decisions.continue).toBe(1) + expect(view.runs[1]?.settledAt).toBe(70) expect(view.nodes.map((node) => [node.runId, node.id, node.parentId])).toEqual([ ['run:1', 'root:s0', 'root'], ['run:2', 'root:s0', 'root'], @@ -116,7 +166,7 @@ describe('projectPursuit', () => { expect(view.nodes[0]?.status).toBe('running') expect(view.nodes[1]).toMatchObject({ status: 'done', - settledAt: 40, + settledAt: 60, outRef: 'sha256:out', score: 0.9, valid: true, @@ -124,6 +174,43 @@ describe('projectPursuit', () => { }) }) + it('projects an authoritative root failure without treating a child as the pursuit verdict', () => { + const first = record(1, { + kind: 'event', + event: { + id: 'before', + pursuitId: 'pursuit:test', + runId: 'run:failed', + target: 'agent.run', + phase: 'before', + timestamp: 1, + }, + }) + const second = record( + 2, + { + kind: 'event', + event: { + id: 'error', + pursuitId: 'pursuit:test', + runId: 'run:failed', + target: 'agent.run', + phase: 'error', + timestamp: 2, + payload: { status: 'failed', error: 'driver crashed' }, + }, + }, + first.digest, + ) + + expect(projectPursuit([first, second]).runs[0]).toMatchObject({ + runId: 'run:failed', + status: 'failed', + settledAt: 20, + error: 'driver crashed', + }) + }) + it('refuses mixed or tampered observer history before projecting it', () => { const first = record(1, { kind: 'event', @@ -160,7 +247,7 @@ describe('projectPursuit', () => { const tampered: ObserverRecord = { ...first, - event: first.event ? { ...first.event, runId: 'run:forged' } : undefined, + event: { ...first.event!, runId: 'run:forged' }, } expect(() => projectPursuit([tampered])).toThrow(/digest mismatch/) }) From 3be10fadb34bc488c62a85a5120a731c050461a2 Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 09:56:27 -0700 Subject: [PATCH 21/33] test(durable): refuse incomplete observer projections --- src/durable/tests/observer-journal.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/durable/tests/observer-journal.test.ts b/src/durable/tests/observer-journal.test.ts index a68e97a0..ba75f076 100644 --- a/src/durable/tests/observer-journal.test.ts +++ b/src/durable/tests/observer-journal.test.ts @@ -17,7 +17,7 @@ function event(id: string, pursuitId = 'pursuit:test') { } describe('FileObserverJournal', () => { - it('persists one pursuit across multiple run events with a verified chain', async () => { + it('persists one execution journal with a verified chain', async () => { const dir = await mkdtemp(join(tmpdir(), 'runtime-observer-')) const path = join(dir, 'observer.jsonl') const journal = new FileObserverJournal(path, 'pursuit:test') @@ -81,4 +81,14 @@ describe('FileObserverJournal', () => { /digest mismatch/, ) }) + + it('never returns a trusted projection after a durable append failure', async () => { + const dir = await mkdtemp(join(tmpdir(), 'runtime-observer-write-failure-')) + const blocker = join(dir, 'not-a-directory') + await writeFile(blocker, 'block', 'utf8') + const journal = new FileObserverJournal(join(blocker, 'observer.jsonl'), 'pursuit:one') + + await expect(journal.appendEvent(event('cannot-write', 'pursuit:one'))).rejects.toThrow() + await expect(journal.read()).rejects.toThrow(/completeness is unknown/) + }) }) From d641acd3762f86b525b68a508ef91bc3b0061a36 Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 10:11:12 -0700 Subject: [PATCH 22/33] chore(durable): satisfy canonical export ordering --- src/durable/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/durable/index.ts b/src/durable/index.ts index 3ff0f4d3..a7e9a3f6 100644 --- a/src/durable/index.ts +++ b/src/durable/index.ts @@ -48,8 +48,8 @@ export { projectPursuit, } from './observer-projection' export { - type SupervisedPursuitResult, SupervisePursuitError, + type SupervisedPursuitResult, type SupervisePursuitOptions, supervisePursuit, } from './supervise-pursuit' From 36ed4e7429c9f1b722232e112754d2755894ede4 Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 10:11:42 -0700 Subject: [PATCH 23/33] chore(durable): organize pursuit adapter imports --- src/durable/supervise-pursuit.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/durable/supervise-pursuit.ts b/src/durable/supervise-pursuit.ts index 776928c6..df75c860 100644 --- a/src/durable/supervise-pursuit.ts +++ b/src/durable/supervise-pursuit.ts @@ -2,8 +2,8 @@ import { resolve } from 'node:path' import { type SuperviseOptions, supervise } from '../runtime/supervise/supervise' import type { SupervisorProfile } from '../runtime/supervise/supervisor-agent' import { - composeRuntimeHooks, type RuntimeHookEvent, + composeRuntimeHooks, withPursuitContext, } from '../runtime-hooks' import { createFileObserverHooks } from './observer-journal' @@ -108,9 +108,10 @@ export async function supervisePursuit( observerError = failure } if (observerError !== undefined || pursuit === undefined) { + const causes = observerError === undefined ? [error] : [error, observerError] throw new Error( 'supervisePursuit: Runtime failed and durable observer completeness could not be proven', - { cause: new AggregateError([error, observerError]) }, + { cause: new AggregateError(causes) }, ) } throw new SupervisePursuitError(error, pursuit, observerPath) From 52eabff1e9b8bf77b1b9bcc5fde282fd245136e7 Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 10:12:34 -0700 Subject: [PATCH 24/33] chore(durable): format observer journal --- src/durable/observer-journal.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/durable/observer-journal.ts b/src/durable/observer-journal.ts index f235bc64..dfe42aaf 100644 --- a/src/durable/observer-journal.ts +++ b/src/durable/observer-journal.ts @@ -58,8 +58,9 @@ export class FileObserverJournal implements ObserverJournal { constructor(path: string, pursuitId: string) { const stableId = pursuitId.trim() - if (stableId.length === 0) + if (stableId.length === 0) { throw new TypeError('FileObserverJournal: pursuitId must be non-empty') + } this.path = resolve(path) this.pursuitId = stableId } @@ -236,8 +237,9 @@ export function verifyObserverRecords( } const { digest, ...unsigned } = record const expected = observerRecordDigest(unsigned) - if (digest !== expected) + if (digest !== expected) { throw new Error(`observer journal: digest mismatch at sequence ${record.sequence}`) + } previousDigest = digest expectedSequence += 1 } From 9acf168f31938152326422b76e8f5e5ac093f59d Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 10:13:20 -0700 Subject: [PATCH 25/33] chore(durable): format pursuit projection --- src/durable/observer-projection.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/durable/observer-projection.ts b/src/durable/observer-projection.ts index 376686de..fed01af7 100644 --- a/src/durable/observer-projection.ts +++ b/src/durable/observer-projection.ts @@ -175,7 +175,11 @@ export function projectPursuit(records: readonly ObserverRecord[]): PursuitProje }) } -function getRun(runs: Map, runId: string, record: ObserverRecord): MutableRun { +function getRun( + runs: Map, + runId: string, + record: ObserverRecord, +): MutableRun { const existing = runs.get(runId) if (existing) return existing const created: MutableRun = { From 8dc67cbdbfdb8d182bb31f9c693f4fa80a8d2b06 Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 10:24:44 -0700 Subject: [PATCH 26/33] chore(durable): apply canonical Biome formatting --- src/durable/index.ts | 2 +- src/durable/observer-projection.ts | 6 +----- src/durable/supervise-pursuit.ts | 11 ++--------- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/durable/index.ts b/src/durable/index.ts index a7e9a3f6..3ff0f4d3 100644 --- a/src/durable/index.ts +++ b/src/durable/index.ts @@ -48,8 +48,8 @@ export { projectPursuit, } from './observer-projection' export { - SupervisePursuitError, type SupervisedPursuitResult, + SupervisePursuitError, type SupervisePursuitOptions, supervisePursuit, } from './supervise-pursuit' diff --git a/src/durable/observer-projection.ts b/src/durable/observer-projection.ts index fed01af7..376686de 100644 --- a/src/durable/observer-projection.ts +++ b/src/durable/observer-projection.ts @@ -175,11 +175,7 @@ export function projectPursuit(records: readonly ObserverRecord[]): PursuitProje }) } -function getRun( - runs: Map, - runId: string, - record: ObserverRecord, -): MutableRun { +function getRun(runs: Map, runId: string, record: ObserverRecord): MutableRun { const existing = runs.get(runId) if (existing) return existing const created: MutableRun = { diff --git a/src/durable/supervise-pursuit.ts b/src/durable/supervise-pursuit.ts index df75c860..24876732 100644 --- a/src/durable/supervise-pursuit.ts +++ b/src/durable/supervise-pursuit.ts @@ -1,11 +1,7 @@ import { resolve } from 'node:path' import { type SuperviseOptions, supervise } from '../runtime/supervise/supervise' import type { SupervisorProfile } from '../runtime/supervise/supervisor-agent' -import { - type RuntimeHookEvent, - composeRuntimeHooks, - withPursuitContext, -} from '../runtime-hooks' +import { composeRuntimeHooks, type RuntimeHookEvent, withPursuitContext } from '../runtime-hooks' import { createFileObserverHooks } from './observer-journal' import { type PursuitProjection, projectPursuit } from './observer-projection' @@ -80,10 +76,7 @@ export async function supervisePursuit( ...superviseOptions, // The observer runs first so a caller hook that throws cannot prevent the // canonical lifecycle fact from entering the durable journal. - hooks: withPursuitContext( - pursuitId, - composeRuntimeHooks(observer.hooks, hooks), - ), + hooks: withPursuitContext(pursuitId, composeRuntimeHooks(observer.hooks, hooks)), }) await observer.journal.appendEvent( rootEvent(pursuitId, runId, 'after', now(), { status: 'done' }), From 62b819f38639185ea1b97912a8e581af64dfb3ba Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 10:45:37 -0700 Subject: [PATCH 27/33] ci: add temporary API reference repair job --- .github/workflows/repair-api-docs.yml | 41 +++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/repair-api-docs.yml diff --git a/.github/workflows/repair-api-docs.yml b/.github/workflows/repair-api-docs.yml new file mode 100644 index 00000000..fab7ce6a --- /dev/null +++ b/.github/workflows/repair-api-docs.yml @@ -0,0 +1,41 @@ +name: Repair API docs + +on: + push: + branches: + - codex/pursuit-observer-plane + +permissions: + contents: write + +jobs: + repair: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Regenerate API reference + run: pnpm run docs:api + + - name: Commit generated reference + run: | + if git diff --quiet -- docs/api; then + echo "API reference already current" + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add docs/api + git commit -m "docs(api): regenerate pursuit observer reference" + git push From 9855f73ad6c761edd800c855dd00ea9a22d7f482 Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 10:47:38 -0700 Subject: [PATCH 28/33] ci: preserve API generator output as artifact --- .github/workflows/repair-api-docs.yml | 37 ++++++++++++++++----------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/.github/workflows/repair-api-docs.yml b/.github/workflows/repair-api-docs.yml index fab7ce6a..d729535e 100644 --- a/.github/workflows/repair-api-docs.yml +++ b/.github/workflows/repair-api-docs.yml @@ -6,11 +6,10 @@ on: - codex/pursuit-observer-plane permissions: - contents: write + contents: read jobs: repair: - if: github.actor != 'github-actions[bot]' runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -25,17 +24,25 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Regenerate API reference - run: pnpm run docs:api - - - name: Commit generated reference + - name: Regenerate API reference and capture output run: | - if git diff --quiet -- docs/api; then - echo "API reference already current" - exit 0 - fi - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add docs/api - git commit -m "docs(api): regenerate pursuit observer reference" - git push + mkdir -p diagnostics + set +e + pnpm run docs:api > diagnostics/docs-api.log 2>&1 + status=$? + set -e + { + echo + echo "exit_status=$status" + } >> diagnostics/docs-api.log + cat diagnostics/docs-api.log + + - name: Upload generated reference and diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: runtime-api-docs-diagnostic + path: | + diagnostics/docs-api.log + docs/api + if-no-files-found: error From 31e7c0c88a47122a64bf6c1024ce22ba953a4e70 Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 10:50:43 -0700 Subject: [PATCH 29/33] ci: generate docs with self-describing digest signature --- .github/workflows/repair-api-docs.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/repair-api-docs.yml b/.github/workflows/repair-api-docs.yml index d729535e..f179ebff 100644 --- a/.github/workflows/repair-api-docs.yml +++ b/.github/workflows/repair-api-docs.yml @@ -24,6 +24,19 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Make exported digest parameter self-describing + run: | + node --input-type=module <<'NODE' + import { readFile, writeFile } from 'node:fs/promises' + const path = 'src/durable/observer-journal.ts' + const source = await readFile(path, 'utf8') + const before = 'export function observerRecordDigest(record: UnsignedObserverRecord): string {' + const after = "export function observerRecordDigest(record: Omit): string {" + if (!source.includes(before)) throw new Error('expected digest signature was not found') + await writeFile(path, source.replace(before, after)) + NODE + pnpm biome check --write src/durable/observer-journal.ts + - name: Regenerate API reference and capture output run: | mkdir -p diagnostics @@ -37,12 +50,13 @@ jobs: } >> diagnostics/docs-api.log cat diagnostics/docs-api.log - - name: Upload generated reference and diagnostics + - name: Upload source repair, generated reference, and diagnostics if: always() uses: actions/upload-artifact@v4 with: name: runtime-api-docs-diagnostic path: | + src/durable/observer-journal.ts diagnostics/docs-api.log docs/api if-no-files-found: error From b0a696dc3fcbefeab6c0896a9e225c6f78ce4edc Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 10:55:44 -0700 Subject: [PATCH 30/33] ci: validate documented observer digest API --- .github/workflows/repair-api-docs.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/repair-api-docs.yml b/.github/workflows/repair-api-docs.yml index f179ebff..9a5b5ee5 100644 --- a/.github/workflows/repair-api-docs.yml +++ b/.github/workflows/repair-api-docs.yml @@ -24,14 +24,15 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Make exported digest parameter self-describing + - name: Publish a documented digest parameter shape run: | node --input-type=module <<'NODE' import { readFile, writeFile } from 'node:fs/promises' const path = 'src/durable/observer-journal.ts' const source = await readFile(path, 'utf8') const before = 'export function observerRecordDigest(record: UnsignedObserverRecord): string {' - const after = "export function observerRecordDigest(record: Omit): string {" + const after = `/** Compute the canonical SHA-256 digest for an unsigned observer record. */ + export function observerRecordDigest(record: Omit): string {` if (!source.includes(before)) throw new Error('expected digest signature was not found') await writeFile(path, source.replace(before, after)) NODE From c4de16ea3c3707ce35e3b2d035fc4ceea94b003a Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 10:59:24 -0700 Subject: [PATCH 31/33] ci: finalize generated pursuit observer API reference --- .github/workflows/repair-api-docs.yml | 39 ++++++++++----------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/.github/workflows/repair-api-docs.yml b/.github/workflows/repair-api-docs.yml index 9a5b5ee5..8179ec88 100644 --- a/.github/workflows/repair-api-docs.yml +++ b/.github/workflows/repair-api-docs.yml @@ -1,4 +1,4 @@ -name: Repair API docs +name: Finalize pursuit observer API docs on: push: @@ -6,10 +6,11 @@ on: - codex/pursuit-observer-plane permissions: - contents: read + contents: write jobs: - repair: + finalize: + if: github.actor != 'github-actions[bot]' runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -38,26 +39,14 @@ jobs: NODE pnpm biome check --write src/durable/observer-journal.ts - - name: Regenerate API reference and capture output + - name: Regenerate and verify API reference + run: pnpm run docs:api + + - name: Commit canonical source and generated reference run: | - mkdir -p diagnostics - set +e - pnpm run docs:api > diagnostics/docs-api.log 2>&1 - status=$? - set -e - { - echo - echo "exit_status=$status" - } >> diagnostics/docs-api.log - cat diagnostics/docs-api.log - - - name: Upload source repair, generated reference, and diagnostics - if: always() - uses: actions/upload-artifact@v4 - with: - name: runtime-api-docs-diagnostic - path: | - src/durable/observer-journal.ts - diagnostics/docs-api.log - docs/api - if-no-files-found: error + rm .github/workflows/repair-api-docs.yml + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add src/durable/observer-journal.ts docs/api .github/workflows/repair-api-docs.yml + git commit -m "fix(durable): publish digest shape and regenerate API docs" + git push origin HEAD:codex/pursuit-observer-plane From 64370d07914b7fbbc415ee09a01262d48bf87dc2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:00:06 +0000 Subject: [PATCH 32/33] fix(durable): publish digest shape and regenerate API docs --- .github/workflows/repair-api-docs.yml | 52 - docs/api/durable.md | 1496 +++++++++++++++++++++++-- docs/api/index.md | 17 + docs/api/primitive-catalog.md | 12 +- docs/api/runtime.md | 1 + src/durable/observer-journal.ts | 3 +- 6 files changed, 1459 insertions(+), 122 deletions(-) delete mode 100644 .github/workflows/repair-api-docs.yml diff --git a/.github/workflows/repair-api-docs.yml b/.github/workflows/repair-api-docs.yml deleted file mode 100644 index 8179ec88..00000000 --- a/.github/workflows/repair-api-docs.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Finalize pursuit observer API docs - -on: - push: - branches: - - codex/pursuit-observer-plane - -permissions: - contents: write - -jobs: - finalize: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 22 - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Publish a documented digest parameter shape - run: | - node --input-type=module <<'NODE' - import { readFile, writeFile } from 'node:fs/promises' - const path = 'src/durable/observer-journal.ts' - const source = await readFile(path, 'utf8') - const before = 'export function observerRecordDigest(record: UnsignedObserverRecord): string {' - const after = `/** Compute the canonical SHA-256 digest for an unsigned observer record. */ - export function observerRecordDigest(record: Omit): string {` - if (!source.includes(before)) throw new Error('expected digest signature was not found') - await writeFile(path, source.replace(before, after)) - NODE - pnpm biome check --write src/durable/observer-journal.ts - - - name: Regenerate and verify API reference - run: pnpm run docs:api - - - name: Commit canonical source and generated reference - run: | - rm .github/workflows/repair-api-docs.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add src/durable/observer-journal.ts docs/api .github/workflows/repair-api-docs.yml - git commit -m "fix(durable): publish digest shape and regenerate API docs" - git push origin HEAD:codex/pursuit-observer-plane diff --git a/docs/api/durable.md b/docs/api/durable.md index b9ab24e7..e9942b86 100644 --- a/docs/api/durable.md +++ b/docs/api/durable.md @@ -6,6 +6,163 @@ # durable +## Classes + +### FileObserverJournal + +Durable, append-only third-person history for one concrete Runtime execution. +It consumes Runtime's existing hook stream and does not participate in execution +decisions. A broken observer therefore cannot change what an agent is allowed to do. + +The write discipline deliberately matches `FileSpawnJournal`: serialized appends, +torn-tail recovery, short-write handling, and fsync before acknowledgement. One +execution owns one journal file; higher-level pursuit aggregation joins isolated +journals by `pursuitId` instead of making independent processes share a write head. + +#### Implements + +- [`ObserverJournal`](#observerjournal) + +#### Constructors + +##### Constructor + +> **new FileObserverJournal**(`path`, `pursuitId`): [`FileObserverJournal`](#fileobserverjournal) + +###### Parameters + +###### path + +`string` + +###### pursuitId + +`string` + +###### Returns + +[`FileObserverJournal`](#fileobserverjournal) + +#### Properties + +##### path + +> `readonly` **path**: `string` + +##### pursuitId + +> `readonly` **pursuitId**: `string` + +#### Methods + +##### hooks() + +> **hooks**(): [`RuntimeHooks`](index.md#runtimehooks) + +###### Returns + +[`RuntimeHooks`](index.md#runtimehooks) + +###### Implementation of + +[`ObserverJournal`](#observerjournal).[`hooks`](#hooks-1) + +##### appendEvent() + +> **appendEvent**(`event`): `Promise`\<[`ObserverRecord`](#observerrecord)\> + +###### Parameters + +###### event + +[`RuntimeHookEvent`](index.md#runtimehookevent) + +###### Returns + +`Promise`\<[`ObserverRecord`](#observerrecord)\> + +###### Implementation of + +[`ObserverJournal`](#observerjournal).[`appendEvent`](#appendevent) + +##### appendDecision() + +> **appendDecision**(`point`): `Promise`\<[`ObserverRecord`](#observerrecord)\> + +###### Parameters + +###### point + +[`RuntimeDecisionPoint`](index.md#runtimedecisionpoint) + +###### Returns + +`Promise`\<[`ObserverRecord`](#observerrecord)\> + +###### Implementation of + +[`ObserverJournal`](#observerjournal).[`appendDecision`](#appenddecision) + +##### read() + +> **read**(): `Promise`\ + +###### Returns + +`Promise`\ + +###### Implementation of + +[`ObserverJournal`](#observerjournal).[`read`](#read) + +*** + +### SupervisePursuitError + +A failed Runtime execution whose complete third-person projection was retained. + +#### Extends + +- `Error` + +#### Constructors + +##### Constructor + +> **new SupervisePursuitError**(`cause`, `pursuit`, `observerPath`): [`SupervisePursuitError`](#supervisepursuiterror) + +###### Parameters + +###### cause + +`unknown` + +###### pursuit + +[`PursuitProjection`](#pursuitprojection) + +###### observerPath + +`string` + +###### Returns + +[`SupervisePursuitError`](#supervisepursuiterror) + +###### Overrides + +`Error.constructor` + +#### Properties + +##### pursuit + +> `readonly` **pursuit**: [`PursuitProjection`](#pursuitprojection) + +##### observerPath + +> `readonly` **observerPath**: `string` + ## Interfaces ### ChatStreamEvent @@ -271,132 +428,1337 @@ Content type for the response. *** -### DurableCoordinationStreamIdentity +### ObserverRecord + +One immutable record in the observer plane. `sequence` is journal order, not +execution order; causal/runtime order remains available on the underlying event. +`previousDigest` + `digest` make deletion, reordering, or mutation detectable. #### Properties -##### runId +##### schemaVersion -> `readonly` **runId**: `string` +> `readonly` **schemaVersion**: `1` -##### ownerIds +##### pursuitId -> `readonly` **ownerIds**: readonly `string`[] +> `readonly` **pursuitId**: `string` -Exact owner ids present in the side-log, sorted for deterministic display. +##### sequence -##### unscopedRecords +> `readonly` **sequence**: `number` -> `readonly` **unscopedRecords**: `number` +##### kind -Records written before owner-scoped coordination identities were introduced. +> `readonly` **kind**: [`ObserverRecordKind`](#observerrecordkind) -##### recordCount +##### observedAt -> `readonly` **recordCount**: `number` +> `readonly` **observedAt**: `number` + +##### previousDigest? + +> `readonly` `optional` **previousDigest?**: `string` + +##### event? + +> `readonly` `optional` **event?**: [`RuntimeHookEvent`](index.md#runtimehookevent)\<`unknown`\> + +##### decision? + +> `readonly` `optional` **decision?**: [`RuntimeDecisionPoint`](index.md#runtimedecisionpoint) + +##### digest + +> `readonly` **digest**: `string` *** -### DurableSupervisionDiscovery +### ObserverJournal -Identities discoverable from one `supervise({ runDir })` directory without -already knowing the root node or coordination run id stored inside it. +#### Methods + +##### appendEvent() + +> **appendEvent**(`event`): `Promise`\<[`ObserverRecord`](#observerrecord)\> + +###### Parameters + +###### event + +[`RuntimeHookEvent`](index.md#runtimehookevent) + +###### Returns + +`Promise`\<[`ObserverRecord`](#observerrecord)\> + +##### appendDecision() + +> **appendDecision**(`point`): `Promise`\<[`ObserverRecord`](#observerrecord)\> + +###### Parameters + +###### point + +[`RuntimeDecisionPoint`](index.md#runtimedecisionpoint) + +###### Returns + +`Promise`\<[`ObserverRecord`](#observerrecord)\> + +##### read() + +> **read**(): `Promise`\ + +###### Returns + +`Promise`\ + +##### hooks() + +> **hooks**(): [`RuntimeHooks`](index.md#runtimehooks) + +###### Returns + +[`RuntimeHooks`](index.md#runtimehooks) + +*** + +### PursuitRunProjection #### Properties -##### runDir +##### runId -> `readonly` **runDir**: `string` +> `readonly` **runId**: `string` -##### spawnJournalPath +##### status -> `readonly` **spawnJournalPath**: `string` +> `readonly` **status**: [`PursuitRunStatus`](#pursuitrunstatus) -##### coordinationLogPath +##### settledAt? -> `readonly` **coordinationLogPath**: `string` +> `readonly` `optional` **settledAt?**: `number` -##### roots +##### error? -> `readonly` **roots**: readonly `string`[] +> `readonly` `optional` **error?**: `string` -##### coordinationStreams +##### firstSequence -> `readonly` **coordinationStreams**: readonly [`DurableCoordinationStreamIdentity`](#durablecoordinationstreamidentity)[] +> `readonly` **firstSequence**: `number` -## Functions +##### lastSequence -### handleChatTurn() +> `readonly` **lastSequence**: `number` -> **handleChatTurn**(`input`): [`ChatTurnResult`](#chatturnresult) +##### firstObservedAt -Run one chat turn. Returns immediately with a `ReadableStream` body; -execution starts while the stream is constructed. Backend -failures surface as `error` + `session.run.failed` events. +> `readonly` **firstObservedAt**: `number` -#### Parameters +##### lastObservedAt -##### input +> `readonly` **lastObservedAt**: `number` -[`RunChatTurnInput`](#runchatturninput) +##### eventCount -#### Returns +> `readonly` **eventCount**: `number` -[`ChatTurnResult`](#chatturnresult) +##### decisionCount + +> `readonly` **decisionCount**: `number` + +##### targets + +> `readonly` **targets**: `Readonly`\<`Record`\<`string`, `number`\>\> + +##### decisions + +> `readonly` **decisions**: `Readonly`\<`Record`\<`string`, `number`\>\> *** -### deriveExecutionId() +### PursuitNodeProjection -> **deriveExecutionId**(`input`): `string` +#### Properties -Derive a stable execution id from the run identity. -The same `(projectId, sessionId, turnIndex)` tuple yields the same id. +##### id -Use the result as both `PromptOptions.executionId` and -`PromptOptions.turnId` on the first dispatch. -The execution id addresses the server-side execution for reconnect and -replay; the turn id makes a repeated dispatch idempotent. -An execution id alone does not make a repeated POST idempotent. +> `readonly` **id**: `string` -Format is readable, not hashed: operators grepping orchestrator logs -for `gtm-agent:thread-abc:3` find the run without translating an -opaque id. Components are URL-encoded so delimiters inside caller ids -cannot collapse distinct tuples. The final id is limited to the -orchestrator replay route's 256-byte maximum. Execution ids are not a -secrecy boundary. +##### parentId? -Wire integration: - - Initial dispatch: pass the result as `executionId` and `turnId`. - - Stream replay: pass it as `executionId` with `lastEventId`. +> `readonly` `optional` **parentId?**: `string` -#### Parameters +##### runId -##### input +> `readonly` **runId**: `string` -###### projectId +Node ids are scoped to this concrete Runtime tree; `(runId,id)` is identity. -`string` +##### label? -###### sessionId +> `readonly` `optional` **label?**: `string` -`string` +##### runtime? -###### turnIndex +> `readonly` `optional` **runtime?**: `string` -`number` +##### depth? -#### Returns +> `readonly` `optional` **depth?**: `number` -`string` +##### assignmentId? -#### Throws +> `readonly` `optional` **assignmentId?**: `string` -`TypeError` when either string id is blank. +##### identity? -#### Throws +> `readonly` `optional` **identity?**: `unknown` -`RangeError` when `turnIndex` is invalid or the result exceeds 256 bytes. +##### budget? + +> `readonly` `optional` **budget?**: `unknown` + +##### status + +> `readonly` **status**: [`PursuitNodeStatus`](#pursuitnodestatus) + +##### settledAt? + +> `readonly` `optional` **settledAt?**: `number` + +##### spent? + +> `readonly` `optional` **spent?**: `unknown` + +##### outRef? + +> `readonly` `optional` **outRef?**: `string` + +##### score? + +> `readonly` `optional` **score?**: `number` + +##### valid? + +> `readonly` `optional` **valid?**: `boolean` + +##### reason? + +> `readonly` `optional` **reason?**: `string` + +##### infra? + +> `readonly` `optional` **infra?**: `boolean` + +##### wait? + +> `readonly` `optional` **wait?**: `unknown` + +##### firstSequence + +> `readonly` **firstSequence**: `number` + +##### lastSequence + +> `readonly` **lastSequence**: `number` + +##### firstObservedAt + +> `readonly` **firstObservedAt**: `number` + +##### lastObservedAt + +> `readonly` **lastObservedAt**: `number` + +##### eventCount + +> `readonly` **eventCount**: `number` + +*** + +### PursuitProjection + +#### Properties + +##### pursuitId + +> `readonly` **pursuitId**: `string` + +##### sequence + +> `readonly` **sequence**: `number` + +Number of records in this concrete execution journal. + +##### chainTip + +> `readonly` **chainTip**: `string` + +Digest-chain tip for this concrete execution journal. + +##### firstObservedAt + +> `readonly` **firstObservedAt**: `number` + +##### lastObservedAt + +> `readonly` **lastObservedAt**: `number` + +##### runs + +> `readonly` **runs**: readonly [`PursuitRunProjection`](#pursuitrunprojection)[] + +##### nodes + +> `readonly` **nodes**: readonly [`PursuitNodeProjection`](#pursuitnodeprojection)[] + +##### eventCount + +> `readonly` **eventCount**: `number` + +##### decisionCount + +> `readonly` **decisionCount**: `number` + +*** + +### SupervisePursuitOptions + +#### Extends + +- [`SuperviseOptions`](runtime.md#superviseoptions) + +#### Properties + +##### pursuitId + +> `readonly` **pursuitId**: `string` + +Stable objective identity spanning concrete Runtime runs. + +##### runDir + +> `readonly` **runDir**: `string` + +One concrete Runtime execution owns one durable directory and observer journal. +A pursuit spanning several runs reuses `pursuitId` across distinct `runDir`s; +Intelligence joins those isolated projections without a shared write head. + +###### Overrides + +[`SuperviseOptions`](runtime.md#superviseoptions).[`runDir`](runtime.md#rundir-1) + +##### budget + +> `readonly` **budget**: [`Budget`](index.md#budget-4) + +The conserved compute pool for the whole run. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`budget`](runtime.md#budget-15) + +##### rootHandle? + +> `readonly` `optional` **rootHandle?**: [`RootHandle`](runtime.md#roothandle-1)\<`unknown`\> + +Caller-created live handle for observing, steering, or cancelling this root manager. Runtime +attaches it before execution and detaches it after the join barrier. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`rootHandle`](runtime.md#roothandle) + +##### signal? + +> `readonly` `optional` **signal?**: `AbortSignal` + +Caller-owned cancellation for the complete recursive run. Aborting it cascades through the +root scope and every live child, including acquisition and backend execution. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`signal`](runtime.md#signal-21) + +##### execution? + +> `readonly` `optional` **execution?**: [`AgentExecutionRef`](runtime.md#agentexecutionref) + +Trusted candidate and pursuit attribution for the root. The runtime derives profile/task +digests itself from the exact detached values it executes. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`execution`](runtime.md#execution-1) + +##### backend? + +> `readonly` `optional` **backend?**: [`ExecutorConfig`](runtime.md#executorconfig) + +WHERE workers run — derives the worker seam. Provide this OR an explicit `makeWorkerAgent`. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`backend`](runtime.md#backend-4) + +##### deliverable? + +> `readonly` `optional` **deliverable?**: `string` \| [`DeliverableSpec`](runtime.md#deliverablespec)\<`unknown`\> + +The independent completion check for backend-derived workers and direct supervisor + submissions. Strongly recommended: without it the supervisor cannot submit its own work and + backend-derived workers fall back to their own validity signal. A `string` names an entry in + `registry.deliverables`. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`deliverable`](runtime.md#deliverable-4) + +##### resolveDeliverable? + +> `readonly` `optional` **resolveDeliverable?**: (`input`) => [`DeliverableSpec`](runtime.md#deliverablespec)\<`unknown`\> \| `undefined` + +Resolve the completion check for one exact authorized backend-derived leaf. The callback runs +after spawn authorization and driver classification, receives a detached immutable context, +and may return `undefined` to use the run-wide `deliverable`. Driver profiles never call it. + +###### Parameters + +###### input + +[`AuthorizedSpawnContext`](runtime.md#authorizedspawncontext) + +###### Returns + +[`DeliverableSpec`](runtime.md#deliverablespec)\<`unknown`\> \| `undefined` + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`resolveDeliverable`](runtime.md#resolvedeliverable) + +##### registry? + +> `readonly` `optional` **registry?**: [`SuperviseRegistry`](runtime.md#superviseregistry) + +Name→value tables for the four code-valued options, so a recorded run configuration can name + them instead of carrying closures. See [SuperviseRegistry](runtime.md#superviseregistry). + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`registry`](runtime.md#registry-3) + +##### coordination? + +> `readonly` `optional` **coordination?**: [`CoordinationBinding`](runtime.md#coordinationbinding) + +Where the coordination MCP binds when the supervisor is harness-driven. Omit = an ephemeral + port on `127.0.0.1`, which an off-host root cannot reach. A non-loopback host is refused + unless `allowUnauthenticatedRemote` acknowledges that the verbs are unauthenticated. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`coordination`](runtime.md#coordination) + +##### makeWorkerAgent? + +> `readonly` `optional` **makeWorkerAgent?**: [`MakeWorkerAgent`](runtime.md#makeworkeragent) + +Override the worker seam directly (tests / advanced) instead of deriving it from `backend`. + This is caller-owned execution: profile security, spawn authorization, and recursive-driver + selection below apply only to the backend-derived worker path. `authorizeMessage` still + governs continuations sent through Runtime's coordination tools. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`makeWorkerAgent`](runtime.md#makeworkeragent-2) + +##### driverBackend? + +> `readonly` `optional` **driverBackend?**: [`ExecutorConfig`](runtime.md#executorconfig) + +Run harness-brained supervisors here. Automatic execution supports a local `bridge`; a remote + sandbox requires an explicit `driveHarness` with a reachable coordination relay or tunnel. + Defaults to `backend`; separate it when managers and workers use different services. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`driverBackend`](runtime.md#driverbackend-1) + +##### profileSecurity? + +> `readonly` `optional` **profileSecurity?**: `AgentProfileSecurityPolicy` + +Security policy applied to every manager-authored child profile before budget reservation. + The default blocks local and remote MCP, hooks, and connection grants. Pass an explicit + allowlist to grant remote MCP hosts or other author-controlled capabilities. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`profileSecurity`](runtime.md#profilesecurity) + +##### authorizeSpawn? + +> `readonly` `optional` **authorizeSpawn?**: (`input`) => [`AuthorizedSpawn`](runtime.md#authorizedspawn) + +Product authority over one complete manager-authored spawn. The callback sees the detached, + immutable profile, task, budget, label, and key together, so approving a profile cannot + authorize a different task. Return the exact allowed profile (which may be narrowed) plus + trusted candidate/pursuit attribution, or throw to refuse the whole spawn before reservation. + +###### Parameters + +###### input + +###### profile + +`AgentProfile` + +###### parent + +`AgentProfile` + +###### parentIdentity + +[`NodeExecutionIdentity`](runtime.md#nodeexecutionidentity) + +Trusted identity of the manager authorizing this exact child. + +###### parentNodeId + +`string` + +Concrete manager node; never accepted from model-authored tool arguments. + +###### assignmentId + +`string` + +Stable manager-scoped assignment, including deterministic unkeyed siblings. + +###### task + +`unknown` + +###### budget + +[`Budget`](index.md#budget-4) + +###### label + +`string` + +###### key? + +`string` + +###### depth + +`number` + +###### Returns + +[`AuthorizedSpawn`](runtime.md#authorizedspawn) + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`authorizeSpawn`](runtime.md#authorizespawn) + +##### authorizeMessage? + +> `readonly` `optional` **authorizeMessage?**: (`input`) => [`AuthorizedDownMessage`](runtime.md#authorizeddownmessage) + +Product authority over every continuation sent to a live child. When spawn authorization is +enabled, omitting this refuses steer/answer instructions instead of silently extending the +authorized task. The exact worker identity and detached bytes are recorded before delivery. + +###### Parameters + +###### input + +[`DownMessageAuthorizationInput`](runtime.md#downmessageauthorizationinput) & `object` + +###### Returns + +[`AuthorizedDownMessage`](runtime.md#authorizeddownmessage) + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`authorizeMessage`](runtime.md#authorizemessage-1) + +##### isDriverProfile? + +> `readonly` `optional` **isDriverProfile?**: (`input`) => `boolean` + +Decide whether an authorized child becomes another supervisor. By default only + `metadata.role === 'driver'` does. Products receive the same frozen post-authorization + context as `resolveDeliverable`, so trusted execution/assignment authority can override + model-authored metadata without a side channel. + +###### Parameters + +###### input + +[`AuthorizedSpawnContext`](runtime.md#authorizedspawncontext) + +###### Returns + +`boolean` + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`isDriverProfile`](runtime.md#isdriverprofile) + +##### router? + +> `readonly` `optional` **router?**: [`RouterTransportConfig`](runtime.md#routertransportconfig) + +The supervisor's router substrate (`profile.harness` omitted or `cli-base`). The profile's + model wins. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`router`](runtime.md#router-5) + +##### driveHarness? + +> `readonly` `optional` **driveHarness?**: [`DriveHarness`](runtime.md#driveharness-1) + +Run an external-harness supervisor explicitly. Required for a remote sandbox; optional as a + caller-owned override for a local bridge. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`driveHarness`](runtime.md#driveharness) + +##### driverRetry? + +> `readonly` `optional` **driverRetry?**: [`DriverRetryPolicy`](runtime.md#driverretrypolicy) + +How hard a transiently-failed EXTERNAL driver is re-entered before the run ends +`driver-failed`. A harness process SIGKILLed at a bridge timeout, a stream cut mid-turn, or an +upstream 5xx used to end a run of arbitrary length while its budget and deadline sat almost +untouched (#741). A retry re-enters the driver over the SAME scope, coordination server, and +live children; the bridge backend reattaches the harness session by its durable execution id. + +Runtime's own refusals (a validation guard, an exhausted budget, an abort, a client-side +transport status) are never retried — they were decisions. Retries stop at the budget, the +deadline, an abort, or a run of attempts that changed nothing at all. + +Omit = retry under the defaults. `{ enabled: false }` = the historical behavior where the first +driver failure ends the run. Applies to the root manager and every recursive manager under it. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`driverRetry`](runtime.md#driverretry) + +##### onDriverAttempt? + +> `readonly` `optional` **onDriverAttempt?**: (`record`) => `void` \| `Promise`\<`void`\> + +Per-attempt record for every external driver in the tree — what makes "failed after N + attempts, last cause X" visible instead of one backend's last words. + +###### Parameters + +###### record + +[`DriverAttemptRecord`](runtime.md#driverattemptrecord) + +###### Returns + +`void` \| `Promise`\<`void`\> + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`onDriverAttempt`](runtime.md#ondriverattempt) + +##### childSettleGraceMs? + +> `readonly` `optional` **childSettleGraceMs?**: `number` + +How long live children may keep running after the ROOT DRIVER FAILED, before the join barrier +cascades the abort into them. A root that died did not make its children unhealthy: a child +mid-unit holds work already paid for, and an immediate cascade discards everything it has not +yet written. Bounded by the run's own deadline. Omit/`0` = immediate teardown. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`childSettleGraceMs`](runtime.md#childsettlegracems) + +##### resolveDriveHarness? + +> `readonly` `optional` **resolveDriveHarness?**: [`ResolveDriveHarness`](runtime.md#resolvedriveharness-1) + +Resolve one custom external-harness session per trusted manager identity. Use this instead of +`driveHarness` when recursive managers must be independently steerable. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`resolveDriveHarness`](runtime.md#resolvedriveharness) + +##### driveHarnessMaterialization? + +> `readonly` `optional` **driveHarnessMaterialization?**: [`ProfileMaterializationContract`](agent.md#profilematerializationcontract) + +Required with a custom `driveHarness` or `resolveDriveHarness`: declares which complete +AgentProfile axes that path really applies. Built-in bridge driving supplies its own +full-profile contract. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`driveHarnessMaterialization`](runtime.md#driveharnessmaterialization) + +##### resolveSupervisorTools? + +> `readonly` `optional` **resolveSupervisorTools?**: [`ResolveSupervisorTools`](runtime.md#resolvesupervisortools-1) + +Resolve product-owned tools from the exact trusted manager context. The same descriptors and +handlers are bound to router and external-harness managers; resolution happens once per node. +Each handler receives that manager scope's live cancellation signal in its trusted invocation +context, including recursive parent and root cascades. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`resolveSupervisorTools`](runtime.md#resolvesupervisortools) + +##### onCoordinationEvent? + +> `readonly` `optional` **onCoordinationEvent?**: (`context`, `eventId`, `record`) => `void` \| `Promise`\<`void`\> + +Awaited product transaction hook for every coordination record. `eventId` is stable across a +lost acknowledgement and durable restart; the record is not pull-visible until this commits. + +###### Parameters + +###### context + +[`SupervisorNodeContext`](runtime.md#supervisornodecontext) + +###### eventId + +`` `sha256:${string}` `` + +###### record + +[`BusRecord`](runtime.md#busrecord)\<[`CoordinationEvent`](index.md#coordinationevent)\> + +###### Returns + +`void` \| `Promise`\<`void`\> + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`onCoordinationEvent`](runtime.md#oncoordinationevent) + +##### extraTools? + +> `readonly` `optional` **extraTools?**: readonly `object`[] + +WORK tools the supervisor may call DIRECTLY — so a recursive atom can ACT (do simple work + itself) OR SPAWN (delegate when it needs parallelism), not be a pure manager. Pair with + `executeExtraTool`. Router arm only (`profile.harness` omitted or `cli-base`). + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`extraTools`](runtime.md#extratools) + +##### executeExtraTool? + +> `readonly` `optional` **executeExtraTool?**: (`name`, `args`) => `Promise`\<`string` \| `null` \| `undefined`\> + +Runs an `extraTools` call; null/undefined falls through to the coordination dispatch. + +###### Parameters + +###### name + +`string` + +###### args + +`Record`\<`string`, `unknown`\> + +###### Returns + +`Promise`\<`string` \| `null` \| `undefined`\> + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`executeExtraTool`](runtime.md#executeextratool) + +##### perWorker? + +> `readonly` `optional` **perWorker?**: [`Budget`](index.md#budget-4) + +Per-child budget reserved on each spawn. Defaults to a quarter of the pool's tokens. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`perWorker`](runtime.md#perworker-1) + +##### maxLiveWorkers? + +> `readonly` `optional` **maxLiveWorkers?**: `number` + +Hard cap on simultaneously executing spawned workers across the WHOLE recursive tree. The + root is excluded; nested drivers and leaves share one allocation, so recursion cannot multiply + the cap. Omit/`<= 0` = no cap (the conserved pool stays the only bound). + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`maxLiveWorkers`](runtime.md#maxliveworkers-4) + +##### analysts? + +> `readonly` `optional` **analysts?**: `string` \| [`AnalystRegistry`](index.md#analystregistry) + +Analyst lenses available to the driver. Required for `analyzeOnSettle`. Unset → status quo + (the driver receives settled worker outputs, no analyst findings). A `string` names an entry in + `registry.analysts`. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`analysts`](runtime.md#analysts-3) + +##### analyzeOnSettle? + +> `readonly` `optional` **analyzeOnSettle?**: readonly (`string` \| [`AnalyzeOnSettleRoute`](runtime.md#analyzeonsettleroute))[] + +Analyst kind ids run AUTOMATICALLY when a worker settles `done` — each re-enters as a `finding` + the driver pulls (`await_event`) and composes its next steer from. The self-improving UP-leg, + threaded to the driver at this level (propagate to sub-drivers via a recursive `makeWorkerAgent`). + Omit/empty = status quo (no analyst feed). Requires `analysts`. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`analyzeOnSettle`](runtime.md#analyzeonsettle) + +##### watchWorkers? + +> `readonly` `optional` **watchWorkers?**: [`WorkerWatchOptions`](runtime.md#workerwatchoptions) + +Watch every worker's LIVE tool trace with the online detector panel and raise a `finding` the +moment one loops or error-storms — so the supervisor learns it mid-run (via `await_event`) +instead of at settle. Pairs with a steerable worker: the finding is the evidence, `steer_agent` +is the correction. Requires a backend whose executor exposes a trace source (the steerable +sandbox worker and the pi wrapper do); other runtimes are simply not watched. + +Omit = off (status quo — no online watching, no extra events). + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`watchWorkers`](runtime.md#watchworkers-1) + +##### stallAfterMs? + +> `readonly` `optional` **stallAfterMs?**: `number` + +Idle time after which `observe_agent` reports a running worker as `stalled`. A derived read + at observation time — nothing is killed or retried. Omit = the runtime default. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`stallAfterMs`](runtime.md#stallafterms-3) + +##### continuityByProfile? + +> `readonly` `optional` **continuityByProfile?**: `Readonly`\<`Record`\<`string`, [`ContinuityMode`](runtime.md#continuitymode)\>\> + +Default continuity per worker PROFILE NAME: `'resume'` makes each spawn of that name after + the first re-attach to the node's most recent SETTLED worker — a NEW live worker whose spawn + context carries the prior worker's identity (`WorkerSpawnContext.resume`), which the executor + seam re-attaches with. `spawn_agent`'s per-call `continuity` argument overrides in either + direction; `runGraph` derives this from delegates-edge `continuity`. Omit = every spawn is + `'fresh'` (status quo). See `CoordinationToolsOptions.continuityByProfile` for the + refusal semantics (no-prior / while-live / with-key) and the process-local resume boundary. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`continuityByProfile`](runtime.md#continuitybyprofile) + +##### blobs? + +> `readonly` `optional` **blobs?**: [`ResultBlobStore`](runtime.md#resultblobstore) + +Worker output store. Defaults to in-memory. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`blobs`](runtime.md#blobs-4) + +##### journal? + +> `readonly` `optional` **journal?**: [`SpawnJournal`](runtime.md#spawnjournal) + +Override the spawn journal directly (advanced; `runDir` is the ordinary durable path). Pair + with `blobs` — a journal whose result payloads live in a different store cannot replay. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`journal`](runtime.md#journal-4) + +##### probes? + +> `readonly` `optional` **probes?**: `string` \| [`WaitProbeRegistry`](runtime.md#waitproberegistry) + +Predicate registry for `poll` wait-states (`Scope.wait`). A `poll` names its predicate so the + wait survives a restart; this is what the name resolves against. Unset ⇒ `poll` waits are + refused `unknown-probe` and `timer` waits still work. A `string` names an entry in + `registry.probes`. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`probes`](runtime.md#probes-2) + +##### stopRule? + +> `readonly` `optional` **stopRule?**: [`StopRule`](runtime.md#stoprule) + +PROGRESS-derived stop rule (router-brained supervisor). Ends a run that has stopped LEARNING +before it exhausts a ceiling — the answer to "a run should end because it is done or stuck, +not because it ran out". It composes with the budget guards and can never override one. + +Build it from `supervise/stop-rules`: `plateau({window, minDelta})`, +`noProgressFor({ms, settles})`, `allWorkersStalled({...})`, combined with `anyOf`/`allOf`. The +thresholds are policy and stay with you; the enforcement lives in the runtime. Omit = ceilings +only (unchanged behavior). + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`stopRule`](runtime.md#stoprule-1) + +##### onProgressStop? + +> `readonly` `optional` **onProgressStop?**: (`reason`) => `void` + +One-shot notification of WHY a `stopRule` ended the run — so a caller records the reason + instead of inferring an early stop from an unexhausted budget. + +###### Parameters + +###### reason + +`string` + +###### Returns + +`void` + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`onProgressStop`](runtime.md#onprogressstop) + +##### maxDepth? + +> `readonly` `optional` **maxDepth?**: `number` + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`maxDepth`](runtime.md#maxdepth-2) + +##### maxTurns? + +> `readonly` `optional` **maxTurns?**: `number` + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`maxTurns`](runtime.md#maxturns-2) + +##### compaction? + +> `readonly` `optional` **compaction?**: [`ToolLoopCompactionOptions`](runtime.md#toolloopcompactionoptions) + +Give the supervisor brain a chapter-lifecycle on its OWN context window (router arm only): once + its coordination transcript exceeds `thresholdTokens` it distills to a compact progress note and + continues, instead of re-billing the whole transcript every turn (the cost that makes the LLM-brain + front door lose to a dumb-Ralph respawn). The live `Scope` roster is the durable state across + chapters. Default off. `distill` defaults to a brain self-summary + the settled-worker roster. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`compaction`](runtime.md#compaction) + +##### runId? + +> `readonly` `optional` **runId?**: `string` + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`runId`](runtime.md#runid-17) + +##### now? + +> `readonly` `optional` **now?**: () => `number` + +###### Returns + +`number` + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`now`](runtime.md#now-16) + +##### allowedModels? + +> `readonly` `optional` **allowedModels?**: readonly `string`[] + +Restrict the run to this subset of models. When set, every configured model — the + supervisor router model, the profile's model, and the backend's model — must be a member, + or `supervise()` throws a `ConfigError` before any compute is spent. Unset = unrestricted. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`allowedModels`](runtime.md#allowedmodels-2) + +##### finalizer? + +> `readonly` `optional` **finalizer?**: `string` \| [`SupervisorFinalizer`](index.md#supervisorfinalizer) + +How the settled-worker ledger becomes the run's output. Default `bestDelivered` — the single + highest-scoring DELIVERED child (the exact behavior every existing caller had). Alternatives: + `collectDelivered` (every verified distinct output with provenance — a Pareto set / recorded + disagreement) or a custom `SupervisorFinalizer`. Whatever the finalizer, it operates on + structurally DELIVERED outputs only — an undelivered or invalid child stays ineligible. A + `string` names an entry in `registry.finalizers`. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`finalizer`](runtime.md#finalizer) + +##### hooks? + +> `readonly` `optional` **hooks?**: [`RuntimeHooks`](index.md#runtimehooks) + +Lifecycle observers for the whole recursive tree (`Scope` re-seeds them into every nested + scope). Composed with the `otel` recorder below when both are set. Omit = no observers, which + is the behavior every existing caller has. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`hooks`](runtime.md#hooks-8) + +##### otel? + +> `readonly` `optional` **otel?**: `Omit`\<[`SupervisorSpanOptions`](runtime.md#supervisorspanoptions), `"runId"` \| `"now"`\> + +OPT-IN OTLP tracing: emit one span per supervised node (opened at spawn, closed at settle, +parented to its parent node's span) plus an `LLM` child span per metered driver turn, so the +tree is readable by any trace viewer instead of only by a journal parser. See `otel-spans.ts`. + +Omit and the run emits nothing, allocates no recorder, and installs no hook — telemetry is +never a default. Present with no reachable endpoint (no `exportConfig.endpoint` and no +`OTEL_EXPORTER_OTLP_ENDPOINT`) is also a no-op. The spawn journal is untouched either way: +spans are telemetry, never the replay/resume record. + +###### Inherited from + +[`SuperviseOptions`](runtime.md#superviseoptions).[`otel`](runtime.md#otel-1) + +*** + +### SupervisedPursuitResult + +#### Type Parameters + +##### Result + +`Result` + +#### Properties + +##### result + +> `readonly` **result**: `Result` + +##### pursuit + +> `readonly` **pursuit**: [`PursuitProjection`](#pursuitprojection) + +##### observerPath + +> `readonly` **observerPath**: `string` + +*** + +### DurableCoordinationStreamIdentity + +#### Properties + +##### runId + +> `readonly` **runId**: `string` + +##### ownerIds + +> `readonly` **ownerIds**: readonly `string`[] + +Exact owner ids present in the side-log, sorted for deterministic display. + +##### unscopedRecords + +> `readonly` **unscopedRecords**: `number` + +Records written before owner-scoped coordination identities were introduced. + +##### recordCount + +> `readonly` **recordCount**: `number` + +*** + +### DurableSupervisionDiscovery + +Identities discoverable from one `supervise({ runDir })` directory without +already knowing the root node or coordination run id stored inside it. + +#### Properties + +##### runDir + +> `readonly` **runDir**: `string` + +##### spawnJournalPath + +> `readonly` **spawnJournalPath**: `string` + +##### coordinationLogPath + +> `readonly` **coordinationLogPath**: `string` + +##### roots + +> `readonly` **roots**: readonly `string`[] + +##### coordinationStreams + +> `readonly` **coordinationStreams**: readonly [`DurableCoordinationStreamIdentity`](#durablecoordinationstreamidentity)[] + +## Type Aliases + +### ObserverRecordKind + +> **ObserverRecordKind** = `"event"` \| `"decision"` + +*** + +### PursuitRunStatus + +> **PursuitRunStatus** = `"running"` \| `"done"` \| `"failed"` + +*** + +### PursuitNodeStatus + +> **PursuitNodeStatus** = `"running"` \| `"done"` \| `"down"` + +## Functions + +### handleChatTurn() + +> **handleChatTurn**(`input`): [`ChatTurnResult`](#chatturnresult) + +Run one chat turn. Returns immediately with a `ReadableStream` body; +execution starts while the stream is constructed. Backend +failures surface as `error` + `session.run.failed` events. + +#### Parameters + +##### input + +[`RunChatTurnInput`](#runchatturninput) + +#### Returns + +[`ChatTurnResult`](#chatturnresult) + +*** + +### deriveExecutionId() + +> **deriveExecutionId**(`input`): `string` + +Derive a stable execution id from the run identity. +The same `(projectId, sessionId, turnIndex)` tuple yields the same id. + +Use the result as both `PromptOptions.executionId` and +`PromptOptions.turnId` on the first dispatch. +The execution id addresses the server-side execution for reconnect and +replay; the turn id makes a repeated dispatch idempotent. +An execution id alone does not make a repeated POST idempotent. + +Format is readable, not hashed: operators grepping orchestrator logs +for `gtm-agent:thread-abc:3` find the run without translating an +opaque id. Components are URL-encoded so delimiters inside caller ids +cannot collapse distinct tuples. The final id is limited to the +orchestrator replay route's 256-byte maximum. Execution ids are not a +secrecy boundary. + +Wire integration: + - Initial dispatch: pass the result as `executionId` and `turnId`. + - Stream replay: pass it as `executionId` with `lastEventId`. + +#### Parameters + +##### input + +###### projectId + +`string` + +###### sessionId + +`string` + +###### turnIndex + +`number` + +#### Returns + +`string` + +#### Throws + +`TypeError` when either string id is blank. + +#### Throws + +`RangeError` when `turnIndex` is invalid or the result exceeds 256 bytes. + +*** + +### verifyObserverRecords() + +> **verifyObserverRecords**(`records`, `pursuitId?`): readonly [`ObserverRecord`](#observerrecord)[] + +Verify identity, monotonic sequence, payload shape, and the complete digest chain. + +#### Parameters + +##### records + +readonly [`ObserverRecord`](#observerrecord)[] + +##### pursuitId? + +`string` + +#### Returns + +readonly [`ObserverRecord`](#observerrecord)[] + +*** + +### observerRecordDigest() + +> **observerRecordDigest**(`record`): `string` + +Compute the canonical SHA-256 digest for an unsigned observer record. + +#### Parameters + +##### record + +`Omit`\<[`ObserverRecord`](#observerrecord), `"digest"`\> + +#### Returns + +`string` + +*** + +### createFileObserverHooks() + +> **createFileObserverHooks**(`path`, `pursuitId`): `object` + +Build the canonical durable observer hook in one call. + +#### Parameters + +##### path + +`string` + +##### pursuitId + +`string` + +#### Returns + +`object` + +##### journal + +> `readonly` **journal**: [`FileObserverJournal`](#fileobserverjournal) + +##### hooks + +> `readonly` **hooks**: [`RuntimeHooks`](index.md#runtimehooks) + +*** + +### projectPursuit() + +> **projectPursuit**(`records`): [`PursuitProjection`](#pursuitprojection) + +Fold one append-only execution journal into a deterministic operator projection. + +This is intentionally a READ model, not another state machine: it does not own +execution, cannot steer agents, and can be rebuilt from the journal at any time. +Projection verifies the complete hash chain first, so an operator view can never +silently render a mutated or reordered observer history as trustworthy state. + +Topology comes only from Runtime's canonical `agent.spawn` facts. Terminal node +state comes only from `agent.child`; concrete run state comes only from the root +`agent.run` lifecycle emitted by `supervisePursuit`. Node identity is scoped to the +concrete Runtime run so independent trees may both contain `root:s0` without aliasing. + +#### Parameters + +##### records + +readonly [`ObserverRecord`](#observerrecord)[] + +#### Returns + +[`PursuitProjection`](#pursuitprojection) + +*** + +### supervisePursuit() + +> **supervisePursuit**(`profile`, `task`, `opts`): `Promise`\<[`SupervisedPursuitResult`](#supervisedpursuitresult)\<\{ `rootProviderModel`: [`ProviderModelExecutionEvidence`](index.md#providermodelexecutionevidence); `kind`: `"no-winner"`; `reason`: `"budget-exhausted"` \| `"all-children-down"` \| `"aborted"`; `tree`: [`TreeView`](runtime.md#treeview); `downCount`: `number`; `spentTotal`: [`Spend`](index.md#spend); `providerModel?`: [`ProviderModelExecutionEvidence`](index.md#providermodelexecutionevidence); `spendGaps?`: readonly [`SpendGap`](index.md#spendgap)[]; `error?`: `undefined`; \} \| \{ `rootProviderModel`: [`ProviderModelExecutionEvidence`](index.md#providermodelexecutionevidence); `kind`: `"no-winner"`; `reason`: `"driver-failed"`; `tree`: [`TreeView`](runtime.md#treeview); `downCount`: `number`; `spentTotal`: [`Spend`](index.md#spend); `providerModel?`: [`ProviderModelExecutionEvidence`](index.md#providermodelexecutionevidence); `spendGaps?`: readonly [`SpendGap`](index.md#spendgap)[]; `error`: [`NoWinnerError`](runtime.md#nowinnererror); \} \| \{ `rootProviderModel`: [`ProviderModelExecutionEvidence`](index.md#providermodelexecutionevidence); `kind`: `"winner"`; `out`: `unknown`; `outRef`: `string`; `verdict?`: `DefaultVerdict`; `tree`: [`TreeView`](runtime.md#treeview); `spentTotal`: [`Spend`](index.md#spend); `providerModel?`: [`ProviderModelExecutionEvidence`](index.md#providermodelexecutionevidence); `spendGaps?`: readonly [`SpendGap`](index.md#spendgap)[]; `spentBreakdown?`: \{ `driverInference`: [`Spend`](index.md#spend); `childWork`: [`Spend`](index.md#spend); \}; \}\>\> + +One-call durable pursuit execution over the canonical `supervise()` kernel. + +This is an adapter, not a second executor: it composes a durable third-person +observer into Runtime's existing recursive hook stream and then rebuilds the +operator projection after the same `supervise()` call settles. Agents never +receive the observer path or projection and their behavior does not depend on it. + +Every concrete execution writes only inside its own `runDir`. Cross-run pursuit +aggregation is therefore lock-free at the observer layer: reuse `pursuitId` across +run directories and let Intelligence join the independently verified projections. + +#### Parameters + +##### profile + +`AgentProfile` + +##### task + +`unknown` + +##### opts + +[`SupervisePursuitOptions`](#supervisepursuitoptions) + +#### Returns + +`Promise`\<[`SupervisedPursuitResult`](#supervisedpursuitresult)\<\{ `rootProviderModel`: [`ProviderModelExecutionEvidence`](index.md#providermodelexecutionevidence); `kind`: `"no-winner"`; `reason`: `"budget-exhausted"` \| `"all-children-down"` \| `"aborted"`; `tree`: [`TreeView`](runtime.md#treeview); `downCount`: `number`; `spentTotal`: [`Spend`](index.md#spend); `providerModel?`: [`ProviderModelExecutionEvidence`](index.md#providermodelexecutionevidence); `spendGaps?`: readonly [`SpendGap`](index.md#spendgap)[]; `error?`: `undefined`; \} \| \{ `rootProviderModel`: [`ProviderModelExecutionEvidence`](index.md#providermodelexecutionevidence); `kind`: `"no-winner"`; `reason`: `"driver-failed"`; `tree`: [`TreeView`](runtime.md#treeview); `downCount`: `number`; `spentTotal`: [`Spend`](index.md#spend); `providerModel?`: [`ProviderModelExecutionEvidence`](index.md#providermodelexecutionevidence); `spendGaps?`: readonly [`SpendGap`](index.md#spendgap)[]; `error`: [`NoWinnerError`](runtime.md#nowinnererror); \} \| \{ `rootProviderModel`: [`ProviderModelExecutionEvidence`](index.md#providermodelexecutionevidence); `kind`: `"winner"`; `out`: `unknown`; `outRef`: `string`; `verdict?`: `DefaultVerdict`; `tree`: [`TreeView`](runtime.md#treeview); `spentTotal`: [`Spend`](index.md#spend); `providerModel?`: [`ProviderModelExecutionEvidence`](index.md#providermodelexecutionevidence); `spendGaps?`: readonly [`SpendGap`](index.md#spendgap)[]; `spentBreakdown?`: \{ `driverInference`: [`Spend`](index.md#spend); `childWork`: [`Spend`](index.md#spend); \}; \}\>\> *** diff --git a/docs/api/index.md b/docs/api/index.md index 0859f13c..387e7680 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -7782,6 +7782,12 @@ Idempotency-Key header (e.g. the runId) — safe retries + upsert. > **id**: `string` +##### pursuitId? + +> `optional` **pursuitId?**: `string` + +Stable identity for the long-lived objective. One pursuit may contain many runs. + ##### runId > **runId**: `string` @@ -7860,6 +7866,12 @@ Idempotency-Key header (e.g. the runId) — safe retries + upsert. > **id**: `string` +##### pursuitId? + +> `optional` **pursuitId?**: `string` + +Stable identity for the long-lived objective. One pursuit may contain many runs. + ##### runId > **runId**: `string` @@ -11861,6 +11873,11 @@ Runtime hook contracts. Hooks are execution-scoped observers, not part of an `AgentProfile`: profiles stay portable agent recipes; hooks attach to the loop or product harness that is running the profile. +A `pursuitId` is deliberately orthogonal to `runId`: a pursuit can span many +resumed/retried/forked runs while every event remains attributable to the +durable objective that caused it. The observer plane is outside the agent +environment and must never be required for agent correctness. + *** ### RuntimeHookTarget diff --git a/docs/api/primitive-catalog.md b/docs/api/primitive-catalog.md index 21194206..09ee935f 100644 --- a/docs/api/primitive-catalog.md +++ b/docs/api/primitive-catalog.md @@ -370,22 +370,30 @@ Import from `@tangle-network/agent-runtime/conversation` — 54 exports. ### Product chat turns — edge-safe streaming, persistence, and stable execution IDs -Import from `@tangle-network/agent-runtime/durable` — 11 exports. +Import from `@tangle-network/agent-runtime/durable` — 28 exports. | Symbol | Kind | Summary | |---|---|---| +| `createFileObserverHooks` | function | Build the canonical durable observer hook in one call. | | `deriveExecutionId` | function | Derive a stable execution id from the run identity. | | `discoverDurableSupervisionRun` | function | Discover the stable identities recorded by Runtime's durable supervision | | `handleChatTurn` | function | Run one chat turn. Returns immediately with a `ReadableStream` body; | +| `observerRecordDigest` | function | Compute the canonical SHA-256 digest for an unsigned observer record. | +| `projectPursuit` | function | Fold one append-only execution journal into a deterministic operator projection. | +| `supervisePursuit` | function | One-call durable pursuit execution over the canonical `supervise()` kernel. | +| `verifyObserverRecords` | function | Verify identity, monotonic sequence, payload shape, and the complete digest chain. | +| `FileObserverJournal` | class | Durable, append-only third-person history for one concrete Runtime execution. | +| `SupervisePursuitError` | class | A failed Runtime execution whose complete third-person projection was retained. | | `ChatStreamEvent` | interface | The NDJSON line protocol every product chat client already speaks. | | `ChatTurnHooks` | interface | Product callbacks invoked while one chat turn runs. | | `ChatTurnIdentity` | interface | Identity of a chat turn. `tenantId` is the workspace id for workspace- | | `ChatTurnProducer` | interface | The live side of a turn returned by the product's `produce` hook. | | `ChatTurnResult` | interface | HTTP response values returned for one chat turn. | | `DurableSupervisionDiscovery` | interface | Identities discoverable from one `supervise({ runDir })` directory without | +| `ObserverRecord` | interface | One immutable record in the observer plane. `sequence` is journal order, not | | `RunChatTurnInput` | interface | Inputs for one streamed product chat turn. | -**Undocumented supporting types** (add a TSDoc line at the declaration to earn a table row): `DurableCoordinationStreamIdentity`. +**Undocumented supporting types** (add a TSDoc line at the declaration to earn a table row): `DurableCoordinationStreamIdentity`, `ObserverJournal`, `PursuitNodeProjection`, `PursuitProjection`, `PursuitRunProjection`, `SupervisedPursuitResult`, `SupervisePursuitOptions`, `ObserverRecordKind`, `PursuitNodeStatus`, `PursuitRunStatus`. ### Bounded tool calls for browser and edge runtimes diff --git a/docs/api/runtime.md b/docs/api/runtime.md index 76f1ea10..0d9628d7 100644 --- a/docs/api/runtime.md +++ b/docs/api/runtime.md @@ -14905,6 +14905,7 @@ caller that owns the code registers it here once and names it from data thereaft #### Extended by +- [`SupervisePursuitOptions`](durable.md#supervisepursuitoptions) - [`SuperviseTestOptions`](testing.md#supervisetestoptions) #### Properties diff --git a/src/durable/observer-journal.ts b/src/durable/observer-journal.ts index dfe42aaf..6f9d9924 100644 --- a/src/durable/observer-journal.ts +++ b/src/durable/observer-journal.ts @@ -246,7 +246,8 @@ export function verifyObserverRecords( return Object.freeze([...records]) } -export function observerRecordDigest(record: UnsignedObserverRecord): string { +/** Compute the canonical SHA-256 digest for an unsigned observer record. */ +export function observerRecordDigest(record: Omit): string { return createHash('sha256').update(JSON.stringify(record)).digest('hex') } From 766c5731aee14fece8b6d75210d7e56700004c20 Mon Sep 17 00:00:00 2001 From: drewstone Date: Mon, 17 Aug 2026 11:01:10 -0700 Subject: [PATCH 33/33] ci: verify generated pursuit observer reference