@cmgmyr/hive 1.11.0 → 1.11.2
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 +1 -0
- package/dist/build-info.json +1 -1
- package/dist/cli.js +13 -8
- package/dist/context.js +42 -4
- package/dist/dashboard.js +52 -0
- package/dist/db.js +8 -0
- package/dist/portfolio.js +6 -2
- package/dist/queenAudit.js +14 -2
- package/dist/queenDashboard.js +14 -8
- package/dist/questions.js +29 -4
- package/dist/scheduler.js +41 -1
- package/dist/staleServers.js +17 -4
- package/dist/tools/agents.js +2 -2
- package/dist/tools/queenAudit.js +1 -1
- package/dist/tools/questions.js +2 -1
- package/dist/tools/wakes.js +19 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,6 +70,7 @@ hive is a personal daily-driver tool, released low-key. It is single-user by des
|
|
|
70
70
|
| [Dashboard](docs/dashboard.md) | The generated dashboard: enabling it, where it lives, what it shows |
|
|
71
71
|
| [Install details](docs/install.md) | The interpreter pin, iTerm settings, the status line, MCP scope, codex workers, updating, uninstalling |
|
|
72
72
|
| [Troubleshooting](docs/troubleshooting.md) | Common errors and their fixes |
|
|
73
|
+
| [Security](docs/security.md) | What hive runs, writes and fetches on your machine, with the code to check |
|
|
73
74
|
| [Tools](docs/tools.md) | The MCP tools: what each does and when to use it |
|
|
74
75
|
| [tmux settings](docs/tmux.md) | Attach modes, pane options, and what to put in `~/.tmux.conf` |
|
|
75
76
|
| [Development](docs/development.md) | Building and testing hive itself |
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.11.
|
|
1
|
+
{"version":"1.11.2","sha":"6919b35","dirty":false,"build_id":"f1a0c66c3af90eb5af9a295a8b233d987cf4133f6355eea5cf46b87f5289e0bd"}
|
package/dist/cli.js
CHANGED
|
@@ -78,7 +78,7 @@ Usage:
|
|
|
78
78
|
snapshot; read-only, writes nothing
|
|
79
79
|
hive portfolio [--json] one row per registered project: lane, lead, workers,
|
|
80
80
|
todos, needs-human count, wakes; read-only
|
|
81
|
-
hive queen-audit
|
|
81
|
+
hive queen-audit queen writes and refused calls, newest first; 30-day history
|
|
82
82
|
[--project-id <id>] filter the target project (queen only across projects)
|
|
83
83
|
[--limit <n>] [--json] default 20, maximum 100; JSON prints one object
|
|
84
84
|
hive next [--print] attach to the lead of the project that needs you most
|
|
@@ -590,9 +590,10 @@ async function cmdLead(argv) {
|
|
|
590
590
|
try {
|
|
591
591
|
wonRace = db.transaction(() => {
|
|
592
592
|
const updated = db
|
|
593
|
-
.prepare("UPDATE agents SET tmux_target = ?, tmux_socket = ?, pane_pid = ?, command = ?, codex_home =
|
|
593
|
+
.prepare("UPDATE agents SET tmux_target = ?, tmux_socket = ?, pane_pid = ?, command = ?, codex_home = ?, " +
|
|
594
|
+
"pane_started_at = CASE WHEN ? = 1 THEN strftime('%Y-%m-%d %H:%M:%f', 'now') ELSE pane_started_at END " +
|
|
594
595
|
"WHERE id = ? AND tmux_target = ? AND tmux_socket = ? AND pane_pid = ? AND status = 'running'")
|
|
595
|
-
.run(leadPane, tmuxSocketPath(process.env.TMUX, process.env.TMUX_TMPDIR), recordedPanePid, recordedCommand, recordedCodexHome, leadAgentId, casExpected, casSocket, casPid).changes;
|
|
596
|
+
.run(leadPane, tmuxSocketPath(process.env.TMUX, process.env.TMUX_TMPDIR), recordedPanePid, recordedCommand, recordedCodexHome, createdPane ? 1 : 0, leadAgentId, casExpected, casSocket, casPid).changes;
|
|
596
597
|
if (updated === 0)
|
|
597
598
|
return false;
|
|
598
599
|
db.prepare(`UPDATE wakes SET deliver_pane = ?, held_at = NULL, held_reason = NULL
|
|
@@ -2857,19 +2858,20 @@ function canonicalPath(path) {
|
|
|
2857
2858
|
function nextCandidates() {
|
|
2858
2859
|
const queenRoot = canonicalPath(join(storeDir(), "queen"));
|
|
2859
2860
|
const byCount = (a, b) => b - a;
|
|
2861
|
+
const waitsOnYou = (p) => p.needs_human + p.questions_pending;
|
|
2860
2862
|
return collectPortfolio()
|
|
2861
|
-
.projects.filter((p) => (p.lane === "waiting_on_you" || p.lane === "stuck" || (p.lane === "moving" && p
|
|
2863
|
+
.projects.filter((p) => (p.lane === "waiting_on_you" || p.lane === "stuck" || (p.lane === "moving" && waitsOnYou(p) > 0)) &&
|
|
2862
2864
|
canonicalPath(p.root) !== queenRoot)
|
|
2863
2865
|
.sort((a, b) => {
|
|
2864
|
-
const tier = (p) => (p.lane === "waiting_on_you" ? 0 : p
|
|
2866
|
+
const tier = (p) => (p.lane === "waiting_on_you" ? 0 : waitsOnYou(p) > 0 ? 1 : 2);
|
|
2865
2867
|
if (tier(a) !== tier(b))
|
|
2866
2868
|
return tier(a) - tier(b);
|
|
2867
2869
|
if (a.lane === "waiting_on_you") {
|
|
2868
|
-
return (byCount(a
|
|
2870
|
+
return (byCount(waitsOnYou(a), waitsOnYou(b)) ||
|
|
2869
2871
|
a.last_activity_at.localeCompare(b.last_activity_at) ||
|
|
2870
2872
|
a.id - b.id);
|
|
2871
2873
|
}
|
|
2872
|
-
return (byCount(a
|
|
2874
|
+
return (byCount(waitsOnYou(a), waitsOnYou(b)) ||
|
|
2873
2875
|
byCount(a.todos.blocked_in_progress, b.todos.blocked_in_progress) ||
|
|
2874
2876
|
byCount(a.wakes.overdue, b.wakes.overdue) ||
|
|
2875
2877
|
byCount(a.workers.needs_input, b.workers.needs_input) ||
|
|
@@ -2948,7 +2950,7 @@ function cmdQueenAudit(argv) {
|
|
|
2948
2950
|
return;
|
|
2949
2951
|
}
|
|
2950
2952
|
if (report.entries.length === 0) {
|
|
2951
|
-
console.log("No queen
|
|
2953
|
+
console.log("No queen audit entries recorded.");
|
|
2952
2954
|
return;
|
|
2953
2955
|
}
|
|
2954
2956
|
for (const entry of report.entries) {
|
|
@@ -3371,6 +3373,9 @@ function cmdAnswer(argv) {
|
|
|
3371
3373
|
if (receipt.wake_id === null) {
|
|
3372
3374
|
console.log(`No wake queued: ${receipt.wake_skipped ?? "the asking session could not be reached"}.`);
|
|
3373
3375
|
}
|
|
3376
|
+
else if (receipt.wake_rerouted_to) {
|
|
3377
|
+
console.log(`Wake #${receipt.wake_id} queued for ${receipt.wake_rerouted_to}; the asker is not running.`);
|
|
3378
|
+
}
|
|
3374
3379
|
else {
|
|
3375
3380
|
console.log(`Wake #${receipt.wake_id} queued for the asking session.`);
|
|
3376
3381
|
}
|
package/dist/context.js
CHANGED
|
@@ -5,6 +5,8 @@ import { basename, dirname, join, resolve, sep } from "node:path";
|
|
|
5
5
|
import { storeDir } from "./dataDir.js";
|
|
6
6
|
import { db } from "./db.js";
|
|
7
7
|
import { LEAD_KIND } from "./spawn.js";
|
|
8
|
+
import { rowOwnership } from "./tmux.js";
|
|
9
|
+
import { recordQueenRefusal } from "./queenAudit.js";
|
|
8
10
|
let selectedId = null;
|
|
9
11
|
let cachedActorId = null;
|
|
10
12
|
let lastTouchMs = 0;
|
|
@@ -294,6 +296,30 @@ export function captureQueenWriteIdentity(targetProjectId) {
|
|
|
294
296
|
return null;
|
|
295
297
|
return { actor_id: process.env.HIVE_AGENT_ID, home_project_id: home };
|
|
296
298
|
}
|
|
299
|
+
export function resolveQueenLeadTarget(operation, targetProjectId, ref, resolve) {
|
|
300
|
+
if (!captureQueenWriteIdentity(targetProjectId))
|
|
301
|
+
return resolve();
|
|
302
|
+
let agent;
|
|
303
|
+
try {
|
|
304
|
+
agent = resolve();
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
throw queenRefusal(operation, targetProjectId, `recipient ${JSON.stringify(ref)} could not be resolved: ${e instanceof Error ? e.message : String(e)}`);
|
|
308
|
+
}
|
|
309
|
+
const ownership = rowOwnership(agent);
|
|
310
|
+
const problem = agent.project_id !== targetProjectId
|
|
311
|
+
? `Agent ${agent.id} ("${agent.name}") belongs to project ${agent.project_id}, not project ${targetProjectId}`
|
|
312
|
+
: agent.kind !== LEAD_KIND
|
|
313
|
+
? `Agent ${agent.id} ("${agent.name}") is kind "${agent.kind}", not a lead`
|
|
314
|
+
: agent.status !== "running"
|
|
315
|
+
? `Agent ${agent.id} ("${agent.name}") is ${agent.status}, not running`
|
|
316
|
+
: ownership !== "live"
|
|
317
|
+
? `Agent ${agent.id} ("${agent.name}") does not own a live pane (ownership: ${ownership})`
|
|
318
|
+
: null;
|
|
319
|
+
if (problem)
|
|
320
|
+
throw queenRefusal(operation, targetProjectId, problem);
|
|
321
|
+
return agent;
|
|
322
|
+
}
|
|
297
323
|
export const QUEEN_REACH = {
|
|
298
324
|
whoami: "read",
|
|
299
325
|
help: "read",
|
|
@@ -379,7 +405,7 @@ function foreignWriteProblem(reach, targetProjectId, args) {
|
|
|
379
405
|
const lead = runningLead(targetProjectId);
|
|
380
406
|
if (!lead)
|
|
381
407
|
return "that project has no running lead to address";
|
|
382
|
-
if (
|
|
408
|
+
if (args.deliver_to == null) {
|
|
383
409
|
return `deliver_to must name that project's running lead ("${lead.name}" or agent id ${lead.id})`;
|
|
384
410
|
}
|
|
385
411
|
return null;
|
|
@@ -402,8 +428,8 @@ function foreignWriteProblem(reach, targetProjectId, args) {
|
|
|
402
428
|
const lead = runningLead(targetProjectId);
|
|
403
429
|
if (!lead)
|
|
404
430
|
return "that project has no running lead to address";
|
|
405
|
-
if (
|
|
406
|
-
return `the recipient must
|
|
431
|
+
if (args.agent_id == null && args.name == null) {
|
|
432
|
+
return `the recipient must name that project's running lead ("${lead.name}" or agent id ${lead.id})`;
|
|
407
433
|
}
|
|
408
434
|
return null;
|
|
409
435
|
}
|
|
@@ -446,8 +472,20 @@ export function assertQueenToolCall(tool, args) {
|
|
|
446
472
|
function queenRefusal(operation, targetProjectId, problem) {
|
|
447
473
|
const target = targetProjectId === null ? null : getProject(targetProjectId);
|
|
448
474
|
const where = target ? `in project ${target.id} ("${target.name}")` : "outside the queen's own project";
|
|
449
|
-
|
|
475
|
+
const refusal = new Error(`${QUEEN_REFUSAL}: the queen cannot run ${operation} ${where}: ${problem}. Into another project the ` +
|
|
450
476
|
"queen may only todo_create, todo_comment, send text or wakes to its running lead, start its lead " +
|
|
451
477
|
"with `hive lead <path>`, and run project_prune or `hive project rm` on it. Everything else there is its lead's to do.");
|
|
478
|
+
if (targetProjectId !== null) {
|
|
479
|
+
const identity = captureQueenWriteIdentity(targetProjectId);
|
|
480
|
+
if (identity) {
|
|
481
|
+
try {
|
|
482
|
+
recordQueenRefusal(operation, targetProjectId, problem, identity);
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
// A refusal must survive an unavailable audit write.
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return refusal;
|
|
452
490
|
}
|
|
453
491
|
export const HUMAN_ACTOR_PREFIX = "user:";
|
package/dist/dashboard.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { db } from "./db.js";
|
|
3
4
|
import { getProject } from "./context.js";
|
|
4
5
|
export const describeVisibility = (visibility) => visibility === null ? "running" : visibility === "window" ? "own window" : visibility;
|
|
@@ -12,6 +13,7 @@ export function processCounts(procs) {
|
|
|
12
13
|
}
|
|
13
14
|
import { awaitingFirstPrompt } from "./firstPrompt.js";
|
|
14
15
|
import { SLUG_MAX_LEN, cutToUnitBudget } from "./slug.js";
|
|
16
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
15
17
|
export const TODO_CAP = 100;
|
|
16
18
|
export const AGENT_CAP = 50;
|
|
17
19
|
export const WAKE_CAP = 50;
|
|
@@ -449,6 +451,45 @@ function renderWakesSection(index, projectId) {
|
|
|
449
451
|
const body = renderCappedList(shown.length, all.length, "pending wakes", "wakes", rows, emptyState("No pending wakes.", "Scheduled and idle-triggered check-ins from wake_set show up here."));
|
|
450
452
|
return section(index, "wakes", "Wakes", `${all.length} pending`, "bell", String(shown.length), body);
|
|
451
453
|
}
|
|
454
|
+
function questionAge(createdAt) {
|
|
455
|
+
const timestamp = Date.parse(`${createdAt.replace(" ", "T")}Z`);
|
|
456
|
+
if (!Number.isFinite(timestamp))
|
|
457
|
+
return "unknown";
|
|
458
|
+
const seconds = Math.max(0, Math.floor((Date.now() - timestamp) / 1000));
|
|
459
|
+
if (seconds >= 86400)
|
|
460
|
+
return `${Math.floor(seconds / 86400)}d ago`;
|
|
461
|
+
if (seconds >= 3600)
|
|
462
|
+
return `${Math.floor(seconds / 3600)}h ago`;
|
|
463
|
+
if (seconds >= 60)
|
|
464
|
+
return `${Math.floor(seconds / 60)}m ago`;
|
|
465
|
+
return "now";
|
|
466
|
+
}
|
|
467
|
+
function renderQuestionsSection(index, projectId) {
|
|
468
|
+
// A static import of questions.js cycles through tools/wakes.ts and scheduler.ts.
|
|
469
|
+
const { listQuestions, pendingQuestionCount } = requireFromHere("./questions.js");
|
|
470
|
+
const questions = listQuestions(projectId);
|
|
471
|
+
if (questions.length === 0)
|
|
472
|
+
return "";
|
|
473
|
+
const pendingCount = pendingQuestionCount(projectId);
|
|
474
|
+
const rows = questions.map((question) => {
|
|
475
|
+
const options = question.kind === "free_text"
|
|
476
|
+
? '<p class="question-options">free text</p>'
|
|
477
|
+
: `<ul class="question-options">${(question.options ?? []).map((option) => `<li>${prose(option.label)}${option.recommended ? ' <span class="recommended">recommended</span>' : ""}` +
|
|
478
|
+
`<span class="question-option-description">${prose(option.description)}</span></li>`).join("")}</ul>`;
|
|
479
|
+
const ref = question.ref ? `<span class="question-ref">${question.ref.kind === "todo" ? "todo" : "PR"} #${question.ref.number}</span>` : "";
|
|
480
|
+
const state = question.state === "defaulted"
|
|
481
|
+
? '<span class="question-state">defaulted - proceeding on the default</span>'
|
|
482
|
+
: '<span class="question-state">pending</span>';
|
|
483
|
+
return (`<li class="question" data-question="${question.id}"><div class="question-head">${mono(`#${question.id}`)} ` +
|
|
484
|
+
`<strong>${escapeHtml(question.header)}</strong> ${state}</div>` +
|
|
485
|
+
`<p class="question-text">${prose(question.question)}</p>${options}` +
|
|
486
|
+
`<p class="question-default">Default answer: ${prose(question.default_answer)}</p>` +
|
|
487
|
+
`<p class="question-meta">Asked by ${mono(question.asker)} · ${escapeHtml(questionAge(question.created_at))}` +
|
|
488
|
+
`${ref ? ` · ${ref}` : ""}</p>` +
|
|
489
|
+
`<p class="question-answer">To answer: <code>hive answer ${question.id}</code></p></li>`);
|
|
490
|
+
}).join("\n");
|
|
491
|
+
return section(index, "questions", "Questions", `${pendingCount} pending`, "inbox", String(pendingCount), `<ul class="question-list">${rows}</ul>`);
|
|
492
|
+
}
|
|
452
493
|
function fetchCommentActivity(projectId) {
|
|
453
494
|
const rows = db
|
|
454
495
|
.prepare(`SELECT c.id, c.body, c.author, c.created_at, t.id AS todo_id, t.title AS todo_title,
|
|
@@ -682,6 +723,15 @@ export const SECTION_STYLE = `
|
|
|
682
723
|
.priority-low { color: var(--fg-subtle); }
|
|
683
724
|
.blockers { font-size: 0.8125rem; color: var(--fg-muted); margin: 0 0 0.45rem 0.6rem; }
|
|
684
725
|
.wake-body { color: var(--fg); font-size: 0.875rem; overflow-wrap: anywhere; }
|
|
726
|
+
.question-list { display: grid; gap: 0.85rem; list-style: none; margin: 0; padding: 0; }
|
|
727
|
+
.question { border-bottom: 1px solid var(--border); padding-bottom: 0.85rem; overflow-wrap: anywhere; }
|
|
728
|
+
.question:last-child { border-bottom: 0; padding-bottom: 0; }
|
|
729
|
+
.question-head, .question-meta, .question-answer, .question-default, .question-text, .question-options { margin: 0.2rem 0; }
|
|
730
|
+
.question-head, .question-meta, .question-answer { font-size: 0.8125rem; }
|
|
731
|
+
.question-state, .question-ref, .recommended { color: var(--fg-muted); font-size: 0.75rem; }
|
|
732
|
+
.question-options { padding-left: 1.2rem; }
|
|
733
|
+
.question-option-description { display: block; padding-left: 0.5rem; color: var(--fg-muted); }
|
|
734
|
+
.question-text { overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
685
735
|
.activity-body { font-size: 0.8125rem; color: var(--fg-muted); white-space: pre-wrap;
|
|
686
736
|
overflow-wrap: anywhere; margin-top: 0.1rem; }
|
|
687
737
|
|
|
@@ -1171,7 +1221,9 @@ function buildDashboard(projectId, procs) {
|
|
|
1171
1221
|
const project = fetchProject(projectId);
|
|
1172
1222
|
const stats = fetchDayStats(projectId);
|
|
1173
1223
|
const index = [];
|
|
1224
|
+
const questionsSection = renderQuestionsSection(index, projectId);
|
|
1174
1225
|
const cards = [
|
|
1226
|
+
...(questionsSection ? [questionsSection] : []),
|
|
1175
1227
|
renderThroughputSection(index, stats),
|
|
1176
1228
|
renderBoardSection(index, projectId),
|
|
1177
1229
|
renderTodosSection(index, projectId),
|
package/dist/db.js
CHANGED
|
@@ -568,6 +568,14 @@ CREATE TABLE questions (
|
|
|
568
568
|
CREATE UNIQUE INDEX idx_questions_pending_ref ON questions(project_id, ref_kind, ref_number)
|
|
569
569
|
WHERE state = 'pending' AND ref_kind IS NOT NULL;
|
|
570
570
|
CREATE INDEX idx_questions_project_state ON questions(project_id, state, id);
|
|
571
|
+
`,
|
|
572
|
+
`
|
|
573
|
+
ALTER TABLE queen_audit ADD COLUMN outcome TEXT NOT NULL DEFAULT 'confirmed'
|
|
574
|
+
CHECK (outcome IN ('confirmed', 'refused'));
|
|
575
|
+
ALTER TABLE queen_audit ADD COLUMN problem TEXT NOT NULL DEFAULT '';
|
|
576
|
+
`,
|
|
577
|
+
`
|
|
578
|
+
ALTER TABLE agents ADD COLUMN pane_started_at TEXT;
|
|
571
579
|
`,
|
|
572
580
|
];
|
|
573
581
|
export function storeSchemaAhead(database) {
|
package/dist/portfolio.js
CHANGED
|
@@ -8,6 +8,7 @@ import { leadSessionEnded, readLeadTurnState } from "./leadState.js";
|
|
|
8
8
|
import { liveTargets, ownershipLiveness, rowOwnership } from "./tmux.js";
|
|
9
9
|
export const PORTFOLIO_REASONS = [
|
|
10
10
|
"needs_human",
|
|
11
|
+
"questions_pending",
|
|
11
12
|
"dead_lead_pane",
|
|
12
13
|
"missing_root_with_work",
|
|
13
14
|
"worker_needs_input",
|
|
@@ -136,9 +137,12 @@ function projectRow(project, now, snapshot) {
|
|
|
136
137
|
const staleClock = lastActivity(project.id, false);
|
|
137
138
|
const staleBefore = shiftedClock(now, STALE_IN_PROGRESS);
|
|
138
139
|
const activeTodos = todos.open + todos.in_progress;
|
|
140
|
+
const questionsPending = pendingQuestionCount(project.id);
|
|
139
141
|
const found = new Set();
|
|
140
142
|
if (tagged.length > 0)
|
|
141
143
|
found.add("needs_human");
|
|
144
|
+
if (questionsPending > 0)
|
|
145
|
+
found.add("questions_pending");
|
|
142
146
|
if ((lead.state === "dead_pane" || lead.state === "dormant") && (todos.in_progress > 0 || wakeCounts.pending > 0))
|
|
143
147
|
found.add("dead_lead_pane");
|
|
144
148
|
if (!rootExists && (activeTodos > 0 || wakeCounts.pending > 0))
|
|
@@ -173,7 +177,7 @@ function projectRow(project, now, snapshot) {
|
|
|
173
177
|
lane = "stuck";
|
|
174
178
|
else if (movingSignal)
|
|
175
179
|
lane = "moving";
|
|
176
|
-
else if (found.has("needs_human"))
|
|
180
|
+
else if (found.has("needs_human") || found.has("questions_pending"))
|
|
177
181
|
lane = "waiting_on_you";
|
|
178
182
|
else if (TODO_GRAPH.some((r) => found.has(r)))
|
|
179
183
|
lane = "stuck";
|
|
@@ -192,7 +196,7 @@ function projectRow(project, now, snapshot) {
|
|
|
192
196
|
workers,
|
|
193
197
|
todos,
|
|
194
198
|
needs_human: tagged.length,
|
|
195
|
-
questions_pending:
|
|
199
|
+
questions_pending: questionsPending,
|
|
196
200
|
needs_human_items: tagged.slice(0, NEEDS_HUMAN_ITEM_CAP).map((t) => ({
|
|
197
201
|
todo_id: t.id,
|
|
198
202
|
title: t.title,
|
package/dist/queenAudit.js
CHANGED
|
@@ -13,6 +13,11 @@ function removalSummary(r) {
|
|
|
13
13
|
const counts = Object.entries((r.counts ?? {})).map(([t, n]) => `${t} ${n}`);
|
|
14
14
|
return `removed project "${r.name}"${counts.length > 0 ? ` with ${counts.join(", ")}` : ""}`;
|
|
15
15
|
}
|
|
16
|
+
function insertQueenAuditRow(operation, targetProjectId, identity, resourceType, resourceId, summary, outcome = "confirmed", problem = "") {
|
|
17
|
+
db.prepare(`INSERT INTO queen_audit
|
|
18
|
+
(actor_id, home_project_id, target_project_id, operation, resource_type, resource_id, summary, outcome, problem)
|
|
19
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(identity.actor_id, identity.home_project_id, targetProjectId, operation, resourceType, resourceId, summary, outcome, problem);
|
|
20
|
+
}
|
|
16
21
|
export const QUEEN_AUDIT_OPERATIONS = {
|
|
17
22
|
todo_create: {
|
|
18
23
|
mode: "database",
|
|
@@ -78,8 +83,15 @@ function insertAudit(operation, targetProjectId, args, receipt, identity) {
|
|
|
78
83
|
return;
|
|
79
84
|
const text = flatten(spec.summary(args, receipt));
|
|
80
85
|
const summary = text.length <= QUEEN_AUDIT_SUMMARY_MAX ? text : cutToUnitBudget(text, QUEEN_AUDIT_SUMMARY_MAX - 1) + "…";
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
insertQueenAuditRow(operation, targetProjectId, identity, touched.type, touched.id, summary);
|
|
87
|
+
}
|
|
88
|
+
export function recordQueenRefusal(operation, targetProjectId, problem, identity) {
|
|
89
|
+
const text = flatten(problem);
|
|
90
|
+
const summaryText = `refused: ${text}`;
|
|
91
|
+
const summary = summaryText.length <= QUEEN_AUDIT_SUMMARY_MAX
|
|
92
|
+
? summaryText
|
|
93
|
+
: cutToUnitBudget(summaryText, QUEEN_AUDIT_SUMMARY_MAX - 1) + "…";
|
|
94
|
+
insertQueenAuditRow(operation, targetProjectId, identity, "project", targetProjectId, summary, "refused", text);
|
|
83
95
|
}
|
|
84
96
|
export function commitQueenStateWrite(operation, targetProjectId, args, mutate) {
|
|
85
97
|
const identity = captureQueenWriteIdentity(targetProjectId);
|
package/dist/queenDashboard.js
CHANGED
|
@@ -84,6 +84,7 @@ const LANE_COLOR = {
|
|
|
84
84
|
};
|
|
85
85
|
const REASON_LABEL = {
|
|
86
86
|
needs_human: "Needs-human todo",
|
|
87
|
+
questions_pending: "Pending question",
|
|
87
88
|
dead_lead_pane: "Lead pane is dead",
|
|
88
89
|
missing_root_with_work: "Project folder is missing",
|
|
89
90
|
worker_needs_input: "A worker is waiting for input",
|
|
@@ -210,10 +211,12 @@ function footCounts(p, asOf, rank) {
|
|
|
210
211
|
(rank ? `<span class="age">seen ${esc(age(p.last_activity_at, asOf))}</span>` : ""));
|
|
211
212
|
}
|
|
212
213
|
function movingBadges(p) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
const questions = p.questions_pending > 0
|
|
215
|
+
? `<span class="n-questions">${p.questions_pending} question${p.questions_pending === 1 ? "" : "s"}</span>`
|
|
216
|
+
: "";
|
|
217
|
+
return ((p.lane === "moving" && p.needs_human > 0 ? `<span class="n-needs">${p.needs_human} need you</span>` : "") +
|
|
218
|
+
questions +
|
|
219
|
+
(p.lane === "moving" && p.todos.blocked_in_progress > 0 ? `<span class="n-muted">${p.todos.blocked_in_progress} blocked</span>` : ""));
|
|
217
220
|
}
|
|
218
221
|
function card(p, report, rank, href) {
|
|
219
222
|
const asOf = report.as_of;
|
|
@@ -226,7 +229,7 @@ function card(p, report, rank, href) {
|
|
|
226
229
|
items.push(`<li><span class="ref">+${p.needs_human - p.needs_human_items.length} more</span></li>`);
|
|
227
230
|
}
|
|
228
231
|
for (const r of p.reasons) {
|
|
229
|
-
if (r === "needs_human" || r === "quiet")
|
|
232
|
+
if (r === "needs_human" || r === "questions_pending" || r === "quiet")
|
|
230
233
|
continue;
|
|
231
234
|
const cls = REASON_SEVERE.has(r) ? " age-fail" : "";
|
|
232
235
|
items.push(`<li><span class="${cls.trim()}">${esc(REASON_LABEL[r])}</span></li>`);
|
|
@@ -263,19 +266,22 @@ function gridRow(p, asOf, href) {
|
|
|
263
266
|
`<span class="c-work">${workersCell(p)}</span>` +
|
|
264
267
|
`<span class="c-n g-num">${num(t.open)}</span><span class="c-n g-num">${num(t.in_progress)}</span>` +
|
|
265
268
|
`<span class="c-n g-num">${num(t.blocked, "n-blocked")}</span><span class="c-n g-num">${num(t.high, "n-high")}</span>` +
|
|
266
|
-
`<span class="c-n g-num">${num(p.needs_human, "n-needs")}</span
|
|
269
|
+
`<span class="c-n g-num">${num(p.needs_human, "n-needs")}</span>` +
|
|
270
|
+
`<span class="c-n g-num">${num(p.questions_pending, "n-questions")}</span>` +
|
|
271
|
+
`<span class="c-n g-num c-wake">${wakesCell(p)}</span>` +
|
|
267
272
|
`<span class="c-last g-num age">${esc(age(p.last_activity_at, asOf))}</span>` +
|
|
268
273
|
'<span class="c-nums">' +
|
|
269
274
|
`<span>${t.open} open</span><span>${t.in_progress} doing</span>` +
|
|
270
275
|
(t.blocked ? `<span class="n-blocked">${t.blocked} blocked</span>` : "") +
|
|
271
276
|
(t.high ? `<span class="n-high">${t.high} high</span>` : "") +
|
|
272
277
|
(p.needs_human ? `<span class="n-needs">${p.needs_human} need you</span>` : "") +
|
|
278
|
+
(p.questions_pending ? `<span class="n-questions">${p.questions_pending} question${p.questions_pending === 1 ? "" : "s"}</span>` : "") +
|
|
273
279
|
wakesText(p) +
|
|
274
280
|
"</span></div>");
|
|
275
281
|
}
|
|
276
282
|
const GRID_HEAD = '<div class="g-row g-head" role="row"><span>Project</span><span>Lead</span><span>Workers</span>' +
|
|
277
283
|
'<span class="g-num">Open</span><span class="g-num">Doing</span><span class="g-num">Blocked</span>' +
|
|
278
|
-
'<span class="g-num">High</span><span class="g-num">Needs you</span><span class="g-num c-wake">Wakes</span>' +
|
|
284
|
+
'<span class="g-num">High</span><span class="g-num">Needs you</span><span class="g-num">Questions</span><span class="g-num c-wake">Wakes</span>' +
|
|
279
285
|
'<span class="g-num">Last seen</span></div>';
|
|
280
286
|
function laneName(lane) {
|
|
281
287
|
return LANE_TITLE[lane].toLowerCase();
|
|
@@ -409,7 +415,7 @@ a.proj:hover { color: var(--accent); text-decoration: underline; text-underline-
|
|
|
409
415
|
.drift { margin: 0.5rem 0 0; grid-column: 1 / -1; padding: 0.55rem 0.75rem; border-radius: var(--r-ctl); background: var(--warn-bg); color: var(--fg); font-size: 0.8125rem; }
|
|
410
416
|
.drift strong { color: var(--warn); font-weight: 650; }
|
|
411
417
|
.grid { display: grid; }
|
|
412
|
-
.g-row { display: grid; grid-template-columns: minmax(8.5rem, 1.3fr) 10.5rem minmax(8rem, 1.2fr) repeat(4, 3.2rem) 4.6rem 5.2rem 5.6rem; align-items: center; gap: 0.6rem; padding: 0.5rem 1.1rem; border-top: 1px solid var(--border); font-size: 0.875rem; }
|
|
418
|
+
.g-row { display: grid; grid-template-columns: minmax(8.5rem, 1.3fr) 10.5rem minmax(8rem, 1.2fr) repeat(4, 3.2rem) 4.6rem 4.6rem 5.2rem 5.6rem; align-items: center; gap: 0.6rem; padding: 0.5rem 1.1rem; border-top: 1px solid var(--border); font-size: 0.875rem; }
|
|
413
419
|
.g-row:hover:not(.g-head) { background: var(--hover); }
|
|
414
420
|
.g-head { font-size: 0.75rem; color: var(--fg-subtle); font-weight: 550; border-top: 0; padding-block: 0.2rem 0.4rem; }
|
|
415
421
|
.g-num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; text-align: right; }
|
package/dist/questions.js
CHANGED
|
@@ -94,6 +94,12 @@ function runningAgentOf(projectId, actor) {
|
|
|
94
94
|
.get(projectId, actor);
|
|
95
95
|
return row?.id ?? null;
|
|
96
96
|
}
|
|
97
|
+
function runningLeadOf(projectId) {
|
|
98
|
+
const row = db
|
|
99
|
+
.prepare("SELECT id, actor_id FROM agents WHERE project_id = ? AND kind = 'lead' AND status = 'running' ORDER BY id DESC LIMIT 1")
|
|
100
|
+
.get(projectId);
|
|
101
|
+
return row ?? null;
|
|
102
|
+
}
|
|
97
103
|
const optionsJson = (input) => (input.kind === "choice" ? JSON.stringify(input.options) : null);
|
|
98
104
|
function pendingIdForRef(projectId, ref) {
|
|
99
105
|
const row = db
|
|
@@ -214,8 +220,19 @@ function planAnswerWake(projectId, row, actor) {
|
|
|
214
220
|
if (actor === row.asker)
|
|
215
221
|
return { skipped: "answered by the asker" };
|
|
216
222
|
const agentId = runningAgentOf(projectId, row.asker);
|
|
217
|
-
if (agentId === null)
|
|
218
|
-
|
|
223
|
+
if (agentId === null) {
|
|
224
|
+
const lead = runningLeadOf(projectId);
|
|
225
|
+
if (lead === null)
|
|
226
|
+
return { skipped: `the asker ${row.asker} has no running agent` };
|
|
227
|
+
if (lead.actor_id === actor)
|
|
228
|
+
return { skipped: "answered by the lead that would receive it" };
|
|
229
|
+
try {
|
|
230
|
+
return { delivery: resolveWakeDelivery(projectId, lead.id), reroutedFrom: row.asker };
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
return { skipped: e instanceof Error ? e.message : String(e) };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
219
236
|
try {
|
|
220
237
|
return { delivery: resolveWakeDelivery(projectId, agentId) };
|
|
221
238
|
}
|
|
@@ -232,11 +249,19 @@ const commitAnswer = db.transaction((projectId, actor, id, answer, notes, plan)
|
|
|
232
249
|
updated_at = datetime('now') WHERE id = ?`).run(answer, notes, actor, actor, id);
|
|
233
250
|
if ("skipped" in plan)
|
|
234
251
|
return { question_id: id, wake_id: null, wake_skipped: plan.skipped };
|
|
252
|
+
const body = answerWakeBody(id, row.question, answer, notes);
|
|
253
|
+
const wakeBody = "reroutedFrom" in plan && plan.reroutedFrom !== undefined
|
|
254
|
+
? `${body}\nAsked by ${plan.reroutedFrom}, which is not running; delivered to this project's lead.`
|
|
255
|
+
: body;
|
|
235
256
|
const wake = db
|
|
236
257
|
.prepare(`INSERT INTO wakes (project_id, owner, body, kind, deliver_actor, deliver_pane, due_at)
|
|
237
258
|
VALUES (?, ?, ?, 'delay', ?, ?, datetime('now')) RETURNING id`)
|
|
238
|
-
.get(projectId, actor,
|
|
239
|
-
return {
|
|
259
|
+
.get(projectId, actor, wakeBody, plan.delivery.actor, plan.delivery.pane);
|
|
260
|
+
return {
|
|
261
|
+
question_id: id,
|
|
262
|
+
wake_id: wake.id,
|
|
263
|
+
...("reroutedFrom" in plan && plan.reroutedFrom !== undefined ? { wake_rerouted_to: plan.delivery.actor } : {}),
|
|
264
|
+
};
|
|
240
265
|
});
|
|
241
266
|
export function answerQuestion(projectId, actor, id, answer, notes) {
|
|
242
267
|
if (blank(answer))
|
package/dist/scheduler.js
CHANGED
|
@@ -600,6 +600,8 @@ function holdTimer(timer, reason) {
|
|
|
600
600
|
OR (${socketAwaitingWhere()}))`, reason, timer.id, timer.due_at, count, count);
|
|
601
601
|
}
|
|
602
602
|
const HELD_REASON_MODAL_CHOICE = "pane is awaiting a modal choice (folder-trust or /model picker)";
|
|
603
|
+
const LEAD_BOOT_BOX_WAIT_MS = 60_000;
|
|
604
|
+
const HELD_REASON_LEAD_BOOT_INPUT_BOX = "the lead's input box is not on screen yet; holding while its session starts";
|
|
603
605
|
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 " +
|
|
604
606
|
"cancellation for this alone, so it is held rather than lost";
|
|
605
607
|
const HELD_REASON_PANE_REISSUED_PREFIX = "the pane id recorded for this wake now belongs to a different pane than the one it was set " +
|
|
@@ -664,6 +666,25 @@ export function isUnsubmittedInputHold(heldReason) {
|
|
|
664
666
|
export const HELD_REASON_CONVERSATION = "a human talked to this lead more recently than the conversation-hold window; holding so a wake " +
|
|
665
667
|
"does not split an in-progress discussion - it delivers once the window passes or the hold's own " +
|
|
666
668
|
"ceiling is reached, whichever comes first";
|
|
669
|
+
function leadBootSessionIsFresh(agentId, panePid) {
|
|
670
|
+
const row = stmt("SELECT pane_started_at FROM agents WHERE id = ? AND pane_pid = ?").get(agentId, panePid);
|
|
671
|
+
if (!row?.pane_started_at)
|
|
672
|
+
return false;
|
|
673
|
+
const timestamp = Date.parse(`${row.pane_started_at.replace(" ", "T")}Z`);
|
|
674
|
+
return Number.isFinite(timestamp) && Date.now() - timestamp < LEAD_BOOT_BOX_WAIT_MS;
|
|
675
|
+
}
|
|
676
|
+
function leadInputBoxObservation(pane, command, cache) {
|
|
677
|
+
const entry = cacheEntry(pane, cache);
|
|
678
|
+
if (entry.snapshot === undefined) {
|
|
679
|
+
entry.snapshot = paneClassifierFor(command)?.inputBoxSnapshot?.(pane) ?? null;
|
|
680
|
+
}
|
|
681
|
+
if (entry.snapshot === null) {
|
|
682
|
+
entry.box = null;
|
|
683
|
+
return { captured: false, box: null };
|
|
684
|
+
}
|
|
685
|
+
entry.box = entry.snapshot.box;
|
|
686
|
+
return { captured: true, box: entry.snapshot.box };
|
|
687
|
+
}
|
|
667
688
|
// A human message keeps a lead-bound wake held for this long after it lands. Measured deferral rates
|
|
668
689
|
// for this and CONVERSATION_HOLD_MAX are in hive-internals/references/worker-state.md.
|
|
669
690
|
const CONVERSATION_HOLD_TTL_SECONDS = 5 * 60;
|
|
@@ -1557,7 +1578,26 @@ function deliverable(timer, snapshot, choices) {
|
|
|
1557
1578
|
noteModalHold(timer, snapshot);
|
|
1558
1579
|
return { ok: false };
|
|
1559
1580
|
}
|
|
1560
|
-
|
|
1581
|
+
const claudeLeadSnapshot = isLeadActorId(timer.deliver_actor) && paneClassifierFor(timer.deliver_command)?.inputBoxSnapshot !== undefined;
|
|
1582
|
+
if (claudeLeadSnapshot) {
|
|
1583
|
+
const bootWindow = row.deliver_row_id !== null && leadBootSessionIsFresh(row.deliver_row_id, row.deliver_pane_pid);
|
|
1584
|
+
const observation = leadInputBoxObservation(timer.deliver_pane, timer.deliver_command, choices);
|
|
1585
|
+
if (!observation.captured) {
|
|
1586
|
+
if (bootWindow)
|
|
1587
|
+
return { ok: false };
|
|
1588
|
+
}
|
|
1589
|
+
else {
|
|
1590
|
+
if (holdsHumanInput(observation.box)) {
|
|
1591
|
+
noteUnsubmittedInputHold(timer, snapshot);
|
|
1592
|
+
return { ok: false };
|
|
1593
|
+
}
|
|
1594
|
+
if (observation.box === null && bootWindow) {
|
|
1595
|
+
holdTimer(timer, HELD_REASON_LEAD_BOOT_INPUT_BOX);
|
|
1596
|
+
return { ok: false };
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
else if (inputBoxHoldsWake(timer.deliver_pane, timer.deliver_command, choices)) {
|
|
1561
1601
|
noteUnsubmittedInputHold(timer, snapshot);
|
|
1562
1602
|
return { ok: false };
|
|
1563
1603
|
}
|
package/dist/staleServers.js
CHANGED
|
@@ -4,6 +4,19 @@ import { dirname, resolve } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { DEFAULT_DATA_DIR } from "./dataDir.js";
|
|
6
6
|
export const STALE_SERVER_BOUNDARY_SLACK_MS = 3000;
|
|
7
|
+
export function readProcessEnvironment(pid) {
|
|
8
|
+
try {
|
|
9
|
+
return execFileSync("ps", ["eww", "-p", String(pid), "-o", "command="], { encoding: "utf8" });
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (typeof error === "object" && error !== null && "status" in error && error.status === 1 &&
|
|
13
|
+
"stdout" in error && String(error.stdout ?? "").trim() === "" &&
|
|
14
|
+
"stderr" in error && (String(error.stderr ?? "").trim() === "" || /no such process|process id .* (?:not found|does not exist)/i.test(String(error.stderr)))) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
7
20
|
const serverEntrypoint = resolve(dirname(fileURLToPath(import.meta.url)), "index.js");
|
|
8
21
|
export function elapsedSeconds(value) {
|
|
9
22
|
const dayForm = /^(\d+)-(\d{1,2}):(\d{2}):(\d{2})$/.exec(value);
|
|
@@ -94,7 +107,7 @@ export function parseStaleHiveServers(psOutput, appliedAt, observedAt = Date.now
|
|
|
94
107
|
}));
|
|
95
108
|
return { servers };
|
|
96
109
|
}
|
|
97
|
-
export function staleHiveServers(appliedAt, dataDir = DEFAULT_DATA_DIR) {
|
|
110
|
+
export function staleHiveServers(appliedAt, dataDir = DEFAULT_DATA_DIR, readEnvironment = readProcessEnvironment) {
|
|
98
111
|
if (appliedAt === null)
|
|
99
112
|
return { skipped: "the newest migration time is unavailable" };
|
|
100
113
|
try {
|
|
@@ -104,9 +117,9 @@ export function staleHiveServers(appliedAt, dataDir = DEFAULT_DATA_DIR) {
|
|
|
104
117
|
return parsed;
|
|
105
118
|
const servers = [];
|
|
106
119
|
for (const server of parsed.servers) {
|
|
107
|
-
const envOutput =
|
|
108
|
-
|
|
109
|
-
|
|
120
|
+
const envOutput = readEnvironment(server.pid);
|
|
121
|
+
if (envOutput === null)
|
|
122
|
+
continue;
|
|
110
123
|
const sameStore = processUsesDataDir(envOutput, dataDir);
|
|
111
124
|
if (sameStore === null)
|
|
112
125
|
return { skipped: "ps returned environment output that could not be parsed" };
|
package/dist/tools/agents.js
CHANGED
|
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { db } from "../db.js";
|
|
5
5
|
import { confirmQueenWrite } from "../queenAudit.js";
|
|
6
|
-
import { captureQueenWriteIdentity } from "../context.js";
|
|
6
|
+
import { captureQueenWriteIdentity, resolveQueenLeadTarget } from "../context.js";
|
|
7
7
|
import { agentBriefPath, mergedBriefVars, readAgentBrief, workerBrief, workerCommandString, writeAgentBrief, } from "../brief.js";
|
|
8
8
|
import { codexHomeDir, codexInstructionsPhrase, codexLaunchArgs, ensureCodexHome, ensureCodexHooksFile, refreshCodexMcpEnv } from "../codexHome.js";
|
|
9
9
|
import { currentActor, findProjectForDir, getProject, linkedWorktreePrimaryRoot, resolveProject } from "../context.js";
|
|
@@ -1171,7 +1171,7 @@ export function registerAgents(server) {
|
|
|
1171
1171
|
},
|
|
1172
1172
|
}, (args) => run(async () => {
|
|
1173
1173
|
const project = resolveProject(args.project_id);
|
|
1174
|
-
const agent = findAgent(project.id, args);
|
|
1174
|
+
const agent = resolveQueenLeadTarget("agent_send", project.id, args.agent_id ?? args.name, () => findAgent(project.id, args));
|
|
1175
1175
|
requireLive(agent);
|
|
1176
1176
|
const target = agent.tmux_target;
|
|
1177
1177
|
const auditIdentity = captureQueenWriteIdentity(project.id);
|
package/dist/tools/queenAudit.js
CHANGED
|
@@ -3,7 +3,7 @@ import { run } from "../result.js";
|
|
|
3
3
|
import { idParam, limitParam } from "./params.js";
|
|
4
4
|
export function registerQueenAudit(server) {
|
|
5
5
|
server.registerTool("queen_audit_list", {
|
|
6
|
-
description: "List confirmed queen writes
|
|
6
|
+
description: "List confirmed queen writes and refused cross-project guard calls, newest first. Refusal rows include outcome=refused and the problem. The queen may read all targets; other callers see only their own project. Default 20, maximum 100 entries. History is retained for 30 days with a 20,000 id-range backstop. A crash between a completed terminal send and its audit insert can leave that send unrecorded.",
|
|
7
7
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
8
8
|
inputSchema: {
|
|
9
9
|
project_id: idParam.optional().describe("Filter by the target project id. Only the queen may name another project."),
|
package/dist/tools/questions.js
CHANGED
|
@@ -71,7 +71,7 @@ export function registerQuestions(server) {
|
|
|
71
71
|
return updateQuestion(effectiveProjectId(), currentActor(), question_id, patch);
|
|
72
72
|
}));
|
|
73
73
|
server.registerTool("question_answer", {
|
|
74
|
-
description: "Record the human's answer to a pending or defaulted question in this project
|
|
74
|
+
description: "Record the human's answer to a pending or defaulted question in this project, plus optional notes. The answer may be a picked option label or free text, including text outside the offered labels. The answer update comes first; the answer and any wake commit together, and the answer stays readable with `question_get`. An answered or withdrawn question refuses with `QUESTION_STATE_CONFLICT`. A queued wake carries the question, answer and notes verbatim. When you are the asker, skip with `wake_id: null` and `wake_skipped`; otherwise, queue a wake to the running asker or, if the asker has no running row, to the project's running lead with `wake_rerouted_to`. Skip that fallback with `wake_id: null` and `wake_skipped` if no lead runs or the lead is the answerer. `wake_id` means queued, not delivered.",
|
|
75
75
|
annotations: writes,
|
|
76
76
|
inputSchema: {
|
|
77
77
|
question_id: idParam,
|
|
@@ -82,6 +82,7 @@ export function registerQuestions(server) {
|
|
|
82
82
|
question_id: idParam,
|
|
83
83
|
wake_id: idParam.nullable(),
|
|
84
84
|
wake_skipped: z.string().optional(),
|
|
85
|
+
wake_rerouted_to: z.string().optional(),
|
|
85
86
|
},
|
|
86
87
|
}, (args) => run(() => answerQuestion(effectiveProjectId(), currentActor(), args.question_id, args.answer, args.notes)));
|
|
87
88
|
}
|
package/dist/tools/wakes.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { db } from "../db.js";
|
|
3
3
|
import { commitQueenStateWrite } from "../queenAudit.js";
|
|
4
|
-
import { currentActor, effectiveProjectId, getProject, isQueenLead } from "../context.js";
|
|
4
|
+
import { captureQueenWriteIdentity, currentActor, effectiveProjectId, getProject, isQueenLead, resolveQueenLeadTarget, } from "../context.js";
|
|
5
5
|
import { run } from "../result.js";
|
|
6
6
|
import { findAgent, observeOwnership, notOwnedError, summaryLiveness } from "./agents.js";
|
|
7
7
|
import { ACTIVE_TIMER_WHERE, LOG_RETENTION, OWNED_BY_WATCH, WATCH_SCOPE_PROJECT, seedGoneCursor, } from "../scheduler.js";
|
|
@@ -35,9 +35,11 @@ function refuseUnclassifiableTarget(agent) {
|
|
|
35
35
|
"error you can read now. Drive this pane with agent_send(keys: [...]) instead, which is unguarded for " +
|
|
36
36
|
"exactly this reason.");
|
|
37
37
|
}
|
|
38
|
-
export function resolveWakeDelivery(projectId, deliverTo) {
|
|
38
|
+
export function resolveWakeDelivery(projectId, deliverTo, queenOperation) {
|
|
39
39
|
if (deliverTo != null) {
|
|
40
|
-
const agent =
|
|
40
|
+
const agent = queenOperation
|
|
41
|
+
? resolveQueenLeadTarget(queenOperation, projectId, deliverTo, () => resolveAgentRef(projectId, deliverTo))
|
|
42
|
+
: resolveAgentRef(projectId, deliverTo);
|
|
41
43
|
// Before the liveness probe: the harness is a durable fact about the row, while liveness is a
|
|
42
44
|
// question about right now, so a dead bash worker should say what is actually wrong with it.
|
|
43
45
|
refuseUnclassifiableTarget(agent);
|
|
@@ -52,18 +54,24 @@ export function resolveWakeDelivery(projectId, deliverTo) {
|
|
|
52
54
|
const own = db
|
|
53
55
|
.prepare("SELECT * FROM agents WHERE actor_id = ? AND status = 'running' ORDER BY id DESC LIMIT 1")
|
|
54
56
|
.get(actor);
|
|
57
|
+
if (queenOperation && !own && captureQueenWriteIdentity(projectId)) {
|
|
58
|
+
resolveQueenLeadTarget(queenOperation, projectId, deliverTo, () => {
|
|
59
|
+
throw new Error("the calling session has no running agent row to receive this wake");
|
|
60
|
+
});
|
|
61
|
+
}
|
|
55
62
|
// Checked on the ROW, not behind the liveness probe: isLive is three-state, and letting a null
|
|
56
63
|
// fall through to the TMUX_PANE branch below accepts a wake the scheduler will then hold forever
|
|
57
64
|
// against that same row's command - the never-firing timer this refusal exists to prevent.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
const target = own && queenOperation ? resolveQueenLeadTarget(queenOperation, projectId, deliverTo, () => own) : own;
|
|
66
|
+
if (target)
|
|
67
|
+
refuseUnclassifiableTarget(target);
|
|
68
|
+
if (target) {
|
|
69
|
+
const ownership = observeOwnership(target);
|
|
62
70
|
if (ownership === "live")
|
|
63
|
-
return { actor, pane:
|
|
71
|
+
return { actor, pane: target.tmux_target };
|
|
64
72
|
// Unknown never falls through to the ambient pane: that would bind this row's actor to a pane it may not own.
|
|
65
73
|
if (ownership === "unknown")
|
|
66
|
-
throw notOwnedError(
|
|
74
|
+
throw notOwnedError(target, ownership);
|
|
67
75
|
}
|
|
68
76
|
const pane = process.env.TMUX_PANE;
|
|
69
77
|
if (pane)
|
|
@@ -331,7 +339,7 @@ export function registerWakes(server) {
|
|
|
331
339
|
}, (args) => run(() => {
|
|
332
340
|
assertSafeWakeBody(args.body);
|
|
333
341
|
const projectId = effectiveProjectId(args.project_id);
|
|
334
|
-
const delivery = resolveWakeDelivery(projectId, args.deliver_to);
|
|
342
|
+
const delivery = resolveWakeDelivery(projectId, args.deliver_to, "wake_set");
|
|
335
343
|
return commitQueenStateWrite("wake_set", projectId, args, () => {
|
|
336
344
|
const row = db
|
|
337
345
|
.prepare(`INSERT INTO wakes (project_id, owner, body, kind, deliver_actor, deliver_pane, due_at, repeat_every_ms)
|
|
@@ -441,7 +449,7 @@ export function registerWakes(server) {
|
|
|
441
449
|
"reported as a timeout instead of the failure it actually is. A lead's turn has its own channel, " +
|
|
442
450
|
"which only the queen may watch, with lead_project_id=<that lead's project id>.");
|
|
443
451
|
}
|
|
444
|
-
const delivery = resolveWakeDelivery(projectId, args.deliver_to);
|
|
452
|
+
const delivery = resolveWakeDelivery(projectId, args.deliver_to, "wake_when_idle");
|
|
445
453
|
const snapshot = liveTargets();
|
|
446
454
|
if (mode === "all") {
|
|
447
455
|
const allIdle = watched.every((a) => {
|
package/package.json
CHANGED