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.
- package/README.md +61 -3
- package/dist/cli/commands/logs.js +5 -1
- package/dist/cli/commands/logs.js.map +1 -1
- package/dist/cli/commands/run.js +91 -28
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/show.js +4 -0
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/wait.js +57 -0
- package/dist/cli/commands/wait.js.map +1 -0
- package/dist/cli/index.js +10 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/wait.js +44 -0
- package/dist/cli/wait.js.map +1 -0
- package/dist/core/driver.js +9 -1
- package/dist/core/driver.js.map +1 -1
- package/dist/core/errors.js +56 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/events.js.map +1 -1
- package/dist/core/session.js.map +1 -1
- package/dist/daemon/daemon.js +316 -71
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/drivers/claude/driver.js +34 -205
- package/dist/drivers/claude/driver.js.map +1 -1
- package/dist/drivers/codex/driver.js +65 -205
- package/dist/drivers/codex/driver.js.map +1 -1
- package/dist/drivers/helpers.js +47 -37
- package/dist/drivers/helpers.js.map +1 -1
- package/dist/drivers/omp/driver.js +38 -206
- package/dist/drivers/omp/driver.js.map +1 -1
- package/dist/drivers/opencode/driver.js +30 -182
- package/dist/drivers/opencode/driver.js.map +1 -1
- package/dist/drivers/session-driver.js +122 -0
- package/dist/drivers/session-driver.js.map +1 -0
- package/dist/drivers/session-runtime.js +333 -0
- package/dist/drivers/session-runtime.js.map +1 -0
- package/dist/drivers/tailer.js +149 -0
- package/dist/drivers/tailer.js.map +1 -0
- package/dist/drivers/terminal.js +53 -0
- package/dist/drivers/terminal.js.map +1 -0
- package/dist/store/database.js +28 -1
- package/dist/store/database.js.map +1 -1
- package/dist/store/events.js +8 -3
- package/dist/store/events.js.map +1 -1
- package/dist/store/sessions.js +27 -4
- package/dist/store/sessions.js.map +1 -1
- package/dist/utils/process.js +104 -4
- package/dist/utils/process.js.map +1 -1
- package/package.json +10 -2
package/dist/core/errors.js
CHANGED
|
@@ -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
|
package/dist/core/errors.js.map
CHANGED
|
@@ -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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"}
|
package/dist/core/events.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/core/events.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/core/session.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/core/session.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/daemon/daemon.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
77
|
+
const paths = getPaths();
|
|
71
78
|
const actives = this.sessions.listActive();
|
|
72
79
|
for (const s of actives) {
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
//
|
|
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:
|
|
251
|
+
this.sessions.setStatus(sessionId, "failed", { lastEvent: msg.slice(0, 200), failure });
|
|
163
252
|
}
|
|
164
253
|
catch { }
|
|
165
|
-
this.broadcast(sessionId,
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
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, {
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
this.
|
|
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
|
-
|
|
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:
|
|
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
|
-
//
|
|
464
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 { }
|