codedeck 0.1.2 → 0.1.4

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 (48) hide show
  1. package/README.md +61 -3
  2. package/dist/cli/commands/logs.js +5 -1
  3. package/dist/cli/commands/logs.js.map +1 -1
  4. package/dist/cli/commands/run.js +91 -28
  5. package/dist/cli/commands/run.js.map +1 -1
  6. package/dist/cli/commands/show.js +4 -0
  7. package/dist/cli/commands/show.js.map +1 -1
  8. package/dist/cli/commands/wait.js +57 -0
  9. package/dist/cli/commands/wait.js.map +1 -0
  10. package/dist/cli/index.js +10 -1
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/cli/wait.js +44 -0
  13. package/dist/cli/wait.js.map +1 -0
  14. package/dist/core/driver.js +9 -1
  15. package/dist/core/driver.js.map +1 -1
  16. package/dist/core/errors.js +56 -0
  17. package/dist/core/errors.js.map +1 -1
  18. package/dist/core/events.js.map +1 -1
  19. package/dist/core/session.js.map +1 -1
  20. package/dist/daemon/daemon.js +316 -71
  21. package/dist/daemon/daemon.js.map +1 -1
  22. package/dist/drivers/claude/driver.js +34 -205
  23. package/dist/drivers/claude/driver.js.map +1 -1
  24. package/dist/drivers/codex/driver.js +65 -205
  25. package/dist/drivers/codex/driver.js.map +1 -1
  26. package/dist/drivers/helpers.js +47 -37
  27. package/dist/drivers/helpers.js.map +1 -1
  28. package/dist/drivers/omp/driver.js +38 -206
  29. package/dist/drivers/omp/driver.js.map +1 -1
  30. package/dist/drivers/opencode/driver.js +30 -182
  31. package/dist/drivers/opencode/driver.js.map +1 -1
  32. package/dist/drivers/session-driver.js +122 -0
  33. package/dist/drivers/session-driver.js.map +1 -0
  34. package/dist/drivers/session-runtime.js +333 -0
  35. package/dist/drivers/session-runtime.js.map +1 -0
  36. package/dist/drivers/tailer.js +149 -0
  37. package/dist/drivers/tailer.js.map +1 -0
  38. package/dist/drivers/terminal.js +53 -0
  39. package/dist/drivers/terminal.js.map +1 -0
  40. package/dist/store/database.js +28 -1
  41. package/dist/store/database.js.map +1 -1
  42. package/dist/store/events.js +8 -3
  43. package/dist/store/events.js.map +1 -1
  44. package/dist/store/sessions.js +27 -4
  45. package/dist/store/sessions.js.map +1 -1
  46. package/dist/utils/process.js +104 -4
  47. package/dist/utils/process.js.map +1 -1
  48. package/package.json +10 -2
@@ -58,4 +58,60 @@ export class ProtocolError extends RunAgentError {
58
58
  super(message, "PROTOCOL_ERROR", { cause });
59
59
  }
60
60
  }
61
+ // Order matters: the most specific signatures first. "EPIPE" alone is LAST —
62
+ // it is a substring task output can legitimately contain (e.g. a test grepping
63
+ // for EPIPE), while process-level "unhandled rejection" text virtually never
64
+ // comes from the task's work.
65
+ const HARNESS_CRASH_PATTERNS = [
66
+ [/unhandled\s+(rejection|promise)/i, "unhandled_rejection"],
67
+ [/unhandled\s+exception/i, "unhandled_exception"],
68
+ [/segmentation fault/i, "segfault"],
69
+ [/heap\s+out\s+of\s+memory|fatal error:/i, "v8_fatal"],
70
+ [/epipe/i, "EPIPE"],
71
+ [/err_stream_/i, "stream_error"],
72
+ ];
73
+ const FATAL_SIGNALS = { SIGSEGV: true, SIGBUS: true, SIGABRT: true, SIGILL: true, SIGKILL: true };
74
+ // Exit code 128+N is the shell convention for "died by signal N".
75
+ const SIGNAL_EXIT_CODES = {
76
+ 134: "SIGABRT",
77
+ 137: "SIGKILL",
78
+ 139: "SIGSEGV",
79
+ };
80
+ export function classifyFailure(text, exitCode, signal) {
81
+ const clean = (text || "").trim();
82
+ const detail = clean.slice(0, 300) || undefined;
83
+ for (const [pattern, reason] of HARNESS_CRASH_PATTERNS) {
84
+ if (pattern.test(clean)) {
85
+ return { code: "HARNESS_CRASH", blame: "harness", retryable: true, reason, detail };
86
+ }
87
+ }
88
+ if (signal && FATAL_SIGNALS[signal]) {
89
+ return { code: "HARNESS_CRASH", blame: "harness", retryable: true, signal };
90
+ }
91
+ if (exitCode != null && SIGNAL_EXIT_CODES[exitCode]) {
92
+ return { code: "HARNESS_CRASH", blame: "harness", retryable: true, signal: SIGNAL_EXIT_CODES[exitCode] };
93
+ }
94
+ if (exitCode != null && exitCode !== 0) {
95
+ // Non-zero without a harness-crash signature: the honest default is
96
+ // "the work failed" — a blind retry of the same prompt rarely differs.
97
+ return { code: "TASK_ERROR", blame: "task", retryable: false, detail: detail || `exit code ${exitCode}` };
98
+ }
99
+ // No signature at all. Callers reaching this lost the stream mid-flight
100
+ // (driver exception, socket death), which is a harness-side failure.
101
+ return { code: "UNKNOWN", blame: "harness", retryable: true, detail };
102
+ }
103
+ // CLI exit codes for `codedeck run` (non-detach), documented in README:
104
+ // 0 completed/stopped · 1 task failed · 2 harness crashed (retryable) · 3 infra
105
+ export function exitCodeForOutcome(outcome) {
106
+ const status = outcome.status || "";
107
+ if (status === "failed" || status === "orphaned") {
108
+ const blame = outcome.failure?.blame;
109
+ if (blame === "harness")
110
+ return 2;
111
+ if (blame === "infra")
112
+ return 3;
113
+ return 1;
114
+ }
115
+ return 0;
116
+ }
61
117
  //# sourceMappingURL=errors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,aAAc,SAAQ,KAAK;IAGpB;IACA;IAHlB,YACE,OAAe,EACC,IAAY,EACZ,OAAiB;QAEjC,KAAK,CAAC,OAAO,CAAC,CAAC;QAHC,SAAI,GAAJ,IAAI,CAAQ;QACZ,YAAO,GAAP,OAAO,CAAU;QAGjC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;IACpC,CAAC;CACF;AAED,MAAM,OAAO,sBAAuB,SAAQ,aAAa;IACvD,YAAY,KAAa;QACvB,KAAK,CAAC,UAAU,KAAK,oBAAoB,EAAE,qBAAqB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/E,CAAC;CACF;AAED,MAAM,OAAO,gCAAiC,SAAQ,aAAa;IACjE,YAAY,KAAa;QACvB,KAAK,CAAC,UAAU,KAAK,2BAA2B,EAAE,qBAAqB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACtF,CAAC;CACF;AAED,MAAM,OAAO,qBAAsB,SAAQ,aAAa;IACtD,YAAY,KAAa,EAAE,KAAe;QACxC,KAAK,CAAC,0BAA0B,KAAK,GAAG,EAAE,oBAAoB,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IACpF,CAAC;CACF;AAED,MAAM,OAAO,oBAAqB,SAAQ,aAAa;IACrD,YAAY,EAAU;QACpB,KAAK,CAAC,YAAY,EAAE,aAAa,EAAE,mBAAmB,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IAClE,CAAC;CACF;AAED,MAAM,OAAO,sBAAuB,SAAQ,aAAa;IACvD,YAAY,EAAU;QACpB,KAAK,CAAC,YAAY,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IACzE,CAAC;CACF;AAED,MAAM,OAAO,2BAA4B,SAAQ,aAAa;IAC5D,YAAY,KAAa,EAAE,UAAkB;QAC3C,KAAK,CAAC,UAAU,KAAK,uBAAuB,UAAU,GAAG,EAAE,0BAA0B,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;IAChH,CAAC;CACF;AAED,MAAM,OAAO,uBAAwB,SAAQ,aAAa;IACxD,YAAY,GAAW;QACrB,KAAK,CAAC,+BAA+B,GAAG,GAAG,EAAE,sBAAsB,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;IAChF,CAAC;CACF;AAED,MAAM,OAAO,2BAA4B,SAAQ,aAAa;IAC5D,YAAY,KAAe;QACzB,KAAK,CAAC,+BAA+B,EAAE,0BAA0B,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IAChF,CAAC;CACF;AAED,MAAM,OAAO,sBAAuB,SAAQ,aAAa;IACvD,YAAY,KAAe;QACzB,KAAK,CAAC,gCAAgC,EAAE,oBAAoB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3E,CAAC;CACF;AAED,MAAM,OAAO,aAAc,SAAQ,aAAa;IAC9C,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,gBAAgB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IAC9C,CAAC;CACF"}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/core/events.ts"],"names":[],"mappings":"AA8IA,MAAM,UAAU,WAAW,CACzB,IAAmD;IAEnD,OAAO;QACL,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,GAAG,IAAI;KACH,CAAC;AACT,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAiB;IAC/C,OAAO,KAAK,CAAC,IAAI,KAAK,mBAAmB,IAAI,KAAK,CAAC,IAAI,KAAK,gBAAgB,CAAC;AAC/E,CAAC"}
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/core/events.ts"],"names":[],"mappings":"AAsJA,MAAM,UAAU,WAAW,CACzB,IAAmD;IAEnD,OAAO;QACL,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,GAAG,IAAI;KACH,CAAC;AACT,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAiB;IAC/C,OAAO,KAAK,CAAC,IAAI,KAAK,mBAAmB,IAAI,KAAK,CAAC,IAAI,KAAK,gBAAgB,CAAC;AAC/E,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/core/session.ts"],"names":[],"mappings":"AAkDA,MAAM,UAAU,gBAAgB,CAAC,MAAqB;IACpD,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,UAAU,CAAC;AACxG,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAqB;IAClD,OAAO,MAAM,KAAK,UAAU,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,aAAa,IAAI,MAAM,KAAK,MAAM,CAAC;AACxG,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,2EAA2E;IAC3E,kDAAkD;IAClD,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;IAChC,cAAc;IACd,uDAAuD;IACvD,IAAI,CAAC;QACH,aAAa;QACb,UAAU,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,KAAK,CAAC,CAAC;QAC5C,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC;IACpG,CAAC;IAAC,MAAM,CAAC;QACP,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;YAAE,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC5F,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,SAAiB;IAChE,MAAM,KAAK,GAAG,IAAI;SACf,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC;IAC1B,OAAO,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC;AACpC,CAAC"}
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/core/session.ts"],"names":[],"mappings":"AAgEA,MAAM,UAAU,gBAAgB,CAAC,MAAqB;IACpD,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,UAAU,CAAC;AACxG,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAqB;IAClD,OAAO,MAAM,KAAK,UAAU,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,aAAa,IAAI,MAAM,KAAK,MAAM,CAAC;AACxG,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,2EAA2E;IAC3E,kDAAkD;IAClD,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;IAChC,cAAc;IACd,uDAAuD;IACvD,IAAI,CAAC;QACH,aAAa;QACb,UAAU,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,KAAK,CAAC,CAAC;QAC5C,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC;IACpG,CAAC;IAAC,MAAM,CAAC;QACP,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;YAAE,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC5F,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,SAAiB;IAChE,MAAM,KAAK,GAAG,IAAI;SACf,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC;IAC1B,OAAO,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC;AACpC,CAAC"}
@@ -7,21 +7,24 @@ import { EventStore } from "../store/events.js";
7
7
  import { getPaths, ensureDirs } from "../config/paths.js";
8
8
  import { createIpcServer } from "./ipc.js";
9
9
  import { getRegistry } from "../drivers/registry.js";
10
+ import { isTerminalStatus } from "../core/session.js";
10
11
  import { generateSessionId } from "../core/session.js";
11
12
  import { getGitInfo } from "../git/repository.js";
12
13
  import { createWorktree } from "../git/worktree.js";
13
14
  import { getDiff } from "../git/diff.js";
14
- import { ProcessManager } from "./process-manager.js";
15
+ import { processAlive, processStartTime } from "../utils/process.js";
16
+ import { readSessionProcessMetadata } from "../drivers/session-runtime.js";
15
17
  import { loadConfig } from "../config/config.js";
18
+ import { classifyFailure } from "../core/errors.js";
16
19
  class Daemon {
17
20
  db;
18
21
  sessions;
19
22
  events;
20
23
  registry = getRegistry();
21
- pm = new ProcessManager();
22
24
  server;
23
25
  subscribers = new Map(); // sessionId -> sockets
24
26
  startTime = Date.now();
27
+ sessionLocks = new Set();
25
28
  constructor() {
26
29
  ensureDirs();
27
30
  this.db = new Database();
@@ -66,20 +69,94 @@ class Daemon {
66
69
  }
67
70
  catch { }
68
71
  }
72
+ // Sessions whose detached process outlived the daemon are RE-ATTACHED, not
73
+ // orphaned: the process is independent (own process group, output in log
74
+ // files), so a daemon restart is invisible to it. A live process keeps
75
+ // streaming; a dead one is drained and classified from its log tail.
69
76
  async recover() {
70
- // Mark active sessions as orphaned if process not alive
77
+ const paths = getPaths();
71
78
  const actives = this.sessions.listActive();
72
79
  for (const s of actives) {
73
- if (s.pid) {
80
+ const driver = this.registry.get(s.agent);
81
+ const metadata = readSessionProcessMetadata(s.id);
82
+ const pid = metadata?.pid ?? s.pid;
83
+ const recordedStart = metadata?.pidStartTime ?? s.pidStartTime;
84
+ const processPresent = pid != null && processAlive(pid);
85
+ const currentStart = pid != null ? processStartTime(pid) : undefined;
86
+ const pidReused = processPresent &&
87
+ recordedStart !== undefined &&
88
+ currentStart !== undefined &&
89
+ currentStart !== recordedStart;
90
+ // Never attach to or signal a PID whose identity changed while the
91
+ // daemon was away. The original harness is gone; the replacement is
92
+ // somebody else's process.
93
+ if (pidReused) {
94
+ const failure = {
95
+ code: "HARNESS_CRASH",
96
+ blame: "harness",
97
+ retryable: true,
98
+ reason: "pid_reused",
99
+ detail: `PID ${pid} no longer identifies the launched harness`,
100
+ };
101
+ const detail = failure.detail ?? `PID ${pid} no longer identifies the launched harness`;
102
+ const event = {
103
+ type: "session.failed",
104
+ sessionId: s.id,
105
+ timestamp: new Date().toISOString(),
106
+ error: detail,
107
+ failure,
108
+ raw: { pid, expectedStartTime: recordedStart, currentStart },
109
+ };
110
+ this.events.append(s.id, event);
111
+ this.sessions.setStatus(s.id, "failed", { lastEvent: detail, failure });
112
+ continue;
113
+ }
114
+ const alive = processPresent;
115
+ const stdoutPath = path.join(paths.logsDir, `${s.id}.ndjson`);
116
+ const stderrPath = path.join(paths.logsDir, `${s.id}.stderr.log`);
117
+ const hasDetachedLogs = fs.existsSync(stdoutPath) || fs.existsSync(stderrPath);
118
+ // A live PID without a recorded identity is unsafe to attach: it may be
119
+ // a recycled process. A dead PID is safe to drain from its own log.
120
+ const identityVerified = !processPresent || (recordedStart !== undefined && currentStart === recordedStart);
121
+ if (typeof driver.attach === "function" && pid != null && hasDetachedLogs && identityVerified) {
74
122
  try {
75
- process.kill(s.pid, 0);
76
- // still alive externally but not managed by us; treat as orphaned
77
- this.sessions.setStatus(s.id, "orphaned");
123
+ await driver.attach({
124
+ sessionId: s.id,
125
+ pid,
126
+ pidStartTime: recordedStart,
127
+ nativeSessionId: s.nativeSessionId,
128
+ logOffset: s.logOffset,
129
+ stderrOffset: s.stderrOffset,
130
+ });
78
131
  }
79
- catch {
80
- // not alive
81
- this.sessions.setStatus(s.id, "failed");
132
+ catch (e) {
133
+ // Corrupt log / fs failure: fall back to the legacy marking.
134
+ this.sessions.setStatus(s.id, alive ? "orphaned" : "failed", {
135
+ lastEvent: e instanceof Error ? e.message.slice(0, 200) : "reattach failed",
136
+ });
137
+ continue;
138
+ }
139
+ if (!s.pid || !s.pidStartTime) {
140
+ this.sessions.update(s.id, { pid, pidStartTime: recordedStart ?? currentStart });
141
+ }
142
+ const drvSession = {
143
+ id: s.id,
144
+ nativeSessionId: s.nativeSessionId,
145
+ pid,
146
+ pidStartTime: recordedStart,
147
+ cwd: s.worktree || s.cwd,
148
+ model: s.model,
149
+ };
150
+ if (alive) {
151
+ this.sessions.setStatus(s.id, "working", { lastEvent: "reattached after daemon restart" });
82
152
  }
153
+ void this.attachDriverEvents(s.id, driver, drvSession);
154
+ continue;
155
+ }
156
+ // Sessions created before file transport have no reattachable log, and
157
+ // live sessions without a PID identity are not safe to trust.
158
+ if (pid) {
159
+ this.sessions.setStatus(s.id, alive ? "orphaned" : "failed");
83
160
  }
84
161
  else {
85
162
  this.sessions.setStatus(s.id, "failed");
@@ -142,6 +219,8 @@ class Daemon {
142
219
  name: p.name,
143
220
  agent,
144
221
  model: p.model,
222
+ effort: p.effort,
223
+ fast: !!p.fast,
145
224
  status: "starting",
146
225
  repository,
147
226
  cwd,
@@ -158,11 +237,21 @@ class Daemon {
158
237
  send({ result: { session } });
159
238
  // Now start driver in background
160
239
  this.startDriverForSession(sessionId, prompt, p.model).catch((e) => {
240
+ const msg = e instanceof Error ? e.message : String(e);
241
+ const failure = classifyFailure(msg);
242
+ const failEv = {
243
+ type: "session.failed",
244
+ sessionId,
245
+ timestamp: new Date().toISOString(),
246
+ error: msg,
247
+ failure,
248
+ raw: e,
249
+ };
161
250
  try {
162
- this.sessions.setStatus(sessionId, "failed", { lastEvent: e instanceof Error ? e.message : String(e) });
251
+ this.sessions.setStatus(sessionId, "failed", { lastEvent: msg.slice(0, 200), failure });
163
252
  }
164
253
  catch { }
165
- this.broadcast(sessionId, { type: "session.failed", sessionId, timestamp: new Date().toISOString(), error: e instanceof Error ? e.message : String(e), raw: e });
254
+ this.broadcast(sessionId, failEv);
166
255
  });
167
256
  break;
168
257
  }
@@ -194,40 +283,78 @@ class Daemon {
194
283
  return;
195
284
  }
196
285
  const driver = this.registry.get(s.agent);
197
- // Check capabilities resume
286
+ // Do not start a second harness while the current one is still live:
287
+ // both runtimes would tail the same per-session file and duplicate
288
+ // every event from the follow-up turn.
289
+ if (this.sessionLocks.has(s.id)) {
290
+ send({ error: { code: "SESSION_BUSY", message: `Session ${s.id} has another lifecycle operation in progress` } });
291
+ return;
292
+ }
293
+ const handle = driver.getHandle?.(s.id);
294
+ const runtimeState = handle && typeof handle === "object"
295
+ ? handle
296
+ : undefined;
297
+ const runtimeDraining = handle !== undefined && (runtimeState?.done !== true || runtimeState?.drained !== true);
298
+ if (s.status === "starting" || runtimeDraining || (s.status === "working" && s.pid != null && processAlive(s.pid))) {
299
+ send({ error: { code: "SESSION_BUSY", message: `Session ${s.id} is still running` } });
300
+ return;
301
+ }
198
302
  if (!driver.capabilities().resume) {
199
303
  send({ error: { code: "CAPABILITY_NOT_SUPPORTED", message: `Agent ${s.agent} does not support resume` } });
200
304
  return;
201
305
  }
202
- // Update status to working
203
- this.sessions.setStatus(s.id, "working", { lastEvent: `send: ${p.message.slice(0, 80)}` });
204
- // Emit turn.started
205
- const turnEvent = { type: "turn.started", sessionId: s.id, timestamp: new Date().toISOString(), prompt: p.message };
206
- this.events.append(s.id, turnEvent);
207
- this.broadcast(s.id, turnEvent);
306
+ this.sessionLocks.add(s.id);
208
307
  try {
209
- // driver.send will spawn new process
210
- // Need to get current handle session object
211
- const nativeId = s.nativeSessionId;
212
- // Create a ephemeral DriverSession for send
213
- const drvSession = { id: s.id, nativeSessionId: nativeId, cwd: s.worktree || s.cwd, model: s.model, pid: s.pid };
308
+ this.sessions.setStatus(s.id, "working", { lastEvent: `send: ${p.message.slice(0, 80)}` });
309
+ const turnEvent = {
310
+ type: "turn.started",
311
+ sessionId: s.id,
312
+ timestamp: new Date().toISOString(),
313
+ prompt: p.message,
314
+ };
315
+ this.events.append(s.id, turnEvent);
316
+ this.broadcast(s.id, turnEvent);
317
+ const drvSession = {
318
+ id: s.id,
319
+ nativeSessionId: s.nativeSessionId,
320
+ cwd: s.worktree || s.cwd,
321
+ model: s.model,
322
+ pid: s.pid,
323
+ pidStartTime: s.pidStartTime,
324
+ };
214
325
  await driver.send(drvSession, p.message);
215
- // Update nativeSessionId if driver discovered new one
216
326
  const handle = driver.getHandle?.(s.id);
217
- const newNative = handle?.nativeSessionId || drvSession.nativeSessionId;
327
+ const handleNativeId = handle &&
328
+ typeof handle === "object" &&
329
+ "nativeSessionId" in handle &&
330
+ typeof handle.nativeSessionId === "string"
331
+ ? handle.nativeSessionId
332
+ : undefined;
333
+ const newNative = handleNativeId || drvSession.nativeSessionId;
218
334
  if (newNative && newNative !== s.nativeSessionId) {
219
335
  this.sessions.update(s.id, { nativeSessionId: newNative });
220
336
  }
221
- if (drvSession.pid)
222
- this.sessions.update(s.id, { pid: drvSession.pid });
337
+ if (drvSession.pid) {
338
+ this.sessions.update(s.id, {
339
+ pid: drvSession.pid,
340
+ pidStartTime: processStartTime(drvSession.pid),
341
+ });
342
+ }
223
343
  this.sessions.update(s.id, { status: "working" });
224
344
  // Attach event loop for new turn
225
345
  this.attachDriverEvents(s.id, driver, drvSession).catch(() => { });
226
346
  send({ result: { ok: true } });
227
347
  }
228
348
  catch (e) {
229
- this.sessions.setStatus(s.id, "failed");
230
- send({ error: { code: "SEND_FAILED", message: e instanceof Error ? e.message : String(e) } });
349
+ const message = e instanceof Error ? e.message : String(e);
350
+ try {
351
+ this.sessions.setStatus(s.id, "failed");
352
+ }
353
+ catch { }
354
+ send({ error: { code: "SEND_FAILED", message } });
355
+ }
356
+ finally {
357
+ this.sessionLocks.delete(s.id);
231
358
  }
232
359
  break;
233
360
  }
@@ -239,20 +366,74 @@ class Daemon {
239
366
  return;
240
367
  }
241
368
  const driver = this.registry.get(s.agent);
242
- try {
243
- await driver.stop({ id: s.id, nativeSessionId: s.nativeSessionId, cwd: s.worktree || s.cwd, pid: s.pid });
369
+ if (this.sessionLocks.has(s.id)) {
370
+ send({ error: { code: "SESSION_BUSY", message: `Session ${s.id} has another lifecycle operation in progress` } });
371
+ return;
244
372
  }
245
- catch { }
246
- // Also try process manager
373
+ const handle = driver.getHandle?.(s.id);
374
+ const hasRuntime = handle !== undefined;
375
+ const liveIdentity = s.pid != null &&
376
+ s.pidStartTime != null &&
377
+ processAlive(s.pid) &&
378
+ processStartTime(s.pid) === s.pidStartTime;
379
+ if (s.status === "stopped" || ((s.status === "completed" || s.status === "failed") && !hasRuntime && !liveIdentity)) {
380
+ send({ error: { code: "SESSION_NOT_RUNNING", message: `Session ${s.id} is ${s.status}` } });
381
+ return;
382
+ }
383
+ if (s.pid != null && !hasRuntime && !s.pidStartTime) {
384
+ send({ error: { code: "STOP_UNSAFE", message: `Cannot safely stop session ${s.id}: PID identity is unavailable` } });
385
+ return;
386
+ }
387
+ this.sessionLocks.add(s.id);
388
+ const previousStatus = s.status;
389
+ const hadTerminalStatus = s.status === "completed" || s.status === "failed";
390
+ const eventsBeforeStop = this.events.count(s.id);
247
391
  try {
248
- await this.pm.stop(s.id);
392
+ // Mark active sessions first so an attached event loop that ends
393
+ // during stop cannot synthesize a failure or overwrite the outcome.
394
+ if (!hadTerminalStatus)
395
+ this.sessions.setStatus(s.id, "stopped");
396
+ await driver.stop({
397
+ id: s.id,
398
+ nativeSessionId: s.nativeSessionId,
399
+ cwd: s.worktree || s.cwd,
400
+ pid: s.pid,
401
+ pidStartTime: s.pidStartTime,
402
+ });
403
+ const last = this.events.last(s.id);
404
+ const terminalArrivedDuringStop = this.events.count(s.id) > eventsBeforeStop &&
405
+ (last?.type === "session.completed" || last?.type === "session.failed");
406
+ if (terminalArrivedDuringStop) {
407
+ if (last.type === "session.completed") {
408
+ this.sessions.setStatus(s.id, "completed", { lastEvent: last.reason || "completed" });
409
+ }
410
+ else {
411
+ this.sessions.setStatus(s.id, "failed", { lastEvent: last.error.slice(0, 200), failure: last.failure });
412
+ }
413
+ }
414
+ else if (!hadTerminalStatus) {
415
+ const ev = {
416
+ type: "session.completed",
417
+ sessionId: s.id,
418
+ timestamp: new Date().toISOString(),
419
+ reason: "stopped",
420
+ exitCode: 130,
421
+ };
422
+ this.events.append(s.id, ev);
423
+ this.broadcast(s.id, ev);
424
+ }
425
+ send({ result: { ok: true } });
426
+ }
427
+ catch (e) {
428
+ try {
429
+ this.sessions.setStatus(s.id, previousStatus);
430
+ }
431
+ catch { }
432
+ send({ error: { code: "STOP_FAILED", message: e instanceof Error ? e.message : String(e) } });
433
+ }
434
+ finally {
435
+ this.sessionLocks.delete(s.id);
249
436
  }
250
- catch { }
251
- this.sessions.setStatus(s.id, "stopped");
252
- const ev = { type: "session.completed", sessionId: s.id, timestamp: new Date().toISOString(), reason: "stopped", exitCode: 130 };
253
- this.events.append(s.id, ev);
254
- this.broadcast(s.id, ev);
255
- send({ result: { ok: true } });
256
437
  break;
257
438
  }
258
439
  case "session.logs": {
@@ -286,6 +467,23 @@ class Daemon {
286
467
  send({ error: { code: "SESSION_NOT_FOUND", message: `Session ${p.id} not found` } });
287
468
  return;
288
469
  }
470
+ // A late subscriber must not wait forever for a broadcast that already
471
+ // happened. Replay the terminal event when available, then close the
472
+ // stream just like a live terminal broadcast.
473
+ if (isTerminalStatus(s.status)) {
474
+ const last = this.events.last(s.id);
475
+ if (last?.type === "session.completed" || last?.type === "session.failed") {
476
+ try {
477
+ socket.write(JSON.stringify({ type: "event", event: last, id: s.id }) + "\n");
478
+ }
479
+ catch { }
480
+ }
481
+ try {
482
+ socket.write(JSON.stringify({ type: "done", id: s.id }) + "\n");
483
+ }
484
+ catch { }
485
+ return;
486
+ }
289
487
  // Send buffered events first?
290
488
  // Then keep streaming
291
489
  this.addSubscriber(s.id, socket);
@@ -404,10 +602,14 @@ class Daemon {
404
602
  prompt,
405
603
  cwd: session.worktree || session.cwd,
406
604
  model: model || session.model,
605
+ // Read back from the session rather than the request so a follow-up turn
606
+ // from send() runs with the same effort/tier the session was started with.
607
+ effort: session.effort,
608
+ fast: session.fast,
407
609
  });
408
610
  // Update pid and native id when available
409
611
  if (drvSession.pid)
410
- this.sessions.update(sessionId, { pid: drvSession.pid });
612
+ this.sessions.update(sessionId, { pid: drvSession.pid, pidStartTime: processStartTime(drvSession.pid) });
411
613
  // Poll native id shortly
412
614
  for (let i = 0; i < 10; i++) {
413
615
  await new Promise((r) => setTimeout(r, 200));
@@ -422,30 +624,61 @@ class Daemon {
422
624
  break;
423
625
  }
424
626
  }
425
- // Store proc for manager if handle has proc
426
- const handle = drvSession.handle;
427
- if (handle?.proc) {
428
- this.pm.register(sessionId, handle.proc);
429
- }
430
627
  // Attach events
431
628
  await this.attachDriverEvents(sessionId, driver, drvSession);
432
629
  }
433
630
  async attachDriverEvents(sessionId, driver, drvSession) {
434
631
  try {
435
632
  for await (const ev of driver.events(drvSession)) {
436
- // Persist
437
- this.events.append(sessionId, ev, ev.raw);
438
- // Update session status based on event
439
- this.updateSessionFromEvent(sessionId, ev);
440
- // Broadcast
441
- this.broadcast(sessionId, ev);
633
+ // A stop operation owns the terminal outcome. A terminal frame already
634
+ // buffered by the harness must not race it into the event log.
635
+ if (this.sessionLocks.has(sessionId) && (ev.type === "session.completed" || ev.type === "session.failed")) {
636
+ continue;
637
+ }
638
+ const db = this.db.getHandle();
639
+ db.exec("BEGIN");
640
+ let inserted = 0;
641
+ try {
642
+ inserted = this.events.append(sessionId, ev, ev.raw);
643
+ const offsets = driver.getOffsets?.(sessionId);
644
+ if (offsets) {
645
+ this.sessions.update(sessionId, { logOffset: offsets.log, stderrOffset: offsets.stderr });
646
+ }
647
+ if (inserted !== 0) {
648
+ // Update session status based on event.
649
+ this.updateSessionFromEvent(sessionId, ev);
650
+ }
651
+ db.exec("COMMIT");
652
+ }
653
+ catch (error) {
654
+ try {
655
+ db.exec("ROLLBACK");
656
+ }
657
+ catch { }
658
+ throw error;
659
+ }
660
+ // Broadcast only after the durable event+cursor commit, and never
661
+ // broadcast a replay that was already in the event store.
662
+ if (inserted !== 0)
663
+ this.broadcast(sessionId, ev);
442
664
  }
443
665
  }
444
666
  catch (e) {
445
- const errEv = { type: "session.failed", sessionId, timestamp: new Date().toISOString(), error: e instanceof Error ? e.message : String(e), raw: e };
667
+ // A driver exception mid-stream is a harness/pipe failure, not task
668
+ // output — classify so agents can retry instead of blaming the work.
669
+ const msg = e instanceof Error ? e.message : String(e);
670
+ const failure = classifyFailure(msg);
671
+ const errEv = {
672
+ type: "session.failed",
673
+ sessionId,
674
+ timestamp: new Date().toISOString(),
675
+ error: msg,
676
+ failure,
677
+ raw: e,
678
+ };
446
679
  this.events.append(sessionId, errEv);
447
680
  this.broadcast(sessionId, errEv);
448
- this.sessions.setStatus(sessionId, "failed", { lastEvent: errEv.error });
681
+ this.sessions.setStatus(sessionId, "failed", { lastEvent: msg.slice(0, 200), failure });
449
682
  return;
450
683
  }
451
684
  // When events done, ensure terminal status if not already
@@ -457,43 +690,55 @@ class Daemon {
457
690
  this.sessions.setStatus(sessionId, "completed", { lastEvent: last.reason || "completed" });
458
691
  }
459
692
  else if (last?.type === "session.failed") {
460
- this.sessions.setStatus(sessionId, "failed", { lastEvent: last.error });
693
+ this.sessions.setStatus(sessionId, "failed", { lastEvent: last.error?.slice(0, 200), failure: last.failure });
461
694
  }
462
695
  else {
463
- // If no terminal, assume completed
464
- this.sessions.setStatus(sessionId, "completed");
696
+ // Stream ended with NO terminal event: the harness died without
697
+ // reporting (the EPIPE class of crash). A false "completed" makes a
698
+ // polling agent proceed on missing work; "failed" is the honest
699
+ // default and matches what the dead process implies.
700
+ const failure = {
701
+ code: "HARNESS_CRASH",
702
+ blame: "harness",
703
+ retryable: true,
704
+ detail: "event stream ended without a terminal event",
705
+ };
706
+ const error = sess.lastEvent || "event stream ended without a terminal event";
707
+ const errEv = { type: "session.failed", sessionId, timestamp: new Date().toISOString(), error, failure };
708
+ this.events.append(sessionId, errEv);
709
+ this.broadcast(sessionId, errEv);
710
+ this.sessions.setStatus(sessionId, "failed", { lastEvent: error.slice(0, 200), failure });
465
711
  }
466
712
  }
467
- // Unregister process
468
- this.pm.unregister(sessionId);
469
713
  }
470
714
  updateSessionFromEvent(sessionId, ev) {
471
715
  try {
472
- if (ev.type === "session.completed") {
716
+ const current = this.sessions.get(sessionId);
717
+ if (current?.status === "stopped" && (ev.type === "session.completed" || ev.type === "session.failed")) {
718
+ return;
719
+ }
720
+ if (ev.type === "session.started") {
721
+ if (ev.nativeSessionId)
722
+ this.sessions.update(sessionId, { nativeSessionId: ev.nativeSessionId });
723
+ }
724
+ else if (ev.type === "session.completed") {
473
725
  this.sessions.setStatus(sessionId, "completed", { lastEvent: ev.reason || "completed" });
474
726
  }
475
727
  else if (ev.type === "session.failed") {
476
- this.sessions.setStatus(sessionId, "failed", { lastEvent: ev.error?.slice(0, 200) });
728
+ this.sessions.setStatus(sessionId, "failed", { lastEvent: ev.error.slice(0, 200), failure: ev.failure });
477
729
  }
478
730
  else if (ev.type === "tool.started") {
479
731
  this.sessions.update(sessionId, { lastEvent: `tool: ${ev.tool.name}` });
480
732
  }
481
733
  else if (ev.type === "message") {
482
- const content = ev.content || "";
483
- this.sessions.update(sessionId, { lastEvent: content.slice(0, 80) });
484
- }
485
- else if (ev.type === "text.delta") {
486
- // Don't spam updatedAt too much? Just lastEvent
734
+ this.sessions.update(sessionId, { lastEvent: ev.content.slice(0, 80) });
487
735
  }
488
736
  else if (ev.type === "usage.updated") {
489
- const u = ev.usage;
490
737
  const sess = this.sessions.get(sessionId);
491
738
  if (sess) {
492
- const usage = { ...(sess.usage || {}), ...u };
493
- this.sessions.update(sessionId, { usage });
739
+ this.sessions.update(sessionId, { usage: { ...(sess.usage || {}), ...ev.usage } });
494
740
  }
495
741
  }
496
- // Also update updatedAt
497
742
  this.sessions.update(sessionId, { updatedAt: new Date() });
498
743
  }
499
744
  catch { }