@minhspark/codex-mcp-bridge 1.22.3 → 1.22.5
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 +38 -0
- package/README.md +77 -380
- package/docs/daily-handoffs.md +32 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install-native-relay.mjs +1 -1
- package/src/claude-bridge.mjs +30 -19
- package/src/claude-project-discovery.mjs +25 -0
- package/src/claude-session-creation.mjs +43 -7
- package/src/codex-native-response.mjs +96 -15
- package/src/desktop-reply-receipts.mjs +28 -0
- package/src/index.mjs +6 -4
- package/src/native-relay-companion.mjs +1 -1
- package/src/peer-protocol.mjs +51 -7
- package/src/platform.mjs +7 -3
- package/src/project-policy.mjs +9 -0
- package/src/thread-delivery.mjs +59 -14
package/src/peer-protocol.mjs
CHANGED
|
@@ -38,9 +38,11 @@ const projectsDir = () => path.join(homeDir(), ".claude", "projects");
|
|
|
38
38
|
* listens for peer messages on a local socket or Windows named pipe. Messages are newline-delimited
|
|
39
39
|
* JSON; the wrapper element is what Claude renders in its chat surface.
|
|
40
40
|
*/
|
|
41
|
-
export function buildFrame({ text, fromSocket, priority = "next", permissionMode }) {
|
|
41
|
+
export function buildFrame({ text, fromSocket, fromName, priority = "next", permissionMode }) {
|
|
42
42
|
const msgId = crypto.randomUUID();
|
|
43
43
|
const from = `uds:${fromSocket.replace(/[^A-Za-z0-9:_/.\-]/gu, (character) => Array.from(Buffer.from(character), (byte) => `%${byte.toString(16).toUpperCase().padStart(2, "0")}`).join(""))}`;
|
|
44
|
+
const displayName = typeof fromName === "string" ? fromName.replace(/["<>\p{Cf}\p{Cc}\p{Cs}\p{Zl}\p{Zp}]/gu, "").trim() : "";
|
|
45
|
+
const name = displayName ? ` from-name="${displayName}"` : "";
|
|
44
46
|
const mode = ["bypass", "prompting"].includes(permissionMode) ? ` from-mode="${permissionMode}"` : "";
|
|
45
47
|
return {
|
|
46
48
|
msgV: PEER_PROTOCOL_VERSION,
|
|
@@ -49,7 +51,7 @@ export function buildFrame({ text, fromSocket, priority = "next", permissionMode
|
|
|
49
51
|
type: "user",
|
|
50
52
|
message: {
|
|
51
53
|
role: "user",
|
|
52
|
-
content: `<cross-session-message from="${from}"${mode}>\n${text}\n</cross-session-message>`,
|
|
54
|
+
content: `<cross-session-message from="${from}"${name}${mode}>\n${text}\n</cross-session-message>`,
|
|
53
55
|
},
|
|
54
56
|
priority,
|
|
55
57
|
from,
|
|
@@ -413,6 +415,38 @@ function isImageToolCompanion(entry, entriesById) {
|
|
|
413
415
|
&& typeof tool.id === "string" && tool.id === part.tool_use_id));
|
|
414
416
|
}
|
|
415
417
|
|
|
418
|
+
function compactContinuation(entry, root, entriesById, sessionId, cwd) {
|
|
419
|
+
if (entry.type !== "system" || entry.subtype !== "compact_boundary" || entry.parentUuid !== null
|
|
420
|
+
|| typeof entry.logicalParentUuid !== "string") return null;
|
|
421
|
+
const sameIdentity = (record) => record?.entrypoint === "claude-desktop"
|
|
422
|
+
&& record.sessionId === sessionId && record.cwd === root.cwd;
|
|
423
|
+
if (!sameIdentity(root) || !sameIdentity(entry) || typeof root.cwd !== "string"
|
|
424
|
+
|| path.resolve(root.cwd) !== path.resolve(cwd)) return null;
|
|
425
|
+
const segment = entry.compactMetadata?.preservedSegment;
|
|
426
|
+
const preserved = entry.compactMetadata?.preservedMessages;
|
|
427
|
+
const ids = preserved?.uuids;
|
|
428
|
+
if (!Array.isArray(ids) || !ids.length || ids.length > 65536
|
|
429
|
+
|| ids.some((id) => typeof id !== "string" || !id) || new Set(ids).size !== ids.length
|
|
430
|
+
|| !Array.isArray(preserved.allUuids) || preserved.allUuids.length !== ids.length
|
|
431
|
+
|| ids.some((id, index) => preserved.allUuids[index] !== id)
|
|
432
|
+
|| !ids.includes(root.uuid) || segment?.headUuid !== ids[0]
|
|
433
|
+
|| segment.tailUuid !== ids.at(-1) || segment.tailUuid !== entry.logicalParentUuid
|
|
434
|
+
|| typeof segment.anchorUuid !== "string" || preserved.anchorUuid !== segment.anchorUuid) return null;
|
|
435
|
+
if (ids.some((id, index) => !sameIdentity(entriesById.get(id))
|
|
436
|
+
|| index > 0 && entriesById.get(id).parentUuid !== ids[index - 1])
|
|
437
|
+
|| ids.includes(entriesById.get(ids[0]).parentUuid)) return null;
|
|
438
|
+
const summary = entriesById.get(segment.anchorUuid);
|
|
439
|
+
if (!sameIdentity(summary) || summary.type !== "user" || summary.message?.role !== "user"
|
|
440
|
+
|| summary.isCompactSummary !== true || summary.isVisibleInTranscriptOnly !== true
|
|
441
|
+
|| summary.turnOrigin !== "peer" || summary.origin != null || summary.promptSource != null) return null;
|
|
442
|
+
let ancestor = entriesById.get(summary.parentUuid);
|
|
443
|
+
for (let depth = 0; depth < 64 && ancestor?.type === "attachment"; depth++) {
|
|
444
|
+
if (!sameIdentity(ancestor)) return null;
|
|
445
|
+
ancestor = entriesById.get(ancestor.parentUuid);
|
|
446
|
+
}
|
|
447
|
+
return ancestor === entry ? summary.uuid : null;
|
|
448
|
+
}
|
|
449
|
+
|
|
416
450
|
// Interruption closes reply observation, not execution. Never attach a later
|
|
417
451
|
// user turn's answer to the original request or claim its side effects stopped.
|
|
418
452
|
export function readTranscriptOutcome(sessionId, cwd, msgId) {
|
|
@@ -445,6 +479,7 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
|
|
|
445
479
|
const openLeaves = new Set();
|
|
446
480
|
const passiveLeaves = new Set();
|
|
447
481
|
const interruptions = [];
|
|
482
|
+
const compactSummaries = new Set();
|
|
448
483
|
const interrupt = (entry, outstanding) => {
|
|
449
484
|
if (typeof entry.uuid !== "string" || !entry.uuid.trim()) return;
|
|
450
485
|
openLeaves.delete(entry.parentUuid);
|
|
@@ -452,7 +487,16 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
|
|
|
452
487
|
};
|
|
453
488
|
for (const entry of entries) {
|
|
454
489
|
if (entry === root) { descendants.set(entry.uuid, new Map()); openLeaves.add(entry.uuid); continue; }
|
|
455
|
-
|
|
490
|
+
let parentUuid = entry.parentUuid;
|
|
491
|
+
if (!ambiguous && !interruptions.length && descendants.has(entry.logicalParentUuid)
|
|
492
|
+
&& openLeaves.has(entry.logicalParentUuid)) {
|
|
493
|
+
const summaryId = compactContinuation(entry, root, entriesById, sessionId, cwd);
|
|
494
|
+
if (summaryId) {
|
|
495
|
+
parentUuid = entry.logicalParentUuid;
|
|
496
|
+
compactSummaries.add(summaryId);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (!descendants.has(parentUuid)) continue;
|
|
456
500
|
if (entry.type === "attachment" && entry.attachment?.type === "queued_command") {
|
|
457
501
|
// Only a native command record is a boundary. Its text is never used as
|
|
458
502
|
// proof of authorization, cancellation, or successful task completion.
|
|
@@ -469,10 +513,10 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
|
|
|
469
513
|
}
|
|
470
514
|
const role = entry.message?.role;
|
|
471
515
|
const content = entry.message?.content;
|
|
472
|
-
const outstanding = new Map(descendants.get(
|
|
516
|
+
const outstanding = new Map(descendants.get(parentUuid));
|
|
473
517
|
const toolResults = Array.isArray(content) && content.length > 0 && content.every((part) => part?.type === "tool_result");
|
|
474
518
|
const imageCompanion = isImageToolCompanion(entry, entriesById);
|
|
475
|
-
if (role === "user" && !toolResults && !imageCompanion) {
|
|
519
|
+
if (role === "user" && !toolResults && !imageCompanion && !compactSummaries.has(entry.uuid)) {
|
|
476
520
|
const notificationText = typeof content === "string" ? content
|
|
477
521
|
: Array.isArray(content) ? content.filter((part) => typeof part?.text === "string").map((part) => part.text).join("\n") : "";
|
|
478
522
|
const nativeHuman = entry.origin?.kind === "human" && entry.promptSource === "sdk" && entry.turnOrigin === "human";
|
|
@@ -524,7 +568,7 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
|
|
|
524
568
|
&& ["hook_success", "total_tokens_reminder", "deferred_tools_record"].includes(entry.attachment?.type)) {
|
|
525
569
|
passiveLeaves.add(entry.uuid);
|
|
526
570
|
}
|
|
527
|
-
openLeaves.delete(
|
|
571
|
+
openLeaves.delete(parentUuid);
|
|
528
572
|
openLeaves.add(entry.uuid);
|
|
529
573
|
if (role !== "assistant" || !["end_turn", "stop_sequence"].includes(entry.message.stop_reason)) continue;
|
|
530
574
|
if (outstanding.size > 0) continue;
|
|
@@ -858,7 +902,7 @@ export class PeerEndpoint {
|
|
|
858
902
|
|
|
859
903
|
async send(targetSocket, text, { priority = "next", msgId, permissionMode = this.permissionMode, beforeSend } = {}) {
|
|
860
904
|
if (this.reloadPaused) throw new Error("The peer endpoint is quiesced; no message was sent");
|
|
861
|
-
const frame = buildFrame({ text, fromSocket: this.socketPath, priority, permissionMode });
|
|
905
|
+
const frame = buildFrame({ text, fromSocket: this.socketPath, fromName: this.name, priority, permissionMode });
|
|
862
906
|
if (msgId) frame.uuid = frame.msg_id = msgId;
|
|
863
907
|
const token = readPeerToken(targetSocket);
|
|
864
908
|
const line = (token ? JSON.stringify({ type: "auth", token }) + "\n" : "") + JSON.stringify(frame) + "\n";
|
package/src/platform.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile, execFileSync } from "node:child_process";
|
|
2
|
-
import { accessSync, constants, existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { accessSync, constants, existsSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -338,7 +338,7 @@ function remapCandidates(input) {
|
|
|
338
338
|
* checkout is unavailable locally. UNC shares are tried after local remap
|
|
339
339
|
* candidates so a disconnected network location cannot stall the bridge.
|
|
340
340
|
*/
|
|
341
|
-
export function resolveWorkspacePath(input) {
|
|
341
|
+
export function resolveWorkspacePath(input, { requireUnique = false } = {}) {
|
|
342
342
|
if (!input) return { path: input, remapped: false, writable: false, note: null };
|
|
343
343
|
const original = input;
|
|
344
344
|
const mapped = pathMapCandidates(input);
|
|
@@ -348,7 +348,11 @@ export function resolveWorkspacePath(input) {
|
|
|
348
348
|
: [...mapped, input, ...remapped];
|
|
349
349
|
const ordered = candidates.filter((candidate, index) => candidates.indexOf(candidate) === index);
|
|
350
350
|
|
|
351
|
-
const
|
|
351
|
+
const usable = requireUnique ? ordered.filter((candidate) => existsSync(candidate) && isWritableDir(candidate)) : null;
|
|
352
|
+
if (usable && new Set(usable.map(candidate => realpathSync.native(candidate))).size > 1) {
|
|
353
|
+
throw new Error("Multiple usable workspace directories match; select an unambiguous project path");
|
|
354
|
+
}
|
|
355
|
+
const writable = usable ? usable[0] : ordered.find((candidate) => existsSync(candidate) && isWritableDir(candidate));
|
|
352
356
|
if (writable) {
|
|
353
357
|
return {
|
|
354
358
|
path: path.resolve(writable),
|
package/src/project-policy.mjs
CHANGED
|
@@ -150,6 +150,15 @@ export function createProjectScope(file, options = {}) {
|
|
|
150
150
|
assert: (candidate, label) => capture(candidate, label).path,
|
|
151
151
|
allows(candidate) { try { capture(candidate); return true; } catch { return false; } },
|
|
152
152
|
capture,
|
|
153
|
+
assertDiscoverySource(candidate) {
|
|
154
|
+
const original = canonicalPolicyPath(candidate);
|
|
155
|
+
const namespaces = [path.resolve(candidate), original];
|
|
156
|
+
const { policy } = readProjectPolicy(file);
|
|
157
|
+
if (policy.denies.some(entry => namespaces.some(source => inside(entry.path, source) || entry.worktrees?.some(root => inside(root.path, source))))) {
|
|
158
|
+
throw new Error("The original project path was revoked; relocation discovery is not authorized");
|
|
159
|
+
}
|
|
160
|
+
return original;
|
|
161
|
+
},
|
|
153
162
|
recheck(binding, label = "Working directory") {
|
|
154
163
|
if (!binding?.identity) throw new Error(`${label} has no original directory identity`);
|
|
155
164
|
const now = capture(binding.path, label);
|
package/src/thread-delivery.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { creationRequestHash } from "./native-creation-receipts.mjs";
|
|
7
7
|
import { DesktopTaskReceipts } from "./desktop-task-receipts.mjs";
|
|
8
|
-
import { captureCodexRolloutWatermark, inspectCodexNativeTurn, readCodexNativeTurnResponse, readCodexNativeCreationResponse } from "./codex-native-response.mjs";
|
|
8
|
+
import { captureCodexRolloutWatermark, inspectCodexNativeTurn, readCodexNativeTurnResponse, readCodexNativeCreationResponse, resolveCodexNativeDispatchResponse } from "./codex-native-response.mjs";
|
|
9
9
|
import { captureProjectScope, recheckProjectScope } from "./project-scope.mjs";
|
|
10
10
|
import { requireClaudeSenderContext } from "./claude-sender-context.mjs";
|
|
11
11
|
|
|
@@ -59,7 +59,7 @@ export function matchDesktopProject(projects, cwd, { canonicalize = realpathSync
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export class DesktopTaskDelivery {
|
|
62
|
-
constructor({ relay = new NativeDesktopRelay({ socketPath: desktopTaskSocketPath(), accountSocketPath: accountRelaySocketPath() }), security, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), now = Date.now, receipts = new DesktopTaskReceipts(), beforeRequest, accountContext, senderContext, captureResponse = captureCodexRolloutWatermark, readResponse = readCodexNativeTurnResponse, inspectResponse = inspectCodexNativeTurn } = {}) {
|
|
62
|
+
constructor({ relay = new NativeDesktopRelay({ socketPath: desktopTaskSocketPath(), accountSocketPath: accountRelaySocketPath() }), security, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), now = Date.now, receipts = new DesktopTaskReceipts(), beforeRequest, accountContext, senderContext, captureResponse = captureCodexRolloutWatermark, readResponse = readCodexNativeTurnResponse, inspectResponse = inspectCodexNativeTurn, resolveResponse = readResponse === readCodexNativeTurnResponse ? resolveCodexNativeDispatchResponse : null } = {}) {
|
|
63
63
|
this.relay = relay;
|
|
64
64
|
this.security = security;
|
|
65
65
|
this.sleep = sleep;
|
|
@@ -71,6 +71,7 @@ export class DesktopTaskDelivery {
|
|
|
71
71
|
this.captureResponse = captureResponse;
|
|
72
72
|
this.readResponse = readResponse;
|
|
73
73
|
this.inspectResponse = inspectResponse;
|
|
74
|
+
this.resolveResponse = resolveResponse;
|
|
74
75
|
this.threadOperations = new Map();
|
|
75
76
|
}
|
|
76
77
|
|
|
@@ -101,7 +102,7 @@ export class DesktopTaskDelivery {
|
|
|
101
102
|
const projectScope = expectedScope ?? await this.mutationProjectScope(operation, args, { deadline });
|
|
102
103
|
if (expectedScope) recheckProjectScope(expectedScope, this.senderProjectScope(expectedScope.recipient.input));
|
|
103
104
|
const remaining = deadline === undefined ? undefined : deadline - this.now();
|
|
104
|
-
if (remaining !== undefined && remaining <= 0) throw new Error("The bridge's response deadline has elapsed; this operation was not sent");
|
|
105
|
+
if (remaining !== undefined && remaining <= 0) throw Object.assign(new Error("The bridge's response deadline has elapsed; this operation was not sent"), { code: "DESKTOP_RESPONSE_DEADLINE" });
|
|
105
106
|
const accountContext = this.accountContext?.();
|
|
106
107
|
const response = await this.relay.requestDesktop(operation, args, {
|
|
107
108
|
...(remaining === undefined ? {} : { timeoutMs: remaining }),
|
|
@@ -432,7 +433,7 @@ export class DesktopTaskDelivery {
|
|
|
432
433
|
if (response?.navigated !== true) throw new Error(`Desktop did not confirm opening task ${threadId}`);
|
|
433
434
|
}
|
|
434
435
|
|
|
435
|
-
async observeNativeResponse(threadId, turnId, responseObservation, { deadline } = {}) {
|
|
436
|
+
async observeNativeResponse(threadId, turnId, responseObservation, { deadline, resolveDispatch = false } = {}) {
|
|
436
437
|
if (!responseObservation) return unavailableObservation("Codex Desktop completed the turn without exposing assistant response text or a pre-send observation binding.");
|
|
437
438
|
const { expectedCwd } = responseObservation;
|
|
438
439
|
const args = { threadId, hostId: "local", turnLimit: 1 };
|
|
@@ -445,12 +446,12 @@ export class DesktopTaskDelivery {
|
|
|
445
446
|
};
|
|
446
447
|
await recheck();
|
|
447
448
|
const initial = await this.inspect(threadId, expectedCwd, { deadline });
|
|
448
|
-
if (initial.latestTurnId !== turnId) throw new Error("The selected native task changed before its response could be read; reply content was withheld");
|
|
449
|
-
const read = responseObservation.operation === "create_thread" ? readCodexNativeCreationResponse : this.readResponse;
|
|
449
|
+
if (!resolveDispatch && initial.latestTurnId !== turnId) throw new Error("The selected native task changed before its response could be read; reply content was withheld");
|
|
450
|
+
const read = resolveDispatch ? this.resolveResponse : responseObservation.operation === "create_thread" ? readCodexNativeCreationResponse : this.readResponse;
|
|
450
451
|
const observed = read({ ...responseObservation, threadId, turnId, desktopEvidence: initial });
|
|
451
452
|
await recheck();
|
|
452
453
|
const inspected = await this.inspect(threadId, expectedCwd, { deadline });
|
|
453
|
-
if (inspected.latestTurnId !== turnId) throw new Error("The selected native task changed before its response could be confirmed; reply content was withheld");
|
|
454
|
+
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");
|
|
454
455
|
await recheck();
|
|
455
456
|
return observed;
|
|
456
457
|
}
|
|
@@ -491,6 +492,19 @@ export class DesktopTaskDelivery {
|
|
|
491
492
|
return { polls: [{ thread, latestTurn: result.turns?.[0] ?? null }] };
|
|
492
493
|
};
|
|
493
494
|
const expired = () => ({ threadId, turnId, status: "timeout", text: "", responseStatus: "unavailable", assistantItems: [], replySha256: null, activity: [], errors: [], durationMs: this.now() - startedAt });
|
|
495
|
+
const deadlineError = (error) => {
|
|
496
|
+
const cause = error.cause ?? error;
|
|
497
|
+
return this.now() - startedAt >= timeoutMs && (["DESKTOP_RESPONSE_DEADLINE", "RELAY_TIMEOUT"].includes(cause.code) ||
|
|
498
|
+
cause.code === "RELAY_UNREACHABLE" && cause.message === "The Codex Desktop relay send deadline elapsed before dispatch");
|
|
499
|
+
};
|
|
500
|
+
const observe = async (selectedTurnId, resolveDispatch = false) => {
|
|
501
|
+
try {
|
|
502
|
+
return await this.observeNativeResponse(threadId, selectedTurnId, responseObservation, { deadline: startedAt + timeoutMs, resolveDispatch });
|
|
503
|
+
} catch (error) {
|
|
504
|
+
if (deadlineError(error)) return null;
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
};
|
|
494
508
|
for (;;) {
|
|
495
509
|
if (this.now() - startedAt >= timeoutMs) return expired();
|
|
496
510
|
let response;
|
|
@@ -499,13 +513,13 @@ export class DesktopTaskDelivery {
|
|
|
499
513
|
targets: [{ threadId, hostId: "local", ...(cursor ? { afterCursor: cursor } : {}) }], timeoutMs: 0,
|
|
500
514
|
}, { deadline: startedAt + timeoutMs });
|
|
501
515
|
} catch (err) {
|
|
502
|
-
if (
|
|
516
|
+
if (deadlineError(err)) return expired();
|
|
503
517
|
if (readSnapshots || !unavailableWaitRenderer(err)) throw err;
|
|
504
518
|
readSnapshots = true;
|
|
505
519
|
try {
|
|
506
520
|
response = await readSnapshot();
|
|
507
521
|
} catch (readError) {
|
|
508
|
-
if (
|
|
522
|
+
if (deadlineError(readError)) return expired();
|
|
509
523
|
throw readError;
|
|
510
524
|
}
|
|
511
525
|
}
|
|
@@ -514,15 +528,45 @@ export class DesktopTaskDelivery {
|
|
|
514
528
|
cursor = poll.cursor;
|
|
515
529
|
const turn = poll.latestTurn;
|
|
516
530
|
const threadStatus = typeof poll.thread.status === "string" ? poll.thread.status : poll.thread.status?.type;
|
|
517
|
-
|
|
518
|
-
|
|
531
|
+
const needsAttention = ["systemError", "waitingOnApproval", "waitingOnUserInput"].includes(threadStatus);
|
|
532
|
+
const attention = () => ({ threadId, turnId: turn?.id !== previousTurnId ? turn?.id ?? null : null, status: threadStatus, text: "", activity: [], errors: [], durationMs: this.now() - startedAt });
|
|
533
|
+
if (this.resolveResponse && responseObservation?.watermark?.status === "available" && responseObservation.operation !== "create_thread") {
|
|
534
|
+
const observed = await observe(null, true);
|
|
535
|
+
if (!observed) return expired();
|
|
536
|
+
turnId = observed.turnId ?? null;
|
|
537
|
+
if (observed.status !== "pending") {
|
|
538
|
+
return {
|
|
539
|
+
threadId, turnId, status: observed.status === "unavailable" ? "failed" : "completed",
|
|
540
|
+
responseStatus: observed.status, observationStatus: observed.status,
|
|
541
|
+
text: observed.status === "completed" ? observed.text : "",
|
|
542
|
+
...(observed.reason ? { observationReason: observed.reason } : {}),
|
|
543
|
+
assistantItems: observed.assistantItems ?? [], replySha256: observed.replySha256 ?? null,
|
|
544
|
+
responseSource: observed.source ?? null,
|
|
545
|
+
...(observed.responseScope ? { responseScope: observed.responseScope } : {}),
|
|
546
|
+
activity: [], errors: [], durationMs: this.now() - startedAt,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
if (needsAttention) return attention();
|
|
550
|
+
if (this.now() - startedAt >= timeoutMs) return expired();
|
|
551
|
+
await this.sleep(Math.min(1500, timeoutMs - (this.now() - startedAt)));
|
|
552
|
+
continue;
|
|
519
553
|
}
|
|
520
|
-
if (
|
|
521
|
-
|
|
554
|
+
if (needsAttention) return attention();
|
|
555
|
+
const existingTurn = turn?.id === previousTurnId;
|
|
556
|
+
if (turn?.id && (!existingTurn || responseObservation?.previousTurnId === previousTurnId)) {
|
|
557
|
+
if (!existingTurn) turnId = turn.id;
|
|
522
558
|
const status = turn.status;
|
|
523
559
|
if (RELEASE_STATUSES.has(status)) {
|
|
524
|
-
const observed = await
|
|
560
|
+
const observed = await observe(turn.id);
|
|
561
|
+
if (!observed) return expired();
|
|
525
562
|
const responseStatus = observed.status;
|
|
563
|
+
if (existingTurn && !observed.dispatchObserved && !["completed", "completed_no_reply"].includes(responseStatus)) {
|
|
564
|
+
turnId = null;
|
|
565
|
+
if (this.now() - startedAt >= timeoutMs) return expired();
|
|
566
|
+
await this.sleep(Math.min(1500, timeoutMs - (this.now() - startedAt)));
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
turnId = turn.id;
|
|
526
570
|
return {
|
|
527
571
|
threadId, turnId, status,
|
|
528
572
|
text: responseStatus === "completed" ? observed.text : "",
|
|
@@ -532,6 +576,7 @@ export class DesktopTaskDelivery {
|
|
|
532
576
|
assistantItems: observed.assistantItems ?? [],
|
|
533
577
|
replySha256: observed.replySha256 ?? null,
|
|
534
578
|
responseSource: observed.source ?? null,
|
|
579
|
+
...(observed.responseScope ? { responseScope: observed.responseScope } : {}),
|
|
535
580
|
activity: [], errors: turn.error ? [turn.error] : [], durationMs: turn.durationMs ?? this.now() - startedAt,
|
|
536
581
|
};
|
|
537
582
|
}
|