@cmgmyr/hive 1.8.0 → 1.9.1
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 +2 -2
- package/dist/build-info.json +1 -1
- package/dist/claudeWake.js +44 -0
- package/dist/cli.js +11 -7
- package/dist/dashboard.js +65 -14
- package/dist/db.js +20 -0
- package/dist/harnesses.js +2 -2
- package/dist/help.js +7 -3
- package/dist/hook.js +22 -0
- package/dist/hooks.js +14 -0
- package/dist/leadMessage.js +29 -8
- package/dist/leadMessageDelivery.js +379 -0
- package/dist/projectYml.js +14 -0
- package/dist/queenDashboard.js +1 -1
- package/dist/scheduler.js +257 -33
- package/dist/tmux.js +42 -7
- package/dist/tools/agents.js +56 -4
- package/dist/tools/wakes.js +26 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Run a crew of Claude Code and Codex workers from one lead session: visible tmux
|
|
|
5
5
|
|
|
6
6
|
- Visible workers: each one is a real tmux pane you can read and type into.
|
|
7
7
|
- Exact state: workers report it through their own CLI's hooks, so nothing polls.
|
|
8
|
-
- One local SQLite store: no daemon, and
|
|
8
|
+
- One local SQLite store: no daemon. The MCP server, hooks, and scheduler make no outbound requests; the CLI contacts npm only to check for updates, and upgrades can download ([details](docs/security.md)).
|
|
9
9
|
|
|
10
10
|

|
|
11
11
|
|
|
@@ -32,7 +32,7 @@ Run `cd ~/Code/your-project && hive`. It is shorthand for `hive lead`, and it op
|
|
|
32
32
|
|
|
33
33
|
## How it works
|
|
34
34
|
|
|
35
|
-
Each Claude Code session runs its own `hive` MCP server over stdio, and every instance reads and writes one SQLite database (WAL mode) at `~/.hive/hive.db`, so every session sees the same state. There is no daemon and
|
|
35
|
+
Each Claude Code session runs its own `hive` MCP server over stdio, and every instance reads and writes one SQLite database (WAL mode) at `~/.hive/hive.db`, so every session sees the same state. There is no daemon. The MCP server, hooks, and scheduler make no outbound requests; the CLI contacts npm only to check for updates, and upgrades can download ([details](docs/security.md)). State is scoped to a project (a directory), resolved from the working directory; a lead spawns workers into tmux panes locked to that project.
|
|
36
36
|
|
|
37
37
|
## Why not subagents?
|
|
38
38
|
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.
|
|
1
|
+
{"version":"1.9.1","sha":"2056b89","dirty":false,"build_id":"6b9bbe7ee1cda1fc2700518664c980aac9692d27caead18970553a3f6b3cbae9"}
|
|
@@ -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 and worker messages 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
|
|
@@ -3372,7 +3376,7 @@ function cmdPad(argv) {
|
|
|
3372
3376
|
process.stdout.write(withTrailingNewline(pad.content));
|
|
3373
3377
|
}
|
|
3374
3378
|
process.stdout.on("error", (error) => {
|
|
3375
|
-
if (error.code === "EPIPE")
|
|
3379
|
+
if (error.code === "EPIPE" || error.code === "ENOTCONN")
|
|
3376
3380
|
process.exit(0);
|
|
3377
3381
|
throw error;
|
|
3378
3382
|
});
|
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,26 @@ 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;
|
|
418
|
+
`,
|
|
419
|
+
`ALTER TABLE wakes ADD COLUMN delivered_by TEXT;`,
|
|
420
|
+
`
|
|
421
|
+
ALTER TABLE agent_messages ADD COLUMN delivery_status TEXT;
|
|
422
|
+
ALTER TABLE agent_messages ADD COLUMN delivery_method TEXT;
|
|
423
|
+
ALTER TABLE agent_messages ADD COLUMN socket_attempt_at TEXT;
|
|
424
|
+
ALTER TABLE agent_messages ADD COLUMN confirmed_at TEXT;
|
|
425
|
+
ALTER TABLE agent_messages ADD COLUMN fallback_claimed_at TEXT;
|
|
426
|
+
ALTER TABLE agent_messages ADD COLUMN typed_at TEXT;
|
|
427
|
+
ALTER TABLE agent_messages ADD COLUMN delivery_note TEXT;
|
|
428
|
+
ALTER TABLE agent_messages ADD COLUMN target_identity TEXT;
|
|
429
|
+
ALTER TABLE agent_messages ADD COLUMN sender_tag TEXT;
|
|
430
|
+
CREATE INDEX idx_agent_messages_delivery ON agent_messages(delivery_status, id) WHERE delivery_status IS NOT NULL;
|
|
411
431
|
`,
|
|
412
432
|
];
|
|
413
433
|
export function storeSchemaAhead(database) {
|
package/dist/harnesses.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { claudeTrailerView, codexInputBoxState, codexTrailerView, codexPaneChoiceCheck, codexPaneHasInputBox, inputBoxState, paneChoiceCheck, paneHasInputBox, } from "./tmux.js";
|
|
1
|
+
import { claudeTrailerView, codexInputBoxState, codexTrailerView, codexPaneChoiceCheck, codexPaneHasInputBox, inputBoxSnapshot, inputBoxState, paneChoiceCheck, paneHasInputBox, } from "./tmux.js";
|
|
2
2
|
export function commandHead(command) {
|
|
3
3
|
return command.trim().split(/\s+/)[0] ?? "";
|
|
4
4
|
}
|
|
@@ -52,7 +52,7 @@ const claudeHarness = {
|
|
|
52
52
|
supportsResume: true,
|
|
53
53
|
supportsRename: true,
|
|
54
54
|
classifiesPaneScreen: true,
|
|
55
|
-
paneClassifier: { choiceCheck: paneChoiceCheck, inputBoxState, hasInputBox: paneHasInputBox, trailerView: claudeTrailerView },
|
|
55
|
+
paneClassifier: { choiceCheck: paneChoiceCheck, inputBoxState, inputBoxSnapshot, hasInputBox: paneHasInputBox, trailerView: claudeTrailerView },
|
|
56
56
|
hasScopes: true,
|
|
57
57
|
needsHome: false,
|
|
58
58
|
// "--" so a message that starts with a dash is never read as a flag.
|
package/dist/help.js
CHANGED
|
@@ -148,9 +148,13 @@ runbook (fork it first), not a pad write.`,
|
|
|
148
148
|
Text is prefixed with the sender tag.
|
|
149
149
|
Text over 300 chars sent to a LEAD by anyone but that lead is stored and
|
|
150
150
|
delivered as a one-line pointer, so the human's own pane stays readable;
|
|
151
|
-
the receipt says shortened and names the message id.
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
the receipt says shortened and names the message id. With quiet_messaging
|
|
152
|
+
on, a worker's submitted text to a Claude lead is stored with an id and
|
|
153
|
+
posted to the lead's messaging socket instead; the receipt says pending
|
|
154
|
+
until the lead's prompt confirms it, and hive types it into the pane once
|
|
155
|
+
if nothing does. Do not resend a pending send.
|
|
156
|
+
agent_message_get(message_id) — the full text behind a pointer line, plus
|
|
157
|
+
the delivery state of a quiet message. Kept 7 days; a lookup for a pruned id says it expired, not that it is missing.
|
|
154
158
|
agent_output(name|agent_id, lines?) — read the rendered terminal.
|
|
155
159
|
agent_status(name|agent_id, include_brief?) — liveness, current command,
|
|
156
160
|
short tail, and the path to the brief this worker was given
|
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/leadMessage.js
CHANGED
|
@@ -14,6 +14,17 @@ export const MESSAGE_RETENTION = "-7 days";
|
|
|
14
14
|
// direction. Far above any observed rate, and lower than agent_state_log's 20,000 because a row here
|
|
15
15
|
// holds unbounded message text rather than a hook payload.
|
|
16
16
|
export const MESSAGE_MAX_ROWS = 5_000;
|
|
17
|
+
export const DELIVERY_FIELDS = [
|
|
18
|
+
"delivery_status",
|
|
19
|
+
"delivery_method",
|
|
20
|
+
"socket_attempt_at",
|
|
21
|
+
"confirmed_at",
|
|
22
|
+
"fallback_claimed_at",
|
|
23
|
+
"typed_at",
|
|
24
|
+
"delivery_note",
|
|
25
|
+
"target_identity",
|
|
26
|
+
"sender_tag",
|
|
27
|
+
];
|
|
17
28
|
function senderName(projectId, actorId) {
|
|
18
29
|
const row = db
|
|
19
30
|
.prepare(`SELECT name FROM agents WHERE project_id = ? AND actor_id = ?
|
|
@@ -44,7 +55,7 @@ export function storeLeadMessage(projectId, fromActor, toAgentId, text) {
|
|
|
44
55
|
}
|
|
45
56
|
export function readLeadMessage(projectId, messageId) {
|
|
46
57
|
return db
|
|
47
|
-
.prepare(`SELECT id, from_actor, from_name, to_agent_id, text, created_at
|
|
58
|
+
.prepare(`SELECT id, from_actor, from_name, to_agent_id, text, created_at, ${DELIVERY_FIELDS.join(", ")}
|
|
48
59
|
FROM agent_messages WHERE id = ? AND project_id = ?`)
|
|
49
60
|
.get(messageId, projectId);
|
|
50
61
|
}
|
|
@@ -75,17 +86,27 @@ export function missMessage(messageId, kind) {
|
|
|
75
86
|
// also covers format characters. The head comes from outside, and a raw control byte in it reaches tmux
|
|
76
87
|
// as a keystroke rather than as text, submitting this pointer early and splitting it. The sender's name
|
|
77
88
|
// is flattened by senderTag when it builds the tag ahead of this marker - the tag is who, this is what.
|
|
78
|
-
export function leadPointerMarker(id, text) {
|
|
79
|
-
return `[message #${id}, ${text.length} chars]`;
|
|
89
|
+
export function leadPointerMarker(id, text, redelivered = false) {
|
|
90
|
+
return `[message #${id}, ${text.length} chars${redelivered ? ", re-delivered" : ""}]`;
|
|
80
91
|
}
|
|
81
|
-
|
|
92
|
+
const redeliveredLead = (id, redelivered) => redelivered ? ` If you already handled message #${id}, ignore this.` : "";
|
|
93
|
+
export function renderLeadPointer(id, text, tag, redelivered = false) {
|
|
82
94
|
const flat = flatten(text);
|
|
83
95
|
const head = flat.length > HEAD_BUDGET ? cutToUnitBudget(flat, HEAD_BUDGET) + ELLIPSIS : flat;
|
|
84
|
-
return `${tag}${leadPointerMarker(id, text)} ${head} agent_message_get(${id}) for the full text.`;
|
|
96
|
+
return `${tag}${leadPointerMarker(id, text, redelivered)}${redeliveredLead(id, redelivered)} ${head} agent_message_get(${id}) for the full text.`;
|
|
85
97
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
98
|
+
// A short message carries its id too, so a quiet send has a marker its confirmation can match.
|
|
99
|
+
export function renderLeadMessage(id, text, tag, redelivered = false) {
|
|
100
|
+
return text.length > LEAD_MESSAGE_THRESHOLD
|
|
101
|
+
? renderLeadPointer(id, text, tag, redelivered)
|
|
102
|
+
: `${tag}${leadPointerMarker(id, text, redelivered)}${redeliveredLead(id, redelivered)} ${text}`;
|
|
103
|
+
}
|
|
104
|
+
export function shortenedSendNote(id, deliveredChars, channel = "pane") {
|
|
105
|
+
return (`Over ${LEAD_MESSAGE_THRESHOLD} characters to a LEAD, so ` +
|
|
106
|
+
(channel === "pane"
|
|
107
|
+
? `that pane got a ${deliveredChars}-character pointer instead of this text`
|
|
108
|
+
: `hive delivers a ${deliveredChars}-character pointer instead of this text, over the lead's messaging socket or, failing that, typed once into its pane`) +
|
|
109
|
+
`: a lead's pane is a human's own window and hive keeps it quiet. Nothing ` +
|
|
89
110
|
`was lost - the full text is stored as message ${id} and the pointer names agent_message_get(${id}) - ` +
|
|
90
111
|
`but the lead reads the rest only if it chooses to. Put whatever must be ACTED on in the first ` +
|
|
91
112
|
`${HEAD_BUDGET} characters, or on the todo, where it is durable. Worker-bound sends are never shortened.`);
|