@cmgmyr/hive 1.11.6 → 1.11.7
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 +52 -4
- package/dist/cli.js +66 -41
- package/dist/crew.js +4 -3
- package/dist/dashboard.js +6 -5
- package/dist/etime.js +37 -0
- package/dist/firstPrompt.js +6 -0
- package/dist/heldLabel.js +1 -1
- package/dist/leadHandoff.js +12 -11
- package/dist/leadHandoffCli.js +22 -1
- package/dist/leadHandoffState.js +1 -2
- package/dist/leadMessage.js +13 -0
- package/dist/leadMessageDelivery.js +14 -74
- package/dist/leadState.js +3 -2
- package/dist/leaseStore.js +54 -0
- package/dist/portfolio.js +5 -2
- package/dist/ptys.js +2 -9
- package/dist/queenDashboard.js +2 -1
- package/dist/scheduler.js +40 -166
- package/dist/staleServers.js +4 -28
- package/dist/stateProvenance.js +1 -3
- package/dist/storeClock.js +4 -0
- package/dist/teardown.js +2 -1
- package/dist/tools/leases.js +5 -25
- package/dist/tools/wakes.js +7 -15
- package/dist/wakeSql.js +135 -1
- package/package.json +1 -1
|
@@ -1,52 +1,22 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
1
|
import { dataDir, db } from "./db.js";
|
|
3
|
-
import { CROSS_SESSION_CLOSE, postClaudeWake,
|
|
2
|
+
import { CROSS_SESSION_CLOSE, promptMarkerEvidenceSql, postClaudeWake, quietMessagingOn, socketGraceDue, socketSenderAddress, SOCKET_MESSAGE_FOOTER, } from "./claudeWake.js";
|
|
4
3
|
import { harnessFor, paneClassifierFor } from "./harnesses.js";
|
|
5
|
-
import { LEAD_MESSAGE_THRESHOLD, renderLeadMessage, senderTag, shortenedSendNote, storeLeadMessage, } from "./leadMessage.js";
|
|
4
|
+
import { LEAD_MESSAGE_THRESHOLD, MESSAGE_METHOD, MESSAGE_RETENTION, MESSAGE_STATUS, renderLeadMessage, senderTag, shortenedSendNote, storeLeadMessage, } from "./leadMessage.js";
|
|
6
5
|
import { readHandoffGate } from "./leadHandoffState.js";
|
|
7
6
|
import { HANDED_OFF_STATES_SQL, readLeadTurnState } from "./leadState.js";
|
|
8
|
-
import { loadProjectYml } from "./projectYml.js";
|
|
9
7
|
import { currentOwnership, PaneOwnershipLost, paneIdentity, requireStillOwned } from "./spawn.js";
|
|
8
|
+
import { PANE_LEASE_SETTLE_MS, releasePaneLease, tryAcquirePaneLease } from "./leaseStore.js";
|
|
10
9
|
import { holdsHumanInput, paneInCopyMode, sendText, sleep, TmuxTimeoutError, } from "./tmux.js";
|
|
11
|
-
export const SOCKET_MESSAGE_FOOTER = "Automated hive worker message. Do not reply to this socket sender.";
|
|
12
|
-
export const MESSAGE_STATUS = {
|
|
13
|
-
socketPending: "socket-pending",
|
|
14
|
-
fallbackPending: "fallback-pending",
|
|
15
|
-
fallbackClaimed: "fallback-claimed",
|
|
16
|
-
complete: "complete",
|
|
17
|
-
failed: "failed",
|
|
18
|
-
};
|
|
19
|
-
export const MESSAGE_METHOD = {
|
|
20
|
-
socket: "socket",
|
|
21
|
-
pty: "pty",
|
|
22
|
-
ptyAfterSocketTimeout: "pty-after-socket-timeout",
|
|
23
|
-
};
|
|
24
|
-
const PANE_LEASE_TTL_SECONDS = 6;
|
|
25
|
-
const PANE_LEASE_SETTLE_MS = 500;
|
|
26
|
-
const CONFIRM_GRACE_SECONDS = 60;
|
|
27
|
-
const ENVELOPE_OPEN = '<cross-session-message from="';
|
|
28
|
-
const ENVELOPE_SENDER_CLOSE = '" from-name="hive">';
|
|
29
|
-
const EV_PROMPT = "(CASE WHEN json_valid(ev.payload) THEN COALESCE(json_extract(ev.payload, '$.prompt'), '') ELSE ev.payload END)";
|
|
30
10
|
const EV_MARKER = "(agent_messages.sender_tag || '[message #' || agent_messages.id || ',')";
|
|
31
|
-
const
|
|
11
|
+
const messageMarkerAt = (promptSql, startSql) => `substr(${promptSql}, ${startSql}, length(${EV_MARKER})) = ${EV_MARKER}`;
|
|
32
12
|
// The exact "[hive:worker NAME] [message #N," at prompt offset 0 (typed) or opening hive's own envelope body (socket).
|
|
33
13
|
const messageEvidenceSql = (select) => `
|
|
34
14
|
SELECT ${select} FROM agent_state_log ev
|
|
35
15
|
WHERE ev.actor_id = (SELECT a.actor_id FROM agents a WHERE a.id = agent_messages.to_agent_id)
|
|
36
16
|
AND ev.event = 'prompt'
|
|
37
17
|
AND ev.created_at >= COALESCE(agent_messages.socket_attempt_at, agent_messages.created_at)
|
|
38
|
-
AND
|
|
39
|
-
|
|
40
|
-
AND substr(${EV_PROMPT}, 1, ${ENVELOPE_OPEN.length}) = '${ENVELOPE_OPEN}'
|
|
41
|
-
AND substr(${EV_PROMPT}, ${EV_HEADER_END} - ${ENVELOPE_SENDER_CLOSE.length}, ${ENVELOPE_SENDER_CLOSE.length}) = '${ENVELOPE_SENDER_CLOSE}'
|
|
42
|
-
AND instr(substr(${EV_PROMPT}, ${ENVELOPE_OPEN.length + 1}, ${EV_HEADER_END} - ${ENVELOPE_OPEN.length + ENVELOPE_SENDER_CLOSE.length + 1}), '"') = 0
|
|
43
|
-
AND substr(${EV_PROMPT}, ${EV_HEADER_END} + 1, length(${EV_MARKER})) = ${EV_MARKER}
|
|
44
|
-
AND instr(${EV_PROMPT}, char(10) || '${CROSS_SESSION_CLOSE}') > ${EV_HEADER_END}))`;
|
|
45
|
-
const RETAINED = "agent_messages.created_at >= datetime('now', '-7 days')";
|
|
46
|
-
function quietMessagingOn(projectId) {
|
|
47
|
-
const project = db.prepare("SELECT path FROM projects WHERE id = ?").get(projectId);
|
|
48
|
-
return project !== undefined && loadProjectYml(project.path).config?.quiet_messaging === true;
|
|
49
|
-
}
|
|
18
|
+
AND ${promptMarkerEvidenceSql(messageMarkerAt)}`;
|
|
19
|
+
const RETAINED = `agent_messages.created_at >= datetime('now', '${MESSAGE_RETENTION}')`;
|
|
50
20
|
const handoffHolds = (agentId) => readHandoffGate(agentId)?.holdAutomation === true;
|
|
51
21
|
const HANDOFF_HOLD_NOTE = "the lead is handing off to a fresh session; held until the successor completes its first turn";
|
|
52
22
|
function eligible(options) {
|
|
@@ -97,29 +67,6 @@ function stamp(id, column) {
|
|
|
97
67
|
function socketText(id, text, tag) {
|
|
98
68
|
return `${renderLeadMessage(id, text, tag)}\n\n${SOCKET_MESSAGE_FOOTER}`;
|
|
99
69
|
}
|
|
100
|
-
function acquirePaneLease(row, key) {
|
|
101
|
-
try {
|
|
102
|
-
db.prepare("INSERT OR IGNORE INTO actors (id, name, kind) VALUES (?, ?, 'agent')").run(row.from_actor, row.from_name);
|
|
103
|
-
db.prepare("DELETE FROM leases WHERE project_id = ? AND lock_key = ? AND expires_at < datetime('now')").run(row.project_id, key);
|
|
104
|
-
const held = db
|
|
105
|
-
.prepare(`INSERT INTO leases (project_id, lock_key, owner, expires_at)
|
|
106
|
-
VALUES (?, ?, ?, datetime('now', printf('+%d seconds', ?)))
|
|
107
|
-
ON CONFLICT(project_id, lock_key) DO NOTHING
|
|
108
|
-
RETURNING expires_at`)
|
|
109
|
-
.get(row.project_id, key, row.from_actor, PANE_LEASE_TTL_SECONDS);
|
|
110
|
-
return held?.expires_at ?? null;
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
function releasePaneLease(row, key, expiresAt) {
|
|
117
|
-
try {
|
|
118
|
-
db.prepare("DELETE FROM leases WHERE project_id = ? AND lock_key = ? AND expires_at = ?").run(row.project_id, key, expiresAt);
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
70
|
function hold(row, reason) {
|
|
124
71
|
try {
|
|
125
72
|
db.prepare("UPDATE agent_messages SET delivery_note = ? WHERE id = ? AND delivery_status = ?").run(reason, row.id, MESSAGE_STATUS.fallbackPending);
|
|
@@ -189,8 +136,11 @@ async function paneFallback(original, snapshot) {
|
|
|
189
136
|
if (ownership !== "live" || target === undefined) {
|
|
190
137
|
return failPending(row, `the lead pane this message was accepted for is ${ownership}; nothing is typed into a replacement`);
|
|
191
138
|
}
|
|
192
|
-
const
|
|
193
|
-
|
|
139
|
+
const expiresAt = tryAcquirePaneLease(row.project_id, identity.tmux_socket, identity.tmux_target, {
|
|
140
|
+
id: row.from_actor,
|
|
141
|
+
name: row.from_name,
|
|
142
|
+
kind: "agent",
|
|
143
|
+
});
|
|
194
144
|
if (expiresAt === null)
|
|
195
145
|
return hold(row, "another delivery holds the lead's pane");
|
|
196
146
|
let wrote = false;
|
|
@@ -241,7 +191,7 @@ async function paneFallback(original, snapshot) {
|
|
|
241
191
|
finally {
|
|
242
192
|
if (wrote)
|
|
243
193
|
await sleep(PANE_LEASE_SETTLE_MS);
|
|
244
|
-
releasePaneLease(row,
|
|
194
|
+
releasePaneLease(row.project_id, identity.tmux_socket, identity.tmux_target, expiresAt);
|
|
245
195
|
}
|
|
246
196
|
}
|
|
247
197
|
function earlierFallbackPending(row) {
|
|
@@ -277,7 +227,7 @@ export async function sendQuietLeadMessage(options) {
|
|
|
277
227
|
if (skip === null) {
|
|
278
228
|
const posted = await postClaudeWake({
|
|
279
229
|
socketPath: target.claude_messaging_socket,
|
|
280
|
-
senderAddress:
|
|
230
|
+
senderAddress: socketSenderAddress(dataDir),
|
|
281
231
|
text: body,
|
|
282
232
|
beforeWrite: () => {
|
|
283
233
|
requireStillOwned(target.id, identity, `message #${id} socket delivery`);
|
|
@@ -348,17 +298,7 @@ export async function sendQuietLeadMessage(options) {
|
|
|
348
298
|
`Do NOT resend blindly: read the lead's pane first, and agent_message_get(${id}) shows the recorded state.`);
|
|
349
299
|
}
|
|
350
300
|
function graceExpired(row, actorId, panePid) {
|
|
351
|
-
|
|
352
|
-
.prepare("SELECT MIN(created_at) AS t FROM agent_state_log WHERE actor_id = ? AND event = 'stop' AND created_at >= ?")
|
|
353
|
-
.get(actorId, row.socket_attempt_at).t;
|
|
354
|
-
let from = firstStop;
|
|
355
|
-
if (from === null) {
|
|
356
|
-
const turn = readLeadTurnState(row.to_agent_id);
|
|
357
|
-
if (turn !== null && turn.state === "working" && turn.pane_pid === panePid)
|
|
358
|
-
return false;
|
|
359
|
-
from = row.socket_attempt_at;
|
|
360
|
-
}
|
|
361
|
-
return db.prepare("SELECT (julianday('now') - julianday(?)) * 86400 >= ? AS due").get(from, CONFIRM_GRACE_SECONDS).due === 1;
|
|
301
|
+
return socketGraceDue(actorId, row.socket_attempt_at, panePid, () => readLeadTurnState(row.to_agent_id));
|
|
362
302
|
}
|
|
363
303
|
function confirmFromPrompts() {
|
|
364
304
|
db.prepare(`UPDATE agent_messages SET confirmed_at = (${messageEvidenceSql("MIN(ev.created_at)")}), delivery_status = ?
|
package/dist/leadState.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { liveBackgroundTasks } from "./backgroundTasks.js";
|
|
2
2
|
import { db } from "./db.js";
|
|
3
|
+
import { hiveWorkerEnvelopeLikeSql, isHiveWorkerEnvelope } from "./claudeWake.js";
|
|
3
4
|
import { FIRST_MESSAGE_SHA_ENV, firstMessageDigest } from "./firstMessage.js";
|
|
4
5
|
import { postponeGrace, readActiveHandoff } from "./leadHandoffState.js";
|
|
5
6
|
export function readLeadTurnState(agentId) {
|
|
@@ -16,14 +17,14 @@ export function leadSessionEnded(row, ownership) {
|
|
|
16
17
|
export function humanPromptSql(col) {
|
|
17
18
|
const p = `CASE WHEN json_valid(${col}) THEN COALESCE(json_extract(${col}, '$.prompt'), ${col}) ELSE ${col} END`;
|
|
18
19
|
return (`${p} NOT LIKE '%[hive wake #%' AND ${p} NOT LIKE '[hive:%' AND ${p} NOT LIKE '<task-notification>%' ` +
|
|
19
|
-
`AND ${p}
|
|
20
|
+
`AND NOT ${hiveWorkerEnvelopeLikeSql(p)}`);
|
|
20
21
|
}
|
|
21
22
|
// Includes SQLite LIKE's ASCII case-insensitivity.
|
|
22
23
|
export function isHumanPrompt(prompt) {
|
|
23
24
|
const p = prompt.toLowerCase();
|
|
24
25
|
if (p.includes("[hive wake #") || p.startsWith("[hive:") || p.startsWith("<task-notification>"))
|
|
25
26
|
return false;
|
|
26
|
-
return
|
|
27
|
+
return !isHiveWorkerEnvelope(p);
|
|
27
28
|
}
|
|
28
29
|
// The agent_state_log state label on hive's own first message to a lead; conversationHoldsWake skips it.
|
|
29
30
|
export const FIRST_MESSAGE_LOG_STATE = "first_message";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { db } from "./db.js";
|
|
2
|
+
export function purgeExpiredLeases(projectId, key) {
|
|
3
|
+
if (key === undefined) {
|
|
4
|
+
db.prepare("DELETE FROM leases WHERE project_id = ? AND expires_at < datetime('now')").run(projectId);
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
db.prepare("DELETE FROM leases WHERE project_id = ? AND lock_key = ? AND expires_at < datetime('now')").run(projectId, key);
|
|
8
|
+
}
|
|
9
|
+
export function insertLease(projectId, key, actor, ttlSeconds) {
|
|
10
|
+
return db
|
|
11
|
+
.prepare(`INSERT INTO leases (project_id, lock_key, owner, expires_at)
|
|
12
|
+
VALUES (?, ?, ?, datetime('now', printf('+%d seconds', ?)))
|
|
13
|
+
ON CONFLICT(project_id, lock_key) DO NOTHING
|
|
14
|
+
RETURNING expires_at`)
|
|
15
|
+
.get(projectId, key, actor, ttlSeconds);
|
|
16
|
+
}
|
|
17
|
+
export function readLease(projectId, key) {
|
|
18
|
+
return db.prepare("SELECT * FROM leases WHERE project_id = ? AND lock_key = ?").get(projectId, key);
|
|
19
|
+
}
|
|
20
|
+
export function extendOwnedLease(projectId, key, actor, ttlSeconds) {
|
|
21
|
+
const info = db
|
|
22
|
+
.prepare(`UPDATE leases SET expires_at = datetime('now', printf('+%d seconds', ?))
|
|
23
|
+
WHERE project_id = ? AND lock_key = ? AND owner = ?`)
|
|
24
|
+
.run(ttlSeconds, projectId, key, actor);
|
|
25
|
+
return info.changes > 0;
|
|
26
|
+
}
|
|
27
|
+
export function releaseOwnedLease(projectId, key, actor) {
|
|
28
|
+
return db
|
|
29
|
+
.prepare("DELETE FROM leases WHERE project_id = ? AND lock_key = ? AND owner = ?")
|
|
30
|
+
.run(projectId, key, actor).changes > 0;
|
|
31
|
+
}
|
|
32
|
+
export function paneLeaseKey(socket, pane) {
|
|
33
|
+
return `wake-pane:${socket}:${pane}`;
|
|
34
|
+
}
|
|
35
|
+
export const PANE_LEASE_TTL_SECONDS = 6;
|
|
36
|
+
export const PANE_LEASE_SETTLE_MS = 500;
|
|
37
|
+
export function tryAcquirePaneLease(projectId, socket, pane, actor) {
|
|
38
|
+
try {
|
|
39
|
+
db.prepare("INSERT OR IGNORE INTO actors (id, name, kind) VALUES (?, ?, ?)").run(actor.id, actor.name, actor.kind);
|
|
40
|
+
const key = paneLeaseKey(socket, pane);
|
|
41
|
+
purgeExpiredLeases(projectId, key);
|
|
42
|
+
return insertLease(projectId, key, actor.id, PANE_LEASE_TTL_SECONDS)?.expires_at ?? null;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function releasePaneLease(projectId, socket, pane, expiresAt) {
|
|
49
|
+
try {
|
|
50
|
+
db.prepare("DELETE FROM leases WHERE project_id = ? AND lock_key = ? AND expires_at = ?").run(projectId, paneLeaseKey(socket, pane), expiresAt);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
package/dist/portfolio.js
CHANGED
|
@@ -7,6 +7,7 @@ import { pendingQuestionCount } from "./questions.js";
|
|
|
7
7
|
import { ACTIVE_TIMER_WHERE as ACTIVE_WAKE_WHERE } from "./wakeSql.js";
|
|
8
8
|
export { ACTIVE_WAKE_WHERE };
|
|
9
9
|
import { leadSessionEnded, readLeadTurnState } from "./leadState.js";
|
|
10
|
+
import { finishedIdle } from "./firstPrompt.js";
|
|
10
11
|
import { liveTargets, ownershipLiveness, rowOwnership } from "./tmux.js";
|
|
11
12
|
export const PORTFOLIO_REASONS = [
|
|
12
13
|
"needs_human",
|
|
@@ -85,7 +86,7 @@ function projectRow(project, now, snapshot) {
|
|
|
85
86
|
}
|
|
86
87
|
tagged.sort((a, b) => (a.updated_at < b.updated_at ? -1 : a.updated_at > b.updated_at ? 1 : a.id - b.id));
|
|
87
88
|
const agents = db
|
|
88
|
-
.prepare("SELECT id, kind, agent_state, tmux_target, tmux_socket, pane_pid FROM agents WHERE project_id = ? AND status = 'running' ORDER BY id")
|
|
89
|
+
.prepare("SELECT id, kind, agent_state, resumed_at, tmux_target, tmux_socket, pane_pid FROM agents WHERE project_id = ? AND status = 'running' ORDER BY id")
|
|
89
90
|
.all(project.id);
|
|
90
91
|
const liveness = (a) => ownershipLiveness(rowOwnership(a, snapshot));
|
|
91
92
|
const workers = { working: 0, idle: 0, needs_input: 0, other: 0, unreachable: 0, unconfirmed: 0 };
|
|
@@ -116,8 +117,10 @@ function projectRow(project, now, snapshot) {
|
|
|
116
117
|
workers.working++;
|
|
117
118
|
liveWorking++;
|
|
118
119
|
}
|
|
119
|
-
else if (a
|
|
120
|
+
else if (finishedIdle(a))
|
|
120
121
|
workers.idle++;
|
|
122
|
+
else if (a.agent_state === "idle")
|
|
123
|
+
workers.other++;
|
|
121
124
|
else if (a.agent_state === "waiting") {
|
|
122
125
|
workers.needs_input++;
|
|
123
126
|
liveWaiting++;
|
package/dist/ptys.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { platform } from "node:os";
|
|
4
|
+
import { parseEtimeSeconds } from "./etime.js";
|
|
5
|
+
export { parseEtimeSeconds } from "./etime.js";
|
|
4
6
|
const PS_LINE = /^\s*(\S+)\s+(\d+)\s+(.+?)\s*$/;
|
|
5
7
|
export function parsePsSnapshot(output) {
|
|
6
8
|
const rows = [];
|
|
@@ -100,15 +102,6 @@ export function ptyHeadroom() {
|
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
const PS_AGE_LINE = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.+?)\s*$/;
|
|
103
|
-
export function parseEtimeSeconds(etime) {
|
|
104
|
-
const m = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(etime) ?? /^(\d+)$/.exec(etime);
|
|
105
|
-
if (!m)
|
|
106
|
-
return null;
|
|
107
|
-
if (m.length === 2)
|
|
108
|
-
return Number(m[1]);
|
|
109
|
-
const [, days, hours, minutes, seconds] = m;
|
|
110
|
-
return (Number(days ?? 0) * 24 + Number(hours ?? 0)) * 3600 + Number(minutes) * 60 + Number(seconds);
|
|
111
|
-
}
|
|
112
105
|
export function parsePsSnapshotWithAge(output) {
|
|
113
106
|
const rows = [];
|
|
114
107
|
for (const line of output.split("\n")) {
|
package/dist/queenDashboard.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join, sep } from "node:path";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { db } from "./db.js";
|
|
5
5
|
import { SECTION_STYLE, SPRITE, chevron, renderBoardPadsTodos, sectionScript } from "./dashboard.js";
|
|
6
|
+
import { parseStoreTimestamp } from "./storeClock.js";
|
|
6
7
|
export const QUEEN_BRIEF_KEY = "queen:brief";
|
|
7
8
|
const LANE_ORDER = ["waiting_on_you", "stuck", "moving", "quiet"];
|
|
8
9
|
const LANE_RANK = { waiting_on_you: 0, stuck: 1, moving: 2, quiet: 3 };
|
|
@@ -103,7 +104,7 @@ const REASON_SEVERE = new Set([
|
|
|
103
104
|
"wake_overdue_5m",
|
|
104
105
|
]);
|
|
105
106
|
function parseUtc(ts) {
|
|
106
|
-
return
|
|
107
|
+
return parseStoreTimestamp(ts);
|
|
107
108
|
}
|
|
108
109
|
function age(from, asOf) {
|
|
109
110
|
const seconds = Math.max(0, Math.floor((parseUtc(asOf) - parseUtc(from)) / 1000));
|