@cmgmyr/hive 1.8.0 → 1.9.0
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/build-info.json +1 -1
- package/dist/claudeWake.js +44 -0
- package/dist/cli.js +10 -6
- package/dist/dashboard.js +65 -14
- package/dist/db.js +7 -0
- package/dist/hook.js +22 -0
- package/dist/hooks.js +14 -0
- package/dist/projectYml.js +14 -0
- package/dist/queenDashboard.js +1 -1
- package/dist/scheduler.js +117 -16
- package/dist/tools/wakes.js +6 -3
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.
|
|
1
|
+
{"version":"1.9.0","sha":"b0be61b","dirty":false,"build_id":"b0c86484d75006efe978e316cdbccb3c9b4e09604634cfff5f7a61c8b8d825b0"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createConnection } from "node:net";
|
|
2
|
+
export const CROSS_SESSION_CLOSE = "</cross-session-message>";
|
|
3
|
+
export const SOCKET_WAKE_FOOTER = "Automated hive wake. Do not reply to this sender.";
|
|
4
|
+
const SOCKET_WRITE_BOUND_MS = 1000;
|
|
5
|
+
export function senderAddress(socketPath) {
|
|
6
|
+
const encoded = socketPath.replace(/[^A-Za-z0-9/._-]/gu, (c) => [...Buffer.from(c)].map((b) => `%${b.toString(16).toUpperCase().padStart(2, "0")}`).join(""));
|
|
7
|
+
return `uds:${encoded}`;
|
|
8
|
+
}
|
|
9
|
+
function claudeWakeFrame(senderAddress, text) {
|
|
10
|
+
const content = `<cross-session-message from="${senderAddress}" from-name="hive">\n${text}\n${CROSS_SESSION_CLOSE}`;
|
|
11
|
+
return JSON.stringify({ type: "user", from: senderAddress, message: { role: "user", content } }) + "\n";
|
|
12
|
+
}
|
|
13
|
+
// True only for a completed local write. The socket sends no ack, so true never means the lead read it.
|
|
14
|
+
export function postClaudeWake(options) {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
let settled = false;
|
|
17
|
+
const finish = (ok) => {
|
|
18
|
+
if (settled)
|
|
19
|
+
return;
|
|
20
|
+
settled = true;
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
if (ok)
|
|
23
|
+
socket.end();
|
|
24
|
+
else
|
|
25
|
+
socket.destroy();
|
|
26
|
+
resolve(ok);
|
|
27
|
+
};
|
|
28
|
+
const socket = createConnection(options.socketPath);
|
|
29
|
+
const timer = setTimeout(() => finish(false), SOCKET_WRITE_BOUND_MS);
|
|
30
|
+
socket.on("error", () => finish(false));
|
|
31
|
+
socket.on("connect", () => {
|
|
32
|
+
let allowed = false;
|
|
33
|
+
try {
|
|
34
|
+
allowed = options.beforeWrite();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
allowed = false;
|
|
38
|
+
}
|
|
39
|
+
if (!allowed)
|
|
40
|
+
return finish(false);
|
|
41
|
+
socket.write(claudeWakeFrame(options.senderAddress, options.text), (err) => finish(!err));
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -20,16 +20,16 @@ import { isLowHeadroom, orphanLoginShellDetails, ptyHeadroom } from "./ptys.js";
|
|
|
20
20
|
import { addProject, agentProjectPin, assertQueenCrossProjectWrite, captureQueenWriteIdentity, currentActor, effectiveProjectId, findProjectForCwd, findProjectForDir, getProjectByPath, gitPrimaryRoot, getProject, listProjects, QUEEN_PROFILE, queenHomeDir, registeredAncestor, takeRegistrationNotice, } from "./context.js";
|
|
21
21
|
import { confirmQueenWrite, listQueenAudit } from "./queenAudit.js";
|
|
22
22
|
import { formatRowCounts, projectRowCounts, removeProject } from "./projectRemove.js";
|
|
23
|
-
import { ensureHooksFile } from "./hooks.js";
|
|
23
|
+
import { ensureHooksFile, ensureLeadHooksFile } from "./hooks.js";
|
|
24
24
|
import { errorMessage, registrationNoticeText, withTrailingNewline } from "./result.js";
|
|
25
|
-
import { ACTIVE_TIMER_WHERE, dashboardFileContained, describeStall, HELD_REASON_CONVERSATION, HELD_REASON_LEAD_PANE_DEAD, HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX, isLeadRowClosedHold, isUnclassifiablePaneHold, HELD_REASON_UNSUBMITTED_INPUT_PREFIX, isUnsubmittedInputHold, janitor, resolveDashboardFile, generateQueenDashboardNow, transcriptStaleness, wasHeldForPaneIdentity, } from "./scheduler.js";
|
|
25
|
+
import { ACTIVE_TIMER_WHERE, socketAwaitingWhere, dashboardFileContained, describeStall, HELD_REASON_CONVERSATION, HELD_REASON_LEAD_PANE_DEAD, HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX, isLeadRowClosedHold, isUnclassifiablePaneHold, HELD_REASON_UNSUBMITTED_INPUT_PREFIX, isUnsubmittedInputHold, janitor, resolveDashboardFile, generateQueenDashboardNow, transcriptStaleness, wasHeldForPaneIdentity, } from "./scheduler.js";
|
|
26
26
|
import { readTurnCount } from "./turnCount.js";
|
|
27
27
|
import { collectPortfolio, leadText } from "./portfolio.js";
|
|
28
28
|
import { STALL_BOUND_SECONDS } from "./backgroundTasks.js";
|
|
29
29
|
import { probeSessionInterpreter, reexecTarget, sessionStartVerdict, } from "./sessionProbe.js";
|
|
30
30
|
import { backupHealth, backupNow, backupsDir, formatBytes, listSnapshots, previewRestore, restoreSnapshot, totalSizeBytes, } from "./backup.js";
|
|
31
31
|
import { describeTeardownWindow, readTeardowns, teardownLogPath, } from "./teardown.js";
|
|
32
|
-
import { asNameClash, buildEnvFlags, closeAgentRow, isReservedAgentName, launchAgent, LEAD_KIND, LEAD_NAME, mintLeadActorId, upsertActor, withWindowClaim, } from "./spawn.js";
|
|
32
|
+
import { asNameClash, buildEnvFlags, closeAgentRow, isReservedAgentName, launchAgent, LEAD_KIND, LEAD_NAME, mintLeadActorId, relayoutAfterPaneLeft, upsertActor, withWindowClaim, } from "./spawn.js";
|
|
33
33
|
import { adoptableWindow, applyLayout, claimInitialWindow, configureHiveWindow, controlModeFor, createWindow, DEFAULT_LAYOUT, describePaneChoice, ensureSession, findProjectWindow, foreignSocket, isViewSessionName, listOwnedWindows, liveTargets, observationFailed, observeRowOwnership, ORPHAN_MIN_AGE_MS, orphanScratchServers, orphansWorthWarningAbout, paneIsAloneInWindow, paneVisibility, panePidForRecord, PROCESSES_LAYOUT, processesPaneTitle, processesWindowName, projectWindows, RAW_ATTACH_TMUX_CONFIG, renderAttachCommand, resolveAttachTarget, resolveInTmuxTarget, rowLive, rowOwnership, SESSION_PREFIX, appendGlobalHook, globalHooks, replaceGlobalHooks, sessionName, setPaneTitle, shellQuote, shownPaneTitle, makeProcessesWindow, tmux, TMUX_DOC, tmuxSaysNothingThere, tmuxTimeoutOverride, tmuxSocketPath, untrustedTmuxServer, waitForPaneEstablished, windowLayout, } from "./tmux.js";
|
|
34
34
|
import { describeVisibility, processCounts } from "./dashboard.js";
|
|
35
35
|
import { runningCommandRow, runningCommandRows, snapshotProcesses, stopAllProcesses, stopLine, stopProcess, STOP_REASONS, } from "./processes.js";
|
|
@@ -400,7 +400,7 @@ async function cmdLead(argv) {
|
|
|
400
400
|
for (const w of warnings)
|
|
401
401
|
console.log(`! ${w}`);
|
|
402
402
|
const session = sessionName();
|
|
403
|
-
const hooksPath =
|
|
403
|
+
const hooksPath = ensureLeadHooksFile(project.id, config?.quiet_messaging === true);
|
|
404
404
|
let leadCommand = "claude";
|
|
405
405
|
if (!config) {
|
|
406
406
|
console.log("No hive.yml here; starting a plain claude lead. Add a hive.yml to define project commands and a custom lead (run hive --help for the format).");
|
|
@@ -546,7 +546,9 @@ async function cmdLead(argv) {
|
|
|
546
546
|
createdPane = true;
|
|
547
547
|
}
|
|
548
548
|
else {
|
|
549
|
-
|
|
549
|
+
const firstPane = tmux("list-panes", "-t", foundWindow, "-F", "#{pane_id}").split("\n")[0];
|
|
550
|
+
leadPane = tmux("split-window", "-b", ...(detach ? ["-d"] : []), "-P", "-F", "#{pane_id}", "-t", firstPane || foundWindow, "-c", project.path, ...envFlags, launchCommand);
|
|
551
|
+
relayoutAfterPaneLeft(foundWindow);
|
|
550
552
|
leadWindow = foundWindow;
|
|
551
553
|
createdPane = true;
|
|
552
554
|
}
|
|
@@ -601,7 +603,7 @@ async function cmdLead(argv) {
|
|
|
601
603
|
if (updated === 0)
|
|
602
604
|
return false;
|
|
603
605
|
db.prepare(`UPDATE wakes SET deliver_pane = ?, held_at = NULL, held_reason = NULL
|
|
604
|
-
WHERE ${ACTIVE_TIMER_WHERE} AND deliver_actor = ?
|
|
606
|
+
WHERE ((${ACTIVE_TIMER_WHERE}) OR (${socketAwaitingWhere()})) AND deliver_actor = ?
|
|
605
607
|
AND (? = 1 OR held_reason IS NULL OR held_reason NOT LIKE ?)`).run(leadPane, leadActorId, createdPane ? 1 : 0, `${HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX}%`);
|
|
606
608
|
confirmQueenWrite("hive lead", project.id, { detach }, {
|
|
607
609
|
agent_id: leadAgentId,
|
|
@@ -743,6 +745,8 @@ const HIVE_YML_TEMPLATE = `# hive project config. Read by \`hive lead\` from the
|
|
|
743
745
|
|
|
744
746
|
# dashboard: true # write .hive/dashboard.html (default: false)
|
|
745
747
|
|
|
748
|
+
# quiet_messaging: true # wakes reach a claude lead as one row (default: false; restart the lead)
|
|
749
|
+
|
|
746
750
|
# vars: # substituted into the profile runbook
|
|
747
751
|
# repo: owner/name # {{repo}}
|
|
748
752
|
# ticket_prefix: DEVX # sections needing it drop when it is unset
|
package/dist/dashboard.js
CHANGED
|
@@ -799,7 +799,7 @@ const STYLE = `
|
|
|
799
799
|
.muted { font-size: 0.8125rem; }
|
|
800
800
|
.cap-note { display: flex; align-items: center; gap: 0.4rem; color: var(--fg-subtle);
|
|
801
801
|
font-size: 0.78125rem; margin: 0 0 0.6rem; }
|
|
802
|
-
pre.board { white-space: pre; overflow: auto; max-height: 60vh; background: var(--panel-sunken);
|
|
802
|
+
pre.board { white-space: pre-wrap; overflow-wrap: anywhere; overflow: auto; max-height: 60vh; background: var(--panel-sunken);
|
|
803
803
|
border: 1px solid var(--border); border-radius: var(--r-ctl); padding: 0.85rem;
|
|
804
804
|
font-family: var(--font-mono); font-size: 0.8125rem; line-height: 1.5; margin: 0;
|
|
805
805
|
overscroll-behavior: contain; }
|
|
@@ -999,6 +999,7 @@ const SCRIPT = `
|
|
|
999
999
|
s.filterCaret = filterInput.selectionStart;
|
|
1000
1000
|
save(s);
|
|
1001
1001
|
applyFilter(filterInput.value);
|
|
1002
|
+
paintStamp();
|
|
1002
1003
|
deferReload();
|
|
1003
1004
|
});
|
|
1004
1005
|
}
|
|
@@ -1039,7 +1040,21 @@ const SCRIPT = `
|
|
|
1039
1040
|
window.addEventListener("resize", function () { measureHeader(); paintNav(); });
|
|
1040
1041
|
}
|
|
1041
1042
|
|
|
1042
|
-
|
|
1043
|
+
function absTop(el) { return el.getBoundingClientRect().top + window.scrollY; }
|
|
1044
|
+
var anchorEl = state.anchor && typeof state.anchor.id === "string"
|
|
1045
|
+
? document.getElementById(state.anchor.id) : null;
|
|
1046
|
+
if (anchorEl && typeof state.anchor.offset === "number") {
|
|
1047
|
+
window.scrollTo(0, absTop(anchorEl) + state.anchor.offset);
|
|
1048
|
+
} else if (typeof state.scrollY === "number") {
|
|
1049
|
+
window.scrollTo(0, state.scrollY);
|
|
1050
|
+
}
|
|
1051
|
+
var itemSel = "details.pad-item, details.todo-item";
|
|
1052
|
+
var padItems = document.querySelectorAll("details.pad-item");
|
|
1053
|
+
for (var pi = 0; pi < padItems.length; pi++) {
|
|
1054
|
+
var saved = state.padScroll && state.padScroll[padItems[pi].id];
|
|
1055
|
+
var padPre = padItems[pi].querySelector("pre.board");
|
|
1056
|
+
if (padItems[pi].open && padPre && typeof saved === "number") padPre.scrollTop = saved;
|
|
1057
|
+
}
|
|
1043
1058
|
|
|
1044
1059
|
// Live toggle. Replaces <meta http-equiv="refresh">, which the browser
|
|
1045
1060
|
// schedules at PARSE TIME - removing the tag afterward does not cancel it,
|
|
@@ -1056,39 +1071,75 @@ const SCRIPT = `
|
|
|
1056
1071
|
function clearReload() {
|
|
1057
1072
|
if (reloadTimer !== null) { clearTimeout(reloadTimer); reloadTimer = null; }
|
|
1058
1073
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1074
|
+
function anyItemOpen() {
|
|
1075
|
+
var items = document.querySelectorAll(itemSel);
|
|
1076
|
+
for (var i = 0; i < items.length; i++) {
|
|
1077
|
+
var it = items[i];
|
|
1078
|
+
if (it.open && !it.closest("details.section:not([open])") && !it.closest("[hidden]")) return true;
|
|
1079
|
+
}
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
// Typing and scrolling restart the 10s rather than suspending it, so
|
|
1083
|
+
// "refreshes every 10s" stays true; an open pad or todo suspends it.
|
|
1061
1084
|
function deferReload() {
|
|
1062
1085
|
if (!live) return;
|
|
1063
1086
|
clearReload();
|
|
1064
|
-
armReload();
|
|
1087
|
+
if (!anyItemOpen()) armReload();
|
|
1065
1088
|
}
|
|
1066
1089
|
var toggle = document.getElementById("live-toggle");
|
|
1067
1090
|
var stamp = document.getElementById("generated-stamp");
|
|
1068
1091
|
var stampTime = stamp ? stamp.getAttribute("data-time") : "";
|
|
1069
|
-
|
|
1092
|
+
var tail = " local. Read-only: nothing here writes back to the store.";
|
|
1093
|
+
function paintStamp() {
|
|
1070
1094
|
if (!stamp) return;
|
|
1071
|
-
stamp.textContent = live
|
|
1072
|
-
? "generated " + stampTime +
|
|
1073
|
-
:
|
|
1095
|
+
stamp.textContent = !live
|
|
1096
|
+
? "paused, generated " + stampTime + tail
|
|
1097
|
+
: anyItemOpen()
|
|
1098
|
+
? "paused while a pad or todo is open, generated " + stampTime + tail
|
|
1099
|
+
: "generated " + stampTime + " local, refreshes every 10s. Read-only: nothing here writes back to the store.";
|
|
1074
1100
|
}
|
|
1075
1101
|
var live = typeof state.live === "boolean" ? state.live : true;
|
|
1076
1102
|
if (toggle) toggle.checked = live;
|
|
1077
|
-
paintStamp(
|
|
1078
|
-
|
|
1103
|
+
paintStamp();
|
|
1104
|
+
deferReload();
|
|
1105
|
+
var items = document.querySelectorAll(itemSel);
|
|
1106
|
+
for (var ii = 0; ii < items.length; ii++) {
|
|
1107
|
+
items[ii].addEventListener("toggle", function () { paintStamp(); deferReload(); });
|
|
1108
|
+
}
|
|
1109
|
+
var sectionEls = document.querySelectorAll("details.section");
|
|
1110
|
+
for (var si = 0; si < sectionEls.length; si++) {
|
|
1111
|
+
sectionEls[si].addEventListener("toggle", function () { paintStamp(); deferReload(); });
|
|
1112
|
+
}
|
|
1079
1113
|
if (toggle) {
|
|
1080
1114
|
toggle.addEventListener("change", function () {
|
|
1081
1115
|
var s = load();
|
|
1082
|
-
|
|
1116
|
+
live = toggle.checked;
|
|
1117
|
+
s.live = live;
|
|
1083
1118
|
save(s);
|
|
1084
|
-
paintStamp(
|
|
1085
|
-
if (
|
|
1119
|
+
paintStamp();
|
|
1120
|
+
if (live) deferReload(); else clearReload();
|
|
1086
1121
|
});
|
|
1087
1122
|
}
|
|
1123
|
+
window.addEventListener("scroll", function () {
|
|
1124
|
+
if (reloadTimer !== null) deferReload();
|
|
1125
|
+
}, { passive: true });
|
|
1088
1126
|
|
|
1089
1127
|
window.addEventListener("beforeunload", function () {
|
|
1090
1128
|
var s = load();
|
|
1091
1129
|
s.scrollY = window.scrollY;
|
|
1130
|
+
s.anchor = null;
|
|
1131
|
+
var secs = document.querySelectorAll("details.section");
|
|
1132
|
+
for (var k = 0; k < secs.length; k++) {
|
|
1133
|
+
if (secs[k].getBoundingClientRect().top <= headerOffset) {
|
|
1134
|
+
s.anchor = { id: secs[k].id, offset: window.scrollY - absTop(secs[k]) };
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
s.padScroll = {};
|
|
1138
|
+
var openPads = document.querySelectorAll("details.pad-item");
|
|
1139
|
+
for (var m = 0; m < openPads.length; m++) {
|
|
1140
|
+
var pre = openPads[m].querySelector("pre.board");
|
|
1141
|
+
if (openPads[m].open && pre && pre.scrollTop > 0) s.padScroll[openPads[m].id] = pre.scrollTop;
|
|
1142
|
+
}
|
|
1092
1143
|
if (filterInput) {
|
|
1093
1144
|
s.filterFocused = document.activeElement === filterInput;
|
|
1094
1145
|
s.filterCaret = filterInput.selectionStart;
|
package/dist/db.js
CHANGED
|
@@ -408,6 +408,13 @@ CREATE TABLE queen_audit (
|
|
|
408
408
|
CHECK(home_project_id != target_project_id)
|
|
409
409
|
);
|
|
410
410
|
CREATE INDEX idx_queen_audit_target ON queen_audit(target_project_id, id);
|
|
411
|
+
`,
|
|
412
|
+
`
|
|
413
|
+
ALTER TABLE agents ADD COLUMN claude_messaging_socket TEXT NOT NULL DEFAULT '';
|
|
414
|
+
ALTER TABLE agents ADD COLUMN claude_messaging_pane_pid TEXT NOT NULL DEFAULT '';
|
|
415
|
+
ALTER TABLE wakes ADD COLUMN delivery_method TEXT;
|
|
416
|
+
ALTER TABLE wakes ADD COLUMN socket_attempt_at TEXT;
|
|
417
|
+
ALTER TABLE wakes ADD COLUMN socket_delivery_note TEXT;
|
|
411
418
|
`,
|
|
412
419
|
];
|
|
413
420
|
export function storeSchemaAhead(database) {
|
package/dist/hook.js
CHANGED
|
@@ -185,6 +185,21 @@ async function stopProcessesForEndedLead(actorId, payload) {
|
|
|
185
185
|
const { stopAllProcesses, STOP_REASONS } = await import("./processes.js");
|
|
186
186
|
stopAllProcesses(row.project_id, STOP_REASONS.leadSessionEnded);
|
|
187
187
|
}
|
|
188
|
+
// Bound to the pid of the pane this hook runs in, not to agents.pane_pid: on a fresh launch this can
|
|
189
|
+
// run before cmdLead records the new pane. The scheduler uses it only when the two pids agree.
|
|
190
|
+
async function registerLeadMessagingSocket(actorId) {
|
|
191
|
+
const raw = process.env.CLAUDE_CODE_MESSAGING_SOCKET ?? "";
|
|
192
|
+
const pane = process.env.TMUX_PANE ?? "";
|
|
193
|
+
let socket = isAbsolute(raw) && !raw.includes("\0") ? raw : "";
|
|
194
|
+
let pid = "";
|
|
195
|
+
if (socket !== "" && pane !== "") {
|
|
196
|
+
const { panePidForRecord } = await import("./tmux.js");
|
|
197
|
+
pid = panePidForRecord(pane);
|
|
198
|
+
}
|
|
199
|
+
if (pid === "")
|
|
200
|
+
socket = "";
|
|
201
|
+
db.prepare("UPDATE agents SET claude_messaging_socket = ?, claude_messaging_pane_pid = ? WHERE actor_id = ? AND kind = 'lead' AND status = 'running'").run(socket, pid, actorId);
|
|
202
|
+
}
|
|
188
203
|
function reconcileSessionId(actorId, payload) {
|
|
189
204
|
const sessionId = typeof payload.session_id === "string" ? payload.session_id : "";
|
|
190
205
|
if (!sessionId)
|
|
@@ -306,6 +321,13 @@ try {
|
|
|
306
321
|
catch {
|
|
307
322
|
}
|
|
308
323
|
}
|
|
324
|
+
if (event === "session_start" && process.env.HIVE_LEAD === "1") {
|
|
325
|
+
try {
|
|
326
|
+
await registerLeadMessagingSocket(actorId);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
}
|
|
330
|
+
}
|
|
309
331
|
if (event === "session_end") {
|
|
310
332
|
try {
|
|
311
333
|
await stopProcessesForEndedLead(actorId, readPayload());
|
package/dist/hooks.js
CHANGED
|
@@ -22,6 +22,20 @@ export function ensureHooksFile() {
|
|
|
22
22
|
writeFileSync(path, JSON.stringify(settings, null, 2) + "\n");
|
|
23
23
|
return path;
|
|
24
24
|
}
|
|
25
|
+
// Off returns the store-wide file unchanged. On writes a per-project file, because hooks.json is shared
|
|
26
|
+
// by every project's lead and an accept there would reach leads that never opted in.
|
|
27
|
+
export function ensureLeadHooksFile(projectId, quietMessaging) {
|
|
28
|
+
if (!quietMessaging)
|
|
29
|
+
return ensureHooksFile();
|
|
30
|
+
const base = stateHookSettings();
|
|
31
|
+
const settings = {
|
|
32
|
+
hooks: { ...base.hooks, SessionStart: [hookEntry("session_start")] },
|
|
33
|
+
crossSessionInbound: "accept",
|
|
34
|
+
};
|
|
35
|
+
const path = join(dataDir, `lead-${projectId}-hooks.json`);
|
|
36
|
+
writeFileSync(path, JSON.stringify(settings, null, 2) + "\n");
|
|
37
|
+
return path;
|
|
38
|
+
}
|
|
25
39
|
function stateHookSettings() {
|
|
26
40
|
return {
|
|
27
41
|
hooks: {
|
package/dist/projectYml.js
CHANGED
|
@@ -18,6 +18,7 @@ export const BUILT_IN_PROJECT_YML = Object.freeze({
|
|
|
18
18
|
context_checkpoint_percent: null,
|
|
19
19
|
lead_turn_budget: null,
|
|
20
20
|
dashboard: false,
|
|
21
|
+
quiet_messaging: false,
|
|
21
22
|
vars: Object.freeze({}),
|
|
22
23
|
processes: Object.freeze({}),
|
|
23
24
|
});
|
|
@@ -198,6 +199,19 @@ function applyLayer(root, path, source, config, sources, warnings) {
|
|
|
198
199
|
else
|
|
199
200
|
warn(`dashboard must be true or false; ignoring "${String(value)}".`);
|
|
200
201
|
}
|
|
202
|
+
if (has("quiet_messaging")) {
|
|
203
|
+
const value = root.quiet_messaging;
|
|
204
|
+
if (value == null) {
|
|
205
|
+
config.quiet_messaging = false;
|
|
206
|
+
sources.quiet_messaging = source;
|
|
207
|
+
}
|
|
208
|
+
else if (typeof value === "boolean") {
|
|
209
|
+
config.quiet_messaging = value;
|
|
210
|
+
sources.quiet_messaging = source;
|
|
211
|
+
}
|
|
212
|
+
else
|
|
213
|
+
warn(`quiet_messaging must be true or false; ignoring "${String(value)}".`);
|
|
214
|
+
}
|
|
201
215
|
if (has("agents")) {
|
|
202
216
|
const value = root.agents;
|
|
203
217
|
if (value == null) {
|
package/dist/queenDashboard.js
CHANGED
|
@@ -444,7 +444,7 @@ a.proj:hover { color: var(--accent); text-decoration: underline; text-underline-
|
|
|
444
444
|
.c-brief li { counter-increment: pick; font-size: 0.875rem; min-width: 0; overflow-wrap: anywhere; }
|
|
445
445
|
.c-brief li::before { content: counter(pick); font-family: var(--font-mono); font-weight: 650; color: var(--accent); margin-right: 0.4rem; }
|
|
446
446
|
.c-brief .why { display: block; color: var(--fg-muted); font-size: 0.8125rem; margin-top: 0.15rem; }
|
|
447
|
-
.brief-summary, .brief-empty { margin: 0; font-size: 0.875rem; color: var(--fg-muted); grid-column: 2; }
|
|
447
|
+
.brief-summary, .brief-empty { margin: 0; font-size: 0.875rem; color: var(--fg-muted); grid-column: 2; min-width: 0; overflow-wrap: anywhere; }
|
|
448
448
|
.brief-summary { color: var(--fg); }
|
|
449
449
|
@media (max-width: 52rem) {
|
|
450
450
|
.c-brief { grid-template-columns: minmax(0, 1fr); }
|
package/dist/scheduler.js
CHANGED
|
@@ -17,6 +17,8 @@ import { awaitingFirstPrompt, awaitingFirstPromptSql } from "./firstPrompt.js";
|
|
|
17
17
|
import { evaluateLeadWatch, leadSubscription } from "./leadWatch.js";
|
|
18
18
|
import { describeLiveTasks, describeOneTask, liveBackgroundTasks, STALL_BOUND_SECONDS, } from "./backgroundTasks.js";
|
|
19
19
|
import { MESSAGE_MAX_ROWS, MESSAGE_RETENTION } from "./leadMessage.js";
|
|
20
|
+
import { CROSS_SESSION_CLOSE, postClaudeWake, senderAddress, SOCKET_WAKE_FOOTER } from "./claudeWake.js";
|
|
21
|
+
import { readLeadTurnState } from "./leadState.js";
|
|
20
22
|
import { harnessFor, hasTranscriptSignal, paneClassifierFor, screenClassifiable, transcriptDirFor } from "./harnesses.js";
|
|
21
23
|
import { transcriptDir, readContextFill } from "./transcript.js";
|
|
22
24
|
import { ageSecondsSince, describeLastLogEvent, humanizeAge, lastLogEvent, reportsAgentStateLog, } from "./stateProvenance.js";
|
|
@@ -32,7 +34,9 @@ const DELIVER_ROW_COLUMNS = `COALESCE(agents.tmux_socket, '') AS deliver_socket,
|
|
|
32
34
|
COALESCE(agents.pane_pid, '') AS deliver_pane_pid,
|
|
33
35
|
COALESCE(agents.command, '') AS deliver_command,
|
|
34
36
|
agents.id AS deliver_row_id, agents.status AS deliver_row_status,
|
|
35
|
-
COALESCE(agents.tmux_target, '') AS deliver_row_target
|
|
37
|
+
COALESCE(agents.tmux_target, '') AS deliver_row_target,
|
|
38
|
+
COALESCE(agents.claude_messaging_socket, '') AS deliver_messaging_socket,
|
|
39
|
+
COALESCE(agents.claude_messaging_pane_pid, '') AS deliver_messaging_pane_pid`;
|
|
36
40
|
// A row's pid certifies only the pane that row records, never a different deliver_pane.
|
|
37
41
|
const deliveryIdentity = (row) => ({
|
|
38
42
|
tmux_target: row.deliver_pane,
|
|
@@ -455,18 +459,15 @@ function maybeGenerateDashboards() {
|
|
|
455
459
|
}
|
|
456
460
|
}
|
|
457
461
|
export function checkConfirmations() {
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
WHERE actor_id = wakes.deliver_actor AND event = 'prompt' AND created_at >= wakes.typed_at
|
|
461
|
-
AND (payload LIKE '%[hive wake #' || wakes.id || ']%'
|
|
462
|
-
OR payload LIKE '%[hive wake #' || wakes.id || ',%')`;
|
|
462
|
+
const sentAt = "COALESCE(wakes.socket_attempt_at, wakes.typed_at)";
|
|
463
|
+
const confirmationQuery = promptEvidenceSql(sentAt);
|
|
463
464
|
try {
|
|
464
465
|
const pending = stmt(`SELECT 1 AS hit FROM wakes
|
|
465
|
-
WHERE
|
|
466
|
+
WHERE ${sentAt} IS NOT NULL AND confirmed_at IS NULL AND ${sentAt} >= datetime('now', ?) LIMIT 1`).get(LOG_RETENTION);
|
|
466
467
|
if (!pending)
|
|
467
468
|
return;
|
|
468
469
|
stmt(`UPDATE wakes SET confirmed_at = (${confirmationQuery})
|
|
469
|
-
WHERE
|
|
470
|
+
WHERE ${sentAt} IS NOT NULL AND confirmed_at IS NULL AND ${sentAt} >= datetime('now', ?)
|
|
470
471
|
AND EXISTS (${confirmationQuery.replace("MIN(created_at)", "1")})`).run(LOG_RETENTION);
|
|
471
472
|
}
|
|
472
473
|
catch {
|
|
@@ -511,6 +512,7 @@ export async function tick(snapshot) {
|
|
|
511
512
|
else
|
|
512
513
|
await maybeFireIdle(timer, snapshot, now, choices);
|
|
513
514
|
}
|
|
515
|
+
await retryUnconfirmedSocketWakes(snapshot, choices);
|
|
514
516
|
}
|
|
515
517
|
catch {
|
|
516
518
|
}
|
|
@@ -570,7 +572,8 @@ function holdTimer(timer, reason) {
|
|
|
570
572
|
bestEffortRun(`UPDATE wakes SET held_at = datetime('now'), held_reason = ?,
|
|
571
573
|
first_held_at = COALESCE(CASE WHEN held_at IS NULL THEN NULL ELSE first_held_at END, datetime('now'))
|
|
572
574
|
WHERE id = ? AND cancelled_at IS NULL
|
|
573
|
-
AND (fired_at IS NULL OR (repeat_every_ms IS NOT NULL AND due_at = ?)
|
|
575
|
+
AND (fired_at IS NULL OR (repeat_every_ms IS NOT NULL AND due_at = ?)
|
|
576
|
+
OR (${socketAwaitingWhere()}))`, reason, timer.id, timer.due_at);
|
|
574
577
|
}
|
|
575
578
|
const HELD_REASON_MODAL_CHOICE = "pane is awaiting a modal choice (folder-trust or /model picker)";
|
|
576
579
|
export const HELD_REASON_LEAD_PANE_DEAD = "the lead's pane is not live right now (likely mid-restart); lead-owned wakes are exempt from " +
|
|
@@ -931,7 +934,7 @@ const STANDING_EXPIRED_NOTE = "this standing watch has expired and nothing is wa
|
|
|
931
934
|
const unreported = (condition, episode) => `NOT EXISTS (
|
|
932
935
|
SELECT 1 FROM wake_idle_notices n LEFT JOIN wakes nt ON nt.id = n.notice_wake_id
|
|
933
936
|
WHERE n.wake_id = ? AND n.agent_id = a.id AND n.condition = '${condition}' AND n.episode = ${episode}
|
|
934
|
-
AND NOT (nt.fired_at IS NOT NULL AND nt.typed_at IS NULL AND nt.cancelled_at IS NULL
|
|
937
|
+
AND NOT (nt.fired_at IS NOT NULL AND COALESCE(nt.typed_at, nt.socket_attempt_at) IS NULL AND nt.cancelled_at IS NULL
|
|
935
938
|
AND nt.fired_at < datetime('now', '${NOTICE_RETRY_AFTER}')))`;
|
|
936
939
|
const CREW_COLUMNS = `a.id, a.name, a.actor_id, a.tmux_target, a.tmux_socket, a.pane_pid, a.agent_state,
|
|
937
940
|
a.state_changed_at, a.status, a.command, a.kind, a.resumed_at, a.closed_at,
|
|
@@ -1174,7 +1177,7 @@ function rearmSpentEpisode(timerId, agentId, condition, episode) {
|
|
|
1174
1177
|
WHERE wake_id = ? AND agent_id = ? AND condition = ? AND episode = ?
|
|
1175
1178
|
AND notice_wake_id IS NOT NULL
|
|
1176
1179
|
AND EXISTS (SELECT 1 FROM wakes t WHERE t.id = wake_idle_notices.notice_wake_id
|
|
1177
|
-
AND t.fired_at IS NOT NULL AND t.typed_at IS NULL AND t.cancelled_at IS NULL
|
|
1180
|
+
AND t.fired_at IS NOT NULL AND COALESCE(t.typed_at, t.socket_attempt_at) IS NULL AND t.cancelled_at IS NULL
|
|
1178
1181
|
AND t.fired_at < datetime('now', '${NOTICE_RETRY_AFTER}'))`).run(timerId, agentId, condition, episode);
|
|
1179
1182
|
}
|
|
1180
1183
|
function claimEpisode(timerId, agentId, condition, episode) {
|
|
@@ -1892,10 +1895,69 @@ function recordLostAfterClaim(timer, pasted, why) {
|
|
|
1892
1895
|
cancelled_at = CASE WHEN repeat_every_ms IS NULL THEN datetime('now') ELSE cancelled_at END
|
|
1893
1896
|
WHERE id = ?`, HELD_REASON_OWNERSHIP_LOST_AFTER_CLAIM + why + (pasted ? " The body was pasted; no Enter was sent." : ""), pasted ? 1 : 0, timer.id);
|
|
1894
1897
|
}
|
|
1895
|
-
|
|
1898
|
+
export const DELIVERY_METHOD = { socket: "socket", pty: "pty", ptyAfterSocketTimeout: "pty-after-socket-timeout" };
|
|
1899
|
+
export const socketAwaitingWhere = (prefix = "") => `${prefix}delivery_method = '${DELIVERY_METHOD.socket}' AND ${prefix}confirmed_at IS NULL AND ${prefix}typed_at IS NULL AND ${prefix}cancelled_at IS NULL`;
|
|
1900
|
+
const SOCKET_CONFIRM_GRACE_SECONDS = 60;
|
|
1901
|
+
const withReDelivered = (note) => (note ? `${note}, re-delivered` : "re-delivered");
|
|
1902
|
+
const promptEvidenceSql = (lowerBound) => `
|
|
1903
|
+
SELECT MIN(created_at) FROM agent_state_log
|
|
1904
|
+
WHERE actor_id = wakes.deliver_actor AND event = 'prompt' AND created_at >= ${lowerBound}
|
|
1905
|
+
AND (payload LIKE '%[hive wake #' || wakes.id || ']%'
|
|
1906
|
+
OR payload LIKE '%[hive wake #' || wakes.id || ',%')`;
|
|
1907
|
+
function quietMessagingOn(projectId) {
|
|
1908
|
+
const project = stmt("SELECT path FROM projects WHERE id = ?").get(projectId);
|
|
1909
|
+
return project !== undefined && loadProjectYml(project.path).config?.quiet_messaging === true;
|
|
1910
|
+
}
|
|
1911
|
+
// Only a one-shot wake to a live, row-owned Claude lead whose socket was registered by this pane epoch.
|
|
1912
|
+
function socketRoute(timer, owner) {
|
|
1913
|
+
return (owner !== null &&
|
|
1914
|
+
timer.repeat_every_ms == null &&
|
|
1915
|
+
isLeadActorId(timer.deliver_actor) &&
|
|
1916
|
+
harnessFor(timer.deliver_command).name === "claude" &&
|
|
1917
|
+
timer.deliver_messaging_socket !== "" &&
|
|
1918
|
+
timer.deliver_messaging_pane_pid !== "" &&
|
|
1919
|
+
timer.deliver_messaging_pane_pid === timer.deliver_pane_pid &&
|
|
1920
|
+
timer.deliver_row_target === timer.deliver_pane &&
|
|
1921
|
+
quietMessagingOn(timer.project_id));
|
|
1922
|
+
}
|
|
1923
|
+
function socketStillRegistered(timer, owner) {
|
|
1924
|
+
requireStillOwned(owner.rowId, owner.identity, `wake #${timer.id} socket delivery`);
|
|
1925
|
+
const row = stmt(`SELECT claude_messaging_socket AS socket, claude_messaging_pane_pid AS pid, pane_pid FROM agents
|
|
1926
|
+
WHERE id = ? AND status = 'running' AND kind = 'lead'`).get(owner.rowId);
|
|
1927
|
+
const wake = stmt("SELECT 1 AS live FROM wakes WHERE id = ? AND cancelled_at IS NULL").get(timer.id);
|
|
1928
|
+
return (wake !== undefined &&
|
|
1929
|
+
row !== undefined &&
|
|
1930
|
+
row.socket === timer.deliver_messaging_socket &&
|
|
1931
|
+
row.pid === timer.deliver_messaging_pane_pid &&
|
|
1932
|
+
row.pid === row.pane_pid);
|
|
1933
|
+
}
|
|
1934
|
+
async function deliver(timer, note, choices, typedSeen, firstHeldAt, owner, fallback = false) {
|
|
1896
1935
|
const tail = watchedTail(timer);
|
|
1897
|
-
const prefix = `[hive wake #${timer.id}${note ? `, ${note}` : ""}] `;
|
|
1898
1936
|
const body = (isLeadActorId(timer.deliver_actor) ? shortRenderForLeadDelivery(timer) : null) ?? timer.body;
|
|
1937
|
+
const staleness = noticeStalenessNote(timer);
|
|
1938
|
+
if (!fallback && socketRoute(timer, owner)) {
|
|
1939
|
+
const text = `[hive wake #${timer.id}${note ? `, ${note}` : ""}] ${body + staleness + tail}\n\n${SOCKET_WAKE_FOOTER}`;
|
|
1940
|
+
if (!text.includes(CROSS_SESSION_CLOSE)) {
|
|
1941
|
+
bestEffortRun(`UPDATE wakes SET socket_attempt_at = strftime('%Y-%m-%d %H:%M:%f', 'now'), socket_delivery_note = ?,
|
|
1942
|
+
delivery_method = ?, first_held_at = ?, held_at = NULL, held_reason = NULL, confirmed_at = NULL
|
|
1943
|
+
WHERE id = ?`, note, DELIVERY_METHOD.socket, firstHeldAt, timer.id);
|
|
1944
|
+
const posted = await postClaudeWake({
|
|
1945
|
+
socketPath: timer.deliver_messaging_socket,
|
|
1946
|
+
senderAddress: senderAddress(join(dataDir, "wake-sender.sock")),
|
|
1947
|
+
text,
|
|
1948
|
+
beforeWrite: () => socketStillRegistered(timer, owner),
|
|
1949
|
+
});
|
|
1950
|
+
if (posted) {
|
|
1951
|
+
forgetPaneAnswers(timer.deliver_pane, choices);
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
note = withReDelivered(note);
|
|
1955
|
+
fallback = true;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
const prefix = `[hive wake #${timer.id}${note ? `, ${note}` : ""}] ` +
|
|
1959
|
+
(fallback ? `If you already handled wake #${timer.id}, ignore this.\n` : "");
|
|
1960
|
+
const method = fallback ? DELIVERY_METHOD.ptyAfterSocketTimeout : DELIVERY_METHOD.pty;
|
|
1899
1961
|
let typedBusy;
|
|
1900
1962
|
try {
|
|
1901
1963
|
const lastEvent = lastLogEvent(timer.deliver_actor);
|
|
@@ -1905,19 +1967,23 @@ async function deliver(timer, note, choices, typedSeen, firstHeldAt, owner) {
|
|
|
1905
1967
|
typedBusy = null;
|
|
1906
1968
|
}
|
|
1907
1969
|
const recordTyped = () => bestEffortRun(`UPDATE wakes SET typed_at = strftime('%Y-%m-%d %H:%M:%f', 'now'), typed_busy = ?,
|
|
1908
|
-
typed_seen = ?, first_held_at = ?, held_at = NULL, held_reason = NULL,
|
|
1909
|
-
|
|
1970
|
+
typed_seen = ?, first_held_at = ?, held_at = NULL, held_reason = NULL, delivery_method = ?,
|
|
1971
|
+
confirmed_at = CASE WHEN socket_attempt_at IS NULL THEN NULL ELSE confirmed_at END
|
|
1972
|
+
WHERE id = ?`, typedBusy, typedSeen, firstHeldAt, method, timer.id);
|
|
1910
1973
|
const box = cacheEntry(timer.deliver_pane, choices).box;
|
|
1911
1974
|
const strandedTextWouldHold = box !== undefined && box !== null && box.state !== "unknown";
|
|
1912
1975
|
let pasted = false;
|
|
1913
1976
|
try {
|
|
1914
|
-
await sendText(timer.deliver_pane, prefix + body +
|
|
1977
|
+
await sendText(timer.deliver_pane, prefix + body + staleness + tail, true, () => {
|
|
1915
1978
|
pasted = true;
|
|
1916
1979
|
if (strandedTextWouldHold)
|
|
1917
1980
|
recordTyped();
|
|
1918
1981
|
}, undefined, owner ? () => requireStillOwned(owner.rowId, owner.identity, `wake #${timer.id} delivery`) : undefined);
|
|
1919
1982
|
}
|
|
1920
1983
|
catch (err) {
|
|
1984
|
+
if (fallback && !pasted) {
|
|
1985
|
+
bestEffortRun("UPDATE wakes SET delivery_method = ? WHERE id = ? AND typed_at IS NULL", DELIVERY_METHOD.socket, timer.id);
|
|
1986
|
+
}
|
|
1921
1987
|
if (!(err instanceof PaneOwnershipLost))
|
|
1922
1988
|
throw err;
|
|
1923
1989
|
recordLostAfterClaim(timer, pasted, err.message);
|
|
@@ -1929,6 +1995,41 @@ async function deliver(timer, note, choices, typedSeen, firstHeldAt, owner) {
|
|
|
1929
1995
|
if (!strandedTextWouldHold)
|
|
1930
1996
|
recordTyped();
|
|
1931
1997
|
}
|
|
1998
|
+
// Measured from the lead's first turn end after the post, because a busy lead reads the post only then.
|
|
1999
|
+
function socketFallbackDue(timer) {
|
|
2000
|
+
const firstStop = stmt("SELECT MIN(created_at) AS t FROM agent_state_log WHERE actor_id = ? AND event = 'stop' AND created_at >= ?").get(timer.deliver_actor, timer.socket_attempt_at).t;
|
|
2001
|
+
let from = firstStop;
|
|
2002
|
+
if (from === null) {
|
|
2003
|
+
const turn = timer.deliver_row_id === null ? null : readLeadTurnState(timer.deliver_row_id);
|
|
2004
|
+
if (turn !== null && turn.state === "working" && turn.pane_pid === timer.deliver_pane_pid)
|
|
2005
|
+
return false;
|
|
2006
|
+
from = timer.socket_attempt_at;
|
|
2007
|
+
}
|
|
2008
|
+
return stmt("SELECT (julianday('now') - julianday(?)) * 86400 >= ? AS due").get(from, SOCKET_CONFIRM_GRACE_SECONDS).due === 1;
|
|
2009
|
+
}
|
|
2010
|
+
async function fallbackClaimed(timer, snapshot, choices) {
|
|
2011
|
+
const decision = deliverable(timer, snapshot, choices);
|
|
2012
|
+
if (!decision.ok)
|
|
2013
|
+
return false;
|
|
2014
|
+
const claimed = stmt(`UPDATE wakes SET delivery_method = ?
|
|
2015
|
+
WHERE id = ? AND ${socketAwaitingWhere()}
|
|
2016
|
+
AND NOT EXISTS (${promptEvidenceSql("wakes.socket_attempt_at").replace("MIN(created_at)", "1")})`).run(DELIVERY_METHOD.ptyAfterSocketTimeout, timer.id).changes === 1;
|
|
2017
|
+
if (!claimed)
|
|
2018
|
+
return false;
|
|
2019
|
+
await deliver(timer, withReDelivered(timer.socket_delivery_note), choices, decision.typedSeen, decision.firstHeldAt, decision.owner, true);
|
|
2020
|
+
return true;
|
|
2021
|
+
}
|
|
2022
|
+
async function retryUnconfirmedSocketWakes(snapshot, choices) {
|
|
2023
|
+
const awaiting = stmt(`SELECT wakes.*, ${DELIVER_ROW_COLUMNS}
|
|
2024
|
+
FROM wakes ${DELIVER_SOCKET_JOIN}
|
|
2025
|
+
WHERE ${socketAwaitingWhere("wakes.")}
|
|
2026
|
+
AND wakes.socket_attempt_at >= datetime('now', ?)`).all(LOG_RETENTION);
|
|
2027
|
+
for (const timer of awaiting) {
|
|
2028
|
+
if (!socketFallbackDue(timer))
|
|
2029
|
+
continue;
|
|
2030
|
+
await withPaneClaim(timer, () => fallbackClaimed(timer, snapshot, choices));
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
1932
2033
|
export const DEFAULT_SCHEDULER_INTERVAL_MS = 3000;
|
|
1933
2034
|
const MIN_SCHEDULER_INTERVAL_MS = 100;
|
|
1934
2035
|
// Node clamps a setInterval delay above this to 1ms.
|
package/dist/tools/wakes.js
CHANGED
|
@@ -102,7 +102,7 @@ function deliveryState(t, hasChannel) {
|
|
|
102
102
|
held_at: t.held_at,
|
|
103
103
|
held_reason: t.held_reason,
|
|
104
104
|
confirmed_at: t.confirmed_at,
|
|
105
|
-
confirmation: t.typed_at == null
|
|
105
|
+
confirmation: t.typed_at == null && t.socket_attempt_at == null
|
|
106
106
|
? null
|
|
107
107
|
: t.confirmed_at != null
|
|
108
108
|
? "confirmed"
|
|
@@ -111,6 +111,8 @@ function deliveryState(t, hasChannel) {
|
|
|
111
111
|
: t.typed_busy === 1
|
|
112
112
|
? "unconfirmed_busy"
|
|
113
113
|
: "unconfirmed",
|
|
114
|
+
delivery_method: t.delivery_method,
|
|
115
|
+
socket_attempt_at: t.socket_attempt_at,
|
|
114
116
|
};
|
|
115
117
|
}
|
|
116
118
|
export const truncateBody = (body) => body.length > 120 ? `${cutToUnitBudget(body, 120)}…` : body;
|
|
@@ -484,7 +486,8 @@ export function registerWakes(server) {
|
|
|
484
486
|
server.registerTool("wake_get", {
|
|
485
487
|
description: "Read one wake-up by id, in this project, with its UNTRUNCATED body. wake_list truncates " +
|
|
486
488
|
"body at 120 chars; use this to see exactly what a wake will say, or to confirm what " +
|
|
487
|
-
"wake_update just changed."
|
|
489
|
+
"wake_update just changed. Its delivery state includes delivery_method (socket, pty or " +
|
|
490
|
+
"pty-after-socket-timeout) and socket_attempt_at, set when a wake was posted to a Claude lead's socket.",
|
|
488
491
|
annotations: {
|
|
489
492
|
readOnlyHint: false,
|
|
490
493
|
destructiveHint: false,
|
|
@@ -623,7 +626,7 @@ export function registerWakes(server) {
|
|
|
623
626
|
server.registerTool("wake_list", {
|
|
624
627
|
description: "List pending wake-ups in this project, plus recently_delivered: the last " +
|
|
625
628
|
`${RECENTLY_FIRED_LIMIT} one-shot wakes that have already fired, with their delivery ` +
|
|
626
|
-
"state (typed_at, held_at/held_reason, confirmation). A one-shot wake leaves the " +
|
|
629
|
+
"state (typed_at, held_at/held_reason, confirmation, delivery_method, socket_attempt_at). A one-shot wake leaves the " +
|
|
627
630
|
"pending list the moment it fires; recently_delivered is where to check whether it " +
|
|
628
631
|
"was actually typed and, if its target has a confirmation channel, acknowledged.",
|
|
629
632
|
annotations: {
|
package/package.json
CHANGED