@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-mcp-bridge",
3
- "version": "1.22.5",
3
+ "version": "1.22.6",
4
4
  "description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
5
5
  "author": {
6
6
  "name": "Bui Dang Minh"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhspark/codex-mcp-bridge",
3
- "version": "1.22.5",
3
+ "version": "1.22.6",
4
4
  "description": "Two-way MCP bridge between Claude and Codex: prompts into a live Codex thread, messages into a running Claude Code session.",
5
5
  "keywords": [
6
6
  "mcp",
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.5",
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.5";
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"];
@@ -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.5";
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.5";
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
- const resolved = await desktopTasks.resolveCreation(delivered, { deadline });
226
- if (!resolved) return replyResult(delivered, deliveryId, { threadId: null, status: "timeout", responseStatus: "unavailable", text: "" }, ["Creation acknowledgement pending; do not create again."]);
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.5";
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) {
@@ -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) { if (this.now() >= deadline && /deadline|within \d+ms/.test(error.message)) return null; throw error; }
351
- if (inspected.executorThreadId !== envelope.executorThreadId) throw new Error("Native creation executor changed");
352
- if (!sameAccountContext(binding.accountContext, this.accountContext?.())) throw new Error("Creation receipt account changed");
353
- this.security.assertCwd(delivered.cwd);
354
- this.senderProjectScope(delivered.cwd);
355
- if (this.security.hardenedRoots?.mode === "project-policy") this.security.hardenedRoots.recheck(inspected.scopeBinding);
356
- if (inspected.projectScope) recheckProjectScope(inspected.projectScope);
357
- this.security.registerThread(threadId);
358
- this.security.assertThread(threadId, inspected.thread.cwd);
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.security.assertCwd(thread.cwd);
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.security.assertCwd(thread.cwd);
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;