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.
- package/README.md +24 -8
- package/dist/cli/commands/run.js +3 -2
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/service.js +139 -0
- package/dist/cli/commands/service.js.map +1 -0
- package/dist/cli/index.js +4 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/paths.js +14 -14
- package/dist/config/paths.js.map +1 -1
- package/dist/core/errors.js +5 -42
- package/dist/core/errors.js.map +1 -1
- package/dist/core/session.js +1 -1
- package/dist/core/session.js.map +1 -1
- package/dist/core/usage-source.js +2 -0
- package/dist/core/usage-source.js.map +1 -1
- package/dist/daemon/auto-resume.js +18 -0
- package/dist/daemon/auto-resume.js.map +1 -0
- package/dist/daemon/daemon.js +188 -125
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/drivers/antigravity/parser.js +7 -10
- package/dist/drivers/antigravity/parser.js.map +1 -1
- package/dist/drivers/codex/parser.js +20 -2
- package/dist/drivers/codex/parser.js.map +1 -1
- package/dist/drivers/grok/driver.js +163 -0
- package/dist/drivers/grok/driver.js.map +1 -0
- package/dist/drivers/grok/parser.js +46 -0
- package/dist/drivers/grok/parser.js.map +1 -0
- package/dist/drivers/registry.js +2 -0
- package/dist/drivers/registry.js.map +1 -1
- package/dist/git/repository.js +0 -20
- package/dist/git/repository.js.map +1 -1
- package/dist/git/worktree.js +0 -14
- package/dist/git/worktree.js.map +1 -1
- package/dist/open/injection.js +4 -3
- package/dist/open/injection.js.map +1 -1
- package/dist/plugin/agents/auditor.md +1 -1
- package/dist/plugin/agents/orchestrator-edit.md +1 -1
- package/dist/plugin/agents/orchestrator-read.md +1 -1
- package/dist/plugin/agents/orchestrator.md +1 -1
- package/dist/plugin/agents/reviewer.md +1 -1
- package/dist/plugin/mods/agents/pane.ts +2 -0
- package/dist/plugin/prompts/roles/auditor.md +1 -1
- package/dist/plugin/prompts/roles/orchestrator-edit.md +1 -1
- package/dist/plugin/prompts/roles/orchestrator-read.md +1 -1
- package/dist/plugin/prompts/roles/orchestrator.md +1 -1
- package/dist/plugin/prompts/roles/reviewer.md +1 -1
- package/dist/store/claims.js +4 -4
- package/dist/store/database.js +1 -7
- package/dist/store/database.js.map +1 -1
- package/dist/store/sessions.js +63 -73
- package/dist/store/sessions.js.map +1 -1
- package/dist/utils/process.js +2 -35
- package/dist/utils/process.js.map +1 -1
- package/dist/web/setup-icons.js +2 -0
- package/dist/web/setup-icons.js.map +1 -1
- package/package.json +6 -5
package/dist/daemon/daemon.js
CHANGED
|
@@ -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,
|
|
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 =
|
|
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:
|
|
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: ${
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
657
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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 = (
|
|
773
|
-
if (!
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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: ${
|
|
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.
|
|
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
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
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.
|
|
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 =
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
-
|
|
2072
|
-
const paths = getPaths();
|
|
2137
|
+
if (this.boundPaths) {
|
|
2073
2138
|
try {
|
|
2074
|
-
fs.unlinkSync(
|
|
2139
|
+
fs.unlinkSync(this.boundPaths.sock);
|
|
2075
2140
|
}
|
|
2076
2141
|
catch { }
|
|
2077
2142
|
try {
|
|
2078
|
-
fs.unlinkSync(
|
|
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.
|
|
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
|
|
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);
|