diff --git a/apps/server/src/provider/prime/PrimeAgentCompactionHistory.test.ts b/apps/server/src/provider/prime/PrimeAgentCompactionHistory.test.ts new file mode 100644 index 000000000..2d8b649e8 --- /dev/null +++ b/apps/server/src/provider/prime/PrimeAgentCompactionHistory.test.ts @@ -0,0 +1,248 @@ +// @effect-diagnostics nodeBuiltinImport:off +import * as NodeCrypto from "node:crypto"; +import { describe, expect, it } from "@effect/vitest"; +import nativeFixture from "./fixtures/native-compaction/isolated-manual.json" with { type: "json" }; +import legacyFixture from "./fixtures/native-compaction/legacy-timestamp-mismatch.json" with { type: "json" }; +import { decodePrimeAgentDaemonMessage } from "./PrimeAgentDaemonEvents.ts"; +import { + decodePrimeCompactionHistory, + planPrimeCompactionReplacement, +} from "./PrimeAgentCompactionHistory.ts"; +import { planPrimeAgentRestartReplay } from "./PrimeAgentDaemonAdapter.ts"; + +const timestamp = "2026-09-14T12:00:00.000Z"; +const nativeUser = (text: string, time: number) => ({ + role: "user", + content: text, + timestamp: time, +}); +const fingerprint = (message: unknown) => + NodeCrypto.createHash("sha256").update(JSON.stringify(message)).digest("hex"); +const entry = >( + id: string, + parentId: string | null, + fields: T, +) => ({ + id, + parentId, + timestamp, + ...fields, +}); +const chain = (entries: ReadonlyArray>): unknown => + entries.reduceRight((children, value) => [{ entry: value, children }], []); +const first = entry("a", null, { type: "message", message: nativeUser("old", 1) }); +const retained = entry("b", "a", { type: "message", message: nativeUser("kept", 2) }); +const compact = entry("c", "b", { + type: "compaction", + firstKeptEntryId: "b", + summary: "private summary", + tokensBefore: 500, + harnessDigest: "private memory", +}); +const post = entry("d", "c", { type: "message", message: nativeUser("after", 3) }); +const tree = ( + entries: ReadonlyArray> = [first, retained, compact, post], + leafId = "d", +) => ({ + tree: chain(entries), + leafId, +}); +const decode = (value: unknown, leafId = "d") => + decodePrimeCompactionHistory(value, leafId, decodePrimeAgentDaemonMessage); +const requireHistory = () => { + const history = decode(tree()); + if (history === undefined) throw new Error("missing history"); + return history; +}; + +describe("compaction history proof", () => { + it("proves a real Prime faux-provider compaction captured from the public session tree", () => { + // Captured from Prime 72e4e9fbb (issue #76 timestamp fix): two persisted prompts, then session.compact(), + // using suite/harness.ts with faux responses, auto-refine disabled and keepRecentTokens=1. + const history = decodePrimeCompactionHistory( + nativeFixture.tree, + nativeFixture.tree.leafId, + decodePrimeAgentDaemonMessage, + ); + const messages = (values: ReadonlyArray) => + values.map((value) => { + const message = decodePrimeAgentDaemonMessage(value); + if (message === undefined) throw new Error("unsupported fixture message"); + return message; + }); + const before = messages(nativeFixture.before); + const after = messages(nativeFixture.after); + expect(history?.previous).toEqual(before); + expect(history?.current).toEqual(after); + expect( + planPrimeAgentRestartReplay({ + authorityMessageCount: before.length, + authorityFingerprints: before.map(fingerprint), + snapshotMessageCount: after.length, + snapshotMessages: after, + compactionHistory: history, + }), + ).toEqual({ valid: true, backlog: [] }); + }); + + it("does not excuse the real legacy custom timestamp mismatch during compaction", () => { + const history = decodePrimeCompactionHistory( + legacyFixture.tree, + legacyFixture.tree.leafId, + decodePrimeAgentDaemonMessage, + ); + const before = legacyFixture.before.map(decodePrimeAgentDaemonMessage); + const after = legacyFixture.after.map((value) => { + const message = decodePrimeAgentDaemonMessage(value); + if (message === undefined) throw new Error("unsupported legacy fixture message"); + return message; + }); + expect( + planPrimeAgentRestartReplay({ + authorityMessageCount: before.length, + authorityFingerprints: before.map(fingerprint), + snapshotMessageCount: after.length, + snapshotMessages: after, + compactionHistory: history, + }), + ).toEqual({ valid: false }); + }); + + it("reconstructs the exact retained boundary and private summary", () => { + const history = requireHistory(); + expect(history.previous).toEqual([ + decodePrimeAgentDaemonMessage(first.message), + decodePrimeAgentDaemonMessage(retained.message), + ]); + expect(history.current).toEqual([ + decodePrimeAgentDaemonMessage({ + role: "compactionSummary", + summary: compact.summary, + tokensBefore: compact.tokensBefore, + harnessDigest: compact.harnessDigest, + retainedMessageCount: 1, + timestamp: Date.parse(timestamp), + }), + decodePrimeAgentDaemonMessage(retained.message), + decodePrimeAgentDaemonMessage(post.message), + ]); + expect(JSON.stringify(history)).not.toContain("private"); + for (const observed of [history.previous, [...history.previous, ...history.appended]]) { + const planned = planPrimeCompactionReplacement({ + history, + observedCount: observed.length, + observedFingerprints: observed.map(fingerprint), + snapshotCount: history.current.length, + snapshot: history.current, + fingerprint, + }); + expect(planned).toMatchObject({ + observedCount: observed.length, + previousCount: 2, + retainedCount: 1, + }); + } + }); + + it("rejects malformed, cyclic, duplicated, foreign and unknown history", () => { + for (const value of [ + tree([first, retained, compact, post], "foreign"), + tree([first, { ...retained, parentId: "foreign" }, compact, post]), + tree([first, { ...retained, id: "a" }, compact, post]), + tree([first, retained, { ...compact, firstKeptEntryId: "foreign" }, post]), + tree([first, retained, { ...compact, firstKeptEntryId: "d" }, post]), + tree([first, retained, { ...compact, timestamp: "invalid" }, post]), + tree([first, retained, compact, { ...post, type: "unknown" }]), + tree([ + first, + retained, + compact, + { + ...post, + type: "message", + message: { + role: "custom", + customType: "heartbeat_prompt", + content: "foreign", + timestamp: 3, + display: true, + }, + }, + ]), + { leafId: "d", tree: Array.from({ length: 8193 }, () => ({ entry: first, children: [] })) }, + ]) + expect(decode(value)).toBeUndefined(); + }); + + it("rejects changed old, retained, summary and current history and incorrect counts", () => { + const history = requireHistory(); + const input = { + history, + observedCount: history.previous.length, + observedFingerprints: history.previous.map(fingerprint), + snapshotCount: history.current.length, + snapshot: history.current, + fingerprint, + }; + for (const changed of [ + { ...input, observedCount: 1, observedFingerprints: [fingerprint(history.previous[0])] }, + { ...input, observedFingerprints: input.observedFingerprints.toReversed() }, + { ...input, observedFingerprints: ["changed", input.observedFingerprints[1]!] }, + { ...input, snapshot: history.current.toReversed() }, + { ...input, snapshotCount: 99 }, + { + ...input, + snapshot: history.current.map((message, index) => + index === 0 ? { ...message, timestamp: 99 } : message, + ), + }, + { + ...input, + snapshot: history.current.map((message, index) => + index === 1 ? { ...message, timestamp: 99 } : message, + ), + }, + ]) + expect(planPrimeCompactionReplacement(changed)).toBeUndefined(); + }); + + it("recovers a shorter context across restart only with a matching history proof", () => { + const extra = entry("a2", "a", { type: "message", message: nativeUser("also old", 1.5) }); + const history = decode( + tree([first, extra, { ...retained, parentId: "a2" }, compact], "c"), + "c", + ); + if (history === undefined) throw new Error("missing shorter history"); + const input = { + authorityMessageCount: history.previous.length, + authorityFingerprints: history.previous.map(fingerprint), + snapshotMessageCount: history.current.length, + snapshotMessages: history.current, + }; + expect(planPrimeAgentRestartReplay(input)).toEqual({ valid: false }); + expect(planPrimeAgentRestartReplay({ ...input, compactionHistory: history })).toEqual({ + valid: true, + backlog: [], + }); + expect( + planPrimeAgentRestartReplay({ + ...input, + compactionHistory: history, + authorityFingerprints: input.authorityFingerprints.toReversed(), + }), + ).toEqual({ valid: false }); + }); + + it("reconstructs consecutive compactions without treating the old summary as a retained message", () => { + const second = entry("e", "d", { + type: "compaction", + firstKeptEntryId: "d", + summary: "second", + tokensBefore: 700, + }); + const history = decode(tree([first, retained, compact, post, second], "e"), "e"); + expect(history?.previous).toEqual(requireHistory().current); + expect(history?.retainedCount).toBe(1); + expect(history?.current).toHaveLength(2); + }); +}); diff --git a/apps/server/src/provider/prime/PrimeAgentCompactionHistory.ts b/apps/server/src/provider/prime/PrimeAgentCompactionHistory.ts new file mode 100644 index 000000000..9cec7c3e2 --- /dev/null +++ b/apps/server/src/provider/prime/PrimeAgentCompactionHistory.ts @@ -0,0 +1,200 @@ +import type { PrimeDaemonMessage } from "./PrimeAgentDaemonEvents.ts"; + +const MAX_HISTORY_ENTRIES = 8_192; +const TRANSCRIPT_TAIL = 1_024; +const metadataKinds = new Set([ + "thinking_level_change", + "service_tier_change", + "model_change", + "custom", + "child_usage_attributed", + "label", + "session_info", + "session_state", + "agent_status", + "git_state", +]); +const record = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); + +type Entry = Record & { + id: string; + parentId: string | null; + type: string; + timestamp: string; +}; +export interface PrimeCompactionHistory { + readonly previous: ReadonlyArray; + readonly current: ReadonlyArray; + readonly appended: ReadonlyArray; + readonly retainedCount: number; +} + +/** The public tree is evidence only when its exact leaf and rendered context match the snapshot. */ +export function decodePrimeCompactionHistory( + value: unknown, + leafId: string, + decodeMessage: (value: unknown) => PrimeDaemonMessage | undefined, +): PrimeCompactionHistory | undefined { + if (!record(value) || value.leafId !== leafId || !Array.isArray(value.tree)) return undefined; + const entries = new Map(); + const pending: Array<{ node: unknown; parentId: string | null }> = value.tree.map( + (node: unknown) => ({ node, parentId: null }), + ); + if (pending.length > MAX_HISTORY_ENTRIES) return undefined; + while (pending.length > 0) { + const next = pending.pop()!; + if (!record(next.node) || !record(next.node.entry) || !Array.isArray(next.node.children)) + return undefined; + const entry = next.node.entry; + if ( + typeof entry.id !== "string" || + entry.id.length === 0 || + entries.has(entry.id) || + entry.parentId !== next.parentId || + typeof entry.type !== "string" || + typeof entry.timestamp !== "string" || + !Number.isFinite(Date.parse(entry.timestamp)) + ) + return undefined; + entries.set(entry.id, { + ...entry, + id: entry.id, + parentId: next.parentId, + type: entry.type, + timestamp: entry.timestamp, + }); + if (entries.size + pending.length + next.node.children.length > MAX_HISTORY_ENTRIES) + return undefined; + for (const node of next.node.children) pending.push({ node, parentId: entry.id }); + } + const path: Entry[] = []; + let cursor: string | null = leafId; + while (cursor !== null) { + const entry = entries.get(cursor); + if (entry === undefined || path.length >= entries.size) return undefined; + path.push(entry); + cursor = entry.parentId; + } + path.reverse(); + const boundary = path.findLastIndex((entry) => entry.type === "compaction"); + if (boundary < 0) return undefined; + + const messagesFrom = (source: ReadonlyArray): PrimeDaemonMessage[] | undefined => { + const result: PrimeDaemonMessage[] = []; + for (const entry of source) { + let raw: unknown; + if (entry.type === "message") raw = entry.message; + else if (entry.type === "custom_message") + raw = { + role: "custom", + customType: entry.customType, + content: entry.content, + display: entry.display, + details: entry.details, + timestamp: Date.parse(entry.timestamp), + }; + else if (entry.type === "branch_summary") { + if (entry.summary === "") continue; + raw = { + role: "branchSummary", + summary: entry.summary, + fromId: entry.fromId, + timestamp: Date.parse(entry.timestamp), + }; + } else if (entry.type === "compaction" || metadataKinds.has(entry.type)) continue; + else return undefined; + const message = decodeMessage(raw); + if (message === undefined) return undefined; + result.push(message); + } + return result; + }; + const render = ( + source: ReadonlyArray, + ): { messages: PrimeDaemonMessage[]; retainedCount: number } | undefined => { + const index = source.findLastIndex((entry) => entry.type === "compaction"); + if (index < 0) { + const messages = messagesFrom(source); + return messages === undefined ? undefined : { messages, retainedCount: 0 }; + } + const compaction = source[index]!; + const firstKept = source.findIndex((entry) => entry.id === compaction.firstKeptEntryId); + if (firstKept < 0 || firstKept >= index) return undefined; + const retained = messagesFrom(source.slice(firstKept, index)); + const appended = messagesFrom(source.slice(index + 1)); + if (retained === undefined || appended === undefined) return undefined; + const summary = decodeMessage({ + role: "compactionSummary", + summary: compaction.summary, + tokensBefore: compaction.tokensBefore, + retainedMessageCount: retained.length, + customInstructions: compaction.customInstructions, + harnessDigest: compaction.harnessDigest, + timestamp: Date.parse(compaction.timestamp), + }); + return summary === undefined + ? undefined + : { messages: [summary, ...retained, ...appended], retainedCount: retained.length }; + }; + const previous = render(path.slice(0, boundary)); + const current = render(path); + const appended = messagesFrom(path.slice(boundary + 1)); + if (previous === undefined || current === undefined || appended === undefined) return undefined; + return { + previous: previous.messages, + current: current.messages, + appended, + retainedCount: current.retainedCount, + }; +} + +/** Translate an exactly proved old context prefix into the new context's count space. */ +export function planPrimeCompactionReplacement(input: { + readonly history: PrimeCompactionHistory; + readonly observedCount: number; + readonly observedFingerprints: ReadonlyArray; + readonly snapshotCount: number; + readonly snapshot: ReadonlyArray; + readonly fingerprint: (message: PrimeDaemonMessage) => string; +}): + | { + readonly observedCount: number; + readonly observed: ReadonlyArray; + readonly previousCount: number; + readonly retainedCount: number; + } + | undefined { + const { history, fingerprint } = input; + if ( + input.snapshotCount !== history.current.length || + input.snapshot.length !== Math.min(input.snapshotCount, TRANSCRIPT_TAIL) || + input.observedCount < history.previous.length || + input.observedCount > history.previous.length + history.appended.length || + input.observedFingerprints.length !== Math.min(input.observedCount, TRANSCRIPT_TAIL) + ) + return undefined; + const currentTail = history.current.slice(-TRANSCRIPT_TAIL); + if ( + input.snapshot.some( + (message, index) => fingerprint(message) !== fingerprint(currentTail[index]!), + ) + ) + return undefined; + const oldPrefix = [...history.previous, ...history.appended] + .slice(0, input.observedCount) + .slice(-TRANSCRIPT_TAIL); + if ( + input.observedFingerprints.some( + (identity, index) => identity !== fingerprint(oldPrefix[index]!), + ) + ) + return undefined; + const observedCount = 1 + history.retainedCount + input.observedCount - history.previous.length; + return { + observedCount, + observed: history.current.slice(0, observedCount).slice(-TRANSCRIPT_TAIL), + previousCount: history.previous.length, + retainedCount: history.retainedCount, + }; +} diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.test.ts b/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.test.ts index 2fdb26d00..1c790222b 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.test.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.test.ts @@ -1,3 +1,4 @@ +import type { PrimeCompactionHistory } from "./PrimeAgentCompactionHistory.ts"; // @effect-diagnostics nodeBuiltinImport:off import * as NodeFSP from "node:fs/promises"; import * as NodePath from "node:path"; @@ -178,6 +179,7 @@ function initialSnapshot(): Extract; readonly prompts: Array<{ readonly text: string; @@ -628,6 +630,7 @@ function fakeRuntimeFactory( inputQueueModesAvailable: captures.inputQueueModesAvailable, inputQueueMutationAvailable: captures.inputQueueMutationAvailable, compactionAvailable: captures.compactionAvailable, + getCompactionHistory: () => Effect.sync(() => captures.compactionHistory), refinementAvailable: captures.refinementAvailable && input.resumeCursor === undefined, refineLocalHarness: Effect.gen(function* () { captures.refinementCalls += 1; @@ -3208,6 +3211,283 @@ describe("PrimeAgentDaemonAdapter", () => { } } + for (const customType of [ + "compaction_outcome", + "ipython_state_restored", + "ipython_state", + "session_slash_command", + "session_slash_command_result", + "rlm_child_failure", + "rlm_child_terminal_notice", + "async_bash_completion", + "agent_message", + ]) { + for (const changed of [false, true]) { + for (const withNotice of [false]) { + it.effect( + `verifies a background ${customType} before a later tool snapshot: changed=${changed}, notice=${withNotice}`, + () => + Effect.scoped( + Effect.gen(function* () { + const captures = makeCaptures(); + captures.correlatedPromptLifecycleAvailable = true; + captures.correlatedRecoveryProofEpoch = 0; + captures.correlatedPromptObserved = yield* Queue.unbounded(); + const resolutions = + yield* Queue.unbounded(); + captures.reconnectSnapshotResolutionObserved = (resolution) => { + Queue.offerUnsafe(resolutions, resolution); + }; + const adapter = yield* makePrimeAgentDaemonAdapter(decodeSettings({}), manager, { + instanceId, + runtimeFactory: fakeRuntimeFactory(captures), + }); + const subscription = yield* subscribe(adapter); + yield* adapter.startSession({ + threadId, + cwd: process.cwd(), + runtimeMode: "full-access", + }); + const refinement = decodePrimeAgentDaemonEvent({ + type: "session_event", + event: { + type: "message_end", + message: { + role: "custom", + customType, + display: true, + content: "private memory update", + details: { refinementId: "refine-1", edits: [] }, + timestamp: 1, + }, + }, + }); + if (refinement?._tag !== "MessageCompleted") throw new Error("missing refinement"); + yield* offer(captures, { ...refinement, attribution: { scope: "session" } }); + const notice = decodePrimeAgentDaemonEvent( + { + type: "session_event", + attribution: { scope: "session" }, + event: { + type: "message_end", + promptCorrelationId: null, + message: { + role: "custom", + customType: "refinement_notice", + display: false, + content: "private applied memory", + details: { refinementId: "refine-1", source: "auto" }, + timestamp: 1.5, + }, + }, + }, + { correlatedPromptLifecycle: true }, + ); + if (notice._tag !== "MessageCompleted") + throw new Error("missing refinement notice"); + if (withNotice) yield* offer(captures, notice); + // Drain startup notifications, then wait for an ordered event behind the refinement. + yield* Queue.takeAll(subscription.observed); + yield* offer(captures, { _tag: "ConnectionStatus", status: "connected" }); + yield* awaitObservedType(subscription.observed, "session.state.changed"); + const turnFiber = yield* adapter + .sendTurn({ threadId, input: "Read fixture" }) + .pipe(Effect.forkChild); + const correlationId = yield* Queue.take(captures.correlatedPromptObserved); + const delivered = lifecycleSnapshot(correlationId, "delivered", 2); + yield* offer(captures, { _tag: "PromptLifecycleUpdated", lifecycle: delivered }); + const prompt = { + role: "user", + timestamp: 2, + text: "Read fixture", + imageMimeTypes: [], + imageDigests: [], + } satisfies PrimeDaemonMessage; + const call = { + ...assistantMessage("", "toolUse"), + timestamp: 3, + toolCalls: [{ id: "read-1", name: "read" }], + }; + for (const message of [prompt, call]) { + yield* offer(captures, { + _tag: "MessageCompleted", + message, + attribution: { scope: "prompt", correlationId }, + }); + } + const result = { + role: "toolResult", + timestamp: 4, + toolCallId: "read-1", + toolName: "read", + text: "fixture", + imageMimeTypes: [], + isError: false, + } satisfies PrimeDaemonMessage; + yield* offer(captures, { + ...initialSnapshot(), + state: { + ...initialSnapshot().state, + isStreaming: true, + messageCount: withNotice ? 5 : 4, + }, + messages: [ + changed ? { ...refinement.message, timestamp: 999 } : refinement.message, + ...(withNotice ? [notice.message] : []), + prompt, + call, + result, + ], + orderedSnapshot: true, + replayContinuity: "unknown", + connectionGeneration: 0, + correlatedProofEpoch: 0, + promptLifecycles: { records: [delivered], expired: [] }, + }); + expect((yield* Queue.take(resolutions)).reconciled).toBe(!changed); + if (!changed) { + yield* offer(captures, { + _tag: "MessageCompleted", + message: { ...assistantMessage("Read complete"), timestamp: 5 }, + attribution: { scope: "prompt", correlationId }, + }); + yield* offer(captures, { + _tag: "PromptLifecycleUpdated", + lifecycle: lifecycleSnapshot(correlationId, "completed", 3, { usage }), + }); + } + const settled = yield* Fiber.join(turnFiber); + const events = subscription.events.filter( + (event) => event.turnId === settled.turnId, + ); + expect(events.findLast((event) => event.type === "turn.completed")).toMatchObject({ + payload: { state: changed ? "failed" : "completed" }, + }); + expect(events.filter((event) => event.type === "runtime.error")).toHaveLength( + changed ? 1 : 0, + ); + expect(encodeUnknownJson(subscription.events)).not.toContain("private memory"); + expect(encodeUnknownJson(subscription.events)).not.toContain("refinementOutcome"); + expect(encodeUnknownJson(subscription.events)).not.toContain("refinementNotice"); + }), + ).pipe(Effect.provide(testLayer)), + ); + } + } + } + + for (const variant of ["valid", "changed", "missing proof"] as const) { + it.effect(`proves context replacement after compaction: ${variant}`, () => + Effect.scoped( + Effect.gen(function* () { + const captures = makeCaptures(); + captures.correlatedPromptLifecycleAvailable = true; + captures.correlatedRecoveryProofEpoch = 0; + captures.correlatedPromptObserved = yield* Queue.unbounded(); + const resolutions = + yield* Queue.unbounded(); + captures.reconnectSnapshotResolutionObserved = (resolution) => { + Queue.offerUnsafe(resolutions, resolution); + }; + const adapter = yield* makePrimeAgentDaemonAdapter(decodeSettings({}), manager, { + instanceId, + runtimeFactory: fakeRuntimeFactory(captures), + }); + const subscription = yield* subscribe(adapter); + yield* adapter.startSession({ threadId, cwd: process.cwd(), runtimeMode: "full-access" }); + const old: PrimeDaemonMessage[] = [1, 2, 3].map((timestamp) => ({ + role: "refinementOutcome", + timestamp, + contentDigest: `old-${timestamp}`, + })); + for (const message of old) + yield* offer(captures, { + _tag: "MessageCompleted", + message, + attribution: { scope: "session" }, + }); + yield* Queue.takeAll(subscription.observed); + yield* offer(captures, { _tag: "ConnectionStatus", status: "connected" }); + yield* awaitObservedType(subscription.observed, "session.state.changed"); + const turn = yield* adapter + .sendTurn({ threadId, input: "Continue after compaction" }) + .pipe(Effect.forkChild); + const correlationId = yield* Queue.take(captures.correlatedPromptObserved); + const delivered = lifecycleSnapshot(correlationId, "delivered", 2); + yield* offer(captures, { _tag: "PromptLifecycleUpdated", lifecycle: delivered }); + const prompt: PrimeDaemonMessage = { + role: "user", + timestamp: 4, + text: "Continue after compaction", + imageMimeTypes: [], + imageDigests: [], + }; + yield* offer(captures, { + _tag: "MessageCompleted", + message: prompt, + attribution: { scope: "prompt", correlationId }, + }); + yield* offer(captures, { + _tag: "CompactionStarted", + attribution: { scope: "prompt", correlationId }, + }); + yield* offer(captures, { + _tag: "CompactionCompleted", + outcome: "completed", + willRetry: true, + attribution: { scope: "prompt", correlationId }, + }); + const summary: PrimeDaemonMessage = { + role: "nativePrivate", + kind: "compactionSummary", + timestamp: 5, + contentDigest: "private-summary", + }; + const current = [summary, prompt]; + if (variant !== "missing proof") + captures.compactionHistory = { + previous: [...old, prompt], + current, + appended: [], + retainedCount: 1, + }; + const snapshot = { + ...initialSnapshot(), + state: { ...initialSnapshot().state, messageCount: 2, isStreaming: true }, + messages: variant === "changed" ? [summary, { ...prompt, timestamp: 99 }] : current, + orderedSnapshot: true, + replayContinuity: "unknown" as const, + connectionGeneration: 0, + correlatedProofEpoch: 0, + promptLifecycles: { records: [delivered], expired: [] }, + }; + yield* offer(captures, snapshot); + expect((yield* Queue.take(resolutions)).reconciled).toBe(variant === "valid"); + if (variant === "valid") { + yield* offer(captures, snapshot); + expect((yield* Queue.take(resolutions)).reconciled).toBe(true); + yield* offer(captures, { + _tag: "MessageCompleted", + message: { ...assistantMessage("Continued safely"), timestamp: 6 }, + attribution: { scope: "prompt", correlationId }, + }); + yield* offer(captures, { + _tag: "PromptLifecycleUpdated", + lifecycle: lifecycleSnapshot(correlationId, "completed", 3, { usage }), + }); + } + const settled = yield* Fiber.join(turn); + expect( + subscription.events.findLast( + (event) => event.type === "turn.completed" && event.turnId === settled.turnId, + ), + ).toMatchObject({ payload: { state: variant === "valid" ? "completed" : "failed" } }); + expect(encodeUnknownJson(subscription.events)).not.toContain("private-summary"); + }), + ).pipe(Effect.provide(testLayer)), + ); + } + it.effect("accepts exact complete capable recovery with already observed output", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts b/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts index 37ef6f34a..b9329504b 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts @@ -74,6 +74,10 @@ import { ProviderAdapterValidationError, type ProviderAdapterError, } from "../Errors.ts"; +import { + planPrimeCompactionReplacement, + type PrimeCompactionHistory, +} from "./PrimeAgentCompactionHistory.ts"; import type { PrimeAgentAdapterShape } from "../Services/PrimeAgentAdapter.ts"; import { BUILT_IN_ADAPTER_CONVERSATION_ROLLBACK_MODES, @@ -270,7 +274,7 @@ interface PrimeAgentDaemonActiveTurn { awaitingQueuedRun: boolean; queuedActionObserved: boolean; readonly completedRunMessages: Array; - readonly nativeTranscriptBaselineMessageCount: number; + nativeTranscriptBaselineMessageCount: number; readonly observedToolStarts: Set; readonly observedToolCompletions: Set; /** Durable assistant tool-call messages, retained only for in-memory correlation. */ @@ -678,6 +682,32 @@ export function planPrimeAgentRestartReplay(input: { readonly authorityFingerprints: ReadonlyArray; readonly snapshotMessageCount: number; readonly snapshotMessages: ReadonlyArray; + readonly compactionHistory?: PrimeCompactionHistory | undefined; +}) { + const unchanged = planPrimeAgentRestartReplayUnchanged(input); + if (unchanged.valid || input.compactionHistory === undefined) return unchanged; + const replacement = planPrimeCompactionReplacement({ + history: input.compactionHistory, + observedCount: input.authorityMessageCount, + observedFingerprints: input.authorityFingerprints, + snapshotCount: input.snapshotMessageCount, + snapshot: input.snapshotMessages, + fingerprint: primeDaemonMessageFingerprint, + }); + return replacement === undefined + ? unchanged + : planPrimeAgentRestartReplayUnchanged({ + ...input, + authorityMessageCount: replacement.observedCount, + authorityFingerprints: replacement.observed.map(primeDaemonMessageFingerprint), + }); +} + +function planPrimeAgentRestartReplayUnchanged(input: { + readonly authorityMessageCount: number; + readonly authorityFingerprints: ReadonlyArray; + readonly snapshotMessageCount: number; + readonly snapshotMessages: ReadonlyArray; }): | { readonly valid: true; readonly backlog: ReadonlyArray } | { @@ -743,6 +773,38 @@ function reconcileTranscriptTail(input: { readonly observedCount: number; readonly snapshot: ReadonlyArray; readonly snapshotCount: number; + readonly compactionHistory?: PrimeCompactionHistory | undefined; +}): + | { + readonly missingMessages: ReadonlyArray; + readonly overlapCount: number; + readonly transition?: { readonly previousCount: number; readonly retainedCount: number }; + } + | undefined { + const unchanged = reconcileUnchangedTranscriptTail(input); + if (unchanged !== undefined || input.compactionHistory === undefined) return unchanged; + const replacement = planPrimeCompactionReplacement({ + history: input.compactionHistory, + observedCount: input.observedCount, + observedFingerprints: input.observed.map(primeDaemonMessageFingerprint), + snapshotCount: input.snapshotCount, + snapshot: input.snapshot, + fingerprint: primeDaemonMessageFingerprint, + }); + if (replacement === undefined) return undefined; + const reconciled = reconcileUnchangedTranscriptTail({ + ...input, + observed: replacement.observed, + observedCount: replacement.observedCount, + }); + return reconciled === undefined ? undefined : { ...reconciled, transition: replacement }; +} + +function reconcileUnchangedTranscriptTail(input: { + readonly observed: ReadonlyArray; + readonly observedCount: number; + readonly snapshot: ReadonlyArray; + readonly snapshotCount: number; }): | { readonly missingMessages: ReadonlyArray; @@ -1746,6 +1808,7 @@ export function makePrimeAgentDaemonAdapter( observedCount: context.nativeTranscriptMessageCount, snapshot: event.messages, snapshotCount: event.state.messageCount, + compactionHistory: event.compactionHistory, }); if (reconciliation === undefined) return false; const transcriptContinuityVerified = @@ -1785,6 +1848,16 @@ export function makePrimeAgentDaemonAdapter( } if (turn === undefined) return true; + if (reconciliation.transition !== undefined) { + const transition = reconciliation.transition; + turn.nativeTranscriptBaselineMessageCount = + 1 + + Math.max( + 0, + turn.nativeTranscriptBaselineMessageCount - + (transition.previousCount - transition.retainedCount), + ); + } const snapshotStartMessageCount = event.state.messageCount - event.messages.length; const currentTurnMessages = event.messages.slice( Math.max(0, turn.nativeTranscriptBaselineMessageCount - snapshotStartMessageCount), @@ -2584,6 +2657,18 @@ export function makePrimeAgentDaemonAdapter( return; } if (event._tag === "SessionResynced") { + const unchanged = reconcileUnchangedTranscriptTail({ + observed: context.nativeTranscript, + observedCount: context.nativeTranscriptMessageCount, + snapshot: event.messages, + snapshotCount: event.state.messageCount, + }); + const compactionHistory = + unchanged === undefined && context.runtime.getCompactionHistory !== undefined + ? yield* context.runtime.getCompactionHistory(event) + : undefined; + const snapshotEvent = + compactionHistory === undefined ? event : { ...event, compactionHistory }; const managedSourceVerified = yield* fileSystem .readFileString(context.managedExtensionPath) .pipe( @@ -2596,12 +2681,12 @@ export function makePrimeAgentDaemonAdapter( context.threadId, Effect.gen(function* () { if (sessions.get(context.threadId) === context && !context.stopped) { - const reconnectGeneration = event.connectionGeneration; + const reconnectGeneration = snapshotEvent.connectionGeneration; if ( reconnectGeneration !== undefined && !context.runtime.isConnectionGenerationCurrent( reconnectGeneration, - event.correlatedProofEpoch, + snapshotEvent.correlatedProofEpoch, ) ) { return; @@ -2624,11 +2709,11 @@ export function makePrimeAgentDaemonAdapter( context.managedPlanProjectionEnabled = true; const activeTurn = context.activeTurn; if (context.runtime.correlatedPromptLifecycleAvailable) { - if (event.initialSnapshot === true) { + if (snapshotEvent.initialSnapshot === true) { const lifecycle = activeTurn?.correlationId === undefined ? undefined - : event.promptLifecycles?.records.find( + : snapshotEvent.promptLifecycles?.records.find( (candidate) => candidate.correlationId === activeTurn.correlationId, ); if (lifecycle !== undefined) { @@ -2643,14 +2728,15 @@ export function makePrimeAgentDaemonAdapter( const transcriptPlan = reconcileTranscriptTail({ observed: context.nativeTranscript, observedCount: context.nativeTranscriptMessageCount, - snapshot: event.messages, - snapshotCount: event.state.messageCount, + snapshot: snapshotEvent.messages, + snapshotCount: snapshotEvent.state.messageCount, + compactionHistory: snapshotEvent.compactionHistory, }); const missingMessages = transcriptPlan?.missingMessages ?? []; const lifecycle = activeTurn?.correlationId === undefined ? undefined - : event.promptLifecycles?.records.find( + : snapshotEvent.promptLifecycles?.records.find( (candidate) => candidate.correlationId === activeTurn.correlationId, ); const currentLifecycle = activeTurn?.correlatedLifecycle; @@ -2669,8 +2755,8 @@ export function makePrimeAgentDaemonAdapter( : 0; const snapshotRecoversSubmittedUser = activeTurn !== undefined && - event.connectionGeneration !== undefined && - event.correlatedProofEpoch !== undefined && + snapshotEvent.connectionGeneration !== undefined && + snapshotEvent.correlatedProofEpoch !== undefined && activeTurn.queuedInputCount === 0 && (context.nativeTranscriptMessageCount === activeTurn.nativeTranscriptBaselineMessageCount || @@ -2695,8 +2781,8 @@ export function makePrimeAgentDaemonAdapter( const recoveredToolCalls = new Map(); const snapshotRecoversCurrentToolCycles = activeTurn !== undefined && - event.connectionGeneration !== undefined && - event.correlatedProofEpoch !== undefined && + snapshotEvent.connectionGeneration !== undefined && + snapshotEvent.correlatedProofEpoch !== undefined && activeTurn.queuedInputCount === 0 && currentLifecycle?.kind === "model_prompt" && currentLifecycle.phase === "delivered" && @@ -2760,11 +2846,12 @@ export function makePrimeAgentDaemonAdapter( missingMessages[0]?.role === "assistant" && context.nativeTranscript.at(-1)?.role === "user"); const transcriptReconciled = - (event.replayContinuity === "complete" || - (event.orderedSnapshot === true && event.replayContinuity === "unknown")) && + (snapshotEvent.replayContinuity === "complete" || + (snapshotEvent.orderedSnapshot === true && + snapshotEvent.replayContinuity === "unknown")) && transcriptPlan !== undefined && snapshotIsExactOrCurrentTerminal && - (yield* reconcileTranscriptSnapshotLocked(context, event)); + (yield* reconcileTranscriptSnapshotLocked(context, snapshotEvent)); if (!transcriptReconciled) { if (reconnectGeneration !== undefined) { context.runtime.resolveReconnectSnapshot(reconnectGeneration, false, false); @@ -2785,7 +2872,7 @@ export function makePrimeAgentDaemonAdapter( // A settings snapshot can precede native prompt ownership. It // may preserve an exact transcript, but cannot settle the turn. const snapshotPrecedesPromptDelivery = - event.orderedSnapshot === true && + snapshotEvent.orderedSnapshot === true && missingMessages.length === 0 && currentLifecycle?.deliveryCrossed !== true; if ( @@ -2821,7 +2908,10 @@ export function makePrimeAgentDaemonAdapter( } } else if (reconnectGeneration !== undefined) { const pendingRunCompletionBefore = activeTurn?.pendingRunCompletionHandoff; - const reconciled = yield* reconcileTranscriptSnapshotLocked(context, event); + const reconciled = yield* reconcileTranscriptSnapshotLocked( + context, + snapshotEvent, + ); recoveredSnapshotRunCompletion = activeTurn !== undefined && pendingRunCompletionBefore === undefined && @@ -2852,46 +2942,48 @@ export function makePrimeAgentDaemonAdapter( return; } } - context.autoCompactionEnabled = event.state.autoCompactionEnabled; - context.nativeRunActive = event.state.isStreaming; + context.autoCompactionEnabled = snapshotEvent.state.autoCompactionEnabled; + context.nativeRunActive = snapshotEvent.state.isStreaming; if ( context.activeTurn === undefined && - (event.state.isStreaming || - event.state.isCompacting || - event.state.isBashRunning || - event.state.retryAttempt > 0 || - event.state.inputQueue.activeAction || - event.state.inputQueue.steeringCount + event.state.inputQueue.followUpCount > + (snapshotEvent.state.isStreaming || + snapshotEvent.state.isCompacting || + snapshotEvent.state.isBashRunning || + snapshotEvent.state.retryAttempt > 0 || + snapshotEvent.state.inputQueue.activeAction || + snapshotEvent.state.inputQueue.steeringCount + + snapshotEvent.state.inputQueue.followUpCount > 0 || - event.children.some( + snapshotEvent.children.some( (child) => child.status === "queued" || child.status === "running", )) ) { yield* startBackgroundQuiescenceWatchLocked(context); } - context.nativeBashActive = event.state.isBashRunning; + context.nativeBashActive = snapshotEvent.state.isBashRunning; const compactionWasActive = context.activeCompactionScope !== undefined; - context.activeCompactionScope = event.state.isCompacting + context.activeCompactionScope = snapshotEvent.state.isCompacting ? (context.activeCompactionScope ?? {}) : undefined; - if (!event.state.isCompacting && !context.manualCompactionRequestActive) { + if (!snapshotEvent.state.isCompacting && !context.manualCompactionRequestActive) { context.compactionAbortRequested = false; } const initialRosterAlreadyProjected = context.agentRosterProjected && - event.lastEventSequence !== undefined && - event.lastEventSequence === context.runtime.initialSnapshot.lastEventSequence; + snapshotEvent.lastEventSequence !== undefined && + snapshotEvent.lastEventSequence === + context.runtime.initialSnapshot.lastEventSequence; yield* applyAgentRosterSnapshot( context, - event.children, + snapshotEvent.children, !initialRosterAlreadyProjected, ); - context.nativeQueueActionActive = event.state.inputQueue.activeAction; - yield* updateInputQueueProjection(context, event.state.inputQueue); + context.nativeQueueActionActive = snapshotEvent.state.inputQueue.activeAction; + yield* updateInputQueueProjection(context, snapshotEvent.state.inputQueue); yield* updateGoalProjection( context, context.session.runtimeMode === "full-access" - ? event.state.goal + ? snapshotEvent.state.goal : unavailableSessionGoal, ); yield* updateCompactionProjectionLocked(context, { @@ -2899,18 +2991,18 @@ export function makePrimeAgentDaemonAdapter( (context.manualCompactionRequestActive && context.compaction.status === "starting") || (context.compactionAbortRequested && - event.state.isCompacting && + snapshotEvent.state.isCompacting && context.compaction.status === "abort-requested") ? context.compaction.status - : event.state.isCompacting + : snapshotEvent.state.isCompacting ? "compacting" : "idle", abortable: - event.state.isCompacting && context.manualCompactionRequestActive + snapshotEvent.state.isCompacting && context.manualCompactionRequestActive ? context.compaction.abortable : false, ...(context.compaction.available - ? { autoCompactionEnabled: event.state.autoCompactionEnabled } + ? { autoCompactionEnabled: snapshotEvent.state.autoCompactionEnabled } : {}), }); const turn = @@ -2920,16 +3012,17 @@ export function makePrimeAgentDaemonAdapter( : context.activeTurn; if (turn !== undefined) { turn.queuedInputCount = - event.state.inputQueue.steeringCount + event.state.inputQueue.followUpCount; - if (event.state.isStreaming) { + snapshotEvent.state.inputQueue.steeringCount + + snapshotEvent.state.inputQueue.followUpCount; + if (snapshotEvent.state.isStreaming) { // The continuation may have started while disconnected. Apply every // RunStarted invariant from this authoritative snapshot too. observeNativeRunStarted(context, turn); } const authoritativeIdle = turn.queuedInputCount === 0 && - !event.state.inputQueue.activeAction && - !event.state.isStreaming; + !snapshotEvent.state.inputQueue.activeAction && + !snapshotEvent.state.isStreaming; if (turn.pendingRunCompletionHandoff !== undefined) { if ( recoveredSnapshotRunCompletion && @@ -2946,7 +3039,7 @@ export function makePrimeAgentDaemonAdapter( }); if (settled) yield* refreshContextUsage(context).pipe(Effect.forkDetach); } - } else if (compactionWasActive && !event.state.isCompacting) { + } else if (compactionWasActive && !snapshotEvent.state.isCompacting) { // The compaction terminal event may have been lost while // disconnected. Replace its consumed grace from the snapshot. yield* restartPendingRunCompletionHandoffLocked(context, turn); @@ -2962,7 +3055,10 @@ export function makePrimeAgentDaemonAdapter( if (explicitClear) { const settled = yield* settleActiveTurnLocked(context, turn, { state: "completed", - event: { _tag: "RunCompleted", messages: turn.completedRunMessages }, + event: { + _tag: "RunCompleted", + messages: turn.completedRunMessages, + }, }); if (settled) yield* refreshContextUsage(context).pipe(Effect.forkDetach); } else { @@ -4809,12 +4905,19 @@ export function makePrimeAgentDaemonAdapter( let recoveryBacklog: ReadonlyArray = []; if (recoveryStart?.kind === "adopt") { const authority = recoveryStart.authority; - const replay = planPrimeAgentRestartReplay({ + const replayInput = { authorityMessageCount: authority.transcriptMessageCount, authorityFingerprints: authority.transcriptFingerprints, snapshotMessageCount: runtime.initialSnapshot.state.messageCount, snapshotMessages: runtime.initialSnapshot.messages, - }); + }; + let replay = planPrimeAgentRestartReplay(replayInput); + if (!replay.valid && runtime.getCompactionHistory !== undefined) { + const compactionHistory = yield* runtime.getCompactionHistory( + runtime.initialSnapshot, + ); + replay = planPrimeAgentRestartReplay({ ...replayInput, compactionHistory }); + } if ( !replay.valid || (authority.turnId === null && diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonBridge.ts b/apps/server/src/provider/prime/PrimeAgentDaemonBridge.ts index 637660e08..dafa1e6f8 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonBridge.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonBridge.ts @@ -189,6 +189,7 @@ export interface PrimeAgentDaemonSessionWatcher { export interface PrimeAgentDaemonAgentConnection { readonly subscribe: (listener: (event: unknown) => void | Promise) => () => void; readonly getInitialSnapshot: () => Promise; + readonly getSessionTree?: () => Promise; readonly getRlmChildSnapshots?: () => Promise; readonly getState?: () => Promise; readonly navigateTree?: ( diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonEvents.test.ts b/apps/server/src/provider/prime/PrimeAgentDaemonEvents.test.ts index 798732bf8..9e3c3b70f 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonEvents.test.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonEvents.test.ts @@ -142,6 +142,30 @@ describe("PrimeAgentDaemonEvents", () => { } }); + it("accepts compaction summaries only in snapshots, never as live messages", () => { + const message = { + role: "compactionSummary", + summary: "private compaction", + tokensBefore: 100, + retainedMessageCount: 0, + timestamp: 10, + }; + for (const type of ["message_start", "message_end"]) { + expect(decodePrimeAgentDaemonEvent(sessionEvent({ type, message }))).toEqual({ + _tag: "CorrelatedProtocolViolation", + }); + } + const snapshot = decodePrimeAgentDaemonEvent({ + type: "session_resynced", + snapshot: { state: { ...state, messageCount: 1 }, messages: [message] }, + }); + expect(snapshot).toMatchObject({ + _tag: "SessionResynced", + messages: [{ role: "nativePrivate", kind: "compactionSummary", timestamp: 10 }], + }); + expect(JSON.stringify(snapshot)).not.toContain("private compaction"); + }); + for (const customType of ["refinement_outcome", "refinement_notice"] as const) { it(`retains ${customType} identity in live events and snapshots without private content`, () => { const refinement = { @@ -220,9 +244,147 @@ describe("PrimeAgentDaemonEvents", () => { }); } + for (const customType of [ + "compaction_outcome", + "ipython_state_restored", + "ipython_state", + "session_slash_command", + "session_slash_command_result", + "rlm_child_failure", + "rlm_child_terminal_notice", + "async_bash_completion", + "agent_message", + ] as const) { + it(`retains ${customType} identity in live events and snapshots without private content`, () => { + const refinement = { + role: "custom", + customType, + display: true, + content: "private memory update", + details: { refinementId: "refine-1", edits: [{ content: "private memory" }] }, + timestamp: 10, + }; + const completed = decodePrimeAgentDaemonEvent( + sessionEvent({ type: "message_end", message: refinement }), + ); + expect(completed).toMatchObject({ + _tag: "MessageCompleted", + message: { + role: "nativePrivate", + kind: customType, + timestamp: 10, + contentDigest: expect.stringMatching(/^[a-f0-9]{64}$/), + }, + }); + for (const attribution of [ + { scope: "session" }, + { scope: "prompt", correlationId: "fixture-turn" }, + ]) { + const event = { + type: "message_end", + message: refinement, + promptCorrelationId: attribution.correlationId ?? null, + }; + expect( + decodePrimeAgentDaemonEvent( + { type: "session_event", attribution, event }, + { correlatedPromptLifecycle: true }, + ), + ).toEqual({ ...completed, attribution }); + expect( + decodePrimeAgentDaemonEvent( + { + type: "session_event", + attribution, + event: { ...event, promptCorrelationId: "foreign" }, + }, + { correlatedPromptLifecycle: true }, + ), + ).toEqual({ _tag: "CorrelatedProtocolViolation" }); + } + const snapshot = decodePrimeAgentDaemonEvent({ + type: "session_resynced", + snapshot: { state: { ...state, messageCount: 1 }, messages: [refinement] }, + }); + if (completed?._tag !== "MessageCompleted") throw new Error("missing refinement"); + expect(snapshot).toMatchObject({ + _tag: "SessionResynced", + state: { messageCount: 1 }, + messages: [completed.message], + }); + expect( + decodePrimeAgentDaemonEvent(sessionEvent({ type: "message_start", message: refinement })), + ).toEqual({ _tag: "MessageStarted", message: completed.message }); + expect(JSON.stringify(snapshot)).not.toContain("private"); + for (const changed of [ + { ...refinement, content: "changed" }, + { ...refinement, display: !refinement.display }, + { + ...refinement, + customType: "refinement_outcome", + }, + { ...refinement, details: { refinementId: "refine-2" } }, + ]) { + expect( + decodePrimeAgentDaemonEvent(sessionEvent({ type: "message_end", message: changed })), + ).not.toEqual(completed); + } + }); + } + + it("retains exact native branch and bash records without exposing their content", () => { + for (const message of [ + { role: "branchSummary", summary: "private branch", fromId: "private-source", timestamp: 1 }, + { + role: "bashExecution", + command: "private command", + output: "private output", + exitCode: 0, + cancelled: false, + truncated: true, + fullOutputPath: "/private/output", + excludeFromContext: true, + timestamp: 1, + }, + ]) { + const completed = decodePrimeAgentDaemonEvent(sessionEvent({ type: "message_end", message })); + expect(completed).toMatchObject({ + _tag: "MessageCompleted", + message: { role: "nativePrivate", kind: message.role }, + }); + if (completed?._tag !== "MessageCompleted") throw new Error("missing native record"); + const snapshot = decodePrimeAgentDaemonEvent({ + type: "session_resynced", + snapshot: { state: { ...state, messageCount: 1 }, messages: [message] }, + }); + expect(snapshot).toMatchObject({ messages: [completed.message] }); + expect(JSON.stringify(snapshot)).not.toContain("private"); + for (const [key, value] of Object.entries(message)) { + if (key === "role") continue; + const changed = { + ...message, + [key]: + typeof value === "boolean" + ? !value + : typeof value === "number" + ? value + 1 + : `${value}-changed`, + }; + expect( + decodePrimeAgentDaemonEvent(sessionEvent({ type: "message_end", message: changed })), + ).not.toEqual(completed); + } + } + }); + it("does not recognize unrelated or visible custom messages as hidden harness digests", () => { for (const hidden of [ { customType: "other", display: false, details: { digest: "x" } }, + ...["heartbeat_prompt", "thread_goal_state", "goal_context"].map((customType) => ({ + customType, + display: true, + details: {}, + })), { customType: "harness_digest", display: true, details: { digest: "x" } }, { customType: "harness_digest", display: false, details: {} }, ]) { diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonEvents.ts b/apps/server/src/provider/prime/PrimeAgentDaemonEvents.ts index d1dec5a75..261153791 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonEvents.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonEvents.ts @@ -8,6 +8,7 @@ import { type SessionGoalUpdatedPayload, type SessionInputQueueDeliveryMode, } from "@t3tools/contracts"; +import type { PrimeCompactionHistory } from "./PrimeAgentCompactionHistory.ts"; import * as Option from "effect/Option"; import * as Predicate from "effect/Predicate"; import * as Schema from "effect/Schema"; @@ -170,12 +171,65 @@ const PrimeAgentDaemonRefinementMessage = Schema.Struct({ timestamp: Schema.Number, }); +// These built-in records participate in native history, but do not own a Pylon turn. +// Automation messages remain unsupported until Pylon owns their occurrences. +const PrimeAgentDaemonPrivateMessage = Schema.Struct({ + role: Schema.Literal("custom"), + customType: Schema.Literals([ + "compaction_outcome", + "ipython_state_restored", + "ipython_state", + "session_slash_command", + "session_slash_command_result", + "rlm_child_failure", + "rlm_child_terminal_notice", + "async_bash_completion", + "agent_message", + ]), + display: Schema.Boolean, + content: Schema.Union([Schema.String, Schema.Array(Schema.Union([textContent, imageContent]))]), + details: Schema.optional(Schema.Unknown), + timestamp: Schema.Finite, +}); + +const PrimeAgentDaemonBranchSummary = Schema.Struct({ + role: Schema.Literal("branchSummary"), + summary: Schema.String, + fromId: Schema.String, + timestamp: Schema.Finite, +}); +const PrimeAgentDaemonBashExecution = Schema.Struct({ + role: Schema.Literal("bashExecution"), + command: Schema.String, + output: Schema.String, + exitCode: Schema.optional(Schema.Finite), + cancelled: Schema.Boolean, + truncated: Schema.Boolean, + fullOutputPath: Schema.optional(Schema.String), + excludeFromContext: Schema.optional(Schema.Boolean), + timestamp: Schema.Finite, +}); + +const PrimeAgentDaemonCompactionSummary = Schema.Struct({ + role: Schema.Literal("compactionSummary"), + summary: Schema.String, + tokensBefore: Schema.Finite, + retainedMessageCount: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))), + customInstructions: Schema.optional(Schema.String), + harnessDigest: Schema.optional(Schema.String), + timestamp: Schema.Finite, +}); + export const PrimeAgentDaemonMessage = Schema.Union([ PrimeAgentDaemonUserMessage, PrimeAgentDaemonAssistantMessage, PrimeAgentDaemonToolResultMessage, PrimeAgentDaemonHarnessDigestMessage, PrimeAgentDaemonRefinementMessage, + PrimeAgentDaemonPrivateMessage, + PrimeAgentDaemonBranchSummary, + PrimeAgentDaemonBashExecution, + PrimeAgentDaemonCompactionSummary, ]); export type PrimeAgentDaemonMessage = typeof PrimeAgentDaemonMessage.Type; @@ -484,6 +538,7 @@ const contextUsage = Schema.Struct({ const sessionState = Schema.Struct({ activeSessionId: Schema.optional(Schema.String), + leafId: Schema.optional(Schema.NullOr(Schema.String)), cwd: Schema.String, thinkingLevel, serviceTier, @@ -775,6 +830,16 @@ export interface PrimeDaemonPlanUpdate { } export type PrimeDaemonMessage = + | { + readonly role: "nativePrivate"; + readonly kind: + | typeof PrimeAgentDaemonPrivateMessage.Type.customType + | "branchSummary" + | "bashExecution" + | "compactionSummary"; + readonly timestamp: number; + readonly contentDigest: string; + } | { readonly role: "refinementOutcome" | "refinementNotice"; readonly timestamp: number; @@ -820,6 +885,7 @@ export type PrimeDaemonMessage = }; export interface PrimeDaemonSessionState { + readonly leafId?: string | undefined; readonly activeSessionId?: string | undefined; readonly sessionId: string; readonly sessionName?: string | undefined; @@ -1262,6 +1328,7 @@ export type PrimeDaemonEvent = ( } | { readonly _tag: "SessionResynced"; + readonly compactionHistory?: PrimeCompactionHistory | undefined; readonly state: PrimeDaemonSessionState; readonly messages: ReadonlyArray; readonly streamingMessage?: PrimeDaemonMessage | undefined; @@ -1428,6 +1495,16 @@ function mapMessage(value: PrimeAgentDaemonMessage): PrimeDaemonMessage { .digest("hex"), }; } + if (value.customType !== "refinement_outcome" && value.customType !== "refinement_notice") { + return { + role: "nativePrivate", + kind: value.customType, + timestamp: value.timestamp, + contentDigest: NodeCrypto.createHash("sha256") + .update(JSON.stringify([value.display, value.content, value.details]), "utf8") + .digest("hex"), + }; + } return { role: value.customType === "refinement_outcome" ? "refinementOutcome" : "refinementNotice", timestamp: value.timestamp, @@ -1435,6 +1512,17 @@ function mapMessage(value: PrimeAgentDaemonMessage): PrimeDaemonMessage { .update(JSON.stringify([value.display, value.content, value.details]), "utf8") .digest("hex"), }; + case "branchSummary": + case "bashExecution": + case "compactionSummary": + return { + role: "nativePrivate", + kind: value.role, + timestamp: value.timestamp, + contentDigest: NodeCrypto.createHash("sha256") + .update(JSON.stringify(value), "utf8") + .digest("hex"), + }; case "user": { if (Predicate.isString(value.content)) { return { @@ -1510,6 +1598,11 @@ function mapMessage(value: PrimeAgentDaemonMessage): PrimeDaemonMessage { } } +export function decodePrimeAgentDaemonMessage(value: unknown): PrimeDaemonMessage | undefined { + const decoded = decodeMessage(value); + return Option.isSome(decoded) ? mapMessage(decoded.value) : undefined; +} + function mapUnknownMessages(values: ReadonlyArray): ReadonlyArray { return values.slice(-PRIME_AGENT_DAEMON_TRANSCRIPT_MAX_MESSAGES).flatMap((value) => { const decoded = decodeMessage(value); @@ -1566,6 +1659,7 @@ export function decodePrimeAgentDaemonSessionState( function mapState(value: typeof sessionState.Type): PrimeDaemonSessionState { return { + ...(typeof value.leafId === "string" ? { leafId: value.leafId } : {}), activeSessionId: optionalBounded(value.activeSessionId, MAX_PREVIEW_LENGTH), sessionId: bounded(value.sessionId, MAX_PREVIEW_LENGTH), sessionName: optionalBounded(value.sessionName, MAX_PREVIEW_LENGTH), @@ -1703,10 +1797,14 @@ function mapSessionEvent(event: typeof agentSessionEvent.Type): PrimeDaemonEvent toolResults: event.toolResults.map((message) => mapMessage(message)), }; case "message_start": + if (event.message.role === "compactionSummary") + return { _tag: "CorrelatedProtocolViolation" }; return { _tag: "MessageStarted", message: mapMessage(event.message) }; case "message_update": return mapAssistantStream(event.assistantMessageEvent); case "message_end": + if (event.message.role === "compactionSummary") + return { _tag: "CorrelatedProtocolViolation" }; return { _tag: "MessageCompleted", message: mapMessage(event.message) }; case "tool_execution_start": return { diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonRuntimeEvents.test.ts b/apps/server/src/provider/prime/PrimeAgentDaemonRuntimeEvents.test.ts index 780e0b885..a4f078db4 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonRuntimeEvents.test.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonRuntimeEvents.test.ts @@ -768,6 +768,39 @@ describe("mapPrimeAgentDaemonRuntimeEventDrafts", () => { } }); + it("never projects built-in private native history as user or assistant output", () => { + for (const kind of [ + "compaction_outcome", + "ipython_state_restored", + "ipython_state", + "session_slash_command", + "session_slash_command_result", + "rlm_child_failure", + "rlm_child_terminal_notice", + "async_bash_completion", + "agent_message", + "branchSummary", + "bashExecution", + ] as const) { + for (const _tag of ["MessageStarted", "MessageCompleted"] as const) { + expect( + mapPrimeAgentDaemonRuntimeEventDrafts({ + ...context, + event: { + _tag, + message: { + role: "nativePrivate", + kind, + timestamp: 1, + contentDigest: "private-identity", + }, + }, + }), + ).toEqual([]); + } + } + }); + it("maps compaction lifecycle without provider instructions, summaries, or error text", () => { expect( mapPrimeAgentDaemonRuntimeEventDrafts({ diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonSessionRuntime.test.ts b/apps/server/src/provider/prime/PrimeAgentDaemonSessionRuntime.test.ts index 9f05de8e6..40121408d 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonSessionRuntime.test.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonSessionRuntime.test.ts @@ -17,6 +17,7 @@ import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; import * as Queue from "effect/Queue"; import * as Scheduler from "effect/Scheduler"; +import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; @@ -69,6 +70,7 @@ const goal = { continuationsUsed: 0, }; const activeSignal = () => new AbortController().signal; +const encodePrivateHistory = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); function snapshot(sequence = 4) { return { @@ -299,6 +301,7 @@ function fixture(options?: { readonly omitQueueMutation?: boolean; readonly queueMutationCapability?: boolean; readonly getStateImpl?: () => Promise; + readonly getSessionTreeImpl?: (() => Promise) | undefined; readonly navigateTreeImpl?: ( targetId: string, options?: { readonly summarize?: boolean }, @@ -525,6 +528,8 @@ function fixture(options?: { class FakeConnection implements PrimeAgentDaemonAgentConnection { constructor() { + if (options?.getSessionTreeImpl === undefined) + Object.defineProperty(this, "getSessionTree", { value: undefined }); if (options?.omitSendAgentMessage === true) { Object.defineProperty(this, "sendAgentMessage", { value: undefined }); } @@ -597,6 +602,10 @@ function fixture(options?: { } return options?.rawSnapshotImpl?.() ?? options?.rawSnapshot ?? snapshot(); } + getSessionTree(): Promise { + captures.connectionCalls.push({ method: "getSessionTree", args: [] }); + return options?.getSessionTreeImpl?.() ?? Promise.resolve(undefined); + } getRlmChildSnapshots(): Promise { captures.connectionCalls.push({ method: "getRlmChildSnapshots", args: [] }); return Promise.resolve(options?.authoritativeRlmChildren); @@ -12047,6 +12056,76 @@ describe("PrimeAgentDaemonSessionRuntime", () => { ), ); + for (const variant of [ + "valid", + "wrong leaf", + "wrong session", + "stale generation", + "retired during read", + "unavailable", + ] as const) { + it.effect(`fences compaction tree reads: ${variant}`, () => + Effect.scoped( + Effect.gen(function* () { + const timestamp = "2026-09-14T12:00:00.000Z"; + const tree = { + leafId: variant === "wrong leaf" ? "foreign" : "c", + tree: [ + { + entry: { + id: "a", + parentId: null, + timestamp, + type: "message", + message: { role: "user", content: "before", timestamp: 1 }, + }, + children: [ + { + entry: { + id: "c", + parentId: "a", + timestamp, + type: "compaction", + summary: "private summary", + tokensBefore: 10, + firstKeptEntryId: "a", + }, + children: [], + }, + ], + }, + ], + }; + const test = fixture({ + getSessionTreeImpl: + variant === "unavailable" + ? undefined + : async () => { + if (variant === "retired during read") + await test.emit({ type: "connection_status", status: "reconnecting" }); + return tree; + }, + }); + const runtime = yield* test.make(); + if (runtime.getCompactionHistory === undefined) throw new Error("missing history reader"); + const candidate = { + ...runtime.initialSnapshot, + state: { + ...runtime.initialSnapshot.state, + leafId: "c", + ...(variant === "wrong session" ? { sessionId: "foreign" } : {}), + }, + ...(variant === "stale generation" ? { connectionGeneration: 99 } : {}), + }; + const history = yield* runtime.getCompactionHistory(candidate); + expect(history !== undefined).toBe(variant === "valid"); + if (history !== undefined) + expect(encodePrivateHistory(history)).not.toContain("private summary"); + }), + ), + ); + } + it.effect("uses argument-free compaction controls and projects only safe state", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonSessionRuntime.ts b/apps/server/src/provider/prime/PrimeAgentDaemonSessionRuntime.ts index 6a06105a3..44f290e89 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonSessionRuntime.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonSessionRuntime.ts @@ -59,6 +59,7 @@ import { decodePrimeAgentDaemonSessionState, decodePrimeAgentPromptLifecycleCancellationResult, decodePrimeAgentPromptLifecycleStateSnapshot, + decodePrimeAgentDaemonMessage, decodePrimeAgentPromptLifecycleSubmitResult, primeAgentDaemonImageDigest, PRIME_AGENT_DAEMON_MESSAGE_TEXT_MAX_CHARS, @@ -73,6 +74,10 @@ import { type PrimeDaemonPromptLifecycleStateSnapshot, type PrimeDaemonUsage, } from "./PrimeAgentDaemonEvents.ts"; +import { + decodePrimeCompactionHistory, + type PrimeCompactionHistory, +} from "./PrimeAgentCompactionHistory.ts"; import type { PrimeAgentDaemonManager } from "./PrimeAgentDaemonManager.ts"; import type { PrimeAgentOwnershipReceiptHandle } from "./PrimeAgentOwnershipReceipt.ts"; import type { PrimeAgentRuntimeContext } from "./PrimeAgentRuntimeContext.ts"; @@ -121,6 +126,10 @@ export const isPrimeAgentWorkerRecovering = (cause: unknown, activeSessionId: st Predicate.isString(cause.activeSessionId) && cause.activeSessionId === activeSessionId)); +function recordCompactionSummary(value: unknown): boolean { + return Predicate.isObject(value) && value.role === "compactionSummary"; +} + function workerRecoverySnapshotIsUnsafe( raw: unknown, baselineMessageCount: number, @@ -130,7 +139,14 @@ function workerRecoverySnapshotIsUnsafe( const messages = raw.snapshot.messages; if (!Array.isArray(messages)) return true; const advancedMessageCount = messageCount - baselineMessageCount; - if (advancedMessageCount < 0 || advancedMessageCount > messages.length) return true; + if (advancedMessageCount < 0) { + return !( + recordCompactionSummary(messages[0]) && + Predicate.isObject(raw.snapshot.state) && + typeof raw.snapshot.state.leafId === "string" + ); + } + if (advancedMessageCount > messages.length) return true; if (advancedMessageCount === 0) return false; return messages .slice(-advancedMessageCount) @@ -1091,6 +1107,9 @@ export interface PrimeAgentDaemonSessionRuntime { /** Stable only within one compatible daemon supervisor generation. */ readonly conversationRuntimeGeneration?: string; readonly initialSnapshot: PrimeAgentDaemonCanonicalSnapshot; + readonly getCompactionHistory?: ( + snapshot: PrimeAgentDaemonCanonicalSnapshot, + ) => Effect.Effect; /** Private immutable root selected from the raw initial snapshot. */ readonly initialConversationLeafId?: string; readonly conversationRollbackAvailable: boolean; @@ -5722,6 +5741,42 @@ export const makePrimeAgentDaemonSessionRuntime = Effect.fn("makePrimeAgentDaemo } satisfies PrimeAgentDaemonCompactionState; }); + const getCompactionHistory = (snapshot: PrimeAgentDaemonCanonicalSnapshot) => + Effect.gen(function* () { + const native = connection; + const generation = connectionGeneration; + const epoch = correlatedProofEpoch; + const leafId = snapshot.state.leafId; + if ( + disposeStarted || + disposed || + native?.getSessionTree === undefined || + leafId === undefined || + snapshot.state.sessionId !== sessionId || + snapshot.state.activeSessionId !== activeSessionId || + (snapshot.connectionGeneration !== undefined && + snapshot.connectionGeneration !== generation) + ) + return undefined; + const tree = yield* Effect.tryPromise({ + try: () => native.getSessionTree!(), + catch: () => undefined, + }).pipe( + Effect.timeoutOption(COMMAND_TIMEOUT_MS), + Effect.orElseSucceed(() => Option.none()), + ); + if ( + Option.isNone(tree) || + connection !== native || + connectionGeneration !== generation || + correlatedProofEpoch !== epoch || + disposeStarted || + disposed + ) + return undefined; + return decodePrimeCompactionHistory(tree.value, leafId, decodePrimeAgentDaemonMessage); + }); + const compact = Effect.gen(function* () { yield* ensureOpen("compact"); if (!compactionAvailable) { @@ -8783,6 +8838,7 @@ export const makePrimeAgentDaemonSessionRuntime = Effect.fn("makePrimeAgentDaemo autoCompactionWritable, initialCompactionState, getCompactionState, + getCompactionHistory, compact, refineLocalHarness, abortCompaction, diff --git a/apps/server/src/provider/prime/PrimeAgentRestartReplay.test.ts b/apps/server/src/provider/prime/PrimeAgentRestartReplay.test.ts index 6abcd4997..7664bd24d 100644 --- a/apps/server/src/provider/prime/PrimeAgentRestartReplay.test.ts +++ b/apps/server/src/provider/prime/PrimeAgentRestartReplay.test.ts @@ -75,6 +75,67 @@ describe("planPrimeAgentRestartReplay", () => { } }); + it("resumes built-in private messages without accepting changed or reordered history", () => { + const refinements = [ + "compaction_outcome", + "ipython_state_restored", + "ipython_state", + "session_slash_command", + "session_slash_command_result", + "rlm_child_failure", + "rlm_child_terminal_notice", + "async_bash_completion", + "agent_message", + ].map((customType, index) => { + const decoded = decodePrimeAgentDaemonEvent( + { + type: "session_event", + attribution: { scope: "session" }, + event: { + type: "message_end", + promptCorrelationId: null, + message: { + role: "custom", + customType, + display: index === 0, + timestamp: index, + content: "private memory", + details: { refinementId: "fixture", source: "auto" }, + }, + }, + }, + { correlatedPromptLifecycle: true }, + ); + if (decoded._tag !== "MessageCompleted") throw new Error("missing refinement"); + return decoded.message; + }); + const snapshotMessages = [...refinements, message(20)]; + const authority = { + authorityMessageCount: refinements.length, + authorityFingerprints: refinements.map(fingerprint), + }; + expect( + planPrimeAgentRestartReplay({ + ...authority, + snapshotMessageCount: refinements.length + 1, + snapshotMessages, + }), + ).toEqual({ valid: true, backlog: [message(20)] }); + for (const history of [ + refinements.toReversed(), + refinements.map((item) => ({ ...item, timestamp: 99 })), + refinements.slice(0, 1), + ]) { + expect( + planPrimeAgentRestartReplay({ + ...authority, + snapshotMessageCount: refinements.length + 1, + snapshotMessages: [...history, message(20)], + }), + ).toEqual({ valid: false }); + } + }); + it("fails closed on changed overlap or a transcript retention gap", () => { const messages = [1, 2, 3, 4, 5].map(message); expect( diff --git a/apps/server/src/provider/prime/fixtures/native-compaction/isolated-manual.json b/apps/server/src/provider/prime/fixtures/native-compaction/isolated-manual.json new file mode 100644 index 000000000..5ac225d5e --- /dev/null +++ b/apps/server/src/provider/prime/fixtures/native-compaction/isolated-manual.json @@ -0,0 +1,223 @@ +{ + "before": [ + { + "role": "custom", + "customType": "harness_digest", + "content": "[harness-digest]\n\nThe persistent memories produced across this session so far:\n\n\n# Continual Harness State\n\nLocal continual harness entries belong to this Prime Agent session. Global continual harness entries persist across Prime Agent sessions.\nThe continual harness entries below are compact summaries, not full descriptions. Use them as routing/context hints; inspect or refine the underlying continual harness entry only when detail matters.\nDefault to local continual harness refinement for current task progress, temporary blockers, and session coordination. Use global continual harness refinement only for stable cross-session lessons, durable user preferences, reusable skills/subagents, or explicitly project-qualified facts.\nUse these continual harness prompt notes, memories, skills, and subagent specs when they are relevant. The base system prompt is immutable; prompt entries below are supplemental notes only.\n\nWhen to refine the continual harness: after a repeated failure, a reusable tactic emerges, a repeated delegation role should become a subagent spec, a repeated procedure should become a skill, a durable fact/preference should become a memory, a narrow behavioral policy should become a prompt addendum, a user corrects behavior that should persist locally or globally, validation shows a continual harness entry is wrong, or a skill/subagent/memory/prompt note should be created, updated, deleted, or rolled back. Keep continual harness edits small and evidence-backed.\n\nCall contract: read each installed Python skill's SKILL.md and call its documented module function in the Python REPL; do not assume a `.run` entrypoint. Use ` ...` in shell when a CLI exists. Continual harness skill entries are Python REPL skills with an explicit Python `reference` and `arguments` contract. Spawn a continual harness subagent spec by composing a concise task prompt and calling `handle = await rlm.spawn('sub-task', name='worker')`; admission returns immediately with `rlm_child_id`, `name`, `session_dir`, and `model`, never the child's answer. Results arrive only through explicit `agent_message` replies or files; children reply with `await agent_message.send(message, receiver_role='parent')`. Use `await rlm.list_subagents()` to recover direct child handles and `await agent_message.send(..., receiver_role='child', receiver_name=handle.name)` for follow-ups. Do not invent wrappers such as `call_skill(...)`, `run_subagent(...)`, or named subagent registries.\n\nprompt: 2\n- [global:bounded-subprocess-probes] Bound subprocess probes and report long operations (policy/process-safety, v1): Never use unbounded blocking reads while probing subprocesses. Use bounded subprocess APIs such as run or communicate with explicit timeouts, terminate only a process handle cap...\n- [global:refinement-recommendation-checkpoints] Offer refinement recommendations at meaningful checkpoints (policy/refinement, v1): At meaningful milestones—completed substantial work, a verified incident conclusion, repeated user correction or successful tactic, before pausing a long task, or when session-l...\n\nmemory: 1\n- [global:prime-thinking-levels-opt-in] Prime xhigh/max thinking levels are opt-in per model (prime-agent/thinking-levels, v1): Prime Agent surfaces `off/minimal/low/medium/high` for any reasoning model, but `xhigh` and `max` are opt-in per model via `thinkingLevelMap` (e.g. `{ xhigh: \"xhigh\", max: \"max\"...\n\nskill: 0\n\nsubagent: 0\n\nrecent refinements: 2\n- [refine_0001] User asked whether a just-resolved incident memory was valuable or noise; audit found the entry was trajectory-shaped (~400 words) with a now-stale pending_action.: create memory:prime-thinking-levels-opt-in (global), delete memory:prime-thinking-level-gating-meridian (local); outcome: Lesson for future refinement: capture the transferable rule, not the incident narrative. Do not persist applied diffs that the code already documents, and never leave a pending_...\n- [refine_0002] User approved the subprocess-probe safety recommendation after an unbounded readline blocked progress.: create prompt:bounded-subprocess-probes (global); outcome: Future subprocess diagnostics are required to be bounded, handle-owned, and announced when long-running.\n", + "display": false, + "details": { + "digest": "# Continual Harness State\n\nLocal continual harness entries belong to this Prime Agent session. Global continual harness entries persist across Prime Agent sessions.\nThe continual harness entries below are compact summaries, not full descriptions. Use them as routing/context hints; inspect or refine the underlying continual harness entry only when detail matters.\nDefault to local continual harness refinement for current task progress, temporary blockers, and session coordination. Use global continual harness refinement only for stable cross-session lessons, durable user preferences, reusable skills/subagents, or explicitly project-qualified facts.\nUse these continual harness prompt notes, memories, skills, and subagent specs when they are relevant. The base system prompt is immutable; prompt entries below are supplemental notes only.\n\nWhen to refine the continual harness: after a repeated failure, a reusable tactic emerges, a repeated delegation role should become a subagent spec, a repeated procedure should become a skill, a durable fact/preference should become a memory, a narrow behavioral policy should become a prompt addendum, a user corrects behavior that should persist locally or globally, validation shows a continual harness entry is wrong, or a skill/subagent/memory/prompt note should be created, updated, deleted, or rolled back. Keep continual harness edits small and evidence-backed.\n\nCall contract: read each installed Python skill's SKILL.md and call its documented module function in the Python REPL; do not assume a `.run` entrypoint. Use ` ...` in shell when a CLI exists. Continual harness skill entries are Python REPL skills with an explicit Python `reference` and `arguments` contract. Spawn a continual harness subagent spec by composing a concise task prompt and calling `handle = await rlm.spawn('sub-task', name='worker')`; admission returns immediately with `rlm_child_id`, `name`, `session_dir`, and `model`, never the child's answer. Results arrive only through explicit `agent_message` replies or files; children reply with `await agent_message.send(message, receiver_role='parent')`. Use `await rlm.list_subagents()` to recover direct child handles and `await agent_message.send(..., receiver_role='child', receiver_name=handle.name)` for follow-ups. Do not invent wrappers such as `call_skill(...)`, `run_subagent(...)`, or named subagent registries.\n\nprompt: 2\n- [global:bounded-subprocess-probes] Bound subprocess probes and report long operations (policy/process-safety, v1): Never use unbounded blocking reads while probing subprocesses. Use bounded subprocess APIs such as run or communicate with explicit timeouts, terminate only a process handle cap...\n- [global:refinement-recommendation-checkpoints] Offer refinement recommendations at meaningful checkpoints (policy/refinement, v1): At meaningful milestones—completed substantial work, a verified incident conclusion, repeated user correction or successful tactic, before pausing a long task, or when session-l...\n\nmemory: 1\n- [global:prime-thinking-levels-opt-in] Prime xhigh/max thinking levels are opt-in per model (prime-agent/thinking-levels, v1): Prime Agent surfaces `off/minimal/low/medium/high` for any reasoning model, but `xhigh` and `max` are opt-in per model via `thinkingLevelMap` (e.g. `{ xhigh: \"xhigh\", max: \"max\"...\n\nskill: 0\n\nsubagent: 0\n\nrecent refinements: 2\n- [refine_0001] User asked whether a just-resolved incident memory was valuable or noise; audit found the entry was trajectory-shaped (~400 words) with a now-stale pending_action.: create memory:prime-thinking-levels-opt-in (global), delete memory:prime-thinking-level-gating-meridian (local); outcome: Lesson for future refinement: capture the transferable rule, not the incident narrative. Do not persist applied diffs that the code already documents, and never leave a pending_...\n- [refine_0002] User approved the subprocess-probe safety recommendation after an unbounded readline blocked progress.: create prompt:bounded-subprocess-probes (global); outcome: Future subprocess diagnostics are required to be bounded, handle-owned, and announced when long-running." + }, + "timestamp": 1789433260649 + }, + { "role": "user", "content": [{ "type": "text", "text": "one" }], "timestamp": 1789433260646 }, + { + "role": "assistant", + "content": [{ "type": "text", "text": "one response" }], + "api": "faux:1789433260513:3pa7uv3cjyh", + "provider": "faux", + "model": "faux-1", + "usage": { + "input": 3593, + "output": 3, + "cacheRead": 0, + "cacheWrite": 0, + "totalTokens": 3596, + "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0, "total": 0 } + }, + "stopReason": "stop", + "timestamp": 1789433260645 + }, + { "role": "user", "content": [{ "type": "text", "text": "two" }], "timestamp": 1789433260774 }, + { + "role": "assistant", + "content": [{ "type": "text", "text": "two response" }], + "api": "faux:1789433260513:3pa7uv3cjyh", + "provider": "faux", + "model": "faux-1", + "usage": { + "input": 3601, + "output": 3, + "cacheRead": 0, + "cacheWrite": 0, + "totalTokens": 3604, + "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0, "total": 0 } + }, + "stopReason": "stop", + "timestamp": 1789433260645 + } + ], + "after": [ + { + "role": "compactionSummary", + "summary": "summary\n\n---\n\n**Turn Context (split turn):**\n\nturn summary", + "tokensBefore": 3604, + "retainedMessageCount": 1, + "harnessDigest": "# Continual Harness State\n\nLocal continual harness entries belong to this Prime Agent session. Global continual harness entries persist across Prime Agent sessions.\nThe continual harness entries below are compact summaries, not full descriptions. Use them as routing/context hints; inspect or refine the underlying continual harness entry only when detail matters.\nDefault to local continual harness refinement for current task progress, temporary blockers, and session coordination. Use global continual harness refinement only for stable cross-session lessons, durable user preferences, reusable skills/subagents, or explicitly project-qualified facts.\nUse these continual harness prompt notes, memories, skills, and subagent specs when they are relevant. The base system prompt is immutable; prompt entries below are supplemental notes only.\n\nWhen to refine the continual harness: after a repeated failure, a reusable tactic emerges, a repeated delegation role should become a subagent spec, a repeated procedure should become a skill, a durable fact/preference should become a memory, a narrow behavioral policy should become a prompt addendum, a user corrects behavior that should persist locally or globally, validation shows a continual harness entry is wrong, or a skill/subagent/memory/prompt note should be created, updated, deleted, or rolled back. Keep continual harness edits small and evidence-backed.\n\nCall contract: read each installed Python skill's SKILL.md and call its documented module function in the Python REPL; do not assume a `.run` entrypoint. Use ` ...` in shell when a CLI exists. Continual harness skill entries are Python REPL skills with an explicit Python `reference` and `arguments` contract. Spawn a continual harness subagent spec by composing a concise task prompt and calling `handle = await rlm.spawn('sub-task', name='worker')`; admission returns immediately with `rlm_child_id`, `name`, `session_dir`, and `model`, never the child's answer. Results arrive only through explicit `agent_message` replies or files; children reply with `await agent_message.send(message, receiver_role='parent')`. Use `await rlm.list_subagents()` to recover direct child handles and `await agent_message.send(..., receiver_role='child', receiver_name=handle.name)` for follow-ups. Do not invent wrappers such as `call_skill(...)`, `run_subagent(...)`, or named subagent registries.\n\nprompt: 2\n- [global:bounded-subprocess-probes] Bound subprocess probes and report long operations (policy/process-safety, v1): Never use unbounded blocking reads while probing subprocesses. Use bounded subprocess APIs such as run or communicate with explicit timeouts, terminate only a process handle cap...\n- [global:refinement-recommendation-checkpoints] Offer refinement recommendations at meaningful checkpoints (policy/refinement, v1): At meaningful milestones—completed substantial work, a verified incident conclusion, repeated user correction or successful tactic, before pausing a long task, or when session-l...\n\nmemory: 1\n- [global:prime-thinking-levels-opt-in] Prime xhigh/max thinking levels are opt-in per model (prime-agent/thinking-levels, v1): Prime Agent surfaces `off/minimal/low/medium/high` for any reasoning model, but `xhigh` and `max` are opt-in per model via `thinkingLevelMap` (e.g. `{ xhigh: \"xhigh\", max: \"max\"...\n\nskill: 0\n\nsubagent: 0\n\nrecent refinements: 2\n- [refine_0001] User asked whether a just-resolved incident memory was valuable or noise; audit found the entry was trajectory-shaped (~400 words) with a now-stale pending_action.: create memory:prime-thinking-levels-opt-in (global), delete memory:prime-thinking-level-gating-meridian (local); outcome: Lesson for future refinement: capture the transferable rule, not the incident narrative. Do not persist applied diffs that the code already documents, and never leave a pending_...\n- [refine_0002] User approved the subprocess-probe safety recommendation after an unbounded readline blocked progress.: create prompt:bounded-subprocess-probes (global); outcome: Future subprocess diagnostics are required to be bounded, handle-owned, and announced when long-running.", + "timestamp": 1789433260893 + }, + { + "role": "assistant", + "content": [{ "type": "text", "text": "two response" }], + "api": "faux:1789433260513:3pa7uv3cjyh", + "provider": "faux", + "model": "faux-1", + "usage": { + "input": 3601, + "output": 3, + "cacheRead": 0, + "cacheWrite": 0, + "totalTokens": 3604, + "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0, "total": 0 } + }, + "stopReason": "stop", + "timestamp": 1789433260645 + } + ], + "tree": { + "tree": [ + { + "entry": { + "type": "custom_message", + "customType": "harness_digest", + "content": "[harness-digest]\n\nThe persistent memories produced across this session so far:\n\n\n# Continual Harness State\n\nLocal continual harness entries belong to this Prime Agent session. Global continual harness entries persist across Prime Agent sessions.\nThe continual harness entries below are compact summaries, not full descriptions. Use them as routing/context hints; inspect or refine the underlying continual harness entry only when detail matters.\nDefault to local continual harness refinement for current task progress, temporary blockers, and session coordination. Use global continual harness refinement only for stable cross-session lessons, durable user preferences, reusable skills/subagents, or explicitly project-qualified facts.\nUse these continual harness prompt notes, memories, skills, and subagent specs when they are relevant. The base system prompt is immutable; prompt entries below are supplemental notes only.\n\nWhen to refine the continual harness: after a repeated failure, a reusable tactic emerges, a repeated delegation role should become a subagent spec, a repeated procedure should become a skill, a durable fact/preference should become a memory, a narrow behavioral policy should become a prompt addendum, a user corrects behavior that should persist locally or globally, validation shows a continual harness entry is wrong, or a skill/subagent/memory/prompt note should be created, updated, deleted, or rolled back. Keep continual harness edits small and evidence-backed.\n\nCall contract: read each installed Python skill's SKILL.md and call its documented module function in the Python REPL; do not assume a `.run` entrypoint. Use ` ...` in shell when a CLI exists. Continual harness skill entries are Python REPL skills with an explicit Python `reference` and `arguments` contract. Spawn a continual harness subagent spec by composing a concise task prompt and calling `handle = await rlm.spawn('sub-task', name='worker')`; admission returns immediately with `rlm_child_id`, `name`, `session_dir`, and `model`, never the child's answer. Results arrive only through explicit `agent_message` replies or files; children reply with `await agent_message.send(message, receiver_role='parent')`. Use `await rlm.list_subagents()` to recover direct child handles and `await agent_message.send(..., receiver_role='child', receiver_name=handle.name)` for follow-ups. Do not invent wrappers such as `call_skill(...)`, `run_subagent(...)`, or named subagent registries.\n\nprompt: 2\n- [global:bounded-subprocess-probes] Bound subprocess probes and report long operations (policy/process-safety, v1): Never use unbounded blocking reads while probing subprocesses. Use bounded subprocess APIs such as run or communicate with explicit timeouts, terminate only a process handle cap...\n- [global:refinement-recommendation-checkpoints] Offer refinement recommendations at meaningful checkpoints (policy/refinement, v1): At meaningful milestones—completed substantial work, a verified incident conclusion, repeated user correction or successful tactic, before pausing a long task, or when session-l...\n\nmemory: 1\n- [global:prime-thinking-levels-opt-in] Prime xhigh/max thinking levels are opt-in per model (prime-agent/thinking-levels, v1): Prime Agent surfaces `off/minimal/low/medium/high` for any reasoning model, but `xhigh` and `max` are opt-in per model via `thinkingLevelMap` (e.g. `{ xhigh: \"xhigh\", max: \"max\"...\n\nskill: 0\n\nsubagent: 0\n\nrecent refinements: 2\n- [refine_0001] User asked whether a just-resolved incident memory was valuable or noise; audit found the entry was trajectory-shaped (~400 words) with a now-stale pending_action.: create memory:prime-thinking-levels-opt-in (global), delete memory:prime-thinking-level-gating-meridian (local); outcome: Lesson for future refinement: capture the transferable rule, not the incident narrative. Do not persist applied diffs that the code already documents, and never leave a pending_...\n- [refine_0002] User approved the subprocess-probe safety recommendation after an unbounded readline blocked progress.: create prompt:bounded-subprocess-probes (global); outcome: Future subprocess diagnostics are required to be bounded, handle-owned, and announced when long-running.\n", + "display": false, + "details": { + "digest": "# Continual Harness State\n\nLocal continual harness entries belong to this Prime Agent session. Global continual harness entries persist across Prime Agent sessions.\nThe continual harness entries below are compact summaries, not full descriptions. Use them as routing/context hints; inspect or refine the underlying continual harness entry only when detail matters.\nDefault to local continual harness refinement for current task progress, temporary blockers, and session coordination. Use global continual harness refinement only for stable cross-session lessons, durable user preferences, reusable skills/subagents, or explicitly project-qualified facts.\nUse these continual harness prompt notes, memories, skills, and subagent specs when they are relevant. The base system prompt is immutable; prompt entries below are supplemental notes only.\n\nWhen to refine the continual harness: after a repeated failure, a reusable tactic emerges, a repeated delegation role should become a subagent spec, a repeated procedure should become a skill, a durable fact/preference should become a memory, a narrow behavioral policy should become a prompt addendum, a user corrects behavior that should persist locally or globally, validation shows a continual harness entry is wrong, or a skill/subagent/memory/prompt note should be created, updated, deleted, or rolled back. Keep continual harness edits small and evidence-backed.\n\nCall contract: read each installed Python skill's SKILL.md and call its documented module function in the Python REPL; do not assume a `.run` entrypoint. Use ` ...` in shell when a CLI exists. Continual harness skill entries are Python REPL skills with an explicit Python `reference` and `arguments` contract. Spawn a continual harness subagent spec by composing a concise task prompt and calling `handle = await rlm.spawn('sub-task', name='worker')`; admission returns immediately with `rlm_child_id`, `name`, `session_dir`, and `model`, never the child's answer. Results arrive only through explicit `agent_message` replies or files; children reply with `await agent_message.send(message, receiver_role='parent')`. Use `await rlm.list_subagents()` to recover direct child handles and `await agent_message.send(..., receiver_role='child', receiver_name=handle.name)` for follow-ups. Do not invent wrappers such as `call_skill(...)`, `run_subagent(...)`, or named subagent registries.\n\nprompt: 2\n- [global:bounded-subprocess-probes] Bound subprocess probes and report long operations (policy/process-safety, v1): Never use unbounded blocking reads while probing subprocesses. Use bounded subprocess APIs such as run or communicate with explicit timeouts, terminate only a process handle cap...\n- [global:refinement-recommendation-checkpoints] Offer refinement recommendations at meaningful checkpoints (policy/refinement, v1): At meaningful milestones—completed substantial work, a verified incident conclusion, repeated user correction or successful tactic, before pausing a long task, or when session-l...\n\nmemory: 1\n- [global:prime-thinking-levels-opt-in] Prime xhigh/max thinking levels are opt-in per model (prime-agent/thinking-levels, v1): Prime Agent surfaces `off/minimal/low/medium/high` for any reasoning model, but `xhigh` and `max` are opt-in per model via `thinkingLevelMap` (e.g. `{ xhigh: \"xhigh\", max: \"max\"...\n\nskill: 0\n\nsubagent: 0\n\nrecent refinements: 2\n- [refine_0001] User asked whether a just-resolved incident memory was valuable or noise; audit found the entry was trajectory-shaped (~400 words) with a now-stale pending_action.: create memory:prime-thinking-levels-opt-in (global), delete memory:prime-thinking-level-gating-meridian (local); outcome: Lesson for future refinement: capture the transferable rule, not the incident narrative. Do not persist applied diffs that the code already documents, and never leave a pending_...\n- [refine_0002] User approved the subprocess-probe safety recommendation after an unbounded readline blocked progress.: create prompt:bounded-subprocess-probes (global); outcome: Future subprocess diagnostics are required to be bounded, handle-owned, and announced when long-running." + }, + "id": "e758cfc6", + "parentId": null, + "timestamp": "2026-09-15T00:47:40.649Z" + }, + "children": [ + { + "entry": { + "type": "message", + "id": "aa66fc81", + "parentId": "e758cfc6", + "timestamp": "2026-09-15T00:47:40.711Z", + "message": { + "role": "user", + "content": [{ "type": "text", "text": "one" }], + "timestamp": 1789433260646 + } + }, + "children": [ + { + "entry": { + "type": "message", + "id": "359f8d3a", + "parentId": "aa66fc81", + "timestamp": "2026-09-15T00:47:40.712Z", + "message": { + "role": "assistant", + "content": [{ "type": "text", "text": "one response" }], + "api": "faux:1789433260513:3pa7uv3cjyh", + "provider": "faux", + "model": "faux-1", + "usage": { + "input": 3593, + "output": 3, + "cacheRead": 0, + "cacheWrite": 0, + "totalTokens": 3596, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0, + "total": 0 + } + }, + "stopReason": "stop", + "timestamp": 1789433260645 + } + }, + "children": [ + { + "entry": { + "type": "message", + "id": "f8f15914", + "parentId": "359f8d3a", + "timestamp": "2026-09-15T00:47:40.832Z", + "message": { + "role": "user", + "content": [{ "type": "text", "text": "two" }], + "timestamp": 1789433260774 + } + }, + "children": [ + { + "entry": { + "type": "message", + "id": "48eaa6e5", + "parentId": "f8f15914", + "timestamp": "2026-09-15T00:47:40.833Z", + "message": { + "role": "assistant", + "content": [{ "type": "text", "text": "two response" }], + "api": "faux:1789433260513:3pa7uv3cjyh", + "provider": "faux", + "model": "faux-1", + "usage": { + "input": 3601, + "output": 3, + "cacheRead": 0, + "cacheWrite": 0, + "totalTokens": 3604, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0, + "total": 0 + } + }, + "stopReason": "stop", + "timestamp": 1789433260645 + } + }, + "children": [ + { + "entry": { + "type": "compaction", + "id": "12716c91", + "parentId": "48eaa6e5", + "timestamp": "2026-09-15T00:47:40.893Z", + "summary": "summary\n\n---\n\n**Turn Context (split turn):**\n\nturn summary", + "firstKeptEntryId": "48eaa6e5", + "tokensBefore": 3604, + "details": { "readFiles": [], "modifiedFiles": [] }, + "fromHook": false, + "usage": { + "input": 594, + "output": 5, + "cacheRead": 0, + "cacheWrite": 0, + "totalTokens": 599, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0, + "total": 0 + } + }, + "harnessDigest": "# Continual Harness State\n\nLocal continual harness entries belong to this Prime Agent session. Global continual harness entries persist across Prime Agent sessions.\nThe continual harness entries below are compact summaries, not full descriptions. Use them as routing/context hints; inspect or refine the underlying continual harness entry only when detail matters.\nDefault to local continual harness refinement for current task progress, temporary blockers, and session coordination. Use global continual harness refinement only for stable cross-session lessons, durable user preferences, reusable skills/subagents, or explicitly project-qualified facts.\nUse these continual harness prompt notes, memories, skills, and subagent specs when they are relevant. The base system prompt is immutable; prompt entries below are supplemental notes only.\n\nWhen to refine the continual harness: after a repeated failure, a reusable tactic emerges, a repeated delegation role should become a subagent spec, a repeated procedure should become a skill, a durable fact/preference should become a memory, a narrow behavioral policy should become a prompt addendum, a user corrects behavior that should persist locally or globally, validation shows a continual harness entry is wrong, or a skill/subagent/memory/prompt note should be created, updated, deleted, or rolled back. Keep continual harness edits small and evidence-backed.\n\nCall contract: read each installed Python skill's SKILL.md and call its documented module function in the Python REPL; do not assume a `.run` entrypoint. Use ` ...` in shell when a CLI exists. Continual harness skill entries are Python REPL skills with an explicit Python `reference` and `arguments` contract. Spawn a continual harness subagent spec by composing a concise task prompt and calling `handle = await rlm.spawn('sub-task', name='worker')`; admission returns immediately with `rlm_child_id`, `name`, `session_dir`, and `model`, never the child's answer. Results arrive only through explicit `agent_message` replies or files; children reply with `await agent_message.send(message, receiver_role='parent')`. Use `await rlm.list_subagents()` to recover direct child handles and `await agent_message.send(..., receiver_role='child', receiver_name=handle.name)` for follow-ups. Do not invent wrappers such as `call_skill(...)`, `run_subagent(...)`, or named subagent registries.\n\nprompt: 2\n- [global:bounded-subprocess-probes] Bound subprocess probes and report long operations (policy/process-safety, v1): Never use unbounded blocking reads while probing subprocesses. Use bounded subprocess APIs such as run or communicate with explicit timeouts, terminate only a process handle cap...\n- [global:refinement-recommendation-checkpoints] Offer refinement recommendations at meaningful checkpoints (policy/refinement, v1): At meaningful milestones—completed substantial work, a verified incident conclusion, repeated user correction or successful tactic, before pausing a long task, or when session-l...\n\nmemory: 1\n- [global:prime-thinking-levels-opt-in] Prime xhigh/max thinking levels are opt-in per model (prime-agent/thinking-levels, v1): Prime Agent surfaces `off/minimal/low/medium/high` for any reasoning model, but `xhigh` and `max` are opt-in per model via `thinkingLevelMap` (e.g. `{ xhigh: \"xhigh\", max: \"max\"...\n\nskill: 0\n\nsubagent: 0\n\nrecent refinements: 2\n- [refine_0001] User asked whether a just-resolved incident memory was valuable or noise; audit found the entry was trajectory-shaped (~400 words) with a now-stale pending_action.: create memory:prime-thinking-levels-opt-in (global), delete memory:prime-thinking-level-gating-meridian (local); outcome: Lesson for future refinement: capture the transferable rule, not the incident narrative. Do not persist applied diffs that the code already documents, and never leave a pending_...\n- [refine_0002] User approved the subprocess-probe safety recommendation after an unbounded readline blocked progress.: create prompt:bounded-subprocess-probes (global); outcome: Future subprocess diagnostics are required to be bounded, handle-owned, and announced when long-running." + }, + "children": [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ], + "leafId": "12716c91" + } +} diff --git a/apps/server/src/provider/prime/fixtures/native-compaction/legacy-timestamp-mismatch.json b/apps/server/src/provider/prime/fixtures/native-compaction/legacy-timestamp-mismatch.json new file mode 100644 index 000000000..bed6d44c2 --- /dev/null +++ b/apps/server/src/provider/prime/fixtures/native-compaction/legacy-timestamp-mismatch.json @@ -0,0 +1,223 @@ +{ + "before": [ + { + "role": "custom", + "customType": "harness_digest", + "content": "[harness-digest]\n\nThe persistent memories produced across this session so far:\n\n\n# Continual Harness State\n\nLocal continual harness entries belong to this Prime Agent session. Global continual harness entries persist across Prime Agent sessions.\nThe continual harness entries below are compact summaries, not full descriptions. Use them as routing/context hints; inspect or refine the underlying continual harness entry only when detail matters.\nDefault to local continual harness refinement for current task progress, temporary blockers, and session coordination. Use global continual harness refinement only for stable cross-session lessons, durable user preferences, reusable skills/subagents, or explicitly project-qualified facts.\nUse these continual harness prompt notes, memories, skills, and subagent specs when they are relevant. The base system prompt is immutable; prompt entries below are supplemental notes only.\n\nWhen to refine the continual harness: after a repeated failure, a reusable tactic emerges, a repeated delegation role should become a subagent spec, a repeated procedure should become a skill, a durable fact/preference should become a memory, a narrow behavioral policy should become a prompt addendum, a user corrects behavior that should persist locally or globally, validation shows a continual harness entry is wrong, or a skill/subagent/memory/prompt note should be created, updated, deleted, or rolled back. Keep continual harness edits small and evidence-backed.\n\nCall contract: read each installed Python skill's SKILL.md and call its documented module function in the Python REPL; do not assume a `.run` entrypoint. Use ` ...` in shell when a CLI exists. Continual harness skill entries are Python REPL skills with an explicit Python `reference` and `arguments` contract. Spawn a continual harness subagent spec by composing a concise task prompt and calling `handle = await rlm.spawn('sub-task', name='worker')`; admission returns immediately with `rlm_child_id`, `name`, `session_dir`, and `model`, never the child's answer. Results arrive only through explicit `agent_message` replies or files; children reply with `await agent_message.send(message, receiver_role='parent')`. Use `await rlm.list_subagents()` to recover direct child handles and `await agent_message.send(..., receiver_role='child', receiver_name=handle.name)` for follow-ups. Do not invent wrappers such as `call_skill(...)`, `run_subagent(...)`, or named subagent registries.\n\nprompt: 2\n- [global:bounded-subprocess-probes] Bound subprocess probes and report long operations (policy/process-safety, v1): Never use unbounded blocking reads while probing subprocesses. Use bounded subprocess APIs such as run or communicate with explicit timeouts, terminate only a process handle cap...\n- [global:refinement-recommendation-checkpoints] Offer refinement recommendations at meaningful checkpoints (policy/refinement, v1): At meaningful milestones—completed substantial work, a verified incident conclusion, repeated user correction or successful tactic, before pausing a long task, or when session-l...\n\nmemory: 1\n- [global:prime-thinking-levels-opt-in] Prime xhigh/max thinking levels are opt-in per model (prime-agent/thinking-levels, v1): Prime Agent surfaces `off/minimal/low/medium/high` for any reasoning model, but `xhigh` and `max` are opt-in per model via `thinkingLevelMap` (e.g. `{ xhigh: \"xhigh\", max: \"max\"...\n\nskill: 0\n\nsubagent: 0\n\nrecent refinements: 2\n- [refine_0001] User asked whether a just-resolved incident memory was valuable or noise; audit found the entry was trajectory-shaped (~400 words) with a now-stale pending_action.: create memory:prime-thinking-levels-opt-in (global), delete memory:prime-thinking-level-gating-meridian (local); outcome: Lesson for future refinement: capture the transferable rule, not the incident narrative. Do not persist applied diffs that the code already documents, and never leave a pending_...\n- [refine_0002] User approved the subprocess-probe safety recommendation after an unbounded readline blocked progress.: create prompt:bounded-subprocess-probes (global); outcome: Future subprocess diagnostics are required to be bounded, handle-owned, and announced when long-running.\n", + "display": false, + "details": { + "digest": "# Continual Harness State\n\nLocal continual harness entries belong to this Prime Agent session. Global continual harness entries persist across Prime Agent sessions.\nThe continual harness entries below are compact summaries, not full descriptions. Use them as routing/context hints; inspect or refine the underlying continual harness entry only when detail matters.\nDefault to local continual harness refinement for current task progress, temporary blockers, and session coordination. Use global continual harness refinement only for stable cross-session lessons, durable user preferences, reusable skills/subagents, or explicitly project-qualified facts.\nUse these continual harness prompt notes, memories, skills, and subagent specs when they are relevant. The base system prompt is immutable; prompt entries below are supplemental notes only.\n\nWhen to refine the continual harness: after a repeated failure, a reusable tactic emerges, a repeated delegation role should become a subagent spec, a repeated procedure should become a skill, a durable fact/preference should become a memory, a narrow behavioral policy should become a prompt addendum, a user corrects behavior that should persist locally or globally, validation shows a continual harness entry is wrong, or a skill/subagent/memory/prompt note should be created, updated, deleted, or rolled back. Keep continual harness edits small and evidence-backed.\n\nCall contract: read each installed Python skill's SKILL.md and call its documented module function in the Python REPL; do not assume a `.run` entrypoint. Use ` ...` in shell when a CLI exists. Continual harness skill entries are Python REPL skills with an explicit Python `reference` and `arguments` contract. Spawn a continual harness subagent spec by composing a concise task prompt and calling `handle = await rlm.spawn('sub-task', name='worker')`; admission returns immediately with `rlm_child_id`, `name`, `session_dir`, and `model`, never the child's answer. Results arrive only through explicit `agent_message` replies or files; children reply with `await agent_message.send(message, receiver_role='parent')`. Use `await rlm.list_subagents()` to recover direct child handles and `await agent_message.send(..., receiver_role='child', receiver_name=handle.name)` for follow-ups. Do not invent wrappers such as `call_skill(...)`, `run_subagent(...)`, or named subagent registries.\n\nprompt: 2\n- [global:bounded-subprocess-probes] Bound subprocess probes and report long operations (policy/process-safety, v1): Never use unbounded blocking reads while probing subprocesses. Use bounded subprocess APIs such as run or communicate with explicit timeouts, terminate only a process handle cap...\n- [global:refinement-recommendation-checkpoints] Offer refinement recommendations at meaningful checkpoints (policy/refinement, v1): At meaningful milestones—completed substantial work, a verified incident conclusion, repeated user correction or successful tactic, before pausing a long task, or when session-l...\n\nmemory: 1\n- [global:prime-thinking-levels-opt-in] Prime xhigh/max thinking levels are opt-in per model (prime-agent/thinking-levels, v1): Prime Agent surfaces `off/minimal/low/medium/high` for any reasoning model, but `xhigh` and `max` are opt-in per model via `thinkingLevelMap` (e.g. `{ xhigh: \"xhigh\", max: \"max\"...\n\nskill: 0\n\nsubagent: 0\n\nrecent refinements: 2\n- [refine_0001] User asked whether a just-resolved incident memory was valuable or noise; audit found the entry was trajectory-shaped (~400 words) with a now-stale pending_action.: create memory:prime-thinking-levels-opt-in (global), delete memory:prime-thinking-level-gating-meridian (local); outcome: Lesson for future refinement: capture the transferable rule, not the incident narrative. Do not persist applied diffs that the code already documents, and never leave a pending_...\n- [refine_0002] User approved the subprocess-probe safety recommendation after an unbounded readline blocked progress.: create prompt:bounded-subprocess-probes (global); outcome: Future subprocess diagnostics are required to be bounded, handle-owned, and announced when long-running." + }, + "timestamp": 1789432878875 + }, + { "role": "user", "content": [{ "type": "text", "text": "one" }], "timestamp": 1789432878872 }, + { + "role": "assistant", + "content": [{ "type": "text", "text": "one response" }], + "api": "faux:1789432878812:6i2sfc7317r", + "provider": "faux", + "model": "faux-1", + "usage": { + "input": 3593, + "output": 3, + "cacheRead": 0, + "cacheWrite": 0, + "totalTokens": 3596, + "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0, "total": 0 } + }, + "stopReason": "stop", + "timestamp": 1789432878871 + }, + { "role": "user", "content": [{ "type": "text", "text": "two" }], "timestamp": 1789432878970 }, + { + "role": "assistant", + "content": [{ "type": "text", "text": "two response" }], + "api": "faux:1789432878812:6i2sfc7317r", + "provider": "faux", + "model": "faux-1", + "usage": { + "input": 3601, + "output": 3, + "cacheRead": 0, + "cacheWrite": 0, + "totalTokens": 3604, + "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0, "total": 0 } + }, + "stopReason": "stop", + "timestamp": 1789432878871 + } + ], + "after": [ + { + "role": "compactionSummary", + "summary": "summary\n\n---\n\n**Turn Context (split turn):**\n\nturn summary", + "tokensBefore": 3604, + "retainedMessageCount": 1, + "harnessDigest": "# Continual Harness State\n\nLocal continual harness entries belong to this Prime Agent session. Global continual harness entries persist across Prime Agent sessions.\nThe continual harness entries below are compact summaries, not full descriptions. Use them as routing/context hints; inspect or refine the underlying continual harness entry only when detail matters.\nDefault to local continual harness refinement for current task progress, temporary blockers, and session coordination. Use global continual harness refinement only for stable cross-session lessons, durable user preferences, reusable skills/subagents, or explicitly project-qualified facts.\nUse these continual harness prompt notes, memories, skills, and subagent specs when they are relevant. The base system prompt is immutable; prompt entries below are supplemental notes only.\n\nWhen to refine the continual harness: after a repeated failure, a reusable tactic emerges, a repeated delegation role should become a subagent spec, a repeated procedure should become a skill, a durable fact/preference should become a memory, a narrow behavioral policy should become a prompt addendum, a user corrects behavior that should persist locally or globally, validation shows a continual harness entry is wrong, or a skill/subagent/memory/prompt note should be created, updated, deleted, or rolled back. Keep continual harness edits small and evidence-backed.\n\nCall contract: read each installed Python skill's SKILL.md and call its documented module function in the Python REPL; do not assume a `.run` entrypoint. Use ` ...` in shell when a CLI exists. Continual harness skill entries are Python REPL skills with an explicit Python `reference` and `arguments` contract. Spawn a continual harness subagent spec by composing a concise task prompt and calling `handle = await rlm.spawn('sub-task', name='worker')`; admission returns immediately with `rlm_child_id`, `name`, `session_dir`, and `model`, never the child's answer. Results arrive only through explicit `agent_message` replies or files; children reply with `await agent_message.send(message, receiver_role='parent')`. Use `await rlm.list_subagents()` to recover direct child handles and `await agent_message.send(..., receiver_role='child', receiver_name=handle.name)` for follow-ups. Do not invent wrappers such as `call_skill(...)`, `run_subagent(...)`, or named subagent registries.\n\nprompt: 2\n- [global:bounded-subprocess-probes] Bound subprocess probes and report long operations (policy/process-safety, v1): Never use unbounded blocking reads while probing subprocesses. Use bounded subprocess APIs such as run or communicate with explicit timeouts, terminate only a process handle cap...\n- [global:refinement-recommendation-checkpoints] Offer refinement recommendations at meaningful checkpoints (policy/refinement, v1): At meaningful milestones—completed substantial work, a verified incident conclusion, repeated user correction or successful tactic, before pausing a long task, or when session-l...\n\nmemory: 1\n- [global:prime-thinking-levels-opt-in] Prime xhigh/max thinking levels are opt-in per model (prime-agent/thinking-levels, v1): Prime Agent surfaces `off/minimal/low/medium/high` for any reasoning model, but `xhigh` and `max` are opt-in per model via `thinkingLevelMap` (e.g. `{ xhigh: \"xhigh\", max: \"max\"...\n\nskill: 0\n\nsubagent: 0\n\nrecent refinements: 2\n- [refine_0001] User asked whether a just-resolved incident memory was valuable or noise; audit found the entry was trajectory-shaped (~400 words) with a now-stale pending_action.: create memory:prime-thinking-levels-opt-in (global), delete memory:prime-thinking-level-gating-meridian (local); outcome: Lesson for future refinement: capture the transferable rule, not the incident narrative. Do not persist applied diffs that the code already documents, and never leave a pending_...\n- [refine_0002] User approved the subprocess-probe safety recommendation after an unbounded readline blocked progress.: create prompt:bounded-subprocess-probes (global); outcome: Future subprocess diagnostics are required to be bounded, handle-owned, and announced when long-running.", + "timestamp": 1789432879073 + }, + { + "role": "assistant", + "content": [{ "type": "text", "text": "two response" }], + "api": "faux:1789432878812:6i2sfc7317r", + "provider": "faux", + "model": "faux-1", + "usage": { + "input": 3601, + "output": 3, + "cacheRead": 0, + "cacheWrite": 0, + "totalTokens": 3604, + "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0, "total": 0 } + }, + "stopReason": "stop", + "timestamp": 1789432878871 + } + ], + "tree": { + "tree": [ + { + "entry": { + "type": "custom_message", + "customType": "harness_digest", + "content": "[harness-digest]\n\nThe persistent memories produced across this session so far:\n\n\n# Continual Harness State\n\nLocal continual harness entries belong to this Prime Agent session. Global continual harness entries persist across Prime Agent sessions.\nThe continual harness entries below are compact summaries, not full descriptions. Use them as routing/context hints; inspect or refine the underlying continual harness entry only when detail matters.\nDefault to local continual harness refinement for current task progress, temporary blockers, and session coordination. Use global continual harness refinement only for stable cross-session lessons, durable user preferences, reusable skills/subagents, or explicitly project-qualified facts.\nUse these continual harness prompt notes, memories, skills, and subagent specs when they are relevant. The base system prompt is immutable; prompt entries below are supplemental notes only.\n\nWhen to refine the continual harness: after a repeated failure, a reusable tactic emerges, a repeated delegation role should become a subagent spec, a repeated procedure should become a skill, a durable fact/preference should become a memory, a narrow behavioral policy should become a prompt addendum, a user corrects behavior that should persist locally or globally, validation shows a continual harness entry is wrong, or a skill/subagent/memory/prompt note should be created, updated, deleted, or rolled back. Keep continual harness edits small and evidence-backed.\n\nCall contract: read each installed Python skill's SKILL.md and call its documented module function in the Python REPL; do not assume a `.run` entrypoint. Use ` ...` in shell when a CLI exists. Continual harness skill entries are Python REPL skills with an explicit Python `reference` and `arguments` contract. Spawn a continual harness subagent spec by composing a concise task prompt and calling `handle = await rlm.spawn('sub-task', name='worker')`; admission returns immediately with `rlm_child_id`, `name`, `session_dir`, and `model`, never the child's answer. Results arrive only through explicit `agent_message` replies or files; children reply with `await agent_message.send(message, receiver_role='parent')`. Use `await rlm.list_subagents()` to recover direct child handles and `await agent_message.send(..., receiver_role='child', receiver_name=handle.name)` for follow-ups. Do not invent wrappers such as `call_skill(...)`, `run_subagent(...)`, or named subagent registries.\n\nprompt: 2\n- [global:bounded-subprocess-probes] Bound subprocess probes and report long operations (policy/process-safety, v1): Never use unbounded blocking reads while probing subprocesses. Use bounded subprocess APIs such as run or communicate with explicit timeouts, terminate only a process handle cap...\n- [global:refinement-recommendation-checkpoints] Offer refinement recommendations at meaningful checkpoints (policy/refinement, v1): At meaningful milestones—completed substantial work, a verified incident conclusion, repeated user correction or successful tactic, before pausing a long task, or when session-l...\n\nmemory: 1\n- [global:prime-thinking-levels-opt-in] Prime xhigh/max thinking levels are opt-in per model (prime-agent/thinking-levels, v1): Prime Agent surfaces `off/minimal/low/medium/high` for any reasoning model, but `xhigh` and `max` are opt-in per model via `thinkingLevelMap` (e.g. `{ xhigh: \"xhigh\", max: \"max\"...\n\nskill: 0\n\nsubagent: 0\n\nrecent refinements: 2\n- [refine_0001] User asked whether a just-resolved incident memory was valuable or noise; audit found the entry was trajectory-shaped (~400 words) with a now-stale pending_action.: create memory:prime-thinking-levels-opt-in (global), delete memory:prime-thinking-level-gating-meridian (local); outcome: Lesson for future refinement: capture the transferable rule, not the incident narrative. Do not persist applied diffs that the code already documents, and never leave a pending_...\n- [refine_0002] User approved the subprocess-probe safety recommendation after an unbounded readline blocked progress.: create prompt:bounded-subprocess-probes (global); outcome: Future subprocess diagnostics are required to be bounded, handle-owned, and announced when long-running.\n", + "display": false, + "details": { + "digest": "# Continual Harness State\n\nLocal continual harness entries belong to this Prime Agent session. Global continual harness entries persist across Prime Agent sessions.\nThe continual harness entries below are compact summaries, not full descriptions. Use them as routing/context hints; inspect or refine the underlying continual harness entry only when detail matters.\nDefault to local continual harness refinement for current task progress, temporary blockers, and session coordination. Use global continual harness refinement only for stable cross-session lessons, durable user preferences, reusable skills/subagents, or explicitly project-qualified facts.\nUse these continual harness prompt notes, memories, skills, and subagent specs when they are relevant. The base system prompt is immutable; prompt entries below are supplemental notes only.\n\nWhen to refine the continual harness: after a repeated failure, a reusable tactic emerges, a repeated delegation role should become a subagent spec, a repeated procedure should become a skill, a durable fact/preference should become a memory, a narrow behavioral policy should become a prompt addendum, a user corrects behavior that should persist locally or globally, validation shows a continual harness entry is wrong, or a skill/subagent/memory/prompt note should be created, updated, deleted, or rolled back. Keep continual harness edits small and evidence-backed.\n\nCall contract: read each installed Python skill's SKILL.md and call its documented module function in the Python REPL; do not assume a `.run` entrypoint. Use ` ...` in shell when a CLI exists. Continual harness skill entries are Python REPL skills with an explicit Python `reference` and `arguments` contract. Spawn a continual harness subagent spec by composing a concise task prompt and calling `handle = await rlm.spawn('sub-task', name='worker')`; admission returns immediately with `rlm_child_id`, `name`, `session_dir`, and `model`, never the child's answer. Results arrive only through explicit `agent_message` replies or files; children reply with `await agent_message.send(message, receiver_role='parent')`. Use `await rlm.list_subagents()` to recover direct child handles and `await agent_message.send(..., receiver_role='child', receiver_name=handle.name)` for follow-ups. Do not invent wrappers such as `call_skill(...)`, `run_subagent(...)`, or named subagent registries.\n\nprompt: 2\n- [global:bounded-subprocess-probes] Bound subprocess probes and report long operations (policy/process-safety, v1): Never use unbounded blocking reads while probing subprocesses. Use bounded subprocess APIs such as run or communicate with explicit timeouts, terminate only a process handle cap...\n- [global:refinement-recommendation-checkpoints] Offer refinement recommendations at meaningful checkpoints (policy/refinement, v1): At meaningful milestones—completed substantial work, a verified incident conclusion, repeated user correction or successful tactic, before pausing a long task, or when session-l...\n\nmemory: 1\n- [global:prime-thinking-levels-opt-in] Prime xhigh/max thinking levels are opt-in per model (prime-agent/thinking-levels, v1): Prime Agent surfaces `off/minimal/low/medium/high` for any reasoning model, but `xhigh` and `max` are opt-in per model via `thinkingLevelMap` (e.g. `{ xhigh: \"xhigh\", max: \"max\"...\n\nskill: 0\n\nsubagent: 0\n\nrecent refinements: 2\n- [refine_0001] User asked whether a just-resolved incident memory was valuable or noise; audit found the entry was trajectory-shaped (~400 words) with a now-stale pending_action.: create memory:prime-thinking-levels-opt-in (global), delete memory:prime-thinking-level-gating-meridian (local); outcome: Lesson for future refinement: capture the transferable rule, not the incident narrative. Do not persist applied diffs that the code already documents, and never leave a pending_...\n- [refine_0002] User approved the subprocess-probe safety recommendation after an unbounded readline blocked progress.: create prompt:bounded-subprocess-probes (global); outcome: Future subprocess diagnostics are required to be bounded, handle-owned, and announced when long-running." + }, + "id": "fbbccb2b", + "parentId": null, + "timestamp": "2026-09-15T00:41:18.921Z" + }, + "children": [ + { + "entry": { + "type": "message", + "id": "a1a4cb30", + "parentId": "fbbccb2b", + "timestamp": "2026-09-15T00:41:18.922Z", + "message": { + "role": "user", + "content": [{ "type": "text", "text": "one" }], + "timestamp": 1789432878872 + } + }, + "children": [ + { + "entry": { + "type": "message", + "id": "08a1c03d", + "parentId": "a1a4cb30", + "timestamp": "2026-09-15T00:41:18.923Z", + "message": { + "role": "assistant", + "content": [{ "type": "text", "text": "one response" }], + "api": "faux:1789432878812:6i2sfc7317r", + "provider": "faux", + "model": "faux-1", + "usage": { + "input": 3593, + "output": 3, + "cacheRead": 0, + "cacheWrite": 0, + "totalTokens": 3596, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0, + "total": 0 + } + }, + "stopReason": "stop", + "timestamp": 1789432878871 + } + }, + "children": [ + { + "entry": { + "type": "message", + "id": "aed87587", + "parentId": "08a1c03d", + "timestamp": "2026-09-15T00:41:19.017Z", + "message": { + "role": "user", + "content": [{ "type": "text", "text": "two" }], + "timestamp": 1789432878970 + } + }, + "children": [ + { + "entry": { + "type": "message", + "id": "cfd70ee8", + "parentId": "aed87587", + "timestamp": "2026-09-15T00:41:19.018Z", + "message": { + "role": "assistant", + "content": [{ "type": "text", "text": "two response" }], + "api": "faux:1789432878812:6i2sfc7317r", + "provider": "faux", + "model": "faux-1", + "usage": { + "input": 3601, + "output": 3, + "cacheRead": 0, + "cacheWrite": 0, + "totalTokens": 3604, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0, + "total": 0 + } + }, + "stopReason": "stop", + "timestamp": 1789432878871 + } + }, + "children": [ + { + "entry": { + "type": "compaction", + "id": "a078443a", + "parentId": "cfd70ee8", + "timestamp": "2026-09-15T00:41:19.073Z", + "summary": "summary\n\n---\n\n**Turn Context (split turn):**\n\nturn summary", + "firstKeptEntryId": "cfd70ee8", + "tokensBefore": 3604, + "details": { "readFiles": [], "modifiedFiles": [] }, + "fromHook": false, + "usage": { + "input": 505, + "output": 5, + "cacheRead": 89, + "cacheWrite": 506, + "totalTokens": 1105, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0, + "total": 0 + } + }, + "harnessDigest": "# Continual Harness State\n\nLocal continual harness entries belong to this Prime Agent session. Global continual harness entries persist across Prime Agent sessions.\nThe continual harness entries below are compact summaries, not full descriptions. Use them as routing/context hints; inspect or refine the underlying continual harness entry only when detail matters.\nDefault to local continual harness refinement for current task progress, temporary blockers, and session coordination. Use global continual harness refinement only for stable cross-session lessons, durable user preferences, reusable skills/subagents, or explicitly project-qualified facts.\nUse these continual harness prompt notes, memories, skills, and subagent specs when they are relevant. The base system prompt is immutable; prompt entries below are supplemental notes only.\n\nWhen to refine the continual harness: after a repeated failure, a reusable tactic emerges, a repeated delegation role should become a subagent spec, a repeated procedure should become a skill, a durable fact/preference should become a memory, a narrow behavioral policy should become a prompt addendum, a user corrects behavior that should persist locally or globally, validation shows a continual harness entry is wrong, or a skill/subagent/memory/prompt note should be created, updated, deleted, or rolled back. Keep continual harness edits small and evidence-backed.\n\nCall contract: read each installed Python skill's SKILL.md and call its documented module function in the Python REPL; do not assume a `.run` entrypoint. Use ` ...` in shell when a CLI exists. Continual harness skill entries are Python REPL skills with an explicit Python `reference` and `arguments` contract. Spawn a continual harness subagent spec by composing a concise task prompt and calling `handle = await rlm.spawn('sub-task', name='worker')`; admission returns immediately with `rlm_child_id`, `name`, `session_dir`, and `model`, never the child's answer. Results arrive only through explicit `agent_message` replies or files; children reply with `await agent_message.send(message, receiver_role='parent')`. Use `await rlm.list_subagents()` to recover direct child handles and `await agent_message.send(..., receiver_role='child', receiver_name=handle.name)` for follow-ups. Do not invent wrappers such as `call_skill(...)`, `run_subagent(...)`, or named subagent registries.\n\nprompt: 2\n- [global:bounded-subprocess-probes] Bound subprocess probes and report long operations (policy/process-safety, v1): Never use unbounded blocking reads while probing subprocesses. Use bounded subprocess APIs such as run or communicate with explicit timeouts, terminate only a process handle cap...\n- [global:refinement-recommendation-checkpoints] Offer refinement recommendations at meaningful checkpoints (policy/refinement, v1): At meaningful milestones—completed substantial work, a verified incident conclusion, repeated user correction or successful tactic, before pausing a long task, or when session-l...\n\nmemory: 1\n- [global:prime-thinking-levels-opt-in] Prime xhigh/max thinking levels are opt-in per model (prime-agent/thinking-levels, v1): Prime Agent surfaces `off/minimal/low/medium/high` for any reasoning model, but `xhigh` and `max` are opt-in per model via `thinkingLevelMap` (e.g. `{ xhigh: \"xhigh\", max: \"max\"...\n\nskill: 0\n\nsubagent: 0\n\nrecent refinements: 2\n- [refine_0001] User asked whether a just-resolved incident memory was valuable or noise; audit found the entry was trajectory-shaped (~400 words) with a now-stale pending_action.: create memory:prime-thinking-levels-opt-in (global), delete memory:prime-thinking-level-gating-meridian (local); outcome: Lesson for future refinement: capture the transferable rule, not the incident narrative. Do not persist applied diffs that the code already documents, and never leave a pending_...\n- [refine_0002] User approved the subprocess-probe safety recommendation after an unbounded readline blocked progress.: create prompt:bounded-subprocess-probes (global); outcome: Future subprocess diagnostics are required to be bounded, handle-owned, and announced when long-running." + }, + "children": [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ], + "leafId": "a078443a" + } +}