| 1 | import assert from "node:assert/strict"; |
| 2 | import { canonicalMessage } from "../lib/canonicalTranscriptBackend"; |
| 3 | import type { RuntimeState } from "../lib/runtimeStateStore"; |
| 4 | import { applyResolvedField, entryToRecord } from "../lib/transcriptRecordProjection"; |
| 5 | import type { HistoryContentRef } from "../lib/types"; |
| 6 | import { initialState, reducer } from "../lib/useController"; |
| 7 | import type { Item, State } from "../lib/useController"; |
| 8 | |
| 9 | type CompactionItem = Extract<Item, { kind: "compaction" }>; |
| 10 | |
| 11 | function operationCard(state: State, operationId: string): CompactionItem | undefined { |
| 12 | return state.items.find((item): item is CompactionItem => item.kind === "compaction" && item.operationId === operationId); |
| 13 | } |
| 14 | |
| 15 | const runtime = (revision: number, maintenance: RuntimeState["maintenance"]): RuntimeState => ({ |
| 16 | schemaVersion: 1, |
| 17 | projectionEpoch: "projection-1", |
| 18 | runtimeEpoch: "runtime-1", |
| 19 | activityRevision: revision, |
| 20 | revision, |
| 21 | phase: maintenance?.activity === "recovery_required" ? "recovery_required" : "executing", |
| 22 | running: Boolean(maintenance), |
| 23 | turnId: "", |
| 24 | turnStatus: "", |
| 25 | turnEventSeq: 0, |
| 26 | pendingPrompt: false, |
| 27 | cancelRequested: maintenance?.activity === "cancelling", |
| 28 | cancellable: Boolean(maintenance), |
| 29 | backgroundJobs: 0, |
| 30 | activity: maintenance ? "maintenance" : "idle", |
| 31 | maintenance, |
| 32 | }); |
| 33 | |
| 34 | let state = reducer(initialState, { type: "event", e: { |
| 35 | kind: "session_operation", |
| 36 | sessionOperation: { operationId: "op-1", kind: "compact", activity: "running", status: "running", inputTokens: 1200, operationRevision: 1, runtimeEpoch: "runtime-1" }, |
| 37 | } }); |
| 38 | let cards = state.items.filter(item => item.kind === "compaction"); |
| 39 | assert.equal(cards.length, 1); |
| 40 | assert.equal(cards[0].pending, true); |
| 41 | assert.equal(cards[0].id, "maintenance:op-1"); |
| 42 | |
| 43 | state = reducer(state, { type: "event", e: { |
| 44 | kind: "session_operation", |
| 45 | sessionOperation: { operationId: "op-1", kind: "compact", activity: "cancelling", status: "cancelling", inputTokens: 1200, operationRevision: 2, runtimeEpoch: "runtime-1" }, |
| 46 | } }); |
| 47 | cards = state.items.filter(item => item.kind === "compaction"); |
| 48 | assert.equal(cards.length, 1, "cancellation updates the stable operation card"); |
| 49 | assert.equal(cards[0].status, "cancelling"); |
| 50 | |
| 51 | state = reducer(state, { type: "event", e: { |
| 52 | kind: "compaction_done", |
| 53 | compaction: { trigger: "manual", messages: 8, summary: "summary", archive: "archive.jsonl" }, |
| 54 | } }); |
| 55 | cards = state.items.filter(item => item.kind === "compaction"); |
| 56 | assert.equal(cards.length, 1, "agent completion metadata does not create a second manual card"); |
| 57 | assert.equal(cards[0].summary, "summary"); |
| 58 | |
| 59 | state = reducer(state, { type: "event", e: { |
| 60 | kind: "session_operation", |
| 61 | sessionOperation: { operationId: "op-1", kind: "compact", activity: "finalizing", status: "completed", applied: true, inputTokens: 1200, resultTokens: 400, messages: 8, summary: "summary", operationRevision: 4, runtimeEpoch: "runtime-1" }, |
| 62 | } }); |
| 63 | cards = state.items.filter(item => item.kind === "compaction"); |
| 64 | assert.equal(cards.length, 1); |
| 65 | assert.equal(cards[0].pending, false); |
| 66 | assert.equal(cards[0].status, "completed"); |
| 67 | assert.equal(cards[0].resultTokens, 400); |
| 68 | |
| 69 | // An event delayed behind terminal persistence cannot regress the operation. |
| 70 | state = reducer(state, { type: "event", e: { |
| 71 | kind: "session_operation", |
| 72 | sessionOperation: { operationId: "op-1", kind: "compact", activity: "cancelling", status: "cancelling", operationRevision: 3, runtimeEpoch: "runtime-1" }, |
| 73 | } }); |
| 74 | cards = state.items.filter(item => item.kind === "compaction"); |
| 75 | assert.equal(cards[0].status, "completed"); |
| 76 | assert.equal(cards[0].pending, false); |
| 77 | assert.equal(cards[0].operationRevision, 4); |
| 78 | |
| 79 | // A runtime refresh is a partial observation: omitted result fields retain the |
| 80 | // durable event values and a terminal card does not become pending again. |
| 81 | state = reducer(state, { type: "runtime_snapshot", snapshot: runtime(5, { |
| 82 | operationId: "op-1", |
| 83 | kind: "compact", |
| 84 | activity: "running", |
| 85 | operationRevision: 3, |
| 86 | runtimeEpoch: "runtime-1", |
| 87 | }) }); |
| 88 | cards = state.items.filter(item => item.kind === "compaction"); |
| 89 | assert.equal(cards[0].status, "completed"); |
| 90 | assert.equal(cards[0].applied, true); |
| 91 | assert.equal(cards[0].inputTokens, 1200); |
| 92 | assert.equal(cards[0].resultTokens, 400); |
| 93 | |
| 94 | let failed = reducer(initialState, { type: "event", e: { |
| 95 | kind: "session_operation", |
| 96 | sessionOperation: { |
| 97 | operationId: "op-failed", kind: "compact", activity: "recovery_required", status: "recovery_required", |
| 98 | errorCode: "save_failed", detail: "disk full", applied: true, inputTokens: 900, resultTokens: 350, |
| 99 | operationRevision: 7, runtimeEpoch: "runtime-1", |
| 100 | }, |
| 101 | } }); |
| 102 | failed = reducer(failed, { type: "runtime_snapshot", snapshot: runtime(8, { |
| 103 | operationId: "op-failed", |
| 104 | kind: "compact", |
| 105 | activity: "recovery_required", |
| 106 | operationRevision: 7, |
| 107 | runtimeEpoch: "runtime-1", |
| 108 | }) }); |
| 109 | const failedCard = failed.items.filter(item => item.kind === "compaction").find(item => item.operationId === "op-failed"); |
| 110 | assert.equal(failedCard?.detail, "disk full"); |
| 111 | assert.equal(failedCard?.errorCode, "save_failed"); |
| 112 | assert.equal(failedCard?.applied, true); |
| 113 | assert.equal(failedCard?.inputTokens, 900); |
| 114 | assert.equal(failedCard?.resultTokens, 350); |
| 115 | |
| 116 | // session-maintenance-v1 stores a provider compaction row whose content is the |
| 117 | // operation JSON. Canonical history must recover the display fields, not show |
| 118 | // the JSON as a successful generic compaction. |
| 119 | const persisted = canonicalMessage({ |
| 120 | messageId: "maintenance:op-history", position: 4, version: 1, role: "compaction", eventSequence: 9, visibleTurn: 2, |
| 121 | }, { |
| 122 | id: "maintenance:op-history", |
| 123 | role: "compaction", |
| 124 | content: JSON.stringify({ |
| 125 | operationId: "op-history", kind: "compact", activity: "recovery_required", status: "recovery_required", |
| 126 | errorCode: "save_failed", detail: "could not save", applied: true, inputTokens: 1000, resultTokens: 450, |
| 127 | operationRevision: 6, runtimeEpoch: "runtime-history", |
| 128 | }), |
| 129 | }); |
| 130 | assert.equal(persisted.role, "compaction"); |
| 131 | assert.equal(persisted.operationId, "op-history"); |
| 132 | assert.equal(persisted.operationStatus, "recovery_required"); |
| 133 | assert.equal(persisted.errorCode, "save_failed"); |
| 134 | assert.equal(persisted.detail, "could not save"); |
| 135 | assert.equal(persisted.applied, true); |
| 136 | assert.equal(persisted.operationRevision, 6); |
| 137 | assert.equal(persisted.runtimeEpoch, "runtime-history"); |
| 138 | |
| 139 | // Large summaries are reference-backed. Their placeholder must be loading (or |
| 140 | // explicitly unavailable), never a false success, and hydration uses the same |
| 141 | // canonical parser as the inline row. |
| 142 | const lazySource = { |
| 143 | id: "maintenance:op-lazy", |
| 144 | role: "compaction", |
| 145 | content: JSON.stringify({ |
| 146 | operationId: "op-lazy", kind: "compact", activity: "finalizing", status: "failed", |
| 147 | errorCode: "summary_failed", detail: "late body", summary: "x".repeat(5000), |
| 148 | operationRevision: 9, runtimeEpoch: "runtime-lazy", |
| 149 | }), |
| 150 | }; |
| 151 | const lazyPersistent = { |
| 152 | messageId: "maintenance:op-lazy", position: 6, version: 1, role: "compaction", eventSequence: 11, visibleTurn: 2, |
| 153 | contentRef: { digest: "lazy", bytes: JSON.stringify(lazySource).length }, |
| 154 | }; |
| 155 | const lazyMessage = canonicalMessage(lazyPersistent, undefined); |
| 156 | assert.equal(lazyMessage.operationStatus, "loading"); |
| 157 | assert.equal(lazyMessage.pending, true); |
| 158 | const lazyRef: HistoryContentRef = { entryId: "m:maintenance:op-lazy", field: "canonicalMessage", size: lazyPersistent.contentRef.bytes, chunks: 1, revision: 1, digest: "lazy" }; |
| 159 | const lazyRecord = entryToRecord({ entryId: lazyRef.entryId, turn: 2, order: 6, message: lazyMessage, refs: [lazyRef] }); |
| 160 | const lazyBytes = [...new TextEncoder().encode(JSON.stringify(lazySource))].map(byte => String.fromCharCode(byte)).join(""); |
| 161 | assert.equal(applyResolvedField(lazyRecord, lazyRef, lazyBytes), true); |
| 162 | assert.equal(lazyRecord.message.operationStatus, "failed"); |
| 163 | assert.equal(lazyRecord.message.errorCode, "summary_failed"); |
| 164 | assert.equal(lazyRecord.message.detail, "late body"); |
| 165 | assert.equal(lazyRecord.message.operationRevision, 9); |
| 166 | |
| 167 | // History and live state use the operation id as one stable node identity. |
| 168 | let reconciled = reducer(initialState, { type: "history", messages: [persisted] }); |
| 169 | reconciled = reducer(reconciled, { type: "runtime_snapshot", snapshot: runtime(1, { |
| 170 | operationId: "op-history", kind: "compact", activity: "recovery_required", |
| 171 | operationRevision: 6, runtimeEpoch: "runtime-history", status: "recovery_required", |
| 172 | }) }); |
| 173 | const reconciledCards = reconciled.items.filter(item => item.kind === "compaction").filter(item => item.operationId === "op-history"); |
| 174 | assert.equal(reconciledCards.length, 1); |
| 175 | assert.equal(reconciledCards[0].id, "maintenance:op-history"); |
| 176 | assert.equal(reconciledCards[0].errorCode, "save_failed"); |
| 177 | |
| 178 | // The inverse arrival order is equally monotonic: an in-flight history read |
| 179 | // cannot replace a newer live terminal record with its older running row. |
| 180 | const staleHistory = canonicalMessage({ |
| 181 | messageId: "maintenance:op-race", position: 5, version: 1, role: "compaction", eventSequence: 10, visibleTurn: 2, |
| 182 | }, { |
| 183 | id: "maintenance:op-race", |
| 184 | role: "compaction", |
| 185 | content: JSON.stringify({ |
| 186 | operationId: "op-race", kind: "compact", activity: "running", status: "running", |
| 187 | inputTokens: 800, operationRevision: 1, runtimeEpoch: "runtime-1", |
| 188 | }), |
| 189 | }); |
| 190 | let liveFirst = reducer(initialState, { type: "event", e: { |
| 191 | kind: "session_operation", |
| 192 | sessionOperation: { |
| 193 | operationId: "op-race", kind: "compact", activity: "finalizing", status: "failed", |
| 194 | errorCode: "summary_failed", detail: "provider failed", inputTokens: 800, resultTokens: 800, |
| 195 | operationRevision: 3, runtimeEpoch: "runtime-1", |
| 196 | }, |
| 197 | } }); |
| 198 | liveFirst = reducer(liveFirst, { type: "history", messages: [staleHistory] }); |
| 199 | const liveFirstCards = liveFirst.items.filter(item => item.kind === "compaction").filter(item => item.operationId === "op-race"); |
| 200 | assert.equal(liveFirstCards.length, 1); |
| 201 | assert.equal(liveFirstCards[0].status, "failed"); |
| 202 | assert.equal(liveFirstCards[0].detail, "provider failed"); |
| 203 | assert.equal(liveFirstCards[0].operationRevision, 3); |
| 204 | |
| 205 | // A persisted progress row stays pending until runtime synchronization can |
| 206 | // prove whether its operation still exists. Both arrival orders converge on |
| 207 | // interrupted when the synchronized runtime is idle. |
| 208 | let historyBeforeRuntime = reducer(initialState, { type: "history", messages: [staleHistory] }); |
| 209 | let historyBeforeRuntimeCard = operationCard(historyBeforeRuntime, "op-race"); |
| 210 | assert.equal(historyBeforeRuntimeCard?.pending, true); |
| 211 | assert.equal(historyBeforeRuntimeCard?.status, "confirming"); |
| 212 | historyBeforeRuntime = reducer(historyBeforeRuntime, { type: "runtime_snapshot", snapshot: runtime(11, undefined) }); |
| 213 | historyBeforeRuntimeCard = operationCard(historyBeforeRuntime, "op-race"); |
| 214 | assert.equal(historyBeforeRuntimeCard?.pending, false); |
| 215 | assert.equal(historyBeforeRuntimeCard?.status, "interrupted"); |
| 216 | |
| 217 | let runtimeBeforeHistory = reducer(initialState, { type: "runtime_snapshot", snapshot: runtime(12, undefined) }); |
| 218 | runtimeBeforeHistory = reducer(runtimeBeforeHistory, { type: "history", messages: [staleHistory] }); |
| 219 | const runtimeBeforeHistoryCards = runtimeBeforeHistory.items.filter((item): item is CompactionItem => item.kind === "compaction" && item.operationId === "op-race"); |
| 220 | assert.equal(runtimeBeforeHistoryCards.length, 1); |
| 221 | assert.equal(runtimeBeforeHistoryCards[0].pending, false); |
| 222 | assert.equal(runtimeBeforeHistoryCards[0].status, "interrupted"); |
| 223 | |
| 224 | // A matching active identity retains its running state after synchronization. |
| 225 | let activeRecovery = reducer(initialState, { type: "history", messages: [staleHistory] }); |
| 226 | activeRecovery = reducer(activeRecovery, { type: "runtime_snapshot", snapshot: runtime(13, { |
| 227 | operationId: "op-race", kind: "compact", activity: "running", status: "running", |
| 228 | operationRevision: 1, runtimeEpoch: "runtime-1", |
| 229 | }) }); |
| 230 | const activeRecoveryCards = activeRecovery.items.filter((item): item is CompactionItem => item.kind === "compaction" && item.operationId === "op-race"); |
| 231 | assert.equal(activeRecoveryCards.length, 1); |
| 232 | assert.equal(activeRecoveryCards[0].pending, true); |
| 233 | assert.equal(activeRecoveryCards[0].status, "running"); |
| 234 | |
| 235 | console.log("maintenance lifecycle: persisted parsing, monotonic merge and reconciliation passed"); |
| 236 | |
| 237 | // A previously cached idle snapshot is not evidence that a newer live |
| 238 | // operation has stopped. History refresh must not manufacture a terminal. |
| 239 | let cachedIdle = reducer(initialState, { type: "runtime_snapshot", snapshot: runtime(20, undefined) }); |
| 240 | const freshOp = { operationId: "op-race", kind: "compact", activity: "running", status: "running", operationRevision: 1, runtimeEpoch: "runtime-1" }; |
| 241 | cachedIdle = reducer(cachedIdle, { type: "event", e: { kind: "session_operation", sessionOperation: freshOp } }); |
| 242 | cachedIdle = reducer(cachedIdle, { type: "history", messages: [staleHistory] }); |
| 243 | assert.equal(operationCard(cachedIdle, "op-race")?.status, "running"); |
| 244 | cachedIdle = reducer(cachedIdle, { type: "runtime_snapshot", snapshot: runtime(21, freshOp) }); |
| 245 | assert.equal(operationCard(cachedIdle, "op-race")?.status, "running"); |
| 246 | |
| 247 | // An inferred interruption is reversible when fresh authoritative evidence |
| 248 | // arrives; a persisted interruption remains a real terminal state. |
| 249 | historyBeforeRuntime = reducer(historyBeforeRuntime, { type: "runtime_snapshot", snapshot: runtime(22, freshOp) }); |
| 250 | assert.equal(operationCard(historyBeforeRuntime, "op-race")?.status, "running"); |
| 251 | |
| 252 | for (const status of ["future_state", ""]) { |
| 253 | const unknown = reducer(initialState, { type: "event", e: { kind: "session_operation", sessionOperation: { ...freshOp, status, activity: "" } } }); |
| 254 | assert.equal(operationCard(unknown, "op-race")?.status, "unavailable"); |
| 255 | assert.equal(operationCard(unknown, "op-race")?.pending, false); |
| 256 | } |
| 257 | |
| 258 | let formalInterruption = reducer(initialState, { type: "event", e: { kind: "session_operation", sessionOperation: { ...freshOp, status: "interrupted", operationRevision: 2 } } }); |
| 259 | formalInterruption = reducer(formalInterruption, { type: "runtime_snapshot", snapshot: runtime(23, { ...freshOp, operationRevision: 3 }) }); |
| 260 | assert.equal(operationCard(formalInterruption, "op-race")?.status, "interrupted", "formal terminal states remain irreversible"); |
| 261 | const unknownSaving = reducer(initialState, { type: "event", e: { kind: "session_operation", sessionOperation: { ...freshOp, status: "future_state", activity: "finalizing" } } }); |
| 262 | assert.equal(operationCard(unknownSaving, "op-race")?.status, "unavailable", "unknown status cannot inherit a familiar activity"); |
| 263 |