@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.
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- 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
|
|
32
|
-
* the
|
|
33
|
-
*
|
|
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
|
+
}
|