@minhspark/codex-mcp-bridge 1.20.1 → 1.21.1

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 (53) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +41 -0
  3. package/README.md +57 -15
  4. package/assets/README.md +30 -0
  5. package/assets/agent-pets.svg +1 -0
  6. package/docs/daily-handoffs.md +106 -0
  7. package/docs/desktop-handoff-design.md +180 -0
  8. package/docs/new-machine-setup.md +112 -0
  9. package/docs/project-onboarding.md +122 -0
  10. package/package.json +9 -3
  11. package/plugin.json +1 -1
  12. package/scripts/bridge-projects.mjs +41 -0
  13. package/scripts/check.mjs +2 -1
  14. package/scripts/collect-repo-analytics.mjs +5 -2
  15. package/scripts/configure-message-automation.mjs +18 -0
  16. package/scripts/install-claude-code.mjs +77 -0
  17. package/scripts/install-claude-desktop.mjs +48 -5
  18. package/scripts/install-native-relay.mjs +6 -1
  19. package/scripts/install-npm-footer.mjs +13 -1
  20. package/scripts/publish-repo-analytics.mjs +24 -1
  21. package/src/app-server-client.mjs +5 -2
  22. package/src/bridge-readiness.mjs +23 -0
  23. package/src/card-settings-sync.mjs +125 -0
  24. package/src/claude-bridge.mjs +54 -20
  25. package/src/claude-code-registration.mjs +54 -0
  26. package/src/claude-sender-context.mjs +1 -1
  27. package/src/claude-session-creation.mjs +10 -2
  28. package/src/codex-mcp-registration.mjs +1 -1
  29. package/src/codex-native-response.mjs +139 -107
  30. package/src/codex-sender-context.mjs +62 -61
  31. package/src/desktop-reply-receipts.mjs +93 -0
  32. package/src/desktop-task-receipts.mjs +30 -7
  33. package/src/hardened-root-policy.mjs +5 -0
  34. package/src/index.mjs +97 -19
  35. package/src/mcp-supervisor.mjs +6 -2
  36. package/src/message-automation.mjs +92 -0
  37. package/src/native-creation-receipts.mjs +159 -0
  38. package/src/native-relay-companion.mjs +37 -6
  39. package/src/native-relay.mjs +12 -3
  40. package/src/peer-protocol.mjs +192 -18
  41. package/src/project-manager-registration.mjs +31 -0
  42. package/src/project-onboarding-tools.mjs +30 -0
  43. package/src/project-onboarding.mjs +174 -0
  44. package/src/project-policy.mjs +256 -0
  45. package/src/project-scope.mjs +33 -5
  46. package/src/prompt-guidance.mjs +2 -0
  47. package/src/release-snapshot.mjs +30 -5
  48. package/src/rollout-reader.mjs +59 -0
  49. package/src/rollout-segments.mjs +65 -0
  50. package/src/security-policy.mjs +4 -2
  51. package/src/sites-connector.mjs +17 -2
  52. package/src/telemetry.mjs +10 -8
  53. package/src/thread-delivery.mjs +134 -35
@@ -2,8 +2,10 @@ import { NativeDesktopRelay, accountRelaySocketPath, desktopTaskSocketPath, desk
2
2
  import { runTurn } from "./turn.mjs";
3
3
  import { realpathSync } from "node:fs";
4
4
  import path from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ import { creationRequestHash } from "./native-creation-receipts.mjs";
5
7
  import { DesktopTaskReceipts } from "./desktop-task-receipts.mjs";
6
- import { captureCodexRolloutWatermark, inspectCodexNativeTurn, readCodexNativeTurnResponse } from "./codex-native-response.mjs";
8
+ import { captureCodexRolloutWatermark, inspectCodexNativeTurn, readCodexNativeTurnResponse, readCodexNativeCreationResponse } from "./codex-native-response.mjs";
7
9
  import { captureProjectScope, recheckProjectScope } from "./project-scope.mjs";
8
10
  import { requireClaudeSenderContext } from "./claude-sender-context.mjs";
9
11
 
@@ -85,9 +87,10 @@ export class DesktopTaskDelivery {
85
87
  return inspected.projectScope;
86
88
  }
87
89
 
88
- async request(operation, args, { deadline, includeRelayContext = false, projectScope: expectedScope } = {}) {
90
+ async request(operation, args, { deadline, includeRelayContext = false, scopeBinding, creationReceiptId, projectScope: expectedScope } = {}) {
89
91
  try {
90
92
  await this.beforeRequest?.({ operation, args });
93
+ if (scopeBinding) this.security.hardenedRoots.recheck(scopeBinding);
91
94
  const projectScope = expectedScope ?? await this.mutationProjectScope(operation, args, { deadline });
92
95
  if (expectedScope) recheckProjectScope(expectedScope, this.senderProjectScope(expectedScope.recipient.input));
93
96
  const remaining = deadline === undefined ? undefined : deadline - this.now();
@@ -95,15 +98,17 @@ export class DesktopTaskDelivery {
95
98
  const accountContext = this.accountContext?.();
96
99
  const response = await this.relay.requestDesktop(operation, args, {
97
100
  ...(remaining === undefined ? {} : { timeoutMs: remaining }),
98
- ...(this.beforeRequest || projectScope ? { beforeSend: async () => {
101
+ ...(this.beforeRequest || projectScope || scopeBinding ? { beforeSend: async () => {
99
102
  await this.beforeRequest?.({ operation, args, phase: "write" });
100
103
  if (projectScope) {
101
104
  recheckProjectScope(projectScope);
102
105
  recheckProjectScope(projectScope, await this.mutationProjectScope(operation, args, { deadline }));
103
106
  }
104
107
  await this.beforeRequest?.({ operation, args, phase: "dispatch" });
108
+ if (scopeBinding) this.security.hardenedRoots.recheck(scopeBinding);
105
109
  } } : {}),
106
110
  ...(accountContext ? { accountContext } : {}),
111
+ ...(creationReceiptId ? { creationReceiptId } : {}),
107
112
  });
108
113
  return includeRelayContext ? { result: response.result, executorThreadId: response.executorThreadId } : response.result;
109
114
  } catch (err) {
@@ -175,12 +180,16 @@ export class DesktopTaskDelivery {
175
180
  return { rows, coverage: "Codex Desktop's recent/pinned snapshot; local Codex workspaces only. Agent-created tasks visible in the sidebar can be omitted; read a known task ID directly before assuming creation failed." };
176
181
  }
177
182
 
178
- async reuseReceipt(receipt, { cwd, promptHash, deadline, accountContext }) {
183
+ async reuseReceipt(receipt, { cwd, promptHash, deadline, accountContext, includeReplyReceipt }) {
179
184
  if (!receipt) return null;
180
185
  if (accountContext && (!receipt.accountContext || ["claude", "codex"].some((provider) => receipt.accountContext[provider] !== accountContext[provider]))) {
181
186
  throw new Error(`The existing Desktop creation receipt ${receipt.threadId ? `for task ${receipt.threadId} ` : ""}${receipt.accountContext ? "belongs to different accounts" : "has no verified original account binding"}. Its receipt and task ID were retained. No creation or prompt resend was attempted; inspect the explicit existing task before continuing.`);
182
187
  }
183
188
  if (path.relative(cwd, realpathSync.native(receipt.cwd))) throw new Error("The stored Desktop creation receipt belongs to another workspace. No prompt was sent.");
189
+ if (receipt.state === "unknown" && includeReplyReceipt && receipt.deliveryId) return {
190
+ threadId: null, cwd, deliveryId: receipt.deliveryId, deliveryStatus: "unconfirmed", reused: true, name: receipt.name,
191
+ projectId: receipt.projectId, projectName: receipt.projectName, promptChanged: receipt.promptHash !== promptHash,
192
+ };
184
193
  if (receipt.state !== "known") throw new Error(`An earlier Desktop creation is ${receipt.state} and may already have started. ${receipt.threadId ? `threadId: ${receipt.threadId}. ` : ""}Do not resend or create another task; inspect the existing Desktop task and resolve its creation receipt first.`);
185
194
  let response;
186
195
  try {
@@ -194,7 +203,7 @@ export class DesktopTaskDelivery {
194
203
  this.security.assertCwd(thread.cwd);
195
204
  this.security.assertThread(thread.id, thread.cwd);
196
205
  const assignment = await this.receiptProjectAssignment(receipt, cwd, { deadline });
197
- return { threadId: receipt.threadId, name: thread.title ?? receipt.name ?? "(unnamed)", cwd, ...assignment, backend: NATIVE_BACKEND, reused: true, promptChanged: receipt.promptHash !== promptHash };
206
+ return { threadId: receipt.threadId, name: thread.title ?? receipt.name ?? "(unnamed)", cwd, ...assignment, backend: NATIVE_BACKEND, reused: true, promptChanged: receipt.promptHash !== promptHash, ...(includeReplyReceipt && receipt.deliveryId ? { deliveryId: receipt.deliveryId } : {}) };
198
207
  }
199
208
 
200
209
  async receiptProjectAssignment(receipt, cwd, { deadline }) {
@@ -234,16 +243,17 @@ export class DesktopTaskDelivery {
234
243
  return { projectId: project.projectId, projectName: project.label, projectAssignmentStatus: "verified" };
235
244
  }
236
245
 
237
- async create({ cwd, prompt, name, dedupeName = name, requestId, model, effort, deadline = this.now() + DESKTOP_TOOL_BUDGET_MS }) {
246
+ async create({ cwd, prompt, name, dedupeName = name, requestId, model, effort, onAccepted, onPrepared, deadline = this.now() + DESKTOP_TOOL_BUDGET_MS }) {
238
247
  this.security.assertCwd(cwd);
239
248
  const projectScope = this.senderProjectScope(cwd);
240
249
  cwd = realpathSync.native(cwd);
241
250
  this.security.assertCwd(cwd);
251
+ const scopeBinding = this.security.hardenedRoots?.mode === "project-policy" ? this.security.hardenedRoots.capture(cwd) : undefined;
242
252
  dedupeName = dedupeName?.normalize("NFC").trim().replace(/\s+/g, " ") || undefined;
243
253
  const identity = this.receipts.key({ cwd, prompt, name: dedupeName, requestId });
244
254
  const accountContext = this.accountContext?.();
245
255
  if (this.accountContext && !accountContext) throw new Error("Desktop creation requires the original verified account context.");
246
- const options = { cwd, promptHash: identity.promptHash, deadline, accountContext };
256
+ const options = { cwd, promptHash: identity.promptHash, deadline, accountContext, includeReplyReceipt: Boolean(onAccepted || onPrepared) };
247
257
  const reused = await this.reuseReceipt(await this.receipts.read(identity.key), options);
248
258
  if (reused) return reused;
249
259
  const listed = await this.request("list_projects", {}, { deadline });
@@ -254,15 +264,27 @@ export class DesktopTaskDelivery {
254
264
  if (existing) return existing;
255
265
  if (this.now() >= deadline) throw new Error("The response deadline elapsed before Desktop creation. No task was created.");
256
266
  const receipt = { version: 1, ...identity, cwd, state: "pending", startedAt: this.now(), ...(accountContext ? { accountContext: { ...accountContext } } : {}), ...(dedupeName ? { name: dedupeName } : {}), projectId: project.projectId, ...(project.label ? { projectName: project.label } : {}) };
267
+ const creationArgs = { prompt, title: name,
268
+ target: { type: "project", projectId: project.projectId, environment: { type: "local" } },
269
+ ...(model ? { model } : {}), ...(effort ? { thinking: effort } : {}) };
270
+ const creationReceiptId = onPrepared ? randomUUID() : undefined;
271
+ if (onPrepared) {
272
+ // Feature-detect before mutation; old companions refuse this read-only
273
+ // probe. Never create first and discover missing recovery afterwards.
274
+ if (!accountContext) throw new Error("Creation continuation requires verified accounts");
275
+ const requestHash = creationRequestHash(creationArgs);
276
+ const probe = await this.request("get_creation_receipt", { receiptId: creationReceiptId, requestHash }, { deadline });
277
+ if (probe?.status !== "missing") throw new Error("Native creation receipt capability could not be confirmed; no task was created");
278
+ receipt.deliveryId = onPrepared({ threadId: null, cwd, previousTurnId: null, deliveryStatus: "unconfirmed",
279
+ creationObservation: { receiptId: creationReceiptId, requestHash, receiptKey: identity.key, args: creationArgs, accountContext: { ...accountContext } } });
280
+ }
257
281
  await this.receipts.write(identity.key, receipt);
258
282
  let threadId;
259
283
  let creationConfirmed = false;
260
284
  try {
261
- const response = await this.request("create_thread", {
262
- prompt, title: name,
263
- target: { type: "project", projectId: project.projectId, environment: { type: "local" } },
264
- ...(model ? { model } : {}), ...(effort ? { thinking: effort } : {}),
265
- }, { deadline, projectScope });
285
+ const envelope = await this.request("create_thread", creationArgs,
286
+ { deadline, projectScope, scopeBinding, creationReceiptId, includeRelayContext: true });
287
+ const response = envelope.result;
266
288
  const confirmedId = response?.threadId ?? response?.conversationId;
267
289
  threadId = confirmedId;
268
290
  if (!threadId && (response?.status === "outcome-unknown" || response?.firstTurn?.status === "outcome-unknown")) threadId = response?.clientThreadId;
@@ -271,56 +293,130 @@ export class DesktopTaskDelivery {
271
293
  creationConfirmed = true;
272
294
  this.security.registerThread(threadId);
273
295
  if (response.firstTurn && response.firstTurn.status !== "accepted") throw new Error(`Desktop created task ${threadId}; its first turn reports ${response.firstTurn.status ?? "an unconfirmed state"}: ${JSON.stringify(response)}`);
274
- return { threadId, name, cwd, projectId: project.projectId, projectName: project.label, backend: NATIVE_BACKEND };
296
+ const created = { threadId, name, cwd, projectId: project.projectId, projectName: project.label, backend: NATIVE_BACKEND };
297
+ const delivered = { ...created, previousTurnId: null, deliveryStatus: "accepted", responseObservation: {
298
+ operation: "create_thread", threadId, previousTurnId: null, expectedCwd: cwd,
299
+ executorThreadId: envelope.executorThreadId ?? null, prompt,
300
+ accountContext: accountContext ? { ...accountContext } : null,
301
+ },
302
+ };
303
+ const deliveryId = receipt.deliveryId ?? onAccepted?.(delivered);
304
+ if (deliveryId) {
305
+ await this.receipts.write(identity.key, { ...receipt, state: "known", threadId, deliveryId });
306
+ created.deliveryId = deliveryId;
307
+ }
308
+ return created;
275
309
  } catch (err) {
276
310
  if (!creationConfirmed) await this.receipts.write(identity.key, { ...receipt, state: "unknown", ...(typeof threadId === "string" && threadId ? { threadId } : {}) }).catch(() => {});
311
+ const cause = err.cause ?? err;
312
+ if (receipt.deliveryId && cause.reachedCompanion === true && ["RELAY_TIMEOUT", "RELAY_DELIVERY_UNCONFIRMED"].includes(cause.code)) {
313
+ return { threadId: null, cwd, name, projectId: project.projectId, projectName: project.label, deliveryStatus: "unconfirmed", deliveryId: receipt.deliveryId };
314
+ }
277
315
  throw new Error(`${err.message}. ${threadId ? `threadId: ${threadId}. ` : ""}Do not resend the prompt. The creation receipt blocks duplicate tasks even after a bridge restart.`, { cause: err });
278
316
  }
279
317
  });
280
318
  }
281
319
 
320
+ async resolveCreation(delivered, { deadline = this.now() + DESKTOP_TOOL_BUDGET_MS } = {}) {
321
+ const binding = delivered.creationObservation;
322
+ if (!binding) return delivered;
323
+ for (;;) {
324
+ if (this.now() >= deadline) return null;
325
+ this.security.assertCwd(delivered.cwd);
326
+ this.senderProjectScope(delivered.cwd);
327
+ if (!sameAccountContext(binding.accountContext, this.accountContext?.())) throw new Error("Creation receipt account changed");
328
+ const receipt = await this.receipts.read(binding.receiptKey);
329
+ if (!receipt || !sameAccountContext(receipt.accountContext, binding.accountContext) || receipt.cwd !== delivered.cwd ||
330
+ receipt.promptHash !== this.receipts.key({ cwd: delivered.cwd, prompt: binding.args.prompt }).promptHash) throw new Error("Original creation receipt binding changed");
331
+ let envelope;
332
+ try { envelope = await this.request("get_creation_receipt", { receiptId: binding.receiptId, requestHash: binding.requestHash }, { deadline, includeRelayContext: true }); }
333
+ catch (error) { if (this.now() >= deadline && ["RELAY_TIMEOUT", "RELAY_UNREACHABLE"].includes(error.cause?.code)) return null; throw error; }
334
+ const row = envelope.result;
335
+ if (row?.status === "completed") {
336
+ const response = row.result, threadId = response?.threadId;
337
+ if (typeof threadId !== "string" || !threadId.trim() || response.hostId !== "local" || response.status === "outcome-unknown" ||
338
+ response.firstTurn && response.firstTurn.status !== "accepted" || receipt.threadId && receipt.threadId !== threadId) throw new Error("Native creation is not confirmed; do not resend");
339
+ let inspected;
340
+ try { inspected = await this.#inspectMetadata(threadId, delivered.cwd, { deadline }); }
341
+ catch (error) { if (this.now() >= deadline && /deadline|within \d+ms/.test(error.message)) return null; throw error; }
342
+ if (inspected.executorThreadId !== envelope.executorThreadId) throw new Error("Native creation executor changed");
343
+ if (!sameAccountContext(binding.accountContext, this.accountContext?.())) throw new Error("Creation receipt account changed");
344
+ this.security.assertCwd(delivered.cwd);
345
+ this.senderProjectScope(delivered.cwd);
346
+ if (this.security.hardenedRoots?.mode === "project-policy") this.security.hardenedRoots.recheck(inspected.scopeBinding);
347
+ if (inspected.projectScope) recheckProjectScope(inspected.projectScope);
348
+ this.security.registerThread(threadId);
349
+ this.security.assertThread(threadId, inspected.thread.cwd);
350
+ return { threadId, cwd: delivered.cwd, previousTurnId: null, deliveryStatus: "accepted",
351
+ responseObservation: { operation: "create_thread", threadId, previousTurnId: null, expectedCwd: delivered.cwd,
352
+ executorThreadId: envelope.executorThreadId, prompt: binding.args.prompt, accountContext: binding.accountContext } };
353
+ }
354
+ if (row?.status === "failed") throw new Error(`Native creation remains unconfirmed: ${row.error?.message ?? "unknown failure"}. Do not resend.`);
355
+ if (!["missing", "pending"].includes(row?.status)) throw new Error("Invalid native creation receipt response");
356
+ await this.sleep(Math.min(1000, Math.max(0, deadline - this.now())));
357
+ }
358
+ }
359
+
282
360
  async inspect(threadId, cwd, { deadline } = {}) {
283
- const response = await this.request("read_thread", { threadId, hostId: "local", turnLimit: 1 }, { deadline });
361
+ const inspected = await this.#inspectMetadata(threadId, cwd, { deadline });
362
+ this.security.assertThread(threadId, inspected.thread.cwd);
363
+ return inspected;
364
+ }
365
+
366
+ async #inspectMetadata(threadId, cwd, { deadline } = {}) {
367
+ const envelope = await this.request("read_thread", { threadId, hostId: "local", turnLimit: 1 }, { deadline, includeRelayContext: true });
368
+ const response = envelope.result;
284
369
  const thread = response?.thread;
285
370
  if (thread?.id !== threadId || thread.hostId !== "local" || !thread.cwd) throw new Error("Desktop did not confirm the task's local workspace.");
286
- this.security.assertThread(threadId, thread.cwd);
287
371
  this.security.assertCwd(thread.cwd);
288
372
  const projectScope = this.senderProjectScope(thread.cwd);
289
373
  if (cwd && path.relative(realpathSync.native(cwd), realpathSync.native(thread.cwd))) {
290
374
  throw new Error("Native Desktop delivery cannot change an existing task's workspace; create a new task at the requested cwd.");
291
375
  }
292
- return { thread, latestTurnId: response.turns?.[0]?.id ?? null, projectScope };
376
+ const scopeBinding = this.security.hardenedRoots?.mode === "project-policy" ? this.security.hardenedRoots.capture(thread.cwd) : undefined;
377
+ return { thread, latestTurnId: response.turns?.[0]?.id ?? null, projectScope, scopeBinding, executorThreadId: envelope.executorThreadId ?? null };
293
378
  }
294
379
 
295
- async send({ threadId, prompt, cwd, model, effort, name, deadline }) {
380
+ async send({ threadId, prompt, cwd, model, effort, name, deadline, onPrepared }) {
296
381
  const inspected = await this.inspect(threadId, cwd, { deadline });
297
- if (name) await this.request("set_thread_title", { threadId, title: name.trim().slice(0, 200) }, { deadline, projectScope: inspected.projectScope });
382
+ const scopeBinding = this.security.hardenedRoots?.mode === "project-policy" ? this.security.hardenedRoots.capture(inspected.thread.cwd) : undefined;
383
+ if (name) await this.request("set_thread_title", { threadId, title: name.trim().slice(0, 200) }, { deadline, projectScope: inspected.projectScope, scopeBinding });
298
384
  const expectedCwd = realpathSync.native(inspected.thread.cwd);
299
385
  const accountContext = this.accountContext?.();
300
- const watermark = this.captureResponse({ threadId, expectedCwd });
301
- const envelope = await this.request("send_message_to_thread", {
386
+ const watermark = this.captureResponse({ threadId, expectedCwd, desktopEvidence: inspected });
387
+ // Preserve the exact pre-send observation before any possibly ambiguous write.
388
+ // An unconfirmed receipt permits observation, never resending or an approval bypass.
389
+ const delivered = {
390
+ threadId, cwd: expectedCwd, name: inspected.thread.title, previousTurnId: inspected.latestTurnId, backend: NATIVE_BACKEND,
391
+ deliveryStatus: "unconfirmed",
392
+ responseObservation: {
393
+ threadId, previousTurnId: inspected.latestTurnId, expectedCwd,
394
+ executorThreadId: inspected.executorThreadId, prompt,
395
+ accountContext: accountContext ? { ...accountContext } : null, watermark,
396
+ },
397
+ };
398
+ onPrepared?.(delivered);
399
+ let envelope;
400
+ try { envelope = await this.request("send_message_to_thread", {
302
401
  threadId, prompt,
303
402
  ...(model ? { model } : {}),
304
403
  ...(effort ? { thinking: effort } : {}),
305
- }, { deadline, includeRelayContext: true, projectScope: inspected.projectScope });
404
+ }, { deadline, includeRelayContext: true, projectScope: inspected.projectScope, scopeBinding });
405
+ } catch (error) {
406
+ const cause = error.cause ?? error;
407
+ if (onPrepared && cause.reachedCompanion === true && ["RELAY_TIMEOUT", "RELAY_DELIVERY_UNCONFIRMED"].includes(cause.code)) {
408
+ return { ...delivered, deliveryNote: error.message };
409
+ }
410
+ throw error;
411
+ }
306
412
  const response = envelope.result;
307
413
  if (response?.threadId !== threadId || response?.success === false || response?.isError === true ||
308
414
  (response?.status !== undefined && !["accepted", "sent"].includes(response.status)) ||
309
415
  (response?.firstTurn && response.firstTurn.status !== "accepted")) {
310
416
  throw new Error(`Desktop send is not confirmed for ${threadId}. Do not resend: ${JSON.stringify(response)}`);
311
417
  }
312
- return {
313
- threadId, cwd: expectedCwd, name: inspected.thread.title, previousTurnId: inspected.latestTurnId, backend: NATIVE_BACKEND,
314
- responseObservation: {
315
- threadId,
316
- previousTurnId: inspected.latestTurnId,
317
- expectedCwd,
318
- executorThreadId: envelope.executorThreadId ?? null,
319
- prompt,
320
- accountContext: accountContext ? { ...accountContext } : null,
321
- watermark,
322
- },
323
- };
418
+ if ((envelope.executorThreadId ?? null) !== inspected.executorThreadId) throw new Error("Native executor changed during dispatch; inspect the existing task without resending");
419
+ return { ...delivered, deliveryStatus: "accepted" };
324
420
  }
325
421
 
326
422
  async open(threadId, { deadline } = {}) {
@@ -340,7 +436,10 @@ export class DesktopTaskDelivery {
340
436
  if (realpathSync.native(expectedCwd) !== expectedCwd) throw new Error("The selected native task workspace changed while its response was being observed");
341
437
  };
342
438
  await recheck();
343
- const observed = this.readResponse({ ...responseObservation, threadId, turnId });
439
+ const initial = await this.inspect(threadId, expectedCwd, { deadline });
440
+ if (initial.latestTurnId !== turnId) throw new Error("The selected native task changed before its response could be read; reply content was withheld");
441
+ const read = responseObservation.operation === "create_thread" ? readCodexNativeCreationResponse : this.readResponse;
442
+ const observed = read({ ...responseObservation, threadId, turnId, desktopEvidence: initial });
344
443
  await recheck();
345
444
  const inspected = await this.inspect(threadId, expectedCwd, { deadline });
346
445
  if (inspected.latestTurnId !== turnId) throw new Error("The selected native task changed before its response could be confirmed; reply content was withheld");
@@ -361,7 +460,7 @@ export class DesktopTaskDelivery {
361
460
  if (realpathSync.native(expectedCwd) !== expectedCwd) throw new Error("The selected native task workspace changed while its turn was being inspected");
362
461
  };
363
462
  await recheck();
364
- const result = this.inspectResponse({ threadId, turnId, expectedCwd });
463
+ const result = this.inspectResponse({ threadId, turnId, expectedCwd, desktopEvidence: initial });
365
464
  await recheck();
366
465
  await this.inspect(threadId, expectedCwd, { deadline });
367
466
  await recheck();