@cmgmyr/hive 1.11.5 → 1.11.7
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/dist/agentKinds.js +6 -0
- package/dist/agentRecords.js +98 -0
- package/dist/backup.js +1 -1
- package/dist/build-info.json +1 -1
- package/dist/claudeWake.js +52 -4
- package/dist/cli.js +69 -43
- package/dist/context.js +2 -2
- package/dist/crew.js +5 -4
- package/dist/dashboard.js +10 -12
- package/dist/db.js +1 -1
- package/dist/errors.js +3 -0
- package/dist/etime.js +37 -0
- package/dist/firstPrompt.js +6 -0
- package/dist/globalConfig.js +1 -1
- package/dist/heldLabel.js +1 -1
- package/dist/kickoff.js +2 -2
- package/dist/leadHandoff.js +12 -11
- package/dist/leadHandoffCli.js +22 -1
- package/dist/leadHandoffState.js +1 -2
- package/dist/leadLaunch.js +1 -1
- package/dist/leadMessage.js +13 -0
- package/dist/leadMessageDelivery.js +14 -74
- package/dist/leadState.js +3 -2
- package/dist/leadWatch.js +1 -1
- package/dist/leaseStore.js +54 -0
- package/dist/pads.js +70 -0
- package/dist/portfolio.js +7 -3
- package/dist/projectRemove.js +1 -1
- package/dist/projectYml.js +1 -1
- package/dist/ptys.js +2 -9
- package/dist/queenAudit.js +2 -18
- package/dist/queenAuditStore.js +20 -0
- package/dist/queenDashboard.js +2 -1
- package/dist/questions.js +1 -1
- package/dist/result.js +2 -3
- package/dist/scheduler.js +42 -166
- package/dist/spawn.js +2 -6
- package/dist/staleServers.js +4 -28
- package/dist/stateProvenance.js +1 -3
- package/dist/storeClock.js +4 -0
- package/dist/teardown.js +2 -1
- package/dist/todoScope.js +1 -0
- package/dist/todos.js +90 -0
- package/dist/tools/agents.js +4 -96
- package/dist/tools/leases.js +5 -25
- package/dist/tools/pads.js +3 -82
- package/dist/tools/todos.js +6 -89
- package/dist/tools/wakes.js +12 -88
- package/dist/wakeSql.js +135 -0
- package/dist/wakeTarget.js +76 -0
- package/package.json +1 -1
package/dist/kickoff.js
CHANGED
|
@@ -17,9 +17,9 @@ export function truncate(text, limit) {
|
|
|
17
17
|
async function digest(projectPath, profile, warnings) {
|
|
18
18
|
const { migrate, db } = await import("./db.js");
|
|
19
19
|
const { findProjectForCwd } = await import("./context.js");
|
|
20
|
-
const { ACTIVE_TIMER_WHERE } = await import("./scheduler.js");
|
|
21
|
-
const { getActivePadByName } = await import("./tools/pads.js");
|
|
22
20
|
const { ACTIVE_TODO_SQL, OPEN_BLOCKERS_SQL } = await import("./todoScope.js");
|
|
21
|
+
const { ACTIVE_TIMER_WHERE } = await import("./wakeSql.js");
|
|
22
|
+
const { getActivePadByName } = await import("./pads.js");
|
|
23
23
|
migrate();
|
|
24
24
|
const project = findProjectForCwd();
|
|
25
25
|
if (!project || project.path !== projectPath)
|
package/dist/leadHandoff.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { db } from "./db.js";
|
|
3
|
+
import { MESSAGE_RETENTION, MESSAGE_STATUS } from "./leadMessage.js";
|
|
4
|
+
import { HELD_REASON_HANDOFF, LOG_RETENTION, handoffSocketInFlightWhere, pendingOneShotWhere, } from "./wakeSql.js";
|
|
3
5
|
import { harnessFor, paneClassifierFor } from "./harnesses.js";
|
|
4
|
-
import { casHandoff,
|
|
6
|
+
import { casHandoff, handoffHoldsAutomation, nowSql, olderThan, PASS_TIMEOUT_SECONDS, PRE_RESPAWN_STATES, passPolicy, quotedPolicy, readActiveHandoff, readHandoff, STOP_HOLD_CAP_SECONDS, SUCCESSOR_NOT_READY, } from "./leadHandoffState.js";
|
|
5
7
|
import { holdsHumanInput, paneInCopyMode, rowOwnership, tmuxSocketPath } from "./tmux.js";
|
|
6
8
|
import { readLeadSafetySnapshot } from "./leadState.js";
|
|
7
9
|
import { readTurnCount } from "./turnCount.js";
|
|
@@ -94,7 +96,7 @@ const failTransaction = db.transaction((id, failure, from, where) => {
|
|
|
94
96
|
if (!casHandoff(id, from, { state: "failed", failure, owner_token: null, owner_pid: null }, where))
|
|
95
97
|
return false;
|
|
96
98
|
db.prepare(`UPDATE wakes SET cancelled_at = datetime('now'), held_reason = ?
|
|
97
|
-
WHERE id = (SELECT request_wake_id FROM lead_handoffs WHERE id = ?) AND
|
|
99
|
+
WHERE id = (SELECT request_wake_id FROM lead_handoffs WHERE id = ?) AND ${pendingOneShotWhere()}`).run(`lead handoff #${id} failed: ${failure}`, id);
|
|
98
100
|
return true;
|
|
99
101
|
});
|
|
100
102
|
export function failHandoff(id, failure, from = PRE_RESPAWN_STATES, where = {}) {
|
|
@@ -158,13 +160,12 @@ export function requestBody(row, turns) {
|
|
|
158
160
|
// Automated deliveries to the lead other than the handoff's own request wakes.
|
|
159
161
|
export function automationBlocker(lead, seconds) {
|
|
160
162
|
const inFlight = db
|
|
161
|
-
.prepare(`SELECT 1 AS hit FROM agent_messages WHERE to_agent_id = ? AND delivery_status IN (
|
|
162
|
-
AND created_at >= datetime('now',
|
|
163
|
+
.prepare(`SELECT 1 AS hit FROM agent_messages WHERE to_agent_id = ? AND delivery_status IN (?, ?)
|
|
164
|
+
AND created_at >= datetime('now', ?)
|
|
163
165
|
UNION ALL
|
|
164
|
-
SELECT 1 FROM wakes WHERE deliver_actor = ? AND body NOT LIKE ? AND
|
|
165
|
-
AND delivery_method LIKE 'socket%' AND confirmed_at IS NULL AND socket_attempt_at >= datetime('now', '-7 days')
|
|
166
|
+
SELECT 1 FROM wakes WHERE deliver_actor = ? AND body NOT LIKE ? AND ${handoffSocketInFlightWhere()}
|
|
166
167
|
LIMIT 1`)
|
|
167
|
-
.get(lead.id, lead.actor_id, `${REQUEST_BODY_PREFIX}
|
|
168
|
+
.get(lead.id, MESSAGE_STATUS.socketPending, MESSAGE_STATUS.fallbackClaimed, MESSAGE_RETENTION, lead.actor_id, `${REQUEST_BODY_PREFIX}%`, LOG_RETENTION);
|
|
168
169
|
if (inFlight !== undefined)
|
|
169
170
|
return "a delivery to the lead is still in flight";
|
|
170
171
|
if (seconds === null)
|
|
@@ -204,7 +205,7 @@ const requestTransaction = db.transaction((row, lead, turns) => {
|
|
|
204
205
|
const waiting = current.request_wake_id === null
|
|
205
206
|
? undefined
|
|
206
207
|
: db
|
|
207
|
-
.prepare(
|
|
208
|
+
.prepare(`SELECT id FROM wakes WHERE id = ? AND due_at IS NULL AND ${pendingOneShotWhere()}`)
|
|
208
209
|
.get(current.request_wake_id);
|
|
209
210
|
let wakeId;
|
|
210
211
|
if (waiting !== undefined) {
|
|
@@ -435,12 +436,12 @@ export function handoffInjection(leadActor) {
|
|
|
435
436
|
.prepare("SELECT name, agent_state FROM agents WHERE project_id = ? AND kind = 'agent' AND status = 'running' ORDER BY id")
|
|
436
437
|
.all(lead.project_id);
|
|
437
438
|
const pending = db
|
|
438
|
-
.prepare(
|
|
439
|
+
.prepare(`SELECT COUNT(*) AS n FROM wakes WHERE deliver_actor = ? AND ${pendingOneShotWhere()}`)
|
|
439
440
|
.get(lead.actor_id).n;
|
|
440
441
|
const heldWakes = db.prepare("SELECT COUNT(*) AS n FROM wakes WHERE deliver_actor = ? AND held_reason = ? AND cancelled_at IS NULL").get(lead.actor_id, HELD_REASON_HANDOFF).n;
|
|
441
442
|
const heldMessages = db
|
|
442
|
-
.prepare("SELECT COUNT(*) AS n FROM agent_messages WHERE to_agent_id = ? AND delivery_status =
|
|
443
|
-
.get(lead.id).n;
|
|
443
|
+
.prepare("SELECT COUNT(*) AS n FROM agent_messages WHERE to_agent_id = ? AND delivery_status = ? AND created_at >= datetime('now', ?)")
|
|
444
|
+
.get(lead.id, MESSAGE_STATUS.fallbackPending, MESSAGE_RETENTION).n;
|
|
444
445
|
const lines = [`[hive] LEAD HANDOFF: pad "${HANDOFF_PAD_NAME}" #${pad.id} revision ${pad.revision}, written ${pad.updated_at} UTC.`];
|
|
445
446
|
{
|
|
446
447
|
lines.push(`Handed off by session ${row.predecessor_session_id} at turn ${row.predecessor_turns ?? "?"} (${row.reason}, pass ${row.pass})` +
|
package/dist/leadHandoffCli.js
CHANGED
|
@@ -21,13 +21,28 @@ function flags(argv, names) {
|
|
|
21
21
|
const out = {};
|
|
22
22
|
for (let i = 0; i < argv.length; i++) {
|
|
23
23
|
const name = argv[i].replace(/^--/, "");
|
|
24
|
-
if (!argv[i].startsWith("--") || !names.includes(name) || argv[i + 1] === undefined)
|
|
24
|
+
if (!argv[i].startsWith("--") || !names.includes(name) || argv[i + 1] === undefined) {
|
|
25
25
|
return `unexpected argument "${argv[i]}"`;
|
|
26
|
+
}
|
|
26
27
|
out[name] = argv[++i];
|
|
27
28
|
}
|
|
28
29
|
const absent = names.filter((n) => out[n] === undefined);
|
|
29
30
|
return absent.length > 0 ? `missing --${absent.join(", --")}` : out;
|
|
30
31
|
}
|
|
32
|
+
function rejectUnknownFlag(argv, command, names, flagsText) {
|
|
33
|
+
const known = new Set(names.map((name) => `--${name}`));
|
|
34
|
+
for (let i = 0; i < argv.length; i++) {
|
|
35
|
+
if (known.has(argv[i])) {
|
|
36
|
+
i++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (argv[i].startsWith("-")) {
|
|
40
|
+
console.error(`hive ${command}: unknown argument "${argv[i]}". Flags are ${flagsText}.`);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
31
46
|
const intFlag = (value) => (/^\d+$/.test(value) ? Number(value) : null);
|
|
32
47
|
function runningLeadById(id) {
|
|
33
48
|
return db
|
|
@@ -36,6 +51,8 @@ function runningLeadById(id) {
|
|
|
36
51
|
.get(id);
|
|
37
52
|
}
|
|
38
53
|
export async function cmdLeadHandoff(argv) {
|
|
54
|
+
if (rejectUnknownFlag(argv, "lead-handoff", ["request", "pad", "revision"], "--request <id>, --pad <id>, --revision <n>"))
|
|
55
|
+
return 1;
|
|
39
56
|
const refuse = (why) => {
|
|
40
57
|
console.error(`hive lead-handoff: ${why}. This session stays as it is.`);
|
|
41
58
|
return 1;
|
|
@@ -152,6 +169,8 @@ const sleepMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
152
169
|
// `hive lead-handoff-grace`: the one detached owner of an attempt. It never outlives a changed
|
|
153
170
|
// attempt or token, and after it claims the respawn it never repeats the kill.
|
|
154
171
|
export async function runHandoffGrace(argv) {
|
|
172
|
+
if (rejectUnknownFlag(argv, "lead-handoff-grace", ["request", "attempt", "token"], "--request <id>, --attempt <n>, --token <token>"))
|
|
173
|
+
return 1;
|
|
155
174
|
try {
|
|
156
175
|
return await graceLoop(argv);
|
|
157
176
|
}
|
|
@@ -245,6 +264,8 @@ async function graceLoop(argv) {
|
|
|
245
264
|
// `hive lead-handoff-start`: runs in the respawned pane before Claude. It publishes the successor's
|
|
246
265
|
// pid on the same lead row by CAS against the predecessor, and only then lets the shell exec Claude.
|
|
247
266
|
export async function runHandoffStart(argv) {
|
|
267
|
+
if (rejectUnknownFlag(argv, "lead-handoff-start", ["request", "attempt", "token"], "--request <id>, --attempt <n>, --token <token>"))
|
|
268
|
+
return 1;
|
|
248
269
|
const parsed = flags(argv, ["request", "attempt", "token"]);
|
|
249
270
|
const refuse = (why, id) => {
|
|
250
271
|
if (id !== undefined) {
|
package/dist/leadHandoffState.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { db } from "./db.js";
|
|
2
|
+
export { HELD_REASON_HANDOFF } from "./wakeSql.js";
|
|
2
3
|
export const PRE_RESPAWN_STATES = ["pending", "wind_down", "requested", "grace", "postponed"];
|
|
3
4
|
export const PASS_TIMEOUT_SECONDS = 10 * 60;
|
|
4
5
|
export const STOP_HOLD_CAP_SECONDS = 15 * 60;
|
|
@@ -53,8 +54,6 @@ export function readHandoffGate(leadAgentId) {
|
|
|
53
54
|
? null
|
|
54
55
|
: { requestId: row.id, requestWakeId: row.request_wake_id, holdAutomation: handoffHoldsAutomation(row) };
|
|
55
56
|
}
|
|
56
|
-
export const HELD_REASON_HANDOFF = "the lead is handing off to a fresh session; held through the wind-down and the successor's boot, and delivered to " +
|
|
57
|
-
"the lead row once the successor completes its first turn, or when the handoff fails or releases its hold";
|
|
58
57
|
// A wake addressed to a lead row is held while that lead's handoff holds automation, except the
|
|
59
58
|
// current request wake itself, which still passes every human-input and ownership guard.
|
|
60
59
|
export function handoffHoldsWake(wakeId, deliverRowId) {
|
package/dist/leadLaunch.js
CHANGED
|
@@ -4,7 +4,7 @@ import { carriesNameFlag } from "./harnesses.js";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { checkoutRoot, renderProfileFile, resolveProfileFile } from "./profiles.js";
|
|
6
6
|
import { activeProfile, configHash, mergedProjectVars } from "./projectYml.js";
|
|
7
|
-
import { LEAD_NAME } from "./
|
|
7
|
+
import { LEAD_NAME } from "./agentKinds.js";
|
|
8
8
|
import { shellQuote } from "./tmux.js";
|
|
9
9
|
// What `hive lead` and the handoff bootstrap both need to launch a claude lead: trust, posture,
|
|
10
10
|
// hooks and identity env. Pane adoption and leftover cleanup stay in cmdLead.
|
package/dist/leadMessage.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { db } from "./db.js";
|
|
2
|
+
import { DELIVERY_METHOD } from "./claudeWake.js";
|
|
2
3
|
import { cutToUnitBudget, flatten } from "./slug.js";
|
|
3
4
|
// 300 sits in the empty band between the two measured populations (todo 475): the largest lead-bound
|
|
4
5
|
// message anyone has argued must arrive whole is 143 bytes, and the smallest real worker report is 440.
|
|
@@ -14,6 +15,18 @@ export const MESSAGE_RETENTION = "-7 days";
|
|
|
14
15
|
// direction. Far above any observed rate, and lower than agent_state_log's 20,000 because a row here
|
|
15
16
|
// holds unbounded message text rather than a hook payload.
|
|
16
17
|
export const MESSAGE_MAX_ROWS = 5_000;
|
|
18
|
+
export const MESSAGE_STATUS = {
|
|
19
|
+
socketPending: "socket-pending",
|
|
20
|
+
fallbackPending: "fallback-pending",
|
|
21
|
+
fallbackClaimed: "fallback-claimed",
|
|
22
|
+
complete: "complete",
|
|
23
|
+
failed: "failed",
|
|
24
|
+
};
|
|
25
|
+
export const MESSAGE_METHOD = {
|
|
26
|
+
socket: DELIVERY_METHOD.socket,
|
|
27
|
+
pty: DELIVERY_METHOD.pty,
|
|
28
|
+
ptyAfterSocketTimeout: DELIVERY_METHOD.ptyAfterSocketTimeout,
|
|
29
|
+
};
|
|
17
30
|
export const DELIVERY_FIELDS = [
|
|
18
31
|
"delivery_status",
|
|
19
32
|
"delivery_method",
|
|
@@ -1,52 +1,22 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
1
|
import { dataDir, db } from "./db.js";
|
|
3
|
-
import { CROSS_SESSION_CLOSE, postClaudeWake,
|
|
2
|
+
import { CROSS_SESSION_CLOSE, promptMarkerEvidenceSql, postClaudeWake, quietMessagingOn, socketGraceDue, socketSenderAddress, SOCKET_MESSAGE_FOOTER, } from "./claudeWake.js";
|
|
4
3
|
import { harnessFor, paneClassifierFor } from "./harnesses.js";
|
|
5
|
-
import { LEAD_MESSAGE_THRESHOLD, renderLeadMessage, senderTag, shortenedSendNote, storeLeadMessage, } from "./leadMessage.js";
|
|
4
|
+
import { LEAD_MESSAGE_THRESHOLD, MESSAGE_METHOD, MESSAGE_RETENTION, MESSAGE_STATUS, renderLeadMessage, senderTag, shortenedSendNote, storeLeadMessage, } from "./leadMessage.js";
|
|
6
5
|
import { readHandoffGate } from "./leadHandoffState.js";
|
|
7
6
|
import { HANDED_OFF_STATES_SQL, readLeadTurnState } from "./leadState.js";
|
|
8
|
-
import { loadProjectYml } from "./projectYml.js";
|
|
9
7
|
import { currentOwnership, PaneOwnershipLost, paneIdentity, requireStillOwned } from "./spawn.js";
|
|
8
|
+
import { PANE_LEASE_SETTLE_MS, releasePaneLease, tryAcquirePaneLease } from "./leaseStore.js";
|
|
10
9
|
import { holdsHumanInput, paneInCopyMode, sendText, sleep, TmuxTimeoutError, } from "./tmux.js";
|
|
11
|
-
export const SOCKET_MESSAGE_FOOTER = "Automated hive worker message. Do not reply to this socket sender.";
|
|
12
|
-
export const MESSAGE_STATUS = {
|
|
13
|
-
socketPending: "socket-pending",
|
|
14
|
-
fallbackPending: "fallback-pending",
|
|
15
|
-
fallbackClaimed: "fallback-claimed",
|
|
16
|
-
complete: "complete",
|
|
17
|
-
failed: "failed",
|
|
18
|
-
};
|
|
19
|
-
export const MESSAGE_METHOD = {
|
|
20
|
-
socket: "socket",
|
|
21
|
-
pty: "pty",
|
|
22
|
-
ptyAfterSocketTimeout: "pty-after-socket-timeout",
|
|
23
|
-
};
|
|
24
|
-
const PANE_LEASE_TTL_SECONDS = 6;
|
|
25
|
-
const PANE_LEASE_SETTLE_MS = 500;
|
|
26
|
-
const CONFIRM_GRACE_SECONDS = 60;
|
|
27
|
-
const ENVELOPE_OPEN = '<cross-session-message from="';
|
|
28
|
-
const ENVELOPE_SENDER_CLOSE = '" from-name="hive">';
|
|
29
|
-
const EV_PROMPT = "(CASE WHEN json_valid(ev.payload) THEN COALESCE(json_extract(ev.payload, '$.prompt'), '') ELSE ev.payload END)";
|
|
30
10
|
const EV_MARKER = "(agent_messages.sender_tag || '[message #' || agent_messages.id || ',')";
|
|
31
|
-
const
|
|
11
|
+
const messageMarkerAt = (promptSql, startSql) => `substr(${promptSql}, ${startSql}, length(${EV_MARKER})) = ${EV_MARKER}`;
|
|
32
12
|
// The exact "[hive:worker NAME] [message #N," at prompt offset 0 (typed) or opening hive's own envelope body (socket).
|
|
33
13
|
const messageEvidenceSql = (select) => `
|
|
34
14
|
SELECT ${select} FROM agent_state_log ev
|
|
35
15
|
WHERE ev.actor_id = (SELECT a.actor_id FROM agents a WHERE a.id = agent_messages.to_agent_id)
|
|
36
16
|
AND ev.event = 'prompt'
|
|
37
17
|
AND ev.created_at >= COALESCE(agent_messages.socket_attempt_at, agent_messages.created_at)
|
|
38
|
-
AND
|
|
39
|
-
|
|
40
|
-
AND substr(${EV_PROMPT}, 1, ${ENVELOPE_OPEN.length}) = '${ENVELOPE_OPEN}'
|
|
41
|
-
AND substr(${EV_PROMPT}, ${EV_HEADER_END} - ${ENVELOPE_SENDER_CLOSE.length}, ${ENVELOPE_SENDER_CLOSE.length}) = '${ENVELOPE_SENDER_CLOSE}'
|
|
42
|
-
AND instr(substr(${EV_PROMPT}, ${ENVELOPE_OPEN.length + 1}, ${EV_HEADER_END} - ${ENVELOPE_OPEN.length + ENVELOPE_SENDER_CLOSE.length + 1}), '"') = 0
|
|
43
|
-
AND substr(${EV_PROMPT}, ${EV_HEADER_END} + 1, length(${EV_MARKER})) = ${EV_MARKER}
|
|
44
|
-
AND instr(${EV_PROMPT}, char(10) || '${CROSS_SESSION_CLOSE}') > ${EV_HEADER_END}))`;
|
|
45
|
-
const RETAINED = "agent_messages.created_at >= datetime('now', '-7 days')";
|
|
46
|
-
function quietMessagingOn(projectId) {
|
|
47
|
-
const project = db.prepare("SELECT path FROM projects WHERE id = ?").get(projectId);
|
|
48
|
-
return project !== undefined && loadProjectYml(project.path).config?.quiet_messaging === true;
|
|
49
|
-
}
|
|
18
|
+
AND ${promptMarkerEvidenceSql(messageMarkerAt)}`;
|
|
19
|
+
const RETAINED = `agent_messages.created_at >= datetime('now', '${MESSAGE_RETENTION}')`;
|
|
50
20
|
const handoffHolds = (agentId) => readHandoffGate(agentId)?.holdAutomation === true;
|
|
51
21
|
const HANDOFF_HOLD_NOTE = "the lead is handing off to a fresh session; held until the successor completes its first turn";
|
|
52
22
|
function eligible(options) {
|
|
@@ -97,29 +67,6 @@ function stamp(id, column) {
|
|
|
97
67
|
function socketText(id, text, tag) {
|
|
98
68
|
return `${renderLeadMessage(id, text, tag)}\n\n${SOCKET_MESSAGE_FOOTER}`;
|
|
99
69
|
}
|
|
100
|
-
function acquirePaneLease(row, key) {
|
|
101
|
-
try {
|
|
102
|
-
db.prepare("INSERT OR IGNORE INTO actors (id, name, kind) VALUES (?, ?, 'agent')").run(row.from_actor, row.from_name);
|
|
103
|
-
db.prepare("DELETE FROM leases WHERE project_id = ? AND lock_key = ? AND expires_at < datetime('now')").run(row.project_id, key);
|
|
104
|
-
const held = db
|
|
105
|
-
.prepare(`INSERT INTO leases (project_id, lock_key, owner, expires_at)
|
|
106
|
-
VALUES (?, ?, ?, datetime('now', printf('+%d seconds', ?)))
|
|
107
|
-
ON CONFLICT(project_id, lock_key) DO NOTHING
|
|
108
|
-
RETURNING expires_at`)
|
|
109
|
-
.get(row.project_id, key, row.from_actor, PANE_LEASE_TTL_SECONDS);
|
|
110
|
-
return held?.expires_at ?? null;
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
function releasePaneLease(row, key, expiresAt) {
|
|
117
|
-
try {
|
|
118
|
-
db.prepare("DELETE FROM leases WHERE project_id = ? AND lock_key = ? AND expires_at = ?").run(row.project_id, key, expiresAt);
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
70
|
function hold(row, reason) {
|
|
124
71
|
try {
|
|
125
72
|
db.prepare("UPDATE agent_messages SET delivery_note = ? WHERE id = ? AND delivery_status = ?").run(reason, row.id, MESSAGE_STATUS.fallbackPending);
|
|
@@ -189,8 +136,11 @@ async function paneFallback(original, snapshot) {
|
|
|
189
136
|
if (ownership !== "live" || target === undefined) {
|
|
190
137
|
return failPending(row, `the lead pane this message was accepted for is ${ownership}; nothing is typed into a replacement`);
|
|
191
138
|
}
|
|
192
|
-
const
|
|
193
|
-
|
|
139
|
+
const expiresAt = tryAcquirePaneLease(row.project_id, identity.tmux_socket, identity.tmux_target, {
|
|
140
|
+
id: row.from_actor,
|
|
141
|
+
name: row.from_name,
|
|
142
|
+
kind: "agent",
|
|
143
|
+
});
|
|
194
144
|
if (expiresAt === null)
|
|
195
145
|
return hold(row, "another delivery holds the lead's pane");
|
|
196
146
|
let wrote = false;
|
|
@@ -241,7 +191,7 @@ async function paneFallback(original, snapshot) {
|
|
|
241
191
|
finally {
|
|
242
192
|
if (wrote)
|
|
243
193
|
await sleep(PANE_LEASE_SETTLE_MS);
|
|
244
|
-
releasePaneLease(row,
|
|
194
|
+
releasePaneLease(row.project_id, identity.tmux_socket, identity.tmux_target, expiresAt);
|
|
245
195
|
}
|
|
246
196
|
}
|
|
247
197
|
function earlierFallbackPending(row) {
|
|
@@ -277,7 +227,7 @@ export async function sendQuietLeadMessage(options) {
|
|
|
277
227
|
if (skip === null) {
|
|
278
228
|
const posted = await postClaudeWake({
|
|
279
229
|
socketPath: target.claude_messaging_socket,
|
|
280
|
-
senderAddress:
|
|
230
|
+
senderAddress: socketSenderAddress(dataDir),
|
|
281
231
|
text: body,
|
|
282
232
|
beforeWrite: () => {
|
|
283
233
|
requireStillOwned(target.id, identity, `message #${id} socket delivery`);
|
|
@@ -348,17 +298,7 @@ export async function sendQuietLeadMessage(options) {
|
|
|
348
298
|
`Do NOT resend blindly: read the lead's pane first, and agent_message_get(${id}) shows the recorded state.`);
|
|
349
299
|
}
|
|
350
300
|
function graceExpired(row, actorId, panePid) {
|
|
351
|
-
|
|
352
|
-
.prepare("SELECT MIN(created_at) AS t FROM agent_state_log WHERE actor_id = ? AND event = 'stop' AND created_at >= ?")
|
|
353
|
-
.get(actorId, row.socket_attempt_at).t;
|
|
354
|
-
let from = firstStop;
|
|
355
|
-
if (from === null) {
|
|
356
|
-
const turn = readLeadTurnState(row.to_agent_id);
|
|
357
|
-
if (turn !== null && turn.state === "working" && turn.pane_pid === panePid)
|
|
358
|
-
return false;
|
|
359
|
-
from = row.socket_attempt_at;
|
|
360
|
-
}
|
|
361
|
-
return db.prepare("SELECT (julianday('now') - julianday(?)) * 86400 >= ? AS due").get(from, CONFIRM_GRACE_SECONDS).due === 1;
|
|
301
|
+
return socketGraceDue(actorId, row.socket_attempt_at, panePid, () => readLeadTurnState(row.to_agent_id));
|
|
362
302
|
}
|
|
363
303
|
function confirmFromPrompts() {
|
|
364
304
|
db.prepare(`UPDATE agent_messages SET confirmed_at = (${messageEvidenceSql("MIN(ev.created_at)")}), delivery_status = ?
|
package/dist/leadState.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { liveBackgroundTasks } from "./backgroundTasks.js";
|
|
2
2
|
import { db } from "./db.js";
|
|
3
|
+
import { hiveWorkerEnvelopeLikeSql, isHiveWorkerEnvelope } from "./claudeWake.js";
|
|
3
4
|
import { FIRST_MESSAGE_SHA_ENV, firstMessageDigest } from "./firstMessage.js";
|
|
4
5
|
import { postponeGrace, readActiveHandoff } from "./leadHandoffState.js";
|
|
5
6
|
export function readLeadTurnState(agentId) {
|
|
@@ -16,14 +17,14 @@ export function leadSessionEnded(row, ownership) {
|
|
|
16
17
|
export function humanPromptSql(col) {
|
|
17
18
|
const p = `CASE WHEN json_valid(${col}) THEN COALESCE(json_extract(${col}, '$.prompt'), ${col}) ELSE ${col} END`;
|
|
18
19
|
return (`${p} NOT LIKE '%[hive wake #%' AND ${p} NOT LIKE '[hive:%' AND ${p} NOT LIKE '<task-notification>%' ` +
|
|
19
|
-
`AND ${p}
|
|
20
|
+
`AND NOT ${hiveWorkerEnvelopeLikeSql(p)}`);
|
|
20
21
|
}
|
|
21
22
|
// Includes SQLite LIKE's ASCII case-insensitivity.
|
|
22
23
|
export function isHumanPrompt(prompt) {
|
|
23
24
|
const p = prompt.toLowerCase();
|
|
24
25
|
if (p.includes("[hive wake #") || p.startsWith("[hive:") || p.startsWith("<task-notification>"))
|
|
25
26
|
return false;
|
|
26
|
-
return
|
|
27
|
+
return !isHiveWorkerEnvelope(p);
|
|
27
28
|
}
|
|
28
29
|
// The agent_state_log state label on hive's own first message to a lead; conversationHoldsWake skips it.
|
|
29
30
|
export const FIRST_MESSAGE_LOG_STATE = "first_message";
|
package/dist/leadWatch.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { db } from "./db.js";
|
|
2
2
|
import { paneClassifierFor } from "./harnesses.js";
|
|
3
3
|
import { readLeadTurnState } from "./leadState.js";
|
|
4
|
-
import { LEAD_KIND } from "./
|
|
4
|
+
import { LEAD_KIND } from "./agentKinds.js";
|
|
5
5
|
import { foreignSocket, holdsHumanInput, observationFailed, rowOwnership } from "./tmux.js";
|
|
6
6
|
export const LEAD_TARGET_GONE = "LEAD_TARGET_GONE";
|
|
7
7
|
export const LEAD_TARGET_REISSUED = "LEAD_TARGET_REISSUED";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { db } from "./db.js";
|
|
2
|
+
export function purgeExpiredLeases(projectId, key) {
|
|
3
|
+
if (key === undefined) {
|
|
4
|
+
db.prepare("DELETE FROM leases WHERE project_id = ? AND expires_at < datetime('now')").run(projectId);
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
db.prepare("DELETE FROM leases WHERE project_id = ? AND lock_key = ? AND expires_at < datetime('now')").run(projectId, key);
|
|
8
|
+
}
|
|
9
|
+
export function insertLease(projectId, key, actor, ttlSeconds) {
|
|
10
|
+
return db
|
|
11
|
+
.prepare(`INSERT INTO leases (project_id, lock_key, owner, expires_at)
|
|
12
|
+
VALUES (?, ?, ?, datetime('now', printf('+%d seconds', ?)))
|
|
13
|
+
ON CONFLICT(project_id, lock_key) DO NOTHING
|
|
14
|
+
RETURNING expires_at`)
|
|
15
|
+
.get(projectId, key, actor, ttlSeconds);
|
|
16
|
+
}
|
|
17
|
+
export function readLease(projectId, key) {
|
|
18
|
+
return db.prepare("SELECT * FROM leases WHERE project_id = ? AND lock_key = ?").get(projectId, key);
|
|
19
|
+
}
|
|
20
|
+
export function extendOwnedLease(projectId, key, actor, ttlSeconds) {
|
|
21
|
+
const info = db
|
|
22
|
+
.prepare(`UPDATE leases SET expires_at = datetime('now', printf('+%d seconds', ?))
|
|
23
|
+
WHERE project_id = ? AND lock_key = ? AND owner = ?`)
|
|
24
|
+
.run(ttlSeconds, projectId, key, actor);
|
|
25
|
+
return info.changes > 0;
|
|
26
|
+
}
|
|
27
|
+
export function releaseOwnedLease(projectId, key, actor) {
|
|
28
|
+
return db
|
|
29
|
+
.prepare("DELETE FROM leases WHERE project_id = ? AND lock_key = ? AND owner = ?")
|
|
30
|
+
.run(projectId, key, actor).changes > 0;
|
|
31
|
+
}
|
|
32
|
+
export function paneLeaseKey(socket, pane) {
|
|
33
|
+
return `wake-pane:${socket}:${pane}`;
|
|
34
|
+
}
|
|
35
|
+
export const PANE_LEASE_TTL_SECONDS = 6;
|
|
36
|
+
export const PANE_LEASE_SETTLE_MS = 500;
|
|
37
|
+
export function tryAcquirePaneLease(projectId, socket, pane, actor) {
|
|
38
|
+
try {
|
|
39
|
+
db.prepare("INSERT OR IGNORE INTO actors (id, name, kind) VALUES (?, ?, ?)").run(actor.id, actor.name, actor.kind);
|
|
40
|
+
const key = paneLeaseKey(socket, pane);
|
|
41
|
+
purgeExpiredLeases(projectId, key);
|
|
42
|
+
return insertLease(projectId, key, actor.id, PANE_LEASE_TTL_SECONDS)?.expires_at ?? null;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function releasePaneLease(projectId, socket, pane, expiresAt) {
|
|
49
|
+
try {
|
|
50
|
+
db.prepare("DELETE FROM leases WHERE project_id = ? AND lock_key = ? AND expires_at = ?").run(projectId, paneLeaseKey(socket, pane), expiresAt);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
package/dist/pads.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { db } from "./db.js";
|
|
2
|
+
import { currentActor } from "./context.js";
|
|
3
|
+
export function splitsSurrogatePair(content, index) {
|
|
4
|
+
if (index <= 0 || index >= content.length)
|
|
5
|
+
return false;
|
|
6
|
+
const before = content.charCodeAt(index - 1);
|
|
7
|
+
const after = content.charCodeAt(index);
|
|
8
|
+
return before >= 0xd800 && before <= 0xdbff && after >= 0xdc00 && after <= 0xdfff;
|
|
9
|
+
}
|
|
10
|
+
function selectPad(projectId, padId, columns) {
|
|
11
|
+
const row = db.prepare(`SELECT ${columns} FROM pads WHERE project_id = ? AND id = ?`).get(projectId, padId);
|
|
12
|
+
if (!row)
|
|
13
|
+
throw new Error(`No pad with id ${padId} in project ${projectId}. Call pad_list.`);
|
|
14
|
+
return row;
|
|
15
|
+
}
|
|
16
|
+
export function getPad(projectId, padId) {
|
|
17
|
+
return selectPad(projectId, padId, "*");
|
|
18
|
+
}
|
|
19
|
+
export function getPadMeta(projectId, padId) {
|
|
20
|
+
return selectPad(projectId, padId, "id, name, revision, archived");
|
|
21
|
+
}
|
|
22
|
+
function revisionMismatchError(padId, expected, current) {
|
|
23
|
+
if (current == null) {
|
|
24
|
+
return new Error(`Pad ${padId} no longer exists; it may have just been deleted by another write. Call pad_list to confirm.`);
|
|
25
|
+
}
|
|
26
|
+
return new Error(`Revision mismatch for pad ${padId}: expected ${expected}, current ${current}. Re-read with pad_read and retry.`);
|
|
27
|
+
}
|
|
28
|
+
function currentRevisionOrDeleted(padId) {
|
|
29
|
+
const row = db.prepare("SELECT revision FROM pads WHERE id = ?").get(padId);
|
|
30
|
+
return row?.revision ?? null;
|
|
31
|
+
}
|
|
32
|
+
export function assertRowChanged(padId, predicateRevision, changed) {
|
|
33
|
+
if (!changed)
|
|
34
|
+
throw revisionMismatchError(padId, predicateRevision, currentRevisionOrDeleted(padId));
|
|
35
|
+
}
|
|
36
|
+
export function checkRevision(pad, expected, required) {
|
|
37
|
+
if (expected == null) {
|
|
38
|
+
if (required)
|
|
39
|
+
throw new Error(`expected_revision is required to overwrite pad ${pad.id}. Current revision: ${pad.revision}. Read it first with pad_read.`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (expected !== pad.revision)
|
|
43
|
+
throw revisionMismatchError(pad.id, expected, pad.revision);
|
|
44
|
+
}
|
|
45
|
+
export function bumpPad(padId, predicateRevision, set, ...params) {
|
|
46
|
+
const where = predicateRevision != null ? "id = ? AND revision = ?" : "id = ?";
|
|
47
|
+
const whereParams = predicateRevision != null ? [padId, predicateRevision] : [padId];
|
|
48
|
+
const row = db.prepare(`UPDATE pads SET ${set}, revision = revision + 1,
|
|
49
|
+
updated_by = ?, updated_at = datetime('now') WHERE ${where} RETURNING revision`).get(...params, currentActor(), ...whereParams);
|
|
50
|
+
assertRowChanged(padId, predicateRevision, row != null);
|
|
51
|
+
return row.revision;
|
|
52
|
+
}
|
|
53
|
+
export const APPEND_WITH_SEPARATOR_SET = "content = content || (CASE WHEN content = '' OR substr(content, -1) = char(10) THEN '' ELSE char(10) END) || ?";
|
|
54
|
+
export function getActivePadByName(projectId, name) {
|
|
55
|
+
return db.prepare("SELECT * FROM pads WHERE project_id = ? AND name = ? AND archived = 0").get(projectId, name);
|
|
56
|
+
}
|
|
57
|
+
export function listActivePads(projectId) {
|
|
58
|
+
return db.prepare(`SELECT name, revision, length(content) AS content_length, updated_by, updated_at
|
|
59
|
+
FROM pads WHERE project_id = ? AND archived = 0 ORDER BY name`).all(projectId);
|
|
60
|
+
}
|
|
61
|
+
export function overwritePadContent(projectId, padId, content, expectedRevision) {
|
|
62
|
+
const pad = getPadMeta(projectId, padId);
|
|
63
|
+
checkRevision(pad, expectedRevision, true);
|
|
64
|
+
return bumpPad(padId, pad.revision, "content = ?", content);
|
|
65
|
+
}
|
|
66
|
+
export function createPad(projectId, name, content, tags) {
|
|
67
|
+
const row = db.prepare(`INSERT OR IGNORE INTO pads (project_id, name, content, tags, updated_by)
|
|
68
|
+
VALUES (?, ?, ?, ?, ?) RETURNING id`).get(projectId, name, content, JSON.stringify(tags), currentActor());
|
|
69
|
+
return row?.id ?? null;
|
|
70
|
+
}
|
package/dist/portfolio.js
CHANGED
|
@@ -4,7 +4,10 @@ import { existsSync } from "node:fs";
|
|
|
4
4
|
import { parseTags } from "./result.js";
|
|
5
5
|
import { ACTIVE_TODO_SQL, OPEN_BLOCKERS_SQL } from "./todoScope.js";
|
|
6
6
|
import { pendingQuestionCount } from "./questions.js";
|
|
7
|
+
import { ACTIVE_TIMER_WHERE as ACTIVE_WAKE_WHERE } from "./wakeSql.js";
|
|
8
|
+
export { ACTIVE_WAKE_WHERE };
|
|
7
9
|
import { leadSessionEnded, readLeadTurnState } from "./leadState.js";
|
|
10
|
+
import { finishedIdle } from "./firstPrompt.js";
|
|
8
11
|
import { liveTargets, ownershipLiveness, rowOwnership } from "./tmux.js";
|
|
9
12
|
export const PORTFOLIO_REASONS = [
|
|
10
13
|
"needs_human",
|
|
@@ -26,7 +29,6 @@ const NEEDS_HUMAN_ITEM_CAP = 5;
|
|
|
26
29
|
const WAKE_OVERDUE_GRACE = "-5 minutes";
|
|
27
30
|
const STALE_IN_PROGRESS = "-48 hours";
|
|
28
31
|
const WAKE_UPCOMING = "+24 hours";
|
|
29
|
-
export const ACTIVE_WAKE_WHERE = "cancelled_at IS NULL AND (fired_at IS NULL OR repeat_every_ms IS NOT NULL)";
|
|
30
32
|
export function leadText(p) {
|
|
31
33
|
return p.lead.state === "alive" && p.lead.turn !== "unknown"
|
|
32
34
|
? `alive, turn ${p.lead.turn === "working" ? "working" : "ended"}`
|
|
@@ -84,7 +86,7 @@ function projectRow(project, now, snapshot) {
|
|
|
84
86
|
}
|
|
85
87
|
tagged.sort((a, b) => (a.updated_at < b.updated_at ? -1 : a.updated_at > b.updated_at ? 1 : a.id - b.id));
|
|
86
88
|
const agents = db
|
|
87
|
-
.prepare("SELECT id, kind, agent_state, tmux_target, tmux_socket, pane_pid FROM agents WHERE project_id = ? AND status = 'running' ORDER BY id")
|
|
89
|
+
.prepare("SELECT id, kind, agent_state, resumed_at, tmux_target, tmux_socket, pane_pid FROM agents WHERE project_id = ? AND status = 'running' ORDER BY id")
|
|
88
90
|
.all(project.id);
|
|
89
91
|
const liveness = (a) => ownershipLiveness(rowOwnership(a, snapshot));
|
|
90
92
|
const workers = { working: 0, idle: 0, needs_input: 0, other: 0, unreachable: 0, unconfirmed: 0 };
|
|
@@ -115,8 +117,10 @@ function projectRow(project, now, snapshot) {
|
|
|
115
117
|
workers.working++;
|
|
116
118
|
liveWorking++;
|
|
117
119
|
}
|
|
118
|
-
else if (a
|
|
120
|
+
else if (finishedIdle(a))
|
|
119
121
|
workers.idle++;
|
|
122
|
+
else if (a.agent_state === "idle")
|
|
123
|
+
workers.other++;
|
|
120
124
|
else if (a.agent_state === "waiting") {
|
|
121
125
|
workers.needs_input++;
|
|
122
126
|
liveWaiting++;
|
package/dist/projectRemove.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { backupNow } from "./backup.js";
|
|
2
2
|
import { dataDir, db } from "./db.js";
|
|
3
|
-
import { LEAD_KIND } from "./
|
|
3
|
+
import { LEAD_KIND } from "./agentKinds.js";
|
|
4
4
|
import { foreignSocket, liveTargets, observationFailed, rowOwnership } from "./tmux.js";
|
|
5
5
|
export const PROJECT_OWNER_TABLES = [
|
|
6
6
|
"pads",
|
package/dist/projectYml.js
CHANGED
|
@@ -4,7 +4,7 @@ import { resolve, sep } from "node:path";
|
|
|
4
4
|
import { parse } from "yaml";
|
|
5
5
|
import { harnessNames } from "./harnesses.js";
|
|
6
6
|
import { isValidProfileName } from "./profiles.js";
|
|
7
|
-
import { errorMessage } from "./
|
|
7
|
+
import { errorMessage } from "./errors.js";
|
|
8
8
|
import { isWindowLayout, WINDOW_LAYOUTS } from "./tmux.js";
|
|
9
9
|
import { globalConfigPath, isEmptyYamlDocument, readGlobalConfig, resolveAttachSetting, } from "./globalConfig.js";
|
|
10
10
|
export const BUILT_IN_PROJECT_YML = Object.freeze({
|
package/dist/ptys.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { platform } from "node:os";
|
|
4
|
+
import { parseEtimeSeconds } from "./etime.js";
|
|
5
|
+
export { parseEtimeSeconds } from "./etime.js";
|
|
4
6
|
const PS_LINE = /^\s*(\S+)\s+(\d+)\s+(.+?)\s*$/;
|
|
5
7
|
export function parsePsSnapshot(output) {
|
|
6
8
|
const rows = [];
|
|
@@ -100,15 +102,6 @@ export function ptyHeadroom() {
|
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
const PS_AGE_LINE = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.+?)\s*$/;
|
|
103
|
-
export function parseEtimeSeconds(etime) {
|
|
104
|
-
const m = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(etime) ?? /^(\d+)$/.exec(etime);
|
|
105
|
-
if (!m)
|
|
106
|
-
return null;
|
|
107
|
-
if (m.length === 2)
|
|
108
|
-
return Number(m[1]);
|
|
109
|
-
const [, days, hours, minutes, seconds] = m;
|
|
110
|
-
return (Number(days ?? 0) * 24 + Number(hours ?? 0)) * 3600 + Number(minutes) * 60 + Number(seconds);
|
|
111
|
-
}
|
|
112
105
|
export function parsePsSnapshotWithAge(output) {
|
|
113
106
|
const rows = [];
|
|
114
107
|
for (const line of output.split("\n")) {
|
package/dist/queenAudit.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { captureQueenWriteIdentity, effectiveProjectId, isQueenLead } from "./context.js";
|
|
2
|
+
import { insertQueenAuditRow, QUEEN_AUDIT_DEFAULT_LIMIT, QUEEN_AUDIT_MAX_LIMIT, QUEEN_AUDIT_SUMMARY_MAX, } from "./queenAuditStore.js";
|
|
2
3
|
import { db } from "./db.js";
|
|
3
4
|
import { cutToUnitBudget, flatten } from "./slug.js";
|
|
4
|
-
export
|
|
5
|
-
export const QUEEN_AUDIT_DEFAULT_LIMIT = 20;
|
|
6
|
-
export const QUEEN_AUDIT_MAX_LIMIT = 100;
|
|
7
|
-
export const QUEEN_AUDIT_RETENTION = "-30 days";
|
|
8
|
-
export const QUEEN_AUDIT_MAX_ROWS = 20_000;
|
|
5
|
+
export { QUEEN_AUDIT_DEFAULT_LIMIT, QUEEN_AUDIT_MAX_LIMIT, QUEEN_AUDIT_MAX_ROWS, QUEEN_AUDIT_RETENTION, QUEEN_AUDIT_SUMMARY_MAX, recordQueenRefusal, } from "./queenAuditStore.js";
|
|
9
6
|
function resource(type, value) {
|
|
10
7
|
return typeof value === "number" && Number.isInteger(value) && value > 0 ? { type, id: value } : null;
|
|
11
8
|
}
|
|
@@ -13,11 +10,6 @@ function removalSummary(r) {
|
|
|
13
10
|
const counts = Object.entries((r.counts ?? {})).map(([t, n]) => `${t} ${n}`);
|
|
14
11
|
return `removed project "${r.name}"${counts.length > 0 ? ` with ${counts.join(", ")}` : ""}`;
|
|
15
12
|
}
|
|
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
|
-
}
|
|
21
13
|
export const QUEEN_AUDIT_OPERATIONS = {
|
|
22
14
|
todo_create: {
|
|
23
15
|
mode: "database",
|
|
@@ -85,14 +77,6 @@ function insertAudit(operation, targetProjectId, args, receipt, identity) {
|
|
|
85
77
|
const summary = text.length <= QUEEN_AUDIT_SUMMARY_MAX ? text : cutToUnitBudget(text, QUEEN_AUDIT_SUMMARY_MAX - 1) + "…";
|
|
86
78
|
insertQueenAuditRow(operation, targetProjectId, identity, touched.type, touched.id, summary);
|
|
87
79
|
}
|
|
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);
|
|
95
|
-
}
|
|
96
80
|
export function commitQueenStateWrite(operation, targetProjectId, args, mutate) {
|
|
97
81
|
const identity = captureQueenWriteIdentity(targetProjectId);
|
|
98
82
|
if (!identity)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { db } from "./db.js";
|
|
2
|
+
import { cutToUnitBudget, flatten } from "./slug.js";
|
|
3
|
+
export const QUEEN_AUDIT_SUMMARY_MAX = 160;
|
|
4
|
+
export const QUEEN_AUDIT_DEFAULT_LIMIT = 20;
|
|
5
|
+
export const QUEEN_AUDIT_MAX_LIMIT = 100;
|
|
6
|
+
export const QUEEN_AUDIT_RETENTION = "-30 days";
|
|
7
|
+
export const QUEEN_AUDIT_MAX_ROWS = 20_000;
|
|
8
|
+
export function insertQueenAuditRow(operation, targetProjectId, identity, resourceType, resourceId, summary, outcome = "confirmed", problem = "") {
|
|
9
|
+
db.prepare(`INSERT INTO queen_audit
|
|
10
|
+
(actor_id, home_project_id, target_project_id, operation, resource_type, resource_id, summary, outcome, problem)
|
|
11
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(identity.actor_id, identity.home_project_id, targetProjectId, operation, resourceType, resourceId, summary, outcome, problem);
|
|
12
|
+
}
|
|
13
|
+
export function recordQueenRefusal(operation, targetProjectId, problem, identity) {
|
|
14
|
+
const text = flatten(problem);
|
|
15
|
+
const summaryText = `refused: ${text}`;
|
|
16
|
+
const summary = summaryText.length <= QUEEN_AUDIT_SUMMARY_MAX
|
|
17
|
+
? summaryText
|
|
18
|
+
: cutToUnitBudget(summaryText, QUEEN_AUDIT_SUMMARY_MAX - 1) + "…";
|
|
19
|
+
insertQueenAuditRow(operation, targetProjectId, identity, "project", targetProjectId, summary, "refused", text);
|
|
20
|
+
}
|