@cmgmyr/hive 1.10.0 → 1.10.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/claude-plugin/crew/model.mjs +1 -0
- package/dist/build-info.json +1 -1
- package/dist/spawn.js +44 -15
- package/dist/tmux.js +9 -6
- package/dist/tools/agents.js +35 -35
- package/package.json +1 -1
|
@@ -66,6 +66,7 @@ function workerRow(lane, snapshot, nowMs, memory, nextMemory) {
|
|
|
66
66
|
else {
|
|
67
67
|
const since = state === "blocked" ? null : w.state_changed_at;
|
|
68
68
|
head.push(`${state} ${duration(w, { label: state, since, lower_bound: false }, nowMs, memory, nextMemory)}`);
|
|
69
|
+
if (state !== "blocked" && w.activity?.label) head.push(w.activity.label);
|
|
69
70
|
}
|
|
70
71
|
const tail = [ago(w.age_seconds)];
|
|
71
72
|
if (w.commits_ahead !== null && w.commits_ahead !== undefined) tail.push(`+${w.commits_ahead} commits`);
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.10.
|
|
1
|
+
{"version":"1.10.1","sha":"47de74a","dirty":false,"build_id":"8d90ac219e940dac865c9770e28e1686dbed96d3d7ec219884aa4b6aa328dab5"}
|
package/dist/spawn.js
CHANGED
|
@@ -4,7 +4,7 @@ import { preservedRolloutPath, reapCodexHome } from "./codexHome.js";
|
|
|
4
4
|
import { getProject } from "./context.js";
|
|
5
5
|
import { dataDir, db } from "./db.js";
|
|
6
6
|
import { loadProjectYml } from "./projectYml.js";
|
|
7
|
-
import { applyLayout, claimInitialWindow, createWindow, DEFAULT_LAYOUT, ensureSession, crossServerRefusal, findProcessesWindow, findProjectWindow, isPaneTarget,
|
|
7
|
+
import { applyLayout, claimInitialWindow, createWindow, DEFAULT_LAYOUT, ensureSession, crossServerRefusal, findProcessesWindow, findProjectWindow, isPaneTarget, paneProcessExited, paneWindow, PROCESSES_LAYOUT, processesPaneTitle, processesWindowName, retainOnExitArgs, rowLive, sessionName, setPaneTitle, setRemainOnExit, makeProcessesWindow, tmux, tmuxSocketPath, untrustedTmuxServer, waitForPaneEstablished, windowLayout, windowOwner, windowTitle, rowOwnership, } from "./tmux.js";
|
|
8
8
|
export function withWindowClaim(claim) {
|
|
9
9
|
if (db.inTransaction) {
|
|
10
10
|
throw new Error("withWindowClaim was called from inside an already-open transaction. better-sqlite3 nests a " +
|
|
@@ -56,13 +56,21 @@ function recordPane(agentId, target, socket, pid) {
|
|
|
56
56
|
.prepare("UPDATE agents SET tmux_target = ?, tmux_socket = ?, pane_pid = ? WHERE id = ? AND status = 'running'")
|
|
57
57
|
.run(target, socket, pid, agentId).changes > 0);
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
// The creation pid proves the pane existed, not that its process still runs. A retained pane is
|
|
60
|
+
// left to the caller's readiness wait, which reports the exit with its tail.
|
|
61
|
+
function refuseIfExitedImmediately(target, retained) {
|
|
62
|
+
if (retained || paneProcessExited(target) !== true)
|
|
63
|
+
return;
|
|
64
|
+
discardOrphanedPane(target);
|
|
65
|
+
throw new Error(`The command exited immediately, so its pane ${target} was discarded rather than recorded as running. ` +
|
|
66
|
+
"Check the command and its arguments, then retry.");
|
|
67
|
+
}
|
|
68
|
+
export function createdPidOrDiscard(target, pid) {
|
|
69
|
+
if (/^\d+$/.test(pid))
|
|
62
70
|
return pid;
|
|
63
71
|
discardOrphanedPane(target);
|
|
64
|
-
throw new Error(`tmux did not report a process id for the new pane ${target}
|
|
65
|
-
"recorded: a row with no pane pid can never be verified as owning its pane. Retry.");
|
|
72
|
+
throw new Error(`tmux did not report a process id for the new pane ${target} when it created it, so it was discarded rather ` +
|
|
73
|
+
"than recorded: a row with no pane pid can never be verified as owning its pane. Retry.");
|
|
66
74
|
}
|
|
67
75
|
// Two callers, one meaning: whatever is left in the window re-tiles to the layout it was arranged
|
|
68
76
|
// with. agent_close kills a worker's pane through this; stopProcess (src/processes.ts) is the only
|
|
@@ -116,10 +124,11 @@ export function upsertActor(actorId, name, kind) {
|
|
|
116
124
|
db.prepare(`INSERT INTO actors (id, name, kind) VALUES (?, ?, ?)
|
|
117
125
|
ON CONFLICT(id) DO UPDATE SET name = excluded.name, last_seen_at = datetime('now')`).run(actorId, name, kind);
|
|
118
126
|
}
|
|
119
|
-
function placeAgentPane(session, spec, envFlags, commandString, title) {
|
|
127
|
+
function placeAgentPane(session, spec, envFlags, commandString, title, onRetainArmed = () => { }) {
|
|
120
128
|
let landedInProjectId = null;
|
|
121
129
|
let layoutApplied = false;
|
|
122
130
|
let inProcessesWindow = false;
|
|
131
|
+
let pid = "";
|
|
123
132
|
const target = withWindowClaim(() => {
|
|
124
133
|
const windowName = spec.placement === "split" ? spec.projectName : title;
|
|
125
134
|
const windowOwnerId = spec.placement === "split" ? spec.projectId : null;
|
|
@@ -127,10 +136,14 @@ function placeAgentPane(session, spec, envFlags, commandString, title) {
|
|
|
127
136
|
if (started.created) {
|
|
128
137
|
// Deliberate for placement "processes" too: this pane IS the new session's initial window, so it
|
|
129
138
|
// takes that window rather than a tile. The caller reports it; `hive hide` moves it in later.
|
|
139
|
+
pid = started.pid;
|
|
130
140
|
return claimInitialWindow(started, windowName, windowOwnerId).pane;
|
|
131
141
|
}
|
|
132
142
|
const splitInto = (window, layout) => {
|
|
133
|
-
const pane = tmux("split-window", "-d", "-P", "-F", "#{pane_id}", "-t", window, "-c", spec.cwd, ...envFlags, commandString, ...(spec.retainOnExit ? retainOnExitArgs(window) : []));
|
|
143
|
+
const [pane, created = ""] = tmux("split-window", "-d", "-P", "-F", "#{pane_id}\t#{pane_pid}", "-t", window, "-c", spec.cwd, ...envFlags, commandString, ...(spec.retainOnExit ? retainOnExitArgs(window) : [])).split("\t");
|
|
144
|
+
pid = created;
|
|
145
|
+
if (spec.retainOnExit)
|
|
146
|
+
onRetainArmed(window);
|
|
134
147
|
applyLayout(window, layout);
|
|
135
148
|
layoutApplied = true;
|
|
136
149
|
return pane;
|
|
@@ -143,6 +156,9 @@ function placeAgentPane(session, spec, envFlags, commandString, title) {
|
|
|
143
156
|
}
|
|
144
157
|
else {
|
|
145
158
|
const made = createWindow(session, processesWindowName(spec.projectName), spec.cwd, envFlags, commandString, null, true, spec.retainOnExit ?? false);
|
|
159
|
+
pid = made.pid;
|
|
160
|
+
if (spec.retainOnExit)
|
|
161
|
+
onRetainArmed(made.window);
|
|
146
162
|
makeProcessesWindow(made.window, spec.projectId);
|
|
147
163
|
pane = made.pane;
|
|
148
164
|
}
|
|
@@ -160,9 +176,13 @@ function placeAgentPane(session, spec, envFlags, commandString, title) {
|
|
|
160
176
|
return pane;
|
|
161
177
|
}
|
|
162
178
|
}
|
|
163
|
-
|
|
179
|
+
const made = createWindow(session, windowName, spec.cwd, envFlags, commandString, windowOwnerId, true, spec.retainOnExit ?? false);
|
|
180
|
+
pid = made.pid;
|
|
181
|
+
if (spec.retainOnExit)
|
|
182
|
+
onRetainArmed(made.window);
|
|
183
|
+
return made.pane;
|
|
164
184
|
});
|
|
165
|
-
return { target, landedInProjectId, layoutApplied, inProcessesWindow };
|
|
185
|
+
return { target, pid, landedInProjectId, layoutApplied, inProcessesWindow };
|
|
166
186
|
}
|
|
167
187
|
export function launchAgent(spec) {
|
|
168
188
|
if (untrustedTmuxServer())
|
|
@@ -183,6 +203,7 @@ export function launchAgent(spec) {
|
|
|
183
203
|
const agentId = Number(info.lastInsertRowid);
|
|
184
204
|
const actorId = `${spec.kind}:${agentId}`;
|
|
185
205
|
let paneUp = false;
|
|
206
|
+
let retainedWindow = null;
|
|
186
207
|
try {
|
|
187
208
|
const commandString = typeof spec.commandString === "string" ? spec.commandString : spec.commandString({ agentId, actorId });
|
|
188
209
|
db.prepare("UPDATE agents SET actor_id = ?, command = ? WHERE id = ?").run(actorId, commandString, agentId);
|
|
@@ -193,16 +214,22 @@ export function launchAgent(spec) {
|
|
|
193
214
|
: spec.env;
|
|
194
215
|
const envFlags = buildEnvFlags(env);
|
|
195
216
|
const title = windowTitle(spec.projectName, spec.name);
|
|
196
|
-
const
|
|
197
|
-
|
|
217
|
+
const placed = placeAgentPane(session, spec, envFlags, commandString, title, (window) => {
|
|
218
|
+
retainedWindow = window;
|
|
219
|
+
});
|
|
220
|
+
const { target, landedInProjectId, layoutApplied, inProcessesWindow } = placed;
|
|
221
|
+
const pid = createdPidOrDiscard(target, placed.pid);
|
|
222
|
+
refuseIfExitedImmediately(target, spec.retainOnExit ?? false);
|
|
198
223
|
paneUp = true;
|
|
199
224
|
if (!recordPane(agentId, target, socket, pid)) {
|
|
200
225
|
discardOrphanedPane(target);
|
|
201
226
|
throw paneRacedRetirement(agentId);
|
|
202
227
|
}
|
|
203
|
-
return { agentId, actorId, target, landedInProjectId, layoutApplied, inProcessesWindow };
|
|
228
|
+
return { agentId, actorId, target, landedInProjectId, layoutApplied, inProcessesWindow, retainedWindow };
|
|
204
229
|
}
|
|
205
230
|
catch (e) {
|
|
231
|
+
if (retainedWindow)
|
|
232
|
+
setRemainOnExit(retainedWindow, false);
|
|
206
233
|
if (paneUp)
|
|
207
234
|
throw e;
|
|
208
235
|
db.prepare("DELETE FROM agents WHERE id = ?").run(agentId);
|
|
@@ -269,8 +296,10 @@ export function resumeAgent(spec) {
|
|
|
269
296
|
});
|
|
270
297
|
const session = sessionName();
|
|
271
298
|
const title = windowTitle(spec.projectName, spec.name);
|
|
272
|
-
const
|
|
273
|
-
const
|
|
299
|
+
const placed = placeAgentPane(session, spec, envFlags, spec.commandString, title);
|
|
300
|
+
const { target, landedInProjectId } = placed;
|
|
301
|
+
const pid = createdPidOrDiscard(target, placed.pid);
|
|
302
|
+
refuseIfExitedImmediately(target, false);
|
|
274
303
|
paneUp = true;
|
|
275
304
|
if (!recordPane(spec.agentId, target, socket, pid)) {
|
|
276
305
|
discardOrphanedPane(target);
|
package/dist/tmux.js
CHANGED
|
@@ -184,16 +184,19 @@ function unsetAtSessionScope(name, envFlags) {
|
|
|
184
184
|
// HIVE_PROJECT_PATH at session scope, silently, for that session's whole life.
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
|
+
// The pid is read from the creating call because tmux 3.8 reports an empty #{pane_pid} once a pane's
|
|
188
|
+
// process has exited, so a later list-panes can lose the race against a command that dies at once.
|
|
189
|
+
export const CREATED_PANE_FORMAT = "#{pane_id}\t#{session_name}:#{window_id}\t#{pane_pid}";
|
|
187
190
|
export function ensureSession(name, cwd, initial) {
|
|
188
191
|
if (quietTmux("has-session", "-t", `=${name}`))
|
|
189
192
|
return { created: false };
|
|
190
193
|
if (untrustedTmuxServer())
|
|
191
194
|
throw crossServerRefusal("create a tmux session");
|
|
192
195
|
try {
|
|
193
|
-
const [pane, window] = tmux("new-session", "-d", "-P", "-F",
|
|
196
|
+
const [pane, window, pid = ""] = tmux("new-session", "-d", "-P", "-F", CREATED_PANE_FORMAT, "-s", name, "-c", cwd, ...(initial && "command" in initial ? [...initial.envFlags, initial.command] : [])).split("\t");
|
|
194
197
|
if (initial && "command" in initial)
|
|
195
198
|
unsetAtSessionScope(name, initial.envFlags);
|
|
196
|
-
return { created: true, pane, window };
|
|
199
|
+
return { created: true, pane, window, pid };
|
|
197
200
|
}
|
|
198
201
|
catch (e) {
|
|
199
202
|
if (!isDuplicateSession(e))
|
|
@@ -360,7 +363,7 @@ export function targetLiveProbe(target) {
|
|
|
360
363
|
const rows = tmux("list-panes", "-t", target, "-F", "#{pane_id} #{pane_pid}")
|
|
361
364
|
.split("\n")
|
|
362
365
|
.map((line) => line.split(" "));
|
|
363
|
-
const pid = isPaneTarget(target) ? (rows.find(([id]) => id === target)?.[1]
|
|
366
|
+
const pid = isPaneTarget(target) ? (rows.find(([id]) => id === target)?.[1] || null) : null;
|
|
364
367
|
return { live: true, pid };
|
|
365
368
|
}
|
|
366
369
|
catch (e) {
|
|
@@ -700,10 +703,10 @@ export function captureFinalScreen(target, lines) {
|
|
|
700
703
|
}
|
|
701
704
|
}
|
|
702
705
|
export function createWindow(session, windowName, cwd, envFlags, command, projectId, detach = false, retainOnExit = false) {
|
|
703
|
-
const created = tmux("new-window", ...(detach ? ["-d"] : []), "-P", "-F",
|
|
704
|
-
const [pane, window] = created.split("\t");
|
|
706
|
+
const created = tmux("new-window", ...(detach ? ["-d"] : []), "-P", "-F", CREATED_PANE_FORMAT, "-t", `=${session}`, "-n", windowName, "-c", cwd, ...envFlags, command, ...(retainOnExit ? retainOnExitArgs(`=${session}:${windowName}`) : []));
|
|
707
|
+
const [pane, window, pid = ""] = created.split("\t");
|
|
705
708
|
configureHiveWindow(window, true, projectId);
|
|
706
|
-
return { pane, window };
|
|
709
|
+
return { pane, window, pid };
|
|
707
710
|
}
|
|
708
711
|
function viewSessionChain(session, view) {
|
|
709
712
|
return [
|
package/dist/tools/agents.js
CHANGED
|
@@ -18,7 +18,7 @@ import { agentIdentityEnv, branchAt, closeAgentRow, currentOwnership, discardOrp
|
|
|
18
18
|
import { COMMAND_KIND, runningCommandRow, stopLine, stopProcess, STOP_REASONS } from "../processes.js";
|
|
19
19
|
import { readContextTokens, readContextFill, resolveTranscriptDir } from "../transcript.js";
|
|
20
20
|
import { leadSessionEnded } from "../leadState.js";
|
|
21
|
-
import { capturePane, captureFinalScreen, DEFAULT_LAYOUT, cancelCopyMode, describePaneChoice, ensureAttached, findUnsafeControlChar, holdsHumanInput, liveTargets, observationFailed, observeRowOwnership, ownershipLiveness, paneCurrentCommand, paneInCopyMode,
|
|
21
|
+
import { capturePane, captureFinalScreen, DEFAULT_LAYOUT, cancelCopyMode, describePaneChoice, ensureAttached, findUnsafeControlChar, holdsHumanInput, liveTargets, observationFailed, observeRowOwnership, ownershipLiveness, paneCurrentCommand, paneInCopyMode, pollPaneReadiness, rowOwnership, sendText, sessionName, setRemainOnExit, sleep, TEXT_ALLOWED_CONTROL_CHARS, tmux, TmuxTimeoutError, WINDOW_LAYOUTS, } from "../tmux.js";
|
|
22
22
|
import { agentIdParam, agentNameParam, idParam, projectIdParam } from "./params.js";
|
|
23
23
|
import { deriveProvenance, lastLogEvent, lastPermissionMode, reportsAgentStateLog, } from "../stateProvenance.js";
|
|
24
24
|
const CLOSED_ROW_ORDER = "(parked_at != '') DESC, closed_at DESC, id DESC";
|
|
@@ -599,48 +599,48 @@ export function registerAgents(server) {
|
|
|
599
599
|
rmSync(codexHomeDir(codexHomeKey), { recursive: true, force: true });
|
|
600
600
|
throw e;
|
|
601
601
|
}
|
|
602
|
-
const { agentId, actorId, target, landedInProjectId, layoutApplied } = spawned;
|
|
603
|
-
ensureAttached(sessionName());
|
|
602
|
+
const { agentId, actorId, target, landedInProjectId, layoutApplied, retainedWindow } = spawned;
|
|
604
603
|
let ready = false;
|
|
605
604
|
let dialogTail;
|
|
606
605
|
let exited = false;
|
|
607
606
|
let exitTail;
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
discardOrphanedPane(target);
|
|
624
|
-
}
|
|
625
|
-
else {
|
|
626
|
-
const { awaitingChoice, tail } = classifier.choiceCheck(target);
|
|
627
|
-
if (awaitingChoice === true) {
|
|
628
|
-
dialogTail = tail;
|
|
607
|
+
try {
|
|
608
|
+
ensureAttached(sessionName());
|
|
609
|
+
if (harness.classifiesPaneScreen) {
|
|
610
|
+
const classifier = harness.paneClassifier;
|
|
611
|
+
try {
|
|
612
|
+
const outcome = await pollPaneReadiness(target, PANE_READY_MS, classifier.hasInputBox);
|
|
613
|
+
if (outcome === "gone") {
|
|
614
|
+
// remain-on-exit held the pane so its final screen is still readable; capture it,
|
|
615
|
+
// persist it onto the row (the pane below is about to be killed), then release it.
|
|
616
|
+
// Never run choiceCheck against a retained-but-dead pane: it is frozen content, not
|
|
617
|
+
// a live dialog, and choiceCheck's own capture is tuned to read a live screen.
|
|
618
|
+
exited = true;
|
|
619
|
+
exitTail = captureFinalScreen(target, 30);
|
|
620
|
+
db.prepare("UPDATE agents SET exit_tail = ? WHERE id = ?").run(exitTail, agentId);
|
|
621
|
+
discardOrphanedPane(target);
|
|
629
622
|
}
|
|
630
|
-
else
|
|
631
|
-
|
|
623
|
+
else {
|
|
624
|
+
const { awaitingChoice, tail } = classifier.choiceCheck(target);
|
|
625
|
+
if (awaitingChoice === true) {
|
|
626
|
+
dialogTail = tail;
|
|
627
|
+
}
|
|
628
|
+
else if (outcome === "ready") {
|
|
629
|
+
ready = true;
|
|
630
|
+
}
|
|
632
631
|
}
|
|
633
632
|
}
|
|
633
|
+
catch {
|
|
634
|
+
ready = false;
|
|
635
|
+
}
|
|
634
636
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
setRemainOnExit(windowTarget, false);
|
|
643
|
-
}
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
// Unconditional: every early return, throw, or timeout between arming remain-on-exit
|
|
640
|
+
// and here must still clear it, or the shared window (the default split placement's)
|
|
641
|
+
// is left silently retaining every later exit in it, not just this one.
|
|
642
|
+
if (retainedWindow)
|
|
643
|
+
setRemainOnExit(retainedWindow, false);
|
|
644
644
|
}
|
|
645
645
|
const codexInstructions = codexHomeKey ? codexInstructionsPhrase(codexInstructionLayers) : undefined;
|
|
646
646
|
const receipt = {
|
package/package.json
CHANGED