@minhspark/codex-mcp-bridge 1.22.5 → 1.22.6
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/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +24 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install-native-relay.mjs +1 -1
- package/src/claude-bridge.mjs +1 -1
- package/src/index.mjs +31 -3
- package/src/native-relay-companion.mjs +1 -1
- package/src/thread-delivery.mjs +104 -18
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.22.6] - 2026-10-20
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Keep a confirmed Desktop creation identifiable when post-creation verification fails. `delegate_to_codex` and `start_codex_thread` now return the task's threadId, its deliveryId and "Task was accepted; do not resend" instead of a bare allowed-roots error that read as if nothing had been created. The confirmed threadId is kept even when a later account, receipt or scope check fails after the bridge waited for the task's workspace.
|
|
12
|
+
- Treat a filesystem root that Codex Desktop briefly reports as a newly created task's workspace, observed as `/` right after creation following an account switch, as "workspace not yet assigned" when it is not the expected project. Creation keeps observing until its deadline and then returns a pending receipt that keeps the accepted threadId; reply observation keeps polling. The tolerance lasts at most 120 seconds from creation, or from the first such report during reply observation, and the bridge acts on nothing at that path and grants no task access from it during that time.
|
|
13
|
+
- Report a pending creation whose task Desktop already confirmed as `deliveryStatus: accepted` instead of `unconfirmed`.
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- A root report that outlasts the grace period, any other workspace outside the allowed roots, a different project, an account change or a sender mismatch still fails closed. Sends and plain thread inspection keep the normal allowed-roots refusal for a root workspace; nothing is sent.
|
|
18
|
+
|
|
19
|
+
### Verification
|
|
20
|
+
|
|
21
|
+
- 10 regression tests reproduce the defect on v1.22.5 and pass with the fix: creation confirmation with a brief `/` report, a pending result at the deadline, grace expiry, an outside-root workspace, a later account change, reply observation in both the wait and snapshot paths, and two MCP-level `delegate_to_codex` tests through the relay companion.
|
|
22
|
+
- The full CI matrix passed on Windows, macOS and Linux with Node 22 and 24. A local run outside the sandbox passed 1406 tests with 0 failures.
|
|
23
|
+
- An independent adversarial review found no security regression; its four confirmed correctness findings were fixed before merge.
|
|
24
|
+
|
|
25
|
+
### Upgrade notes
|
|
26
|
+
|
|
27
|
+
- Reconnect the MCP server after updating and confirm the loaded version from the actual runtime before relying on the fix.
|
|
28
|
+
- A creation that is pending because Desktop has not reported its workspace continues with `wait_codex_reply` and the returned deliveryId. Do not resend the prompt or create the task again.
|
|
29
|
+
- Not yet exercised against a live Desktop account switch. A placeholder workspace other than a filesystem root still fails closed as before.
|
|
30
|
+
|
|
7
31
|
## [1.22.5] - 2026-10-19
|
|
8
32
|
|
|
9
33
|
### Fixed
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "codex-mcp-bridge",
|
|
4
|
-
"version": "1.22.
|
|
4
|
+
"version": "1.22.6",
|
|
5
5
|
"description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Bui Dang Minh",
|
|
@@ -23,7 +23,7 @@ import { enableCodexFullAccess, fullAccessEnabled } from "../src/codex-full-acce
|
|
|
23
23
|
|
|
24
24
|
exitForVersionRequest(import.meta.url);
|
|
25
25
|
|
|
26
|
-
const VERSION = "1.22.
|
|
26
|
+
const VERSION = "1.22.6";
|
|
27
27
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
28
28
|
const entry = path.join(root, "src", "mcp-supervisor.mjs");
|
|
29
29
|
const entryArgs = ["native-relay-companion.mjs"];
|
package/src/claude-bridge.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import { discoverClaudeProject } from "./claude-project-discovery.mjs";
|
|
|
30
30
|
|
|
31
31
|
exitForVersionRequest(import.meta.url);
|
|
32
32
|
|
|
33
|
-
const VERSION = "1.22.
|
|
33
|
+
const VERSION = "1.22.6";
|
|
34
34
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
35
35
|
const FORWARD_MIN_INTERVAL_MS = 5000;
|
|
36
36
|
const FORWARD_MAX_PER_SESSION = 50;
|
package/src/index.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import { PROJECT_SETUP_TOOLS, registerProjectOnboardingTools } from "./project-o
|
|
|
40
40
|
|
|
41
41
|
exitForVersionRequest(import.meta.url);
|
|
42
42
|
|
|
43
|
-
const VERSION = "1.22.
|
|
43
|
+
const VERSION = "1.22.6";
|
|
44
44
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
45
45
|
const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
|
|
46
46
|
|
|
@@ -211,6 +211,25 @@ function threadNameFor({ cwd, prompt, name }) {
|
|
|
211
211
|
);
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Once a Desktop creation was dispatched, a later verification failure must
|
|
216
|
+
* never read like a refusal: the caller keeps the task and receipt identity
|
|
217
|
+
* and is told not to resend. Verification still fails closed; only the report
|
|
218
|
+
* changes.
|
|
219
|
+
*/
|
|
220
|
+
function creationVerificationFailure({ created, deliveryId, requestId, err }) {
|
|
221
|
+
const threadId = created.threadId ?? err?.threadId ?? null;
|
|
222
|
+
const lines = threadId
|
|
223
|
+
? ["Codex Desktop accepted the task, but post-creation verification failed.", `threadId: ${threadId}`]
|
|
224
|
+
: ["Codex Desktop creation was dispatched, but its outcome is not yet confirmed."];
|
|
225
|
+
lines.push(...(requestId ? [`requestId: ${requestId}`] : []), `cwd: ${created.cwd}`, ...(deliveryId ? [`deliveryId: ${deliveryId}`] : []), `error: ${err?.message ?? String(err)}`,
|
|
226
|
+
threadId ? "Task was accepted; do not resend. Resolve the verification error, then continue observing with wait_codex_reply and this deliveryId." : "Do not resend or create again. Continue observing with wait_codex_reply and this deliveryId.");
|
|
227
|
+
return {
|
|
228
|
+
...textResult(lines.join("\n"), true),
|
|
229
|
+
structuredContent: { ...failure(err).structuredContent, threadId, deliveryStatus: threadId ? "accepted" : "unconfirmed", ...(deliveryId ? { deliveryId } : {}), nextAction: deliveryId ? "wait_codex_reply" : "none" },
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
214
233
|
async function delegateDesktopTask({ cwd, prompt, name, requestId, model, effort, timeoutSec, openInApp, waitForReply = true }) {
|
|
215
234
|
const deadline = desktopOperation.getStore()?.deadline ?? Date.now() + Math.min((timeoutSec ?? 40) * 1000, DESKTOP_TOOL_BUDGET_MS);
|
|
216
235
|
const workspace = resolveWorkspacePath(cwd);
|
|
@@ -222,8 +241,17 @@ async function delegateDesktopTask({ cwd, prompt, name, requestId, model, effort
|
|
|
222
241
|
const deliveryId = created.deliveryId;
|
|
223
242
|
let delivered = deliveryId ? replyReceipts.read(deliveryId, desktopOperation.getStore()) : null;
|
|
224
243
|
if (delivered?.creationObservation) {
|
|
225
|
-
|
|
226
|
-
|
|
244
|
+
let resolved;
|
|
245
|
+
try {
|
|
246
|
+
resolved = await desktopTasks.resolveCreation(delivered, { deadline });
|
|
247
|
+
} catch (err) {
|
|
248
|
+
return creationVerificationFailure({ created, deliveryId, requestId, err });
|
|
249
|
+
}
|
|
250
|
+
if (!resolved) {
|
|
251
|
+
const threadId = created.threadId ?? null;
|
|
252
|
+
return replyResult({ ...delivered, threadId, ...(threadId ? { deliveryStatus: "accepted" } : {}) }, deliveryId, { threadId, status: "timeout", responseStatus: "unavailable", text: "" },
|
|
253
|
+
[...(threadId ? [`threadId: ${threadId}`, "Task was accepted; Codex Desktop has not yet confirmed its workspace or creation receipt."] : []), "Creation acknowledgement pending; do not create again."]);
|
|
254
|
+
}
|
|
227
255
|
delivered = resolved;
|
|
228
256
|
created.threadId = resolved.threadId;
|
|
229
257
|
}
|
|
@@ -38,7 +38,7 @@ import { NativeCreationReceipts, creationIdValid } from "./native-creation-recei
|
|
|
38
38
|
|
|
39
39
|
exitForVersionRequest(import.meta.url);
|
|
40
40
|
|
|
41
|
-
const VERSION = "1.22.
|
|
41
|
+
const VERSION = "1.22.6";
|
|
42
42
|
const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
|
|
43
43
|
|
|
44
44
|
function errorResponse(code, message, sent, details) {
|
package/src/thread-delivery.mjs
CHANGED
|
@@ -26,6 +26,8 @@ import { requireClaudeSenderContext } from "./claude-sender-context.mjs";
|
|
|
26
26
|
export const NATIVE_BACKEND = "codex-desktop-native";
|
|
27
27
|
export const APP_SERVER_BACKEND = "app-server";
|
|
28
28
|
export const DESKTOP_TOOL_BUDGET_MS = 40000;
|
|
29
|
+
export const WORKSPACE_UNASSIGNED = "DESKTOP_WORKSPACE_UNASSIGNED";
|
|
30
|
+
export const UNASSIGNED_WORKSPACE_GRACE_MS = 120000;
|
|
29
31
|
const RELEASE_STATUSES = new Set(["completed", "interrupted", "failed"]);
|
|
30
32
|
const METADATA_READ_OPTIONS = { includeOutputs: false, maxOutputCharsPerItem: 1 };
|
|
31
33
|
|
|
@@ -35,6 +37,17 @@ function unavailableWaitRenderer(error) {
|
|
|
35
37
|
/^(Codex app tool request failed\.?|The app tool call ended before producing a result\.?)$/.test(cause.message);
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
function isFilesystemRoot(value) {
|
|
41
|
+
if (typeof value !== "string" || !path.isAbsolute(value)) return false;
|
|
42
|
+
const resolved = path.resolve(value);
|
|
43
|
+
return path.parse(resolved).root === resolved;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function acceptedCreationError(threadId, error) {
|
|
47
|
+
if (error.acceptedCreation) return error;
|
|
48
|
+
return Object.assign(new Error(`Codex Desktop created task ${threadId}, but its post-creation verification failed: ${error.message}. threadId: ${threadId}. Task was accepted; do not resend. Its reply was withheld until the verification error is resolved.`, { cause: error }), { code: "DESKTOP_CREATION_VERIFICATION_FAILED", threadId, acceptedCreation: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
function sameAccountContext(expected, current) {
|
|
39
52
|
return Boolean(expected && current && ["claude", "codex"].every((provider) =>
|
|
40
53
|
typeof expected[provider] === "string" && expected[provider] === current[provider]));
|
|
@@ -75,6 +88,8 @@ export class DesktopTaskDelivery {
|
|
|
75
88
|
this.threadOperations = new Map();
|
|
76
89
|
}
|
|
77
90
|
|
|
91
|
+
#unassignedSince = new Map();
|
|
92
|
+
|
|
78
93
|
senderProjectScope(cwd) {
|
|
79
94
|
if (!this.senderContext) return null;
|
|
80
95
|
const sender = requireClaudeSenderContext(this.senderContext());
|
|
@@ -329,6 +344,15 @@ export class DesktopTaskDelivery {
|
|
|
329
344
|
async resolveCreation(delivered, { deadline = this.now() + DESKTOP_TOOL_BUDGET_MS } = {}) {
|
|
330
345
|
const binding = delivered.creationObservation;
|
|
331
346
|
if (!binding) return delivered;
|
|
347
|
+
let confirmedThreadId = null;
|
|
348
|
+
try {
|
|
349
|
+
return await this.#resolveCreationLoop(delivered, binding, deadline, (threadId) => { confirmedThreadId = threadId; });
|
|
350
|
+
} catch (error) {
|
|
351
|
+
throw confirmedThreadId ? acceptedCreationError(confirmedThreadId, error) : error;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async #resolveCreationLoop(delivered, binding, deadline, onConfirmed) {
|
|
332
356
|
for (;;) {
|
|
333
357
|
if (this.now() >= deadline) return null;
|
|
334
358
|
this.security.assertCwd(delivered.cwd);
|
|
@@ -345,17 +369,29 @@ export class DesktopTaskDelivery {
|
|
|
345
369
|
const response = row.result, threadId = response?.threadId;
|
|
346
370
|
if (typeof threadId !== "string" || !threadId.trim() || response.hostId !== "local" || response.status === "outcome-unknown" ||
|
|
347
371
|
response.firstTurn && response.firstTurn.status !== "accepted" || receipt.threadId && receipt.threadId !== threadId) throw new Error("Native creation is not confirmed; do not resend");
|
|
372
|
+
onConfirmed(threadId);
|
|
348
373
|
let inspected;
|
|
349
|
-
try { inspected = await this.#inspectMetadata(threadId, delivered.cwd, { deadline }); }
|
|
350
|
-
catch (error) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
374
|
+
try { inspected = await this.#inspectMetadata(threadId, delivered.cwd, { deadline, tolerance: { since: receipt.startedAt } }); }
|
|
375
|
+
catch (error) {
|
|
376
|
+
if (error.code === WORKSPACE_UNASSIGNED) {
|
|
377
|
+
await this.sleep(Math.min(1000, Math.max(0, deadline - this.now())));
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (this.now() >= deadline && /deadline|within \d+ms/.test(error.message)) return null;
|
|
381
|
+
throw acceptedCreationError(threadId, error);
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
if (inspected.executorThreadId !== envelope.executorThreadId) throw new Error("Native creation executor changed");
|
|
385
|
+
if (!sameAccountContext(binding.accountContext, this.accountContext?.())) throw new Error("Creation receipt account changed");
|
|
386
|
+
this.security.assertCwd(delivered.cwd);
|
|
387
|
+
this.senderProjectScope(delivered.cwd);
|
|
388
|
+
if (this.security.hardenedRoots?.mode === "project-policy") this.security.hardenedRoots.recheck(inspected.scopeBinding);
|
|
389
|
+
if (inspected.projectScope) recheckProjectScope(inspected.projectScope);
|
|
390
|
+
this.security.registerThread(threadId);
|
|
391
|
+
this.security.assertThread(threadId, inspected.thread.cwd);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
throw acceptedCreationError(threadId, error);
|
|
394
|
+
}
|
|
359
395
|
return { threadId, cwd: delivered.cwd, previousTurnId: null, deliveryStatus: "accepted",
|
|
360
396
|
responseObservation: { operation: "create_thread", threadId, previousTurnId: null, expectedCwd: delivered.cwd,
|
|
361
397
|
executorThreadId: envelope.executorThreadId, prompt: binding.args.prompt, accountContext: binding.accountContext } };
|
|
@@ -366,18 +402,18 @@ export class DesktopTaskDelivery {
|
|
|
366
402
|
}
|
|
367
403
|
}
|
|
368
404
|
|
|
369
|
-
async inspect(threadId, cwd, { deadline } = {}) {
|
|
370
|
-
const inspected = await this.#inspectMetadata(threadId, cwd, { deadline });
|
|
405
|
+
async inspect(threadId, cwd, { deadline, tolerateUnassigned = false } = {}) {
|
|
406
|
+
const inspected = await this.#inspectMetadata(threadId, cwd, { deadline, ...(tolerateUnassigned ? { tolerance: {} } : {}) });
|
|
371
407
|
this.security.assertThread(threadId, inspected.thread.cwd);
|
|
372
408
|
return inspected;
|
|
373
409
|
}
|
|
374
410
|
|
|
375
|
-
async #inspectMetadata(threadId, cwd, { deadline } = {}) {
|
|
411
|
+
async #inspectMetadata(threadId, cwd, { deadline, tolerance } = {}) {
|
|
376
412
|
const envelope = await this.request("read_thread", { threadId, hostId: "local", turnLimit: 1, ...METADATA_READ_OPTIONS }, { deadline, includeRelayContext: true });
|
|
377
413
|
const response = envelope.result;
|
|
378
414
|
const thread = response?.thread;
|
|
379
415
|
if (thread?.id !== threadId || thread.hostId !== "local" || !thread.cwd) throw new Error("Desktop did not confirm the task's local workspace.");
|
|
380
|
-
this
|
|
416
|
+
this.#assertReportedWorkspace(threadId, thread.cwd, cwd, tolerance);
|
|
381
417
|
const projectScope = this.senderProjectScope(thread.cwd);
|
|
382
418
|
if (cwd && path.relative(realpathSync.native(cwd), realpathSync.native(thread.cwd))) {
|
|
383
419
|
throw new Error("Native Desktop delivery cannot change an existing task's workspace; create a new task at the requested cwd.");
|
|
@@ -386,6 +422,39 @@ export class DesktopTaskDelivery {
|
|
|
386
422
|
return { thread, latestTurnId: response.turns?.[0]?.id ?? null, projectScope, scopeBinding, executorThreadId: envelope.executorThreadId ?? null };
|
|
387
423
|
}
|
|
388
424
|
|
|
425
|
+
/**
|
|
426
|
+
* Codex Desktop can briefly report a filesystem root as a task's workspace
|
|
427
|
+
* before it finishes assigning the task's saved project (observed as "/"
|
|
428
|
+
* right after creation, following an account switch). Only observers that
|
|
429
|
+
* pass a tolerance - creation confirmation and reply observation, both of
|
|
430
|
+
* which already know the expected workspace - treat a root that differs from
|
|
431
|
+
* that workspace as "not yet assigned", and only for a bounded grace period
|
|
432
|
+
* measured from creation or from the first such report. During it the bridge
|
|
433
|
+
* acts on nothing at that path, grants no access and keeps observing. After
|
|
434
|
+
* it, and for every other caller or path, the normal fail-closed
|
|
435
|
+
* allowed-roots check applies.
|
|
436
|
+
*/
|
|
437
|
+
#assertReportedWorkspace(threadId, reported, expected, tolerance) {
|
|
438
|
+
if (!isFilesystemRoot(reported)) this.#unassignedSince.delete(threadId);
|
|
439
|
+
else if (tolerance && expected && !this.#isExpectedWorkspace(reported, expected)) {
|
|
440
|
+
const now = this.now();
|
|
441
|
+
if (tolerance.since === undefined && !this.#unassignedSince.has(threadId)) this.#unassignedSince.set(threadId, now);
|
|
442
|
+
const since = tolerance.since ?? this.#unassignedSince.get(threadId);
|
|
443
|
+
if (now - since < UNASSIGNED_WORKSPACE_GRACE_MS) {
|
|
444
|
+
throw Object.assign(new Error(`Codex Desktop has not yet reported task ${threadId}'s project workspace (it currently reports ${reported}); the bridge did not act on that path.`), { code: WORKSPACE_UNASSIGNED, threadId });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
this.security.assertCwd(reported);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
#isExpectedWorkspace(reported, expected) {
|
|
451
|
+
try {
|
|
452
|
+
return path.relative(realpathSync.native(expected), realpathSync.native(reported)) === "";
|
|
453
|
+
} catch {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
389
458
|
async send({ threadId, prompt, cwd, model, effort, name, deadline, onPrepared }) {
|
|
390
459
|
const inspected = await this.inspect(threadId, cwd, { deadline });
|
|
391
460
|
const scopeBinding = this.security.hardenedRoots?.mode === "project-policy" ? this.security.hardenedRoots.capture(inspected.thread.cwd) : undefined;
|
|
@@ -445,12 +514,12 @@ export class DesktopTaskDelivery {
|
|
|
445
514
|
if (realpathSync.native(expectedCwd) !== expectedCwd) throw new Error("The selected native task workspace changed while its response was being observed");
|
|
446
515
|
};
|
|
447
516
|
await recheck();
|
|
448
|
-
const initial = await this.inspect(threadId, expectedCwd, { deadline });
|
|
517
|
+
const initial = await this.inspect(threadId, expectedCwd, { deadline, tolerateUnassigned: true });
|
|
449
518
|
if (!resolveDispatch && initial.latestTurnId !== turnId) throw new Error("The selected native task changed before its response could be read; reply content was withheld");
|
|
450
519
|
const read = resolveDispatch ? this.resolveResponse : responseObservation.operation === "create_thread" ? readCodexNativeCreationResponse : this.readResponse;
|
|
451
520
|
const observed = read({ ...responseObservation, threadId, turnId, desktopEvidence: initial });
|
|
452
521
|
await recheck();
|
|
453
|
-
const inspected = await this.inspect(threadId, expectedCwd, { deadline });
|
|
522
|
+
const inspected = await this.inspect(threadId, expectedCwd, { deadline, tolerateUnassigned: true });
|
|
454
523
|
if (inspected.latestTurnId !== (resolveDispatch ? initial.latestTurnId : turnId)) throw new Error("The selected native task changed before its response could be confirmed; reply content was withheld");
|
|
455
524
|
await recheck();
|
|
456
525
|
return observed;
|
|
@@ -485,7 +554,7 @@ export class DesktopTaskDelivery {
|
|
|
485
554
|
const result = await this.request("read_thread", { threadId, hostId: "local", turnLimit: 1, ...METADATA_READ_OPTIONS }, { deadline: startedAt + timeoutMs });
|
|
486
555
|
const thread = result?.thread;
|
|
487
556
|
if (thread?.id !== threadId || thread.hostId !== "local" || !thread.cwd) throw new Error("Desktop did not confirm the observed task's local workspace.");
|
|
488
|
-
this
|
|
557
|
+
this.#assertReportedWorkspace(threadId, thread.cwd, responseObservation?.expectedCwd, {});
|
|
489
558
|
this.security.assertThread(threadId, thread.cwd);
|
|
490
559
|
this.senderProjectScope(thread.cwd);
|
|
491
560
|
if (responseObservation?.expectedCwd && realpathSync.native(thread.cwd) !== responseObservation.expectedCwd) throw new Error("The selected native task workspace changed during observation");
|
|
@@ -497,14 +566,17 @@ export class DesktopTaskDelivery {
|
|
|
497
566
|
return this.now() - startedAt >= timeoutMs && (["DESKTOP_RESPONSE_DEADLINE", "RELAY_TIMEOUT"].includes(cause.code) ||
|
|
498
567
|
cause.code === "RELAY_UNREACHABLE" && cause.message === "The Codex Desktop relay send deadline elapsed before dispatch");
|
|
499
568
|
};
|
|
569
|
+
const unassigned = Symbol("workspace-unassigned");
|
|
500
570
|
const observe = async (selectedTurnId, resolveDispatch = false) => {
|
|
501
571
|
try {
|
|
502
572
|
return await this.observeNativeResponse(threadId, selectedTurnId, responseObservation, { deadline: startedAt + timeoutMs, resolveDispatch });
|
|
503
573
|
} catch (error) {
|
|
504
574
|
if (deadlineError(error)) return null;
|
|
575
|
+
if (error.code === WORKSPACE_UNASSIGNED) return unassigned;
|
|
505
576
|
throw error;
|
|
506
577
|
}
|
|
507
578
|
};
|
|
579
|
+
const pause = () => this.sleep(Math.min(1500, timeoutMs - (this.now() - startedAt)));
|
|
508
580
|
for (;;) {
|
|
509
581
|
if (this.now() - startedAt >= timeoutMs) return expired();
|
|
510
582
|
let response;
|
|
@@ -514,13 +586,19 @@ export class DesktopTaskDelivery {
|
|
|
514
586
|
}, { deadline: startedAt + timeoutMs });
|
|
515
587
|
} catch (err) {
|
|
516
588
|
if (deadlineError(err)) return expired();
|
|
589
|
+
if (err.code === WORKSPACE_UNASSIGNED) {
|
|
590
|
+
await pause();
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
517
593
|
if (readSnapshots || !unavailableWaitRenderer(err)) throw err;
|
|
518
594
|
readSnapshots = true;
|
|
519
595
|
try {
|
|
520
596
|
response = await readSnapshot();
|
|
521
597
|
} catch (readError) {
|
|
522
598
|
if (deadlineError(readError)) return expired();
|
|
523
|
-
throw readError;
|
|
599
|
+
if (readError.code !== WORKSPACE_UNASSIGNED) throw readError;
|
|
600
|
+
await pause();
|
|
601
|
+
continue;
|
|
524
602
|
}
|
|
525
603
|
}
|
|
526
604
|
const poll = response?.polls?.find((item) => item.thread?.id === threadId && item.thread?.hostId === "local");
|
|
@@ -533,6 +611,10 @@ export class DesktopTaskDelivery {
|
|
|
533
611
|
if (this.resolveResponse && responseObservation?.watermark?.status === "available" && responseObservation.operation !== "create_thread") {
|
|
534
612
|
const observed = await observe(null, true);
|
|
535
613
|
if (!observed) return expired();
|
|
614
|
+
if (observed === unassigned) {
|
|
615
|
+
await pause();
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
536
618
|
turnId = observed.turnId ?? null;
|
|
537
619
|
if (observed.status !== "pending") {
|
|
538
620
|
return {
|
|
@@ -559,6 +641,10 @@ export class DesktopTaskDelivery {
|
|
|
559
641
|
if (RELEASE_STATUSES.has(status)) {
|
|
560
642
|
const observed = await observe(turn.id);
|
|
561
643
|
if (!observed) return expired();
|
|
644
|
+
if (observed === unassigned) {
|
|
645
|
+
await pause();
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
562
648
|
const responseStatus = observed.status;
|
|
563
649
|
if (existingTurn && !observed.dispatchObserved && !["completed", "completed_no_reply"].includes(responseStatus)) {
|
|
564
650
|
turnId = null;
|