@mu-crew/mu 3.13.3 → 4.1.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-4ULY3ZKO.js +4482 -0
- package/dist/chunk-4ULY3ZKO.js.map +1 -0
- package/dist/chunk-7M634JMD.js +375 -0
- package/dist/chunk-7M634JMD.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-SS7NQSSZ.js +3660 -0
- package/dist/chunk-SS7NQSSZ.js.map +1 -0
- package/dist/chunk-UCZPJX7K.js +14253 -0
- package/dist/chunk-UCZPJX7K.js.map +1 -0
- package/dist/{cli-CKOMHBXV.js → cli-TV3N47AZ.js} +7317 -4593
- package/dist/cli-TV3N47AZ.js.map +1 -0
- package/dist/cli.js +2 -3
- package/dist/cli.js.map +1 -1
- package/dist/errors-GRONKWFS.js +65 -0
- package/dist/extension/mu-pi.js +1632 -837
- package/dist/extension/mu-pi.js.map +1 -1
- package/dist/index.d.ts +4067 -2561
- package/dist/index.js +614 -297
- package/dist/index.js.map +1 -1
- package/dist/tui-FUBQ326D.js +1848 -0
- package/dist/tui-FUBQ326D.js.map +1 -0
- package/docs/ARCHITECTURE.md +118 -66
- package/docs/VOCABULARY.md +113 -49
- package/docs/guide/README.md +3 -3
- package/docs/guide/backends.md +8 -4
- package/docs/guide/cleanup.md +69 -33
- package/docs/guide/delegate.md +149 -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 +122 -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 +24 -32
- package/docs/reference/naming.md +14 -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 +17 -318
- package/skills/mu/recipes/tasks-or-calls.md +15 -78
- package/skills/mu/recipes/test-health.md +19 -77
- 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-GRONKWFS.js.map} +0 -0
|
@@ -0,0 +1,3660 @@
|
|
|
1
|
+
import { createRequire as __muCreateRequire } from "node:module"; const require = __muCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
CURRENT_SCHEMA_VERSION,
|
|
4
|
+
DEFAULT_EFFORT_DAYS,
|
|
5
|
+
DEFAULT_IMPACT,
|
|
6
|
+
DEFAULT_SUBSTATE,
|
|
7
|
+
MACHINE_LOCAL_ENTITIES,
|
|
8
|
+
MAX_AGENT_NAME_LEN,
|
|
9
|
+
NOTE_KINDS,
|
|
10
|
+
RESERVED_WORKSTREAM_NAMES,
|
|
11
|
+
SEGMENT_FORMAT_VERSION,
|
|
12
|
+
SEGMENT_READ_VERSIONS,
|
|
13
|
+
SELECT_TASK_COLS,
|
|
14
|
+
SYNCED_ENTITIES,
|
|
15
|
+
TASK_FROM_JOIN,
|
|
16
|
+
TASK_KINDS,
|
|
17
|
+
TaskNotFoundError,
|
|
18
|
+
addTasks,
|
|
19
|
+
compareHlc,
|
|
20
|
+
compareStrings,
|
|
21
|
+
decodeLine,
|
|
22
|
+
encodeSegmentLine,
|
|
23
|
+
expectsCtl,
|
|
24
|
+
formatPair,
|
|
25
|
+
getAgent,
|
|
26
|
+
getReceipt,
|
|
27
|
+
getTask,
|
|
28
|
+
getTaskNote,
|
|
29
|
+
insertNote,
|
|
30
|
+
isJsonMode,
|
|
31
|
+
isScratchWorkstream,
|
|
32
|
+
isTaskStatus,
|
|
33
|
+
listReady,
|
|
34
|
+
locksDir,
|
|
35
|
+
mapLegacyStatus,
|
|
36
|
+
newMsgId,
|
|
37
|
+
parseHlc,
|
|
38
|
+
prepareCached,
|
|
39
|
+
readWaitRegistration,
|
|
40
|
+
receiveHlc,
|
|
41
|
+
requestSha,
|
|
42
|
+
reserveReceipt,
|
|
43
|
+
resolvePair,
|
|
44
|
+
resolveWorkstreamId,
|
|
45
|
+
rowFromDb,
|
|
46
|
+
savedIdentity,
|
|
47
|
+
say,
|
|
48
|
+
shellWord,
|
|
49
|
+
taskIdFor,
|
|
50
|
+
taskRevision,
|
|
51
|
+
updateReceipt,
|
|
52
|
+
withCaptureSuppressed,
|
|
53
|
+
withFileLock,
|
|
54
|
+
withOpContext
|
|
55
|
+
} from "./chunk-UCZPJX7K.js";
|
|
56
|
+
|
|
57
|
+
// src/fold.ts
|
|
58
|
+
function fieldsOf(payload) {
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(payload);
|
|
62
|
+
} catch {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
66
|
+
const out = {};
|
|
67
|
+
for (const [field, value] of Object.entries(parsed)) {
|
|
68
|
+
if (value === null || typeof value === "string" || typeof value === "number") {
|
|
69
|
+
out[field] = value;
|
|
70
|
+
} else if (typeof value === "boolean") {
|
|
71
|
+
out[field] = value ? 1 : 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
function isoOfHlc(hlc) {
|
|
77
|
+
try {
|
|
78
|
+
return new Date(parseHlc(hlc).wallMs).toISOString();
|
|
79
|
+
} catch {
|
|
80
|
+
return (/* @__PURE__ */ new Date(0)).toISOString();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
var ascending = (ops) => [...ops].sort((a, b) => compareHlc(a.hlc, b.hlc));
|
|
84
|
+
function newestDel(ops) {
|
|
85
|
+
let newest = null;
|
|
86
|
+
for (const o of ops) {
|
|
87
|
+
if (o.op === "del" && (newest === null || compareHlc(o.hlc, newest) > 0)) newest = o.hlc;
|
|
88
|
+
}
|
|
89
|
+
return newest;
|
|
90
|
+
}
|
|
91
|
+
function laterHlc(a, b) {
|
|
92
|
+
if (a === null) return b;
|
|
93
|
+
if (b === null) return a;
|
|
94
|
+
return compareHlc(a, b) >= 0 ? a : b;
|
|
95
|
+
}
|
|
96
|
+
function livePuts(ops, horizon) {
|
|
97
|
+
return ascending(ops).filter(
|
|
98
|
+
(o) => o.op === "put" && (horizon === null || compareHlc(o.hlc, horizon) > 0)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
function lww(puts, field, accept) {
|
|
102
|
+
let found;
|
|
103
|
+
for (const p of puts) {
|
|
104
|
+
if (!(field in p.fields)) continue;
|
|
105
|
+
const v = accept(p.fields[field] ?? null);
|
|
106
|
+
if (v !== void 0) found = { value: v };
|
|
107
|
+
}
|
|
108
|
+
return found;
|
|
109
|
+
}
|
|
110
|
+
var str = (v) => typeof v === "string" ? v : void 0;
|
|
111
|
+
var strOrNull = (v) => v === null || typeof v === "string" ? v : void 0;
|
|
112
|
+
function foldWorkstream(ops, firstTaskPutAfter) {
|
|
113
|
+
const horizon = newestDel(ops);
|
|
114
|
+
const puts = livePuts(ops, horizon).map((o) => ({ hlc: o.hlc, fields: fieldsOf(o.payload) }));
|
|
115
|
+
const created = lww(puts, "created_at", str);
|
|
116
|
+
const torn = lww(puts, "torn_down_at", strOrNull);
|
|
117
|
+
const own = puts[0]?.hlc ?? null;
|
|
118
|
+
if (own === null && firstTaskPutAfter(horizon) === null) return null;
|
|
119
|
+
let createdAt = created?.value;
|
|
120
|
+
if (createdAt === void 0) {
|
|
121
|
+
const task = firstTaskPutAfter(horizon);
|
|
122
|
+
const start = own === null ? task : task === null ? own : earlierHlc(own, task);
|
|
123
|
+
createdAt = isoOfHlc(start ?? "");
|
|
124
|
+
}
|
|
125
|
+
return { createdAt, tornDownAt: torn?.value ?? null };
|
|
126
|
+
}
|
|
127
|
+
function earlierHlc(a, b) {
|
|
128
|
+
return compareHlc(a, b) <= 0 ? a : b;
|
|
129
|
+
}
|
|
130
|
+
function carriedSubstate(fields2) {
|
|
131
|
+
const { status, substate } = fields2;
|
|
132
|
+
if (typeof status === "string") {
|
|
133
|
+
const legacy = mapLegacyStatus(status);
|
|
134
|
+
if (legacy) return legacy.substate;
|
|
135
|
+
if (typeof substate !== "string" && isTaskStatus(status)) return DEFAULT_SUBSTATE[status];
|
|
136
|
+
}
|
|
137
|
+
return typeof substate === "string" ? substate : null;
|
|
138
|
+
}
|
|
139
|
+
function knownStatus(v) {
|
|
140
|
+
if (typeof v !== "string") return void 0;
|
|
141
|
+
const legacy = mapLegacyStatus(v);
|
|
142
|
+
if (legacy) return legacy.status;
|
|
143
|
+
return isTaskStatus(v) ? v : void 0;
|
|
144
|
+
}
|
|
145
|
+
var impactOf = (v) => typeof v === "number" && Number.isFinite(v) && v >= 1 && v <= 100 ? v : void 0;
|
|
146
|
+
var effortOf = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
|
|
147
|
+
var scoredOf = (v) => v === 0 || v === 1 ? v : void 0;
|
|
148
|
+
var kindOf = (v) => typeof v === "string" && TASK_KINDS.includes(v) ? v : void 0;
|
|
149
|
+
function foldTask(ops, horizon, localId) {
|
|
150
|
+
const puts = livePuts(ops, horizon).map((o) => ({ hlc: o.hlc, fields: fieldsOf(o.payload) }));
|
|
151
|
+
const began = puts[0]?.hlc;
|
|
152
|
+
if (began === void 0) return null;
|
|
153
|
+
const born = isoOfHlc(began);
|
|
154
|
+
const status = lww(puts, "status", knownStatus)?.value ?? "OPEN";
|
|
155
|
+
let winner = null;
|
|
156
|
+
for (const p of puts) winner = carriedSubstate(p.fields) ?? winner;
|
|
157
|
+
const pair = resolvePair(status, winner ?? DEFAULT_SUBSTATE[status]);
|
|
158
|
+
return {
|
|
159
|
+
title: lww(puts, "title", str)?.value ?? localId,
|
|
160
|
+
status,
|
|
161
|
+
substate: pair?.substate ?? DEFAULT_SUBSTATE[status],
|
|
162
|
+
impact: lww(puts, "impact", impactOf)?.value ?? DEFAULT_IMPACT,
|
|
163
|
+
effortDays: lww(puts, "effort_days", effortOf)?.value ?? DEFAULT_EFFORT_DAYS,
|
|
164
|
+
scored: lww(puts, "scored", scoredOf)?.value ?? 0,
|
|
165
|
+
dispatchHost: lww(puts, "dispatch_host", strOrNull)?.value ?? null,
|
|
166
|
+
kind: lww(puts, "kind", kindOf)?.value ?? "work",
|
|
167
|
+
wave: lww(puts, "wave", strOrNull)?.value ?? null,
|
|
168
|
+
createdAt: lww(puts, "created_at", str)?.value ?? born,
|
|
169
|
+
updatedAt: lww(puts, "updated_at", str)?.value ?? born
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function noteIdentity(author, content, createdAt) {
|
|
173
|
+
return JSON.stringify([author ?? "", content, createdAt]);
|
|
174
|
+
}
|
|
175
|
+
function notePut(o) {
|
|
176
|
+
const f = fieldsOf(o.payload);
|
|
177
|
+
const author = typeof f.author === "string" ? f.author : f.author == null ? null : String(f.author);
|
|
178
|
+
const sentKind = f.kind;
|
|
179
|
+
return {
|
|
180
|
+
hlc: o.hlc,
|
|
181
|
+
key: o.key,
|
|
182
|
+
author,
|
|
183
|
+
content: f.content == null ? "" : String(f.content),
|
|
184
|
+
createdAt: typeof f.created_at === "string" ? f.created_at : isoOfHlc(o.hlc),
|
|
185
|
+
kind: NOTE_KINDS.includes(sentKind) ? sentKind : "note"
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function foldNotes(ops, horizon) {
|
|
189
|
+
const sorted = ascending(ops);
|
|
190
|
+
const puts = sorted.filter((o) => o.op === "put").map(notePut);
|
|
191
|
+
const kills = [];
|
|
192
|
+
for (const d of sorted) {
|
|
193
|
+
if (d.op !== "del") continue;
|
|
194
|
+
const f = fieldsOf(d.payload);
|
|
195
|
+
if (f.content != null) {
|
|
196
|
+
kills.push({
|
|
197
|
+
hlc: d.hlc,
|
|
198
|
+
author: f.author == null ? null : String(f.author),
|
|
199
|
+
content: String(f.content),
|
|
200
|
+
createdAt: typeof f.created_at === "string" ? f.created_at : null
|
|
201
|
+
});
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const src = puts.filter((p) => p.key === d.key && compareHlc(p.hlc, d.hlc) < 0).pop();
|
|
205
|
+
if (src)
|
|
206
|
+
kills.push({
|
|
207
|
+
hlc: d.hlc,
|
|
208
|
+
author: src.author,
|
|
209
|
+
content: src.content,
|
|
210
|
+
createdAt: src.createdAt
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const out = /* @__PURE__ */ new Map();
|
|
214
|
+
for (const p of puts) {
|
|
215
|
+
if (horizon !== null && compareHlc(p.hlc, horizon) <= 0) continue;
|
|
216
|
+
const killed = kills.some(
|
|
217
|
+
(k) => compareHlc(k.hlc, p.hlc) > 0 && k.content === p.content && (k.author ?? "") === (p.author ?? "") && (k.createdAt === null || k.createdAt === p.createdAt)
|
|
218
|
+
);
|
|
219
|
+
if (killed) continue;
|
|
220
|
+
const id = noteIdentity(p.author, p.content, p.createdAt);
|
|
221
|
+
if (!out.has(id))
|
|
222
|
+
out.set(id, { author: p.author, content: p.content, createdAt: p.createdAt, kind: p.kind });
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
function foldEdge(ops, endpointHorizons) {
|
|
227
|
+
let horizon = newestDel(ops);
|
|
228
|
+
for (const h of endpointHorizons) horizon = laterHlc(horizon, h);
|
|
229
|
+
const first = livePuts(ops, horizon)[0];
|
|
230
|
+
if (first === void 0) return null;
|
|
231
|
+
const stamped = fieldsOf(first.payload).created_at;
|
|
232
|
+
return { createdAt: typeof stamped === "string" ? stamped : isoOfHlc(first.hlc) };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/apply.ts
|
|
236
|
+
var OpEntityNotSyncedError = class extends Error {
|
|
237
|
+
constructor(entity) {
|
|
238
|
+
super(
|
|
239
|
+
`op entity ${JSON.stringify(entity)} is machine-local and must never arrive from a peer (synced entities: ${SYNCED_ENTITIES.join(", ")})`
|
|
240
|
+
);
|
|
241
|
+
this.entity = entity;
|
|
242
|
+
this.name = "OpEntityNotSyncedError";
|
|
243
|
+
}
|
|
244
|
+
entity;
|
|
245
|
+
};
|
|
246
|
+
var OpKeyMalformedError = class extends Error {
|
|
247
|
+
constructor(entity, key) {
|
|
248
|
+
super(`malformed ${entity} key: ${JSON.stringify(key)}`);
|
|
249
|
+
this.entity = entity;
|
|
250
|
+
this.key = key;
|
|
251
|
+
this.name = "OpKeyMalformedError";
|
|
252
|
+
}
|
|
253
|
+
entity;
|
|
254
|
+
key;
|
|
255
|
+
};
|
|
256
|
+
function isSyncedEntity(entity) {
|
|
257
|
+
return SYNCED_ENTITIES.includes(entity);
|
|
258
|
+
}
|
|
259
|
+
function isKnownMachineLocalEntity(entity) {
|
|
260
|
+
return MACHINE_LOCAL_ENTITIES.includes(entity);
|
|
261
|
+
}
|
|
262
|
+
function parseTaskKey(key) {
|
|
263
|
+
const slash = key.indexOf("/");
|
|
264
|
+
if (slash <= 0 || slash === key.length - 1) throw new OpKeyMalformedError("task", key);
|
|
265
|
+
const workstream = key.slice(0, slash);
|
|
266
|
+
const localId = key.slice(slash + 1);
|
|
267
|
+
if (workstream.length === 0 || localId.length === 0) {
|
|
268
|
+
throw new OpKeyMalformedError("task", key);
|
|
269
|
+
}
|
|
270
|
+
return { workstream, localId };
|
|
271
|
+
}
|
|
272
|
+
function parseNoteKey(key) {
|
|
273
|
+
const hash = key.lastIndexOf("#");
|
|
274
|
+
if (hash <= 0 || hash === key.length - 1) throw new OpKeyMalformedError("note", key);
|
|
275
|
+
return { taskKey: key.slice(0, hash) };
|
|
276
|
+
}
|
|
277
|
+
function parseEdgeKey(key) {
|
|
278
|
+
const arrow = key.indexOf("->");
|
|
279
|
+
if (arrow <= 0) throw new OpKeyMalformedError("edge", key);
|
|
280
|
+
const blocker = key.slice(0, arrow);
|
|
281
|
+
const blocked = key.slice(arrow + 2);
|
|
282
|
+
if (blocker.length === 0 || blocked.length === 0) throw new OpKeyMalformedError("edge", key);
|
|
283
|
+
return { blocker, blocked };
|
|
284
|
+
}
|
|
285
|
+
var hi = (p) => `${p.slice(0, -1)}${String.fromCharCode(p.charCodeAt(p.length - 1) + 1)}`;
|
|
286
|
+
var Log = class {
|
|
287
|
+
constructor(db, op) {
|
|
288
|
+
this.db = db;
|
|
289
|
+
this.pending = op && { hlc: op.hlc, op: op.op, key: op.key, payload: op.payload };
|
|
290
|
+
this.pendingEntity = op?.entity ?? "";
|
|
291
|
+
}
|
|
292
|
+
db;
|
|
293
|
+
pending;
|
|
294
|
+
pendingEntity;
|
|
295
|
+
withPending(entity, rows, inScope) {
|
|
296
|
+
const p = this.pending;
|
|
297
|
+
if (p === void 0 || entity !== this.pendingEntity || !inScope(p.key)) return rows;
|
|
298
|
+
return rows.some((r) => r.hlc === p.hlc) ? rows : [...rows, p];
|
|
299
|
+
}
|
|
300
|
+
/** The ops of one natural key. */
|
|
301
|
+
ofKey(entity, key) {
|
|
302
|
+
const rows = prepareCached(
|
|
303
|
+
this.db,
|
|
304
|
+
"SELECT hlc, op, key, payload FROM ops WHERE entity = ? AND key = ?"
|
|
305
|
+
).all(entity, key);
|
|
306
|
+
return this.withPending(entity, rows, (k) => k === key);
|
|
307
|
+
}
|
|
308
|
+
/** The ops of every key in [lo, hi), grouped by key. */
|
|
309
|
+
range(entity, lo, hiKey) {
|
|
310
|
+
const rows = this.withPending(
|
|
311
|
+
entity,
|
|
312
|
+
prepareCached(
|
|
313
|
+
this.db,
|
|
314
|
+
"SELECT hlc, op, key, payload FROM ops WHERE entity = ? AND key >= ? AND key < ?"
|
|
315
|
+
).all(entity, lo, hiKey),
|
|
316
|
+
(k) => k >= lo && k < hiKey
|
|
317
|
+
);
|
|
318
|
+
const out = /* @__PURE__ */ new Map();
|
|
319
|
+
for (const r of rows) {
|
|
320
|
+
const list = out.get(r.key);
|
|
321
|
+
if (list) list.push(r);
|
|
322
|
+
else out.set(r.key, [r]);
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
/** The oldest task put under workstream `ws` newer than `after`. */
|
|
327
|
+
firstTaskPutAfter(ws, after) {
|
|
328
|
+
const lo = `${ws}/`;
|
|
329
|
+
const row = prepareCached(
|
|
330
|
+
this.db,
|
|
331
|
+
`SELECT MIN(hlc) AS hlc FROM ops
|
|
332
|
+
WHERE entity = 'task' AND op = 'put' AND key >= @lo AND key < @hi
|
|
333
|
+
AND (@after IS NULL OR hlc > @after)`
|
|
334
|
+
).get({ lo, hi: hi(lo), after });
|
|
335
|
+
const p = this.pending;
|
|
336
|
+
let best = row.hlc;
|
|
337
|
+
if (p !== void 0 && this.pendingEntity === "task" && p.op === "put" && p.key >= lo && p.key < hi(lo) && (after === null || p.hlc > after)) {
|
|
338
|
+
best = best === null || p.hlc < best ? p.hlc : best;
|
|
339
|
+
}
|
|
340
|
+
return best;
|
|
341
|
+
}
|
|
342
|
+
/** Edge keys naming a task in `scope`: a workstream (`name/`) or one task. */
|
|
343
|
+
edgeKeysTouching(scope) {
|
|
344
|
+
const candidates = "workstream" in scope ? prepareCached(
|
|
345
|
+
this.db,
|
|
346
|
+
`SELECT DISTINCT key FROM ops
|
|
347
|
+
WHERE entity = 'edge' AND (instr(key, @to) > 0 OR (key >= @lo AND key < @hi))`
|
|
348
|
+
).all({
|
|
349
|
+
to: `->${scope.workstream}/`,
|
|
350
|
+
lo: `${scope.workstream}/`,
|
|
351
|
+
hi: hi(`${scope.workstream}/`)
|
|
352
|
+
}) : prepareCached(
|
|
353
|
+
this.db,
|
|
354
|
+
`SELECT DISTINCT key FROM ops
|
|
355
|
+
WHERE entity = 'edge'
|
|
356
|
+
AND ((key >= @lo AND key < @hi) OR substr(key, -length(@to)) = @to)`
|
|
357
|
+
).all({ to: `->${scope.task}`, lo: `${scope.task}->`, hi: hi(`${scope.task}->`) });
|
|
358
|
+
const keys = new Set(candidates.map((r) => r.key));
|
|
359
|
+
const p = this.pending;
|
|
360
|
+
if (p !== void 0 && this.pendingEntity === "edge") keys.add(p.key);
|
|
361
|
+
return [...keys].filter((key) => {
|
|
362
|
+
let ends;
|
|
363
|
+
try {
|
|
364
|
+
ends = parseEdgeKey(key);
|
|
365
|
+
} catch {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
const inScope = (t) => "workstream" in scope ? t.startsWith(`${scope.workstream}/`) : t === scope.task;
|
|
369
|
+
return inScope(ends.blocker) || inScope(ends.blocked);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
function taskHorizon(log, taskKey) {
|
|
374
|
+
const { workstream } = parseTaskKey(taskKey);
|
|
375
|
+
return laterHlc(
|
|
376
|
+
newestDel(log.ofKey("task", taskKey)),
|
|
377
|
+
newestDel(log.ofKey("workstream", workstream))
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
function differing(a, b) {
|
|
381
|
+
return Object.keys(a).filter((k) => a[k] !== b[k]);
|
|
382
|
+
}
|
|
383
|
+
var workstreamId = (db, name) => prepareCached(db, "SELECT id FROM workstreams WHERE name = ?").get(name)?.id ?? null;
|
|
384
|
+
function taskRowId(db, key) {
|
|
385
|
+
const { workstream, localId } = parseTaskKey(key);
|
|
386
|
+
const row = prepareCached(
|
|
387
|
+
db,
|
|
388
|
+
`SELECT t.id AS id FROM tasks t JOIN workstreams w ON w.id = t.workstream_id
|
|
389
|
+
WHERE w.name = ? AND t.local_id = ?`
|
|
390
|
+
).get(workstream, localId);
|
|
391
|
+
return row?.id ?? null;
|
|
392
|
+
}
|
|
393
|
+
var UNCHANGED = { changed: false, fields: [] };
|
|
394
|
+
function syncWorkstream(db, log, name) {
|
|
395
|
+
const state = foldWorkstream(
|
|
396
|
+
log.ofKey("workstream", name),
|
|
397
|
+
(after) => log.firstTaskPutAfter(name, after)
|
|
398
|
+
);
|
|
399
|
+
const row = prepareCached(
|
|
400
|
+
db,
|
|
401
|
+
"SELECT id, created_at, torn_down_at FROM workstreams WHERE name = ?"
|
|
402
|
+
).get(name);
|
|
403
|
+
if (state === null) {
|
|
404
|
+
if (row === void 0) return UNCHANGED;
|
|
405
|
+
prepareCached(db, "DELETE FROM workstreams WHERE id = ?").run(row.id);
|
|
406
|
+
return { changed: true, fields: [] };
|
|
407
|
+
}
|
|
408
|
+
const want = { created_at: state.createdAt, torn_down_at: state.tornDownAt };
|
|
409
|
+
if (row === void 0) {
|
|
410
|
+
prepareCached(
|
|
411
|
+
db,
|
|
412
|
+
"INSERT INTO workstreams (name, created_at, torn_down_at) VALUES (?, ?, ?)"
|
|
413
|
+
).run(name, state.createdAt, state.tornDownAt);
|
|
414
|
+
return { changed: true, fields: Object.keys(want) };
|
|
415
|
+
}
|
|
416
|
+
const fields2 = differing(want, { created_at: row.created_at, torn_down_at: row.torn_down_at });
|
|
417
|
+
if (fields2.length === 0) return UNCHANGED;
|
|
418
|
+
prepareCached(db, "UPDATE workstreams SET created_at = ?, torn_down_at = ? WHERE id = ?").run(
|
|
419
|
+
state.createdAt,
|
|
420
|
+
state.tornDownAt,
|
|
421
|
+
row.id
|
|
422
|
+
);
|
|
423
|
+
return { changed: true, fields: fields2 };
|
|
424
|
+
}
|
|
425
|
+
function syncTask(db, log, key) {
|
|
426
|
+
const { workstream, localId } = parseTaskKey(key);
|
|
427
|
+
const state = foldTask(log.ofKey("task", key), taskHorizon(log, key), localId);
|
|
428
|
+
const existing = prepareCached(
|
|
429
|
+
db,
|
|
430
|
+
`SELECT t.id, t.title, t.status, t.substate, t.impact, t.effort_days, t.scored,
|
|
431
|
+
t.dispatch_host, t.kind, t.wave, t.created_at, t.updated_at
|
|
432
|
+
FROM tasks t JOIN workstreams w ON w.id = t.workstream_id
|
|
433
|
+
WHERE w.name = ? AND t.local_id = ?`
|
|
434
|
+
).get(workstream, localId);
|
|
435
|
+
if (state === null) {
|
|
436
|
+
if (existing === void 0) return UNCHANGED;
|
|
437
|
+
prepareCached(db, "DELETE FROM tasks WHERE id = ?").run(existing.id);
|
|
438
|
+
return { changed: true, fields: [] };
|
|
439
|
+
}
|
|
440
|
+
const want = {
|
|
441
|
+
title: state.title,
|
|
442
|
+
status: state.status,
|
|
443
|
+
substate: state.substate,
|
|
444
|
+
impact: state.impact,
|
|
445
|
+
effort_days: state.effortDays,
|
|
446
|
+
scored: state.scored,
|
|
447
|
+
dispatch_host: state.dispatchHost,
|
|
448
|
+
kind: state.kind,
|
|
449
|
+
wave: state.wave,
|
|
450
|
+
created_at: state.createdAt,
|
|
451
|
+
updated_at: state.updatedAt
|
|
452
|
+
};
|
|
453
|
+
if (existing === void 0) {
|
|
454
|
+
const wsId = workstreamId(db, workstream);
|
|
455
|
+
if (wsId === null) return UNCHANGED;
|
|
456
|
+
prepareCached(
|
|
457
|
+
db,
|
|
458
|
+
`INSERT INTO tasks (workstream_id, local_id, title, status, substate, impact, effort_days,
|
|
459
|
+
scored, dispatch_host, kind, wave, owner_id, created_at, updated_at)
|
|
460
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)`
|
|
461
|
+
).run(
|
|
462
|
+
wsId,
|
|
463
|
+
localId,
|
|
464
|
+
state.title,
|
|
465
|
+
state.status,
|
|
466
|
+
state.substate,
|
|
467
|
+
state.impact,
|
|
468
|
+
state.effortDays,
|
|
469
|
+
state.scored,
|
|
470
|
+
state.dispatchHost,
|
|
471
|
+
state.kind,
|
|
472
|
+
state.wave,
|
|
473
|
+
state.createdAt,
|
|
474
|
+
state.updatedAt
|
|
475
|
+
);
|
|
476
|
+
return { changed: true, fields: Object.keys(want) };
|
|
477
|
+
}
|
|
478
|
+
const fields2 = differing(want, existing);
|
|
479
|
+
if (fields2.length === 0) return UNCHANGED;
|
|
480
|
+
prepareCached(
|
|
481
|
+
db,
|
|
482
|
+
`UPDATE tasks SET title = ?, status = ?, substate = ?, impact = ?, effort_days = ?,
|
|
483
|
+
scored = ?, dispatch_host = ?, kind = ?, wave = ?, created_at = ?, updated_at = ?
|
|
484
|
+
WHERE id = ?`
|
|
485
|
+
).run(
|
|
486
|
+
state.title,
|
|
487
|
+
state.status,
|
|
488
|
+
state.substate,
|
|
489
|
+
state.impact,
|
|
490
|
+
state.effortDays,
|
|
491
|
+
state.scored,
|
|
492
|
+
state.dispatchHost,
|
|
493
|
+
state.kind,
|
|
494
|
+
state.wave,
|
|
495
|
+
state.createdAt,
|
|
496
|
+
state.updatedAt,
|
|
497
|
+
existing.id
|
|
498
|
+
);
|
|
499
|
+
return { changed: true, fields: fields2 };
|
|
500
|
+
}
|
|
501
|
+
function syncNotes(db, log, taskKey) {
|
|
502
|
+
const taskId = taskRowId(db, taskKey);
|
|
503
|
+
if (taskId === null) return false;
|
|
504
|
+
const ops = [...log.range("note", `${taskKey}#`, `${taskKey}$`).values()].flat();
|
|
505
|
+
const want = foldNotes(ops, taskHorizon(log, taskKey));
|
|
506
|
+
const rows = prepareCached(
|
|
507
|
+
db,
|
|
508
|
+
"SELECT id, author, content, created_at, kind FROM task_notes WHERE task_id = ? ORDER BY id"
|
|
509
|
+
).all(taskId);
|
|
510
|
+
let changed = false;
|
|
511
|
+
const seen = /* @__PURE__ */ new Set();
|
|
512
|
+
for (const r of rows) {
|
|
513
|
+
const id = noteIdentity(r.author, r.content, r.created_at);
|
|
514
|
+
if (want.has(id) && !seen.has(id)) {
|
|
515
|
+
seen.add(id);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
prepareCached(db, "DELETE FROM task_notes WHERE id = ?").run(r.id);
|
|
519
|
+
changed = true;
|
|
520
|
+
}
|
|
521
|
+
for (const [id, n] of want) {
|
|
522
|
+
if (seen.has(id)) continue;
|
|
523
|
+
prepareCached(
|
|
524
|
+
db,
|
|
525
|
+
"INSERT INTO task_notes (task_id, author, content, created_at, kind) VALUES (?, ?, ?, ?, ?)"
|
|
526
|
+
).run(taskId, n.author, n.content, n.createdAt, n.kind);
|
|
527
|
+
changed = true;
|
|
528
|
+
}
|
|
529
|
+
return changed;
|
|
530
|
+
}
|
|
531
|
+
function syncEdge(db, log, key) {
|
|
532
|
+
const { blocker, blocked } = parseEdgeKey(key);
|
|
533
|
+
const state = blocker === blocked ? null : foldEdge(log.ofKey("edge", key), [taskHorizon(log, blocker), taskHorizon(log, blocked)]);
|
|
534
|
+
const fromId = taskRowId(db, blocker);
|
|
535
|
+
const toId = taskRowId(db, blocked);
|
|
536
|
+
const existing = fromId === null || toId === null ? void 0 : prepareCached(
|
|
537
|
+
db,
|
|
538
|
+
"SELECT created_at FROM task_edges WHERE from_task_id = ? AND to_task_id = ?"
|
|
539
|
+
).get(fromId, toId);
|
|
540
|
+
const wanted = state !== null && fromId !== null && toId !== null;
|
|
541
|
+
if (!wanted) {
|
|
542
|
+
if (existing === void 0 || fromId === null || toId === null) return false;
|
|
543
|
+
prepareCached(db, "DELETE FROM task_edges WHERE from_task_id = ? AND to_task_id = ?").run(
|
|
544
|
+
fromId,
|
|
545
|
+
toId
|
|
546
|
+
);
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
if (existing === void 0) {
|
|
550
|
+
prepareCached(
|
|
551
|
+
db,
|
|
552
|
+
"INSERT INTO task_edges (from_task_id, to_task_id, created_at) VALUES (?, ?, ?)"
|
|
553
|
+
).run(fromId, toId, state.createdAt);
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
if (existing.created_at === state.createdAt) return false;
|
|
557
|
+
prepareCached(
|
|
558
|
+
db,
|
|
559
|
+
"UPDATE task_edges SET created_at = ? WHERE from_task_id = ? AND to_task_id = ?"
|
|
560
|
+
).run(state.createdAt, fromId, toId);
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
function syncDependents(db, log, taskKey) {
|
|
564
|
+
let changed = syncNotes(db, log, taskKey);
|
|
565
|
+
for (const key of log.edgeKeysTouching({ task: taskKey })) {
|
|
566
|
+
if (syncEdge(db, log, key)) changed = true;
|
|
567
|
+
}
|
|
568
|
+
return changed;
|
|
569
|
+
}
|
|
570
|
+
function applyWorkstream(db, log, op) {
|
|
571
|
+
const name = op.key;
|
|
572
|
+
if (name.length === 0) throw new OpKeyMalformedError("workstream", name);
|
|
573
|
+
const ws = syncWorkstream(db, log, name);
|
|
574
|
+
let changed = ws.changed;
|
|
575
|
+
if (op.op === "del") {
|
|
576
|
+
const prefix = `${name}/`;
|
|
577
|
+
const taskKeys = [...log.range("task", prefix, hi(prefix)).keys()];
|
|
578
|
+
for (const key of taskKeys) if (syncTask(db, log, key).changed) changed = true;
|
|
579
|
+
for (const key of taskKeys) if (syncNotes(db, log, key)) changed = true;
|
|
580
|
+
const edges = log.edgeKeysTouching({ workstream: name });
|
|
581
|
+
for (const key of edges) if (syncEdge(db, log, key)) changed = true;
|
|
582
|
+
}
|
|
583
|
+
return { changed, appliedFields: ws.fields };
|
|
584
|
+
}
|
|
585
|
+
function applyTask(db, log, op) {
|
|
586
|
+
const { workstream } = parseTaskKey(op.key);
|
|
587
|
+
const existed = taskRowId(db, op.key) !== null;
|
|
588
|
+
const ws = syncWorkstream(db, log, workstream);
|
|
589
|
+
const task = syncTask(db, log, op.key);
|
|
590
|
+
let changed = ws.changed || task.changed;
|
|
591
|
+
const exists = taskRowId(db, op.key) !== null;
|
|
592
|
+
if ((op.op === "del" || existed !== exists) && syncDependents(db, log, op.key)) changed = true;
|
|
593
|
+
return { changed, appliedFields: task.fields };
|
|
594
|
+
}
|
|
595
|
+
function applyNote(db, log, op) {
|
|
596
|
+
const { taskKey } = parseNoteKey(op.key);
|
|
597
|
+
return { changed: syncNotes(db, log, taskKey), appliedFields: [] };
|
|
598
|
+
}
|
|
599
|
+
function applyEdge(db, log, op) {
|
|
600
|
+
parseEdgeKey(op.key);
|
|
601
|
+
return { changed: syncEdge(db, log, op.key), appliedFields: [] };
|
|
602
|
+
}
|
|
603
|
+
function applyOp(db, op) {
|
|
604
|
+
if (isKnownMachineLocalEntity(op.entity)) throw new OpEntityNotSyncedError(op.entity);
|
|
605
|
+
if (!isSyncedEntity(op.entity)) return { changed: false, appliedFields: [] };
|
|
606
|
+
if (op.entity === "message") return { changed: false, appliedFields: [] };
|
|
607
|
+
return withCaptureSuppressed(
|
|
608
|
+
db,
|
|
609
|
+
() => db.transaction(() => {
|
|
610
|
+
const log = new Log(db, op);
|
|
611
|
+
switch (op.entity) {
|
|
612
|
+
case "workstream":
|
|
613
|
+
return applyWorkstream(db, log, op);
|
|
614
|
+
case "task":
|
|
615
|
+
return applyTask(db, log, op);
|
|
616
|
+
case "note":
|
|
617
|
+
return applyNote(db, log, op);
|
|
618
|
+
default:
|
|
619
|
+
return applyEdge(db, log, op);
|
|
620
|
+
}
|
|
621
|
+
}).immediate()
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
function projectLog(db) {
|
|
625
|
+
const log = new Log(db);
|
|
626
|
+
const keys = (entity) => prepareCached(db, "SELECT DISTINCT key FROM ops WHERE entity = ?").all(entity).map((r) => r.key);
|
|
627
|
+
const taskKeys = keys("task");
|
|
628
|
+
const noteTasks = [...new Set(keys("note").map((k) => parseNoteKey(k).taskKey))];
|
|
629
|
+
const workstreams = /* @__PURE__ */ new Set([
|
|
630
|
+
...keys("workstream"),
|
|
631
|
+
...taskKeys.map((k) => parseTaskKey(k).workstream)
|
|
632
|
+
]);
|
|
633
|
+
let written = 0;
|
|
634
|
+
return withCaptureSuppressed(
|
|
635
|
+
db,
|
|
636
|
+
() => db.transaction(() => {
|
|
637
|
+
for (const name of workstreams) if (syncWorkstream(db, log, name).changed) written += 1;
|
|
638
|
+
for (const key of taskKeys) if (syncTask(db, log, key).changed) written += 1;
|
|
639
|
+
for (const key of noteTasks) if (syncNotes(db, log, key)) written += 1;
|
|
640
|
+
for (const key of keys("edge")) if (syncEdge(db, log, key)) written += 1;
|
|
641
|
+
return written;
|
|
642
|
+
}).immediate()
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
function applyOps(db, ops) {
|
|
646
|
+
const ordered = [...ops].sort((a, b) => compareHlc(a.hlc, b.hlc));
|
|
647
|
+
const run = db.transaction(() => ordered.map((op) => applyOp(db, op)));
|
|
648
|
+
return run.immediate();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/segment-manifest.ts
|
|
652
|
+
import { createHash } from "crypto";
|
|
653
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
654
|
+
|
|
655
|
+
// src/sha256-resumable.ts
|
|
656
|
+
var K = new Uint32Array([
|
|
657
|
+
1116352408,
|
|
658
|
+
1899447441,
|
|
659
|
+
3049323471,
|
|
660
|
+
3921009573,
|
|
661
|
+
961987163,
|
|
662
|
+
1508970993,
|
|
663
|
+
2453635748,
|
|
664
|
+
2870763221,
|
|
665
|
+
3624381080,
|
|
666
|
+
310598401,
|
|
667
|
+
607225278,
|
|
668
|
+
1426881987,
|
|
669
|
+
1925078388,
|
|
670
|
+
2162078206,
|
|
671
|
+
2614888103,
|
|
672
|
+
3248222580,
|
|
673
|
+
3835390401,
|
|
674
|
+
4022224774,
|
|
675
|
+
264347078,
|
|
676
|
+
604807628,
|
|
677
|
+
770255983,
|
|
678
|
+
1249150122,
|
|
679
|
+
1555081692,
|
|
680
|
+
1996064986,
|
|
681
|
+
2554220882,
|
|
682
|
+
2821834349,
|
|
683
|
+
2952996808,
|
|
684
|
+
3210313671,
|
|
685
|
+
3336571891,
|
|
686
|
+
3584528711,
|
|
687
|
+
113926993,
|
|
688
|
+
338241895,
|
|
689
|
+
666307205,
|
|
690
|
+
773529912,
|
|
691
|
+
1294757372,
|
|
692
|
+
1396182291,
|
|
693
|
+
1695183700,
|
|
694
|
+
1986661051,
|
|
695
|
+
2177026350,
|
|
696
|
+
2456956037,
|
|
697
|
+
2730485921,
|
|
698
|
+
2820302411,
|
|
699
|
+
3259730800,
|
|
700
|
+
3345764771,
|
|
701
|
+
3516065817,
|
|
702
|
+
3600352804,
|
|
703
|
+
4094571909,
|
|
704
|
+
275423344,
|
|
705
|
+
430227734,
|
|
706
|
+
506948616,
|
|
707
|
+
659060556,
|
|
708
|
+
883997877,
|
|
709
|
+
958139571,
|
|
710
|
+
1322822218,
|
|
711
|
+
1537002063,
|
|
712
|
+
1747873779,
|
|
713
|
+
1955562222,
|
|
714
|
+
2024104815,
|
|
715
|
+
2227730452,
|
|
716
|
+
2361852424,
|
|
717
|
+
2428436474,
|
|
718
|
+
2756734187,
|
|
719
|
+
3204031479,
|
|
720
|
+
3329325298
|
|
721
|
+
]);
|
|
722
|
+
var INITIAL_H = "6a09e667bb67ae853c6ef372a54ff53a510e527f9b05688c1f83d9ab5be0cd19";
|
|
723
|
+
function sha256Initial() {
|
|
724
|
+
return { h: INITIAL_H, n: 0, tail: "" };
|
|
725
|
+
}
|
|
726
|
+
function wordsOf(hex) {
|
|
727
|
+
const h = new Uint32Array(8);
|
|
728
|
+
for (let i = 0; i < 8; i++) h[i] = Number.parseInt(hex.slice(i * 8, i * 8 + 8), 16);
|
|
729
|
+
return h;
|
|
730
|
+
}
|
|
731
|
+
function hexOf(h) {
|
|
732
|
+
let out = "";
|
|
733
|
+
for (const word of h) out += word.toString(16).padStart(8, "0");
|
|
734
|
+
return out;
|
|
735
|
+
}
|
|
736
|
+
var W = new Uint32Array(64);
|
|
737
|
+
function compress(h, data, offset, end) {
|
|
738
|
+
let h0 = h[0] ?? 0;
|
|
739
|
+
let h1 = h[1] ?? 0;
|
|
740
|
+
let h2 = h[2] ?? 0;
|
|
741
|
+
let h3 = h[3] ?? 0;
|
|
742
|
+
let h4 = h[4] ?? 0;
|
|
743
|
+
let h5 = h[5] ?? 0;
|
|
744
|
+
let h6 = h[6] ?? 0;
|
|
745
|
+
let h7 = h[7] ?? 0;
|
|
746
|
+
for (let p = offset; p + 64 <= end; p += 64) {
|
|
747
|
+
for (let i = 0; i < 16; i++) {
|
|
748
|
+
const j = p + i * 4;
|
|
749
|
+
W[i] = (data[j] ?? 0) << 24 | (data[j + 1] ?? 0) << 16 | (data[j + 2] ?? 0) << 8 | (data[j + 3] ?? 0);
|
|
750
|
+
}
|
|
751
|
+
for (let i = 16; i < 64; i++) {
|
|
752
|
+
const w15 = W[i - 15] ?? 0;
|
|
753
|
+
const w2 = W[i - 2] ?? 0;
|
|
754
|
+
const s0 = (w15 >>> 7 | w15 << 25) ^ (w15 >>> 18 | w15 << 14) ^ w15 >>> 3;
|
|
755
|
+
const s1 = (w2 >>> 17 | w2 << 15) ^ (w2 >>> 19 | w2 << 13) ^ w2 >>> 10;
|
|
756
|
+
W[i] = (W[i - 16] ?? 0) + s0 + (W[i - 7] ?? 0) + s1 | 0;
|
|
757
|
+
}
|
|
758
|
+
let a = h0;
|
|
759
|
+
let b = h1;
|
|
760
|
+
let c = h2;
|
|
761
|
+
let d = h3;
|
|
762
|
+
let e = h4;
|
|
763
|
+
let f = h5;
|
|
764
|
+
let g = h6;
|
|
765
|
+
let hh = h7;
|
|
766
|
+
for (let i = 0; i < 64; i++) {
|
|
767
|
+
const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7);
|
|
768
|
+
const ch = e & f ^ ~e & g;
|
|
769
|
+
const t1 = hh + S1 + ch + (K[i] ?? 0) + (W[i] ?? 0) | 0;
|
|
770
|
+
const S0 = (a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10);
|
|
771
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
772
|
+
const t2 = S0 + maj | 0;
|
|
773
|
+
hh = g;
|
|
774
|
+
g = f;
|
|
775
|
+
f = e;
|
|
776
|
+
e = d + t1 | 0;
|
|
777
|
+
d = c;
|
|
778
|
+
c = b;
|
|
779
|
+
b = a;
|
|
780
|
+
a = t1 + t2 | 0;
|
|
781
|
+
}
|
|
782
|
+
h0 = h0 + a | 0;
|
|
783
|
+
h1 = h1 + b | 0;
|
|
784
|
+
h2 = h2 + c | 0;
|
|
785
|
+
h3 = h3 + d | 0;
|
|
786
|
+
h4 = h4 + e | 0;
|
|
787
|
+
h5 = h5 + f | 0;
|
|
788
|
+
h6 = h6 + g | 0;
|
|
789
|
+
h7 = h7 + hh | 0;
|
|
790
|
+
}
|
|
791
|
+
h.set([h0, h1, h2, h3, h4, h5, h6, h7]);
|
|
792
|
+
}
|
|
793
|
+
function sha256Update(state, data) {
|
|
794
|
+
const pending = Buffer.from(state.tail, "base64");
|
|
795
|
+
const all = pending.length === 0 ? data : Buffer.concat([pending, data]);
|
|
796
|
+
const h = wordsOf(state.h);
|
|
797
|
+
const whole = all.length - all.length % 64;
|
|
798
|
+
compress(h, all, 0, whole);
|
|
799
|
+
return {
|
|
800
|
+
h: hexOf(h),
|
|
801
|
+
n: state.n + data.length,
|
|
802
|
+
tail: Buffer.from(all.subarray(whole)).toString("base64")
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
function sha256Digest(state) {
|
|
806
|
+
const pending = Buffer.from(state.tail, "base64");
|
|
807
|
+
const padLen = pending.length < 56 ? 64 : 128;
|
|
808
|
+
const block = Buffer.alloc(padLen);
|
|
809
|
+
pending.copy(block);
|
|
810
|
+
block[pending.length] = 128;
|
|
811
|
+
const bits = state.n * 8;
|
|
812
|
+
block.writeUInt32BE(Math.floor(bits / 2 ** 32), padLen - 8);
|
|
813
|
+
block.writeUInt32BE(bits >>> 0, padLen - 4);
|
|
814
|
+
const h = wordsOf(state.h);
|
|
815
|
+
compress(h, block, 0, padLen);
|
|
816
|
+
return hexOf(h);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/segment-manifest.ts
|
|
820
|
+
var SEGMENT_EXT = ".jsonl";
|
|
821
|
+
var MANIFEST_EXT = ".manifest";
|
|
822
|
+
function manifestPath(segment) {
|
|
823
|
+
return segment.replace(new RegExp(`${SEGMENT_EXT}$`), MANIFEST_EXT);
|
|
824
|
+
}
|
|
825
|
+
function manifestSeal(m) {
|
|
826
|
+
return createHash("sha256").update(
|
|
827
|
+
[
|
|
828
|
+
m.v,
|
|
829
|
+
m.machine,
|
|
830
|
+
m.count,
|
|
831
|
+
m.lastHlc ?? "",
|
|
832
|
+
m.sha256,
|
|
833
|
+
m.size,
|
|
834
|
+
m.mtimeMs,
|
|
835
|
+
m.shaState.h,
|
|
836
|
+
m.shaState.n,
|
|
837
|
+
m.shaState.tail
|
|
838
|
+
].join("")
|
|
839
|
+
).digest("hex");
|
|
840
|
+
}
|
|
841
|
+
var HEX64 = /^[0-9a-f]{64}$/;
|
|
842
|
+
function manifestSelfConsistent(m) {
|
|
843
|
+
const { count, lastHlc, size, mtimeMs, shaState, seal } = m;
|
|
844
|
+
if (!Number.isSafeInteger(count) || count < 0) return false;
|
|
845
|
+
if (typeof size !== "number" || !Number.isSafeInteger(size) || size < 0) return false;
|
|
846
|
+
if (typeof mtimeMs !== "number" || !Number.isFinite(mtimeMs)) return false;
|
|
847
|
+
if (lastHlc !== null && typeof lastHlc !== "string") return false;
|
|
848
|
+
if (count === 0 !== (size === 0) || count === 0 !== (lastHlc === null)) return false;
|
|
849
|
+
if (typeof m.sha256 !== "string" || !HEX64.test(m.sha256)) return false;
|
|
850
|
+
if (typeof seal !== "string" || !HEX64.test(seal)) return false;
|
|
851
|
+
if (typeof shaState !== "object" || shaState === null || typeof shaState.h !== "string" || !HEX64.test(shaState.h) || shaState.n !== size || typeof shaState.tail !== "string") {
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
const tail = Buffer.from(shaState.tail, "base64");
|
|
855
|
+
if (tail.length !== size % 64 || tail.toString("base64") !== shaState.tail) return false;
|
|
856
|
+
if (sha256Digest(shaState) !== m.sha256) return false;
|
|
857
|
+
return manifestSeal({ ...m, size, mtimeMs, shaState }) === seal;
|
|
858
|
+
}
|
|
859
|
+
function writeManifestFile(segment, fields2) {
|
|
860
|
+
const manifest = {
|
|
861
|
+
...fields2,
|
|
862
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
863
|
+
seal: manifestSeal(fields2)
|
|
864
|
+
};
|
|
865
|
+
writeFileSync(manifestPath(segment), `${JSON.stringify(manifest, null, 2)}
|
|
866
|
+
`, "utf8");
|
|
867
|
+
}
|
|
868
|
+
function readManifest(segment) {
|
|
869
|
+
const path = manifestPath(segment);
|
|
870
|
+
if (!existsSync(path)) return null;
|
|
871
|
+
try {
|
|
872
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
873
|
+
} catch {
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/sync-dir-guard.ts
|
|
879
|
+
import { closeSync, openSync, readdirSync, readFileSync as readFileSync2, readSync } from "fs";
|
|
880
|
+
import { join } from "path";
|
|
881
|
+
var SYNC_DIR_FOREIGN_MESSAGE = "MU_SYNC_DIR holds 3.x segments; give mu 4 its own dir";
|
|
882
|
+
var SyncDirForeignError = class extends Error {
|
|
883
|
+
constructor(dir, segment) {
|
|
884
|
+
super(`${SYNC_DIR_FOREIGN_MESSAGE} (${segment})`);
|
|
885
|
+
this.dir = dir;
|
|
886
|
+
this.segment = segment;
|
|
887
|
+
}
|
|
888
|
+
dir;
|
|
889
|
+
segment;
|
|
890
|
+
name = "SyncDirForeignError";
|
|
891
|
+
};
|
|
892
|
+
var VERSION_PREFIX = /^\{"v":(\d+)[,}]/;
|
|
893
|
+
var guardOffForTests = false;
|
|
894
|
+
function manifestIsPreV2(path) {
|
|
895
|
+
try {
|
|
896
|
+
const v = JSON.parse(readFileSync2(path, "utf8"))?.v;
|
|
897
|
+
return typeof v === "number" && v < 2;
|
|
898
|
+
} catch {
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
function startsAsV2OrUnknown(path) {
|
|
903
|
+
let fd;
|
|
904
|
+
try {
|
|
905
|
+
fd = openSync(path, "r");
|
|
906
|
+
} catch {
|
|
907
|
+
return true;
|
|
908
|
+
}
|
|
909
|
+
try {
|
|
910
|
+
const buf = Buffer.alloc(24);
|
|
911
|
+
const n = readSync(fd, buf, 0, buf.length, 0);
|
|
912
|
+
const m = VERSION_PREFIX.exec(buf.toString("utf8", 0, n));
|
|
913
|
+
return m === null || Number(m[1]) >= 2;
|
|
914
|
+
} catch {
|
|
915
|
+
return true;
|
|
916
|
+
} finally {
|
|
917
|
+
closeSync(fd);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
function assertSyncDirV2(dir) {
|
|
921
|
+
if (guardOffForTests) return;
|
|
922
|
+
let names;
|
|
923
|
+
try {
|
|
924
|
+
names = readdirSync(dir);
|
|
925
|
+
} catch {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
for (const name of names.sort()) {
|
|
929
|
+
const path = join(dir, name);
|
|
930
|
+
const foreign = name.endsWith(".jsonl") ? !startsAsV2OrUnknown(path) : name.endsWith(".manifest") && manifestIsPreV2(path);
|
|
931
|
+
if (foreign) throw new SyncDirForeignError(dir, name);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// src/segments.ts
|
|
936
|
+
import { createHash as createHash3 } from "crypto";
|
|
937
|
+
import {
|
|
938
|
+
appendFileSync,
|
|
939
|
+
closeSync as closeSync3,
|
|
940
|
+
existsSync as existsSync2,
|
|
941
|
+
mkdirSync,
|
|
942
|
+
openSync as openSync3,
|
|
943
|
+
readdirSync as readdirSync2,
|
|
944
|
+
readFileSync as readFileSync4,
|
|
945
|
+
readSync as readSync3,
|
|
946
|
+
rmSync,
|
|
947
|
+
statSync as statSync2,
|
|
948
|
+
writeFileSync as writeFileSync2
|
|
949
|
+
} from "fs";
|
|
950
|
+
import { tmpdir } from "os";
|
|
951
|
+
import { dirname, join as join2, resolve, sep } from "path";
|
|
952
|
+
|
|
953
|
+
// src/segment-resume.ts
|
|
954
|
+
import { createHash as createHash2 } from "crypto";
|
|
955
|
+
import { closeSync as closeSync2, fstatSync, openSync as openSync2, readFileSync as readFileSync3, readSync as readSync2, statSync } from "fs";
|
|
956
|
+
function usableShaState(state, length) {
|
|
957
|
+
if (typeof state !== "object" || state === null) return null;
|
|
958
|
+
const { h, n, tail } = state;
|
|
959
|
+
if (typeof h !== "string" || !/^[0-9a-f]{64}$/.test(h)) return null;
|
|
960
|
+
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 0 || n !== length) return null;
|
|
961
|
+
if (typeof tail !== "string") return null;
|
|
962
|
+
const raw = Buffer.from(tail, "base64");
|
|
963
|
+
if (raw.length !== n % 64 || raw.toString("base64") !== tail) return null;
|
|
964
|
+
return { h, n, tail };
|
|
965
|
+
}
|
|
966
|
+
function encodeFingerprint(key, resume) {
|
|
967
|
+
if (resume === null) return key;
|
|
968
|
+
const { lines, size, lastHlc, state } = resume;
|
|
969
|
+
return [key, lines, size, lastHlc, state.h, state.tail].join(" ");
|
|
970
|
+
}
|
|
971
|
+
function decodeFingerprint(raw) {
|
|
972
|
+
const [key = "", lines, size, lastHlc, h, tail] = raw.split(" ");
|
|
973
|
+
if (h === void 0 || tail === void 0 || lines === void 0 || size === void 0) {
|
|
974
|
+
return { key, resume: null };
|
|
975
|
+
}
|
|
976
|
+
const n = Number(size);
|
|
977
|
+
const count = Number(lines);
|
|
978
|
+
if (!Number.isSafeInteger(n) || n <= 0 || !Number.isSafeInteger(count) || count <= 0) {
|
|
979
|
+
return { key, resume: null };
|
|
980
|
+
}
|
|
981
|
+
if (!lastHlc || !/^[0-9a-f]{64}$/.test(h)) return { key, resume: null };
|
|
982
|
+
return { key, resume: { lines: count, size: n, lastHlc, state: { h, n, tail } } };
|
|
983
|
+
}
|
|
984
|
+
function readAppended(path, resume, manifestSha) {
|
|
985
|
+
let fd;
|
|
986
|
+
try {
|
|
987
|
+
fd = openSync2(path, "r");
|
|
988
|
+
} catch {
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
991
|
+
try {
|
|
992
|
+
const size = fstatSync(fd).size;
|
|
993
|
+
if (size <= resume.size) return null;
|
|
994
|
+
const bytes3 = Buffer.alloc(size - resume.size);
|
|
995
|
+
for (let got = 0; got < bytes3.length; ) {
|
|
996
|
+
const n = readSync2(fd, bytes3, got, bytes3.length - got, resume.size + got);
|
|
997
|
+
if (n === 0) return null;
|
|
998
|
+
got += n;
|
|
999
|
+
}
|
|
1000
|
+
const state = sha256Update(resume.state, bytes3);
|
|
1001
|
+
return sha256Digest(state) === manifestSha ? { bytes: bytes3, state } : null;
|
|
1002
|
+
} finally {
|
|
1003
|
+
closeSync2(fd);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function segmentFingerprint(path) {
|
|
1007
|
+
try {
|
|
1008
|
+
const { size, mtimeMs } = statSync(path);
|
|
1009
|
+
const manifest = readFileSync3(manifestPath(path));
|
|
1010
|
+
const sha = createHash2("sha256").update(manifest).digest("hex");
|
|
1011
|
+
let count = null;
|
|
1012
|
+
let parsed = null;
|
|
1013
|
+
try {
|
|
1014
|
+
parsed = JSON.parse(manifest.toString("utf8"));
|
|
1015
|
+
count = parsed?.count;
|
|
1016
|
+
} catch {
|
|
1017
|
+
}
|
|
1018
|
+
return { key: `${size} ${mtimeMs} ${sha}`, count, manifest: parsed };
|
|
1019
|
+
} catch {
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function recordedFingerprint(db, watermarkKey) {
|
|
1024
|
+
const row = prepareCached(
|
|
1025
|
+
db,
|
|
1026
|
+
"SELECT fingerprint FROM sync_fingerprints WHERE machine_id = ?"
|
|
1027
|
+
).get(watermarkKey);
|
|
1028
|
+
return row?.fingerprint ?? null;
|
|
1029
|
+
}
|
|
1030
|
+
function segmentCaughtUp(db, watermarkKey, path, lines) {
|
|
1031
|
+
const recorded = recordedFingerprint(db, watermarkKey);
|
|
1032
|
+
if (recorded === null) return false;
|
|
1033
|
+
const now = segmentFingerprint(path);
|
|
1034
|
+
return now !== null && now.count === lines && now.key === decodeFingerprint(recorded).key;
|
|
1035
|
+
}
|
|
1036
|
+
function resumePoint(db, watermarkKey, lines) {
|
|
1037
|
+
const recorded = recordedFingerprint(db, watermarkKey);
|
|
1038
|
+
const resume = recorded === null ? null : decodeFingerprint(recorded).resume;
|
|
1039
|
+
return resume !== null && resume.lines === lines ? resume : null;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/segments.ts
|
|
1043
|
+
var SEGMENT_EXT2 = ".jsonl";
|
|
1044
|
+
function syncDir() {
|
|
1045
|
+
const dir = process.env.MU_SYNC_DIR;
|
|
1046
|
+
if (dir === void 0 || dir.trim() === "") return null;
|
|
1047
|
+
refuseUserSyncDirDuringTests(dir);
|
|
1048
|
+
return dir;
|
|
1049
|
+
}
|
|
1050
|
+
function refuseUserSyncDirDuringTests(dir) {
|
|
1051
|
+
const inTest = process.env.VITEST !== void 0 || process.env.NODE_ENV === "test";
|
|
1052
|
+
if (!inTest) return;
|
|
1053
|
+
const resolved = resolve(dir);
|
|
1054
|
+
const temp = resolve(tmpdir());
|
|
1055
|
+
if (resolved === temp || resolved.startsWith(temp + sep)) return;
|
|
1056
|
+
throw new Error(
|
|
1057
|
+
`syncDir refused: tests must NEVER flush ops into a real sync dir (MU_SYNC_DIR=${resolved}). Doing so writes a segment named after this run's machine_id and leaves a permanent phantom peer. Point MU_SYNC_DIR at a mkdtemp() path under ${temp}, or leave it unset (test/_setup.ts clears it by default).`
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
function localMachineId(db) {
|
|
1061
|
+
const row = db.prepare("SELECT machine_id FROM machine_identity WHERE id = 1").get();
|
|
1062
|
+
if (row === void 0) throw new Error("machine_identity row missing; not a mu DB");
|
|
1063
|
+
return row.machine_id;
|
|
1064
|
+
}
|
|
1065
|
+
function segmentPath(dir, machineId) {
|
|
1066
|
+
return join2(dir, `${machineId}${SEGMENT_EXT2}`);
|
|
1067
|
+
}
|
|
1068
|
+
var AMBIENT_FLUSH_LOCK_WAIT_MS = 2e3;
|
|
1069
|
+
var VERIFY_FLUSH_LOCK_WAIT_MS = 15e3;
|
|
1070
|
+
async function flushSegment(db, dir = syncDir(), opts) {
|
|
1071
|
+
if (dir === null)
|
|
1072
|
+
return {
|
|
1073
|
+
segmentPath: null,
|
|
1074
|
+
appended: 0,
|
|
1075
|
+
total: 0,
|
|
1076
|
+
skippedLocal: 0,
|
|
1077
|
+
selfRepaired: null,
|
|
1078
|
+
foreignVersionAt: null,
|
|
1079
|
+
foreignVersion: null
|
|
1080
|
+
};
|
|
1081
|
+
assertSyncDirV2(dir);
|
|
1082
|
+
const machineId = localMachineId(db);
|
|
1083
|
+
mkdirSync(dir, { recursive: true });
|
|
1084
|
+
const path = segmentPath(dir, machineId);
|
|
1085
|
+
const verify = opts?.verify === true;
|
|
1086
|
+
const pre = verify ? null : trustedTail(path, machineId);
|
|
1087
|
+
if (pre !== null) {
|
|
1088
|
+
const pending = pendingLines(db, machineId, pre.lastHlc, false);
|
|
1089
|
+
if (pending.lines.length === 0) {
|
|
1090
|
+
return {
|
|
1091
|
+
segmentPath: path,
|
|
1092
|
+
appended: 0,
|
|
1093
|
+
total: pre.count,
|
|
1094
|
+
skippedLocal: pending.skippedLocal,
|
|
1095
|
+
selfRepaired: null,
|
|
1096
|
+
foreignVersionAt: null,
|
|
1097
|
+
foreignVersion: null
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
const lockName = createHash3("sha256").update(`${dir}${machineId}`).digest("hex").slice(0, 16);
|
|
1102
|
+
return withFileLock(
|
|
1103
|
+
join2(locksDir(), `segment-${lockName}.lock`),
|
|
1104
|
+
`segment:${machineId}`,
|
|
1105
|
+
() => Promise.resolve(flushLocked(db, path, machineId, verify)),
|
|
1106
|
+
{
|
|
1107
|
+
acquireTimeoutMs: opts?.lockWaitMs ?? (verify ? VERIFY_FLUSH_LOCK_WAIT_MS : AMBIENT_FLUSH_LOCK_WAIT_MS),
|
|
1108
|
+
onUnavailable: () => ({
|
|
1109
|
+
segmentPath: path,
|
|
1110
|
+
appended: 0,
|
|
1111
|
+
total: pre?.count ?? 0,
|
|
1112
|
+
skippedLocal: 0,
|
|
1113
|
+
selfRepaired: null,
|
|
1114
|
+
foreignVersionAt: null,
|
|
1115
|
+
foreignVersion: null,
|
|
1116
|
+
lockBusy: true
|
|
1117
|
+
})
|
|
1118
|
+
}
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
function flushLocked(db, path, machineId, verify) {
|
|
1122
|
+
const trusted = verify ? null : trustedTail(path, machineId);
|
|
1123
|
+
let count;
|
|
1124
|
+
let since;
|
|
1125
|
+
let selfRepaired = null;
|
|
1126
|
+
if (trusted !== null) {
|
|
1127
|
+
count = trusted.count;
|
|
1128
|
+
since = trusted.lastHlc;
|
|
1129
|
+
} else {
|
|
1130
|
+
const existing = readSegmentTail(path);
|
|
1131
|
+
count = existing.count;
|
|
1132
|
+
since = existing.lastHlc;
|
|
1133
|
+
if (existing.foreign !== null) {
|
|
1134
|
+
rmSync(manifestPath(path), { force: true });
|
|
1135
|
+
return {
|
|
1136
|
+
segmentPath: path,
|
|
1137
|
+
appended: 0,
|
|
1138
|
+
total: existing.lineCount,
|
|
1139
|
+
skippedLocal: 0,
|
|
1140
|
+
selfRepaired: null,
|
|
1141
|
+
foreignVersionAt: existing.foreign.line,
|
|
1142
|
+
foreignVersion: existing.foreign.version
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
if (existing.defect !== null) {
|
|
1146
|
+
selfRepaired = existing.defect;
|
|
1147
|
+
writeFileSync2(
|
|
1148
|
+
path,
|
|
1149
|
+
existing.goodLines.length > 0 ? `${existing.goodLines.join("\n")}
|
|
1150
|
+
` : "",
|
|
1151
|
+
"utf8"
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
const pending = pendingLines(db, machineId, since, trusted === null);
|
|
1156
|
+
let appended = null;
|
|
1157
|
+
if (pending.lines.length > 0) {
|
|
1158
|
+
appended = Buffer.from(`${pending.lines.join("\n")}
|
|
1159
|
+
`, "utf8");
|
|
1160
|
+
appendFileSync(path, appended);
|
|
1161
|
+
} else if (!existsSync2(path)) {
|
|
1162
|
+
writeFileSync2(path, "", "utf8");
|
|
1163
|
+
}
|
|
1164
|
+
const total = count + pending.lines.length;
|
|
1165
|
+
const result = {
|
|
1166
|
+
segmentPath: path,
|
|
1167
|
+
appended: pending.lines.length,
|
|
1168
|
+
total,
|
|
1169
|
+
skippedLocal: pending.skippedLocal,
|
|
1170
|
+
selfRepaired,
|
|
1171
|
+
foreignVersionAt: null,
|
|
1172
|
+
foreignVersion: null
|
|
1173
|
+
};
|
|
1174
|
+
if (trusted !== null && appended === null) return result;
|
|
1175
|
+
writeManifest(
|
|
1176
|
+
path,
|
|
1177
|
+
machineId,
|
|
1178
|
+
total,
|
|
1179
|
+
pending.lastHlc ?? since,
|
|
1180
|
+
trusted !== null && appended !== null ? { state: trusted.shaState, size: trusted.size, appended } : null
|
|
1181
|
+
);
|
|
1182
|
+
return result;
|
|
1183
|
+
}
|
|
1184
|
+
var SYNCED_ENTITIES_SQL = SYNCED_ENTITIES.map((e) => `'${e}'`).join(", ");
|
|
1185
|
+
function pendingLines(db, machineId, since, countTrailing) {
|
|
1186
|
+
const from = since ?? "";
|
|
1187
|
+
const rows = prepareCached(
|
|
1188
|
+
db,
|
|
1189
|
+
`SELECT hlc, machine_id, group_id, intent, actor, entity, key, op, payload
|
|
1190
|
+
FROM ops INDEXED BY idx_ops_machine_entity_hlc
|
|
1191
|
+
WHERE machine_id = @machineId
|
|
1192
|
+
AND entity IN (${SYNCED_ENTITIES_SQL})
|
|
1193
|
+
AND hlc > @from
|
|
1194
|
+
ORDER BY hlc`
|
|
1195
|
+
).all({ machineId, from });
|
|
1196
|
+
const lines = [];
|
|
1197
|
+
let lastHlc = null;
|
|
1198
|
+
for (const row2 of rows) {
|
|
1199
|
+
lines.push(
|
|
1200
|
+
encodeSegmentLine({
|
|
1201
|
+
hlc: row2.hlc,
|
|
1202
|
+
machineId: row2.machine_id,
|
|
1203
|
+
groupId: row2.group_id,
|
|
1204
|
+
intent: row2.intent,
|
|
1205
|
+
actor: row2.actor,
|
|
1206
|
+
entity: row2.entity,
|
|
1207
|
+
key: row2.key,
|
|
1208
|
+
op: row2.op === "del" ? "del" : "put",
|
|
1209
|
+
payload: row2.payload
|
|
1210
|
+
})
|
|
1211
|
+
);
|
|
1212
|
+
lastHlc = row2.hlc;
|
|
1213
|
+
}
|
|
1214
|
+
let row;
|
|
1215
|
+
if (countTrailing) {
|
|
1216
|
+
row = prepareCached(
|
|
1217
|
+
db,
|
|
1218
|
+
"SELECT COUNT(*) AS n FROM ops WHERE machine_id = @machineId AND hlc > @from"
|
|
1219
|
+
).get({ machineId, from });
|
|
1220
|
+
} else if (lastHlc !== null) {
|
|
1221
|
+
row = prepareCached(
|
|
1222
|
+
db,
|
|
1223
|
+
`SELECT COUNT(*) AS n FROM ops
|
|
1224
|
+
WHERE machine_id = @machineId AND hlc > @from AND hlc <= @upTo`
|
|
1225
|
+
).get({ machineId, from, upTo: lastHlc });
|
|
1226
|
+
} else {
|
|
1227
|
+
return { lines, lastHlc, skippedLocal: 0 };
|
|
1228
|
+
}
|
|
1229
|
+
return { lines, lastHlc, skippedLocal: row.n - lines.length };
|
|
1230
|
+
}
|
|
1231
|
+
function trustedTail(path, machineId) {
|
|
1232
|
+
const m = readManifest(path);
|
|
1233
|
+
if (m === null || !SEGMENT_READ_VERSIONS.includes(m.v) || m.machine !== machineId) return null;
|
|
1234
|
+
if (!manifestSelfConsistent(m)) return null;
|
|
1235
|
+
const { count, lastHlc, size, mtimeMs, shaState } = m;
|
|
1236
|
+
let stat;
|
|
1237
|
+
try {
|
|
1238
|
+
stat = statSync2(path);
|
|
1239
|
+
} catch {
|
|
1240
|
+
return null;
|
|
1241
|
+
}
|
|
1242
|
+
if (stat.size !== size || stat.mtimeMs !== mtimeMs) return null;
|
|
1243
|
+
if (count === 0) return lastHlc === null ? { count, lastHlc, size, shaState } : null;
|
|
1244
|
+
const last = readLastLine(path, size);
|
|
1245
|
+
if (last === null) return null;
|
|
1246
|
+
const decoded = decodeLine(last);
|
|
1247
|
+
if (!decoded.ok || decoded.line.hlc !== lastHlc) return null;
|
|
1248
|
+
return { count, lastHlc, size, shaState };
|
|
1249
|
+
}
|
|
1250
|
+
function readLastLine(path, size) {
|
|
1251
|
+
if (size === 0) return null;
|
|
1252
|
+
const fd = openSync3(path, "r");
|
|
1253
|
+
try {
|
|
1254
|
+
const last = Buffer.alloc(1);
|
|
1255
|
+
readSync3(fd, last, 0, 1, size - 1);
|
|
1256
|
+
if (last[0] !== 10) return null;
|
|
1257
|
+
const chunks = [];
|
|
1258
|
+
let end = size - 1;
|
|
1259
|
+
while (end > 0) {
|
|
1260
|
+
const start = Math.max(0, end - 64 * 1024);
|
|
1261
|
+
const buf = Buffer.alloc(end - start);
|
|
1262
|
+
readSync3(fd, buf, 0, buf.length, start);
|
|
1263
|
+
const nl = buf.lastIndexOf(10);
|
|
1264
|
+
if (nl >= 0) {
|
|
1265
|
+
chunks.unshift(buf.subarray(nl + 1));
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
chunks.unshift(buf);
|
|
1269
|
+
end = start;
|
|
1270
|
+
}
|
|
1271
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1272
|
+
} finally {
|
|
1273
|
+
closeSync3(fd);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
function ownSegmentForeignLine(db, dir = syncDir()) {
|
|
1277
|
+
if (dir === null) return null;
|
|
1278
|
+
const path = segmentPath(dir, localMachineId(db));
|
|
1279
|
+
const found = readSegmentTail(path).foreign;
|
|
1280
|
+
return found === null ? null : { path, ...found };
|
|
1281
|
+
}
|
|
1282
|
+
function countUnpublishedOps(db, dir) {
|
|
1283
|
+
const machineId = localMachineId(db);
|
|
1284
|
+
const { lastHlc } = readSegmentTail(segmentPath(dir, machineId));
|
|
1285
|
+
const row = prepareCached(
|
|
1286
|
+
db,
|
|
1287
|
+
`SELECT COUNT(*) AS n FROM ops INDEXED BY idx_ops_machine_entity_hlc
|
|
1288
|
+
WHERE machine_id = @machineId AND entity IN (${SYNCED_ENTITIES_SQL}) AND hlc > @from`
|
|
1289
|
+
).get({ machineId, from: lastHlc ?? "" });
|
|
1290
|
+
return row.n;
|
|
1291
|
+
}
|
|
1292
|
+
function segmentLineCount(path) {
|
|
1293
|
+
return readSegmentTail(path).count;
|
|
1294
|
+
}
|
|
1295
|
+
function readSegmentTail(path) {
|
|
1296
|
+
if (!existsSync2(path))
|
|
1297
|
+
return { count: 0, lastHlc: null, goodLines: [], defect: null, foreign: null, lineCount: 0 };
|
|
1298
|
+
const lines = segmentLines(readFileSync4(path, "utf8"));
|
|
1299
|
+
let lastHlc = null;
|
|
1300
|
+
let count = 0;
|
|
1301
|
+
let defect = null;
|
|
1302
|
+
for (let index = 0; index < lines.length; index++) {
|
|
1303
|
+
const line = lines[index];
|
|
1304
|
+
if (line === void 0) break;
|
|
1305
|
+
if (line.trim() === "") {
|
|
1306
|
+
defect = { kind: "malformed-shape", line: index + 1, detail: "blank line" };
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
const decoded = decodeLine(line);
|
|
1310
|
+
if (!decoded.ok) {
|
|
1311
|
+
defect = {
|
|
1312
|
+
kind: decoded.kind,
|
|
1313
|
+
line: index + 1,
|
|
1314
|
+
detail: decoded.detail,
|
|
1315
|
+
...decoded.version === void 0 ? {} : { version: decoded.version }
|
|
1316
|
+
};
|
|
1317
|
+
break;
|
|
1318
|
+
}
|
|
1319
|
+
lastHlc = decoded.line.hlc;
|
|
1320
|
+
count += 1;
|
|
1321
|
+
}
|
|
1322
|
+
let foreign = null;
|
|
1323
|
+
if (defect?.kind === "unknown-version" && defect.version !== void 0) {
|
|
1324
|
+
foreign = { line: defect.line, version: defect.version };
|
|
1325
|
+
} else if (defect !== null) {
|
|
1326
|
+
for (let i = count + 1; i < lines.length && foreign === null; i++) {
|
|
1327
|
+
const later = decodeLine(lines[i] ?? "");
|
|
1328
|
+
if (!later.ok && later.kind === "unknown-version" && later.version !== void 0)
|
|
1329
|
+
foreign = { line: i + 1, version: later.version };
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return {
|
|
1333
|
+
count,
|
|
1334
|
+
lastHlc,
|
|
1335
|
+
goodLines: lines.slice(0, count),
|
|
1336
|
+
defect,
|
|
1337
|
+
foreign,
|
|
1338
|
+
lineCount: lines.length
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
function segmentLines(raw) {
|
|
1342
|
+
const lines = raw.split("\n");
|
|
1343
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
1344
|
+
return lines;
|
|
1345
|
+
}
|
|
1346
|
+
function writeManifest(path, machine, count, lastHlc, incremental) {
|
|
1347
|
+
let stat = statSync2(path);
|
|
1348
|
+
let shaState;
|
|
1349
|
+
if (incremental !== null && stat.size === incremental.size + incremental.appended.length) {
|
|
1350
|
+
shaState = sha256Update(incremental.state, incremental.appended);
|
|
1351
|
+
} else {
|
|
1352
|
+
const bytes3 = readFileSync4(path);
|
|
1353
|
+
shaState = sha256Update(sha256Initial(), bytes3);
|
|
1354
|
+
stat = statSync2(path);
|
|
1355
|
+
}
|
|
1356
|
+
writeManifestFile(path, {
|
|
1357
|
+
v: SEGMENT_FORMAT_VERSION,
|
|
1358
|
+
machine,
|
|
1359
|
+
count,
|
|
1360
|
+
lastHlc,
|
|
1361
|
+
sha256: sha256Digest(shaState),
|
|
1362
|
+
size: shaState.n,
|
|
1363
|
+
mtimeMs: stat.mtimeMs,
|
|
1364
|
+
shaState
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
function verifyAgainstManifest(segment) {
|
|
1368
|
+
const manifest = readManifest(segment);
|
|
1369
|
+
if (manifest === null) return { ok: true };
|
|
1370
|
+
if (!existsSync2(segment)) return { ok: false, reason: "segment missing but manifest present" };
|
|
1371
|
+
return checkBytesAgainstManifest(segment, manifest, readFileSync4(segment));
|
|
1372
|
+
}
|
|
1373
|
+
function checkBytesAgainstManifest(segment, manifest, bytes3) {
|
|
1374
|
+
const sha = createHash3("sha256").update(bytes3).digest("hex");
|
|
1375
|
+
if (sha === manifest.sha256) return { ok: true, exact: true };
|
|
1376
|
+
const tail = readSegmentTail(segment);
|
|
1377
|
+
if (tail.count >= manifest.count) {
|
|
1378
|
+
return { ok: true, exact: false };
|
|
1379
|
+
}
|
|
1380
|
+
return {
|
|
1381
|
+
ok: false,
|
|
1382
|
+
reason: `sha mismatch and file has FEWER records than the manifest (${tail.count} < ${manifest.count}): truncated`
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
function discoverPeers(dir, selfMachineId) {
|
|
1386
|
+
if (!existsSync2(dir)) return [];
|
|
1387
|
+
assertSyncDirV2(dir);
|
|
1388
|
+
const peers = [];
|
|
1389
|
+
for (const name of readdirSync2(dir)) {
|
|
1390
|
+
if (!name.endsWith(SEGMENT_EXT2)) continue;
|
|
1391
|
+
const stem = name.slice(0, -SEGMENT_EXT2.length);
|
|
1392
|
+
const conflictAt = stem.indexOf(".sync-conflict-");
|
|
1393
|
+
const conflictCopy = conflictAt > 0;
|
|
1394
|
+
const machineId = conflictCopy ? stem.slice(0, conflictAt) : stem;
|
|
1395
|
+
if (machineId === selfMachineId && !conflictCopy) continue;
|
|
1396
|
+
const path = join2(dir, name);
|
|
1397
|
+
try {
|
|
1398
|
+
if (!statSync2(path).isFile()) continue;
|
|
1399
|
+
} catch {
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
peers.push({ machineId, watermarkKey: conflictCopy ? stem : machineId, path, conflictCopy });
|
|
1403
|
+
}
|
|
1404
|
+
return peers.sort((a, b) => compareStrings(a.path, b.path));
|
|
1405
|
+
}
|
|
1406
|
+
function peerWatermarkKey(peer) {
|
|
1407
|
+
return peer.watermarkKey ?? peer.machineId;
|
|
1408
|
+
}
|
|
1409
|
+
function getWatermark(db, watermarkKey) {
|
|
1410
|
+
const row = db.prepare("SELECT last_applied_seq AS n FROM sync_peers WHERE machine_id = ?").get(watermarkKey);
|
|
1411
|
+
return row?.n ?? 0;
|
|
1412
|
+
}
|
|
1413
|
+
function setWatermark(db, watermarkKey, value, fingerprint) {
|
|
1414
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1415
|
+
prepareCached(
|
|
1416
|
+
db,
|
|
1417
|
+
`INSERT INTO sync_peers (machine_id, last_applied_seq, last_seen_at)
|
|
1418
|
+
VALUES (@machineId, @value, @seenAt)
|
|
1419
|
+
ON CONFLICT (machine_id) DO UPDATE
|
|
1420
|
+
SET last_applied_seq = @value, last_seen_at = @seenAt`
|
|
1421
|
+
).run({ machineId: watermarkKey, value, seenAt: now });
|
|
1422
|
+
if (fingerprint == null) {
|
|
1423
|
+
prepareCached(db, "DELETE FROM sync_fingerprints WHERE machine_id = ?").run(watermarkKey);
|
|
1424
|
+
} else {
|
|
1425
|
+
prepareCached(
|
|
1426
|
+
db,
|
|
1427
|
+
`INSERT INTO sync_fingerprints (machine_id, fingerprint) VALUES (?, ?)
|
|
1428
|
+
ON CONFLICT (machine_id) DO UPDATE SET fingerprint = excluded.fingerprint`
|
|
1429
|
+
).run(watermarkKey, fingerprint);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
function resetWatermark(db, watermarkKey) {
|
|
1433
|
+
setWatermark(db, watermarkKey, 0);
|
|
1434
|
+
}
|
|
1435
|
+
function ingestSegment(db, peer, opts) {
|
|
1436
|
+
assertSyncDirV2(dirname(peer.path));
|
|
1437
|
+
const defects = [];
|
|
1438
|
+
const watermarkKey = peerWatermarkKey(peer);
|
|
1439
|
+
const start = getWatermark(db, watermarkKey);
|
|
1440
|
+
const nothing = {
|
|
1441
|
+
machineId: peer.machineId,
|
|
1442
|
+
path: peer.path,
|
|
1443
|
+
read: 0,
|
|
1444
|
+
applied: 0,
|
|
1445
|
+
changed: 0,
|
|
1446
|
+
watermark: start,
|
|
1447
|
+
defects,
|
|
1448
|
+
truncatedAt: null
|
|
1449
|
+
};
|
|
1450
|
+
if (opts?.verify !== true && segmentCaughtUp(db, watermarkKey, peer.path, start)) return nothing;
|
|
1451
|
+
if (!existsSync2(peer.path)) return nothing;
|
|
1452
|
+
const fingerprint = segmentFingerprint(peer.path);
|
|
1453
|
+
const resume = opts?.verify !== true && fingerprint?.manifest != null && start > 0 ? resumePoint(db, watermarkKey, start) : null;
|
|
1454
|
+
const appended = resume !== null && fingerprint?.manifest != null ? readAppended(peer.path, resume, fingerprint.manifest.sha256) : null;
|
|
1455
|
+
let lines;
|
|
1456
|
+
let base;
|
|
1457
|
+
let seedHlc = null;
|
|
1458
|
+
let nextResume = null;
|
|
1459
|
+
let resumeState = null;
|
|
1460
|
+
let resumeSize = 0;
|
|
1461
|
+
if (resume !== null && appended !== null) {
|
|
1462
|
+
lines = segmentLines(appended.bytes.toString("utf8"));
|
|
1463
|
+
base = resume.lines;
|
|
1464
|
+
seedHlc = resume.lastHlc;
|
|
1465
|
+
resumeState = appended.bytes[appended.bytes.length - 1] === 10 ? appended.state : null;
|
|
1466
|
+
resumeSize = appended.state.n;
|
|
1467
|
+
} else {
|
|
1468
|
+
const manifest = readManifest(peer.path);
|
|
1469
|
+
const bytes3 = readFileSync4(peer.path);
|
|
1470
|
+
if (manifest !== null) {
|
|
1471
|
+
const verified = checkBytesAgainstManifest(peer.path, manifest, bytes3);
|
|
1472
|
+
if (!verified.ok) {
|
|
1473
|
+
defects.push({ kind: "manifest-mismatch", line: 0, detail: verified.reason });
|
|
1474
|
+
} else if (verified.exact) {
|
|
1475
|
+
const state = usableShaState(manifest.shaState, bytes3.length);
|
|
1476
|
+
if (state !== null && bytes3[bytes3.length - 1] === 10 && sha256Digest(state) === manifest.sha256) {
|
|
1477
|
+
resumeState = state;
|
|
1478
|
+
resumeSize = bytes3.length;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
lines = segmentLines(bytes3.toString("utf8"));
|
|
1483
|
+
base = 0;
|
|
1484
|
+
if (start > 0 && start <= lines.length) {
|
|
1485
|
+
const prior = lines[start - 1];
|
|
1486
|
+
if (prior !== void 0) {
|
|
1487
|
+
const decoded = decodeLine(prior);
|
|
1488
|
+
if (decoded.ok) seedHlc = decoded.line.hlc;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
const total = base + lines.length;
|
|
1493
|
+
let applied = 0;
|
|
1494
|
+
let changed = 0;
|
|
1495
|
+
let watermark = start;
|
|
1496
|
+
let truncatedAt = null;
|
|
1497
|
+
let previousHlc = seedHlc;
|
|
1498
|
+
const run = db.transaction(() => {
|
|
1499
|
+
for (let index = start - base; index < lines.length; index++) {
|
|
1500
|
+
const raw = lines[index];
|
|
1501
|
+
const lineNo = base + index + 1;
|
|
1502
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
1503
|
+
defects.push({ kind: "malformed-shape", line: lineNo, detail: "blank line" });
|
|
1504
|
+
truncatedAt = lineNo;
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1507
|
+
const decoded = decodeLine(raw);
|
|
1508
|
+
if (!decoded.ok) {
|
|
1509
|
+
defects.push({
|
|
1510
|
+
kind: decoded.kind,
|
|
1511
|
+
line: lineNo,
|
|
1512
|
+
detail: decoded.detail,
|
|
1513
|
+
...decoded.version === void 0 ? {} : { version: decoded.version }
|
|
1514
|
+
});
|
|
1515
|
+
truncatedAt = lineNo;
|
|
1516
|
+
break;
|
|
1517
|
+
}
|
|
1518
|
+
const op = {
|
|
1519
|
+
hlc: decoded.line.hlc,
|
|
1520
|
+
machineId: decoded.line.machine,
|
|
1521
|
+
groupId: decoded.line.group,
|
|
1522
|
+
actor: decoded.line.actor,
|
|
1523
|
+
intent: decoded.line.intent,
|
|
1524
|
+
entity: decoded.line.entity,
|
|
1525
|
+
key: decoded.line.key,
|
|
1526
|
+
op: decoded.line.op,
|
|
1527
|
+
payload: decoded.payloadText
|
|
1528
|
+
};
|
|
1529
|
+
if (previousHlc !== null && decoded.line.hlc <= previousHlc) {
|
|
1530
|
+
if (isAlreadyRecorded(db, op)) {
|
|
1531
|
+
defects.push({
|
|
1532
|
+
kind: "duplicate-op",
|
|
1533
|
+
line: lineNo,
|
|
1534
|
+
detail: `hlc ${decoded.line.hlc} re-delivered \u2014 skipped`
|
|
1535
|
+
});
|
|
1536
|
+
watermark = lineNo;
|
|
1537
|
+
continue;
|
|
1538
|
+
}
|
|
1539
|
+
defects.push({
|
|
1540
|
+
kind: "non-monotonic-hlc",
|
|
1541
|
+
line: lineNo,
|
|
1542
|
+
detail: `hlc ${decoded.line.hlc} <= previous ${previousHlc}`
|
|
1543
|
+
});
|
|
1544
|
+
truncatedAt = lineNo;
|
|
1545
|
+
break;
|
|
1546
|
+
}
|
|
1547
|
+
try {
|
|
1548
|
+
const result = applyIncomingOp(db, op);
|
|
1549
|
+
if (result.changed) changed += 1;
|
|
1550
|
+
} catch (err) {
|
|
1551
|
+
if (err instanceof OpEntityNotSyncedError) {
|
|
1552
|
+
defects.push({
|
|
1553
|
+
kind: "entity-not-synced",
|
|
1554
|
+
line: lineNo,
|
|
1555
|
+
detail: `peer sent machine-local entity '${op.entity}' \u2014 skipped`
|
|
1556
|
+
});
|
|
1557
|
+
previousHlc = op.hlc;
|
|
1558
|
+
watermark = lineNo;
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1561
|
+
throw err;
|
|
1562
|
+
}
|
|
1563
|
+
applied += 1;
|
|
1564
|
+
previousHlc = op.hlc;
|
|
1565
|
+
watermark = lineNo;
|
|
1566
|
+
}
|
|
1567
|
+
const clean = defects.length === 0 && watermark === total;
|
|
1568
|
+
if (clean && resumeState !== null && previousHlc !== null) {
|
|
1569
|
+
nextResume = { lines: total, size: resumeSize, lastHlc: previousHlc, state: resumeState };
|
|
1570
|
+
}
|
|
1571
|
+
setWatermark(
|
|
1572
|
+
db,
|
|
1573
|
+
watermarkKey,
|
|
1574
|
+
watermark,
|
|
1575
|
+
clean && fingerprint !== null ? encodeFingerprint(fingerprint.key, nextResume) : null
|
|
1576
|
+
);
|
|
1577
|
+
});
|
|
1578
|
+
run.immediate();
|
|
1579
|
+
return {
|
|
1580
|
+
machineId: peer.machineId,
|
|
1581
|
+
path: peer.path,
|
|
1582
|
+
read: watermark - start,
|
|
1583
|
+
applied,
|
|
1584
|
+
changed,
|
|
1585
|
+
watermark,
|
|
1586
|
+
defects,
|
|
1587
|
+
truncatedAt
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
function isAlreadyRecorded(db, op) {
|
|
1591
|
+
const row = prepareCached(
|
|
1592
|
+
db,
|
|
1593
|
+
`SELECT 1 FROM ops
|
|
1594
|
+
WHERE machine_id = @machineId AND hlc = @hlc
|
|
1595
|
+
AND entity = @entity AND key = @key AND op = @op
|
|
1596
|
+
LIMIT 1`
|
|
1597
|
+
).get({
|
|
1598
|
+
machineId: op.machineId,
|
|
1599
|
+
hlc: op.hlc,
|
|
1600
|
+
entity: op.entity,
|
|
1601
|
+
key: op.key,
|
|
1602
|
+
op: op.op
|
|
1603
|
+
});
|
|
1604
|
+
return row !== void 0;
|
|
1605
|
+
}
|
|
1606
|
+
function applyIncomingOp(db, op) {
|
|
1607
|
+
receiveHlc(db, op.hlc);
|
|
1608
|
+
const result = applyOp(db, op);
|
|
1609
|
+
prepareCached(
|
|
1610
|
+
db,
|
|
1611
|
+
`INSERT OR IGNORE INTO ops
|
|
1612
|
+
(hlc, machine_id, group_id, actor, intent, entity, key, op, payload, created_at)
|
|
1613
|
+
VALUES (@hlc, @machineId, @groupId, @actor, @intent, @entity, @key, @op, @payload, @createdAt)`
|
|
1614
|
+
).run({
|
|
1615
|
+
hlc: op.hlc,
|
|
1616
|
+
machineId: op.machineId,
|
|
1617
|
+
groupId: op.groupId,
|
|
1618
|
+
actor: op.actor ?? null,
|
|
1619
|
+
intent: op.intent ?? null,
|
|
1620
|
+
entity: op.entity,
|
|
1621
|
+
key: op.key,
|
|
1622
|
+
op: op.op,
|
|
1623
|
+
payload: op.payload,
|
|
1624
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1625
|
+
});
|
|
1626
|
+
return { changed: result.changed };
|
|
1627
|
+
}
|
|
1628
|
+
async function syncPass(db, dir = syncDir()) {
|
|
1629
|
+
if (dir === null) {
|
|
1630
|
+
return {
|
|
1631
|
+
flushed: {
|
|
1632
|
+
segmentPath: null,
|
|
1633
|
+
appended: 0,
|
|
1634
|
+
total: 0,
|
|
1635
|
+
skippedLocal: 0,
|
|
1636
|
+
selfRepaired: null,
|
|
1637
|
+
foreignVersionAt: null,
|
|
1638
|
+
foreignVersion: null
|
|
1639
|
+
},
|
|
1640
|
+
ingested: [],
|
|
1641
|
+
defective: false
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
const flushed = await flushSegment(db, dir, { verify: true });
|
|
1645
|
+
const self = localMachineId(db);
|
|
1646
|
+
const ingested = discoverPeers(dir, self).map(
|
|
1647
|
+
(peer) => ingestSegment(db, peer, { verify: true })
|
|
1648
|
+
);
|
|
1649
|
+
return {
|
|
1650
|
+
flushed,
|
|
1651
|
+
ingested,
|
|
1652
|
+
defective: flushed.selfRepaired !== null || flushed.foreignVersionAt !== null || ingested.some((r) => r.defects.length > 0)
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// src/task-auto.ts
|
|
1657
|
+
var CLI_KEY_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
1658
|
+
var TaskAutoRefusedError = class extends Error {
|
|
1659
|
+
constructor(message, workstream, taskId) {
|
|
1660
|
+
super(message);
|
|
1661
|
+
this.workstream = workstream;
|
|
1662
|
+
this.taskId = taskId;
|
|
1663
|
+
}
|
|
1664
|
+
workstream;
|
|
1665
|
+
taskId;
|
|
1666
|
+
name = "TaskAutoRefusedError";
|
|
1667
|
+
};
|
|
1668
|
+
var TaskAutoArgError = class extends Error {
|
|
1669
|
+
name = "TaskAutoArgError";
|
|
1670
|
+
};
|
|
1671
|
+
var configOf = (r) => ({
|
|
1672
|
+
cli: r.cli,
|
|
1673
|
+
worktree: r.worktree === 1,
|
|
1674
|
+
maxRuns: r.max_runs,
|
|
1675
|
+
runs: r.runs,
|
|
1676
|
+
lastSpawn: r.last_spawn,
|
|
1677
|
+
enabled: r.enabled === 1
|
|
1678
|
+
});
|
|
1679
|
+
function getAutoConfig(db, taskId) {
|
|
1680
|
+
const row = db.prepare(
|
|
1681
|
+
"SELECT cli, worktree, max_runs, runs, last_spawn, enabled FROM task_auto WHERE task_id = ?"
|
|
1682
|
+
).get(taskId);
|
|
1683
|
+
return row === void 0 ? void 0 : configOf(row);
|
|
1684
|
+
}
|
|
1685
|
+
function autoPermitted(db, taskId) {
|
|
1686
|
+
return db.prepare(
|
|
1687
|
+
`SELECT 1 AS x FROM task_auto a JOIN tasks t ON t.id = a.task_id
|
|
1688
|
+
WHERE a.task_id = ? AND a.enabled = 1 AND t.kind = 'work' AND t.dispatch_host = ?`
|
|
1689
|
+
).get(taskId, localMachineId(db)) !== void 0;
|
|
1690
|
+
}
|
|
1691
|
+
function authorisedReady(db, workstream) {
|
|
1692
|
+
if (RESERVED_WORKSTREAM_NAMES.has(workstream)) return [];
|
|
1693
|
+
const rows = db.prepare(
|
|
1694
|
+
`SELECT t.local_id AS local_id, a.cli, a.worktree, a.max_runs, a.runs, a.last_spawn, a.enabled
|
|
1695
|
+
FROM task_auto a
|
|
1696
|
+
JOIN tasks t ON t.id = a.task_id
|
|
1697
|
+
JOIN workstreams w ON w.id = t.workstream_id AND w.name = ?
|
|
1698
|
+
WHERE a.enabled = 1 AND a.runs < a.max_runs
|
|
1699
|
+
AND t.kind = 'work' AND t.dispatch_host = ?`
|
|
1700
|
+
).all(workstream, localMachineId(db));
|
|
1701
|
+
const byId = new Map(rows.map((r) => [r.local_id, configOf(r)]));
|
|
1702
|
+
const out = [];
|
|
1703
|
+
for (const t of listReady(db, workstream)) {
|
|
1704
|
+
const auto = byId.get(t.name);
|
|
1705
|
+
if (auto !== void 0) out.push({ ...t, auto });
|
|
1706
|
+
}
|
|
1707
|
+
return out;
|
|
1708
|
+
}
|
|
1709
|
+
function openBlockersClosure(db, workstreamId2, umbrellaId) {
|
|
1710
|
+
const blockersOf = db.prepare(
|
|
1711
|
+
`SELECT b.id AS id, b.local_id AS local_id, b.status AS status
|
|
1712
|
+
FROM task_edges e JOIN tasks b ON b.id = e.from_task_id
|
|
1713
|
+
WHERE e.to_task_id = ? AND b.workstream_id = ?`
|
|
1714
|
+
);
|
|
1715
|
+
const seen = /* @__PURE__ */ new Set([umbrellaId]);
|
|
1716
|
+
const queue = [umbrellaId];
|
|
1717
|
+
const found = [];
|
|
1718
|
+
for (let cur = queue.shift(); cur !== void 0; cur = queue.shift()) {
|
|
1719
|
+
const rows = blockersOf.all(cur, workstreamId2);
|
|
1720
|
+
for (const r of rows) {
|
|
1721
|
+
if (seen.has(r.id) || r.status === "CLOSED") continue;
|
|
1722
|
+
seen.add(r.id);
|
|
1723
|
+
found.push(r.local_id);
|
|
1724
|
+
queue.push(r.id);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
return found.sort();
|
|
1728
|
+
}
|
|
1729
|
+
function authorizeTasks(db, ids, opts) {
|
|
1730
|
+
const ws = opts.workstream;
|
|
1731
|
+
if (RESERVED_WORKSTREAM_NAMES.has(ws)) {
|
|
1732
|
+
throw new TaskAutoArgError(say("task.auto_reserved_ws", ws));
|
|
1733
|
+
}
|
|
1734
|
+
if (!opts.off) {
|
|
1735
|
+
if (!CLI_KEY_RE.test(opts.cli)) {
|
|
1736
|
+
throw new TaskAutoArgError(say("task.auto_cli_key", opts.cli));
|
|
1737
|
+
}
|
|
1738
|
+
if (!Number.isSafeInteger(opts.maxRuns) || opts.maxRuns < 1) {
|
|
1739
|
+
throw new TaskAutoArgError(say("task.auto_max", opts.maxRuns));
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
return withOpContext(
|
|
1743
|
+
db,
|
|
1744
|
+
{ intent: opts.off ? "task.auto-off" : "task.auto", actor: opts.actor, group: "new" },
|
|
1745
|
+
() => {
|
|
1746
|
+
const me = localMachineId(db);
|
|
1747
|
+
const selected = [...ids];
|
|
1748
|
+
if (opts.blockersOf !== void 0) {
|
|
1749
|
+
const umbrella = getTask(db, opts.blockersOf, ws);
|
|
1750
|
+
if (!umbrella) throw new TaskNotFoundError(opts.blockersOf, ws);
|
|
1751
|
+
selected.push(...openBlockersClosure(db, umbrella.workstreamId, tidOf(db, umbrella)));
|
|
1752
|
+
}
|
|
1753
|
+
const names = [...new Set(selected)];
|
|
1754
|
+
if (names.length === 0) {
|
|
1755
|
+
throw new TaskAutoArgError(
|
|
1756
|
+
opts.blockersOf !== void 0 ? say("task.auto_no_blockers", opts.blockersOf) : say("task.auto_none_given")
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
const plan = [];
|
|
1760
|
+
for (const name of names) {
|
|
1761
|
+
const task = getTask(db, name, ws);
|
|
1762
|
+
if (!task) throw new TaskNotFoundError(name, ws);
|
|
1763
|
+
const id = tidOf(db, task);
|
|
1764
|
+
if (!opts.off) refuseUnauthorisable(task, me);
|
|
1765
|
+
plan.push({ task, id });
|
|
1766
|
+
}
|
|
1767
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1768
|
+
let changed = false;
|
|
1769
|
+
for (const { task, id } of plan) {
|
|
1770
|
+
const row = db.prepare("SELECT cli, worktree, max_runs, enabled FROM task_auto WHERE task_id = ?").get(id);
|
|
1771
|
+
if (opts.off) {
|
|
1772
|
+
if (row !== void 0 && row.enabled === 1) {
|
|
1773
|
+
db.prepare("UPDATE task_auto SET enabled = 0 WHERE task_id = ?").run(id);
|
|
1774
|
+
changed = true;
|
|
1775
|
+
}
|
|
1776
|
+
if (task.dispatchHost === me) {
|
|
1777
|
+
setHost(db, id, null, now);
|
|
1778
|
+
changed = true;
|
|
1779
|
+
}
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1782
|
+
const wt = opts.worktree ? 1 : 0;
|
|
1783
|
+
if (row === void 0) {
|
|
1784
|
+
db.prepare(
|
|
1785
|
+
"INSERT INTO task_auto (task_id, cli, worktree, max_runs, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
1786
|
+
).run(id, opts.cli, wt, opts.maxRuns, now);
|
|
1787
|
+
changed = true;
|
|
1788
|
+
} else if (row.cli !== opts.cli || row.worktree !== wt || row.max_runs !== opts.maxRuns || row.enabled !== 1) {
|
|
1789
|
+
db.prepare(
|
|
1790
|
+
"UPDATE task_auto SET cli = ?, worktree = ?, max_runs = ?, enabled = 1 WHERE task_id = ?"
|
|
1791
|
+
).run(opts.cli, wt, opts.maxRuns, id);
|
|
1792
|
+
changed = true;
|
|
1793
|
+
}
|
|
1794
|
+
if (task.dispatchHost !== me) {
|
|
1795
|
+
setHost(db, id, me, now);
|
|
1796
|
+
changed = true;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
return { ids: names, changed };
|
|
1800
|
+
}
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1803
|
+
function tidOf(db, task) {
|
|
1804
|
+
const id = taskIdFor(db, task.name, task.workstreamName);
|
|
1805
|
+
if (id === null) throw new TaskNotFoundError(task.name, task.workstreamName);
|
|
1806
|
+
return id;
|
|
1807
|
+
}
|
|
1808
|
+
function setHost(db, taskId, host, now) {
|
|
1809
|
+
db.prepare("UPDATE tasks SET dispatch_host = ?, updated_at = ? WHERE id = ?").run(
|
|
1810
|
+
host,
|
|
1811
|
+
now,
|
|
1812
|
+
taskId
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
function refuseUnauthorisable(task, me) {
|
|
1816
|
+
const refuse2 = (why) => {
|
|
1817
|
+
throw new TaskAutoRefusedError(
|
|
1818
|
+
`cannot authorise ${task.name}: ${why}`,
|
|
1819
|
+
task.workstreamName,
|
|
1820
|
+
task.name
|
|
1821
|
+
);
|
|
1822
|
+
};
|
|
1823
|
+
if (task.kind !== "work") refuse2(`it is a ${task.kind} task, not a worker job`);
|
|
1824
|
+
if (task.status === "CLOSED") refuse2("it is CLOSED");
|
|
1825
|
+
if (task.substate === "parked") refuse2("it is parked (unpark it first)");
|
|
1826
|
+
if (task.substate === "triage") refuse2("it is in triage (accept it first)");
|
|
1827
|
+
if (task.dispatchHost !== null && task.dispatchHost !== me) {
|
|
1828
|
+
refuse2(`another host (${task.dispatchHost}) is designated to dispatch it`);
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
// src/task-brief.ts
|
|
1833
|
+
var TaskBriefMissingError = class extends Error {
|
|
1834
|
+
constructor(task, workstream, reason) {
|
|
1835
|
+
super(say("task.brief_missing", task, reason));
|
|
1836
|
+
this.task = task;
|
|
1837
|
+
this.workstream = workstream;
|
|
1838
|
+
}
|
|
1839
|
+
task;
|
|
1840
|
+
workstream;
|
|
1841
|
+
name = "TaskBriefMissingError";
|
|
1842
|
+
};
|
|
1843
|
+
function pinnedNotes(db, workstream, task) {
|
|
1844
|
+
const taskId = taskIdFor(db, task, workstream);
|
|
1845
|
+
if (taskId === null) throw new TaskNotFoundError(task, workstream);
|
|
1846
|
+
const kinds = db.prepare("SELECT kind FROM task_notes WHERE task_id = ? ORDER BY id").all(taskId).map((r) => r.kind);
|
|
1847
|
+
return { briefN: kinds.indexOf("brief") + 1, reportN: kinds.lastIndexOf("report") + 1 };
|
|
1848
|
+
}
|
|
1849
|
+
function buildTaskBrief(db, workstream, task, opts = {}) {
|
|
1850
|
+
const { briefN, reportN } = pinnedNotes(db, workstream, task);
|
|
1851
|
+
if (briefN < 1) {
|
|
1852
|
+
throw new TaskBriefMissingError(task, workstream, "it has no brief note (kind brief)");
|
|
1853
|
+
}
|
|
1854
|
+
const brief = getTaskNote(db, task, workstream, briefN).content;
|
|
1855
|
+
const report = reportN < 1 ? null : getTaskNote(db, task, workstream, reportN).content;
|
|
1856
|
+
const parts = [brief.trimEnd()];
|
|
1857
|
+
if (report !== null) parts.push(`Latest report on this task:
|
|
1858
|
+
${report.trimEnd()}`);
|
|
1859
|
+
if (opts.retry === true) {
|
|
1860
|
+
parts.push("The previous attempt died before it finished; continue from its report.");
|
|
1861
|
+
} else if (opts.retry === "error") {
|
|
1862
|
+
parts.push(
|
|
1863
|
+
"Your previous run stopped on an API error before it finished; continue where you stopped."
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
parts.push(
|
|
1867
|
+
`Work only on task ${task} (workstream ${workstream}). Commit, then end your final message with:
|
|
1868
|
+
CLOSE: task=${task} evidence="<command + result>"
|
|
1869
|
+
REPORT:
|
|
1870
|
+
<what you did>`
|
|
1871
|
+
);
|
|
1872
|
+
return { text: parts.join("\n\n") };
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// src/auto-attempt.ts
|
|
1876
|
+
var IN_FLIGHT_STATES = /* @__PURE__ */ new Set([
|
|
1877
|
+
"reserved",
|
|
1878
|
+
"pane",
|
|
1879
|
+
"claimed",
|
|
1880
|
+
"sending",
|
|
1881
|
+
"unknown"
|
|
1882
|
+
]);
|
|
1883
|
+
var UNSENT_STATES = /* @__PURE__ */ new Set(["reserved", "pane", "claimed"]);
|
|
1884
|
+
var refuse = (code, reason) => ({ ok: false, code, reason });
|
|
1885
|
+
function workerGone(db, taskId, receipt) {
|
|
1886
|
+
if (savedIdentity(db, receipt).stale === void 0) return false;
|
|
1887
|
+
const head = `[reaper] previous owner ${receipt.name} gone`;
|
|
1888
|
+
return db.prepare(
|
|
1889
|
+
`SELECT 1 AS x FROM task_notes
|
|
1890
|
+
WHERE task_id = ? AND author = 'reaper' AND created_at >= ? AND instr(content, ?) = 1
|
|
1891
|
+
LIMIT 1`
|
|
1892
|
+
).get(taskId, receipt.createdAt, head) !== void 0;
|
|
1893
|
+
}
|
|
1894
|
+
function parseAttempt(raw) {
|
|
1895
|
+
if (raw === null) return void 0;
|
|
1896
|
+
try {
|
|
1897
|
+
const r = JSON.parse(raw);
|
|
1898
|
+
if (r.v !== 1 || typeof r.key !== "string" || typeof r.attempt !== "number" || typeof r.text !== "string") {
|
|
1899
|
+
return void 0;
|
|
1900
|
+
}
|
|
1901
|
+
return r;
|
|
1902
|
+
} catch {
|
|
1903
|
+
return void 0;
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
function readAttempt(db, taskId) {
|
|
1907
|
+
const auto = getAutoConfig(db, taskId);
|
|
1908
|
+
const record = parseAttempt(auto?.lastSpawn ?? null);
|
|
1909
|
+
const receipt = record === void 0 ? void 0 : getReceipt(db, taskId, record.key);
|
|
1910
|
+
return {
|
|
1911
|
+
...auto !== void 0 ? { auto } : {},
|
|
1912
|
+
...record !== void 0 ? { record } : {},
|
|
1913
|
+
...receipt !== void 0 ? { receipt } : {}
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
function deliveredOf(db, taskId) {
|
|
1917
|
+
const { record, receipt } = readAttempt(db, taskId);
|
|
1918
|
+
if (record === void 0 || receipt === void 0 || record.end !== void 0) return void 0;
|
|
1919
|
+
if (receipt.state === "sent") return { record, receipt };
|
|
1920
|
+
if (record.inPlace !== true || receipt.state !== "failed") return void 0;
|
|
1921
|
+
const prior = db.prepare(
|
|
1922
|
+
`SELECT attempt_key FROM spawn_receipts
|
|
1923
|
+
WHERE task_id = ? AND state = 'sent' AND attempt_key LIKE 'auto:%' AND agent_id = ?
|
|
1924
|
+
ORDER BY created_at DESC, attempt_key DESC LIMIT 1`
|
|
1925
|
+
).get(taskId, receipt.agentId);
|
|
1926
|
+
const sent = prior === void 0 ? void 0 : getReceipt(db, taskId, prior.attempt_key);
|
|
1927
|
+
return sent === void 0 ? void 0 : { record, receipt: sent };
|
|
1928
|
+
}
|
|
1929
|
+
function pendingAttempts(db, workstream) {
|
|
1930
|
+
const rows = db.prepare(
|
|
1931
|
+
`SELECT t.id AS id, t.local_id AS local_id
|
|
1932
|
+
FROM task_auto a
|
|
1933
|
+
JOIN tasks t ON t.id = a.task_id
|
|
1934
|
+
JOIN workstreams w ON w.id = t.workstream_id AND w.name = ?
|
|
1935
|
+
JOIN spawn_receipts r ON r.task_id = a.task_id
|
|
1936
|
+
AND r.attempt_key = json_extract(a.last_spawn, '$.key')
|
|
1937
|
+
WHERE a.enabled = 1 AND t.kind = 'work' AND t.dispatch_host = ?
|
|
1938
|
+
AND r.state IN ('reserved', 'pane', 'claimed', 'sending', 'unknown')
|
|
1939
|
+
AND json_extract(a.last_spawn, '$.end') IS NULL
|
|
1940
|
+
AND json_extract(a.last_spawn, '$.text') IS NOT NULL
|
|
1941
|
+
ORDER BY t.id`
|
|
1942
|
+
).all(workstream, localMachineId(db));
|
|
1943
|
+
return rows.filter((r) => autoPermitted(db, r.id)).map((r) => r.local_id);
|
|
1944
|
+
}
|
|
1945
|
+
function attemptName(task, attempt) {
|
|
1946
|
+
const suffix = `-${attempt}`;
|
|
1947
|
+
return `${task.slice(0, MAX_AGENT_NAME_LEN - suffix.length)}${suffix}`;
|
|
1948
|
+
}
|
|
1949
|
+
var attemptSha = (name, text) => requestSha({ name, nextFree: true, text });
|
|
1950
|
+
function planAttempt(db, workstream, task, opts = {}) {
|
|
1951
|
+
const taskId = taskIdFor(db, task, workstream);
|
|
1952
|
+
if (taskId === null) return refuse("not-found", `no task ${task} in ${workstream}`);
|
|
1953
|
+
if (!autoPermitted(db, taskId)) {
|
|
1954
|
+
return refuse("not-permitted", `${task} is not authorised for this machine (mu task auto)`);
|
|
1955
|
+
}
|
|
1956
|
+
const { auto, record, receipt } = readAttempt(db, taskId);
|
|
1957
|
+
if (auto === void 0) return refuse("not-permitted", `${task} has no auto config`);
|
|
1958
|
+
let retry = false;
|
|
1959
|
+
if (record?.end?.state === "needs_human") {
|
|
1960
|
+
return refuse(
|
|
1961
|
+
"parked",
|
|
1962
|
+
`${record.key} of ${task} may have been delivered and waits for a human (${record.end.reason})`
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
if (record !== void 0 && receipt !== void 0 && record.end === void 0) {
|
|
1966
|
+
if (IN_FLIGHT_STATES.has(receipt.state)) {
|
|
1967
|
+
return refuse("in-flight", `${record.key} of ${task} is ${receipt.state}: resume it`);
|
|
1968
|
+
}
|
|
1969
|
+
const delivered = deliveredOf(db, taskId);
|
|
1970
|
+
if (delivered !== void 0) {
|
|
1971
|
+
if (!workerGone(db, taskId, delivered.receipt)) {
|
|
1972
|
+
return refuse(
|
|
1973
|
+
"delivered",
|
|
1974
|
+
`${record.key} of ${task} was delivered and its worker is not known to be gone: not re-dispatched`
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
retry = true;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
if (auto.runs >= auto.maxRuns) {
|
|
1981
|
+
return refuse("ceiling", `${task} used its ${auto.maxRuns} run(s)`);
|
|
1982
|
+
}
|
|
1983
|
+
if (!listReady(db, workstream).some((t) => t.name === task)) {
|
|
1984
|
+
return refuse("not-ready", `${task} is not ready`);
|
|
1985
|
+
}
|
|
1986
|
+
const revision = taskRevision(db, workstream, task);
|
|
1987
|
+
if (opts.revision !== void 0 && opts.revision !== revision) {
|
|
1988
|
+
return refuse(
|
|
1989
|
+
"changed",
|
|
1990
|
+
`${task} changed since it was planned (rev ${opts.revision} \u2192 ${revision})`
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
const attempt = auto.runs + 1;
|
|
1994
|
+
const name = attemptName(task, attempt);
|
|
1995
|
+
let brief;
|
|
1996
|
+
try {
|
|
1997
|
+
brief = buildTaskBrief(db, workstream, task, { retry });
|
|
1998
|
+
} catch (err) {
|
|
1999
|
+
if (err instanceof TaskBriefMissingError) return refuse("no-brief", err.message);
|
|
2000
|
+
throw err;
|
|
2001
|
+
}
|
|
2002
|
+
return {
|
|
2003
|
+
ok: true,
|
|
2004
|
+
plan: {
|
|
2005
|
+
v: 1,
|
|
2006
|
+
attempt,
|
|
2007
|
+
key: `auto:${attempt}`,
|
|
2008
|
+
name,
|
|
2009
|
+
cli: auto.cli,
|
|
2010
|
+
worktree: auto.worktree,
|
|
2011
|
+
text: brief.text,
|
|
2012
|
+
retry,
|
|
2013
|
+
task,
|
|
2014
|
+
taskId,
|
|
2015
|
+
workstream,
|
|
2016
|
+
revision
|
|
2017
|
+
}
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
function reserveAttempt(db, workstream, task, opts = {}) {
|
|
2021
|
+
return db.transaction(() => {
|
|
2022
|
+
const planned = planAttempt(db, workstream, task, opts);
|
|
2023
|
+
if (!planned.ok) return planned;
|
|
2024
|
+
const { plan } = planned;
|
|
2025
|
+
const { task: _t, taskId, workstream: _w, revision: _r, ...record } = plan;
|
|
2026
|
+
const charged = db.prepare(
|
|
2027
|
+
"UPDATE task_auto SET runs = runs + 1, last_spawn = ? WHERE task_id = ? AND runs = ? AND enabled = 1"
|
|
2028
|
+
).run(JSON.stringify(record), taskId, plan.attempt - 1);
|
|
2029
|
+
if (charged.changes !== 1) return refuse("changed", `${task}'s run counter moved`);
|
|
2030
|
+
reserveReceipt(db, {
|
|
2031
|
+
taskId,
|
|
2032
|
+
attemptKey: plan.key,
|
|
2033
|
+
name: plan.name,
|
|
2034
|
+
workstream,
|
|
2035
|
+
requestSha: attemptSha(plan.name, plan.text),
|
|
2036
|
+
msgId: newMsgId()
|
|
2037
|
+
});
|
|
2038
|
+
return planned;
|
|
2039
|
+
}).immediate();
|
|
2040
|
+
}
|
|
2041
|
+
function endAttempt(db, taskId, key, end) {
|
|
2042
|
+
return db.transaction(() => {
|
|
2043
|
+
const { record, receipt } = readAttempt(db, taskId);
|
|
2044
|
+
if (record === void 0 || record.key !== key || record.end !== void 0) return false;
|
|
2045
|
+
const changed = db.prepare("UPDATE task_auto SET last_spawn = ? WHERE task_id = ?").run(JSON.stringify({ ...record, end }), taskId).changes;
|
|
2046
|
+
if (changed !== 1) return false;
|
|
2047
|
+
if (receipt !== void 0 && UNSENT_STATES.has(receipt.state)) {
|
|
2048
|
+
updateReceipt(db, taskId, key, { state: "failed", detail: `${end.state}: ${end.reason}` });
|
|
2049
|
+
}
|
|
2050
|
+
return true;
|
|
2051
|
+
}).immediate();
|
|
2052
|
+
}
|
|
2053
|
+
function reserveRetry(db, workstream, task, target) {
|
|
2054
|
+
return db.transaction(() => {
|
|
2055
|
+
const taskId = taskIdFor(db, task, workstream);
|
|
2056
|
+
if (taskId === null) return refuse("not-found", `no task ${task} in ${workstream}`);
|
|
2057
|
+
if (!autoPermitted(db, taskId)) {
|
|
2058
|
+
return refuse("not-permitted", `${task} is not authorised for this machine (mu task auto)`);
|
|
2059
|
+
}
|
|
2060
|
+
const { auto } = readAttempt(db, taskId);
|
|
2061
|
+
const delivered = deliveredOf(db, taskId);
|
|
2062
|
+
if (auto === void 0 || delivered === void 0) {
|
|
2063
|
+
return refuse("changed", `${task} has no delivered attempt`);
|
|
2064
|
+
}
|
|
2065
|
+
const { receipt } = delivered;
|
|
2066
|
+
const { agent } = target;
|
|
2067
|
+
if (receipt.agentId !== agent.incarnation || receipt.paneId !== agent.paneId || receipt.name !== agent.name || savedIdentity(db, receipt).stale !== void 0) {
|
|
2068
|
+
return refuse("changed", `${task}: the worker is not the one its attempt delivered to`);
|
|
2069
|
+
}
|
|
2070
|
+
if (receipt.runs === null || target.errorRuns <= receipt.runs) {
|
|
2071
|
+
return refuse("changed", `${task}: that error was already answered`);
|
|
2072
|
+
}
|
|
2073
|
+
const row = db.prepare("SELECT status, owner_id FROM tasks WHERE id = ?").get(taskId);
|
|
2074
|
+
if (row.status !== "IN_PROGRESS" || row.owner_id !== agent.incarnation) {
|
|
2075
|
+
return refuse("changed", `${task} is no longer in progress with ${agent.name}`);
|
|
2076
|
+
}
|
|
2077
|
+
if (auto.runs >= auto.maxRuns) {
|
|
2078
|
+
return refuse("ceiling", `${task} used its ${auto.maxRuns} run(s)`);
|
|
2079
|
+
}
|
|
2080
|
+
let brief;
|
|
2081
|
+
try {
|
|
2082
|
+
brief = buildTaskBrief(db, workstream, task, { retry: "error" });
|
|
2083
|
+
} catch (err) {
|
|
2084
|
+
if (err instanceof TaskBriefMissingError) return refuse("no-brief", err.message);
|
|
2085
|
+
throw err;
|
|
2086
|
+
}
|
|
2087
|
+
const attempt = auto.runs + 1;
|
|
2088
|
+
const plan = {
|
|
2089
|
+
v: 1,
|
|
2090
|
+
attempt,
|
|
2091
|
+
key: `auto:${attempt}`,
|
|
2092
|
+
name: agent.name,
|
|
2093
|
+
cli: auto.cli,
|
|
2094
|
+
worktree: auto.worktree,
|
|
2095
|
+
text: brief.text,
|
|
2096
|
+
retry: true,
|
|
2097
|
+
inPlace: true,
|
|
2098
|
+
task,
|
|
2099
|
+
taskId,
|
|
2100
|
+
workstream,
|
|
2101
|
+
revision: taskRevision(db, workstream, task)
|
|
2102
|
+
};
|
|
2103
|
+
const { task: _t, taskId: _i, workstream: _w, revision: _r, ...stored } = plan;
|
|
2104
|
+
const charged = db.prepare(
|
|
2105
|
+
"UPDATE task_auto SET runs = runs + 1, last_spawn = ? WHERE task_id = ? AND runs = ? AND enabled = 1"
|
|
2106
|
+
).run(JSON.stringify(stored), taskId, auto.runs);
|
|
2107
|
+
if (charged.changes !== 1) return refuse("changed", `${task}'s run counter moved`);
|
|
2108
|
+
reserveReceipt(db, {
|
|
2109
|
+
taskId,
|
|
2110
|
+
attemptKey: plan.key,
|
|
2111
|
+
name: agent.name,
|
|
2112
|
+
workstream,
|
|
2113
|
+
requestSha: attemptSha(agent.name, plan.text),
|
|
2114
|
+
msgId: newMsgId()
|
|
2115
|
+
});
|
|
2116
|
+
updateReceipt(db, taskId, plan.key, {
|
|
2117
|
+
state: "claimed",
|
|
2118
|
+
agentId: agent.incarnation,
|
|
2119
|
+
paneId: agent.paneId,
|
|
2120
|
+
incarnation: target.pi
|
|
2121
|
+
});
|
|
2122
|
+
return { ok: true, plan };
|
|
2123
|
+
}).immediate();
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
// src/dag.ts
|
|
2127
|
+
var FRONTIER_MAX_DEPTH = 12;
|
|
2128
|
+
var FRONTIER_MAX_NODES = 2e3;
|
|
2129
|
+
var FRONTIER_CLOSED_PER_NODE = 3;
|
|
2130
|
+
function loadOpenTaskFrontier(db, workstream, root, direction = "dependents", opts = {}) {
|
|
2131
|
+
const out = { direction, entries: [], rootsCut: 0, againCut: 0 };
|
|
2132
|
+
const uncapped = opts.uncapped === true;
|
|
2133
|
+
const maxDepth = uncapped ? Number.POSITIVE_INFINITY : Math.min(opts.maxDepth ?? FRONTIER_MAX_DEPTH, FRONTIER_MAX_DEPTH);
|
|
2134
|
+
const maxNodes = uncapped ? Number.POSITIVE_INFINITY : FRONTIER_MAX_NODES;
|
|
2135
|
+
const closedPerNode = uncapped ? Number.POSITIVE_INFINITY : FRONTIER_CLOSED_PER_NODE;
|
|
2136
|
+
const wsRow = db.prepare("SELECT id FROM workstreams WHERE name = ? AND torn_down_at IS NULL").get(workstream);
|
|
2137
|
+
if (wsRow === void 0) return out;
|
|
2138
|
+
const wsId = wsRow.id;
|
|
2139
|
+
const [from, to] = direction === "dependents" ? ["from_task_id", "to_task_id"] : ["to_task_id", "from_task_id"];
|
|
2140
|
+
const includeClosed = opts.includeClosed === true || uncapped;
|
|
2141
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
2142
|
+
const rootIds = /* @__PURE__ */ new Set();
|
|
2143
|
+
let level = [];
|
|
2144
|
+
const startRows = [];
|
|
2145
|
+
if (root !== void 0) {
|
|
2146
|
+
const id = taskIdFor(db, root, workstream);
|
|
2147
|
+
if (id === null) return out;
|
|
2148
|
+
startRows.push(
|
|
2149
|
+
db.prepare(`SELECT ${SELECT_TASK_COLS} ${TASK_FROM_JOIN} WHERE t.id = ?`).get(id)
|
|
2150
|
+
);
|
|
2151
|
+
} else {
|
|
2152
|
+
startRows.push(
|
|
2153
|
+
...db.prepare(
|
|
2154
|
+
`SELECT ${SELECT_TASK_COLS} ${TASK_FROM_JOIN}
|
|
2155
|
+
WHERE t.workstream_id = ? AND t.status <> 'CLOSED'
|
|
2156
|
+
AND NOT EXISTS (SELECT 1 FROM task_edges e JOIN tasks p ON p.id = e.${from}
|
|
2157
|
+
WHERE e.${to} = t.id AND p.status <> 'CLOSED'
|
|
2158
|
+
AND p.workstream_id = t.workstream_id)
|
|
2159
|
+
ORDER BY t.local_id`
|
|
2160
|
+
).all(wsId)
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
for (const raw of startRows.slice(0, maxNodes)) {
|
|
2164
|
+
nodes.set(raw.id, rowFromDb(raw));
|
|
2165
|
+
rootIds.add(raw.id);
|
|
2166
|
+
level.push(raw.id);
|
|
2167
|
+
}
|
|
2168
|
+
const startCut = Math.max(0, startRows.length - maxNodes);
|
|
2169
|
+
const children = /* @__PURE__ */ new Map();
|
|
2170
|
+
const dropped = /* @__PURE__ */ new Map();
|
|
2171
|
+
const capDropped = /* @__PURE__ */ new Map();
|
|
2172
|
+
const closedHidden = /* @__PURE__ */ new Map();
|
|
2173
|
+
const expands = (id) => includeClosed || rootIds.has(id) || nodes.get(id)?.status !== "CLOSED";
|
|
2174
|
+
for (let depth = 0; level.length > 0; depth++) {
|
|
2175
|
+
const ids = level.filter(expands);
|
|
2176
|
+
if (ids.length === 0) break;
|
|
2177
|
+
if (depth >= maxDepth) {
|
|
2178
|
+
const counts = [];
|
|
2179
|
+
for (let i = 0; i < ids.length; i += 500) {
|
|
2180
|
+
const chunk = ids.slice(i, i + 500);
|
|
2181
|
+
counts.push(
|
|
2182
|
+
...db.prepare(
|
|
2183
|
+
`SELECT e.${from} AS src,
|
|
2184
|
+
SUM(CASE WHEN t.status = 'CLOSED' AND ? = 0 THEN 0 ELSE 1 END) AS open,
|
|
2185
|
+
SUM(CASE WHEN t.status = 'CLOSED' AND ? = 0 THEN 1 ELSE 0 END) AS closed
|
|
2186
|
+
FROM task_edges e JOIN tasks t ON t.id = e.${to}
|
|
2187
|
+
WHERE e.${from} IN (${chunk.map(() => "?").join(", ")}) AND t.workstream_id = ? GROUP BY e.${from}`
|
|
2188
|
+
).all(includeClosed ? 1 : 0, includeClosed ? 1 : 0, ...chunk, wsId)
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
for (const c of counts) {
|
|
2192
|
+
if (c.open > 0) dropped.set(c.src, (dropped.get(c.src) ?? 0) + c.open);
|
|
2193
|
+
if (c.closed > 0) closedHidden.set(c.src, (closedHidden.get(c.src) ?? 0) + c.closed);
|
|
2194
|
+
}
|
|
2195
|
+
break;
|
|
2196
|
+
}
|
|
2197
|
+
const rows = [];
|
|
2198
|
+
for (let i = 0; i < ids.length; i += 500) {
|
|
2199
|
+
const chunk = ids.slice(i, i + 500);
|
|
2200
|
+
rows.push(
|
|
2201
|
+
...db.prepare(
|
|
2202
|
+
`SELECT e.${from} AS src, ${SELECT_TASK_COLS}
|
|
2203
|
+
FROM task_edges e
|
|
2204
|
+
JOIN tasks t ON t.id = e.${to}
|
|
2205
|
+
JOIN workstreams ws ON ws.id = t.workstream_id AND ws.torn_down_at IS NULL
|
|
2206
|
+
LEFT JOIN agents ag ON ag.id = t.owner_id
|
|
2207
|
+
WHERE e.${from} IN (${chunk.map(() => "?").join(", ")}) AND t.workstream_id = ?
|
|
2208
|
+
ORDER BY e.${from}, t.local_id`
|
|
2209
|
+
).all(...chunk, wsId)
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
const next = [];
|
|
2213
|
+
const closedSeen = /* @__PURE__ */ new Map();
|
|
2214
|
+
let capped = false;
|
|
2215
|
+
for (const row of rows) {
|
|
2216
|
+
const leaf = row.status === "CLOSED" && !includeClosed;
|
|
2217
|
+
if (!capped) {
|
|
2218
|
+
if (leaf) {
|
|
2219
|
+
const n = closedSeen.get(row.src) ?? 0;
|
|
2220
|
+
closedSeen.set(row.src, n + 1);
|
|
2221
|
+
if (n >= closedPerNode) {
|
|
2222
|
+
closedHidden.set(row.src, (closedHidden.get(row.src) ?? 0) + 1);
|
|
2223
|
+
continue;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
if (!nodes.has(row.id) && nodes.size >= maxNodes) capped = true;
|
|
2227
|
+
}
|
|
2228
|
+
if (capped) {
|
|
2229
|
+
capDropped.set(row.src, (capDropped.get(row.src) ?? 0) + 1);
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
if (!nodes.has(row.id)) {
|
|
2233
|
+
nodes.set(row.id, rowFromDb(row));
|
|
2234
|
+
next.push(row.id);
|
|
2235
|
+
}
|
|
2236
|
+
const kids = children.get(row.src) ?? [];
|
|
2237
|
+
kids.push(row.id);
|
|
2238
|
+
children.set(row.src, kids);
|
|
2239
|
+
}
|
|
2240
|
+
level = next;
|
|
2241
|
+
}
|
|
2242
|
+
const listed = /* @__PURE__ */ new Set();
|
|
2243
|
+
let recurrences = 0;
|
|
2244
|
+
const walk = (id, parent, depth) => {
|
|
2245
|
+
const task = nodes.get(id);
|
|
2246
|
+
if (task === void 0) return;
|
|
2247
|
+
const entry = {
|
|
2248
|
+
task,
|
|
2249
|
+
depth,
|
|
2250
|
+
parent,
|
|
2251
|
+
again: false,
|
|
2252
|
+
kids: dropped.get(id) ?? 0,
|
|
2253
|
+
closed: closedHidden.get(id) ?? 0,
|
|
2254
|
+
capCut: capDropped.get(id) ?? 0
|
|
2255
|
+
};
|
|
2256
|
+
out.entries.push(entry);
|
|
2257
|
+
listed.add(id);
|
|
2258
|
+
const kids = children.get(id) ?? [];
|
|
2259
|
+
if (depth >= maxDepth) {
|
|
2260
|
+
for (const child of kids) {
|
|
2261
|
+
if (nodes.get(child)?.status === "CLOSED" && !includeClosed) entry.closed++;
|
|
2262
|
+
else entry.kids++;
|
|
2263
|
+
}
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
for (const child of kids) {
|
|
2267
|
+
const c = nodes.get(child);
|
|
2268
|
+
if (c === void 0) continue;
|
|
2269
|
+
if (listed.has(child)) {
|
|
2270
|
+
if (recurrences >= maxNodes) {
|
|
2271
|
+
out.againCut++;
|
|
2272
|
+
entry.kids++;
|
|
2273
|
+
} else {
|
|
2274
|
+
recurrences++;
|
|
2275
|
+
out.entries.push({
|
|
2276
|
+
task: c,
|
|
2277
|
+
depth: depth + 1,
|
|
2278
|
+
parent: task.name,
|
|
2279
|
+
again: true,
|
|
2280
|
+
kids: 0,
|
|
2281
|
+
closed: 0,
|
|
2282
|
+
capCut: 0
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
continue;
|
|
2286
|
+
}
|
|
2287
|
+
walk(child, task.name, depth + 1);
|
|
2288
|
+
}
|
|
2289
|
+
};
|
|
2290
|
+
for (const id of [...rootIds].sort(
|
|
2291
|
+
(a, b) => (nodes.get(a)?.name ?? "").localeCompare(nodes.get(b)?.name ?? "")
|
|
2292
|
+
)) {
|
|
2293
|
+
if (listed.has(id)) continue;
|
|
2294
|
+
walk(id, null, 0);
|
|
2295
|
+
}
|
|
2296
|
+
out.rootsCut = startCut;
|
|
2297
|
+
return out;
|
|
2298
|
+
}
|
|
2299
|
+
function renderFrontierLines(entries, label) {
|
|
2300
|
+
const lines = [];
|
|
2301
|
+
const open = [];
|
|
2302
|
+
entries.forEach((e, i) => {
|
|
2303
|
+
if (e.depth === 0) {
|
|
2304
|
+
lines.push(label(e));
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
let last = true;
|
|
2308
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
2309
|
+
const d = entries[j]?.depth ?? 0;
|
|
2310
|
+
if (d < e.depth) break;
|
|
2311
|
+
if (d === e.depth) {
|
|
2312
|
+
last = false;
|
|
2313
|
+
break;
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
open[e.depth] = !last;
|
|
2317
|
+
let prefix = "";
|
|
2318
|
+
for (let d = 1; d < e.depth; d++) prefix += open[d] ? "\u2502 " : " ";
|
|
2319
|
+
lines.push(`${prefix}${last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${label(e)}`);
|
|
2320
|
+
});
|
|
2321
|
+
return lines;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
// src/log-render.ts
|
|
2325
|
+
function fields(payload) {
|
|
2326
|
+
if (!payload.startsWith("{")) return {};
|
|
2327
|
+
try {
|
|
2328
|
+
const parsed = JSON.parse(payload);
|
|
2329
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
2330
|
+
return parsed;
|
|
2331
|
+
} catch {
|
|
2332
|
+
return {};
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
function str2(bag, key) {
|
|
2336
|
+
const v = bag[key];
|
|
2337
|
+
return typeof v === "string" ? v : void 0;
|
|
2338
|
+
}
|
|
2339
|
+
function num(bag, key) {
|
|
2340
|
+
const v = bag[key];
|
|
2341
|
+
return typeof v === "number" ? v : void 0;
|
|
2342
|
+
}
|
|
2343
|
+
var NOISE_FIELDS = /* @__PURE__ */ new Set(["updated_at", "created_at"]);
|
|
2344
|
+
function changedFields(bag) {
|
|
2345
|
+
return Object.keys(bag).filter((k) => !NOISE_FIELDS.has(k));
|
|
2346
|
+
}
|
|
2347
|
+
function parseOpKey(key) {
|
|
2348
|
+
if (key === null || key === "") return {};
|
|
2349
|
+
const arrow = key.indexOf("->");
|
|
2350
|
+
if (arrow !== -1) {
|
|
2351
|
+
const from = parseOpKey(key.slice(0, arrow));
|
|
2352
|
+
const to = parseOpKey(key.slice(arrow + 2));
|
|
2353
|
+
const out = {};
|
|
2354
|
+
if (from.workstream !== void 0) out.workstream = from.workstream;
|
|
2355
|
+
if (from.local !== void 0) out.local = from.local;
|
|
2356
|
+
if (to.local !== void 0) out.to = to.local;
|
|
2357
|
+
return out;
|
|
2358
|
+
}
|
|
2359
|
+
const slash = key.indexOf("/");
|
|
2360
|
+
if (slash === -1) return { workstream: key };
|
|
2361
|
+
const workstream = key.slice(0, slash);
|
|
2362
|
+
const rest = key.slice(slash + 1);
|
|
2363
|
+
const hash = rest.indexOf("#");
|
|
2364
|
+
if (hash === -1) return { workstream, local: rest };
|
|
2365
|
+
return { workstream, local: rest.slice(0, hash), note: rest.slice(hash + 1) };
|
|
2366
|
+
}
|
|
2367
|
+
var VERBS = {
|
|
2368
|
+
"task.add": "task add",
|
|
2369
|
+
"task.update": "task update",
|
|
2370
|
+
"task.note": "task note",
|
|
2371
|
+
"task.delete": "task delete",
|
|
2372
|
+
"task.close": "task close",
|
|
2373
|
+
"close.apply": "close apply",
|
|
2374
|
+
"task.open": "task open",
|
|
2375
|
+
"task.park": "task park",
|
|
2376
|
+
"task.unpark": "task unpark",
|
|
2377
|
+
"task.accept": "task accept",
|
|
2378
|
+
"task.reject": "task reject",
|
|
2379
|
+
"task.defer": "task defer",
|
|
2380
|
+
"task.claim": "task claim",
|
|
2381
|
+
"task.release": "task release",
|
|
2382
|
+
"task.reap": "task reap",
|
|
2383
|
+
"task.block": "task block",
|
|
2384
|
+
"task.unblock": "task unblock",
|
|
2385
|
+
"task.reparent": "task reparent",
|
|
2386
|
+
"workstream.init": "workstream init",
|
|
2387
|
+
"workstream.teardown": "workstream teardown",
|
|
2388
|
+
"sql.write": "sql write",
|
|
2389
|
+
"agent.spawn": "agent spawn",
|
|
2390
|
+
"agent.spawn_failed": "agent spawn failed",
|
|
2391
|
+
"agent.close": "agent close",
|
|
2392
|
+
"agent.adopt": "agent adopt",
|
|
2393
|
+
"agent.kick": "agent kick",
|
|
2394
|
+
"agent.send": "agent send",
|
|
2395
|
+
"agent.abort": "agent abort",
|
|
2396
|
+
// Payload prose reads "agent stalled ..."; keep the verb matching it
|
|
2397
|
+
// so the emitted text is not printed with a second, different verb.
|
|
2398
|
+
"agent.stall": "agent stalled"
|
|
2399
|
+
};
|
|
2400
|
+
var KNOWN_INTENTS = Object.keys(VERBS);
|
|
2401
|
+
var KNOWN_VERBS = Object.values(VERBS);
|
|
2402
|
+
function isKnownIntent(intent) {
|
|
2403
|
+
return Object.hasOwn(VERBS, intent);
|
|
2404
|
+
}
|
|
2405
|
+
function renderOp(row) {
|
|
2406
|
+
const intent = row.intent;
|
|
2407
|
+
if (intent === null) return null;
|
|
2408
|
+
const setPrefix = "task.set-";
|
|
2409
|
+
if (intent.startsWith(setPrefix)) {
|
|
2410
|
+
const status = intent.slice(setPrefix.length).toUpperCase();
|
|
2411
|
+
const { local } = parseOpKey(row.workstreamName);
|
|
2412
|
+
return { verb: "task status", subject: local ?? "", detail: `\u2192 ${status}` };
|
|
2413
|
+
}
|
|
2414
|
+
if (!isKnownIntent(intent)) {
|
|
2415
|
+
const parts = [`intent=${intent}`, `kind=${row.kind}`];
|
|
2416
|
+
if (row.payload.startsWith("{")) {
|
|
2417
|
+
const patch = changedFields(fields(row.payload));
|
|
2418
|
+
if (patch.length > 0) parts.push(`patch=${patch.join(",")}`);
|
|
2419
|
+
} else if (row.payload !== "") {
|
|
2420
|
+
parts.push(`: ${oneLine(row.payload)}`);
|
|
2421
|
+
}
|
|
2422
|
+
return {
|
|
2423
|
+
verb: row.op === void 0 ? "op" : `op ${row.op}`,
|
|
2424
|
+
subject: row.workstreamName ?? "",
|
|
2425
|
+
detail: parts.join(" ")
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
if (row.kind === "note" && row.op !== "del") return renderKnown(row, "task.note");
|
|
2429
|
+
return renderKnown(row, intent);
|
|
2430
|
+
}
|
|
2431
|
+
function renderKnown(row, intent) {
|
|
2432
|
+
const bag = fields(row.payload);
|
|
2433
|
+
const key = parseOpKey(row.workstreamName);
|
|
2434
|
+
const subject = key.local ?? key.workstream ?? "";
|
|
2435
|
+
const deleted = row.op === "del";
|
|
2436
|
+
switch (intent) {
|
|
2437
|
+
case "task.add": {
|
|
2438
|
+
const impact = num(bag, "impact");
|
|
2439
|
+
const effort = num(bag, "effort_days");
|
|
2440
|
+
const title = str2(bag, "title");
|
|
2441
|
+
const bits = [];
|
|
2442
|
+
if (title !== void 0) bits.push(`"${title}"`);
|
|
2443
|
+
if (impact !== void 0) bits.push(`impact=${impact}`);
|
|
2444
|
+
if (effort !== void 0) bits.push(`effort=${effort}`);
|
|
2445
|
+
return { verb: VERBS[intent], subject, detail: bits.join(" ") };
|
|
2446
|
+
}
|
|
2447
|
+
case "task.update": {
|
|
2448
|
+
const changed = changedFields(bag);
|
|
2449
|
+
const detail = changed.map((f) => {
|
|
2450
|
+
const v = bag[f];
|
|
2451
|
+
return typeof v === "object" ? f : `${f}=${String(v)}`;
|
|
2452
|
+
}).join(" ");
|
|
2453
|
+
return { verb: VERBS[intent], subject, detail: detail === "" ? "(touched)" : detail };
|
|
2454
|
+
}
|
|
2455
|
+
case "task.note": {
|
|
2456
|
+
const author = str2(bag, "author");
|
|
2457
|
+
const content = str2(bag, "content") ?? "";
|
|
2458
|
+
const n = key.note;
|
|
2459
|
+
const head = n === void 0 ? "" : `#${n} `;
|
|
2460
|
+
const by = author === void 0 || author === "" ? "" : ` by ${author}`;
|
|
2461
|
+
return { verb: VERBS[intent], subject, detail: `${head}${oneLine(content)}${by}` };
|
|
2462
|
+
}
|
|
2463
|
+
case "task.delete":
|
|
2464
|
+
return { verb: VERBS[intent], subject, detail: "" };
|
|
2465
|
+
case "task.close":
|
|
2466
|
+
case "close.apply":
|
|
2467
|
+
case "task.open":
|
|
2468
|
+
case "task.reject":
|
|
2469
|
+
case "task.defer": {
|
|
2470
|
+
const status = str2(bag, "status");
|
|
2471
|
+
return {
|
|
2472
|
+
verb: VERBS[intent],
|
|
2473
|
+
subject,
|
|
2474
|
+
detail: status === void 0 ? "" : `\u2192 ${pairText(bag, status)}`
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
case "task.park":
|
|
2478
|
+
case "task.unpark":
|
|
2479
|
+
case "task.accept": {
|
|
2480
|
+
const detail = str2(bag, "substate") === void 0 ? "" : `\u2192 ${pairText(bag, "OPEN")}`;
|
|
2481
|
+
return { verb: VERBS[intent], subject, detail };
|
|
2482
|
+
}
|
|
2483
|
+
case "task.claim": {
|
|
2484
|
+
const by = row.source === "" ? "" : `by ${row.source}`;
|
|
2485
|
+
const status = str2(bag, "status");
|
|
2486
|
+
const tail = status === void 0 ? "" : ` \u2192 ${status}`;
|
|
2487
|
+
return { verb: VERBS[intent], subject, detail: `${by}${tail}` };
|
|
2488
|
+
}
|
|
2489
|
+
case "task.release":
|
|
2490
|
+
case "task.reap": {
|
|
2491
|
+
const status = str2(bag, "status");
|
|
2492
|
+
const bits = [];
|
|
2493
|
+
if (Object.hasOwn(bag, "owner_id") && bag.owner_id === null) bits.push("owner cleared");
|
|
2494
|
+
if (status !== void 0) bits.push(`\u2192 ${status}`);
|
|
2495
|
+
return { verb: VERBS[intent], subject, detail: bits.join(" ") };
|
|
2496
|
+
}
|
|
2497
|
+
case "task.block":
|
|
2498
|
+
case "task.unblock":
|
|
2499
|
+
case "task.reparent": {
|
|
2500
|
+
if (key.to !== void 0) {
|
|
2501
|
+
const dir = intent === "task.unblock" ? "no longer blocks" : "blocks";
|
|
2502
|
+
return { verb: VERBS[intent], subject, detail: `${dir} ${key.to}` };
|
|
2503
|
+
}
|
|
2504
|
+
return { verb: VERBS[intent], subject, detail: deleted ? "(edge removed)" : "(touched)" };
|
|
2505
|
+
}
|
|
2506
|
+
case "workstream.init":
|
|
2507
|
+
return { verb: VERBS[intent], subject, detail: "" };
|
|
2508
|
+
case "workstream.teardown":
|
|
2509
|
+
return {
|
|
2510
|
+
verb: VERBS[intent],
|
|
2511
|
+
subject,
|
|
2512
|
+
detail: key.local === void 0 ? "" : "(cascaded)"
|
|
2513
|
+
};
|
|
2514
|
+
case "sql.write": {
|
|
2515
|
+
if (deleted) return { verb: VERBS[intent], subject, detail: `${row.kind} deleted` };
|
|
2516
|
+
const edge = key.to === void 0 ? "" : ` -> ${key.to}`;
|
|
2517
|
+
const changed = changedFields(bag).map((f) => {
|
|
2518
|
+
const v = bag[f];
|
|
2519
|
+
return typeof v === "object" && v !== null ? f : `${f}=${oneLine(String(v))}`;
|
|
2520
|
+
}).join(" ");
|
|
2521
|
+
return {
|
|
2522
|
+
verb: VERBS[intent],
|
|
2523
|
+
subject,
|
|
2524
|
+
detail: `${row.kind}${edge} ${changed === "" ? "(touched)" : changed}`
|
|
2525
|
+
};
|
|
2526
|
+
}
|
|
2527
|
+
// Local intents: no trigger can see these, so `emitEvent` wrote the
|
|
2528
|
+
// payload as prose. It is already human-readable — show it as the
|
|
2529
|
+
// detail rather than inventing a second phrasing. The leading verb
|
|
2530
|
+
// is stripped so it is not printed twice.
|
|
2531
|
+
case "agent.spawn":
|
|
2532
|
+
case "agent.spawn_failed":
|
|
2533
|
+
case "agent.close":
|
|
2534
|
+
case "agent.adopt":
|
|
2535
|
+
case "agent.kick":
|
|
2536
|
+
case "agent.send":
|
|
2537
|
+
case "agent.abort":
|
|
2538
|
+
case "agent.stall": {
|
|
2539
|
+
const verb = VERBS[intent];
|
|
2540
|
+
if (row.payload.startsWith("{")) {
|
|
2541
|
+
const named = str2(bag, "name") ?? subject;
|
|
2542
|
+
return { verb, subject: named, detail: changedFields(bag).join(" ") };
|
|
2543
|
+
}
|
|
2544
|
+
const prose = row.payload.startsWith(verb) ? row.payload.slice(verb.length).trim() : row.payload;
|
|
2545
|
+
const firstSpace = prose.indexOf(" ");
|
|
2546
|
+
const name = firstSpace === -1 ? prose : prose.slice(0, firstSpace);
|
|
2547
|
+
const rest = firstSpace === -1 ? "" : prose.slice(firstSpace + 1).trim();
|
|
2548
|
+
return { verb, subject: name, detail: rest };
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
function pairText(bag, status) {
|
|
2553
|
+
const pair = resolvePair(status, str2(bag, "substate"));
|
|
2554
|
+
return pair === null ? status : formatPair(pair);
|
|
2555
|
+
}
|
|
2556
|
+
function oneLine(text) {
|
|
2557
|
+
return text.replace(/\s+/g, " ").trim();
|
|
2558
|
+
}
|
|
2559
|
+
function renderOpLine(row) {
|
|
2560
|
+
const r = renderOp(row);
|
|
2561
|
+
if (r === null) return row.payload;
|
|
2562
|
+
return [r.verb, r.subject, r.detail].filter((s) => s !== "").join(" ");
|
|
2563
|
+
}
|
|
2564
|
+
function opSubject(row) {
|
|
2565
|
+
const intent = row.intent;
|
|
2566
|
+
if (intent === null) return null;
|
|
2567
|
+
const { local } = parseOpKey(row.workstreamName);
|
|
2568
|
+
if (intent.startsWith("task.") && local !== void 0 && local !== "") {
|
|
2569
|
+
return { kind: "task", id: local };
|
|
2570
|
+
}
|
|
2571
|
+
if (intent.startsWith("agent.") && isKnownIntent(intent)) {
|
|
2572
|
+
const r = renderOp(row);
|
|
2573
|
+
if (r !== null && r.subject !== "") return { kind: "agent", id: r.subject };
|
|
2574
|
+
}
|
|
2575
|
+
return null;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
// src/output-contract.ts
|
|
2579
|
+
var SGR = /\u001b\[[0-9;]*m/g;
|
|
2580
|
+
var stripAnsi = (s) => s.replace(SGR, "");
|
|
2581
|
+
function safeJsonLine(json) {
|
|
2582
|
+
return json.replace(
|
|
2583
|
+
/[\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/gu,
|
|
2584
|
+
(c) => `\\u${(c.codePointAt(0) ?? 0).toString(16).padStart(4, "0")}`
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2587
|
+
function encodeEnvelope(envelope) {
|
|
2588
|
+
return safeJsonLine(encodeEnvelopeRaw(envelope));
|
|
2589
|
+
}
|
|
2590
|
+
function encodeEnvelopeRaw(envelope) {
|
|
2591
|
+
if (envelope.ok) {
|
|
2592
|
+
return JSON.stringify({ ok: true, data: envelope.data, next: envelope.next });
|
|
2593
|
+
}
|
|
2594
|
+
const { extra, data } = envelope;
|
|
2595
|
+
const doc = {
|
|
2596
|
+
ok: false,
|
|
2597
|
+
error: stripAnsi(envelope.error),
|
|
2598
|
+
message: stripAnsi(envelope.message),
|
|
2599
|
+
next: envelope.next,
|
|
2600
|
+
exit: envelope.exit
|
|
2601
|
+
};
|
|
2602
|
+
for (const [k, v] of Object.entries(extra ?? {})) if (!(k in doc)) doc[k] = v;
|
|
2603
|
+
if (data !== void 0 && !("data" in doc)) doc.data = data;
|
|
2604
|
+
return JSON.stringify(doc);
|
|
2605
|
+
}
|
|
2606
|
+
function isEnvelope(value) {
|
|
2607
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
2608
|
+
const keys = Object.keys(value);
|
|
2609
|
+
return keys.length === 3 && value.ok === true && "data" in value && "next" in value;
|
|
2610
|
+
}
|
|
2611
|
+
function okEnvelope(data) {
|
|
2612
|
+
if (isEnvelope(data)) return data;
|
|
2613
|
+
return { ok: true, data, next: null };
|
|
2614
|
+
}
|
|
2615
|
+
var RESULT_BYTES = 4096;
|
|
2616
|
+
|
|
2617
|
+
// src/human-needed.ts
|
|
2618
|
+
var HumanNeededInvalidError = class extends Error {
|
|
2619
|
+
name = "HumanNeededInvalidError";
|
|
2620
|
+
};
|
|
2621
|
+
var TITLE_CHARS = 60;
|
|
2622
|
+
function recordHumanNeeded(db, opts) {
|
|
2623
|
+
const reason = opts.reason.trim();
|
|
2624
|
+
const caller = opts.caller.trim();
|
|
2625
|
+
if (reason === "") throw new HumanNeededInvalidError("human-needed: reason is blank");
|
|
2626
|
+
if (caller === "") {
|
|
2627
|
+
throw new HumanNeededInvalidError(
|
|
2628
|
+
"human-needed: no caller identity (run inside a mu agent or a pi session)"
|
|
2629
|
+
);
|
|
2630
|
+
}
|
|
2631
|
+
return withOpContext(
|
|
2632
|
+
db,
|
|
2633
|
+
{ intent: "task.note", actor: caller, group: "new" },
|
|
2634
|
+
() => db.transaction(() => {
|
|
2635
|
+
const open = openNeedFor(db, caller, opts.workstream);
|
|
2636
|
+
if (open !== void 0) {
|
|
2637
|
+
if (open.briefs.includes(reason)) return { id: open.id, changed: false };
|
|
2638
|
+
insertNote(db, open.id, reason, {
|
|
2639
|
+
workstream: opts.workstream,
|
|
2640
|
+
author: caller,
|
|
2641
|
+
kind: "brief"
|
|
2642
|
+
});
|
|
2643
|
+
return { id: open.id, changed: true };
|
|
2644
|
+
}
|
|
2645
|
+
const title = `human needed: ${reason.replace(/\s+/g, " ").slice(0, TITLE_CHARS)}`;
|
|
2646
|
+
const { ids } = addTasks(db, [{ title, note: reason }], {
|
|
2647
|
+
workstream: opts.workstream,
|
|
2648
|
+
kind: "human",
|
|
2649
|
+
noteAuthor: caller
|
|
2650
|
+
});
|
|
2651
|
+
const id = ids[0];
|
|
2652
|
+
if (id === void 0) throw new Error("recordHumanNeeded: no task created");
|
|
2653
|
+
const taskId = db.prepare("SELECT id FROM tasks WHERE workstream_id = ? AND local_id = ?").get(resolveWorkstreamId(db, opts.workstream), id).id;
|
|
2654
|
+
db.prepare("INSERT INTO human_needs (task_id, caller) VALUES (?, ?)").run(taskId, caller);
|
|
2655
|
+
return { id, changed: true };
|
|
2656
|
+
}).immediate()
|
|
2657
|
+
);
|
|
2658
|
+
}
|
|
2659
|
+
function openNeedRows(db, caller, workstream) {
|
|
2660
|
+
return db.prepare(
|
|
2661
|
+
`SELECT t.id AS taskId, t.local_id AS id, w.name AS workstream
|
|
2662
|
+
FROM human_needs h
|
|
2663
|
+
JOIN tasks t ON t.id = h.task_id
|
|
2664
|
+
JOIN workstreams w ON w.id = t.workstream_id
|
|
2665
|
+
WHERE h.caller = ? AND t.kind = 'human' AND t.status = 'OPEN'
|
|
2666
|
+
AND w.torn_down_at IS NULL AND (? IS NULL OR w.name = ?)
|
|
2667
|
+
ORDER BY t.id`
|
|
2668
|
+
).all(caller, workstream ?? null, workstream ?? null);
|
|
2669
|
+
}
|
|
2670
|
+
function briefsOf(db, taskId) {
|
|
2671
|
+
return db.prepare("SELECT content FROM task_notes WHERE task_id = ? AND kind = 'brief' ORDER BY id").all(taskId).map((r) => r.content);
|
|
2672
|
+
}
|
|
2673
|
+
function openNeedFor(db, caller, workstream) {
|
|
2674
|
+
const row = openNeedRows(db, caller, workstream).at(-1);
|
|
2675
|
+
return row === void 0 ? void 0 : { id: row.id, briefs: briefsOf(db, row.taskId) };
|
|
2676
|
+
}
|
|
2677
|
+
function openHumanNeeds(db, caller) {
|
|
2678
|
+
return openNeedRows(db, caller).map((r) => ({
|
|
2679
|
+
workstream: r.workstream,
|
|
2680
|
+
id: r.id,
|
|
2681
|
+
reason: briefsOf(db, r.taskId).join("; ")
|
|
2682
|
+
}));
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
// src/next-hints.ts
|
|
2686
|
+
var UnguardedMoveError = class extends Error {
|
|
2687
|
+
name = "UnguardedMoveError";
|
|
2688
|
+
};
|
|
2689
|
+
var NEXT_BYTES = 1024;
|
|
2690
|
+
var MAX_ALTERNATIVES = 3;
|
|
2691
|
+
function dispatchPrompt(task) {
|
|
2692
|
+
return `Claim done: work on ${task.id}. Read: mu task notes ${task.id} -w ${task.workstream}`;
|
|
2693
|
+
}
|
|
2694
|
+
function agentRefOf(agent, runs, incarnation) {
|
|
2695
|
+
return `agent:${agent.workstreamName}/${agent.name}${runs === void 0 ? "" : `@${runs}`}${incarnation === void 0 ? "" : `#${incarnation}`}`;
|
|
2696
|
+
}
|
|
2697
|
+
function resumeArgv(id, scope) {
|
|
2698
|
+
return ["mu", "wait", "--resume", id, ...scope === void 0 ? [] : ["-w", scope]];
|
|
2699
|
+
}
|
|
2700
|
+
function renderArgv(argv) {
|
|
2701
|
+
if (argv[0] !== "mu") throw new Error(`a next move starts with mu (got ${argv[0] ?? "nothing"})`);
|
|
2702
|
+
return argv.map(shellWord).join(" ");
|
|
2703
|
+
}
|
|
2704
|
+
function renderPlan(mode, moves) {
|
|
2705
|
+
for (const m of moves) {
|
|
2706
|
+
if (m.destructive === true && !m.argv.includes("--if")) {
|
|
2707
|
+
throw new UnguardedMoveError(`destructive move without --if: ${m.argv.join(" ")}`);
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
const ordered = mode === "seq" ? moves : [...moves].sort((a, b) => rank(a) - rank(b));
|
|
2711
|
+
const commands = [...new Set(ordered.map((m) => renderArgv(m.argv)))];
|
|
2712
|
+
const kept = mode === "alt" ? commands.slice(0, MAX_ALTERNATIVES) : commands;
|
|
2713
|
+
return kept.length === 0 ? null : { mode, commands: kept };
|
|
2714
|
+
}
|
|
2715
|
+
var rank = (m) => m.destructive === true ? 1 : 0;
|
|
2716
|
+
var utf8 = (s) => Buffer.byteLength(s, "utf8");
|
|
2717
|
+
function capPlan(plan, bytes3 = NEXT_BYTES) {
|
|
2718
|
+
if (plan === null || utf8(JSON.stringify(plan)) <= bytes3) return { next: plan, cut: 0 };
|
|
2719
|
+
if (plan.mode === "seq") return { next: null, cut: plan.commands.length };
|
|
2720
|
+
const commands = [...plan.commands];
|
|
2721
|
+
while (commands.length > 0 && utf8(JSON.stringify({ mode: "alt", commands })) > bytes3) {
|
|
2722
|
+
commands.pop();
|
|
2723
|
+
}
|
|
2724
|
+
const cut = plan.commands.length - commands.length;
|
|
2725
|
+
return { next: commands.length === 0 ? null : { mode: "alt", commands }, cut };
|
|
2726
|
+
}
|
|
2727
|
+
function outcomeMove(db, o) {
|
|
2728
|
+
switch (o.kind) {
|
|
2729
|
+
case "sent":
|
|
2730
|
+
if (o.runs === void 0) return void 0;
|
|
2731
|
+
return {
|
|
2732
|
+
argv: ["mu", "wait", agentRefOf(agentLike(o.agent), o.runs, o.incarnation)]
|
|
2733
|
+
};
|
|
2734
|
+
case "send-unknown": {
|
|
2735
|
+
const row = getAgent(db, o.agent.name, o.agent.workstream);
|
|
2736
|
+
if (row === void 0 || row.incarnation !== o.agentId) return void 0;
|
|
2737
|
+
return {
|
|
2738
|
+
argv: ["mu", "agent", "check", o.agent.name, o.msgId, "-w", o.agent.workstream, "--json"]
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
case "gate-spawned":
|
|
2742
|
+
return { argv: ["mu", "wait", agentRefOf(agentLike(o.agent))] };
|
|
2743
|
+
case "claimed-for": {
|
|
2744
|
+
const owner = getAgent(db, o.owner.name, o.owner.workstream);
|
|
2745
|
+
if (owner === void 0) return void 0;
|
|
2746
|
+
const ctl = expectsCtl(owner);
|
|
2747
|
+
if (!ctl && o.muxCanSend === false) return void 0;
|
|
2748
|
+
const fresh = ctl ? ["--fresh"] : [];
|
|
2749
|
+
return {
|
|
2750
|
+
argv: [
|
|
2751
|
+
"mu",
|
|
2752
|
+
"agent",
|
|
2753
|
+
"send",
|
|
2754
|
+
owner.name,
|
|
2755
|
+
...fresh,
|
|
2756
|
+
dispatchPrompt(o.task),
|
|
2757
|
+
"-w",
|
|
2758
|
+
owner.workstreamName
|
|
2759
|
+
]
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
case "preview":
|
|
2763
|
+
return { argv: [...o.apply, "--if", String(o.revision)], destructive: true };
|
|
2764
|
+
case "waited":
|
|
2765
|
+
if (o.resumeId === null) return void 0;
|
|
2766
|
+
return { argv: resumeArgv(o.resumeId, o.scope) };
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
var agentLike = (a) => ({ name: a.name, workstreamName: a.workstream });
|
|
2770
|
+
function executorMove(db, ws) {
|
|
2771
|
+
if (isScratchWorkstream(ws)) return void 0;
|
|
2772
|
+
if (pendingAttempts(db, ws).length === 0 && authorisedReady(db, ws).length === 0) {
|
|
2773
|
+
return void 0;
|
|
2774
|
+
}
|
|
2775
|
+
return { argv: ["mu", "next", "-w", ws] };
|
|
2776
|
+
}
|
|
2777
|
+
function ownedWaitMove(db, callers, ws) {
|
|
2778
|
+
const owes = callers.some(
|
|
2779
|
+
(c) => (readWaitRegistration(db, c)?.refs ?? []).some((r) => ws === void 0 || r.workstream === ws)
|
|
2780
|
+
);
|
|
2781
|
+
if (!owes) return void 0;
|
|
2782
|
+
return { argv: ws === void 0 ? ["mu", "wait"] : ["mu", "wait", "-w", ws] };
|
|
2783
|
+
}
|
|
2784
|
+
var COURSE_CHECK_CLOSED = 10;
|
|
2785
|
+
var COURSE_CHECK_AGE_MS = 24 * 36e5;
|
|
2786
|
+
function courseCheckHint(db, ws, now = Date.now()) {
|
|
2787
|
+
const row = db.prepare(
|
|
2788
|
+
`WITH w AS (SELECT id, created_at FROM workstreams WHERE name = ? AND torn_down_at IS NULL),
|
|
2789
|
+
a AS (SELECT COALESCE(
|
|
2790
|
+
(SELECT MAX(n.created_at) FROM task_notes n JOIN tasks t ON t.id = n.task_id
|
|
2791
|
+
WHERE t.workstream_id = w.id AND n.content LIKE 'COURSE CHECK%'),
|
|
2792
|
+
w.created_at) AS since, w.id AS wid FROM w)
|
|
2793
|
+
SELECT a.since AS since,
|
|
2794
|
+
(SELECT COUNT(*) FROM tasks t WHERE t.workstream_id = a.wid AND t.status = 'CLOSED'
|
|
2795
|
+
AND t.substate = 'done' AND t.kind = 'work' AND (SELECT MAX(c.created_at)
|
|
2796
|
+
FROM task_notes c WHERE c.task_id = t.id AND c.kind = 'close') > a.since) AS closed
|
|
2797
|
+
FROM a`
|
|
2798
|
+
).get(ws);
|
|
2799
|
+
if (row === void 0 || row.closed === 0) return void 0;
|
|
2800
|
+
const old = now - Date.parse(row.since) >= COURSE_CHECK_AGE_MS;
|
|
2801
|
+
if (row.closed < COURSE_CHECK_CLOSED && !old) return void 0;
|
|
2802
|
+
return `course check due: ${row.closed} closed since ${row.since.slice(0, 16)}Z. Stop dispatching: finish merging, then follow recipes/course-correct.md`;
|
|
2803
|
+
}
|
|
2804
|
+
function computeNext(db, ctx) {
|
|
2805
|
+
const moves = [];
|
|
2806
|
+
const own = ctx.outcome === void 0 ? void 0 : outcomeMove(db, ctx.outcome);
|
|
2807
|
+
if (own !== void 0) moves.push(own);
|
|
2808
|
+
const stopped = ctx.callers.some((c) => openHumanNeeds(db, c).length > 0);
|
|
2809
|
+
if (!stopped && ctx.outcome?.kind !== "preview") {
|
|
2810
|
+
const exec = ctx.workstream === void 0 ? void 0 : executorMove(db, ctx.workstream);
|
|
2811
|
+
if (exec !== void 0) moves.push(exec);
|
|
2812
|
+
if (ctx.armed !== false && (own === void 0 || own.argv[1] !== "wait")) {
|
|
2813
|
+
const wait = ownedWaitMove(db, ctx.callers, ctx.workstream);
|
|
2814
|
+
if (wait !== void 0) moves.push(wait);
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
return capPlan(renderPlan("alt", moves)).next;
|
|
2818
|
+
}
|
|
2819
|
+
function computeNotices(db, ctx, now = Date.now()) {
|
|
2820
|
+
const hint = ctx.workstream === void 0 || isScratchWorkstream(ctx.workstream) ? void 0 : courseCheckHint(db, ctx.workstream, now);
|
|
2821
|
+
return hint === void 0 ? [] : [hint];
|
|
2822
|
+
}
|
|
2823
|
+
function noticesField(db, workstreams, now = Date.now()) {
|
|
2824
|
+
const notices = [...new Set(workstreams)].flatMap(
|
|
2825
|
+
(workstream) => computeNotices(db, { callers: [], workstream }, now)
|
|
2826
|
+
);
|
|
2827
|
+
return notices.length > 0 ? { notices } : {};
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
// src/sync.ts
|
|
2831
|
+
import { existsSync as existsSync3, statSync as statSync3 } from "fs";
|
|
2832
|
+
import Database from "better-sqlite3";
|
|
2833
|
+
var SyncPeerNotFoundError = class extends Error {
|
|
2834
|
+
constructor(ref, known) {
|
|
2835
|
+
super(say("sync.peer_not_found", ref, known));
|
|
2836
|
+
this.ref = ref;
|
|
2837
|
+
this.known = known;
|
|
2838
|
+
}
|
|
2839
|
+
ref;
|
|
2840
|
+
known;
|
|
2841
|
+
name = "SyncPeerNotFoundError";
|
|
2842
|
+
};
|
|
2843
|
+
var SyncPeerRefAmbiguousError = class extends Error {
|
|
2844
|
+
constructor(ref, candidates) {
|
|
2845
|
+
super(say("sync.peer_ambiguous", ref, candidates));
|
|
2846
|
+
this.ref = ref;
|
|
2847
|
+
this.candidates = candidates;
|
|
2848
|
+
}
|
|
2849
|
+
ref;
|
|
2850
|
+
candidates;
|
|
2851
|
+
name = "SyncPeerRefAmbiguousError";
|
|
2852
|
+
};
|
|
2853
|
+
var SyncSourceNotFoundError = class extends Error {
|
|
2854
|
+
constructor(path) {
|
|
2855
|
+
super(say("sync.source_missing", path));
|
|
2856
|
+
this.path = path;
|
|
2857
|
+
}
|
|
2858
|
+
path;
|
|
2859
|
+
name = "SyncSourceNotFoundError";
|
|
2860
|
+
};
|
|
2861
|
+
var SyncSourceTooNewError = class extends Error {
|
|
2862
|
+
constructor(path, detectedVersion) {
|
|
2863
|
+
super(say("sync.source_too_new", path, detectedVersion, CURRENT_SCHEMA_VERSION));
|
|
2864
|
+
this.path = path;
|
|
2865
|
+
this.detectedVersion = detectedVersion;
|
|
2866
|
+
}
|
|
2867
|
+
path;
|
|
2868
|
+
detectedVersion;
|
|
2869
|
+
name = "SyncSourceTooNewError";
|
|
2870
|
+
};
|
|
2871
|
+
var PEER_STALE_MS = 24 * 60 * 60 * 1e3;
|
|
2872
|
+
var PEER_SHORT_LEN = 8;
|
|
2873
|
+
function peerStatuses(db, dir) {
|
|
2874
|
+
const self = localMachineId(db);
|
|
2875
|
+
const now = Date.now();
|
|
2876
|
+
const rows = discoverPeers(dir, self).map((peer) => {
|
|
2877
|
+
let lastSeenMs = null;
|
|
2878
|
+
try {
|
|
2879
|
+
lastSeenMs = statSync3(peer.path).mtimeMs;
|
|
2880
|
+
} catch {
|
|
2881
|
+
lastSeenMs = null;
|
|
2882
|
+
}
|
|
2883
|
+
const watermarkKey = peerWatermarkKey(peer);
|
|
2884
|
+
const watermark = getWatermark(db, watermarkKey);
|
|
2885
|
+
const total = segmentCaughtUp(db, watermarkKey, peer.path, watermark) ? watermark : segmentLineCount(peer.path);
|
|
2886
|
+
const ageMs = lastSeenMs === null ? null : Math.max(0, now - lastSeenMs);
|
|
2887
|
+
return {
|
|
2888
|
+
machineId: peer.machineId,
|
|
2889
|
+
short: peer.machineId.slice(0, PEER_SHORT_LEN),
|
|
2890
|
+
path: peer.path,
|
|
2891
|
+
conflictCopy: peer.conflictCopy,
|
|
2892
|
+
watermarkKey,
|
|
2893
|
+
watermark,
|
|
2894
|
+
total,
|
|
2895
|
+
behind: Math.max(0, total - watermark),
|
|
2896
|
+
lastSeenMs,
|
|
2897
|
+
ageMs,
|
|
2898
|
+
stale: ageMs === null || ageMs > PEER_STALE_MS
|
|
2899
|
+
};
|
|
2900
|
+
});
|
|
2901
|
+
return rows.sort((a, b) => (b.lastSeenMs ?? 0) - (a.lastSeenMs ?? 0));
|
|
2902
|
+
}
|
|
2903
|
+
function resolvePeerRef(peers, ref) {
|
|
2904
|
+
const matches = peers.filter((p) => p.machineId.startsWith(ref));
|
|
2905
|
+
const exact = matches.filter((p) => p.machineId === ref);
|
|
2906
|
+
const candidates = exact.length > 0 ? exact : matches;
|
|
2907
|
+
const machines = [...new Set(candidates.map((p) => p.machineId))];
|
|
2908
|
+
const first = candidates.find((p) => !p.conflictCopy) ?? candidates[0];
|
|
2909
|
+
if (first === void 0) {
|
|
2910
|
+
throw new SyncPeerNotFoundError(
|
|
2911
|
+
ref,
|
|
2912
|
+
peers.map((p) => p.short)
|
|
2913
|
+
);
|
|
2914
|
+
}
|
|
2915
|
+
if (machines.length > 1) {
|
|
2916
|
+
throw new SyncPeerRefAmbiguousError(
|
|
2917
|
+
ref,
|
|
2918
|
+
machines.map((m) => m.slice(0, PEER_SHORT_LEN))
|
|
2919
|
+
);
|
|
2920
|
+
}
|
|
2921
|
+
return first;
|
|
2922
|
+
}
|
|
2923
|
+
function repairPeer(db, ref, dir) {
|
|
2924
|
+
const peers = peerStatuses(db, dir);
|
|
2925
|
+
const peer = resolvePeerRef(peers, ref);
|
|
2926
|
+
for (const p of peers) {
|
|
2927
|
+
if (p.machineId === peer.machineId) resetWatermark(db, peerWatermarkKey(p));
|
|
2928
|
+
}
|
|
2929
|
+
return peer;
|
|
2930
|
+
}
|
|
2931
|
+
function syncEnabled() {
|
|
2932
|
+
return syncDir() !== null;
|
|
2933
|
+
}
|
|
2934
|
+
function warn(message, opts) {
|
|
2935
|
+
if (opts?.quiet === true) return;
|
|
2936
|
+
if (isJsonMode()) return;
|
|
2937
|
+
process.stderr.write(`mu: sync: ${message}
|
|
2938
|
+
`);
|
|
2939
|
+
}
|
|
2940
|
+
function foreignVersionMessage(r) {
|
|
2941
|
+
return `segment ${r.segmentPath ?? "?"} has a v${r.foreignVersion ?? "?"} line written by a newer mu; upgrade mu on this machine`;
|
|
2942
|
+
}
|
|
2943
|
+
function describeDefects(peerShort, defects) {
|
|
2944
|
+
const first = defects[0];
|
|
2945
|
+
const detail = first === void 0 ? "unknown defect" : `${first.kind} at line ${first.line}`;
|
|
2946
|
+
const more = defects.length > 1 ? ` (+${defects.length - 1} more)` : "";
|
|
2947
|
+
const skipped = /* @__PURE__ */ new Set(["entity-not-synced", "duplicate-op"]);
|
|
2948
|
+
const fileWide = /* @__PURE__ */ new Set(["manifest-mismatch"]);
|
|
2949
|
+
let hint;
|
|
2950
|
+
if (defects.every((d) => skipped.has(d.kind))) {
|
|
2951
|
+
hint = " \u2014 skipped; the rest of the segment applied";
|
|
2952
|
+
} else if (defects.every((d) => skipped.has(d.kind) || fileWide.has(d.kind))) {
|
|
2953
|
+
hint = " \u2014 the file is shorter than its manifest; copy it from the peer again";
|
|
2954
|
+
} else {
|
|
2955
|
+
hint = ` \u2014 re-read with \`mu sync --repair ${peerShort}\``;
|
|
2956
|
+
}
|
|
2957
|
+
return `peer ${peerShort}: ${detail}${more}${hint}`;
|
|
2958
|
+
}
|
|
2959
|
+
async function ambientIngest(db, opts) {
|
|
2960
|
+
const dir = syncDir();
|
|
2961
|
+
if (dir === null) return { ingested: [], warnings: [] };
|
|
2962
|
+
const warnings = [];
|
|
2963
|
+
const ingested = [];
|
|
2964
|
+
try {
|
|
2965
|
+
const self = localMachineId(db);
|
|
2966
|
+
for (const peer of discoverPeers(dir, self)) {
|
|
2967
|
+
try {
|
|
2968
|
+
const result = ingestSegment(db, peer);
|
|
2969
|
+
ingested.push(result);
|
|
2970
|
+
if (result.defects.length > 0) {
|
|
2971
|
+
const message = describeDefects(peer.machineId.slice(0, PEER_SHORT_LEN), result.defects);
|
|
2972
|
+
warnings.push(message);
|
|
2973
|
+
warn(message, opts);
|
|
2974
|
+
}
|
|
2975
|
+
} catch (err) {
|
|
2976
|
+
const message = `peer ${peer.machineId.slice(0, PEER_SHORT_LEN)}: ${err instanceof Error ? err.message : String(err)}`;
|
|
2977
|
+
warnings.push(message);
|
|
2978
|
+
warn(message, opts);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
} catch (err) {
|
|
2982
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2983
|
+
warnings.push(message);
|
|
2984
|
+
warn(message, opts);
|
|
2985
|
+
}
|
|
2986
|
+
return { ingested, warnings };
|
|
2987
|
+
}
|
|
2988
|
+
async function ambientFlush(db, opts) {
|
|
2989
|
+
const dir = syncDir();
|
|
2990
|
+
if (dir === null) return null;
|
|
2991
|
+
try {
|
|
2992
|
+
const result = await flushSegment(db, dir, { lockWaitMs: opts?.lockWaitMs });
|
|
2993
|
+
if (result.selfRepaired !== null) {
|
|
2994
|
+
const d = result.selfRepaired;
|
|
2995
|
+
warn(
|
|
2996
|
+
`own segment truncated back to its last good line: ${d.kind} at line ${d.line} (${d.detail}) \u2014 regenerated from the ops table`,
|
|
2997
|
+
opts
|
|
2998
|
+
);
|
|
2999
|
+
}
|
|
3000
|
+
if (result.foreignVersionAt !== null) warn(foreignVersionMessage(result), opts);
|
|
3001
|
+
if (result.lockBusy === true) {
|
|
3002
|
+
warn("segment locked by another mu; this op publishes on the next flush", opts);
|
|
3003
|
+
}
|
|
3004
|
+
return result;
|
|
3005
|
+
} catch (err) {
|
|
3006
|
+
warn(err instanceof Error ? err.message : String(err), opts);
|
|
3007
|
+
return null;
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
async function ambientSyncPass(db, opts) {
|
|
3011
|
+
if (!syncEnabled()) return { ingested: [], flushed: null };
|
|
3012
|
+
const { ingested } = await ambientIngest(db, opts);
|
|
3013
|
+
const flushed = await ambientFlush(db, opts);
|
|
3014
|
+
return { ingested, flushed };
|
|
3015
|
+
}
|
|
3016
|
+
function sourceSchemaVersion(source) {
|
|
3017
|
+
try {
|
|
3018
|
+
const row = source.prepare("SELECT version FROM schema_version WHERE id = 1").get();
|
|
3019
|
+
return row?.version ?? null;
|
|
3020
|
+
} catch {
|
|
3021
|
+
return null;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
function ingestFromDb(db, path) {
|
|
3025
|
+
if (!existsSync3(path)) throw new SyncSourceNotFoundError(path);
|
|
3026
|
+
const self = localMachineId(db);
|
|
3027
|
+
const source = new Database(path, { readonly: true, fileMustExist: true });
|
|
3028
|
+
try {
|
|
3029
|
+
const theirs = sourceSchemaVersion(source);
|
|
3030
|
+
if (theirs !== null && theirs > CURRENT_SCHEMA_VERSION) {
|
|
3031
|
+
throw new SyncSourceTooNewError(path, theirs);
|
|
3032
|
+
}
|
|
3033
|
+
const rows = source.prepare(
|
|
3034
|
+
`SELECT hlc, machine_id, group_id, actor, intent, entity, key, op, payload
|
|
3035
|
+
FROM ops
|
|
3036
|
+
WHERE machine_id != ?
|
|
3037
|
+
ORDER BY hlc`
|
|
3038
|
+
).all(self);
|
|
3039
|
+
const synced = new Set(SYNCED_ENTITIES);
|
|
3040
|
+
let read = 0;
|
|
3041
|
+
let changed = 0;
|
|
3042
|
+
let skippedLocal = 0;
|
|
3043
|
+
const run = db.transaction(() => {
|
|
3044
|
+
for (const row of rows) {
|
|
3045
|
+
if (!synced.has(row.entity)) {
|
|
3046
|
+
skippedLocal += 1;
|
|
3047
|
+
continue;
|
|
3048
|
+
}
|
|
3049
|
+
const op = {
|
|
3050
|
+
hlc: row.hlc,
|
|
3051
|
+
machineId: row.machine_id,
|
|
3052
|
+
groupId: row.group_id,
|
|
3053
|
+
actor: row.actor,
|
|
3054
|
+
intent: row.intent,
|
|
3055
|
+
entity: row.entity,
|
|
3056
|
+
key: row.key,
|
|
3057
|
+
op: row.op === "del" ? "del" : "put",
|
|
3058
|
+
payload: row.payload
|
|
3059
|
+
};
|
|
3060
|
+
const result = applyIncomingOp(db, op);
|
|
3061
|
+
read += 1;
|
|
3062
|
+
if (result.changed) changed += 1;
|
|
3063
|
+
}
|
|
3064
|
+
});
|
|
3065
|
+
run.immediate();
|
|
3066
|
+
return { path, read, changed, skippedLocal };
|
|
3067
|
+
} finally {
|
|
3068
|
+
try {
|
|
3069
|
+
source.close();
|
|
3070
|
+
} catch {
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
// src/wait-callers.ts
|
|
3076
|
+
function currentCallers(env = process.env) {
|
|
3077
|
+
const out = [];
|
|
3078
|
+
const { MU_MANAGED_AGENT: managed, MU_WORKSTREAM: ws, MU_AGENT_NAME: name } = env;
|
|
3079
|
+
if (managed === "1" && ws && name) out.push(`${ws}/${name}`);
|
|
3080
|
+
if (env.PI_SESSION_ID) out.push(`pi:${env.PI_SESSION_ID}`);
|
|
3081
|
+
return out;
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
// src/agent-output.ts
|
|
3085
|
+
function workstreamIdOf(db, workstream) {
|
|
3086
|
+
const row = db.prepare("SELECT id FROM workstreams WHERE name = ?").get(workstream);
|
|
3087
|
+
return row?.id ?? 0;
|
|
3088
|
+
}
|
|
3089
|
+
function agentToOutput(row, workstreamId2, live = {}) {
|
|
3090
|
+
return {
|
|
3091
|
+
id: row.incarnation,
|
|
3092
|
+
name: row.name,
|
|
3093
|
+
workstream_id: workstreamId2,
|
|
3094
|
+
workstream: row.workstreamName,
|
|
3095
|
+
cli: row.cli,
|
|
3096
|
+
pane_id: row.paneId,
|
|
3097
|
+
role: row.role,
|
|
3098
|
+
tab: row.tab,
|
|
3099
|
+
cwd: row.cwd,
|
|
3100
|
+
base: row.base,
|
|
3101
|
+
command: row.command,
|
|
3102
|
+
command_source: row.commandSource,
|
|
3103
|
+
spawner: row.spawner,
|
|
3104
|
+
host: row.host,
|
|
3105
|
+
collected_head: row.collectedHead,
|
|
3106
|
+
repo: row.repo,
|
|
3107
|
+
gate_token: row.gateToken,
|
|
3108
|
+
created_at: row.createdAt,
|
|
3109
|
+
updated_at: row.updatedAt,
|
|
3110
|
+
...live.state !== void 0 ? { state: live.state } : {},
|
|
3111
|
+
...live.source !== void 0 ? { source: live.source } : {},
|
|
3112
|
+
...live.ctl !== void 0 ? { ctl: live.ctl } : {},
|
|
3113
|
+
...live.ctlSocket !== void 0 ? { ctl_socket: live.ctlSocket } : {},
|
|
3114
|
+
...live.since !== void 0 ? { since: live.since } : {},
|
|
3115
|
+
...live.reason !== void 0 ? { reason: live.reason } : {},
|
|
3116
|
+
...live.idle !== void 0 ? { idle: live.idle } : {},
|
|
3117
|
+
...live.trustDialog !== void 0 ? { trust_dialog: live.trustDialog } : {}
|
|
3118
|
+
};
|
|
3119
|
+
}
|
|
3120
|
+
function closePreviewToOutput(p) {
|
|
3121
|
+
const { incarnation, paneId, ...rest } = p;
|
|
3122
|
+
return { ...rest, id: incarnation, pane_id: paneId, recovery_id: p.recovery?.id ?? null };
|
|
3123
|
+
}
|
|
3124
|
+
function liveAgentToOutput(agent, workstreamId2) {
|
|
3125
|
+
return agentToOutput(agent, workstreamId2, {
|
|
3126
|
+
state: agent.state,
|
|
3127
|
+
source: agent.source,
|
|
3128
|
+
...agent.ctl !== void 0 ? { ctl: agent.ctl } : {},
|
|
3129
|
+
since: agent.since,
|
|
3130
|
+
...agent.reason !== void 0 ? { reason: agent.reason } : {},
|
|
3131
|
+
...agent.idle !== void 0 ? { idle: agent.idle } : {}
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
// src/cli/tasks/records.ts
|
|
3136
|
+
var RECORD_CHARS = 400;
|
|
3137
|
+
var JSON_TITLE_CHARS = 120;
|
|
3138
|
+
var JSON_FIELD_CHARS = 80;
|
|
3139
|
+
var UNSAFE = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069\\]/u;
|
|
3140
|
+
var UNSAFE_G = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069\\]/gu;
|
|
3141
|
+
function escapeRecordText(s) {
|
|
3142
|
+
if (!UNSAFE.test(s)) return s;
|
|
3143
|
+
return s.replace(UNSAFE_G, (c) => {
|
|
3144
|
+
switch (c) {
|
|
3145
|
+
case "\\":
|
|
3146
|
+
return "\\\\";
|
|
3147
|
+
case "\n":
|
|
3148
|
+
return "\\n";
|
|
3149
|
+
case " ":
|
|
3150
|
+
return "\\t";
|
|
3151
|
+
case "\r":
|
|
3152
|
+
return "\\r";
|
|
3153
|
+
default:
|
|
3154
|
+
return `\\u{${(c.codePointAt(0) ?? 0).toString(16)}}`;
|
|
3155
|
+
}
|
|
3156
|
+
});
|
|
3157
|
+
}
|
|
3158
|
+
function jsonDocument(data) {
|
|
3159
|
+
return encodeEnvelope(okEnvelope(data));
|
|
3160
|
+
}
|
|
3161
|
+
var bytes = (s) => Buffer.byteLength(s);
|
|
3162
|
+
var linesBytes = (lines) => bytes(lines.join("\n")) + 1;
|
|
3163
|
+
var codePoints = (s) => [...s];
|
|
3164
|
+
function clipEscaped(s, maxChars, maxBytes = Number.POSITIVE_INFINITY) {
|
|
3165
|
+
const cps = codePoints(s);
|
|
3166
|
+
let out = "";
|
|
3167
|
+
let used = 0;
|
|
3168
|
+
let n = 0;
|
|
3169
|
+
for (const cp of cps) {
|
|
3170
|
+
const esc = escapeRecordText(cp);
|
|
3171
|
+
if (out.length + esc.length > maxChars || used + bytes(esc) > maxBytes) break;
|
|
3172
|
+
out += esc;
|
|
3173
|
+
used += bytes(esc);
|
|
3174
|
+
n++;
|
|
3175
|
+
}
|
|
3176
|
+
return { text: out, cut: cps.length - n };
|
|
3177
|
+
}
|
|
3178
|
+
function taskToOutput(row, opts = {}) {
|
|
3179
|
+
const out = {
|
|
3180
|
+
id: row.name,
|
|
3181
|
+
local_id: row.name,
|
|
3182
|
+
workstream_id: row.workstreamId,
|
|
3183
|
+
title: row.title,
|
|
3184
|
+
status: row.status,
|
|
3185
|
+
substate: row.substate,
|
|
3186
|
+
impact: row.impact,
|
|
3187
|
+
effort_days: row.effortDays,
|
|
3188
|
+
scored: row.scored ? 1 : 0,
|
|
3189
|
+
dispatch_host: row.dispatchHost,
|
|
3190
|
+
owner_id: row.ownerId,
|
|
3191
|
+
created_at: row.createdAt,
|
|
3192
|
+
updated_at: row.updatedAt,
|
|
3193
|
+
kind: row.kind,
|
|
3194
|
+
wave: row.wave
|
|
3195
|
+
};
|
|
3196
|
+
if (opts.withWorkstream === true) out.workstream = row.workstreamName;
|
|
3197
|
+
if (row.status === "IN_PROGRESS" && row.ownerName !== null) out.owner = row.ownerName;
|
|
3198
|
+
if (opts.revision !== void 0) out.revision = opts.revision;
|
|
3199
|
+
return out;
|
|
3200
|
+
}
|
|
3201
|
+
var SHOW_TITLE_CHARS = 600;
|
|
3202
|
+
function footerLine(c, next) {
|
|
3203
|
+
if (c.more <= 0 && c.cut <= 0) return null;
|
|
3204
|
+
const parts = [c.more > 0 ? `more=${c.more}` : "", c.cut > 0 ? `cut=${c.cut}` : ""];
|
|
3205
|
+
return `${parts.filter(Boolean).join(" ")} next=${next}`;
|
|
3206
|
+
}
|
|
3207
|
+
function taskItemCut(row, opts = {}) {
|
|
3208
|
+
const item = taskToOutput(row, opts);
|
|
3209
|
+
if (opts.whole === true) return { item, cut: 0 };
|
|
3210
|
+
let cut = opts.extraCut ?? 0;
|
|
3211
|
+
const clip = (s, max) => {
|
|
3212
|
+
const cps = codePoints(s);
|
|
3213
|
+
if (cps.length <= max) return s;
|
|
3214
|
+
cut += cps.length - Math.max(0, max - 1);
|
|
3215
|
+
return max <= 0 ? "" : `${cps.slice(0, max - 1).join("")}\u2026`;
|
|
3216
|
+
};
|
|
3217
|
+
item.title = clip(row.title, opts.titleChars ?? JSON_TITLE_CHARS);
|
|
3218
|
+
if (item.wave !== null) item.wave = clip(item.wave, JSON_FIELD_CHARS);
|
|
3219
|
+
if (item.dispatch_host !== null) item.dispatch_host = clip(item.dispatch_host, JSON_FIELD_CHARS);
|
|
3220
|
+
if (item.owner !== void 0) item.owner = clip(item.owner, JSON_FIELD_CHARS);
|
|
3221
|
+
return { item, cut };
|
|
3222
|
+
}
|
|
3223
|
+
function taskRecordCut(row, opts = {}) {
|
|
3224
|
+
const whole = opts.whole === true;
|
|
3225
|
+
let fieldCut = 0;
|
|
3226
|
+
const clip = (s, chars = 40) => {
|
|
3227
|
+
if (whole) return escapeRecordText(s);
|
|
3228
|
+
const { text: text2, cut: cut2 } = clipEscaped(s, chars);
|
|
3229
|
+
if (cut2 === 0) return text2;
|
|
3230
|
+
fieldCut += cut2;
|
|
3231
|
+
return `${text2}\u2026`;
|
|
3232
|
+
};
|
|
3233
|
+
const kv = [`impact=${row.impact}`, `effort_days=${row.effortDays}`];
|
|
3234
|
+
if (!row.scored) kv.push("scored=0");
|
|
3235
|
+
if (row.status === "IN_PROGRESS" && row.ownerName !== null)
|
|
3236
|
+
kv.push(`owner=${clip(row.ownerName)}`);
|
|
3237
|
+
if (opts.withWorkstream === true) kv.push(`ws=${clip(row.workstreamName)}`);
|
|
3238
|
+
if (row.status !== "OPEN" && row.ownerName === null && opts.claimedBy) {
|
|
3239
|
+
kv.push(`self=${clip(opts.claimedBy)}`);
|
|
3240
|
+
}
|
|
3241
|
+
if (row.kind !== "work") kv.push(`kind=${row.kind}`);
|
|
3242
|
+
if (row.wave !== null) kv.push(`wave=${clip(row.wave)}`);
|
|
3243
|
+
if (row.dispatchHost !== null) kv.push(`host=${clip(row.dispatchHost)}`);
|
|
3244
|
+
if (opts.times === true) kv.push(`created=${row.createdAt}`, `updated=${row.updatedAt}`);
|
|
3245
|
+
else if (opts.basis === "updatedAt") kv.push(`updated=${row.updatedAt}`);
|
|
3246
|
+
else if (opts.basis === "createdAt") kv.push(`created=${row.createdAt}`);
|
|
3247
|
+
if (opts.revision !== void 0) kv.push(`rev=${opts.revision}`);
|
|
3248
|
+
kv.push(...opts.extra ?? []);
|
|
3249
|
+
const id = clip(row.name, 64);
|
|
3250
|
+
const head = ["task", id, formatPair(row), kv.join(" ")].join(" ");
|
|
3251
|
+
const wholeTitle = escapeRecordText(row.title);
|
|
3252
|
+
if (whole) return { line: `${head} ${wholeTitle}`, cut: 0 };
|
|
3253
|
+
const max = opts.maxChars ?? RECORD_CHARS;
|
|
3254
|
+
const bound = opts.titleChars ?? Number.POSITIVE_INFINITY;
|
|
3255
|
+
const place = (h, extraCut) => {
|
|
3256
|
+
const roomMax = max - codePoints(h).length - 1;
|
|
3257
|
+
if (roomMax < 9) return void 0;
|
|
3258
|
+
const room2 = Math.min(roomMax, bound);
|
|
3259
|
+
if (codePoints(wholeTitle).length <= room2)
|
|
3260
|
+
return { line: `${h} ${wholeTitle}`, cut: extraCut };
|
|
3261
|
+
const { text: text2, cut: cut2 } = clipEscaped(row.title, Math.max(0, room2 - 1));
|
|
3262
|
+
return { line: `${h} ${text2}\u2026`, cut: cut2 + extraCut };
|
|
3263
|
+
};
|
|
3264
|
+
const first = place(head, fieldCut);
|
|
3265
|
+
if (first !== void 0) return first;
|
|
3266
|
+
const dropped = [
|
|
3267
|
+
row.status === "IN_PROGRESS" ? row.ownerName : null,
|
|
3268
|
+
opts.withWorkstream === true ? row.workstreamName : null,
|
|
3269
|
+
row.wave,
|
|
3270
|
+
row.dispatchHost,
|
|
3271
|
+
row.status !== "OPEN" && row.ownerName === null ? opts.claimedBy ?? null : null
|
|
3272
|
+
].filter((v) => typeof v === "string").reduce((n, v) => n + codePoints(v).length, 0);
|
|
3273
|
+
const lean = [
|
|
3274
|
+
"task",
|
|
3275
|
+
id,
|
|
3276
|
+
formatPair(row),
|
|
3277
|
+
[...kv.slice(0, 2), ...opts.extra ?? []].join(" ")
|
|
3278
|
+
].join(" ");
|
|
3279
|
+
const room = Math.min(Math.max(0, max - codePoints(lean).length - 2), Math.max(0, bound - 1));
|
|
3280
|
+
const { text, cut } = clipEscaped(row.title, room);
|
|
3281
|
+
return { line: `${lean} ${text}\u2026`, cut: cut + dropped };
|
|
3282
|
+
}
|
|
3283
|
+
function pageRecords(all, size, budget = RESULT_BYTES, min = 1) {
|
|
3284
|
+
const build = (k) => ({ shown: all.slice(0, k), more: all.length - k });
|
|
3285
|
+
let page = build(Math.min(min, all.length));
|
|
3286
|
+
for (let k = min + 1; k <= all.length; k++) {
|
|
3287
|
+
const candidate = build(k);
|
|
3288
|
+
if (size(candidate) > budget) break;
|
|
3289
|
+
page = candidate;
|
|
3290
|
+
}
|
|
3291
|
+
return page;
|
|
3292
|
+
}
|
|
3293
|
+
function taskPage(rows, opts) {
|
|
3294
|
+
const ws = opts.withWorkstream === true;
|
|
3295
|
+
const whole = opts.all;
|
|
3296
|
+
const render = (page2) => {
|
|
3297
|
+
let cut = 0;
|
|
3298
|
+
const lines = [];
|
|
3299
|
+
const items = [];
|
|
3300
|
+
for (const r of page2.shown) {
|
|
3301
|
+
const revision = opts.revisions?.get(r.name);
|
|
3302
|
+
const rev = revision === void 0 ? {} : { revision };
|
|
3303
|
+
if (opts.json) {
|
|
3304
|
+
const one = taskItemCut(r, { withWorkstream: ws, whole, ...rev });
|
|
3305
|
+
cut += one.cut;
|
|
3306
|
+
items.push(one.item);
|
|
3307
|
+
} else {
|
|
3308
|
+
const one = taskRecordCut(r, {
|
|
3309
|
+
withWorkstream: ws,
|
|
3310
|
+
whole,
|
|
3311
|
+
...rev,
|
|
3312
|
+
...opts.basis ? { basis: opts.basis } : {}
|
|
3313
|
+
});
|
|
3314
|
+
cut += one.cut;
|
|
3315
|
+
lines.push(one.line);
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
const foot = footerLine({ more: page2.more, cut }, opts.next);
|
|
3319
|
+
if (foot !== null) lines.push(foot);
|
|
3320
|
+
return {
|
|
3321
|
+
lines: opts.json ? [] : lines,
|
|
3322
|
+
data: {
|
|
3323
|
+
items,
|
|
3324
|
+
count: items.length,
|
|
3325
|
+
more: page2.more,
|
|
3326
|
+
...cut > 0 ? { cut } : {},
|
|
3327
|
+
...foot === null ? {} : { retrieve: opts.next }
|
|
3328
|
+
}
|
|
3329
|
+
};
|
|
3330
|
+
};
|
|
3331
|
+
const page = whole ? { shown: [...rows], more: 0 } : pageRecords(rows, (p) => {
|
|
3332
|
+
const r = render(p);
|
|
3333
|
+
return opts.json ? bytes(jsonDocument(r.data)) + 1 : linesBytes(r.lines);
|
|
3334
|
+
});
|
|
3335
|
+
const out = render(page);
|
|
3336
|
+
return {
|
|
3337
|
+
lines: out.lines,
|
|
3338
|
+
...opts.json ? { data: out.data } : {},
|
|
3339
|
+
shown: page.shown.length,
|
|
3340
|
+
more: page.more
|
|
3341
|
+
};
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
// src/cli/page-records.ts
|
|
3345
|
+
var bytes2 = (s) => Buffer.byteLength(s);
|
|
3346
|
+
var COLUMN_CHARS = 64;
|
|
3347
|
+
var ITEM_BYTES = 2048;
|
|
3348
|
+
function clipPlain(s, max) {
|
|
3349
|
+
const cps = codePoints(s);
|
|
3350
|
+
if (cps.length <= max) return { text: s, cut: 0 };
|
|
3351
|
+
return { text: cps.slice(0, max).join(""), cut: cps.length - max };
|
|
3352
|
+
}
|
|
3353
|
+
function shrinkItem(build) {
|
|
3354
|
+
let out = build(1);
|
|
3355
|
+
for (const scale of [0.5, 0.25, 0.125, 0]) {
|
|
3356
|
+
if (bytes2(jsonDocument({ items: [out.item] })) <= ITEM_BYTES) break;
|
|
3357
|
+
out = build(scale);
|
|
3358
|
+
}
|
|
3359
|
+
return out;
|
|
3360
|
+
}
|
|
3361
|
+
function pageOut(rows, spec) {
|
|
3362
|
+
const newest = spec.newest === true;
|
|
3363
|
+
const whole = spec.whole === true;
|
|
3364
|
+
const render = (page2) => {
|
|
3365
|
+
const shown = newest ? [...page2.shown].reverse() : page2.shown;
|
|
3366
|
+
let cut = 0;
|
|
3367
|
+
const clipped = [];
|
|
3368
|
+
const lines = [];
|
|
3369
|
+
const items = [];
|
|
3370
|
+
for (const r of shown) {
|
|
3371
|
+
if (spec.json) {
|
|
3372
|
+
const one = spec.item(r, whole);
|
|
3373
|
+
if (one.cut > 0) clipped.push(r);
|
|
3374
|
+
cut += one.cut;
|
|
3375
|
+
items.push(one.item);
|
|
3376
|
+
} else {
|
|
3377
|
+
const one = spec.text(r, whole);
|
|
3378
|
+
if (one.cut > 0) clipped.push(r);
|
|
3379
|
+
cut += one.cut;
|
|
3380
|
+
lines.push(one.line);
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
const resume = page2.more > 0 ? spec.resume(shown, page2.more) : void 0;
|
|
3384
|
+
const retrieve = cut > 0 ? (spec.retrieve ?? ((_c) => spec.resume(shown, page2.more)))(clipped) : void 0;
|
|
3385
|
+
if (resume !== void 0 && resume === retrieve) {
|
|
3386
|
+
lines.push(`more=${page2.more} cut=${cut} next=${resume}`);
|
|
3387
|
+
} else {
|
|
3388
|
+
if (resume !== void 0) lines.push(`more=${page2.more} next=${resume}`);
|
|
3389
|
+
if (retrieve !== void 0) lines.push(`cut=${cut} next=${retrieve}`);
|
|
3390
|
+
}
|
|
3391
|
+
const single = spec.singleRetrieve === true;
|
|
3392
|
+
const way = retrieve ?? (single ? resume : void 0);
|
|
3393
|
+
return {
|
|
3394
|
+
lines: spec.json ? [] : lines,
|
|
3395
|
+
data: {
|
|
3396
|
+
items,
|
|
3397
|
+
count: items.length,
|
|
3398
|
+
more: page2.more,
|
|
3399
|
+
...cut > 0 ? { cut } : {},
|
|
3400
|
+
...resume === void 0 || single ? {} : { resume },
|
|
3401
|
+
...way === void 0 ? {} : { retrieve: way }
|
|
3402
|
+
}
|
|
3403
|
+
};
|
|
3404
|
+
};
|
|
3405
|
+
const input = newest ? [...rows].reverse() : rows;
|
|
3406
|
+
const page = whole ? { shown: [...input], more: 0 } : pageRecords(
|
|
3407
|
+
input,
|
|
3408
|
+
(p) => {
|
|
3409
|
+
const r = render(p);
|
|
3410
|
+
return spec.json ? bytes2(jsonDocument(r.data)) + 1 : linesBytes(r.lines);
|
|
3411
|
+
},
|
|
3412
|
+
RESULT_BYTES,
|
|
3413
|
+
0
|
|
3414
|
+
);
|
|
3415
|
+
const out = render(page);
|
|
3416
|
+
return {
|
|
3417
|
+
lines: out.lines,
|
|
3418
|
+
...spec.json ? { data: out.data } : {},
|
|
3419
|
+
shown: page.shown.length,
|
|
3420
|
+
more: page.more
|
|
3421
|
+
};
|
|
3422
|
+
}
|
|
3423
|
+
function clippedRecord(columns, free, whole = false) {
|
|
3424
|
+
if (whole) return { line: [...columns, free].map(escapeRecordText).join(" "), cut: 0 };
|
|
3425
|
+
let cut = 0;
|
|
3426
|
+
const lean = columns.map((c) => {
|
|
3427
|
+
const one = clipEscaped(c, COLUMN_CHARS);
|
|
3428
|
+
cut += one.cut;
|
|
3429
|
+
return one.text;
|
|
3430
|
+
}).join(" ");
|
|
3431
|
+
const room = Math.max(0, RECORD_CHARS - lean.length - 2);
|
|
3432
|
+
const tail = clipEscaped(free, room);
|
|
3433
|
+
cut += tail.cut;
|
|
3434
|
+
return { line: `${lean} ${tail.text}${tail.cut > 0 ? "\u2026" : ""}`, cut };
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
// src/cli/log-records.ts
|
|
3438
|
+
function whatLine(row) {
|
|
3439
|
+
const r = renderOp(row);
|
|
3440
|
+
if (r === null) return `${row.kind}: ${row.payload}`;
|
|
3441
|
+
return [r.verb, r.subject, r.detail].filter((s) => s !== "").join(" ");
|
|
3442
|
+
}
|
|
3443
|
+
function logRecord(row, whole = false) {
|
|
3444
|
+
const time = row.createdAt.replace(/\.\d+Z$/, "Z");
|
|
3445
|
+
const scope = parseOpKey(row.workstreamName).workstream ?? "-";
|
|
3446
|
+
return clippedRecord(["log", String(row.seq), time, row.source, scope], whatLine(row), whole);
|
|
3447
|
+
}
|
|
3448
|
+
var LIMITS = {
|
|
3449
|
+
key: 120,
|
|
3450
|
+
actor: 80,
|
|
3451
|
+
intent: 80,
|
|
3452
|
+
group_id: 64,
|
|
3453
|
+
op: 16,
|
|
3454
|
+
entity: 80,
|
|
3455
|
+
payload: 240,
|
|
3456
|
+
created_at: 40,
|
|
3457
|
+
rendered: 160
|
|
3458
|
+
};
|
|
3459
|
+
function buildItem(row, scale) {
|
|
3460
|
+
let cut = 0;
|
|
3461
|
+
const clip = (s, limit) => {
|
|
3462
|
+
const one = clipPlain(s, Math.floor(limit * scale));
|
|
3463
|
+
cut += one.cut;
|
|
3464
|
+
return one.text;
|
|
3465
|
+
};
|
|
3466
|
+
const item = {
|
|
3467
|
+
seq: row.seq,
|
|
3468
|
+
key: clip(row.workstreamName ?? "", LIMITS.key),
|
|
3469
|
+
actor: clip(row.source, LIMITS.actor),
|
|
3470
|
+
intent: row.intent === null ? null : clip(row.intent, LIMITS.intent),
|
|
3471
|
+
group_id: clip(row.group, LIMITS.group_id),
|
|
3472
|
+
op: clip(row.op, LIMITS.op),
|
|
3473
|
+
entity: clip(row.kind, LIMITS.entity),
|
|
3474
|
+
payload: clip(row.payload, LIMITS.payload),
|
|
3475
|
+
created_at: clip(row.createdAt, LIMITS.created_at),
|
|
3476
|
+
rendered: clip(renderOpLine(row), LIMITS.rendered)
|
|
3477
|
+
};
|
|
3478
|
+
if (cut > 0) item.cut = cut;
|
|
3479
|
+
return { item, cut };
|
|
3480
|
+
}
|
|
3481
|
+
function logItem(row, whole = false) {
|
|
3482
|
+
if (whole) {
|
|
3483
|
+
return {
|
|
3484
|
+
cut: 0,
|
|
3485
|
+
item: {
|
|
3486
|
+
seq: row.seq,
|
|
3487
|
+
key: row.workstreamName ?? "",
|
|
3488
|
+
actor: row.source,
|
|
3489
|
+
intent: row.intent,
|
|
3490
|
+
group_id: row.group,
|
|
3491
|
+
op: row.op,
|
|
3492
|
+
entity: row.kind,
|
|
3493
|
+
payload: row.payload,
|
|
3494
|
+
created_at: row.createdAt,
|
|
3495
|
+
rendered: renderOpLine(row)
|
|
3496
|
+
}
|
|
3497
|
+
};
|
|
3498
|
+
}
|
|
3499
|
+
return shrinkItem((scale) => buildItem(row, scale));
|
|
3500
|
+
}
|
|
3501
|
+
var MAX_RUNS = 8;
|
|
3502
|
+
function seqCommand(seqs, gapEmpty, json) {
|
|
3503
|
+
const sorted = [...new Set(seqs)].sort((a, b) => a - b);
|
|
3504
|
+
const runs = [];
|
|
3505
|
+
for (const n of sorted) {
|
|
3506
|
+
const last = runs[runs.length - 1];
|
|
3507
|
+
if (last !== void 0 && (n === last[1] + 1 || gapEmpty(last[1], n))) last[1] = n;
|
|
3508
|
+
else runs.push([n, n]);
|
|
3509
|
+
if (runs.length > MAX_RUNS) break;
|
|
3510
|
+
}
|
|
3511
|
+
const tail = json ? " --json" : "";
|
|
3512
|
+
if (runs.length > MAX_RUNS || runs.length === 0) {
|
|
3513
|
+
return `mu log --after ${Math.max(0, (sorted[0] ?? 1) - 1)}${tail}`;
|
|
3514
|
+
}
|
|
3515
|
+
const spec = runs.map(([a, b]) => a === b ? String(a) : `${a}..${b}`).join(",");
|
|
3516
|
+
return `mu log --seq ${spec}${tail}`;
|
|
3517
|
+
}
|
|
3518
|
+
function logPage(rows, opts) {
|
|
3519
|
+
const say2 = (seqs) => seqCommand(seqs, opts.gapEmpty, opts.json);
|
|
3520
|
+
return pageOut(rows, {
|
|
3521
|
+
json: opts.json,
|
|
3522
|
+
newest: opts.newest,
|
|
3523
|
+
whole: opts.whole === true,
|
|
3524
|
+
text: (r, all) => logRecord(r, all),
|
|
3525
|
+
item: (r, all) => logItem(r, all),
|
|
3526
|
+
// The ops left out, by seq: a latest-N read omits the older ones, a cursor read the later.
|
|
3527
|
+
resume: (shown) => {
|
|
3528
|
+
if (opts.cursor === true) {
|
|
3529
|
+
const at = shown[shown.length - 1]?.seq ?? (rows[0]?.seq ?? 1) - 1;
|
|
3530
|
+
return `mu log --after ${at}${opts.json ? " --json" : ""}`;
|
|
3531
|
+
}
|
|
3532
|
+
const on = new Set(shown.map((r) => r.seq));
|
|
3533
|
+
return say2(rows.filter((r) => !on.has(r.seq)).map((r) => r.seq));
|
|
3534
|
+
},
|
|
3535
|
+
retrieve: (clipped) => say2(clipped.map((r) => r.seq))
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
function logOut(row) {
|
|
3539
|
+
const { item } = logItem(row);
|
|
3540
|
+
return item.cut === void 0 ? item : { ...item, retrieve: `mu log --seq ${row.seq} --json` };
|
|
3541
|
+
}
|
|
3542
|
+
function logReceipt(row) {
|
|
3543
|
+
const one = logRecord(row);
|
|
3544
|
+
return one.cut === 0 ? [one.line] : [one.line, `cut=${one.cut} next=mu log --seq ${row.seq}`];
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
export {
|
|
3548
|
+
OpEntityNotSyncedError,
|
|
3549
|
+
OpKeyMalformedError,
|
|
3550
|
+
applyOp,
|
|
3551
|
+
projectLog,
|
|
3552
|
+
applyOps,
|
|
3553
|
+
readManifest,
|
|
3554
|
+
SyncDirForeignError,
|
|
3555
|
+
assertSyncDirV2,
|
|
3556
|
+
syncDir,
|
|
3557
|
+
localMachineId,
|
|
3558
|
+
segmentPath,
|
|
3559
|
+
flushSegment,
|
|
3560
|
+
ownSegmentForeignLine,
|
|
3561
|
+
countUnpublishedOps,
|
|
3562
|
+
segmentLineCount,
|
|
3563
|
+
verifyAgainstManifest,
|
|
3564
|
+
discoverPeers,
|
|
3565
|
+
getWatermark,
|
|
3566
|
+
setWatermark,
|
|
3567
|
+
resetWatermark,
|
|
3568
|
+
ingestSegment,
|
|
3569
|
+
applyIncomingOp,
|
|
3570
|
+
syncPass,
|
|
3571
|
+
TaskAutoRefusedError,
|
|
3572
|
+
TaskAutoArgError,
|
|
3573
|
+
getAutoConfig,
|
|
3574
|
+
autoPermitted,
|
|
3575
|
+
authorisedReady,
|
|
3576
|
+
openBlockersClosure,
|
|
3577
|
+
authorizeTasks,
|
|
3578
|
+
TaskBriefMissingError,
|
|
3579
|
+
pinnedNotes,
|
|
3580
|
+
buildTaskBrief,
|
|
3581
|
+
IN_FLIGHT_STATES,
|
|
3582
|
+
readAttempt,
|
|
3583
|
+
deliveredOf,
|
|
3584
|
+
pendingAttempts,
|
|
3585
|
+
attemptName,
|
|
3586
|
+
planAttempt,
|
|
3587
|
+
reserveAttempt,
|
|
3588
|
+
endAttempt,
|
|
3589
|
+
reserveRetry,
|
|
3590
|
+
FRONTIER_MAX_DEPTH,
|
|
3591
|
+
FRONTIER_MAX_NODES,
|
|
3592
|
+
FRONTIER_CLOSED_PER_NODE,
|
|
3593
|
+
loadOpenTaskFrontier,
|
|
3594
|
+
renderFrontierLines,
|
|
3595
|
+
workstreamIdOf,
|
|
3596
|
+
agentToOutput,
|
|
3597
|
+
closePreviewToOutput,
|
|
3598
|
+
liveAgentToOutput,
|
|
3599
|
+
parseOpKey,
|
|
3600
|
+
KNOWN_INTENTS,
|
|
3601
|
+
KNOWN_VERBS,
|
|
3602
|
+
renderOp,
|
|
3603
|
+
renderOpLine,
|
|
3604
|
+
opSubject,
|
|
3605
|
+
safeJsonLine,
|
|
3606
|
+
encodeEnvelope,
|
|
3607
|
+
okEnvelope,
|
|
3608
|
+
RESULT_BYTES,
|
|
3609
|
+
escapeRecordText,
|
|
3610
|
+
jsonDocument,
|
|
3611
|
+
linesBytes,
|
|
3612
|
+
clipEscaped,
|
|
3613
|
+
taskToOutput,
|
|
3614
|
+
SHOW_TITLE_CHARS,
|
|
3615
|
+
footerLine,
|
|
3616
|
+
taskItemCut,
|
|
3617
|
+
taskRecordCut,
|
|
3618
|
+
pageRecords,
|
|
3619
|
+
taskPage,
|
|
3620
|
+
clipPlain,
|
|
3621
|
+
shrinkItem,
|
|
3622
|
+
pageOut,
|
|
3623
|
+
clippedRecord,
|
|
3624
|
+
logItem,
|
|
3625
|
+
logPage,
|
|
3626
|
+
logOut,
|
|
3627
|
+
logReceipt,
|
|
3628
|
+
HumanNeededInvalidError,
|
|
3629
|
+
recordHumanNeeded,
|
|
3630
|
+
openHumanNeeds,
|
|
3631
|
+
UnguardedMoveError,
|
|
3632
|
+
NEXT_BYTES,
|
|
3633
|
+
resumeArgv,
|
|
3634
|
+
renderArgv,
|
|
3635
|
+
renderPlan,
|
|
3636
|
+
capPlan,
|
|
3637
|
+
COURSE_CHECK_CLOSED,
|
|
3638
|
+
COURSE_CHECK_AGE_MS,
|
|
3639
|
+
courseCheckHint,
|
|
3640
|
+
computeNext,
|
|
3641
|
+
computeNotices,
|
|
3642
|
+
noticesField,
|
|
3643
|
+
SyncPeerNotFoundError,
|
|
3644
|
+
SyncPeerRefAmbiguousError,
|
|
3645
|
+
SyncSourceNotFoundError,
|
|
3646
|
+
SyncSourceTooNewError,
|
|
3647
|
+
PEER_STALE_MS,
|
|
3648
|
+
PEER_SHORT_LEN,
|
|
3649
|
+
peerStatuses,
|
|
3650
|
+
resolvePeerRef,
|
|
3651
|
+
repairPeer,
|
|
3652
|
+
syncEnabled,
|
|
3653
|
+
foreignVersionMessage,
|
|
3654
|
+
ambientIngest,
|
|
3655
|
+
ambientFlush,
|
|
3656
|
+
ambientSyncPass,
|
|
3657
|
+
ingestFromDb,
|
|
3658
|
+
currentCallers
|
|
3659
|
+
};
|
|
3660
|
+
//# sourceMappingURL=chunk-SS7NQSSZ.js.map
|