@cmgmyr/hive 1.10.1 → 1.11.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 +1 -0
- package/claude-plugin/crew/model.mjs +2 -0
- package/dist/build-info.json +1 -1
- package/dist/cli.js +135 -52
- package/dist/context.js +49 -5
- package/dist/crew.js +20 -2
- package/dist/dashboard.js +52 -0
- package/dist/db.js +144 -0
- package/dist/help.js +27 -0
- package/dist/hook.js +32 -2
- package/dist/index.js +2 -0
- package/dist/kickoff.js +29 -6
- package/dist/leadHandoff.js +525 -0
- package/dist/leadHandoffCli.js +295 -0
- package/dist/leadHandoffState.js +96 -0
- package/dist/leadLaunch.js +54 -0
- package/dist/leadMessageDelivery.js +44 -7
- package/dist/leadState.js +98 -17
- package/dist/portfolio.js +7 -1
- package/dist/projectRemove.js +1 -0
- package/dist/projectYml.js +8 -1
- package/dist/queenAudit.js +14 -2
- package/dist/queenDashboard.js +14 -8
- package/dist/questions.js +279 -0
- package/dist/scheduler.js +25 -5
- package/dist/staleServers.js +135 -0
- package/dist/tools/agents.js +2 -2
- package/dist/tools/meta.js +4 -1
- package/dist/tools/queenAudit.js +1 -1
- package/dist/tools/questions.js +88 -0
- package/dist/tools/wakes.js +25 -17
- package/package.json +1 -1
package/dist/db.js
CHANGED
|
@@ -430,6 +430,150 @@ ALTER TABLE agent_messages ADD COLUMN sender_tag TEXT;
|
|
|
430
430
|
CREATE INDEX idx_agent_messages_delivery ON agent_messages(delivery_status, id) WHERE delivery_status IS NOT NULL;
|
|
431
431
|
`,
|
|
432
432
|
`ALTER TABLE agents ADD COLUMN todo_id INTEGER REFERENCES todos(id) ON DELETE SET NULL;`,
|
|
433
|
+
`
|
|
434
|
+
ALTER TABLE lead_turn_state ADD COLUMN snapshot_pane_pid TEXT NOT NULL DEFAULT '';
|
|
435
|
+
ALTER TABLE lead_turn_state ADD COLUMN snapshot_session_id TEXT NOT NULL DEFAULT '';
|
|
436
|
+
ALTER TABLE lead_turn_state ADD COLUMN prompt_seq INTEGER NOT NULL DEFAULT 0;
|
|
437
|
+
ALTER TABLE lead_turn_state ADD COLUMN human_prompt_seq INTEGER NOT NULL DEFAULT 0;
|
|
438
|
+
ALTER TABLE lead_turn_state ADD COLUMN human_prompt_at TEXT;
|
|
439
|
+
ALTER TABLE lead_turn_state ADD COLUMN transcript_path TEXT NOT NULL DEFAULT '';
|
|
440
|
+
ALTER TABLE lead_turn_state ADD COLUMN stop_prompt_seq INTEGER;
|
|
441
|
+
ALTER TABLE lead_turn_state ADD COLUMN stop_background TEXT;
|
|
442
|
+
ALTER TABLE lead_turn_state ADD COLUMN stop_at TEXT;
|
|
443
|
+
|
|
444
|
+
CREATE TABLE lead_handoffs (
|
|
445
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
446
|
+
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
447
|
+
lead_agent_id INTEGER NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
448
|
+
pane_target TEXT NOT NULL,
|
|
449
|
+
tmux_socket TEXT NOT NULL DEFAULT '',
|
|
450
|
+
predecessor_pane_pid TEXT NOT NULL,
|
|
451
|
+
predecessor_session_id TEXT NOT NULL,
|
|
452
|
+
reason TEXT NOT NULL CHECK (reason IN ('warn', 'stop')),
|
|
453
|
+
state TEXT NOT NULL CHECK (state IN ('pending', 'wind_down', 'requested', 'grace', 'postponed',
|
|
454
|
+
'respawning', 'started', 'completed', 'failed', 'ambiguous')),
|
|
455
|
+
pass INTEGER NOT NULL DEFAULT 1,
|
|
456
|
+
request_reason TEXT CHECK (request_reason IN ('warn', 'stop')),
|
|
457
|
+
pass_started_at TEXT,
|
|
458
|
+
request_wake_id INTEGER,
|
|
459
|
+
attempt INTEGER NOT NULL DEFAULT 0,
|
|
460
|
+
owner_token TEXT,
|
|
461
|
+
owner_pid INTEGER,
|
|
462
|
+
grace_seconds INTEGER,
|
|
463
|
+
grace_started_at TEXT,
|
|
464
|
+
human_prompt_baseline INTEGER,
|
|
465
|
+
pad_id INTEGER,
|
|
466
|
+
pad_revision INTEGER,
|
|
467
|
+
pad_length INTEGER,
|
|
468
|
+
pad_sha256 TEXT,
|
|
469
|
+
respawn_claimed_at TEXT,
|
|
470
|
+
predecessor_turns INTEGER,
|
|
471
|
+
hold_since TEXT,
|
|
472
|
+
hold_released_at TEXT,
|
|
473
|
+
blocked_reason TEXT,
|
|
474
|
+
successor_pane_pid TEXT,
|
|
475
|
+
successor_session_id TEXT,
|
|
476
|
+
delivered_pad_id INTEGER,
|
|
477
|
+
delivered_pad_revision INTEGER,
|
|
478
|
+
started_at TEXT,
|
|
479
|
+
completed_at TEXT,
|
|
480
|
+
failure TEXT,
|
|
481
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now')),
|
|
482
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now'))
|
|
483
|
+
);
|
|
484
|
+
CREATE UNIQUE INDEX idx_lead_handoffs_epoch
|
|
485
|
+
ON lead_handoffs(project_id, lead_agent_id, predecessor_pane_pid, predecessor_session_id);
|
|
486
|
+
CREATE UNIQUE INDEX idx_lead_handoffs_active
|
|
487
|
+
ON lead_handoffs(lead_agent_id) WHERE state NOT IN ('completed', 'failed');
|
|
488
|
+
`,
|
|
489
|
+
`
|
|
490
|
+
CREATE TRIGGER fence_handoff_held_claim
|
|
491
|
+
BEFORE UPDATE OF fire_count, delivery_method ON wakes
|
|
492
|
+
FOR EACH ROW
|
|
493
|
+
WHEN (NEW.fire_count > OLD.fire_count
|
|
494
|
+
OR (NEW.delivery_method IS 'pty-after-socket-timeout'
|
|
495
|
+
AND OLD.delivery_method IS NOT 'pty-after-socket-timeout'
|
|
496
|
+
AND NEW.typed_at IS NULL))
|
|
497
|
+
AND EXISTS (
|
|
498
|
+
SELECT 1 FROM lead_handoffs h
|
|
499
|
+
WHERE h.lead_agent_id = (SELECT a.id FROM agents a
|
|
500
|
+
WHERE a.actor_id = NEW.deliver_actor
|
|
501
|
+
ORDER BY (a.status = 'running') DESC,
|
|
502
|
+
a.id DESC
|
|
503
|
+
LIMIT 1)
|
|
504
|
+
AND h.state NOT IN ('completed', 'failed')
|
|
505
|
+
AND h.request_wake_id IS NOT NEW.id
|
|
506
|
+
AND (h.state = 'grace'
|
|
507
|
+
OR (h.hold_released_at IS NULL AND (
|
|
508
|
+
h.state IN ('respawning', 'ambiguous', 'wind_down')
|
|
509
|
+
OR (h.state = 'requested'
|
|
510
|
+
AND (h.reason = 'stop' OR h.pass >= 2))
|
|
511
|
+
OR (h.state = 'postponed' AND h.reason = 'stop'))))
|
|
512
|
+
AND (julianday('now') - julianday(h.updated_at)) * 86400 < 3600)
|
|
513
|
+
BEGIN
|
|
514
|
+
SELECT RAISE(IGNORE);
|
|
515
|
+
END;
|
|
516
|
+
`,
|
|
517
|
+
`DROP TRIGGER fence_handoff_held_claim;`,
|
|
518
|
+
`
|
|
519
|
+
CREATE TRIGGER fence_handoff_held_claim
|
|
520
|
+
BEFORE UPDATE OF fire_count, delivery_method ON wakes
|
|
521
|
+
FOR EACH ROW
|
|
522
|
+
WHEN (NEW.fire_count > OLD.fire_count
|
|
523
|
+
OR (NEW.delivery_method IS 'pty-after-socket-timeout'
|
|
524
|
+
AND OLD.delivery_method IS NOT 'pty-after-socket-timeout'
|
|
525
|
+
AND NEW.typed_at IS NULL))
|
|
526
|
+
AND EXISTS (
|
|
527
|
+
SELECT 1 FROM lead_handoffs h
|
|
528
|
+
WHERE h.lead_agent_id = (SELECT a.id FROM agents a
|
|
529
|
+
WHERE a.actor_id = NEW.deliver_actor
|
|
530
|
+
ORDER BY (a.status = 'running') DESC,
|
|
531
|
+
a.id DESC
|
|
532
|
+
LIMIT 1)
|
|
533
|
+
AND h.state NOT IN ('completed', 'failed')
|
|
534
|
+
AND h.request_wake_id IS NOT NEW.id
|
|
535
|
+
AND (h.state IN ('grace', 'started')
|
|
536
|
+
OR (h.hold_released_at IS NULL AND (
|
|
537
|
+
h.state IN ('respawning', 'ambiguous', 'wind_down')
|
|
538
|
+
OR (h.state = 'requested'
|
|
539
|
+
AND (h.reason = 'stop' OR h.pass >= 2))
|
|
540
|
+
OR (h.state = 'postponed' AND h.reason = 'stop'))))
|
|
541
|
+
AND (julianday('now') - julianday(h.updated_at)) * 86400 < 3600)
|
|
542
|
+
BEGIN
|
|
543
|
+
SELECT RAISE(IGNORE);
|
|
544
|
+
END;
|
|
545
|
+
`,
|
|
546
|
+
`
|
|
547
|
+
CREATE TABLE questions (
|
|
548
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
549
|
+
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
550
|
+
asker TEXT NOT NULL,
|
|
551
|
+
kind TEXT NOT NULL CHECK (kind IN ('choice', 'free_text')),
|
|
552
|
+
question TEXT NOT NULL CHECK (trim(question) != ''),
|
|
553
|
+
header TEXT NOT NULL CHECK (trim(header) != ''),
|
|
554
|
+
options TEXT,
|
|
555
|
+
default_answer TEXT NOT NULL CHECK (trim(default_answer) != ''),
|
|
556
|
+
ref_kind TEXT CHECK (ref_kind IN ('todo', 'pr')),
|
|
557
|
+
ref_number INTEGER CHECK (ref_number > 0),
|
|
558
|
+
state TEXT NOT NULL DEFAULT 'pending' CHECK (state IN ('pending', 'answered', 'defaulted', 'withdrawn')),
|
|
559
|
+
answer TEXT,
|
|
560
|
+
notes TEXT,
|
|
561
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
562
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
563
|
+
updated_by TEXT NOT NULL,
|
|
564
|
+
answered_by TEXT,
|
|
565
|
+
CHECK ((ref_kind IS NULL) = (ref_number IS NULL)),
|
|
566
|
+
CHECK ((kind = 'choice') = (options IS NOT NULL))
|
|
567
|
+
);
|
|
568
|
+
CREATE UNIQUE INDEX idx_questions_pending_ref ON questions(project_id, ref_kind, ref_number)
|
|
569
|
+
WHERE state = 'pending' AND ref_kind IS NOT NULL;
|
|
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
|
+
`,
|
|
433
577
|
];
|
|
434
578
|
export function storeSchemaAhead(database) {
|
|
435
579
|
try {
|
package/dist/help.js
CHANGED
|
@@ -40,6 +40,9 @@ KV — small shared status values (with optional TTL)
|
|
|
40
40
|
LEASES — advisory expiring claims on shared state
|
|
41
41
|
lease_acquire, lease_release
|
|
42
42
|
|
|
43
|
+
QUESTIONS — open questions for the human, recorded in the store
|
|
44
|
+
question_ask, question_get, question_list, question_update, question_answer
|
|
45
|
+
|
|
43
46
|
PROJECTS — scope management
|
|
44
47
|
project_list, project_add, project_select, project_prune
|
|
45
48
|
|
|
@@ -246,6 +249,9 @@ Delivery contract:
|
|
|
246
249
|
and the next action. Plain English, no markup.
|
|
247
250
|
Waking your OWN lead pane is the exception: it is not cold, so carry the
|
|
248
251
|
action and the ids and point at where the detail lives.
|
|
252
|
+
With lead_turn_budget.auto_handoff on, a lead past its warn budget gets
|
|
253
|
+
a "hive lead handoff request #N" wake: follow its steps, then run
|
|
254
|
+
hive lead-handoff. A human prompt during the grace postpones it.
|
|
249
255
|
|
|
250
256
|
Receiving wake-ups:
|
|
251
257
|
Spawned workers can always receive (deliver_to their name or id).
|
|
@@ -378,6 +384,27 @@ todo_list without marking anything done.`,
|
|
|
378
384
|
|
|
379
385
|
Use kv for values another session needs to discover: current branch, a port,
|
|
380
386
|
a phase flag. Use pads for anything longer than a line or two.`,
|
|
387
|
+
questions: `QUESTIONS — open questions for the human, recorded in the store
|
|
388
|
+
|
|
389
|
+
question_ask(kind, question, header, default_answer, options?, ref?) —
|
|
390
|
+
record a question. kind=choice takes 2-4 options (label, description,
|
|
391
|
+
recommended; exactly one recommended, and default_answer is its label).
|
|
392
|
+
kind=free_text takes no options. header is 12 characters or fewer. ref
|
|
393
|
+
({kind: "todo"|"pr", number}) is the dedupe key: asking again with a ref
|
|
394
|
+
that has a pending question in this project updates that row. The
|
|
395
|
+
receipt lists similar pending questions so you can question_update one.
|
|
396
|
+
question_get(question_id) — one question, any state, with its answer
|
|
397
|
+
question_list() — pending questions, then defaulted ones, oldest first
|
|
398
|
+
question_update(question_id, ...) — edit a pending question's text, or set
|
|
399
|
+
state to defaulted (you acted on your default) or withdrawn (moot)
|
|
400
|
+
question_answer(question_id, answer, notes?) — record the human's answer
|
|
401
|
+
to a pending or defaulted question
|
|
402
|
+
|
|
403
|
+
hive stores the question; it never puts it to the human. Render it in your
|
|
404
|
+
own turn. Answering queues a wake to the asker carrying the question,
|
|
405
|
+
answer and notes, unless the answerer is the asker or the asker cannot be
|
|
406
|
+
reached; the answer is kept either way. Nothing expires or defaults on its
|
|
407
|
+
own: only the asker or the human moves a question out of pending.`,
|
|
381
408
|
leases: `LEASES — advisory expiring claims on shared state
|
|
382
409
|
|
|
383
410
|
lease_acquire(key, ttl_seconds) — non-blocking; returns the holder if
|
package/dist/hook.js
CHANGED
|
@@ -177,6 +177,13 @@ async function stopProcessesForEndedLead(actorId, payload) {
|
|
|
177
177
|
.get(actorId);
|
|
178
178
|
if (!row)
|
|
179
179
|
return;
|
|
180
|
+
// A deliberate handoff's predecessor ending is not the lead ending; its successor keeps the crew.
|
|
181
|
+
const lead = db.prepare("SELECT id FROM agents WHERE actor_id = ? AND kind = 'lead' AND status = 'running'").get(actorId);
|
|
182
|
+
if (lead !== undefined) {
|
|
183
|
+
const { predecessorFence } = await import("./leadState.js");
|
|
184
|
+
if (predecessorFence(lead.id, payload.session_id))
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
180
187
|
// Imported here and nowhere above: harnesses.ts itself pulls tmux at module scope, and every
|
|
181
188
|
// other event in this file runs on every turn of every session without needing any of it.
|
|
182
189
|
const { harnessFor } = await import("./harnesses.js");
|
|
@@ -313,7 +320,8 @@ try {
|
|
|
313
320
|
if (!guarded)
|
|
314
321
|
state = applyWorkerTransition(actorId, event);
|
|
315
322
|
// A lead's turn state is its own table; agents.agent_state stays worker-only.
|
|
316
|
-
if (process.env.HIVE_LEAD === "1" &&
|
|
323
|
+
if (process.env.HIVE_LEAD === "1" &&
|
|
324
|
+
["prompt", "stop", "session_end"].includes(event)) {
|
|
317
325
|
try {
|
|
318
326
|
const { applyLeadHook } = await import("./leadState.js");
|
|
319
327
|
applyLeadHook(actorId, event, readPayload(), () => waitingOnSubagents(actorId, readPayload()));
|
|
@@ -321,6 +329,18 @@ try {
|
|
|
321
329
|
catch {
|
|
322
330
|
}
|
|
323
331
|
}
|
|
332
|
+
if (event === "stop" && process.env.HIVE_LEAD === "1") {
|
|
333
|
+
try {
|
|
334
|
+
const started = db
|
|
335
|
+
.prepare(`SELECT 1 AS hit FROM lead_handoffs h JOIN agents a ON a.id = h.lead_agent_id
|
|
336
|
+
WHERE a.actor_id = ? AND a.kind = 'lead' AND h.state = 'started'`)
|
|
337
|
+
.get(actorId);
|
|
338
|
+
if (started !== undefined)
|
|
339
|
+
(await import("./leadHandoff.js")).completeHandoffOnStop(actorId);
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
}
|
|
343
|
+
}
|
|
324
344
|
if (event === "session_start" && process.env.HIVE_LEAD === "1") {
|
|
325
345
|
try {
|
|
326
346
|
await registerLeadMessagingSocket(actorId);
|
|
@@ -340,7 +360,17 @@ try {
|
|
|
340
360
|
reconcileTranscriptPath(actorId, hookPayload);
|
|
341
361
|
db.prepare("UPDATE actors SET last_seen_at = datetime('now') WHERE id = ?").run(actorId);
|
|
342
362
|
}
|
|
343
|
-
|
|
363
|
+
let logState = state ?? UNCHANGED;
|
|
364
|
+
if (event === "prompt" && process.env.HIVE_LEAD === "1") {
|
|
365
|
+
try {
|
|
366
|
+
const { isHiveFirstMessage, FIRST_MESSAGE_LOG_STATE } = await import("./leadState.js");
|
|
367
|
+
if (isHiveFirstMessage(readPayload()))
|
|
368
|
+
logState = FIRST_MESSAGE_LOG_STATE;
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
record(actorId, event, logState);
|
|
344
374
|
}
|
|
345
375
|
}
|
|
346
376
|
catch {
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerTodos } from "./tools/todos.js";
|
|
|
10
10
|
import { registerKv } from "./tools/kv.js";
|
|
11
11
|
import { registerLeases } from "./tools/leases.js";
|
|
12
12
|
import { registerWakes } from "./tools/wakes.js";
|
|
13
|
+
import { registerQuestions } from "./tools/questions.js";
|
|
13
14
|
import { registerQueenAudit } from "./tools/queenAudit.js";
|
|
14
15
|
import { registerPrompts } from "./prompts.js";
|
|
15
16
|
import { schedulerIntervalMs, startScheduler } from "./scheduler.js";
|
|
@@ -34,6 +35,7 @@ registerTodos(server);
|
|
|
34
35
|
registerKv(server);
|
|
35
36
|
registerLeases(server);
|
|
36
37
|
registerWakes(server);
|
|
38
|
+
registerQuestions(server);
|
|
37
39
|
registerQueenAudit(server);
|
|
38
40
|
registerPrompts(server);
|
|
39
41
|
startScheduler(schedulerIntervalMs());
|
package/dist/kickoff.js
CHANGED
|
@@ -97,6 +97,20 @@ async function digest(projectPath, profile, warnings) {
|
|
|
97
97
|
lines.push("", "Standing process for this project: run `hive runbook`.");
|
|
98
98
|
return truncate(lines.join("\n"), CONTEXT_BUDGET);
|
|
99
99
|
}
|
|
100
|
+
// Lead-only: a worker returned above, and a session hive did not start as a lead gets nothing.
|
|
101
|
+
async function leadHandoffText() {
|
|
102
|
+
const actor = process.env.HIVE_AGENT_ID;
|
|
103
|
+
if (process.env.HIVE_LEAD !== "1" || !actor)
|
|
104
|
+
return null;
|
|
105
|
+
try {
|
|
106
|
+
const { migrate } = await import("./db.js");
|
|
107
|
+
migrate();
|
|
108
|
+
return (await import("./leadHandoff.js")).handoffInjection(actor);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
100
114
|
// Shared with `hive lead`, which passes the first message on the command line only when this holds.
|
|
101
115
|
export async function kickoffGate(dir) {
|
|
102
116
|
const { activeProfile, DEFAULT_LEAD_BRANCHES, loadProjectYml } = await import("./projectYml.js");
|
|
@@ -128,13 +142,20 @@ export async function evaluate(cwd, opts = {}) {
|
|
|
128
142
|
catch {
|
|
129
143
|
return { fired: false, reason: "cwd does not exist" };
|
|
130
144
|
}
|
|
145
|
+
const handoff = await leadHandoffText();
|
|
146
|
+
const handoffOnly = (warnings) => ({
|
|
147
|
+
fired: true,
|
|
148
|
+
payload: JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: handoff } }),
|
|
149
|
+
warnings,
|
|
150
|
+
});
|
|
131
151
|
const gate = await kickoffGate(dir);
|
|
132
152
|
if (!gate.ok)
|
|
133
|
-
return { fired: false, reason: gate.reason, warnings: gate.warnings };
|
|
153
|
+
return handoff === null ? { fired: false, reason: gate.reason, warnings: gate.warnings } : handoffOnly(gate.warnings);
|
|
134
154
|
const { config, warnings, sources, profile } = gate;
|
|
135
155
|
const context = await digest(dir, profile, warnings);
|
|
136
|
-
if (context == null)
|
|
137
|
-
return { fired: false, reason: "not a registered hive project root", warnings };
|
|
156
|
+
if (context == null) {
|
|
157
|
+
return handoff === null ? { fired: false, reason: "not a registered hive project root", warnings } : handoffOnly(warnings);
|
|
158
|
+
}
|
|
138
159
|
// Codex's SessionStartHookSpecificOutputWire is additionalProperties:false and permits only
|
|
139
160
|
// hookEventName/additionalContext - initialUserMessage inside it fails the whole payload
|
|
140
161
|
// ("SessionStart Failed"), silently dropping the board too. Measured, todo 567 comment 1864.
|
|
@@ -145,13 +166,15 @@ export async function evaluate(cwd, opts = {}) {
|
|
|
145
166
|
const build = (text) => JSON.stringify({
|
|
146
167
|
hookSpecificOutput: {
|
|
147
168
|
hookEventName: "SessionStart",
|
|
148
|
-
additionalContext: text
|
|
169
|
+
additionalContext: handoff === null ? text : `${text}\n\n${handoff}`,
|
|
149
170
|
...(hookMessage === null ? {} : { initialUserMessage: hookMessage }),
|
|
150
171
|
},
|
|
151
172
|
});
|
|
152
173
|
let payload = build(context);
|
|
153
|
-
|
|
154
|
-
|
|
174
|
+
// The handoff rides outside the digest budget: only the digest is ever cut to fit.
|
|
175
|
+
const budget = OUTPUT_BUDGET + (handoff === null ? 0 : JSON.stringify(handoff).length);
|
|
176
|
+
if (payload.length > budget) {
|
|
177
|
+
const overflow = payload.length - budget;
|
|
155
178
|
payload = build(truncate(context, Math.max(0, context.length - overflow - 32)));
|
|
156
179
|
}
|
|
157
180
|
return { fired: true, payload, warnings, firstMessage: first };
|