@bitkyc08/opencodex 2.55.0-preview.20260914 → 2.56.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 (167) hide show
  1. package/gui/dist/assets/{index-DH2PUHqr.js → index-D4zuyIxQ.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -1
  4. package/src/adapters/base.ts +21 -0
  5. package/src/adapters/cursor/transport-retry.ts +46 -1
  6. package/src/adapters/cursor.ts +4 -0
  7. package/src/adapters/kiro/adapter.ts +42 -1
  8. package/src/adapters/kiro-retry.ts +23 -4
  9. package/src/adapters/openai-chat/errors.ts +116 -0
  10. package/src/adapters/openai-chat/messages.ts +346 -0
  11. package/src/adapters/openai-chat/passthrough.ts +146 -0
  12. package/src/adapters/openai-chat/response-events.ts +117 -0
  13. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  14. package/src/adapters/openai-chat/tool-schema.ts +477 -0
  15. package/src/adapters/openai-chat/wire.ts +50 -0
  16. package/src/adapters/openai-chat.ts +33 -1445
  17. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  18. package/src/adapters/openai-responses/image-gen.ts +406 -0
  19. package/src/adapters/openai-responses/internal.ts +3 -0
  20. package/src/adapters/openai-responses/passthrough.ts +611 -0
  21. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  22. package/src/adapters/openai-responses/reasoning.ts +220 -0
  23. package/src/adapters/openai-responses/request-strips.ts +185 -0
  24. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  25. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  26. package/src/adapters/openai-responses/web-search.ts +156 -0
  27. package/src/adapters/openai-responses.ts +4 -2625
  28. package/src/bridge/errors.ts +34 -0
  29. package/src/bridge/internal.ts +174 -0
  30. package/src/bridge/response-json.ts +624 -0
  31. package/src/bridge/sse.ts +1444 -0
  32. package/src/bridge.ts +5 -2204
  33. package/src/chat/inbound.ts +12 -1
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-store.ts +71 -9
  36. package/src/codex/auth-api/account-list.ts +507 -0
  37. package/src/codex/auth-api/http.ts +32 -0
  38. package/src/codex/auth-api/login-flow.ts +554 -0
  39. package/src/codex/auth-api/login-state.ts +64 -0
  40. package/src/codex/auth-api/main-account-probe.ts +331 -0
  41. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  42. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  43. package/src/codex/auth-api/reset-credit-service.ts +422 -0
  44. package/src/codex/auth-api/routes.ts +425 -0
  45. package/src/codex/auth-api/runtime-config.ts +48 -0
  46. package/src/codex/auth-api.ts +27 -3118
  47. package/src/codex/auth-context.ts +95 -28
  48. package/src/codex/catalog/auto-review.ts +507 -0
  49. package/src/codex/catalog/build-entries.ts +981 -0
  50. package/src/codex/catalog/combo-member.ts +375 -0
  51. package/src/codex/catalog/derive-entry.ts +229 -0
  52. package/src/codex/catalog/effort.ts +0 -1
  53. package/src/codex/catalog/gated-native-warn.ts +63 -0
  54. package/src/codex/catalog/gather-capture.ts +533 -0
  55. package/src/codex/catalog/model-hints.ts +691 -0
  56. package/src/codex/catalog/model-visibility.ts +304 -0
  57. package/src/codex/catalog/provider-fetch.ts +52 -2942
  58. package/src/codex/catalog/provider-models.ts +685 -0
  59. package/src/codex/catalog/restore.ts +132 -0
  60. package/src/codex/catalog/retained-sync.ts +706 -0
  61. package/src/codex/catalog/routed-gather.ts +858 -0
  62. package/src/codex/catalog/subagent-roster.ts +176 -0
  63. package/src/codex/catalog/sync.ts +52 -2698
  64. package/src/codex/inject/config-toml.ts +563 -0
  65. package/src/codex/inject/remove.ts +192 -0
  66. package/src/codex/inject/restore.ts +540 -0
  67. package/src/codex/inject/routing-classify.ts +109 -0
  68. package/src/codex/inject/routing-target.ts +125 -0
  69. package/src/codex/inject.ts +81 -1436
  70. package/src/codex/lineage.ts +458 -0
  71. package/src/codex/pool-refresh-backoff.ts +152 -0
  72. package/src/codex/routing/active-account.ts +194 -0
  73. package/src/codex/routing/cooldown-math.ts +275 -0
  74. package/src/codex/routing/health-store.ts +402 -0
  75. package/src/codex/routing/probe-lease.ts +358 -0
  76. package/src/codex/routing/selection.ts +703 -0
  77. package/src/codex/routing/thread-affinity.ts +538 -0
  78. package/src/codex/routing.ts +353 -2234
  79. package/src/codex/shim-fingerprint.ts +223 -0
  80. package/src/codex/shim-inspect.ts +175 -0
  81. package/src/codex/shim-probe.ts +367 -0
  82. package/src/codex/shim-restore-lock.ts +169 -0
  83. package/src/codex/shim-state-file.ts +151 -0
  84. package/src/codex/shim-templates.ts +265 -0
  85. package/src/codex/shim.ts +48 -1268
  86. package/src/config/diagnostics.ts +705 -0
  87. package/src/config/feature-flags.ts +55 -0
  88. package/src/config/live-reconcile.ts +403 -0
  89. package/src/config/load-degrade.ts +880 -0
  90. package/src/config/mutation-lock.ts +244 -0
  91. package/src/config/openai-tier-backup.ts +268 -0
  92. package/src/config/persist-unlocked.ts +92 -0
  93. package/src/config/proxy-env.ts +188 -0
  94. package/src/config/salvage.ts +244 -0
  95. package/src/config/schema/config-schema.ts +640 -0
  96. package/src/config/schema/leaf-validators.ts +855 -0
  97. package/src/config/warn-memo.ts +28 -0
  98. package/src/config.ts +234 -4481
  99. package/src/generated/compatibility-version.json +539 -39
  100. package/src/lib/request-execution-budget.ts +69 -20
  101. package/src/lib/spend-reservation-ledger.ts +940 -0
  102. package/src/lib/upstream-retry.ts +55 -11
  103. package/src/lib/workflow-budget.ts +553 -30
  104. package/src/providers/quota/account-cache.ts +441 -0
  105. package/src/providers/quota/antigravity.ts +295 -0
  106. package/src/providers/quota/report-cache.ts +320 -0
  107. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  108. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  109. package/src/providers/quota.ts +324 -3079
  110. package/src/providers/registry/entries-core.ts +1221 -0
  111. package/src/providers/registry/entries-extended.ts +1204 -0
  112. package/src/providers/registry/model-seeds.ts +908 -0
  113. package/src/providers/registry/types.ts +352 -0
  114. package/src/providers/registry.ts +24 -3536
  115. package/src/responses/continuation-ownership.ts +29 -0
  116. package/src/responses/state/replay-fingerprint.ts +80 -0
  117. package/src/responses/state/snapshot-codec.ts +104 -0
  118. package/src/responses/state/spill-failure.ts +118 -0
  119. package/src/responses/state/spill-queue.ts +665 -0
  120. package/src/responses/state/temp-recovery.ts +257 -0
  121. package/src/responses/state.ts +82 -1143
  122. package/src/routing/identity-domains.ts +449 -0
  123. package/src/routing/probe-lease.ts +511 -0
  124. package/src/server/index/bounded-request.ts +88 -0
  125. package/src/server/index/live-sideband.ts +565 -0
  126. package/src/server/index/serve-options.ts +1766 -0
  127. package/src/server/index/startup-warnings.ts +213 -0
  128. package/src/server/index/websocket-handler.ts +335 -0
  129. package/src/server/index.ts +40 -2547
  130. package/src/server/management/route-registry.ts +26 -23
  131. package/src/server/management/shared.ts +8 -5
  132. package/src/server/management/workflow-budget-routes.ts +133 -0
  133. package/src/server/management-api.ts +12 -0
  134. package/src/server/request-log-conversation.ts +9 -7
  135. package/src/server/request-log.ts +245 -1
  136. package/src/server/responses/account-change-state.ts +233 -0
  137. package/src/server/responses/adapter-continuation.ts +514 -0
  138. package/src/server/responses/adapter-delivery.ts +214 -0
  139. package/src/server/responses/adapter-dispatch.ts +971 -0
  140. package/src/server/responses/compact.ts +59 -4
  141. package/src/server/responses/completion-policy.ts +33 -0
  142. package/src/server/responses/core-auth.ts +527 -0
  143. package/src/server/responses/core-codex-account.ts +859 -0
  144. package/src/server/responses/core-combo-failure.ts +210 -0
  145. package/src/server/responses/core-combo.ts +707 -0
  146. package/src/server/responses/core-errors.ts +152 -0
  147. package/src/server/responses/core-lifetime.ts +95 -0
  148. package/src/server/responses/core-normalize.ts +350 -0
  149. package/src/server/responses/core-opaque-recovery.ts +380 -0
  150. package/src/server/responses/core-options.ts +159 -0
  151. package/src/server/responses/core-replay.ts +225 -0
  152. package/src/server/responses/core.ts +192 -8893
  153. package/src/server/responses/passthrough-delivery.ts +856 -0
  154. package/src/server/responses/passthrough-dispatch.ts +1476 -0
  155. package/src/server/responses/passthrough-execution.ts +54 -0
  156. package/src/server/responses/request-prepare.ts +970 -0
  157. package/src/server/responses/request-send-budget.ts +164 -0
  158. package/src/server/responses/request-sidecar-auth.ts +149 -0
  159. package/src/server/responses/request-transport.ts +744 -0
  160. package/src/server/responses/response-effects.ts +157 -0
  161. package/src/server/responses/run-turn-execution.ts +448 -0
  162. package/src/server/responses/sidecar-execution.ts +469 -0
  163. package/src/server/responses-image-gen-repair.ts +1 -1
  164. package/src/server/workflow-refusal.ts +84 -0
  165. package/src/types/config.ts +30 -0
  166. package/src/usage/log.ts +146 -0
  167. package/src/usage/summary.ts +171 -21
@@ -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
+ }
@@ -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
+ }