@bitkyc08/opencodex 2.10.2 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +31 -0
  2. package/bin/ocx.mjs +10 -0
  3. package/gui/dist/assets/index-Bk-PN-70.css +1 -0
  4. package/gui/dist/assets/index-BynIEIV-.js +70 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +4 -2
  7. package/src/adapters/cursor/effort-map.ts +11 -0
  8. package/src/adapters/cursor/live-transport.ts +11 -0
  9. package/src/adapters/cursor/native-exec-fs.ts +9 -6
  10. package/src/adapters/cursor/native-exec.ts +4 -2
  11. package/src/adapters/cursor/protobuf-events.ts +176 -4
  12. package/src/adapters/cursor/request-builder.ts +15 -4
  13. package/src/adapters/cursor/tool-definitions.ts +118 -2
  14. package/src/adapters/google.ts +15 -5
  15. package/src/adapters/openai-chat.ts +24 -2
  16. package/src/adapters/openai-responses.ts +2 -1
  17. package/src/bridge.ts +9 -5
  18. package/src/chat/outbound.ts +4 -3
  19. package/src/claude/desktop-3p.ts +222 -2
  20. package/src/claude/outbound.ts +15 -6
  21. package/src/cli/account-api.ts +4 -0
  22. package/src/cli/account-extended.ts +112 -0
  23. package/src/cli/account.ts +23 -6
  24. package/src/cli/claude-desktop.ts +26 -3
  25. package/src/cli/config-command.ts +9 -0
  26. package/src/cli/help.ts +18 -2
  27. package/src/cli/index.ts +277 -55
  28. package/src/cli/models.ts +5 -1
  29. package/src/cli/provider.ts +8 -2
  30. package/src/cli/ready.ts +301 -0
  31. package/src/cli/system-restart-client.ts +146 -0
  32. package/src/cli/tray-proxy.ts +153 -6
  33. package/src/clients/config-export.ts +12 -19
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-namespaces.ts +49 -3
  36. package/src/codex/account-priority.ts +83 -0
  37. package/src/codex/auth-api.ts +83 -0
  38. package/src/codex/auth-context.ts +5 -2
  39. package/src/codex/catalog/provider-fetch.ts +11 -0
  40. package/src/codex/catalog/sync.ts +23 -1
  41. package/src/codex/codex-write-lock.ts +16 -4
  42. package/src/codex/desired-state.ts +37 -4
  43. package/src/codex/history-job.ts +15 -5
  44. package/src/codex/history-provider.ts +31 -14
  45. package/src/codex/history-worker.ts +28 -4
  46. package/src/codex/inject-coordination.ts +13 -1
  47. package/src/codex/inject.ts +360 -66
  48. package/src/codex/internal/history-writer.ts +1 -1
  49. package/src/codex/native-main-lock-file.ts +5 -1
  50. package/src/codex/native-main-owner.ts +17 -3
  51. package/src/codex/native-profile-manager.ts +19 -0
  52. package/src/codex/native-profile-startup.ts +8 -0
  53. package/src/codex/native-residue.ts +140 -27
  54. package/src/codex/pool-rotation.ts +74 -4
  55. package/src/codex/refresh.ts +7 -0
  56. package/src/codex/routing.ts +177 -36
  57. package/src/codex/subagent-model-fallback.ts +34 -4
  58. package/src/codex/sync.ts +61 -0
  59. package/src/codex/upstream-host-health.ts +329 -31
  60. package/src/combos/request.ts +2 -0
  61. package/src/config.ts +221 -2
  62. package/src/images/loop.ts +1 -1
  63. package/src/integrations/native/ownership-preflight.ts +39 -2
  64. package/src/lib/bun-stream-caps.ts +3 -3
  65. package/src/lib/sse-decoder.ts +41 -0
  66. package/src/lib/system-restart-contract.ts +73 -0
  67. package/src/lib/windows-secret-acl.ts +141 -39
  68. package/src/lib/windows-user-principal.ts +283 -0
  69. package/src/lib/winsw.ts +18 -2
  70. package/src/oauth/key-providers.ts +12 -0
  71. package/src/providers/derive.ts +54 -2
  72. package/src/providers/free-directory.ts +6 -5
  73. package/src/providers/model-discovery.ts +9 -3
  74. package/src/providers/quota.ts +592 -0
  75. package/src/providers/registry.ts +316 -13
  76. package/src/responses/parser.ts +26 -10
  77. package/src/responses/reasoning-replay-cache.ts +1 -0
  78. package/src/routing/profile-namespace.ts +15 -0
  79. package/src/routing/profile.ts +2 -1
  80. package/src/server/auth-cors.ts +44 -13
  81. package/src/server/chat-completions.ts +0 -4
  82. package/src/server/claude-messages.ts +73 -15
  83. package/src/server/github-copilot-responses-repair.ts +338 -0
  84. package/src/server/index.ts +328 -111
  85. package/src/server/lifecycle.ts +36 -0
  86. package/src/server/management/agent-settings-routes.ts +147 -56
  87. package/src/server/management/config-routes.ts +7 -2
  88. package/src/server/management/context.ts +4 -0
  89. package/src/server/management/native-integration-routes.ts +199 -20
  90. package/src/server/management/provider-routes.ts +41 -0
  91. package/src/server/management/routing-profile-routes.ts +234 -5
  92. package/src/server/management/system-restart.ts +12 -10
  93. package/src/server/management/system-routes.ts +20 -0
  94. package/src/server/management-auth.ts +51 -3
  95. package/src/server/ports.ts +41 -1
  96. package/src/server/proxy-liveness.ts +129 -4
  97. package/src/server/readiness.ts +99 -0
  98. package/src/server/relay.ts +113 -97
  99. package/src/server/request-log.ts +10 -4
  100. package/src/server/responses/compact.ts +107 -12
  101. package/src/server/responses/core.ts +220 -39
  102. package/src/server/responses-item-id-repair.ts +22 -3
  103. package/src/server/responses-model-rewrite.ts +29 -0
  104. package/src/server/sse-frame-buffer.ts +292 -0
  105. package/src/server/sse-payload-rewrite.ts +25 -14
  106. package/src/server/ws-bridge.ts +27 -22
  107. package/src/service-manager-probe.ts +520 -10
  108. package/src/service.ts +134 -2
  109. package/src/storage/worker-lifecycle.ts +14 -14
  110. package/src/tray/windows-tray.ps1 +74 -9
  111. package/src/types.ts +68 -2
  112. package/src/update/index.ts +12 -0
  113. package/src/update/job.ts +392 -18
  114. package/src/update/npm-cache-preflight.d.mts +47 -0
  115. package/src/update/npm-cache-preflight.mjs +201 -0
  116. package/src/usage/log.ts +1 -1
  117. package/src/vision/index.ts +77 -2
  118. package/src/web-search/loop.ts +1 -1
  119. package/src/web-search/parse.ts +4 -1
  120. package/gui/dist/assets/index-BKVqyYqT.js +0 -70
  121. package/gui/dist/assets/index-Ca_3269W.css +0 -1
@@ -1,5 +1,15 @@
1
1
  import { createHash } from "node:crypto";
2
- import { lstatSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
2
+ import {
3
+ closeSync,
4
+ fstatSync,
5
+ lstatSync,
6
+ openSync,
7
+ readFileSync,
8
+ readdirSync,
9
+ realpathSync,
10
+ readSync,
11
+ statSync,
12
+ } from "node:fs";
3
13
  import type { Stats } from "node:fs";
4
14
  import { basename, dirname, join, resolve } from "node:path";
5
15
 
@@ -69,6 +79,8 @@ const MODELS_CACHE_FILE_NAME = basename(CODEX_MODELS_CACHE_PATH);
69
79
  const JOURNAL_FILE_NAME = "opencodex-journal.json";
70
80
  const HISTORY_DATABASE_FILE_NAME = "state_5.sqlite";
71
81
  const ROUTED_CATALOG_DESCRIPTION_PREFIX = "Routed via opencodex → ";
82
+ const MAX_ROLLOUT_INSPECTION_BYTES = 64 * 1024 * 1024;
83
+ const ROLLOUT_READ_CHUNK_BYTES = 64 * 1024;
72
84
 
73
85
  function errorCode(error: unknown): string | undefined {
74
86
  return (error as NodeJS.ErrnoException | undefined)?.code;
@@ -141,6 +153,53 @@ function indeterminate(
141
153
  return { kind: "indeterminate", surface, path, reason };
142
154
  }
143
155
 
156
+ function rolloutSessionMetaPayload(
157
+ line: string,
158
+ ): { kind: "payload"; payload: Record<string, unknown> | null } | { kind: "malformed"; reason: string } {
159
+ let parsed: unknown;
160
+ try {
161
+ parsed = JSON.parse(line);
162
+ } catch (error) {
163
+ return { kind: "malformed", reason: `malformed rollout JSONL: ${errorReason(error)}` };
164
+ }
165
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
166
+ return { kind: "malformed", reason: "rollout JSONL record is not an object" };
167
+ }
168
+ const record = parsed as Record<string, unknown>;
169
+ if (record.type !== "session_meta") return { kind: "payload", payload: null };
170
+ if (!record.payload || typeof record.payload !== "object" || Array.isArray(record.payload)) {
171
+ return { kind: "malformed", reason: "session_meta payload has an unknown shape" };
172
+ }
173
+ return { kind: "payload", payload: record.payload as Record<string, unknown> };
174
+ }
175
+
176
+ function consumeRolloutLines(
177
+ surface: "history" | "history-backup",
178
+ path: string,
179
+ partial: string,
180
+ first: Record<string, unknown> | undefined,
181
+ latest: Record<string, unknown> | undefined,
182
+ ): NativeRoutedResidueResult | { kind: "continue"; partial: string; first: Record<string, unknown> | undefined; latest: Record<string, unknown> | undefined } {
183
+ let rest = partial;
184
+ let newline = rest.indexOf("\n");
185
+ while (newline !== -1) {
186
+ const line = rest.slice(0, newline);
187
+ rest = rest.slice(newline + 1);
188
+ if (line.trim()) {
189
+ const payload = rolloutSessionMetaPayload(line);
190
+ if (payload.kind === "malformed") {
191
+ return indeterminate(surface, path, payload.reason);
192
+ }
193
+ if (payload.payload !== null) {
194
+ first ??= payload.payload;
195
+ latest = payload.payload;
196
+ }
197
+ }
198
+ newline = rest.indexOf("\n");
199
+ }
200
+ return { kind: "continue", partial: rest, first, latest };
201
+ }
202
+
144
203
  function classifyToml(
145
204
  surface: "config" | "profile",
146
205
  path: string,
@@ -358,50 +417,104 @@ function classifyReferencedRollout(
358
417
  surface: "history" | "history-backup",
359
418
  reference: RolloutReference,
360
419
  ): NativeRoutedResidueResult {
361
- const read = readRegularFile(reference.path);
362
- if (read.kind === "absent") {
420
+ const resolved = resolveRegularFile(reference.path);
421
+ if (resolved.kind === "absent") {
363
422
  return indeterminate(surface, reference.path, "referenced rollout is absent");
364
423
  }
365
- if (read.kind === "indeterminate") return indeterminate(surface, reference.path, read.reason);
424
+ if (resolved.kind === "indeterminate") return indeterminate(surface, reference.path, resolved.reason);
425
+
426
+ let handle: number;
427
+ try {
428
+ handle = openSync(resolved.path, "r");
429
+ } catch (error) {
430
+ return indeterminate(surface, resolved.path, `unreadable rollout: ${errorReason(error)}`);
431
+ }
366
432
 
367
433
  let first: Record<string, unknown> | undefined;
368
434
  let latest: Record<string, unknown> | undefined;
369
- for (const line of read.content.split("\n")) {
370
- if (!line.trim()) continue;
371
- let parsed: unknown;
372
- try {
373
- parsed = JSON.parse(line);
374
- } catch (error) {
375
- return indeterminate(surface, read.path, `malformed rollout JSONL: ${errorReason(error)}`);
435
+ let partial = "";
436
+ let totalRead = 0;
437
+ try {
438
+ const opened = fstatSync(handle);
439
+ if (opened.size > MAX_ROLLOUT_INSPECTION_BYTES) {
440
+ return indeterminate(
441
+ surface,
442
+ resolved.path,
443
+ `referenced rollout exceeds the ${MAX_ROLLOUT_INSPECTION_BYTES} byte inspection limit`,
444
+ );
376
445
  }
377
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
378
- return indeterminate(surface, read.path, "rollout JSONL record is not an object");
446
+ const decoder = new TextDecoder("utf-8", { ignoreBOM: true });
447
+ const buffer = Buffer.allocUnsafe(ROLLOUT_READ_CHUNK_BYTES);
448
+ while (totalRead < opened.size) {
449
+ const remaining = Math.min(buffer.length, opened.size - totalRead);
450
+ const count = readSync(handle, buffer, 0, remaining, totalRead);
451
+ if (count === 0) {
452
+ return indeterminate(surface, resolved.path, "rollout read ended before the observed size");
453
+ }
454
+ if (count < 0) {
455
+ return indeterminate(surface, resolved.path, "rollout read ended before the observed size");
456
+ }
457
+ totalRead += count;
458
+ partial += decoder.decode(buffer.subarray(0, count), { stream: true });
459
+ const consumed = consumeRolloutLines(surface, resolved.path, partial, first, latest);
460
+ if (consumed.kind !== "continue") return consumed;
461
+ partial = consumed.partial;
462
+ first = consumed.first;
463
+ latest = consumed.latest;
379
464
  }
380
- const record = parsed as Record<string, unknown>;
381
- if (record.type !== "session_meta") continue;
382
- if (!record.payload || typeof record.payload !== "object" || Array.isArray(record.payload)) {
383
- return indeterminate(surface, read.path, "session_meta payload has an unknown shape");
465
+ partial += decoder.decode();
466
+ const consumed = consumeRolloutLines(surface, resolved.path, partial, first, latest);
467
+ if (consumed.kind !== "continue") return consumed;
468
+ partial = consumed.partial;
469
+ first = consumed.first;
470
+ latest = consumed.latest;
471
+ if (partial.trim()) {
472
+ const payload = rolloutSessionMetaPayload(partial);
473
+ if (payload.kind === "malformed") {
474
+ return indeterminate(surface, resolved.path, payload.reason);
475
+ }
476
+ if (payload.payload !== null) {
477
+ first ??= payload.payload;
478
+ latest = payload.payload;
479
+ }
480
+ }
481
+ const after = fstatSync(handle);
482
+ if (!sameStat(resolved.stat, after)) {
483
+ return indeterminate(surface, resolved.path, "rollout changed while it was being observed");
484
+ }
485
+ const pathAfter = statSync(resolved.path);
486
+ if (!sameStat(resolved.stat, pathAfter)) {
487
+ return indeterminate(surface, resolved.path, "rollout pathname was replaced while it was being observed");
488
+ }
489
+ } catch (error) {
490
+ if (errorCode(error) === "ENOENT") {
491
+ return indeterminate(surface, resolved.path, "referenced rollout is absent");
492
+ }
493
+ return indeterminate(surface, resolved.path, `unreadable rollout: ${errorReason(error)}`);
494
+ } finally {
495
+ try {
496
+ closeSync(handle);
497
+ } catch {
498
+ // Closing an already-closed descriptor cannot affect the classification.
384
499
  }
385
- const payload = record.payload as Record<string, unknown>;
386
- first ??= payload;
387
- latest = payload;
388
500
  }
389
501
 
390
502
  if (!first || !latest) {
391
- return indeterminate(surface, read.path, "referenced rollout has no session_meta metadata");
503
+ return indeterminate(surface, resolved.path, "referenced rollout has no session_meta metadata");
392
504
  }
505
+ let hasOpenCodexProvider = false;
393
506
  for (const [position, payload] of [["first", first], ["latest", latest]] as const) {
394
507
  if (payload.id !== reference.id) {
395
- return indeterminate(surface, read.path, `${position} session_meta does not identify the referenced thread`);
508
+ return indeterminate(surface, resolved.path, `${position} session_meta does not identify the referenced thread`);
396
509
  }
397
510
  if (typeof payload.model_provider !== "string" || !payload.model_provider) {
398
- return indeterminate(surface, read.path, `${position} session_meta has no provider metadata`);
399
- }
400
- if (payload.model_provider === "opencodex") {
401
- return { kind: "residue", surface, path: read.path };
511
+ return indeterminate(surface, resolved.path, `${position} session_meta has no provider metadata`);
402
512
  }
513
+ hasOpenCodexProvider = hasOpenCodexProvider || payload.model_provider === "opencodex";
403
514
  }
404
- return { kind: "clean" };
515
+ return hasOpenCodexProvider
516
+ ? { kind: "residue", surface, path: resolved.path }
517
+ : { kind: "clean" };
405
518
  }
406
519
 
407
520
  function classifyReferencedRollouts(
@@ -19,6 +19,11 @@ const MAX_STICKY_LIMIT = 100;
19
19
  const DEFAULT_STRATEGY: OcxAccountPoolRotationStrategy = "quota";
20
20
  const VALID_STRATEGIES = new Set<OcxAccountPoolRotationStrategy>(["quota", "round-robin", "fill-first"]);
21
21
 
22
+ /** Selection order for an account with no stored preference: one flat tier. */
23
+ export const DEFAULT_ACCOUNT_PRIORITY = 0;
24
+ export const MIN_ACCOUNT_PRIORITY = -100;
25
+ export const MAX_ACCOUNT_PRIORITY = 100;
26
+
22
27
  /** Strict parse for management APIs — returns null instead of defaulting. */
23
28
  export function parseAccountPoolStrategy(raw: unknown): OcxAccountPoolRotationStrategy | null {
24
29
  if (typeof raw === "string" && VALID_STRATEGIES.has(raw as OcxAccountPoolRotationStrategy)) {
@@ -43,6 +48,71 @@ export function normalizeAccountPoolStickyLimit(raw: unknown): number {
43
48
  return parseAccountPoolStickyLimit(raw) ?? DEFAULT_STICKY_LIMIT;
44
49
  }
45
50
 
51
+ /** Strict parse for management APIs — returns null instead of defaulting. */
52
+ export function parseAccountPriority(raw: unknown): number | null {
53
+ if (
54
+ typeof raw === "number"
55
+ && Number.isInteger(raw)
56
+ && raw >= MIN_ACCOUNT_PRIORITY
57
+ && raw <= MAX_ACCOUNT_PRIORITY
58
+ ) {
59
+ return raw;
60
+ }
61
+ return null;
62
+ }
63
+
64
+ export function normalizeAccountPriority(raw: unknown): number {
65
+ return parseAccountPriority(raw) ?? DEFAULT_ACCOUNT_PRIORITY;
66
+ }
67
+
68
+ /**
69
+ * Narrow an already-eligible account list to the highest selection-order tier that
70
+ * still has usable quota. Priority is an *ordering* boundary layered on top of
71
+ * eligibility: it never admits an account the caller already filtered out, and it
72
+ * never keeps the pool on a tier whose every member is drained.
73
+ *
74
+ * Contract (each clause is load-bearing for "no behavior change when unconfigured"):
75
+ * - one distinct priority across `ids` (the unconfigured case) returns `ids` unchanged,
76
+ * so today's pick sequence is preserved byte for byte;
77
+ * - input order is preserved inside the returned tier, which keeps the `__main__`
78
+ * head-of-list bias and the first-index tie-break used by SWRR/lowest-usage;
79
+ * - every tier drained returns `ids` unchanged, reproducing today's
80
+ * stay-put-until-429 behavior rather than inventing a pick;
81
+ * - a `pinnedId` that is present *and* has headroom lowers the ceiling to its own
82
+ * tier, which is what makes a manual "use this now" survive round-robin and
83
+ * fill-first without any mutable selection state. A drained or absent pin is
84
+ * ignored, so the pin expires on its own once the account crosses the threshold.
85
+ */
86
+ export function selectPriorityTier(
87
+ ids: readonly string[],
88
+ priorityOf: (id: string) => number,
89
+ hasHeadroom: (id: string) => boolean,
90
+ pinnedId?: string,
91
+ ): readonly string[] {
92
+ // Readonly out as well as in: the no-change cases return the caller's own array, so a
93
+ // mutating caller would corrupt its input in exactly the cases that must not change.
94
+ const list = ids;
95
+ if (list.length <= 1) return list;
96
+
97
+ const priorities = list.map(priorityOf);
98
+ const firstPriority = priorities[0]!;
99
+ if (priorities.every(priority => priority === firstPriority)) return list;
100
+
101
+ let ceiling = Number.POSITIVE_INFINITY;
102
+ if (pinnedId !== undefined) {
103
+ const pinnedIndex = list.indexOf(pinnedId);
104
+ if (pinnedIndex >= 0 && hasHeadroom(pinnedId)) ceiling = priorities[pinnedIndex]!;
105
+ }
106
+
107
+ const tiers = [...new Set(priorities)].sort((a, b) => b - a);
108
+ for (const tier of tiers) {
109
+ if (tier > ceiling) continue;
110
+ const members = list.filter((_, index) => priorities[index] === tier);
111
+ if (members.some(hasHeadroom)) return members;
112
+ }
113
+ return list;
114
+ }
115
+
46
116
  function getOrCreateState(poolKey: string): SelectionState {
47
117
  let state = selectionState.get(poolKey);
48
118
  if (!state) {
@@ -60,7 +130,7 @@ function cloneSelectionState(state: SelectionState): SelectionState {
60
130
  };
61
131
  }
62
132
 
63
- function smoothWeightedIndex(ids: string[], state: SelectionState): number {
133
+ function smoothWeightedIndex(ids: readonly string[], state: SelectionState): number {
64
134
  let best = -1;
65
135
  let bestScore = Number.NEGATIVE_INFINITY;
66
136
  let total = 0;
@@ -87,7 +157,7 @@ function smoothWeightedIndex(ids: string[], state: SelectionState): number {
87
157
  * either the live map entry or a scratch/clone for dry-run peek.
88
158
  */
89
159
  function pickRoundRobinFromState(
90
- eligibleIds: string[],
160
+ eligibleIds: readonly string[],
91
161
  stickyLimit: number,
92
162
  state: SelectionState,
93
163
  commitSticky: boolean,
@@ -118,7 +188,7 @@ function pickRoundRobinFromState(
118
188
 
119
189
  export function pickRoundRobinAccount(
120
190
  poolKey: string,
121
- eligibleIds: string[],
191
+ eligibleIds: readonly string[],
122
192
  stickyLimit: number,
123
193
  ): string | null {
124
194
  return pickRoundRobinFromState(eligibleIds, stickyLimit, getOrCreateState(poolKey), true);
@@ -130,7 +200,7 @@ export function pickRoundRobinAccount(
130
200
  */
131
201
  export function peekRoundRobinAccount(
132
202
  poolKey: string,
133
- eligibleIds: string[],
203
+ eligibleIds: readonly string[],
134
204
  stickyLimit: number,
135
205
  ): string | null {
136
206
  const live = selectionState.get(poolKey);
@@ -12,6 +12,8 @@ export interface CodexCatalogRefreshResult {
12
12
  catalogWritten: boolean;
13
13
  cacheSynced: boolean;
14
14
  comboOmissions: ComboCatalogOmission[];
15
+ /** Desired OFF observed under K during the catalog commit; no cache write either. */
16
+ skippedReason?: "desired_disabled";
15
17
  }
16
18
 
17
19
  interface RefreshDeps {
@@ -45,6 +47,11 @@ export async function refreshCodexModelCatalog(
45
47
  const catalogExists = deps.existsSync(result.path);
46
48
  const catalogWritten = result.catalogWritten === true;
47
49
  const comboOmissions = result.comboOmissions ?? [];
50
+ if (result.skippedReason === "desired_disabled") {
51
+ // The commit path observed OFF under K. Invalidate nothing: rewriting the
52
+ // models cache here would be exactly the routed-cache write the skip refused.
53
+ return { ...result, catalogExists, catalogWritten: false, cacheSynced: false, comboOmissions };
54
+ }
48
55
  if (!catalogExists) {
49
56
  return { ...result, catalogExists, catalogWritten: false, cacheSynced: false, comboOmissions };
50
57
  }