@bitkyc08/opencodex 2.81.0 → 2.82.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 +166 -73
- package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-BqHF5Trd.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-D3FBCNiP.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DTBC8n7-.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -4
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +19 -17
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +25 -2
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +20 -1
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +43 -35
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +303 -171
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +50 -4
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/writer.ts +9 -3
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/request-execution-budget.ts +8 -0
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +6 -2
- package/src/oauth/store.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +1 -0
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/serve-options.ts +10 -2
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +1 -0
- package/src/server/management/agent-settings-routes.ts +93 -32
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log.ts +31 -2
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +34 -13
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +19 -2
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +25 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +5 -0
- package/src/types/wire.ts +1 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
package/src/responses/state.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { recoverStaleResponseStateTemps } from "./state/temp-recovery";
|
|
|
31
31
|
export type { ResponseSpillWriteFailureCode, ResponseSpillWriteStatus, ResponseSpillWriteFailureOrigin } from "./state/spill-failure";
|
|
32
32
|
import type { ResponseSpillWriteFailureCode, ResponseSpillWriteStatus, ResponseSpillWriteFailureOrigin } from "./state/spill-failure";
|
|
33
33
|
export { responseAdmissionCountersForTests } from "./state/spill-failure";
|
|
34
|
-
import { admissionCounters, noteSpillWriteFailure, noteSpillWriteSuccess, spillCounters, spillWriteHealth } from "./state/spill-failure";
|
|
34
|
+
import { admissionCounters, noteSpillWriteFailure, noteSpillWriteSuccess, resetSpillCountersForTests, spillCounters, spillWriteHealth } from "./state/spill-failure";
|
|
35
35
|
import { loadSnapshotEntry } from "./state/snapshot-codec";
|
|
36
36
|
import { isBodyNonPersistable } from "./state/body-policy";
|
|
37
37
|
export { isBodyNonPersistable, markBodyNonPersistable } from "./state/body-policy";
|
|
@@ -876,20 +876,29 @@ export function replayOverlapSkipsForTests(): number {
|
|
|
876
876
|
* This ceiling therefore bounds what the store owns, which is every file it can
|
|
877
877
|
* account for, and not the directory as a whole.
|
|
878
878
|
*/
|
|
879
|
-
function enforceSpilledResponseBudget(): number {
|
|
879
|
+
function enforceSpilledResponseBudget(requiredHeadroomBytes?: number): number {
|
|
880
880
|
// Price in-flight publications too: a file being created by
|
|
881
881
|
// `writeResponseSpillDurablyAsync` occupies the volume before it reaches `states`.
|
|
882
882
|
let spilledBytes = accountedResponseSpillBytes();
|
|
883
|
-
|
|
883
|
+
// Admission (#6747) passes the room its publication needs, 0 included. Evict only while that
|
|
884
|
+
// room is still reachable: re-read accounting after each attempt, since a failed unlink stays
|
|
885
|
+
// charged, and stop the moment the publication cannot fit so no unrelated continuation pays.
|
|
886
|
+
const admission = requiredHeadroomBytes !== undefined;
|
|
887
|
+
const headroom = admission && requiredHeadroomBytes > 0 ? requiredHeadroomBytes : 0;
|
|
888
|
+
const feasible = (): boolean => !admission
|
|
889
|
+
|| (spilledBytes = accountedResponseSpillBytes()) - spilledResponseBytes() + headroom <= spillByteCap();
|
|
890
|
+
if (!feasible() || spilledBytes <= spillByteCap() - headroom) return 0;
|
|
884
891
|
const before = spilledBytes;
|
|
885
892
|
// Deferred generations go first. They are already superseded, so releasing one
|
|
886
893
|
// costs only the crash window the queue exists to cover — the same trade
|
|
887
894
|
// PENDING_SPILL_UNLINKS_MAX already makes against unbounded disk. Evicting a
|
|
888
895
|
// live continuation to make room for a dead file would be the wrong order.
|
|
889
|
-
while (spilledBytes > spillByteCap() && pendingSpillUnlinks.length > 0) {
|
|
896
|
+
while (spilledBytes > spillByteCap() - headroom && pendingSpillUnlinks.length > 0) {
|
|
890
897
|
const ref = pendingSpillUnlinks.shift()!;
|
|
891
898
|
spilledBytes -= ref.payloadBytes;
|
|
892
899
|
deleteResponseSpill(ref);
|
|
900
|
+
if (admission) spillCounters.headroomEvictions += 1;
|
|
901
|
+
if (!feasible()) return before - spilledBytes;
|
|
893
902
|
}
|
|
894
903
|
// Ordered by createdAt, not by map order. `states` is not an age index:
|
|
895
904
|
// demotion and spill replacement delete and reinsert entries, and
|
|
@@ -906,9 +915,11 @@ function enforceSpilledResponseBudget(): number {
|
|
|
906
915
|
.sort((a, b) => a[1].createdAt - b[1].createdAt
|
|
907
916
|
|| (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
908
917
|
for (const [id, entry] of spilled) {
|
|
909
|
-
if (spilledBytes <= spillByteCap()) break;
|
|
918
|
+
if (spilledBytes <= spillByteCap() - headroom) break;
|
|
910
919
|
spilledBytes -= entry.spill.payloadBytes;
|
|
911
920
|
deleteEntry(id);
|
|
921
|
+
if (admission) spillCounters.headroomEvictions += 1;
|
|
922
|
+
if (!feasible()) return before - spilledBytes;
|
|
912
923
|
}
|
|
913
924
|
return before - spilledBytes;
|
|
914
925
|
}
|
|
@@ -1210,6 +1221,7 @@ export function responseStateMetrics(): ResponseStateMetrics {
|
|
|
1210
1221
|
spillLastSuccessAt: spillWriteHealth.lastSuccessAt,
|
|
1211
1222
|
spillAclRetryReturnedTimeouts: spillCounters.aclRetryReturnedTimeouts,
|
|
1212
1223
|
spillAclTimeoutMemoRefusals: spillCounters.aclTimeoutMemoRefusals,
|
|
1224
|
+
spillCapacityRefusals: spillCounters.capacityRefusals, spillHeadroomEvictions: spillCounters.headroomEvictions,
|
|
1213
1225
|
spillLastFailureAt: spillWriteHealth.lastFailureAt,
|
|
1214
1226
|
spillReadFailures: spillCounters.readFailures,
|
|
1215
1227
|
replayScopeMismatchDrops,
|
|
@@ -1321,11 +1333,7 @@ export function clearResponseStateMemoryForTests(): void {
|
|
|
1321
1333
|
oldestResidentAt = null;
|
|
1322
1334
|
stateRevision = 0;
|
|
1323
1335
|
pendingSpillUnlinks.length = 0;
|
|
1324
|
-
|
|
1325
|
-
spillCounters.writeFailures = 0;
|
|
1326
|
-
spillCounters.readFailures = 0;
|
|
1327
|
-
spillCounters.aclRetryReturnedTimeouts = 0;
|
|
1328
|
-
spillCounters.aclTimeoutMemoRefusals = 0;
|
|
1336
|
+
resetSpillCountersForTests();
|
|
1329
1337
|
spillWriteHealth.consecutiveFailures = 0;
|
|
1330
1338
|
spillWriteHealth.lastFailureCode = null;
|
|
1331
1339
|
spillWriteHealth.lastFailureOrigin = null;
|
|
@@ -54,3 +54,20 @@ export function externalTaskInputContent(item: unknown): string | OcxContentPart
|
|
|
54
54
|
block.type === "output_text" ? { ...block, type: "input_text" } : block,
|
|
55
55
|
));
|
|
56
56
|
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The same recognition, emitted as Responses-wire user content for the raw-body repairs (#6764).
|
|
60
|
+
*
|
|
61
|
+
* The passthrough and routed-compaction paths never read the parsed context, so they used to
|
|
62
|
+
* send this envelope through the generic orphan repair, which prefixes
|
|
63
|
+
* "[tool output for unknown call]". A summarizer then read an authorized handover as a stray tool
|
|
64
|
+
* result. Recognition is shared with the parser so both paths agree on what is task input.
|
|
65
|
+
*/
|
|
66
|
+
export function externalTaskInputResponsesContent(item: unknown): Record<string, unknown>[] | undefined {
|
|
67
|
+
if (externalTaskInputContent(item) === undefined) return undefined;
|
|
68
|
+
const output = (item as { output: string | TaskInputBlock[] }).output;
|
|
69
|
+
if (typeof output === "string") return [{ type: "input_text", text: output }];
|
|
70
|
+
return output.map(block => block.type === "input_image"
|
|
71
|
+
? { type: "input_image", image_url: block.image_url, ...(block.detail ? { detail: block.detail === "original" ? "high" : block.detail } : {}) }
|
|
72
|
+
: { type: "input_text", text: block.text });
|
|
73
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "../storage/policy-scheduler";
|
|
15
15
|
import { startQuotaResetPoller, stopQuotaResetPoller } from "../quota/reset-poller";
|
|
16
16
|
import {
|
|
17
|
+
setCatalogAutoRefreshClientFanout,
|
|
17
18
|
startCatalogAutoRefresh,
|
|
18
19
|
stopCatalogAutoRefresh,
|
|
19
20
|
syncCatalogAutoRefreshCadence,
|
|
@@ -40,6 +41,7 @@ type ProcessLoops = {
|
|
|
40
41
|
|
|
41
42
|
type LeaseOwner = {
|
|
42
43
|
token: symbol;
|
|
44
|
+
fanoutRevoked: boolean;
|
|
43
45
|
applyPolicy: PolicyApply;
|
|
44
46
|
resources: ServerResourceOwnerLease;
|
|
45
47
|
lowQuota: LowQuotaRegistration;
|
|
@@ -48,6 +50,7 @@ type LeaseOwner = {
|
|
|
48
50
|
export type ServerBackgroundLifecycleLease = {
|
|
49
51
|
scheduleStartupRun(): void;
|
|
50
52
|
listLowQuotaEvents(limit?: number): LowQuotaEvent[];
|
|
53
|
+
revokeClientFanout(): void;
|
|
51
54
|
release(): Promise<void>;
|
|
52
55
|
releaseAfterFailedStart(): void;
|
|
53
56
|
};
|
|
@@ -61,6 +64,25 @@ function setLivePolicyOwner(applyPolicy: PolicyApply | null): void {
|
|
|
61
64
|
setStorageCleanupPolicyJobLiveApply(applyPolicy);
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
/** Resolve the current process owner at delivery time so lease hand-over cannot retain an old port. */
|
|
68
|
+
async function refreshConnectedClientsAfterCatalogChange(isCurrent: () => boolean) {
|
|
69
|
+
const captured = owners.findLast(owner => !owner.fanoutRevoked);
|
|
70
|
+
if (!captured) return [];
|
|
71
|
+
const current = () => isCurrent() && !captured.fanoutRevoked && owners.includes(captured);
|
|
72
|
+
const [{ readRuntimePort }, { loadConfig }, { localClientSyncAllowed }, { syncEnabledClientIntegrations }] =
|
|
73
|
+
await Promise.all([
|
|
74
|
+
import("../config/process-state"),
|
|
75
|
+
import("../config"),
|
|
76
|
+
import("../codex/desired-state"),
|
|
77
|
+
import("./management/config-routes"),
|
|
78
|
+
]);
|
|
79
|
+
const runtime = readRuntimePort(process.pid);
|
|
80
|
+
const config = loadConfig();
|
|
81
|
+
// An explicit catalog tick on a hub is not permission to enroll its local clients.
|
|
82
|
+
if (!runtime || !current() || !localClientSyncAllowed(config)) return [];
|
|
83
|
+
return syncEnabledClientIntegrations(runtime.port, config, {}, { unattended: { isCurrent: current } });
|
|
84
|
+
}
|
|
85
|
+
|
|
64
86
|
function startProcessLoops(applyPolicy: PolicyApply): ProcessLoops {
|
|
65
87
|
let memoryWatchdog: MemoryWatchdog | null = null;
|
|
66
88
|
let stateStoreSweeper: ReturnType<typeof startStateStoreSweeper> | null = null;
|
|
@@ -83,6 +105,7 @@ function startProcessLoops(applyPolicy: PolicyApply): ProcessLoops {
|
|
|
83
105
|
// and interval timers are unref'd. The scheduler
|
|
84
106
|
// module keeps every heavy import inside its tick, so naming it statically here
|
|
85
107
|
// costs a module record and nothing else.
|
|
108
|
+
setCatalogAutoRefreshClientFanout(refreshConnectedClientsAfterCatalogChange);
|
|
86
109
|
startCatalogAutoRefresh();
|
|
87
110
|
// The scheduler starts at its default cadence because resolving the operator's value
|
|
88
111
|
// reads the config barrel. Fire-and-forget: startup must not await an optional
|
|
@@ -106,6 +129,7 @@ function startProcessLoops(applyPolicy: PolicyApply): ProcessLoops {
|
|
|
106
129
|
stopStorageCleanupScheduler();
|
|
107
130
|
stopQuotaResetPoller();
|
|
108
131
|
stopCatalogAutoRefresh();
|
|
132
|
+
setCatalogAutoRefreshClientFanout(null);
|
|
109
133
|
setLivePolicyOwner(null);
|
|
110
134
|
throw error;
|
|
111
135
|
}
|
|
@@ -119,6 +143,7 @@ function stopProcessLoops(): void {
|
|
|
119
143
|
stopStorageCleanupScheduler();
|
|
120
144
|
stopQuotaResetPoller();
|
|
121
145
|
stopCatalogAutoRefresh();
|
|
146
|
+
setCatalogAutoRefreshClientFanout(null);
|
|
122
147
|
setLivePolicyOwner(null);
|
|
123
148
|
}
|
|
124
149
|
|
|
@@ -181,6 +206,7 @@ export function acquireServerBackgroundLifecycle(
|
|
|
181
206
|
|
|
182
207
|
const owner: LeaseOwner = {
|
|
183
208
|
token: Symbol("server-background-lifecycle"),
|
|
209
|
+
fanoutRevoked: false,
|
|
184
210
|
applyPolicy,
|
|
185
211
|
resources: acquireServerResourceOwner(),
|
|
186
212
|
lowQuota: registerCodexLowQuotaProtection(config),
|
|
@@ -206,6 +232,7 @@ export function acquireServerBackgroundLifecycle(
|
|
|
206
232
|
return stopStoragePolicyWorker().finally(() => { cleanupInProgress = false; });
|
|
207
233
|
}
|
|
208
234
|
return {
|
|
235
|
+
revokeClientFanout() { owner.fanoutRevoked = true; },
|
|
209
236
|
listLowQuotaEvents(limit) { return owner.lowQuota.listEvents(limit); },
|
|
210
237
|
scheduleStartupRun() {
|
|
211
238
|
if (owners.some(candidate => candidate.token === owner.token)) {
|
|
@@ -113,6 +113,7 @@ export async function handleChatCompletions(
|
|
|
113
113
|
return finalizeTranslatorBudgetResponse(
|
|
114
114
|
await handleChatCompletionsWithBudget(req, config, logCtx, translatorBudget, logIds),
|
|
115
115
|
translatorBudget,
|
|
116
|
+
req.signal,
|
|
116
117
|
);
|
|
117
118
|
} catch (error) {
|
|
118
119
|
translatorBudget.dispose();
|
|
@@ -361,14 +361,16 @@ export async function runNativeChatAttempt(
|
|
|
361
361
|
// key rotation so recovery cannot replace the ceiling along with the active credential.
|
|
362
362
|
const requestTransientPolicy = transientRetryPolicyFor(activeProvider);
|
|
363
363
|
let transientSendsUsed = 0;
|
|
364
|
-
// A
|
|
365
|
-
//
|
|
364
|
+
// A Combo child's initial send is prepaid, not exempt from the shared ceiling. Only its
|
|
365
|
+
// owning permit exposes that one booking in addition to the still-unspent allowance, and only
|
|
366
|
+
// until this child's first physical send has started and settled the booking.
|
|
366
367
|
const sharedSendCap = requestTransientPolicy?.attempts ?? TRANSIENT_RETRY_MAX_ATTEMPTS;
|
|
367
368
|
let physicalSends = 0;
|
|
369
|
+
let initialSendStarted = false;
|
|
368
370
|
const remainingSharedSends = (): number => {
|
|
369
371
|
if (!sendBudget) return Number.POSITIVE_INFINITY;
|
|
370
372
|
const remaining = sendBudget.remainingBaseSends(sharedSendCap);
|
|
371
|
-
return
|
|
373
|
+
return Math.min(sharedSendCap, remaining + (!initialSendStarted && execution.comboDispatchPermit ? 1 : 0));
|
|
372
374
|
};
|
|
373
375
|
const remainingTransientSends = (): number => Math.min(
|
|
374
376
|
requestTransientPolicy
|
|
@@ -404,6 +406,21 @@ export async function runNativeChatAttempt(
|
|
|
404
406
|
claimAmbiguousResend,
|
|
405
407
|
);
|
|
406
408
|
|
|
409
|
+
// Non-enforced accounting for one physical send. Enforced spend is owned by the reporter.
|
|
410
|
+
const comboReceipt = Boolean(sendBudget && execution.comboDispatchPermit);
|
|
411
|
+
const settleNonEnforcedSend = (url: string): void => {
|
|
412
|
+
if (sendBudget) {
|
|
413
|
+
// Backstop for sends the helper cannot see coming (a reset replay). The first
|
|
414
|
+
// report settles the combo's booking; each later one is charged and booked.
|
|
415
|
+
if (physicalSends > 0 && sendBudget.remainingBaseSends(sharedSendCap) <= 0) {
|
|
416
|
+
throw new SendBudgetExhaustedError(safeHostLabel(url));
|
|
417
|
+
}
|
|
418
|
+
physicalSends += 1;
|
|
419
|
+
reportDispatchSends(sendBudget, 1, execution.comboDispatchPermit);
|
|
420
|
+
initialSendStarted = true;
|
|
421
|
+
} else if (!spendTracker?.charge()) throw new NativeChatSpendRefusal();
|
|
422
|
+
};
|
|
423
|
+
|
|
407
424
|
const send = async (
|
|
408
425
|
request: AdapterRequest,
|
|
409
426
|
recovery?: "rate-limit-429" | "key-429" | UpstreamSendRecovery,
|
|
@@ -433,6 +450,9 @@ export async function runNativeChatAttempt(
|
|
|
433
450
|
providerFetch(activeProvider, undefined, {
|
|
434
451
|
providerName: route.providerName,
|
|
435
452
|
modelId: route.modelId,
|
|
453
|
+
onPhysicalDispatch: comboReceipt
|
|
454
|
+
? () => { if (!physicalBudget.spendEnforced) settleNonEnforcedSend(request.url); }
|
|
455
|
+
: undefined,
|
|
436
456
|
dispatchOverride: async (_input, init, execute) => {
|
|
437
457
|
if (!providerApiKeySelectionIsCurrent(config, route.providerName, activeProvider)) {
|
|
438
458
|
const current = resolveCurrentProviderApiKeyTransport(config, route.providerName, activeProvider);
|
|
@@ -466,15 +486,11 @@ export async function runNativeChatAttempt(
|
|
|
466
486
|
if ((logCtx.spendTracker as { refusals?: number } | undefined)?.refusals) throw new NativeChatSpendRefusal();
|
|
467
487
|
throw new SendBudgetExhaustedError();
|
|
468
488
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
475
|
-
physicalSends += 1;
|
|
476
|
-
reportDispatchSends(sendBudget, 1, execution.comboDispatchPermit);
|
|
477
|
-
} else if (!physicalBudget.spendEnforced && !spendTracker?.charge()) throw new NativeChatSpendRefusal();
|
|
489
|
+
// Enforced spend started this send at `beforeSend`. A Combo child's non-enforced
|
|
490
|
+
// send is booked by the physical-dispatch receipt instead, so a local refusal
|
|
491
|
+
// before the wire refunds the booking rather than charging it.
|
|
492
|
+
if (physicalBudget.spendEnforced) initialSendStarted = true;
|
|
493
|
+
else if (!comboReceipt) settleNonEnforcedSend(request.url);
|
|
478
494
|
noteProviderAttemptSend(logCtx, route.providerName, activeProvider, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
|
|
479
495
|
// A reselected provider transport is still a physical send: the connection policy
|
|
480
496
|
// and manual-redirect ownership wrap the selected implementation (#4992).
|
|
@@ -21,7 +21,8 @@ import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize"
|
|
|
21
21
|
import { createToolCallIdAllocator } from "../adapters/tool-call-id";
|
|
22
22
|
import { openAIChatSerializesThinking } from "../adapters/openai-chat/messages";
|
|
23
23
|
import { messagesToResponsesTranslation } from "../protocols/codecs/messages";
|
|
24
|
-
import { AnthropicRequestError, DesktopModelMappingUnavailableError, extractOcxEffortDirective, extractOcxRouteDirective, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
|
|
24
|
+
import { AnthropicRequestError, DesktopModelMappingUnavailableError, extractOcxEffortDirective, extractOcxRouteDirective, nativeAnthropicProjection, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
|
|
25
|
+
import { nativeReasoningTag, type NativeReasoningOwner } from "../responses/reasoning-replay-cache";
|
|
25
26
|
import { isKnownDesktop3pModelId, resolveDesktop3pAlias } from "../claude/desktop-3p";
|
|
26
27
|
import { resolveAlias, claudeCodeNativeAlias, legacyAliasForNative } from "../claude/alias";
|
|
27
28
|
import { recordDesktopRequest } from "../claude/desktop-health";
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
} from "../lib/upstream-retry";
|
|
42
43
|
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
43
44
|
import { anthropicRateLimitHeaders } from "./anthropic-rate-limit-headers";
|
|
45
|
+
import { upstreamMessagesRequestIdHeaders } from "./messages-response-headers";
|
|
44
46
|
import {
|
|
45
47
|
anthropicErrorBody,
|
|
46
48
|
anthropicErrorResponse,
|
|
@@ -634,7 +636,10 @@ async function anthropicNativePassthrough(
|
|
|
634
636
|
const upstream = result.upstream;
|
|
635
637
|
|
|
636
638
|
const contentType = upstream.headers.get("content-type") ?? "application/json";
|
|
637
|
-
const rateLimitHeaders =
|
|
639
|
+
const rateLimitHeaders = {
|
|
640
|
+
...anthropicRateLimitHeaders(upstream.headers),
|
|
641
|
+
...(pathname === "/v1/messages" ? upstreamMessagesRequestIdHeaders(upstream.headers) : {}),
|
|
642
|
+
};
|
|
638
643
|
const bodyGuard = resolvePassthroughBodyGuard(config, req.signal);
|
|
639
644
|
if (upstream.ok && contentType.includes("text/event-stream") && upstream.body) {
|
|
640
645
|
return new Response(tapAnthropicSseForLog(upstream.body, logCtx, finalize, bodyGuard), {
|
|
@@ -837,6 +842,7 @@ export async function handleClaudeMessages(
|
|
|
837
842
|
return finalizeTranslatorBudgetResponse(
|
|
838
843
|
await handleClaudeMessagesWithBudget(req, config, logCtx, translatorBudget, logIds, requestPolicy, ingress),
|
|
839
844
|
translatorBudget,
|
|
845
|
+
req.signal,
|
|
840
846
|
);
|
|
841
847
|
} catch (error) {
|
|
842
848
|
translatorBudget.dispose();
|
|
@@ -872,6 +878,9 @@ async function handleClaudeMessagesWithBudget(
|
|
|
872
878
|
let anthropicBody: unknown;
|
|
873
879
|
let internalBody: Rec;
|
|
874
880
|
let cacheKeySource: ClaudeCacheKeySource = null;
|
|
881
|
+
let nativeReasoningReplay: ReadonlyMap<string, string> | undefined;
|
|
882
|
+
const nativeReasoningMint: { owner?: NativeReasoningOwner } = {};
|
|
883
|
+
let nativeProjectionOnlyError: AnthropicRequestError | undefined;
|
|
875
884
|
let effortOverride: string | null = null;
|
|
876
885
|
let effortRow: ParsedEffortRowId | null = null;
|
|
877
886
|
let fastRow: ParsedFastRowId | null = null;
|
|
@@ -962,7 +971,15 @@ async function handleClaudeMessagesWithBudget(
|
|
|
962
971
|
if (!effortRow && !fastRow && isRec(anthropicBody) && wantsNativePassthrough(req, config, requestPolicy, anthropicBody.model, cc)) {
|
|
963
972
|
markProtocolEntry(logCtx, { inbound: "messages", lane: "native", features: messagesFeatures });
|
|
964
973
|
recordProtocolShadowPlan(logCtx, config, { inbound: "messages", model: requestedModel });
|
|
965
|
-
|
|
974
|
+
let projectedBody: Rec;
|
|
975
|
+
try {
|
|
976
|
+
projectedBody = nativeAnthropicProjection(anthropicBody, translatorBudget);
|
|
977
|
+
} catch (err) {
|
|
978
|
+
if (!isTranslatorBudgetExceededError(err)) throw err;
|
|
979
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 413, { closeReason: "non_stream" });
|
|
980
|
+
return anthropicErrorResponse(413, "request translation buffer exceeded the safe limit", "request_too_large", "translation_buffer_limit");
|
|
981
|
+
}
|
|
982
|
+
return await anthropicNativePassthrough(req, config, logCtx, logIds, projectedBody, "/v1/messages");
|
|
966
983
|
}
|
|
967
984
|
if (isRec(anthropicBody) && typeof anthropicBody.model === "string"
|
|
968
985
|
&& messagesSecondaryInstanceUnavailable(config, anthropicBody.model, cc)) {
|
|
@@ -1017,7 +1034,19 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1017
1034
|
};
|
|
1018
1035
|
delete anthropicBody.thinking;
|
|
1019
1036
|
}
|
|
1020
|
-
|
|
1037
|
+
let translation;
|
|
1038
|
+
try {
|
|
1039
|
+
translation = messagesToResponsesTranslation(anthropicBody, cc, translatorBudget);
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
// A native Messages route can discard an undecodable proxy envelope. Keep the
|
|
1042
|
+
// original error for any route that ultimately translates to Responses.
|
|
1043
|
+
if (!(err instanceof AnthropicRequestError) || err.message !== "malformed ocxr1 reasoning signature"
|
|
1044
|
+
|| !isRec(anthropicBody)) throw err;
|
|
1045
|
+
const projected = nativeAnthropicProjection(anthropicBody, translatorBudget);
|
|
1046
|
+
if (projected === anthropicBody) throw err;
|
|
1047
|
+
translation = messagesToResponsesTranslation(projected, cc, translatorBudget);
|
|
1048
|
+
nativeProjectionOnlyError = err;
|
|
1049
|
+
}
|
|
1021
1050
|
internalBody = translation.body;
|
|
1022
1051
|
// The Anthropic translator builds its body from model/input/store/stream plus sampling
|
|
1023
1052
|
// fields only, so the caller intent is applied to the TRANSLATED body rather than the
|
|
@@ -1025,6 +1054,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1025
1054
|
if (fastRow) internalBody.service_tier = "priority";
|
|
1026
1055
|
translatorBudget.chargeRetained(jsonUtf8Bytes(internalBody), { kind: "request_copies" });
|
|
1027
1056
|
cacheKeySource = translation.cacheKeySource;
|
|
1057
|
+
nativeReasoningReplay = translation.nativeReasoningReplay;
|
|
1028
1058
|
} catch (err) {
|
|
1029
1059
|
const overflow = isTranslatorBudgetExceededError(err);
|
|
1030
1060
|
const unavailable = err instanceof DesktopModelMappingUnavailableError;
|
|
@@ -1171,6 +1201,10 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1171
1201
|
? nativeMessagesDeclineReason(settledRoute, anthropicBody, config, nativeSelector)
|
|
1172
1202
|
: "rollout-disabled";
|
|
1173
1203
|
const nativeMessagesRoute = settledRoute && nativeDecline === undefined ? settledRoute : undefined;
|
|
1204
|
+
if (nativeProjectionOnlyError && !nativeMessagesRoute) {
|
|
1205
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 400, { closeReason: "non_stream" });
|
|
1206
|
+
return anthropicErrorResponse(400, nativeProjectionOnlyError.message, "invalid_request_error");
|
|
1207
|
+
}
|
|
1174
1208
|
// With the switch on, a declined route says why on its bridge mark, as native Chat does.
|
|
1175
1209
|
if (nativeDecline && nativeDecline !== "rollout-disabled") {
|
|
1176
1210
|
markProtocolEntry(logCtx, {
|
|
@@ -1209,7 +1243,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1209
1243
|
let nativeBody: Rec;
|
|
1210
1244
|
try {
|
|
1211
1245
|
// Built from the source envelope when there is one, after the managed-client steps above.
|
|
1212
|
-
nativeBody = envelope ? envelope.freshBody() : anthropicBody as Rec;
|
|
1246
|
+
nativeBody = nativeAnthropicProjection(envelope ? envelope.freshBody() : anthropicBody as Rec, translatorBudget);
|
|
1213
1247
|
} catch (err) {
|
|
1214
1248
|
if (!isTranslatorBudgetExceededError(err)) throw err;
|
|
1215
1249
|
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 413, { closeReason: "non_stream" });
|
|
@@ -1314,6 +1348,8 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1314
1348
|
// Without this the replay would look native and a Responses-scoped wire default
|
|
1315
1349
|
// would fire, disagreeing with the pre-flight decision above.
|
|
1316
1350
|
inboundWire: "anthropic",
|
|
1351
|
+
nativeReasoningReplay,
|
|
1352
|
+
nativeReasoningMint,
|
|
1317
1353
|
claudeGoAffinity: { sessionLane: claudeGoSessionLane },
|
|
1318
1354
|
claudeNativeSessionId,
|
|
1319
1355
|
stripClaudeMainAuthForNoncanonicalForward: true,
|
|
@@ -1395,6 +1431,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1395
1431
|
if (contentType.includes("text/event-stream") && response.body) {
|
|
1396
1432
|
const anthropicSse = responsesSseToAnthropicSse(response.body, requestedModel, {
|
|
1397
1433
|
translatorBudget,
|
|
1434
|
+
nativeReasoningTagFor: blob => nativeReasoningTag(nativeReasoningMint.owner, blob),
|
|
1398
1435
|
// Only a floor, and only for the first frame: an upstream that reports usage early wins
|
|
1399
1436
|
// over it inside the translator, and the terminal `message_delta` carries the
|
|
1400
1437
|
// authoritative count either way (#4857).
|
|
@@ -1464,7 +1501,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1464
1501
|
}
|
|
1465
1502
|
let message: Rec;
|
|
1466
1503
|
try {
|
|
1467
|
-
message = responsesJsonToAnthropicMessage(json, requestedModel, translatorBudget);
|
|
1504
|
+
message = responsesJsonToAnthropicMessage(json, requestedModel, translatorBudget, blob => nativeReasoningTag(nativeReasoningMint.owner, blob));
|
|
1468
1505
|
} catch (err) {
|
|
1469
1506
|
if (!isTranslatorBudgetExceededError(err)) throw err;
|
|
1470
1507
|
return anthropicErrorResponse(413, "upstream translation buffer exceeded the safe limit", "request_too_large", "translation_buffer_limit");
|
|
@@ -181,6 +181,7 @@ export function runAdmittedBodyWork(
|
|
|
181
181
|
addFinalRequestLog(refusalLog.requestId, refusalLog.start, refusalLog.logCtx, refused.status, {
|
|
182
182
|
closeReason: "terminal",
|
|
183
183
|
});
|
|
184
|
+
refusalLog.onLogged?.();
|
|
184
185
|
}
|
|
185
186
|
return withCors(refused, req, policy);
|
|
186
187
|
});
|
|
@@ -41,10 +41,11 @@ export function buildInterceptDesiredClients(config: OcxConfig, observed: Claude
|
|
|
41
41
|
* imports keep the catalog and discovery off the synchronous startup path.
|
|
42
42
|
*/
|
|
43
43
|
export async function loadPickerRoutesFromCatalog(): Promise<PickerRouteInput> {
|
|
44
|
-
const [{ loadConfig }, { fetchAllModels }, catalog] = await Promise.all([
|
|
44
|
+
const [{ loadConfig }, { fetchAllModels }, catalog, { resolveAutoContext }] = await Promise.all([
|
|
45
45
|
import("../../config"),
|
|
46
46
|
import("../management-api"),
|
|
47
47
|
import("../../codex/catalog"),
|
|
48
|
+
import("../../claude/context-windows"),
|
|
48
49
|
]);
|
|
49
50
|
const config = loadConfig();
|
|
50
51
|
const models = await fetchAllModels(config);
|
|
@@ -54,6 +55,7 @@ export async function loadPickerRoutesFromCatalog(): Promise<PickerRouteInput> {
|
|
|
54
55
|
.map(model => ({ provider: model.provider, id: model.id, contextWindow: model.contextWindow })),
|
|
55
56
|
...(config.claudeCode?.desktopProfile ? { profile: config.claudeCode.desktopProfile } : {}),
|
|
56
57
|
nativeContextCap: catalog.nativeContextLimits(config),
|
|
58
|
+
auto: resolveAutoContext(config.claudeCode),
|
|
57
59
|
};
|
|
58
60
|
}
|
|
59
61
|
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "./live-sideband";
|
|
22
22
|
import {
|
|
23
23
|
withRequestLogId,
|
|
24
|
+
withMessagesRequestLogId,
|
|
24
25
|
} from "./startup-warnings";
|
|
25
26
|
|
|
26
27
|
import { remoteWorkspaceEnabled } from "../../remote-control/workspace-activation";
|
|
@@ -118,6 +119,7 @@ import { handleChatCompletions } from "../chat-completions";
|
|
|
118
119
|
import { anthropicErrorResponse } from "../../claude/outbound";
|
|
119
120
|
import {
|
|
120
121
|
buildDesktop3pRegistry,
|
|
122
|
+
desktop3pModelOptions,
|
|
121
123
|
generateDesktop3pModels,
|
|
122
124
|
} from "../../claude/desktop-3p";
|
|
123
125
|
import { buildDesktopDiscoveryInputs } from "../../claude/desktop-discovery-inputs";
|
|
@@ -1024,6 +1026,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1024
1026
|
const models = config.claudeCode?.enabled === false ? [] : generateDesktop3pModels(
|
|
1025
1027
|
desktopInputs.nativeSlugs, desktopInputs.routedModels,
|
|
1026
1028
|
config.claudeCode?.desktopProfile, desktopInputs.nativeContextCap,
|
|
1029
|
+
desktop3pModelOptions(config.claudeCode),
|
|
1027
1030
|
);
|
|
1028
1031
|
const response = jsonResponse({ version: 1, models }, 200, req, policy);
|
|
1029
1032
|
response.headers.set("Cache-Control", "no-store");
|
|
@@ -1556,11 +1559,16 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1556
1559
|
// pre-translation stream + native passthrough callbacks) — do not re-wrap the
|
|
1557
1560
|
// translated Anthropic stream here.
|
|
1558
1561
|
const sessionReq = withCallerSessionIdentity(req, admission);
|
|
1559
|
-
|
|
1562
|
+
// Entering the handler, or a logged refusal, means the final request log owns requestId.
|
|
1563
|
+
let ownsRequestLog = false;
|
|
1564
|
+
const claimRequestLog = (work: (lease: ActiveTurnLease) => Promise<Response>) =>
|
|
1565
|
+
(lease: ActiveTurnLease) => { ownsRequestLog = true; return work(lease); };
|
|
1566
|
+
const response = await runAdmittedHttpTurn(sessionReq, policy, claimRequestLog(async turnAdmissionLease => withCors(
|
|
1560
1567
|
await handleClaudeMessages(sessionReq, config, logCtx, { requestId, start, turnAdmissionLease, admission }, policy, { claudeIntercept: ingress === "claude-intercept" }),
|
|
1561
1568
|
req,
|
|
1562
1569
|
policy,
|
|
1563
|
-
), { requestId, start, logCtx });
|
|
1570
|
+
)), { requestId, start, logCtx, onLogged: () => { ownsRequestLog = true; } });
|
|
1571
|
+
return ownsRequestLog ? withMessagesRequestLogId(response, requestId) : response;
|
|
1564
1572
|
}
|
|
1565
1573
|
|
|
1566
1574
|
// OpenAI Chat Completions inbound (GitHub Copilot App / OpenAI-compatible clients).
|
|
@@ -37,7 +37,8 @@ export interface SpendLedgerServerLifecycle {
|
|
|
37
37
|
export function acquireSpendLedgerServerLifecycle(configDir: string): SpendLedgerServerLifecycle {
|
|
38
38
|
const owner: SpendLedgerOwnerLease = acquireSpendLedgerOwner(configDir);
|
|
39
39
|
// Advisory: a synced state directory makes the journal's hard-link guard refuse intermittently
|
|
40
|
-
// (#6314)
|
|
40
|
+
// (#6314), including Google Drive syncing the native Desktop or Documents folder. Said once
|
|
41
|
+
// at startup instead of being discovered from a 502. The nlink guard itself is unchanged.
|
|
41
42
|
warnIfSyncedStateDirectory(configDir);
|
|
42
43
|
// Each entry returns whatever the listener's own stop returned. Typed as void-or-promise
|
|
43
44
|
// because the rollback below has to WAIT on it: declaring it `() => void` let the call site
|
|
@@ -100,6 +100,25 @@ import {
|
|
|
100
100
|
|
|
101
101
|
const REQUEST_LOG_ID_RESPONSE_HEADER = "x-opencodex-request-id";
|
|
102
102
|
|
|
103
|
+
/** Messages clients persist request-id; the upstream diagnostic id has a separate namespace. */
|
|
104
|
+
export function withMessagesRequestLogId(response: Response, requestId: string): Response {
|
|
105
|
+
const upstreamId = response.headers.get("request-id");
|
|
106
|
+
const correlated = withRequestLogId(response, requestId);
|
|
107
|
+
correlated.headers.delete("x-opencodex-upstream-request-id");
|
|
108
|
+
if (upstreamId && /^req_[A-Za-z0-9_-]{1,128}$/.test(upstreamId)) {
|
|
109
|
+
correlated.headers.set("x-opencodex-upstream-request-id", upstreamId);
|
|
110
|
+
}
|
|
111
|
+
correlated.headers.set("request-id", requestId);
|
|
112
|
+
const exposed = correlated.headers.get("Access-Control-Expose-Headers") ?? "";
|
|
113
|
+
const names = exposed.split(",").map(name => name.trim().toLowerCase());
|
|
114
|
+
const additions = ["request-id", ...(correlated.headers.has("x-opencodex-upstream-request-id")
|
|
115
|
+
? ["x-opencodex-upstream-request-id"] : [])].filter(name => !names.includes(name));
|
|
116
|
+
if (additions.length) {
|
|
117
|
+
correlated.headers.set("Access-Control-Expose-Headers", [exposed, ...additions].filter(Boolean).join(", "));
|
|
118
|
+
}
|
|
119
|
+
return correlated;
|
|
120
|
+
}
|
|
121
|
+
|
|
103
122
|
export function withRequestLogId(response: Response, requestId: string): Response {
|
|
104
123
|
const headers = new Headers(response.headers);
|
|
105
124
|
headers.set(REQUEST_LOG_ID_RESPONSE_HEADER, requestId);
|
package/src/server/index.ts
CHANGED
|
@@ -750,6 +750,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
750
750
|
Object.defineProperty(server, "stop", {
|
|
751
751
|
configurable: true,
|
|
752
752
|
value: async (closeActiveConnections?: boolean): Promise<void> => {
|
|
753
|
+
backgroundLifecycle?.revokeClientFanout();
|
|
753
754
|
remoteWorkspaceStopping = true;
|
|
754
755
|
liveCallBindings.clear();
|
|
755
756
|
// Disarm the package-tree restart timer before teardown can schedule another restart.
|