@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/dist/scheduler.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { QUEEN_AUDIT_RETENTION, QUEEN_AUDIT_MAX_ROWS, recentQueenAudit } from "./queenAudit.js";
|
|
2
|
-
import { runningBuildChange, runningBuildNotice } from "./version.js";
|
|
2
|
+
import { runningBuildChange, runningBuildNotice, versionInfo } from "./version.js";
|
|
3
3
|
import { existsSync, lstatSync, mkdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { dirname, join, sep } from "node:path";
|
|
@@ -17,12 +17,17 @@ 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";
|
|
22
|
+
import { retryQuietLeadMessages } from "./leadMessageDelivery.js";
|
|
20
23
|
import { harnessFor, hasTranscriptSignal, paneClassifierFor, screenClassifiable, transcriptDirFor } from "./harnesses.js";
|
|
21
24
|
import { transcriptDir, readContextFill } from "./transcript.js";
|
|
22
25
|
import { ageSecondsSince, describeLastLogEvent, humanizeAge, lastLogEvent, reportsAgentStateLog, } from "./stateProvenance.js";
|
|
23
26
|
import { capturePane, foreignSocket, holdsHumanInput, liveTargets, maskChoiceMarker, observationFailed, paneInCopyMode, paneReissued, rowAlive, rowAliveProbe, rowLiveProbe, rowOwnership, renderTrailerView, sanitizeTail, sendText, tailCaptureLines, sleep, tmuxSocketPath, } from "./tmux.js";
|
|
24
27
|
import { appendTeardown, NOT_ATTRIBUTED, sqlNow, teardownWindow, TEARDOWN_TRIGGER, } from "./teardown.js";
|
|
25
28
|
export const WATCH_SCOPE_PROJECT = "project";
|
|
29
|
+
const DELIVERED_BY = `pid ${process.pid}, ${versionInfo().line}`;
|
|
30
|
+
const deliveredByForFiring = (timer) => timer.repeat_every_ms == null ? DELIVERED_BY : `${DELIVERED_BY}, firing #${timer.fire_count}`;
|
|
26
31
|
const isStandingWatch = (timer) => timer.watch_scope === WATCH_SCOPE_PROJECT;
|
|
27
32
|
export const DELIVER_SOCKET_JOIN = `LEFT JOIN agents ON agents.id = (
|
|
28
33
|
SELECT a.id FROM agents a WHERE a.actor_id = wakes.deliver_actor
|
|
@@ -32,7 +37,9 @@ const DELIVER_ROW_COLUMNS = `COALESCE(agents.tmux_socket, '') AS deliver_socket,
|
|
|
32
37
|
COALESCE(agents.pane_pid, '') AS deliver_pane_pid,
|
|
33
38
|
COALESCE(agents.command, '') AS deliver_command,
|
|
34
39
|
agents.id AS deliver_row_id, agents.status AS deliver_row_status,
|
|
35
|
-
COALESCE(agents.tmux_target, '') AS deliver_row_target
|
|
40
|
+
COALESCE(agents.tmux_target, '') AS deliver_row_target,
|
|
41
|
+
COALESCE(agents.claude_messaging_socket, '') AS deliver_messaging_socket,
|
|
42
|
+
COALESCE(agents.claude_messaging_pane_pid, '') AS deliver_messaging_pane_pid`;
|
|
36
43
|
// A row's pid certifies only the pane that row records, never a different deliver_pane.
|
|
37
44
|
const deliveryIdentity = (row) => ({
|
|
38
45
|
tmux_target: row.deliver_pane,
|
|
@@ -455,18 +462,20 @@ function maybeGenerateDashboards() {
|
|
|
455
462
|
}
|
|
456
463
|
}
|
|
457
464
|
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 || ',%')`;
|
|
465
|
+
const sentAt = "COALESCE(wakes.socket_attempt_at, wakes.typed_at)";
|
|
466
|
+
const confirmationQuery = promptEvidenceSql(sentAt);
|
|
463
467
|
try {
|
|
464
468
|
const pending = stmt(`SELECT 1 AS hit FROM wakes
|
|
465
|
-
WHERE
|
|
469
|
+
WHERE ${sentAt} IS NOT NULL AND confirmed_at IS NULL AND ${sentAt} >= datetime('now', ?) LIMIT 1`).get(LOG_RETENTION);
|
|
466
470
|
if (!pending)
|
|
467
471
|
return;
|
|
472
|
+
stmt(`UPDATE wakes SET confirmed_at = (${repeatingEvidenceSql("MIN(ev.created_at)")})
|
|
473
|
+
WHERE ${REPEATING_SOCKET_FIRING} AND socket_attempt_at IS NOT NULL AND confirmed_at IS NULL
|
|
474
|
+
AND socket_attempt_at >= datetime('now', ?)
|
|
475
|
+
AND EXISTS (${repeatingEvidenceSql("1")})`).run(LOG_RETENTION);
|
|
468
476
|
stmt(`UPDATE wakes SET confirmed_at = (${confirmationQuery})
|
|
469
|
-
WHERE
|
|
477
|
+
WHERE ${sentAt} IS NOT NULL AND confirmed_at IS NULL AND ${sentAt} >= datetime('now', ?)
|
|
478
|
+
AND NOT (${REPEATING_SOCKET_FIRING})
|
|
470
479
|
AND EXISTS (${confirmationQuery.replace("MIN(created_at)", "1")})`).run(LOG_RETENTION);
|
|
471
480
|
}
|
|
472
481
|
catch {
|
|
@@ -511,6 +520,8 @@ export async function tick(snapshot) {
|
|
|
511
520
|
else
|
|
512
521
|
await maybeFireIdle(timer, snapshot, now, choices);
|
|
513
522
|
}
|
|
523
|
+
await retryUnconfirmedSocketWakes(snapshot, choices);
|
|
524
|
+
await retryQuietLeadMessages(snapshot);
|
|
514
525
|
}
|
|
515
526
|
catch {
|
|
516
527
|
}
|
|
@@ -567,10 +578,12 @@ function forgetPaneAnswers(pane, choices) {
|
|
|
567
578
|
noDialogUntil.delete(pane);
|
|
568
579
|
}
|
|
569
580
|
function holdTimer(timer, reason) {
|
|
581
|
+
const count = timer.fire_count ?? null;
|
|
570
582
|
bestEffortRun(`UPDATE wakes SET held_at = datetime('now'), held_reason = ?,
|
|
571
583
|
first_held_at = COALESCE(CASE WHEN held_at IS NULL THEN NULL ELSE first_held_at END, datetime('now'))
|
|
572
584
|
WHERE id = ? AND cancelled_at IS NULL
|
|
573
|
-
AND (fired_at IS NULL OR (repeat_every_ms IS NOT NULL AND due_at = ?
|
|
585
|
+
AND (fired_at IS NULL OR (repeat_every_ms IS NOT NULL AND due_at = ? AND (? IS NULL OR fire_count = ?))
|
|
586
|
+
OR (${socketAwaitingWhere()}))`, reason, timer.id, timer.due_at, count, count);
|
|
574
587
|
}
|
|
575
588
|
const HELD_REASON_MODAL_CHOICE = "pane is awaiting a modal choice (folder-trust or /model picker)";
|
|
576
589
|
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 " +
|
|
@@ -655,6 +668,8 @@ function conversationHoldsWake(timer, snapshot) {
|
|
|
655
668
|
}
|
|
656
669
|
return (stmt(`SELECT 1 AS hit FROM agent_state_log
|
|
657
670
|
WHERE actor_id = ? AND event = 'prompt' AND CASE WHEN json_valid(payload) THEN COALESCE(json_extract(payload, '$.prompt'), payload) ELSE payload END NOT LIKE '%[hive wake #%' AND CASE WHEN json_valid(payload) THEN COALESCE(json_extract(payload, '$.prompt'), payload) ELSE payload END NOT LIKE '[hive:%'
|
|
671
|
+
AND CASE WHEN json_valid(payload) THEN COALESCE(json_extract(payload, '$.prompt'), payload) ELSE payload END NOT LIKE '<task-notification>%'
|
|
672
|
+
AND CASE WHEN json_valid(payload) THEN COALESCE(json_extract(payload, '$.prompt'), payload) ELSE payload END NOT LIKE '<cross-session-message from="%" from-name="hive">' || char(10) || '[hive:worker %] [message #%,%' || char(10) || '</cross-session-message>%'
|
|
658
673
|
AND created_at >= datetime('now', ?)
|
|
659
674
|
ORDER BY id DESC LIMIT 1`).get(timer.deliver_actor, CONVERSATION_HOLD_TTL) !== undefined);
|
|
660
675
|
}
|
|
@@ -931,7 +946,7 @@ const STANDING_EXPIRED_NOTE = "this standing watch has expired and nothing is wa
|
|
|
931
946
|
const unreported = (condition, episode) => `NOT EXISTS (
|
|
932
947
|
SELECT 1 FROM wake_idle_notices n LEFT JOIN wakes nt ON nt.id = n.notice_wake_id
|
|
933
948
|
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
|
|
949
|
+
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
950
|
AND nt.fired_at < datetime('now', '${NOTICE_RETRY_AFTER}')))`;
|
|
936
951
|
const CREW_COLUMNS = `a.id, a.name, a.actor_id, a.tmux_target, a.tmux_socket, a.pane_pid, a.agent_state,
|
|
937
952
|
a.state_changed_at, a.status, a.command, a.kind, a.resumed_at, a.closed_at,
|
|
@@ -1174,7 +1189,7 @@ function rearmSpentEpisode(timerId, agentId, condition, episode) {
|
|
|
1174
1189
|
WHERE wake_id = ? AND agent_id = ? AND condition = ? AND episode = ?
|
|
1175
1190
|
AND notice_wake_id IS NOT NULL
|
|
1176
1191
|
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
|
|
1192
|
+
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
1193
|
AND t.fired_at < datetime('now', '${NOTICE_RETRY_AFTER}'))`).run(timerId, agentId, condition, episode);
|
|
1179
1194
|
}
|
|
1180
1195
|
function claimEpisode(timerId, agentId, condition, episode) {
|
|
@@ -1607,28 +1622,33 @@ async function fireDelay(timer, snapshot, choices) {
|
|
|
1607
1622
|
bestEffortRun("UPDATE wakes SET cancelled_at = datetime('now') WHERE id = ? AND created_at IS ?", timer.id, timer.created_at);
|
|
1608
1623
|
return;
|
|
1609
1624
|
}
|
|
1625
|
+
if (timer.repeat_every_ms != null && repeatingFiringPending(timer.id))
|
|
1626
|
+
return;
|
|
1610
1627
|
await withPaneClaim(timer, () => fireDelayClaimed(timer, snapshot, choices));
|
|
1611
1628
|
}
|
|
1612
1629
|
async function fireDelayClaimed(timer, snapshot, choices) {
|
|
1613
1630
|
const decision = deliverable(timer, snapshot, choices);
|
|
1614
1631
|
if (!decision.ok)
|
|
1615
1632
|
return false;
|
|
1616
|
-
let
|
|
1633
|
+
let firing = timer;
|
|
1617
1634
|
if (timer.repeat_every_ms != null) {
|
|
1618
1635
|
const seconds = Math.max(1, Math.round(timer.repeat_every_ms / 1000));
|
|
1619
|
-
claimed =
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
claimed
|
|
1636
|
+
const claimed = stmt(`UPDATE wakes SET due_at = datetime('now', printf('+%d seconds', ?)),
|
|
1637
|
+
fired_at = datetime('now'), fire_count = fire_count + 1,
|
|
1638
|
+
typed_at = NULL, confirmed_at = NULL, held_at = NULL, held_reason = NULL, typed_busy = NULL,
|
|
1639
|
+
typed_seen = NULL, first_held_at = NULL,
|
|
1640
|
+
socket_attempt_at = NULL, socket_delivery_note = NULL, delivery_method = NULL, delivered_by = NULL
|
|
1641
|
+
WHERE id = ? AND due_at IS ? AND body IS ? AND repeat_every_ms IS ? AND cancelled_at IS NULL
|
|
1642
|
+
AND fire_count = ? AND NOT COALESCE((${REPEATING_FIRING_PENDING}), 0)
|
|
1643
|
+
RETURNING fire_count, fired_at`).get(seconds, timer.id, timer.due_at, timer.body, timer.repeat_every_ms, timer.fire_count);
|
|
1644
|
+
if (claimed === undefined)
|
|
1645
|
+
return false;
|
|
1646
|
+
firing = { ...timer, fire_count: claimed.fire_count, fired_at: claimed.fired_at, socket_attempt_at: null, socket_delivery_note: null, delivery_method: null };
|
|
1628
1647
|
}
|
|
1629
|
-
if (!
|
|
1648
|
+
else if (!claimOneShot(timer)) {
|
|
1630
1649
|
return false;
|
|
1631
|
-
|
|
1650
|
+
}
|
|
1651
|
+
await deliver(firing, "", choices, decision.typedSeen, decision.firstHeldAt, decision.owner);
|
|
1632
1652
|
return true;
|
|
1633
1653
|
}
|
|
1634
1654
|
function claimOneShot(timer) {
|
|
@@ -1886,16 +1906,167 @@ export function shortRenderForLeadDelivery(timer) {
|
|
|
1886
1906
|
return standingNoticeBodyShort(timer, candidates, timer.id);
|
|
1887
1907
|
}
|
|
1888
1908
|
// The claim already happened, so this records why and never re-arms or replays the wake.
|
|
1889
|
-
function recordLostAfterClaim(timer, pasted, why) {
|
|
1909
|
+
function recordLostAfterClaim(timer, pasted, why, firing = null) {
|
|
1890
1910
|
bestEffortRun(`UPDATE wakes SET held_reason = ?,
|
|
1891
1911
|
typed_at = CASE WHEN ? = 1 THEN strftime('%Y-%m-%d %H:%M:%f', 'now') ELSE typed_at END,
|
|
1892
1912
|
cancelled_at = CASE WHEN repeat_every_ms IS NULL THEN datetime('now') ELSE cancelled_at END
|
|
1893
|
-
WHERE id =
|
|
1913
|
+
WHERE id = ? AND (? IS NULL OR fire_count = ?)`, HELD_REASON_OWNERSHIP_LOST_AFTER_CLAIM + why + (pasted ? " The body was pasted; no Enter was sent." : ""), pasted ? 1 : 0, timer.id, firing, firing);
|
|
1914
|
+
}
|
|
1915
|
+
export const DELIVERY_METHOD = {
|
|
1916
|
+
socket: "socket",
|
|
1917
|
+
socketRepeating: "socket-repeating",
|
|
1918
|
+
pty: "pty",
|
|
1919
|
+
ptyAfterSocketTimeout: "pty-after-socket-timeout",
|
|
1920
|
+
};
|
|
1921
|
+
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`;
|
|
1922
|
+
// An attempt older than the row's latest claim belongs to an earlier firing (an old-build claim leaves it behind).
|
|
1923
|
+
const currentAttempt = (p) => `${p}socket_attempt_at IS NOT NULL AND ${p}confirmed_at IS NULL AND ${p}typed_at IS NULL AND ${p}cancelled_at IS NULL
|
|
1924
|
+
AND ${p}fired_at IS NOT NULL AND ${p}socket_attempt_at >= ${p}fired_at`;
|
|
1925
|
+
const repeatingSocketAwaitingWhere = (prefix = "") => `${prefix}delivery_method = '${DELIVERY_METHOD.socketRepeating}' AND ${currentAttempt(prefix)}`;
|
|
1926
|
+
const FIRING_DROPPED_NOTE = "dropped";
|
|
1927
|
+
const repeatingFallbackClaimedWhere = (prefix = "") => `${prefix}repeat_every_ms IS NOT NULL AND ${prefix}delivery_method = '${DELIVERY_METHOD.ptyAfterSocketTimeout}' AND ${currentAttempt(prefix)}
|
|
1928
|
+
AND ${prefix}socket_delivery_note IS NOT '${FIRING_DROPPED_NOTE}'`;
|
|
1929
|
+
// Bounded like retry and confirmation: past retention neither can settle it, so it must stop deferring.
|
|
1930
|
+
const REPEATING_FIRING_PENDING = `((${repeatingSocketAwaitingWhere()}) OR (${repeatingFallbackClaimedWhere()}))
|
|
1931
|
+
AND socket_attempt_at >= datetime('now', '${LOG_RETENTION}')`;
|
|
1932
|
+
const REPEATING_SOCKET_FIRING = `repeat_every_ms IS NOT NULL AND COALESCE(delivery_method, '') IN ('${DELIVERY_METHOD.socketRepeating}', '${DELIVERY_METHOD.ptyAfterSocketTimeout}')`;
|
|
1933
|
+
function repeatingFiringPending(id) {
|
|
1934
|
+
try {
|
|
1935
|
+
return stmt(`SELECT 1 AS hit FROM wakes WHERE id = ? AND (${REPEATING_FIRING_PENDING})`).get(id) !== undefined;
|
|
1936
|
+
}
|
|
1937
|
+
catch {
|
|
1938
|
+
return true;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
const firingMarker = (timer) => `[hive wake #${timer.id} firing #${timer.fire_count}`;
|
|
1942
|
+
const SOCKET_CONFIRM_GRACE_SECONDS = 60;
|
|
1943
|
+
const withReDelivered = (note) => (note ? `${note}, re-delivered` : "re-delivered");
|
|
1944
|
+
const promptEvidenceSql = (lowerBound) => `
|
|
1945
|
+
SELECT MIN(created_at) FROM agent_state_log
|
|
1946
|
+
WHERE actor_id = wakes.deliver_actor AND event = 'prompt' AND created_at >= ${lowerBound}
|
|
1947
|
+
AND (payload LIKE '%[hive wake #' || wakes.id || ']%'
|
|
1948
|
+
OR payload LIKE '%[hive wake #' || wakes.id || ',%')`;
|
|
1949
|
+
const ENVELOPE_OPEN = '<cross-session-message from="';
|
|
1950
|
+
const ENVELOPE_SENDER_CLOSE = '" from-name="hive">';
|
|
1951
|
+
const EV_PROMPT = "(CASE WHEN json_valid(ev.payload) THEN COALESCE(json_extract(ev.payload, '$.prompt'), '') ELSE ev.payload END)";
|
|
1952
|
+
const EV_MARKER = "('[hive wake #' || wakes.id || ' firing #' || wakes.fire_count)";
|
|
1953
|
+
const markerAt = (start) => `substr(${EV_PROMPT}, ${start}, length(${EV_MARKER}) + 1) IN (${EV_MARKER} || ']', ${EV_MARKER} || ',')`;
|
|
1954
|
+
const EV_HEADER_END = `instr(${EV_PROMPT}, char(10))`;
|
|
1955
|
+
// The current firing's exact marker, at prompt offset 0 (typed) or opening hive's envelope body (socket).
|
|
1956
|
+
const repeatingEvidenceSql = (select) => `
|
|
1957
|
+
SELECT ${select} FROM agent_state_log ev
|
|
1958
|
+
WHERE ev.actor_id = wakes.deliver_actor AND ev.event = 'prompt' AND ev.created_at >= wakes.socket_attempt_at
|
|
1959
|
+
AND (${markerAt("1")}
|
|
1960
|
+
OR (${EV_HEADER_END} > ${ENVELOPE_OPEN.length + ENVELOPE_SENDER_CLOSE.length}
|
|
1961
|
+
AND substr(${EV_PROMPT}, 1, ${ENVELOPE_OPEN.length}) = '${ENVELOPE_OPEN}'
|
|
1962
|
+
AND substr(${EV_PROMPT}, ${EV_HEADER_END} - ${ENVELOPE_SENDER_CLOSE.length}, ${ENVELOPE_SENDER_CLOSE.length}) = '${ENVELOPE_SENDER_CLOSE}'
|
|
1963
|
+
AND instr(substr(${EV_PROMPT}, ${ENVELOPE_OPEN.length + 1}, ${EV_HEADER_END} - ${ENVELOPE_OPEN.length + ENVELOPE_SENDER_CLOSE.length + 1}), '"') = 0
|
|
1964
|
+
AND ${markerAt(`${EV_HEADER_END} + 1`)}
|
|
1965
|
+
AND instr(${EV_PROMPT}, char(10) || '${CROSS_SESSION_CLOSE}') > ${EV_HEADER_END}))`;
|
|
1966
|
+
function quietMessagingOn(projectId) {
|
|
1967
|
+
const project = stmt("SELECT path FROM projects WHERE id = ?").get(projectId);
|
|
1968
|
+
return project !== undefined && loadProjectYml(project.path).config?.quiet_messaging === true;
|
|
1969
|
+
}
|
|
1970
|
+
// Only a wake to a live, row-owned Claude lead whose socket was registered by this pane epoch.
|
|
1971
|
+
function socketRoute(timer, owner) {
|
|
1972
|
+
return (owner !== null &&
|
|
1973
|
+
isLeadActorId(timer.deliver_actor) &&
|
|
1974
|
+
harnessFor(timer.deliver_command).name === "claude" &&
|
|
1975
|
+
timer.deliver_messaging_socket !== "" &&
|
|
1976
|
+
timer.deliver_messaging_pane_pid !== "" &&
|
|
1977
|
+
timer.deliver_messaging_pane_pid === timer.deliver_pane_pid &&
|
|
1978
|
+
timer.deliver_row_target === timer.deliver_pane &&
|
|
1979
|
+
quietMessagingOn(timer.project_id));
|
|
1980
|
+
}
|
|
1981
|
+
function socketStillRegistered(timer, owner) {
|
|
1982
|
+
requireStillOwned(owner.rowId, owner.identity, `wake #${timer.id} socket delivery`);
|
|
1983
|
+
const row = stmt(`SELECT claude_messaging_socket AS socket, claude_messaging_pane_pid AS pid, pane_pid FROM agents
|
|
1984
|
+
WHERE id = ? AND status = 'running' AND kind = 'lead'`).get(owner.rowId);
|
|
1985
|
+
const wake = timer.repeat_every_ms == null
|
|
1986
|
+
? stmt("SELECT 1 AS live FROM wakes WHERE id = ? AND cancelled_at IS NULL").get(timer.id)
|
|
1987
|
+
: stmt("SELECT 1 AS live FROM wakes WHERE id = ? AND cancelled_at IS NULL AND fire_count = ?").get(timer.id, timer.fire_count);
|
|
1988
|
+
return (wake !== undefined &&
|
|
1989
|
+
row !== undefined &&
|
|
1990
|
+
row.socket === timer.deliver_messaging_socket &&
|
|
1991
|
+
row.pid === timer.deliver_messaging_pane_pid &&
|
|
1992
|
+
row.pid === row.pane_pid);
|
|
1993
|
+
}
|
|
1994
|
+
// A failed stamp means the firing was cancelled or superseded, so nothing is posted.
|
|
1995
|
+
function stampRepeatingAttempt(timer, note, firstHeldAt) {
|
|
1996
|
+
try {
|
|
1997
|
+
return (stmt(`UPDATE wakes SET socket_attempt_at = strftime('%Y-%m-%d %H:%M:%f', 'now'), socket_delivery_note = ?,
|
|
1998
|
+
delivery_method = ?, delivered_by = ?, first_held_at = ?, held_at = NULL, held_reason = NULL, confirmed_at = NULL
|
|
1999
|
+
WHERE id = ? AND fire_count = ? AND cancelled_at IS NULL AND typed_at IS NULL AND socket_attempt_at IS NULL`).run(note, DELIVERY_METHOD.socketRepeating, deliveredByForFiring(timer), firstHeldAt, timer.id, timer.fire_count).changes === 1);
|
|
2000
|
+
}
|
|
2001
|
+
catch {
|
|
2002
|
+
return false;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
// At most one fallback per (id, firing): the method leaves socket-repeating here and never returns to it.
|
|
2006
|
+
function claimRepeatingFallback(timer) {
|
|
2007
|
+
try {
|
|
2008
|
+
return (stmt(`UPDATE wakes SET delivery_method = ?, delivered_by = ?
|
|
2009
|
+
WHERE id = ? AND fire_count = ? AND ${repeatingSocketAwaitingWhere()}
|
|
2010
|
+
AND NOT EXISTS (${repeatingEvidenceSql("1")})`).run(DELIVERY_METHOD.ptyAfterSocketTimeout, deliveredByForFiring(timer), timer.id, timer.fire_count).changes === 1);
|
|
2011
|
+
}
|
|
2012
|
+
catch {
|
|
2013
|
+
return false;
|
|
2014
|
+
}
|
|
1894
2015
|
}
|
|
1895
|
-
|
|
2016
|
+
class FiringSuperseded extends Error {
|
|
2017
|
+
}
|
|
2018
|
+
// Nothing reached the pane, so this firing is settled as dropped: the next one delivers, nothing replays it.
|
|
2019
|
+
// The marker lives in socket_delivery_note because hive lead's re-point clears held_reason.
|
|
2020
|
+
function dropRepeatingFiring(timer, firing, err) {
|
|
2021
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
2022
|
+
bestEffortRun(`UPDATE wakes SET socket_delivery_note = ?, held_reason = ?
|
|
2023
|
+
WHERE id = ? AND fire_count = ? AND delivery_method = ? AND typed_at IS NULL`, FIRING_DROPPED_NOTE, `${HELD_REASON_OWNERSHIP_LOST_AFTER_CLAIM}${why} Firing #${firing} was not delivered; the next firing goes out on schedule.`, timer.id, firing, DELIVERY_METHOD.ptyAfterSocketTimeout);
|
|
2024
|
+
}
|
|
2025
|
+
function requireCurrentFiring(id, firing) {
|
|
2026
|
+
const row = stmt("SELECT 1 AS hit FROM wakes WHERE id = ? AND fire_count = ? AND cancelled_at IS NULL").get(id, firing);
|
|
2027
|
+
if (row === undefined)
|
|
2028
|
+
throw new FiringSuperseded(`wake #${id} firing #${firing} was superseded`);
|
|
2029
|
+
}
|
|
2030
|
+
async function deliver(timer, note, choices, typedSeen, firstHeldAt, owner, fallback = false) {
|
|
1896
2031
|
const tail = watchedTail(timer);
|
|
1897
|
-
const prefix = `[hive wake #${timer.id}${note ? `, ${note}` : ""}] `;
|
|
1898
2032
|
const body = (isLeadActorId(timer.deliver_actor) ? shortRenderForLeadDelivery(timer) : null) ?? timer.body;
|
|
2033
|
+
const staleness = noticeStalenessNote(timer);
|
|
2034
|
+
// A repeating wake that took the socket keeps its firing number in every marker and every write.
|
|
2035
|
+
let firing = fallback && timer.delivery_method === DELIVERY_METHOD.socketRepeating ? timer.fire_count : null;
|
|
2036
|
+
if (!fallback && socketRoute(timer, owner)) {
|
|
2037
|
+
const repeating = timer.repeat_every_ms != null;
|
|
2038
|
+
const head = repeating ? firingMarker(timer) : `[hive wake #${timer.id}`;
|
|
2039
|
+
const text = `${head}${note ? `, ${note}` : ""}] ${body + staleness + tail}\n\n${SOCKET_WAKE_FOOTER}`;
|
|
2040
|
+
if (!text.includes(CROSS_SESSION_CLOSE)) {
|
|
2041
|
+
if (repeating) {
|
|
2042
|
+
if (!stampRepeatingAttempt(timer, note, firstHeldAt))
|
|
2043
|
+
return;
|
|
2044
|
+
firing = timer.fire_count;
|
|
2045
|
+
}
|
|
2046
|
+
else {
|
|
2047
|
+
bestEffortRun(`UPDATE wakes SET socket_attempt_at = strftime('%Y-%m-%d %H:%M:%f', 'now'), socket_delivery_note = ?,
|
|
2048
|
+
delivery_method = ?, delivered_by = ?, first_held_at = ?, held_at = NULL, held_reason = NULL, confirmed_at = NULL
|
|
2049
|
+
WHERE id = ?`, note, DELIVERY_METHOD.socket, deliveredByForFiring(timer), firstHeldAt, timer.id);
|
|
2050
|
+
}
|
|
2051
|
+
const posted = await postClaudeWake({
|
|
2052
|
+
socketPath: timer.deliver_messaging_socket,
|
|
2053
|
+
senderAddress: senderAddress(join(dataDir, "wake-sender.sock")),
|
|
2054
|
+
text,
|
|
2055
|
+
beforeWrite: () => socketStillRegistered(timer, owner),
|
|
2056
|
+
});
|
|
2057
|
+
if (posted) {
|
|
2058
|
+
forgetPaneAnswers(timer.deliver_pane, choices);
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
if (firing !== null && !claimRepeatingFallback(timer))
|
|
2062
|
+
return;
|
|
2063
|
+
note = withReDelivered(note);
|
|
2064
|
+
fallback = true;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
const named = firing === null ? `wake #${timer.id}` : `wake #${timer.id} firing #${firing}`;
|
|
2068
|
+
const prefix = `[hive ${named}${note ? `, ${note}` : ""}] ` + (fallback ? `If you already handled ${named}, ignore this.\n` : "");
|
|
2069
|
+
const method = fallback ? DELIVERY_METHOD.ptyAfterSocketTimeout : DELIVERY_METHOD.pty;
|
|
1899
2070
|
let typedBusy;
|
|
1900
2071
|
try {
|
|
1901
2072
|
const lastEvent = lastLogEvent(timer.deliver_actor);
|
|
@@ -1905,22 +2076,38 @@ async function deliver(timer, note, choices, typedSeen, firstHeldAt, owner) {
|
|
|
1905
2076
|
typedBusy = null;
|
|
1906
2077
|
}
|
|
1907
2078
|
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
|
-
|
|
2079
|
+
typed_seen = ?, first_held_at = ?, held_at = NULL, held_reason = NULL, delivery_method = ?, delivered_by = ?,
|
|
2080
|
+
confirmed_at = CASE WHEN socket_attempt_at IS NULL THEN NULL ELSE confirmed_at END
|
|
2081
|
+
WHERE id = ? AND (? IS NULL OR fire_count = ?)`, typedBusy, typedSeen, firstHeldAt, method, deliveredByForFiring(timer), timer.id, firing, firing);
|
|
1910
2082
|
const box = cacheEntry(timer.deliver_pane, choices).box;
|
|
1911
2083
|
const strandedTextWouldHold = box !== undefined && box !== null && box.state !== "unknown";
|
|
1912
2084
|
let pasted = false;
|
|
1913
2085
|
try {
|
|
1914
|
-
await sendText(timer.deliver_pane, prefix + body +
|
|
2086
|
+
await sendText(timer.deliver_pane, prefix + body + staleness + tail, true, () => {
|
|
1915
2087
|
pasted = true;
|
|
1916
2088
|
if (strandedTextWouldHold)
|
|
1917
2089
|
recordTyped();
|
|
1918
|
-
}, undefined, owner
|
|
2090
|
+
}, undefined, owner || firing !== null
|
|
2091
|
+
? () => {
|
|
2092
|
+
if (owner)
|
|
2093
|
+
requireStillOwned(owner.rowId, owner.identity, `wake #${timer.id} delivery`);
|
|
2094
|
+
if (firing !== null)
|
|
2095
|
+
requireCurrentFiring(timer.id, firing);
|
|
2096
|
+
}
|
|
2097
|
+
: undefined);
|
|
1919
2098
|
}
|
|
1920
2099
|
catch (err) {
|
|
2100
|
+
if (fallback && !pasted && firing === null) {
|
|
2101
|
+
bestEffortRun("UPDATE wakes SET delivery_method = ?, delivered_by = ? WHERE id = ? AND typed_at IS NULL", DELIVERY_METHOD.socket, deliveredByForFiring(timer), timer.id);
|
|
2102
|
+
}
|
|
2103
|
+
if (err instanceof FiringSuperseded)
|
|
2104
|
+
return;
|
|
2105
|
+
if (firing !== null && !pasted)
|
|
2106
|
+
dropRepeatingFiring(timer, firing, err);
|
|
1921
2107
|
if (!(err instanceof PaneOwnershipLost))
|
|
1922
2108
|
throw err;
|
|
1923
|
-
|
|
2109
|
+
if (firing === null || pasted)
|
|
2110
|
+
recordLostAfterClaim(timer, pasted, err.message, firing);
|
|
1924
2111
|
return;
|
|
1925
2112
|
}
|
|
1926
2113
|
finally {
|
|
@@ -1929,6 +2116,43 @@ async function deliver(timer, note, choices, typedSeen, firstHeldAt, owner) {
|
|
|
1929
2116
|
if (!strandedTextWouldHold)
|
|
1930
2117
|
recordTyped();
|
|
1931
2118
|
}
|
|
2119
|
+
// Measured from the lead's first turn end after the post, because a busy lead reads the post only then.
|
|
2120
|
+
function socketFallbackDue(timer) {
|
|
2121
|
+
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;
|
|
2122
|
+
let from = firstStop;
|
|
2123
|
+
if (from === null) {
|
|
2124
|
+
const turn = timer.deliver_row_id === null ? null : readLeadTurnState(timer.deliver_row_id);
|
|
2125
|
+
if (turn !== null && turn.state === "working" && turn.pane_pid === timer.deliver_pane_pid)
|
|
2126
|
+
return false;
|
|
2127
|
+
from = timer.socket_attempt_at;
|
|
2128
|
+
}
|
|
2129
|
+
return stmt("SELECT (julianday('now') - julianday(?)) * 86400 >= ? AS due").get(from, SOCKET_CONFIRM_GRACE_SECONDS).due === 1;
|
|
2130
|
+
}
|
|
2131
|
+
async function fallbackClaimed(timer, snapshot, choices) {
|
|
2132
|
+
const decision = deliverable(timer, snapshot, choices);
|
|
2133
|
+
if (!decision.ok)
|
|
2134
|
+
return false;
|
|
2135
|
+
const claimed = timer.delivery_method === DELIVERY_METHOD.socketRepeating
|
|
2136
|
+
? claimRepeatingFallback(timer)
|
|
2137
|
+
: stmt(`UPDATE wakes SET delivery_method = ?, delivered_by = ?
|
|
2138
|
+
WHERE id = ? AND ${socketAwaitingWhere()}
|
|
2139
|
+
AND NOT EXISTS (${promptEvidenceSql("wakes.socket_attempt_at").replace("MIN(created_at)", "1")})`).run(DELIVERY_METHOD.ptyAfterSocketTimeout, deliveredByForFiring(timer), timer.id).changes === 1;
|
|
2140
|
+
if (!claimed)
|
|
2141
|
+
return false;
|
|
2142
|
+
await deliver(timer, withReDelivered(timer.socket_delivery_note), choices, decision.typedSeen, decision.firstHeldAt, decision.owner, true);
|
|
2143
|
+
return true;
|
|
2144
|
+
}
|
|
2145
|
+
async function retryUnconfirmedSocketWakes(snapshot, choices) {
|
|
2146
|
+
const awaiting = stmt(`SELECT wakes.*, ${DELIVER_ROW_COLUMNS}
|
|
2147
|
+
FROM wakes ${DELIVER_SOCKET_JOIN}
|
|
2148
|
+
WHERE ((${socketAwaitingWhere("wakes.")}) OR (${repeatingSocketAwaitingWhere("wakes.")}))
|
|
2149
|
+
AND wakes.socket_attempt_at >= datetime('now', ?)`).all(LOG_RETENTION);
|
|
2150
|
+
for (const timer of awaiting) {
|
|
2151
|
+
if (!socketFallbackDue(timer))
|
|
2152
|
+
continue;
|
|
2153
|
+
await withPaneClaim(timer, () => fallbackClaimed(timer, snapshot, choices));
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
1932
2156
|
export const DEFAULT_SCHEDULER_INTERVAL_MS = 3000;
|
|
1933
2157
|
const MIN_SCHEDULER_INTERVAL_MS = 100;
|
|
1934
2158
|
// Node clamps a setInterval delay above this to 1ms.
|
package/dist/tmux.js
CHANGED
|
@@ -123,7 +123,11 @@ function tmuxWithin(timeoutMs, ...args) {
|
|
|
123
123
|
throw new TmuxError(`tmux ${args[0]} failed${detail ? `: ${detail}` : ""}`, detail ?? "");
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
|
-
const NOTHING_THERE = /no server running|
|
|
126
|
+
const NOTHING_THERE = /no server running|no current target|can't find (pane|window|session)/;
|
|
127
|
+
// A connect failure is an empty server only when the errno proves nothing listens; EACCES/EPERM
|
|
128
|
+
// (a sandboxed caller) and any unreadable reason mean nobody answered.
|
|
129
|
+
const CONNECT_FAILED = /error connecting to .*\(([^)]*)\)\s*$/;
|
|
130
|
+
const NO_LISTENER = /^(No such file or directory|Connection refused)$/;
|
|
127
131
|
// "no server running" is tmux ANSWERING that the socket is empty. A missing binary is nobody
|
|
128
132
|
// answering at all, and the two must never read the same to anything that reports a death.
|
|
129
133
|
export function tmuxNotInstalled(e) {
|
|
@@ -136,6 +140,9 @@ export function tmuxSaysNothingThere(e) {
|
|
|
136
140
|
return false;
|
|
137
141
|
if (e.notInstalled)
|
|
138
142
|
return true;
|
|
143
|
+
const reason = CONNECT_FAILED.exec(e.stderr)?.[1];
|
|
144
|
+
if (reason !== undefined)
|
|
145
|
+
return NO_LISTENER.test(reason);
|
|
139
146
|
return NOTHING_THERE.test(e.stderr);
|
|
140
147
|
}
|
|
141
148
|
function quietTmux(...args) {
|
|
@@ -854,7 +861,12 @@ function leadingRunIsFaint(s) {
|
|
|
854
861
|
faint = false;
|
|
855
862
|
continue;
|
|
856
863
|
}
|
|
857
|
-
for (
|
|
864
|
+
for (let i = 0; i < params.length; i++) {
|
|
865
|
+
const p = params[i];
|
|
866
|
+
if (p === "38" || p === "48" || p === "58") {
|
|
867
|
+
i += params[i + 1] === "5" ? 2 : params[i + 1] === "2" ? 4 : 0;
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
858
870
|
if (p === "0")
|
|
859
871
|
faint = false;
|
|
860
872
|
else if (p === "2")
|
|
@@ -922,19 +934,42 @@ function classifyInputBox(rows, promptRowIndex, boxBottom) {
|
|
|
922
934
|
.slice(0, TAIL_LINE_CHARS);
|
|
923
935
|
return { state: text === "" ? "empty" : dim ? "ghost" : "pending", text };
|
|
924
936
|
}
|
|
925
|
-
|
|
937
|
+
const SNAPSHOT_ROWS = 10;
|
|
938
|
+
const SNAPSHOT_ROW_CHARS = 120;
|
|
939
|
+
const snapshotRow = (row) => stripControlBytes(stripSgr(row))
|
|
940
|
+
.trimEnd()
|
|
941
|
+
.replace(/─{4,}/g, (run) => `─×${run.length}`)
|
|
942
|
+
.slice(0, SNAPSHOT_ROW_CHARS);
|
|
943
|
+
function snapshotRows(rows, anchor) {
|
|
944
|
+
if (anchor === null) {
|
|
945
|
+
return rows.map(snapshotRow).filter((row) => row !== "").slice(-SNAPSHOT_ROWS);
|
|
946
|
+
}
|
|
947
|
+
return rows.slice(anchor.top, anchor.bottom + 1).map(snapshotRow).slice(0, SNAPSHOT_ROWS);
|
|
948
|
+
}
|
|
949
|
+
function promptSgrOf(rows, anchor) {
|
|
950
|
+
if (anchor === null || anchor.prompt === null)
|
|
951
|
+
return "";
|
|
952
|
+
const row = rows[anchor.prompt];
|
|
953
|
+
const after = row.slice(row.indexOf(PROMPT_GLYPH_NBSP) + PROMPT_GLYPH_NBSP.length);
|
|
954
|
+
return (/^(?:\x1b\[[0-9;]*m)*/.exec(after)?.[0] ?? "").replace(/\x1b/g, "").slice(0, 60);
|
|
955
|
+
}
|
|
956
|
+
export function inputBoxSnapshot(target) {
|
|
926
957
|
try {
|
|
927
958
|
const raw = tmux("capture-pane", "-p", "-e", "-t", target, "-S", `-${tailCaptureLines()}`);
|
|
928
959
|
const rows = raw.split("\n");
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
960
|
+
const anchor = findInputBox(rows);
|
|
961
|
+
const box = anchor === null
|
|
962
|
+
? null
|
|
963
|
+
: anchor.prompt === null
|
|
964
|
+
? { state: "unknown", text: "" }
|
|
965
|
+
: classifyInputBox(rows, anchor.prompt, anchor.bottom);
|
|
966
|
+
return { box, rows: snapshotRows(rows, anchor), promptSgr: promptSgrOf(rows, anchor) };
|
|
933
967
|
}
|
|
934
968
|
catch {
|
|
935
969
|
return null;
|
|
936
970
|
}
|
|
937
971
|
}
|
|
972
|
+
export const inputBoxState = (target) => inputBoxSnapshot(target)?.box ?? null;
|
|
938
973
|
const NOISE_ROW = /^[\s\u2500-\u257F\u2580-\u259F\u2800-\u28FF]*$/;
|
|
939
974
|
const FOOTER_KEEP = /esc to interrupt|\d+ (?:shell|background|task|monitor|local agent|team)s?\b|remote dynamic workflow|cloud session|ultraplan needs your input/i;
|
|
940
975
|
const OSC_SEQUENCE = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
package/dist/tools/agents.js
CHANGED
|
@@ -10,7 +10,8 @@ import { currentActor, findProjectForDir, getProject, linkedWorktreePrimaryRoot,
|
|
|
10
10
|
import { commandHead, harnessFor, harnessNames, readOnlyExtraArgsAllowed, paneClassifierFor, resolvedCommandPrefix, screenClassifiable, } from "../harnesses.js";
|
|
11
11
|
import { ensureWorkerHooksFile } from "../hooks.js";
|
|
12
12
|
import { activeProfile, allowedAgents, loadProjectYml } from "../projectYml.js";
|
|
13
|
-
import { classifyMiss, LEAD_MESSAGE_THRESHOLD, leadPointerMarker, missMessage, readLeadMessage, renderLeadPointer, senderTag, shortenedSendFailureClause, shortenedSendNote, storeLeadMessage, } from "../leadMessage.js";
|
|
13
|
+
import { classifyMiss, DELIVERY_FIELDS, LEAD_MESSAGE_THRESHOLD, leadPointerMarker, missMessage, readLeadMessage, renderLeadPointer, senderTag, shortenedSendFailureClause, shortenedSendNote, storeLeadMessage, } from "../leadMessage.js";
|
|
14
|
+
import { sendQuietLeadMessage } from "../leadMessageDelivery.js";
|
|
14
15
|
import { run } from "../result.js";
|
|
15
16
|
import { markGoneReported } from "../scheduler.js";
|
|
16
17
|
import { agentIdentityEnv, branchAt, closeAgentRow, currentOwnership, discardOrphanedPane, isReservedAgentName, isRunningLeadActor, killAgentPane, launchAgent, LEAD_KIND, PaneOwnershipLost, paneIdentity, parkAgentRow, reapCodexHomeForClosedAgent, requireStillOwned, releaseParkRow, renameAgent, resumeAgent, storedLaunchOptions, } from "../spawn.js";
|
|
@@ -1126,7 +1127,7 @@ export function registerAgents(server) {
|
|
|
1126
1127
|
};
|
|
1127
1128
|
}));
|
|
1128
1129
|
server.registerTool("agent_send", {
|
|
1129
|
-
description: "Type into an agent's terminal, addressed by name (or agent_id). text of any shape is prefixed with the sender tag, delivered as one bracketed paste and submitted with Enter unless submit=false. ONE EXCEPTION: text over 300 characters sent to a LEAD by anyone who is not that lead is stored and delivered as a one-line pointer instead, because a lead's pane is a human's own window; the receipt says so and names agent_message_get for the full text. Worker-bound text is never shortened at any length. Alternatively pass keys (tmux key names like Escape, C-c, Enter). wait_ms (250-10000) returns the terminal tail after sending. A claude worker is already briefed by agent_spawn. A worker whose screen hive cannot classify is REFUSED on the text path entirely (its brief is at the spawn receipt's brief_path); keys still reaches it. A pane in tmux copy mode is REFUSED too, and retriably: tmux clears its bracketed-paste flag there, so the paste would lose its markers and the Enter would be eaten - leave copy mode (or agent_send(keys: [\"-X\", \"cancel\"]) to cancel it deliberately) and send again.",
|
|
1130
|
+
description: "Type into an agent's terminal, addressed by name (or agent_id). text of any shape is prefixed with the sender tag, delivered as one bracketed paste and submitted with Enter unless submit=false. ONE EXCEPTION: text over 300 characters sent to a LEAD by anyone who is not that lead is stored and delivered as a one-line pointer instead, because a lead's pane is a human's own window; the receipt says so and names agent_message_get for the full text. Worker-bound text is never shortened at any length. With quiet_messaging on, a worker's submitted text to a Claude lead is stored with a message id and posted over the lead's messaging socket instead of typed (the same pointer when over 300 characters): the receipt says pending until the lead's prompt confirms it, hive types it into the pane once if no confirmation comes, and the socket post skips the pane checks below; do not resend a pending send. Alternatively pass keys (tmux key names like Escape, C-c, Enter). wait_ms (250-10000) returns the terminal tail after sending. A claude worker is already briefed by agent_spawn. A worker whose screen hive cannot classify is REFUSED on the text path entirely (its brief is at the spawn receipt's brief_path); keys still reaches it. A pane in tmux copy mode is REFUSED too, and retriably: tmux clears its bracketed-paste flag there, so the paste would lose its markers and the Enter would be eaten - leave copy mode (or agent_send(keys: [\"-X\", \"cancel\"]) to cancel it deliberately) and send again.",
|
|
1130
1131
|
annotations: {
|
|
1131
1132
|
readOnlyHint: false,
|
|
1132
1133
|
destructiveHint: true,
|
|
@@ -1151,6 +1152,7 @@ export function registerAgents(server) {
|
|
|
1151
1152
|
let outgoing = args.text ?? "";
|
|
1152
1153
|
const tag = senderTag(project.id, currentActor());
|
|
1153
1154
|
let shortened = null;
|
|
1155
|
+
let boxBefore = null;
|
|
1154
1156
|
const withShortened = (receipt) => shortened === null
|
|
1155
1157
|
? receipt
|
|
1156
1158
|
: {
|
|
@@ -1183,6 +1185,32 @@ export function registerAgents(server) {
|
|
|
1183
1185
|
"turning a text call into a keys call. Tab and newline are the only control characters " +
|
|
1184
1186
|
'allowed. To send an actual keystroke on purpose, use keys instead (e.g. keys: ["C-c"]).');
|
|
1185
1187
|
}
|
|
1188
|
+
const quiet = await sendQuietLeadMessage({
|
|
1189
|
+
projectId: project.id,
|
|
1190
|
+
fromActor: currentActor(),
|
|
1191
|
+
target: agent,
|
|
1192
|
+
text: args.text,
|
|
1193
|
+
submit: args.submit !== false,
|
|
1194
|
+
});
|
|
1195
|
+
if (quiet !== null) {
|
|
1196
|
+
if (quiet.sent) {
|
|
1197
|
+
confirmQueenWrite("agent_send", project.id, args, { sent: true, agent_id: agent.id }, auditIdentity);
|
|
1198
|
+
}
|
|
1199
|
+
if (args.wait_ms == null)
|
|
1200
|
+
return quiet;
|
|
1201
|
+
await sleep(Math.min(Math.max(args.wait_ms, 250), 10000));
|
|
1202
|
+
try {
|
|
1203
|
+
if (observeOwnership(agent) !== "live")
|
|
1204
|
+
throw new Error("not live");
|
|
1205
|
+
return { ...quiet, tail: capturePane(target, 15) };
|
|
1206
|
+
}
|
|
1207
|
+
catch {
|
|
1208
|
+
return {
|
|
1209
|
+
...quiet,
|
|
1210
|
+
tail_note: "The terminal tail could not be read after the wait (the pane may have died or changed owner).",
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1186
1214
|
if (!screenClassifiable(agent.command)) {
|
|
1187
1215
|
return {
|
|
1188
1216
|
agent_id: agent.id,
|
|
@@ -1235,7 +1263,9 @@ export function registerAgents(server) {
|
|
|
1235
1263
|
};
|
|
1236
1264
|
}
|
|
1237
1265
|
if (submitting) {
|
|
1238
|
-
const
|
|
1266
|
+
const snapshot = readInputBox(sendClassifier, target);
|
|
1267
|
+
const box = snapshot?.box ?? null;
|
|
1268
|
+
boxBefore = inputBoxBefore(snapshot);
|
|
1239
1269
|
if (holdsHumanInput(box)) {
|
|
1240
1270
|
return {
|
|
1241
1271
|
agent_id: agent.id,
|
|
@@ -1319,12 +1349,13 @@ export function registerAgents(server) {
|
|
|
1319
1349
|
sent: true,
|
|
1320
1350
|
...tailField,
|
|
1321
1351
|
...inputBoxField(agent.command, target),
|
|
1352
|
+
...(boxBefore === null ? {} : { input_box_before: boxBefore }),
|
|
1322
1353
|
});
|
|
1323
1354
|
}
|
|
1324
1355
|
return withShortened({ agent_id: agent.id, name: agent.name, sent: true });
|
|
1325
1356
|
}));
|
|
1326
1357
|
server.registerTool("agent_message_get", {
|
|
1327
|
-
description: "Read one agent-to-lead message in full, by the id in a \"[hive:worker NAME] [message #N ...]\" pointer line. hive stores a message here
|
|
1358
|
+
description: "Read one agent-to-lead message in full, by the id in a \"[hive:worker NAME] [message #N ...]\" pointer line. hive stores a message here when it shortens one (text over 300 characters sent to a lead by someone who is not that lead) and, with quiet_messaging on, for every submitted worker send to a Claude lead, whose result then also carries its delivery state (delivery_status, delivery_method, socket_attempt_at, confirmed_at, fallback_claimed_at, typed_at, delivery_note and the accepted target). Every other send is typed with its sender tag and stores nothing, so there is no id to read. Messages are pruned after 7 days, and a lookup for a pruned id says so rather than reporting it missing.",
|
|
1328
1359
|
annotations: {
|
|
1329
1360
|
readOnlyHint: false,
|
|
1330
1361
|
destructiveHint: false,
|
|
@@ -1349,6 +1380,9 @@ export function registerAgents(server) {
|
|
|
1349
1380
|
chars: row.text.length,
|
|
1350
1381
|
created_at: row.created_at,
|
|
1351
1382
|
text: row.text,
|
|
1383
|
+
...(row.delivery_status === null
|
|
1384
|
+
? {}
|
|
1385
|
+
: Object.fromEntries(DELIVERY_FIELDS.map((field) => [field, row[field]]))),
|
|
1352
1386
|
};
|
|
1353
1387
|
}));
|
|
1354
1388
|
server.registerTool("agent_output", {
|
|
@@ -1528,3 +1562,21 @@ export function registerAgents(server) {
|
|
|
1528
1562
|
};
|
|
1529
1563
|
}));
|
|
1530
1564
|
}
|
|
1565
|
+
function inputBoxBefore(snapshot) {
|
|
1566
|
+
if (snapshot === null)
|
|
1567
|
+
return { state: "unreadable" };
|
|
1568
|
+
const { box, rows, promptSgr } = snapshot;
|
|
1569
|
+
const sgr = promptSgr ? { prompt_sgr: promptSgr } : {};
|
|
1570
|
+
const withRows = rows === undefined ? {} : { rows };
|
|
1571
|
+
if (box === null)
|
|
1572
|
+
return { state: "not_found", ...withRows, ...sgr };
|
|
1573
|
+
if (box.state === "ghost")
|
|
1574
|
+
return { state: "ghost", ...sgr };
|
|
1575
|
+
return { state: box.state, text: box.text, ...withRows, ...sgr };
|
|
1576
|
+
}
|
|
1577
|
+
function readInputBox(classifier, target) {
|
|
1578
|
+
if (classifier.inputBoxSnapshot)
|
|
1579
|
+
return classifier.inputBoxSnapshot(target);
|
|
1580
|
+
const box = classifier.inputBoxState(target);
|
|
1581
|
+
return box === null ? null : { box };
|
|
1582
|
+
}
|