codedeck 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/done.js +35 -0
- package/dist/cli/commands/done.js.map +1 -0
- package/dist/cli/commands/send.js +2 -0
- package/dist/cli/commands/send.js.map +1 -1
- package/dist/cli/commands/web.js +89 -6
- package/dist/cli/commands/web.js.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/events.js.map +1 -1
- package/dist/core/roles.js +28 -5
- package/dist/core/roles.js.map +1 -1
- package/dist/core/session.js.map +1 -1
- package/dist/daemon/daemon.js +289 -93
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/drivers/antigravity/driver.js +52 -38
- package/dist/drivers/antigravity/driver.js.map +1 -1
- package/dist/open/contract.js +6 -2
- package/dist/open/contract.js.map +1 -1
- package/dist/open/launchers/claude.js +8 -2
- package/dist/open/launchers/claude.js.map +1 -1
- package/dist/plugin/agents/auditor.md +13 -5
- package/dist/plugin/agents/general.md +355 -15
- package/dist/plugin/agents/orchestrator-edit.md +16 -5
- package/dist/plugin/agents/orchestrator-read.md +16 -5
- package/dist/plugin/agents/orchestrator.md +16 -5
- package/dist/plugin/agents/reviewer.md +6 -2
- package/dist/plugin/prompts/_partials/commit.md +145 -0
- package/dist/plugin/prompts/_partials/core.md +9 -0
- package/dist/plugin/prompts/_partials/dispatch.md +5 -0
- package/dist/plugin/prompts/_partials/pr-writer.md +186 -0
- package/dist/plugin/prompts/_partials/proof.md +2 -0
- package/dist/plugin/prompts/_partials/rename-run.md +1 -0
- package/dist/plugin/prompts/_partials/review-self.md +4 -0
- package/dist/plugin/prompts/_partials/worktree.md +2 -0
- package/dist/plugin/prompts/roles/auditor.md +35 -0
- package/dist/plugin/prompts/roles/general.md +29 -0
- package/dist/plugin/prompts/roles/orchestrator-edit.md +68 -0
- package/dist/plugin/prompts/roles/orchestrator-read.md +68 -0
- package/dist/plugin/prompts/roles/orchestrator.md +90 -0
- package/dist/plugin/prompts/roles/reviewer.md +37 -0
- package/dist/store/database.js +4 -0
- package/dist/store/database.js.map +1 -1
- package/dist/store/sessions.js +8 -3
- package/dist/store/sessions.js.map +1 -1
- package/dist/web/canvas-page.js +208 -57
- package/dist/web/canvas-page.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon/daemon.js
CHANGED
|
@@ -49,6 +49,27 @@ function withLiveStatus(s) {
|
|
|
49
49
|
const status = liveStatus(s.status, s.pid, processAlive(s.pid));
|
|
50
50
|
return status === s.status ? s : { ...s, status: status };
|
|
51
51
|
}
|
|
52
|
+
// Send-queue cap: bytes, matching MAX_SEND_BODY_BYTES on the web route.
|
|
53
|
+
const MAX_SEND_MESSAGE_BYTES = 64 * 1024;
|
|
54
|
+
function validateSendMessage(raw) {
|
|
55
|
+
if (typeof raw !== "string")
|
|
56
|
+
return { code: "INVALID", error: "message required" };
|
|
57
|
+
const message = raw.trim();
|
|
58
|
+
if (!message)
|
|
59
|
+
return { code: "INVALID", error: "message required" };
|
|
60
|
+
if (Buffer.byteLength(message, "utf8") > MAX_SEND_MESSAGE_BYTES) {
|
|
61
|
+
return { code: "INVALID", error: "message too large" };
|
|
62
|
+
}
|
|
63
|
+
return { message };
|
|
64
|
+
}
|
|
65
|
+
// PID identity triple, same rule as the stop path: a recycled PID must
|
|
66
|
+
// neither block a legitimate resume nor permit killing a stranger.
|
|
67
|
+
function livePidIdentity(s) {
|
|
68
|
+
return (s.pid != null &&
|
|
69
|
+
s.pidStartTime != null &&
|
|
70
|
+
processAlive(s.pid) &&
|
|
71
|
+
processStartTime(s.pid) === s.pidStartTime);
|
|
72
|
+
}
|
|
52
73
|
class Daemon {
|
|
53
74
|
db;
|
|
54
75
|
sessions;
|
|
@@ -476,8 +497,20 @@ class Daemon {
|
|
|
476
497
|
send({ error: { code: "SESSION_NOT_FOUND", message: `Session ${p.id} not found` } });
|
|
477
498
|
return;
|
|
478
499
|
}
|
|
479
|
-
|
|
480
|
-
|
|
500
|
+
// Release owns the terminal outcome: a queued message must not
|
|
501
|
+
// survive as a phantom "na fila" on every exit, including the
|
|
502
|
+
// terminal early return below.
|
|
503
|
+
this.clearPending(s.id);
|
|
504
|
+
// Interrupted is an archival terminal: a power-shutdown row stays
|
|
505
|
+
// actionable and release may finalize it to completed/failed — unless
|
|
506
|
+
// its process is still alive under the same identity, which keeps
|
|
507
|
+
// the send path's stop-first guard instead of orphaning it.
|
|
508
|
+
if (s.status === "interrupted" && livePidIdentity(s)) {
|
|
509
|
+
send({ error: { code: "SESSION_BUSY", message: `Session ${s.id} is still running (stop it first)` } });
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (isTerminalStatus(s.status) && s.status !== "interrupted") {
|
|
513
|
+
send({ result: { session: this.sessions.get(s.id) } });
|
|
481
514
|
return;
|
|
482
515
|
}
|
|
483
516
|
const targetStatus = p.status === "failed" ? "failed" : "completed";
|
|
@@ -550,6 +583,12 @@ class Daemon {
|
|
|
550
583
|
}
|
|
551
584
|
case "session.send": {
|
|
552
585
|
const p = params;
|
|
586
|
+
const validated = validateSendMessage(p.message);
|
|
587
|
+
if ("code" in validated) {
|
|
588
|
+
send({ error: { code: validated.code, message: validated.error } });
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const message = validated.message;
|
|
553
592
|
const s = this.sessions.get(p.id);
|
|
554
593
|
if (!s) {
|
|
555
594
|
send({ error: { code: "SESSION_NOT_FOUND", message: `Session ${p.id} not found` } });
|
|
@@ -560,118 +599,94 @@ class Daemon {
|
|
|
560
599
|
return;
|
|
561
600
|
}
|
|
562
601
|
const driver = this.registry.get(s.agent);
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
602
|
+
if (!driver.capabilities().resume) {
|
|
603
|
+
send({ error: { code: "CAPABILITY_NOT_SUPPORTED", message: `Agent ${s.agent} does not support resume` } });
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (s.status === "interrupted" && !s.nativeSessionId) {
|
|
607
|
+
send({ error: { code: "CAPABILITY_NOT_SUPPORTED", message: `Session ${s.id} cannot resume (no native session id or agent ${s.agent} does not support resume)` } });
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
// Power-interrupted sessions with a live process keep the old rule:
|
|
611
|
+
// stop first, never queue behind a running harness.
|
|
612
|
+
if (s.status === "interrupted" && livePidIdentity(s)) {
|
|
613
|
+
send({ error: { code: "SESSION_BUSY", message: `Session ${s.id} is still running (stop it first)` } });
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
// A lifecycle operation owns the session: retry instead of queueing,
|
|
617
|
+
// so a send racing stop cannot strand a phantom slot with no stream
|
|
618
|
+
// tail left to consume it (stop cancels the queue on exit).
|
|
566
619
|
if (this.sessionLocks.has(s.id)) {
|
|
567
620
|
send({ error: { code: "SESSION_BUSY", message: `Session ${s.id} has another lifecycle operation in progress` } });
|
|
568
621
|
return;
|
|
569
622
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
623
|
+
// Do not start a second harness while the current one is still live:
|
|
624
|
+
// both runtimes would tail the same per-session file and duplicate
|
|
625
|
+
// every event from the follow-up turn. Queue behind a live turn only.
|
|
626
|
+
const busyHandle = driver.getHandle?.(s.id);
|
|
627
|
+
const busyState = busyHandle && typeof busyHandle === "object"
|
|
628
|
+
? busyHandle
|
|
629
|
+
: undefined;
|
|
630
|
+
const runtimeBusy = busyHandle !== undefined && (busyState?.done !== true || busyState?.drained !== true);
|
|
631
|
+
const busy = s.status === "starting" || runtimeBusy || livePidIdentity(s);
|
|
632
|
+
if (busy) {
|
|
633
|
+
// One-slot queue, last-wins: persist without holding the lifecycle
|
|
634
|
+
// lock, then converge — the turn may have ended between the busy
|
|
635
|
+
// read and this persist, in which case tryDispatch starts now.
|
|
636
|
+
const pendingAt = new Date().toISOString();
|
|
637
|
+
try {
|
|
638
|
+
this.sessions.update(s.id, { pendingMessage: message, pendingAt });
|
|
577
639
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
processAlive(s.pid) &&
|
|
581
|
-
processStartTime(s.pid) === s.pidStartTime;
|
|
582
|
-
if (liveIdentity) {
|
|
583
|
-
send({ error: { code: "SESSION_BUSY", message: `Session ${s.id} is still running (stop it first)` } });
|
|
640
|
+
catch (e) {
|
|
641
|
+
send({ error: { code: "SEND_FAILED", message: e instanceof Error ? e.message : String(e) } });
|
|
584
642
|
return;
|
|
585
643
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
return;
|
|
644
|
+
const queuedEvent = {
|
|
645
|
+
type: "message.queued",
|
|
646
|
+
sessionId: s.id,
|
|
647
|
+
timestamp: pendingAt,
|
|
648
|
+
prompt: message,
|
|
649
|
+
pendingAt,
|
|
650
|
+
};
|
|
651
|
+
try {
|
|
652
|
+
this.events.append(s.id, queuedEvent);
|
|
596
653
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
654
|
+
catch { }
|
|
655
|
+
this.broadcast(s.id, queuedEvent);
|
|
656
|
+
try {
|
|
657
|
+
this.sessions.update(s.id, { lastEvent: `queued: ${message.slice(0, 80)}` });
|
|
600
658
|
}
|
|
659
|
+
catch { }
|
|
660
|
+
void this.tryDispatch(s.id);
|
|
661
|
+
send({ result: { ok: true, queued: true } });
|
|
662
|
+
return;
|
|
601
663
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
cwd: s.worktree || s.cwd,
|
|
606
|
-
model: s.model,
|
|
607
|
-
effort: s.effort,
|
|
608
|
-
fast: s.fast,
|
|
609
|
-
sandbox: s.sandbox,
|
|
610
|
-
dangerouslyBypassApprovalsAndSandbox: s.dangerouslyBypassApprovalsAndSandbox,
|
|
611
|
-
pid: s.pid,
|
|
612
|
-
pidStartTime: s.pidStartTime,
|
|
613
|
-
};
|
|
664
|
+
// Resumable now (terminal completed/failed/stopped/orphaned, or
|
|
665
|
+
// interrupted without a live process): a stale queued slot loses to
|
|
666
|
+
// the new message, which starts immediately.
|
|
614
667
|
this.sessionLocks.add(s.id);
|
|
615
668
|
try {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
this.sessions.setStatus(s.id, "working", { lastEvent: `send: ${p.message.slice(0, 80)}` });
|
|
621
|
-
const turnEvent = {
|
|
622
|
-
type: "turn.started",
|
|
623
|
-
sessionId: s.id,
|
|
624
|
-
timestamp: new Date().toISOString(),
|
|
625
|
-
prompt: p.message,
|
|
626
|
-
};
|
|
627
|
-
this.events.append(s.id, turnEvent);
|
|
628
|
-
this.broadcast(s.id, turnEvent);
|
|
629
|
-
await driver.send(drvSession, p.message);
|
|
630
|
-
if (this.shuttingDown) {
|
|
631
|
-
try {
|
|
632
|
-
await driver.stop(drvSession);
|
|
633
|
-
}
|
|
634
|
-
catch { }
|
|
635
|
-
send({ error: { code: "SERVICE_UNAVAILABLE", message: "daemon is shutting down" } });
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
const handle = driver.getHandle?.(s.id);
|
|
639
|
-
const handleNativeId = handle &&
|
|
640
|
-
typeof handle === "object" &&
|
|
641
|
-
"nativeSessionId" in handle &&
|
|
642
|
-
typeof handle.nativeSessionId === "string"
|
|
643
|
-
? handle.nativeSessionId
|
|
644
|
-
: undefined;
|
|
645
|
-
const newNative = handleNativeId || drvSession.nativeSessionId;
|
|
646
|
-
if (newNative && newNative !== s.nativeSessionId) {
|
|
647
|
-
this.sessions.update(s.id, { nativeSessionId: newNative });
|
|
648
|
-
}
|
|
649
|
-
if (drvSession.pid) {
|
|
650
|
-
this.sessions.update(s.id, {
|
|
651
|
-
pid: drvSession.pid,
|
|
652
|
-
pidStartTime: processStartTime(drvSession.pid),
|
|
653
|
-
});
|
|
669
|
+
try {
|
|
670
|
+
this.sessions.update(s.id, { pendingMessage: null, pendingAt: null });
|
|
654
671
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
send({ result: { ok: true } });
|
|
672
|
+
catch { }
|
|
673
|
+
await this.runResumeTurn(s, message);
|
|
674
|
+
send({ result: { ok: true, queued: false } });
|
|
659
675
|
}
|
|
660
676
|
catch (e) {
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
send({ error: { code: "SERVICE_UNAVAILABLE", message: "daemon is shutting down" } });
|
|
677
|
+
const errText = e instanceof Error ? e.message : String(e);
|
|
678
|
+
const code = e instanceof Error && e.code === "SERVICE_UNAVAILABLE"
|
|
679
|
+
? "SERVICE_UNAVAILABLE"
|
|
680
|
+
: this.shuttingDown ? "SERVICE_UNAVAILABLE" : "SEND_FAILED";
|
|
681
|
+
if (code === "SERVICE_UNAVAILABLE") {
|
|
682
|
+
send({ error: { code, message: errText } });
|
|
668
683
|
return;
|
|
669
684
|
}
|
|
670
685
|
try {
|
|
671
686
|
this.sessions.setStatus(s.id, "failed");
|
|
672
687
|
}
|
|
673
688
|
catch { }
|
|
674
|
-
send({ error: { code: "SEND_FAILED", message } });
|
|
689
|
+
send({ error: { code: "SEND_FAILED", message: errText } });
|
|
675
690
|
}
|
|
676
691
|
finally {
|
|
677
692
|
if (!this.shuttingDown)
|
|
@@ -686,6 +701,9 @@ class Daemon {
|
|
|
686
701
|
send({ error: { code: "SESSION_NOT_FOUND", message: `Session ${p.id} not found` } });
|
|
687
702
|
return;
|
|
688
703
|
}
|
|
704
|
+
// Stop cancels a queued message on every exit (explicit cancel),
|
|
705
|
+
// including the BUSY/NOT_RUNNING/UNSAFE early returns below.
|
|
706
|
+
this.clearPending(s.id);
|
|
689
707
|
const driver = this.registry.get(s.agent);
|
|
690
708
|
if (this.sessionLocks.has(s.id)) {
|
|
691
709
|
send({ error: { code: "SESSION_BUSY", message: `Session ${s.id} has another lifecycle operation in progress` } });
|
|
@@ -747,6 +765,9 @@ class Daemon {
|
|
|
747
765
|
this.events.append(s.id, ev);
|
|
748
766
|
this.broadcast(s.id, ev);
|
|
749
767
|
}
|
|
768
|
+
// A send may have persisted a slot after the entry clear (it saw a
|
|
769
|
+
// live turn before this stop took the lock): stop wins, clear again.
|
|
770
|
+
this.clearPending(s.id);
|
|
750
771
|
send({ result: { ok: true } });
|
|
751
772
|
}
|
|
752
773
|
catch (e) {
|
|
@@ -1077,6 +1098,173 @@ class Daemon {
|
|
|
1077
1098
|
// Attach events
|
|
1078
1099
|
await this.attachDriverEvents(sessionId, driver, drvSession);
|
|
1079
1100
|
}
|
|
1101
|
+
/** Best-effort queue cancel; stop/release own the terminal outcome. */
|
|
1102
|
+
clearPending(sessionId) {
|
|
1103
|
+
try {
|
|
1104
|
+
this.sessions.update(sessionId, { pendingMessage: null, pendingAt: null });
|
|
1105
|
+
}
|
|
1106
|
+
catch { }
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
+ * Starts a resumed turn; caller MUST hold sessionLocks for the session.
|
|
1110
|
+
+ * Shared by session.send (immediate) and tryDispatch (queued) so both
|
|
1111
|
+
+ * paths emit the same frames in the same order.
|
|
1112
|
+
+ */
|
|
1113
|
+
async runResumeTurn(s, message) {
|
|
1114
|
+
const driver = this.registry.get(s.agent);
|
|
1115
|
+
if (this.shuttingDown) {
|
|
1116
|
+
const early = new Error("daemon is shutting down");
|
|
1117
|
+
early.code = "SERVICE_UNAVAILABLE";
|
|
1118
|
+
throw early;
|
|
1119
|
+
}
|
|
1120
|
+
this.sessions.setStatus(s.id, "working", { lastEvent: `send: ${message.slice(0, 80)}` });
|
|
1121
|
+
const turnEvent = {
|
|
1122
|
+
type: "turn.started",
|
|
1123
|
+
sessionId: s.id,
|
|
1124
|
+
timestamp: new Date().toISOString(),
|
|
1125
|
+
prompt: message,
|
|
1126
|
+
};
|
|
1127
|
+
this.events.append(s.id, turnEvent);
|
|
1128
|
+
this.broadcast(s.id, turnEvent);
|
|
1129
|
+
const drvSession = {
|
|
1130
|
+
id: s.id,
|
|
1131
|
+
nativeSessionId: s.nativeSessionId,
|
|
1132
|
+
cwd: s.worktree || s.cwd,
|
|
1133
|
+
model: s.model,
|
|
1134
|
+
effort: s.effort,
|
|
1135
|
+
fast: s.fast,
|
|
1136
|
+
sandbox: s.sandbox,
|
|
1137
|
+
dangerouslyBypassApprovalsAndSandbox: s.dangerouslyBypassApprovalsAndSandbox,
|
|
1138
|
+
pid: s.pid,
|
|
1139
|
+
pidStartTime: s.pidStartTime,
|
|
1140
|
+
};
|
|
1141
|
+
try {
|
|
1142
|
+
await driver.send(drvSession, message);
|
|
1143
|
+
}
|
|
1144
|
+
catch (e) {
|
|
1145
|
+
if (this.shuttingDown) {
|
|
1146
|
+
try {
|
|
1147
|
+
await driver.stop(drvSession);
|
|
1148
|
+
}
|
|
1149
|
+
catch { }
|
|
1150
|
+
}
|
|
1151
|
+
throw e;
|
|
1152
|
+
}
|
|
1153
|
+
if (this.shuttingDown) {
|
|
1154
|
+
try {
|
|
1155
|
+
await driver.stop(drvSession);
|
|
1156
|
+
}
|
|
1157
|
+
catch { }
|
|
1158
|
+
const late = new Error("daemon is shutting down");
|
|
1159
|
+
late.code = "SERVICE_UNAVAILABLE";
|
|
1160
|
+
throw late;
|
|
1161
|
+
}
|
|
1162
|
+
const handle = driver.getHandle?.(s.id);
|
|
1163
|
+
const handleNativeId = handle &&
|
|
1164
|
+
typeof handle === "object" &&
|
|
1165
|
+
"nativeSessionId" in handle &&
|
|
1166
|
+
typeof handle.nativeSessionId === "string"
|
|
1167
|
+
? handle.nativeSessionId
|
|
1168
|
+
: undefined;
|
|
1169
|
+
const newNative = handleNativeId || drvSession.nativeSessionId;
|
|
1170
|
+
if (newNative && newNative !== s.nativeSessionId) {
|
|
1171
|
+
this.sessions.update(s.id, { nativeSessionId: newNative });
|
|
1172
|
+
}
|
|
1173
|
+
if (drvSession.pid) {
|
|
1174
|
+
this.sessions.update(s.id, {
|
|
1175
|
+
pid: drvSession.pid,
|
|
1176
|
+
pidStartTime: processStartTime(drvSession.pid),
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
this.sessions.update(s.id, { status: "working" });
|
|
1180
|
+
// Attach event loop for new turn
|
|
1181
|
+
this.attachDriverEvents(s.id, driver, drvSession).catch(() => { });
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
+ * Starts the queued message as a new turn when the session is resumable.
|
|
1185
|
+
+ * Never blocks: returns false leaving the slot intact when busy,
|
|
1186
|
+
+ * unresumable, or shutting down. The stream tail calls this, and the
|
|
1187
|
+
+ * busy send path calls it to converge on races (enqueue landing as the
|
|
1188
|
+
+ * turn ends starts immediately instead of stranding).
|
|
1189
|
+
+ */
|
|
1190
|
+
async tryDispatch(sessionId) {
|
|
1191
|
+
if (this.shuttingDown || this.sessionLocks.has(sessionId))
|
|
1192
|
+
return false;
|
|
1193
|
+
const queued = this.sessions.get(sessionId);
|
|
1194
|
+
if (!queued || !queued.pendingMessage || queued.origin === "open")
|
|
1195
|
+
return false;
|
|
1196
|
+
this.sessionLocks.add(sessionId);
|
|
1197
|
+
let message;
|
|
1198
|
+
let pendingAt;
|
|
1199
|
+
try {
|
|
1200
|
+
const s = this.sessions.get(sessionId);
|
|
1201
|
+
if (!s || !s.pendingMessage || s.origin === "open")
|
|
1202
|
+
return false;
|
|
1203
|
+
const driver = this.registry.get(s.agent);
|
|
1204
|
+
if (!driver.capabilities().resume)
|
|
1205
|
+
return false;
|
|
1206
|
+
const handle = driver.getHandle?.(s.id);
|
|
1207
|
+
const hs = handle && typeof handle === "object"
|
|
1208
|
+
? handle
|
|
1209
|
+
: undefined;
|
|
1210
|
+
// Same resolution as SessionDriver.send: without a native id the start
|
|
1211
|
+
// would only throw after emitting turn.started and flipping status, so
|
|
1212
|
+
// leave the slot quietly for a manual send instead.
|
|
1213
|
+
const nativeId = s.nativeSessionId
|
|
1214
|
+
|| (typeof hs?.nativeSessionId === "string" ? hs.nativeSessionId : undefined);
|
|
1215
|
+
if (!nativeId)
|
|
1216
|
+
return false;
|
|
1217
|
+
if (s.status === "starting")
|
|
1218
|
+
return false;
|
|
1219
|
+
if (handle !== undefined && (hs?.done !== true || hs?.drained !== true))
|
|
1220
|
+
return false;
|
|
1221
|
+
if (livePidIdentity(s))
|
|
1222
|
+
return false;
|
|
1223
|
+
message = s.pendingMessage;
|
|
1224
|
+
pendingAt = s.pendingAt;
|
|
1225
|
+
// Clear first so a crash mid-start cannot double-send; restored below
|
|
1226
|
+
// when the start fails.
|
|
1227
|
+
this.sessions.update(s.id, { pendingMessage: null, pendingAt: null });
|
|
1228
|
+
await this.runResumeTurn(s, message);
|
|
1229
|
+
return true;
|
|
1230
|
+
}
|
|
1231
|
+
catch (e) {
|
|
1232
|
+
const errText = e instanceof Error ? e.message : String(e);
|
|
1233
|
+
const unavailable = e?.code === "SERVICE_UNAVAILABLE" || this.shuttingDown;
|
|
1234
|
+
if (!unavailable) {
|
|
1235
|
+
const failure = classifyFailure(errText);
|
|
1236
|
+
const errEv = {
|
|
1237
|
+
type: "session.failed",
|
|
1238
|
+
sessionId,
|
|
1239
|
+
timestamp: new Date().toISOString(),
|
|
1240
|
+
error: errText,
|
|
1241
|
+
failure,
|
|
1242
|
+
};
|
|
1243
|
+
try {
|
|
1244
|
+
this.events.append(sessionId, errEv);
|
|
1245
|
+
}
|
|
1246
|
+
catch { }
|
|
1247
|
+
this.broadcast(sessionId, errEv);
|
|
1248
|
+
try {
|
|
1249
|
+
this.sessions.setStatus(sessionId, "failed", { lastEvent: errText.slice(0, 200), failure });
|
|
1250
|
+
}
|
|
1251
|
+
catch { }
|
|
1252
|
+
}
|
|
1253
|
+
// Restore the slot so a failed dispatch never vanishes silently; the
|
|
1254
|
+
// next manual send (or a future tail) retries it.
|
|
1255
|
+
try {
|
|
1256
|
+
if (message !== undefined) {
|
|
1257
|
+
this.sessions.update(sessionId, { pendingMessage: message, pendingAt: pendingAt ?? new Date().toISOString() });
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
catch { }
|
|
1261
|
+
return false;
|
|
1262
|
+
}
|
|
1263
|
+
finally {
|
|
1264
|
+
if (!this.shuttingDown)
|
|
1265
|
+
this.sessionLocks.delete(sessionId);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1080
1268
|
async attachDriverEvents(sessionId, driver, drvSession) {
|
|
1081
1269
|
try {
|
|
1082
1270
|
for await (const ev of driver.events(drvSession)) {
|
|
@@ -1166,6 +1354,11 @@ class Daemon {
|
|
|
1166
1354
|
this.sessions.setStatus(sessionId, "failed", { lastEvent: error.slice(0, 200), failure });
|
|
1167
1355
|
}
|
|
1168
1356
|
}
|
|
1357
|
+
// A message queued while the turn ran starts now as the next turn.
|
|
1358
|
+
// tryDispatch rechecks resumability under the lifecycle lock; a stale
|
|
1359
|
+
// or unresumable slot stays put for a manual send.
|
|
1360
|
+
if (!this.shuttingDown)
|
|
1361
|
+
void this.tryDispatch(sessionId);
|
|
1169
1362
|
}
|
|
1170
1363
|
updateSessionFromEvent(sessionId, ev) {
|
|
1171
1364
|
try {
|
|
@@ -1186,6 +1379,9 @@ class Daemon {
|
|
|
1186
1379
|
else if (ev.type === "tool.started") {
|
|
1187
1380
|
this.sessions.update(sessionId, { lastEvent: `tool: ${ev.tool.name}` });
|
|
1188
1381
|
}
|
|
1382
|
+
else if (ev.type === "message.queued") {
|
|
1383
|
+
this.sessions.update(sessionId, { lastEvent: `queued: ${ev.prompt.slice(0, 80)}` });
|
|
1384
|
+
}
|
|
1189
1385
|
else if (ev.type === "message") {
|
|
1190
1386
|
this.sessions.update(sessionId, { lastEvent: ev.content.slice(0, 80) });
|
|
1191
1387
|
}
|