@mu-crew/mu 3.13.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +73 -35
- package/README.md +71 -57
- package/dist/chunk-5HECDBK3.js +4482 -0
- package/dist/chunk-5HECDBK3.js.map +1 -0
- package/dist/chunk-AW2NONYU.js +301 -0
- package/dist/chunk-AW2NONYU.js.map +1 -0
- package/dist/chunk-GQNHIPJN.js +4060 -0
- package/dist/chunk-GQNHIPJN.js.map +1 -0
- package/dist/{chunk-3BARPJLY.js → chunk-PZ5GOCWH.js} +16 -2
- package/dist/chunk-PZ5GOCWH.js.map +1 -0
- package/dist/chunk-S424D5EP.js +13766 -0
- package/dist/chunk-S424D5EP.js.map +1 -0
- package/dist/{cli-CKOMHBXV.js → cli-GB2CAVVW.js} +6776 -4350
- package/dist/cli-GB2CAVVW.js.map +1 -0
- package/dist/cli.js +2 -3
- package/dist/cli.js.map +1 -1
- package/dist/errors-KHOYKD7D.js +65 -0
- package/dist/extension/mu-pi.js +1617 -836
- package/dist/extension/mu-pi.js.map +1 -1
- package/dist/index.d.ts +4015 -2561
- package/dist/index.js +614 -297
- package/dist/index.js.map +1 -1
- package/dist/tui-ALPHYTA2.js +1773 -0
- package/dist/tui-ALPHYTA2.js.map +1 -0
- package/docs/ARCHITECTURE.md +117 -66
- package/docs/VOCABULARY.md +112 -49
- package/docs/guide/README.md +3 -3
- package/docs/guide/backends.md +8 -4
- package/docs/guide/cleanup.md +61 -33
- package/docs/guide/delegate.md +141 -66
- package/docs/guide/dispatch.md +148 -53
- package/docs/guide/getting-started.md +37 -44
- package/docs/guide/other-agent-clis.md +102 -0
- package/docs/guide/recovery.md +31 -50
- package/docs/guide/remote.md +115 -45
- package/docs/guide/sql.md +104 -24
- package/docs/guide/stop-a-worker.md +30 -37
- package/docs/guide/sync.md +11 -3
- package/docs/guide/upgrade.md +93 -43
- package/docs/reference/env.md +23 -32
- package/docs/reference/naming.md +12 -7
- package/docs/reference/surface-contract.md +153 -0
- package/package.json +33 -11
- package/prompts/mu-debug.md +3 -1
- package/prompts/mu-refute.md +2 -1
- package/prompts/mu-research.md +3 -1
- package/prompts/mu-review.md +2 -1
- package/prompts/mu-rules-audit.md +5 -4
- package/prompts/mu-sweep.md +2 -1
- package/prompts/mu-tournament.md +4 -2
- package/prompts/mu-until.md +3 -1
- package/prompts/ultrathink.md +1 -1
- package/skills/mu/SKILL.md +60 -291
- package/skills/mu/recipes/adversarial-review.md +12 -103
- package/skills/mu/recipes/brief.md +14 -61
- package/skills/mu/recipes/course-correct.md +23 -0
- package/skills/mu/recipes/findings.md +17 -111
- package/skills/mu/recipes/models.md +17 -92
- package/skills/mu/recipes/move.md +14 -0
- package/skills/mu/recipes/orchestrator-loop.md +20 -119
- package/skills/mu/recipes/recovery.md +8 -29
- package/skills/mu/recipes/remote-workers.md +16 -318
- package/skills/mu/recipes/tasks-or-calls.md +15 -78
- package/skills/mu/recipes/test-health.md +19 -69
- package/skills/mu/recipes/worker.md +17 -24
- package/dist/chunk-45KQLMEL.js +0 -2908
- package/dist/chunk-45KQLMEL.js.map +0 -1
- package/dist/chunk-G434JKV2.js +0 -641
- package/dist/chunk-G434JKV2.js.map +0 -1
- package/dist/chunk-KUGUFBWH.js +0 -12527
- package/dist/chunk-KUGUFBWH.js.map +0 -1
- package/dist/chunk-QITLNZEM.js +0 -87
- package/dist/chunk-QITLNZEM.js.map +0 -1
- package/dist/chunk-VIEYQMQA.js +0 -393
- package/dist/chunk-VIEYQMQA.js.map +0 -1
- package/dist/chunk-WI6AWCMK.js +0 -19
- package/dist/chunk-WI6AWCMK.js.map +0 -1
- package/dist/cli-CKOMHBXV.js.map +0 -1
- package/dist/devtools-LS4ZEPWM.js +0 -3772
- package/dist/devtools-LS4ZEPWM.js.map +0 -1
- package/dist/tui-LFJ5P4Y2.js +0 -40956
- package/dist/tui-LFJ5P4Y2.js.map +0 -1
- package/dist/tui-sync-worker.js +0 -17
- package/dist/tui-sync-worker.js.map +0 -1
- package/dist/yank-XDOONQSL.js +0 -13
- package/dist/yank-XDOONQSL.js.map +0 -1
- package/docs/ROADMAP.md +0 -123
- package/docs/VISION.md +0 -298
- package/docs/architecture/control-socket.md +0 -142
- package/docs/architecture/dag.md +0 -117
- package/docs/architecture/mux.md +0 -167
- package/docs/architecture/ops-log.md +0 -160
- package/docs/architecture/sdk.md +0 -187
- package/docs/architecture/sync.md +0 -159
- package/docs/architecture/tui.md +0 -165
- package/docs/guide/tui.md +0 -98
- package/docs/history/CHANGELOG-pre-3.md +0 -4866
- package/docs/img/tui-dashboard.png +0 -0
- package/docs/specs/2026-09-28-agent-state-from-murmur.md +0 -333
- package/docs/specs/2026-09-29-task-substates.md +0 -247
- package/docs/specs/2026-10-03-keep-driving-nudge.md +0 -163
- package/docs/specs/2026-10-21-fast-and-slow-gates.md +0 -229
- package/skills/mu/recipes/backlog-triage.md +0 -45
- package/skills/mu/recipes/codemode-driver.md +0 -118
- package/skills/mu/recipes/deep-research.md +0 -69
- package/skills/mu/recipes/drift-audit.md +0 -75
- package/skills/mu/recipes/fan-out.md +0 -42
- package/skills/mu/recipes/hypothesis-panel.md +0 -48
- package/skills/mu/recipes/long-run.md +0 -32
- package/skills/mu/recipes/loop-until-done.md +0 -45
- package/skills/mu/recipes/plan.md +0 -67
- package/skills/mu/recipes/refute.md +0 -55
- package/skills/mu/recipes/review-panel.md +0 -65
- package/skills/mu/recipes/rules-audit.md +0 -89
- package/skills/mu/recipes/tournament.md +0 -44
- package/skills/mu/recipes/ultrathink.md +0 -86
- package/skills/mu/recipes/watcher.md +0 -28
- package/skills/mu/recipes/waves.md +0 -21
- /package/dist/{chunk-3BARPJLY.js.map → errors-KHOYKD7D.js.map} +0 -0
|
@@ -0,0 +1,4482 @@
|
|
|
1
|
+
import { createRequire as __muCreateRequire } from "node:module"; const require = __muCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
DelegateCallConflictError,
|
|
4
|
+
DelegateNotFoundError,
|
|
5
|
+
DelegateStateError,
|
|
6
|
+
IN_FLIGHT_STATES,
|
|
7
|
+
TERMINAL_DELEGATE_STATES,
|
|
8
|
+
ambientFlush,
|
|
9
|
+
ambientIngest,
|
|
10
|
+
authorisedReady,
|
|
11
|
+
autoPermitted,
|
|
12
|
+
countUnpublishedOps,
|
|
13
|
+
currentCallers,
|
|
14
|
+
deliveredOf,
|
|
15
|
+
endAttempt,
|
|
16
|
+
finishDelegate,
|
|
17
|
+
foreignVersionMessage,
|
|
18
|
+
getAutoConfig,
|
|
19
|
+
getDelegateCall,
|
|
20
|
+
listDelegates,
|
|
21
|
+
listDelegatesOf,
|
|
22
|
+
liveAgentToOutput,
|
|
23
|
+
localMachineId,
|
|
24
|
+
markDelegate,
|
|
25
|
+
openHumanNeeds,
|
|
26
|
+
ownSegmentForeignLine,
|
|
27
|
+
pendingAttempts,
|
|
28
|
+
pinnedNotes,
|
|
29
|
+
planAttempt,
|
|
30
|
+
projectLog,
|
|
31
|
+
readAttempt,
|
|
32
|
+
recordHumanNeeded,
|
|
33
|
+
registerDelegateCall,
|
|
34
|
+
reserveAttempt,
|
|
35
|
+
reserveRetry,
|
|
36
|
+
tallyCalls,
|
|
37
|
+
verdictTally
|
|
38
|
+
} from "./chunk-GQNHIPJN.js";
|
|
39
|
+
import {
|
|
40
|
+
AgentLockBusyError,
|
|
41
|
+
AgentNotFoundError,
|
|
42
|
+
AgentReplacedError,
|
|
43
|
+
AgentRestartedError,
|
|
44
|
+
AgentSendRefusedError,
|
|
45
|
+
CTL_DEFAULT_TIMEOUT_MS,
|
|
46
|
+
CtlWriteVetoedError,
|
|
47
|
+
DEFAULT_SUBSTATE,
|
|
48
|
+
DELEGATE_TOKEN,
|
|
49
|
+
NoMultiplexerError,
|
|
50
|
+
RemoteRecoveryRefusedError,
|
|
51
|
+
SYNCED_ENTITIES,
|
|
52
|
+
SpawnAttemptBusyError,
|
|
53
|
+
SpawnAttemptGuardError,
|
|
54
|
+
TaskNotFoundError,
|
|
55
|
+
WorkstreamNotFoundError,
|
|
56
|
+
activeMux,
|
|
57
|
+
addNote,
|
|
58
|
+
agentCtlSocket,
|
|
59
|
+
agentKey,
|
|
60
|
+
agentOwed,
|
|
61
|
+
assertRecoveryProfile,
|
|
62
|
+
assertRecoveryUnchanged,
|
|
63
|
+
assertRecoveryUsable,
|
|
64
|
+
beginWait,
|
|
65
|
+
checkCaller,
|
|
66
|
+
checkMessage,
|
|
67
|
+
closeTask,
|
|
68
|
+
closedToken,
|
|
69
|
+
compareHlc,
|
|
70
|
+
compareStrings,
|
|
71
|
+
ctlProbe,
|
|
72
|
+
cursorKey,
|
|
73
|
+
delegateOutcome,
|
|
74
|
+
deleteAgent,
|
|
75
|
+
dropOwnedRefs,
|
|
76
|
+
emitEvent,
|
|
77
|
+
endWait,
|
|
78
|
+
expectsCtl,
|
|
79
|
+
fenceAgentRef,
|
|
80
|
+
getAgent,
|
|
81
|
+
getReceipt,
|
|
82
|
+
getTask,
|
|
83
|
+
getTaskNote,
|
|
84
|
+
getTaskOwner,
|
|
85
|
+
git,
|
|
86
|
+
hasDoneMark,
|
|
87
|
+
isConsumed,
|
|
88
|
+
isValidTaskId,
|
|
89
|
+
listBlocked,
|
|
90
|
+
listInProgress,
|
|
91
|
+
listReady,
|
|
92
|
+
listTasks,
|
|
93
|
+
listTasksByOwner,
|
|
94
|
+
localPaneAlive,
|
|
95
|
+
locksDir,
|
|
96
|
+
logStallOnce,
|
|
97
|
+
newMsgId,
|
|
98
|
+
observeAgentStates,
|
|
99
|
+
openDb,
|
|
100
|
+
parseHlc,
|
|
101
|
+
pinnedByOthers,
|
|
102
|
+
readCursors,
|
|
103
|
+
readLiveAgents,
|
|
104
|
+
readWaitRegistration,
|
|
105
|
+
recordCursors,
|
|
106
|
+
recordSend,
|
|
107
|
+
recoveryGone,
|
|
108
|
+
recoveryHolds,
|
|
109
|
+
registerOwnedWait,
|
|
110
|
+
releaseTask,
|
|
111
|
+
resolveCliCommand,
|
|
112
|
+
runLeasedTemplate,
|
|
113
|
+
savedIdentity,
|
|
114
|
+
say,
|
|
115
|
+
selectRemoteRecovery,
|
|
116
|
+
sendViaTransport,
|
|
117
|
+
setAgentDone,
|
|
118
|
+
setRecoveryCollectedHead,
|
|
119
|
+
settledCloseInput,
|
|
120
|
+
spawnAutoAttempt,
|
|
121
|
+
spawnForTask,
|
|
122
|
+
speaksMuCtl,
|
|
123
|
+
taskIdFor,
|
|
124
|
+
taskRevision,
|
|
125
|
+
tryResolveLiveWorkstreamId,
|
|
126
|
+
watchAgentRun,
|
|
127
|
+
withAgentLock,
|
|
128
|
+
withCaptureSuppressed,
|
|
129
|
+
withFileLock,
|
|
130
|
+
withHostChannel,
|
|
131
|
+
withOpContext
|
|
132
|
+
} from "./chunk-S424D5EP.js";
|
|
133
|
+
|
|
134
|
+
// src/rebuild.ts
|
|
135
|
+
import { existsSync, mkdirSync, rmSync, statSync } from "fs";
|
|
136
|
+
import { dirname } from "path";
|
|
137
|
+
var RebuildTargetExistsError = class extends Error {
|
|
138
|
+
constructor(path) {
|
|
139
|
+
super(say("rebuild.target_exists", path));
|
|
140
|
+
this.path = path;
|
|
141
|
+
this.name = "RebuildTargetExistsError";
|
|
142
|
+
}
|
|
143
|
+
path;
|
|
144
|
+
};
|
|
145
|
+
var RebuildTargetIsSourceError = class extends Error {
|
|
146
|
+
constructor(path) {
|
|
147
|
+
super(say("rebuild.target_is_source", path));
|
|
148
|
+
this.path = path;
|
|
149
|
+
this.name = "RebuildTargetIsSourceError";
|
|
150
|
+
}
|
|
151
|
+
path;
|
|
152
|
+
};
|
|
153
|
+
var UNREBUILDABLE_TABLES = [
|
|
154
|
+
"agents",
|
|
155
|
+
"task_auto",
|
|
156
|
+
"worktrees",
|
|
157
|
+
"delegate_calls",
|
|
158
|
+
"wait_cursors",
|
|
159
|
+
"wait_registrations",
|
|
160
|
+
// The condition sets behind `mu wait --resume <id>`: a handle is worthless on a rebuilt DB.
|
|
161
|
+
"wait_resumes",
|
|
162
|
+
"agent_settles",
|
|
163
|
+
"host_profiles",
|
|
164
|
+
"host_channels",
|
|
165
|
+
"spawn_receipts",
|
|
166
|
+
// The stall actions a wait took (steer exclusion, release record): no op carries them.
|
|
167
|
+
"wait_action_receipts",
|
|
168
|
+
// A remote worker's worktree, kept when its row was deleted: no op carries it.
|
|
169
|
+
"remote_recovery",
|
|
170
|
+
// The human task and its reason rebuild from ops; WHO recorded it does
|
|
171
|
+
// not. Ownership is never inferred from a note author.
|
|
172
|
+
"human_needs"
|
|
173
|
+
];
|
|
174
|
+
var LOG_ONLY_ENTITIES = /* @__PURE__ */ new Set(["message", "event", "broadcast"]);
|
|
175
|
+
function isProjectable(entity) {
|
|
176
|
+
if (LOG_ONLY_ENTITIES.has(entity)) return false;
|
|
177
|
+
return SYNCED_ENTITIES.includes(entity);
|
|
178
|
+
}
|
|
179
|
+
function rebuildInto(source, opts) {
|
|
180
|
+
const { targetPath } = opts;
|
|
181
|
+
const sourceFile = source.pragma("database_list").find(
|
|
182
|
+
(row) => row.name === "main"
|
|
183
|
+
)?.file;
|
|
184
|
+
if (sourceFile !== void 0 && sourceFile.length > 0) {
|
|
185
|
+
const sameFile = sourceFile === targetPath || existsSync(targetPath) && existsSync(sourceFile) && sameInode(sourceFile, targetPath);
|
|
186
|
+
if (sameFile) throw new RebuildTargetIsSourceError(targetPath);
|
|
187
|
+
}
|
|
188
|
+
if (existsSync(targetPath) && opts.force !== true) {
|
|
189
|
+
throw new RebuildTargetExistsError(targetPath);
|
|
190
|
+
}
|
|
191
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
192
|
+
const snapshot2 = source.transaction(() => {
|
|
193
|
+
const identity2 = source.prepare(
|
|
194
|
+
"SELECT machine_id, hostname, created_at, last_wall, last_counter FROM machine_identity WHERE id = 1"
|
|
195
|
+
).get();
|
|
196
|
+
if (!identity2) throw new Error(say("rebuild.no_identity"));
|
|
197
|
+
const opRows2 = source.prepare(
|
|
198
|
+
`SELECT hlc, machine_id, group_id, actor, intent, entity, key, op, payload, created_at
|
|
199
|
+
FROM ops`
|
|
200
|
+
).all();
|
|
201
|
+
opRows2.sort((a, b) => compareHlc(a.hlc, b.hlc));
|
|
202
|
+
const peerRows2 = source.prepare("SELECT machine_id, last_applied_seq, last_seen_at FROM sync_peers").all();
|
|
203
|
+
const machineLocalLost2 = [];
|
|
204
|
+
for (const table of UNREBUILDABLE_TABLES) {
|
|
205
|
+
const row = source.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
|
|
206
|
+
if (row.n > 0) machineLocalLost2.push({ table, rows: row.n });
|
|
207
|
+
}
|
|
208
|
+
return { identity: identity2, opRows: opRows2, peerRows: peerRows2, machineLocalLost: machineLocalLost2 };
|
|
209
|
+
});
|
|
210
|
+
const { identity, opRows, peerRows, machineLocalLost } = snapshot2();
|
|
211
|
+
if (opts.force === true) {
|
|
212
|
+
for (const suffix of ["", "-wal", "-shm"]) rmSync(`${targetPath}${suffix}`, { force: true });
|
|
213
|
+
}
|
|
214
|
+
const target = openDb({ path: targetPath });
|
|
215
|
+
try {
|
|
216
|
+
target.prepare(
|
|
217
|
+
`UPDATE machine_identity
|
|
218
|
+
SET machine_id = @machineId, hostname = @hostname, created_at = @createdAt,
|
|
219
|
+
last_wall = @lastWall, last_counter = @lastCounter
|
|
220
|
+
WHERE id = 1`
|
|
221
|
+
).run({
|
|
222
|
+
machineId: identity.machine_id,
|
|
223
|
+
hostname: identity.hostname,
|
|
224
|
+
createdAt: identity.created_at,
|
|
225
|
+
lastWall: identity.last_wall,
|
|
226
|
+
lastCounter: identity.last_counter
|
|
227
|
+
});
|
|
228
|
+
const insertOp = target.prepare(
|
|
229
|
+
`INSERT OR IGNORE INTO ops
|
|
230
|
+
(hlc, machine_id, group_id, actor, intent, entity, key, op, payload, created_at)
|
|
231
|
+
VALUES (@hlc, @machineId, @groupId, @actor, @intent, @entity, @key, @op, @payload, @createdAt)`
|
|
232
|
+
);
|
|
233
|
+
const insertPeer = target.prepare(
|
|
234
|
+
`INSERT OR REPLACE INTO sync_peers (machine_id, last_applied_seq, last_seen_at)
|
|
235
|
+
VALUES (@machineId, @lastAppliedSeq, @lastSeenAt)`
|
|
236
|
+
);
|
|
237
|
+
const logOnlyByEntity = {};
|
|
238
|
+
let opsProjected = 0;
|
|
239
|
+
let opsChangedRows = 0;
|
|
240
|
+
const replay = target.transaction(() => {
|
|
241
|
+
withCaptureSuppressed(target, () => {
|
|
242
|
+
for (const row of opRows) {
|
|
243
|
+
insertOp.run({
|
|
244
|
+
hlc: row.hlc,
|
|
245
|
+
machineId: row.machine_id,
|
|
246
|
+
groupId: row.group_id,
|
|
247
|
+
actor: row.actor,
|
|
248
|
+
intent: row.intent,
|
|
249
|
+
entity: row.entity,
|
|
250
|
+
key: row.key,
|
|
251
|
+
op: row.op,
|
|
252
|
+
payload: row.payload,
|
|
253
|
+
createdAt: row.created_at
|
|
254
|
+
});
|
|
255
|
+
if (isProjectable(row.entity)) opsProjected += 1;
|
|
256
|
+
else logOnlyByEntity[row.entity] = (logOnlyByEntity[row.entity] ?? 0) + 1;
|
|
257
|
+
}
|
|
258
|
+
advanceClockPast(target, opRows);
|
|
259
|
+
opsChangedRows = projectLog(target);
|
|
260
|
+
for (const peer of peerRows) {
|
|
261
|
+
insertPeer.run({
|
|
262
|
+
machineId: peer.machine_id,
|
|
263
|
+
lastAppliedSeq: peer.last_applied_seq,
|
|
264
|
+
lastSeenAt: peer.last_seen_at
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
replay.immediate();
|
|
270
|
+
const rebuiltRows = {};
|
|
271
|
+
for (const table of ["workstreams", "tasks", "task_edges", "task_notes", "ops"]) {
|
|
272
|
+
const row = target.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
|
|
273
|
+
rebuiltRows[table] = row.n;
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
targetPath,
|
|
277
|
+
opsCopied: rebuiltRows.ops ?? 0,
|
|
278
|
+
opsProjected,
|
|
279
|
+
opsChangedRows,
|
|
280
|
+
logOnlyByEntity,
|
|
281
|
+
rebuiltRows,
|
|
282
|
+
machineLocalLost,
|
|
283
|
+
machineId: identity.machine_id
|
|
284
|
+
};
|
|
285
|
+
} finally {
|
|
286
|
+
target.close();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function advanceClockPast(target, rows) {
|
|
290
|
+
let newest;
|
|
291
|
+
for (const row of rows) {
|
|
292
|
+
let h;
|
|
293
|
+
try {
|
|
294
|
+
h = parseHlc(row.hlc);
|
|
295
|
+
} catch {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (!newest || h.wallMs > newest.wallMs || h.wallMs === newest.wallMs && h.counter > newest.counter) {
|
|
299
|
+
newest = h;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (!newest) return;
|
|
303
|
+
target.prepare(
|
|
304
|
+
`UPDATE machine_identity SET last_wall = @wall, last_counter = @counter
|
|
305
|
+
WHERE id = 1 AND (last_wall < @wall OR (last_wall = @wall AND last_counter < @counter))`
|
|
306
|
+
).run({ wall: newest.wallMs, counter: newest.counter });
|
|
307
|
+
}
|
|
308
|
+
function sameInode(a, b) {
|
|
309
|
+
try {
|
|
310
|
+
const sa = statSync(a);
|
|
311
|
+
const sb = statSync(b);
|
|
312
|
+
return sa.ino === sb.ino && sa.dev === sb.dev;
|
|
313
|
+
} catch {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/drift.ts
|
|
319
|
+
import { mkdtempSync, rmSync as rmSync2 } from "fs";
|
|
320
|
+
import { tmpdir } from "os";
|
|
321
|
+
import { join } from "path";
|
|
322
|
+
var DRIFT_REPORT_CAP = 20;
|
|
323
|
+
function checkCheapDriftInvariant(db) {
|
|
324
|
+
const started = Date.now();
|
|
325
|
+
const unexplained = [];
|
|
326
|
+
const probes = [
|
|
327
|
+
{
|
|
328
|
+
table: "workstreams",
|
|
329
|
+
sql: `SELECT w.name AS key FROM workstreams w
|
|
330
|
+
WHERE NOT EXISTS (
|
|
331
|
+
SELECT 1 FROM ops o WHERE o.entity = 'workstream' AND o.key = w.name)`
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
table: "tasks",
|
|
335
|
+
sql: `SELECT w.name || '/' || t.local_id AS key
|
|
336
|
+
FROM tasks t JOIN workstreams w ON w.id = t.workstream_id
|
|
337
|
+
WHERE NOT EXISTS (
|
|
338
|
+
SELECT 1 FROM ops o
|
|
339
|
+
WHERE o.entity = 'task' AND o.key = w.name || '/' || t.local_id)`
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
table: "task_edges",
|
|
343
|
+
sql: `SELECT wf.name || '/' || f.local_id || '->' || wt.name || '/' || t.local_id AS key
|
|
344
|
+
FROM task_edges e
|
|
345
|
+
JOIN tasks f ON f.id = e.from_task_id
|
|
346
|
+
JOIN tasks t ON t.id = e.to_task_id
|
|
347
|
+
JOIN workstreams wf ON wf.id = f.workstream_id
|
|
348
|
+
JOIN workstreams wt ON wt.id = t.workstream_id
|
|
349
|
+
WHERE NOT EXISTS (
|
|
350
|
+
SELECT 1 FROM ops o
|
|
351
|
+
WHERE o.entity = 'edge'
|
|
352
|
+
AND o.key = wf.name || '/' || f.local_id || '->' || wt.name || '/' || t.local_id)`
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
// Notes are keyed <task-key>#<origin-id> and the origin id is the
|
|
356
|
+
// AUTHORING machine's surrogate id, which need not match ours (see
|
|
357
|
+
// src/apply.ts § syncNotes). So match on the task-key PREFIX
|
|
358
|
+
// rather than the exact key: the question here is "does the log
|
|
359
|
+
// know about a note on this task at all", not "which note".
|
|
360
|
+
//
|
|
361
|
+
// Express the prefix as an indexed half-open range. A correlated
|
|
362
|
+
// `LIKE task_key || '#%'` made SQLite scan every note op for every
|
|
363
|
+
// live note (seconds on the dogfood DB); '#'..'$' contains exactly
|
|
364
|
+
// the strings beginning with `task_key || '#'` because task ids
|
|
365
|
+
// cannot contain either delimiter.
|
|
366
|
+
//
|
|
367
|
+
// The answer is per TASK, so probe each task that has notes once
|
|
368
|
+
// (2.3k probes, not 10.7k on the dogfood DB), then list the notes
|
|
369
|
+
// of the tasks that fail. MATERIALIZED stops SQLite flattening the
|
|
370
|
+
// probe back into a per-note subquery.
|
|
371
|
+
table: "task_notes",
|
|
372
|
+
sql: `WITH bare AS MATERIALIZED (
|
|
373
|
+
SELECT t.id AS task_id, w.name || '/' || t.local_id AS task_key
|
|
374
|
+
FROM tasks t
|
|
375
|
+
JOIN workstreams w ON w.id = t.workstream_id
|
|
376
|
+
WHERE t.id IN (SELECT task_id FROM task_notes)
|
|
377
|
+
AND NOT EXISTS (
|
|
378
|
+
SELECT 1 FROM ops o
|
|
379
|
+
WHERE o.entity = 'note'
|
|
380
|
+
AND o.key >= w.name || '/' || t.local_id || '#'
|
|
381
|
+
AND o.key < w.name || '/' || t.local_id || '$'))
|
|
382
|
+
SELECT b.task_key || '#' || n.id AS key
|
|
383
|
+
FROM bare b
|
|
384
|
+
JOIN task_notes n ON n.task_id = b.task_id
|
|
385
|
+
ORDER BY n.task_id, n.id`
|
|
386
|
+
}
|
|
387
|
+
];
|
|
388
|
+
for (const probe of probes) {
|
|
389
|
+
const rows = db.prepare(probe.sql).all();
|
|
390
|
+
for (const row of rows) unexplained.push({ table: probe.table, key: row.key });
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
clean: unexplained.length === 0,
|
|
394
|
+
unexplainedRows: unexplained.slice(0, DRIFT_REPORT_CAP),
|
|
395
|
+
totalUnexplained: unexplained.length,
|
|
396
|
+
elapsedMs: Date.now() - started
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
var COMPARED = {
|
|
400
|
+
workstreams: ["created_at", "torn_down_at"],
|
|
401
|
+
tasks: [
|
|
402
|
+
"title",
|
|
403
|
+
"status",
|
|
404
|
+
"substate",
|
|
405
|
+
"impact",
|
|
406
|
+
"effort_days",
|
|
407
|
+
"scored",
|
|
408
|
+
"dispatch_host",
|
|
409
|
+
"kind",
|
|
410
|
+
"wave",
|
|
411
|
+
"created_at",
|
|
412
|
+
"updated_at"
|
|
413
|
+
],
|
|
414
|
+
task_notes: ["author", "content", "created_at", "kind"],
|
|
415
|
+
task_edges: ["created_at"]
|
|
416
|
+
};
|
|
417
|
+
var SNAPSHOT_SQL = {
|
|
418
|
+
workstreams: `SELECT w.name AS key, w.created_at, w.torn_down_at
|
|
419
|
+
FROM workstreams w`,
|
|
420
|
+
tasks: `SELECT w.name || '/' || t.local_id AS key,
|
|
421
|
+
t.title, t.status, t.substate, t.impact, t.effort_days, t.scored,
|
|
422
|
+
t.dispatch_host, t.kind, t.wave, t.created_at, t.updated_at
|
|
423
|
+
FROM tasks t JOIN workstreams w ON w.id = t.workstream_id`,
|
|
424
|
+
// Notes are a GROW-ONLY SET whose local surrogate id is not portable,
|
|
425
|
+
// so identity for diffing is (task, author, content, created_at) —
|
|
426
|
+
// exactly the identity syncNotes (src/apply.ts) folds notes by. Using the
|
|
427
|
+
// '#<id>' key here would report drift on every note that replayed to a
|
|
428
|
+
// different rowid, which is normal and not drift.
|
|
429
|
+
//
|
|
430
|
+
// char(31) (ASCII Unit Separator) joins the parts: it cannot occur in a
|
|
431
|
+
// workstream/task id and is vanishingly unlikely in note prose, so two
|
|
432
|
+
// distinct notes cannot collide into one composite key. A literal NUL
|
|
433
|
+
// would be the textbook choice but SQLite truncates TEXT at NUL.
|
|
434
|
+
task_notes: `SELECT w.name || '/' || t.local_id || char(31) ||
|
|
435
|
+
COALESCE(n.author, '') || char(31) || n.content || char(31) ||
|
|
436
|
+
n.created_at AS key,
|
|
437
|
+
n.author, n.content, n.created_at, n.kind
|
|
438
|
+
FROM task_notes n
|
|
439
|
+
JOIN tasks t ON t.id = n.task_id
|
|
440
|
+
JOIN workstreams w ON w.id = t.workstream_id`,
|
|
441
|
+
task_edges: `SELECT wf.name || '/' || f.local_id || '->' || wt.name || '/' || t.local_id AS key,
|
|
442
|
+
e.created_at
|
|
443
|
+
FROM task_edges e
|
|
444
|
+
JOIN tasks f ON f.id = e.from_task_id
|
|
445
|
+
JOIN tasks t ON t.id = e.to_task_id
|
|
446
|
+
JOIN workstreams wf ON wf.id = f.workstream_id
|
|
447
|
+
JOIN workstreams wt ON wt.id = t.workstream_id`
|
|
448
|
+
};
|
|
449
|
+
function snapshot(db, table) {
|
|
450
|
+
const sql = SNAPSHOT_SQL[table];
|
|
451
|
+
if (sql === void 0) throw new Error(`no drift snapshot defined for table ${table}`);
|
|
452
|
+
const out = /* @__PURE__ */ new Map();
|
|
453
|
+
for (const row of db.prepare(sql).all()) {
|
|
454
|
+
const key = row.key;
|
|
455
|
+
if (typeof key !== "string") continue;
|
|
456
|
+
out.set(key, row);
|
|
457
|
+
}
|
|
458
|
+
return out;
|
|
459
|
+
}
|
|
460
|
+
function show(v) {
|
|
461
|
+
if (v === null || v === void 0) return null;
|
|
462
|
+
return String(v);
|
|
463
|
+
}
|
|
464
|
+
function checkDrift(db) {
|
|
465
|
+
const started = Date.now();
|
|
466
|
+
const dir = mkdtempSync(join(tmpdir(), "mu-drift-"));
|
|
467
|
+
const target = join(dir, "rebuilt.db");
|
|
468
|
+
try {
|
|
469
|
+
rebuildInto(db, { targetPath: target });
|
|
470
|
+
const rebuilt = openDb({ path: target });
|
|
471
|
+
try {
|
|
472
|
+
const records = [];
|
|
473
|
+
const rowsCompared = {};
|
|
474
|
+
let total = 0;
|
|
475
|
+
const push = (record) => {
|
|
476
|
+
total += 1;
|
|
477
|
+
if (records.length < DRIFT_REPORT_CAP) records.push(record);
|
|
478
|
+
};
|
|
479
|
+
for (const table of Object.keys(SNAPSHOT_SQL)) {
|
|
480
|
+
const live = snapshot(db, table);
|
|
481
|
+
const expected = snapshot(rebuilt, table);
|
|
482
|
+
rowsCompared[table] = live.size;
|
|
483
|
+
for (const [key, liveRow] of live) {
|
|
484
|
+
const expectedRow = expected.get(key);
|
|
485
|
+
if (expectedRow === void 0) {
|
|
486
|
+
push({
|
|
487
|
+
table,
|
|
488
|
+
key,
|
|
489
|
+
field: "<row>",
|
|
490
|
+
live: "present",
|
|
491
|
+
expected: null,
|
|
492
|
+
presence: "missing-in-rebuild"
|
|
493
|
+
});
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
for (const field of COMPARED[table] ?? []) {
|
|
497
|
+
const a = liveRow[field];
|
|
498
|
+
const b = expectedRow[field];
|
|
499
|
+
if (show(a) !== show(b)) {
|
|
500
|
+
push({ table, key, field, live: show(a), expected: show(b) });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
for (const key of expected.keys()) {
|
|
505
|
+
if (live.has(key)) continue;
|
|
506
|
+
push({
|
|
507
|
+
table,
|
|
508
|
+
key,
|
|
509
|
+
field: "<row>",
|
|
510
|
+
live: null,
|
|
511
|
+
expected: "present",
|
|
512
|
+
presence: "missing-in-live"
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
clean: total === 0,
|
|
518
|
+
records,
|
|
519
|
+
totalDrift: total,
|
|
520
|
+
rowsCompared,
|
|
521
|
+
elapsedMs: Date.now() - started
|
|
522
|
+
};
|
|
523
|
+
} finally {
|
|
524
|
+
rebuilt.close();
|
|
525
|
+
}
|
|
526
|
+
} finally {
|
|
527
|
+
rmSync2(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 25 });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function formatDriftRecord(record) {
|
|
531
|
+
if (record.field === "<row>") {
|
|
532
|
+
return record.presence === "missing-in-live" ? `${record.table} ${record.key}: row MISSING from live tables (the log says it exists)` : `${record.table} ${record.key}: row present in live tables but the log cannot explain it`;
|
|
533
|
+
}
|
|
534
|
+
const live = record.live === null ? "NULL" : record.live;
|
|
535
|
+
const expected = record.expected === null ? "NULL" : record.expected;
|
|
536
|
+
return `${record.table} ${record.key}.${record.field}: live=${live} log=${expected}`;
|
|
537
|
+
}
|
|
538
|
+
function driftRemediation() {
|
|
539
|
+
return [
|
|
540
|
+
"Drift means the ops log and the live tables disagree. One of them is wrong.",
|
|
541
|
+
"",
|
|
542
|
+
" * If CAPTURE missed a mutation, the LIVE tables hold work the log never",
|
|
543
|
+
" recorded. Rebuilding would DISCARD it. Do not rebuild reflexively.",
|
|
544
|
+
" * If APPLY is lossy, the log is authoritative and a rebuild is the fix.",
|
|
545
|
+
"",
|
|
546
|
+
"Do this, in order:",
|
|
547
|
+
" 1. Back up first, before anything else: mu db backup /tmp/mu-drift-evidence.db",
|
|
548
|
+
" 2. Materialize what the log believes: mu rebuild /tmp/mu-rebuilt.db",
|
|
549
|
+
" 3. Compare the two by hand for the keys named above, and decide which",
|
|
550
|
+
" side is correct. `mu db query` against both files is the fastest way.",
|
|
551
|
+
" 4. Report it: drift is a capture/apply BUG, not operator error. The",
|
|
552
|
+
" named table/key/field above is the reproduction."
|
|
553
|
+
];
|
|
554
|
+
}
|
|
555
|
+
var DriftDetectedError = class extends Error {
|
|
556
|
+
constructor(totalDrift, records) {
|
|
557
|
+
super(
|
|
558
|
+
`ops-log drift: ${totalDrift} divergence${totalDrift === 1 ? "" : "s"} between the live tables and the ops log`
|
|
559
|
+
);
|
|
560
|
+
this.totalDrift = totalDrift;
|
|
561
|
+
this.records = records;
|
|
562
|
+
this.name = "DriftDetectedError";
|
|
563
|
+
}
|
|
564
|
+
totalDrift;
|
|
565
|
+
records;
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// src/fleet-hazards.ts
|
|
569
|
+
import { existsSync as existsSync2, lstatSync, readlinkSync, realpathSync, statfsSync } from "fs";
|
|
570
|
+
import { basename, dirname as dirname2, join as join2, resolve, sep } from "path";
|
|
571
|
+
var HazardFailError = class extends Error {
|
|
572
|
+
constructor(failedRows) {
|
|
573
|
+
super(
|
|
574
|
+
`doctor: ${failedRows.length} row${failedRows.length === 1 ? "" : "s"} failed (${failedRows.join(", ")}); see the remediation above`
|
|
575
|
+
);
|
|
576
|
+
this.failedRows = failedRows;
|
|
577
|
+
}
|
|
578
|
+
failedRows;
|
|
579
|
+
name = "HazardFailError";
|
|
580
|
+
};
|
|
581
|
+
var DbInsideSyncDirError = class extends Error {
|
|
582
|
+
constructor(dbPath, syncDir) {
|
|
583
|
+
super(
|
|
584
|
+
`DB ${dbPath} is inside MU_SYNC_DIR ${syncDir}: move it out (WAL files do not sync safely)`
|
|
585
|
+
);
|
|
586
|
+
this.dbPath = dbPath;
|
|
587
|
+
this.syncDir = syncDir;
|
|
588
|
+
}
|
|
589
|
+
dbPath;
|
|
590
|
+
syncDir;
|
|
591
|
+
name = "DbInsideSyncDirError";
|
|
592
|
+
};
|
|
593
|
+
function canonicalPath(path, hops = 0) {
|
|
594
|
+
const abs = resolve(path);
|
|
595
|
+
const tail = [];
|
|
596
|
+
let cur = abs;
|
|
597
|
+
for (; ; ) {
|
|
598
|
+
try {
|
|
599
|
+
return join2(realpathSync(cur), ...tail);
|
|
600
|
+
} catch {
|
|
601
|
+
try {
|
|
602
|
+
if (hops < 32 && lstatSync(cur).isSymbolicLink()) {
|
|
603
|
+
return join2(canonicalPath(resolve(dirname2(cur), readlinkSync(cur)), hops + 1), ...tail);
|
|
604
|
+
}
|
|
605
|
+
} catch {
|
|
606
|
+
}
|
|
607
|
+
const parent = dirname2(cur);
|
|
608
|
+
if (parent === cur) return abs;
|
|
609
|
+
tail.unshift(basename(cur));
|
|
610
|
+
cur = parent;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function isPathInside(child, parent) {
|
|
615
|
+
const c = canonicalPath(child);
|
|
616
|
+
const p = canonicalPath(parent);
|
|
617
|
+
if (c === p) return true;
|
|
618
|
+
return c.startsWith(p.endsWith(sep) ? p : p + sep);
|
|
619
|
+
}
|
|
620
|
+
function checkDbInsideSyncDir(dbPath, syncDir) {
|
|
621
|
+
if (syncDir === void 0 || syncDir.trim() === "") {
|
|
622
|
+
return {
|
|
623
|
+
name: "db-vs-sync",
|
|
624
|
+
severity: "ok",
|
|
625
|
+
detail: "MU_SYNC_DIR not set (no sync configured)"
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
if (!isPathInside(dbPath, syncDir)) {
|
|
629
|
+
return { name: "db-vs-sync", severity: "ok", detail: "DB is outside the sync dir" };
|
|
630
|
+
}
|
|
631
|
+
return {
|
|
632
|
+
name: "db-vs-sync",
|
|
633
|
+
severity: "fail",
|
|
634
|
+
detail: `DB is INSIDE MU_SYNC_DIR \u2014 this WILL corrupt it (${dbPath})`,
|
|
635
|
+
remediation: [
|
|
636
|
+
"The mu database must never live inside the synced folder.",
|
|
637
|
+
"",
|
|
638
|
+
"A live WAL-mode SQLite DB is three files (mu.db, -wal, -shm) whose mutual",
|
|
639
|
+
"consistency is the basis of durability. File-sync tools copy whole files,",
|
|
640
|
+
"out of order, and across machines: a stale -wal resurrected from a peer, or",
|
|
641
|
+
"a main file copied mid-checkpoint, yields a DB that OPENS FINE and is",
|
|
642
|
+
"silently corrupt. Two machines writing the same synced file is worse \u2014 last",
|
|
643
|
+
"writer wins on the whole FILE, so one machine's entire history disappears.",
|
|
644
|
+
"",
|
|
645
|
+
"mu syncs append-only per-machine SEGMENTS precisely to avoid this: one",
|
|
646
|
+
"writer per file, never contended, so any file-mover is adequate transport.",
|
|
647
|
+
"",
|
|
648
|
+
"Fix: move the DB out of the sync dir, e.g.",
|
|
649
|
+
" export MU_DB_PATH=$HOME/.local/state/mu-next/mu.db",
|
|
650
|
+
"and keep MU_SYNC_DIR pointing at the shared folder for segments only."
|
|
651
|
+
]
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
var NETWORK_FS_MAGIC = /* @__PURE__ */ new Map([
|
|
655
|
+
// Every value here is transcribed from linux/magic.h, not recalled.
|
|
656
|
+
// An invented magic is worse than a missing one: it would either never
|
|
657
|
+
// match (silent no-op) or match the wrong filesystem and warn falsely.
|
|
658
|
+
[26985, "NFS"],
|
|
659
|
+
// NFS_SUPER_MAGIC
|
|
660
|
+
[4283649346, "CIFS/SMB"],
|
|
661
|
+
// CIFS_SUPER_MAGIC
|
|
662
|
+
[4266872130, "SMB2"],
|
|
663
|
+
// SMB2_SUPER_MAGIC
|
|
664
|
+
[20859, "SMBFS"],
|
|
665
|
+
// SMB_SUPER_MAGIC
|
|
666
|
+
[1702057286, "FUSE (may be sshfs/rclone/cloud drive)"],
|
|
667
|
+
// FUSE_SUPER_MAGIC
|
|
668
|
+
[12805120, "Ceph"]
|
|
669
|
+
// CEPH_SUPER_MAGIC
|
|
670
|
+
]);
|
|
671
|
+
function classifyFsType(magic, platform = process.platform) {
|
|
672
|
+
if (platform !== "linux") {
|
|
673
|
+
return { kind: "unknown", label: `unrecognised (${platform})` };
|
|
674
|
+
}
|
|
675
|
+
const network = NETWORK_FS_MAGIC.get(magic);
|
|
676
|
+
if (network !== void 0) return { kind: "network", label: network };
|
|
677
|
+
return { kind: "local", label: `local (0x${magic.toString(16)})` };
|
|
678
|
+
}
|
|
679
|
+
function probeFilesystem(path) {
|
|
680
|
+
try {
|
|
681
|
+
let probeTarget = resolve(path);
|
|
682
|
+
while (!existsSync2(probeTarget)) {
|
|
683
|
+
const parent = resolve(probeTarget, "..");
|
|
684
|
+
if (parent === probeTarget) break;
|
|
685
|
+
probeTarget = parent;
|
|
686
|
+
}
|
|
687
|
+
const stats = statfsSync(probeTarget);
|
|
688
|
+
return classifyFsType(Number(stats.type));
|
|
689
|
+
} catch {
|
|
690
|
+
return { kind: "unknown", label: "probe failed" };
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function checkNetworkMount(dbPath, probe = probeFilesystem(dbPath)) {
|
|
694
|
+
if (probe.kind === "network") {
|
|
695
|
+
return {
|
|
696
|
+
name: "db-filesystem",
|
|
697
|
+
severity: "warn",
|
|
698
|
+
detail: `DB is on a network mount (${probe.label}) \u2014 WAL locking is unreliable there`,
|
|
699
|
+
remediation: [
|
|
700
|
+
`The DB appears to live on ${probe.label}.`,
|
|
701
|
+
"",
|
|
702
|
+
"SQLite's WAL mode needs working POSIX advisory locks AND a shared-memory",
|
|
703
|
+
"file. NFS, SMB/CIFS and FUSE mounts (sshfs, rclone, cloud drives) provide",
|
|
704
|
+
"neither dependably. Expect 'database is locked' with no contention, or",
|
|
705
|
+
"silent corruption with real contention from parallel mu invocations.",
|
|
706
|
+
"",
|
|
707
|
+
"Fix: keep the DB on local disk and sync SEGMENTS instead \u2014",
|
|
708
|
+
" export MU_DB_PATH=$HOME/.local/state/mu-next/mu.db",
|
|
709
|
+
"That is what the segment design is for: append-only, single-writer files",
|
|
710
|
+
"that any file-mover can carry safely."
|
|
711
|
+
]
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
if (probe.kind === "unknown") {
|
|
715
|
+
return {
|
|
716
|
+
name: "db-filesystem",
|
|
717
|
+
severity: "ok",
|
|
718
|
+
detail: `filesystem not classifiable on this platform (${probe.label}); assuming local`
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
return { name: "db-filesystem", severity: "ok", detail: `DB is on ${probe.label}` };
|
|
722
|
+
}
|
|
723
|
+
function findCaseCollisions(db) {
|
|
724
|
+
const rows = db.prepare("SELECT name FROM workstreams ORDER BY name").all();
|
|
725
|
+
const byFolded = /* @__PURE__ */ new Map();
|
|
726
|
+
for (const row of rows) {
|
|
727
|
+
const folded = row.name.toLowerCase();
|
|
728
|
+
const bucket = byFolded.get(folded);
|
|
729
|
+
if (bucket === void 0) byFolded.set(folded, [row.name]);
|
|
730
|
+
else bucket.push(row.name);
|
|
731
|
+
}
|
|
732
|
+
const collisions = [];
|
|
733
|
+
for (const [folded, names] of byFolded) {
|
|
734
|
+
if (names.length > 1) collisions.push({ folded, names });
|
|
735
|
+
}
|
|
736
|
+
return collisions;
|
|
737
|
+
}
|
|
738
|
+
function checkCaseCollisions(db) {
|
|
739
|
+
const collisions = findCaseCollisions(db);
|
|
740
|
+
if (collisions.length === 0) {
|
|
741
|
+
return {
|
|
742
|
+
name: "name-case",
|
|
743
|
+
severity: "ok",
|
|
744
|
+
detail: "no case-colliding workstream names"
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
const shown = collisions.slice(0, 5).map((c) => c.names.join(" / ")).join(", ");
|
|
748
|
+
return {
|
|
749
|
+
name: "name-case",
|
|
750
|
+
severity: "warn",
|
|
751
|
+
detail: `${collisions.length} case-colliding workstream name(s): ${shown}`,
|
|
752
|
+
remediation: [
|
|
753
|
+
"These workstream names differ only by letter case:",
|
|
754
|
+
...collisions.map((c) => ` ${c.names.join(" / ")}`),
|
|
755
|
+
"",
|
|
756
|
+
"They coexist on Linux (ext4 is case-sensitive) but COLLIDE on macOS (APFS",
|
|
757
|
+
"is case-insensitive) and on Windows (NTFS). Because a workstream name IS a",
|
|
758
|
+
"tmux session name and seeds every worktree path, a Mac in the fleet sees",
|
|
759
|
+
"one session and one directory where Linux sees two \u2014 so the fleet reaches",
|
|
760
|
+
"different states depending on which machine applies an op first.",
|
|
761
|
+
"",
|
|
762
|
+
"Fix: rename one side to a distinct name before adding a Mac to the fleet.",
|
|
763
|
+
"There is no in-place rename verb; back the DB up, re-init under the new",
|
|
764
|
+
"name, and tear down the old one:",
|
|
765
|
+
" mu db backup <file>",
|
|
766
|
+
" mu workstream init <new-name>",
|
|
767
|
+
" mu workstream teardown <old-name> # preview; then apply with --if <rev>"
|
|
768
|
+
]
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
function checkFleetHazards(db, opts = { dbPath: "" }) {
|
|
772
|
+
const syncDir = opts.syncDir !== void 0 ? opts.syncDir : process.env.MU_SYNC_DIR;
|
|
773
|
+
return [
|
|
774
|
+
checkDbInsideSyncDir(opts.dbPath, syncDir),
|
|
775
|
+
checkNetworkMount(opts.dbPath),
|
|
776
|
+
checkCaseCollisions(db)
|
|
777
|
+
];
|
|
778
|
+
}
|
|
779
|
+
function checkSegmentVersion(db, syncDir) {
|
|
780
|
+
if (syncDir === void 0 || syncDir.trim() === "") {
|
|
781
|
+
return { name: "segment-version", severity: "ok", detail: "MU_SYNC_DIR not set" };
|
|
782
|
+
}
|
|
783
|
+
const found = ownSegmentForeignLine(db, syncDir);
|
|
784
|
+
if (found === null) {
|
|
785
|
+
return { name: "segment-version", severity: "ok", detail: "own segment readable by this mu" };
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
name: "segment-version",
|
|
789
|
+
severity: "warn",
|
|
790
|
+
detail: foreignVersionMessage({ segmentPath: found.path, foreignVersion: found.version }),
|
|
791
|
+
remediation: [
|
|
792
|
+
`Line ${found.line} of the segment was written by a newer mu. This mu appends nothing`,
|
|
793
|
+
"and repairs nothing there; new ops stay in the local DB and are written",
|
|
794
|
+
"once mu is upgraded on this machine."
|
|
795
|
+
]
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
var GUARD_LIST_CAP = 10;
|
|
799
|
+
function listIds(rows) {
|
|
800
|
+
const shown = rows.slice(0, GUARD_LIST_CAP).map((r) => r.id);
|
|
801
|
+
return rows.length > GUARD_LIST_CAP ? `${shown.join(", ")}, \u2026 (${rows.length} total)` : shown.join(", ");
|
|
802
|
+
}
|
|
803
|
+
function checkOrphanedAgents(db) {
|
|
804
|
+
const rows = db.prepare(
|
|
805
|
+
`SELECT w.name AS ws, a.name AS agent, a.pane_id AS pane
|
|
806
|
+
FROM agents a JOIN workstreams w ON w.id = a.workstream_id
|
|
807
|
+
WHERE w.torn_down_at IS NOT NULL
|
|
808
|
+
ORDER BY w.name, a.name`
|
|
809
|
+
).all();
|
|
810
|
+
if (rows.length === 0) {
|
|
811
|
+
return {
|
|
812
|
+
name: "orphaned-agents",
|
|
813
|
+
severity: "ok",
|
|
814
|
+
detail: "no agent rows in torn-down workstreams"
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
const shown = rows.slice(0, GUARD_LIST_CAP).map((r) => `${r.ws}/${r.agent} (${r.pane})`);
|
|
818
|
+
return {
|
|
819
|
+
name: "orphaned-agents",
|
|
820
|
+
severity: "warn",
|
|
821
|
+
detail: `${rows.length} agent row(s) in torn-down workstreams: ${shown.join(", ")}${rows.length > GUARD_LIST_CAP ? ", \u2026" : ""}`,
|
|
822
|
+
remediation: [
|
|
823
|
+
"Their panes may still run. Per row: `mu agent close <agent> -w <ws>` (kills the pane; keeps the row while it lives)."
|
|
824
|
+
]
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function checkCloseGuards(db) {
|
|
828
|
+
const key = "w.name || '/' || t.local_id";
|
|
829
|
+
const openBlockers2 = db.prepare(
|
|
830
|
+
`SELECT ${key} AS id
|
|
831
|
+
FROM tasks t JOIN workstreams w ON w.id = t.workstream_id AND w.torn_down_at IS NULL
|
|
832
|
+
WHERE t.status = 'CLOSED' AND t.substate = 'done'
|
|
833
|
+
AND EXISTS (SELECT 1 FROM blockers b WHERE b.task_id = t.id)
|
|
834
|
+
ORDER BY 1`
|
|
835
|
+
).all();
|
|
836
|
+
const noEvidence = db.prepare(
|
|
837
|
+
`SELECT ${key} AS id
|
|
838
|
+
FROM tasks t JOIN workstreams w ON w.id = t.workstream_id AND w.torn_down_at IS NULL
|
|
839
|
+
WHERE t.status = 'CLOSED'
|
|
840
|
+
AND NOT EXISTS (SELECT 1 FROM task_notes n WHERE n.task_id = t.id AND n.kind = 'close')
|
|
841
|
+
ORDER BY 1`
|
|
842
|
+
).all();
|
|
843
|
+
const row = (name, found, what, fix) => found.length === 0 ? { name, severity: "ok", detail: `no ${what}` } : {
|
|
844
|
+
name,
|
|
845
|
+
severity: "warn",
|
|
846
|
+
detail: `${found.length} ${what}: ${listIds(found)}`,
|
|
847
|
+
remediation: [fix]
|
|
848
|
+
};
|
|
849
|
+
return [
|
|
850
|
+
row(
|
|
851
|
+
"closed-blockers",
|
|
852
|
+
openBlockers2,
|
|
853
|
+
"CLOSED/done task(s) with open blockers",
|
|
854
|
+
"A peer merged these states (the local guard refuses them). Inspect with `mu task tree <id>`; reopen or close the blockers."
|
|
855
|
+
),
|
|
856
|
+
row(
|
|
857
|
+
"closed-evidence",
|
|
858
|
+
noEvidence,
|
|
859
|
+
"CLOSED task(s) with no close note",
|
|
860
|
+
'A peer merged the close without its evidence note. Per listed task: `mu task open <ws>/<id>`, then `mu task close <ws>/<id> --evidence "<command + result>"` (close its open blockers first).'
|
|
861
|
+
)
|
|
862
|
+
];
|
|
863
|
+
}
|
|
864
|
+
function checkUnpublishedOps(db, syncDir) {
|
|
865
|
+
if (syncDir === void 0 || syncDir.trim() === "") {
|
|
866
|
+
return { name: "unpublished-ops", severity: "ok", detail: "MU_SYNC_DIR not set" };
|
|
867
|
+
}
|
|
868
|
+
const n = countUnpublishedOps(db, syncDir);
|
|
869
|
+
if (n === 0) {
|
|
870
|
+
return { name: "unpublished-ops", severity: "ok", detail: "every own op is in the segment" };
|
|
871
|
+
}
|
|
872
|
+
return {
|
|
873
|
+
name: "unpublished-ops",
|
|
874
|
+
severity: "warn",
|
|
875
|
+
detail: `${n} own op(s) not yet in this machine's segment; peers cannot see them`,
|
|
876
|
+
remediation: [
|
|
877
|
+
"Run `mu sync`: it flushes the backlog. If it reports the segment as locked,",
|
|
878
|
+
"another mu process holds the lock (a stale one is broken after 30s)."
|
|
879
|
+
]
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// src/glyphs.ts
|
|
884
|
+
var AGENT_STATE_GLYPH = {
|
|
885
|
+
busy: "\uF04B",
|
|
886
|
+
// nf-fa-play
|
|
887
|
+
needs_input: "\uF186",
|
|
888
|
+
// nf-fa-moon_o
|
|
889
|
+
needs_permission: "\uF023",
|
|
890
|
+
// nf-fa-lock
|
|
891
|
+
unknown: "\uF059"
|
|
892
|
+
// nf-fa-question_circle
|
|
893
|
+
};
|
|
894
|
+
function agentStateGlyph(state) {
|
|
895
|
+
return AGENT_STATE_GLYPH[state] ?? GLYPH.unknown;
|
|
896
|
+
}
|
|
897
|
+
var GLYPH = {
|
|
898
|
+
/** Healthy, clean, passing, closed-successfully. */
|
|
899
|
+
ok: "\uF058",
|
|
900
|
+
// nf-fa-check_circle
|
|
901
|
+
/** Failed check. */
|
|
902
|
+
fail: "\uF057",
|
|
903
|
+
// nf-fa-times_circle
|
|
904
|
+
/** Needs attention but not broken (idle-but-assigned, stale warning). */
|
|
905
|
+
warn: "\uF071",
|
|
906
|
+
// nf-fa-warning
|
|
907
|
+
/** Task is blocked by an incoming edge. */
|
|
908
|
+
blocked: "\uF0C1",
|
|
909
|
+
// nf-fa-link (chain)
|
|
910
|
+
/** Task is parked (OPEN/parked): open, but set aside on purpose. */
|
|
911
|
+
parked: "\uF04C",
|
|
912
|
+
// nf-fa-pause
|
|
913
|
+
/** Track whose roots merged (diamond dependency). */
|
|
914
|
+
merge: "\uF074",
|
|
915
|
+
// nf-fa-random
|
|
916
|
+
/** Agent owns more than one task (pane-title multi-count slot). */
|
|
917
|
+
multi: "\uF055",
|
|
918
|
+
// nf-fa-plus_circle
|
|
919
|
+
/** Filter toggle: enabled / disabled. */
|
|
920
|
+
on: "\uF111",
|
|
921
|
+
// nf-fa-circle
|
|
922
|
+
off: "\uF10C",
|
|
923
|
+
// nf-fa-circle_o
|
|
924
|
+
/** Status outside the known enum. */
|
|
925
|
+
unknown: "\uF059"
|
|
926
|
+
// nf-fa-question_circle
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
// src/wait-types.ts
|
|
930
|
+
var WAIT_HEARTBEAT_CAP_MS = 15e5;
|
|
931
|
+
var DEFAULT_STUCK_AFTER_MS = 12e4;
|
|
932
|
+
var DEFAULT_CTL_STUCK_AFTER_MS = 5e3;
|
|
933
|
+
var WaitNothingError = class extends Error {
|
|
934
|
+
name = "WaitNothingError";
|
|
935
|
+
constructor() {
|
|
936
|
+
super("wait: nothing to wait for (no refs given and none owed to this caller)");
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
var WaveNotFoundError = class extends Error {
|
|
940
|
+
constructor(wave, yours) {
|
|
941
|
+
super(say("wait.wave_not_found", wave, yours));
|
|
942
|
+
this.wave = wave;
|
|
943
|
+
}
|
|
944
|
+
wave;
|
|
945
|
+
name = "WaveNotFoundError";
|
|
946
|
+
};
|
|
947
|
+
var WaitRefError = class extends Error {
|
|
948
|
+
name = "WaitRefError";
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
// src/wait-regex.ts
|
|
952
|
+
import { createContext, Script } from "vm";
|
|
953
|
+
var WAIT_REGEX_MAX_LEN = 200;
|
|
954
|
+
var WAIT_TEXT_MAX = 8192;
|
|
955
|
+
var WAIT_REGEX_TIMEOUT_MS = 50;
|
|
956
|
+
var MAX_COUNT = 1e3;
|
|
957
|
+
var WaitRegexError = class extends Error {
|
|
958
|
+
name = "WaitRegexError";
|
|
959
|
+
};
|
|
960
|
+
function quantifierAt(src, i) {
|
|
961
|
+
const c = src[i];
|
|
962
|
+
if (c === "*" || c === "+" || c === "?") return { len: 1, counts: [] };
|
|
963
|
+
if (c !== "{") return void 0;
|
|
964
|
+
const m = /^\{(\d+)(?:,(\d*))?\}/.exec(src.slice(i, i + 24));
|
|
965
|
+
if (m === null) return void 0;
|
|
966
|
+
const counts2 = [Number(m[1])];
|
|
967
|
+
if (m[2] !== void 0 && m[2] !== "") counts2.push(Number(m[2]));
|
|
968
|
+
return { len: m[0].length, counts: counts2 };
|
|
969
|
+
}
|
|
970
|
+
function refuse(why) {
|
|
971
|
+
throw new WaitRegexError(`--until-text: ${why}`);
|
|
972
|
+
}
|
|
973
|
+
function checkRegexGrammar(src) {
|
|
974
|
+
if (src === "") refuse("the pattern is empty");
|
|
975
|
+
if (src.length > WAIT_REGEX_MAX_LEN) {
|
|
976
|
+
refuse(`the pattern is ${src.length} characters; the limit is ${WAIT_REGEX_MAX_LEN}`);
|
|
977
|
+
}
|
|
978
|
+
if (/[\u0000-\u001f\u007f]/.test(src)) refuse("the pattern has control characters");
|
|
979
|
+
const stack = [];
|
|
980
|
+
let top = { quant: false, alt: false };
|
|
981
|
+
let inClass = false;
|
|
982
|
+
for (let i = 0; i < src.length; i++) {
|
|
983
|
+
const c = src[i];
|
|
984
|
+
if (c === "\\") {
|
|
985
|
+
const n = src[i + 1] ?? "";
|
|
986
|
+
if (/[1-9k]/.test(n)) refuse("backreferences are not supported");
|
|
987
|
+
i++;
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
if (inClass) {
|
|
991
|
+
if (c === "]") inClass = false;
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
if (c === "[") {
|
|
995
|
+
inClass = true;
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
if (c === "(") {
|
|
999
|
+
if (src[i + 1] === "?") {
|
|
1000
|
+
if (src[i + 2] !== ":") refuse("lookaround and named groups are not supported");
|
|
1001
|
+
i += 2;
|
|
1002
|
+
}
|
|
1003
|
+
stack.push(top);
|
|
1004
|
+
top = { quant: false, alt: false };
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
if (c === ")") {
|
|
1008
|
+
const parent = stack.pop();
|
|
1009
|
+
if (parent === void 0) refuse("unbalanced `)`");
|
|
1010
|
+
const q2 = quantifierAt(src, i + 1);
|
|
1011
|
+
if (q2 !== void 0 && (top.quant || top.alt)) {
|
|
1012
|
+
refuse("a repeated group may not contain a repeat or an alternation");
|
|
1013
|
+
}
|
|
1014
|
+
parent.quant ||= top.quant || q2 !== void 0;
|
|
1015
|
+
parent.alt ||= top.alt;
|
|
1016
|
+
top = parent;
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
if (c === "|") {
|
|
1020
|
+
top.alt = true;
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
const q = quantifierAt(src, i);
|
|
1024
|
+
if (q !== void 0) {
|
|
1025
|
+
if (q.counts.some((n) => n > MAX_COUNT)) refuse(`a repeat count is over ${MAX_COUNT}`);
|
|
1026
|
+
top.quant = true;
|
|
1027
|
+
i += q.len - 1;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if (stack.length > 0) refuse("unbalanced `(`");
|
|
1031
|
+
}
|
|
1032
|
+
var SCRIPT = new Script("new RegExp(src).test(text)");
|
|
1033
|
+
function compileTextMatcher(src) {
|
|
1034
|
+
checkRegexGrammar(src);
|
|
1035
|
+
try {
|
|
1036
|
+
new RegExp(src);
|
|
1037
|
+
} catch (e) {
|
|
1038
|
+
refuse(`not a valid regular expression (${e instanceof Error ? e.message : String(e)})`);
|
|
1039
|
+
}
|
|
1040
|
+
const ctx = createContext({ src, text: "" });
|
|
1041
|
+
return {
|
|
1042
|
+
source: src,
|
|
1043
|
+
test(text) {
|
|
1044
|
+
ctx.text = text.length > WAIT_TEXT_MAX ? text.slice(-WAIT_TEXT_MAX) : text;
|
|
1045
|
+
try {
|
|
1046
|
+
return SCRIPT.runInContext(ctx, { timeout: WAIT_REGEX_TIMEOUT_MS }) === true;
|
|
1047
|
+
} catch (e) {
|
|
1048
|
+
throw new WaitRegexError(
|
|
1049
|
+
`--until-text: matching exceeded ${WAIT_REGEX_TIMEOUT_MS} ms (${e instanceof Error ? e.message : String(e)}); use a simpler pattern`
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/close-block.ts
|
|
1057
|
+
var HEADER = /^CLOSE: task=(\S+) evidence="(.*)"$/;
|
|
1058
|
+
var TEMPLATE = /^CLOSE:[ \t]+task=<[^>\s]*>/;
|
|
1059
|
+
var FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
1060
|
+
var FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
|
|
1061
|
+
function quotedLines(lines) {
|
|
1062
|
+
const quoted = [];
|
|
1063
|
+
let open;
|
|
1064
|
+
for (const line of lines) {
|
|
1065
|
+
if (open) {
|
|
1066
|
+
quoted.push(true);
|
|
1067
|
+
const c = FENCE_CLOSE.exec(line);
|
|
1068
|
+
const run2 = c?.[1] ?? "";
|
|
1069
|
+
if (run2[0] === open.ch && run2.length >= open.len) open = void 0;
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
const o = FENCE_OPEN.exec(line);
|
|
1073
|
+
const run = o?.[1] ?? "";
|
|
1074
|
+
if (o && !(run[0] === "`" && (o[2] ?? "").includes("`"))) {
|
|
1075
|
+
open = { ch: run[0], len: run.length };
|
|
1076
|
+
quoted.push(true);
|
|
1077
|
+
} else {
|
|
1078
|
+
quoted.push(false);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return { quoted, unclosed: open !== void 0 };
|
|
1082
|
+
}
|
|
1083
|
+
var isStart = (line) => line.startsWith("CLOSE:") && !TEMPLATE.test(line);
|
|
1084
|
+
var none = (reason) => ({ kind: "none", reason });
|
|
1085
|
+
var malformed = (reason) => ({ kind: "malformed", reason });
|
|
1086
|
+
function parseCloseBlock(text) {
|
|
1087
|
+
if (text.trim() === "") return none("empty message");
|
|
1088
|
+
const body = text.replace(/(?:\r?\n)+$/, "");
|
|
1089
|
+
const raw = body.split("\n");
|
|
1090
|
+
const offsets = [];
|
|
1091
|
+
let at = 0;
|
|
1092
|
+
for (const l of raw) {
|
|
1093
|
+
offsets.push(at);
|
|
1094
|
+
at += l.length + 1;
|
|
1095
|
+
}
|
|
1096
|
+
const lines = raw.map((l) => l.replace(/\r$/, ""));
|
|
1097
|
+
const { quoted, unclosed } = quotedLines(lines);
|
|
1098
|
+
if (unclosed) return none("the message ends inside a code fence");
|
|
1099
|
+
let start = -1;
|
|
1100
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1101
|
+
if (!quoted[i] && isStart(lines[i] ?? "")) start = i;
|
|
1102
|
+
}
|
|
1103
|
+
if (start < 0) return none("no CLOSE: line at column 0");
|
|
1104
|
+
const m = HEADER.exec(lines[start] ?? "");
|
|
1105
|
+
if (!m) return malformed('the CLOSE: line is not `CLOSE: task=<id> evidence="<text>"`');
|
|
1106
|
+
const task = m[1] ?? "";
|
|
1107
|
+
if (!isValidTaskId(task)) return malformed("task= is not a valid task id");
|
|
1108
|
+
const evidence = unescapeEvidence(m[2] ?? "");
|
|
1109
|
+
if (evidence === void 0) return malformed("evidence has a bare or unterminated quote");
|
|
1110
|
+
if (evidence.trim() === "") return malformed("evidence is empty");
|
|
1111
|
+
if (start + 1 >= lines.length || lines[start + 1] !== "REPORT:") {
|
|
1112
|
+
return malformed("the line after CLOSE: is not exactly REPORT:");
|
|
1113
|
+
}
|
|
1114
|
+
const report = body.slice(offsets[start + 2] ?? body.length);
|
|
1115
|
+
if (report.trim() === "") return malformed("the report is empty");
|
|
1116
|
+
const normalized = body.length < text.length ? `${report}
|
|
1117
|
+
` : report;
|
|
1118
|
+
return { kind: "valid", task, evidence, report: normalized };
|
|
1119
|
+
}
|
|
1120
|
+
function unescapeEvidence(body) {
|
|
1121
|
+
let out = "";
|
|
1122
|
+
for (let i = 0; i < body.length; i++) {
|
|
1123
|
+
const c = body[i];
|
|
1124
|
+
if (c === '"') return void 0;
|
|
1125
|
+
if (c !== "\\") {
|
|
1126
|
+
out += c;
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
const n = body[i + 1];
|
|
1130
|
+
if (n === '"' || n === "\\") {
|
|
1131
|
+
out += n;
|
|
1132
|
+
i++;
|
|
1133
|
+
} else if (n === "n") {
|
|
1134
|
+
out += "\n";
|
|
1135
|
+
i++;
|
|
1136
|
+
} else if (n === void 0) {
|
|
1137
|
+
return void 0;
|
|
1138
|
+
} else {
|
|
1139
|
+
out += c;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return out;
|
|
1143
|
+
}
|
|
1144
|
+
var CLOSE_MALFORMED = "no close (malformed)";
|
|
1145
|
+
var CLOSE_TRUNCATED = "CLOSE block not applied: the final message was truncated; close with mu task close";
|
|
1146
|
+
var no = (reason, task = null) => ({
|
|
1147
|
+
applied: false,
|
|
1148
|
+
task,
|
|
1149
|
+
reason
|
|
1150
|
+
});
|
|
1151
|
+
function applySettledClose(db, input) {
|
|
1152
|
+
if (!input.settled) return no("not settled");
|
|
1153
|
+
if (input.lastError !== void 0) return no("run ended on an error");
|
|
1154
|
+
return withOpContext(
|
|
1155
|
+
db,
|
|
1156
|
+
{ intent: "close.apply", actor: input.agent, group: "new" },
|
|
1157
|
+
() => applyInTxn(db, input)
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
function applyInTxn(db, input) {
|
|
1161
|
+
const agent = db.prepare(
|
|
1162
|
+
`SELECT a.cli FROM agents a JOIN workstreams w ON w.id = a.workstream_id
|
|
1163
|
+
WHERE a.id = ? AND a.name = ? AND w.name = ? AND w.torn_down_at IS NULL`
|
|
1164
|
+
).get(input.agentId, input.agent, input.workstream);
|
|
1165
|
+
if (!agent) return no("unknown agent");
|
|
1166
|
+
if (agent.cli !== "pi") return no("not a pi agent");
|
|
1167
|
+
const seen = db.prepare("SELECT 1 FROM agent_settles WHERE agent_id = ? AND incarnation = ? AND runs = ?").get(input.agentId, input.incarnation, input.runs);
|
|
1168
|
+
if (seen) return no("already applied");
|
|
1169
|
+
const record = (taskId) => {
|
|
1170
|
+
db.prepare(
|
|
1171
|
+
"INSERT INTO agent_settles (agent_id, incarnation, runs, task_id) VALUES (?, ?, ?, ?)"
|
|
1172
|
+
).run(input.agentId, input.incarnation, input.runs, taskId);
|
|
1173
|
+
};
|
|
1174
|
+
const diagnose = (reason) => {
|
|
1175
|
+
const open = db.prepare(
|
|
1176
|
+
`SELECT t.local_id FROM tasks t JOIN workstreams w ON w.id = t.workstream_id
|
|
1177
|
+
WHERE t.owner_id = ? AND t.status = 'IN_PROGRESS' AND w.name = ?`
|
|
1178
|
+
).all(input.agentId, input.workstream);
|
|
1179
|
+
const only = open.length === 1 ? open[0]?.local_id ?? null : null;
|
|
1180
|
+
if (only !== null) {
|
|
1181
|
+
addNote(db, only, reason, {
|
|
1182
|
+
workstream: input.workstream,
|
|
1183
|
+
author: input.agent,
|
|
1184
|
+
group: "inherit"
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
record(null);
|
|
1188
|
+
return no(reason, only);
|
|
1189
|
+
};
|
|
1190
|
+
if (input.lastTextTruncated === true) return diagnose(CLOSE_TRUNCATED);
|
|
1191
|
+
const block = parseCloseBlock(input.lastText);
|
|
1192
|
+
if (block.kind === "none") return no(`no CLOSE block: ${block.reason}`);
|
|
1193
|
+
if (block.kind === "malformed") return diagnose(`${CLOSE_MALFORMED}: ${block.reason}`);
|
|
1194
|
+
const task = db.prepare(
|
|
1195
|
+
`SELECT t.id, t.status, t.owner_id, t.kind FROM tasks t JOIN workstreams w ON w.id = t.workstream_id
|
|
1196
|
+
WHERE w.name = ? AND w.torn_down_at IS NULL AND t.local_id = ?`
|
|
1197
|
+
).get(input.workstream, block.task);
|
|
1198
|
+
if (task?.kind === "human") {
|
|
1199
|
+
record(task.id);
|
|
1200
|
+
return no(
|
|
1201
|
+
`human task: close it with \`mu task close ${block.task} -w ${input.workstream}\``,
|
|
1202
|
+
block.task
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
if (task?.kind === "delegate") {
|
|
1206
|
+
record(task.id);
|
|
1207
|
+
return no("delegate task: its call records the answer and closes it", block.task);
|
|
1208
|
+
}
|
|
1209
|
+
if (!task || task.owner_id !== input.agentId) {
|
|
1210
|
+
record(null);
|
|
1211
|
+
return no("task is not owned by this agent", block.task);
|
|
1212
|
+
}
|
|
1213
|
+
if (task.status === "CLOSED") {
|
|
1214
|
+
record(task.id);
|
|
1215
|
+
return no("task already closed", block.task);
|
|
1216
|
+
}
|
|
1217
|
+
closeTask(db, block.task, {
|
|
1218
|
+
workstream: input.workstream,
|
|
1219
|
+
evidence: block.evidence,
|
|
1220
|
+
report: block.report,
|
|
1221
|
+
author: input.agent,
|
|
1222
|
+
group: "inherit"
|
|
1223
|
+
});
|
|
1224
|
+
record(task.id);
|
|
1225
|
+
return { applied: true, task: block.task, reason: "closed" };
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// src/wait-db.ts
|
|
1229
|
+
function closeSettledRun(db, ref, r, enabled) {
|
|
1230
|
+
if (!enabled || r.settled !== true) return void 0;
|
|
1231
|
+
const row = db.prepare(
|
|
1232
|
+
`SELECT a.id FROM agents a JOIN workstreams w ON w.id = a.workstream_id
|
|
1233
|
+
WHERE a.name = ? AND w.name = ?`
|
|
1234
|
+
).get(ref.id, ref.workstream);
|
|
1235
|
+
const facts = settledCloseInput({
|
|
1236
|
+
workstream: ref.workstream,
|
|
1237
|
+
name: ref.id,
|
|
1238
|
+
agentId: r.agentId ?? row?.id ?? 0,
|
|
1239
|
+
status: r.status,
|
|
1240
|
+
settled: true,
|
|
1241
|
+
...r.incarnation !== void 0 ? { incarnation: r.incarnation } : {},
|
|
1242
|
+
...r.runs !== void 0 ? { runs: r.runs } : {},
|
|
1243
|
+
...r.lastText !== void 0 ? { lastText: r.lastText } : {},
|
|
1244
|
+
...r.lastTextTruncated !== void 0 ? { lastTextTruncated: r.lastTextTruncated } : {},
|
|
1245
|
+
...r.lastError !== void 0 ? { lastError: r.lastError } : {}
|
|
1246
|
+
});
|
|
1247
|
+
if (facts === void 0) return void 0;
|
|
1248
|
+
try {
|
|
1249
|
+
const res = applySettledClose(db, facts);
|
|
1250
|
+
return { applied: res.applied, task: res.task, reason: res.reason };
|
|
1251
|
+
} catch (e) {
|
|
1252
|
+
return { applied: false, task: null, reason: e instanceof Error ? e.message : String(e) };
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
function agentExists(db, ref) {
|
|
1256
|
+
return db.prepare(
|
|
1257
|
+
`SELECT 1 FROM agents a JOIN workstreams ws ON ws.id = a.workstream_id
|
|
1258
|
+
WHERE a.name = ? AND ws.name = ?`
|
|
1259
|
+
).get(ref.id, ref.workstream) !== void 0;
|
|
1260
|
+
}
|
|
1261
|
+
function openBlockers(db, umbrella) {
|
|
1262
|
+
return db.prepare(BLOCKERS_SQL).all(umbrella);
|
|
1263
|
+
}
|
|
1264
|
+
var BLOCKERS_SQL = `
|
|
1265
|
+
WITH RECURSIVE reach(node) AS (
|
|
1266
|
+
SELECT e.from_task_id FROM task_edges e WHERE e.to_task_id = ?
|
|
1267
|
+
UNION
|
|
1268
|
+
SELECT e.from_task_id FROM task_edges e
|
|
1269
|
+
JOIN reach r ON e.to_task_id = r.node
|
|
1270
|
+
JOIN tasks t ON t.id = r.node
|
|
1271
|
+
WHERE t.status <> 'CLOSED'
|
|
1272
|
+
)
|
|
1273
|
+
SELECT t.local_id AS id, w.name AS ws FROM reach r
|
|
1274
|
+
JOIN tasks t ON t.id = r.node JOIN workstreams w ON w.id = t.workstream_id
|
|
1275
|
+
ORDER BY w.name, t.local_id`;
|
|
1276
|
+
function agentRowId(db, ref) {
|
|
1277
|
+
const row = db.prepare(
|
|
1278
|
+
`SELECT a.id FROM agents a JOIN workstreams ws ON ws.id = a.workstream_id
|
|
1279
|
+
WHERE a.name = ? AND ws.name = ?`
|
|
1280
|
+
).get(ref.id, ref.workstream);
|
|
1281
|
+
return row?.id ?? 0;
|
|
1282
|
+
}
|
|
1283
|
+
function opsHead(db) {
|
|
1284
|
+
return db.prepare("SELECT COALESCE(MAX(seq), 0) AS s FROM ops").get().s;
|
|
1285
|
+
}
|
|
1286
|
+
function reapFlip(db, ref, since) {
|
|
1287
|
+
const key = `${ref.workstream}/${ref.id}`;
|
|
1288
|
+
const last = db.prepare(
|
|
1289
|
+
`SELECT seq, intent FROM ops WHERE entity = 'task' AND key = ?
|
|
1290
|
+
AND (json_extract(payload, '$.status') IS NOT NULL OR json_extract(payload, '$.substate') IS NOT NULL)
|
|
1291
|
+
ORDER BY seq DESC LIMIT 1`
|
|
1292
|
+
).get(key);
|
|
1293
|
+
if (last === void 0 || last.intent !== "task.reap" || last.seq <= since) return void 0;
|
|
1294
|
+
const owner = db.prepare(
|
|
1295
|
+
`SELECT json_extract(payload, '$.owner_id') AS id, actor FROM ops
|
|
1296
|
+
WHERE entity = 'task' AND key = ? AND seq < ? AND json_extract(payload, '$.owner_id') IS NOT NULL
|
|
1297
|
+
ORDER BY seq DESC LIMIT 1`
|
|
1298
|
+
).get(key, last.seq);
|
|
1299
|
+
return {
|
|
1300
|
+
seq: last.seq,
|
|
1301
|
+
ownerId: Number(owner?.id ?? 0),
|
|
1302
|
+
...owner?.actor != null ? { ownerName: owner.actor } : {}
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
function taskOwnerRowId(db, ref) {
|
|
1306
|
+
const row = db.prepare(
|
|
1307
|
+
`SELECT COALESCE(t.owner_id, 0) AS id FROM tasks t JOIN workstreams ws ON ws.id = t.workstream_id
|
|
1308
|
+
WHERE t.local_id = ? AND ws.name = ?`
|
|
1309
|
+
).get(ref.id, ref.workstream);
|
|
1310
|
+
return row?.id ?? 0;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// src/wait-item.ts
|
|
1314
|
+
var refKey = (r) => cursorKey(r.kind, r.workstream, r.id);
|
|
1315
|
+
var registered = (r) => ({
|
|
1316
|
+
kind: r.kind,
|
|
1317
|
+
workstream: r.workstream,
|
|
1318
|
+
id: r.id
|
|
1319
|
+
});
|
|
1320
|
+
var URGENCY = {
|
|
1321
|
+
dead: 0,
|
|
1322
|
+
input: 1,
|
|
1323
|
+
stall: 2,
|
|
1324
|
+
delegate: 3
|
|
1325
|
+
};
|
|
1326
|
+
var urgency = (k) => k === void 0 ? 9 : URGENCY[k] ?? 4;
|
|
1327
|
+
var isCompletion = (i) => i.kind === "closed" || i.kind === "settled" || i.kind === "pane-text" || i.kind === "exited" || i.kind === "delegate" || i.kind === "dead" && i.ref.kind === "agent";
|
|
1328
|
+
var exitFor = (k) => k === "dead" ? 6 : k === "input" || k === "stall" ? 7 : k === "interrupted" ? 130 : 0;
|
|
1329
|
+
var inputToken = (r) => ({
|
|
1330
|
+
incarnation: `input:${r.incarnation ?? ""}`,
|
|
1331
|
+
runs: r.blocked?.since ?? 0
|
|
1332
|
+
});
|
|
1333
|
+
var agentSnapshot = (t) => {
|
|
1334
|
+
const r = t.latched?.reading ?? t.reading;
|
|
1335
|
+
const status = t.latched?.kind === "dead" || t.latched?.kind === "exited" ? null : r?.status ?? "unknown";
|
|
1336
|
+
return {
|
|
1337
|
+
ref: t.ref,
|
|
1338
|
+
status,
|
|
1339
|
+
...r?.agentId !== void 0 ? { agentId: r.agentId } : {},
|
|
1340
|
+
...r?.paneId !== void 0 ? { paneId: r.paneId } : {},
|
|
1341
|
+
...r?.settled !== void 0 ? { settled: r.settled } : {},
|
|
1342
|
+
...r?.lastTextTruncated !== void 0 ? { lastTextTruncated: r.lastTextTruncated } : {},
|
|
1343
|
+
...r?.textSource !== void 0 ? { textSource: r.textSource } : {},
|
|
1344
|
+
...r?.incarnation !== void 0 ? { incarnation: r.incarnation } : {},
|
|
1345
|
+
...r?.runs !== void 0 ? { runs: r.runs } : {},
|
|
1346
|
+
...r?.lastText !== void 0 ? { lastText: r.lastText } : {},
|
|
1347
|
+
...r?.lastError !== void 0 ? { lastError: r.lastError } : {},
|
|
1348
|
+
...r?.sessionFile !== void 0 ? { sessionFile: r.sessionFile } : {},
|
|
1349
|
+
...r?.blocked !== void 0 ? { blocked: r.blocked } : {},
|
|
1350
|
+
...t.latched?.kind === "exited" && r?.exit !== void 0 ? { exit: r.exit } : {},
|
|
1351
|
+
...status === "unknown" && r?.reason !== void 0 ? { unknownReason: r.reason } : {},
|
|
1352
|
+
...t.baseline !== void 0 ? { afterRuns: t.baseline } : {},
|
|
1353
|
+
wasBusy: t.wasBusy,
|
|
1354
|
+
...t.unknownBaseline ? { unknownBaseline: true } : {},
|
|
1355
|
+
fired: t.latched?.kind === "settled" || t.latched?.kind === "pane-text" || t.latched?.kind === "exited",
|
|
1356
|
+
dead: t.latched?.kind === "dead",
|
|
1357
|
+
...t.close !== void 0 ? { close: t.close } : {}
|
|
1358
|
+
};
|
|
1359
|
+
};
|
|
1360
|
+
var delegateItem = (call, isTarget, suppressible, order) => {
|
|
1361
|
+
const ref = { kind: "delegate", workstream: call.workstream, id: call.callId };
|
|
1362
|
+
const terminal = TERMINAL_DELEGATE_STATES.includes(call.state);
|
|
1363
|
+
const item = {
|
|
1364
|
+
key: refKey(ref),
|
|
1365
|
+
ref,
|
|
1366
|
+
snap: {
|
|
1367
|
+
ref,
|
|
1368
|
+
status: call.state,
|
|
1369
|
+
verdict: call.verdict,
|
|
1370
|
+
owner: call.agent,
|
|
1371
|
+
// Which finding this is: the delegate's own task, the task it judges, its wave.
|
|
1372
|
+
callTask: call.task,
|
|
1373
|
+
...call.record !== null ? { record: `${call.record.workstream}/${call.record.task}` } : {},
|
|
1374
|
+
...call.wave !== null ? { wave: call.wave } : {},
|
|
1375
|
+
fired: terminal,
|
|
1376
|
+
dead: false
|
|
1377
|
+
},
|
|
1378
|
+
target: isTarget,
|
|
1379
|
+
suppressible,
|
|
1380
|
+
order,
|
|
1381
|
+
done: terminal,
|
|
1382
|
+
ready: false
|
|
1383
|
+
};
|
|
1384
|
+
if (terminal) {
|
|
1385
|
+
item.kind = "delegate";
|
|
1386
|
+
item.token = DELEGATE_TOKEN;
|
|
1387
|
+
}
|
|
1388
|
+
return item;
|
|
1389
|
+
};
|
|
1390
|
+
var sameDeath = (a, b) => {
|
|
1391
|
+
const task = a.ref.kind === "task" ? a : b;
|
|
1392
|
+
const agent = task === a ? b : a;
|
|
1393
|
+
const t = task.death;
|
|
1394
|
+
const g = agent.death;
|
|
1395
|
+
if (a.kind !== "dead" || b.kind !== "dead" || t === void 0 || g === void 0) return false;
|
|
1396
|
+
if (task.ref.kind !== "task" || agent.ref.kind !== "agent") return false;
|
|
1397
|
+
if (g.rowId !== 0) return g.rowId === t.rowId;
|
|
1398
|
+
return t.name === agent.ref.id && (t.workstream === void 0 || t.workstream === agent.ref.workstream);
|
|
1399
|
+
};
|
|
1400
|
+
var failedGate = (i) => i.kind === "exited" && i.snap.exit !== void 0 && i.snap.exit !== 0;
|
|
1401
|
+
var snapshotsOf = (items, extra = []) => {
|
|
1402
|
+
const targets = items.filter((i) => i.target);
|
|
1403
|
+
const base = targets.length > 0 ? targets : items;
|
|
1404
|
+
const shown = [...base];
|
|
1405
|
+
for (const e of extra) if (!shown.includes(e)) shown.push(e);
|
|
1406
|
+
return shown.map((i) => i.snap);
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
// src/wait-latch.ts
|
|
1410
|
+
function rearmLatch(t, baseline, watching) {
|
|
1411
|
+
t.latched = void 0;
|
|
1412
|
+
t.close = void 0;
|
|
1413
|
+
t.handle = void 0;
|
|
1414
|
+
t.result = void 0;
|
|
1415
|
+
t.needWatch = watching;
|
|
1416
|
+
t.baseline = baseline;
|
|
1417
|
+
t.wasBusy = false;
|
|
1418
|
+
}
|
|
1419
|
+
function supersededBy(t, r) {
|
|
1420
|
+
const held = t.latched?.kind === "settled" ? t.latched.reading : void 0;
|
|
1421
|
+
if (held === void 0 || r.runs === void 0) return void 0;
|
|
1422
|
+
const differs = (a, b) => a !== void 0 && b !== void 0 && a !== b;
|
|
1423
|
+
if (differs(held.agentId, r.agentId) || differs(held.incarnation, r.incarnation)) return 0;
|
|
1424
|
+
if (held.runs !== void 0 && r.runs > held.runs) return held.runs;
|
|
1425
|
+
return void 0;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// src/wait.ts
|
|
1429
|
+
var sleepSeam;
|
|
1430
|
+
async function waitForEvents(db, input, opts) {
|
|
1431
|
+
const callers = [...new Set(opts.callers ?? [])];
|
|
1432
|
+
for (const c of callers) checkCaller(c);
|
|
1433
|
+
const all = opts.mode === "all";
|
|
1434
|
+
const ownedScope = opts.eventScope !== "refs";
|
|
1435
|
+
const stuckExplicit = opts.stuckAfterMs;
|
|
1436
|
+
const stuckEnabled = stuckExplicit !== 0 && opts.status !== "OPEN" && opts.status !== "IN_PROGRESS";
|
|
1437
|
+
const stuckAfter = (viaCtl) => stuckExplicit ?? (viaCtl ? DEFAULT_CTL_STUCK_AFTER_MS : DEFAULT_STUCK_AFTER_MS);
|
|
1438
|
+
const watchDeaths = callers.length > 0 || ownedScope;
|
|
1439
|
+
const scope = opts.scope;
|
|
1440
|
+
const inScope = (workstream) => scope === void 0 || workstream === scope;
|
|
1441
|
+
const callInScope = (c) => scope === void 0 || c.workstream === scope || c.record?.workstream === scope;
|
|
1442
|
+
const observeOwners = (watchDeaths || stuckEnabled) && opts.readAgents !== void 0;
|
|
1443
|
+
const consumes = ownedScope && callers.length > 0;
|
|
1444
|
+
const target = opts.status ?? "CLOSED";
|
|
1445
|
+
const discover = consumes && target === "CLOSED";
|
|
1446
|
+
const matcher = typeof opts.until === "object" ? compileTextMatcher(opts.until.text) : void 0;
|
|
1447
|
+
const applyClose = opts.applyClose !== false;
|
|
1448
|
+
const pollMs = opts.pollMs ?? 1e3;
|
|
1449
|
+
const requested = opts.timeoutMs ?? 0;
|
|
1450
|
+
const timeoutMs = opts.uncapped ? requested : requested === 0 ? WAIT_HEARTBEAT_CAP_MS : Math.min(requested, WAIT_HEARTBEAT_CAP_MS);
|
|
1451
|
+
const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : Number.POSITIVE_INFINITY;
|
|
1452
|
+
const sinceSeq = opts.sinceSeq ?? opsHead(db);
|
|
1453
|
+
for (const ref of input) {
|
|
1454
|
+
if (ref.kind === "task") {
|
|
1455
|
+
if (getTask(db, ref.id, ref.workstream) === void 0) {
|
|
1456
|
+
throw new TaskNotFoundError(`${ref.workstream}/${ref.id}`);
|
|
1457
|
+
}
|
|
1458
|
+
} else if (ref.kind === "agent") {
|
|
1459
|
+
if (!agentExists(db, ref)) throw new AgentNotFoundError(ref.id);
|
|
1460
|
+
} else {
|
|
1461
|
+
const call = getDelegateCall(db, ref.id);
|
|
1462
|
+
if (call === void 0 || call.workstream !== ref.workstream) {
|
|
1463
|
+
throw new DelegateNotFoundError(ref.id);
|
|
1464
|
+
}
|
|
1465
|
+
if (callers.length > 0 && !callers.includes(call.caller)) {
|
|
1466
|
+
throw new WaitRefError(`delegate call ${ref.id} belongs to another caller`);
|
|
1467
|
+
}
|
|
1468
|
+
if (call.state === "refused") {
|
|
1469
|
+
throw new WaitRefError(
|
|
1470
|
+
`delegate call ${ref.id} was refused (it never started): reissue it, mu_delegate_reissue`
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
const umbrella = opts.blockersOf === void 0 ? void 0 : taskIdFor(db, opts.blockersOf.id, opts.blockersOf.workstream);
|
|
1476
|
+
if (opts.blockersOf !== void 0 && umbrella === null) {
|
|
1477
|
+
throw new TaskNotFoundError(`${opts.blockersOf.workstream}/${opts.blockersOf.id}`);
|
|
1478
|
+
}
|
|
1479
|
+
const explicit = /* @__PURE__ */ new Map();
|
|
1480
|
+
for (const r of input) explicit.set(refKey(r), r);
|
|
1481
|
+
if (opts.wave !== void 0 && callers.length === 0) {
|
|
1482
|
+
throw new WaitRefError("wait: a wave belongs to callers; none given (no all-caller fallback)");
|
|
1483
|
+
}
|
|
1484
|
+
if (input.length === 0 && !opts.wave && !opts.blockersOf && callers.length === 0) {
|
|
1485
|
+
throw new WaitNothingError();
|
|
1486
|
+
}
|
|
1487
|
+
if (opts.wave !== void 0) {
|
|
1488
|
+
const mine = listDelegatesOf(db, callers, opts.wave.workstream);
|
|
1489
|
+
if (!mine.some((c) => c.wave === opts.wave?.name)) {
|
|
1490
|
+
throw new WaveNotFoundError(
|
|
1491
|
+
opts.wave.name,
|
|
1492
|
+
mine.map((c) => c.wave)
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
const begun = /* @__PURE__ */ new Map();
|
|
1497
|
+
if (consumes) {
|
|
1498
|
+
for (const c of callers) {
|
|
1499
|
+
const b = beginWait(db, c);
|
|
1500
|
+
if (b !== void 0) begun.set(c, b);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
const cursorsBy = new Map(
|
|
1504
|
+
(consumes ? callers : []).map((c) => [c, readCursors(db, c)])
|
|
1505
|
+
);
|
|
1506
|
+
const ownerOfKey = /* @__PURE__ */ new Map();
|
|
1507
|
+
const baseOfCkey = /* @__PURE__ */ new Map();
|
|
1508
|
+
const urgentKey = (kind, key) => {
|
|
1509
|
+
const k = `${kind}|${key}`;
|
|
1510
|
+
baseOfCkey.set(k, key);
|
|
1511
|
+
return k;
|
|
1512
|
+
};
|
|
1513
|
+
const ownerOf = (key) => ownerOfKey.get(baseOfCkey.get(key) ?? key) ?? callers[0];
|
|
1514
|
+
const cursorOf = (key) => {
|
|
1515
|
+
const o = ownerOf(key);
|
|
1516
|
+
return o === void 0 ? void 0 : cursorsBy.get(o)?.get(key);
|
|
1517
|
+
};
|
|
1518
|
+
const baselineOf = /* @__PURE__ */ new Map();
|
|
1519
|
+
const ownedKeys = /* @__PURE__ */ new Set();
|
|
1520
|
+
const watchAbort = new AbortController();
|
|
1521
|
+
let stopReason;
|
|
1522
|
+
let stopNow = () => {
|
|
1523
|
+
};
|
|
1524
|
+
const stopP = new Promise((resolve4) => {
|
|
1525
|
+
stopNow = (why) => {
|
|
1526
|
+
stopReason ??= why;
|
|
1527
|
+
resolve4();
|
|
1528
|
+
};
|
|
1529
|
+
});
|
|
1530
|
+
const onAbort = () => {
|
|
1531
|
+
watchAbort.abort();
|
|
1532
|
+
stopNow("abort");
|
|
1533
|
+
};
|
|
1534
|
+
if (opts.signal?.aborted) onAbort();
|
|
1535
|
+
else opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1536
|
+
const deadlineTimer = deadline === Number.POSITIVE_INFINITY ? void 0 : setTimeout(() => stopNow("deadline"), Math.max(0, deadline - Date.now()));
|
|
1537
|
+
const within = (p) => Promise.race([
|
|
1538
|
+
p.then((v) => ({ lost: false, v })),
|
|
1539
|
+
stopP.then(() => ({ lost: true }))
|
|
1540
|
+
]);
|
|
1541
|
+
let fatal;
|
|
1542
|
+
let wake = () => {
|
|
1543
|
+
};
|
|
1544
|
+
let woken = new Promise((resolve4) => {
|
|
1545
|
+
wake = resolve4;
|
|
1546
|
+
});
|
|
1547
|
+
const tracks = /* @__PURE__ */ new Map();
|
|
1548
|
+
const ownerReadings = /* @__PURE__ */ new Map();
|
|
1549
|
+
const openLocal = /* @__PURE__ */ new Set();
|
|
1550
|
+
const priorTask = /* @__PURE__ */ new Map();
|
|
1551
|
+
let firstRefresh = true;
|
|
1552
|
+
const delivered = [];
|
|
1553
|
+
const discoverSpawned = () => {
|
|
1554
|
+
if (!discover) return;
|
|
1555
|
+
const rows = db.prepare(
|
|
1556
|
+
`SELECT a.name AS agent, a.id AS agentId, a.spawner AS spawner, aws.name AS agentWs,
|
|
1557
|
+
t.local_id AS task, tws.name AS taskWs
|
|
1558
|
+
FROM agents a
|
|
1559
|
+
JOIN workstreams aws ON aws.id = a.workstream_id
|
|
1560
|
+
JOIN tasks t ON t.owner_id = a.id AND t.status = 'IN_PROGRESS' AND t.kind = 'work'
|
|
1561
|
+
JOIN workstreams tws ON tws.id = t.workstream_id AND tws.torn_down_at IS NULL
|
|
1562
|
+
WHERE a.spawner IN (${callers.map(() => "?").join(",")})
|
|
1563
|
+
ORDER BY a.id, t.id`
|
|
1564
|
+
).all(...callers);
|
|
1565
|
+
const bySpawner = /* @__PURE__ */ new Map();
|
|
1566
|
+
for (const r of rows) {
|
|
1567
|
+
if (!inScope(r.taskWs) && !inScope(r.agentWs)) continue;
|
|
1568
|
+
const list = bySpawner.get(r.spawner) ?? [];
|
|
1569
|
+
list.push(
|
|
1570
|
+
{
|
|
1571
|
+
kind: "agent",
|
|
1572
|
+
workstream: r.agentWs,
|
|
1573
|
+
id: r.agent,
|
|
1574
|
+
baseline: { afterRuns: 0, agentId: r.agentId }
|
|
1575
|
+
},
|
|
1576
|
+
{ kind: "task", workstream: r.taskWs, id: r.task }
|
|
1577
|
+
);
|
|
1578
|
+
bySpawner.set(r.spawner, list);
|
|
1579
|
+
}
|
|
1580
|
+
for (const [spawner, refs] of bySpawner) {
|
|
1581
|
+
const reg = readWaitRegistration(db, spawner);
|
|
1582
|
+
const have = new Set((reg?.refs ?? []).map((r) => refKey(r)));
|
|
1583
|
+
const fresh = refs.filter((r) => !have.has(refKey(r)));
|
|
1584
|
+
if (fresh.length === 0) continue;
|
|
1585
|
+
try {
|
|
1586
|
+
registerOwnedWait(db, spawner, fresh);
|
|
1587
|
+
} catch {
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
const ownedRefs = () => {
|
|
1592
|
+
if (!consumes) return [];
|
|
1593
|
+
const out = /* @__PURE__ */ new Map();
|
|
1594
|
+
ownedKeys.clear();
|
|
1595
|
+
for (const c of callers) {
|
|
1596
|
+
const reg = readWaitRegistration(db, c);
|
|
1597
|
+
if (reg === void 0) continue;
|
|
1598
|
+
const done = reg.refs.filter(
|
|
1599
|
+
(r) => r.kind === "agent" && !agentExists(db, r) && hasDoneMark(db, c, r)
|
|
1600
|
+
);
|
|
1601
|
+
if (done.length > 0) dropOwnedRefs(db, c, done);
|
|
1602
|
+
for (const r of reg.refs) {
|
|
1603
|
+
if (done.includes(r)) continue;
|
|
1604
|
+
if (r.kind !== "delegate" && !inScope(r.workstream)) continue;
|
|
1605
|
+
const key = refKey(r);
|
|
1606
|
+
ownedKeys.add(key);
|
|
1607
|
+
if (!out.has(key)) {
|
|
1608
|
+
out.set(key, { ...r });
|
|
1609
|
+
ownerOfKey.set(key, c);
|
|
1610
|
+
}
|
|
1611
|
+
const b = reg.baselines[key];
|
|
1612
|
+
if (b !== void 0) {
|
|
1613
|
+
const old = baselineOf.get(key);
|
|
1614
|
+
if (old === void 0 || old.incarnation === b.incarnation && b.afterRuns < old.afterRuns) {
|
|
1615
|
+
baselineOf.set(key, b);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
return [...out.values()];
|
|
1621
|
+
};
|
|
1622
|
+
const track = (ref, isExplicit) => {
|
|
1623
|
+
const key = refKey(ref);
|
|
1624
|
+
let t = tracks.get(key);
|
|
1625
|
+
if (t === void 0) {
|
|
1626
|
+
t = {
|
|
1627
|
+
ref,
|
|
1628
|
+
explicit: isExplicit,
|
|
1629
|
+
wasBusy: false,
|
|
1630
|
+
baseline: ref.afterRuns,
|
|
1631
|
+
handle: void 0,
|
|
1632
|
+
needWatch: opts.watchAgent !== void 0,
|
|
1633
|
+
result: void 0,
|
|
1634
|
+
reading: void 0,
|
|
1635
|
+
latched: void 0,
|
|
1636
|
+
close: void 0,
|
|
1637
|
+
muted: false,
|
|
1638
|
+
unknownBaseline: false
|
|
1639
|
+
};
|
|
1640
|
+
tracks.set(key, t);
|
|
1641
|
+
} else if (isExplicit) t.explicit = true;
|
|
1642
|
+
return t;
|
|
1643
|
+
};
|
|
1644
|
+
const setupWatches = async () => {
|
|
1645
|
+
const watch = opts.watchAgent;
|
|
1646
|
+
const want = [...tracks.values()].filter((t) => t.needWatch && t.latched === void 0);
|
|
1647
|
+
if (watch === void 0 || want.length === 0) return;
|
|
1648
|
+
for (const t of want) t.needWatch = false;
|
|
1649
|
+
const got = await within(
|
|
1650
|
+
Promise.all(
|
|
1651
|
+
want.map((t) => {
|
|
1652
|
+
const key = refKey(t.ref);
|
|
1653
|
+
const cur = cursorOf(key);
|
|
1654
|
+
const base = baselineOf.get(key);
|
|
1655
|
+
return watch(
|
|
1656
|
+
{ ...t.ref, ...t.baseline !== void 0 ? { afterRuns: t.baseline } : {} },
|
|
1657
|
+
watchAbort.signal,
|
|
1658
|
+
{
|
|
1659
|
+
owed: ownedKeys.has(key),
|
|
1660
|
+
...cur !== void 0 ? { cursor: cur } : {},
|
|
1661
|
+
...base !== void 0 ? { baseline: base } : {}
|
|
1662
|
+
}
|
|
1663
|
+
);
|
|
1664
|
+
})
|
|
1665
|
+
)
|
|
1666
|
+
);
|
|
1667
|
+
if (got.lost) return;
|
|
1668
|
+
got.v.forEach((h, i) => {
|
|
1669
|
+
const t = want[i];
|
|
1670
|
+
if (t === void 0 || h === void 0) return;
|
|
1671
|
+
t.handle = h;
|
|
1672
|
+
t.reading = h.initial;
|
|
1673
|
+
if (t.baseline === void 0 && h.afterRuns !== void 0) t.baseline = h.afterRuns;
|
|
1674
|
+
if (h.unknownBaseline !== void 0) {
|
|
1675
|
+
t.unknownBaseline = true;
|
|
1676
|
+
const key = refKey(t.ref);
|
|
1677
|
+
const owner = ownerOf(key);
|
|
1678
|
+
if (owner !== void 0 && consumes)
|
|
1679
|
+
fenceAgentRef(db, owner, registered(t.ref), h.unknownBaseline);
|
|
1680
|
+
}
|
|
1681
|
+
if (h.initial.status === "busy") t.wasBusy = true;
|
|
1682
|
+
void h.settled.then(
|
|
1683
|
+
(r) => {
|
|
1684
|
+
if (t.handle === h) t.result = r;
|
|
1685
|
+
wake();
|
|
1686
|
+
},
|
|
1687
|
+
(error) => {
|
|
1688
|
+
fatal ??= { error };
|
|
1689
|
+
wake();
|
|
1690
|
+
}
|
|
1691
|
+
);
|
|
1692
|
+
});
|
|
1693
|
+
};
|
|
1694
|
+
const settle = (t, r) => {
|
|
1695
|
+
const consumed = !t.explicit && r.runs !== void 0 && isConsumed(cursorOf(refKey(t.ref)), { incarnation: r.incarnation ?? "", runs: r.runs });
|
|
1696
|
+
const close = consumed ? void 0 : closeSettledRun(db, t.ref, r, applyClose);
|
|
1697
|
+
const textOk = matcher === void 0 || r.lastError === void 0 && (r.lastText ?? "").trim() !== "" && matcher.test(r.lastText ?? "");
|
|
1698
|
+
if (!textOk || consumed) {
|
|
1699
|
+
if (r.runs !== void 0) t.baseline = r.runs;
|
|
1700
|
+
t.wasBusy = false;
|
|
1701
|
+
t.handle = void 0;
|
|
1702
|
+
t.result = void 0;
|
|
1703
|
+
t.needWatch = opts.watchAgent !== void 0;
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
t.latched = { kind: "settled", reading: r };
|
|
1707
|
+
if (close !== void 0) t.close = close;
|
|
1708
|
+
};
|
|
1709
|
+
const rearm = (t, baseline) => rearmLatch(t, baseline, opts.watchAgent !== void 0);
|
|
1710
|
+
const advance = (t, r, fromWatch) => {
|
|
1711
|
+
if (t.latched !== void 0) return;
|
|
1712
|
+
const pinned = t.ref.incarnation;
|
|
1713
|
+
if (pinned !== void 0 && r.incarnation !== void 0 && r.incarnation !== pinned) {
|
|
1714
|
+
throw new AgentRestartedError(t.ref.id, t.ref.workstream, pinned, r.incarnation);
|
|
1715
|
+
}
|
|
1716
|
+
if (r.blocked === void 0 && t.muted) {
|
|
1717
|
+
t.muted = false;
|
|
1718
|
+
if (opts.watchAgent !== void 0 && r.status !== null) {
|
|
1719
|
+
t.needWatch = true;
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
if (r.exit !== void 0) {
|
|
1724
|
+
t.latched = { kind: "exited", reading: r };
|
|
1725
|
+
} else if (r.status === null) {
|
|
1726
|
+
t.latched = { kind: "dead", reading: r };
|
|
1727
|
+
} else if (r.blocked !== void 0) {
|
|
1728
|
+
if (!t.explicit && isConsumed(cursorOf(urgentKey("input", refKey(t.ref))), inputToken(r))) {
|
|
1729
|
+
t.muted = true;
|
|
1730
|
+
t.wasBusy = true;
|
|
1731
|
+
t.handle = void 0;
|
|
1732
|
+
t.result = void 0;
|
|
1733
|
+
t.needWatch = false;
|
|
1734
|
+
} else t.latched = { kind: "input", reading: r };
|
|
1735
|
+
} else if (!fromWatch && matcher !== void 0 && r.textSource === "pane" && r.lastText !== void 0 && matcher.test(r.lastText)) {
|
|
1736
|
+
t.latched = { kind: "pane-text", reading: r };
|
|
1737
|
+
} else if (fromWatch) {
|
|
1738
|
+
if (r.status === "unknown") t.handle = void 0;
|
|
1739
|
+
else settle(t, r);
|
|
1740
|
+
} else if (r.status === "busy") {
|
|
1741
|
+
t.wasBusy = true;
|
|
1742
|
+
} else if (r.status !== "unknown" && t.wasBusy) {
|
|
1743
|
+
settle(t, r);
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
const ownerOfAgent = (t) => ({
|
|
1747
|
+
name: t.ref.id,
|
|
1748
|
+
workstreamName: t.ref.workstream
|
|
1749
|
+
});
|
|
1750
|
+
const markOwed = () => {
|
|
1751
|
+
if (!consumes) return;
|
|
1752
|
+
for (const t of tracks.values()) {
|
|
1753
|
+
const key = refKey(t.ref);
|
|
1754
|
+
const caller = ownerOfKey.get(key);
|
|
1755
|
+
const r = t.latched === void 0 ? ownerReadings.get(agentKey(ownerOfAgent(t))) : void 0;
|
|
1756
|
+
if (caller === void 0 || !ownedKeys.has(key) || r === void 0) continue;
|
|
1757
|
+
const id = agentRowId(db, t.ref);
|
|
1758
|
+
if (id === 0) continue;
|
|
1759
|
+
const owed = agentOwed(db, id, r, baselineOf.get(key), t.unknownBaseline, t.wasBusy);
|
|
1760
|
+
if (owed !== "unsure")
|
|
1761
|
+
setAgentDone(db, t.ref.workstream, t.ref.id, owed === "done", [caller]);
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
const refresh = async () => {
|
|
1765
|
+
if (opts.beforePoll) {
|
|
1766
|
+
const b = await within(opts.beforePoll());
|
|
1767
|
+
if (b.lost) return;
|
|
1768
|
+
}
|
|
1769
|
+
if (opts.sync !== false) await ambientIngest(db, { quiet: true });
|
|
1770
|
+
discoverSpawned();
|
|
1771
|
+
const owned = ownedRefs();
|
|
1772
|
+
for (const r of explicit.values()) if (r.kind === "agent") track(r, true);
|
|
1773
|
+
for (const r of owned) if (r.kind === "agent") track(r, explicit.has(refKey(r)));
|
|
1774
|
+
await setupWatches();
|
|
1775
|
+
if (stopReason !== void 0) return;
|
|
1776
|
+
const reads = /* @__PURE__ */ new Map();
|
|
1777
|
+
for (const t of tracks.values()) {
|
|
1778
|
+
if (t.latched === void 0 && t.handle === void 0 || t.latched?.kind === "settled") {
|
|
1779
|
+
reads.set(refKey(t.ref), t.ref);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
if (consumes && opts.readAgents !== void 0) {
|
|
1783
|
+
for (const r of owned) {
|
|
1784
|
+
const t = r.kind === "agent" ? tracks.get(refKey(r)) : void 0;
|
|
1785
|
+
if (t !== void 0 && t.latched === void 0) reads.set(refKey(r), r);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
if (observeOwners) {
|
|
1789
|
+
const taskRefs = [...explicit.values(), ...owned].filter((r) => r.kind === "task");
|
|
1790
|
+
for (const r of taskRefs) {
|
|
1791
|
+
const row = getTask(db, r.id, r.workstream);
|
|
1792
|
+
const o = row?.status === "IN_PROGRESS" ? getTaskOwner(db, r.id, r.workstream) : void 0;
|
|
1793
|
+
if (o !== void 0) {
|
|
1794
|
+
const ref = { kind: "agent", workstream: o.workstreamName, id: o.name };
|
|
1795
|
+
reads.set(refKey(ref), ref);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
for (const t of tracks.values()) {
|
|
1800
|
+
const r = t.result;
|
|
1801
|
+
if (r === void 0 || t.handle === void 0) continue;
|
|
1802
|
+
t.result = void 0;
|
|
1803
|
+
t.reading = r;
|
|
1804
|
+
advance(t, r, true);
|
|
1805
|
+
}
|
|
1806
|
+
if (reads.size > 0) {
|
|
1807
|
+
if (opts.readAgents === void 0) throw new Error("wait: an agent reader is required");
|
|
1808
|
+
const got = await within(opts.readAgents([...reads.values()], watchAbort.signal));
|
|
1809
|
+
if (got.lost) return;
|
|
1810
|
+
ownerReadings.clear();
|
|
1811
|
+
for (const ref of reads.values()) {
|
|
1812
|
+
const key = agentKey({ name: ref.id, workstreamName: ref.workstream });
|
|
1813
|
+
let r = got.v.get(key);
|
|
1814
|
+
if (r === void 0) {
|
|
1815
|
+
r = watchDeaths && !agentExists(db, ref) ? { status: null, reason: "agent is no longer registered" } : { status: "unknown", reason: "state reader returned no row" };
|
|
1816
|
+
}
|
|
1817
|
+
ownerReadings.set(key, r);
|
|
1818
|
+
const t = tracks.get(refKey(ref));
|
|
1819
|
+
const held = t === void 0 ? void 0 : supersededBy(t, r);
|
|
1820
|
+
if (t !== void 0 && held !== void 0) {
|
|
1821
|
+
rearm(t, held);
|
|
1822
|
+
if (t.needWatch) continue;
|
|
1823
|
+
t.wasBusy = true;
|
|
1824
|
+
}
|
|
1825
|
+
if (t !== void 0 && t.latched === void 0 && t.handle === void 0) {
|
|
1826
|
+
t.reading = r;
|
|
1827
|
+
advance(t, r, false);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
markOwed();
|
|
1832
|
+
if (firstRefresh) {
|
|
1833
|
+
firstRefresh = false;
|
|
1834
|
+
const watched = [...tracks.values()].filter((t) => t.explicit);
|
|
1835
|
+
if (watched.length > 0 && watched.every((t) => (t.latched?.reading ?? t.reading)?.status === "unknown")) {
|
|
1836
|
+
opts.onInitialUnknown?.(watched.map((t) => agentSnapshot(t)));
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
const ctlBrokenSince = /* @__PURE__ */ new Map();
|
|
1841
|
+
const stallEmitted = /* @__PURE__ */ new Set();
|
|
1842
|
+
const stallStates = /* @__PURE__ */ new Map();
|
|
1843
|
+
const stallAgeMs = (ownerKey, r) => {
|
|
1844
|
+
if (r === void 0 || r.status === null) return null;
|
|
1845
|
+
const alive = true;
|
|
1846
|
+
if (alive && (r.ctl === "missing" || r.ctl === "refused")) {
|
|
1847
|
+
const first = ctlBrokenSince.get(ownerKey) ?? Date.now();
|
|
1848
|
+
ctlBrokenSince.set(ownerKey, first);
|
|
1849
|
+
const age2 = Date.now() - first;
|
|
1850
|
+
return age2 >= stuckAfter(false) ? age2 : null;
|
|
1851
|
+
}
|
|
1852
|
+
ctlBrokenSince.delete(ownerKey);
|
|
1853
|
+
if (r.status !== "needs_input" || r.since === void 0 || r.since === null) return null;
|
|
1854
|
+
const viaCtl = r.source === "ctl" && r.ctl === "ok";
|
|
1855
|
+
if (viaCtl && r.pending === true) return null;
|
|
1856
|
+
const age = Date.now() - r.since;
|
|
1857
|
+
return age >= stuckAfter(viaCtl) ? age : null;
|
|
1858
|
+
};
|
|
1859
|
+
const markStall = (item, stall) => {
|
|
1860
|
+
const r = stall.owner;
|
|
1861
|
+
item.snap.stuck = true;
|
|
1862
|
+
item.snap.stuckWhy = r.ctl === "missing" || r.ctl === "refused" ? r.reason ?? `ctl ${r.ctl}` : r.status ?? "needs_input";
|
|
1863
|
+
item.snap.stuckSecs = stall.ageSecs;
|
|
1864
|
+
if (r.runs !== void 0) item.snap.ownerRuns = r.runs;
|
|
1865
|
+
item.kind = "stall";
|
|
1866
|
+
item.ckey = urgentKey("stall", item.key);
|
|
1867
|
+
item.token = { incarnation: `stall:${stall.token}`, runs: 1 };
|
|
1868
|
+
item.stall = stall;
|
|
1869
|
+
item.snap.owner ??= stall.ownerRef.name;
|
|
1870
|
+
item.snap.ownerWorkstream ??= stall.ownerRef.workstreamName;
|
|
1871
|
+
if (stall.close !== void 0) item.snap.close = stall.close;
|
|
1872
|
+
};
|
|
1873
|
+
const taskItem = (ref, want, isTarget, suppressible, order) => {
|
|
1874
|
+
const row = getTask(db, ref.id, ref.workstream);
|
|
1875
|
+
const status = row?.status ?? "OPEN";
|
|
1876
|
+
const ownerRef = row === void 0 ? void 0 : getTaskOwner(db, ref.id, ref.workstream);
|
|
1877
|
+
const ownerName = ownerRef?.name ?? row?.ownerName ?? null;
|
|
1878
|
+
const snap = {
|
|
1879
|
+
ref,
|
|
1880
|
+
status,
|
|
1881
|
+
substate: row?.substate ?? DEFAULT_SUBSTATE.OPEN,
|
|
1882
|
+
owner: ownerName,
|
|
1883
|
+
...ownerRef !== void 0 ? { ownerWorkstream: ownerRef.workstreamName } : {},
|
|
1884
|
+
fired: false,
|
|
1885
|
+
dead: false
|
|
1886
|
+
};
|
|
1887
|
+
const item = {
|
|
1888
|
+
key: refKey(ref),
|
|
1889
|
+
ref,
|
|
1890
|
+
snap,
|
|
1891
|
+
target: isTarget,
|
|
1892
|
+
suppressible,
|
|
1893
|
+
order,
|
|
1894
|
+
done: false,
|
|
1895
|
+
ready: false
|
|
1896
|
+
};
|
|
1897
|
+
if (row === void 0) return item;
|
|
1898
|
+
if (watchDeaths && want === "CLOSED" && status !== "CLOSED") {
|
|
1899
|
+
const prior = priorTask.get(item.key);
|
|
1900
|
+
const ownerKey = ownerRef === void 0 ? void 0 : agentKey(ownerRef);
|
|
1901
|
+
const flip = status === "OPEN" ? reapFlip(db, ref, ownedKeys.has(item.key) ? 0 : sinceSeq) : void 0;
|
|
1902
|
+
const dead = ownerKey !== void 0 && ownerReadings.get(ownerKey)?.status === null || // The reaper flip seen live: IN_PROGRESS -> OPEN while the owner's row (that very row) is
|
|
1903
|
+
// gone. A release leaves the owner registered; a replaced agent is another row.
|
|
1904
|
+
prior?.status === "IN_PROGRESS" && status === "OPEN" && prior.owner !== null && prior.ownerId !== 0 && agentRowId(db, { workstream: prior.ownerWs, id: prior.owner }) !== prior.ownerId || flip !== void 0;
|
|
1905
|
+
if (dead) {
|
|
1906
|
+
snap.dead = true;
|
|
1907
|
+
item.kind = "dead";
|
|
1908
|
+
item.done = true;
|
|
1909
|
+
item.ckey = urgentKey("dead", item.key);
|
|
1910
|
+
const rowId = ownerRef !== void 0 ? taskOwnerRowId(db, ref) : prior?.ownerId || flip?.ownerId || 0;
|
|
1911
|
+
item.token = { incarnation: `dead:${rowId}`, runs: 1 };
|
|
1912
|
+
const name = ownerRef?.name ?? prior?.owner ?? flip?.ownerName;
|
|
1913
|
+
const workstream = ownerRef?.workstreamName ?? (prior?.owner != null ? prior.ownerWs : void 0);
|
|
1914
|
+
item.death = {
|
|
1915
|
+
rowId,
|
|
1916
|
+
...name != null ? { name } : {},
|
|
1917
|
+
...workstream !== void 0 ? { workstream } : {}
|
|
1918
|
+
};
|
|
1919
|
+
return item;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
if (stuckEnabled && status === "IN_PROGRESS" && row.kind === "work" && ownerRef !== void 0 && want === "CLOSED") {
|
|
1923
|
+
const ownerKey = agentKey(ownerRef);
|
|
1924
|
+
const r = ownerReadings.get(ownerKey);
|
|
1925
|
+
const watched = tracks.get(
|
|
1926
|
+
refKey({ kind: "agent", workstream: ownerRef.workstreamName, id: ownerRef.name })
|
|
1927
|
+
);
|
|
1928
|
+
const settleOwed = watched !== void 0 && // the settle is in hand but not delivered yet: under --first it goes first, the stall
|
|
1929
|
+
// (if the task is still open after its CLOSE block) is the NEXT wait's news. Under --all
|
|
1930
|
+
// a settle is no target and is never delivered before the end: its CLOSE is already
|
|
1931
|
+
// applied (settle()), so a task it left open (a malformed block) is a stall NOW.
|
|
1932
|
+
(!all && watched.latched?.kind === "settled" || watched.latched === void 0 && (watched.handle !== void 0 || watched.needWatch) && r?.runs !== void 0 && (watched.baseline ?? 0) < r.runs);
|
|
1933
|
+
const age = settleOwed ? null : stallAgeMs(ownerKey, r);
|
|
1934
|
+
if (age !== null && r !== void 0) {
|
|
1935
|
+
const token = [
|
|
1936
|
+
row.workstreamId,
|
|
1937
|
+
row.name,
|
|
1938
|
+
ownerKey,
|
|
1939
|
+
r.incarnation ?? "",
|
|
1940
|
+
r.runs ?? "",
|
|
1941
|
+
r.since ?? ""
|
|
1942
|
+
].join("|");
|
|
1943
|
+
markStall(item, {
|
|
1944
|
+
token,
|
|
1945
|
+
owner: r,
|
|
1946
|
+
ownerRef,
|
|
1947
|
+
ageSecs: Math.round(age / 1e3),
|
|
1948
|
+
// The owner's settled run was refused its CLOSE (a malformed block): say why on the task.
|
|
1949
|
+
...watched?.close?.applied === false ? { close: watched.close } : {}
|
|
1950
|
+
});
|
|
1951
|
+
if (!stallEmitted.has(token)) {
|
|
1952
|
+
stallEmitted.add(token);
|
|
1953
|
+
const identity = {
|
|
1954
|
+
agentId: r.agentId ?? taskOwnerRowId(db, ref),
|
|
1955
|
+
incarnation: `${r.incarnation ?? ""}${r.runs === void 0 ? `|since:${r.since ?? ""}` : ""}`,
|
|
1956
|
+
runs: r.runs ?? 0
|
|
1957
|
+
};
|
|
1958
|
+
logStallOnce(
|
|
1959
|
+
db,
|
|
1960
|
+
ref,
|
|
1961
|
+
identity,
|
|
1962
|
+
() => emitEvent(
|
|
1963
|
+
db,
|
|
1964
|
+
ref.workstream,
|
|
1965
|
+
"agent.stall",
|
|
1966
|
+
`agent stalled ${ownerRef.name} owns ${ref.id} for ${Math.round(age / 1e3)}s`,
|
|
1967
|
+
ownerRef.name
|
|
1968
|
+
)
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
return item;
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
if (status !== want) return item;
|
|
1975
|
+
if (matcher !== void 0) {
|
|
1976
|
+
const rep = db.prepare(
|
|
1977
|
+
`SELECT n.content FROM task_notes n JOIN tasks t ON t.id = n.task_id
|
|
1978
|
+
JOIN workstreams w ON w.id = t.workstream_id
|
|
1979
|
+
WHERE w.name = ? AND t.local_id = ? AND n.kind = 'report' ORDER BY n.id DESC LIMIT 1`
|
|
1980
|
+
).get(ref.workstream, ref.id);
|
|
1981
|
+
if (row.substate !== "done" || rep === void 0 || !matcher.test(rep.content)) return item;
|
|
1982
|
+
}
|
|
1983
|
+
const note = db.prepare(
|
|
1984
|
+
`SELECT COALESCE(MAX(n.id), 0) AS n FROM task_notes n JOIN tasks t ON t.id = n.task_id
|
|
1985
|
+
JOIN workstreams w ON w.id = t.workstream_id WHERE w.name = ? AND t.local_id = ?`
|
|
1986
|
+
).get(ref.workstream, ref.id);
|
|
1987
|
+
snap.fired = true;
|
|
1988
|
+
item.kind = "closed";
|
|
1989
|
+
item.done = true;
|
|
1990
|
+
item.token = closedToken(note.n);
|
|
1991
|
+
return item;
|
|
1992
|
+
};
|
|
1993
|
+
const agentItem = (t, isTarget, order) => {
|
|
1994
|
+
const l = t.latched;
|
|
1995
|
+
const r = l?.reading;
|
|
1996
|
+
const key = refKey(t.ref);
|
|
1997
|
+
let token;
|
|
1998
|
+
let ckey;
|
|
1999
|
+
let death;
|
|
2000
|
+
if (l?.kind === "settled" && r?.runs !== void 0) {
|
|
2001
|
+
token = { incarnation: r.incarnation ?? "", runs: r.runs };
|
|
2002
|
+
} else if (l?.kind === "input" && r !== void 0) {
|
|
2003
|
+
ckey = urgentKey("input", key);
|
|
2004
|
+
token = inputToken(r);
|
|
2005
|
+
} else if (l?.kind === "dead") {
|
|
2006
|
+
ckey = urgentKey("dead", key);
|
|
2007
|
+
const rowId = agentRowId(db, t.ref);
|
|
2008
|
+
token = { incarnation: `dead:${rowId}`, runs: 1 };
|
|
2009
|
+
death = { rowId: rowId || (r?.agentId ?? 0), name: t.ref.id, workstream: t.ref.workstream };
|
|
2010
|
+
} else if (l?.kind === "exited") {
|
|
2011
|
+
token = { incarnation: `exited:${agentRowId(db, t.ref)}`, runs: 1 };
|
|
2012
|
+
}
|
|
2013
|
+
return {
|
|
2014
|
+
key,
|
|
2015
|
+
ref: t.ref,
|
|
2016
|
+
snap: agentSnapshot(t),
|
|
2017
|
+
target: isTarget,
|
|
2018
|
+
suppressible: !t.explicit,
|
|
2019
|
+
order,
|
|
2020
|
+
done: l?.kind === "settled" || l?.kind === "pane-text" || l?.kind === "dead" || l?.kind === "exited",
|
|
2021
|
+
ready: false,
|
|
2022
|
+
...l !== void 0 ? { kind: l.kind } : {},
|
|
2023
|
+
...token !== void 0 ? { token } : {},
|
|
2024
|
+
...ckey !== void 0 ? { ckey } : {},
|
|
2025
|
+
...death !== void 0 ? { death } : {}
|
|
2026
|
+
};
|
|
2027
|
+
};
|
|
2028
|
+
const OPEN_TOKEN = { incarnation: "open", runs: 1 };
|
|
2029
|
+
const markOpen = (okey) => {
|
|
2030
|
+
if (cursorOf(okey) !== void 0) return;
|
|
2031
|
+
const owner = ownerOf(okey);
|
|
2032
|
+
if (owner === void 0) return;
|
|
2033
|
+
recordCursors(db, owner, [{ key: okey, token: OPEN_TOKEN }]);
|
|
2034
|
+
cursorsBy.get(owner)?.set(okey, OPEN_TOKEN);
|
|
2035
|
+
};
|
|
2036
|
+
let tally;
|
|
2037
|
+
const evaluate = () => {
|
|
2038
|
+
const items = [];
|
|
2039
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2040
|
+
let order = 0;
|
|
2041
|
+
const push = (it) => {
|
|
2042
|
+
const prev = byKey.get(it.key);
|
|
2043
|
+
if (prev !== void 0) {
|
|
2044
|
+
prev.target ||= it.target;
|
|
2045
|
+
prev.suppressible &&= it.suppressible;
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
byKey.set(it.key, it);
|
|
2049
|
+
items.push(it);
|
|
2050
|
+
};
|
|
2051
|
+
const owned = ownedRefs();
|
|
2052
|
+
const calls = /* @__PURE__ */ new Map();
|
|
2053
|
+
const addCall = (c, isTarget, suppressible) => {
|
|
2054
|
+
if (c === void 0 || c.state === "refused") return;
|
|
2055
|
+
const prev = calls.get(c.callId);
|
|
2056
|
+
calls.set(c.callId, {
|
|
2057
|
+
call: c,
|
|
2058
|
+
target: (prev?.target ?? false) || isTarget,
|
|
2059
|
+
suppressible: (prev?.suppressible ?? true) && suppressible
|
|
2060
|
+
});
|
|
2061
|
+
};
|
|
2062
|
+
for (const r of input) {
|
|
2063
|
+
if (r.kind === "task") push(taskItem(r, target, true, false, order++));
|
|
2064
|
+
else if (r.kind === "agent") push(agentItem(track(r, true), true, order++));
|
|
2065
|
+
else addCall(getDelegateCall(db, r.id), true, false);
|
|
2066
|
+
}
|
|
2067
|
+
if (opts.wave !== void 0) {
|
|
2068
|
+
const waveCalls = listDelegatesOf(db, callers, opts.wave.workstream, {
|
|
2069
|
+
wave: opts.wave.name
|
|
2070
|
+
});
|
|
2071
|
+
for (const c of waveCalls) addCall(c, true, !all);
|
|
2072
|
+
tally = tallyCalls(waveCalls);
|
|
2073
|
+
}
|
|
2074
|
+
for (const r of owned) {
|
|
2075
|
+
if (r.kind !== "delegate") continue;
|
|
2076
|
+
const c = getDelegateCall(db, r.id);
|
|
2077
|
+
if (c !== void 0 && callInScope(c)) addCall(c, false, true);
|
|
2078
|
+
}
|
|
2079
|
+
if (consumes) {
|
|
2080
|
+
for (const c of listDelegatesOf(db, callers)) {
|
|
2081
|
+
if (!callInScope(c)) continue;
|
|
2082
|
+
addCall(c, false, true);
|
|
2083
|
+
ownerOfKey.set(
|
|
2084
|
+
refKey({ kind: "delegate", workstream: c.workstream, id: c.callId }),
|
|
2085
|
+
c.caller
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
for (const { call, target: isTarget, suppressible } of calls.values()) {
|
|
2090
|
+
push(delegateItem(call, isTarget, suppressible, order++));
|
|
2091
|
+
}
|
|
2092
|
+
if (opts.blockersOf !== void 0 && umbrella !== void 0 && umbrella !== null) {
|
|
2093
|
+
const rows = openBlockers(db, umbrella);
|
|
2094
|
+
let open = false;
|
|
2095
|
+
let readyBlocker = false;
|
|
2096
|
+
for (const b of rows) {
|
|
2097
|
+
const ref = { kind: "task", workstream: b.ws, id: b.id };
|
|
2098
|
+
const it = taskItem(ref, "CLOSED", true, !all, order++);
|
|
2099
|
+
const okey = urgentKey("open", it.key);
|
|
2100
|
+
if (!it.done) {
|
|
2101
|
+
open = true;
|
|
2102
|
+
if (consumes) markOpen(okey);
|
|
2103
|
+
else openLocal.add(it.key);
|
|
2104
|
+
} else if (!all && !(consumes ? cursorOf(okey) !== void 0 : openLocal.has(it.key))) {
|
|
2105
|
+
it.suppressible = true;
|
|
2106
|
+
it.token = void 0;
|
|
2107
|
+
it.kind = void 0;
|
|
2108
|
+
} else if (it.token === void 0 || !isConsumed(cursorOf(it.key), it.token)) {
|
|
2109
|
+
readyBlocker = true;
|
|
2110
|
+
}
|
|
2111
|
+
push(it);
|
|
2112
|
+
}
|
|
2113
|
+
if (!open && !readyBlocker) {
|
|
2114
|
+
const ref = { kind: "task", ...opts.blockersOf };
|
|
2115
|
+
push({
|
|
2116
|
+
key: `blockers:${refKey(ref)}`,
|
|
2117
|
+
ref,
|
|
2118
|
+
snap: { ref, status: "CLOSED", fired: true, dead: false },
|
|
2119
|
+
target: true,
|
|
2120
|
+
suppressible: false,
|
|
2121
|
+
order: order++,
|
|
2122
|
+
kind: "closed",
|
|
2123
|
+
done: true,
|
|
2124
|
+
ready: false,
|
|
2125
|
+
// A state ("nothing is left to wait on"), never news: behind every real event.
|
|
2126
|
+
seen: true
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
for (const r of owned) {
|
|
2131
|
+
if (r.kind === "task") push(taskItem(r, target, false, true, order++));
|
|
2132
|
+
}
|
|
2133
|
+
for (const r of owned) {
|
|
2134
|
+
if (r.kind === "agent") push(agentItem(track(r, false), false, order++));
|
|
2135
|
+
}
|
|
2136
|
+
for (const it of items) {
|
|
2137
|
+
if (it.ref.kind !== "task") continue;
|
|
2138
|
+
const owner = it.snap.owner ?? null;
|
|
2139
|
+
const before = priorTask.get(it.key);
|
|
2140
|
+
const keep = owner === null && it.snap.status === "OPEN" ? before : void 0;
|
|
2141
|
+
priorTask.set(it.key, {
|
|
2142
|
+
status: it.snap.status ?? "OPEN",
|
|
2143
|
+
owner: owner ?? keep?.owner ?? null,
|
|
2144
|
+
ownerWs: it.snap.ownerWorkstream ?? keep?.ownerWs ?? it.ref.workstream,
|
|
2145
|
+
ownerId: owner === null ? keep?.ownerId ?? 0 : taskOwnerRowId(db, it.ref)
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
const shown = [];
|
|
2149
|
+
for (const it of items) {
|
|
2150
|
+
const consumed = it.token !== void 0 && isConsumed(cursorOf(it.ckey ?? it.key), it.token);
|
|
2151
|
+
it.seen ||= consumed;
|
|
2152
|
+
const hidden = it.suppressible && consumed;
|
|
2153
|
+
it.hidden = hidden;
|
|
2154
|
+
it.ready = it.kind !== void 0 && !hidden;
|
|
2155
|
+
const urgent = it.kind === "input" || it.kind === "stall" || it.kind === "dead";
|
|
2156
|
+
if (hidden && urgent) it.done = false;
|
|
2157
|
+
if (hidden && !it.target && !urgent) continue;
|
|
2158
|
+
shown.push(it);
|
|
2159
|
+
}
|
|
2160
|
+
return shown;
|
|
2161
|
+
};
|
|
2162
|
+
const driveStalls = (items) => {
|
|
2163
|
+
const handler = opts.handleStall;
|
|
2164
|
+
for (const st of stallStates.values()) {
|
|
2165
|
+
if (st.job !== void 0 || st.result?.disposition !== "return" || st.carry === void 0) {
|
|
2166
|
+
continue;
|
|
2167
|
+
}
|
|
2168
|
+
const { key, stall } = st.carry;
|
|
2169
|
+
const gone = items.find((i) => i.key === key && i.kind !== "stall");
|
|
2170
|
+
if (gone !== void 0 && stall !== void 0) {
|
|
2171
|
+
markStall(gone, stall);
|
|
2172
|
+
gone.hidden = false;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
for (const it of items) {
|
|
2176
|
+
if (it.stall === void 0 || it.kind !== "stall" || it.hidden === true) continue;
|
|
2177
|
+
if (opts.onStall === "warn") {
|
|
2178
|
+
it.ready = false;
|
|
2179
|
+
continue;
|
|
2180
|
+
}
|
|
2181
|
+
const steering = opts.onStall === "steer" || opts.onStall === "release";
|
|
2182
|
+
const caller = ownerOf(it.key);
|
|
2183
|
+
if (!steering || handler === void 0 || caller === void 0) {
|
|
2184
|
+
it.ready = true;
|
|
2185
|
+
continue;
|
|
2186
|
+
}
|
|
2187
|
+
const { token, owner, ownerRef } = it.stall;
|
|
2188
|
+
let st = stallStates.get(token);
|
|
2189
|
+
if (st === void 0) {
|
|
2190
|
+
const own = db.prepare(
|
|
2191
|
+
`SELECT a.id, a.pane_id FROM agents a JOIN workstreams w ON w.id = a.workstream_id
|
|
2192
|
+
WHERE a.name = ? AND w.name = ?`
|
|
2193
|
+
).get(ownerRef.name, ownerRef.workstreamName);
|
|
2194
|
+
const taskId = taskIdFor(db, it.ref.id, it.ref.workstream);
|
|
2195
|
+
if (own === void 0 || taskId === null) {
|
|
2196
|
+
it.ready = true;
|
|
2197
|
+
continue;
|
|
2198
|
+
}
|
|
2199
|
+
const ctx = {
|
|
2200
|
+
caller,
|
|
2201
|
+
generation: begun.get(caller)?.generation ?? null,
|
|
2202
|
+
ref: it.ref,
|
|
2203
|
+
taskId,
|
|
2204
|
+
revision: taskRevision(db, it.ref.workstream, it.ref.id),
|
|
2205
|
+
owner: {
|
|
2206
|
+
workstream: ownerRef.workstreamName,
|
|
2207
|
+
name: ownerRef.name,
|
|
2208
|
+
agentId: owner.agentId ?? own.id,
|
|
2209
|
+
paneId: owner.paneId ?? own.pane_id,
|
|
2210
|
+
status: owner.status,
|
|
2211
|
+
source: owner.source ?? "none",
|
|
2212
|
+
since: owner.since ?? null,
|
|
2213
|
+
settled: false,
|
|
2214
|
+
...owner.ctl !== void 0 ? { ctl: owner.ctl } : {},
|
|
2215
|
+
...owner.incarnation !== void 0 ? { incarnation: owner.incarnation } : {},
|
|
2216
|
+
...owner.runs !== void 0 ? { runs: owner.runs } : {},
|
|
2217
|
+
...owner.pending !== void 0 ? { pending: owner.pending } : {},
|
|
2218
|
+
...owner.reason !== void 0 ? { reason: owner.reason } : {}
|
|
2219
|
+
},
|
|
2220
|
+
stallToken: token,
|
|
2221
|
+
deadlineMs: Number.isFinite(deadline) ? Math.max(0, deadline - Date.now()) : Number.MAX_SAFE_INTEGER,
|
|
2222
|
+
signal: watchAbort.signal
|
|
2223
|
+
};
|
|
2224
|
+
const fresh = {};
|
|
2225
|
+
const action = opts.onStall === "release" ? "release" : "steer";
|
|
2226
|
+
fresh.job = Promise.resolve().then(() => handler(ctx)).catch(
|
|
2227
|
+
(e) => ({
|
|
2228
|
+
disposition: "return",
|
|
2229
|
+
action,
|
|
2230
|
+
state: "unknown",
|
|
2231
|
+
reason: e instanceof Error ? e.message : String(e)
|
|
2232
|
+
})
|
|
2233
|
+
).then((res) => {
|
|
2234
|
+
fresh.result = res;
|
|
2235
|
+
fresh.job = void 0;
|
|
2236
|
+
const t = tracks.get(
|
|
2237
|
+
refKey({
|
|
2238
|
+
kind: "agent",
|
|
2239
|
+
...{ workstream: ownerRef.workstreamName, id: ownerRef.name }
|
|
2240
|
+
})
|
|
2241
|
+
);
|
|
2242
|
+
if (t !== void 0 && res.afterRuns !== void 0) {
|
|
2243
|
+
if (t.latched?.kind === "settled") rearm(t, res.afterRuns);
|
|
2244
|
+
else if (t.latched === void 0) {
|
|
2245
|
+
t.baseline = res.afterRuns;
|
|
2246
|
+
t.wasBusy = false;
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
wake();
|
|
2250
|
+
});
|
|
2251
|
+
fresh.carry = { key: it.key, stall: it.stall };
|
|
2252
|
+
st = fresh;
|
|
2253
|
+
stallStates.set(token, st);
|
|
2254
|
+
}
|
|
2255
|
+
it.ready = st.job === void 0 && st.result?.disposition === "return";
|
|
2256
|
+
}
|
|
2257
|
+
};
|
|
2258
|
+
const select2 = (items) => {
|
|
2259
|
+
const rank = (a, b) => (
|
|
2260
|
+
// Never-returned events first: an event that was already returned once (an
|
|
2261
|
+
// unchanged dialog, a dead pane, a stall) never outranks one that was not.
|
|
2262
|
+
Number(a.seen === true) - Number(b.seen === true) || urgency(a.kind) - urgency(b.kind) || // Under --all a failed gate is the firing event, so its code is the exit.
|
|
2263
|
+
Number(failedGate(b)) - Number(failedGate(a)) || Number(b.target) - Number(a.target) || a.order - b.order
|
|
2264
|
+
);
|
|
2265
|
+
const ready = items.filter((i) => i.ready).sort(rank);
|
|
2266
|
+
if (!all) {
|
|
2267
|
+
const best2 = ready[0];
|
|
2268
|
+
if (best2 === void 0) return void 0;
|
|
2269
|
+
const same = (a, b) => a.kind === "settled" && a.ref.kind === "agent" && a.snap.close?.applied === true && a.snap.close.task === b.ref.id && a.ref.workstream === b.ref.workstream && b.ref.kind === "task" && b.kind === "closed";
|
|
2270
|
+
const companions = ready.filter(
|
|
2271
|
+
(i) => i !== best2 && (same(i, best2) || same(best2, i) || sameDeath(i, best2))
|
|
2272
|
+
);
|
|
2273
|
+
return { event: best2, fired: [best2, ...companions] };
|
|
2274
|
+
}
|
|
2275
|
+
const urgent = ready.find(
|
|
2276
|
+
(i) => i.kind === "input" || i.kind === "stall" || !i.target && (i.kind === "dead" || i.kind === "delegate")
|
|
2277
|
+
);
|
|
2278
|
+
if (urgent !== void 0) {
|
|
2279
|
+
const companions = ready.filter((i) => i !== urgent && sameDeath(i, urgent));
|
|
2280
|
+
return { event: urgent, fired: [urgent, ...companions] };
|
|
2281
|
+
}
|
|
2282
|
+
const targets = items.some((i) => i.target) ? items.filter((i) => i.target) : items;
|
|
2283
|
+
if (targets.length === 0 || !targets.every((i) => i.done)) return void 0;
|
|
2284
|
+
const sorted = [...targets].sort(rank);
|
|
2285
|
+
const best = sorted.find((i) => i.ready) ?? sorted[0];
|
|
2286
|
+
return best === void 0 ? void 0 : { event: best, fired: targets.filter((i) => i.done) };
|
|
2287
|
+
};
|
|
2288
|
+
const build = (kind, items, chosen, drained = false) => {
|
|
2289
|
+
const e = chosen?.event;
|
|
2290
|
+
for (const f of chosen?.fired ?? []) delivered.push(f);
|
|
2291
|
+
return {
|
|
2292
|
+
event: {
|
|
2293
|
+
kind,
|
|
2294
|
+
ref: e?.ref ?? null,
|
|
2295
|
+
...e?.snap.incarnation !== void 0 ? { incarnation: e.snap.incarnation } : {},
|
|
2296
|
+
...e?.snap.runs !== void 0 ? { runs: e.snap.runs } : {}
|
|
2297
|
+
},
|
|
2298
|
+
refs: snapshotsOf(items, chosen?.fired ?? []),
|
|
2299
|
+
delivered: (chosen?.fired ?? []).filter(isCompletion).map((i) => i.ref),
|
|
2300
|
+
timedOut: kind === "heartbeat" && !drained,
|
|
2301
|
+
interrupted: kind === "interrupted",
|
|
2302
|
+
exit: exitFor(kind) !== 0 ? exitFor(kind) : (chosen?.fired ?? []).find(failedGate)?.snap.exit ?? 0,
|
|
2303
|
+
cursor: {},
|
|
2304
|
+
...e?.stall !== void 0 && stallStates.get(e.stall.token)?.result !== void 0 ? { action: stallStates.get(e.stall.token)?.result } : {},
|
|
2305
|
+
humanNeeds: callers.flatMap((c) => openHumanNeeds(db, c)),
|
|
2306
|
+
sinceSeq,
|
|
2307
|
+
...tally !== void 0 ? { tally } : {},
|
|
2308
|
+
...drained ? { drained: true } : {}
|
|
2309
|
+
};
|
|
2310
|
+
};
|
|
2311
|
+
const rethrow = () => {
|
|
2312
|
+
if (fatal === void 0) return;
|
|
2313
|
+
watchAbort.abort();
|
|
2314
|
+
throw fatal.error;
|
|
2315
|
+
};
|
|
2316
|
+
let committed = false;
|
|
2317
|
+
let applied = true;
|
|
2318
|
+
const commit = (event) => {
|
|
2319
|
+
if (!consumes) return true;
|
|
2320
|
+
const ok = db.transaction(() => {
|
|
2321
|
+
for (const c of callers) cursorsBy.set(c, readCursors(db, c));
|
|
2322
|
+
if (
|
|
2323
|
+
// Only news this wait saw as unconsumed can turn stale; the re-read hides it next time.
|
|
2324
|
+
event?.token !== void 0 && event.ready && event.suppressible && isConsumed(cursorOf(event.ckey ?? event.key), event.token)
|
|
2325
|
+
) {
|
|
2326
|
+
return false;
|
|
2327
|
+
}
|
|
2328
|
+
const completed = delivered.filter(isCompletion);
|
|
2329
|
+
for (const c of callers) {
|
|
2330
|
+
const mine = completed.filter((i) => ownerOf(i.key) === c);
|
|
2331
|
+
const tokens = delivered.filter((i) => i.token !== void 0 && ownerOf(i.ckey ?? i.key) === c).flatMap(
|
|
2332
|
+
(i) => i.token === void 0 ? [] : [{ key: i.ckey ?? i.key, token: i.token }]
|
|
2333
|
+
);
|
|
2334
|
+
const b = begun.get(c);
|
|
2335
|
+
if (b !== void 0) {
|
|
2336
|
+
const ended = endWait(
|
|
2337
|
+
db,
|
|
2338
|
+
c,
|
|
2339
|
+
b.generation,
|
|
2340
|
+
mine.map((i) => ({
|
|
2341
|
+
...registered(i.ref),
|
|
2342
|
+
...i.kind === "settled" && i.snap.runs !== void 0 ? {
|
|
2343
|
+
runs: i.snap.runs,
|
|
2344
|
+
...i.snap.incarnation !== void 0 ? { incarnation: i.snap.incarnation } : {}
|
|
2345
|
+
} : {}
|
|
2346
|
+
}))
|
|
2347
|
+
);
|
|
2348
|
+
if (!ended.applied) {
|
|
2349
|
+
applied = false;
|
|
2350
|
+
continue;
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
recordCursors(db, c, tokens);
|
|
2354
|
+
}
|
|
2355
|
+
return true;
|
|
2356
|
+
}).immediate();
|
|
2357
|
+
if (ok) committed = true;
|
|
2358
|
+
else delivered.length = 0;
|
|
2359
|
+
return ok;
|
|
2360
|
+
};
|
|
2361
|
+
let result;
|
|
2362
|
+
try {
|
|
2363
|
+
for (; ; ) {
|
|
2364
|
+
if (stopReason === "abort") {
|
|
2365
|
+
result = build("interrupted", evaluate());
|
|
2366
|
+
break;
|
|
2367
|
+
}
|
|
2368
|
+
if (stopReason === "deadline" || Date.now() >= deadline) {
|
|
2369
|
+
const items2 = evaluate();
|
|
2370
|
+
driveStalls(items2);
|
|
2371
|
+
const chosen2 = select2(items2);
|
|
2372
|
+
const r = chosen2 ? build(chosen2.event.kind ?? "closed", items2, chosen2) : build("heartbeat", items2);
|
|
2373
|
+
if (!commit(chosen2?.event)) continue;
|
|
2374
|
+
result = r;
|
|
2375
|
+
break;
|
|
2376
|
+
}
|
|
2377
|
+
await refresh();
|
|
2378
|
+
rethrow();
|
|
2379
|
+
if (stopReason === "abort") continue;
|
|
2380
|
+
const items = evaluate();
|
|
2381
|
+
driveStalls(items);
|
|
2382
|
+
opts.onPicture?.(items.map((i) => i.snap));
|
|
2383
|
+
if (items.length === 0) throw new WaitNothingError();
|
|
2384
|
+
const chosen = select2(items);
|
|
2385
|
+
if (chosen !== void 0) {
|
|
2386
|
+
const r = build(chosen.event.kind ?? "closed", items, chosen);
|
|
2387
|
+
if (!commit(chosen.event)) continue;
|
|
2388
|
+
result = r;
|
|
2389
|
+
break;
|
|
2390
|
+
}
|
|
2391
|
+
if (!all && items.every((i) => i.done)) {
|
|
2392
|
+
result = build("heartbeat", items, void 0, true);
|
|
2393
|
+
break;
|
|
2394
|
+
}
|
|
2395
|
+
if (stopReason !== void 0) continue;
|
|
2396
|
+
const polled = watchDeaths || observeOwners || items.some((i) => i.ref.kind !== "agent") || [...tracks.values()].some((t) => t.latched === void 0 && t.handle === void 0);
|
|
2397
|
+
const left = deadline - Date.now();
|
|
2398
|
+
const ms = polled ? Math.min(pollMs, left) : left;
|
|
2399
|
+
if (ms <= 0) continue;
|
|
2400
|
+
let timer;
|
|
2401
|
+
const sleeper = opts.sleep ?? sleepSeam;
|
|
2402
|
+
const slept = !polled ? new Promise(() => {
|
|
2403
|
+
}) : sleeper ? sleeper(ms) : new Promise((resolve4) => {
|
|
2404
|
+
timer = setTimeout(resolve4, ms);
|
|
2405
|
+
});
|
|
2406
|
+
await Promise.race([woken, slept, stopP]);
|
|
2407
|
+
clearTimeout(timer);
|
|
2408
|
+
woken = new Promise((resolve4) => {
|
|
2409
|
+
wake = resolve4;
|
|
2410
|
+
});
|
|
2411
|
+
rethrow();
|
|
2412
|
+
}
|
|
2413
|
+
} finally {
|
|
2414
|
+
clearTimeout(deadlineTimer);
|
|
2415
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
2416
|
+
watchAbort.abort();
|
|
2417
|
+
if (consumes) {
|
|
2418
|
+
if (!committed) commit();
|
|
2419
|
+
if (result !== void 0) {
|
|
2420
|
+
result.cursor = Object.fromEntries(callers.flatMap((c) => [...readCursors(db, c)]));
|
|
2421
|
+
if (!applied) result.superseded = true;
|
|
2422
|
+
}
|
|
2423
|
+
} else if (result !== void 0) result.cursor = {};
|
|
2424
|
+
if (opts.sync !== false) await ambientFlush(db, { quiet: true });
|
|
2425
|
+
}
|
|
2426
|
+
return result;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// src/wait-action-receipts.ts
|
|
2430
|
+
var fromRaw = (r) => ({
|
|
2431
|
+
taskId: r.task_id,
|
|
2432
|
+
ownerId: r.owner_id,
|
|
2433
|
+
incarnation: r.incarnation,
|
|
2434
|
+
action: r.action,
|
|
2435
|
+
caller: r.caller,
|
|
2436
|
+
taskRef: r.task_ref,
|
|
2437
|
+
ownerRef: r.owner_ref,
|
|
2438
|
+
paneId: r.pane_id,
|
|
2439
|
+
previewRevision: r.preview_revision,
|
|
2440
|
+
msgId: r.msg_id,
|
|
2441
|
+
state: r.state,
|
|
2442
|
+
runs: r.runs,
|
|
2443
|
+
detail: r.detail,
|
|
2444
|
+
createdAt: r.created_at,
|
|
2445
|
+
updatedAt: r.updated_at
|
|
2446
|
+
});
|
|
2447
|
+
var EDGES = {
|
|
2448
|
+
reserved: ["sending", "refused", "released"],
|
|
2449
|
+
sending: ["sent", "unknown", "refused"],
|
|
2450
|
+
unknown: ["sent"],
|
|
2451
|
+
sent: [],
|
|
2452
|
+
refused: [],
|
|
2453
|
+
released: []
|
|
2454
|
+
};
|
|
2455
|
+
function getActionReceipt(db, key) {
|
|
2456
|
+
const row = db.prepare(
|
|
2457
|
+
`SELECT * FROM wait_action_receipts
|
|
2458
|
+
WHERE task_id = ? AND owner_id = ? AND incarnation = ? AND action = ?`
|
|
2459
|
+
).get(key.taskId, key.ownerId, key.incarnation, key.action);
|
|
2460
|
+
return row === void 0 ? void 0 : fromRaw(row);
|
|
2461
|
+
}
|
|
2462
|
+
function reserveActionReceipt(db, key, info) {
|
|
2463
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2464
|
+
const res = db.prepare(
|
|
2465
|
+
`INSERT OR IGNORE INTO wait_action_receipts
|
|
2466
|
+
(task_id, owner_id, incarnation, action, caller, task_ref, owner_ref, pane_id,
|
|
2467
|
+
preview_revision, msg_id, state, created_at, updated_at)
|
|
2468
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'reserved', ?, ?)`
|
|
2469
|
+
).run(
|
|
2470
|
+
key.taskId,
|
|
2471
|
+
key.ownerId,
|
|
2472
|
+
key.incarnation,
|
|
2473
|
+
key.action,
|
|
2474
|
+
info.caller,
|
|
2475
|
+
info.taskRef,
|
|
2476
|
+
info.ownerRef,
|
|
2477
|
+
info.paneId,
|
|
2478
|
+
info.previewRevision,
|
|
2479
|
+
info.msgId ?? null,
|
|
2480
|
+
now,
|
|
2481
|
+
now
|
|
2482
|
+
);
|
|
2483
|
+
const receipt = getActionReceipt(db, key);
|
|
2484
|
+
if (receipt === void 0) throw new Error("wait action receipt vanished after reservation");
|
|
2485
|
+
return { created: res.changes === 1, receipt };
|
|
2486
|
+
}
|
|
2487
|
+
function transitionActionReceipt(db, key, from, to, patch = {}) {
|
|
2488
|
+
if (!EDGES[from].includes(to)) {
|
|
2489
|
+
throw new Error(`wait action receipt: illegal transition ${from} -> ${to}`);
|
|
2490
|
+
}
|
|
2491
|
+
const res = db.prepare(
|
|
2492
|
+
`UPDATE wait_action_receipts
|
|
2493
|
+
SET state = ?, msg_id = COALESCE(?, msg_id), runs = COALESCE(?, runs),
|
|
2494
|
+
detail = COALESCE(?, detail), updated_at = ?
|
|
2495
|
+
WHERE task_id = ? AND owner_id = ? AND incarnation = ? AND action = ? AND state = ?`
|
|
2496
|
+
).run(
|
|
2497
|
+
to,
|
|
2498
|
+
patch.msgId ?? null,
|
|
2499
|
+
patch.runs ?? null,
|
|
2500
|
+
patch.detail ?? null,
|
|
2501
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
2502
|
+
key.taskId,
|
|
2503
|
+
key.ownerId,
|
|
2504
|
+
key.incarnation,
|
|
2505
|
+
key.action,
|
|
2506
|
+
from
|
|
2507
|
+
);
|
|
2508
|
+
return res.changes === 1;
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
// src/wait-actions.ts
|
|
2512
|
+
var refused = (reason, action) => ({
|
|
2513
|
+
disposition: "return",
|
|
2514
|
+
action,
|
|
2515
|
+
state: "refused",
|
|
2516
|
+
reason
|
|
2517
|
+
});
|
|
2518
|
+
function guardStall(db, ctx, callers, action) {
|
|
2519
|
+
const no2 = (reason) => ({ ok: false, reason });
|
|
2520
|
+
if (ctx.signal.aborted) return no2("the wait was aborted");
|
|
2521
|
+
if (!callers.includes(ctx.caller)) {
|
|
2522
|
+
return no2(`caller ${ctx.caller} is not one of this process's callers`);
|
|
2523
|
+
}
|
|
2524
|
+
if (ctx.generation !== null) {
|
|
2525
|
+
const reg = readWaitRegistration(db, ctx.caller);
|
|
2526
|
+
if (reg === void 0 || reg.generation !== ctx.generation) {
|
|
2527
|
+
return no2("the wait was superseded (generation changed)");
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
const task = db.prepare(
|
|
2531
|
+
`SELECT t.local_id, w.name AS workstream, t.status, t.kind, t.owner_id
|
|
2532
|
+
FROM tasks t JOIN workstreams w ON w.id = t.workstream_id WHERE t.id = ?`
|
|
2533
|
+
).get(ctx.taskId);
|
|
2534
|
+
if (task === void 0) return no2("the task no longer exists");
|
|
2535
|
+
if (task.local_id !== ctx.ref.id || task.workstream !== ctx.ref.workstream) {
|
|
2536
|
+
return no2("the ref does not name this task");
|
|
2537
|
+
}
|
|
2538
|
+
if (task.kind !== "work") return no2(`a ${task.kind} task is never ${action}d`);
|
|
2539
|
+
if (task.status !== "IN_PROGRESS") return no2(`the task is ${task.status}, not IN_PROGRESS`);
|
|
2540
|
+
if (task.owner_id === null || task.owner_id !== ctx.owner.agentId) {
|
|
2541
|
+
return no2("the task's owner is no longer the stalled agent");
|
|
2542
|
+
}
|
|
2543
|
+
const agent = db.prepare("SELECT a.name, a.pane_id, a.spawner FROM agents a WHERE a.id = ?").get(task.owner_id);
|
|
2544
|
+
if (agent === void 0) return no2("the owner is no longer registered");
|
|
2545
|
+
if (agent.pane_id !== ctx.owner.paneId) return no2("the owner's pane changed (pane mismatch)");
|
|
2546
|
+
if (agent.spawner === null || !callers.includes(agent.spawner)) {
|
|
2547
|
+
return no2("the owner was not spawned by this caller (not yours to act on)");
|
|
2548
|
+
}
|
|
2549
|
+
if (action === "steer") {
|
|
2550
|
+
if (ctx.owner.status !== "needs_input" || ctx.owner.ctl !== "ok") {
|
|
2551
|
+
return no2(`the owner is not idle on a healthy control socket (state ${ctx.owner.status})`);
|
|
2552
|
+
}
|
|
2553
|
+
if (ctx.owner.incarnation === void 0 || ctx.owner.incarnation === "") {
|
|
2554
|
+
return no2("the owner's pi named no incarnation: a steer could not be bound to it");
|
|
2555
|
+
}
|
|
2556
|
+
} else {
|
|
2557
|
+
const brokenCtl = ctx.owner.ctl === "missing" || ctx.owner.ctl === "refused";
|
|
2558
|
+
const needsAttention = ctx.owner.status === "needs_input" || brokenCtl && (ctx.owner.status === "unknown" || ctx.owner.status === "busy");
|
|
2559
|
+
if (!needsAttention) {
|
|
2560
|
+
return no2(`the owner is not waiting on anyone (runtime state ${ctx.owner.status})`);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
if (!autoPermitted(db, ctx.taskId)) {
|
|
2564
|
+
return no2("the task is not permitted for auto action on this machine (mu task auto)");
|
|
2565
|
+
}
|
|
2566
|
+
if (callers.some((c) => openHumanNeeds(db, c).length > 0)) {
|
|
2567
|
+
return no2("a human stop is open for this caller: leave the stall to the human");
|
|
2568
|
+
}
|
|
2569
|
+
const revision = taskRevision(db, task.workstream, task.local_id);
|
|
2570
|
+
if (revision !== ctx.revision) {
|
|
2571
|
+
return no2(
|
|
2572
|
+
`the task changed since the stall was seen (revision ${ctx.revision} -> ${revision})`
|
|
2573
|
+
);
|
|
2574
|
+
}
|
|
2575
|
+
return {
|
|
2576
|
+
ok: true,
|
|
2577
|
+
found: {
|
|
2578
|
+
localId: task.local_id,
|
|
2579
|
+
workstream: task.workstream,
|
|
2580
|
+
ownerId: task.owner_id,
|
|
2581
|
+
ownerName: agent.name,
|
|
2582
|
+
paneId: agent.pane_id
|
|
2583
|
+
}
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
var keyOf = (ctx, ownerId, action) => ({
|
|
2587
|
+
taskId: ctx.taskId,
|
|
2588
|
+
ownerId,
|
|
2589
|
+
incarnation: ctx.owner.incarnation ?? "",
|
|
2590
|
+
action
|
|
2591
|
+
});
|
|
2592
|
+
var infoOf = (ctx, f) => ({
|
|
2593
|
+
caller: ctx.caller,
|
|
2594
|
+
taskRef: `${f.workstream}/${f.localId}`,
|
|
2595
|
+
ownerRef: `${ctx.owner.workstream}/${f.ownerName}`,
|
|
2596
|
+
paneId: f.paneId,
|
|
2597
|
+
previewRevision: ctx.revision
|
|
2598
|
+
});
|
|
2599
|
+
function releaseBody(db, ctx, callers) {
|
|
2600
|
+
const g = guardStall(db, ctx, callers, "release");
|
|
2601
|
+
if (!g.ok) return refused(g.reason, "release");
|
|
2602
|
+
const { found } = g;
|
|
2603
|
+
const key = keyOf(ctx, found.ownerId, "release");
|
|
2604
|
+
if (getActionReceipt(db, key) !== void 0) {
|
|
2605
|
+
return {
|
|
2606
|
+
disposition: "return",
|
|
2607
|
+
action: "release",
|
|
2608
|
+
state: "budget-exhausted",
|
|
2609
|
+
reason: "this owner incarnation was already released once for this task"
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2612
|
+
const res = releaseTask(db, found.localId, {
|
|
2613
|
+
workstream: found.workstream,
|
|
2614
|
+
expectedOwnerId: found.ownerId,
|
|
2615
|
+
ifRevision: ctx.revision,
|
|
2616
|
+
author: ctx.caller,
|
|
2617
|
+
evidence: `stall: ${found.ownerName} needed attention (stall ${ctx.stallToken}); released for another worker`
|
|
2618
|
+
});
|
|
2619
|
+
if (!res.changed) return refused("the release changed nothing", "release");
|
|
2620
|
+
reserveActionReceipt(db, key, infoOf(ctx, found));
|
|
2621
|
+
transitionActionReceipt(db, key, "reserved", "released", {
|
|
2622
|
+
detail: `released from ${found.ownerName}`
|
|
2623
|
+
});
|
|
2624
|
+
return {
|
|
2625
|
+
disposition: "return",
|
|
2626
|
+
action: "release",
|
|
2627
|
+
state: "released",
|
|
2628
|
+
...ctx.owner.incarnation !== void 0 ? { incarnation: ctx.owner.incarnation } : {},
|
|
2629
|
+
reason: `released ${found.localId} from ${found.ownerName}`
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2632
|
+
async function releaseStall(db, ctx, opts = {}) {
|
|
2633
|
+
const callers = opts.callers ?? currentCallers();
|
|
2634
|
+
try {
|
|
2635
|
+
return withOpContext(
|
|
2636
|
+
db,
|
|
2637
|
+
{ intent: "task.release", actor: ctx.caller, group: "new" },
|
|
2638
|
+
() => releaseBody(db, ctx, callers)
|
|
2639
|
+
);
|
|
2640
|
+
} catch (e) {
|
|
2641
|
+
return refused(
|
|
2642
|
+
`release failed, nothing changed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2643
|
+
"release"
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
function releaseStallHandler(db, opts = {}) {
|
|
2648
|
+
return (ctx) => releaseStall(db, ctx, opts);
|
|
2649
|
+
}
|
|
2650
|
+
function steerText(workstream, taskId) {
|
|
2651
|
+
return `[mu] ${workstream}/${taskId} is still yours (IN_PROGRESS) and mu is waiting on it. If the work is done and verified, close it: mu task close ${taskId} -w ${workstream} --evidence "<what you verified>". If you are blocked or need an answer, say so in one line and stop.`;
|
|
2652
|
+
}
|
|
2653
|
+
var steerResult = (state, disposition, extra = {}) => ({ disposition, action: "steer", state, ...extra });
|
|
2654
|
+
function planSteer(db, ctx, callers) {
|
|
2655
|
+
return db.transaction(() => {
|
|
2656
|
+
const key = keyOf(ctx, ctx.owner.agentId, "steer");
|
|
2657
|
+
const have = getActionReceipt(db, key);
|
|
2658
|
+
if (have !== void 0 && have.state !== "reserved") {
|
|
2659
|
+
if (have.state === "sending" || have.state === "unknown") {
|
|
2660
|
+
if (have.msgId === null) {
|
|
2661
|
+
return {
|
|
2662
|
+
kind: "done",
|
|
2663
|
+
result: steerResult("unknown", "return", {
|
|
2664
|
+
reason: "a steer may have been written but its id was lost: not resent"
|
|
2665
|
+
})
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2668
|
+
return {
|
|
2669
|
+
kind: "probe",
|
|
2670
|
+
msgId: have.msgId,
|
|
2671
|
+
key,
|
|
2672
|
+
paneId: have.paneId,
|
|
2673
|
+
state: have.state
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
return {
|
|
2677
|
+
kind: "done",
|
|
2678
|
+
result: steerResult(have.state === "refused" ? "refused" : "budget-exhausted", "return", {
|
|
2679
|
+
reason: have.state === "refused" ? `pi refused the steer for this owner incarnation (${have.detail ?? "no reason"}); not retried` : "this owner incarnation was already nudged once for this task",
|
|
2680
|
+
...have.msgId !== null ? { msgId: have.msgId } : {}
|
|
2681
|
+
})
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
const g = guardStall(db, ctx, callers, "steer");
|
|
2685
|
+
if (!g.ok) return { kind: "done", result: refused(g.reason, "steer") };
|
|
2686
|
+
const msgId = have?.msgId ?? newMsgId();
|
|
2687
|
+
if (have === void 0) {
|
|
2688
|
+
reserveActionReceipt(db, key, { ...infoOf(ctx, g.found), msgId });
|
|
2689
|
+
}
|
|
2690
|
+
return { kind: "send", msgId, key, found: g.found };
|
|
2691
|
+
}).immediate();
|
|
2692
|
+
}
|
|
2693
|
+
async function probeSteer(db, ctx, plan, timeoutMs) {
|
|
2694
|
+
const unknown = (reason) => {
|
|
2695
|
+
if (plan.state === "sending") {
|
|
2696
|
+
try {
|
|
2697
|
+
transitionActionReceipt(db, plan.key, "sending", "unknown", { detail: reason });
|
|
2698
|
+
} catch {
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
return steerResult("unknown", "return", { msgId: plan.msgId, reason });
|
|
2702
|
+
};
|
|
2703
|
+
const agent = getAgent(db, ctx.owner.name, ctx.owner.workstream);
|
|
2704
|
+
if (agent === void 0 || agent.incarnation !== plan.key.ownerId || agent.paneId !== plan.paneId) {
|
|
2705
|
+
return unknown("the owner was replaced: the earlier steer cannot be confirmed (not resent)");
|
|
2706
|
+
}
|
|
2707
|
+
if (ctx.signal.aborted) return unknown("the wait was aborted before the receipt probe");
|
|
2708
|
+
const check = await checkMessage(agentCtlSocket(db, agent), plan.msgId, timeoutMs);
|
|
2709
|
+
if (check.state !== "accepted" || check.incarnation !== plan.key.incarnation) {
|
|
2710
|
+
return unknown(
|
|
2711
|
+
check.state === "accepted" ? "the receipt came from another pi incarnation: not confirmed" : "the earlier steer is not confirmed (receipt absent or unreachable): not resent"
|
|
2712
|
+
);
|
|
2713
|
+
}
|
|
2714
|
+
try {
|
|
2715
|
+
transitionActionReceipt(db, plan.key, plan.state, "sent", {
|
|
2716
|
+
...check.runs !== void 0 ? { runs: check.runs } : {},
|
|
2717
|
+
detail: "confirmed by receipt probe"
|
|
2718
|
+
});
|
|
2719
|
+
} catch {
|
|
2720
|
+
}
|
|
2721
|
+
if (check.runs !== void 0 && ctx.owner.runs !== void 0 && check.runs < ctx.owner.runs) {
|
|
2722
|
+
return steerResult("sent", "return", {
|
|
2723
|
+
msgId: plan.msgId,
|
|
2724
|
+
incarnation: plan.key.incarnation,
|
|
2725
|
+
afterRuns: check.runs,
|
|
2726
|
+
reason: "nudged earlier, owner idle again"
|
|
2727
|
+
});
|
|
2728
|
+
}
|
|
2729
|
+
return sentResult(plan.msgId, plan.key.incarnation, check.runs);
|
|
2730
|
+
}
|
|
2731
|
+
function sentResult(msgId, incarnation, runs) {
|
|
2732
|
+
return steerResult("sent", runs === void 0 ? "return" : "continue", {
|
|
2733
|
+
msgId,
|
|
2734
|
+
incarnation,
|
|
2735
|
+
...runs !== void 0 ? { afterRuns: runs } : {},
|
|
2736
|
+
...runs === void 0 ? { reason: "sent, but pi reported no run baseline to wait from" } : {}
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
async function steerStall(db, ctx, opts = {}) {
|
|
2740
|
+
const callers = opts.callers ?? currentCallers();
|
|
2741
|
+
const timeoutMs = Math.min(CTL_DEFAULT_TIMEOUT_MS, Math.max(1, ctx.deadlineMs));
|
|
2742
|
+
let plan;
|
|
2743
|
+
try {
|
|
2744
|
+
plan = planSteer(db, ctx, callers);
|
|
2745
|
+
} catch (e) {
|
|
2746
|
+
return refused(`steer failed, nothing sent: ${errText(e)}`, "steer");
|
|
2747
|
+
}
|
|
2748
|
+
if (plan.kind === "done") return plan.result;
|
|
2749
|
+
if (plan.kind === "probe") return probeSteer(db, ctx, plan, timeoutMs);
|
|
2750
|
+
const { key, msgId, found } = plan;
|
|
2751
|
+
const agent = getAgent(db, found.ownerName, ctx.owner.workstream);
|
|
2752
|
+
if (agent === void 0 || agent.incarnation !== found.ownerId) {
|
|
2753
|
+
return refused("the owner is no longer registered", "steer");
|
|
2754
|
+
}
|
|
2755
|
+
let won = false;
|
|
2756
|
+
let vetoed;
|
|
2757
|
+
const beforeWrite = () => {
|
|
2758
|
+
const go = db.transaction(() => {
|
|
2759
|
+
const g = guardStall(db, ctx, callers, "steer");
|
|
2760
|
+
if (!g.ok) {
|
|
2761
|
+
vetoed = refused(g.reason, "steer");
|
|
2762
|
+
return false;
|
|
2763
|
+
}
|
|
2764
|
+
if (!transitionActionReceipt(db, key, "reserved", "sending", { msgId })) {
|
|
2765
|
+
vetoed = steerResult("budget-exhausted", "return", {
|
|
2766
|
+
reason: "another waiter owns this nudge"
|
|
2767
|
+
});
|
|
2768
|
+
return false;
|
|
2769
|
+
}
|
|
2770
|
+
return true;
|
|
2771
|
+
}).immediate();
|
|
2772
|
+
won = go;
|
|
2773
|
+
return go;
|
|
2774
|
+
};
|
|
2775
|
+
try {
|
|
2776
|
+
const sent = await sendViaTransport(agent, steerText(found.workstream, found.localId), {
|
|
2777
|
+
socket: agentCtlSocket(db, agent),
|
|
2778
|
+
mode: "steer",
|
|
2779
|
+
msgId,
|
|
2780
|
+
expectIncarnation: key.incarnation,
|
|
2781
|
+
ctlGuard: { signal: ctx.signal, timeoutMs, beforeWrite }
|
|
2782
|
+
});
|
|
2783
|
+
try {
|
|
2784
|
+
const patch = {
|
|
2785
|
+
...sent.runs !== void 0 ? { runs: sent.runs } : {},
|
|
2786
|
+
detail: sent.receipt ?? "accepted"
|
|
2787
|
+
};
|
|
2788
|
+
if (!transitionActionReceipt(db, key, "sending", "sent", patch)) {
|
|
2789
|
+
transitionActionReceipt(db, key, "unknown", "sent", patch);
|
|
2790
|
+
}
|
|
2791
|
+
recordSend(
|
|
2792
|
+
db,
|
|
2793
|
+
agent,
|
|
2794
|
+
"steer",
|
|
2795
|
+
Buffer.byteLength(steerText(found.workstream, found.localId)),
|
|
2796
|
+
{
|
|
2797
|
+
transport: "ctl",
|
|
2798
|
+
state: sent.state
|
|
2799
|
+
},
|
|
2800
|
+
ctx.caller
|
|
2801
|
+
);
|
|
2802
|
+
} catch {
|
|
2803
|
+
}
|
|
2804
|
+
return sentResult(msgId, key.incarnation, sent.runs);
|
|
2805
|
+
} catch (e) {
|
|
2806
|
+
if (e instanceof CtlWriteVetoedError || !won) {
|
|
2807
|
+
return vetoed ?? refused(`steer not sent: ${errText(e)}`, "steer");
|
|
2808
|
+
}
|
|
2809
|
+
const definite = e instanceof AgentSendRefusedError || e instanceof AgentReplacedError;
|
|
2810
|
+
const to = definite ? "refused" : "unknown";
|
|
2811
|
+
try {
|
|
2812
|
+
transitionActionReceipt(db, key, "sending", to, { detail: errText(e) });
|
|
2813
|
+
} catch {
|
|
2814
|
+
}
|
|
2815
|
+
return steerResult(to, "return", { msgId, reason: errText(e) });
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
var errText = (e) => e instanceof Error ? e.message : String(e);
|
|
2819
|
+
function steerStallHandler(db, opts = {}) {
|
|
2820
|
+
return (ctx) => steerStall(db, ctx, opts);
|
|
2821
|
+
}
|
|
2822
|
+
function waitWithActions(db, refs, opts) {
|
|
2823
|
+
if (opts.handleStall !== void 0) return waitForEvents(db, refs, opts);
|
|
2824
|
+
const actionOpts = { callers: opts.callers };
|
|
2825
|
+
const handleStall = opts.onStall === "steer" ? steerStallHandler(db, actionOpts) : opts.onStall === "release" ? releaseStallHandler(db, actionOpts) : void 0;
|
|
2826
|
+
return waitForEvents(db, refs, handleStall === void 0 ? opts : { ...opts, handleStall });
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
// src/wait-ctl.ts
|
|
2830
|
+
function ctlAgentHooks(db, opts = {}) {
|
|
2831
|
+
return {
|
|
2832
|
+
readAgents: async (refs) => {
|
|
2833
|
+
const agents = refs.flatMap((r) => {
|
|
2834
|
+
const a = getAgent(db, r.id, r.workstream);
|
|
2835
|
+
return a === void 0 ? [] : [a];
|
|
2836
|
+
});
|
|
2837
|
+
const observed = await observeAgentStates(db, agents);
|
|
2838
|
+
if (opts.paneText !== true) return observed;
|
|
2839
|
+
const out = new Map(observed);
|
|
2840
|
+
for (const a of agents) {
|
|
2841
|
+
const key = `${a.workstreamName}/${a.name}`;
|
|
2842
|
+
const base = out.get(key);
|
|
2843
|
+
if (base === void 0 || base.status === null) continue;
|
|
2844
|
+
if (base.ctl === "ok") continue;
|
|
2845
|
+
try {
|
|
2846
|
+
const text = await (await activeMux()).capture(a.paneId, { lines: 200 });
|
|
2847
|
+
out.set(key, { ...base, lastText: text.slice(-WAIT_TEXT_MAX), textSource: "pane" });
|
|
2848
|
+
} catch {
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
return out;
|
|
2852
|
+
},
|
|
2853
|
+
watchAgent: async (ref, signal, ctx) => {
|
|
2854
|
+
const agent = getAgent(db, ref.id, ref.workstream);
|
|
2855
|
+
if (agent === void 0) return void 0;
|
|
2856
|
+
const sock = agentCtlSocket(db, agent);
|
|
2857
|
+
let afterRuns = ref.afterRuns;
|
|
2858
|
+
let unknownBaseline;
|
|
2859
|
+
if (afterRuns === void 0 && (ctx.owed || ctx.baseline !== void 0 || ctx.cursor !== void 0)) {
|
|
2860
|
+
if (!expectsCtl(agent, sock)) return void 0;
|
|
2861
|
+
const probe = await ctlProbe(sock);
|
|
2862
|
+
if (probe.kind !== "ok") return void 0;
|
|
2863
|
+
const inc = probe.incarnation;
|
|
2864
|
+
const b = ctx.baseline;
|
|
2865
|
+
const baseOk = b !== void 0 && (b.agentId === void 0 || b.agentId === agent.incarnation) && (b.incarnation === void 0 || inc !== void 0 && b.incarnation === inc);
|
|
2866
|
+
const cursorOk = ctx.cursor !== void 0 && inc !== void 0 && ctx.cursor.incarnation === inc;
|
|
2867
|
+
const fromCursor = cursorOk && ctx.cursor !== void 0 ? ctx.cursor.runs : void 0;
|
|
2868
|
+
if (baseOk && b !== void 0) {
|
|
2869
|
+
afterRuns = Math.max(b.afterRuns, fromCursor ?? 0);
|
|
2870
|
+
} else if (fromCursor !== void 0) afterRuns = fromCursor;
|
|
2871
|
+
else if (ctx.owed) {
|
|
2872
|
+
afterRuns = probe.status.runs;
|
|
2873
|
+
unknownBaseline = {
|
|
2874
|
+
afterRuns,
|
|
2875
|
+
...inc !== void 0 ? { incarnation: inc } : {},
|
|
2876
|
+
agentId: agent.incarnation
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
const handle = await watchAgentRun(agent, sock, signal, {
|
|
2881
|
+
...afterRuns !== void 0 ? { afterRuns } : {},
|
|
2882
|
+
...ref.incarnation !== void 0 ? { inc: ref.incarnation } : {}
|
|
2883
|
+
});
|
|
2884
|
+
return handle !== void 0 && unknownBaseline !== void 0 ? { ...handle, unknownBaseline } : handle;
|
|
2885
|
+
}
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
// src/next-recover.ts
|
|
2890
|
+
var OBSERVE_MS = 3e3;
|
|
2891
|
+
var idleOnCtl = (f) => f.status === "needs_input" && f.ctl === "ok" && f.pending !== true && f.blocked === void 0;
|
|
2892
|
+
var workerKey = (a) => `${a.workstreamName}/${a.name}`;
|
|
2893
|
+
var observeWorkers = (db) => async (agents, { signal, ms }) => {
|
|
2894
|
+
const states = await observeAgentStates(db, agents);
|
|
2895
|
+
const out = /* @__PURE__ */ new Map();
|
|
2896
|
+
await Promise.all(
|
|
2897
|
+
agents.map(async (agent) => {
|
|
2898
|
+
const key = workerKey(agent);
|
|
2899
|
+
const s = states.get(key);
|
|
2900
|
+
if (s === void 0) return;
|
|
2901
|
+
const facts = {
|
|
2902
|
+
status: s.status,
|
|
2903
|
+
...s.ctl !== void 0 ? { ctl: s.ctl } : {},
|
|
2904
|
+
...s.pending !== void 0 ? { pending: s.pending } : {},
|
|
2905
|
+
...s.blocked !== void 0 ? { blocked: s.blocked } : {},
|
|
2906
|
+
...s.incarnation !== void 0 ? { incarnation: s.incarnation } : {},
|
|
2907
|
+
...s.runs !== void 0 ? { runs: s.runs } : {}
|
|
2908
|
+
};
|
|
2909
|
+
out.set(key, facts);
|
|
2910
|
+
const sock = agentCtlSocket(db, agent);
|
|
2911
|
+
if (!idleOnCtl(s) || s.runs === void 0 || s.runs < 1 || !expectsCtl(agent, sock)) return;
|
|
2912
|
+
const stop = new AbortController();
|
|
2913
|
+
const timer = setTimeout(() => stop.abort(), ms);
|
|
2914
|
+
const onAbort = () => stop.abort();
|
|
2915
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2916
|
+
try {
|
|
2917
|
+
const watch = await watchAgentRun(agent, sock, stop.signal, {
|
|
2918
|
+
afterRuns: s.runs - 1,
|
|
2919
|
+
...s.incarnation !== void 0 ? { inc: s.incarnation } : {}
|
|
2920
|
+
});
|
|
2921
|
+
if (watch === void 0) return;
|
|
2922
|
+
const settled = await watch.settled;
|
|
2923
|
+
if (settled.settled) {
|
|
2924
|
+
out.set(key, {
|
|
2925
|
+
...facts,
|
|
2926
|
+
...settled.runs !== void 0 ? { runs: settled.runs } : {},
|
|
2927
|
+
...settled.incarnation !== void 0 ? { incarnation: settled.incarnation } : {},
|
|
2928
|
+
...settled.lastError !== void 0 ? { lastError: settled.lastError } : {}
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
2931
|
+
} catch {
|
|
2932
|
+
} finally {
|
|
2933
|
+
clearTimeout(timer);
|
|
2934
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2935
|
+
}
|
|
2936
|
+
})
|
|
2937
|
+
);
|
|
2938
|
+
return out;
|
|
2939
|
+
};
|
|
2940
|
+
function inFlightWorkers(db, workstream) {
|
|
2941
|
+
const rows = db.prepare(
|
|
2942
|
+
`SELECT t.id AS id, t.local_id AS local_id
|
|
2943
|
+
FROM task_auto a
|
|
2944
|
+
JOIN tasks t ON t.id = a.task_id
|
|
2945
|
+
JOIN workstreams w ON w.id = t.workstream_id AND w.name = ?
|
|
2946
|
+
WHERE a.enabled = 1 AND t.kind = 'work' AND t.dispatch_host = ?
|
|
2947
|
+
AND t.status = 'IN_PROGRESS' AND t.owner_id IS NOT NULL
|
|
2948
|
+
ORDER BY t.id`
|
|
2949
|
+
).all(workstream, localMachineId(db));
|
|
2950
|
+
const out = [];
|
|
2951
|
+
for (const r of rows) {
|
|
2952
|
+
if (!autoPermitted(db, r.id)) continue;
|
|
2953
|
+
const delivered = deliveredOf(db, r.id);
|
|
2954
|
+
if (delivered === void 0) continue;
|
|
2955
|
+
const { receipt } = delivered;
|
|
2956
|
+
const id = savedIdentity(db, receipt);
|
|
2957
|
+
if (id.agent === void 0) continue;
|
|
2958
|
+
const owner = db.prepare("SELECT owner_id FROM tasks WHERE id = ?").get(r.id);
|
|
2959
|
+
if (owner.owner_id !== id.agent.incarnation) continue;
|
|
2960
|
+
out.push({
|
|
2961
|
+
task: r.local_id,
|
|
2962
|
+
taskId: r.id,
|
|
2963
|
+
agent: id.agent,
|
|
2964
|
+
runs: receipt.runs,
|
|
2965
|
+
incarnation: receipt.incarnation
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
return out;
|
|
2969
|
+
}
|
|
2970
|
+
function exhaustedAfterDeath(db, workstream) {
|
|
2971
|
+
const rows = db.prepare(
|
|
2972
|
+
`SELECT t.id AS id, t.local_id AS local_id, a.max_runs AS max
|
|
2973
|
+
FROM task_auto a
|
|
2974
|
+
JOIN tasks t ON t.id = a.task_id
|
|
2975
|
+
JOIN workstreams w ON w.id = t.workstream_id AND w.name = ?
|
|
2976
|
+
WHERE a.enabled = 1 AND t.kind = 'work' AND t.dispatch_host = ?
|
|
2977
|
+
AND t.status = 'OPEN' AND a.runs >= a.max_runs
|
|
2978
|
+
ORDER BY t.id`
|
|
2979
|
+
).all(workstream, localMachineId(db));
|
|
2980
|
+
const out = [];
|
|
2981
|
+
for (const r of rows) {
|
|
2982
|
+
if (!autoPermitted(db, r.id)) continue;
|
|
2983
|
+
const delivered = deliveredOf(db, r.id);
|
|
2984
|
+
if (delivered === void 0) continue;
|
|
2985
|
+
if (savedIdentity(db, delivered.receipt).stale === void 0) continue;
|
|
2986
|
+
out.push({
|
|
2987
|
+
task: r.local_id,
|
|
2988
|
+
reason: `its worker is gone and it used its ${r.max} run(s): raise the ceiling (mu task auto --max) or take it over`
|
|
2989
|
+
});
|
|
2990
|
+
}
|
|
2991
|
+
return out;
|
|
2992
|
+
}
|
|
2993
|
+
async function scanRecovery(db, workstream, observe, opts = {}) {
|
|
2994
|
+
const workers = inFlightWorkers(db, workstream);
|
|
2995
|
+
const out = [];
|
|
2996
|
+
if (workers.length > 0) {
|
|
2997
|
+
const facts = await observe(
|
|
2998
|
+
workers.map((w) => w.agent),
|
|
2999
|
+
{ ...opts.signal !== void 0 ? { signal: opts.signal } : {}, ms: opts.ms ?? OBSERVE_MS }
|
|
3000
|
+
);
|
|
3001
|
+
for (const w of workers) {
|
|
3002
|
+
const f = facts.get(workerKey(w.agent));
|
|
3003
|
+
if (f === void 0) continue;
|
|
3004
|
+
if (f.status === null) {
|
|
3005
|
+
if (w.agent.host !== null) {
|
|
3006
|
+
out.push({
|
|
3007
|
+
kind: "attention",
|
|
3008
|
+
task: w.task,
|
|
3009
|
+
agent: w.agent.name,
|
|
3010
|
+
reason: `remote worker ${w.agent.name} on ${w.agent.host} has no pane: its tree is kept, not reaped (mu agent collect, then close it)`
|
|
3011
|
+
});
|
|
3012
|
+
} else {
|
|
3013
|
+
out.push({
|
|
3014
|
+
kind: "reap",
|
|
3015
|
+
task: w.task,
|
|
3016
|
+
agent: w.agent,
|
|
3017
|
+
why: "its pane is gone"
|
|
3018
|
+
});
|
|
3019
|
+
}
|
|
3020
|
+
continue;
|
|
3021
|
+
}
|
|
3022
|
+
if (f.lastError === void 0) continue;
|
|
3023
|
+
if (!idleOnCtl(f)) continue;
|
|
3024
|
+
const attention = (reason) => {
|
|
3025
|
+
out.push({ kind: "attention", task: w.task, agent: w.agent.name, reason });
|
|
3026
|
+
};
|
|
3027
|
+
if (w.agent.host !== null) {
|
|
3028
|
+
attention(`remote worker ${w.agent.name} stopped on an API error: not retried from here`);
|
|
3029
|
+
continue;
|
|
3030
|
+
}
|
|
3031
|
+
if (f.incarnation === void 0 || f.runs === void 0 || w.runs === null) {
|
|
3032
|
+
attention(
|
|
3033
|
+
`${w.agent.name} stopped on an API error but no run baseline or pi incarnation is known: not retried`
|
|
3034
|
+
);
|
|
3035
|
+
continue;
|
|
3036
|
+
}
|
|
3037
|
+
if (f.runs <= w.runs) continue;
|
|
3038
|
+
const auto = getAutoConfig(db, w.taskId);
|
|
3039
|
+
if (auto !== void 0 && auto.runs >= auto.maxRuns) {
|
|
3040
|
+
attention(
|
|
3041
|
+
`${w.agent.name} stopped on an API error and ${w.task} used its ${auto.maxRuns} run(s): raise the ceiling (mu task auto --max) or take it over`
|
|
3042
|
+
);
|
|
3043
|
+
continue;
|
|
3044
|
+
}
|
|
3045
|
+
out.push({
|
|
3046
|
+
kind: "retry",
|
|
3047
|
+
task: w.task,
|
|
3048
|
+
error: f.lastError,
|
|
3049
|
+
target: {
|
|
3050
|
+
agent: { incarnation: w.agent.incarnation, paneId: w.agent.paneId, name: w.agent.name },
|
|
3051
|
+
pi: f.incarnation,
|
|
3052
|
+
errorRuns: f.runs
|
|
3053
|
+
}
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
return out;
|
|
3058
|
+
}
|
|
3059
|
+
function liveWorkers(db, workstream) {
|
|
3060
|
+
const row = db.prepare(
|
|
3061
|
+
`SELECT COUNT(*) AS n FROM agents a
|
|
3062
|
+
JOIN workstreams w ON w.id = a.workstream_id AND w.name = ?
|
|
3063
|
+
WHERE EXISTS (SELECT 1 FROM tasks t WHERE t.owner_id = a.id AND t.status = 'IN_PROGRESS')
|
|
3064
|
+
OR EXISTS (SELECT 1 FROM spawn_receipts r JOIN tasks t ON t.id = r.task_id
|
|
3065
|
+
WHERE r.agent_id = a.id AND r.pane_id = a.pane_id
|
|
3066
|
+
AND r.attempt_key LIKE 'auto:%'
|
|
3067
|
+
AND r.state IN ('pane', 'claimed', 'sending', 'unknown')
|
|
3068
|
+
AND t.status != 'CLOSED')`
|
|
3069
|
+
).get(workstream);
|
|
3070
|
+
return row.n;
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
// src/next-refute.ts
|
|
3074
|
+
var REFUTE_WORKSTREAM = "scratch";
|
|
3075
|
+
var READ_MS = 3e3;
|
|
3076
|
+
var CHECK_TIMEOUT_S = 1800;
|
|
3077
|
+
var readChecker = async (db, agent, base, { signal, ms }) => {
|
|
3078
|
+
const live = (await observeAgentStates(db, [agent])).get(agentKey(agent));
|
|
3079
|
+
if (live === void 0) return { kind: "running" };
|
|
3080
|
+
if (live.status === null) return { kind: "died" };
|
|
3081
|
+
if (!idleOnCtl(live) || live.runs === void 0 || live.runs <= base.afterRuns) {
|
|
3082
|
+
return { kind: "running" };
|
|
3083
|
+
}
|
|
3084
|
+
const stop = new AbortController();
|
|
3085
|
+
const timer = setTimeout(() => stop.abort(), ms);
|
|
3086
|
+
const onAbort = () => stop.abort();
|
|
3087
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3088
|
+
try {
|
|
3089
|
+
const watch = await watchAgentRun(agent, agentCtlSocket(db, agent), stop.signal, {
|
|
3090
|
+
afterRuns: base.afterRuns,
|
|
3091
|
+
...base.inc !== null ? { inc: base.inc } : {}
|
|
3092
|
+
});
|
|
3093
|
+
const done = watch === void 0 ? void 0 : await watch.settled;
|
|
3094
|
+
if (done === void 0 || !done.settled) return { kind: "running" };
|
|
3095
|
+
const outcome = delegateOutcome({ fired: true, dead: false, ...done }, false);
|
|
3096
|
+
return { kind: "answer", outcome, text: done.lastText ?? done.lastError };
|
|
3097
|
+
} catch (err) {
|
|
3098
|
+
if (err instanceof AgentRestartedError) return { kind: "died" };
|
|
3099
|
+
throw err;
|
|
3100
|
+
} finally {
|
|
3101
|
+
clearTimeout(timer);
|
|
3102
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3103
|
+
}
|
|
3104
|
+
};
|
|
3105
|
+
function nextCheck(db, workstream, caller, skip) {
|
|
3106
|
+
const rows = db.prepare(
|
|
3107
|
+
`SELECT t.id AS id, t.local_id AS local_id
|
|
3108
|
+
FROM task_auto a
|
|
3109
|
+
JOIN tasks t ON t.id = a.task_id
|
|
3110
|
+
JOIN workstreams w ON w.id = t.workstream_id AND w.name = ?
|
|
3111
|
+
WHERE t.status = 'CLOSED' AND t.substate = 'done' ORDER BY t.id`
|
|
3112
|
+
).all(workstream);
|
|
3113
|
+
for (const r of rows) {
|
|
3114
|
+
if (!autoPermitted(db, r.id)) continue;
|
|
3115
|
+
const attempt = deliveredOf(db, r.id)?.receipt.attemptKey;
|
|
3116
|
+
if (attempt === void 0) continue;
|
|
3117
|
+
const callId = `refute:${workstream}:${r.local_id}:${attempt}`;
|
|
3118
|
+
if (skip.has(callId)) continue;
|
|
3119
|
+
const call = getDelegateCall(db, callId);
|
|
3120
|
+
if (call === void 0) return { callId, task: r.local_id, attempt, workstream };
|
|
3121
|
+
if (call.caller !== caller) continue;
|
|
3122
|
+
if (["starting", "running", "recording"].includes(call.state)) {
|
|
3123
|
+
return { callId, task: r.local_id, attempt, workstream, call };
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
return void 0;
|
|
3127
|
+
}
|
|
3128
|
+
function checkerOf(db, c) {
|
|
3129
|
+
const taskId = taskIdFor(db, c.task, c.workstream);
|
|
3130
|
+
const receipt = taskId === null ? void 0 : getReceipt(db, taskId, `manual:${c.task}`);
|
|
3131
|
+
const agent = receipt === void 0 ? void 0 : savedIdentity(db, receipt).agent;
|
|
3132
|
+
return receipt !== void 0 && agent !== void 0 ? { receipt, agent } : void 0;
|
|
3133
|
+
}
|
|
3134
|
+
function checkerRefs(db, workstream, caller) {
|
|
3135
|
+
const refs = [];
|
|
3136
|
+
for (const c of liveCalls(db, workstream, caller)) {
|
|
3137
|
+
const held = checkerOf(db, c);
|
|
3138
|
+
if (held === void 0) continue;
|
|
3139
|
+
const { receipt, agent } = held;
|
|
3140
|
+
refs.push({
|
|
3141
|
+
kind: "agent",
|
|
3142
|
+
workstream: agent.workstreamName,
|
|
3143
|
+
id: agent.name,
|
|
3144
|
+
afterRuns: receipt.runs ?? 0,
|
|
3145
|
+
...receipt.incarnation !== null ? { incarnation: receipt.incarnation } : {}
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
return refs;
|
|
3149
|
+
}
|
|
3150
|
+
function liveCalls(db, workstream, caller) {
|
|
3151
|
+
return db.prepare(
|
|
3152
|
+
"SELECT call_id FROM delegate_calls WHERE caller = ? AND call_id LIKE ? AND state IN ('running','recording')"
|
|
3153
|
+
).all(caller, `refute:${workstream}:%`).map((r) => getDelegateCall(db, r.call_id)).filter((c) => c !== void 0);
|
|
3154
|
+
}
|
|
3155
|
+
var stem = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 13).replace(/^-+|-+$/g, "");
|
|
3156
|
+
function checkerText(db, ws, task, attempt) {
|
|
3157
|
+
const { briefN, reportN } = pinnedNotes(db, ws, task);
|
|
3158
|
+
const part = (what, n) => n < 1 ? `${what}: (none)` : `${what} (note #${n}):
|
|
3159
|
+
${getTaskNote(db, task, ws, n).content.trimEnd()}`;
|
|
3160
|
+
return [
|
|
3161
|
+
`You are an independent checker of the work on task ${task} (workstream ${ws}), attempt ${attempt}. Read only: do not edit, commit, merge, close or accept anything.`,
|
|
3162
|
+
part("Pinned brief", briefN),
|
|
3163
|
+
part("The worker's report", reportN),
|
|
3164
|
+
"Try to refute the claim that the work meets the brief: read the code and run the checks yourself. End your final message with a line starting `VERDICT:` and one of CONFIRMED, REFUTED, UNVERIFIED, then `EVIDENCE:` lines naming what you ran and saw."
|
|
3165
|
+
].join("\n\n");
|
|
3166
|
+
}
|
|
3167
|
+
var planCheck = (c) => ({
|
|
3168
|
+
kind: "refute",
|
|
3169
|
+
task: c.task,
|
|
3170
|
+
key: c.callId,
|
|
3171
|
+
state: c.call?.state ?? "new"
|
|
3172
|
+
});
|
|
3173
|
+
async function runCheck(db, c, deps) {
|
|
3174
|
+
const base = { task: c.task, key: c.callId };
|
|
3175
|
+
const taskId = taskIdFor(db, c.task, c.workstream);
|
|
3176
|
+
const auto = taskId === null ? void 0 : getAutoConfig(db, taskId);
|
|
3177
|
+
let call = c.call;
|
|
3178
|
+
if (call === void 0) {
|
|
3179
|
+
const cli = deps.cli ?? auto?.cli ?? "pi";
|
|
3180
|
+
if (!speaksMuCtl(cli, resolveCliCommand(cli))) {
|
|
3181
|
+
return {
|
|
3182
|
+
kind: "skip",
|
|
3183
|
+
...base,
|
|
3184
|
+
cli,
|
|
3185
|
+
reason: `cli ${cli} cannot report an answer over ctl: pass --refute-cli pi`
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
3188
|
+
try {
|
|
3189
|
+
call = registerDelegateCall(db, {
|
|
3190
|
+
callId: c.callId,
|
|
3191
|
+
caller: deps.caller,
|
|
3192
|
+
workstream: REFUTE_WORKSTREAM,
|
|
3193
|
+
wave: `refute-${c.workstream}`,
|
|
3194
|
+
record: { workstream: c.workstream, task: c.task },
|
|
3195
|
+
state: "starting",
|
|
3196
|
+
request: {
|
|
3197
|
+
text: checkerText(db, c.workstream, c.task, c.attempt),
|
|
3198
|
+
label: `refute-${c.task}`,
|
|
3199
|
+
cli,
|
|
3200
|
+
timeoutS: CHECK_TIMEOUT_S
|
|
3201
|
+
}
|
|
3202
|
+
}).call;
|
|
3203
|
+
} catch (err) {
|
|
3204
|
+
if (err instanceof DelegateCallConflictError) {
|
|
3205
|
+
return { kind: "skip", ...base, reason: "another mu registered this check first" };
|
|
3206
|
+
}
|
|
3207
|
+
throw err;
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
if (call.state === "starting") return launch(db, call, base);
|
|
3211
|
+
return readOne(db, call, base, deps);
|
|
3212
|
+
}
|
|
3213
|
+
async function launch(db, call, base) {
|
|
3214
|
+
const cli = call.request.cli ?? "pi";
|
|
3215
|
+
try {
|
|
3216
|
+
const out = await spawnForTask(db, {
|
|
3217
|
+
task: call.task,
|
|
3218
|
+
text: call.request.text,
|
|
3219
|
+
spawn: {
|
|
3220
|
+
name: `delegate-refute-${stem(base.task)}`,
|
|
3221
|
+
workstream: call.workstream,
|
|
3222
|
+
cli,
|
|
3223
|
+
nextFree: true
|
|
3224
|
+
}
|
|
3225
|
+
});
|
|
3226
|
+
const agent = out.agent?.name ?? out.receipt.name;
|
|
3227
|
+
if (out.state === "unknown") {
|
|
3228
|
+
if (!out.replay)
|
|
3229
|
+
return {
|
|
3230
|
+
kind: "refute",
|
|
3231
|
+
...base,
|
|
3232
|
+
agent,
|
|
3233
|
+
cli,
|
|
3234
|
+
state: "starting",
|
|
3235
|
+
reason: out.reason ?? "delivery unknown"
|
|
3236
|
+
};
|
|
3237
|
+
return end(
|
|
3238
|
+
db,
|
|
3239
|
+
call,
|
|
3240
|
+
base,
|
|
3241
|
+
"error",
|
|
3242
|
+
`delivery unknown after one probe (${out.reason ?? "no reply"}); pane ${agent} kept`
|
|
3243
|
+
);
|
|
3244
|
+
}
|
|
3245
|
+
try {
|
|
3246
|
+
markDelegate(db, call.callId, "running", { agent });
|
|
3247
|
+
} catch (err) {
|
|
3248
|
+
if (!(err instanceof DelegateStateError)) throw err;
|
|
3249
|
+
}
|
|
3250
|
+
return { kind: "refute", ...base, agent, cli, state: "running" };
|
|
3251
|
+
} catch (err) {
|
|
3252
|
+
if (err instanceof NoMultiplexerError) throw err;
|
|
3253
|
+
if (err instanceof SpawnAttemptBusyError) {
|
|
3254
|
+
return { kind: "skip", ...base, reason: "another mu is launching this checker" };
|
|
3255
|
+
}
|
|
3256
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
3257
|
+
const done = end(db, call, base, "error", `the checker could not be started: ${reason}`);
|
|
3258
|
+
return { ...done, kind: "failed", reason };
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
function end(db, call, base, outcome, text, agent) {
|
|
3262
|
+
const r = finishDelegate(db, call.callId, {
|
|
3263
|
+
outcome,
|
|
3264
|
+
...text !== void 0 ? { text } : {},
|
|
3265
|
+
elapsedMs: Math.max(0, Date.now() - Date.parse(call.updatedAt)),
|
|
3266
|
+
author: agent ?? call.agent ?? "next"
|
|
3267
|
+
});
|
|
3268
|
+
return {
|
|
3269
|
+
kind: "refute",
|
|
3270
|
+
...base,
|
|
3271
|
+
...call.agent !== null ? { agent: call.agent } : {},
|
|
3272
|
+
state: r.state,
|
|
3273
|
+
...r.verdict !== null ? { verdict: r.verdict } : {},
|
|
3274
|
+
...r.error !== void 0 ? { reason: r.error } : {}
|
|
3275
|
+
};
|
|
3276
|
+
}
|
|
3277
|
+
async function readOne(db, call, base, deps) {
|
|
3278
|
+
const held = checkerOf(db, call);
|
|
3279
|
+
if (held === void 0) {
|
|
3280
|
+
return end(db, call, base, "died", "the checker's agent is gone", call.agent ?? "next");
|
|
3281
|
+
}
|
|
3282
|
+
const { receipt, agent } = held;
|
|
3283
|
+
const got = await deps.read(
|
|
3284
|
+
db,
|
|
3285
|
+
agent,
|
|
3286
|
+
{ afterRuns: receipt.runs ?? 0, inc: receipt.incarnation },
|
|
3287
|
+
{ ...deps.signal !== void 0 ? { signal: deps.signal } : {}, ms: READ_MS }
|
|
3288
|
+
);
|
|
3289
|
+
if (got.kind === "running") {
|
|
3290
|
+
const late = Date.now() - Date.parse(call.updatedAt) > (call.request.timeoutS ?? CHECK_TIMEOUT_S) * 1e3;
|
|
3291
|
+
if (!late) return { kind: "refute", ...base, agent: agent.name, state: "running" };
|
|
3292
|
+
return end(db, call, base, "timeout", void 0, agent.name);
|
|
3293
|
+
}
|
|
3294
|
+
return end(
|
|
3295
|
+
db,
|
|
3296
|
+
call,
|
|
3297
|
+
base,
|
|
3298
|
+
got.kind === "died" ? "died" : got.outcome,
|
|
3299
|
+
got.kind === "answer" ? got.text : void 0,
|
|
3300
|
+
agent.name
|
|
3301
|
+
);
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
// src/next.ts
|
|
3305
|
+
var NextArgError = class extends Error {
|
|
3306
|
+
name = "NextArgError";
|
|
3307
|
+
};
|
|
3308
|
+
var DEFAULT_NEXT_CAP = 5;
|
|
3309
|
+
var MAX_NEXT_CAP = 100;
|
|
3310
|
+
function checkCap(cap) {
|
|
3311
|
+
if (cap === void 0) return DEFAULT_NEXT_CAP;
|
|
3312
|
+
if (!Number.isInteger(cap) || cap < 1 || cap > MAX_NEXT_CAP) {
|
|
3313
|
+
throw new NextArgError(`--cap must be an integer from 1 to ${MAX_NEXT_CAP} (got ${cap})`);
|
|
3314
|
+
}
|
|
3315
|
+
return cap;
|
|
3316
|
+
}
|
|
3317
|
+
function checkWorkers(n) {
|
|
3318
|
+
if (n === void 0) return void 0;
|
|
3319
|
+
if (!Number.isInteger(n) || n < 1 || n > MAX_NEXT_CAP) {
|
|
3320
|
+
throw new NextArgError(`--workers must be an integer from 1 to ${MAX_NEXT_CAP} (got ${n})`);
|
|
3321
|
+
}
|
|
3322
|
+
return n;
|
|
3323
|
+
}
|
|
3324
|
+
function checkTimeout(ms) {
|
|
3325
|
+
if (ms === void 0) return WAIT_HEARTBEAT_CAP_MS;
|
|
3326
|
+
if (!Number.isFinite(ms) || ms < 0) {
|
|
3327
|
+
throw new NextArgError(`--timeout must be a number of seconds, 0 or more (got ${ms / 1e3})`);
|
|
3328
|
+
}
|
|
3329
|
+
return ms === 0 ? WAIT_HEARTBEAT_CAP_MS : Math.min(ms, WAIT_HEARTBEAT_CAP_MS);
|
|
3330
|
+
}
|
|
3331
|
+
function select(db, ws, done) {
|
|
3332
|
+
const pending = pendingAttempts(db, ws).find((t) => !done.has(t));
|
|
3333
|
+
if (pending !== void 0) return { task: pending, resume: true };
|
|
3334
|
+
const ready = authorisedReady(db, ws).find((t) => !done.has(t.name));
|
|
3335
|
+
return ready === void 0 ? void 0 : { task: ready.name, resume: false };
|
|
3336
|
+
}
|
|
3337
|
+
function needsPane(db, ws, c) {
|
|
3338
|
+
if (!c.resume) return true;
|
|
3339
|
+
const id = taskIdFor(db, c.task, ws);
|
|
3340
|
+
const receipt = id === null ? void 0 : readAttempt(db, id).receipt;
|
|
3341
|
+
return receipt?.state === "reserved";
|
|
3342
|
+
}
|
|
3343
|
+
function counts(m) {
|
|
3344
|
+
if (m.kind === "skip" || m.kind === "abandoned" || m.kind === "attention") return false;
|
|
3345
|
+
if (m.kind === "needs_human") return m.checked !== void 0;
|
|
3346
|
+
return true;
|
|
3347
|
+
}
|
|
3348
|
+
var HoldHumanStop = class extends Error {
|
|
3349
|
+
};
|
|
3350
|
+
var DEAD_OR_ERRORED = (r) => {
|
|
3351
|
+
const e = r.event;
|
|
3352
|
+
if (e.kind === "dead") return true;
|
|
3353
|
+
if (e.kind !== "settled" || e.ref === null) return false;
|
|
3354
|
+
const snap = r.refs.find(
|
|
3355
|
+
(s) => s.ref.kind === e.ref?.kind && s.ref.workstream === e.ref.workstream && s.ref.id === e.ref.id
|
|
3356
|
+
);
|
|
3357
|
+
return snap?.lastError !== void 0 && snap.close?.applied !== true;
|
|
3358
|
+
};
|
|
3359
|
+
var refText = (r) => r.kind === "delegate" ? `delegate:${r.id}` : `${r.kind}:${r.workstream}/${r.id}`;
|
|
3360
|
+
async function executeNext(db, opts) {
|
|
3361
|
+
const cap = checkCap(opts.cap);
|
|
3362
|
+
const timeoutMs = checkTimeout(opts.timeoutMs);
|
|
3363
|
+
const workers = checkWorkers(opts.workers);
|
|
3364
|
+
const refute = opts.refute === true;
|
|
3365
|
+
if (refute && opts.caller === "") {
|
|
3366
|
+
throw new NextArgError(
|
|
3367
|
+
"--refute needs a caller identity (a managed agent, or PI_SESSION_ID): a checker belongs to the caller that launched it"
|
|
3368
|
+
);
|
|
3369
|
+
}
|
|
3370
|
+
const ws = opts.workstream;
|
|
3371
|
+
const out = { workstream: ws, dry: opts.dry === true, cap, used: 0, moves: [] };
|
|
3372
|
+
const done = /* @__PURE__ */ new Set();
|
|
3373
|
+
const ingest = opts.ingest ?? (() => ambientIngest(db, { quiet: true }));
|
|
3374
|
+
const humanStop = () => {
|
|
3375
|
+
const needs = openHumanNeeds(db, opts.caller);
|
|
3376
|
+
if (needs.length === 0) return false;
|
|
3377
|
+
out.stopped = "human";
|
|
3378
|
+
out.human_needed = needs;
|
|
3379
|
+
return true;
|
|
3380
|
+
};
|
|
3381
|
+
const ctx = { db, ws, caller: opts.caller, humanStop, workers };
|
|
3382
|
+
const observe = opts.observe ?? observeWorkers(db);
|
|
3383
|
+
const deadline = Date.now() + timeoutMs;
|
|
3384
|
+
const aborted = () => {
|
|
3385
|
+
if (opts.signal?.aborted !== true) return false;
|
|
3386
|
+
out.stopped = "interrupted";
|
|
3387
|
+
return true;
|
|
3388
|
+
};
|
|
3389
|
+
const told = /* @__PURE__ */ new Set();
|
|
3390
|
+
const tell = (task, reason, agent) => {
|
|
3391
|
+
if (told.has(`${task}|${reason}`)) return;
|
|
3392
|
+
told.add(`${task}|${reason}`);
|
|
3393
|
+
out.moves.push({ kind: "attention", task, ...agent !== void 0 ? { agent } : {}, reason });
|
|
3394
|
+
};
|
|
3395
|
+
const attend = () => {
|
|
3396
|
+
for (const a of exhaustedAfterDeath(db, ws)) tell(a.task, a.reason);
|
|
3397
|
+
};
|
|
3398
|
+
const pass = async () => {
|
|
3399
|
+
let actions;
|
|
3400
|
+
for (; ; ) {
|
|
3401
|
+
if (aborted()) return;
|
|
3402
|
+
if (out.used >= cap) {
|
|
3403
|
+
if (out.stopped === void 0 && (select(db, ws, done) !== void 0 || (actions?.length ?? 0) > 0 || refute && nextCheck(db, ws, opts.caller, done) !== void 0)) {
|
|
3404
|
+
out.stopped = "cap";
|
|
3405
|
+
}
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
if (!out.dry) await ingest();
|
|
3409
|
+
if (aborted()) return;
|
|
3410
|
+
if (humanStop()) return;
|
|
3411
|
+
if (actions === void 0) {
|
|
3412
|
+
const found = out.dry ? [] : await scanRecovery(
|
|
3413
|
+
db,
|
|
3414
|
+
ws,
|
|
3415
|
+
observe,
|
|
3416
|
+
opts.signal !== void 0 ? { signal: opts.signal } : {}
|
|
3417
|
+
);
|
|
3418
|
+
if (aborted()) return;
|
|
3419
|
+
if (out.dry) out.inFlight = inFlightWorkers(db, ws).map((w) => w.task);
|
|
3420
|
+
actions = [];
|
|
3421
|
+
for (const a of found) {
|
|
3422
|
+
if (a.kind === "attention") tell(a.task, a.reason, a.agent);
|
|
3423
|
+
else actions.push(a);
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
const act = actions.shift();
|
|
3427
|
+
if (act !== void 0 && !out.dry) {
|
|
3428
|
+
const move2 = await recover(ctx, act, observe, opts.signal);
|
|
3429
|
+
if (act.kind === "retry") done.add(act.task);
|
|
3430
|
+
out.moves.push(move2);
|
|
3431
|
+
if (counts(move2)) out.used++;
|
|
3432
|
+
if (move2.kind === "failed") {
|
|
3433
|
+
out.stopped = "failed";
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
continue;
|
|
3437
|
+
}
|
|
3438
|
+
const next = select(db, ws, done);
|
|
3439
|
+
if (next === void 0) {
|
|
3440
|
+
const check = refute ? nextCheck(db, ws, opts.caller, done) : void 0;
|
|
3441
|
+
if (check !== void 0) {
|
|
3442
|
+
done.add(check.callId);
|
|
3443
|
+
const move2 = out.dry ? planCheck(check) : await runCheck(db, check, {
|
|
3444
|
+
caller: opts.caller,
|
|
3445
|
+
read: opts.readChecker ?? readChecker,
|
|
3446
|
+
...opts.refuteCli !== void 0 ? { cli: opts.refuteCli } : {},
|
|
3447
|
+
...opts.signal !== void 0 ? { signal: opts.signal } : {}
|
|
3448
|
+
});
|
|
3449
|
+
out.moves.push(move2);
|
|
3450
|
+
if (counts(move2)) out.used++;
|
|
3451
|
+
if (move2.kind === "failed" && !out.dry) {
|
|
3452
|
+
out.stopped = "failed";
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
3455
|
+
continue;
|
|
3456
|
+
}
|
|
3457
|
+
attend();
|
|
3458
|
+
return;
|
|
3459
|
+
}
|
|
3460
|
+
if (workers !== void 0 && needsPane(db, ws, next) && liveWorkers(db, ws) >= workers) {
|
|
3461
|
+
tell(
|
|
3462
|
+
next.task,
|
|
3463
|
+
`worker ceiling ${workers} reached (${liveWorkers(db, ws)} working): no new pane until one finishes`
|
|
3464
|
+
);
|
|
3465
|
+
out.stopped = "workers";
|
|
3466
|
+
return;
|
|
3467
|
+
}
|
|
3468
|
+
done.add(next.task);
|
|
3469
|
+
const move = out.dry ? planMove(db, ws, next.task, next.resume) : next.resume ? await resume(ctx, next.task) : await allocate(ctx, next.task);
|
|
3470
|
+
out.moves.push(move);
|
|
3471
|
+
if (counts(move)) out.used++;
|
|
3472
|
+
if (move.kind === "failed" && !out.dry) {
|
|
3473
|
+
out.stopped = "failed";
|
|
3474
|
+
return;
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
};
|
|
3478
|
+
for (let waits = 0; ; ) {
|
|
3479
|
+
const before = out.used;
|
|
3480
|
+
await pass();
|
|
3481
|
+
const atCeiling = out.stopped === "workers";
|
|
3482
|
+
if (opts.hold !== true || out.dry || out.stopped !== void 0 && !atCeiling) return out;
|
|
3483
|
+
if (waits > 0 && out.used === before) return out;
|
|
3484
|
+
const inFlight = inFlightWorkers(db, ws);
|
|
3485
|
+
const checkers = refute ? checkerRefs(db, ws, opts.caller) : [];
|
|
3486
|
+
if (inFlight.length === 0 && checkers.length === 0) {
|
|
3487
|
+
out.held = { waits, event: "none", ref: null, exit: 0, timedOut: false, open: [] };
|
|
3488
|
+
return out;
|
|
3489
|
+
}
|
|
3490
|
+
const left = deadline - Date.now();
|
|
3491
|
+
if (left <= 0) {
|
|
3492
|
+
out.held = { waits, event: "heartbeat", ref: null, exit: 0, timedOut: true, open: [] };
|
|
3493
|
+
return out;
|
|
3494
|
+
}
|
|
3495
|
+
const refs = checkers.concat(
|
|
3496
|
+
inFlight.flatMap((w) => [
|
|
3497
|
+
{ kind: "task", workstream: ws, id: w.task },
|
|
3498
|
+
{
|
|
3499
|
+
kind: "agent",
|
|
3500
|
+
workstream: w.agent.workstreamName,
|
|
3501
|
+
id: w.agent.name,
|
|
3502
|
+
...w.runs !== null ? { afterRuns: w.runs } : {},
|
|
3503
|
+
...w.incarnation !== null ? { incarnation: w.incarnation } : {}
|
|
3504
|
+
}
|
|
3505
|
+
])
|
|
3506
|
+
);
|
|
3507
|
+
waits++;
|
|
3508
|
+
let res;
|
|
3509
|
+
try {
|
|
3510
|
+
res = await (opts.wait ?? waitWithActions)(db, refs, {
|
|
3511
|
+
callers: opts.caller === "" ? [] : [opts.caller],
|
|
3512
|
+
mode: "first",
|
|
3513
|
+
eventScope: "refs",
|
|
3514
|
+
timeoutMs: left,
|
|
3515
|
+
...opts.signal !== void 0 ? { signal: opts.signal } : {},
|
|
3516
|
+
onStall: opts.onStall ?? "exit",
|
|
3517
|
+
...ctlAgentHooks(db),
|
|
3518
|
+
beforePoll: async () => {
|
|
3519
|
+
if (humanStop()) throw new HoldHumanStop();
|
|
3520
|
+
}
|
|
3521
|
+
});
|
|
3522
|
+
} catch (err) {
|
|
3523
|
+
if (err instanceof HoldHumanStop) return out;
|
|
3524
|
+
if (err instanceof AgentRestartedError) {
|
|
3525
|
+
out.moves.push({ kind: "attention", task: "", reason: err.message });
|
|
3526
|
+
out.held = { waits, event: "restarted", ref: null, exit: 7, timedOut: false, open: [] };
|
|
3527
|
+
return out;
|
|
3528
|
+
}
|
|
3529
|
+
throw err;
|
|
3530
|
+
}
|
|
3531
|
+
out.held = {
|
|
3532
|
+
waits,
|
|
3533
|
+
event: res.event.kind,
|
|
3534
|
+
ref: res.event.ref === null ? null : refText(res.event.ref),
|
|
3535
|
+
exit: res.exit,
|
|
3536
|
+
timedOut: res.timedOut,
|
|
3537
|
+
open: res.refs.filter((x) => !x.fired && !x.dead).slice(0, 8).map((x) => ({
|
|
3538
|
+
ref: refText(x.ref),
|
|
3539
|
+
status: x.status,
|
|
3540
|
+
...x.lastError !== void 0 ? { lastError: x.lastError.slice(0, 200) } : {}
|
|
3541
|
+
}))
|
|
3542
|
+
};
|
|
3543
|
+
if (res.interrupted) {
|
|
3544
|
+
out.stopped = "interrupted";
|
|
3545
|
+
return out;
|
|
3546
|
+
}
|
|
3547
|
+
const freed = atCeiling && res.event.kind === "closed";
|
|
3548
|
+
const checkerEvent = checkers.some(
|
|
3549
|
+
(c) => res.event.ref?.kind === "agent" && res.event.ref.id === c.id
|
|
3550
|
+
);
|
|
3551
|
+
if (res.event.kind === "heartbeat" || !DEAD_OR_ERRORED(res) && !freed && !checkerEvent) {
|
|
3552
|
+
return out;
|
|
3553
|
+
}
|
|
3554
|
+
if (atCeiling) out.stopped = void 0;
|
|
3555
|
+
done.clear();
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
function planMove(db, ws, task, resume2) {
|
|
3559
|
+
const taskId = taskIdFor(db, task, ws);
|
|
3560
|
+
if (resume2 && taskId !== null) {
|
|
3561
|
+
const { record, receipt } = readAttempt(db, taskId);
|
|
3562
|
+
if (record !== void 0 && receipt !== void 0) {
|
|
3563
|
+
const base = { task, attempt: record.attempt, key: record.key, cli: record.cli };
|
|
3564
|
+
const end2 = unreconcilable(db, ws, task, receipt);
|
|
3565
|
+
if (end2 !== void 0) return { kind: end2.state, ...base, reason: end2.reason };
|
|
3566
|
+
return {
|
|
3567
|
+
kind: "replay",
|
|
3568
|
+
...base,
|
|
3569
|
+
agent: receipt.name,
|
|
3570
|
+
state: receipt.state
|
|
3571
|
+
};
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
const planned = planAttempt(db, ws, task);
|
|
3575
|
+
if (!planned.ok) return { kind: "skip", task, reason: planned.reason };
|
|
3576
|
+
const p = planned.plan;
|
|
3577
|
+
return { kind: "spawn", task, attempt: p.attempt, key: p.key, agent: p.name, cli: p.cli };
|
|
3578
|
+
}
|
|
3579
|
+
function unreconcilable(db, ws, task, receipt) {
|
|
3580
|
+
const t = getTask(db, task, ws);
|
|
3581
|
+
if (t === void 0) return { state: "abandoned", reason: "the task is gone" };
|
|
3582
|
+
if (t.status === "CLOSED") return { state: "abandoned", reason: "the task is closed" };
|
|
3583
|
+
const sent = receipt.state === "sending" || receipt.state === "unknown";
|
|
3584
|
+
const lost = (reason) => ({
|
|
3585
|
+
state: sent ? "needs_human" : "abandoned",
|
|
3586
|
+
reason
|
|
3587
|
+
});
|
|
3588
|
+
if (receipt.state === "reserved") {
|
|
3589
|
+
return t.ownerId === null ? void 0 : lost("the task is owned by another agent");
|
|
3590
|
+
}
|
|
3591
|
+
const id = savedIdentity(db, receipt);
|
|
3592
|
+
if (id.stale !== void 0) {
|
|
3593
|
+
return lost(`the agent of this attempt is ${id.stale}`);
|
|
3594
|
+
}
|
|
3595
|
+
if (sent && receipt.transport !== "ctl") {
|
|
3596
|
+
return lost("the send went through the pane: there is no receipt to ask");
|
|
3597
|
+
}
|
|
3598
|
+
return void 0;
|
|
3599
|
+
}
|
|
3600
|
+
function park(ctx, task, record, end2) {
|
|
3601
|
+
if (end2.state === "needs_human" && ctx.caller !== "") {
|
|
3602
|
+
recordHumanNeeded(ctx.db, {
|
|
3603
|
+
workstream: ctx.ws,
|
|
3604
|
+
caller: ctx.caller,
|
|
3605
|
+
reason: `attempt ${record.key} of task ${task} may have been delivered and cannot be confirmed (${end2.reason}); never resent`
|
|
3606
|
+
});
|
|
3607
|
+
}
|
|
3608
|
+
const taskId = taskIdFor(ctx.db, task, ctx.ws);
|
|
3609
|
+
if (taskId !== null) endAttempt(ctx.db, taskId, record.key, end2);
|
|
3610
|
+
return {
|
|
3611
|
+
kind: end2.state,
|
|
3612
|
+
task,
|
|
3613
|
+
attempt: record.attempt,
|
|
3614
|
+
key: record.key,
|
|
3615
|
+
cli: record.cli,
|
|
3616
|
+
reason: end2.reason
|
|
3617
|
+
};
|
|
3618
|
+
}
|
|
3619
|
+
function guardOf(ctx, task, key) {
|
|
3620
|
+
let refusedAt;
|
|
3621
|
+
const guard = (phase) => {
|
|
3622
|
+
const stop = (why) => {
|
|
3623
|
+
refusedAt = phase;
|
|
3624
|
+
throw new SpawnAttemptGuardError(task, key, why);
|
|
3625
|
+
};
|
|
3626
|
+
if (ctx.humanStop()) stop("this caller has an open human need");
|
|
3627
|
+
const taskId = taskIdFor(ctx.db, task, ctx.ws);
|
|
3628
|
+
if (taskId === null || !autoPermitted(ctx.db, taskId)) {
|
|
3629
|
+
stop("it is no longer authorised for this machine");
|
|
3630
|
+
}
|
|
3631
|
+
if (phase === "spawn" && !listReady(ctx.db, ctx.ws).some((t) => t.name === task)) {
|
|
3632
|
+
stop("it is no longer ready");
|
|
3633
|
+
}
|
|
3634
|
+
if (phase === "spawn" && ctx.workers !== void 0 && liveWorkers(ctx.db, ctx.ws) >= ctx.workers) {
|
|
3635
|
+
stop(`the worker ceiling ${ctx.workers} is reached`);
|
|
3636
|
+
}
|
|
3637
|
+
};
|
|
3638
|
+
return { guard, refusedAt: () => refusedAt };
|
|
3639
|
+
}
|
|
3640
|
+
function outcomeMove(kind, task, rec, out) {
|
|
3641
|
+
const reason = out.reason;
|
|
3642
|
+
return {
|
|
3643
|
+
kind,
|
|
3644
|
+
task,
|
|
3645
|
+
attempt: rec.attempt,
|
|
3646
|
+
key: rec.key,
|
|
3647
|
+
agent: out.agent?.name ?? out.receipt.name,
|
|
3648
|
+
cli: rec.cli,
|
|
3649
|
+
state: out.state,
|
|
3650
|
+
...out.stale !== void 0 ? { stale: out.stale } : {},
|
|
3651
|
+
...reason !== void 0 ? { reason } : {},
|
|
3652
|
+
...out.checked !== void 0 ? { checked: out.checked } : {}
|
|
3653
|
+
};
|
|
3654
|
+
}
|
|
3655
|
+
async function drive(ctx, task, kind, rec) {
|
|
3656
|
+
const { db, ws } = ctx;
|
|
3657
|
+
const g = guardOf(ctx, task, rec.key);
|
|
3658
|
+
try {
|
|
3659
|
+
const out = await spawnAutoAttempt(db, {
|
|
3660
|
+
task,
|
|
3661
|
+
text: rec.text,
|
|
3662
|
+
key: rec.key,
|
|
3663
|
+
guard: g.guard,
|
|
3664
|
+
...rec.inPlace === true ? { keepAgent: true } : {},
|
|
3665
|
+
spawn: {
|
|
3666
|
+
name: rec.name,
|
|
3667
|
+
workstream: ws,
|
|
3668
|
+
cli: rec.cli,
|
|
3669
|
+
...rec.worktree ? { worktree: true } : {},
|
|
3670
|
+
nextFree: true
|
|
3671
|
+
}
|
|
3672
|
+
});
|
|
3673
|
+
const move = outcomeMove(kind, task, rec, out);
|
|
3674
|
+
if (kind === "replay" && out.state === "unknown") {
|
|
3675
|
+
return {
|
|
3676
|
+
...park(ctx, task, rec, {
|
|
3677
|
+
state: "needs_human",
|
|
3678
|
+
reason: out.reason ?? "the send could not be confirmed"
|
|
3679
|
+
}),
|
|
3680
|
+
...out.checked !== void 0 ? { checked: out.checked } : {}
|
|
3681
|
+
};
|
|
3682
|
+
}
|
|
3683
|
+
return move;
|
|
3684
|
+
} catch (err) {
|
|
3685
|
+
if (err instanceof NoMultiplexerError) throw err;
|
|
3686
|
+
const base = { task, attempt: rec.attempt, key: rec.key, cli: rec.cli };
|
|
3687
|
+
if (err instanceof SpawnAttemptBusyError) {
|
|
3688
|
+
return { kind: "skip", ...base, reason: `attempt in flight: ${err.message}` };
|
|
3689
|
+
}
|
|
3690
|
+
if (err instanceof SpawnAttemptGuardError) {
|
|
3691
|
+
const kind2 = g.refusedAt() === "send" ? "rolled_back" : "skip";
|
|
3692
|
+
return { kind: kind2, ...base, reason: err.message };
|
|
3693
|
+
}
|
|
3694
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
3695
|
+
const taskId = taskIdFor(db, task, ws);
|
|
3696
|
+
const left = taskId === null ? void 0 : getReceipt(db, taskId, rec.key);
|
|
3697
|
+
if (left !== void 0 && IN_FLIGHT_STATES.has(left.state)) {
|
|
3698
|
+
const sent = left.state === "sending" || left.state === "unknown";
|
|
3699
|
+
park(ctx, task, rec, { state: sent ? "needs_human" : "abandoned", reason });
|
|
3700
|
+
}
|
|
3701
|
+
return { kind: "failed", ...base, reason };
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
async function resume(ctx, task) {
|
|
3705
|
+
const { db, ws } = ctx;
|
|
3706
|
+
const taskId = taskIdFor(db, task, ws);
|
|
3707
|
+
const { record, receipt } = taskId === null ? {} : readAttempt(db, taskId);
|
|
3708
|
+
if (record === void 0 || receipt === void 0) {
|
|
3709
|
+
return { kind: "skip", task, reason: "no stored attempt" };
|
|
3710
|
+
}
|
|
3711
|
+
const end2 = unreconcilable(db, ws, task, receipt);
|
|
3712
|
+
if (end2 !== void 0) return park(ctx, task, record, end2);
|
|
3713
|
+
return drive(ctx, task, "replay", record);
|
|
3714
|
+
}
|
|
3715
|
+
async function allocate(ctx, task) {
|
|
3716
|
+
const reserved = reserveAttempt(ctx.db, ctx.ws, task);
|
|
3717
|
+
if (!reserved.ok) {
|
|
3718
|
+
return {
|
|
3719
|
+
kind: "skip",
|
|
3720
|
+
task,
|
|
3721
|
+
reason: `attempt ${reserved.code === "in-flight" ? "in flight" : reserved.code}: ${reserved.reason}`
|
|
3722
|
+
};
|
|
3723
|
+
}
|
|
3724
|
+
return drive(ctx, task, "spawn", reserved.plan);
|
|
3725
|
+
}
|
|
3726
|
+
async function recover(ctx, act, observe, signal) {
|
|
3727
|
+
const { db, ws } = ctx;
|
|
3728
|
+
const skip = (reason) => ({ kind: "skip", task: act.task, reason });
|
|
3729
|
+
if (ctx.humanStop()) return skip("this caller has an open human need");
|
|
3730
|
+
const taskId = taskIdFor(db, act.task, ws);
|
|
3731
|
+
if (taskId === null || !autoPermitted(db, taskId)) {
|
|
3732
|
+
return skip("it is no longer authorised for this machine");
|
|
3733
|
+
}
|
|
3734
|
+
if (act.kind === "reap") {
|
|
3735
|
+
const a = act.agent;
|
|
3736
|
+
const now2 = getAgent(db, a.name, a.workstreamName);
|
|
3737
|
+
if (now2?.host != null) return skip("a remote worker is never reaped from here");
|
|
3738
|
+
const deleted = deleteAgent(db, a.name, a.workstreamName, {
|
|
3739
|
+
paneId: a.paneId,
|
|
3740
|
+
incarnation: a.incarnation
|
|
3741
|
+
});
|
|
3742
|
+
return deleted ? { kind: "reap", task: act.task, agent: a.name, reason: act.why } : skip("the worker was replaced before it could be reaped");
|
|
3743
|
+
}
|
|
3744
|
+
const { agent } = act.target;
|
|
3745
|
+
const row = getAgent(db, agent.name, ws);
|
|
3746
|
+
if (row === void 0) return skip("the worker is gone");
|
|
3747
|
+
const now = (await observe([row], { ms: OBSERVE_MS, ...signal !== void 0 ? { signal } : {} })).get(workerKey(row));
|
|
3748
|
+
if (now === void 0 || !idleOnCtl(now) || now.incarnation !== act.target.pi || now.runs !== act.target.errorRuns || now.lastError === void 0) {
|
|
3749
|
+
return skip("the worker is no longer idle at the errored run: left to the wait");
|
|
3750
|
+
}
|
|
3751
|
+
const reserved = reserveRetry(db, ws, act.task, act.target);
|
|
3752
|
+
if (!reserved.ok) {
|
|
3753
|
+
return skip(
|
|
3754
|
+
`retry ${reserved.code === "ceiling" ? "ceiling" : reserved.code}: ${reserved.reason}`
|
|
3755
|
+
);
|
|
3756
|
+
}
|
|
3757
|
+
const { task: _t, taskId: _i, workstream: _w, revision: _r, ...rec } = reserved.plan;
|
|
3758
|
+
const move = await drive(ctx, act.task, "retry", rec);
|
|
3759
|
+
return { ...move, reason: move.reason ?? `API error: ${act.error.slice(0, 200)}` };
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
// src/pi-migrate.ts
|
|
3763
|
+
import { createHash } from "crypto";
|
|
3764
|
+
import { lstatSync as lstatSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync, realpathSync as realpathSync2, renameSync } from "fs";
|
|
3765
|
+
import { homedir } from "os";
|
|
3766
|
+
import { dirname as dirname3, isAbsolute, join as join3, relative, resolve as resolve2, sep as sep2 } from "path";
|
|
3767
|
+
import { fileURLToPath } from "url";
|
|
3768
|
+
|
|
3769
|
+
// src/pi-known-skills.ts
|
|
3770
|
+
var KNOWN_SKILL_DIGESTS = [
|
|
3771
|
+
"bab34e7d50b31dd9960f64a8192f78f023ce3e9a5dd6a080bda3c7d3aaf68ba8",
|
|
3772
|
+
// v0.2.0
|
|
3773
|
+
"a76d272878cb90afc4f5a64ef903a0e80dd3eb7fd92dda23bcbba03854786051",
|
|
3774
|
+
// v0.3.0
|
|
3775
|
+
"cb4116919f86bff5c61268cce5059c0965b6c5673d92fb1466ebe654c560425d",
|
|
3776
|
+
// v0.3.1
|
|
3777
|
+
"1fc6a019ee71c06d311070058ba76572f6b778e160a5ebd93faeef32002a67dd",
|
|
3778
|
+
// v0.3.2
|
|
3779
|
+
"4029bcea85a7470798261041c3626b7fea16f9c84d195a3c4266221e7af4fafb",
|
|
3780
|
+
// v0.4.0
|
|
3781
|
+
"272984f7db1812241f8659a71deeac1e7d81367ecf843d6436385d6cec410000",
|
|
3782
|
+
// v0.4.1
|
|
3783
|
+
"7d58aebec9bbb40cad90b6da9e9a369cee0aebed03d194ea153c8e4229fbfe16",
|
|
3784
|
+
// v0.4.5
|
|
3785
|
+
"e4645a869fbd5153a7390d1cbfe2f3842038f895b4f754a4c44e23512270b5c7",
|
|
3786
|
+
// v1.0.0
|
|
3787
|
+
"41479003fe7f7a7d757413dcf31eb46330b9689c70aa0200347aaf6bafd972f1",
|
|
3788
|
+
// v1.0.1
|
|
3789
|
+
"1ba368054d76b931639e3dfe02bd53ed8277e47ab28d989ce3bfb7df7497dc7e",
|
|
3790
|
+
// v1.1.0
|
|
3791
|
+
"e10e2cf93a31a6b24ce1f19951e1a278299b0c33f8020bd293eff807b24fb20f",
|
|
3792
|
+
// v1.1.1
|
|
3793
|
+
"b38c8da4c685b9478aebb13905607bf295af9d9880c7da2aa1e750f24d508e28",
|
|
3794
|
+
// v1.2.0
|
|
3795
|
+
"82c14d2c9cbe7bf81f89e4031aeccee8284ffd4ddddea298547b31e1f78bef39",
|
|
3796
|
+
// v1.3.0
|
|
3797
|
+
"f787792a820b08aaa7cc338931bf239b5b54e3b536d06127aa8da2d9c938ef5a",
|
|
3798
|
+
// v1.3.2
|
|
3799
|
+
"9c5ac614ff9bf3db7d8c5aca3d2d422f8c175efa35533d0c2e174bd74682e073",
|
|
3800
|
+
// v1.4.0
|
|
3801
|
+
"6aa1d11a96d9ccd7587e979b8ef2d48d55d9b486f2ccfd160e228c57671cf340",
|
|
3802
|
+
// v1.6.0
|
|
3803
|
+
"c33778ae1c9efbb3306395e970c2e9d4a859e19aea11d6ba0429c38ea7f9d05a",
|
|
3804
|
+
// v2.0.0
|
|
3805
|
+
"8aabe979d125399274e6ed42c40353f775be61730e4c07c882d16f68d6fd3da8",
|
|
3806
|
+
// v3.0.0
|
|
3807
|
+
"bce15b51a9d8b1d0d277e7e3a79ff902d7ab306085c51eb5cb7e8e126b92564c",
|
|
3808
|
+
// v3.1.0
|
|
3809
|
+
"7dba4e5b3058e14c4314f886a26a5b5f50b99a3180d5fbe81b4751ac4e187f49",
|
|
3810
|
+
// v3.10.0
|
|
3811
|
+
"177fdc1ab4c273eef1ad004732c35fc4820a72b57991e4754d5f53063d782e49",
|
|
3812
|
+
// v3.10.1
|
|
3813
|
+
"a648aed46f7e1120ef8fbf021748a40c5434621e036fc443a817899a87dbb028",
|
|
3814
|
+
// v3.11.0
|
|
3815
|
+
"ace13205c56b40bb8de0265e4f524eec18a0583b508d7951eb6185c102e4470a",
|
|
3816
|
+
// v3.13.0
|
|
3817
|
+
"ff47c6b99435f0b30ef9fea3f6d8384c5718d7c952bbe3cada79b82ea70680fc",
|
|
3818
|
+
// v3.13.2
|
|
3819
|
+
"76e5b12be5e78f4b53981718938c33377f896be41b70be88c6feedc0e101af2b",
|
|
3820
|
+
// v3.13.3
|
|
3821
|
+
"3bd49e0b22440bd670e915683409e8929579128a9ea3f50d6c7ea3eb6ea124cc",
|
|
3822
|
+
// v3.2.0
|
|
3823
|
+
"13428e619c423d8e3dc39772343ac6be12b08b0e648113d791364c447d4f6dee",
|
|
3824
|
+
// v3.2.1
|
|
3825
|
+
"258fc3f9212387669f4b8a9442cdebfd04736378fedc4055acf2096b7759e962",
|
|
3826
|
+
// v3.3.0
|
|
3827
|
+
"f1a31d16d644bc484b3755ad9019e760098e8f089359cbd12dad3e4570ef14c0",
|
|
3828
|
+
// v3.4.0
|
|
3829
|
+
"75cb30f7291b5613df1ffa11d497ff91972c576c625141c6cb7a5ac34f206d6b",
|
|
3830
|
+
// v3.5.0
|
|
3831
|
+
"4123cd6e5a19b27bae298a1b56896e06237e8187ace51ccd8aef99c6486a6800",
|
|
3832
|
+
// v3.7.0
|
|
3833
|
+
"848eb0fe7fff3108162027f65fe1482675e5ecf074312e9799e6056c6212800f",
|
|
3834
|
+
// v3.8.0
|
|
3835
|
+
"7d9784b1c881f545e376a5ddb37da66b589e12b4dadbb204db69767835887546"
|
|
3836
|
+
// v3.8.1
|
|
3837
|
+
];
|
|
3838
|
+
|
|
3839
|
+
// src/pi-migrate.ts
|
|
3840
|
+
var MU_SHIM_MARKER = "// mu:shim";
|
|
3841
|
+
function lstatOrUndefined(path) {
|
|
3842
|
+
try {
|
|
3843
|
+
return lstatSync2(path);
|
|
3844
|
+
} catch {
|
|
3845
|
+
return void 0;
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
function freeName(dest) {
|
|
3849
|
+
if (!lstatOrUndefined(dest)) return dest;
|
|
3850
|
+
for (let i = 1; ; i++) {
|
|
3851
|
+
const candidate = `${dest}.${i}`;
|
|
3852
|
+
if (!lstatOrUndefined(candidate)) return candidate;
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
function park2(from, dest) {
|
|
3856
|
+
const to = freeName(dest);
|
|
3857
|
+
mkdirSync2(join3(to, ".."), { recursive: true });
|
|
3858
|
+
renameSync(from, to);
|
|
3859
|
+
return to;
|
|
3860
|
+
}
|
|
3861
|
+
var SHIM_HEAD = `${MU_SHIM_MARKER}
|
|
3862
|
+
// Generated by \`mu link pi\`. Do not edit.
|
|
3863
|
+
//
|
|
3864
|
+
// A re-export, not a copy: the extension code lives in the mu install, so
|
|
3865
|
+
// upgrading mu upgrades the extension. Re-run \`mu link pi\` only if the
|
|
3866
|
+
// install path itself moves.
|
|
3867
|
+
const { default: extension } = await import(`;
|
|
3868
|
+
var SHIM_TAIL = `);
|
|
3869
|
+
export default extension;
|
|
3870
|
+
`;
|
|
3871
|
+
function isGeneratedImport(arg) {
|
|
3872
|
+
try {
|
|
3873
|
+
const url = JSON.parse(arg);
|
|
3874
|
+
return typeof url === "string" && JSON.stringify(url) === arg && new URL(url).href === url && url.startsWith("file:///") && url.endsWith("/dist/extension/mu-pi.js");
|
|
3875
|
+
} catch {
|
|
3876
|
+
return false;
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
function isMuShimText(text) {
|
|
3880
|
+
return text.startsWith(SHIM_HEAD) && text.endsWith(SHIM_TAIL) && isGeneratedImport(text.slice(SHIM_HEAD.length, text.length - SHIM_TAIL.length));
|
|
3881
|
+
}
|
|
3882
|
+
function isMuShim(path) {
|
|
3883
|
+
try {
|
|
3884
|
+
return isMuShimText(readFileSync(path, "utf8"));
|
|
3885
|
+
} catch {
|
|
3886
|
+
return false;
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
function defaultPackageRoot() {
|
|
3890
|
+
return resolve2(dirname3(fileURLToPath(import.meta.url)), "..");
|
|
3891
|
+
}
|
|
3892
|
+
function skillDirDigest(dir) {
|
|
3893
|
+
const lines = [];
|
|
3894
|
+
const walk = (d) => {
|
|
3895
|
+
for (const ent of readdirSync(d, { withFileTypes: true })) {
|
|
3896
|
+
const p = join3(d, ent.name);
|
|
3897
|
+
if (ent.isDirectory()) {
|
|
3898
|
+
if (!walk(p)) return false;
|
|
3899
|
+
} else if (ent.isFile()) {
|
|
3900
|
+
const sum = createHash("sha256").update(readFileSync(p)).digest("hex");
|
|
3901
|
+
lines.push(`${relative(dir, p).split(sep2).join("/")}\0${sum}
|
|
3902
|
+
`);
|
|
3903
|
+
} else return false;
|
|
3904
|
+
}
|
|
3905
|
+
return true;
|
|
3906
|
+
};
|
|
3907
|
+
try {
|
|
3908
|
+
if (!walk(dir)) return void 0;
|
|
3909
|
+
} catch {
|
|
3910
|
+
return void 0;
|
|
3911
|
+
}
|
|
3912
|
+
return createHash("sha256").update(lines.sort().join("")).digest("hex");
|
|
3913
|
+
}
|
|
3914
|
+
function isMuSkill(path, st, root, known) {
|
|
3915
|
+
try {
|
|
3916
|
+
if (st.isSymbolicLink()) {
|
|
3917
|
+
const own = realpathSync2(join3(root, "skills", "mu"));
|
|
3918
|
+
const rel = relative(own, realpathSync2(path));
|
|
3919
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
3920
|
+
}
|
|
3921
|
+
if (!st.isDirectory()) return false;
|
|
3922
|
+
const digest = skillDirDigest(path);
|
|
3923
|
+
return digest !== void 0 && known.includes(digest);
|
|
3924
|
+
} catch {
|
|
3925
|
+
return false;
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
function migratePi(opts = {}) {
|
|
3929
|
+
const home = opts.home ?? process.env.MU_PI_HOME ?? homedir();
|
|
3930
|
+
const extPath = join3(home, ".pi", "agent", "extensions", "mu.ts");
|
|
3931
|
+
const skillPath = join3(home, ".agents", "skills", "mu");
|
|
3932
|
+
const extSt = lstatOrUndefined(extPath);
|
|
3933
|
+
const extension = {
|
|
3934
|
+
path: extPath,
|
|
3935
|
+
state: !extSt ? "absent" : extSt.isFile() && isMuShim(extPath) ? "shim" : "foreign",
|
|
3936
|
+
moved: false
|
|
3937
|
+
};
|
|
3938
|
+
const skSt = lstatOrUndefined(skillPath);
|
|
3939
|
+
const skill = {
|
|
3940
|
+
path: skillPath,
|
|
3941
|
+
state: !skSt ? "absent" : isMuSkill(
|
|
3942
|
+
skillPath,
|
|
3943
|
+
skSt,
|
|
3944
|
+
opts.packageRoot ?? defaultPackageRoot(),
|
|
3945
|
+
opts.knownSkillDigests ?? KNOWN_SKILL_DIGESTS
|
|
3946
|
+
) ? "shim" : "foreign",
|
|
3947
|
+
moved: false
|
|
3948
|
+
};
|
|
3949
|
+
if (opts.apply === true) {
|
|
3950
|
+
if (extension.state === "shim") {
|
|
3951
|
+
extension.to = park2(extPath, join3(home, ".pi", "agent", "disabled", "mu.ts.shim"));
|
|
3952
|
+
extension.moved = true;
|
|
3953
|
+
}
|
|
3954
|
+
if (skill.state === "shim") {
|
|
3955
|
+
skill.to = park2(skillPath, join3(home, ".agents", "disabled", "skills", "mu"));
|
|
3956
|
+
skill.moved = true;
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
const guidance = [];
|
|
3960
|
+
if (opts.apply !== true && (extension.state === "shim" || skill.state === "shim")) {
|
|
3961
|
+
guidance.push("Nothing moved. Re-run with --apply to move the mu-owned files aside.");
|
|
3962
|
+
}
|
|
3963
|
+
for (const e of [extension, skill]) {
|
|
3964
|
+
if (e.state === "foreign") {
|
|
3965
|
+
guidance.push(
|
|
3966
|
+
`Left ${e.path} in place: not proven mu's. If it duplicates the package, move it aside yourself.`
|
|
3967
|
+
);
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
if (extension.moved || skill.moved) {
|
|
3971
|
+
guidance.push(
|
|
3972
|
+
"Running pi agents keep the old extension until they restart: restart pi or run /reload."
|
|
3973
|
+
);
|
|
3974
|
+
}
|
|
3975
|
+
guidance.push("Install the package once: pi install npm:@mu-crew/mu");
|
|
3976
|
+
return { extension, skill, guidance };
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
// src/remote-collect.ts
|
|
3980
|
+
import { createHash as createHash2 } from "crypto";
|
|
3981
|
+
import { rm } from "fs/promises";
|
|
3982
|
+
import { dirname as dirname4, join as join4, resolve as resolve3 } from "path";
|
|
3983
|
+
var REMOTE_COLLECT_TIMEOUT_MS = 3e5;
|
|
3984
|
+
var RemoteCollectUnsupportedError = class extends Error {
|
|
3985
|
+
name = "RemoteCollectUnsupportedError";
|
|
3986
|
+
constructor(agent, why) {
|
|
3987
|
+
super(say("remote.collect_unsupported", agent, why));
|
|
3988
|
+
}
|
|
3989
|
+
};
|
|
3990
|
+
var RemoteCollectError = class extends Error {
|
|
3991
|
+
constructor(host, detail) {
|
|
3992
|
+
super(say("remote.collect_failed", host, detail));
|
|
3993
|
+
this.host = host;
|
|
3994
|
+
}
|
|
3995
|
+
host;
|
|
3996
|
+
name = "RemoteCollectError";
|
|
3997
|
+
};
|
|
3998
|
+
function recoveryTarget(db, rec) {
|
|
3999
|
+
return {
|
|
4000
|
+
host: rec.host,
|
|
4001
|
+
cwd: rec.cwd,
|
|
4002
|
+
base: rec.base,
|
|
4003
|
+
collectedHead: rec.collectedHead,
|
|
4004
|
+
ownerId: rec.agentId,
|
|
4005
|
+
record: (head) => head === rec.collectedHead ? recoveryHolds(db, rec) : setRecoveryCollectedHead(db, rec, head),
|
|
4006
|
+
recheck: () => {
|
|
4007
|
+
assertRecoveryUnchanged(db, rec);
|
|
4008
|
+
assertRecoveryUsable(db, rec);
|
|
4009
|
+
},
|
|
4010
|
+
lost: () => new RemoteRecoveryRefusedError(
|
|
4011
|
+
rec.workstream,
|
|
4012
|
+
rec.name,
|
|
4013
|
+
"changed",
|
|
4014
|
+
`recovery ${rec.agentId} changed while its commits were being collected; the pin was restored`,
|
|
4015
|
+
[rec.agentId]
|
|
4016
|
+
)
|
|
4017
|
+
};
|
|
4018
|
+
}
|
|
4019
|
+
function collectRef(workstream, name) {
|
|
4020
|
+
return `refs/mu/collect/${workstream}/${name}`;
|
|
4021
|
+
}
|
|
4022
|
+
var firstLine = (s) => s.split("\n").find((l) => l.trim() !== "")?.trim() ?? "";
|
|
4023
|
+
function repoLockPath(db, repo) {
|
|
4024
|
+
const base = db.memory || db.name === "" ? locksDir() : join4(dirname4(db.name), "locks");
|
|
4025
|
+
return join4(base, `collect-${createHash2("sha256").update(repo).digest("hex").slice(0, 16)}.lock`);
|
|
4026
|
+
}
|
|
4027
|
+
async function collectRemoteAgent(db, opts) {
|
|
4028
|
+
const { workstream, name, repo } = opts;
|
|
4029
|
+
return withAgentLock(
|
|
4030
|
+
db,
|
|
4031
|
+
workstream,
|
|
4032
|
+
name,
|
|
4033
|
+
() => withFileLock(
|
|
4034
|
+
repoLockPath(db, repo),
|
|
4035
|
+
`collect ${repo}`,
|
|
4036
|
+
async () => {
|
|
4037
|
+
const agent = getAgent(db, name, workstream);
|
|
4038
|
+
const rec = opts.recoveryId !== void 0 || agent === void 0 ? selectRemoteRecovery(db, workstream, name, opts.recoveryId) : void 0;
|
|
4039
|
+
if (rec === void 0 && opts.recoveryId !== void 0) {
|
|
4040
|
+
throw recoveryGone(workstream, name, opts.recoveryId);
|
|
4041
|
+
}
|
|
4042
|
+
let target;
|
|
4043
|
+
if (rec !== void 0) {
|
|
4044
|
+
assertRecoveryUsable(db, rec);
|
|
4045
|
+
target = recoveryTarget(db, rec);
|
|
4046
|
+
} else if (agent !== void 0) {
|
|
4047
|
+
target = agentTarget(db, agent);
|
|
4048
|
+
} else {
|
|
4049
|
+
throw new AgentNotFoundError(name, workstream);
|
|
4050
|
+
}
|
|
4051
|
+
const { host, cwd, base } = target;
|
|
4052
|
+
const r = await withHostChannel(
|
|
4053
|
+
db,
|
|
4054
|
+
host,
|
|
4055
|
+
`collect:${workstream}/${name}`,
|
|
4056
|
+
async (profile, lease) => {
|
|
4057
|
+
if (rec !== void 0) assertRecoveryProfile(rec, profile);
|
|
4058
|
+
const fetchHead = await git(["rev-parse", "--git-path", "FETCH_HEAD"], repo);
|
|
4059
|
+
await rm(resolve3(repo, fetchHead), { force: true });
|
|
4060
|
+
return runLeasedTemplate(
|
|
4061
|
+
db,
|
|
4062
|
+
lease,
|
|
4063
|
+
profile,
|
|
4064
|
+
"fetch",
|
|
4065
|
+
{ cwd },
|
|
4066
|
+
REMOTE_COLLECT_TIMEOUT_MS,
|
|
4067
|
+
repo
|
|
4068
|
+
);
|
|
4069
|
+
},
|
|
4070
|
+
{ paneAlive: localPaneAlive }
|
|
4071
|
+
);
|
|
4072
|
+
if (r.exitCode === null) {
|
|
4073
|
+
throw new RemoteCollectError(
|
|
4074
|
+
host,
|
|
4075
|
+
`timed out after ${REMOTE_COLLECT_TIMEOUT_MS}ms; nothing recorded`
|
|
4076
|
+
);
|
|
4077
|
+
}
|
|
4078
|
+
if (r.exitCode !== 0) {
|
|
4079
|
+
const why = firstLine(r.stderr) || firstLine(r.stdout);
|
|
4080
|
+
throw new RemoteCollectError(
|
|
4081
|
+
host,
|
|
4082
|
+
`fetch exited ${r.exitCode}${why === "" ? "" : `: ${why}`}; nothing recorded`
|
|
4083
|
+
);
|
|
4084
|
+
}
|
|
4085
|
+
let head;
|
|
4086
|
+
try {
|
|
4087
|
+
head = await git(["rev-parse", "--verify", "FETCH_HEAD^{commit}"], repo);
|
|
4088
|
+
} catch {
|
|
4089
|
+
throw new RemoteCollectError(
|
|
4090
|
+
host,
|
|
4091
|
+
"the fetch template succeeded but left no commit in FETCH_HEAD (does it `git fetch`?); nothing recorded"
|
|
4092
|
+
);
|
|
4093
|
+
}
|
|
4094
|
+
target.recheck();
|
|
4095
|
+
const ref = collectRef(workstream, name);
|
|
4096
|
+
const prior = await git(["rev-parse", "--verify", "-q", ref], repo).catch(() => void 0);
|
|
4097
|
+
if (prior !== void 0 && prior !== head) {
|
|
4098
|
+
const holders = pinnedByOthers(db, { workstream, name, agentId: target.ownerId }, prior);
|
|
4099
|
+
if (holders.length > 0) {
|
|
4100
|
+
throw new RemoteRecoveryRefusedError(
|
|
4101
|
+
workstream,
|
|
4102
|
+
name,
|
|
4103
|
+
"pin",
|
|
4104
|
+
`${ref} pins ${prior.slice(0, 12)}, the collected HEAD of recovery ${holders.join(", ")}; close that recovery first, then collect`,
|
|
4105
|
+
holders
|
|
4106
|
+
);
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
await git(["update-ref", ref, head], repo);
|
|
4110
|
+
if (await git(["rev-parse", "--verify", ref], repo) !== head) {
|
|
4111
|
+
throw new RemoteCollectError(host, `could not pin ${ref}; nothing recorded`);
|
|
4112
|
+
}
|
|
4113
|
+
const changed = head !== (target.collectedHead ?? base);
|
|
4114
|
+
await recordCollected(target, head, ref, repo, prior);
|
|
4115
|
+
return {
|
|
4116
|
+
head,
|
|
4117
|
+
ref,
|
|
4118
|
+
merge: `git merge ${head}`,
|
|
4119
|
+
changed,
|
|
4120
|
+
...rec === void 0 ? {} : { recoveryId: rec.agentId }
|
|
4121
|
+
};
|
|
4122
|
+
},
|
|
4123
|
+
{
|
|
4124
|
+
strict: true,
|
|
4125
|
+
onUnavailable: () => {
|
|
4126
|
+
throw new AgentLockBusyError(name, workstream);
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
)
|
|
4130
|
+
);
|
|
4131
|
+
}
|
|
4132
|
+
function agentTarget(db, agent) {
|
|
4133
|
+
const { host, cwd, base } = agent;
|
|
4134
|
+
if (host === null || cwd === null) {
|
|
4135
|
+
throw new RemoteCollectUnsupportedError(agent.name, "it is a local agent");
|
|
4136
|
+
}
|
|
4137
|
+
if (base === null) {
|
|
4138
|
+
throw new RemoteCollectUnsupportedError(
|
|
4139
|
+
agent.name,
|
|
4140
|
+
"it was spawned with --cwd, not --worktree: there is no remote worktree of its own"
|
|
4141
|
+
);
|
|
4142
|
+
}
|
|
4143
|
+
const fence = "id = ? AND pane_id = ? AND host IS ? AND cwd IS ? AND base IS ?";
|
|
4144
|
+
const who = [agent.incarnation, agent.paneId, agent.host, agent.cwd, agent.base];
|
|
4145
|
+
return {
|
|
4146
|
+
host,
|
|
4147
|
+
cwd,
|
|
4148
|
+
base,
|
|
4149
|
+
collectedHead: agent.collectedHead,
|
|
4150
|
+
ownerId: agent.incarnation,
|
|
4151
|
+
// An unchanged head writes nothing, but is still fenced: success names this incarnation.
|
|
4152
|
+
record: (head) => head === agent.collectedHead ? db.prepare(`SELECT 1 FROM agents WHERE ${fence}`).all(...who).length === 1 : db.prepare(`UPDATE agents SET collected_head = ?, updated_at = ? WHERE ${fence}`).run(head, (/* @__PURE__ */ new Date()).toISOString(), ...who).changes === 1,
|
|
4153
|
+
recheck: () => {
|
|
4154
|
+
const now = getAgent(db, agent.name, agent.workstreamName);
|
|
4155
|
+
if (now?.incarnation !== agent.incarnation || now.paneId !== agent.paneId) {
|
|
4156
|
+
throw new AgentReplacedError(agent.name, agent.workstreamName, "collected");
|
|
4157
|
+
}
|
|
4158
|
+
},
|
|
4159
|
+
lost: () => new AgentReplacedError(agent.name, agent.workstreamName, "collected")
|
|
4160
|
+
};
|
|
4161
|
+
}
|
|
4162
|
+
async function recordCollected(target, head, ref, repo, prior) {
|
|
4163
|
+
if (target.record(head)) return;
|
|
4164
|
+
await (prior === void 0 ? git(["update-ref", "-d", ref, head], repo) : git(["update-ref", ref, prior, head], repo)).catch(() => {
|
|
4165
|
+
});
|
|
4166
|
+
throw target.lost();
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
// src/tasks/sort.ts
|
|
4170
|
+
var TASK_SORT_KEYS = ["roi", "recency", "age", "id"];
|
|
4171
|
+
function isTaskSortKey(s) {
|
|
4172
|
+
return TASK_SORT_KEYS.includes(s);
|
|
4173
|
+
}
|
|
4174
|
+
function roiOf(t) {
|
|
4175
|
+
return t.effortDays > 0 ? t.impact / t.effortDays : Number.POSITIVE_INFINITY;
|
|
4176
|
+
}
|
|
4177
|
+
function byRoiDesc(a, b) {
|
|
4178
|
+
if (a.scored !== b.scored) return a.scored ? -1 : 1;
|
|
4179
|
+
if (!a.scored) {
|
|
4180
|
+
const byCreated = compareStrings(a.createdAt, b.createdAt);
|
|
4181
|
+
return byCreated !== 0 ? byCreated : compareStrings(a.name, b.name);
|
|
4182
|
+
}
|
|
4183
|
+
const ra = roiOf(a);
|
|
4184
|
+
const rb = roiOf(b);
|
|
4185
|
+
if (rb !== ra) return rb - ra;
|
|
4186
|
+
if (a.effortDays !== b.effortDays) return a.effortDays - b.effortDays;
|
|
4187
|
+
return compareStrings(a.name, b.name);
|
|
4188
|
+
}
|
|
4189
|
+
function sortTasks(tasks, key) {
|
|
4190
|
+
const out = tasks.slice();
|
|
4191
|
+
switch (key) {
|
|
4192
|
+
case "roi":
|
|
4193
|
+
return out.sort(byRoiDesc);
|
|
4194
|
+
case "recency":
|
|
4195
|
+
return out.sort((a, b) => {
|
|
4196
|
+
const byUpdated = compareStrings(b.updatedAt, a.updatedAt);
|
|
4197
|
+
return byUpdated !== 0 ? byUpdated : compareStrings(a.name, b.name);
|
|
4198
|
+
});
|
|
4199
|
+
case "age":
|
|
4200
|
+
return out.sort((a, b) => {
|
|
4201
|
+
const byCreated = compareStrings(a.createdAt, b.createdAt);
|
|
4202
|
+
return byCreated !== 0 ? byCreated : compareStrings(a.name, b.name);
|
|
4203
|
+
});
|
|
4204
|
+
case "id":
|
|
4205
|
+
return out.sort((a, b) => compareStrings(a.name, b.name));
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
function relTimeBasisForSort(key) {
|
|
4209
|
+
if (key === "recency") return "updatedAt";
|
|
4210
|
+
if (key === "age") return "createdAt";
|
|
4211
|
+
return null;
|
|
4212
|
+
}
|
|
4213
|
+
|
|
4214
|
+
// src/state.ts
|
|
4215
|
+
var RECENT_CLOSED = 5;
|
|
4216
|
+
var BIG_WORKSTREAM_TASKS = 300;
|
|
4217
|
+
function taskActivity(db, workstream) {
|
|
4218
|
+
const prefix = `${workstream}/`;
|
|
4219
|
+
const rows = db.prepare(
|
|
4220
|
+
`SELECT entity, key, MAX(seq) AS seq FROM ops
|
|
4221
|
+
WHERE entity IN ('task', 'note', 'edge') AND key >= ? AND key < ?
|
|
4222
|
+
GROUP BY entity, key`
|
|
4223
|
+
).all(prefix, `${workstream}0`);
|
|
4224
|
+
const out = /* @__PURE__ */ new Map();
|
|
4225
|
+
const bump = (id, seq) => {
|
|
4226
|
+
if (seq > (out.get(id) ?? 0)) out.set(id, seq);
|
|
4227
|
+
};
|
|
4228
|
+
for (const r of rows) {
|
|
4229
|
+
if (r.entity === "task") bump(r.key.slice(prefix.length), r.seq);
|
|
4230
|
+
else if (r.entity === "note") bump(r.key.slice(prefix.length).split("#")[0] ?? "", r.seq);
|
|
4231
|
+
else {
|
|
4232
|
+
const [from, to] = r.key.split("->");
|
|
4233
|
+
if (from !== void 0) bump(from.slice(prefix.length), r.seq);
|
|
4234
|
+
if (to !== void 0) bump(to.slice(prefix.length), r.seq);
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
return out;
|
|
4238
|
+
}
|
|
4239
|
+
function stateTask(row, activity) {
|
|
4240
|
+
return {
|
|
4241
|
+
id: row.name,
|
|
4242
|
+
title: row.title,
|
|
4243
|
+
status: row.status,
|
|
4244
|
+
substate: row.substate,
|
|
4245
|
+
impact: row.impact,
|
|
4246
|
+
effort_days: row.effortDays,
|
|
4247
|
+
owner: row.status === "IN_PROGRESS" ? row.ownerName : null,
|
|
4248
|
+
kind: row.kind,
|
|
4249
|
+
wave: row.wave,
|
|
4250
|
+
updated_at: row.updatedAt,
|
|
4251
|
+
activity_seq: activity.get(row.name) ?? 0
|
|
4252
|
+
};
|
|
4253
|
+
}
|
|
4254
|
+
function byActivityDesc(a, b) {
|
|
4255
|
+
if (a.activity_seq !== b.activity_seq) return b.activity_seq - a.activity_seq;
|
|
4256
|
+
if (a.updated_at !== b.updated_at) return a.updated_at < b.updated_at ? 1 : -1;
|
|
4257
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
4258
|
+
}
|
|
4259
|
+
var FINISHED_CALL = ["done", "failed", "cancelled"];
|
|
4260
|
+
function delegatesOf(db, workstream, caller) {
|
|
4261
|
+
if (caller === null) return { delegates: [], waves: [] };
|
|
4262
|
+
const calls = listDelegates(db, caller, workstream);
|
|
4263
|
+
const delegates = calls.filter((c) => !FINISHED_CALL.includes(c.state)).map((c) => ({
|
|
4264
|
+
call_id: c.callId,
|
|
4265
|
+
task: c.task,
|
|
4266
|
+
state: c.state,
|
|
4267
|
+
wave: c.wave,
|
|
4268
|
+
judges: c.record ? `${c.record.workstream}/${c.record.task}` : null,
|
|
4269
|
+
agent: c.agent,
|
|
4270
|
+
verdict: c.verdict,
|
|
4271
|
+
error: c.error,
|
|
4272
|
+
updated_at: c.updatedAt
|
|
4273
|
+
}));
|
|
4274
|
+
const labels = [...new Set(calls.flatMap((c) => c.wave === null ? [] : [c.wave]))].reverse();
|
|
4275
|
+
const waves = labels.map((wave) => waveOf(verdictTally(db, workstream, wave, [caller]), wave));
|
|
4276
|
+
return { delegates, waves };
|
|
4277
|
+
}
|
|
4278
|
+
function waveOf(t, wave) {
|
|
4279
|
+
const sum = (k) => t.findings.reduce((n, f) => n + f[k], 0);
|
|
4280
|
+
const w = {
|
|
4281
|
+
wave,
|
|
4282
|
+
confirmed: sum("confirmed"),
|
|
4283
|
+
refuted: sum("refuted"),
|
|
4284
|
+
unverified: sum("unverified"),
|
|
4285
|
+
pending: sum("pending")
|
|
4286
|
+
};
|
|
4287
|
+
return {
|
|
4288
|
+
...w,
|
|
4289
|
+
calls: w.confirmed + w.refuted + w.unverified + w.pending,
|
|
4290
|
+
queued: t.queued,
|
|
4291
|
+
running: t.running,
|
|
4292
|
+
refused: t.refused
|
|
4293
|
+
};
|
|
4294
|
+
}
|
|
4295
|
+
function countsOf(db, wsId) {
|
|
4296
|
+
const n = (sql) => db.prepare(sql).get(wsId).n;
|
|
4297
|
+
const by = (cond) => n(`SELECT count(*) AS n FROM tasks WHERE workstream_id = ? AND ${cond}`);
|
|
4298
|
+
return {
|
|
4299
|
+
tasks: by("1 = 1"),
|
|
4300
|
+
open: by("status = 'OPEN'"),
|
|
4301
|
+
parked: by("status = 'OPEN' AND substate = 'parked'"),
|
|
4302
|
+
triage: by("status = 'OPEN' AND substate = 'triage'"),
|
|
4303
|
+
ready: n("SELECT count(*) AS n FROM ready WHERE workstream_id = ?"),
|
|
4304
|
+
blocked: n("SELECT count(*) AS n FROM blocked WHERE workstream_id = ?"),
|
|
4305
|
+
in_progress: by("status = 'IN_PROGRESS'"),
|
|
4306
|
+
closed: by("status = 'CLOSED'")
|
|
4307
|
+
};
|
|
4308
|
+
}
|
|
4309
|
+
async function meOf(db, caller, self) {
|
|
4310
|
+
if (self === null) return { caller: caller ?? "", agent: null, owned_tasks: [] };
|
|
4311
|
+
const ws = self.workstreamName;
|
|
4312
|
+
const wsRow = db.prepare("SELECT id, torn_down_at FROM workstreams WHERE name = ?").get(ws);
|
|
4313
|
+
const [live] = await readLiveAgents(db, ws, self.name);
|
|
4314
|
+
const live_ = live === void 0 ? null : liveAgentToOutput(live, wsRow?.id ?? 0);
|
|
4315
|
+
const alive = wsRow !== void 0 && wsRow.torn_down_at === null;
|
|
4316
|
+
const activity = alive ? taskActivity(db, ws) : /* @__PURE__ */ new Map();
|
|
4317
|
+
return {
|
|
4318
|
+
caller: caller ?? `${ws}/${self.name}`,
|
|
4319
|
+
agent: live_,
|
|
4320
|
+
owned_tasks: alive ? listTasksByOwner(db, ws, self.name).map((r) => stateTask(r, activity)).sort(byActivityDesc) : []
|
|
4321
|
+
};
|
|
4322
|
+
}
|
|
4323
|
+
function callerWorkstreams(db, caller) {
|
|
4324
|
+
const live = new Set(
|
|
4325
|
+
db.prepare("SELECT name FROM workstreams WHERE torn_down_at IS NULL").all().map((r) => r.name)
|
|
4326
|
+
);
|
|
4327
|
+
const names = /* @__PURE__ */ new Set([
|
|
4328
|
+
...listDelegates(db, caller).map((c) => c.workstream),
|
|
4329
|
+
...openHumanNeeds(db, caller).map((h) => h.workstream)
|
|
4330
|
+
]);
|
|
4331
|
+
return [...names].filter((n) => live.has(n)).sort();
|
|
4332
|
+
}
|
|
4333
|
+
async function stateSnapshot(db, opts) {
|
|
4334
|
+
const needs = opts.caller === null ? [] : openHumanNeeds(db, opts.caller);
|
|
4335
|
+
const workstreams = [];
|
|
4336
|
+
let me = null;
|
|
4337
|
+
for (const ws of opts.workstreams) {
|
|
4338
|
+
const wsId = tryResolveLiveWorkstreamId(db, ws);
|
|
4339
|
+
if (wsId === null) throw new WorkstreamNotFoundError(ws);
|
|
4340
|
+
const activity = taskActivity(db, ws);
|
|
4341
|
+
const task = (r) => stateTask(r, activity);
|
|
4342
|
+
const state = {
|
|
4343
|
+
workstream: ws,
|
|
4344
|
+
workstream_id: wsId,
|
|
4345
|
+
counts: countsOf(db, wsId),
|
|
4346
|
+
ready: listReady(db, ws).sort(byRoiDesc).map(task),
|
|
4347
|
+
...delegatesOf(db, ws, opts.caller),
|
|
4348
|
+
human_needs: needs.filter((h) => h.workstream === ws).map((h) => ({ id: h.id, reason: h.reason }))
|
|
4349
|
+
};
|
|
4350
|
+
if (opts.me === void 0) {
|
|
4351
|
+
state.in_progress = listInProgress(db, ws).map(task).sort(byActivityDesc);
|
|
4352
|
+
state.blocked = listBlocked(db, ws).map(task).sort(byActivityDesc);
|
|
4353
|
+
state.triage = listTasks(db, ws, { status: "OPEN", substate: "triage" }).map(task).sort(byActivityDesc);
|
|
4354
|
+
state.recent_closed = listTasks(db, ws, { status: "CLOSED" }).map(task).sort(byActivityDesc).slice(0, RECENT_CLOSED);
|
|
4355
|
+
state.agents = (await readLiveAgents(db, ws)).map((a) => liveAgentToOutput(a, wsId));
|
|
4356
|
+
const { tasks, open, in_progress } = state.counts;
|
|
4357
|
+
const unfinished = open + in_progress;
|
|
4358
|
+
if (tasks >= BIG_WORKSTREAM_TASKS && unfinished * 10 < tasks) {
|
|
4359
|
+
state.hint = `${tasks} tasks, ${unfinished} open; start the next effort in a new <project>-<purpose> workstream`;
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
workstreams.push(state);
|
|
4363
|
+
}
|
|
4364
|
+
if (opts.me !== void 0) me = await meOf(db, opts.caller, opts.me.agent);
|
|
4365
|
+
return { caller: opts.caller, me, workstreams };
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
// src/wait-resume.ts
|
|
4369
|
+
import { randomBytes } from "crypto";
|
|
4370
|
+
var RESUME_KEEP_DAYS = 14;
|
|
4371
|
+
var WaitResumeNotFoundError = class extends Error {
|
|
4372
|
+
constructor(resumeId) {
|
|
4373
|
+
super(say("wait.resume_not_found", resumeId, RESUME_KEEP_DAYS));
|
|
4374
|
+
this.resumeId = resumeId;
|
|
4375
|
+
}
|
|
4376
|
+
resumeId;
|
|
4377
|
+
name = "WaitResumeNotFoundError";
|
|
4378
|
+
};
|
|
4379
|
+
function saveResume(db, callers, spec) {
|
|
4380
|
+
const now = /* @__PURE__ */ new Date();
|
|
4381
|
+
const cutoff = new Date(now.getTime() - RESUME_KEEP_DAYS * 864e5).toISOString();
|
|
4382
|
+
for (let attempt = 0; ; attempt++) {
|
|
4383
|
+
const id = randomBytes(5).toString("hex");
|
|
4384
|
+
try {
|
|
4385
|
+
db.transaction(() => {
|
|
4386
|
+
db.prepare("DELETE FROM wait_resumes WHERE created_at < ?").run(cutoff);
|
|
4387
|
+
db.prepare(
|
|
4388
|
+
"INSERT INTO wait_resumes (id, callers_json, spec_json, created_at) VALUES (?, ?, ?, ?)"
|
|
4389
|
+
).run(id, JSON.stringify(callers), JSON.stringify(spec), now.toISOString());
|
|
4390
|
+
}).immediate();
|
|
4391
|
+
return id;
|
|
4392
|
+
} catch (e) {
|
|
4393
|
+
if (attempt < 3 && /UNIQUE|PRIMARY KEY/i.test(String(e))) continue;
|
|
4394
|
+
throw e;
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
function loadResume(db, id) {
|
|
4399
|
+
const row = db.prepare("SELECT callers_json AS callers, spec_json AS spec FROM wait_resumes WHERE id = ?").get(id);
|
|
4400
|
+
if (row === void 0) throw new WaitResumeNotFoundError(id);
|
|
4401
|
+
try {
|
|
4402
|
+
const callers = JSON.parse(row.callers);
|
|
4403
|
+
const spec = JSON.parse(row.spec);
|
|
4404
|
+
if (!Array.isArray(callers) || !Array.isArray(spec.refs)) throw new Error("bad shape");
|
|
4405
|
+
return { id, callers, spec };
|
|
4406
|
+
} catch {
|
|
4407
|
+
throw new WaitRefError(`wait resume handle ${id} is unreadable (its stored row is damaged)`);
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4410
|
+
|
|
4411
|
+
export {
|
|
4412
|
+
RebuildTargetExistsError,
|
|
4413
|
+
RebuildTargetIsSourceError,
|
|
4414
|
+
rebuildInto,
|
|
4415
|
+
DRIFT_REPORT_CAP,
|
|
4416
|
+
checkCheapDriftInvariant,
|
|
4417
|
+
checkDrift,
|
|
4418
|
+
formatDriftRecord,
|
|
4419
|
+
driftRemediation,
|
|
4420
|
+
DriftDetectedError,
|
|
4421
|
+
HazardFailError,
|
|
4422
|
+
DbInsideSyncDirError,
|
|
4423
|
+
isPathInside,
|
|
4424
|
+
checkDbInsideSyncDir,
|
|
4425
|
+
classifyFsType,
|
|
4426
|
+
probeFilesystem,
|
|
4427
|
+
checkNetworkMount,
|
|
4428
|
+
findCaseCollisions,
|
|
4429
|
+
checkCaseCollisions,
|
|
4430
|
+
checkFleetHazards,
|
|
4431
|
+
checkSegmentVersion,
|
|
4432
|
+
checkOrphanedAgents,
|
|
4433
|
+
checkCloseGuards,
|
|
4434
|
+
checkUnpublishedOps,
|
|
4435
|
+
AGENT_STATE_GLYPH,
|
|
4436
|
+
agentStateGlyph,
|
|
4437
|
+
GLYPH,
|
|
4438
|
+
WAIT_HEARTBEAT_CAP_MS,
|
|
4439
|
+
DEFAULT_STUCK_AFTER_MS,
|
|
4440
|
+
DEFAULT_CTL_STUCK_AFTER_MS,
|
|
4441
|
+
WaitNothingError,
|
|
4442
|
+
WaveNotFoundError,
|
|
4443
|
+
WaitRefError,
|
|
4444
|
+
WAIT_REGEX_MAX_LEN,
|
|
4445
|
+
WAIT_TEXT_MAX,
|
|
4446
|
+
WaitRegexError,
|
|
4447
|
+
checkRegexGrammar,
|
|
4448
|
+
compileTextMatcher,
|
|
4449
|
+
waitForEvents,
|
|
4450
|
+
getActionReceipt,
|
|
4451
|
+
reserveActionReceipt,
|
|
4452
|
+
transitionActionReceipt,
|
|
4453
|
+
releaseStall,
|
|
4454
|
+
releaseStallHandler,
|
|
4455
|
+
steerText,
|
|
4456
|
+
steerStall,
|
|
4457
|
+
steerStallHandler,
|
|
4458
|
+
waitWithActions,
|
|
4459
|
+
ctlAgentHooks,
|
|
4460
|
+
NextArgError,
|
|
4461
|
+
DEFAULT_NEXT_CAP,
|
|
4462
|
+
MAX_NEXT_CAP,
|
|
4463
|
+
executeNext,
|
|
4464
|
+
MU_SHIM_MARKER,
|
|
4465
|
+
skillDirDigest,
|
|
4466
|
+
migratePi,
|
|
4467
|
+
RemoteCollectUnsupportedError,
|
|
4468
|
+
RemoteCollectError,
|
|
4469
|
+
collectRef,
|
|
4470
|
+
collectRemoteAgent,
|
|
4471
|
+
TASK_SORT_KEYS,
|
|
4472
|
+
isTaskSortKey,
|
|
4473
|
+
byRoiDesc,
|
|
4474
|
+
sortTasks,
|
|
4475
|
+
relTimeBasisForSort,
|
|
4476
|
+
callerWorkstreams,
|
|
4477
|
+
stateSnapshot,
|
|
4478
|
+
WaitResumeNotFoundError,
|
|
4479
|
+
saveResume,
|
|
4480
|
+
loadResume
|
|
4481
|
+
};
|
|
4482
|
+
//# sourceMappingURL=chunk-5HECDBK3.js.map
|