blun-king-cli 9.0.0 → 9.0.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/LIESMICH.txt +1 -7
- package/README.md +4 -16
- package/bin/blun.js +248 -160
- package/bin/core-bootstrap.js +47 -0
- package/bin/king.js +277 -1
- package/bin/launcher-mode.js +2 -1
- package/bin/launcher-runtime.js +221 -0
- package/bin/plugin-bootstrap.js +0 -0
- package/bin/private-paths.js +0 -0
- package/bin/update-lease.js +399 -0
- package/bin/update-notice.js +1094 -0
- package/blun.mjs +4060 -6667
- package/package.json +3 -10
- package/skills/screenshot-lesen/SKILL.md +0 -1
- package/skills/web-lesen/SKILL.md +0 -1
- package/telegram-plugin/dist/bridge.mjs +1 -21
- package/mnemo/access_routes.js +0 -692
- package/mnemo/agent_governance.js +0 -4242
- package/mnemo/agent_mail.js +0 -901
- package/mnemo/bootstrap_auto.js +0 -137
- package/mnemo/brief_coordination.js +0 -226
- package/mnemo/code_read_tools.js +0 -375
- package/mnemo/context_preview_tools.js +0 -603
- package/mnemo/embeddings.js +0 -66
- package/mnemo/external_repo_ops.js +0 -575
- package/mnemo/facts/example-project-rules.json +0 -90
- package/mnemo/facts/example.json +0 -34
- package/mnemo/identity_schema.sql +0 -139
- package/mnemo/journal_schema.js +0 -561
- package/mnemo/loop_doctor_tools.js +0 -661
- package/mnemo/mail_secret_refs.js +0 -150
- package/mnemo/mcp.js +0 -9309
- package/mnemo/memory_consolidation.js +0 -1914
- package/mnemo/memory_health_tools.js +0 -165
- package/mnemo/package.json +0 -79
- package/mnemo/protected_scope_gate.js +0 -627
- package/mnemo/resource_access_control.js +0 -684
- package/mnemo/runtime_governance.js +0 -1256
- package/mnemo/runtime_turn_gate.js +0 -862
- package/mnemo/sandbox.js +0 -143
- package/mnemo/schema.sql +0 -389
- package/mnemo/shared_utils.js +0 -763
- package/mnemo/skills/agent-auto-resume/SKILL.md +0 -56
- package/mnemo/skills/agent_hand/SKILL.md +0 -43
- package/mnemo/skills/agent_hand/run.js +0 -63
- package/mnemo/skills/book_flight/SKILL.md +0 -34
- package/mnemo/skills/external_repo_review/SKILL.md +0 -43
- package/mnemo/skills/external_repo_review/run.js +0 -73
- package/mnemo/skills/pay_invoice/SKILL.md +0 -34
- package/mnemo/team_quality_ops.js +0 -944
- package/mnemo/timeline_report_tools.js +0 -810
- package/mnemo/write_gate_risk.js +0 -80
- package/mnemo/writer_health.js +0 -152
- package/skills/doku-ingestion/SKILL.md +0 -48
- package/skills/doku-ingestion/ingest_docs.py +0 -133
|
@@ -1,661 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const { requeueStaleDispatchedBriefs } = require("./brief_coordination");
|
|
4
|
-
|
|
5
|
-
const LOOP_DOCTOR_TOOL_DEFS = {
|
|
6
|
-
mem_loop_doctor: {
|
|
7
|
-
description: "Diagnose autonomous agent loop health from Mnemo state: heartbeats, engine cooldowns, pending/dispatched briefs, stale actions, autonomy tasks, and recent handoffs.",
|
|
8
|
-
inputSchema: {
|
|
9
|
-
type: "object",
|
|
10
|
-
properties: {
|
|
11
|
-
agent_name: { type: "string" },
|
|
12
|
-
stale_minutes: { type: "integer", default: 30 },
|
|
13
|
-
recent_hours: { type: "integer", default: 24 },
|
|
14
|
-
include_recent: { type: "boolean", default: false },
|
|
15
|
-
},
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
mem_agent_name_migrate: {
|
|
19
|
-
description: "Consolidate an agent-name variant into a canonical lowercase agent id across Mnemo coordination tables. Defaults to dry-run; pass dry_run:false to update non-conflicting rows.",
|
|
20
|
-
inputSchema: {
|
|
21
|
-
type: "object",
|
|
22
|
-
properties: {
|
|
23
|
-
from_agent: { type: "string" },
|
|
24
|
-
to_agent: { type: "string" },
|
|
25
|
-
dry_run: { type: "boolean", default: true },
|
|
26
|
-
},
|
|
27
|
-
required: ["from_agent", "to_agent"],
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
mem_brief_requeue_stale: {
|
|
31
|
-
description: "Dry-run or requeue stale dispatched briefs back to pending for offline or non-heartbeating agents. Use after mem_loop_doctor reports stale_dispatched_brief.",
|
|
32
|
-
inputSchema: {
|
|
33
|
-
type: "object",
|
|
34
|
-
properties: {
|
|
35
|
-
agent_name: { type: "string" },
|
|
36
|
-
older_than_minutes: { type: "integer", default: 30 },
|
|
37
|
-
agent_stale_sec: { type: "integer", default: 300 },
|
|
38
|
-
limit: { type: "integer", default: 25 },
|
|
39
|
-
dry_run: { type: "boolean", default: true },
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
mem_brief_reconcile_stale: {
|
|
44
|
-
description: "Safely reconcile stale dispatched briefs. Defaults to dry-run. Can close non-executable status/idle/autonomy-pointer briefs and optionally requeue executable briefs.",
|
|
45
|
-
inputSchema: {
|
|
46
|
-
type: "object",
|
|
47
|
-
properties: {
|
|
48
|
-
agent_name: { type: "string" },
|
|
49
|
-
older_than_minutes: { type: "integer", default: 60 },
|
|
50
|
-
limit: { type: "integer", default: 50 },
|
|
51
|
-
close_nonexec: { type: "boolean", default: false },
|
|
52
|
-
requeue_exec: { type: "boolean", default: false },
|
|
53
|
-
dry_run: { type: "boolean", default: true },
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
function handleLoopDoctorTool(db, name, args) {
|
|
60
|
-
if (!LOOP_DOCTOR_TOOL_DEFS[name]) return { handled: false };
|
|
61
|
-
if (name === "mem_loop_doctor") return { handled: true, result: loopDoctor(db, args || {}) };
|
|
62
|
-
if (name === "mem_agent_name_migrate") return { handled: true, result: agentNameMigrate(db, args || {}) };
|
|
63
|
-
if (name === "mem_brief_requeue_stale") return { handled: true, result: requeueStaleDispatchedBriefs(db, args || {}) };
|
|
64
|
-
if (name === "mem_brief_reconcile_stale") return { handled: true, result: briefReconcileStale(db, args || {}) };
|
|
65
|
-
return { handled: false };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function loopDoctor(db, args) {
|
|
69
|
-
const agentFilter = normalizeAgentId(clean(args.agent_name, 80));
|
|
70
|
-
const staleMinutes = clampInt(args.stale_minutes, 30, 5, 24 * 60);
|
|
71
|
-
const recentHours = clampInt(args.recent_hours, 24, 1, 24 * 14);
|
|
72
|
-
const includeRecent = !!args.include_recent;
|
|
73
|
-
const now = new Date();
|
|
74
|
-
const nowIso = now.toISOString();
|
|
75
|
-
const staleIso = new Date(now.getTime() - staleMinutes * 60 * 1000).toISOString();
|
|
76
|
-
const recentIso = new Date(now.getTime() - recentHours * 60 * 60 * 1000).toISOString();
|
|
77
|
-
const agents = agentNames(db, agentFilter);
|
|
78
|
-
|
|
79
|
-
const rows = agents.map((agentName) => agentSnapshot(db, agentName, { now, nowIso, staleIso, recentIso, staleMinutes, includeRecent }));
|
|
80
|
-
const globals = globalSnapshot(db, { agentFilter, staleIso, recentIso });
|
|
81
|
-
const summary = summarize(rows, globals);
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
now: nowIso,
|
|
85
|
-
stale_minutes: staleMinutes,
|
|
86
|
-
recent_hours: recentHours,
|
|
87
|
-
agent_filter: agentFilter || null,
|
|
88
|
-
summary,
|
|
89
|
-
agents: rows,
|
|
90
|
-
global: globals,
|
|
91
|
-
next_step: summary.status === "ok"
|
|
92
|
-
? "Loops look healthy from Mnemo state. Keep watching pending briefs, open findings, and reviewer queues."
|
|
93
|
-
: (summary.status === "blocked"
|
|
94
|
-
? "Fix current engine/auth/heartbeat blockers first, then re-run mem_loop_doctor before assuming the team is working."
|
|
95
|
-
: "Current loops are not hard-blocked. Clean stale queues, case splits, and old actions as maintenance while active work continues."),
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function agentSnapshot(db, agentName, ctx) {
|
|
100
|
-
const registry = tableExists(db, "agent_registry")
|
|
101
|
-
? safeGet(db, "SELECT agent_name, display_name, host, pid, status, last_seen_at, meta_json FROM agent_registry WHERE agent_name=?", [agentName])
|
|
102
|
-
: null;
|
|
103
|
-
const live = tableExists(db, "agent_status_live")
|
|
104
|
-
? safeGet(db, "SELECT agent_name, current_task, current_brief_id, blocked_on, dnd_until, host, pid, last_heartbeat_at, meta_json FROM agent_status_live WHERE agent_name=?", [agentName])
|
|
105
|
-
: null;
|
|
106
|
-
const queue = briefQueue(db, agentName);
|
|
107
|
-
const actions = actionHealth(db, agentName, ctx);
|
|
108
|
-
const autonomy = autonomyHealth(db, agentName);
|
|
109
|
-
const handoff = lastHandoff(db, agentName);
|
|
110
|
-
const registryMeta = parseJson(registry && registry.meta_json);
|
|
111
|
-
const liveMeta = parseJson(live && live.meta_json);
|
|
112
|
-
const currentEngine = String((registryMeta && registryMeta.engine) || (liveMeta && liveMeta.engine) || "").toLowerCase();
|
|
113
|
-
const engineBlocked = !!((registryMeta && registryMeta.engine_blocked) || (liveMeta && liveMeta.engine_blocked));
|
|
114
|
-
const heartbeat = freshestTimestamp([
|
|
115
|
-
{ at: live && live.last_heartbeat_at, source: "agent_status_live" },
|
|
116
|
-
{ at: registry && registry.last_seen_at, source: "agent_registry" },
|
|
117
|
-
]);
|
|
118
|
-
const heartbeatAt = heartbeat.at || "";
|
|
119
|
-
const heartbeatAge = ageMinutes(heartbeatAt, ctx.now);
|
|
120
|
-
const problems = [];
|
|
121
|
-
const nextActions = [];
|
|
122
|
-
|
|
123
|
-
if (!registry && !live) {
|
|
124
|
-
problems.push("agent_not_registered");
|
|
125
|
-
nextActions.push("Start the agent loop or call mem_connect_register/mem_agent_status_set from its runtime.");
|
|
126
|
-
}
|
|
127
|
-
if (heartbeatAge !== null && heartbeatAge > ctx.staleMinutes) {
|
|
128
|
-
problems.push("stale_heartbeat");
|
|
129
|
-
nextActions.push(`Restart or inspect the loop; last heartbeat is ${heartbeatAge} minutes old.`);
|
|
130
|
-
}
|
|
131
|
-
if (engineBlocked) {
|
|
132
|
-
problems.push("engine_blocked");
|
|
133
|
-
nextActions.push("Fix CLI authentication or engine availability; loop is intentionally cooling down.");
|
|
134
|
-
}
|
|
135
|
-
if (live && live.blocked_on) {
|
|
136
|
-
problems.push("blocked_on");
|
|
137
|
-
nextActions.push(`Resolve blocker: ${redact(live.blocked_on, 180)}`);
|
|
138
|
-
}
|
|
139
|
-
if (queue.dispatched > 0 && queue.oldest_dispatched_minutes !== null && queue.oldest_dispatched_minutes > ctx.staleMinutes) {
|
|
140
|
-
problems.push("stale_dispatched_brief");
|
|
141
|
-
nextActions.push("A dispatched brief is stale; requeue, complete, or investigate the worker run.");
|
|
142
|
-
}
|
|
143
|
-
if (queue.pending > 0 && queue.oldest_pending_minutes !== null && queue.oldest_pending_minutes > ctx.staleMinutes) {
|
|
144
|
-
problems.push("pending_brief_waiting");
|
|
145
|
-
nextActions.push("Pending briefs are waiting; confirm the loop polls the right hub and agent name.");
|
|
146
|
-
}
|
|
147
|
-
if (actions.stale_started > 0) {
|
|
148
|
-
problems.push("stale_started_action");
|
|
149
|
-
nextActions.push("Close or investigate started actions older than the stale threshold.");
|
|
150
|
-
}
|
|
151
|
-
const authFailuresStillBlock = actions.auth_failed_recent > 0
|
|
152
|
-
&& !actions.auth_failures_superseded_by_active_work
|
|
153
|
-
&& !(currentEngine && currentEngine !== "agent" && !engineBlocked);
|
|
154
|
-
if (authFailuresStillBlock) {
|
|
155
|
-
problems.push("recent_auth_failures");
|
|
156
|
-
nextActions.push("Fix CLI auth before expecting autonomous execution.");
|
|
157
|
-
}
|
|
158
|
-
if (actions.failed_recent > 0) {
|
|
159
|
-
problems.push("recent_failures");
|
|
160
|
-
nextActions.push("Read recent failed actions and create/clear quality findings or blockers.");
|
|
161
|
-
}
|
|
162
|
-
if (autonomy.claimed > 0 && actions.active_loop_work === 0 && queue.dispatched === 0) {
|
|
163
|
-
problems.push("claimed_autonomy_without_active_work");
|
|
164
|
-
nextActions.push("Claimed autonomy tasks need review, requeue, or a fresh loop execution.");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const status = problems.some((p) => ["engine_blocked", "stale_heartbeat", "recent_auth_failures"].includes(p))
|
|
168
|
-
? "blocked"
|
|
169
|
-
: (problems.length ? "attention" : "ok");
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
agent_name: agentName,
|
|
173
|
-
status,
|
|
174
|
-
problems,
|
|
175
|
-
next_actions: unique(nextActions),
|
|
176
|
-
heartbeat: {
|
|
177
|
-
last_at: heartbeatAt || null,
|
|
178
|
-
age_minutes: heartbeatAge,
|
|
179
|
-
source: heartbeat.source,
|
|
180
|
-
},
|
|
181
|
-
registry: registry ? redactRow(Object.assign({}, registry, { meta_json: registryMeta || registry.meta_json })) : null,
|
|
182
|
-
live_status: live ? redactRow(Object.assign({}, live, { meta_json: liveMeta || live.meta_json })) : null,
|
|
183
|
-
queue,
|
|
184
|
-
actions,
|
|
185
|
-
autonomy,
|
|
186
|
-
last_handoff: handoff,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function agentNames(db, agentFilter) {
|
|
191
|
-
if (agentFilter) return [agentFilter];
|
|
192
|
-
const names = new Set();
|
|
193
|
-
addNames(db, names, "agent_registry", "SELECT agent_name FROM agent_registry WHERE agent_name IS NOT NULL");
|
|
194
|
-
addNames(db, names, "agent_status_live", "SELECT agent_name FROM agent_status_live WHERE agent_name IS NOT NULL");
|
|
195
|
-
addNames(db, names, "agent_brief", "SELECT agent_name FROM agent_brief WHERE agent_name IS NOT NULL");
|
|
196
|
-
addNames(db, names, "agent_action", "SELECT agent_name FROM agent_action WHERE agent_name IS NOT NULL");
|
|
197
|
-
addNames(db, names, "session_handoff", "SELECT agent_name FROM session_handoff WHERE agent_name IS NOT NULL");
|
|
198
|
-
addNames(db, names, "autonomy_task", "SELECT assigned_agent AS agent_name FROM autonomy_task WHERE assigned_agent IS NOT NULL");
|
|
199
|
-
return Array.from(names).sort((a, b) => a.localeCompare(b));
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function addNames(db, names, table, sql) {
|
|
203
|
-
if (!tableExists(db, table)) return;
|
|
204
|
-
for (const row of safeAll(db, sql, [])) {
|
|
205
|
-
const name = clean(row.agent_name, 80);
|
|
206
|
-
if (name) names.add(name);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function briefQueue(db, agentName) {
|
|
211
|
-
if (!tableExists(db, "agent_brief")) return emptyQueue();
|
|
212
|
-
const counts = safeAll(db, "SELECT status, COUNT(*) AS c, MIN(created_at) AS oldest FROM agent_brief WHERE agent_name=? GROUP BY status", [agentName]);
|
|
213
|
-
const out = emptyQueue();
|
|
214
|
-
for (const row of counts) {
|
|
215
|
-
const status = row.status || "unknown";
|
|
216
|
-
out.by_status[status] = row.c || 0;
|
|
217
|
-
if (status === "pending") {
|
|
218
|
-
out.pending = row.c || 0;
|
|
219
|
-
out.oldest_pending_at = row.oldest || null;
|
|
220
|
-
out.oldest_pending_minutes = ageMinutes(row.oldest, new Date());
|
|
221
|
-
}
|
|
222
|
-
if (status === "dispatched") {
|
|
223
|
-
out.dispatched = row.c || 0;
|
|
224
|
-
out.oldest_dispatched_at = row.oldest || null;
|
|
225
|
-
out.oldest_dispatched_minutes = ageMinutes(row.oldest, new Date());
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
out.latest = safeAll(db, "SELECT id, source_agent, status, created_at, dispatched_at, substr(content,1,180) AS preview FROM agent_brief WHERE agent_name=? AND status IN ('pending','dispatched') ORDER BY created_at ASC LIMIT 5", [agentName]).map(redactRow);
|
|
229
|
-
return out;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function emptyQueue() {
|
|
233
|
-
return {
|
|
234
|
-
pending: 0,
|
|
235
|
-
dispatched: 0,
|
|
236
|
-
oldest_pending_at: null,
|
|
237
|
-
oldest_pending_minutes: null,
|
|
238
|
-
oldest_dispatched_at: null,
|
|
239
|
-
oldest_dispatched_minutes: null,
|
|
240
|
-
by_status: {},
|
|
241
|
-
latest: [],
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function actionHealth(db, agentName, ctx) {
|
|
246
|
-
if (!tableExists(db, "agent_action")) {
|
|
247
|
-
return { active_loop_work: 0, stale_started: 0, failed_recent: 0, auth_failed_recent: 0, recent: [] };
|
|
248
|
-
}
|
|
249
|
-
const staleStarted = safeGet(db, "SELECT COUNT(*) AS c FROM agent_action WHERE agent_name=? AND status='started' AND started_at < ?", [agentName, ctx.staleIso]);
|
|
250
|
-
const failedRecent = safeGet(db, "SELECT COUNT(*) AS c FROM agent_action WHERE agent_name=? AND started_at >= ? AND lower(status) IN ('failed','error','blocked','guard_blocked')", [agentName, ctx.recentIso]);
|
|
251
|
-
const authRecent = safeGet(db, "SELECT COUNT(*) AS c FROM agent_action WHERE agent_name=? AND started_at >= ? AND (lower(status)='auth_failed' OR result_json LIKE '%auth_failed%' OR payload_json LIKE '%auth_failed%')", [agentName, ctx.recentIso]);
|
|
252
|
-
const activeLoop = safeGet(db, "SELECT COUNT(*) AS c FROM agent_action WHERE agent_name=? AND status='started' AND topic='agent-loop'", [agentName]);
|
|
253
|
-
const latestAuth = safeGet(db, "SELECT MAX(started_at) AS at FROM agent_action WHERE agent_name=? AND started_at >= ? AND (lower(status)='auth_failed' OR result_json LIKE '%auth_failed%' OR payload_json LIKE '%auth_failed%')", [agentName, ctx.recentIso]);
|
|
254
|
-
const latestActiveLoop = safeGet(db, "SELECT MAX(started_at) AS at FROM agent_action WHERE agent_name=? AND status='started' AND topic='agent-loop'", [agentName]);
|
|
255
|
-
const latestAuthMs = latestAuth && latestAuth.at ? Date.parse(latestAuth.at) : NaN;
|
|
256
|
-
const latestActiveMs = latestActiveLoop && latestActiveLoop.at ? Date.parse(latestActiveLoop.at) : NaN;
|
|
257
|
-
const out = {
|
|
258
|
-
active_loop_work: num(activeLoop && activeLoop.c),
|
|
259
|
-
stale_started: num(staleStarted && staleStarted.c),
|
|
260
|
-
failed_recent: num(failedRecent && failedRecent.c),
|
|
261
|
-
auth_failed_recent: num(authRecent && authRecent.c),
|
|
262
|
-
latest_auth_failed_at: latestAuth && latestAuth.at ? latestAuth.at : null,
|
|
263
|
-
latest_active_loop_started_at: latestActiveLoop && latestActiveLoop.at ? latestActiveLoop.at : null,
|
|
264
|
-
auth_failures_superseded_by_active_work: Number.isFinite(latestAuthMs) && Number.isFinite(latestActiveMs) && latestActiveMs > latestAuthMs,
|
|
265
|
-
};
|
|
266
|
-
if (ctx.includeRecent) {
|
|
267
|
-
out.recent = safeAll(db, "SELECT id, action_kind, target, status, topic, started_at, finished_at FROM agent_action WHERE agent_name=? AND started_at >= ? ORDER BY started_at DESC LIMIT 12", [agentName, ctx.recentIso]).map(redactRow);
|
|
268
|
-
}
|
|
269
|
-
return out;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function autonomyHealth(db, agentName) {
|
|
273
|
-
const out = { open: 0, claimed: 0, review: 0, blocked: 0, assigned: 0, latest: [] };
|
|
274
|
-
if (!tableExists(db, "autonomy_task")) return out;
|
|
275
|
-
const rows = safeAll(db, "SELECT status, COUNT(*) AS c FROM autonomy_task WHERE assigned_agent=? GROUP BY status", [agentName]);
|
|
276
|
-
for (const row of rows) {
|
|
277
|
-
out[row.status || "unknown"] = row.c || 0;
|
|
278
|
-
out.assigned += row.c || 0;
|
|
279
|
-
}
|
|
280
|
-
out.latest = safeAll(db, "SELECT id, project, department_name, title, severity, status, updated_at FROM autonomy_task WHERE assigned_agent=? ORDER BY updated_at DESC LIMIT 5", [agentName]).map(redactRow);
|
|
281
|
-
return out;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function lastHandoff(db, agentName) {
|
|
285
|
-
if (!tableExists(db, "session_handoff")) return null;
|
|
286
|
-
const row = safeGet(db, "SELECT id, project, summary, blockers, next_actions, created_at FROM session_handoff WHERE agent_name=? ORDER BY created_at DESC LIMIT 1", [agentName]);
|
|
287
|
-
return row ? redactRow(row) : null;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function globalSnapshot(db, ctx) {
|
|
291
|
-
const out = {};
|
|
292
|
-
out.name_case_collisions = nameCaseCollisions(db);
|
|
293
|
-
if (tableExists(db, "agent_brief")) {
|
|
294
|
-
out.pending_by_agent = safeAll(db, "SELECT agent_name, COUNT(*) AS pending, MIN(created_at) AS oldest_at FROM agent_brief WHERE status='pending' GROUP BY agent_name ORDER BY pending DESC", []).map(redactRow);
|
|
295
|
-
out.stale_dispatched = safeAll(db, "SELECT id, agent_name, source_agent, dispatched_at, substr(content,1,160) AS preview FROM agent_brief WHERE status='dispatched' AND COALESCE(dispatched_at, created_at) < ? ORDER BY COALESCE(dispatched_at, created_at) ASC LIMIT 20", [ctx.staleIso]).map(redactRow);
|
|
296
|
-
}
|
|
297
|
-
if (tableExists(db, "autonomy_task")) {
|
|
298
|
-
out.open_autonomy_by_department = safeAll(db, "SELECT department_name, COUNT(*) AS open FROM autonomy_task WHERE status='open' GROUP BY department_name ORDER BY open DESC", []).map(redactRow);
|
|
299
|
-
}
|
|
300
|
-
if (tableExists(db, "agent_action")) {
|
|
301
|
-
out.recent_auth_failures = safeAll(db, "SELECT id, agent_name, action_kind, target, status, started_at FROM agent_action WHERE started_at >= ? AND (lower(status)='auth_failed' OR result_json LIKE '%auth_failed%' OR payload_json LIKE '%auth_failed%') ORDER BY started_at DESC LIMIT 20", [ctx.recentIso]).map(redactRow);
|
|
302
|
-
}
|
|
303
|
-
return out;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function summarize(agents, globals) {
|
|
307
|
-
const summary = {
|
|
308
|
-
status: "ok",
|
|
309
|
-
agents_total: agents.length,
|
|
310
|
-
ok: agents.filter((a) => a.status === "ok").length,
|
|
311
|
-
attention: agents.filter((a) => a.status === "attention").length,
|
|
312
|
-
blocked: agents.filter((a) => a.status === "blocked").length,
|
|
313
|
-
pending_briefs: agents.reduce((sum, a) => sum + a.queue.pending, 0),
|
|
314
|
-
dispatched_briefs: agents.reduce((sum, a) => sum + a.queue.dispatched, 0),
|
|
315
|
-
stale_started_actions: agents.reduce((sum, a) => sum + a.actions.stale_started, 0),
|
|
316
|
-
recent_auth_failures: agents.reduce((sum, a) => sum + a.actions.auth_failed_recent, 0),
|
|
317
|
-
top_problems: {},
|
|
318
|
-
};
|
|
319
|
-
for (const agent of agents) {
|
|
320
|
-
for (const problem of agent.problems) summary.top_problems[problem] = (summary.top_problems[problem] || 0) + 1;
|
|
321
|
-
}
|
|
322
|
-
if (globals.name_case_collisions && globals.name_case_collisions.length) {
|
|
323
|
-
summary.top_problems.agent_name_case_split = globals.name_case_collisions.length;
|
|
324
|
-
}
|
|
325
|
-
if (summary.blocked) summary.status = "blocked";
|
|
326
|
-
else if (summary.attention || summary.pending_briefs || summary.dispatched_briefs || (globals.name_case_collisions && globals.name_case_collisions.length)) summary.status = "attention";
|
|
327
|
-
return summary;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function nameCaseCollisions(db) {
|
|
331
|
-
const names = agentNames(db, "");
|
|
332
|
-
const groups = new Map();
|
|
333
|
-
for (const name of names) {
|
|
334
|
-
const key = name.toLowerCase();
|
|
335
|
-
if (!groups.has(key)) groups.set(key, []);
|
|
336
|
-
groups.get(key).push(name);
|
|
337
|
-
}
|
|
338
|
-
return Array.from(groups.entries())
|
|
339
|
-
.filter(([, values]) => values.length > 1)
|
|
340
|
-
.map(([canonical, variants]) => ({
|
|
341
|
-
canonical,
|
|
342
|
-
variants,
|
|
343
|
-
problem: "Multiple case variants split queues, heartbeats, and actions. Pick one canonical agent_name and migrate or supersede the stale queue.",
|
|
344
|
-
}));
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function agentNameMigrate(db, args) {
|
|
348
|
-
const fromAgent = String(args.from_agent || "").trim();
|
|
349
|
-
const toAgent = normalizeAgentId(args.to_agent);
|
|
350
|
-
const dryRun = args.dry_run !== false;
|
|
351
|
-
if (!fromAgent || !toAgent) return { error: "from_agent and to_agent required" };
|
|
352
|
-
if (fromAgent === toAgent) return { ok: true, dry_run: dryRun, from_agent: fromAgent, to_agent: toAgent, updated: [], conflicts: [], note: "from_agent already equals to_agent" };
|
|
353
|
-
|
|
354
|
-
const normalRefs = [
|
|
355
|
-
["agent_brief", "agent_name"],
|
|
356
|
-
["agent_brief", "source_agent"],
|
|
357
|
-
["agent_brief_reaction", "agent_name"],
|
|
358
|
-
["agent_action", "agent_name"],
|
|
359
|
-
["session_handoff", "agent_name"],
|
|
360
|
-
["work_claim", "agent_name"],
|
|
361
|
-
["agent_proposal", "agent_name"],
|
|
362
|
-
["autonomy_task", "assigned_agent"],
|
|
363
|
-
["autonomy_task", "reviewer_agent"],
|
|
364
|
-
["department_member", "agent_name"],
|
|
365
|
-
["peer_consult", "source_agent"],
|
|
366
|
-
["peer_consult", "target_agent"],
|
|
367
|
-
["meeting_turn", "agent_name"],
|
|
368
|
-
["problem_attempt", "agent_name"],
|
|
369
|
-
["escalation", "source_agent"],
|
|
370
|
-
];
|
|
371
|
-
const uniqueRefs = [
|
|
372
|
-
["agent_registry", "agent_name"],
|
|
373
|
-
["agent_status_live", "agent_name"],
|
|
374
|
-
["agent_mode", "agent_name"],
|
|
375
|
-
["agent_idle_config", "agent_name"],
|
|
376
|
-
["agent_focus", "agent_name"],
|
|
377
|
-
];
|
|
378
|
-
const updated = [];
|
|
379
|
-
const conflicts = [];
|
|
380
|
-
|
|
381
|
-
const apply = db.transaction(() => {
|
|
382
|
-
for (const [table, column] of normalRefs) {
|
|
383
|
-
if (!columnExists(db, table, column)) continue;
|
|
384
|
-
const count = countExact(db, table, column, fromAgent);
|
|
385
|
-
updated.push({ table, column, rows: count, mode: "update" });
|
|
386
|
-
if (!dryRun && count > 0) db.prepare(`UPDATE ${table} SET ${column}=? WHERE ${column}=?`).run(toAgent, fromAgent);
|
|
387
|
-
}
|
|
388
|
-
for (const [table, column] of uniqueRefs) {
|
|
389
|
-
if (!columnExists(db, table, column)) continue;
|
|
390
|
-
const count = countExact(db, table, column, fromAgent);
|
|
391
|
-
const targetExists = countExact(db, table, column, toAgent) > 0;
|
|
392
|
-
if (count > 0 && targetExists) {
|
|
393
|
-
conflicts.push({ table, column, rows: count, target_exists: true, action: "skipped_unique_conflict" });
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
updated.push({ table, column, rows: count, mode: "unique_update" });
|
|
397
|
-
if (!dryRun && count > 0) db.prepare(`UPDATE ${table} SET ${column}=? WHERE ${column}=?`).run(toAgent, fromAgent);
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
apply();
|
|
402
|
-
return {
|
|
403
|
-
ok: conflicts.length === 0,
|
|
404
|
-
dry_run: dryRun,
|
|
405
|
-
from_agent: fromAgent,
|
|
406
|
-
to_agent: toAgent,
|
|
407
|
-
updated,
|
|
408
|
-
conflicts,
|
|
409
|
-
next_step: dryRun
|
|
410
|
-
? "Review counts, then re-run with dry_run:false if this is the intended canonical agent id."
|
|
411
|
-
: (conflicts.length ? "Some unique identity rows were skipped because the canonical row already exists. Inspect both rows before deleting old identities." : "Migration applied. Re-run mem_loop_doctor to confirm the case split is gone."),
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function briefRequeueStale(db, args) {
|
|
416
|
-
const agentName = normalizeAgentId(args.agent_name);
|
|
417
|
-
const olderThanMinutes = clampInt(args.older_than_minutes, 60, 5, 60 * 24 * 30);
|
|
418
|
-
const limit = clampInt(args.limit, 25, 1, 500);
|
|
419
|
-
const dryRun = args.dry_run !== false;
|
|
420
|
-
if (!tableExists(db, "agent_brief")) return { error: "agent_brief table not found" };
|
|
421
|
-
const thresholdIso = new Date(Date.now() - olderThanMinutes * 60 * 1000).toISOString();
|
|
422
|
-
const where = ["status='dispatched'", "COALESCE(dispatched_at, created_at) < ?"];
|
|
423
|
-
const params = [thresholdIso];
|
|
424
|
-
if (agentName) {
|
|
425
|
-
where.push("lower(agent_name)=lower(?)");
|
|
426
|
-
params.push(agentName);
|
|
427
|
-
}
|
|
428
|
-
params.push(limit);
|
|
429
|
-
const rows = safeAll(db, `
|
|
430
|
-
SELECT id, agent_name, source_agent, created_at, dispatched_at, substr(content,1,220) AS preview
|
|
431
|
-
FROM agent_brief
|
|
432
|
-
WHERE ${where.join(" AND ")}
|
|
433
|
-
ORDER BY COALESCE(dispatched_at, created_at) ASC
|
|
434
|
-
LIMIT ?`, params).map(redactRow);
|
|
435
|
-
|
|
436
|
-
let changed = 0;
|
|
437
|
-
if (!dryRun && rows.length) {
|
|
438
|
-
const ids = rows.map((row) => row.id);
|
|
439
|
-
const placeholders = ids.map(() => "?").join(",");
|
|
440
|
-
changed = db.prepare(`UPDATE agent_brief SET status='pending', dispatched_at=NULL WHERE id IN (${placeholders})`).run(...ids).changes || 0;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
return {
|
|
444
|
-
ok: true,
|
|
445
|
-
dry_run: dryRun,
|
|
446
|
-
agent_name: agentName || null,
|
|
447
|
-
older_than_minutes: olderThanMinutes,
|
|
448
|
-
threshold: thresholdIso,
|
|
449
|
-
count: rows.length,
|
|
450
|
-
changed,
|
|
451
|
-
briefs: rows,
|
|
452
|
-
next_step: dryRun
|
|
453
|
-
? "Review briefs, then re-run with dry_run:false to requeue them."
|
|
454
|
-
: "Requeued stale dispatched briefs. Re-run mem_loop_doctor and confirm the target loop pulls them.",
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function briefReconcileStale(db, args) {
|
|
459
|
-
const agentName = normalizeAgentId(args.agent_name);
|
|
460
|
-
const olderThanMinutes = clampInt(args.older_than_minutes, 60, 5, 60 * 24 * 30);
|
|
461
|
-
const limit = clampInt(args.limit, 50, 1, 500);
|
|
462
|
-
const closeNonexec = args.close_nonexec === true;
|
|
463
|
-
const requeueExec = args.requeue_exec === true;
|
|
464
|
-
const dryRun = args.dry_run !== false;
|
|
465
|
-
if (!tableExists(db, "agent_brief")) return { error: "agent_brief table not found" };
|
|
466
|
-
|
|
467
|
-
const thresholdIso = new Date(Date.now() - olderThanMinutes * 60 * 1000).toISOString();
|
|
468
|
-
const where = ["status='dispatched'", "COALESCE(dispatched_at, created_at) < ?"];
|
|
469
|
-
const params = [thresholdIso];
|
|
470
|
-
if (agentName) {
|
|
471
|
-
where.push("lower(agent_name)=lower(?)");
|
|
472
|
-
params.push(agentName);
|
|
473
|
-
}
|
|
474
|
-
params.push(limit);
|
|
475
|
-
|
|
476
|
-
const rows = safeAll(db, `
|
|
477
|
-
SELECT id, agent_name, source_agent, channel, status, created_at, dispatched_at, meta_json, content
|
|
478
|
-
FROM agent_brief
|
|
479
|
-
WHERE ${where.join(" AND ")}
|
|
480
|
-
ORDER BY COALESCE(dispatched_at, created_at) ASC
|
|
481
|
-
LIMIT ?`, params);
|
|
482
|
-
|
|
483
|
-
const classified = rows.map((row) => {
|
|
484
|
-
const classification = classifyBrief(row);
|
|
485
|
-
return redactRow({
|
|
486
|
-
id: row.id,
|
|
487
|
-
agent_name: row.agent_name,
|
|
488
|
-
source_agent: row.source_agent,
|
|
489
|
-
created_at: row.created_at,
|
|
490
|
-
dispatched_at: row.dispatched_at,
|
|
491
|
-
classification,
|
|
492
|
-
preview: row.content || "",
|
|
493
|
-
});
|
|
494
|
-
});
|
|
495
|
-
const nonexecIds = rows.filter((row) => classifyBrief(row) !== "executable").map((row) => row.id);
|
|
496
|
-
const execIds = rows.filter((row) => classifyBrief(row) === "executable").map((row) => row.id);
|
|
497
|
-
|
|
498
|
-
let closed_nonexec = 0;
|
|
499
|
-
let requeued_exec = 0;
|
|
500
|
-
if (!dryRun) {
|
|
501
|
-
if (closeNonexec && nonexecIds.length) {
|
|
502
|
-
const placeholders = nonexecIds.map(() => "?").join(",");
|
|
503
|
-
closed_nonexec = db.prepare(
|
|
504
|
-
`UPDATE agent_brief SET status='done', done_at=?, outcome=? WHERE id IN (${placeholders})`
|
|
505
|
-
).run(new Date().toISOString(), "Stale non-executable brief reconciled automatically by mem_brief_reconcile_stale.", ...nonexecIds).changes || 0;
|
|
506
|
-
}
|
|
507
|
-
if (requeueExec && execIds.length) {
|
|
508
|
-
const placeholders = execIds.map(() => "?").join(",");
|
|
509
|
-
requeued_exec = db.prepare(`UPDATE agent_brief SET status='pending', dispatched_at=NULL WHERE id IN (${placeholders})`).run(...execIds).changes || 0;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
return {
|
|
514
|
-
ok: true,
|
|
515
|
-
dry_run: dryRun,
|
|
516
|
-
agent_name: agentName || null,
|
|
517
|
-
older_than_minutes: olderThanMinutes,
|
|
518
|
-
threshold: thresholdIso,
|
|
519
|
-
count: rows.length,
|
|
520
|
-
nonexec_count: nonexecIds.length,
|
|
521
|
-
executable_count: execIds.length,
|
|
522
|
-
closed_nonexec,
|
|
523
|
-
requeued_exec,
|
|
524
|
-
briefs: classified,
|
|
525
|
-
next_step: dryRun
|
|
526
|
-
? "Review classifications, then run with dry_run:false and close_nonexec:true. Only set requeue_exec:true when the old executable briefs are still valid."
|
|
527
|
-
: "Reconciliation applied. Re-run mem_loop_doctor to verify stale dispatched noise is reduced.",
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function classifyBrief(row) {
|
|
532
|
-
const meta = parseJson(row && row.meta_json) || {};
|
|
533
|
-
if (meta.no_action || meta.status_only || meta.idle_cycle || meta.autonomy_task_id) return "nonexec_meta";
|
|
534
|
-
if (meta.type && /^(team_status|deploy_status|status|status_update|info)$/i.test(String(meta.type))) return "nonexec_meta";
|
|
535
|
-
if (meta.status && /^(done|ok|complete|completed|deployed|verified|info|status|update|team_update|team-update)$/i.test(String(meta.status))) return "nonexec_meta";
|
|
536
|
-
|
|
537
|
-
const text = String((row && row.content) || "").trim();
|
|
538
|
-
if (/^\s*\[IDLE-CYCLE\]/i.test(text)) return "nonexec_idle";
|
|
539
|
-
if (/^\s*(\[(status|team-status|info|fyi|update)\]|status\b|team-status\b|info\s*[:\-]|fyi\s*[:\-]|update\s*[:\-])/i.test(text)) {
|
|
540
|
-
if (!/\b(action required|todo|to do|next action|bitte\s+(fix|change|update|deploy|restart|pruef|check|mach|baue|implement)|please\s+(fix|change|update|deploy|restart|check|implement|build))\b/i.test(text)) {
|
|
541
|
-
return "nonexec_status";
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
return "executable";
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function countExact(db, table, column, value) {
|
|
548
|
-
try {
|
|
549
|
-
const row = db.prepare(`SELECT COUNT(*) AS c FROM ${table} WHERE ${column}=?`).get(value);
|
|
550
|
-
return num(row && row.c);
|
|
551
|
-
} catch {
|
|
552
|
-
return 0;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
function columnExists(db, table, column) {
|
|
557
|
-
if (!tableExists(db, table)) return false;
|
|
558
|
-
try {
|
|
559
|
-
return db.prepare(`PRAGMA table_info(${table})`).all().some((row) => row.name === column);
|
|
560
|
-
} catch {
|
|
561
|
-
return false;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function normalizeAgentId(value) {
|
|
566
|
-
return String(value || "").trim().toLowerCase();
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function safeGet(db, sql, params) {
|
|
570
|
-
try { return db.prepare(sql).get(...(params || [])) || null; } catch { return null; }
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
function safeAll(db, sql, params) {
|
|
574
|
-
try { return db.prepare(sql).all(...(params || [])); } catch { return []; }
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
function tableExists(db, name) {
|
|
578
|
-
try { return !!db.prepare("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name=?").get(name); } catch { return false; }
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function parseJson(value) {
|
|
582
|
-
if (!value || typeof value !== "string") return null;
|
|
583
|
-
try { return JSON.parse(value); } catch { return null; }
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
function clean(value, max) {
|
|
587
|
-
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
588
|
-
if (!text) return "";
|
|
589
|
-
return text.length <= max ? text : text.slice(0, max - 1).trimEnd() + "...";
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function clampInt(value, fallback, min, max) {
|
|
593
|
-
const parsed = Number.parseInt(value, 10);
|
|
594
|
-
if (!Number.isFinite(parsed)) return fallback;
|
|
595
|
-
return Math.max(min, Math.min(max, parsed));
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function ageMinutes(value, now) {
|
|
599
|
-
if (!value) return null;
|
|
600
|
-
const ts = Date.parse(value);
|
|
601
|
-
if (!Number.isFinite(ts)) return null;
|
|
602
|
-
return Math.max(0, Math.round(((now || new Date()).getTime() - ts) / 60000));
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
function freshestTimestamp(items) {
|
|
606
|
-
let best = { at: "", source: null, ts: -Infinity };
|
|
607
|
-
for (const item of items || []) {
|
|
608
|
-
if (!item || !item.at) continue;
|
|
609
|
-
const ts = Date.parse(item.at);
|
|
610
|
-
if (!Number.isFinite(ts)) continue;
|
|
611
|
-
if (ts > best.ts) best = { at: item.at, source: item.source || null, ts };
|
|
612
|
-
}
|
|
613
|
-
return { at: best.at, source: best.source };
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function num(value) {
|
|
617
|
-
const n = Number(value);
|
|
618
|
-
return Number.isFinite(n) ? n : 0;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
function unique(items) {
|
|
622
|
-
return Array.from(new Set((items || []).filter(Boolean)));
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
function redactRow(row) {
|
|
626
|
-
const out = {};
|
|
627
|
-
for (const [key, value] of Object.entries(row || {})) {
|
|
628
|
-
if (typeof value === "string") out[key] = redact(value, 260);
|
|
629
|
-
else if (value && typeof value === "object") out[key] = redactObject(value);
|
|
630
|
-
else out[key] = value;
|
|
631
|
-
}
|
|
632
|
-
return out;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function redactObject(value) {
|
|
636
|
-
if (Array.isArray(value)) return value.slice(0, 20).map((item) => (typeof item === "string" ? redact(item, 160) : item));
|
|
637
|
-
const out = {};
|
|
638
|
-
for (const [key, item] of Object.entries(value || {})) {
|
|
639
|
-
out[key] = typeof item === "string" ? redact(item, 200) : item;
|
|
640
|
-
}
|
|
641
|
-
return out;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
function redact(value, max) {
|
|
645
|
-
let text = String(value || "");
|
|
646
|
-
text = text.replace(/<private>[\s\S]*?<\/private>/gi, "[private]");
|
|
647
|
-
text = text.replace(/\b(sk|pk|rk|ghp|gho|github_pat)_[A-Za-z0-9_=-]{12,}\b/g, "[secret]");
|
|
648
|
-
text = text.replace(/\b(password|passwd|token|secret|api[_-]?key)\s*[:=]\s*[^,\s;}]+/gi, "$1=[secret]");
|
|
649
|
-
text = text.replace(/\s+/g, " ").trim();
|
|
650
|
-
if (!text || text.length <= max) return text;
|
|
651
|
-
return text.slice(0, max - 1).trimEnd() + "...";
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
module.exports = {
|
|
655
|
-
LOOP_DOCTOR_TOOL_DEFS,
|
|
656
|
-
handleLoopDoctorTool,
|
|
657
|
-
loopDoctor,
|
|
658
|
-
agentNameMigrate,
|
|
659
|
-
briefRequeueStale,
|
|
660
|
-
briefReconcileStale,
|
|
661
|
-
};
|