@bitkyc08/opencodex 2.66.0-preview.20260925 → 2.67.0-preview.20260926
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 +24 -11
- package/gui/dist/assets/{App-BVjA0T6O.js → App-B3VRRYSj.js} +15 -15
- package/gui/dist/assets/{App-gkoJIEOV.css → App-BJsT8Icc.css} +1 -1
- package/gui/dist/assets/{Tray-BCFtiWwD.js → Tray-TYAA0EQY.js} +1 -1
- package/gui/dist/assets/index-9Ju7nHsu.js +86 -0
- package/gui/dist/assets/index-DtNmX7hW.css +1 -0
- package/gui/dist/assets/{tray-data-C4FiNaxk.js → tray-data-xKfS2gSG.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/chat/inbound.ts +82 -3
- package/src/chat/outbound.ts +5 -3
- package/src/claude/desktop-first-party.ts +12 -3
- package/src/claude/first-party-settings.ts +97 -0
- package/src/claude/intercept/client-class.ts +18 -0
- package/src/claude/intercept/listener.ts +5 -0
- package/src/claude/intercept/runtime.ts +6 -0
- package/src/cli/capabilities.ts +12 -0
- package/src/cli/claude-desktop.ts +11 -6
- package/src/cli/claude.ts +25 -1
- package/src/cli/ensure-desired-integrations.ts +14 -1
- package/src/cli/integrations.ts +9 -1
- package/src/cli/uninstall-client-state.ts +6 -0
- package/src/cli/uninstall-integrations.ts +115 -0
- package/src/codex/history-job.ts +2 -1
- package/src/codex/main-account-hard-lock.ts +36 -19
- package/src/codex/quota.ts +11 -1
- package/src/combos/resolve.ts +11 -3
- package/src/config/load-degrade.ts +4 -0
- package/src/generated/compatibility-version.json +67 -43
- package/src/generated/worker-bundles.gen.ts +4 -0
- package/src/images/loop.ts +10 -3
- package/src/integrations/aside-profile-context.ts +17 -0
- package/src/lab/fabric/executor.ts +25 -3
- package/src/lab/fabric/producer-isolate.ts +199 -32
- package/src/lab/fabric/scratch.ts +9 -2
- package/src/lib/shadow-call.ts +11 -4
- package/src/lib/worker-embed.ts +33 -0
- package/src/oauth/generic-account-failover.ts +5 -3
- package/src/oauth/index.ts +19 -29
- package/src/oauth/store.ts +106 -34
- package/src/remote-control/workspace-rpc.ts +114 -19
- package/src/router.ts +7 -1
- package/src/server/chat-completions.ts +2 -1
- package/src/server/index/claude-intercept-lifecycle.ts +13 -0
- package/src/server/management/agent-settings-routes.ts +123 -5
- package/src/server/management/context.ts +4 -0
- package/src/server/management/native-integration-routes.ts +19 -8
- package/src/server/responses/core-normalize.ts +13 -1
- package/src/server/responses/request-prepare.ts +5 -3
- package/src/server/responses/sidecar-execution.ts +9 -2
- package/src/server/search.ts +28 -7
- package/src/storage/policy-job.ts +2 -1
- package/src/storage/restore-job.ts +2 -1
- package/src/types/config.ts +11 -5
- package/src/update/transactional-install.mjs +5 -1
- package/src/web-search/alpha-search.ts +71 -0
- package/src/web-search/devin-executor.ts +192 -0
- package/src/web-search/loop.ts +10 -3
- package/gui/dist/assets/index-ComVsdSk.css +0 -1
- package/gui/dist/assets/index-vO4dgfiU.js +0 -86
|
@@ -1226,7 +1226,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1226
1226
|
const committed = persistCommittedDesktopGateway(config, state.profile, result.fingerprint);
|
|
1227
1227
|
const modeWarning = committed.ok ? undefined : `Gateway applied, but its mode/profile state was not saved (${committed.reason}).`;
|
|
1228
1228
|
// The durable marker describes the committed gateway even if old-mode cleanup fails.
|
|
1229
|
-
const firstPartyRemoved = removeDesktopFirstParty();
|
|
1229
|
+
const firstPartyRemoved = removeDesktopFirstParty(loadConfig());
|
|
1230
1230
|
if (!firstPartyRemoved.ok) {
|
|
1231
1231
|
return { response: jsonResponse({
|
|
1232
1232
|
error: `Claude Code settings could not be parsed (${firstPartyRemoved.path}); first-party cleanup remains incomplete after gateway apply.`,
|
|
@@ -1235,10 +1235,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1235
1235
|
...(modeWarning ? { warning: modeWarning } : {}),
|
|
1236
1236
|
}, 500) };
|
|
1237
1237
|
}
|
|
1238
|
-
return { result, committed, modeWarning, pickerOff };
|
|
1238
|
+
return { result, committed, modeWarning, pickerOff, firstPartyRemoved };
|
|
1239
1239
|
});
|
|
1240
1240
|
if (outcome.response) return outcome.response;
|
|
1241
|
-
const { result, committed, modeWarning, pickerOff } = outcome as Exclude<typeof outcome, { response: Response }>;
|
|
1241
|
+
const { result, committed, modeWarning, pickerOff, firstPartyRemoved } = outcome as Exclude<typeof outcome, { response: Response }>;
|
|
1242
1242
|
const { claudeDesktopPolicyWarning, getCachedClaudeDesktopPolicy } = await import("../../claude/desktop-policy");
|
|
1243
1243
|
const policyState = deps.probeClaudeDesktopPolicy
|
|
1244
1244
|
? await deps.probeClaudeDesktopPolicy({ platform: deps.platform ?? process.platform })
|
|
@@ -1247,7 +1247,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1247
1247
|
const pickerWarning = pickerOff.residual?.length
|
|
1248
1248
|
? `Picker mode cleanup is incomplete (${pickerOff.residual.join(", ")}); run ocx claude desktop picker off.`
|
|
1249
1249
|
: undefined;
|
|
1250
|
-
const warning = [modeWarning, policyWarning, pickerWarning
|
|
1250
|
+
const warning = [modeWarning, policyWarning, pickerWarning,
|
|
1251
|
+
firstPartyRemoved.retainedFor === "cli"
|
|
1252
|
+
? "Shared first-party settings remain for Claude Code CLI." : undefined].filter(Boolean).join(" ");
|
|
1251
1253
|
return jsonResponse({
|
|
1252
1254
|
ok: true,
|
|
1253
1255
|
mode: "gateway",
|
|
@@ -1447,8 +1449,21 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1447
1449
|
);
|
|
1448
1450
|
const webSearchOverride = config.claudeCode?.webSearchSidecar;
|
|
1449
1451
|
const visionOverride = config.claudeCode?.visionSidecar;
|
|
1452
|
+
const { firstPartyDesired, readFirstPartyProxyStatus } = await import("../../claude/first-party-settings");
|
|
1453
|
+
const { observeClaudeDesktopMode } = await import("../../claude/desktop-first-party");
|
|
1454
|
+
const { claudeInterceptEnabled, getClaudeInterceptState } = await import("../../claude/intercept/runtime");
|
|
1455
|
+
const desired = firstPartyDesired(config, observeClaudeDesktopMode(config));
|
|
1456
|
+
const bound = (deps.getClaudeInterceptState ?? getClaudeInterceptState)();
|
|
1457
|
+
const eligible = claudeInterceptEnabled(config);
|
|
1458
|
+
const sharedProxy = readFirstPartyProxyStatus(config, bound?.proxyPort ?? null);
|
|
1450
1459
|
return jsonResponse({
|
|
1451
1460
|
enabled: config.claudeCode?.enabled !== false,
|
|
1461
|
+
cliFirstParty: config.claudeCode?.cliFirstParty === true,
|
|
1462
|
+
cliFirstPartyApplied: config.claudeCode?.cliFirstParty === true && sharedProxy === "live",
|
|
1463
|
+
desktopFirstParty: desired.desktop,
|
|
1464
|
+
interceptEligible: eligible,
|
|
1465
|
+
interceptRunning: bound !== null && eligible,
|
|
1466
|
+
sharedProxy,
|
|
1452
1467
|
// Three-state intent (devlog 260726_claude_auth_auto): an absent key is AUTO, not
|
|
1453
1468
|
// subscription. The old coercion made every save convert an untouched auto config
|
|
1454
1469
|
// into a sticky manual subscription with no way back.
|
|
@@ -1505,7 +1520,110 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1505
1520
|
return prototype === Object.prototype || prototype === null;
|
|
1506
1521
|
};
|
|
1507
1522
|
if (!isPlainObject(parsedBody)) return jsonResponse({ error: "body must be an object" }, 400);
|
|
1508
|
-
const body = parsedBody as { enabled?: unknown; authMode?: unknown; model?: unknown; smallFastModel?: unknown; modelMap?: unknown; classifierModel?: unknown; classifierFallbacks?: unknown; systemEnv?: unknown; fastMode?: unknown; maxContextTokens?: unknown; alwaysEnableEffort?: unknown; tierModels?: unknown; autoContext?: unknown; autoCompactWindow?: unknown; blockedSkills?: unknown; injectAgents?: unknown; webSearchSidecar?: unknown; visionSidecar?: unknown };
|
|
1523
|
+
const body = parsedBody as { enabled?: unknown; cliFirstParty?: unknown; authMode?: unknown; model?: unknown; smallFastModel?: unknown; modelMap?: unknown; classifierModel?: unknown; classifierFallbacks?: unknown; systemEnv?: unknown; fastMode?: unknown; maxContextTokens?: unknown; alwaysEnableEffort?: unknown; tierModels?: unknown; autoContext?: unknown; autoCompactWindow?: unknown; blockedSkills?: unknown; injectAgents?: unknown; webSearchSidecar?: unknown; visionSidecar?: unknown };
|
|
1524
|
+
if (body.cliFirstParty !== undefined && typeof body.cliFirstParty !== "boolean")
|
|
1525
|
+
return jsonResponse({ error: "cliFirstParty must be a boolean" }, 400);
|
|
1526
|
+
if (body.cliFirstParty !== undefined && Object.keys(body).length !== 1)
|
|
1527
|
+
return jsonResponse({ error: "cliFirstParty must be sent alone", code: "cli_first_party_not_alone" }, 400);
|
|
1528
|
+
if (body.cliFirstParty !== undefined) {
|
|
1529
|
+
const { claudeInterceptEnabled, claudeInterceptProxyPort, getClaudeInterceptState } = await import("../../claude/intercept/runtime");
|
|
1530
|
+
const { captureDesktopFirstPartyRollback, inspectDesktopFirstParty, observeClaudeDesktopMode, resolveClaudeDesktopMode } = await import("../../claude/desktop-first-party");
|
|
1531
|
+
const { firstPartyDesired, readFirstPartyProxyStatus, reconcileClaudeFirstPartySettings } = await import("../../claude/first-party-settings");
|
|
1532
|
+
const { commitClaudeCodeBlock } = await import("../../claude/claude-code-block");
|
|
1533
|
+
const bound = (deps.getClaudeInterceptState ?? getClaudeInterceptState)();
|
|
1534
|
+
if (body.cliFirstParty) {
|
|
1535
|
+
if (!claudeInterceptEnabled(config)) return jsonResponse({ error: "Claude intercept is disabled", code: "intercept_disabled" }, 409);
|
|
1536
|
+
if (bound === null) return jsonResponse({ error: "Claude intercept is unavailable", code: "intercept_unavailable" }, 409);
|
|
1537
|
+
const targetPort = claudeInterceptProxyPort(config, config.port ?? 10100);
|
|
1538
|
+
if (targetPort !== bound.proxyPort) return jsonResponse({
|
|
1539
|
+
error: `Claude intercept port mismatch (configured ${targetPort}, bound ${bound.proxyPort}); restart needed`,
|
|
1540
|
+
code: "intercept_unavailable",
|
|
1541
|
+
}, 409);
|
|
1542
|
+
let inspection: ReturnType<typeof inspectDesktopFirstParty>["settings"];
|
|
1543
|
+
try { inspection = inspectDesktopFirstParty(config).settings; }
|
|
1544
|
+
catch { return jsonResponse({ error: "Claude settings are unreadable", code: "unreadable" }, 500); }
|
|
1545
|
+
if (inspection.kind === "foreign") return jsonResponse({ error: "Claude settings env is foreign", code: "foreign_env" }, 409);
|
|
1546
|
+
if (inspection.kind === "unreadable") return jsonResponse({ error: "Claude settings are unreadable", code: "unreadable" }, 500);
|
|
1547
|
+
}
|
|
1548
|
+
let restoreSettings: (() => boolean) | undefined;
|
|
1549
|
+
try { if (body.cliFirstParty) restoreSettings = captureDesktopFirstPartyRollback(config); }
|
|
1550
|
+
catch { return jsonResponse({ error: "Claude settings are unreadable", code: "unreadable" }, 500); }
|
|
1551
|
+
type FirstPartyMutation =
|
|
1552
|
+
| { refusal: { error: string; code: "intercept_disabled" | "intercept_unavailable" } }
|
|
1553
|
+
| { claudeCode: OcxConfig["claudeCode"]; previous: { present: boolean; value: boolean }; pinnedMode: "first-party" | "gateway" | undefined };
|
|
1554
|
+
let outcome: ReturnType<typeof mutatePersistedConfig<FirstPartyMutation>>;
|
|
1555
|
+
try {
|
|
1556
|
+
outcome = mutatePersistedConfig<FirstPartyMutation>(persisted => {
|
|
1557
|
+
if (body.cliFirstParty) {
|
|
1558
|
+
if (!claudeInterceptEnabled(persisted)) return { changed: false, value: {
|
|
1559
|
+
refusal: { error: "Claude intercept is disabled", code: "intercept_disabled" as const } } };
|
|
1560
|
+
const persistedPort = claudeInterceptProxyPort(persisted, persisted.port ?? 10100);
|
|
1561
|
+
if (bound === null || persistedPort !== bound.proxyPort) return { changed: false, value: {
|
|
1562
|
+
refusal: { error: `Claude intercept port mismatch (configured ${persistedPort}, bound ${bound?.proxyPort ?? "none"}); restart needed`,
|
|
1563
|
+
code: "intercept_unavailable" as const } } };
|
|
1564
|
+
}
|
|
1565
|
+
const before = structuredClone(persisted);
|
|
1566
|
+
const previous = { present: Object.hasOwn(persisted.claudeCode ?? {}, "cliFirstParty"),
|
|
1567
|
+
value: persisted.claudeCode?.cliFirstParty === true };
|
|
1568
|
+
const pinnedMode = body.cliFirstParty && persisted.claudeCode?.desktopMode === undefined
|
|
1569
|
+
? resolveClaudeDesktopMode(before, observeClaudeDesktopMode(before)) : undefined;
|
|
1570
|
+
const nextBlock = { ...(persisted.claudeCode ?? {}) };
|
|
1571
|
+
if (body.cliFirstParty) nextBlock.cliFirstParty = true;
|
|
1572
|
+
else delete nextBlock.cliFirstParty;
|
|
1573
|
+
if (pinnedMode) nextBlock.desktopMode = pinnedMode;
|
|
1574
|
+
commitClaudeCodeBlock(persisted, nextBlock);
|
|
1575
|
+
return { changed: true, value: { claudeCode: structuredClone(persisted.claudeCode), previous, pinnedMode } };
|
|
1576
|
+
});
|
|
1577
|
+
} catch { return jsonResponse({ error: "Could not save Claude settings", code: "write_failed" }, 500); }
|
|
1578
|
+
if (outcome.status === "unavailable") return jsonResponse({ error: "Could not save Claude settings", code: "write_failed" }, 500);
|
|
1579
|
+
const committed = outcome.value;
|
|
1580
|
+
if ("refusal" in committed) return jsonResponse(committed.refusal, 409);
|
|
1581
|
+
adoptPersistedClaudeCode(config, committed.claudeCode);
|
|
1582
|
+
const live = { ...(config.claudeCode ?? {}) };
|
|
1583
|
+
if (body.cliFirstParty) live.cliFirstParty = true; else delete live.cliFirstParty;
|
|
1584
|
+
if (committed.pinnedMode) live.desktopMode = committed.pinnedMode;
|
|
1585
|
+
config.claudeCode = live;
|
|
1586
|
+
let result: ReturnType<typeof reconcileClaudeFirstPartySettings> | undefined;
|
|
1587
|
+
try { result = (deps.reconcileClaudeFirstPartySettings ?? reconcileClaudeFirstPartySettings)(config,
|
|
1588
|
+
firstPartyDesired(config, observeClaudeDesktopMode(config))); }
|
|
1589
|
+
catch { /* An unexpected settings failure is mapped after conditional rollback. */ }
|
|
1590
|
+
if (!result?.ok) {
|
|
1591
|
+
let settingsRestored = true;
|
|
1592
|
+
if (body.cliFirstParty) {
|
|
1593
|
+
try { settingsRestored = restoreSettings?.() ?? false; } catch { settingsRestored = false; }
|
|
1594
|
+
let rollback: ReturnType<typeof mutatePersistedConfig<OcxConfig["claudeCode"]>>;
|
|
1595
|
+
try {
|
|
1596
|
+
rollback = mutatePersistedConfig(persisted => {
|
|
1597
|
+
const block = { ...(persisted.claudeCode ?? {}) };
|
|
1598
|
+
if (block.cliFirstParty === true) {
|
|
1599
|
+
if (committed.previous.present) block.cliFirstParty = committed.previous.value;
|
|
1600
|
+
else delete block.cliFirstParty;
|
|
1601
|
+
}
|
|
1602
|
+
if (committed.pinnedMode && block.desktopMode === committed.pinnedMode)
|
|
1603
|
+
delete block.desktopMode;
|
|
1604
|
+
persisted.claudeCode = block;
|
|
1605
|
+
return { changed: true, value: structuredClone(block) };
|
|
1606
|
+
});
|
|
1607
|
+
} catch { return jsonResponse({ error: "Claude settings rollback failed", code: "write_failed",
|
|
1608
|
+
warnings: ["settings_rollback_incomplete"] }, 500); }
|
|
1609
|
+
if (rollback.status === "unavailable") return jsonResponse({ error: "Claude settings rollback failed", code: "write_failed",
|
|
1610
|
+
warnings: ["settings_rollback_incomplete"] }, 500);
|
|
1611
|
+
adoptPersistedClaudeCode(config, rollback.value);
|
|
1612
|
+
}
|
|
1613
|
+
const code = result?.reason ?? "write_failed";
|
|
1614
|
+
return jsonResponse({ error: "Claude first-party reconciliation failed", code,
|
|
1615
|
+
...(!body.cliFirstParty && code === "unreadable"
|
|
1616
|
+
? { cliFirstParty: false, warnings: ["settings_residual"] } : {}),
|
|
1617
|
+
...(body.cliFirstParty && !settingsRestored
|
|
1618
|
+
? { warnings: ["settings_rollback_incomplete"] } : {}) },
|
|
1619
|
+
code === "intercept_disabled" || code === "foreign_env" ? 409 : 500);
|
|
1620
|
+
}
|
|
1621
|
+
const finalDesired = firstPartyDesired(config, observeClaudeDesktopMode(config));
|
|
1622
|
+
const residual = !finalDesired.desktop && !finalDesired.cli
|
|
1623
|
+
&& readFirstPartyProxyStatus(config, bound?.proxyPort ?? null) !== "none";
|
|
1624
|
+
return jsonResponse({ ok: true, enabled: config.claudeCode?.enabled !== false,
|
|
1625
|
+
cliFirstParty: body.cliFirstParty, warnings: residual ? ["settings_residual"] : [] });
|
|
1626
|
+
}
|
|
1509
1627
|
for (const field of ["webSearchSidecar", "visionSidecar"] as const) {
|
|
1510
1628
|
const section = body[field];
|
|
1511
1629
|
if (section === undefined || section === null) continue;
|
|
@@ -44,6 +44,10 @@ export interface ManagementRequestIngress {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export interface ManagementApiDeps {
|
|
47
|
+
/** Bound Claude intercept state, injectable for isolated management-route tests. */
|
|
48
|
+
getClaudeInterceptState?: typeof import("../../claude/intercept/runtime").getClaudeInterceptState;
|
|
49
|
+
/** Reconciliation seam for field-scoped rollback tests. */
|
|
50
|
+
reconcileClaudeFirstPartySettings?: typeof import("../../claude/first-party-settings").reconcileClaudeFirstPartySettings;
|
|
47
51
|
/** Read-only process-local aggregate metrics; absent keeps the scrape route unavailable. */
|
|
48
52
|
requestMetrics?: RequestMetricsSnapshotter;
|
|
49
53
|
checkPackageUpdate?: (channel: Channel) => Promise<UpdateCheckResult>;
|
|
@@ -21,7 +21,7 @@ import { commitClaudeCodeBlock } from "../../claude/claude-code-block";
|
|
|
21
21
|
*/
|
|
22
22
|
import { join } from "node:path";
|
|
23
23
|
import { existsSync } from "node:fs";
|
|
24
|
-
import { getConfigPath, loadConfig, mutatePersistedConfig, saveConfigPreservingClaudeCode } from "../../config";
|
|
24
|
+
import { adoptPersistedClaudeCode, getConfigPath, loadConfig, mutatePersistedConfig, saveConfigPreservingClaudeCode } from "../../config";
|
|
25
25
|
import { readRuntimePort } from "../../config/process-state";
|
|
26
26
|
import { desktopVisibleNativeSlugs, filterCatalogVisibleModels, nativeContextLimits } from "../../codex/catalog";
|
|
27
27
|
import { getCodexHome } from "../../codex/paths";
|
|
@@ -676,9 +676,16 @@ function pickerCleanupNote(picker: DesktopPickerStatus): string {
|
|
|
676
676
|
}
|
|
677
677
|
|
|
678
678
|
/** Record which Desktop mode is applied; `false` when the config file could not be updated. */
|
|
679
|
-
function persistDesktopModeMarker(desktopMode: ClaudeDesktopMode): boolean {
|
|
680
|
-
const outcome = mutatePersistedConfig(persisted =>
|
|
681
|
-
|
|
679
|
+
function persistDesktopModeMarker(config: ManagementContext["config"], desktopMode: ClaudeDesktopMode): boolean {
|
|
680
|
+
const outcome = mutatePersistedConfig(persisted => {
|
|
681
|
+
const result = recordClaudeDesktopMode(persisted, desktopMode);
|
|
682
|
+
return { changed: result.changed, value: structuredClone(persisted.claudeCode) };
|
|
683
|
+
});
|
|
684
|
+
if (outcome.status === "unavailable") return false;
|
|
685
|
+
adoptPersistedClaudeCode(config, outcome.value);
|
|
686
|
+
// Pin the committed leaf: an unarmed baseline can otherwise retain a stale live mode.
|
|
687
|
+
recordClaudeDesktopMode(config, desktopMode);
|
|
688
|
+
return true;
|
|
682
689
|
}
|
|
683
690
|
|
|
684
691
|
let claudeDesktopToggleFlight: Promise<Response> | null = null;
|
|
@@ -705,13 +712,15 @@ async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Respon
|
|
|
705
712
|
}
|
|
706
713
|
const desiredEnabled = loadConfig().clientIntegrations?.["claude-desktop"] !== false;
|
|
707
714
|
const current = loadConfig();
|
|
715
|
+
// Publish committed Desktop intent to the live config used by intercept routing.
|
|
716
|
+
ctx.config.clientIntegrations = structuredClone(current.clientIntegrations);
|
|
708
717
|
const fingerprint = current.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
|
|
709
718
|
|
|
710
719
|
if (!body.enabled) {
|
|
711
720
|
// Picker mode goes first: stop terminating claude.ai, drop its profile and trust.
|
|
712
721
|
return await runPickerTransition(current, async ops => {
|
|
713
722
|
const pickerOff = await ops.disableLocked({ persist: false });
|
|
714
|
-
const firstPartyRemoved = removeDesktopFirstParty();
|
|
723
|
+
const firstPartyRemoved = removeDesktopFirstParty(loadConfig());
|
|
715
724
|
if (!firstPartyRemoved.ok) {
|
|
716
725
|
return postCommitRefusal(409, "claude-desktop", "write_failed",
|
|
717
726
|
`Claude Code settings could not be read (${firstPartyRemoved.path}); the first-party proxy env was left in place.`, { desiredEnabled });
|
|
@@ -731,6 +740,7 @@ async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Respon
|
|
|
731
740
|
ok: true, clientId: "claude-desktop", changed, state: "absent", desiredEnabled,
|
|
732
741
|
message: [
|
|
733
742
|
changed ? "Claude Desktop integration disabled." : "Claude Desktop integration is already off.",
|
|
743
|
+
firstPartyRemoved.retainedFor === "cli" ? "Shared first-party settings remain for Claude Code CLI." : "",
|
|
734
744
|
pickerCleanupNote(pickerOff),
|
|
735
745
|
].filter(Boolean).join(" "),
|
|
736
746
|
} satisfies NativeToggleEnvelope);
|
|
@@ -755,7 +765,7 @@ async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Respon
|
|
|
755
765
|
return postCommitRefusal(500, "claude-desktop", "write_failed",
|
|
756
766
|
"Gateway cleanup and first-party settings rollback did not complete.", { desiredEnabled });
|
|
757
767
|
}
|
|
758
|
-
const partialModeWarning = !removed.ok && removed.changed && !persistDesktopModeMarker("first-party")
|
|
768
|
+
const partialModeWarning = !removed.ok && removed.changed && !persistDesktopModeMarker(ctx.config, "first-party")
|
|
759
769
|
? " First-party is active but its mode marker was not saved." : "";
|
|
760
770
|
if (removed.kind === "cleanup_incomplete") {
|
|
761
771
|
return postCommitRefusal(500, "claude-desktop", "cleanup_incomplete",
|
|
@@ -768,7 +778,7 @@ async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Respon
|
|
|
768
778
|
}
|
|
769
779
|
gatewayRemoved = removed.changed;
|
|
770
780
|
}
|
|
771
|
-
const modeSaved = persistDesktopModeMarker("first-party");
|
|
781
|
+
const modeSaved = persistDesktopModeMarker(ctx.config, "first-party");
|
|
772
782
|
const changed = applied.changed || gatewayRemoved;
|
|
773
783
|
// Picker mode is on by default in first-party; only a committed mode turns it on.
|
|
774
784
|
const picker = modeSaved && pickerPreferenceOn(loadConfig())
|
|
@@ -816,13 +826,14 @@ async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Respon
|
|
|
816
826
|
if (!result.written) return postCommitRefusal(500, "claude-desktop", "write_failed", "Claude Desktop apply failed.", { desiredEnabled: latestDesiredEnabled });
|
|
817
827
|
const committed = persistCommittedDesktopGateway(ctx.config, latest.claudeCode?.desktopProfile, result.fingerprint);
|
|
818
828
|
const stateWarning = committed.ok ? "" : " The committed gateway mode/profile state was not saved.";
|
|
819
|
-
const removed = removeDesktopFirstParty();
|
|
829
|
+
const removed = removeDesktopFirstParty(loadConfig());
|
|
820
830
|
if (!removed.ok) return postCommitRefusal(500, "claude-desktop", "write_failed", "Gateway applied, but first-party settings cleanup did not complete." + stateWarning, { desiredEnabled: latestDesiredEnabled });
|
|
821
831
|
return jsonResponse({
|
|
822
832
|
ok: true, clientId: "claude-desktop", changed: true, state: "current", desiredEnabled: latestDesiredEnabled,
|
|
823
833
|
message: [
|
|
824
834
|
"Claude Desktop integration enabled.",
|
|
825
835
|
stateWarning,
|
|
836
|
+
removed.retainedFor === "cli" ? "Shared first-party settings remain for Claude Code CLI." : "",
|
|
826
837
|
pickerCleanupNote(pickerOff),
|
|
827
838
|
].filter(Boolean).join(" "),
|
|
828
839
|
} satisfies NativeToggleEnvelope);
|
|
@@ -12,7 +12,7 @@ import { subagentFallbackNeedsModelEntitlements } from "../../codex/subagent-mod
|
|
|
12
12
|
import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
|
|
13
13
|
import type { RequestLogContext } from "../request-log";
|
|
14
14
|
import type { HandleResponsesOptions } from "./core-options";
|
|
15
|
-
import { prepareEffortNormalization } from "../effort-policy";
|
|
15
|
+
import { prepareEffortNormalization, stripEmptyLadderEffort, supportedLadderFor } from "../effort-policy";
|
|
16
16
|
import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
|
|
17
17
|
import { getOrAllocateRequestSessionLane } from "../request-log-conversation";
|
|
18
18
|
import { shouldPreparePlaintextV2AgentMessages } from "../../responses/plaintext-v2-agent-messages";
|
|
@@ -307,6 +307,18 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
307
307
|
logCtx.requestedEffort = `${logCtx.requestedEffort ?? "max"}->${clamped}`;
|
|
308
308
|
}
|
|
309
309
|
}
|
|
310
|
+
// Chat ingress cannot strip effort against a provisional policy pick. Apply the
|
|
311
|
+
// concrete target's restriction to BOTH adapter options and the raw wire copy;
|
|
312
|
+
// policy-fallback retains the original body before this attempt-local mutation.
|
|
313
|
+
if (inboundWire === "chat" && supportedLadderFor(route)?.length === 0) {
|
|
314
|
+
parsed.options.reasoning = undefined;
|
|
315
|
+
const raw = parsed._rawBody as { reasoning?: unknown } | undefined;
|
|
316
|
+
if (raw) {
|
|
317
|
+
const reasoning = stripEmptyLadderEffort(raw.reasoning, []);
|
|
318
|
+
if (reasoning === undefined) delete raw.reasoning;
|
|
319
|
+
else raw.reasoning = reasoning;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
310
322
|
recordAttemptRequestedEffort(logCtx);
|
|
311
323
|
logCtx.modelSupportsServiceTier = SERVICE_TIER_ADAPTERS.has(route.provider.adapter)
|
|
312
324
|
? modelServiceTierSupport
|
|
@@ -229,7 +229,10 @@ export async function prepareResponsesRequest(
|
|
|
229
229
|
// here instead, before comboIdFromRawBody reads `model`, and identify the combo by CONFIG
|
|
230
230
|
// LOOKUP so the check can never observe a one-candidate collapse.
|
|
231
231
|
let shadowCallIntercepted = false;
|
|
232
|
-
|
|
232
|
+
// A spawned sub-agent turn names its model on purpose; gpt-6-luna is both the helper
|
|
233
|
+
// slug and a default sub-agent model, so neither intercept site may rewrite that turn.
|
|
234
|
+
const threadSpawn = isThreadSpawnRequest(req.headers);
|
|
235
|
+
if (!options.comboAttempt && !options.compactionRoutingOverride && !threadSpawn && body && typeof body === "object" && !Array.isArray(body)) {
|
|
233
236
|
const shadowIntercept = config.shadowCallIntercept;
|
|
234
237
|
const rawShadowModel = (body as { model?: unknown }).model;
|
|
235
238
|
if (shadowIntercept?.enabled && shadowIntercept.model && typeof rawShadowModel === "string"
|
|
@@ -483,7 +486,7 @@ export async function prepareResponsesRequest(
|
|
|
483
486
|
: routeModel(config, modelId, evidenceFromBody(parsed._rawBody)));
|
|
484
487
|
const _sci = config.shadowCallIntercept;
|
|
485
488
|
let shadowRoute: RouteResult | undefined;
|
|
486
|
-
if (!options.compactionRoutingOverride && _sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
|
|
489
|
+
if (!options.compactionRoutingOverride && !threadSpawn && _sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
|
|
487
490
|
const sourcePrefix = shadowSourceModelPrefix(parsed.modelId, _sci.sourceModels)!;
|
|
488
491
|
let sourceIdentity = { providerName: OPENAI_CODEX_PROVIDER_ID, modelId: sourcePrefix };
|
|
489
492
|
try {
|
|
@@ -549,7 +552,6 @@ export async function prepareResponsesRequest(
|
|
|
549
552
|
// Exact account selectors are isolated from Pool-wide quota work. A canonical replay miss must
|
|
550
553
|
// also fail closed without polling quota upstream. Cached fallback state can still select a
|
|
551
554
|
// provider with native continuation support below.
|
|
552
|
-
const threadSpawn = isThreadSpawnRequest(req.headers);
|
|
553
555
|
const initialSubagentFallbackChain = threadSpawn && !options.comboAttempt
|
|
554
556
|
? resolveSubagentFallbackChain(parsed, config)
|
|
555
557
|
: null;
|
|
@@ -160,7 +160,12 @@ export async function executeResponsesSidecars(
|
|
|
160
160
|
retryAfter: string | null,
|
|
161
161
|
responseHeaders?: Headers,
|
|
162
162
|
retryParsed?: OcxParsedRequest,
|
|
163
|
-
): Promise<ProviderAdapter | null> => {
|
|
163
|
+
): Promise<{ adapter: ProviderAdapter; recoveryKind: AttemptRecoveryKind } | null> => {
|
|
164
|
+
// Which credential axis actually moved. The main routed path already reports these three
|
|
165
|
+
// separately (`adapter-dispatch`: key-429 / anthropic-oauth-429 / oauth-account-429); the
|
|
166
|
+
// sidecar loops used to flatten all three to `key-429`, so an account rotation read as a key
|
|
167
|
+
// rotation in the attempt row and in the Logs UI.
|
|
168
|
+
let recoveryKind: AttemptRecoveryKind = "key-429";
|
|
164
169
|
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
165
170
|
retryAfter,
|
|
166
171
|
now: Date.now(),
|
|
@@ -209,6 +214,7 @@ export async function executeResponsesSidecars(
|
|
|
209
214
|
hop.permit?.release();
|
|
210
215
|
return null;
|
|
211
216
|
}
|
|
217
|
+
recoveryKind = "oauth-account-429";
|
|
212
218
|
hop.permit?.use();
|
|
213
219
|
} else if (
|
|
214
220
|
// Anthropic's pool is excluded from generic failover, so without this arm a 429 inside a
|
|
@@ -252,6 +258,7 @@ export async function executeResponsesSidecars(
|
|
|
252
258
|
hop.permit?.release();
|
|
253
259
|
return null;
|
|
254
260
|
}
|
|
261
|
+
recoveryKind = "anthropic-oauth-429";
|
|
255
262
|
hop.permit?.use();
|
|
256
263
|
} else {
|
|
257
264
|
// No key pool, no generic OAuth roster, no Anthropic pool could produce a replacement
|
|
@@ -281,7 +288,7 @@ export async function executeResponsesSidecars(
|
|
|
281
288
|
provider: route.provider,
|
|
282
289
|
adapterName: rotatedAdapter.name,
|
|
283
290
|
});
|
|
284
|
-
return rotatedAdapter;
|
|
291
|
+
return { adapter: rotatedAdapter, recoveryKind };
|
|
285
292
|
};
|
|
286
293
|
if ((imgPlan || vidPlan) && wsPlan) {
|
|
287
294
|
// Web search takes priority when both are active — the media bridge cannot run
|
package/src/server/search.ts
CHANGED
|
@@ -38,8 +38,8 @@ import {
|
|
|
38
38
|
resolveFirstUsableOpenAiSidecar,
|
|
39
39
|
type ExactOpenAiSidecarAccount,
|
|
40
40
|
} from "../providers/openai-sidecar";
|
|
41
|
-
import { routeModel } from "../router";
|
|
42
|
-
import { handleAlphaSearchSidecarFallback } from "../web-search/alpha-search";
|
|
41
|
+
import { previewRouteModel, routeModel } from "../router";
|
|
42
|
+
import { handleAlphaSearchSidecarFallback, handleDevinAlphaSearch } from "../web-search/alpha-search";
|
|
43
43
|
import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "./request-decompress";
|
|
44
44
|
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
|
|
45
45
|
import type { RequestLogContext } from "./request-log";
|
|
@@ -65,11 +65,6 @@ export async function handleSearch(
|
|
|
65
65
|
turnAdmissionLease?: AdmissionLease,
|
|
66
66
|
admission?: DataPlaneAdmission,
|
|
67
67
|
): Promise<Response> {
|
|
68
|
-
try { validateForwardAdmissionCredential(req.headers, config); }
|
|
69
|
-
catch (err) {
|
|
70
|
-
if (err instanceof ForwardAdmissionCredentialError) return formatErrorResponse(401, "authentication_error", err.message);
|
|
71
|
-
throw err;
|
|
72
|
-
}
|
|
73
68
|
let body: unknown;
|
|
74
69
|
try {
|
|
75
70
|
body = await readJsonRequestBody(req, undefined, resolveInboundBodyLimitBytes(config.maxInboundBodyBytes));
|
|
@@ -79,6 +74,32 @@ export async function handleSearch(
|
|
|
79
74
|
const model = (body as { model?: unknown } | null)?.model;
|
|
80
75
|
if (typeof model === "string" && model) logCtx.model = model;
|
|
81
76
|
|
|
77
|
+
if (typeof model === "string" && model.trim()) {
|
|
78
|
+
try {
|
|
79
|
+
const route = previewRouteModel(config, model);
|
|
80
|
+
if (route.providerName === "devin" || route.staticPolicy.model.adapter === "devin") {
|
|
81
|
+
const denial = admissionScopeDenial(config, admission, model, route);
|
|
82
|
+
if (denial) return denial;
|
|
83
|
+
logCtx.provider = route.providerName;
|
|
84
|
+
logCtx.routeDecision = route.routeDecision;
|
|
85
|
+
return handleDevinAlphaSearch(
|
|
86
|
+
body,
|
|
87
|
+
"devin",
|
|
88
|
+
config.search?.timeoutMs ?? SEARCH_UPSTREAM_TIMEOUT_MS,
|
|
89
|
+
req.signal,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// Preview is advisory: existing relay/fallback still owns unsupported or unroutable models.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try { validateForwardAdmissionCredential(req.headers, config); }
|
|
98
|
+
catch (err) {
|
|
99
|
+
if (err instanceof ForwardAdmissionCredentialError) return formatErrorResponse(401, "authentication_error", err.message);
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
|
|
82
103
|
let exactAccount: ExactOpenAiSidecarAccount | undefined;
|
|
83
104
|
let relayBody = body;
|
|
84
105
|
const accountNamespace = typeof model === "string"
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* proxy event loop stays responsive.
|
|
7
7
|
*/
|
|
8
8
|
import type { CleanupMode, CleanupResult } from "./cleanup";
|
|
9
|
+
import { spawnWorker } from "../lib/worker-embed";
|
|
9
10
|
import { resolveCodexHomeDir } from "../codex/home";
|
|
10
11
|
import {
|
|
11
12
|
tryBeginStorageMutation,
|
|
@@ -310,7 +311,7 @@ function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number }): Prom
|
|
|
310
311
|
let settled = false;
|
|
311
312
|
let worker: Worker;
|
|
312
313
|
try {
|
|
313
|
-
worker =
|
|
314
|
+
worker = spawnWorker(new URL("./policy-worker.ts", import.meta.url).href, "policy-worker");
|
|
314
315
|
reservation.bind(worker);
|
|
315
316
|
} catch (error) {
|
|
316
317
|
reservation.release();
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* `storage_mutation_busy` (409) instead of queueing.
|
|
10
10
|
*/
|
|
11
11
|
import { resolveCodexHomeDir } from "../codex/home";
|
|
12
|
+
import { spawnWorker } from "../lib/worker-embed";
|
|
12
13
|
import { restoreTrashEntry, type RestoreResult, type RestoreTestHooks } from "./cleanup";
|
|
13
14
|
import {
|
|
14
15
|
resetStorageMutationCoordinatorForTests,
|
|
@@ -167,7 +168,7 @@ function runInWorker(opts: {
|
|
|
167
168
|
let settled = false;
|
|
168
169
|
let worker: Worker;
|
|
169
170
|
try {
|
|
170
|
-
worker =
|
|
171
|
+
worker = spawnWorker(new URL("./restore-worker.ts", import.meta.url).href, "restore-worker");
|
|
171
172
|
reservation.bind(worker);
|
|
172
173
|
} catch (error) {
|
|
173
174
|
reservation.release();
|
package/src/types/config.ts
CHANGED
|
@@ -37,6 +37,9 @@ export interface OcxProtocolsConfig {
|
|
|
37
37
|
* /v1/messages surface, the `ocx claude` launcher, and the GUI Claude page.
|
|
38
38
|
*/
|
|
39
39
|
export interface OcxClaudeCodeConfig {
|
|
40
|
+
/** Route the standalone Claude Code CLI through the first-party intercept (settings.json env).
|
|
41
|
+
* Independent of Desktop's first-party mode. Absent/false = off. */
|
|
42
|
+
cliFirstParty?: boolean;
|
|
40
43
|
/**
|
|
41
44
|
* Opt-in relocation of supported trailing Claude harness notices from system instructions
|
|
42
45
|
* to a user input message on translated routes. Changes the Desktop cache-key prefix.
|
|
@@ -737,19 +740,22 @@ export interface OcxConfig {
|
|
|
737
740
|
/**
|
|
738
741
|
* Shadow call intercept: redirect Codex's hard-coded helper calls (title generation,
|
|
739
742
|
* commit messages, skill orchestration) to a user-chosen model. Default intercepted
|
|
740
|
-
* source
|
|
741
|
-
* gpt-5.4-mini instead; that model is retired upstream,
|
|
742
|
-
* opt-in `sourceModels` prefix so an old client's helper
|
|
743
|
+
* source models: gpt-6-luna (Codex 0.154.0+) and gpt-5.6-luna (0.145.0-0.153.x).
|
|
744
|
+
* Clients through 0.144.x emitted gpt-5.4-mini instead; that model is retired upstream,
|
|
745
|
+
* but it stays available as an opt-in `sourceModels` prefix so an old client's helper
|
|
746
|
+
* calls can still be intercepted.
|
|
743
747
|
* Opt-in; disabled by default. Matching requests preserve their configured reasoning effort.
|
|
744
748
|
* All requests for configured shadow source models are intercepted regardless of request kind,
|
|
745
|
-
* except when the replacement intersects the same provider+model source set
|
|
749
|
+
* except when the replacement intersects the same provider+model source set, and except
|
|
750
|
+
* spawned sub-agent turns (x-openai-subagent: collab_spawn / subagent_kind thread_spawn),
|
|
751
|
+
* which keep the model they were spawned with.
|
|
746
752
|
*/
|
|
747
753
|
shadowCallIntercept?: {
|
|
748
754
|
/** When true, requests for known shadow/helper source models are rewritten to the configured model. */
|
|
749
755
|
enabled?: boolean;
|
|
750
756
|
/** Replacement model id (e.g. "gpt-5.5"). */
|
|
751
757
|
model?: string;
|
|
752
|
-
/** Optional override of intercepted source-model prefixes (default: gpt-5.6-luna). */
|
|
758
|
+
/** Optional override of intercepted source-model prefixes (default: gpt-6-luna, gpt-5.6-luna). */
|
|
753
759
|
sourceModels?: string[];
|
|
754
760
|
};
|
|
755
761
|
/**
|
|
@@ -189,8 +189,12 @@ function createOwnedStage(scopeDir, pkgName, deps = {}) {
|
|
|
189
189
|
pid: process.pid,
|
|
190
190
|
createdAt: (deps.now ?? Date.now)(),
|
|
191
191
|
}), { flag: "wx" });
|
|
192
|
+
// npm's strict script policy plans the global tree before it creates the prefix layout, so
|
|
193
|
+
// `-g --prefix` into a bare stage fails with ENOENT on <stage>/lib (#5760). POSIX global
|
|
194
|
+
// prefixes keep packages under lib/; Windows installs into the prefix itself.
|
|
195
|
+
if (process.platform !== "win32") mkdir(join(stageRoot, "lib"));
|
|
192
196
|
} catch (error) {
|
|
193
|
-
//
|
|
197
|
+
// Created by this call and not yet handed to npm: remove it rather than leave a partial stage.
|
|
194
198
|
try { rmSync(stageRoot, { recursive: true, force: true }); } catch { /* reported by the caller */ }
|
|
195
199
|
throw error;
|
|
196
200
|
}
|
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
* and pulling it in recreates the cycle sidecar-providers.ts exists to avoid.
|
|
13
13
|
*/
|
|
14
14
|
import { formatErrorResponse } from "../bridge";
|
|
15
|
+
import { signalWithTimeout } from "../lib/abort";
|
|
15
16
|
import { redactSecretString } from "../lib/redact";
|
|
16
17
|
import { sidecarEnter } from "../lib/sidecar-tracker";
|
|
17
18
|
import { admissionScopeDenial } from "../server/admission-model-scope";
|
|
18
19
|
import type { DataPlaneAdmission } from "../server/auth-cors";
|
|
19
20
|
import type { OcxConfig, OcxProviderConfig, OcxWebSearchSidecarConfig } from "../types";
|
|
20
21
|
import { runAnthropicWebSearch } from "./anthropic-executor";
|
|
22
|
+
import { resolveDevinWebSearchSnapshot, runDevinWebSearch } from "./devin-executor";
|
|
21
23
|
import { runExaWebSearch } from "./exa-executor";
|
|
22
24
|
import type { SidecarOutcome, SidecarSettings } from "./executor";
|
|
23
25
|
import { runGeminiWebSearch } from "./gemini-executor";
|
|
@@ -203,6 +205,75 @@ async function runAlphaSearchQuery(
|
|
|
203
205
|
}
|
|
204
206
|
}
|
|
205
207
|
|
|
208
|
+
function raceAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
209
|
+
if (signal.aborted) return Promise.reject(signal.reason);
|
|
210
|
+
return new Promise<T>((resolve, reject) => {
|
|
211
|
+
const onAbort = () => reject(signal.reason);
|
|
212
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
213
|
+
promise.then(
|
|
214
|
+
value => { signal.removeEventListener("abort", onAbort); resolve(value); },
|
|
215
|
+
error => { signal.removeEventListener("abort", onAbort); reject(error); },
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function handleDevinAlphaSearch(
|
|
221
|
+
body: unknown,
|
|
222
|
+
providerName: string,
|
|
223
|
+
timeoutMs: number,
|
|
224
|
+
signal?: AbortSignal,
|
|
225
|
+
): Promise<Response> {
|
|
226
|
+
const queries = extractAlphaSearchQueries(body);
|
|
227
|
+
if (queries.length === 0) {
|
|
228
|
+
return formatErrorResponse(
|
|
229
|
+
400,
|
|
230
|
+
"invalid_request_error",
|
|
231
|
+
"Built-in web search request is missing a usable query (commands.search_query, query, q, or search_query).",
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const deadline = signalWithTimeout(timeoutMs, signal);
|
|
235
|
+
try {
|
|
236
|
+
const resolved = await raceAbort(resolveDevinWebSearchSnapshot(providerName), deadline.signal);
|
|
237
|
+
if ("error" in resolved) {
|
|
238
|
+
return formatErrorResponse(502, "upstream_error", `devin web search failed: ${redactSecretString(resolved.error)}`);
|
|
239
|
+
}
|
|
240
|
+
const texts: string[] = [];
|
|
241
|
+
const sources: SidecarOutcome["sources"] = [];
|
|
242
|
+
for (const query of queries) {
|
|
243
|
+
const outcome = await runDevinWebSearch(query, resolved.snapshot, deadline.signal);
|
|
244
|
+
if (outcome.error) {
|
|
245
|
+
if (signal?.aborted) {
|
|
246
|
+
return formatErrorResponse(499, "client_closed_request", "search request canceled by client");
|
|
247
|
+
}
|
|
248
|
+
if (deadline.signal.aborted) {
|
|
249
|
+
return formatErrorResponse(504, "upstream_error", "devin web search timed out");
|
|
250
|
+
}
|
|
251
|
+
const detail = redactSecretString(outcome.error);
|
|
252
|
+
return formatErrorResponse(502, "upstream_error", `devin web search failed: ${detail}`);
|
|
253
|
+
}
|
|
254
|
+
texts.push(queries.length > 1 ? `Results for "${query}":\n${outcome.text}` : outcome.text);
|
|
255
|
+
for (const source of outcome.sources) {
|
|
256
|
+
if (!sources.some(existing => existing.url === source.url)) sources.push(source);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return new Response(JSON.stringify(formatAlphaSearchBody(texts.join("\n\n"), sources)), {
|
|
260
|
+
status: 200,
|
|
261
|
+
headers: { "content-type": "application/json" },
|
|
262
|
+
});
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (signal?.aborted) {
|
|
265
|
+
return formatErrorResponse(499, "client_closed_request", "search request canceled by client");
|
|
266
|
+
}
|
|
267
|
+
if (deadline.signal.aborted) {
|
|
268
|
+
return formatErrorResponse(504, "upstream_error", "devin web search timed out");
|
|
269
|
+
}
|
|
270
|
+
const detail = redactSecretString(error instanceof Error ? error.message : String(error));
|
|
271
|
+
return formatErrorResponse(502, "upstream_error", `devin web search failed: ${detail}`);
|
|
272
|
+
} finally {
|
|
273
|
+
deadline.cleanup();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
206
277
|
function formatAlphaSearchBody(text: string, sources: SidecarOutcome["sources"]): {
|
|
207
278
|
encrypted_output: null;
|
|
208
279
|
output: string;
|