@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.
- package/gui/dist/assets/{App-BOUa-ek3.js → App-MbDVfUeC.js} +3 -3
- package/gui/dist/assets/{Tray-DUtzB4zF.js → Tray-CzkxK82A.js} +1 -1
- package/gui/dist/assets/{index-DwHNDF4s.js → index-UJt6npXk.js} +2 -2
- package/gui/dist/assets/{quota-summary-BuL4WIBo.js → quota-summary-DU3sD6Z-.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/cursor/live-transport.ts +5 -3
- package/src/adapters/cursor/native-exec-fs.ts +12 -9
- package/src/adapters/cursor/native-exec.ts +13 -5
- package/src/adapters/cursor/protobuf-request.ts +13 -7
- package/src/adapters/cursor/request-builder.ts +23 -10
- package/src/adapters/cursor/tool-definitions.ts +9 -6
- package/src/adapters/cursor/tool-guidance.ts +29 -8
- package/src/adapters/cursor/tool-naming.ts +40 -7
- package/src/adapters/cursor/tool-wording.ts +46 -0
- package/src/adapters/cursor.ts +3 -0
- package/src/adapters/devin.ts +25 -11
- package/src/adapters/openai-chat/messages.ts +3 -1
- package/src/claude/inbound.ts +9 -0
- package/src/codex/autostart-health.ts +3 -3
- package/src/codex/journal.ts +36 -5
- package/src/codex/shim-overlay.ts +9 -1
- package/src/combos/jev.ts +3 -1
- package/src/generated/compatibility-version.json +35 -23
- package/src/server/index.ts +12 -14
- package/src/server/messages-native-scope.ts +4 -1
- package/src/server/responses/combo-requested-effort.ts +14 -0
- package/src/server/responses/core-combo.ts +5 -7
- package/src/service/desktop-startup.ts +100 -3
- package/src/web-search/header-deadline.ts +67 -0
- package/src/web-search/loop.ts +5 -5
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
"files": [
|
|
7
7
|
{
|
|
8
8
|
"path": "bun.lock",
|
|
9
|
-
"sha256": "
|
|
9
|
+
"sha256": "7bceb5361a92a8401247dcf131a3e0a40fc2e2b1ededb3c6468e4d435c75fc8c"
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
"path": "package.json",
|
|
13
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
301
|
+
"sha256": "f6796726488c0ccfc81a705c9d179945d02e54a52a111766935671b50631cab7"
|
|
302
302
|
},
|
|
303
303
|
{
|
|
304
304
|
"path": "src/adapters/cursor/request-builder.ts",
|
|
305
|
-
"sha256": "
|
|
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": "
|
|
321
|
+
"sha256": "9a2aefcb82f7040f3581676fa6a8d63c9ca0352400dcf659cf3b664b63d489c0"
|
|
322
322
|
},
|
|
323
323
|
{
|
|
324
324
|
"path": "src/adapters/cursor/tool-guidance.ts",
|
|
325
|
-
"sha256": "
|
|
325
|
+
"sha256": "a4865e3854aadcd979229830c92dc15cc60ef355f574f727348a730d23ae97aa"
|
|
326
326
|
},
|
|
327
327
|
{
|
|
328
328
|
"path": "src/adapters/cursor/tool-naming.ts",
|
|
329
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
7169
|
+
"sha256": "bfe6ae27084edd5848a4ecf7b11f5ca69ddf5cb63df1e204e820d7fa3fef1eed"
|
|
7158
7170
|
},
|
|
7159
7171
|
{
|
|
7160
7172
|
"path": "src/web-search/ollama-executor.ts",
|
package/src/server/index.ts
CHANGED
|
@@ -463,21 +463,19 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
463
463
|
return serveGuiFile(rawPath) !== null;
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
-
// Codex
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/web-search/loop.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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 =
|
|
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 =
|
|
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");
|