@wordinweb/agent 0.1.0 → 0.2.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.
- package/README.md +59 -16
- package/dist/{chunk-E7AFDS2O.js → chunk-EMDLXVNY.js} +20243 -8902
- package/dist/chunk-EMDLXVNY.js.map +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +330 -36
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1578 -100
- package/dist/index.js +43 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-E7AFDS2O.js.map +0 -1
package/dist/cli.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ type WakeTarget = {
|
|
|
3
3
|
sessionId: string;
|
|
4
4
|
};
|
|
5
5
|
declare function detectWakeTarget(env?: NodeJS.ProcessEnv): WakeTarget | null;
|
|
6
|
-
declare function agentWakePrompt(agentSessionId: string, text: string): string;
|
|
6
|
+
declare function agentWakePrompt(agentSessionId: string, wakeId: string, text: string): string;
|
|
7
7
|
type Emit = (value: unknown) => void;
|
|
8
8
|
declare function connectAgent(invitationUrl: string, inputStream?: NodeJS.ReadableStream, emit?: Emit): Promise<void>;
|
|
9
9
|
|
package/dist/cli.js
CHANGED
|
@@ -4,14 +4,17 @@ import {
|
|
|
4
4
|
CollabConnection,
|
|
5
5
|
EncryptedCollabConnection,
|
|
6
6
|
createWebSocketTransport,
|
|
7
|
+
isSuggestable,
|
|
8
|
+
isSuggestableTableOp,
|
|
7
9
|
stretchShareCode
|
|
8
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-EMDLXVNY.js";
|
|
9
11
|
|
|
10
12
|
// src/cli.ts
|
|
11
13
|
import { webcrypto } from "crypto";
|
|
12
14
|
import { spawn } from "child_process";
|
|
13
|
-
import { chmodSync, realpathSync, rmSync } from "fs";
|
|
15
|
+
import { chmodSync, mkdirSync, realpathSync, rmSync } from "fs";
|
|
14
16
|
import { createConnection, createServer } from "net";
|
|
17
|
+
import { tmpdir } from "os";
|
|
15
18
|
import { join } from "path";
|
|
16
19
|
import { fileURLToPath } from "url";
|
|
17
20
|
import { createInterface } from "readline";
|
|
@@ -19,7 +22,6 @@ import { PassThrough } from "stream";
|
|
|
19
22
|
import WebSocket from "ws";
|
|
20
23
|
if (!globalThis.crypto) Object.defineProperty(globalThis, "crypto", { value: webcrypto });
|
|
21
24
|
var AGENT_MODE_CONTROL_PREFIX = "\0wordinweb-agent-mode:";
|
|
22
|
-
var AGENT_PACKAGE_URL = "https://collab.word-in-web.com/wordinweb-agent.tgz?v=short-invite-3";
|
|
23
25
|
function base64UrlToBytes(value) {
|
|
24
26
|
const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
25
27
|
return Uint8Array.from(Buffer.from(padded, "base64"));
|
|
@@ -69,17 +71,21 @@ function editRequestForMode(request, mode) {
|
|
|
69
71
|
const operations = request.operations.map((value) => {
|
|
70
72
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Each edit operation must be an object");
|
|
71
73
|
const operation = value;
|
|
74
|
+
if (operation.kind === "tableOp") {
|
|
75
|
+
if (!isSuggestableTableOp(operation.op)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`tableOp ${String(operation.op?.kind ?? operation.op)} restructures the table and is unavailable in suggestion mode. Ask the inviter to switch this agent to editing mode`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return { ...operation, suggest: true };
|
|
81
|
+
}
|
|
82
|
+
if (isSuggestable(operation.kind)) return { ...operation, suggest: true };
|
|
72
83
|
switch (operation.kind) {
|
|
73
|
-
case "insertText":
|
|
74
|
-
case "splitParagraph":
|
|
75
|
-
return { ...operation, suggest: true };
|
|
76
84
|
case "deleteText":
|
|
77
85
|
return {
|
|
78
86
|
kind: "suggestRevision",
|
|
79
87
|
ranges: [{ blockRef: operation.blockRef, runRef: operation.runRef, start: operation.start, end: operation.end }]
|
|
80
88
|
};
|
|
81
|
-
case "mergeParagraph":
|
|
82
|
-
return { kind: "suggestRevision", marks: [{ blockRef: operation.blockRef, glyph: "del" }] };
|
|
83
89
|
case "suggestRevision":
|
|
84
90
|
case "commentRun":
|
|
85
91
|
case "replyComment":
|
|
@@ -90,6 +96,10 @@ function editRequestForMode(request, mode) {
|
|
|
90
96
|
});
|
|
91
97
|
return { ...request, operations };
|
|
92
98
|
}
|
|
99
|
+
function patchRequestForMode(request, mode) {
|
|
100
|
+
if (mode === "edit" || !request) return request;
|
|
101
|
+
return { ...request, suggest: true };
|
|
102
|
+
}
|
|
93
103
|
function randomId(prefix) {
|
|
94
104
|
return `${prefix}_${bytesToBase64Url(crypto.getRandomValues(new Uint8Array(16)))}`;
|
|
95
105
|
}
|
|
@@ -104,25 +114,35 @@ function detectWakeTarget(env = process.env) {
|
|
|
104
114
|
}
|
|
105
115
|
return null;
|
|
106
116
|
}
|
|
107
|
-
function agentWakePrompt(agentSessionId, text) {
|
|
117
|
+
function agentWakePrompt(agentSessionId, wakeId, text) {
|
|
118
|
+
const sessionCommand = `${JSON.stringify(process.execPath)} ${JSON.stringify(fileURLToPath(import.meta.url))} session ${JSON.stringify(agentSessionId)} '<JSON command>'`;
|
|
108
119
|
return [
|
|
109
|
-
"An authenticated private WordInWeb message
|
|
120
|
+
"An authenticated private WordInWeb message started this document-agent turn.",
|
|
110
121
|
`The inviter's message is ${JSON.stringify(text)}.`,
|
|
111
|
-
`The detached document bridge session is ${agentSessionId}.
|
|
112
|
-
`
|
|
113
|
-
|
|
114
|
-
"
|
|
122
|
+
`The detached document bridge session is ${agentSessionId}. Use this existing bridge for the task.`,
|
|
123
|
+
`The wake ID is ${wakeId}. Include ${JSON.stringify(wakeId)} as the wakeId field in every document command.`,
|
|
124
|
+
`Run document commands with: ${sessionCommand}`,
|
|
125
|
+
`Start with {"command":"sync","wakeId":${JSON.stringify(wakeId)}}. For a broad text task, inspect all non-empty stories in one compact call: {"command":"inspect","wakeId":${JSON.stringify(wakeId)},"request":{"kind":"context"}}. Use overview, read, object, or spatial only when the task needs their extra detail.`,
|
|
126
|
+
`For a bulk prose rewrite, read the story as text and send the changed lines back: {"command":"project","wakeId":${JSON.stringify(wakeId)},"request":{"mode":"md"}}, then {"command":"patch","wakeId":${JSON.stringify(wakeId)},"request":{"revision":"<the projection revision>","mode":"md","edits":[{"startLine":4,"endLine":4,"newText":"the rewritten line"}]}}. The patch result carries the refreshed projection, so a second read is unnecessary.`,
|
|
127
|
+
"Inspect the relevant content before every edit. If an edit or patch returns needs_sync, sync and read the content again.",
|
|
115
128
|
"Complete this task, then send the inviter a concise result with the chat command.",
|
|
116
|
-
"End this agent turn after the reply
|
|
129
|
+
"End this agent turn after the reply and stay idle. The detached bridge will start the next turn when another message arrives."
|
|
117
130
|
].join("\n\n");
|
|
118
131
|
}
|
|
119
132
|
function write(value) {
|
|
120
133
|
process.stdout.write(`${JSON.stringify(value)}
|
|
121
134
|
`);
|
|
122
135
|
}
|
|
123
|
-
function
|
|
136
|
+
function validateSessionId(sessionId) {
|
|
124
137
|
if (!/^session_[A-Za-z0-9_-]{16,128}$/.test(sessionId)) throw new Error("The agent session ID is invalid");
|
|
125
|
-
|
|
138
|
+
}
|
|
139
|
+
function sessionDirectory(sessionId) {
|
|
140
|
+
validateSessionId(sessionId);
|
|
141
|
+
return join(tmpdir(), `wiw-${sessionId.slice("session_".length)}`);
|
|
142
|
+
}
|
|
143
|
+
function sessionSocketPath(sessionId) {
|
|
144
|
+
validateSessionId(sessionId);
|
|
145
|
+
return process.platform === "win32" ? `\\\\.\\pipe\\wordinweb-agent-${sessionId}` : join(sessionDirectory(sessionId), "bridge.sock");
|
|
126
146
|
}
|
|
127
147
|
async function importChatKey(value) {
|
|
128
148
|
return crypto.subtle.importKey("raw", base64UrlToBytes(value), "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
@@ -263,7 +283,7 @@ async function connectAgent(invitationUrl, inputStream = process.stdin, emit = w
|
|
|
263
283
|
revision: document.revision,
|
|
264
284
|
mode,
|
|
265
285
|
instructions: payload.agent.instructions,
|
|
266
|
-
commands: ["sync", "capabilities", "inspect", "edit", "chat", "wait", "close"]
|
|
286
|
+
commands: ["sync", "capabilities", "inspect", "edit", "project", "patch", "chat", "wait", "close"]
|
|
267
287
|
});
|
|
268
288
|
const nextEvent = (timeoutMs) => {
|
|
269
289
|
if (events.length) return Promise.resolve(events.shift());
|
|
@@ -317,6 +337,19 @@ async function connectAgent(invitationUrl, inputStream = process.stdin, emit = w
|
|
|
317
337
|
} else throw error;
|
|
318
338
|
}
|
|
319
339
|
break;
|
|
340
|
+
case "project":
|
|
341
|
+
respond(command.id, document.project(command.request));
|
|
342
|
+
break;
|
|
343
|
+
case "patch":
|
|
344
|
+
try {
|
|
345
|
+
if (connectionState.current !== "live") throw new Error("The collaboration connection is offline. Reconnect before editing");
|
|
346
|
+
respond(command.id, await document.patch(patchRequestForMode(command.request, mode)));
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (error instanceof Error && error.message.includes("revision is stale")) {
|
|
349
|
+
respond(command.id, { status: "needs_sync", latestRevision: document.revision });
|
|
350
|
+
} else throw error;
|
|
351
|
+
}
|
|
352
|
+
break;
|
|
320
353
|
case "chat": {
|
|
321
354
|
if (connectionState.current !== "live") throw new Error("The collaboration connection is offline. Reconnect before sending chat");
|
|
322
355
|
if (!command.text?.trim()) throw new Error("Chat text is required");
|
|
@@ -360,57 +393,293 @@ async function listen(server, path) {
|
|
|
360
393
|
});
|
|
361
394
|
});
|
|
362
395
|
}
|
|
396
|
+
async function startCodexWakeHost(sessionId) {
|
|
397
|
+
const cwd = sessionDirectory(sessionId);
|
|
398
|
+
const childEnv = { ...process.env };
|
|
399
|
+
delete childEnv.CODEX_THREAD_ID;
|
|
400
|
+
delete childEnv.CLAUDE_CODE_SESSION_ID;
|
|
401
|
+
delete childEnv.CLAUDECODE;
|
|
402
|
+
const child = spawn("codex", ["app-server"], {
|
|
403
|
+
cwd,
|
|
404
|
+
env: childEnv,
|
|
405
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
406
|
+
});
|
|
407
|
+
const output = createInterface({ input: child.stdout, terminal: false });
|
|
408
|
+
const requests = /* @__PURE__ */ new Map();
|
|
409
|
+
let requestId = 0;
|
|
410
|
+
let threadId = "";
|
|
411
|
+
let activeTurn = null;
|
|
412
|
+
let closed = false;
|
|
413
|
+
const fail = (error) => {
|
|
414
|
+
for (const request2 of requests.values()) {
|
|
415
|
+
clearTimeout(request2.timer);
|
|
416
|
+
request2.reject(error);
|
|
417
|
+
}
|
|
418
|
+
requests.clear();
|
|
419
|
+
activeTurn?.reject(error);
|
|
420
|
+
activeTurn = null;
|
|
421
|
+
};
|
|
422
|
+
child.once("error", (error) => fail(error));
|
|
423
|
+
child.once("exit", (code) => {
|
|
424
|
+
if (!closed) {
|
|
425
|
+
fail(new Error(`The Codex document session exited with code ${code ?? "unknown"}`));
|
|
426
|
+
closed = true;
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
output.on("line", (line) => {
|
|
430
|
+
let message;
|
|
431
|
+
try {
|
|
432
|
+
message = JSON.parse(line);
|
|
433
|
+
} catch {
|
|
434
|
+
fail(new Error("The Codex document session returned invalid JSON"));
|
|
435
|
+
child.kill();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (typeof message.id === "number") {
|
|
439
|
+
const request2 = requests.get(message.id);
|
|
440
|
+
if (!request2) return;
|
|
441
|
+
requests.delete(message.id);
|
|
442
|
+
clearTimeout(request2.timer);
|
|
443
|
+
if (message.error) {
|
|
444
|
+
const error = message.error;
|
|
445
|
+
request2.reject(new Error(error.message ?? "The Codex document session request failed"));
|
|
446
|
+
} else {
|
|
447
|
+
request2.resolve(message.result ?? {});
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (message.method !== "turn/completed" || !activeTurn) return;
|
|
452
|
+
const params = message.params;
|
|
453
|
+
if (params?.threadId !== threadId) return;
|
|
454
|
+
if (activeTurn.turnId && params.turn?.id !== activeTurn.turnId) return;
|
|
455
|
+
const turn = activeTurn;
|
|
456
|
+
activeTurn = null;
|
|
457
|
+
if (params.turn?.status === "completed") turn.resolve();
|
|
458
|
+
else turn.reject(new Error(params.turn?.error?.message ?? `The Codex document turn ${params.turn?.status ?? "failed"}`));
|
|
459
|
+
});
|
|
460
|
+
const request = (method, params, timeoutMs = 3e4) => {
|
|
461
|
+
const id = ++requestId;
|
|
462
|
+
return new Promise((resolve, reject) => {
|
|
463
|
+
const timer = setTimeout(() => {
|
|
464
|
+
requests.delete(id);
|
|
465
|
+
reject(new Error(`The Codex document session timed out during ${method}`));
|
|
466
|
+
}, timeoutMs);
|
|
467
|
+
requests.set(id, { resolve, reject, timer });
|
|
468
|
+
child.stdin.write(`${JSON.stringify({ id, method, params })}
|
|
469
|
+
`);
|
|
470
|
+
});
|
|
471
|
+
};
|
|
472
|
+
try {
|
|
473
|
+
await request("initialize", {
|
|
474
|
+
clientInfo: { name: "wordinweb-agent", title: "WordInWeb document agent", version: "0.1.0" }
|
|
475
|
+
});
|
|
476
|
+
child.stdin.write(`${JSON.stringify({ method: "initialized" })}
|
|
477
|
+
`);
|
|
478
|
+
const started = await request("thread/start", {
|
|
479
|
+
cwd,
|
|
480
|
+
approvalPolicy: "never",
|
|
481
|
+
sandbox: "workspace-write",
|
|
482
|
+
ephemeral: true,
|
|
483
|
+
serviceName: "wordinweb-agent",
|
|
484
|
+
developerInstructions: "Act as a WordInWeb document collaborator. Treat each private message as one document task. Use the exact local session command in the message. Complete the task and reply through document chat."
|
|
485
|
+
});
|
|
486
|
+
threadId = String(started.thread?.id ?? "");
|
|
487
|
+
if (!threadId) throw new Error("The Codex document session returned no thread ID");
|
|
488
|
+
} catch (error) {
|
|
489
|
+
closed = true;
|
|
490
|
+
child.kill();
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
async startTurn(prompt) {
|
|
495
|
+
if (closed) throw new Error("The Codex document session is closed");
|
|
496
|
+
if (activeTurn) throw new Error("The Codex document session already has an active turn");
|
|
497
|
+
let turn;
|
|
498
|
+
const completed = new Promise((resolve, reject) => {
|
|
499
|
+
turn = { resolve, reject };
|
|
500
|
+
activeTurn = turn;
|
|
501
|
+
});
|
|
502
|
+
try {
|
|
503
|
+
const started = await request("turn/start", {
|
|
504
|
+
threadId,
|
|
505
|
+
effort: "low",
|
|
506
|
+
sandboxPolicy: { type: "workspaceWrite", networkAccess: true },
|
|
507
|
+
input: [{ type: "text", text: prompt }]
|
|
508
|
+
});
|
|
509
|
+
turn.turnId = String(started.turn?.id ?? "");
|
|
510
|
+
await completed;
|
|
511
|
+
} catch (error) {
|
|
512
|
+
activeTurn = null;
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
async interrupt() {
|
|
517
|
+
const turn = activeTurn;
|
|
518
|
+
if (!turn) return;
|
|
519
|
+
try {
|
|
520
|
+
if (turn.turnId) await request("turn/interrupt", { threadId, turnId: turn.turnId }, 5e3);
|
|
521
|
+
} finally {
|
|
522
|
+
if (activeTurn === turn) {
|
|
523
|
+
activeTurn = null;
|
|
524
|
+
turn.reject(new Error("The Codex document turn timed out"));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
async close() {
|
|
529
|
+
if (closed) return;
|
|
530
|
+
closed = true;
|
|
531
|
+
activeTurn?.reject(new Error("The Codex document session closed"));
|
|
532
|
+
activeTurn = null;
|
|
533
|
+
await request("thread/delete", { threadId }, 2e3).catch(() => void 0).finally(() => {
|
|
534
|
+
output.close();
|
|
535
|
+
child.kill();
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
}
|
|
363
540
|
async function runDaemon(invitationUrl, sessionId) {
|
|
541
|
+
const directory = sessionDirectory(sessionId);
|
|
542
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
543
|
+
chmodSync(directory, 448);
|
|
364
544
|
const path = sessionSocketPath(sessionId);
|
|
365
545
|
const input = new PassThrough();
|
|
366
546
|
const wakeTarget = detectWakeTarget();
|
|
547
|
+
const wakeTimeoutMs = Number(process.env.WORDINWEB_WAKE_TIMEOUT_MS) || 6e4;
|
|
367
548
|
const wakeQueue = [];
|
|
368
549
|
const pending = /* @__PURE__ */ new Map();
|
|
369
550
|
let readyEvent = null;
|
|
370
551
|
let fatalError = null;
|
|
371
552
|
let closeTimer = null;
|
|
372
553
|
let wakeChild = null;
|
|
554
|
+
let codexWakeHost = null;
|
|
555
|
+
let activeWake = null;
|
|
556
|
+
let wakeState = "idle";
|
|
557
|
+
let wakeId = null;
|
|
558
|
+
let wakeStartedAt = null;
|
|
559
|
+
let lastWakeError = null;
|
|
560
|
+
let lastSuccessfulChatAt = null;
|
|
373
561
|
let requestCounter = 0;
|
|
374
562
|
let stopped = false;
|
|
375
|
-
|
|
376
|
-
|
|
563
|
+
let shutdownPromise = null;
|
|
564
|
+
const sendWakeFailure = (text) => {
|
|
565
|
+
input.write(`${JSON.stringify({ command: "chat", text })}
|
|
377
566
|
`);
|
|
378
567
|
};
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const
|
|
568
|
+
let startNextWake = () => void 0;
|
|
569
|
+
const finishWake = (id, error, failureReply) => {
|
|
570
|
+
const wake = activeWake;
|
|
571
|
+
if (!wake || wake.id !== id) return;
|
|
572
|
+
clearTimeout(wake.timer);
|
|
573
|
+
activeWake = null;
|
|
574
|
+
if (error && !wake.replied) {
|
|
575
|
+
wakeState = "failed";
|
|
576
|
+
lastWakeError = error;
|
|
577
|
+
if (!stopped) sendWakeFailure(failureReply);
|
|
578
|
+
} else {
|
|
579
|
+
wakeState = "idle";
|
|
580
|
+
lastWakeError = null;
|
|
581
|
+
}
|
|
582
|
+
startNextWake();
|
|
583
|
+
};
|
|
584
|
+
startNextWake = () => {
|
|
585
|
+
if (!wakeTarget || wakeChild || activeWake || wakeQueue.length === 0 || stopped) return;
|
|
586
|
+
const text = wakeQueue.shift();
|
|
587
|
+
const id = randomId("wake");
|
|
588
|
+
const startedAt = Date.now();
|
|
589
|
+
wakeId = id;
|
|
590
|
+
wakeStartedAt = startedAt;
|
|
591
|
+
wakeState = "active";
|
|
592
|
+
lastWakeError = null;
|
|
593
|
+
const timer = setTimeout(() => {
|
|
594
|
+
const wake = activeWake;
|
|
595
|
+
if (!wake || wake.id !== id) return;
|
|
596
|
+
wake.timedOut = true;
|
|
597
|
+
if (wakeTarget.provider === "codex") {
|
|
598
|
+
const interrupted = codexWakeHost ? codexWakeHost.interrupt() : Promise.resolve();
|
|
599
|
+
void interrupted.finally(() => {
|
|
600
|
+
finishWake(id, "turn_timed_out", "The bridge stayed connected, but this document task timed out. Send the task again.");
|
|
601
|
+
});
|
|
602
|
+
} else {
|
|
603
|
+
wakeChild?.kill();
|
|
604
|
+
}
|
|
605
|
+
}, wakeTimeoutMs);
|
|
606
|
+
timer.unref();
|
|
607
|
+
activeWake = { id, startedAt, replied: false, timedOut: false, timer };
|
|
608
|
+
const prompt = agentWakePrompt(sessionId, id, text);
|
|
609
|
+
if (wakeTarget.provider === "codex") {
|
|
610
|
+
if (!codexWakeHost) {
|
|
611
|
+
finishWake(id, "wake_host_unavailable", "The bridge stayed connected, but this document task could not start. Send the task again.");
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
void codexWakeHost.startTurn(prompt).then(
|
|
615
|
+
() => {
|
|
616
|
+
const replied = activeWake?.id === id && activeWake.replied;
|
|
617
|
+
finishWake(
|
|
618
|
+
id,
|
|
619
|
+
replied ? null : "turn_completed_without_chat",
|
|
620
|
+
"The bridge stayed connected, but this document task ended before the agent sent a reply. Send the task again."
|
|
621
|
+
);
|
|
622
|
+
},
|
|
623
|
+
() => {
|
|
624
|
+
const timedOut = activeWake?.id === id && activeWake.timedOut;
|
|
625
|
+
finishWake(
|
|
626
|
+
id,
|
|
627
|
+
timedOut ? "turn_timed_out" : "turn_failed",
|
|
628
|
+
timedOut ? "The bridge stayed connected, but this document task timed out. Send the task again." : "The bridge stayed connected, but this document task failed. Send the task again."
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
382
634
|
const childEnv = { ...process.env };
|
|
383
635
|
delete childEnv.CODEX_THREAD_ID;
|
|
384
636
|
delete childEnv.CLAUDE_CODE_SESSION_ID;
|
|
385
637
|
delete childEnv.CLAUDECODE;
|
|
386
|
-
wakeChild =
|
|
638
|
+
wakeChild = spawn("claude", ["-p", "--resume", wakeTarget.sessionId], { env: childEnv, stdio: ["pipe", "ignore", "ignore"] });
|
|
387
639
|
let finished = false;
|
|
388
640
|
const finish = (ok) => {
|
|
389
641
|
if (finished) return;
|
|
390
642
|
finished = true;
|
|
391
643
|
wakeChild = null;
|
|
392
|
-
|
|
393
|
-
|
|
644
|
+
const wake = activeWake?.id === id ? activeWake : null;
|
|
645
|
+
const error = wake?.timedOut ? "turn_timed_out" : ok && wake?.replied ? null : "turn_failed";
|
|
646
|
+
finishWake(
|
|
647
|
+
id,
|
|
648
|
+
error,
|
|
649
|
+
wake?.timedOut ? "The bridge stayed connected, but this document task timed out. Send the task again." : "The bridge stayed connected, but this document task failed. Send the task again."
|
|
650
|
+
);
|
|
394
651
|
};
|
|
395
652
|
wakeChild.once("error", () => finish(false));
|
|
396
653
|
wakeChild.once("exit", (code) => finish(code === 0));
|
|
397
654
|
wakeChild.stdin?.on("error", () => void 0);
|
|
398
655
|
wakeChild.stdin?.end(prompt);
|
|
399
656
|
};
|
|
657
|
+
let serverListening = false;
|
|
400
658
|
const shutdown = () => {
|
|
401
|
-
if (
|
|
659
|
+
if (shutdownPromise) return shutdownPromise;
|
|
402
660
|
stopped = true;
|
|
403
661
|
if (closeTimer) clearTimeout(closeTimer);
|
|
662
|
+
if (activeWake) clearTimeout(activeWake.timer);
|
|
663
|
+
activeWake = null;
|
|
404
664
|
wakeChild?.kill();
|
|
665
|
+
const host = codexWakeHost;
|
|
666
|
+
codexWakeHost = null;
|
|
405
667
|
input.end();
|
|
406
|
-
|
|
407
|
-
|
|
668
|
+
shutdownPromise = Promise.all([
|
|
669
|
+
host?.close() ?? Promise.resolve(),
|
|
670
|
+
serverListening ? new Promise((resolve) => server.close(() => resolve())) : Promise.resolve()
|
|
671
|
+
]).then(() => {
|
|
672
|
+
rmSync(directory, { recursive: true, force: true });
|
|
673
|
+
});
|
|
674
|
+
return shutdownPromise;
|
|
408
675
|
};
|
|
409
676
|
const emit = (value) => {
|
|
410
677
|
const message = value;
|
|
411
678
|
if (message.event === "ready") readyEvent = message;
|
|
412
679
|
if (message.event === "connection_closed" && !closeTimer) {
|
|
413
|
-
closeTimer = setTimeout(
|
|
680
|
+
closeTimer = setTimeout(() => {
|
|
681
|
+
void shutdown();
|
|
682
|
+
}, 6e4);
|
|
414
683
|
closeTimer.unref();
|
|
415
684
|
}
|
|
416
685
|
if (message.event === "chat" && typeof message.text === "string" && wakeTarget) {
|
|
@@ -421,6 +690,10 @@ async function runDaemon(invitationUrl, sessionId) {
|
|
|
421
690
|
const command = pending.get(message.id);
|
|
422
691
|
if (!command) return;
|
|
423
692
|
pending.delete(message.id);
|
|
693
|
+
if (message.ok === true && command.command === "chat" && command.wakeId && activeWake?.id === command.wakeId) {
|
|
694
|
+
activeWake.replied = true;
|
|
695
|
+
lastSuccessfulChatAt = Date.now();
|
|
696
|
+
}
|
|
424
697
|
const response = { ...message };
|
|
425
698
|
if (command.originalId === void 0) delete response.id;
|
|
426
699
|
else response.id = command.originalId;
|
|
@@ -438,16 +711,35 @@ async function runDaemon(invitationUrl, sessionId) {
|
|
|
438
711
|
const message = JSON.parse(buffer.slice(0, newline));
|
|
439
712
|
if (message.daemon === "status") {
|
|
440
713
|
if (fatalError) sendSocketValue(socket, { ok: false, error: fatalError });
|
|
441
|
-
else
|
|
442
|
-
|
|
714
|
+
else sendSocketValue(socket, {
|
|
715
|
+
ok: true,
|
|
716
|
+
result: {
|
|
717
|
+
state: readyEvent ? "ready" : "starting",
|
|
718
|
+
ready: readyEvent,
|
|
719
|
+
wakeState,
|
|
720
|
+
wakeId,
|
|
721
|
+
wakeStartedAt,
|
|
722
|
+
lastWakeError,
|
|
723
|
+
lastSuccessfulChatAt
|
|
724
|
+
}
|
|
725
|
+
});
|
|
443
726
|
return;
|
|
444
727
|
}
|
|
445
728
|
if (!readyEvent) {
|
|
446
729
|
sendSocketValue(socket, { ok: false, error: fatalError ?? "The agent bridge is still starting" });
|
|
447
730
|
return;
|
|
448
731
|
}
|
|
732
|
+
if (message.command === "chat" && activeWake && message.wakeId !== activeWake.id) {
|
|
733
|
+
sendSocketValue(socket, { ok: false, error: "The chat command must include the current wakeId" });
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
449
736
|
const id = `request_${++requestCounter}`;
|
|
450
|
-
pending.set(id, {
|
|
737
|
+
pending.set(id, {
|
|
738
|
+
socket,
|
|
739
|
+
originalId: message.id,
|
|
740
|
+
command: typeof message.command === "string" ? message.command : void 0,
|
|
741
|
+
wakeId: typeof message.wakeId === "string" ? message.wakeId : void 0
|
|
742
|
+
});
|
|
451
743
|
input.write(`${JSON.stringify({ ...message, id })}
|
|
452
744
|
`);
|
|
453
745
|
} catch (error) {
|
|
@@ -455,8 +747,10 @@ async function runDaemon(invitationUrl, sessionId) {
|
|
|
455
747
|
}
|
|
456
748
|
});
|
|
457
749
|
});
|
|
458
|
-
await listen(server, path);
|
|
459
750
|
try {
|
|
751
|
+
await listen(server, path);
|
|
752
|
+
serverListening = true;
|
|
753
|
+
if (wakeTarget?.provider === "codex") codexWakeHost = await startCodexWakeHost(sessionId);
|
|
460
754
|
await connectAgent(invitationUrl, input, emit);
|
|
461
755
|
} catch (error) {
|
|
462
756
|
fatalError = error instanceof Error ? error.message : String(error);
|
|
@@ -464,7 +758,7 @@ async function runDaemon(invitationUrl, sessionId) {
|
|
|
464
758
|
pending.clear();
|
|
465
759
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
466
760
|
} finally {
|
|
467
|
-
shutdown();
|
|
761
|
+
await shutdown();
|
|
468
762
|
}
|
|
469
763
|
}
|
|
470
764
|
function daemonRequest(sessionId, request, timeoutMs = 6e4) {
|
|
@@ -519,7 +813,7 @@ async function startDaemon(invitationUrl) {
|
|
|
519
813
|
}
|
|
520
814
|
} catch (error) {
|
|
521
815
|
const code = error.code;
|
|
522
|
-
if (code !== "ENOENT" && code !== "ECONNREFUSED") throw error;
|
|
816
|
+
if (code !== "ENOENT" && code !== "ECONNREFUSED" && code !== "EINVAL") throw error;
|
|
523
817
|
}
|
|
524
818
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
525
819
|
}
|