codedeck 0.7.1 → 0.7.3

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 (57) hide show
  1. package/README.md +24 -8
  2. package/dist/cli/commands/run.js +3 -2
  3. package/dist/cli/commands/run.js.map +1 -1
  4. package/dist/cli/commands/service.js +139 -0
  5. package/dist/cli/commands/service.js.map +1 -0
  6. package/dist/cli/index.js +4 -2
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/config/config.js.map +1 -1
  9. package/dist/config/paths.js +14 -14
  10. package/dist/config/paths.js.map +1 -1
  11. package/dist/core/errors.js +5 -42
  12. package/dist/core/errors.js.map +1 -1
  13. package/dist/core/session.js +1 -1
  14. package/dist/core/session.js.map +1 -1
  15. package/dist/core/usage-source.js +2 -0
  16. package/dist/core/usage-source.js.map +1 -1
  17. package/dist/daemon/auto-resume.js +18 -0
  18. package/dist/daemon/auto-resume.js.map +1 -0
  19. package/dist/daemon/daemon.js +188 -125
  20. package/dist/daemon/daemon.js.map +1 -1
  21. package/dist/drivers/antigravity/parser.js +7 -10
  22. package/dist/drivers/antigravity/parser.js.map +1 -1
  23. package/dist/drivers/codex/parser.js +20 -2
  24. package/dist/drivers/codex/parser.js.map +1 -1
  25. package/dist/drivers/grok/driver.js +163 -0
  26. package/dist/drivers/grok/driver.js.map +1 -0
  27. package/dist/drivers/grok/parser.js +46 -0
  28. package/dist/drivers/grok/parser.js.map +1 -0
  29. package/dist/drivers/registry.js +2 -0
  30. package/dist/drivers/registry.js.map +1 -1
  31. package/dist/git/repository.js +0 -20
  32. package/dist/git/repository.js.map +1 -1
  33. package/dist/git/worktree.js +0 -14
  34. package/dist/git/worktree.js.map +1 -1
  35. package/dist/open/injection.js +4 -3
  36. package/dist/open/injection.js.map +1 -1
  37. package/dist/plugin/agents/auditor.md +1 -1
  38. package/dist/plugin/agents/orchestrator-edit.md +1 -1
  39. package/dist/plugin/agents/orchestrator-read.md +1 -1
  40. package/dist/plugin/agents/orchestrator.md +1 -1
  41. package/dist/plugin/agents/reviewer.md +1 -1
  42. package/dist/plugin/mods/agents/pane.ts +2 -0
  43. package/dist/plugin/prompts/roles/auditor.md +1 -1
  44. package/dist/plugin/prompts/roles/orchestrator-edit.md +1 -1
  45. package/dist/plugin/prompts/roles/orchestrator-read.md +1 -1
  46. package/dist/plugin/prompts/roles/orchestrator.md +1 -1
  47. package/dist/plugin/prompts/roles/reviewer.md +1 -1
  48. package/dist/store/claims.js +4 -4
  49. package/dist/store/database.js +1 -7
  50. package/dist/store/database.js.map +1 -1
  51. package/dist/store/sessions.js +63 -73
  52. package/dist/store/sessions.js.map +1 -1
  53. package/dist/utils/process.js +2 -35
  54. package/dist/utils/process.js.map +1 -1
  55. package/dist/web/setup-icons.js +2 -0
  56. package/dist/web/setup-icons.js.map +1 -1
  57. package/package.json +6 -5
@@ -19,9 +19,10 @@ import { getDiff } from "../git/diff.js";
19
19
  import { killTree, processAlive, processStartTime, sleep } from "../utils/process.js";
20
20
  import { readSessionProcessMetadata } from "../drivers/session-runtime.js";
21
21
  import { loadConfig, resolveDefaultSandbox } from "../config/config.js";
22
+ import { buildAutoResumePrompt, isAutoResumeEligible } from "./auto-resume.js";
22
23
  import { invalidWebHostMessage, resolveWebHost } from "../config/web-host.js";
23
24
  import { invalidWebPortMessage, resolveWebPort } from "../config/web-port.js";
24
- import { classifyFailure, isStoreBusy, RunAgentError } from "../core/errors.js";
25
+ import { classifyFailure, errorMessage, isStoreBusy, CodedeckError } from "../core/errors.js";
25
26
  import { parseRole } from "../core/roles.js";
26
27
  import { getCachedOrDiscoverModels } from "../core/models.js";
27
28
  import { aggregateRunUsage } from "../core/run-usage.js";
@@ -97,10 +98,12 @@ class Daemon {
97
98
  nativeLinks;
98
99
  registry = getRegistry();
99
100
  server;
101
+ boundPaths;
100
102
  subscribers = new Map(); // sessionId -> sockets
101
103
  startTime = Date.now();
102
104
  startupReconcilePromise = Promise.resolve();
103
105
  sessionLocks = new Set();
106
+ autoResumeStarted = false;
104
107
  reservedSessionIds = new Set();
105
108
  // Sessions recover() revived from a store-busy failure: the sequence the
106
109
  // drain starts after, and the log mtime when the harness is already dead.
@@ -261,7 +264,7 @@ class Daemon {
261
264
  queueReleasedTranscriptCursorCleanup(sessionId) {
262
265
  queueMicrotask(() => {
263
266
  void this.cleanupReleasedTranscriptCursors(sessionId).catch((error) => {
264
- const detail = error instanceof Error ? error.message : String(error);
267
+ const detail = errorMessage(error);
265
268
  try {
266
269
  fs.appendFileSync(getPaths().daemonLog, `[${new Date().toISOString()}] usage cursor cleanup failed session=${sessionId}: ${detail}\n`);
267
270
  }
@@ -315,7 +318,7 @@ class Daemon {
315
318
  await this.handleRequest(req, socket);
316
319
  }
317
320
  catch (e) {
318
- const res = { id: req.id, error: { code: "INTERNAL", message: e instanceof Error ? e.message : String(e) } };
321
+ const res = { id: req.id, error: { code: "INTERNAL", message: errorMessage(e) } };
319
322
  try {
320
323
  socket.write(JSON.stringify(res) + "\n");
321
324
  }
@@ -326,6 +329,10 @@ class Daemon {
326
329
  this.server.listen(paths.daemonSock, () => resolve());
327
330
  this.server.on("error", reject);
328
331
  });
332
+ this.boundPaths = {
333
+ sock: paths.daemonSock,
334
+ pid: paths.daemonPid,
335
+ };
329
336
  // Write pid file
330
337
  try {
331
338
  fs.writeFileSync(paths.daemonPid, String(process.pid), "utf-8");
@@ -349,6 +356,102 @@ class Daemon {
349
356
  fs.appendFileSync(paths.daemonLog, `[${new Date().toISOString()}] daemon started pid=${process.pid}\n`);
350
357
  }
351
358
  catch { }
359
+ this.startAutoResume();
360
+ }
361
+ startAutoResume() {
362
+ if (this.autoResumeStarted)
363
+ return;
364
+ this.autoResumeStarted = true;
365
+ const config = loadConfig().autoResume;
366
+ if (config?.enabled !== true)
367
+ return;
368
+ const maxAgeHours = typeof config.maxAgeHours === "number" && Number.isFinite(config.maxAgeHours)
369
+ ? config.maxAgeHours
370
+ : 24;
371
+ void this.autoResumeInterruptedSessions(maxAgeHours).catch((error) => {
372
+ appendDaemonLog(`auto-resume boot scan failed: ${errorMessage(error)}`);
373
+ });
374
+ }
375
+ async autoResumeInterruptedSessions(maxAgeHours) {
376
+ const ids = this.db.getHandle().prepare("SELECT id FROM sessions WHERE status = 'interrupted' ORDER BY updated_at ASC").all();
377
+ for (const { id } of ids) {
378
+ if (this.shuttingDown)
379
+ break;
380
+ const session = this.sessions.getExact(id);
381
+ if (!session)
382
+ continue;
383
+ let driver;
384
+ try {
385
+ driver = this.registry.get(session.agent);
386
+ }
387
+ catch (error) {
388
+ appendDaemonLog(`auto-resume failed for ${session.id}: ${errorMessage(error)}`);
389
+ continue;
390
+ }
391
+ if (!isAutoResumeEligible(session, {
392
+ now: new Date(),
393
+ maxAgeHours,
394
+ canResume: driver.capabilities().resume,
395
+ liveIdentity: livePidIdentity(session),
396
+ }))
397
+ continue;
398
+ if (this.sessionLocks.has(session.id))
399
+ continue;
400
+ const workingDirectory = session.worktree || session.cwd;
401
+ if (!fs.existsSync(workingDirectory)) {
402
+ appendDaemonLog(`auto-resume skipped ${session.id}: working directory missing (${workingDirectory})`);
403
+ continue;
404
+ }
405
+ this.sessionLocks.add(session.id);
406
+ const pendingMessage = session.pendingMessage;
407
+ const pendingAt = session.pendingAt;
408
+ try {
409
+ try {
410
+ this.sessions.update(session.id, { pendingMessage: null, pendingAt: null });
411
+ }
412
+ catch { }
413
+ await this.runResumeTurn(session, buildAutoResumePrompt(session.pendingMessage));
414
+ appendDaemonLog(`auto-resume started session ${session.id}`);
415
+ }
416
+ catch (error) {
417
+ try {
418
+ if (pendingMessage) {
419
+ this.sessions.update(session.id, {
420
+ pendingMessage,
421
+ pendingAt: pendingAt ?? new Date().toISOString(),
422
+ });
423
+ }
424
+ }
425
+ catch { }
426
+ if (this.shuttingDown || error?.code === "SERVICE_UNAVAILABLE") {
427
+ appendDaemonLog(`auto-resume paused for ${session.id}: daemon is shutting down`);
428
+ break;
429
+ }
430
+ const message = errorMessage(error);
431
+ const failure = classifyFailure(message);
432
+ const failedEvent = {
433
+ type: "session.failed",
434
+ sessionId: session.id,
435
+ timestamp: new Date().toISOString(),
436
+ error: message,
437
+ failure,
438
+ };
439
+ try {
440
+ this.events.append(session.id, failedEvent);
441
+ }
442
+ catch { }
443
+ this.broadcast(session.id, failedEvent);
444
+ try {
445
+ this.sessions.setStatus(session.id, "failed", { lastEvent: message.slice(0, 200), failure });
446
+ }
447
+ catch { }
448
+ appendDaemonLog(`auto-resume failed for ${session.id}: ${errorMessage(error)}`);
449
+ }
450
+ finally {
451
+ if (!this.shuttingDown)
452
+ this.sessionLocks.delete(session.id);
453
+ }
454
+ }
352
455
  }
353
456
  // Sessions whose detached process outlived the daemon are RE-ATTACHED, not
354
457
  // orphaned: the process is independent (own process group, output in log
@@ -554,7 +657,7 @@ class Daemon {
554
657
  this.sessions.update(sessionId, { completedAt: heal.endedAt });
555
658
  }
556
659
  catch (error) {
557
- appendDaemonLog(`heal timestamps for ${sessionId} failed: ${error instanceof Error ? error.message : String(error)}`);
660
+ appendDaemonLog(`heal timestamps for ${sessionId} failed: ${errorMessage(error)}`);
558
661
  }
559
662
  }
560
663
  async reconcileOpenUsage(sessionId, nativeIds) {
@@ -605,7 +708,7 @@ class Daemon {
605
708
  return true;
606
709
  }
607
710
  catch (error) {
608
- const detail = error instanceof Error ? error.message : String(error);
711
+ const detail = errorMessage(error);
609
712
  try {
610
713
  fs.appendFileSync(getPaths().daemonLog, `[${new Date().toISOString()}] usage reconcile failed session=${sessionId}: ${detail}\n`);
611
714
  }
@@ -626,6 +729,23 @@ class Daemon {
626
729
  releaseSessionId(sessionId) {
627
730
  this.reservedSessionIds.delete(sessionId);
628
731
  }
732
+ resolveRegisteredAgent(rawAgent) {
733
+ const agent = (normalizeAgentId(rawAgent) ?? rawAgent);
734
+ return { agent, registered: this.registry.has(agent) };
735
+ }
736
+ handleClaimRequest(requestedSessionId, operation, result, send) {
737
+ try {
738
+ const sessionId = this.sessions.get(requestedSessionId)?.id ?? requestedSessionId;
739
+ send(result(operation(sessionId)));
740
+ }
741
+ catch (error) {
742
+ if (error instanceof CodedeckError) {
743
+ send({ error: { code: error.code, message: error.message, details: error.details } });
744
+ return;
745
+ }
746
+ throw error;
747
+ }
748
+ }
629
749
  async handleRequest(req, socket) {
630
750
  const { id, method, params } = req;
631
751
  const send = (res) => {
@@ -634,11 +754,13 @@ class Daemon {
634
754
  }
635
755
  catch { }
636
756
  };
757
+ const sendSessionNotFound = (sessionId) => send({ error: { code: "SESSION_NOT_FOUND", message: `Session ${sessionId} not found` } });
758
+ const sendShuttingDown = () => send({ error: { code: "SERVICE_UNAVAILABLE", message: "daemon is shutting down" } });
637
759
  // Power-shutdown guard: while draining, refuse new work so a send/stop
638
760
  // cannot race the interrupted persist. daemon.stop is the shutdown
639
761
  // trigger itself, so it stays admitted (idempotent via handleShutdown).
640
762
  if (this.shuttingDown && method !== "daemon.stop") {
641
- send({ error: { code: "SERVICE_UNAVAILABLE", message: "daemon is shutting down" } });
763
+ sendShuttingDown();
642
764
  return;
643
765
  }
644
766
  try {
@@ -653,8 +775,8 @@ class Daemon {
653
775
  const cwdIn = p.cwd || process.cwd();
654
776
  const cfg = loadConfig();
655
777
  const rawAgent = p.agent || cfg.defaultAgent || "claude";
656
- let agent = (normalizeAgentId(rawAgent) ?? rawAgent);
657
- if (!this.registry.has(agent)) {
778
+ const { agent, registered } = this.resolveRegisteredAgent(rawAgent);
779
+ if (!registered) {
658
780
  send({ error: { code: "AGENT_NOT_FOUND", message: `Unknown agent ${agent}` } });
659
781
  return;
660
782
  }
@@ -682,7 +804,7 @@ class Daemon {
682
804
  }
683
805
  if (this.shuttingDown) {
684
806
  this.releaseSessionId(sessionId);
685
- send({ error: { code: "SERVICE_UNAVAILABLE", message: "daemon is shutting down" } });
807
+ sendShuttingDown();
686
808
  return;
687
809
  }
688
810
  if (gitInfo)
@@ -694,7 +816,7 @@ class Daemon {
694
816
  const wt = await createWorktree({ repoRoot: gitInfo.root, sessionId, prompt, name: p.name });
695
817
  if (this.shuttingDown) {
696
818
  this.releaseSessionId(sessionId);
697
- send({ error: { code: "SERVICE_UNAVAILABLE", message: "daemon is shutting down" } });
819
+ sendShuttingDown();
698
820
  return;
699
821
  }
700
822
  worktree = wt.path;
@@ -704,7 +826,7 @@ class Daemon {
704
826
  }
705
827
  catch (e) {
706
828
  this.releaseSessionId(sessionId);
707
- send({ error: { code: "WORKTREE_FAILED", message: e instanceof Error ? e.message : String(e) } });
829
+ send({ error: { code: "WORKTREE_FAILED", message: errorMessage(e) } });
708
830
  return;
709
831
  }
710
832
  }
@@ -741,13 +863,12 @@ class Daemon {
741
863
  this.releaseSessionId(sessionId);
742
864
  }
743
865
  // Start driver async (don't block response too long)
744
- // But we need to start before returning? Return quickly then start
745
866
  send({ result: { session } });
746
867
  // Now start driver in background
747
868
  this.startDriverForSession(sessionId, prompt, p.model).catch((e) => {
748
869
  if (this.shuttingDown)
749
870
  return;
750
- const msg = e instanceof Error ? e.message : String(e);
871
+ const msg = errorMessage(e);
751
872
  const failure = classifyFailure(msg);
752
873
  const failEv = {
753
874
  type: "session.failed",
@@ -769,8 +890,8 @@ class Daemon {
769
890
  const p = params;
770
891
  const cwdIn = p.cwd || process.cwd();
771
892
  const rawAgent = p.agent || "claude";
772
- const agent = (normalizeAgentId(rawAgent) ?? rawAgent);
773
- if (!this.registry.has(agent)) {
893
+ const { agent, registered } = this.resolveRegisteredAgent(rawAgent);
894
+ if (!registered) {
774
895
  send({ error: { code: "AGENT_NOT_FOUND", message: `Unknown agent ${agent}` } });
775
896
  return;
776
897
  }
@@ -779,7 +900,7 @@ class Daemon {
779
900
  let baseCommit = p.baseCommit;
780
901
  const gitInfo = await getGitInfo(cwd);
781
902
  if (this.shuttingDown) {
782
- send({ error: { code: "SERVICE_UNAVAILABLE", message: "daemon is shutting down" } });
903
+ sendShuttingDown();
783
904
  return;
784
905
  }
785
906
  const matchingSession = typeof p.resume === "string" && p.resume.length > 0
@@ -808,8 +929,7 @@ class Daemon {
808
929
  agent,
809
930
  nativeSessionId: p.resume,
810
931
  };
811
- this.events.append(revived.id, event);
812
- this.broadcast(revived.id, event);
932
+ this.emit(revived.id, event);
813
933
  send({ result: { session: revived } });
814
934
  break;
815
935
  }
@@ -851,7 +971,7 @@ class Daemon {
851
971
  const p = params;
852
972
  const s = this.sessions.get(p.id);
853
973
  if (!s) {
854
- send({ error: { code: "SESSION_NOT_FOUND", message: `Session ${p.id} not found` } });
974
+ sendSessionNotFound(p.id);
855
975
  return;
856
976
  }
857
977
  let pidStartTime = p.pidStartTime;
@@ -884,7 +1004,7 @@ class Daemon {
884
1004
  }
885
1005
  const session = this.sessions.get(p.id);
886
1006
  if (!session) {
887
- send({ error: { code: "SESSION_NOT_FOUND", message: `Session ${p.id} not found` } });
1007
+ sendSessionNotFound(p.id);
888
1008
  return;
889
1009
  }
890
1010
  if (session.origin !== "open") {
@@ -901,7 +1021,7 @@ class Daemon {
901
1021
  const p = params;
902
1022
  const s = this.sessions.get(p.id);
903
1023
  if (!s) {
904
- send({ error: { code: "SESSION_NOT_FOUND", message: `Session ${p.id} not found` } });
1024
+ sendSessionNotFound(p.id);
905
1025
  return;
906
1026
  }
907
1027
  // Release owns the terminal outcome: a queued message must not
@@ -953,8 +1073,7 @@ class Daemon {
953
1073
  timestamp: new Date().toISOString(),
954
1074
  reason: "released",
955
1075
  };
956
- this.events.append(s.id, ev);
957
- this.broadcast(s.id, ev);
1076
+ this.emit(s.id, ev);
958
1077
  const updated = this.sessions.get(s.id);
959
1078
  send({ result: { session: updated } });
960
1079
  break;
@@ -964,7 +1083,6 @@ class Daemon {
964
1083
  const all = p?.all;
965
1084
  const list = this.sessions.list(100, all).map(withLiveStatus);
966
1085
  const hidden = all ? 0 : this.sessions.countHiddenByWindow();
967
- // Enrich with last event?
968
1086
  send({ result: { sessions: list, hidden } });
969
1087
  break;
970
1088
  }
@@ -972,7 +1090,7 @@ class Daemon {
972
1090
  const p = params;
973
1091
  const stored = this.sessions.get(p.id);
974
1092
  if (!stored) {
975
- send({ error: { code: "SESSION_NOT_FOUND", message: `Session ${p.id} not found` } });
1093
+ sendSessionNotFound(p.id);
976
1094
  return;
977
1095
  }
978
1096
  const s = withLiveStatus(stored);
@@ -985,7 +1103,7 @@ class Daemon {
985
1103
  const p = params;
986
1104
  const s = this.sessions.get(p.id);
987
1105
  if (!s) {
988
- send({ error: { code: "SESSION_NOT_FOUND", message: `Session ${p.id} not found` } });
1106
+ sendSessionNotFound(p.id);
989
1107
  return;
990
1108
  }
991
1109
  if (!p.name || !p.name.trim()) {
@@ -1006,7 +1124,7 @@ class Daemon {
1006
1124
  const message = validated.message;
1007
1125
  const s = this.sessions.get(p.id);
1008
1126
  if (!s) {
1009
- send({ error: { code: "SESSION_NOT_FOUND", message: `Session ${p.id} not found` } });
1127
+ sendSessionNotFound(p.id);
1010
1128
  return;
1011
1129
  }
1012
1130
  if (s.origin === "open") {
@@ -1053,7 +1171,7 @@ class Daemon {
1053
1171
  this.sessions.update(s.id, { pendingMessage: message, pendingAt });
1054
1172
  }
1055
1173
  catch (e) {
1056
- send({ error: { code: "SEND_FAILED", message: e instanceof Error ? e.message : String(e) } });
1174
+ send({ error: { code: "SEND_FAILED", message: errorMessage(e) } });
1057
1175
  return;
1058
1176
  }
1059
1177
  const queuedEvent = {
@@ -1089,7 +1207,7 @@ class Daemon {
1089
1207
  send({ result: { ok: true, queued: false } });
1090
1208
  }
1091
1209
  catch (e) {
1092
- const errText = e instanceof Error ? e.message : String(e);
1210
+ const errText = errorMessage(e);
1093
1211
  const code = e instanceof Error && e.code === "SERVICE_UNAVAILABLE"
1094
1212
  ? "SERVICE_UNAVAILABLE"
1095
1213
  : this.shuttingDown ? "SERVICE_UNAVAILABLE" : "SEND_FAILED";
@@ -1113,7 +1231,7 @@ class Daemon {
1113
1231
  const p = params;
1114
1232
  const s = this.sessions.get(p.id);
1115
1233
  if (!s) {
1116
- send({ error: { code: "SESSION_NOT_FOUND", message: `Session ${p.id} not found` } });
1234
+ sendSessionNotFound(p.id);
1117
1235
  return;
1118
1236
  }
1119
1237
  // Stop cancels a queued message on every exit (explicit cancel),
@@ -1126,10 +1244,7 @@ class Daemon {
1126
1244
  }
1127
1245
  const handle = driver.getHandle?.(s.id);
1128
1246
  const hasRuntime = handle !== undefined;
1129
- const liveIdentity = s.pid != null &&
1130
- s.pidStartTime != null &&
1131
- processAlive(s.pid) &&
1132
- processStartTime(s.pid) === s.pidStartTime;
1247
+ const liveIdentity = livePidIdentity(s);
1133
1248
  if (isTerminalStatus(s.status) && !hasRuntime && !liveIdentity) {
1134
1249
  send({ error: { code: "SESSION_NOT_RUNNING", message: `Session ${s.id} is ${s.status}` } });
1135
1250
  return;
@@ -1155,7 +1270,7 @@ class Daemon {
1155
1270
  pidStartTime: s.pidStartTime,
1156
1271
  });
1157
1272
  if (this.shuttingDown) {
1158
- send({ error: { code: "SERVICE_UNAVAILABLE", message: "daemon is shutting down" } });
1273
+ sendShuttingDown();
1159
1274
  return;
1160
1275
  }
1161
1276
  const last = this.events.last(s.id);
@@ -1177,8 +1292,7 @@ class Daemon {
1177
1292
  reason: "stopped",
1178
1293
  exitCode: 130,
1179
1294
  };
1180
- this.events.append(s.id, ev);
1181
- this.broadcast(s.id, ev);
1295
+ this.emit(s.id, ev);
1182
1296
  }
1183
1297
  // A send may have persisted a slot after the entry clear (it saw a
1184
1298
  // live turn before this stop took the lock): stop wins, clear again.
@@ -1187,14 +1301,14 @@ class Daemon {
1187
1301
  }
1188
1302
  catch (e) {
1189
1303
  if (this.shuttingDown) {
1190
- send({ error: { code: "SERVICE_UNAVAILABLE", message: "daemon is shutting down" } });
1304
+ sendShuttingDown();
1191
1305
  return;
1192
1306
  }
1193
1307
  try {
1194
1308
  this.sessions.setStatus(s.id, previousStatus);
1195
1309
  }
1196
1310
  catch { }
1197
- send({ error: { code: "STOP_FAILED", message: e instanceof Error ? e.message : String(e) } });
1311
+ send({ error: { code: "STOP_FAILED", message: errorMessage(e) } });
1198
1312
  }
1199
1313
  finally {
1200
1314
  if (!this.shuttingDown)
@@ -1206,7 +1320,7 @@ class Daemon {
1206
1320
  const p = params;
1207
1321
  const s = this.sessions.get(p.id);
1208
1322
  if (!s) {
1209
- send({ error: { code: "SESSION_NOT_FOUND", message: `Session ${p.id} not found` } });
1323
+ sendSessionNotFound(p.id);
1210
1324
  return;
1211
1325
  }
1212
1326
  if (p.follow) {
@@ -1230,7 +1344,7 @@ class Daemon {
1230
1344
  const p = params;
1231
1345
  const s = this.sessions.get(p.id);
1232
1346
  if (!s) {
1233
- send({ error: { code: "SESSION_NOT_FOUND", message: `Session ${p.id} not found` } });
1347
+ sendSessionNotFound(p.id);
1234
1348
  return;
1235
1349
  }
1236
1350
  // A late subscriber must not wait forever for a broadcast that already
@@ -1250,23 +1364,14 @@ class Daemon {
1250
1364
  catch { }
1251
1365
  return;
1252
1366
  }
1253
- // Send buffered events first?
1254
- // Then keep streaming
1255
1367
  this.addSubscriber(s.id, socket);
1256
- // Send ack? keep open
1257
- // Optionally send done when session terminal? We'll handle via broadcast.
1258
- // Don't send immediate response; just keep socket open for events
1259
- // But we should send an initial ack to confirm subscription?
1260
- // Use a separate message to indicate subscription started
1261
- // Let's not send anything; client will just wait for events
1262
- // Keep socket open
1263
1368
  return;
1264
1369
  }
1265
1370
  case "session.diff": {
1266
1371
  const p = params;
1267
1372
  const s = this.sessions.get(p.id);
1268
1373
  if (!s) {
1269
- send({ error: { code: "SESSION_NOT_FOUND", message: `Session ${p.id} not found` } });
1374
+ sendSessionNotFound(p.id);
1270
1375
  return;
1271
1376
  }
1272
1377
  const diff = await getDiff({ cwd: s.cwd, worktree: s.worktree, baseCommit: s.baseCommit, repository: s.repository });
@@ -1275,56 +1380,17 @@ class Daemon {
1275
1380
  }
1276
1381
  case "claims.add": {
1277
1382
  const p = params;
1278
- try {
1279
- const sessionId = this.sessions.get(p.sessionId)?.id ?? p.sessionId;
1280
- const claim = this.claims.add(sessionId, p.pathGlob, p.reason);
1281
- send({ result: { claim } });
1282
- }
1283
- catch (error) {
1284
- if (error instanceof RunAgentError) {
1285
- send({ error: { code: error.code, message: error.message, details: error.details } });
1286
- return;
1287
- }
1288
- throw error;
1289
- }
1383
+ this.handleClaimRequest(p.sessionId, (sessionId) => this.claims.add(sessionId, p.pathGlob, p.reason), (claim) => ({ result: { claim } }), send);
1290
1384
  break;
1291
1385
  }
1292
1386
  case "claims.query": {
1293
1387
  const p = params;
1294
- try {
1295
- const sessionId = this.sessions.get(p.sessionId)?.id ?? p.sessionId;
1296
- const claims = this.claims.query(sessionId, p.path);
1297
- send({ result: { claims } });
1298
- }
1299
- catch (error) {
1300
- if (error instanceof RunAgentError) {
1301
- send({ error: { code: error.code, message: error.message, details: error.details } });
1302
- return;
1303
- }
1304
- throw error;
1305
- }
1388
+ this.handleClaimRequest(p.sessionId, (sessionId) => this.claims.query(sessionId, p.path), (claims) => ({ result: { claims } }), send);
1306
1389
  break;
1307
1390
  }
1308
1391
  case "claims.release": {
1309
1392
  const p = params;
1310
- try {
1311
- const sessionId = this.sessions.get(p.sessionId)?.id ?? p.sessionId;
1312
- const claim = this.claims.release(sessionId, p.claimId);
1313
- send({ result: { claim } });
1314
- }
1315
- catch (error) {
1316
- if (error instanceof RunAgentError) {
1317
- send({ error: { code: error.code, message: error.message, details: error.details } });
1318
- return;
1319
- }
1320
- throw error;
1321
- }
1322
- break;
1323
- }
1324
- case "daemon.status": {
1325
- const paths = getPaths();
1326
- let uptime = Date.now() - this.startTime;
1327
- send({ result: { running: true, pid: process.pid, uptime, db: paths.db } });
1393
+ this.handleClaimRequest(p.sessionId, (sessionId) => this.claims.release(sessionId, p.claimId), (claim) => ({ result: { claim } }), send);
1328
1394
  break;
1329
1395
  }
1330
1396
  case "doctor": {
@@ -1461,7 +1527,7 @@ class Daemon {
1461
1527
  send({ result });
1462
1528
  }
1463
1529
  catch (error) {
1464
- send({ error: { code: "USAGE_QUERY_ERROR", message: error instanceof Error ? error.message : String(error) } });
1530
+ send({ error: { code: "USAGE_QUERY_ERROR", message: errorMessage(error) } });
1465
1531
  }
1466
1532
  break;
1467
1533
  }
@@ -1475,7 +1541,7 @@ class Daemon {
1475
1541
  send({ error: { code: error.code, message: error.message, ...(error.details ? { details: error.details } : {}) } });
1476
1542
  }
1477
1543
  else {
1478
- send({ error: { code: "WEB_START_FAILED", message: error instanceof Error ? error.message : String(error) } });
1544
+ send({ error: { code: "WEB_START_FAILED", message: errorMessage(error) } });
1479
1545
  }
1480
1546
  }
1481
1547
  break;
@@ -1531,6 +1597,10 @@ class Daemon {
1531
1597
  }
1532
1598
  }
1533
1599
  }
1600
+ emit(sessionId, event) {
1601
+ this.events.append(sessionId, event);
1602
+ this.broadcast(sessionId, event);
1603
+ }
1534
1604
  webHost() {
1535
1605
  this.web ??= new WebSupervisor({ log: appendDaemonLog, spawnChild: this.spawnWebChild });
1536
1606
  return this.web;
@@ -1549,7 +1619,7 @@ class Daemon {
1549
1619
  appendDaemonLog(invalidWebHostMessage(invalidHost));
1550
1620
  this.webHost()
1551
1621
  .ensure({ preferredPort: port, preferredHost: host })
1552
- .catch((error) => appendDaemonLog(`web autostart failed: ${error instanceof Error ? error.message : String(error)}`));
1622
+ .catch((error) => appendDaemonLog(`web autostart failed: ${errorMessage(error)}`));
1553
1623
  }
1554
1624
  async startDriverForSession(sessionId, prompt, model) {
1555
1625
  if (this.shuttingDown)
@@ -1564,8 +1634,7 @@ class Daemon {
1564
1634
  return;
1565
1635
  this.sessions.update(sessionId, { status: "working" });
1566
1636
  const turnEvent = { type: "turn.started", sessionId, timestamp: new Date().toISOString(), prompt };
1567
- this.events.append(sessionId, turnEvent);
1568
- this.broadcast(sessionId, turnEvent);
1637
+ this.emit(sessionId, turnEvent);
1569
1638
  const drvSession = await driver.start({
1570
1639
  sessionId,
1571
1640
  prompt,
@@ -1632,10 +1701,10 @@ class Daemon {
1632
1701
  catch { }
1633
1702
  }
1634
1703
  /**
1635
- + * Starts a resumed turn; caller MUST hold sessionLocks for the session.
1636
- + * Shared by session.send (immediate) and tryDispatch (queued) so both
1637
- + * paths emit the same frames in the same order.
1638
- + */
1704
+ * Starts a resumed turn; caller MUST hold sessionLocks for the session.
1705
+ * Shared by session.send (immediate) and tryDispatch (queued) so both
1706
+ * paths emit the same frames in the same order.
1707
+ */
1639
1708
  async runResumeTurn(s, message) {
1640
1709
  const driver = this.registry.get(s.agent);
1641
1710
  if (this.shuttingDown) {
@@ -1650,8 +1719,7 @@ class Daemon {
1650
1719
  timestamp: new Date().toISOString(),
1651
1720
  prompt: message,
1652
1721
  };
1653
- this.events.append(s.id, turnEvent);
1654
- this.broadcast(s.id, turnEvent);
1722
+ this.emit(s.id, turnEvent);
1655
1723
  const drvSession = {
1656
1724
  id: s.id,
1657
1725
  runId: s.runId ?? undefined,
@@ -1756,7 +1824,7 @@ class Daemon {
1756
1824
  return true;
1757
1825
  }
1758
1826
  catch (e) {
1759
- const errText = e instanceof Error ? e.message : String(e);
1827
+ const errText = errorMessage(e);
1760
1828
  const unavailable = e?.code === "SERVICE_UNAVAILABLE" || this.shuttingDown;
1761
1829
  if (!unavailable) {
1762
1830
  const failure = classifyFailure(errText);
@@ -1859,7 +1927,7 @@ class Daemon {
1859
1927
  return;
1860
1928
  // A driver exception mid-stream is a harness/pipe failure, not task
1861
1929
  // output — classify so agents can retry instead of blaming the work.
1862
- const msg = e instanceof Error ? e.message : String(e);
1930
+ const msg = errorMessage(e);
1863
1931
  const failure = classifyFailure(msg);
1864
1932
  const errEv = {
1865
1933
  type: "session.failed",
@@ -1869,8 +1937,7 @@ class Daemon {
1869
1937
  failure,
1870
1938
  raw: e,
1871
1939
  };
1872
- this.events.append(sessionId, errEv);
1873
- this.broadcast(sessionId, errEv);
1940
+ this.emit(sessionId, errEv);
1874
1941
  this.sessions.setStatus(sessionId, "failed", { lastEvent: msg.slice(0, 200), failure });
1875
1942
  return;
1876
1943
  }
@@ -1902,8 +1969,7 @@ class Daemon {
1902
1969
  };
1903
1970
  const error = sess.lastEvent || "event stream ended without a terminal event";
1904
1971
  const errEv = { type: "session.failed", sessionId, timestamp: new Date().toISOString(), error, failure };
1905
- this.events.append(sessionId, errEv);
1906
- this.broadcast(sessionId, errEv);
1972
+ this.emit(sessionId, errEv);
1907
1973
  this.sessions.setStatus(sessionId, "failed", { lastEvent: error.slice(0, 200), failure });
1908
1974
  }
1909
1975
  }
@@ -2068,25 +2134,23 @@ class Daemon {
2068
2134
  handle.exec("PRAGMA wal_checkpoint(TRUNCATE)");
2069
2135
  }
2070
2136
  catch { }
2071
- try {
2072
- const paths = getPaths();
2137
+ if (this.boundPaths) {
2073
2138
  try {
2074
- fs.unlinkSync(paths.daemonSock);
2139
+ fs.unlinkSync(this.boundPaths.sock);
2075
2140
  }
2076
2141
  catch { }
2077
2142
  try {
2078
- fs.unlinkSync(paths.daemonPid);
2079
- }
2080
- catch { }
2081
- try {
2082
- this.server?.close();
2083
- }
2084
- catch { }
2085
- try {
2086
- this.db.close();
2143
+ fs.unlinkSync(this.boundPaths.pid);
2087
2144
  }
2088
2145
  catch { }
2089
2146
  }
2147
+ try {
2148
+ this.server?.close();
2149
+ }
2150
+ catch { }
2151
+ try {
2152
+ this.db.close();
2153
+ }
2090
2154
  catch { }
2091
2155
  }
2092
2156
  safeListActive() {
@@ -2152,8 +2216,7 @@ class Daemon {
2152
2216
  failure,
2153
2217
  raw: { reason },
2154
2218
  };
2155
- this.events.append(s.id, event);
2156
- this.broadcast(s.id, event);
2219
+ this.emit(s.id, event);
2157
2220
  this.sessions.setStatus(s.id, "interrupted", { lastEvent: detail.slice(0, 200), failure });
2158
2221
  }
2159
2222
  catch { }
@@ -2164,7 +2227,7 @@ class Daemon {
2164
2227
  let instanceLock = null;
2165
2228
  // Entry
2166
2229
  if (process.argv.includes("--daemon")) {
2167
- // Take the lock before opening run-agent.db: a losing daemon must not
2230
+ // Take the lock before opening codedeck.db: a losing daemon must not
2168
2231
  // migrate, recover or bind the socket. Exit 0 so the spawning CLI keeps
2169
2232
  // polling and connects to the daemon that already runs.
2170
2233
  instanceLock = acquireInstanceLock(getPaths().daemonLock);