@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.
Files changed (60) hide show
  1. package/bin/ocx.mjs +24 -11
  2. package/gui/dist/assets/{App-BVjA0T6O.js → App-B3VRRYSj.js} +15 -15
  3. package/gui/dist/assets/{App-gkoJIEOV.css → App-BJsT8Icc.css} +1 -1
  4. package/gui/dist/assets/{Tray-BCFtiWwD.js → Tray-TYAA0EQY.js} +1 -1
  5. package/gui/dist/assets/index-9Ju7nHsu.js +86 -0
  6. package/gui/dist/assets/index-DtNmX7hW.css +1 -0
  7. package/gui/dist/assets/{tray-data-C4FiNaxk.js → tray-data-xKfS2gSG.js} +1 -1
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -1
  10. package/src/chat/inbound.ts +82 -3
  11. package/src/chat/outbound.ts +5 -3
  12. package/src/claude/desktop-first-party.ts +12 -3
  13. package/src/claude/first-party-settings.ts +97 -0
  14. package/src/claude/intercept/client-class.ts +18 -0
  15. package/src/claude/intercept/listener.ts +5 -0
  16. package/src/claude/intercept/runtime.ts +6 -0
  17. package/src/cli/capabilities.ts +12 -0
  18. package/src/cli/claude-desktop.ts +11 -6
  19. package/src/cli/claude.ts +25 -1
  20. package/src/cli/ensure-desired-integrations.ts +14 -1
  21. package/src/cli/integrations.ts +9 -1
  22. package/src/cli/uninstall-client-state.ts +6 -0
  23. package/src/cli/uninstall-integrations.ts +115 -0
  24. package/src/codex/history-job.ts +2 -1
  25. package/src/codex/main-account-hard-lock.ts +36 -19
  26. package/src/codex/quota.ts +11 -1
  27. package/src/combos/resolve.ts +11 -3
  28. package/src/config/load-degrade.ts +4 -0
  29. package/src/generated/compatibility-version.json +67 -43
  30. package/src/generated/worker-bundles.gen.ts +4 -0
  31. package/src/images/loop.ts +10 -3
  32. package/src/integrations/aside-profile-context.ts +17 -0
  33. package/src/lab/fabric/executor.ts +25 -3
  34. package/src/lab/fabric/producer-isolate.ts +199 -32
  35. package/src/lab/fabric/scratch.ts +9 -2
  36. package/src/lib/shadow-call.ts +11 -4
  37. package/src/lib/worker-embed.ts +33 -0
  38. package/src/oauth/generic-account-failover.ts +5 -3
  39. package/src/oauth/index.ts +19 -29
  40. package/src/oauth/store.ts +106 -34
  41. package/src/remote-control/workspace-rpc.ts +114 -19
  42. package/src/router.ts +7 -1
  43. package/src/server/chat-completions.ts +2 -1
  44. package/src/server/index/claude-intercept-lifecycle.ts +13 -0
  45. package/src/server/management/agent-settings-routes.ts +123 -5
  46. package/src/server/management/context.ts +4 -0
  47. package/src/server/management/native-integration-routes.ts +19 -8
  48. package/src/server/responses/core-normalize.ts +13 -1
  49. package/src/server/responses/request-prepare.ts +5 -3
  50. package/src/server/responses/sidecar-execution.ts +9 -2
  51. package/src/server/search.ts +28 -7
  52. package/src/storage/policy-job.ts +2 -1
  53. package/src/storage/restore-job.ts +2 -1
  54. package/src/types/config.ts +11 -5
  55. package/src/update/transactional-install.mjs +5 -1
  56. package/src/web-search/alpha-search.ts +71 -0
  57. package/src/web-search/devin-executor.ts +192 -0
  58. package/src/web-search/loop.ts +10 -3
  59. package/gui/dist/assets/index-ComVsdSk.css +0 -1
  60. package/gui/dist/assets/index-vO4dgfiU.js +0 -86
@@ -20,7 +20,8 @@ const CLAUDE_USAGE = `Usage:
20
20
  [--compact-window <tokens|default>] [--inject-agents <on|off>]
21
21
  [--small-fast-model <id|->] [--model-map <from=to,from=to|->]
22
22
  [--blocked-skills <name,name|->] [--web-model <id|->] [--web-backend <openai|anthropic|xai|gemini|exa|->]
23
- [--vision-model <id|->] [--vision-backend <openai|anthropic|->] [--json]`;
23
+ [--vision-model <id|->] [--vision-backend <openai|anthropic|->] [--json]
24
+ ocx claude config set --first-party <on|off> [--json]`;
24
25
 
25
26
  const GROK_USAGE = `Usage:
26
27
  ocx grok [status] [--json]
@@ -74,6 +75,7 @@ export async function handleClaudeConfigCommand(argv: string[], deps: RuntimeApi
74
75
  if (action !== "set") throw new CliUsageError(`unknown Claude config command ${action}`, CLAUDE_USAGE);
75
76
  const body: Record<string, unknown> = {};
76
77
  const enabled = takeBooleanOption(args, "--enabled");
78
+ const firstParty = takeBooleanOption(args, "--first-party");
77
79
  const authMode = takeOption(args, "--auth-mode");
78
80
  const systemEnv = takeBooleanOption(args, "--system-env");
79
81
  const fastMode = takeBooleanOption(args, "--fast-mode");
@@ -116,6 +118,12 @@ export async function handleClaudeConfigCommand(argv: string[], deps: RuntimeApi
116
118
  const vision = sidecar(visionModel, visionBackend);
117
119
  if (web) body.webSearchSidecar = web;
118
120
  if (vision) body.visionSidecar = vision;
121
+ if (firstParty !== undefined) {
122
+ if (Object.keys(body).length > 0) {
123
+ throw new CliUsageError("--first-party must be set on its own (it writes Claude Code's settings file immediately)", CLAUDE_USAGE);
124
+ }
125
+ body.cliFirstParty = firstParty;
126
+ }
119
127
  if (Object.keys(body).length === 0) throw new CliUsageError("at least one Claude setting is required", CLAUDE_USAGE);
120
128
  const result = await runtimeRequest("/api/claude-code", { method: "PUT", body: JSON.stringify(body) }, deps);
121
129
  printData(result, wantsJson, ["Claude Code settings updated."]);
@@ -6,6 +6,7 @@ import { inspectRemoteDesktopCleanup, readDesktopDisconnectReceipt } from "../cl
6
6
  import { removeOwnedConfigState, type ConfigRemovalResult } from "../lib/config-ownership";
7
7
  import { windowsSecretAclReapPendingAtOrBelow } from "../lib/windows-secret-acl";
8
8
  import { sharedTeardownAuthorized, type UninstallObservation } from "./uninstall-plan";
9
+ import { cleanupOwnedIntegrationsBeforeUninstall } from "./uninstall-integrations";
9
10
 
10
11
  export interface UninstallClientStateDeps {
11
12
  readConnection: typeof readClientConnectionState;
@@ -13,6 +14,7 @@ export interface UninstallClientStateDeps {
13
14
  readReceipt: typeof readDesktopDisconnectReceipt;
14
15
  disconnect: (options?: Parameters<typeof disconnectClient>[0]) => Promise<unknown>;
15
16
  withLifecycle: typeof withClientLifecycle;
17
+ cleanupIntegrations: typeof cleanupOwnedIntegrationsBeforeUninstall;
16
18
  remove: () => ConfigRemovalResult;
17
19
  /** True while a timed-out icacls child still owns a path at or below the config directory. */
18
20
  aclReapPending: (rootPath: string) => boolean;
@@ -24,6 +26,7 @@ const defaults: UninstallClientStateDeps = {
24
26
  readReceipt: readDesktopDisconnectReceipt,
25
27
  disconnect: options => disconnectClient(options),
26
28
  withLifecycle: withClientLifecycle,
29
+ cleanupIntegrations: cleanupOwnedIntegrationsBeforeUninstall,
27
30
  remove: () => removeOwnedConfigState(getConfigDir()),
28
31
  aclReapPending: rootPath => windowsSecretAclReapPendingAtOrBelow(rootPath),
29
32
  };
@@ -85,6 +88,9 @@ export async function removeOwnedConfigAfterDesktopCleanup(
85
88
  if (deps.aclReapPending(getConfigDir())) {
86
89
  throw new Error("Client cleanup refused: ACL hardening still owns a path under the config directory.");
87
90
  }
91
+ // Integration records are the authority that permits removing only OpenCodex-owned fragments
92
+ // from third-party files. Delete them only after every recorded contribution is retired.
93
+ await deps.cleanupIntegrations();
88
94
  return deps.remove();
89
95
  });
90
96
  }
@@ -0,0 +1,115 @@
1
+ import type { ExportModel } from "../clients/config-export";
2
+ import { guardAsideProfileIO, listAsideProfiles } from "../clients/aside-profiles";
3
+ import { loadConfig } from "../config";
4
+ import { listAsideProfileStores } from "../integrations/aside-profile-context";
5
+ import { isIntegrationClientId, type IntegrationClientId } from "../integrations/registry";
6
+ import { createIntegrationStateStore, type IntegrationStateStore } from "../integrations/store";
7
+ import { disableIntegrationCoordinated, type WriteOutcome } from "../integrations/writer";
8
+ import { loadExportModels } from "../server/management/model-rows";
9
+ import type { OcxConfig } from "../types";
10
+
11
+ export interface UninstallIntegrationCleanupDeps {
12
+ createStore: () => IntegrationStateStore;
13
+ loadConfig: () => OcxConfig;
14
+ loadModels: (config: OcxConfig) => Promise<ExportModel[]>;
15
+ disable: (input: Parameters<typeof disableIntegrationCoordinated>[0]) => Promise<WriteOutcome>;
16
+ env?: NodeJS.ProcessEnv;
17
+ home?: string;
18
+ }
19
+
20
+ const defaults: UninstallIntegrationCleanupDeps = {
21
+ createStore: () => createIntegrationStateStore(),
22
+ loadConfig,
23
+ loadModels: config => loadExportModels(config),
24
+ disable: input => disableIntegrationCoordinated(input),
25
+ };
26
+
27
+ export interface UninstallIntegrationCleanupResult {
28
+ attempted: number;
29
+ changed: number;
30
+ }
31
+
32
+ /**
33
+ * Remove every contribution we can still prove we own before uninstall deletes that proof.
34
+ * A single refusal aborts config removal: preserving recovery state is safer than leaving an
35
+ * external client pointed at a proxy that no longer exists.
36
+ */
37
+ export async function cleanupOwnedIntegrationsBeforeUninstall(
38
+ deps: UninstallIntegrationCleanupDeps = defaults,
39
+ ): Promise<UninstallIntegrationCleanupResult> {
40
+ const store = deps.createStore();
41
+ const records = store.readRecordsStrict();
42
+ const rawIds = Object.keys(records);
43
+ for (const id of rawIds) {
44
+ if (!isIntegrationClientId(id)) {
45
+ throw new Error(`integration cleanup refused: ownership names unknown client ${id}`);
46
+ }
47
+ }
48
+ type CleanupTarget = Pick<Parameters<typeof disableIntegrationCoordinated>[0], "clientId" | "store" | "io" | "resolvedPaths">
49
+ & { store: IntegrationStateStore; profileId?: number };
50
+ const targets: CleanupTarget[] = (rawIds as IntegrationClientId[]).sort()
51
+ .map(clientId => ({ clientId, store }));
52
+ // Read every child before any mutation. A broken child must not look like no ownership.
53
+ for (const child of listAsideProfileStores(store)) {
54
+ const childRecords = child.store.readRecordsStrict();
55
+ if (Object.keys(childRecords).some(id => id !== "aside")) {
56
+ throw new Error("integration cleanup refused: Aside profile storage names another client");
57
+ }
58
+ if (childRecords.aside) targets.push({ clientId: "aside", ...child });
59
+ }
60
+ if (targets.length === 0) return { attempted: 0, changed: 0 };
61
+
62
+ const asideTargets = targets.filter(target => target.clientId === "aside");
63
+ if (asideTargets.length > 0) {
64
+ const profiles = listAsideProfiles(deps.env, deps.home);
65
+ const claimedProfiles = new Set<number>();
66
+ for (const target of asideTargets) {
67
+ const record = target.store.readRecordsStrict().aside;
68
+ const profile = profiles.find(candidate => candidate.configPath === record?.configPath
69
+ && (target.profileId === undefined || target.profileId === candidate.id));
70
+ if (!profile || claimedProfiles.has(profile.id)) {
71
+ throw new Error("integration cleanup refused: Aside profile ownership is missing or mismatched");
72
+ }
73
+ claimedProfiles.add(profile.id);
74
+ target.resolvedPaths = { configPath: profile.configPath, detectDir: profile.detectDir };
75
+ // io() closes over the raw store; route bookkeeping back through its guarded facade.
76
+ target.io = guardAsideProfileIO(profile, {
77
+ ...target.store.io(),
78
+ appendJournal: entry => target.store.appendJournal(entry),
79
+ putRecord: record => target.store.putRecord(record),
80
+ dropRecord: clientId => target.store.dropRecord(clientId),
81
+ }, profiles);
82
+ }
83
+ }
84
+
85
+ const config = deps.loadConfig();
86
+ const models = await deps.loadModels(config);
87
+ let changed = 0;
88
+ for (const { clientId, store: targetStore, io, resolvedPaths } of targets) {
89
+ const result = await deps.disable({
90
+ clientId,
91
+ models,
92
+ config,
93
+ port: config.port,
94
+ store: targetStore,
95
+ ...(io ? { io } : {}),
96
+ ...(resolvedPaths ? { resolvedPaths } : {}),
97
+ ...(deps.env ? { env: deps.env } : {}),
98
+ ...(deps.home ? { home: deps.home } : {}),
99
+ });
100
+ if (!result.ok) {
101
+ throw new Error(`integration cleanup refused for ${clientId}: ${result.message}`
102
+ + (result.residual ? "; recovery did not complete; inspect the client file and retained snapshots before retrying" : ""));
103
+ }
104
+ if (targetStore.readRecordsStrict()[clientId]) {
105
+ throw new Error(`integration cleanup did not retire ownership for ${clientId}`);
106
+ }
107
+ if (result.changed) changed++;
108
+ }
109
+ // A profile can gain ownership while catalog loading or an earlier disable awaited its lock.
110
+ if ([store, ...listAsideProfileStores(store).map(child => child.store)]
111
+ .some(current => Object.keys(current.readRecordsStrict()).length > 0)) {
112
+ throw new Error("integration cleanup refused: ownership changed before removal");
113
+ }
114
+ return { attempted: targets.length, changed };
115
+ }
@@ -24,6 +24,7 @@ import type {
24
24
  HistoryWorkerResult,
25
25
  } from "./history-worker";
26
26
  import { currentHistoryDbBusyTimeoutMs, resolveExistingHistoryBackupPath } from "./history-provider";
27
+ import { spawnWorker } from "../lib/worker-embed";
27
28
  import type { CodexHistoryFailureReason, CodexHistoryVerifiedNoopProof } from "./history-provider";
28
29
  import { getCodexHome, resolveCodexStateDbPath } from "./paths";
29
30
 
@@ -345,7 +346,7 @@ export async function runCodexHistoryJob(
345
346
 
346
347
  let worker: Worker;
347
348
  try {
348
- worker = new Worker(new URL("./history-worker.ts", import.meta.url).href);
349
+ worker = spawnWorker(new URL("./history-worker.ts", import.meta.url).href, "history-worker");
349
350
  } catch (error) {
350
351
  return {
351
352
  kind: "failed",
@@ -31,6 +31,30 @@ function resetTimestamp(value: number | undefined): number | undefined {
31
31
  return value < 10_000_000_000 ? value * 1000 : value;
32
32
  }
33
33
 
34
+ type WindowReading = { percent: number | undefined; resetAt: number | undefined };
35
+
36
+ /**
37
+ * The windows that govern the lock. The 5h and weekly windows each govern on their own, so either
38
+ * one at 98% blocks even while the other has headroom. Monthly governs only a monthly-only account:
39
+ * supplementary monthly data on a 5h/weekly account never becomes a lock. The list is never empty,
40
+ * so a record with no readings at all classifies as unknown rather than vacuously ready.
41
+ */
42
+ function governingWindows(quota: NonNullable<ReturnType<typeof getMainPolicyQuota>>): WindowReading[] {
43
+ const hasShort = quota.shortPercent !== undefined || quota.shortResetAt !== undefined
44
+ || quota.shortWindowSeconds !== undefined;
45
+ const hasWeekly = quota.weeklyPercent !== undefined || quota.weeklyResetAt !== undefined;
46
+ const windows: WindowReading[] = [];
47
+ if (hasShort) windows.push({ percent: quota.shortPercent, resetAt: quota.shortResetAt });
48
+ if (hasWeekly) windows.push({ percent: quota.weeklyPercent, resetAt: quota.weeklyResetAt });
49
+ if (windows.length === 0) windows.push({ percent: quota.monthlyPercent, resetAt: quota.monthlyResetAt });
50
+ return windows;
51
+ }
52
+
53
+ function validPercent(percent: number | undefined): percent is number {
54
+ // The routing score's unknown sentinel is 101. It is never a raw quota observation.
55
+ return typeof percent === "number" && Number.isFinite(percent) && percent >= 0 && percent <= 100;
56
+ }
57
+
34
58
  /** Observed admission policy, not a reservation of the account's remaining quota. */
35
59
  export function getMainAccountHardLockStatus(
36
60
  config: PolicyConfig,
@@ -39,26 +63,19 @@ export function getMainAccountHardLockStatus(
39
63
  if (!isMainAccountHardLockEnabled(config)) return { enabled: false, state: "off" };
40
64
  const quota = getMainPolicyQuota();
41
65
  if (!quota) return { enabled: true, state: "unknown" };
42
- // Account window priority is deliberate: a 5h account uses that window, even if
43
- // its weekly bar is higher. An unknown/expired selected window does not change the choice.
44
- const hasShort = quota.shortPercent !== undefined || quota.shortResetAt !== undefined
45
- || quota.shortWindowSeconds !== undefined;
46
- const hasWeekly = quota.weeklyPercent !== undefined || quota.weeklyResetAt !== undefined;
47
- const [percent, rawReset] = hasShort
48
- ? [quota.shortPercent, quota.shortResetAt]
49
- : hasWeekly ? [quota.weeklyPercent, quota.weeklyResetAt] : [quota.monthlyPercent, quota.monthlyResetAt];
50
- const resetAt = resetTimestamp(rawReset);
51
- // The routing score's unknown sentinel is 101. It is never a raw quota observation.
52
- if (typeof percent !== "number" || !Number.isFinite(percent) || percent < 0 || percent > 100) {
53
- return { enabled: true, state: "unknown" };
66
+ const windows = governingWindows(quota);
67
+ const blocking = windows.filter(w => validPercent(w.percent) && w.percent >= MAIN_ACCOUNT_HARD_LOCK_PERCENT);
68
+ if (blocking.length > 0) {
69
+ // The lock holds until every blocking window reads lower, so the earliest possible unlock is
70
+ // the latest blocking reset. One blocking window without a future reset makes it unknowable.
71
+ // A predicted reset is not evidence of recovery either way: only a fresh lower reading releases.
72
+ const resets = blocking.map(w => resetTimestamp(w.resetAt));
73
+ const resetAt = resets.every(r => r !== undefined && r > now) ? Math.max(...(resets as number[])) : undefined;
74
+ return { enabled: true, state: "blocked", ...(resetAt !== undefined ? { resetAt } : {}) };
54
75
  }
55
- if (percent < MAIN_ACCOUNT_HARD_LOCK_PERCENT) return { enabled: true, state: "ready" };
56
- return {
57
- enabled: true,
58
- state: "blocked",
59
- // A predicted reset is not evidence of recovery. Only a fresh lower reading releases.
60
- ...(resetAt !== undefined && resetAt > now ? { resetAt } : {}),
61
- };
76
+ // Unknown admits, so an unreadable window never hides a blocking one and never blocks alone.
77
+ if (windows.some(w => !validPercent(w.percent))) return { enabled: true, state: "unknown" };
78
+ return { enabled: true, state: "ready" };
62
79
  }
63
80
 
64
81
  export function isMainAccountHardLocked(config: PolicyConfig, now = Date.now()): boolean {
@@ -354,7 +354,17 @@ function mergeAccountQuota(
354
354
  return stampCodexQuotaUsageObservation(next, quota, existing);
355
355
  }
356
356
 
357
- if (snapshotHasWeekly(quota)) {
357
+ const existingWeeklyPercent = existing?.weeklyPercent;
358
+ // A reset-only weekly observation is not a lower reading. Policy mode keeps a blocking weekly
359
+ // tuple whole, exactly like preserveKnownShort; a governing monthly-primary observation still
360
+ // replaces it, so that release path does not depend on this guard.
361
+ const preserveKnownWeekly = policyEvidence
362
+ && quota.weeklyPercent === undefined
363
+ && quota.monthlyIsPrimaryWindow !== true
364
+ && finitePercent(existingWeeklyPercent)
365
+ && existingWeeklyPercent >= MAIN_ACCOUNT_HARD_LOCK_PERCENT
366
+ && existingWeeklyPercent <= 100;
367
+ if (snapshotHasWeekly(quota) && !preserveKnownWeekly) {
358
368
  if (quota.weeklyPercent !== undefined) next.weeklyPercent = quota.weeklyPercent;
359
369
  if (quota.weeklyResetAt !== undefined) next.weeklyResetAt = quota.weeklyResetAt;
360
370
  } else if (snapshotHasMonthly(quota)
@@ -204,6 +204,8 @@ export function pickComboTarget(
204
204
  exclude?: Iterable<string>;
205
205
  eligible?: (target: NormalizedComboTarget) => boolean;
206
206
  now?: number;
207
+ /** Inspect a round-robin choice without mutating its sticky/weight state. */
208
+ preview?: boolean;
207
209
  } = {},
208
210
  ): ComboPick | null {
209
211
  const writerGeneration = captureConfigGeneration();
@@ -222,7 +224,13 @@ export function pickComboTarget(
222
224
  let state = selectionState.get(comboId);
223
225
  if (!state) {
224
226
  state = { successes: 0, currentWeights: new Map(), successfulUses: new Map() };
225
- selectionState.set(comboId, state);
227
+ if (!options.preview) selectionState.set(comboId, state);
228
+ } else if (options.preview) {
229
+ state = {
230
+ ...state,
231
+ currentWeights: new Map(state.currentWeights),
232
+ successfulUses: new Map(state.successfulUses),
233
+ };
226
234
  }
227
235
  if (state.activeKey) {
228
236
  targetIndex = combo.targets.findIndex(target => targetKey(target) === state.activeKey && eligible(target));
@@ -542,11 +550,11 @@ export function clearComboSelectionState(comboId?: string): void {
542
550
  selectionState.delete(comboId);
543
551
  }
544
552
 
545
- export function tryPickComboModel(config: OcxConfig, modelId: string): ComboPick | null {
553
+ export function tryPickComboModel(config: OcxConfig, modelId: string, preview = false): ComboPick | null {
546
554
  const comboId = resolveComboId(config, modelId);
547
555
  if (!comboId) return null;
548
556
  if (!getCombo(config, comboId)) throw new UnknownComboError(comboId);
549
- const picked = pickComboTarget(config, comboId);
557
+ const picked = pickComboTarget(config, comboId, { preview });
550
558
  if (!picked) throw new NoAvailableComboTargetsError(comboId);
551
559
  return picked;
552
560
  }
@@ -544,6 +544,10 @@ export function normalizePersistedClaudeCode(claudeCode: unknown): OcxConfig["cl
544
544
  return claudeCode as OcxConfig["claudeCode"];
545
545
  }
546
546
  const normalized = { ...claudeCode } as Record<string, unknown>;
547
+ // A malformed hand edit must not arm CLI interception or discard the whole config.
548
+ if (Object.hasOwn(normalized, "cliFirstParty") && typeof normalized.cliFirstParty !== "boolean") {
549
+ delete normalized.cliFirstParty;
550
+ }
547
551
  if (Object.hasOwn(normalized, "subagentEffort") && !isClaudeSubagentEffort(normalized.subagentEffort)) {
548
552
  delete normalized.subagentEffort;
549
553
  }