@bitkyc08/opencodex 2.55.0 → 2.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -3
  6. package/src/adapters/base.ts +21 -0
  7. package/src/adapters/codebuddy/adapter.ts +2 -1
  8. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  9. package/src/adapters/command-code.ts +1 -1
  10. package/src/adapters/cursor/envelope-echo.ts +8 -2
  11. package/src/adapters/cursor/transport-retry.ts +46 -1
  12. package/src/adapters/cursor.ts +4 -0
  13. package/src/adapters/google.ts +7 -7
  14. package/src/adapters/kiro/adapter.ts +42 -1
  15. package/src/adapters/kiro/payload.ts +17 -3
  16. package/src/adapters/kiro/reasoning.ts +70 -7
  17. package/src/adapters/kiro/stream.ts +8 -2
  18. package/src/adapters/kiro/wire.ts +2 -1
  19. package/src/adapters/kiro-events.ts +21 -13
  20. package/src/adapters/kiro-retry.ts +23 -4
  21. package/src/adapters/openai-chat/errors.ts +116 -0
  22. package/src/adapters/openai-chat/messages.ts +346 -0
  23. package/src/adapters/openai-chat/passthrough.ts +146 -0
  24. package/src/adapters/openai-chat/response-events.ts +117 -0
  25. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  26. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  27. package/src/adapters/openai-chat/tool-schema.ts +495 -0
  28. package/src/adapters/openai-chat/wire.ts +50 -0
  29. package/src/adapters/openai-chat.ts +40 -1452
  30. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  31. package/src/adapters/openai-responses/image-gen.ts +406 -0
  32. package/src/adapters/openai-responses/internal.ts +3 -0
  33. package/src/adapters/openai-responses/passthrough.ts +642 -0
  34. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  35. package/src/adapters/openai-responses/reasoning.ts +220 -0
  36. package/src/adapters/openai-responses/request-strips.ts +185 -0
  37. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  38. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  39. package/src/adapters/openai-responses/web-search.ts +156 -0
  40. package/src/adapters/openai-responses.ts +4 -2625
  41. package/src/bridge/errors.ts +58 -0
  42. package/src/bridge/internal.ts +174 -0
  43. package/src/bridge/response-json.ts +630 -0
  44. package/src/bridge/sse.ts +1462 -0
  45. package/src/bridge.ts +5 -2204
  46. package/src/chat/inbound.ts +12 -1
  47. package/src/claude/desktop-profile.ts +66 -9
  48. package/src/claude/outbound.ts +18 -0
  49. package/src/cli/account-main.ts +1 -1
  50. package/src/cli/capabilities.ts +2 -2
  51. package/src/cli/combo.ts +10 -1
  52. package/src/cli/index.ts +48 -5
  53. package/src/cli/registry.ts +2 -1
  54. package/src/cli/system-command.ts +4 -4
  55. package/src/clients/config-export.ts +7 -3
  56. package/src/codex/account-label.ts +14 -3
  57. package/src/codex/account-lifecycle.ts +3 -0
  58. package/src/codex/account-store.ts +184 -35
  59. package/src/codex/account-usability.ts +21 -0
  60. package/src/codex/auth-api/account-list.ts +507 -0
  61. package/src/codex/auth-api/http.ts +32 -0
  62. package/src/codex/auth-api/login-flow.ts +566 -0
  63. package/src/codex/auth-api/login-state.ts +64 -0
  64. package/src/codex/auth-api/main-account-probe.ts +331 -0
  65. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  66. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  67. package/src/codex/auth-api/reset-credit-service.ts +431 -0
  68. package/src/codex/auth-api/routes.ts +425 -0
  69. package/src/codex/auth-api/runtime-config.ts +48 -0
  70. package/src/codex/auth-api.ts +27 -3118
  71. package/src/codex/auth-context.ts +252 -35
  72. package/src/codex/catalog/aggregation.ts +80 -1
  73. package/src/codex/catalog/auto-review.ts +507 -0
  74. package/src/codex/catalog/build-entries.ts +981 -0
  75. package/src/codex/catalog/combo-member.ts +375 -0
  76. package/src/codex/catalog/derive-entry.ts +229 -0
  77. package/src/codex/catalog/effort.ts +0 -1
  78. package/src/codex/catalog/gated-native-warn.ts +63 -0
  79. package/src/codex/catalog/gather-capture.ts +533 -0
  80. package/src/codex/catalog/model-hints.ts +691 -0
  81. package/src/codex/catalog/model-visibility.ts +305 -0
  82. package/src/codex/catalog/provider-fetch.ts +52 -2942
  83. package/src/codex/catalog/provider-models.ts +685 -0
  84. package/src/codex/catalog/remote.ts +30 -0
  85. package/src/codex/catalog/restore.ts +132 -0
  86. package/src/codex/catalog/retained-sync.ts +714 -0
  87. package/src/codex/catalog/routed-gather.ts +895 -0
  88. package/src/codex/catalog/subagent-roster.ts +176 -0
  89. package/src/codex/catalog/sync.ts +52 -2698
  90. package/src/codex/cli-install-provenance.ts +7 -1
  91. package/src/codex/convergence.ts +7 -2
  92. package/src/codex/desktop-app/types.ts +11 -2
  93. package/src/codex/desktop-app/windows.ts +5 -5
  94. package/src/codex/inject/config-toml.ts +563 -0
  95. package/src/codex/inject/remove.ts +192 -0
  96. package/src/codex/inject/restore.ts +567 -0
  97. package/src/codex/inject/routing-classify.ts +109 -0
  98. package/src/codex/inject/routing-target.ts +125 -0
  99. package/src/codex/inject.ts +89 -1444
  100. package/src/codex/lineage.ts +458 -0
  101. package/src/codex/model-entitlements.ts +152 -15
  102. package/src/codex/pool-refresh-backoff.ts +161 -0
  103. package/src/codex/quota-rejection.ts +104 -15
  104. package/src/codex/routing/active-account.ts +194 -0
  105. package/src/codex/routing/cache-affinity.ts +70 -0
  106. package/src/codex/routing/cooldown-math.ts +285 -0
  107. package/src/codex/routing/health-store.ts +402 -0
  108. package/src/codex/routing/probe-lease.ts +358 -0
  109. package/src/codex/routing/selection.ts +780 -0
  110. package/src/codex/routing/thread-affinity.ts +586 -0
  111. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  112. package/src/codex/routing.ts +370 -2271
  113. package/src/codex/shim-fingerprint.ts +223 -0
  114. package/src/codex/shim-inspect.ts +175 -0
  115. package/src/codex/shim-probe.ts +367 -0
  116. package/src/codex/shim-restore-lock.ts +169 -0
  117. package/src/codex/shim-state-file.ts +151 -0
  118. package/src/codex/shim-templates.ts +265 -0
  119. package/src/codex/shim.ts +48 -1268
  120. package/src/codex/warmup.ts +1 -1
  121. package/src/combos/failover.ts +85 -0
  122. package/src/combos/request.ts +17 -10
  123. package/src/combos/types.ts +23 -2
  124. package/src/config/diagnostics.ts +705 -0
  125. package/src/config/feature-flags.ts +55 -0
  126. package/src/config/live-reconcile.ts +403 -0
  127. package/src/config/load-degrade.ts +880 -0
  128. package/src/config/mutation-lock.ts +244 -0
  129. package/src/config/openai-tier-backup.ts +268 -0
  130. package/src/config/pending-teardown.ts +31 -0
  131. package/src/config/persist-unlocked.ts +92 -0
  132. package/src/config/proxy-env.ts +188 -0
  133. package/src/config/salvage.ts +244 -0
  134. package/src/config/schema/config-schema.ts +640 -0
  135. package/src/config/schema/leaf-validators.ts +855 -0
  136. package/src/config/warn-memo.ts +28 -0
  137. package/src/config.ts +234 -4481
  138. package/src/generated/compatibility-version.json +649 -121
  139. package/src/images/loop.ts +1 -1
  140. package/src/lib/errors.ts +17 -0
  141. package/src/lib/request-execution-budget.ts +198 -23
  142. package/src/lib/spend-reservation-ledger.ts +958 -0
  143. package/src/lib/state-store-registrations.ts +6 -2
  144. package/src/lib/test-home-guard.ts +85 -1
  145. package/src/lib/upstream-retry.ts +132 -21
  146. package/src/lib/windows-elevation.ts +76 -14
  147. package/src/lib/workflow-budget.ts +553 -30
  148. package/src/oauth/index.ts +2 -2
  149. package/src/oauth/key-providers.ts +2 -2
  150. package/src/providers/kiro-models.ts +4 -3
  151. package/src/providers/label.ts +19 -1
  152. package/src/providers/model-discovery.ts +16 -0
  153. package/src/providers/quota/account-cache.ts +441 -0
  154. package/src/providers/quota/antigravity.ts +295 -0
  155. package/src/providers/quota/report-cache.ts +320 -0
  156. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  157. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  158. package/src/providers/quota.ts +324 -3079
  159. package/src/providers/registry/entries-core.ts +1228 -0
  160. package/src/providers/registry/entries-extended.ts +1213 -0
  161. package/src/providers/registry/model-seeds.ts +912 -0
  162. package/src/providers/registry/types.ts +352 -0
  163. package/src/providers/registry.ts +24 -3536
  164. package/src/responses/continuation-ownership.ts +29 -0
  165. package/src/responses/reasoning-envelope.ts +6 -3
  166. package/src/responses/state/replay-fingerprint.ts +80 -0
  167. package/src/responses/state/snapshot-codec.ts +104 -0
  168. package/src/responses/state/spill-failure.ts +118 -0
  169. package/src/responses/state/spill-queue.ts +665 -0
  170. package/src/responses/state/temp-recovery.ts +257 -0
  171. package/src/responses/state.ts +82 -1143
  172. package/src/routing/identity-domains.ts +456 -0
  173. package/src/routing/probe-lease.ts +613 -0
  174. package/src/server/chat-completions.ts +3 -1
  175. package/src/server/chat-native.ts +37 -9
  176. package/src/server/index/bounded-request.ts +88 -0
  177. package/src/server/index/live-sideband.ts +601 -0
  178. package/src/server/index/serve-options.ts +1766 -0
  179. package/src/server/index/startup-warnings.ts +213 -0
  180. package/src/server/index/websocket-handler.ts +339 -0
  181. package/src/server/index.ts +45 -2552
  182. package/src/server/inspection-tee.ts +107 -0
  183. package/src/server/live.ts +46 -1
  184. package/src/server/management/combo-routes.ts +10 -1
  185. package/src/server/management/route-registry.ts +26 -23
  186. package/src/server/management/shared.ts +8 -5
  187. package/src/server/management/workflow-budget-routes.ts +133 -0
  188. package/src/server/management-api.ts +12 -0
  189. package/src/server/relay-eager.ts +2 -0
  190. package/src/server/relay.ts +14 -19
  191. package/src/server/request-log-conversation.ts +9 -7
  192. package/src/server/request-log.ts +372 -4
  193. package/src/server/response-log-body.ts +153 -0
  194. package/src/server/responses/account-change-state.ts +307 -0
  195. package/src/server/responses/adapter-continuation.ts +540 -0
  196. package/src/server/responses/adapter-delivery.ts +208 -0
  197. package/src/server/responses/adapter-dispatch.ts +1042 -0
  198. package/src/server/responses/codex-ws-wire.ts +5 -0
  199. package/src/server/responses/collaboration.ts +74 -4
  200. package/src/server/responses/combo-session-recall.ts +68 -8
  201. package/src/server/responses/compact.ts +113 -17
  202. package/src/server/responses/completion-policy.ts +33 -0
  203. package/src/server/responses/core-auth.ts +529 -0
  204. package/src/server/responses/core-codex-account.ts +907 -0
  205. package/src/server/responses/core-combo-failure.ts +210 -0
  206. package/src/server/responses/core-combo.ts +787 -0
  207. package/src/server/responses/core-errors.ts +170 -0
  208. package/src/server/responses/core-lifetime.ts +95 -0
  209. package/src/server/responses/core-normalize.ts +350 -0
  210. package/src/server/responses/core-opaque-recovery.ts +380 -0
  211. package/src/server/responses/core-options.ts +159 -0
  212. package/src/server/responses/core-replay.ts +298 -0
  213. package/src/server/responses/core.ts +192 -8893
  214. package/src/server/responses/encrypted-payload.ts +0 -1
  215. package/src/server/responses/input-admission.ts +126 -6
  216. package/src/server/responses/passthrough-delivery.ts +869 -0
  217. package/src/server/responses/passthrough-dispatch.ts +1494 -0
  218. package/src/server/responses/passthrough-error.ts +38 -2
  219. package/src/server/responses/passthrough-execution.ts +54 -0
  220. package/src/server/responses/request-prepare.ts +1080 -0
  221. package/src/server/responses/request-send-budget.ts +259 -0
  222. package/src/server/responses/request-sidecar-auth.ts +149 -0
  223. package/src/server/responses/request-spend.ts +147 -0
  224. package/src/server/responses/request-transport.ts +803 -0
  225. package/src/server/responses/response-effects.ts +157 -0
  226. package/src/server/responses/run-turn-execution.ts +476 -0
  227. package/src/server/responses/sidecar-execution.ts +463 -0
  228. package/src/server/responses/terminal-guard.ts +65 -4
  229. package/src/server/responses-image-gen-repair.ts +1 -1
  230. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  231. package/src/server/workflow-refusal.ts +84 -0
  232. package/src/service/windows-ops.ts +210 -16
  233. package/src/service/windows-scheduler.ts +28 -21
  234. package/src/service.ts +1 -1
  235. package/src/types/config.ts +34 -1
  236. package/src/types/request.ts +8 -5
  237. package/src/types/tools.ts +24 -0
  238. package/src/types.ts +2 -0
  239. package/src/update/index.ts +10 -0
  240. package/src/update/stop-contract.d.mts +1 -0
  241. package/src/update/stop-contract.mjs +19 -0
  242. package/src/update/stop-decision.d.mts +1 -1
  243. package/src/update/stop-decision.mjs +12 -3
  244. package/src/usage/log.ts +147 -1
  245. package/src/usage/summary.ts +171 -21
  246. package/src/vision/anthropic-describe.ts +1 -1
  247. package/src/vision/describe.ts +5 -5
  248. package/src/web-search/anthropic-executor.ts +1 -1
  249. package/src/web-search/exa-executor.ts +1 -1
  250. package/src/web-search/executor.ts +1 -1
  251. package/src/web-search/gemini-executor.ts +1 -1
  252. package/src/web-search/loop.ts +1 -1
  253. package/src/web-search/ollama-executor.ts +1 -1
  254. package/src/web-search/parse.ts +67 -14
  255. package/src/web-search/passthrough-bridge.ts +64 -31
  256. package/src/web-search/xai-executor.ts +1 -1
@@ -0,0 +1,29 @@
1
+ import { effectiveAdapterContract, getAdapterDefinition, type AdapterWire } from "../adapters/registry";
2
+
3
+ /**
4
+ * Wires whose upstream holds the conversation itself, so a turn may reference history this
5
+ * process no longer has.
6
+ *
7
+ * The set is empty, and that is the finding rather than an oversight. The three wires that look
8
+ * like they belong here do not:
9
+ *
10
+ * - devin sends `mapOcxMessagesToDevin(parsed)` — the whole conversation — on every turn
11
+ * (`src/adapters/devin.ts`). Its session/thread id buys prompt caching, not remembered context.
12
+ * - cursor continues from `_providerContinuation.cursor.checkpointRef`, which is read out of the
13
+ * very store that just expired; without it `resolveCursorCheckpoint` returns a reason and the
14
+ * request falls back to `continuationMode: "full-replay"` over `parsed.context.messages`
15
+ * (`src/adapters/cursor/request-builder.ts`).
16
+ * - kiro builds `conversationState.history` from the parsed turns it was given
17
+ * (`src/adapters/kiro/payload.ts`); a conversation id alone reconstructs nothing.
18
+ *
19
+ * So for every translated wire a replay miss means the delta travels alone. Only the native
20
+ * Responses passthrough, which forwards `previous_response_id` untouched to a backend that stored
21
+ * the chain, can answer a turn whose history this process lost.
22
+ */
23
+ export const PROVIDER_OWNED_CONTINUATION_WIRES: ReadonlySet<AdapterWire> = new Set<AdapterWire>();
24
+
25
+ /** The wire an adapter id resolves to through contract inheritance, or undefined if unknown. */
26
+ export function resolvedAdapterWire(adapterId: unknown): AdapterWire | undefined {
27
+ if (typeof adapterId !== "string" || !getAdapterDefinition(adapterId)) return undefined;
28
+ return effectiveAdapterContract(adapterId).wire;
29
+ }
@@ -28,9 +28,12 @@ export interface ReasoningEnvelope {
28
28
  */
29
29
  txt?: string;
30
30
  /**
31
- * Kiro `reasoningContentEvent.redactedContent`: a KMS-encrypted reasoning blob that is opaque to
32
- * the proxy. Kiro's own CLI replays it on the matching `assistantResponseMessage` to preserve
33
- * model reasoning across turns, so it round-trips here the same way a signature does.
31
+ * Kiro's reasoning blob from `reasoningContentEvent`: a KMS-encrypted value that is opaque to the
32
+ * proxy (the GPT-5.6 family sends it as `signature`, other models as the base64
33
+ * `redactedContent`, and the value carries a tag naming which one — see
34
+ * src/adapters/kiro/reasoning.ts). Kiro's own CLI replays it on the matching
35
+ * `assistantResponseMessage` to preserve model reasoning across turns, so it round-trips here the
36
+ * same way a signature does.
34
37
  */
35
38
  krc?: string;
36
39
  }
@@ -0,0 +1,80 @@
1
+ /** Hard cap for canonicalizing ANY item. Past it, the item is not comparable. */
2
+ const REPLAY_FINGERPRINT_MAX_BYTES = 8 * 1024;
3
+ /** Depth ceiling so a pathologically nested item cannot blow the canonicalizer. */
4
+ const REPLAY_FINGERPRINT_MAX_DEPTH = 64;
5
+
6
+ /**
7
+ * Canonical, order-stable fingerprint for one input item, or null when the item cannot be
8
+ * compared safely.
9
+ *
10
+ * Byte-counted DURING the walk rather than serialize-then-measure: a tool result can be
11
+ * megabytes and this runs on the request path, so the point of the cap is to stop early,
12
+ * not to discover afterwards that we should have. Object keys are sorted so two
13
+ * semantically identical items cannot differ by key order alone.
14
+ *
15
+ * The cap applies to EVERY item. An `id`/`call_id` is additional occurrence evidence, never
16
+ * a substitute for content equality, so an over-cap identified tool item is non-comparable
17
+ * exactly like an over-cap message.
18
+ */
19
+ function replayItemFingerprint(item: unknown): string | null {
20
+ const out: string[] = [];
21
+ let bytes = 0;
22
+ const push = (text: string): boolean => {
23
+ bytes += Buffer.byteLength(text, "utf8");
24
+ if (bytes > REPLAY_FINGERPRINT_MAX_BYTES) return false;
25
+ out.push(text);
26
+ return true;
27
+ };
28
+ const walk = (value: unknown, depth: number): boolean => {
29
+ if (depth > REPLAY_FINGERPRINT_MAX_DEPTH) return false;
30
+ if (value === null || typeof value !== "object") return push(JSON.stringify(value) ?? "null");
31
+ if (Array.isArray(value)) {
32
+ if (!push("[")) return false;
33
+ for (const element of value) {
34
+ if (!walk(element, depth + 1)) return false;
35
+ if (!push(",")) return false;
36
+ }
37
+ return push("]");
38
+ }
39
+ if (!push("{")) return false;
40
+ for (const key of Object.keys(value as Record<string, unknown>).sort()) {
41
+ if (!push(JSON.stringify(key))) return false;
42
+ if (!walk((value as Record<string, unknown>)[key], depth + 1)) return false;
43
+ if (!push(",")) return false;
44
+ }
45
+ return push("}");
46
+ };
47
+ return walk(item, 0) ? out.join("") : null;
48
+ }
49
+
50
+ /** Non-empty provider-issued `id`/`call_id` on an item, else null. */
51
+ export function providerIssuedIdentity(item: unknown): string | null {
52
+ if (!item || typeof item !== "object" || Array.isArray(item)) return null;
53
+ const record = item as { id?: unknown; call_id?: unknown };
54
+ for (const candidate of [record.id, record.call_id]) {
55
+ if (typeof candidate === "string" && candidate.trim().length > 0) return candidate;
56
+ }
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * Number of leading stored items the client already carries verbatim, or 0.
62
+ *
63
+ * Requires an exact ordered run: every stored item must match the client input item at the
64
+ * same index. Any not-comparable item aborts to 0 — skipping just that item could align two
65
+ * different occurrences and manufacture a false positive, and a false positive here deletes
66
+ * real conversation history.
67
+ *
68
+ * Known gap (FU-2): stored input can contain proxy-injected guidance the client never saw,
69
+ * and ids repaired after recording. Those sessions do not match here and expand as before.
70
+ */
71
+ export function clientCarriedPrefixLength(stored: readonly unknown[], clientInput: readonly unknown[]): number {
72
+ if (stored.length === 0 || clientInput.length < stored.length) return 0;
73
+ for (let index = 0; index < stored.length; index += 1) {
74
+ const storedPrint = replayItemFingerprint(stored[index]);
75
+ if (storedPrint === null) return 0;
76
+ const clientPrint = replayItemFingerprint(clientInput[index]);
77
+ if (clientPrint === null || storedPrint !== clientPrint) return 0;
78
+ }
79
+ return stored.length;
80
+ }
@@ -0,0 +1,104 @@
1
+ import type {
2
+ ResidentInput,
3
+ ResidentResponseState,
4
+ SpillFailedResponseState,
5
+ SpilledResponseState,
6
+ StoredResponseState,
7
+ } from "../state";
8
+ import type { ResponseSpillRef } from "../spill-store";
9
+ import type { OcxProviderContinuationState } from "../../types";
10
+
11
+ export interface SnapshotLoadStore {
12
+ replaceMapEntry(id: string, next: StoredResponseState, expected?: StoredResponseState): boolean;
13
+ stubSize(id: string, entry: Omit<SpilledResponseState, "sizeBytes">): number;
14
+ tombstone(id: string, createdAt: number): SpillFailedResponseState;
15
+ measureResidentEntry(id: string, entry: ResidentInput): ResidentResponseState | null;
16
+ admitOversizedCandidate(id: string, candidate: ResidentResponseState, expected?: StoredResponseState): void;
17
+ byteCap(): number;
18
+ }
19
+
20
+ interface LegacySnapshotState {
21
+ createdAt?: unknown;
22
+ clientThreadId?: unknown;
23
+ items?: unknown;
24
+ providers?: OcxProviderContinuationState;
25
+ conversationId?: unknown;
26
+ cursorCheckpointUsable?: unknown;
27
+ }
28
+
29
+ function isSpillRef(value: unknown): value is ResponseSpillRef {
30
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
31
+ const ref = value as ResponseSpillRef;
32
+ return ref.version === 1
33
+ && typeof ref.fileName === "string"
34
+ && /^[0-9a-f]{64}$/.test(ref.digest)
35
+ && Number.isSafeInteger(ref.payloadBytes)
36
+ && ref.payloadBytes >= 0;
37
+ }
38
+
39
+ export function loadSnapshotEntry(id: string, value: unknown, store: SnapshotLoadStore): void {
40
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
41
+ const rec = value as LegacySnapshotState & { kind?: unknown; spill?: unknown };
42
+ if (typeof rec.createdAt !== "number" || !Number.isFinite(rec.createdAt)) return;
43
+ const clientThreadId = typeof rec.clientThreadId === "string" && rec.clientThreadId.trim().length > 0
44
+ ? rec.clientThreadId.trim()
45
+ : undefined;
46
+ // A malformed boundary degrades to "never skip" rather than to a bad index: an untrusted
47
+ // snapshot must not be able to authorize dropping conversation history.
48
+ const anchorFor = (itemCount: number): number | undefined => {
49
+ const raw = (rec as { providerOutputStart?: unknown }).providerOutputStart;
50
+ return Number.isSafeInteger(raw) && (raw as number) >= 0 && (raw as number) <= itemCount
51
+ ? raw as number
52
+ : undefined;
53
+ };
54
+ if (rec.kind === "spill") {
55
+ if (!isSpillRef(rec.spill)) return;
56
+ const base: Omit<SpilledResponseState, "sizeBytes"> = {
57
+ kind: "spill",
58
+ createdAt: rec.createdAt,
59
+ ...(clientThreadId ? { clientThreadId } : {}),
60
+ // Item count is unknown until materialization, so accept any non-negative integer
61
+ // here; the spill payload validator re-checks it against the real array.
62
+ ...(anchorFor(Number.MAX_SAFE_INTEGER) !== undefined ? { providerOutputStart: anchorFor(Number.MAX_SAFE_INTEGER) } : {}),
63
+ ...(rec.providers ? { providers: rec.providers } : {}),
64
+ spill: rec.spill,
65
+ };
66
+ store.replaceMapEntry(id, { ...base, sizeBytes: store.stubSize(id, base) });
67
+ return;
68
+ }
69
+ if (rec.kind === "spill-failed") {
70
+ store.replaceMapEntry(id, store.tombstone(id, rec.createdAt));
71
+ return;
72
+ }
73
+ if (rec.kind !== undefined && rec.kind !== "resident") return;
74
+ if (!Array.isArray(rec.items)) return;
75
+ const providers = rec.providers ?? (typeof rec.conversationId === "string"
76
+ ? {
77
+ cursor: {
78
+ conversationId: rec.conversationId,
79
+ ...(typeof rec.cursorCheckpointUsable === "boolean"
80
+ ? { checkpointUsable: rec.cursorCheckpointUsable }
81
+ : {}),
82
+ },
83
+ }
84
+ : undefined);
85
+ const resident = store.measureResidentEntry(id, {
86
+ createdAt: rec.createdAt,
87
+ ...(clientThreadId ? { clientThreadId } : {}),
88
+ items: rec.items,
89
+ ...(anchorFor(rec.items.length) !== undefined ? { providerOutputStart: anchorFor(rec.items.length) } : {}),
90
+ ...(providers ? { providers } : {}),
91
+ });
92
+ if (!resident) {
93
+ store.replaceMapEntry(id, store.tombstone(id, rec.createdAt));
94
+ return;
95
+ }
96
+ // Same admission boundary as live writes: an oversized snapshot row goes
97
+ // straight to spill (or tombstone above the payload ceiling) instead of
98
+ // entering the resident map and demoting unrelated rows on the first prune.
99
+ if (resident.sizeBytes > store.byteCap()) {
100
+ store.admitOversizedCandidate(id, resident, undefined);
101
+ return;
102
+ }
103
+ store.replaceMapEntry(id, resident);
104
+ }
@@ -0,0 +1,118 @@
1
+ export const spillCounters = {
2
+ writes: 0, writeFailures: 0, readFailures: 0,
3
+ aclRetryReturnedTimeouts: 0, aclTimeoutMemoRefusals: 0,
4
+ };
5
+
6
+ export type ResponseSpillWriteFailureCode =
7
+ | "EACLRETRYEXHAUSTED"
8
+ | "ETIMEDOUT"
9
+ | "EACCES"
10
+ | "ENOSPC"
11
+ | "EFBIG"
12
+ | "EIO"
13
+ | "ECAPACITY"
14
+ | "ELOOP"
15
+ | "EUNKNOWN";
16
+
17
+ export type ResponseSpillWriteStatus = "initial" | "healthy" | "degraded";
18
+
19
+ export type ResponseSpillWriteFailureOrigin =
20
+ | "retry_returned_timeout"
21
+ | "timeout_memo_refusal";
22
+
23
+ interface ResponseSpillWriteHealth {
24
+ consecutiveFailures: number;
25
+ lastFailureCode: ResponseSpillWriteFailureCode | null;
26
+ lastFailureOrigin: ResponseSpillWriteFailureOrigin | null;
27
+ lastFailureAt: number | null;
28
+ lastSuccessAt: number | null;
29
+ }
30
+
31
+ export const spillWriteHealth: ResponseSpillWriteHealth = {
32
+ consecutiveFailures: 0,
33
+ lastFailureCode: null,
34
+ lastFailureOrigin: null,
35
+ lastFailureAt: null,
36
+ lastSuccessAt: null,
37
+ };
38
+
39
+ /**
40
+ * Collapse filesystem/runtime errors into a fixed privacy-safe diagnostic union.
41
+ * Messages and paths are deliberately ignored: this projection is returned by the
42
+ * authenticated memory endpoint, and a nested `cause` can contain a username or
43
+ * workspace path even when the public wrapper does not.
44
+ */
45
+ function classifySpillWriteFailure(error: unknown): ResponseSpillWriteFailureCode {
46
+ let cursor = error;
47
+ for (let depth = 0; depth < 4 && cursor && typeof cursor === "object"; depth += 1) {
48
+ const record = cursor as { code?: unknown; cause?: unknown };
49
+ const code = typeof record.code === "string" ? record.code.toUpperCase() : "";
50
+ switch (code) {
51
+ case "EACLRETRYEXHAUSTED": return "EACLRETRYEXHAUSTED";
52
+ case "ETIMEDOUT": return "ETIMEDOUT";
53
+ case "EACCES":
54
+ case "EPERM": return "EACCES";
55
+ case "ENOSPC":
56
+ case "EDQUOT": return "ENOSPC";
57
+ case "EFBIG": return "EFBIG";
58
+ case "EIO": return "EIO";
59
+ case "ECAPACITY": return "ECAPACITY";
60
+ case "ELOOP": return "ELOOP";
61
+ }
62
+ cursor = record.cause;
63
+ }
64
+ return "EUNKNOWN";
65
+ }
66
+
67
+ /** The spill writer preserves ACL errors in cause; only a fixed memo marker is diagnostic. */
68
+ export function spillAclMemoRefusalOrigin(error: unknown): "timeout_memo_refusal" | null {
69
+ let cursor = error;
70
+ for (let depth = 0; depth < 4 && cursor && typeof cursor === "object"; depth += 1) {
71
+ const record = cursor as { code?: unknown; aclFailureOrigin?: unknown; cause?: unknown };
72
+ if ((record.code === "ETIMEDOUT" || record.code === "EACLRETRYEXHAUSTED")
73
+ && record.aclFailureOrigin === "timeout_memo_refusal") {
74
+ return "timeout_memo_refusal";
75
+ }
76
+ cursor = record.cause;
77
+ }
78
+ return null;
79
+ }
80
+
81
+ export function noteSpillWriteSuccess(): void {
82
+ spillCounters.writes += 1;
83
+ spillWriteHealth.consecutiveFailures = 0;
84
+ spillWriteHealth.lastSuccessAt = Date.now();
85
+ }
86
+
87
+ export function noteSpillWriteFailure(
88
+ error: unknown,
89
+ override?: ResponseSpillWriteFailureCode,
90
+ retryOrigin: ResponseSpillWriteFailureOrigin | null = null,
91
+ ): void {
92
+ const code = override ?? classifySpillWriteFailure(error);
93
+ const origin = code === "ETIMEDOUT" || code === "EACLRETRYEXHAUSTED"
94
+ ? spillAclMemoRefusalOrigin(error) ?? retryOrigin
95
+ : null;
96
+ spillCounters.writeFailures += 1;
97
+ spillWriteHealth.consecutiveFailures += 1;
98
+ spillWriteHealth.lastFailureCode = code;
99
+ spillWriteHealth.lastFailureOrigin = origin;
100
+ spillWriteHealth.lastFailureAt = Date.now();
101
+ // Count terminal publications, not ACL calls or a transient first attempt.
102
+ if (origin === "retry_returned_timeout") spillCounters.aclRetryReturnedTimeouts += 1;
103
+ else if (origin === "timeout_memo_refusal") spillCounters.aclTimeoutMemoRefusals += 1;
104
+ }
105
+ /**
106
+ * Admission-boundary observability (test-visible). directSpills: oversized
107
+ * candidates routed straight to durable spill without a resident stay or
108
+ * unrelated demotion. oversizedDrops: candidates above the single-spill
109
+ * payload ceiling, tombstoned instead of retained. snapshotOversizedRefusals:
110
+ * snapshot files refused before parse.
111
+ */
112
+ export const admissionCounters = { directSpills: 0, oversizedDrops: 0, snapshotOversizedRefusals: 0 };
113
+
114
+
115
+ /** Test-only: admission-boundary counters (proves the new paths fire). */
116
+ export function responseAdmissionCountersForTests(): Readonly<typeof admissionCounters> {
117
+ return admissionCounters;
118
+ }