@bitkyc08/opencodex 2.7.41 → 2.7.42

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 (80) hide show
  1. package/README.md +4 -0
  2. package/gui/dist/assets/index-Bl_VBGoI.js +65 -0
  3. package/gui/dist/assets/index-DfVGuN88.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/base.ts +6 -0
  7. package/src/adapters/kiro-constants.ts +6 -2
  8. package/src/adapters/kiro-retry.ts +175 -10
  9. package/src/adapters/kiro.ts +172 -85
  10. package/src/adapters/mimo-free.ts +1 -0
  11. package/src/adapters/openai-chat.ts +30 -4
  12. package/src/adapters/openai-responses.ts +90 -12
  13. package/src/bridge.ts +91 -43
  14. package/src/claude/desktop-3p-paths.ts +84 -0
  15. package/src/claude/desktop-3p.ts +29 -2
  16. package/src/cli/access.ts +108 -0
  17. package/src/cli/account-auth.ts +223 -0
  18. package/src/cli/account.ts +9 -1
  19. package/src/cli/agent.ts +184 -0
  20. package/src/cli/combo.ts +119 -0
  21. package/src/cli/config-command.ts +145 -0
  22. package/src/cli/debug.ts +20 -8
  23. package/src/cli/doctor.ts +45 -8
  24. package/src/cli/help.ts +65 -13
  25. package/src/cli/index.ts +108 -7
  26. package/src/cli/integrations.ts +142 -0
  27. package/src/cli/models-runtime.ts +212 -0
  28. package/src/cli/models.ts +9 -10
  29. package/src/cli/observe.ts +117 -0
  30. package/src/cli/provider-runtime.ts +152 -0
  31. package/src/cli/provider.ts +23 -1
  32. package/src/cli/runtime-api.ts +325 -0
  33. package/src/cli/star-prompt.ts +3 -3
  34. package/src/cli/status.ts +17 -0
  35. package/src/cli/system-command.ts +112 -0
  36. package/src/codex/auth-api.ts +3 -2
  37. package/src/codex/catalog/aggregation.ts +113 -18
  38. package/src/codex/catalog/provider-fetch.ts +24 -13
  39. package/src/codex/catalog/sync.ts +20 -8
  40. package/src/codex/catalog.ts +2 -1
  41. package/src/codex/refresh.ts +10 -3
  42. package/src/codex/routing.ts +21 -32
  43. package/src/codex/sync.ts +17 -0
  44. package/src/config.ts +48 -0
  45. package/src/generated/jawcode-model-metadata.ts +2 -1
  46. package/src/grok/inject.ts +184 -4
  47. package/src/grok/status.ts +33 -0
  48. package/src/lib/retry-after.ts +55 -0
  49. package/src/lib/windows-elevation.ts +627 -0
  50. package/src/providers/openai-sidecar.ts +46 -2
  51. package/src/providers/registry.ts +52 -0
  52. package/src/server/auth-cors.ts +6 -0
  53. package/src/server/chat-completions.ts +6 -1
  54. package/src/server/claude-messages.ts +20 -1
  55. package/src/server/images.ts +14 -7
  56. package/src/server/management/agent-settings-routes.ts +10 -4
  57. package/src/server/management/combo-routes.ts +0 -1
  58. package/src/server/management/config-routes.ts +0 -1
  59. package/src/server/management/logs-usage-routes.ts +94 -0
  60. package/src/server/management/model-routes.ts +0 -1
  61. package/src/server/management/oauth-account-routes.ts +0 -1
  62. package/src/server/management/provider-routes.ts +0 -1
  63. package/src/server/management/shared.ts +0 -1
  64. package/src/server/management/system-routes.ts +27 -15
  65. package/src/server/management-api.ts +0 -1
  66. package/src/server/memory-watchdog.ts +54 -10
  67. package/src/server/request-log-conversation.ts +168 -0
  68. package/src/server/request-log.ts +122 -2
  69. package/src/server/responses/core.ts +76 -13
  70. package/src/server/responses/passthrough-error.ts +38 -13
  71. package/src/server/startup-action-control.ts +266 -15
  72. package/src/service.ts +512 -3
  73. package/src/storage/cleanup.ts +1538 -0
  74. package/src/storage/scanner.ts +4 -1
  75. package/src/types.ts +16 -0
  76. package/src/update/job.ts +229 -25
  77. package/src/usage/log.ts +39 -0
  78. package/src/web-search/loop.ts +8 -1
  79. package/gui/dist/assets/index-B2J4t3te.css +0 -1
  80. package/gui/dist/assets/index-BmvM6wRb.js +0 -65
@@ -1,11 +1,8 @@
1
1
  import { formatErrorResponse } from "../../bridge";
2
- import { parseRetryAfterMs } from "../../combos";
3
-
4
- function sanitizedRetryAfter(value: string | null | undefined, now: number): string | undefined {
5
- const trimmed = value?.trim();
6
- if (!trimmed || trimmed.length > 128) return undefined;
7
- return parseRetryAfterMs(trimmed, now) !== undefined ? trimmed : undefined;
8
- }
2
+ import {
3
+ resolveClientRetryAfter,
4
+ validateClientRetryAfterHeader,
5
+ } from "../../lib/retry-after";
9
6
 
10
7
  /**
11
8
  * Passthrough adapters historically relayed upstream non-2xx bodies verbatim.
@@ -16,9 +13,11 @@ function sanitizedRetryAfter(value: string | null | undefined, now: number): str
16
13
  * HTML/text errors) must keep their original bytes and headers so pool-retry
17
14
  * activation and client diagnostics stay honest.
18
15
  *
19
- * Normalized (empty-body) responses force `Content-Type: application/json` and
20
- * preserve a validated `Retry-After` so Responses clients and the chat-completions
21
- * / Claude bridges that copy that header keep correct backoff.
16
+ * Retry-After is validated independently of the body path:
17
+ * - valid upstream values are preserved
18
+ * - missing/malformed values are replaced when resolveClientRetryAfter yields a value
19
+ * - malformed/expired values are removed when the resolver returns undefined
20
+ * (e.g. quota-exhausted 429s must not keep junk headers or get the synthetic "2")
22
21
  */
23
22
  export function formatPassthroughUpstreamError(
24
23
  status: number,
@@ -31,13 +30,38 @@ export function formatPassthroughUpstreamError(
31
30
  ): Response {
32
31
  const trimmed = bodyText.trim();
33
32
  const now = options?.now ?? Date.now();
34
- const retryAfter = sanitizedRetryAfter(options?.headers?.get("retry-after"), now);
33
+ const upstreamRetryAfter = options?.headers?.get("retry-after")?.trim() || undefined;
34
+ const originalValid = validateClientRetryAfterHeader(upstreamRetryAfter, now);
35
+ const resolved = resolveClientRetryAfter({
36
+ status,
37
+ message: trimmed || `Provider error ${status}: (empty body)`,
38
+ upstreamRetryAfter,
39
+ now,
40
+ });
35
41
 
36
42
  if (trimmed) {
43
+ const needsSet = resolved !== undefined && upstreamRetryAfter !== resolved;
44
+ const needsDelete = resolved === undefined
45
+ && upstreamRetryAfter !== undefined
46
+ && originalValid === undefined;
47
+
48
+ if (!needsSet && !needsDelete) {
49
+ return new Response(bodyText, {
50
+ status,
51
+ ...(options?.statusText ? { statusText: options.statusText } : {}),
52
+ ...(options?.headers ? { headers: options.headers } : { headers: { "Content-Type": "application/json" } }),
53
+ });
54
+ }
55
+
56
+ const headers = options?.headers
57
+ ? new Headers(options.headers)
58
+ : new Headers({ "Content-Type": "application/json" });
59
+ if (needsSet) headers.set("Retry-After", resolved!);
60
+ else headers.delete("Retry-After");
37
61
  return new Response(bodyText, {
38
62
  status,
39
63
  ...(options?.statusText ? { statusText: options.statusText } : {}),
40
- ...(options?.headers ? { headers: options.headers } : { headers: { "Content-Type": "application/json" } }),
64
+ headers,
41
65
  });
42
66
  }
43
67
 
@@ -45,9 +69,10 @@ export function formatPassthroughUpstreamError(
45
69
  status,
46
70
  "upstream_error",
47
71
  `Provider error ${status}: (empty body)`,
72
+ resolved !== undefined ? { retryAfter: resolved } : undefined,
48
73
  );
49
74
  const headers = new Headers(response.headers);
50
75
  headers.set("Content-Type", "application/json");
51
- if (retryAfter !== undefined) headers.set("Retry-After", retryAfter);
76
+ if (resolved !== undefined) headers.set("Retry-After", resolved);
52
77
  return new Response(response.body, { status: response.status, headers });
53
78
  }
@@ -1,9 +1,110 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { execFile } from "node:child_process";
2
3
  import { join } from "node:path";
3
4
  import { durableBunPath } from "../lib/bun-runtime";
5
+ import {
6
+ WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED_MARKER,
7
+ isWindowsSchtasksCreateAccessDenied,
8
+ } from "../lib/windows-elevation";
9
+ import {
10
+ finalizeWindowsSchedulerServiceRegistration,
11
+ type ElevatedReconciliationOutcome,
12
+ type FinalizeWindowsSchedulerOptions,
13
+ type FinalizeWindowsSchedulerResult,
14
+ type WindowsSchedulerTaskProbe,
15
+ } from "../service";
4
16
 
5
17
  export type StartupInstallAction = "install-service" | "install-shim";
6
- let activeInstall: StartupInstallAction | null = null;
18
+
19
+ export type StartupInstallState =
20
+ | { status: "idle" }
21
+ | {
22
+ status: "running";
23
+ action: StartupInstallAction;
24
+ attemptId: string;
25
+ startedAt: number;
26
+ }
27
+ | {
28
+ status: "indeterminate";
29
+ action: "install-service";
30
+ attemptId: string;
31
+ startedAt: number;
32
+ timedOutAt: number;
33
+ reconciliation: Promise<ElevatedReconciliationOutcome>;
34
+ }
35
+ | {
36
+ status: "blocked";
37
+ reason: "partial-elevated-install";
38
+ action: "install-service";
39
+ attemptId: string;
40
+ detail: string;
41
+ };
42
+
43
+ const INSTALL_DETAIL_LIMIT = 2_000;
44
+ const INDETERMINATE_MESSAGE =
45
+ "Windows elevation timed out, but the elevated Task Scheduler transaction may still be running. New service installation attempts are temporarily blocked while OpenCodex reconciles its final state.";
46
+ const RECONCILING_MESSAGE =
47
+ "A previous elevated Windows service installation is still being reconciled. Wait for it to finish or inspect the Task Scheduler state before retrying.";
48
+ const BLOCKED_PARTIAL_MESSAGE =
49
+ "A previous elevated Windows service installation left a partial Task Scheduler state. Remove the OpenCodex scheduler task (or confirm it is absent), then clear the install block before retrying.";
50
+
51
+ let installState: StartupInstallState = { status: "idle" };
52
+
53
+ type FinalizeFn = (
54
+ script?: string,
55
+ options?: FinalizeWindowsSchedulerOptions,
56
+ ) => Promise<FinalizeWindowsSchedulerResult>;
57
+ let finalizeImpl: FinalizeFn = finalizeWindowsSchedulerServiceRegistration;
58
+
59
+ /** Test-only seam so elevation retry tests do not mock.module the service package. */
60
+ export function setStartupInstallFinalizeForTests(next: FinalizeFn | null): void {
61
+ finalizeImpl = next ?? finalizeWindowsSchedulerServiceRegistration;
62
+ }
63
+
64
+ /** Snapshot of the install lock for diagnostics and tests. */
65
+ export function getStartupInstallState(): StartupInstallState {
66
+ return installState;
67
+ }
68
+
69
+ /** True when this attempt still owns the active/indeterminate/blocked lock. */
70
+ export function ownsStartupInstallAttempt(attemptId: string): boolean {
71
+ return (installState.status === "running"
72
+ || installState.status === "indeterminate"
73
+ || installState.status === "blocked")
74
+ && installState.attemptId === attemptId;
75
+ }
76
+
77
+ /**
78
+ * Clear a partial-install block after the operator has cleaned up Task Scheduler.
79
+ * Requires an observed probe and clears only when the task is proven absent.
80
+ * Does not itself delete the task.
81
+ */
82
+ export function clearStartupInstallPartialBlock(options: {
83
+ probe: WindowsSchedulerTaskProbe;
84
+ }): { cleared: boolean; detail: string } {
85
+ if (installState.status !== "blocked") {
86
+ return { cleared: false, detail: `Install lock is ${installState.status}, not blocked.` };
87
+ }
88
+ if (options.probe.status === "present") {
89
+ return {
90
+ cleared: false,
91
+ detail: "OpenCodex Task Scheduler task is still present; remove it before clearing the block.",
92
+ };
93
+ }
94
+ if (options.probe.status === "unknown") {
95
+ return {
96
+ cleared: false,
97
+ detail: `Could not verify Task Scheduler absence (${options.probe.detail}); not clearing the block.`,
98
+ };
99
+ }
100
+ installState = { status: "idle" };
101
+ return { cleared: true, detail: "Partial-install block cleared." };
102
+ }
103
+
104
+ /** Test-only reset of the install lock. */
105
+ export function resetStartupInstallStateForTests(): void {
106
+ installState = { status: "idle" };
107
+ }
7
108
 
8
109
  export function startupInstallArgv(action: StartupInstallAction): string[] {
9
110
  return action === "install-service"
@@ -11,14 +112,52 @@ export function startupInstallArgv(action: StartupInstallAction): string[] {
11
112
  : ["codex-shim", "install"];
12
113
  }
13
114
 
14
- /** Execute the existing fixed CLI installer outside the proxy event loop. */
15
- export function runStartupInstallAction(action: StartupInstallAction): Promise<{ message: string }> {
16
- if (activeInstall) return Promise.reject(new Error(`Another startup installation is already running: ${activeInstall}`));
17
- activeInstall = action;
115
+ export interface CliInstallFailure {
116
+ /** Machine marker such as OCX_ERROR_CODE=..., when present in any stream. */
117
+ code: string | null;
118
+ stdout: string;
119
+ stderr: string;
120
+ message: string;
121
+ /** Bounded user-facing diagnostic (may append the marker if truncated away). */
122
+ detail: string;
123
+ }
124
+
125
+ function extractOcxErrorCode(text: string): string | null {
126
+ const match = text.match(/OCX_ERROR_CODE=[A-Z0-9_]+/);
127
+ return match ? match[0] : null;
128
+ }
129
+
130
+ /**
131
+ * Classify a CLI install failure, keeping the machine marker separate from the
132
+ * truncated user-facing diagnostic.
133
+ */
134
+ export function classifyCliInstallFailure(stdout: string, stderr: string, error: Error): CliInstallFailure {
135
+ const stdoutText = stdout.trim();
136
+ const stderrText = stderr.trim();
137
+ const message = error.message.trim();
138
+ const combined = [stderrText, stdoutText, message].filter(Boolean).join("\n");
139
+ const code = extractOcxErrorCode(combined);
140
+ let detail = combined || message;
141
+ if (detail.length > INSTALL_DETAIL_LIMIT) {
142
+ const truncated = detail.slice(0, INSTALL_DETAIL_LIMIT);
143
+ detail = code && !truncated.includes(code)
144
+ ? `${truncated}\n${code}… (truncated)`
145
+ : `${truncated}… (truncated)`;
146
+ }
147
+ return { code, stdout: stdoutText, stderr: stderrText, message, detail };
148
+ }
149
+
150
+ /** @deprecated Prefer classifyCliInstallFailure; kept for existing tests. */
151
+ export function installFailureDetail(stdout: string, stderr: string, error: Error): string {
152
+ return classifyCliInstallFailure(stdout, stderr, error).detail;
153
+ }
154
+
155
+ function runCliInstall(action: StartupInstallAction): Promise<{ stdout: string; stderr: string }> {
18
156
  const bun = durableBunPath();
19
157
  const cli = join(import.meta.dir, "..", "cli", "index.ts");
20
- const operation = new Promise<{ message: string }>((resolve, reject) => {
21
- execFile(bun, [cli, ...startupInstallArgv(action)], {
158
+ const argv = [cli, ...startupInstallArgv(action)];
159
+ return new Promise((resolve, reject) => {
160
+ execFile(bun, argv, {
22
161
  encoding: "utf8",
23
162
  env: process.env,
24
163
  timeout: 60_000,
@@ -26,16 +165,128 @@ export function runStartupInstallAction(action: StartupInstallAction): Promise<{
26
165
  maxBuffer: 256 * 1024,
27
166
  }, (error, stdout, stderr) => {
28
167
  if (error) {
29
- const detail = stderr.trim() || stdout.trim() || error.message;
30
- reject(new Error(detail.slice(0, 2_000)));
168
+ const failure = classifyCliInstallFailure(stdout, stderr, error);
169
+ reject(Object.assign(new Error(failure.detail), { ocxInstallFailure: failure }));
31
170
  return;
32
171
  }
33
- resolve({
34
- message: action === "install-service"
35
- ? "Background service installed."
36
- : "Codex launcher shim installed.",
37
- });
172
+ resolve({ stdout, stderr });
38
173
  });
39
174
  });
40
- return operation.finally(() => { activeInstall = null; });
175
+ }
176
+
177
+ function installFailureCode(error: unknown): string | null {
178
+ if (error && typeof error === "object" && "ocxInstallFailure" in error) {
179
+ const failure = (error as { ocxInstallFailure?: CliInstallFailure }).ocxInstallFailure;
180
+ if (failure?.code) return failure.code;
181
+ }
182
+ const detail = error instanceof Error ? error.message : String(error);
183
+ return extractOcxErrorCode(detail);
184
+ }
185
+
186
+ function rejectIfBusy(_action: StartupInstallAction): Error | null {
187
+ if (installState.status === "running") {
188
+ return new Error(`Another startup installation is already running: ${installState.action}`);
189
+ }
190
+ if (installState.status === "indeterminate") {
191
+ return new Error(RECONCILING_MESSAGE);
192
+ }
193
+ if (installState.status === "blocked") {
194
+ return new Error(BLOCKED_PARTIAL_MESSAGE);
195
+ }
196
+ return null;
197
+ }
198
+
199
+ function applyReconciliationOutcome(
200
+ attemptId: string,
201
+ outcome: ElevatedReconciliationOutcome,
202
+ ): void {
203
+ if (installState.status !== "indeterminate" || installState.attemptId !== attemptId) {
204
+ return;
205
+ }
206
+ if (outcome === "blocked-partial") {
207
+ installState = {
208
+ status: "blocked",
209
+ reason: "partial-elevated-install",
210
+ action: "install-service",
211
+ attemptId,
212
+ detail: BLOCKED_PARTIAL_MESSAGE,
213
+ };
214
+ return;
215
+ }
216
+ installState = { status: "idle" };
217
+ }
218
+
219
+ /**
220
+ * Execute the existing fixed CLI installer outside the proxy event loop.
221
+ *
222
+ * After an elevation request timeout the lock becomes `indeterminate` until the
223
+ * original elevated transaction completes and is reconciled. A process restart
224
+ * clears this in-memory lock — callers must then inspect Task Scheduler reality
225
+ * (see evaluateSchedulerInstallRestartReconciliation) before installing again.
226
+ */
227
+ export function runStartupInstallAction(action: StartupInstallAction): Promise<{ message: string }> {
228
+ const busy = rejectIfBusy(action);
229
+ if (busy) return Promise.reject(busy);
230
+
231
+ const attemptId = randomUUID();
232
+ const startedAt = Date.now();
233
+ installState = { status: "running", action, attemptId, startedAt };
234
+
235
+ const operation = (async () => {
236
+ try {
237
+ await runCliInstall(action);
238
+ } catch (error) {
239
+ const code = installFailureCode(error);
240
+ const detail = error instanceof Error ? error.message : String(error);
241
+ // Elevate only for a structured Task Scheduler /create access denial — never for
242
+ // WinSW removal, asset writes, or generic permission errors.
243
+ if (
244
+ action === "install-service"
245
+ && process.platform === "win32"
246
+ && (code === WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED_MARKER
247
+ || isWindowsSchtasksCreateAccessDenied(detail))
248
+ ) {
249
+ const finalized = await finalizeImpl(undefined, {
250
+ attemptId,
251
+ stillOwnsAttempt: ownsStartupInstallAttempt,
252
+ });
253
+ if (finalized.kind === "indeterminate") {
254
+ const ownedAttemptId = finalized.attemptId;
255
+ installState = {
256
+ status: "indeterminate",
257
+ action: "install-service",
258
+ attemptId: ownedAttemptId,
259
+ startedAt,
260
+ timedOutAt: Date.now(),
261
+ reconciliation: finalized.reconciliation,
262
+ };
263
+ void finalized.reconciliation.then(
264
+ outcome => {
265
+ applyReconciliationOutcome(ownedAttemptId, outcome);
266
+ },
267
+ () => {
268
+ // Fail closed: a rejected reconciliation must not leave an unhandled rejection
269
+ // or silently idle the lock while Task Scheduler state may still be partial.
270
+ applyReconciliationOutcome(ownedAttemptId, "blocked-partial");
271
+ },
272
+ );
273
+ throw new Error(INDETERMINATE_MESSAGE);
274
+ }
275
+ } else {
276
+ throw error;
277
+ }
278
+ }
279
+ return {
280
+ message: action === "install-service"
281
+ ? "Background service installed."
282
+ : "Codex launcher shim installed.",
283
+ };
284
+ })();
285
+
286
+ return operation.finally(() => {
287
+ // Do not clear an indeterminate or blocked lock from a timed-out elevation.
288
+ if (installState.status === "running" && installState.attemptId === attemptId) {
289
+ installState = { status: "idle" };
290
+ }
291
+ });
41
292
  }