@wordinweb/agent 0.1.0 → 0.2.0

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/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-E7AFDS2O.js";
10
+ } from "./chunk-3HREJUDW.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 resumed this agent task.",
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}. Do not run the connect command again.`,
112
- `Run document commands with: npx -y --package='${AGENT_PACKAGE_URL}' wordinweb-agent session '${agentSessionId}' '<JSON command>'`,
113
- 'Start with sync. For a broad text task, inspect all non-empty stories in one compact call: {"command":"inspect","request":{"kind":"context"}}. Use overview, read, object, or spatial only when the task needs their extra detail.',
114
- "Inspect the relevant content before every edit. If an edit returns needs_sync, sync and inspect again.",
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. Do not call wait. The detached bridge will resume this task for the next message."
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 sessionSocketPath(sessionId) {
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
- return process.platform === "win32" ? `\\\\.\\pipe\\wordinweb-agent-${sessionId}` : join("/tmp", `wordinweb-agent-${sessionId}.sock`);
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
- const sendWakeFailure = () => {
376
- input.write(`${JSON.stringify({ command: "chat", text: "I could not resume the agent task. Reopen the original Codex or Claude task and reconnect this AI." })}
563
+ let shutdownPromise = null;
564
+ const sendWakeFailure = (text) => {
565
+ input.write(`${JSON.stringify({ command: "chat", text })}
377
566
  `);
378
567
  };
379
- const startNextWake = () => {
380
- if (!wakeTarget || wakeChild || wakeQueue.length === 0 || stopped) return;
381
- const prompt = agentWakePrompt(sessionId, wakeQueue.shift());
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 = wakeTarget.provider === "codex" ? spawn("codex", ["exec", "resume", wakeTarget.sessionId, "-"], { env: childEnv, stdio: ["pipe", "ignore", "ignore"] }) : spawn("claude", ["-p", "--resume", wakeTarget.sessionId], { env: childEnv, stdio: ["pipe", "ignore", "ignore"] });
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
- if (!ok && !stopped) sendWakeFailure();
393
- startNextWake();
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 (stopped) return;
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
- server.close();
407
- if (process.platform !== "win32") rmSync(path, { force: true });
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(shutdown, 6e4);
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 if (readyEvent) sendSocketValue(socket, { ok: true, result: { state: "ready", ready: readyEvent } });
442
- else sendSocketValue(socket, { ok: true, result: { state: "starting" } });
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, { socket, originalId: message.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
  }