@cmgmyr/hive 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,12 +10,14 @@ 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";
17
18
  import { COMMAND_KIND, runningCommandRow, stopLine, stopProcess, STOP_REASONS } from "../processes.js";
18
19
  import { readContextTokens, readContextFill, resolveTranscriptDir } from "../transcript.js";
20
+ import { leadSessionEnded } from "../leadState.js";
19
21
  import { capturePane, captureFinalScreen, DEFAULT_LAYOUT, cancelCopyMode, describePaneChoice, ensureAttached, findUnsafeControlChar, holdsHumanInput, liveTargets, observationFailed, observeRowOwnership, ownershipLiveness, paneCurrentCommand, paneInCopyMode, paneWindow, pollPaneReadiness, rowOwnership, sendText, sessionName, setRemainOnExit, sleep, TEXT_ALLOWED_CONTROL_CHARS, tmux, TmuxTimeoutError, WINDOW_LAYOUTS, } from "../tmux.js";
20
22
  import { agentIdParam, agentNameParam, idParam, projectIdParam } from "./params.js";
21
23
  import { deriveProvenance, lastLogEvent, lastPermissionMode, reportsAgentStateLog, } from "../stateProvenance.js";
@@ -356,7 +358,7 @@ function paneField(row, alive) {
356
358
  return { pane: describePaneChoice(awaitingChoice) };
357
359
  }
358
360
  function agentSummary(row, snapshot) {
359
- const alive = summaryLiveness(row, snapshot);
361
+ const { alive, dormant } = row.kind === LEAD_KIND ? leadSummaryLiveness(row, snapshot) : { alive: summaryLiveness(row, snapshot), dormant: null };
360
362
  const { state, ...provenance } = deriveProvenance(row, alive);
361
363
  return {
362
364
  agent_id: row.id,
@@ -377,7 +379,9 @@ function agentSummary(row, snapshot) {
377
379
  command: row.command,
378
380
  cwd: row.cwd,
379
381
  parent_actor_id: row.parent_actor_id,
382
+ todo_id: row.todo_id,
380
383
  created_at: row.created_at,
384
+ ...(dormant ? { dormant_since: dormant.ended_at } : {}),
381
385
  };
382
386
  }
383
387
  export function contextCheckpointEnv(config) {
@@ -418,6 +422,9 @@ export function registerAgents(server) {
418
422
  .describe("Spawn a known harness by name (e.g. \"codex\") instead of a raw command. Ignored when command is also given. Must be in the project's hive.yml agents: list (default: claude only)."),
419
423
  extra_args: z.array(z.string()).optional().describe("Extra CLI arguments."),
420
424
  read_only: z.boolean().optional().describe("Prevent local file writes and mutating shell commands while allowing Hive MCP tools. Only bare claude/codex executables. Extra args allow codex -c model_reasoning_effort=low|medium|high|xhigh, or claude --effort low|medium|high|xhigh|max; all other arguments are refused."),
425
+ todo_id: idParam
426
+ .optional()
427
+ .describe("The todo this worker is on, recorded as a link (a non-archived todo in the same project). Changes no todo status."),
421
428
  cwd: z
422
429
  .string()
423
430
  .optional()
@@ -467,6 +474,20 @@ export function registerAgents(server) {
467
474
  throw new Error(`cwd ${cwd} belongs to project "${cwdProject.name}" (id ${cwdProject.id}), but this spawn resolved to project "${project.name}" (id ${project.id}). ${remedy}`);
468
475
  }
469
476
  }
477
+ if (args.todo_id !== undefined) {
478
+ const linked = db
479
+ .prepare("SELECT archived_at FROM todos WHERE id = ? AND project_id = ?")
480
+ .get(args.todo_id, project.id);
481
+ if (!linked)
482
+ throw new Error(`todo_id ${args.todo_id} is not a todo in project "${project.name}" (id ${project.id}).`);
483
+ if (linked.archived_at != null)
484
+ throw new Error(`todo_id ${args.todo_id} is archived; unarchive it or link another todo.`);
485
+ }
486
+ const omittedTodoWarning = args.todo_id === undefined && db
487
+ .prepare("SELECT 1 FROM todos WHERE project_id = ? AND status = 'in_progress' AND archived_at IS NULL LIMIT 1")
488
+ .get(project.id)
489
+ ? "todo_id was omitted while this project has an in-progress todo; the crew view may infer a link from this worker's comments or pad updates."
490
+ : undefined;
470
491
  const name = args.name != null
471
492
  ? normalizeAgentName(args.name, "name")
472
493
  : nextWorkerName(project.id);
@@ -565,6 +586,7 @@ export function registerAgents(server) {
565
586
  model: args.model,
566
587
  extraArgs: args.extra_args,
567
588
  readOnly: args.read_only,
589
+ todoId: args.todo_id,
568
590
  retainOnExit: harness.classifiesPaneScreen,
569
591
  });
570
592
  }
@@ -621,7 +643,7 @@ export function registerAgents(server) {
621
643
  }
622
644
  }
623
645
  const codexInstructions = codexHomeKey ? codexInstructionsPhrase(codexInstructionLayers) : undefined;
624
- return {
646
+ const receipt = {
625
647
  agent_id: agentId,
626
648
  ...(args.read_only ? { read_only: true } : {}),
627
649
  actor_id: actorId,
@@ -670,6 +692,11 @@ export function registerAgents(server) {
670
692
  "Wakes aimed at this worker are refused for the same reason.",
671
693
  }),
672
694
  };
695
+ if (omittedTodoWarning) {
696
+ const existingNote = typeof receipt.note === "string" ? receipt.note : "";
697
+ receipt.note = existingNote ? `${existingNote} ${omittedTodoWarning}` : omittedTodoWarning;
698
+ }
699
+ return receipt;
673
700
  }));
674
701
  server.registerTool("agent_resume", {
675
702
  description: "Resume a CLOSED claude or codex worker from its recorded session id (claude --resume / codex resume): a fresh pane, the same actor_id, and the worker's full prior context. Addressed by name or agent_id among closed agents (agent_list(include_closed: true)). Send it its next instruction with agent_send once resumed - this tool does not.",
@@ -1126,7 +1153,7 @@ export function registerAgents(server) {
1126
1153
  };
1127
1154
  }));
1128
1155
  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.",
1156
+ 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
1157
  annotations: {
1131
1158
  readOnlyHint: false,
1132
1159
  destructiveHint: true,
@@ -1151,6 +1178,7 @@ export function registerAgents(server) {
1151
1178
  let outgoing = args.text ?? "";
1152
1179
  const tag = senderTag(project.id, currentActor());
1153
1180
  let shortened = null;
1181
+ let boxBefore = null;
1154
1182
  const withShortened = (receipt) => shortened === null
1155
1183
  ? receipt
1156
1184
  : {
@@ -1183,6 +1211,32 @@ export function registerAgents(server) {
1183
1211
  "turning a text call into a keys call. Tab and newline are the only control characters " +
1184
1212
  'allowed. To send an actual keystroke on purpose, use keys instead (e.g. keys: ["C-c"]).');
1185
1213
  }
1214
+ const quiet = await sendQuietLeadMessage({
1215
+ projectId: project.id,
1216
+ fromActor: currentActor(),
1217
+ target: agent,
1218
+ text: args.text,
1219
+ submit: args.submit !== false,
1220
+ });
1221
+ if (quiet !== null) {
1222
+ if (quiet.sent) {
1223
+ confirmQueenWrite("agent_send", project.id, args, { sent: true, agent_id: agent.id }, auditIdentity);
1224
+ }
1225
+ if (args.wait_ms == null)
1226
+ return quiet;
1227
+ await sleep(Math.min(Math.max(args.wait_ms, 250), 10000));
1228
+ try {
1229
+ if (observeOwnership(agent) !== "live")
1230
+ throw new Error("not live");
1231
+ return { ...quiet, tail: capturePane(target, 15) };
1232
+ }
1233
+ catch {
1234
+ return {
1235
+ ...quiet,
1236
+ tail_note: "The terminal tail could not be read after the wait (the pane may have died or changed owner).",
1237
+ };
1238
+ }
1239
+ }
1186
1240
  if (!screenClassifiable(agent.command)) {
1187
1241
  return {
1188
1242
  agent_id: agent.id,
@@ -1235,7 +1289,9 @@ export function registerAgents(server) {
1235
1289
  };
1236
1290
  }
1237
1291
  if (submitting) {
1238
- const box = sendClassifier.inputBoxState(target);
1292
+ const snapshot = readInputBox(sendClassifier, target);
1293
+ const box = snapshot?.box ?? null;
1294
+ boxBefore = inputBoxBefore(snapshot);
1239
1295
  if (holdsHumanInput(box)) {
1240
1296
  return {
1241
1297
  agent_id: agent.id,
@@ -1319,12 +1375,13 @@ export function registerAgents(server) {
1319
1375
  sent: true,
1320
1376
  ...tailField,
1321
1377
  ...inputBoxField(agent.command, target),
1378
+ ...(boxBefore === null ? {} : { input_box_before: boxBefore }),
1322
1379
  });
1323
1380
  }
1324
1381
  return withShortened({ agent_id: agent.id, name: agent.name, sent: true });
1325
1382
  }));
1326
1383
  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 only when it shortens one: text over 300 characters sent to a lead by someone who is not that lead. 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.",
1384
+ 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
1385
  annotations: {
1329
1386
  readOnlyHint: false,
1330
1387
  destructiveHint: false,
@@ -1349,6 +1406,9 @@ export function registerAgents(server) {
1349
1406
  chars: row.text.length,
1350
1407
  created_at: row.created_at,
1351
1408
  text: row.text,
1409
+ ...(row.delivery_status === null
1410
+ ? {}
1411
+ : Object.fromEntries(DELIVERY_FIELDS.map((field) => [field, row[field]]))),
1352
1412
  };
1353
1413
  }));
1354
1414
  server.registerTool("agent_output", {
@@ -1528,3 +1588,26 @@ export function registerAgents(server) {
1528
1588
  };
1529
1589
  }));
1530
1590
  }
1591
+ function inputBoxBefore(snapshot) {
1592
+ if (snapshot === null)
1593
+ return { state: "unreadable" };
1594
+ const { box, rows, promptSgr } = snapshot;
1595
+ const sgr = promptSgr ? { prompt_sgr: promptSgr } : {};
1596
+ const withRows = rows === undefined ? {} : { rows };
1597
+ if (box === null)
1598
+ return { state: "not_found", ...withRows, ...sgr };
1599
+ if (box.state === "ghost")
1600
+ return { state: "ghost", ...sgr };
1601
+ return { state: box.state, text: box.text, ...withRows, ...sgr };
1602
+ }
1603
+ function readInputBox(classifier, target) {
1604
+ if (classifier.inputBoxSnapshot)
1605
+ return classifier.inputBoxSnapshot(target);
1606
+ const box = classifier.inputBoxState(target);
1607
+ return box === null ? null : { box };
1608
+ }
1609
+ function leadSummaryLiveness(row, snapshot) {
1610
+ const ownership = row.status === "running" ? rowOwnership(row, snapshot) : null;
1611
+ const alive = ownership === null ? false : ownershipLiveness(ownership);
1612
+ return { alive, dormant: ownership === null ? null : leadSessionEnded(row, ownership) };
1613
+ }
@@ -112,9 +112,23 @@ function deliveryState(t, hasChannel) {
112
112
  ? "unconfirmed_busy"
113
113
  : "unconfirmed",
114
114
  delivery_method: t.delivery_method,
115
+ delivered_by: t.delivered_by,
115
116
  socket_attempt_at: t.socket_attempt_at,
116
117
  };
117
118
  }
119
+ function deliveredByNote(t) {
120
+ if (t.delivery_method === null)
121
+ return null;
122
+ if (t.delivered_by === null)
123
+ return "delivered by a hive build older than this field";
124
+ if (t.repeat_every_ms === null)
125
+ return null;
126
+ const firing = t.delivered_by.match(/, firing #(\d+)$/);
127
+ return firing !== null && Number(firing[1]) !== t.fire_count
128
+ ? `firing #${t.fire_count} was claimed by a hive build older than this field; ` +
129
+ "that build types it into the pane and does not record delivered_by"
130
+ : null;
131
+ }
118
132
  export const truncateBody = (body) => body.length > 120 ? `${cutToUnitBudget(body, 120)}…` : body;
119
133
  function rejectUnsafeBody(body) {
120
134
  const bad = findUnsafeControlChar(body, TEXT_ALLOWED_CONTROL_CHARS);
@@ -486,8 +500,13 @@ export function registerWakes(server) {
486
500
  server.registerTool("wake_get", {
487
501
  description: "Read one wake-up by id, in this project, with its UNTRUNCATED body. wake_list truncates " +
488
502
  "body at 120 chars; use this to see exactly what a wake will say, or to confirm what " +
489
- "wake_update just changed. Its delivery state includes delivery_method (socket, pty or " +
490
- "pty-after-socket-timeout) and socket_attempt_at, set when a wake was posted to a Claude lead's socket.",
503
+ "wake_update just changed. Its delivery state includes delivery_method (socket, socket-repeating, pty or " +
504
+ "pty-after-socket-timeout), delivered_by (the pid and build of the server that last set the delivery method, " +
505
+ "plus the firing number for repeating wakes), " +
506
+ "and socket_attempt_at, set when a wake was posted to a Claude lead's socket; " +
507
+ "for a repeating wake both describe its current firing (fire_count). An older repeat claim is " +
508
+ "identified in delivered_by_note as a claim; the older build types the firing into the pane " +
509
+ "without recording delivered_by.",
491
510
  annotations: {
492
511
  readOnlyHint: false,
493
512
  destructiveHint: false,
@@ -511,6 +530,7 @@ export function registerWakes(server) {
511
530
  repeating: t.repeat_every_ms != null,
512
531
  fire_count: t.fire_count,
513
532
  cancelled_at: t.cancelled_at,
533
+ delivered_by_note: deliveredByNote(t),
514
534
  first_held_at: t.first_held_at,
515
535
  ...deliveryState(t, hasChannel),
516
536
  };
@@ -626,7 +646,7 @@ export function registerWakes(server) {
626
646
  server.registerTool("wake_list", {
627
647
  description: "List pending wake-ups in this project, plus recently_delivered: the last " +
628
648
  `${RECENTLY_FIRED_LIMIT} one-shot wakes that have already fired, with their delivery ` +
629
- "state (typed_at, held_at/held_reason, confirmation, delivery_method, socket_attempt_at). A one-shot wake leaves the " +
649
+ "state (typed_at, held_at/held_reason, confirmation, delivery_method, delivered_by, socket_attempt_at). A one-shot wake leaves the " +
630
650
  "pending list the moment it fires; recently_delivered is where to check whether it " +
631
651
  "was actually typed and, if its target has a confirmation channel, acknowledged.",
632
652
  annotations: {
@@ -35,6 +35,9 @@ function usageTokens(record) {
35
35
  return tokenNumber(sum) && sum > 0 ? sum : null;
36
36
  }
37
37
  function tailRecords(path) {
38
+ return tailRead(path).records;
39
+ }
40
+ function tailRead(path) {
38
41
  let fd;
39
42
  try {
40
43
  fd = openSync(path, "r");
@@ -54,10 +57,10 @@ function tailRecords(path) {
54
57
  }
55
58
  catch { }
56
59
  }
57
- return records;
60
+ return { records, clipped: start > 0 };
58
61
  }
59
62
  catch {
60
- return [];
63
+ return { records: [], clipped: false };
61
64
  }
62
65
  finally {
63
66
  if (fd !== undefined)
@@ -124,3 +127,133 @@ export function readContextTokens(cwd, sessionId, explicitPath = "") {
124
127
  return null;
125
128
  return claudeTokens(tailRecords(transcriptPath(cwd, sessionId, explicitPath)));
126
129
  }
130
+ export const RECENT_TOOL_CALLS = 8;
131
+ export const SKILL_TOOL = "Skill";
132
+ export function lastComponent(name) {
133
+ return name.split(/__|[.:/]/).filter(Boolean).pop() ?? name;
134
+ }
135
+ function timestamp(record) {
136
+ return typeof record.timestamp === "string" && record.timestamp !== "" ? record.timestamp : null;
137
+ }
138
+ function reviewMarker(texts, at, skills) {
139
+ for (const skill of skills) {
140
+ const invocation = new RegExp(`^\\s*\\$${skill.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`, "m");
141
+ if (texts.some((text) => typeof text === "string" && invocation.test(text))) {
142
+ return [{ name: SKILL_TOOL, input: { skill }, at }];
143
+ }
144
+ }
145
+ return [];
146
+ }
147
+ function claudeToolCalls(record, options) {
148
+ if (record.type !== "assistant")
149
+ return [];
150
+ const content = object(record.message)?.content;
151
+ if (!Array.isArray(content))
152
+ return [];
153
+ const at = timestamp(record);
154
+ const calls = [];
155
+ for (const part of content) {
156
+ const block = object(part);
157
+ if (!block)
158
+ continue;
159
+ if (block.type === "tool_use" && typeof block.name === "string") {
160
+ calls.push({ name: block.name, input: object(block.input) ?? {}, at });
161
+ }
162
+ else if (block.type === "text") {
163
+ calls.push(...reviewMarker([block.text], at, options.reviewSkills ?? []));
164
+ }
165
+ }
166
+ return calls;
167
+ }
168
+ function stringLiteral(text) {
169
+ const quote = text[0];
170
+ if (quote !== '"' && quote !== "'")
171
+ return null;
172
+ const json = quote === '"' ? text : `"${text.slice(1, -1).replace(/\\'/g, "'").replace(/(\\.)|"/g, (m, esc) => esc ?? '\\"')}"`;
173
+ try {
174
+ const value = JSON.parse(json);
175
+ return typeof value === "string" ? value : null;
176
+ }
177
+ catch {
178
+ return null;
179
+ }
180
+ }
181
+ function execInnerCalls(source, at) {
182
+ const marks = [...source.matchAll(/\btools\.([A-Za-z_]\w*)\s*\(/g)];
183
+ if (marks.length === 0)
184
+ return null;
185
+ const calls = [];
186
+ marks.forEach((mark, i) => {
187
+ const name = lastComponent(mark[1]);
188
+ if (name === "write_stdin")
189
+ return;
190
+ if (name === "exec_command") {
191
+ const span = source.slice(mark.index + mark[0].length, marks[i + 1]?.index ?? source.length);
192
+ const cmd = /["']?\bcmd["']?\s*:\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/.exec(span);
193
+ const command = cmd ? stringLiteral(cmd[1]) : null;
194
+ if (command !== null)
195
+ calls.push({ name: "Bash", input: { command }, at });
196
+ return;
197
+ }
198
+ calls.push({ name, input: {}, at });
199
+ });
200
+ return calls;
201
+ }
202
+ function codexToolCalls(record, options) {
203
+ const payload = object(record.payload);
204
+ if (record.type !== "response_item" || !payload)
205
+ return [];
206
+ const at = timestamp(record);
207
+ if (payload.type === "message") {
208
+ if (payload.role !== "assistant" || !Array.isArray(payload.content))
209
+ return [];
210
+ return reviewMarker(payload.content.map((part) => object(part)?.text), at, options.reviewSkills ?? []);
211
+ }
212
+ if (typeof payload.name !== "string")
213
+ return [];
214
+ const name = lastComponent(payload.name);
215
+ if (payload.type === "custom_tool_call") {
216
+ if (name === "exec" && typeof payload.input === "string") {
217
+ const inner = execInnerCalls(payload.input, at);
218
+ if (inner)
219
+ return inner;
220
+ }
221
+ return [{ name, input: typeof payload.input === "string" ? { input: payload.input } : {}, at }];
222
+ }
223
+ if (payload.type !== "function_call")
224
+ return [];
225
+ if (name === "write_stdin" || name === "sleep")
226
+ return [];
227
+ let args = {};
228
+ if (typeof payload.arguments === "string") {
229
+ try {
230
+ args = object(JSON.parse(payload.arguments)) ?? {};
231
+ }
232
+ catch { }
233
+ }
234
+ if (name === "exec_command")
235
+ return typeof args.cmd === "string" ? [{ name: "Bash", input: { command: args.cmd }, at }] : [];
236
+ return [{ name, input: args, at }];
237
+ }
238
+ export function readRecentToolCalls(kind, worker, options = {}) {
239
+ const path = worker.transcript_path || (kind === "claude" && worker.session_id
240
+ ? join(transcriptDir(worker.cwd), `${worker.session_id}.jsonl`) : "");
241
+ if (!path)
242
+ return { calls: [], truncated: false };
243
+ const { records, clipped } = tailRead(path);
244
+ const newestFirst = [];
245
+ let capped = false;
246
+ for (const record of records) {
247
+ const calls = (kind === "claude" ? claudeToolCalls : codexToolCalls)(record, options);
248
+ for (let i = calls.length - 1; i >= 0; i--) {
249
+ if (newestFirst.length === RECENT_TOOL_CALLS) {
250
+ capped = true;
251
+ break;
252
+ }
253
+ newestFirst.push(calls[i]);
254
+ }
255
+ if (capped)
256
+ break;
257
+ }
258
+ return { calls: newestFirst.reverse(), truncated: clipped || capped };
259
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmgmyr/hive",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Run a crew of Claude Code and Codex workers from one lead session: visible tmux panes, exact state from hooks, one local SQLite store.",
5
5
  "type": "module",
6
6
  "bin": {