@bitkyc08/opencodex 2.79.0 → 2.80.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 (31) hide show
  1. package/gui/dist/assets/{App-BOUa-ek3.js → App-MbDVfUeC.js} +3 -3
  2. package/gui/dist/assets/{Tray-DUtzB4zF.js → Tray-CzkxK82A.js} +1 -1
  3. package/gui/dist/assets/{index-DwHNDF4s.js → index-UJt6npXk.js} +2 -2
  4. package/gui/dist/assets/{quota-summary-BuL4WIBo.js → quota-summary-DU3sD6Z-.js} +1 -1
  5. package/gui/dist/index.html +1 -1
  6. package/package.json +1 -1
  7. package/src/adapters/cursor/live-transport.ts +5 -3
  8. package/src/adapters/cursor/native-exec-fs.ts +12 -9
  9. package/src/adapters/cursor/native-exec.ts +13 -5
  10. package/src/adapters/cursor/protobuf-request.ts +13 -7
  11. package/src/adapters/cursor/request-builder.ts +23 -10
  12. package/src/adapters/cursor/tool-definitions.ts +9 -6
  13. package/src/adapters/cursor/tool-guidance.ts +29 -8
  14. package/src/adapters/cursor/tool-naming.ts +40 -7
  15. package/src/adapters/cursor/tool-wording.ts +46 -0
  16. package/src/adapters/cursor.ts +3 -0
  17. package/src/adapters/devin.ts +25 -11
  18. package/src/adapters/openai-chat/messages.ts +3 -1
  19. package/src/claude/inbound.ts +9 -0
  20. package/src/codex/autostart-health.ts +3 -3
  21. package/src/codex/journal.ts +36 -5
  22. package/src/codex/shim-overlay.ts +9 -1
  23. package/src/combos/jev.ts +3 -1
  24. package/src/generated/compatibility-version.json +35 -23
  25. package/src/server/index.ts +12 -14
  26. package/src/server/messages-native-scope.ts +4 -1
  27. package/src/server/responses/combo-requested-effort.ts +14 -0
  28. package/src/server/responses/core-combo.ts +5 -7
  29. package/src/service/desktop-startup.ts +100 -3
  30. package/src/web-search/header-deadline.ts +67 -0
  31. package/src/web-search/loop.ts +5 -5
@@ -6,11 +6,11 @@
6
6
  "files": [
7
7
  {
8
8
  "path": "bun.lock",
9
- "sha256": "c065612db20131683fd657dce98b6c51745ed9fadfeb56ea0c3edd823da39cc4"
9
+ "sha256": "7bceb5361a92a8401247dcf131a3e0a40fc2e2b1ededb3c6468e4d435c75fc8c"
10
10
  },
11
11
  {
12
12
  "path": "package.json",
13
- "sha256": "90cabe2689a17d5376a3d50526cbe2a6b4f5a441dd829476770e14aacda15281"
13
+ "sha256": "a2b957b51f2de2693b6b6e6cfd57da30c2076dc7e8d9b4c17f467b4db79f2bcd"
14
14
  },
15
15
  {
16
16
  "path": "scripts/model-metadata.source.json",
@@ -150,7 +150,7 @@
150
150
  },
151
151
  {
152
152
  "path": "src/adapters/cursor.ts",
153
- "sha256": "a3f0a744b4760bbbe396dd603bf1cd1ee74e26c6eb50283e57fad244a610fe7c"
153
+ "sha256": "e34908e890bf1b6c34dcda0b0ef4665db1254904cd0213c76048816531af2faf"
154
154
  },
155
155
  {
156
156
  "path": "src/adapters/cursor/arg-codec.ts",
@@ -238,7 +238,7 @@
238
238
  },
239
239
  {
240
240
  "path": "src/adapters/cursor/live-transport.ts",
241
- "sha256": "699f44189ff255403edcf770dff925f4711b40cfa3a198f2c220f503f55c5310"
241
+ "sha256": "7c58b4a37097012ebb840ac2dd3121b6964a61faf0c9dc6e92e18551f87f1326"
242
242
  },
243
243
  {
244
244
  "path": "src/adapters/cursor/mcp-config.ts",
@@ -262,7 +262,7 @@
262
262
  },
263
263
  {
264
264
  "path": "src/adapters/cursor/native-exec-fs.ts",
265
- "sha256": "0982cbadbb8886f1c90f8ad9cd2fa1bb53ea09861f88d52a56da63d17efb54ea"
265
+ "sha256": "4a87ce4d504447de532cb55fbe997a2fb0f56fa7142548114ddafb6051d82533"
266
266
  },
267
267
  {
268
268
  "path": "src/adapters/cursor/native-exec-mcp.ts",
@@ -282,7 +282,7 @@
282
282
  },
283
283
  {
284
284
  "path": "src/adapters/cursor/native-exec.ts",
285
- "sha256": "abbd8943857b348d1e4c0310fbebf99028db0ab08c042d13c6dca0cc429119ed"
285
+ "sha256": "f5c6e83e6e9554ab4a0de7817b8385bac716789f0d2372212b92a531f9026aeb"
286
286
  },
287
287
  {
288
288
  "path": "src/adapters/cursor/native-foreground-shell.ts",
@@ -298,11 +298,11 @@
298
298
  },
299
299
  {
300
300
  "path": "src/adapters/cursor/protobuf-request.ts",
301
- "sha256": "f2d09cd8ac83d82e102fba1a8a82613bccd2cbc56b8c3b9dfc251f305ab8cf85"
301
+ "sha256": "f6796726488c0ccfc81a705c9d179945d02e54a52a111766935671b50631cab7"
302
302
  },
303
303
  {
304
304
  "path": "src/adapters/cursor/request-builder.ts",
305
- "sha256": "5de11940153ba2ff83390d47b235e6aa009c363e344f58032893b1f5ddf73464"
305
+ "sha256": "f62c554ff41430331a639d6dbfde6df95cdae854635bbf4b0aa5557e7f2c3d5f"
306
306
  },
307
307
  {
308
308
  "path": "src/adapters/cursor/structured-output.ts",
@@ -318,15 +318,15 @@
318
318
  },
319
319
  {
320
320
  "path": "src/adapters/cursor/tool-definitions.ts",
321
- "sha256": "3aa052f5b70fc54ae59f78ba23e87b8a7c8f1e83f6ed60a31e359a5bcd3baca0"
321
+ "sha256": "9a2aefcb82f7040f3581676fa6a8d63c9ca0352400dcf659cf3b664b63d489c0"
322
322
  },
323
323
  {
324
324
  "path": "src/adapters/cursor/tool-guidance.ts",
325
- "sha256": "8fd5d06a5622cc049c227cff78a24f15d7b9f0152566804dd1346cace9b42b8d"
325
+ "sha256": "a4865e3854aadcd979229830c92dc15cc60ef355f574f727348a730d23ae97aa"
326
326
  },
327
327
  {
328
328
  "path": "src/adapters/cursor/tool-naming.ts",
329
- "sha256": "47f7fb1de1e4fd06d2b29c0811f472cd3380dee0b633d646683dcaab5c959976"
329
+ "sha256": "be1d589e1b8f6e85f620d7f3e33651b5ca09b086a97a80add4f6878c1b69b47d"
330
330
  },
331
331
  {
332
332
  "path": "src/adapters/cursor/tool-result-normalize.ts",
@@ -336,6 +336,10 @@
336
336
  "path": "src/adapters/cursor/tool-schemas.ts",
337
337
  "sha256": "e30dea6deed9ce06d3ecdc51a911137800a36e5ada8687f8c653cc85bbcaf2f9"
338
338
  },
339
+ {
340
+ "path": "src/adapters/cursor/tool-wording.ts",
341
+ "sha256": "a1370fd4945aa378315fb698ce16cf361712e45d861ca52942cdce7008bee539"
342
+ },
339
343
  {
340
344
  "path": "src/adapters/cursor/transport-retry.ts",
341
345
  "sha256": "6e166875c6bfba66b115d39b155178935fb003b1c1861b5dd768cc8e15542d01"
@@ -354,7 +358,7 @@
354
358
  },
355
359
  {
356
360
  "path": "src/adapters/devin.ts",
357
- "sha256": "13c4afa1f3789e63ef73563ab56503970e38dc40c697877e81be8d4cc1e7c734"
361
+ "sha256": "3638c1041cb3fed3822c1d719b0a30945818a1110fddab8f3a9033f35a638f49"
358
362
  },
359
363
  {
360
364
  "path": "src/adapters/devin/cloud-direct/auth.ts",
@@ -578,7 +582,7 @@
578
582
  },
579
583
  {
580
584
  "path": "src/adapters/openai-chat/messages.ts",
581
- "sha256": "6840534bae4cd81f39963725e9107eb6c8157712b53fc227aee9eec7a11e8023"
585
+ "sha256": "830502eb0f9875a20fbaead85a592c6c8b2445e48ad0742aac0cc875d49c966f"
582
586
  },
583
587
  {
584
588
  "path": "src/adapters/openai-chat/parallel-tool-calls.ts",
@@ -950,7 +954,7 @@
950
954
  },
951
955
  {
952
956
  "path": "src/claude/inbound.ts",
953
- "sha256": "be92d4d81f55b72f3ba6dbe06d9015aa6562c48a47f9c93d05f83f0517cf1fcd"
957
+ "sha256": "f4093f29fe4635fd797d7d6fb6bd41484b4320a4b89b9ac3f1eb04525cd07166"
954
958
  },
955
959
  {
956
960
  "path": "src/claude/intercept/cli-catalog.ts",
@@ -1990,7 +1994,7 @@
1990
1994
  },
1991
1995
  {
1992
1996
  "path": "src/codex/autostart-health.ts",
1993
- "sha256": "bbd30686f19e73d40e1b3fbdb7cd20d9439459d19b4697c8410dac58bcb1b239"
1997
+ "sha256": "58e8a3abced23821864f9f1a81cc9f48f55e7e0053eb057f0f163ef45798e235"
1994
1998
  },
1995
1999
  {
1996
2000
  "path": "src/codex/catalog-admission.ts",
@@ -2386,7 +2390,7 @@
2386
2390
  },
2387
2391
  {
2388
2392
  "path": "src/codex/journal.ts",
2389
- "sha256": "a9bd5e04eef84c7c89a8f83bec07450d2a2fadf8809cdea9c17d5fea20cd4885"
2393
+ "sha256": "cdd5c61028cc9d411b0faeed0a0246f133818ec77110116ccfe3e89a98b2bec8"
2390
2394
  },
2391
2395
  {
2392
2396
  "path": "src/codex/legacy-config-keys.ts",
@@ -2798,7 +2802,7 @@
2798
2802
  },
2799
2803
  {
2800
2804
  "path": "src/codex/shim-overlay.ts",
2801
- "sha256": "ad34f2af4f374d6867e44e839b2baeefb3af25e558a445422ed97339eb2eeefc"
2805
+ "sha256": "1df24cd1b1751ebdc642ad7262bf217d3c31cc0db850ec9917e4f8776d44ca87"
2802
2806
  },
2803
2807
  {
2804
2808
  "path": "src/codex/shim-path-resolution.ts",
@@ -2906,7 +2910,7 @@
2906
2910
  },
2907
2911
  {
2908
2912
  "path": "src/combos/jev.ts",
2909
- "sha256": "35b32a363251ecb148c90b54ee2588cb5e880354de984b7243fd29dcf9582177"
2913
+ "sha256": "1d824e402ef1598769dbf90e7d6366f381eea5dd1976c14ca50cd058523d4ff3"
2910
2914
  },
2911
2915
  {
2912
2916
  "path": "src/combos/request.ts",
@@ -5614,7 +5618,7 @@
5614
5618
  },
5615
5619
  {
5616
5620
  "path": "src/server/index.ts",
5617
- "sha256": "72b72c4997af595f0add789b75ec18135a5bac886f16113d6e62fd3e8d459cb0"
5621
+ "sha256": "b2440e8a896fa0ffc6084e2a08e10d8e5f0b1d4e5b10131abaf274d3b7dc34db"
5618
5622
  },
5619
5623
  {
5620
5624
  "path": "src/server/index/bounded-request.ts",
@@ -5998,7 +6002,7 @@
5998
6002
  },
5999
6003
  {
6000
6004
  "path": "src/server/messages-native-scope.ts",
6001
- "sha256": "c529062519b72e9ee5763663b646138726c51380822864b87d6cfad88f7f0297"
6005
+ "sha256": "5f7894d80c9380e537805bd7dbdb03fb7a1d88afd903e5ca28727594955ca566"
6002
6006
  },
6003
6007
  {
6004
6008
  "path": "src/server/messages-native.ts",
@@ -6204,6 +6208,10 @@
6204
6208
  "path": "src/server/responses/collaboration.ts",
6205
6209
  "sha256": "3f9b70ccfcac8d786382c7f205d00eb49fe0d9d2d6a73c86d15d0e92c5594f3c"
6206
6210
  },
6211
+ {
6212
+ "path": "src/server/responses/combo-requested-effort.ts",
6213
+ "sha256": "dea38eb1d677c32b38656a52b3b69be576ca9b69be33849a683e3b519c6dfbd8"
6214
+ },
6207
6215
  {
6208
6216
  "path": "src/server/responses/combo-session-recall.ts",
6209
6217
  "sha256": "f40d4c2fa23ee4203e7805e127b16bcc76d46370798fbd90343cba46ff7d4e49"
@@ -6254,7 +6262,7 @@
6254
6262
  },
6255
6263
  {
6256
6264
  "path": "src/server/responses/core-combo.ts",
6257
- "sha256": "4ede978a89dc8fc04e3797516443ae952981d04a3cd093d2d17c3a00f01793e6"
6265
+ "sha256": "a33687650dabc3f18dda5cd6babfab2f14e4df3c5ad97aa31a1c1e79f964afae"
6258
6266
  },
6259
6267
  {
6260
6268
  "path": "src/server/responses/core-errors.ts",
@@ -6550,7 +6558,7 @@
6550
6558
  },
6551
6559
  {
6552
6560
  "path": "src/service/desktop-startup.ts",
6553
- "sha256": "b20f9d4c158906832c25f0ea2577215745d2d75e8d3cf03a93dac31ed14c4389"
6561
+ "sha256": "367773dc614bc4c4273d5d92150274da758a1656a5fa43d9a4939619d6de32a0"
6554
6562
  },
6555
6563
  {
6556
6564
  "path": "src/service/diagnostics.ts",
@@ -7148,13 +7156,17 @@
7148
7156
  "path": "src/web-search/gemini-executor.ts",
7149
7157
  "sha256": "ad13a59824c0b3bf504e06a72558cedebf0e1c8d1ddab3a6cf2222800b68dfe8"
7150
7158
  },
7159
+ {
7160
+ "path": "src/web-search/header-deadline.ts",
7161
+ "sha256": "ec30e9fb56c13966cfc2405879ede715f86c59c49ddd16f32155b64969749394"
7162
+ },
7151
7163
  {
7152
7164
  "path": "src/web-search/index.ts",
7153
7165
  "sha256": "719c325b6843e300f6177113c3afeacc252349c95acbca70dd09c5247fe4b4a4"
7154
7166
  },
7155
7167
  {
7156
7168
  "path": "src/web-search/loop.ts",
7157
- "sha256": "88cb405912a912c2ccf4cea4261ab727aa4d01b1b49f57081f7cf5d926d6b94b"
7169
+ "sha256": "bfe6ae27084edd5848a4ecf7b11f5ca69ddf5cb63df1e204e820d7fa3fef1eed"
7158
7170
  },
7159
7171
  {
7160
7172
  "path": "src/web-search/ollama-executor.ts",
@@ -463,21 +463,19 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
463
463
  return serveGuiFile(rawPath) !== null;
464
464
  }
465
465
 
466
- // Codex treats empty / non-JSON 503 bodies as "Unknown error" (#452). Keep Retry-After and
467
- // the server_is_overloaded code so clients can back off, but always return a JSON envelope.
468
- // These two run BEFORE the auth/origin checks, so they need the receiving listener's policy
469
- // explicitly (#1102). Reaching for the shared `config` here would attach public-policy CORS
470
- // headers to a 503 on the loopback listener — no model runs and no credential is spent, but
471
- // it is the one error path that would answer a rebinding origin with its own origin echoed
472
- // back.
466
+ // Codex maps 503 + server_is_overloaded to ServerOverloaded ("model at capacity"), retried
467
+ // only with retry advice that older clients ignore (#6642); server_restarting is a
468
+ // retryable UnexpectedStatus in every version and never reads as model capacity.
469
+ // Keep JSON (empty / non-JSON 503 means "Unknown error", #452) and Retry-After.
470
+ // Drain and busy run BEFORE auth/origin checks: use the receiving listener's policy
471
+ // (#1102), because shared public `config` could echo a rebinding origin on loopback.
473
472
  function drainingResponse(req: Request, policy: RequestPolicyView): Response {
474
- const response = formatErrorResponse(503, "server_error", "Service shutting down");
475
- const headers = new Headers(response.headers);
476
- for (const [name, value] of Object.entries(corsHeaders(req, policy))) {
477
- headers.set(name, value);
478
- }
479
- headers.set("Retry-After", "5");
480
- return new Response(response.body, { status: 503, headers });
473
+ return withCors(new Response(JSON.stringify({
474
+ error: { type: "server_error", code: "server_restarting", message: "OpenCodex is restarting; retry this request." },
475
+ }), {
476
+ status: 503,
477
+ headers: { "Content-Type": "application/json", "Retry-After": "5" },
478
+ }), req, policy);
481
479
  }
482
480
 
483
481
  function serverBusyResponse(req: Request, resource: string, policy: RequestPolicyView): Response {
@@ -36,7 +36,10 @@ export function nativeMessagesToolScopeDenial(
36
36
  const definition = tool.definition;
37
37
  if (typeof definition.name !== "string") { anonymous.push(definition); continue; }
38
38
  // A temporary replacement keeps the declaration it shadows in scope as well.
39
- definitions.set(definition.name, permanent ? [definition] : [...definitions.get(definition.name) ?? [], definition]);
39
+ // These arrays belong to this check, not to the caller's declarations.
40
+ const declarations = permanent ? [] : definitions.get(definition.name) ?? [];
41
+ declarations.push(definition);
42
+ definitions.set(definition.name, declarations);
40
43
  withdrawn.delete(definition.name);
41
44
  } else if (tool.type === "tool_reference" && typeof tool.name === "string") {
42
45
  if (block.type === "tool_removal" && permanent) withdrawn.add(tool.name);
@@ -0,0 +1,14 @@
1
+ /** Restore the caller label while retaining JEV's forced effort and later child transitions. */
2
+ export function comboRequestedEffortLabel(
3
+ originalRequestedEffort: string | undefined,
4
+ childRequestedEffort: string | undefined,
5
+ forcedEffort?: string | null,
6
+ ): string | undefined {
7
+ if (originalRequestedEffort === undefined) return childRequestedEffort;
8
+ const transitionIndex = childRequestedEffort?.indexOf("->") ?? -1;
9
+ const laterTransitions = transitionIndex >= 0 ? childRequestedEffort!.slice(transitionIndex) : "";
10
+ const forcedTransition = forcedEffort != null && forcedEffort !== originalRequestedEffort
11
+ ? `->${forcedEffort}`
12
+ : "";
13
+ return `${originalRequestedEffort}${forcedTransition}${laterTransitions}`;
14
+ }
@@ -106,6 +106,7 @@ import { mandatoryResponsesReasoningReplayUnavailable } from "./core-replay";
106
106
  import { settleOperatorReplacement } from "../../lib/upstream-retry";
107
107
  import { createComboProtocolLanes, dispatchNativeComboChild } from "./core-combo-native";
108
108
  import { createJevModelInvoker } from "./jev-model-invoke";
109
+ import { comboRequestedEffortLabel } from "./combo-requested-effort";
109
110
  import { clientWireOf } from "../inference/client-wire";
110
111
 
111
112
  /**
@@ -656,13 +657,9 @@ export async function executeComboResponses(
656
657
  && isDeclaredReasoningEffort(originalRequestedEffortValue)
657
658
  ? originalRequestedEffortValue
658
659
  : undefined;
659
- const restoreOriginalRequestedEffort = (childLog: RequestLogContext): void => {
660
+ const restoreOriginalRequestedEffort = (childLog: RequestLogContext, forcedEffort?: string | null): void => {
660
661
  if (originalRequestedEffort === undefined) return;
661
- const normalizedRequestedEffort = childLog.requestedEffort;
662
- const transitionIndex = normalizedRequestedEffort?.indexOf("->") ?? -1;
663
- childLog.requestedEffort = transitionIndex >= 0
664
- ? `${originalRequestedEffort}${normalizedRequestedEffort!.slice(transitionIndex)}`
665
- : originalRequestedEffort;
662
+ childLog.requestedEffort = comboRequestedEffortLabel(originalRequestedEffort, childLog.requestedEffort, forcedEffort);
666
663
  recordAttemptRequestedEffort(childLog);
667
664
  };
668
665
 
@@ -907,7 +904,8 @@ export async function executeComboResponses(
907
904
  onNativePassthroughCancel: callbackGate.onCancel,
908
905
  onResponseComplete: callbackGate.onResponseComplete,
909
906
  });
910
- restoreOriginalRequestedEffort(childLog);
907
+ // Native Chat rebuilds its own body and does not apply the initial JEV effort.
908
+ restoreOriginalRequestedEffort(childLog, nativeChild ? undefined : initialJevDecision?.effort);
911
909
  } catch (error) {
912
910
  callbackGate.discard();
913
911
  if (options.abortSignal?.aborted) {
@@ -1,7 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { accessSync, constants, readFileSync, realpathSync } from "node:fs";
2
+ import { accessSync, constants, readFileSync, readlinkSync, realpathSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { dirname, join } from "node:path";
4
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
5
5
  import { readPid } from "../config/process-state";
6
6
  import { resolveServiceOwnership, sameServiceOwnershipSubject, type ServiceOwnershipResolution } from "./state";
7
7
 
@@ -29,11 +29,28 @@ interface DesktopStartupDeps {
29
29
  ownership?: () => ServiceOwnershipResolution;
30
30
  readPid?: () => number | null;
31
31
  run?: typeof run;
32
+ env?: NodeJS.ProcessEnv;
33
+ proc?: ProcReader;
32
34
  }
33
35
 
36
+ /** Linux process identity from procfs: executable target and parent pid. */
37
+ interface ProcReader {
38
+ exe(pid: number): string;
39
+ parent(pid: number): number;
40
+ }
41
+ const procfs: ProcReader = {
42
+ exe: pid => readlinkSync(`/proc/${pid}/exe`),
43
+ // comm may contain spaces or parentheses, so fields are counted after the last ')'.
44
+ parent: pid => {
45
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
46
+ return Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
47
+ },
48
+ };
49
+
34
50
  /** Cheap ownership-only snapshot: no launchd/process probes on the server request path. */
35
51
  export function desktopStartupOwnership(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
36
- if ((deps.platform ?? process.platform) !== "darwin") return undefined;
52
+ const platform = deps.platform ?? process.platform;
53
+ if (platform !== "darwin" && platform !== "linux") return undefined;
37
54
  const owner = (deps.ownership ?? resolveServiceOwnership)();
38
55
  return owner.kind === "owned" && owner.ownership.owner === "desktop"
39
56
  ? deriveDesktopStartup({ owned: true, loginEnabled: false, running: false }) : undefined;
@@ -91,3 +108,83 @@ export function diagnoseMacDesktopStartup(deps: DesktopStartupDeps = {}): Deskto
91
108
  return deriveDesktopStartup({ ...facts, running: false });
92
109
  }
93
110
  }
111
+
112
+ /** HOME-based autostart entry written by the desktop app's login item (`tauri-plugin-autostart`). */
113
+ function linuxLoginApp(entry: string): string | null {
114
+ const fields = new Map<string, string>();
115
+ let section = "";
116
+ for (const raw of entry.split(/\r?\n/)) {
117
+ const line = raw.trim();
118
+ if (!line || line.startsWith("#")) continue;
119
+ if (line.startsWith("[")) { section = line; continue; }
120
+ const at = line.indexOf("=");
121
+ if (section === "[Desktop Entry]" && at > 0 && !fields.has(line.slice(0, at).trim())) {
122
+ fields.set(line.slice(0, at).trim(), line.slice(at + 1).trim());
123
+ }
124
+ }
125
+ if (fields.get("Type") !== "Application" || fields.get("Hidden") === "true"
126
+ || fields.get("X-GNOME-Autostart-enabled") === "false"
127
+ || ["OnlyShowIn", "NotShowIn", "TryExec"].some(key => fields.has(key))) return null;
128
+ // Credit only the backend's simple Exec form; desktop quoting/escaping/field codes are unsupported.
129
+ const app = /^([^\s"'\\%`><~|&;$*?#()]+) --autostart$/.exec(fields.get("Exec") ?? "")?.[1];
130
+ return app && isAbsolute(app) && basename(app) === "opencodex-desktop" ? app : null;
131
+ }
132
+
133
+ /** Read-only Linux desktop ownership, HOME login entry and live parent/child checks via procfs. */
134
+ export function diagnoseLinuxDesktopStartup(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
135
+ if ((deps.platform ?? process.platform) !== "linux") return undefined;
136
+ const ownership = deps.ownership ?? resolveServiceOwnership;
137
+ const owner = ownership();
138
+ if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return undefined;
139
+ const facts = { owned: true, loginEnabled: false, running: false };
140
+ const proc = deps.proc ?? procfs;
141
+ const pidReader = deps.readPid ?? readPid;
142
+ try {
143
+ const env = deps.env ?? process.env;
144
+ const home = deps.home ?? homedir();
145
+ const configured = env.XDG_CONFIG_HOME;
146
+ const config = configured && isAbsolute(configured) ? configured : join(home, ".config");
147
+ // auto-launch 0.5.0 writes autostart under HOME; Tauri app_config_dir uses XDG for install-id.
148
+ // A relocated XDG_CONFIG_HOME puts that entry outside the session's autostart search path.
149
+ if (resolve(config) !== resolve(home, ".config")) return deriveDesktopStartup(facts);
150
+ const capture = () => {
151
+ const id = readFileSync(join(config, "com.opencodex.desktop", "install-id"), "utf8");
152
+ if (id.trim() !== owner.ownership.installId) return null;
153
+ const entry = readFileSync(join(home, ".config", "autostart", "OpenCodex.desktop"), "utf8");
154
+ const login = linuxLoginApp(entry);
155
+ if (!login) return null;
156
+ const app = realpathSync(login);
157
+ if (basename(app) !== "opencodex-desktop") return null;
158
+ const proxy = realpathSync(join(dirname(app), "ocx"));
159
+ if (dirname(proxy) !== dirname(app) || basename(proxy) !== "ocx") return null;
160
+ accessSync(app, constants.X_OK);
161
+ accessSync(proxy, constants.X_OK);
162
+ const pid = pidReader();
163
+ const parent = pid !== null ? proc.parent(pid) : null;
164
+ const child = pid !== null ? realpathSync(proc.exe(pid)) : null;
165
+ const parentApp = parent !== null && parent > 1 ? realpathSync(proc.exe(parent)) : null;
166
+ const running = pid !== null && child === proxy && parentApp === app && pidReader() === pid;
167
+ const currentOwner = ownership();
168
+ if (currentOwner.kind === "unknown" || !sameServiceOwnershipSubject(owner, currentOwner)) return null;
169
+ return { id, entry, login, app, proxy, pid, parent, child, parentApp, running };
170
+ };
171
+ const first = capture();
172
+ if (!first) return deriveDesktopStartup(facts);
173
+ const final = capture();
174
+ // Re-read the entire evidence chain, including files, targets and the exact parent PID.
175
+ // A changed or unreadable snapshot cannot retain protection from the first read.
176
+ if (final && JSON.stringify(first) === JSON.stringify(final)) {
177
+ facts.loginEnabled = true;
178
+ facts.running = final.running;
179
+ }
180
+ return deriveDesktopStartup(facts);
181
+ } catch {
182
+ // Unreadable login or procfs evidence never grants protection or releases the claim.
183
+ return deriveDesktopStartup({ ...facts, running: false });
184
+ }
185
+ }
186
+
187
+ /** Platform dispatch for the full (probing) desktop startup diagnostic. */
188
+ export function diagnoseDesktopStartup(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
189
+ return (deps.platform ?? process.platform) === "linux" ? diagnoseLinuxDesktopStartup(deps) : diagnoseMacDesktopStartup(deps);
190
+ }
@@ -0,0 +1,67 @@
1
+ import { clearableDeadline } from "../lib/abort";
2
+ import { sendTrackingRequestSlot } from "../providers/request-pacing";
3
+ import type { ProviderFetch } from "../server/responses/fetch-helpers";
4
+
5
+ /** One iteration's cumulative header budget; only local pacing admission pauses its clock. */
6
+ export function pacingHeaderDeadline(timeoutMs: number, parent: AbortSignal) {
7
+ const controller = new AbortController();
8
+ const signal = AbortSignal.any([parent, controller.signal]);
9
+ let remainingMs = timeoutMs;
10
+ let armedAt = performance.now();
11
+ let current = clearableDeadline(remainingMs, parent);
12
+ const timeoutReason = current.timeoutReason;
13
+ let pacingWaits = 0;
14
+ let cleared = false;
15
+ const relayExpiry = () => {
16
+ if (current.didExpire()) controller.abort(timeoutReason);
17
+ };
18
+ current.signal.addEventListener("abort", relayExpiry, { once: true });
19
+
20
+ const pause = () => {
21
+ if (cleared || pacingWaits++ > 0) return;
22
+ remainingMs = Math.max(0, remainingMs - (performance.now() - armedAt));
23
+ current.clear();
24
+ current.signal.removeEventListener("abort", relayExpiry);
25
+ // A delayed timer task must not let an already-spent budget escape into the queue.
26
+ if (remainingMs === 0 && !signal.aborted) controller.abort(timeoutReason);
27
+ };
28
+ const resume = () => {
29
+ if (cleared || --pacingWaits > 0 || signal.aborted) return;
30
+ armedAt = performance.now();
31
+ current = clearableDeadline(remainingMs, parent);
32
+ current.signal.addEventListener("abort", relayExpiry, { once: true });
33
+ };
34
+
35
+ return {
36
+ signal,
37
+ didExpire: () => signal.aborted && signal.reason === timeoutReason,
38
+ clear: () => {
39
+ cleared = true;
40
+ current.clear();
41
+ current.signal.removeEventListener("abort", relayExpiry);
42
+ },
43
+ /** Preserve the pacing seams used by adapter physical-send helpers, without double admission. */
44
+ pacedFetch(executor: typeof globalThis.fetch): typeof globalThis.fetch {
45
+ const pacing = executor as ProviderFetch;
46
+ if (!pacing.waitForPacing) return executor;
47
+ const waitForPacing: NonNullable<ProviderFetch["waitForPacing"]> = async (sendSignal) => {
48
+ pause();
49
+ try {
50
+ return await pacing.waitForPacing!(sendSignal ? AbortSignal.any([signal, sendSignal]) : signal);
51
+ } finally {
52
+ resume();
53
+ }
54
+ };
55
+ const unpacedFetch = pacing.unpacedFetch ?? executor;
56
+ const wrapped = async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {
57
+ const slot = await waitForPacing(init?.signal ?? undefined);
58
+ return sendTrackingRequestSlot(slot, () => {
59
+ signal.throwIfAborted();
60
+ init?.signal?.throwIfAborted();
61
+ return unpacedFetch(input, init);
62
+ });
63
+ };
64
+ return Object.assign(wrapped, { preconnect: executor.preconnect, waitForPacing, unpacedFetch }) as ProviderFetch;
65
+ },
66
+ };
67
+ }
@@ -11,7 +11,7 @@ import { runXaiWebSearch, type XaiSearchOptions } from "./xai-executor";
11
11
  import { runGeminiWebSearch } from "./gemini-executor";
12
12
  import { runExaWebSearch } from "./exa-executor";
13
13
  import type { WebSearchBackendId } from "./index";
14
- import { clearableDeadline } from "../lib/abort";
14
+ import { pacingHeaderDeadline } from "./header-deadline";
15
15
  import { redactSecretString } from "../lib/redact";
16
16
  import { readBoundedResponseBody } from "../lib/bounded-body";
17
17
  import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait } from "../lib/upstream-retry";
@@ -455,10 +455,10 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
455
455
  ...(recoveringEmptyAnswer ? { options: { ...parsed.options, toolChoice: "none" as const } } : {}),
456
456
  context: { ...parsed.context, messages: iterMessages, tools: recoveringEmptyAnswer ? [] : allTools },
457
457
  };
458
- // One cumulative header deadline spans every pool-key 429 rotation in this model iteration.
458
+ // One cumulative header deadline spans key rotations and reset sends, excluding local pacing.
459
459
  // clear() stops only its timer after final headers; the direct turn signal remains attached to
460
460
  // the returned response body through AbortSignal.any().
461
- let headerDeadline = clearableDeadline(connectTimeoutMs, signal);
461
+ let headerDeadline = pacingHeaderDeadline(connectTimeoutMs, signal);
462
462
  try {
463
463
  /**
464
464
  * Build and fetch one web-search iteration on the given adapter, under the iteration
@@ -491,7 +491,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
491
491
  cachedRequest = request;
492
492
  cachedAdapter = requestAdapter;
493
493
  }
494
- const requestFetch = deps.fetchForRequest?.(request, iterParsed) ?? routedProviderFetch;
494
+ const requestFetch = headerDeadline.pacedFetch(deps.fetchForRequest?.(request, iterParsed) ?? routedProviderFetch);
495
495
  let response: Response;
496
496
  try {
497
497
  if (requestAdapter.fetchResponse) {
@@ -563,7 +563,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
563
563
  // The deliberate backoff must not consume the cumulative response-header deadline:
564
564
  // start a fresh one so the replay gets a new connect budget (504 stays reserved for real
565
565
  // upstream latency).
566
- headerDeadline = clearableDeadline(connectTimeoutMs, signal);
566
+ headerDeadline = pacingHeaderDeadline(connectTimeoutMs, signal);
567
567
  // Stall-watchdog seam between bounded retry fetches.
568
568
  yield { type: "heartbeat" };
569
569
  prepared = await fetchOnce(adapter, "rate-limit-429");