@kky42/pievo-server 3.0.0 → 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/.output/nitro.json +1 -1
- package/.output/public/assets/MarkdownView-BuFKr9Jg.js +66 -0
- package/.output/public/assets/app-x1CeeY7N.css +2 -0
- package/.output/public/assets/{index-Di_wpAtL.js → index-D0FIKiiW.js} +3 -3
- package/.output/public/assets/loops._loopId-DTkIz4C0.js +2 -0
- package/.output/public/assets/loops._loopId_.runs._runId-CsV2Ih4Z.js +3 -0
- package/.output/public/assets/{preload-helper-hRTYB_ju.js → preload-helper-BSxGu4d0.js} +1 -1
- package/.output/public/assets/routes-m4Z90DTR.js +1 -0
- package/.output/server/_ssr/{MachinesModal-CmVMec6B.mjs → MachinesModal-CLBZxZkq.mjs} +4 -4
- package/.output/server/_ssr/MarkdownView-D1J7ImYF.mjs +70 -0
- package/.output/server/_ssr/{artifactFiles-Di1xDKvX.mjs → artifactFiles-B0NPRDOR.mjs} +14 -19
- package/.output/server/_ssr/{artifactKind-DqpmtxDU.mjs → artifactKind-a2oxT2wQ.mjs} +4 -4
- package/.output/server/_ssr/{boot-CYRi-wez.mjs → boot-DmpKtb0A.mjs} +810 -842
- package/.output/server/_ssr/{lifecycleUi-BsbUsi5K.mjs → lifecycleUi-DEjM8Etm.mjs} +5 -5
- package/.output/server/_ssr/{loopApi-Bez1rHV0.mjs → loopApi-DK1lQPrx.mjs} +13 -13
- package/.output/server/_ssr/{loopProjection-esCjyZJg.mjs → loopProjection-DdFivf7-.mjs} +4 -3
- package/.output/server/_ssr/{loops._loopId-KUezNY69.mjs → loops._loopId-DVCvGSEc.mjs} +34 -99
- package/.output/server/_ssr/{loops._loopId_.runs._runId-DOkUEesG.mjs → loops._loopId_.runs._runId-BXbTfLlC.mjs} +20 -9
- package/.output/server/_ssr/{loops._loopId_.runs._runId-BFP9oMrp.mjs → loops._loopId_.runs._runId-Dr6E-FA5.mjs} +3 -3
- package/.output/server/_ssr/{machineFns-CniGcY8s.mjs → machineFns-DLRHgDdS.mjs} +2 -2
- package/.output/server/_ssr/{router-BFIH2UAE.mjs → router-BhHsTGqu.mjs} +95 -102
- package/.output/server/_ssr/{routes-BSVBANI3.mjs → routes-CCallyCT.mjs} +2 -2
- package/.output/server/_ssr/{routes-DnE7XDe_.mjs → routes-CL2Kef-Q.mjs} +4 -4
- package/.output/server/_ssr/ssr.mjs +22 -22
- package/.output/server/{_tanstack-start-manifest_v-DnwAupbZ.mjs → _tanstack-start-manifest_v-BPiJ4yWT.mjs} +8 -9
- package/.output/server/drizzle/0001_loop_files.sql +9 -0
- package/.output/server/drizzle/meta/_journal.json +8 -1
- package/.output/server/index.mjs +49 -42
- package/drizzle/0001_loop_files.sql +9 -0
- package/drizzle/meta/_journal.json +8 -1
- package/package.json +1 -1
- package/.output/public/assets/app-DXLuQNb9.css +0 -2
- package/.output/public/assets/loops._loopId-X3ClNPMY.js +0 -67
- package/.output/public/assets/loops._loopId_.runs._runId-ByU2BSyF.js +0 -3
- package/.output/public/assets/routes-K8D5uBiV.js +0 -1
|
@@ -11,114 +11,117 @@ import { fileURLToPath } from "node:url";
|
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import os from "node:os";
|
|
13
13
|
import fs$1 from "node:fs/promises";
|
|
14
|
-
//#region node_modules/.nitro/vite/services/ssr/assets/loopTags-
|
|
15
|
-
var
|
|
16
|
-
$: () =>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
14
|
+
//#region node_modules/.nitro/vite/services/ssr/assets/loopTags-CnVLi70h.js
|
|
15
|
+
var loopTags_CnVLi70h_exports = /* @__PURE__ */ __exportAll$1({
|
|
16
|
+
$: () => loopIdsWithSnapshots,
|
|
17
|
+
$t: () => BLOB_GC_GRACE_MS,
|
|
18
|
+
A: () => enqueueRun,
|
|
19
|
+
At: () => updateMachine,
|
|
20
|
+
B: () => getRun,
|
|
21
|
+
Bt: () => loopFileNames,
|
|
22
|
+
C: () => buildLoopManifest,
|
|
23
|
+
Ct: () => setMachineOnline,
|
|
24
|
+
D: () => createLoop,
|
|
25
|
+
Dt: () => store_exports,
|
|
26
|
+
E: () => countRuns,
|
|
27
|
+
Et: () => stopLoop,
|
|
28
|
+
F: () => getArtifactFile,
|
|
29
|
+
Ft: () => MEMORY_CHAR_CAP,
|
|
30
|
+
G: () => initializeCronCadence,
|
|
31
|
+
Gt: () => closeClient,
|
|
32
|
+
H: () => getTerminalReportIncidents,
|
|
33
|
+
Ht: () => sha256Buf,
|
|
34
|
+
I: () => getExactTerminalReportIncident,
|
|
35
|
+
It: () => MEMORY_FILE,
|
|
36
|
+
J: () => listLoops,
|
|
37
|
+
Jt: () => runs,
|
|
38
|
+
K: () => lastRun,
|
|
39
|
+
Kt: () => db,
|
|
40
|
+
L: () => getLoop,
|
|
41
|
+
Lt: () => fileLimitLabel,
|
|
42
|
+
M: () => finalizeRunningRun,
|
|
43
|
+
Mt: () => EMPTY_SHA256,
|
|
44
|
+
N: () => forceDeleteLoop,
|
|
45
|
+
Nt: () => LOOP_FILE_BYTE_CAP,
|
|
46
|
+
O: () => createMachine,
|
|
47
|
+
Ot: () => tryDeleteLoop,
|
|
48
|
+
P: () => forceDeleteMachine,
|
|
49
|
+
Pt: () => LoopFiles,
|
|
50
|
+
Q: () => liveBlobRefs,
|
|
51
|
+
Qt: () => logger_exports,
|
|
52
|
+
R: () => getMachine,
|
|
53
|
+
Rt: () => isValidHash,
|
|
54
|
+
S: () => blobSizes,
|
|
55
|
+
St: () => runningRunsForMachine,
|
|
56
|
+
T: () => countRunSnapshots,
|
|
57
|
+
Tt: () => startLoop,
|
|
58
|
+
U: () => hasPendingRun,
|
|
59
|
+
Ut: () => validArtifactName,
|
|
60
|
+
V: () => getRunSnapshot,
|
|
61
|
+
Vt: () => loopFiles_exports,
|
|
62
|
+
W: () => hasRunningRun,
|
|
63
|
+
Wt: () => createBlobStore,
|
|
64
|
+
X: () => listRuns,
|
|
65
|
+
Xt: () => coerceCodingAgent,
|
|
66
|
+
Y: () => listMachines,
|
|
67
|
+
Yt: () => CODING_AGENTS,
|
|
68
|
+
Z: () => listRunsBefore,
|
|
69
|
+
Zt: () => logger,
|
|
70
|
+
_: () => acknowledgeRetiredReport,
|
|
71
|
+
_t: () => releaseAbsentReconciliations,
|
|
71
72
|
a: () => TERMINAL_GRACE_MS,
|
|
72
|
-
at: () =>
|
|
73
|
-
b: () =>
|
|
74
|
-
bt: () =>
|
|
73
|
+
at: () => previousRunFileNotices,
|
|
74
|
+
b: () => artifactFileReferencesHash,
|
|
75
|
+
bt: () => requestRunCancel,
|
|
75
76
|
c: () => machineIdFromToken,
|
|
76
|
-
ct: () =>
|
|
77
|
+
ct: () => putRunSnapshot,
|
|
77
78
|
d: () => readNewIdempotency,
|
|
78
|
-
dt: () =>
|
|
79
|
-
|
|
79
|
+
dt: () => reclaimRun,
|
|
80
|
+
en: () => GC_INTERVAL_MS,
|
|
81
|
+
et: () => loopsForMachine,
|
|
80
82
|
f: () => recordNewIdempotency,
|
|
81
|
-
ft: () =>
|
|
83
|
+
ft: () => reconcileReclaimedRun,
|
|
82
84
|
g: () => tokens_exports,
|
|
83
|
-
gt: () =>
|
|
85
|
+
gt: () => rejectTerminalReport,
|
|
84
86
|
h: () => sha256,
|
|
85
|
-
ht: () =>
|
|
87
|
+
ht: () => refreshRunHeartbeats,
|
|
86
88
|
i: () => machinePresence,
|
|
87
|
-
it: () =>
|
|
88
|
-
j: () =>
|
|
89
|
-
jt: () =>
|
|
90
|
-
k: () =>
|
|
91
|
-
kt: () =>
|
|
89
|
+
it: () => prevRunSnapshot,
|
|
90
|
+
j: () => expirePendingRun,
|
|
91
|
+
jt: () => writeLoopFile,
|
|
92
|
+
k: () => deleteBlob,
|
|
93
|
+
kt: () => updateLoop,
|
|
92
94
|
l: () => mintDeviceToken,
|
|
93
|
-
lt: () =>
|
|
95
|
+
lt: () => putTerminalReportIncidentIfAbsent,
|
|
94
96
|
m: () => retireLease,
|
|
95
|
-
mt: () =>
|
|
97
|
+
mt: () => recordBlob,
|
|
96
98
|
n: () => isOutdated,
|
|
97
|
-
nt: () =>
|
|
99
|
+
nt: () => pauseLoop,
|
|
98
100
|
o: () => connectKeyValid,
|
|
99
|
-
ot: () =>
|
|
101
|
+
ot: () => pruneRunSnapshots,
|
|
100
102
|
p: () => resolveLease,
|
|
101
|
-
pt: () =>
|
|
102
|
-
q: () =>
|
|
103
|
-
qt: () =>
|
|
103
|
+
pt: () => reconciliationStatesForRuns,
|
|
104
|
+
q: () => listArtifacts,
|
|
105
|
+
qt: () => runMigrations,
|
|
104
106
|
r: () => isValidSemver,
|
|
105
|
-
rt: () =>
|
|
107
|
+
rt: () => pauseLoopState,
|
|
106
108
|
s: () => isDeviceTokenShape,
|
|
107
|
-
st: () =>
|
|
109
|
+
st: () => putReportReceiptIfAbsent,
|
|
108
110
|
t: () => validateLoopTags,
|
|
109
|
-
|
|
111
|
+
tn: () => __exportAll,
|
|
112
|
+
tt: () => openRuns,
|
|
110
113
|
u: () => pruneExpiredLeases,
|
|
111
|
-
ut: () =>
|
|
112
|
-
v: () =>
|
|
113
|
-
vt: () =>
|
|
114
|
-
w: () =>
|
|
115
|
-
wt: () =>
|
|
116
|
-
x: () =>
|
|
117
|
-
xt: () =>
|
|
118
|
-
y: () =>
|
|
119
|
-
yt: () =>
|
|
120
|
-
z: () =>
|
|
121
|
-
zt: () =>
|
|
114
|
+
ut: () => recentLoopUsage,
|
|
115
|
+
v: () => acknowledgeRetiredTerminalIncident,
|
|
116
|
+
vt: () => repairTerminalRunLeases,
|
|
117
|
+
w: () => claimReadyRunForMachine,
|
|
118
|
+
wt: () => snapshotBlobRefs,
|
|
119
|
+
x: () => blobHashesOlderThan,
|
|
120
|
+
xt: () => runningRunForLoop,
|
|
121
|
+
y: () => advanceDueSchedules,
|
|
122
|
+
yt: () => requestDeleteLoop,
|
|
123
|
+
z: () => getReportReceipt,
|
|
124
|
+
zt: () => loopFileIssue
|
|
122
125
|
});
|
|
123
126
|
var import_pino = /* @__PURE__ */ __toESM(require_pino());
|
|
124
127
|
var import_pino_pretty = /* @__PURE__ */ __toESM(require_pino_pretty());
|
|
@@ -133,7 +136,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
133
136
|
return target;
|
|
134
137
|
};
|
|
135
138
|
/**
|
|
136
|
-
* Server data directory: the embedded database (`pgdata/`),
|
|
139
|
+
* Server data directory: the embedded database (`pgdata/`), loop-file bytes
|
|
137
140
|
* (`blobs/`), and the launcher's pid/lock/log files. Kept apart from the
|
|
138
141
|
* daemon's `~/.pievo` so a device running both never mixes their state.
|
|
139
142
|
* Override with `PIEVO_DATA_DIR` (absolute; `~` is not expanded).
|
|
@@ -228,7 +231,8 @@ var loops = pgTable("loops", {
|
|
|
228
231
|
statusKeep: text("status_keep").notNull(),
|
|
229
232
|
statusNoChange: text("status_no_change").notNull(),
|
|
230
233
|
statusBlock: text("status_block").notNull(),
|
|
231
|
-
/**
|
|
234
|
+
/** Configured artifact file names (`.md`/`.html`), in addition to the
|
|
235
|
+
* always-present MEMORY.md. See `gateway/loopFiles.ts`. */
|
|
232
236
|
artifacts: jsonb("artifacts").$type().notNull().default([]),
|
|
233
237
|
cron: text("cron").notNull(),
|
|
234
238
|
scheduleMode: text("schedule_mode", { enum: ["cron", "continuous"] }).notNull(),
|
|
@@ -308,7 +312,11 @@ var runs = pgTable("runs", {
|
|
|
308
312
|
mode: "string"
|
|
309
313
|
}),
|
|
310
314
|
heartbeatAt: text("heartbeat_at"),
|
|
311
|
-
reportIncident: jsonb("report_incident").$type()
|
|
315
|
+
reportIncident: jsonb("report_incident").$type(),
|
|
316
|
+
/** Loop-file versions this run produced but that were not kept (over limit,
|
|
317
|
+
* owner edited during the run, upload lost). Shown on the run and repeated in
|
|
318
|
+
* the next run's prompt so the agent can correct course. */
|
|
319
|
+
fileNotices: jsonb("file_notices").$type()
|
|
312
320
|
}, (t) => [
|
|
313
321
|
index("runs_loop_idx").on(t.loopId),
|
|
314
322
|
index("runs_phase_idx").on(t.phase),
|
|
@@ -387,21 +395,23 @@ var blobs = pgTable("blobs", {
|
|
|
387
395
|
createdAt: text("created_at").notNull()
|
|
388
396
|
});
|
|
389
397
|
/**
|
|
390
|
-
* The current
|
|
391
|
-
*
|
|
392
|
-
* (
|
|
393
|
-
*
|
|
398
|
+
* The current file set of each loop — one row per live or tombstoned name.
|
|
399
|
+
* `hash` → `blobs.hash`; null for a tombstoned (deleted or unconfigured) file.
|
|
400
|
+
* The unique (loopId, path) index is the upsert key owner writes and run
|
|
401
|
+
* finalization share.
|
|
394
402
|
*/
|
|
395
403
|
var artifactFiles = pgTable("artifact_files", {
|
|
396
404
|
id: text("id").primaryKey(),
|
|
397
405
|
loopId: text("loop_id").notNull(),
|
|
398
|
-
/**
|
|
406
|
+
/** Loop file name: MEMORY.md or a configured artifact name. */
|
|
399
407
|
path: text("path").notNull(),
|
|
400
408
|
hash: text("hash"),
|
|
401
409
|
size: integer("size"),
|
|
402
|
-
/**
|
|
410
|
+
/** Legacy snapshot/diff metadata. Loop-file ingress rejects NUL, so new rows
|
|
411
|
+
* always store false. */
|
|
403
412
|
binary: boolean("binary").notNull().default(false),
|
|
404
|
-
/**
|
|
413
|
+
/** Legacy snapshot/diff metadata. Loop-file ingress rejects over-limit versions,
|
|
414
|
+
* so new rows always store false. */
|
|
405
415
|
oversize: boolean("oversize").notNull().default(false),
|
|
406
416
|
deleted: boolean("deleted").notNull().default(false),
|
|
407
417
|
updatedAt: text("updated_at").notNull()
|
|
@@ -411,7 +421,7 @@ var artifactFiles = pgTable("artifact_files", {
|
|
|
411
421
|
index("artifact_files_hash_idx").on(t.hash)
|
|
412
422
|
]);
|
|
413
423
|
/**
|
|
414
|
-
* The loop's full
|
|
424
|
+
* The loop's full loop-file manifest captured at each run's finalize — the input
|
|
415
425
|
* to the per-run diff. Written cheaply on report (no diff computed on write);
|
|
416
426
|
* `getRunDiff` lazily compares run N's snapshot with the preceding snapshot.
|
|
417
427
|
* One row per run (runId primary key).
|
|
@@ -496,6 +506,222 @@ async function closeClient() {
|
|
|
496
506
|
} catch {}
|
|
497
507
|
}
|
|
498
508
|
/**
|
|
509
|
+
* Content-addressed blob byte store for loop files.
|
|
510
|
+
*
|
|
511
|
+
* Bytes are keyed by their sha256 hash (content-addressed ⇒ deduped across every
|
|
512
|
+
* loop/run). The store ONLY writes/reads bytes — it never executes or interprets
|
|
513
|
+
* them — preserving the server's zero-exec invariant.
|
|
514
|
+
*
|
|
515
|
+
* `LocalBlobStore` is the durable store under `<dataDir>/blobs`;
|
|
516
|
+
* `MemoryBlobStore` is the injected test fake.
|
|
517
|
+
*/
|
|
518
|
+
var log$3 = logger.child({ mod: "blobstore" });
|
|
519
|
+
var SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
520
|
+
/** Durable filesystem adapter. Hash sharding bounds entries per directory; atomic
|
|
521
|
+
* temp-file renames ensure readers see either complete bytes or no object. */
|
|
522
|
+
var LocalBlobStore = class {
|
|
523
|
+
root;
|
|
524
|
+
initialized;
|
|
525
|
+
constructor(root = path.join(dataDir(), "blobs")) {
|
|
526
|
+
this.root = root;
|
|
527
|
+
}
|
|
528
|
+
file(hash) {
|
|
529
|
+
if (!SHA256_HEX.test(hash)) throw new Error("invalid sha256 blob key");
|
|
530
|
+
return path.join(this.root, hash.slice(0, 2), hash);
|
|
531
|
+
}
|
|
532
|
+
async has(hash) {
|
|
533
|
+
const target = this.file(hash);
|
|
534
|
+
await this.ensureInitialized();
|
|
535
|
+
try {
|
|
536
|
+
return (await fs$1.stat(target)).isFile();
|
|
537
|
+
} catch (err) {
|
|
538
|
+
if (err.code === "ENOENT") return false;
|
|
539
|
+
throw err;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async syncDir(dir) {
|
|
543
|
+
let handle;
|
|
544
|
+
try {
|
|
545
|
+
handle = await fs$1.open(dir, "r");
|
|
546
|
+
await handle.sync();
|
|
547
|
+
} catch (err) {
|
|
548
|
+
const code = err.code;
|
|
549
|
+
if (![
|
|
550
|
+
"EINVAL",
|
|
551
|
+
"ENOTSUP",
|
|
552
|
+
"EBADF",
|
|
553
|
+
"EPERM",
|
|
554
|
+
"ENOENT"
|
|
555
|
+
].includes(code)) throw err;
|
|
556
|
+
} finally {
|
|
557
|
+
await handle?.close().catch(() => {});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
ensureInitialized() {
|
|
561
|
+
return this.initialized ??= (async () => {
|
|
562
|
+
await fs$1.mkdir(this.root, { recursive: true });
|
|
563
|
+
await this.syncDir(path.dirname(path.dirname(this.root)));
|
|
564
|
+
await this.syncDir(path.dirname(this.root));
|
|
565
|
+
await this.syncDir(this.root);
|
|
566
|
+
for (const shard of await fs$1.readdir(this.root, { withFileTypes: true })) {
|
|
567
|
+
if (!shard.isDirectory() || !/^[a-f0-9]{2}$/.test(shard.name)) continue;
|
|
568
|
+
const dir = path.join(this.root, shard.name);
|
|
569
|
+
let removed = false;
|
|
570
|
+
for (const name of await fs$1.readdir(dir)) {
|
|
571
|
+
if (!/^\.[a-f0-9]{64}\.\d+\.[0-9a-f-]+\.tmp$/.test(name)) continue;
|
|
572
|
+
await fs$1.rm(path.join(dir, name), { force: true });
|
|
573
|
+
removed = true;
|
|
574
|
+
}
|
|
575
|
+
if (removed) await this.syncDir(dir);
|
|
576
|
+
}
|
|
577
|
+
})();
|
|
578
|
+
}
|
|
579
|
+
async put(hash, bytes) {
|
|
580
|
+
const target = this.file(hash);
|
|
581
|
+
await this.ensureInitialized();
|
|
582
|
+
const dir = path.dirname(target);
|
|
583
|
+
await fs$1.mkdir(dir, { recursive: true });
|
|
584
|
+
await this.syncDir(this.root);
|
|
585
|
+
const temp = path.join(dir, `.${hash}.${process.pid}.${randomUUID()}.tmp`);
|
|
586
|
+
let handle;
|
|
587
|
+
try {
|
|
588
|
+
handle = await fs$1.open(temp, "wx", 384);
|
|
589
|
+
await handle.writeFile(bytes);
|
|
590
|
+
await handle.sync();
|
|
591
|
+
await handle.close();
|
|
592
|
+
handle = void 0;
|
|
593
|
+
try {
|
|
594
|
+
await fs$1.rename(temp, target);
|
|
595
|
+
await this.syncDir(dir);
|
|
596
|
+
} catch (err) {
|
|
597
|
+
if ([
|
|
598
|
+
"EEXIST",
|
|
599
|
+
"EACCES",
|
|
600
|
+
"EPERM"
|
|
601
|
+
].includes(err.code ?? "") && await this.has(hash)) {
|
|
602
|
+
await fs$1.rm(temp, { force: true });
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
throw err;
|
|
606
|
+
}
|
|
607
|
+
} catch (err) {
|
|
608
|
+
await handle?.close().catch(() => {});
|
|
609
|
+
await fs$1.rm(temp, { force: true }).catch(() => {});
|
|
610
|
+
throw err;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
async get(hash) {
|
|
614
|
+
const target = this.file(hash);
|
|
615
|
+
await this.ensureInitialized();
|
|
616
|
+
try {
|
|
617
|
+
return await fs$1.readFile(target);
|
|
618
|
+
} catch (err) {
|
|
619
|
+
if (err.code === "ENOENT") return null;
|
|
620
|
+
throw err;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
async delete(hash) {
|
|
624
|
+
const target = this.file(hash);
|
|
625
|
+
await this.ensureInitialized();
|
|
626
|
+
await fs$1.rm(target, { force: true });
|
|
627
|
+
await this.syncDir(path.dirname(target));
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
/** The one local blob store shared by loop-file writes/reads and GC. */
|
|
631
|
+
function createBlobStore() {
|
|
632
|
+
const local = new LocalBlobStore();
|
|
633
|
+
log$3.info({ root: local.root }, "blob store: local filesystem");
|
|
634
|
+
return local;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Loop files: a loop's server-owned memory and display artifacts.
|
|
638
|
+
*
|
|
639
|
+
* Every loop has the required `MEMORY.md` plus up to MAX_ARTIFACTS configured
|
|
640
|
+
* `.md`/`.html` artifact names. The server is authoritative: bytes live in the
|
|
641
|
+
* content-addressed BlobStore and the current version of each name is an
|
|
642
|
+
* `artifact_files` row. For each run the daemon downloads the current files into
|
|
643
|
+
* a private run directory, and the terminal report carries the changed versions
|
|
644
|
+
* back with the hash each edit started from, so an owner edit made during the run
|
|
645
|
+
* wins over the agent's (base-hash check under the loop lock).
|
|
646
|
+
*
|
|
647
|
+
* Limits are part of the agent-facing prompt contract and are mirrored in the
|
|
648
|
+
* daemon (`packages/daemon/src/loop-files.ts`); change both together. An over-limit
|
|
649
|
+
* version is rejected and the previous version kept, never truncated, because a cut
|
|
650
|
+
* HTML file renders broken and a cut memory file silently loses its newest notes.
|
|
651
|
+
*/
|
|
652
|
+
var loopFiles_exports = /* @__PURE__ */ __exportAll({
|
|
653
|
+
ARTIFACT_BYTE_CAP: () => ARTIFACT_BYTE_CAP,
|
|
654
|
+
EMPTY_SHA256: () => EMPTY_SHA256,
|
|
655
|
+
LOOP_FILE_BYTE_CAP: () => LOOP_FILE_BYTE_CAP,
|
|
656
|
+
LoopFiles: () => LoopFiles,
|
|
657
|
+
MAX_ARTIFACTS: () => 4,
|
|
658
|
+
MEMORY_CHAR_CAP: () => MEMORY_CHAR_CAP,
|
|
659
|
+
MEMORY_FILE: () => MEMORY_FILE,
|
|
660
|
+
fileLimitLabel: () => fileLimitLabel,
|
|
661
|
+
isValidHash: () => isValidHash,
|
|
662
|
+
loopFileIssue: () => loopFileIssue,
|
|
663
|
+
loopFileNames: () => loopFileNames,
|
|
664
|
+
sha256Buf: () => sha256Buf,
|
|
665
|
+
validArtifactName: () => validArtifactName
|
|
666
|
+
});
|
|
667
|
+
var MEMORY_FILE = "MEMORY.md";
|
|
668
|
+
/** Memory is re-read by every run, so it is bounded in characters (Unicode code
|
|
669
|
+
* points), roughly 2k tokens of prompt context per run. */
|
|
670
|
+
var MEMORY_CHAR_CAP = 8e3;
|
|
671
|
+
/** HTML reports are bulky; 2 MB keeps a run's upload and the dashboard iframe cheap. */
|
|
672
|
+
var ARTIFACT_BYTE_CAP = 2 * 1024 * 1024;
|
|
673
|
+
/** sha256 of zero bytes: the version of a loop file that has never been written. */
|
|
674
|
+
var EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
675
|
+
var ARTIFACT_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}\.(md|html)$/;
|
|
676
|
+
function sha256Buf(bytes) {
|
|
677
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
678
|
+
}
|
|
679
|
+
function isValidHash(hash) {
|
|
680
|
+
return typeof hash === "string" && /^[0-9a-f]{64}$/.test(hash);
|
|
681
|
+
}
|
|
682
|
+
/** A plain file name (no directories) ending in .md or .html, other than MEMORY.md. */
|
|
683
|
+
function validArtifactName(name) {
|
|
684
|
+
return typeof name === "string" && ARTIFACT_NAME_RE.test(name) && name.toLowerCase() !== "MEMORY.md".toLowerCase();
|
|
685
|
+
}
|
|
686
|
+
function loopFileNames(loop) {
|
|
687
|
+
return [MEMORY_FILE, ...loop.artifacts];
|
|
688
|
+
}
|
|
689
|
+
function fileLimitLabel(name) {
|
|
690
|
+
return name === "MEMORY.md" ? `${MEMORY_CHAR_CAP} characters` : "2 MB";
|
|
691
|
+
}
|
|
692
|
+
/** Why `bytes` cannot become the next version of `name`, or undefined if it can. */
|
|
693
|
+
function loopFileIssue(name, bytes) {
|
|
694
|
+
if (bytes.includes(0)) return `${name}: binary content is not allowed`;
|
|
695
|
+
if (name === "MEMORY.md") {
|
|
696
|
+
const chars = Array.from(bytes.toString("utf8")).length;
|
|
697
|
+
if (chars > 8e3) return `${name}: ${chars} characters exceeds the ${MEMORY_CHAR_CAP}-character limit`;
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (bytes.length > 2097152) return `${name}: ${(bytes.length / 1024 / 1024).toFixed(1)} MB exceeds the 2 MB limit`;
|
|
701
|
+
}
|
|
702
|
+
/** The largest byte size any loop file can have (a code point is at most 4 bytes). */
|
|
703
|
+
var LOOP_FILE_BYTE_CAP = Math.max(ARTIFACT_BYTE_CAP, MEMORY_CHAR_CAP * 4);
|
|
704
|
+
var LoopFiles = class {
|
|
705
|
+
blobStore;
|
|
706
|
+
constructor(blobStore = createBlobStore()) {
|
|
707
|
+
this.blobStore = blobStore;
|
|
708
|
+
}
|
|
709
|
+
readBlob(hash) {
|
|
710
|
+
if (!isValidHash(hash)) return Promise.resolve(null);
|
|
711
|
+
if (hash === "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") return Promise.resolve(Buffer.alloc(0));
|
|
712
|
+
return this.blobStore.get(hash);
|
|
713
|
+
}
|
|
714
|
+
/** Persist bytes under their own hash. Idempotent; callers record the metadata row. */
|
|
715
|
+
async putBlob(bytes) {
|
|
716
|
+
const hash = sha256Buf(bytes);
|
|
717
|
+
if (hash !== "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" && !await this.blobStore.has(hash)) await this.blobStore.put(hash, bytes);
|
|
718
|
+
return hash;
|
|
719
|
+
}
|
|
720
|
+
async hasBlob(hash) {
|
|
721
|
+
return hash === "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" || this.blobStore.has(hash);
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
/**
|
|
499
725
|
* Data-access layer over Drizzle. Loop-locked state transitions live here; every
|
|
500
726
|
* DB-touching function is async, while the pure helpers stay sync.
|
|
501
727
|
*/
|
|
@@ -505,7 +731,6 @@ var store_exports = /* @__PURE__ */ __exportAll({
|
|
|
505
731
|
acknowledgeRetiredTerminalIncident: () => acknowledgeRetiredTerminalIncident,
|
|
506
732
|
advanceDueSchedules: () => advanceDueSchedules,
|
|
507
733
|
artifactFileReferencesHash: () => artifactFileReferencesHash,
|
|
508
|
-
blobExists: () => blobExists,
|
|
509
734
|
blobHashesOlderThan: () => blobHashesOlderThan,
|
|
510
735
|
blobSizes: () => blobSizes,
|
|
511
736
|
buildLoopManifest: () => buildLoopManifest,
|
|
@@ -540,24 +765,22 @@ var store_exports = /* @__PURE__ */ __exportAll({
|
|
|
540
765
|
liveBlobRefs: () => liveBlobRefs,
|
|
541
766
|
loopIdsWithSnapshots: () => loopIdsWithSnapshots,
|
|
542
767
|
loopsForMachine: () => loopsForMachine,
|
|
543
|
-
machineReferencesBlob: () => machineReferencesBlob,
|
|
544
768
|
newLoopId: () => newLoopId,
|
|
545
769
|
nextCronAt: () => nextCronAt,
|
|
546
770
|
openRuns: () => openRuns,
|
|
547
771
|
pauseLoop: () => pauseLoop,
|
|
548
772
|
pauseLoopState: () => pauseLoopState,
|
|
549
773
|
prevRunSnapshot: () => prevRunSnapshot,
|
|
774
|
+
previousRunFileNotices: () => previousRunFileNotices,
|
|
550
775
|
pruneRunSnapshots: () => pruneRunSnapshots,
|
|
551
776
|
putReportReceiptIfAbsent: () => putReportReceiptIfAbsent,
|
|
552
777
|
putRunSnapshot: () => putRunSnapshot,
|
|
553
778
|
putTerminalReportIncidentIfAbsent: () => putTerminalReportIncidentIfAbsent,
|
|
554
779
|
recentLoopUsage: () => recentLoopUsage,
|
|
555
780
|
reclaimRun: () => reclaimRun,
|
|
556
|
-
reconcileConfiguredArtifacts: () => reconcileConfiguredArtifacts,
|
|
557
781
|
reconcileReclaimedRun: () => reconcileReclaimedRun,
|
|
558
782
|
reconciliationStatesForRuns: () => reconciliationStatesForRuns,
|
|
559
783
|
recordBlob: () => recordBlob,
|
|
560
|
-
recordRunReportOnce: () => recordRunReportOnce,
|
|
561
784
|
refreshRunHeartbeats: () => refreshRunHeartbeats,
|
|
562
785
|
rejectTerminalReport: () => rejectTerminalReport,
|
|
563
786
|
releaseAbsentReconciliations: () => releaseAbsentReconciliations,
|
|
@@ -574,7 +797,7 @@ var store_exports = /* @__PURE__ */ __exportAll({
|
|
|
574
797
|
tryDeleteLoop: () => tryDeleteLoop,
|
|
575
798
|
updateLoop: () => updateLoop,
|
|
576
799
|
updateMachine: () => updateMachine,
|
|
577
|
-
|
|
800
|
+
writeLoopFile: () => writeLoopFile
|
|
578
801
|
});
|
|
579
802
|
function newLoopId() {
|
|
580
803
|
return `loop-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
|
@@ -697,7 +920,7 @@ async function updateLoopTx(tx, current, patch, at) {
|
|
|
697
920
|
}).where(eq(loops.id, current.id)).returning())[0];
|
|
698
921
|
if (patch.enabled === false) await cancelPendingTx(tx, and(eq(runs.loopId, current.id), eq(runs.phase, "pending"), eq(runs.requestedBy, "system")), "canceled - loop paused before this system run was claimed", at);
|
|
699
922
|
if (patch.artifacts !== void 0) {
|
|
700
|
-
const allowed = new Set(patch.artifacts ?? []);
|
|
923
|
+
const allowed = new Set([MEMORY_FILE, ...patch.artifacts ?? []]);
|
|
701
924
|
const removed = (await tx.select({
|
|
702
925
|
id: artifactFiles.id,
|
|
703
926
|
path: artifactFiles.path
|
|
@@ -1040,26 +1263,32 @@ async function activeRunForReportTx(tx, loopId, runId, leaseTokenHash) {
|
|
|
1040
1263
|
run
|
|
1041
1264
|
} : { state: "run-not-running" };
|
|
1042
1265
|
}
|
|
1043
|
-
/**
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1046
|
-
async function
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
if (
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1266
|
+
/** Apply a run's loop-file versions under the loop lock. A file whose current
|
|
1267
|
+
* version is no longer the one the run started from was changed by the owner (or a
|
|
1268
|
+
* newer run) meanwhile; that version wins and the agent's edit becomes a notice. */
|
|
1269
|
+
async function applyLoopFileUpdatesTx(tx, loop, updates, at) {
|
|
1270
|
+
const notices = [];
|
|
1271
|
+
const configured = new Set(loopFileNames(loop));
|
|
1272
|
+
for (const update of updates) {
|
|
1273
|
+
if (!configured.has(update.name)) {
|
|
1274
|
+
notices.push(`${update.name}: no longer configured on this loop; this run's version was discarded`);
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
const row = (await tx.select().from(artifactFiles).where(and(eq(artifactFiles.loopId, loop.id), eq(artifactFiles.path, update.name))).limit(1).for("update"))[0];
|
|
1278
|
+
if ((row && !row.deleted && row.hash ? row.hash : "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") !== update.baseHash) {
|
|
1279
|
+
notices.push(`${update.name}: changed outside this run while it was running; that version was kept and this run's edit was discarded`);
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
await writeConfiguredArtifactTx(tx, {
|
|
1283
|
+
loopId: loop.id,
|
|
1284
|
+
path: update.name,
|
|
1285
|
+
hash: update.hash,
|
|
1286
|
+
size: update.size,
|
|
1287
|
+
binary: false,
|
|
1288
|
+
oversize: false
|
|
1289
|
+
}, at);
|
|
1290
|
+
}
|
|
1291
|
+
return notices;
|
|
1063
1292
|
}
|
|
1064
1293
|
async function finalizeTerminalRunTx(tx, input) {
|
|
1065
1294
|
if (input.receipt) await lockReportIdTx(tx, input.receipt.reportId);
|
|
@@ -1074,8 +1303,10 @@ async function finalizeTerminalRunTx(tx, input) {
|
|
|
1074
1303
|
if (!target) return void 0;
|
|
1075
1304
|
const runIndex = await ensureRunIndexTx(tx, target);
|
|
1076
1305
|
const at = typeof input.runPatch.ts === "string" ? input.runPatch.ts : nowIso$1();
|
|
1306
|
+
const fileNotices = [...input.runPatch.fileNotices ?? [], ...input.fileUpdates?.length ? await applyLoopFileUpdatesTx(tx, current, input.fileUpdates, at) : []];
|
|
1077
1307
|
const run = (await tx.update(runs).set({
|
|
1078
1308
|
...input.runPatch,
|
|
1309
|
+
fileNotices: fileNotices.length ? fileNotices : null,
|
|
1079
1310
|
runIndex,
|
|
1080
1311
|
updatedAt: at
|
|
1081
1312
|
}).where(and(eq(runs.id, input.runId), eq(runs.loopId, input.loopId), eq(runs.phase, input.runPhase))).returning())[0];
|
|
@@ -1207,12 +1438,13 @@ async function rejectTerminalReport(input) {
|
|
|
1207
1438
|
}
|
|
1208
1439
|
/** Atomically CAS running→terminal and apply the report's loop patch.
|
|
1209
1440
|
* The loop patch is unreachable when cancel/reclaim/another report won. */
|
|
1210
|
-
async function finalizeRunningRun(loopId, runId, runPatch, loopPatch = {}, leaseTokenHash, failureAutopauseStreak = 0, receipt) {
|
|
1441
|
+
async function finalizeRunningRun(loopId, runId, runPatch, loopPatch = {}, leaseTokenHash, failureAutopauseStreak = 0, receipt, fileUpdates) {
|
|
1211
1442
|
return db.transaction(async (tx) => {
|
|
1212
1443
|
return finalizeTerminalRunTx(tx, {
|
|
1213
1444
|
loopId,
|
|
1214
1445
|
runId,
|
|
1215
1446
|
runPhase: "running",
|
|
1447
|
+
fileUpdates,
|
|
1216
1448
|
runPatch,
|
|
1217
1449
|
loopPatch,
|
|
1218
1450
|
leaseTokenHash,
|
|
@@ -1271,12 +1503,13 @@ async function releaseAbsentReconciliations(machineId, recoverableRunIds, at = n
|
|
|
1271
1503
|
* exactly once. Keeping the lease check/delete beside the run + loop writes closes the
|
|
1272
1504
|
* error→error double-report hole where a phase CAS alone cannot distinguish the
|
|
1273
1505
|
* first real failure from a second concurrent one. */
|
|
1274
|
-
async function reconcileReclaimedRun(loopId, runId, leaseTokenHash, runPatch, loopPatch = {}, failureAutopauseStreak = 0, receipt) {
|
|
1506
|
+
async function reconcileReclaimedRun(loopId, runId, leaseTokenHash, runPatch, loopPatch = {}, failureAutopauseStreak = 0, receipt, fileUpdates) {
|
|
1275
1507
|
return db.transaction(async (tx) => {
|
|
1276
1508
|
return finalizeTerminalRunTx(tx, {
|
|
1277
1509
|
loopId,
|
|
1278
1510
|
runId,
|
|
1279
1511
|
runPhase: "error",
|
|
1512
|
+
fileUpdates,
|
|
1280
1513
|
runPatch,
|
|
1281
1514
|
loopPatch,
|
|
1282
1515
|
leaseTokenHash,
|
|
@@ -1521,9 +1754,6 @@ async function setMachineOnline(id, online) {
|
|
|
1521
1754
|
lastSeen: nowIso$1()
|
|
1522
1755
|
}).where(eq(machines.id, id));
|
|
1523
1756
|
}
|
|
1524
|
-
async function blobExists(hash) {
|
|
1525
|
-
return !!(await db.select({ hash: blobs.hash }).from(blobs).where(eq(blobs.hash, hash)))[0];
|
|
1526
|
-
}
|
|
1527
1757
|
/** Verified byte lengths recorded at blob ingress, used to budget reads before
|
|
1528
1758
|
* touching the byte store. Missing metadata is intentionally not estimated. */
|
|
1529
1759
|
async function blobSizes(hashes) {
|
|
@@ -1541,13 +1771,6 @@ async function recordBlob(hash, size) {
|
|
|
1541
1771
|
createdAt: nowIso$1()
|
|
1542
1772
|
}).onConflictDoNothing();
|
|
1543
1773
|
}
|
|
1544
|
-
/** Does any LIVE artifact_files row on a loop bound to `machineId` point at `hash`?
|
|
1545
|
-
* Gates putBlob: a device may only upload bytes the sync handshake actually asked
|
|
1546
|
-
* it for (a row a prior sync wrote for one of ITS loops), never arbitrary
|
|
1547
|
-
* self-hashed blobs — otherwise any device token is an uncapped blob write channel. */
|
|
1548
|
-
async function machineReferencesBlob(machineId, hash) {
|
|
1549
|
-
return !!(await db.select({ id: artifactFiles.id }).from(artifactFiles).innerJoin(loops, eq(artifactFiles.loopId, loops.id)).where(and(eq(loops.machineId, machineId), eq(artifactFiles.hash, hash), eq(artifactFiles.deleted, false), sql`${loops.artifacts} @> jsonb_build_array(${artifactFiles.path})`)).limit(1))[0];
|
|
1550
|
-
}
|
|
1551
1774
|
async function writeConfiguredArtifactTx(tx, input, ts) {
|
|
1552
1775
|
await tx.insert(artifactFiles).values({
|
|
1553
1776
|
id: randomUUID(),
|
|
@@ -1571,36 +1794,32 @@ async function writeConfiguredArtifactTx(tx, input, ts) {
|
|
|
1571
1794
|
}
|
|
1572
1795
|
});
|
|
1573
1796
|
}
|
|
1574
|
-
/**
|
|
1575
|
-
*
|
|
1576
|
-
async function
|
|
1797
|
+
/** Owner replacement of one loop file. Serialized with run finalization by the
|
|
1798
|
+
* loop lock, so a run that started from the previous version cannot overwrite it. */
|
|
1799
|
+
async function writeLoopFile(loopId, name, hash, size) {
|
|
1577
1800
|
return db.transaction(async (tx) => {
|
|
1578
|
-
const loop = (await tx.select(
|
|
1579
|
-
if (!loop || !loop.
|
|
1580
|
-
await writeConfiguredArtifactTx(tx,
|
|
1801
|
+
const loop = (await tx.select().from(loops).where(eq(loops.id, loopId)).for("update"))[0];
|
|
1802
|
+
if (!loop || !loopFileNames(loop).includes(name)) return false;
|
|
1803
|
+
await writeConfiguredArtifactTx(tx, {
|
|
1804
|
+
loopId,
|
|
1805
|
+
path: name,
|
|
1806
|
+
hash,
|
|
1807
|
+
size,
|
|
1808
|
+
binary: false,
|
|
1809
|
+
oversize: false
|
|
1810
|
+
}, nowIso$1());
|
|
1581
1811
|
return true;
|
|
1582
1812
|
});
|
|
1583
1813
|
}
|
|
1584
|
-
/**
|
|
1585
|
-
*
|
|
1586
|
-
*
|
|
1587
|
-
async function
|
|
1588
|
-
return db.
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
const keep = new Set(keepPaths);
|
|
1594
|
-
const stale = (await tx.select().from(artifactFiles).where(and(eq(artifactFiles.loopId, loopId), eq(artifactFiles.deleted, false)))).filter((file) => !current.has(file.path) || started.has(file.path) && !keep.has(file.path));
|
|
1595
|
-
if (!stale.length) return 0;
|
|
1596
|
-
const at = nowIso$1();
|
|
1597
|
-
await tx.update(artifactFiles).set({
|
|
1598
|
-
hash: null,
|
|
1599
|
-
deleted: true,
|
|
1600
|
-
updatedAt: at
|
|
1601
|
-
}).where(inArray(artifactFiles.id, stale.map((file) => file.id)));
|
|
1602
|
-
return stale.length;
|
|
1603
|
-
});
|
|
1814
|
+
/** File notices of the immediately previous terminal run, for the next prompt.
|
|
1815
|
+
* Canceled runs are terminal too: their notices (edits discarded) are repeated,
|
|
1816
|
+
* and an older run's notice must not masquerade as the previous run's. */
|
|
1817
|
+
async function previousRunFileNotices(loopId, beforeRunIndex) {
|
|
1818
|
+
return (await db.select({ fileNotices: runs.fileNotices }).from(runs).where(and(eq(runs.loopId, loopId), isNotNull(runs.runIndex), lt(runs.runIndex, beforeRunIndex), inArray(runs.phase, [
|
|
1819
|
+
"done",
|
|
1820
|
+
"error",
|
|
1821
|
+
"canceled"
|
|
1822
|
+
]))).orderBy(desc(runs.runIndex)).limit(1))[0]?.fileNotices ?? [];
|
|
1604
1823
|
}
|
|
1605
1824
|
async function listArtifacts(loopId) {
|
|
1606
1825
|
return db.select().from(artifactFiles).where(and(eq(artifactFiles.loopId, loopId), eq(artifactFiles.deleted, false))).orderBy(artifactFiles.path);
|
|
@@ -1618,8 +1837,8 @@ async function buildLoopManifest(loopId) {
|
|
|
1618
1837
|
};
|
|
1619
1838
|
return manifest;
|
|
1620
1839
|
}
|
|
1621
|
-
/** Write/overwrite a run's snapshot (
|
|
1622
|
-
* so a re-report of the same run just refreshes the captured
|
|
1840
|
+
/** Write/overwrite a run's snapshot (name → file metadata). Idempotent on runId
|
|
1841
|
+
* so a re-report of the same run just refreshes the captured loop-file state. */
|
|
1623
1842
|
async function putRunSnapshot(runId, loopId, manifest) {
|
|
1624
1843
|
await db.insert(runSnapshots).values({
|
|
1625
1844
|
runId,
|
|
@@ -1708,89 +1927,6 @@ async function countRunSnapshots() {
|
|
|
1708
1927
|
const r = (await db.select({ n: sql`count(*)` }).from(runSnapshots))[0];
|
|
1709
1928
|
return Number(r?.n ?? 0);
|
|
1710
1929
|
}
|
|
1711
|
-
var artifacts_exports = /* @__PURE__ */ __exportAll({
|
|
1712
|
-
BLOB_CAP: () => BLOB_CAP,
|
|
1713
|
-
SYNC_BODY_CAP: () => SYNC_BODY_CAP,
|
|
1714
|
-
isValidHash: () => isValidHash,
|
|
1715
|
-
parseArtifactSyncBody: () => parseArtifactSyncBody,
|
|
1716
|
-
safeRelPath: () => safeRelPath,
|
|
1717
|
-
sha256Buf: () => sha256Buf
|
|
1718
|
-
});
|
|
1719
|
-
/** Per-file byte cap. At or under ⇒ bytes sync; over ⇒ metadata-only (no bytes). */
|
|
1720
|
-
var BLOB_CAP = 10 * 1024 * 1024;
|
|
1721
|
-
/** Hard ceiling on the exact-manifest sync POST body. */
|
|
1722
|
-
var SYNC_BODY_CAP = 32 * 1024 * 1024;
|
|
1723
|
-
var SYNC_FIELDS = new Set(["loopId", "manifest"]);
|
|
1724
|
-
var MANIFEST_FIELDS = new Set([
|
|
1725
|
-
"path",
|
|
1726
|
-
"hash",
|
|
1727
|
-
"size",
|
|
1728
|
-
"binary",
|
|
1729
|
-
"oversize"
|
|
1730
|
-
]);
|
|
1731
|
-
var MAX_STORED_SIZE = 2147483647;
|
|
1732
|
-
function exactRecord(value, fields) {
|
|
1733
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1734
|
-
const proto = Object.getPrototypeOf(value);
|
|
1735
|
-
if (proto !== Object.prototype && proto !== null) return false;
|
|
1736
|
-
const keys = Object.keys(value);
|
|
1737
|
-
return keys.length === fields.size && keys.every((key) => fields.has(key));
|
|
1738
|
-
}
|
|
1739
|
-
function sha256Buf(bytes) {
|
|
1740
|
-
return createHash("sha256").update(bytes).digest("hex");
|
|
1741
|
-
}
|
|
1742
|
-
function isValidHash(hash) {
|
|
1743
|
-
return typeof hash === "string" && /^[0-9a-f]{64}$/.test(hash);
|
|
1744
|
-
}
|
|
1745
|
-
/**
|
|
1746
|
-
* Validate and preserve an untrusted, loop-folder-relative literal path. Returns
|
|
1747
|
-
* null if it is absolute, contains an empty/dot/traversal segment, is empty, or
|
|
1748
|
-
* carries a NUL (no real filesystem produces one, and Postgres rejects it).
|
|
1749
|
-
*/
|
|
1750
|
-
function safeRelPath(raw) {
|
|
1751
|
-
if (typeof raw !== "string" || raw.length === 0 || raw.includes("\0")) return null;
|
|
1752
|
-
if (raw.startsWith("/") || raw.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(raw)) return null;
|
|
1753
|
-
if (raw.split(/[\\/]/).some((part) => part === "" || part === "." || part === "..")) return null;
|
|
1754
|
-
return raw;
|
|
1755
|
-
}
|
|
1756
|
-
/** Parse the one canonical sync envelope without coercion or omitted-field defaults. */
|
|
1757
|
-
function parseArtifactSyncBody(value) {
|
|
1758
|
-
if (!exactRecord(value, SYNC_FIELDS)) return null;
|
|
1759
|
-
const { loopId, manifest } = value;
|
|
1760
|
-
if (typeof loopId !== "string" || loopId.length === 0 || loopId.length > 200 || loopId.includes("\0") || !Array.isArray(manifest)) return null;
|
|
1761
|
-
const entries = [];
|
|
1762
|
-
const paths = /* @__PURE__ */ new Set();
|
|
1763
|
-
for (const raw of manifest) {
|
|
1764
|
-
if (!exactRecord(raw, MANIFEST_FIELDS)) return null;
|
|
1765
|
-
const { path, hash, size, binary, oversize } = raw;
|
|
1766
|
-
const rel = safeRelPath(path);
|
|
1767
|
-
if (rel === null || paths.has(rel) || typeof size !== "number" || !Number.isSafeInteger(size) || size < 0 || size > MAX_STORED_SIZE || typeof binary !== "boolean" || typeof oversize !== "boolean") return null;
|
|
1768
|
-
if (oversize) {
|
|
1769
|
-
if (size <= 10485760 || hash !== null) return null;
|
|
1770
|
-
entries.push({
|
|
1771
|
-
path: rel,
|
|
1772
|
-
hash: null,
|
|
1773
|
-
size,
|
|
1774
|
-
binary,
|
|
1775
|
-
oversize: true
|
|
1776
|
-
});
|
|
1777
|
-
} else {
|
|
1778
|
-
if (size > 10485760 || !isValidHash(hash)) return null;
|
|
1779
|
-
entries.push({
|
|
1780
|
-
path: rel,
|
|
1781
|
-
hash,
|
|
1782
|
-
size,
|
|
1783
|
-
binary,
|
|
1784
|
-
oversize: false
|
|
1785
|
-
});
|
|
1786
|
-
}
|
|
1787
|
-
paths.add(rel);
|
|
1788
|
-
}
|
|
1789
|
-
return {
|
|
1790
|
-
loopId,
|
|
1791
|
-
manifest: entries
|
|
1792
|
-
};
|
|
1793
|
-
}
|
|
1794
1930
|
/**
|
|
1795
1931
|
* Machine + run credential helpers. Device tokens (`dk_…`) identify a machine
|
|
1796
1932
|
* (its id is derived from the token: `m-sha256(token)[:16]`). A run
|
|
@@ -2067,8 +2203,8 @@ function validateLoopTags(value) {
|
|
|
2067
2203
|
};
|
|
2068
2204
|
}
|
|
2069
2205
|
//#endregion
|
|
2070
|
-
//#region node_modules/.nitro/vite/services/ssr/assets/boot-
|
|
2071
|
-
var
|
|
2206
|
+
//#region node_modules/.nitro/vite/services/ssr/assets/boot-DmpKtb0A.js
|
|
2207
|
+
var boot_DmpKtb0A_exports = /* @__PURE__ */ __exportAll$1({
|
|
2072
2208
|
_: () => validOptionalPollString,
|
|
2073
2209
|
a: () => POLL_HTTP_BODY_FIELDS,
|
|
2074
2210
|
c: () => MIN_DAEMON_VERSION,
|
|
@@ -2082,24 +2218,11 @@ var boot_CYRi_wez_exports = /* @__PURE__ */ __exportAll$1({
|
|
|
2082
2218
|
n: () => ensureServer,
|
|
2083
2219
|
o: () => validPollCurrentRun,
|
|
2084
2220
|
p: () => MACHINE_BODY_CAP,
|
|
2085
|
-
r: () =>
|
|
2221
|
+
r: () => getLoopFiles,
|
|
2086
2222
|
s: () => validPollWireId,
|
|
2087
2223
|
t: () => boot_exports,
|
|
2088
2224
|
u: () => canonicalLoopEnvelope
|
|
2089
2225
|
});
|
|
2090
|
-
/**
|
|
2091
|
-
* Machine-route ingress: read + parse a JSON request body under a hard size cap.
|
|
2092
|
-
* The gateway's per-field wire caps (WIRE_TEXT_CAP etc.) clip individual strings
|
|
2093
|
-
* AFTER parse — without this, an unbounded `request.json()` still buffers an
|
|
2094
|
-
* arbitrarily large body first. Framework-free (plain Request) so any machine
|
|
2095
|
-
* route can share it.
|
|
2096
|
-
*/
|
|
2097
|
-
/**
|
|
2098
|
-
* Body cap for standard machine routes (poll / report / CLI).
|
|
2099
|
-
* 2MB — generously above the largest legitimate terminal report or loop edit.
|
|
2100
|
-
* The exact-manifest sync route has its own 32 MB JSON-body cap.
|
|
2101
|
-
*/
|
|
2102
|
-
var MACHINE_BODY_CAP = 2 * 1024 * 1024;
|
|
2103
2226
|
/** Read raw request bytes under a hard cap. Declared oversize requests fail
|
|
2104
2227
|
* before reading; chunked/lying streams are canceled on the first chunk that
|
|
2105
2228
|
* takes the observed count over the cap. */
|
|
@@ -2157,193 +2280,104 @@ async function readJsonBody(request, maxBytes) {
|
|
|
2157
2280
|
body: {}
|
|
2158
2281
|
};
|
|
2159
2282
|
try {
|
|
2160
|
-
return {
|
|
2161
|
-
kind: "ok",
|
|
2162
|
-
body: JSON.parse(text)
|
|
2163
|
-
};
|
|
2164
|
-
} catch {
|
|
2165
|
-
return { kind: "invalid" };
|
|
2166
|
-
}
|
|
2167
|
-
}
|
|
2168
|
-
var WIRE_TEXT_CAP = 512 * 1024;
|
|
2169
|
-
/** Validate an optional poll string without coercion, clipping, or NUL removal. */
|
|
2170
|
-
function validOptionalPollString(value, cap) {
|
|
2171
|
-
return value === void 0 || typeof value === "string" && value.length <= cap && !value.includes("\0");
|
|
2172
|
-
}
|
|
2173
|
-
function nowIso() {
|
|
2174
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2175
|
-
}
|
|
2176
|
-
/** Strip NUL (U+0000) from a wire string: Postgres text/jsonb columns REJECT the
|
|
2177
|
-
* NUL byte (SQLite tolerated it), so a daemon-supplied string carrying one would
|
|
2178
|
-
* throw mid-finalize on the DB write. The single sanitizing primitive behind
|
|
2179
|
-
* `clipText` - and used directly by `cli.ts`
|
|
2180
|
-
* (parseFlags/validateState, the same one-chokepoint discipline). */
|
|
2181
|
-
function stripNul(s) {
|
|
2182
|
-
return s.replace(/\u0000/g, "");
|
|
2183
|
-
}
|
|
2184
|
-
function clipText(s, cap) {
|
|
2185
|
-
return stripNul(s.slice(0, cap));
|
|
2186
|
-
}
|
|
2187
|
-
/**
|
|
2188
|
-
* Deliver the user's prompt unchanged, followed only by the complete report
|
|
2189
|
-
* contract. The run index intentionally remains an internal history fact.
|
|
2190
|
-
*/
|
|
2191
|
-
function buildRunTask(loop) {
|
|
2192
|
-
return `${loop.prompt}\n\nStatus definitions:\n- keep: ${loop.statusKeep}\n- no-change: ${loop.statusNoChange}\n- block: ${loop.statusBlock}\n\nBefore finishing, call exactly once:\npievo report --message "<summary>" --status <keep|no-change|block>`;
|
|
2193
|
-
}
|
|
2194
|
-
async function buildDelivery(loop, queuedRun, runToken, roots) {
|
|
2195
|
-
if (queuedRun.runIndex == null) throw new Error(`claimed run ${queuedRun.id} has no history index`);
|
|
2196
|
-
return {
|
|
2197
|
-
runId: queuedRun.id,
|
|
2198
|
-
runIndex: queuedRun.runIndex,
|
|
2199
|
-
runToken,
|
|
2200
|
-
roots,
|
|
2201
|
-
loop: {
|
|
2202
|
-
id: loop.id,
|
|
2203
|
-
name: loop.name,
|
|
2204
|
-
workdir: loop.workdir,
|
|
2205
|
-
model: loop.model ?? null,
|
|
2206
|
-
reasoningEffort: loop.reasoningEffort ?? null,
|
|
2207
|
-
agent: loop.agent
|
|
2208
|
-
},
|
|
2209
|
-
task: buildRunTask(loop),
|
|
2210
|
-
artifacts: loop.artifacts
|
|
2211
|
-
};
|
|
2212
|
-
}
|
|
2213
|
-
/**
|
|
2214
|
-
* Content-addressed blob byte store for live-synced loop artifacts.
|
|
2215
|
-
*
|
|
2216
|
-
* Bytes are keyed by their sha256 hash (content-addressed ⇒ deduped across every
|
|
2217
|
-
* loop/run). The store ONLY writes/reads bytes — it never executes or interprets
|
|
2218
|
-
* them — preserving the server's zero-exec invariant.
|
|
2219
|
-
*
|
|
2220
|
-
* `LocalBlobStore` is the durable store under `<dataDir>/blobs`;
|
|
2221
|
-
* `MemoryBlobStore` is the injected test fake.
|
|
2222
|
-
*/
|
|
2223
|
-
var log$4 = logger.child({ mod: "blobstore" });
|
|
2224
|
-
var SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
2225
|
-
/** Durable filesystem adapter. Hash sharding bounds entries per directory; atomic
|
|
2226
|
-
* temp-file renames ensure readers see either complete bytes or no object. */
|
|
2227
|
-
var LocalBlobStore = class {
|
|
2228
|
-
root;
|
|
2229
|
-
initialized;
|
|
2230
|
-
constructor(root = path.join(dataDir(), "blobs")) {
|
|
2231
|
-
this.root = root;
|
|
2232
|
-
}
|
|
2233
|
-
file(hash) {
|
|
2234
|
-
if (!SHA256_HEX.test(hash)) throw new Error("invalid sha256 blob key");
|
|
2235
|
-
return path.join(this.root, hash.slice(0, 2), hash);
|
|
2236
|
-
}
|
|
2237
|
-
async has(hash) {
|
|
2238
|
-
const target = this.file(hash);
|
|
2239
|
-
await this.ensureInitialized();
|
|
2240
|
-
try {
|
|
2241
|
-
return (await fs$1.stat(target)).isFile();
|
|
2242
|
-
} catch (err) {
|
|
2243
|
-
if (err.code === "ENOENT") return false;
|
|
2244
|
-
throw err;
|
|
2245
|
-
}
|
|
2246
|
-
}
|
|
2247
|
-
async syncDir(dir) {
|
|
2248
|
-
let handle;
|
|
2249
|
-
try {
|
|
2250
|
-
handle = await fs$1.open(dir, "r");
|
|
2251
|
-
await handle.sync();
|
|
2252
|
-
} catch (err) {
|
|
2253
|
-
const code = err.code;
|
|
2254
|
-
if (![
|
|
2255
|
-
"EINVAL",
|
|
2256
|
-
"ENOTSUP",
|
|
2257
|
-
"EBADF",
|
|
2258
|
-
"EPERM",
|
|
2259
|
-
"ENOENT"
|
|
2260
|
-
].includes(code)) throw err;
|
|
2261
|
-
} finally {
|
|
2262
|
-
await handle?.close().catch(() => {});
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2265
|
-
ensureInitialized() {
|
|
2266
|
-
return this.initialized ??= (async () => {
|
|
2267
|
-
await fs$1.mkdir(this.root, { recursive: true });
|
|
2268
|
-
await this.syncDir(path.dirname(path.dirname(this.root)));
|
|
2269
|
-
await this.syncDir(path.dirname(this.root));
|
|
2270
|
-
await this.syncDir(this.root);
|
|
2271
|
-
for (const shard of await fs$1.readdir(this.root, { withFileTypes: true })) {
|
|
2272
|
-
if (!shard.isDirectory() || !/^[a-f0-9]{2}$/.test(shard.name)) continue;
|
|
2273
|
-
const dir = path.join(this.root, shard.name);
|
|
2274
|
-
let removed = false;
|
|
2275
|
-
for (const name of await fs$1.readdir(dir)) {
|
|
2276
|
-
if (!/^\.[a-f0-9]{64}\.\d+\.[0-9a-f-]+\.tmp$/.test(name)) continue;
|
|
2277
|
-
await fs$1.rm(path.join(dir, name), { force: true });
|
|
2278
|
-
removed = true;
|
|
2279
|
-
}
|
|
2280
|
-
if (removed) await this.syncDir(dir);
|
|
2281
|
-
}
|
|
2282
|
-
})();
|
|
2283
|
-
}
|
|
2284
|
-
async put(hash, bytes) {
|
|
2285
|
-
const target = this.file(hash);
|
|
2286
|
-
await this.ensureInitialized();
|
|
2287
|
-
const dir = path.dirname(target);
|
|
2288
|
-
await fs$1.mkdir(dir, { recursive: true });
|
|
2289
|
-
await this.syncDir(this.root);
|
|
2290
|
-
const temp = path.join(dir, `.${hash}.${process.pid}.${randomUUID()}.tmp`);
|
|
2291
|
-
let handle;
|
|
2292
|
-
try {
|
|
2293
|
-
handle = await fs$1.open(temp, "wx", 384);
|
|
2294
|
-
await handle.writeFile(bytes);
|
|
2295
|
-
await handle.sync();
|
|
2296
|
-
await handle.close();
|
|
2297
|
-
handle = void 0;
|
|
2298
|
-
try {
|
|
2299
|
-
await fs$1.rename(temp, target);
|
|
2300
|
-
await this.syncDir(dir);
|
|
2301
|
-
} catch (err) {
|
|
2302
|
-
if ([
|
|
2303
|
-
"EEXIST",
|
|
2304
|
-
"EACCES",
|
|
2305
|
-
"EPERM"
|
|
2306
|
-
].includes(err.code ?? "") && await this.has(hash)) {
|
|
2307
|
-
await fs$1.rm(temp, { force: true });
|
|
2308
|
-
return;
|
|
2309
|
-
}
|
|
2310
|
-
throw err;
|
|
2311
|
-
}
|
|
2312
|
-
} catch (err) {
|
|
2313
|
-
await handle?.close().catch(() => {});
|
|
2314
|
-
await fs$1.rm(temp, { force: true }).catch(() => {});
|
|
2315
|
-
throw err;
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
async get(hash) {
|
|
2319
|
-
const target = this.file(hash);
|
|
2320
|
-
await this.ensureInitialized();
|
|
2321
|
-
try {
|
|
2322
|
-
return await fs$1.readFile(target);
|
|
2323
|
-
} catch (err) {
|
|
2324
|
-
if (err.code === "ENOENT") return null;
|
|
2325
|
-
throw err;
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2328
|
-
async delete(hash) {
|
|
2329
|
-
const target = this.file(hash);
|
|
2330
|
-
await this.ensureInitialized();
|
|
2331
|
-
await fs$1.rm(target, { force: true });
|
|
2332
|
-
await this.syncDir(path.dirname(target));
|
|
2283
|
+
return {
|
|
2284
|
+
kind: "ok",
|
|
2285
|
+
body: JSON.parse(text)
|
|
2286
|
+
};
|
|
2287
|
+
} catch {
|
|
2288
|
+
return { kind: "invalid" };
|
|
2333
2289
|
}
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2290
|
+
}
|
|
2291
|
+
var WIRE_TEXT_CAP = 512 * 1024;
|
|
2292
|
+
/** Validate an optional poll string without coercion, clipping, or NUL removal. */
|
|
2293
|
+
function validOptionalPollString(value, cap) {
|
|
2294
|
+
return value === void 0 || typeof value === "string" && value.length <= cap && !value.includes("\0");
|
|
2295
|
+
}
|
|
2296
|
+
function nowIso() {
|
|
2297
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2298
|
+
}
|
|
2299
|
+
/** Strip NUL (U+0000) from a wire string: Postgres text/jsonb columns REJECT the
|
|
2300
|
+
* NUL byte (SQLite tolerated it), so a daemon-supplied string carrying one would
|
|
2301
|
+
* throw mid-finalize on the DB write. The single sanitizing primitive behind
|
|
2302
|
+
* `clipText` - and used directly by `cli.ts`
|
|
2303
|
+
* (parseFlags/validateState, the same one-chokepoint discipline). */
|
|
2304
|
+
function stripNul(s) {
|
|
2305
|
+
return s.replace(/\u0000/g, "");
|
|
2306
|
+
}
|
|
2307
|
+
function clipText(s, cap) {
|
|
2308
|
+
return stripNul(s.slice(0, cap));
|
|
2309
|
+
}
|
|
2310
|
+
/** Literal placeholder the daemon replaces with the run's absolute loop-file
|
|
2311
|
+
* directory before launch (and exports as an env var of the same name). */
|
|
2312
|
+
var LOOP_DIR_PLACEHOLDER = "$PIEVO_LOOP_DIR";
|
|
2313
|
+
var STATUS_LINE_RE = /^STATUS:\s*(keep|no-change|block)\.?$/i;
|
|
2314
|
+
/**
|
|
2315
|
+
* Deliver the user's prompt unchanged, followed by the status definitions, the
|
|
2316
|
+
* loop-file contract, and the final-line status contract. The run index
|
|
2317
|
+
* intentionally remains an internal history fact.
|
|
2318
|
+
*/
|
|
2319
|
+
function buildRunTask(loop, previousNotices = []) {
|
|
2320
|
+
const files = [`- ${MEMORY_FILE}: this loop's memory, kept across runs (max ${MEMORY_CHAR_CAP} characters). Read it before you start. Before finishing, update it with anything that would help future runs avoid mistakes or work faster. Keep it concise: merge similar notes, delete stale or wrong ones, no run logs.`, ...loop.artifacts.map((name) => `- ${name}: artifact shown on the Pievo dashboard (max 2 MB). Update it only as the task above asks.`)];
|
|
2321
|
+
const notices = previousNotices.length ? `\n\nNotices from the previous run:\n${previousNotices.map((notice) => `- ${notice}`).join("\n")}` : "";
|
|
2322
|
+
return `${loop.prompt}
|
|
2323
|
+
|
|
2324
|
+
Status definitions:
|
|
2325
|
+
- keep: ${loop.statusKeep}
|
|
2326
|
+
- no-change: ${loop.statusNoChange}
|
|
2327
|
+
- block: ${loop.statusBlock}
|
|
2328
|
+
|
|
2329
|
+
Loop files in ${LOOP_DIR_PLACEHOLDER}:
|
|
2330
|
+
${files.join("\n")}
|
|
2331
|
+
A file over its limit is rejected and its previous version kept. Other files in that directory are discarded.${notices}
|
|
2332
|
+
|
|
2333
|
+
End your final response with a short summary, then exactly one last line:
|
|
2334
|
+
STATUS: keep|no-change|block`;
|
|
2340
2335
|
}
|
|
2341
2336
|
/**
|
|
2342
|
-
*
|
|
2337
|
+
* Split a provider's final response into the reported status and summary. The
|
|
2338
|
+
* final non-empty line must be the status line; markdown emphasis or code ticks
|
|
2339
|
+
* around it are tolerated because models add them unprompted.
|
|
2340
|
+
*/
|
|
2341
|
+
function parseFinalStatus(finalText) {
|
|
2342
|
+
if (!finalText) return void 0;
|
|
2343
|
+
const lines = finalText.split(/\r?\n/);
|
|
2344
|
+
let index = lines.length - 1;
|
|
2345
|
+
while (index >= 0 && lines[index].trim() === "") index--;
|
|
2346
|
+
if (index < 0) return void 0;
|
|
2347
|
+
const bare = lines[index].trim().replace(/^[*_`>\s]+|[*_`\s]+$/g, "");
|
|
2348
|
+
const match = STATUS_LINE_RE.exec(bare);
|
|
2349
|
+
if (!match) return void 0;
|
|
2350
|
+
return {
|
|
2351
|
+
status: match[1].toLowerCase(),
|
|
2352
|
+
message: lines.slice(0, index).join("\n").trim()
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
async function buildDelivery(loop, queuedRun, runToken, roots) {
|
|
2356
|
+
if (queuedRun.runIndex == null) throw new Error(`claimed run ${queuedRun.id} has no history index`);
|
|
2357
|
+
const notices = await previousRunFileNotices(loop.id, queuedRun.runIndex);
|
|
2358
|
+
return {
|
|
2359
|
+
runId: queuedRun.id,
|
|
2360
|
+
runIndex: queuedRun.runIndex,
|
|
2361
|
+
runToken,
|
|
2362
|
+
roots,
|
|
2363
|
+
loop: {
|
|
2364
|
+
id: loop.id,
|
|
2365
|
+
name: loop.name,
|
|
2366
|
+
workdir: loop.workdir,
|
|
2367
|
+
model: loop.model ?? null,
|
|
2368
|
+
reasoningEffort: loop.reasoningEffort ?? null,
|
|
2369
|
+
agent: loop.agent
|
|
2370
|
+
},
|
|
2371
|
+
task: buildRunTask(loop, notices),
|
|
2372
|
+
files: loopFileNames(loop)
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
/**
|
|
2376
|
+
* Loop-file storage retention + garbage collection — the policy that keeps the
|
|
2343
2377
|
* local byte store from growing monotonically.
|
|
2344
2378
|
*
|
|
2345
|
-
* The
|
|
2346
|
-
* deduped across every loop/run. When a
|
|
2379
|
+
* The loop-file pipeline is content-addressed: blob BYTES are keyed by sha256 and
|
|
2380
|
+
* deduped across every loop/run. When a file is deleted or its content changes,
|
|
2347
2381
|
* the OLD blob is no longer pointed at by the current file row — but nothing was
|
|
2348
2382
|
* reclaiming it, so the byte store grew forever. This module closes that gap with two pieces:
|
|
2349
2383
|
*
|
|
@@ -2354,18 +2388,18 @@ function createBlobStore() {
|
|
|
2354
2388
|
* hash. CORRECTNESS over aggressiveness — a still-referenced blob is never
|
|
2355
2389
|
* deleted; a leaked blob is only a cost bug the next pass reclaims.
|
|
2356
2390
|
*
|
|
2357
|
-
* Concurrency: gcBlobs is safe
|
|
2358
|
-
* a grace window (a blob whose metadata row is younger than the window is
|
|
2359
|
-
* collected, so a blob a
|
|
2360
|
-
*
|
|
2391
|
+
* Concurrency: gcBlobs is safe alongside owner writes and run uploads. Three guards
|
|
2392
|
+
* combine — a grace window (a blob whose metadata row is younger than the window is
|
|
2393
|
+
* never collected, so a blob a run just uploaded is untouchable), a point re-check
|
|
2394
|
+
* of each candidate immediately before its bytes are deleted (so a blob
|
|
2361
2395
|
* re-referenced before we touch it keeps both its bytes and metadata), and a
|
|
2362
2396
|
* bytes-before-metadata delete ordering: each candidate's bytes are reclaimed
|
|
2363
2397
|
* FIRST, then its metadata row is dropped unconditionally — so blobExists() goes
|
|
2364
|
-
* false and any
|
|
2398
|
+
* false and any writer that re-references the hash (even one that raced the byte
|
|
2365
2399
|
* delete) re-uploads the bytes (self-healing). We never leave a live blobs row
|
|
2366
2400
|
* pointing at deleted bytes.
|
|
2367
2401
|
*/
|
|
2368
|
-
var log$
|
|
2402
|
+
var log$2 = logger.child({ mod: "retention" });
|
|
2369
2403
|
/**
|
|
2370
2404
|
* Prune every loop's run snapshots down to the configured retention window
|
|
2371
2405
|
* (most-recent-N). Returns the total number pruned. This is what makes an old
|
|
@@ -2418,7 +2452,7 @@ async function gcBlobs(blobStore, graceMs = BLOB_GC_GRACE_MS) {
|
|
|
2418
2452
|
await deleteBlob(hash);
|
|
2419
2453
|
reclaimed++;
|
|
2420
2454
|
} catch (err) {
|
|
2421
|
-
log$
|
|
2455
|
+
log$2.warn({
|
|
2422
2456
|
hash,
|
|
2423
2457
|
err: err instanceof Error ? err.message : String(err)
|
|
2424
2458
|
}, "gc: blob byte-delete failed");
|
|
@@ -2429,7 +2463,7 @@ async function gcBlobs(blobStore, graceMs = BLOB_GC_GRACE_MS) {
|
|
|
2429
2463
|
async function maintainStorage(blobStore, options = {}) {
|
|
2430
2464
|
const snapshotsPruned = await pruneSnapshots(options.keep);
|
|
2431
2465
|
const blobsReclaimed = await gcBlobs(blobStore, options.graceMs);
|
|
2432
|
-
if (snapshotsPruned || blobsReclaimed) log$
|
|
2466
|
+
if (snapshotsPruned || blobsReclaimed) log$2.info({
|
|
2433
2467
|
snapshotsPruned,
|
|
2434
2468
|
blobsReclaimed
|
|
2435
2469
|
}, "storage maintenance");
|
|
@@ -2764,44 +2798,37 @@ function validateStatusDefinitions(value) {
|
|
|
2764
2798
|
}
|
|
2765
2799
|
};
|
|
2766
2800
|
}
|
|
2767
|
-
function
|
|
2801
|
+
function validateArtifactNames(value) {
|
|
2768
2802
|
if (value === void 0 || value === null) return {
|
|
2769
2803
|
ok: true,
|
|
2770
2804
|
value: []
|
|
2771
2805
|
};
|
|
2772
2806
|
if (!Array.isArray(value)) return {
|
|
2773
2807
|
ok: false,
|
|
2774
|
-
detail: "artifacts must be an array of
|
|
2808
|
+
detail: "artifacts must be an array of file names"
|
|
2809
|
+
};
|
|
2810
|
+
if (value.length > 4) return {
|
|
2811
|
+
ok: false,
|
|
2812
|
+
detail: `at most 4 artifacts are allowed`
|
|
2775
2813
|
};
|
|
2776
|
-
const
|
|
2814
|
+
const names = [];
|
|
2777
2815
|
const seen = /* @__PURE__ */ new Set();
|
|
2778
2816
|
for (const entry of value) {
|
|
2779
|
-
if (
|
|
2780
|
-
ok: false,
|
|
2781
|
-
detail: "artifact paths must be non-empty strings"
|
|
2782
|
-
};
|
|
2783
|
-
if (entry.includes("\0")) return {
|
|
2784
|
-
ok: false,
|
|
2785
|
-
detail: "artifact path must not contain NUL"
|
|
2786
|
-
};
|
|
2787
|
-
if (entry.startsWith("/") || entry.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(entry)) return {
|
|
2788
|
-
ok: false,
|
|
2789
|
-
detail: `artifact path must be relative to workdir: ${entry}`
|
|
2790
|
-
};
|
|
2791
|
-
if (entry.split(/[\\/]/).some((part) => part === "" || part === "." || part === "..")) return {
|
|
2817
|
+
if (!validArtifactName(entry)) return {
|
|
2792
2818
|
ok: false,
|
|
2793
|
-
detail: `artifact
|
|
2819
|
+
detail: `artifact must be a plain .md or .html file name other than ${MEMORY_FILE} (letters, digits, . _ -; at most 64 characters before the extension): ${JSON.stringify(entry)}`
|
|
2794
2820
|
};
|
|
2795
|
-
|
|
2821
|
+
const key = entry.toLowerCase();
|
|
2822
|
+
if (seen.has(key)) return {
|
|
2796
2823
|
ok: false,
|
|
2797
|
-
detail: `duplicate artifact
|
|
2824
|
+
detail: `duplicate artifact name: ${entry}`
|
|
2798
2825
|
};
|
|
2799
|
-
seen.add(
|
|
2800
|
-
|
|
2826
|
+
seen.add(key);
|
|
2827
|
+
names.push(entry);
|
|
2801
2828
|
}
|
|
2802
2829
|
return {
|
|
2803
2830
|
ok: true,
|
|
2804
|
-
value:
|
|
2831
|
+
value: names
|
|
2805
2832
|
};
|
|
2806
2833
|
}
|
|
2807
2834
|
function rowSchedule(schedule, retainedCron = SAFE_RETAINED_CRON) {
|
|
@@ -2886,7 +2913,7 @@ function validateLoopCreate(value) {
|
|
|
2886
2913
|
if (!schedule.ok) return schedule;
|
|
2887
2914
|
const statusDefinitions = validateStatusDefinitions(raw.statusDefinitions);
|
|
2888
2915
|
if (!statusDefinitions.ok) return statusDefinitions;
|
|
2889
|
-
const artifacts =
|
|
2916
|
+
const artifacts = validateArtifactNames(raw.artifacts);
|
|
2890
2917
|
if (!artifacts.ok) return artifacts;
|
|
2891
2918
|
const model = optionalProvider(raw.model, "model");
|
|
2892
2919
|
if (!model.ok) return model;
|
|
@@ -2981,7 +3008,7 @@ function validateLoopEdit(loop, value) {
|
|
|
2981
3008
|
update.statusBlock = result.value.block;
|
|
2982
3009
|
}
|
|
2983
3010
|
if (raw.artifacts !== void 0) {
|
|
2984
|
-
const result =
|
|
3011
|
+
const result = validateArtifactNames(raw.artifacts);
|
|
2985
3012
|
if (!result.ok) return result;
|
|
2986
3013
|
update.artifacts = result.value;
|
|
2987
3014
|
}
|
|
@@ -3017,7 +3044,7 @@ function validateLoopEdit(loop, value) {
|
|
|
3017
3044
|
value: update
|
|
3018
3045
|
};
|
|
3019
3046
|
}
|
|
3020
|
-
var MIN_DAEMON_VERSION = "
|
|
3047
|
+
var MIN_DAEMON_VERSION = "4.0.0";
|
|
3021
3048
|
function daemonNeedsUpdate(current, required = MIN_DAEMON_VERSION) {
|
|
3022
3049
|
if (!current || !isValidSemver(current) || !isValidSemver(required)) return true;
|
|
3023
3050
|
if (current === required) return false;
|
|
@@ -3058,6 +3085,7 @@ var validPollInfo = (value) => value === void 0 || value !== null && typeof valu
|
|
|
3058
3085
|
*
|
|
3059
3086
|
* POST /api/machine/poll (Bearer device token) → claim pending runs, deliver
|
|
3060
3087
|
* POST /machine/report (Bearer run token) → finalize a run
|
|
3088
|
+
* GET|PUT /api/machine/run-files/:name (Bearer run token) → loop-file transfer
|
|
3061
3089
|
*
|
|
3062
3090
|
* plus owner create/list/edit methods and retention.
|
|
3063
3091
|
* Also exposes `dispatcher` (a `Dispatcher` for the Scheduler: "is the machine
|
|
@@ -3066,7 +3094,7 @@ var validPollInfo = (value) => value === void 0 || value !== null && typeof valu
|
|
|
3066
3094
|
* (`CliGateway`), which reuses this class's owner methods;
|
|
3067
3095
|
* canonical loop configuration validation lives in `gateway/loopConfig.ts`.
|
|
3068
3096
|
*/
|
|
3069
|
-
var log$
|
|
3097
|
+
var log$1 = logger.child({ mod: "gateway" });
|
|
3070
3098
|
var ONLINE_TTL_MS = 3e4;
|
|
3071
3099
|
/** Auto-pause after this many consecutive errors. Canceled rows are transparent;
|
|
3072
3100
|
* zero disables the breaker. */
|
|
@@ -3081,10 +3109,15 @@ var RUN_TIMEOUT_MS = Number.isFinite(configuredRunTimeoutMs) && configuredRunTim
|
|
|
3081
3109
|
* patched over the device-token edit surface. Exported for `cli.ts`
|
|
3082
3110
|
* (the `new`/`edit` verb help lists these keys). */
|
|
3083
3111
|
var EDITABLE_LOOP_FIELDS = LOOP_EDIT_FIELDS;
|
|
3084
|
-
/**
|
|
3085
|
-
*
|
|
3086
|
-
* this cap. Exported for `cli.ts` so the report verb uses the same budget. */
|
|
3112
|
+
/** Run summary (the final response minus its STATUS line) and run errors share
|
|
3113
|
+
* this cap; the full final response is stored separately as finalText. */
|
|
3087
3114
|
var MESSAGE_CAP = 2e3;
|
|
3115
|
+
/** A run reports at most one version per loop file (MEMORY.md + 4 artifacts). */
|
|
3116
|
+
var REPORT_FILES_MAX = 5;
|
|
3117
|
+
/** One notice per file plus a few upload/rejection reasons; bounds the prompt echo. */
|
|
3118
|
+
var REPORT_NOTICES_MAX = 10;
|
|
3119
|
+
/** One short sentence (name + reason); 500 chars keeps the next prompt small. */
|
|
3120
|
+
var NOTICE_CAP = 500;
|
|
3088
3121
|
/** Provider session IDs are opaque short tokens; this rejects malformed payloads
|
|
3089
3122
|
* without constraining known provider formats. */
|
|
3090
3123
|
var SESSION_ID_CAP = 200;
|
|
@@ -3099,7 +3132,7 @@ var HEARTBEAT_STAMP_REFRESH_MS = heartbeatRefreshMs(RUN_TIMEOUT_MS);
|
|
|
3099
3132
|
* and an idle poll becomes read-only, with worst-case staleness well inside
|
|
3100
3133
|
* the 30s TTL (max stamp gap = refresh + one poll interval). */
|
|
3101
3134
|
var LAST_SEEN_REFRESH_MS = 1e4;
|
|
3102
|
-
var
|
|
3135
|
+
var MACHINE_REPORT_FIELDS = [
|
|
3103
3136
|
"reportId",
|
|
3104
3137
|
"runId",
|
|
3105
3138
|
"result",
|
|
@@ -3108,8 +3141,17 @@ var MACHINE_REPORT_FIELD_SET = new Set([
|
|
|
3108
3141
|
"sessionId",
|
|
3109
3142
|
"usage",
|
|
3110
3143
|
"error",
|
|
3111
|
-
"finalText"
|
|
3112
|
-
|
|
3144
|
+
"finalText",
|
|
3145
|
+
"files",
|
|
3146
|
+
"fileNotices"
|
|
3147
|
+
];
|
|
3148
|
+
var REPORT_FILE_FIELDS = [
|
|
3149
|
+
"baseHash",
|
|
3150
|
+
"hash",
|
|
3151
|
+
"name",
|
|
3152
|
+
"size"
|
|
3153
|
+
];
|
|
3154
|
+
var MACHINE_REPORT_FIELD_SET = new Set(MACHINE_REPORT_FIELDS);
|
|
3113
3155
|
var MACHINE_REPORT_USAGE_FIELDS = new Set([
|
|
3114
3156
|
"inputTokens",
|
|
3115
3157
|
"outputTokens",
|
|
@@ -3138,7 +3180,7 @@ function receiptFor(body, runId, ackStatus = 200, ackBody) {
|
|
|
3138
3180
|
}
|
|
3139
3181
|
function receiptResponse(receipt, expected) {
|
|
3140
3182
|
if (!receipt || receipt.runId !== expected.runId) return void 0;
|
|
3141
|
-
if (receipt.payloadDigest !== expected.payloadDigest) log$
|
|
3183
|
+
if (receipt.payloadDigest !== expected.payloadDigest) log$1.warn({
|
|
3142
3184
|
reportId: expected.reportId,
|
|
3143
3185
|
runId: expected.runId
|
|
3144
3186
|
}, "report: same-run payload changed after commit; replaying authoritative ACK");
|
|
@@ -3166,7 +3208,7 @@ async function committedReportEvidence(reportId, payloadDigest, authoritativeRun
|
|
|
3166
3208
|
foreignRun: false
|
|
3167
3209
|
};
|
|
3168
3210
|
if (normal?.runId === authoritativeRunId) {
|
|
3169
|
-
if (normal.payloadDigest !== payloadDigest) log$
|
|
3211
|
+
if (normal.payloadDigest !== payloadDigest) log$1.warn({
|
|
3170
3212
|
reportId,
|
|
3171
3213
|
runId: authoritativeRunId
|
|
3172
3214
|
}, "report: same-run payload changed after commit; replaying authoritative ACK");
|
|
@@ -3200,14 +3242,36 @@ function incidentDiagnosis(code, issues, reportId, payloadDigest) {
|
|
|
3200
3242
|
recommendedAction
|
|
3201
3243
|
};
|
|
3202
3244
|
}
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
*
|
|
3206
|
-
function
|
|
3207
|
-
const
|
|
3208
|
-
if (
|
|
3209
|
-
|
|
3210
|
-
|
|
3245
|
+
var MISSING_STATUS_ERROR = "run protocol incomplete: the final response has no \"STATUS: keep|no-change|block\" line";
|
|
3246
|
+
/** The run outcome a terminal report establishes. Only a successful provider
|
|
3247
|
+
* exit is read for a status; a successful exit without one is a protocol error. */
|
|
3248
|
+
function reportOutcome(body) {
|
|
3249
|
+
const canceled = body.result === "canceled";
|
|
3250
|
+
if (canceled) return {
|
|
3251
|
+
ok: false,
|
|
3252
|
+
canceled,
|
|
3253
|
+
patch: { error: "stopped by user" }
|
|
3254
|
+
};
|
|
3255
|
+
if (body.result !== "success") return {
|
|
3256
|
+
ok: false,
|
|
3257
|
+
canceled,
|
|
3258
|
+
patch: { error: typeof body.error === "string" ? clipText(body.error, MESSAGE_CAP) : "run failed on machine" }
|
|
3259
|
+
};
|
|
3260
|
+
const parsed = parseFinalStatus(body.finalText);
|
|
3261
|
+
if (!parsed) return {
|
|
3262
|
+
ok: false,
|
|
3263
|
+
canceled,
|
|
3264
|
+
patch: { error: MISSING_STATUS_ERROR }
|
|
3265
|
+
};
|
|
3266
|
+
return {
|
|
3267
|
+
ok: true,
|
|
3268
|
+
canceled,
|
|
3269
|
+
patch: {
|
|
3270
|
+
status: parsed.status,
|
|
3271
|
+
message: clipText(parsed.message, 2e3) || null,
|
|
3272
|
+
error: null
|
|
3273
|
+
}
|
|
3274
|
+
};
|
|
3211
3275
|
}
|
|
3212
3276
|
function validateTerminalReport(body) {
|
|
3213
3277
|
const issues = [];
|
|
@@ -3227,6 +3291,25 @@ function validateTerminalReport(body) {
|
|
|
3227
3291
|
"error",
|
|
3228
3292
|
"finalText"
|
|
3229
3293
|
]) if (has(key) && typeof body[key] !== "string") issues.push(`${key} must be a string`);
|
|
3294
|
+
if (has("files")) {
|
|
3295
|
+
const files = body.files;
|
|
3296
|
+
if (!Array.isArray(files) || files.length > REPORT_FILES_MAX) issues.push(`files must be an array of at most ${REPORT_FILES_MAX} entries`);
|
|
3297
|
+
else {
|
|
3298
|
+
const names = /* @__PURE__ */ new Set();
|
|
3299
|
+
for (const entry of files) {
|
|
3300
|
+
const file = entry;
|
|
3301
|
+
if (!(file !== null && typeof file === "object" && !Array.isArray(file) && Object.keys(file).sort().join(",") === REPORT_FILE_FIELDS.join(",") && typeof file.name === "string" && file.name.length > 0 && file.name.length <= 100 && !names.has(file.name) && isValidHash(file.baseHash) && isValidHash(file.hash) && typeof file.size === "number" && Number.isInteger(file.size) && file.size >= 0 && file.size <= LOOP_FILE_BYTE_CAP)) {
|
|
3302
|
+
issues.push("files entries must be exactly {name, baseHash, hash, size} with unique names, sha256 hashes, and an in-cap size");
|
|
3303
|
+
break;
|
|
3304
|
+
}
|
|
3305
|
+
names.add(file.name);
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
if (has("fileNotices")) {
|
|
3310
|
+
const notices = body.fileNotices;
|
|
3311
|
+
if (!Array.isArray(notices) || notices.length > REPORT_NOTICES_MAX || notices.some((notice) => typeof notice !== "string" || notice.length > NOTICE_CAP)) issues.push(`fileNotices must be an array of at most ${REPORT_NOTICES_MAX} strings of at most ${NOTICE_CAP} characters`);
|
|
3312
|
+
}
|
|
3230
3313
|
if (has("usage")) if (body.usage === null || typeof body.usage !== "object" || Array.isArray(body.usage)) issues.push("usage must be an object");
|
|
3231
3314
|
else {
|
|
3232
3315
|
const usage = body.usage;
|
|
@@ -3267,7 +3350,10 @@ var MachineGateway = class {
|
|
|
3267
3350
|
constructor(scheduler, blobStore = createBlobStore()) {
|
|
3268
3351
|
this.scheduler = scheduler;
|
|
3269
3352
|
this.blobStore = blobStore;
|
|
3353
|
+
this.loopFiles = new LoopFiles(blobStore);
|
|
3270
3354
|
}
|
|
3355
|
+
/** Loop-file bytes over the same blob store that maintenance garbage-collects. */
|
|
3356
|
+
loopFiles;
|
|
3271
3357
|
/** In-flight latch: the maintenance pass is sequential and the first post-deploy
|
|
3272
3358
|
* backlog reclamation can overrun the interval, so a fresh tick skips rather than
|
|
3273
3359
|
* running a second pass concurrently (idempotent but wasteful + double-counts). */
|
|
@@ -3275,7 +3361,7 @@ var MachineGateway = class {
|
|
|
3275
3361
|
applyAutopauseTimer(loopId, terminal) {
|
|
3276
3362
|
if (!terminal.autoPaused) return;
|
|
3277
3363
|
this.scheduler.removeLoop(loopId);
|
|
3278
|
-
log$
|
|
3364
|
+
log$1.warn({
|
|
3279
3365
|
loopId,
|
|
3280
3366
|
streak: terminal.failureStreak
|
|
3281
3367
|
}, "loop auto-paused after block or consecutive errors");
|
|
@@ -3371,13 +3457,14 @@ var MachineGateway = class {
|
|
|
3371
3457
|
* Periodic storage maintenance: prune each loop's run snapshots to the
|
|
3372
3458
|
* retention window, then GC blob bytes no live row needs. Wired to its own
|
|
3373
3459
|
* interval in boot (independent of the faster offline-sweep) and exposed for
|
|
3374
|
-
* tests / on-demand triggers. Safe to run concurrently with
|
|
3375
|
-
* grace window + final re-check protect freshly-written
|
|
3376
|
-
* idempotent with no garbage. Best-effort — never throws
|
|
3460
|
+
* tests / on-demand triggers. Safe to run concurrently with owner writes and
|
|
3461
|
+
* run uploads (a grace window + final re-check protect freshly-written or
|
|
3462
|
+
* referenced blobs) and idempotent with no garbage. Best-effort — never throws
|
|
3463
|
+
* into the caller.
|
|
3377
3464
|
*/
|
|
3378
3465
|
async maintainStorage(options) {
|
|
3379
3466
|
if (this.maintenanceRunning) {
|
|
3380
|
-
log$
|
|
3467
|
+
log$1.info("storage maintenance already in progress — skipping this tick");
|
|
3381
3468
|
return {
|
|
3382
3469
|
snapshotsPruned: 0,
|
|
3383
3470
|
blobsReclaimed: 0
|
|
@@ -3387,7 +3474,7 @@ var MachineGateway = class {
|
|
|
3387
3474
|
try {
|
|
3388
3475
|
return await maintainStorage(this.blobStore, options);
|
|
3389
3476
|
} catch (err) {
|
|
3390
|
-
log$
|
|
3477
|
+
log$1.warn({ err: err instanceof Error ? err.message : String(err) }, "storage maintenance failed");
|
|
3391
3478
|
return {
|
|
3392
3479
|
snapshotsPruned: 0,
|
|
3393
3480
|
blobsReclaimed: 0
|
|
@@ -3413,7 +3500,7 @@ var MachineGateway = class {
|
|
|
3413
3500
|
token: deviceToken,
|
|
3414
3501
|
online: true
|
|
3415
3502
|
});
|
|
3416
|
-
log$
|
|
3503
|
+
log$1.info({
|
|
3417
3504
|
machineId,
|
|
3418
3505
|
host: info?.host
|
|
3419
3506
|
}, "poll: self-registered machine");
|
|
@@ -3445,7 +3532,7 @@ var MachineGateway = class {
|
|
|
3445
3532
|
const claimed = await claimReadyRunForMachine(machineId, void 0, currentRunIds ?? []);
|
|
3446
3533
|
if (claimed) delivery = await buildDelivery(claimed.loop, claimed.run, claimed.runToken, machine.roots ?? []);
|
|
3447
3534
|
}
|
|
3448
|
-
if (delivery) log$
|
|
3535
|
+
if (delivery) log$1.info({
|
|
3449
3536
|
machineId,
|
|
3450
3537
|
runId: delivery.runId
|
|
3451
3538
|
}, "poll: delivered");
|
|
@@ -3463,7 +3550,7 @@ var MachineGateway = class {
|
|
|
3463
3550
|
code: "VALIDATION_ERROR"
|
|
3464
3551
|
}
|
|
3465
3552
|
};
|
|
3466
|
-
if (request.protocolVersion !==
|
|
3553
|
+
if (request.protocolVersion !== 5) {
|
|
3467
3554
|
const auth = await authenticateDeviceToken(deviceToken, { allowUnknown: true });
|
|
3468
3555
|
if (!auth.ok && auth.reason === "mismatch") return auth.response;
|
|
3469
3556
|
if (auth.ok && auth.machine) {
|
|
@@ -3475,7 +3562,7 @@ var MachineGateway = class {
|
|
|
3475
3562
|
body: {
|
|
3476
3563
|
error: "daemon upgrade required; run `npm install -g @kky42/pievo@latest`, then `pievo daemon restart`",
|
|
3477
3564
|
code: "UPGRADE_REQUIRED",
|
|
3478
|
-
requiredProtocol:
|
|
3565
|
+
requiredProtocol: 5
|
|
3479
3566
|
}
|
|
3480
3567
|
};
|
|
3481
3568
|
}
|
|
@@ -3507,7 +3594,7 @@ var MachineGateway = class {
|
|
|
3507
3594
|
const needsUpdate = daemonNeedsUpdate(reportedVersion);
|
|
3508
3595
|
const base = await this.pollCore(deviceToken, request.info, currentIds, !needsUpdate, true);
|
|
3509
3596
|
if (base.status !== 200) return base;
|
|
3510
|
-
if ((await getMachine(machineId))?.daemonProtocol !==
|
|
3597
|
+
if ((await getMachine(machineId))?.daemonProtocol !== 5) await updateMachine(machineId, { daemonProtocol: 5 });
|
|
3511
3598
|
const running = await runningRunsForMachine(machineId);
|
|
3512
3599
|
const currentSet = new Set(currentIds);
|
|
3513
3600
|
return {
|
|
@@ -3524,7 +3611,7 @@ var MachineGateway = class {
|
|
|
3524
3611
|
};
|
|
3525
3612
|
}
|
|
3526
3613
|
async pollV4Wait(deviceToken, request, waitMs = LONG_POLL_WAIT_MS) {
|
|
3527
|
-
if (request.currentRuns?.length || request.protocolVersion !==
|
|
3614
|
+
if (request.currentRuns?.length || request.protocolVersion !== 5) return this.pollV4(deviceToken, request);
|
|
3528
3615
|
const auth = await authenticateDeviceToken(deviceToken, { allowUnknown: true });
|
|
3529
3616
|
if (!auth.ok) return auth.response;
|
|
3530
3617
|
const machineId = auth.machineId;
|
|
@@ -3632,7 +3719,7 @@ var MachineGateway = class {
|
|
|
3632
3719
|
});
|
|
3633
3720
|
this.scheduler.addLoop(loop);
|
|
3634
3721
|
recordNewIdempotency(idempotencyKey, machineId, loop.id);
|
|
3635
|
-
log$
|
|
3722
|
+
log$1.info({
|
|
3636
3723
|
machineId,
|
|
3637
3724
|
loopId: loop.id,
|
|
3638
3725
|
agent: loop.agent
|
|
@@ -3846,7 +3933,7 @@ var MachineGateway = class {
|
|
|
3846
3933
|
}
|
|
3847
3934
|
const response = receiptResponse(stored, expected);
|
|
3848
3935
|
if (response.status < 300) await retireLease(runToken);
|
|
3849
|
-
log$
|
|
3936
|
+
log$1.info({ runId: lease.runId }, "report: ignored (run was canceled)");
|
|
3850
3937
|
return response;
|
|
3851
3938
|
}
|
|
3852
3939
|
async reportRaceResponse(runToken, lease, body, payloadDigest) {
|
|
@@ -3870,7 +3957,7 @@ var MachineGateway = class {
|
|
|
3870
3957
|
const refreshedLease = await resolveLease(runToken);
|
|
3871
3958
|
if (refreshedLease?.state === "retired") return this.retiredReport(runToken, body, lease.runId);
|
|
3872
3959
|
if (retryReconcile && fresh?.phase === "error" && refreshedLease?.state === "terminal-grace") return this.reconcileReclaimedReport(runToken, refreshedLease, fresh, body);
|
|
3873
|
-
log$
|
|
3960
|
+
log$1.info({
|
|
3874
3961
|
runId: lease.runId,
|
|
3875
3962
|
phase: fresh?.phase
|
|
3876
3963
|
}, logMessage);
|
|
@@ -3883,12 +3970,108 @@ var MachineGateway = class {
|
|
|
3883
3970
|
}
|
|
3884
3971
|
};
|
|
3885
3972
|
}
|
|
3973
|
+
/** Reported loop-file versions whose uploaded bytes are present. A canceled run
|
|
3974
|
+
* was killed mid-edit, so none of its files are applied. */
|
|
3975
|
+
async terminalFiles(body) {
|
|
3976
|
+
const notices = (body.fileNotices ?? []).map((notice) => clipText(notice, NOTICE_CAP));
|
|
3977
|
+
const updates = [];
|
|
3978
|
+
if (body.result === "canceled") return {
|
|
3979
|
+
updates,
|
|
3980
|
+
notices
|
|
3981
|
+
};
|
|
3982
|
+
const files = body.files ?? [];
|
|
3983
|
+
const sizes = await blobSizes(files.map((file) => file.hash).filter((hash) => hash !== EMPTY_SHA256));
|
|
3984
|
+
for (const file of files) {
|
|
3985
|
+
if (!await this.loopFiles.hasBlob(file.hash)) {
|
|
3986
|
+
notices.push(`${file.name}: the upload was lost; the previous version was kept`);
|
|
3987
|
+
continue;
|
|
3988
|
+
}
|
|
3989
|
+
const size = file.hash === "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" ? 0 : sizes.get(file.hash);
|
|
3990
|
+
if (size === void 0 || size !== file.size) {
|
|
3991
|
+
notices.push(`${file.name}: reported size did not match the stored version; the previous version was kept`);
|
|
3992
|
+
continue;
|
|
3993
|
+
}
|
|
3994
|
+
updates.push({
|
|
3995
|
+
name: file.name,
|
|
3996
|
+
baseHash: file.baseHash,
|
|
3997
|
+
hash: file.hash,
|
|
3998
|
+
size
|
|
3999
|
+
});
|
|
4000
|
+
}
|
|
4001
|
+
return {
|
|
4002
|
+
updates,
|
|
4003
|
+
notices
|
|
4004
|
+
};
|
|
4005
|
+
}
|
|
4006
|
+
/** Current bytes of one loop file for an executing run (the run's starting version). */
|
|
4007
|
+
async readRunFile(runToken, name) {
|
|
4008
|
+
const lease = await resolveLease(runToken);
|
|
4009
|
+
if (!lease) return {
|
|
4010
|
+
status: 401,
|
|
4011
|
+
body: { error: "invalid or expired token" }
|
|
4012
|
+
};
|
|
4013
|
+
if (lease.state !== "active") return {
|
|
4014
|
+
status: 409,
|
|
4015
|
+
body: { error: "run is no longer executing" }
|
|
4016
|
+
};
|
|
4017
|
+
const loop = await getLoop(lease.loopId);
|
|
4018
|
+
if (!loop || !loopFileNames(loop).includes(name)) return {
|
|
4019
|
+
status: 404,
|
|
4020
|
+
body: { error: "no such loop file" }
|
|
4021
|
+
};
|
|
4022
|
+
const row = await getArtifactFile(loop.id, name);
|
|
4023
|
+
const hash = row && !row.deleted && row.hash ? row.hash : EMPTY_SHA256;
|
|
4024
|
+
const bytes = await this.loopFiles.readBlob(hash);
|
|
4025
|
+
if (!bytes) return {
|
|
4026
|
+
status: 500,
|
|
4027
|
+
body: { error: "loop file bytes are missing" }
|
|
4028
|
+
};
|
|
4029
|
+
return {
|
|
4030
|
+
status: 200,
|
|
4031
|
+
body: {},
|
|
4032
|
+
bytes,
|
|
4033
|
+
hash
|
|
4034
|
+
};
|
|
4035
|
+
}
|
|
4036
|
+
/** Store one loop-file version produced by a run. Accepted until the run's
|
|
4037
|
+
* authority retires, so the durable report outbox can retry after a crash.
|
|
4038
|
+
* Applying it to the loop happens only in the terminal report transaction. */
|
|
4039
|
+
async storeRunFile(runToken, name, bytes, expectedHash) {
|
|
4040
|
+
const lease = await resolveLease(runToken);
|
|
4041
|
+
if (!lease || lease.state === "retired") return {
|
|
4042
|
+
status: 401,
|
|
4043
|
+
body: { error: "invalid or expired token" }
|
|
4044
|
+
};
|
|
4045
|
+
const loop = await getLoop(lease.loopId);
|
|
4046
|
+
if (!loop || !loopFileNames(loop).includes(name)) return {
|
|
4047
|
+
status: 404,
|
|
4048
|
+
body: { error: "no such loop file" }
|
|
4049
|
+
};
|
|
4050
|
+
const issue = loopFileIssue(name, bytes);
|
|
4051
|
+
if (issue) return {
|
|
4052
|
+
status: 413,
|
|
4053
|
+
body: { error: issue }
|
|
4054
|
+
};
|
|
4055
|
+
if (expectedHash !== null && expectedHash !== sha256Buf(bytes)) return {
|
|
4056
|
+
status: 400,
|
|
4057
|
+
body: { error: "hash mismatch" }
|
|
4058
|
+
};
|
|
4059
|
+
const hash = await this.loopFiles.putBlob(bytes);
|
|
4060
|
+
if (hash !== "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") await recordBlob(hash, bytes.length);
|
|
4061
|
+
return {
|
|
4062
|
+
status: 200,
|
|
4063
|
+
body: {
|
|
4064
|
+
ok: true,
|
|
4065
|
+
hash
|
|
4066
|
+
}
|
|
4067
|
+
};
|
|
4068
|
+
}
|
|
3886
4069
|
async captureRunSnapshot(runId, loopId) {
|
|
3887
4070
|
try {
|
|
3888
4071
|
await putRunSnapshot(runId, loopId, await buildLoopManifest(loopId));
|
|
3889
4072
|
await pruneRunSnapshots(loopId, 20);
|
|
3890
4073
|
} catch (err) {
|
|
3891
|
-
log$
|
|
4074
|
+
log$1.warn({
|
|
3892
4075
|
runId,
|
|
3893
4076
|
err: err instanceof Error ? err.message : String(err)
|
|
3894
4077
|
}, "snapshot capture failed");
|
|
@@ -3898,11 +4081,8 @@ var MachineGateway = class {
|
|
|
3898
4081
|
* same loop-lock transaction as the error→done/error patch and loop state, so
|
|
3899
4082
|
* concurrent late reports cannot both win (including error→error). */
|
|
3900
4083
|
async reconcileReclaimedReport(runToken, lease, run, body) {
|
|
3901
|
-
const
|
|
3902
|
-
const
|
|
3903
|
-
const protocolMissing = providerOk ? runProtocolMissing(run) : [];
|
|
3904
|
-
const ok = providerOk && protocolMissing.length === 0;
|
|
3905
|
-
const message = run.message ?? void 0;
|
|
4084
|
+
const { ok, canceled, patch } = reportOutcome(body);
|
|
4085
|
+
const files = await this.terminalFiles(body);
|
|
3906
4086
|
const loopPatch = {};
|
|
3907
4087
|
const receipt = receiptFor(body, lease.runId);
|
|
3908
4088
|
const payloadDigest = sha256(JSON.stringify(body));
|
|
@@ -3911,10 +4091,10 @@ var MachineGateway = class {
|
|
|
3911
4091
|
reconciled = await reconcileReclaimedRun(lease.loopId, lease.runId, sha256(runToken), {
|
|
3912
4092
|
phase: canceled ? "canceled" : ok ? "done" : "error",
|
|
3913
4093
|
...coerceTelemetry(body),
|
|
3914
|
-
...
|
|
3915
|
-
|
|
4094
|
+
...patch,
|
|
4095
|
+
fileNotices: files.notices,
|
|
3916
4096
|
ts: nowIso()
|
|
3917
|
-
}, loopPatch, AUTOPAUSE_STREAK, receipt);
|
|
4097
|
+
}, loopPatch, AUTOPAUSE_STREAK, receipt, files.updates);
|
|
3918
4098
|
} catch (error) {
|
|
3919
4099
|
const raced = await this.reportRaceResponse(runToken, lease, body, payloadDigest);
|
|
3920
4100
|
if (raced) return raced;
|
|
@@ -3927,7 +4107,7 @@ var MachineGateway = class {
|
|
|
3927
4107
|
if (!reportOnly) this.scheduler.addLoop(reconciled.loop);
|
|
3928
4108
|
if (!reportOnly) this.applyAutopauseTimer(lease.loopId, reconciled);
|
|
3929
4109
|
if (deleting) await tryDeleteLoop(lease.loopId);
|
|
3930
|
-
log$
|
|
4110
|
+
log$1.info({
|
|
3931
4111
|
runId: lease.runId,
|
|
3932
4112
|
ok,
|
|
3933
4113
|
reclaimed: true
|
|
@@ -3980,7 +4160,7 @@ var MachineGateway = class {
|
|
|
3980
4160
|
this.scheduler.addLoop(rejected.loop);
|
|
3981
4161
|
if (rejected.state === "run-error") this.applyAutopauseTimer(lease.loopId, rejected);
|
|
3982
4162
|
if (rejected.loop.deleteRequestedAt) await tryDeleteLoop(lease.loopId);
|
|
3983
|
-
log$
|
|
4163
|
+
log$1.warn({
|
|
3984
4164
|
runId: lease.runId,
|
|
3985
4165
|
reportId,
|
|
3986
4166
|
code,
|
|
@@ -4034,8 +4214,6 @@ var MachineGateway = class {
|
|
|
4034
4214
|
if (evidence.foreignRun) return this.rejectTerminalAttempt(runToken, lease, body, payloadDigest, "REPORT_CONFLICT", ["reportId was already committed for another run"]);
|
|
4035
4215
|
if (issues.length) return this.rejectTerminalAttempt(runToken, lease, body, payloadDigest, "REPORT_INVALID", issues);
|
|
4036
4216
|
const expected = receiptFor(body, lease.runId);
|
|
4037
|
-
const ok = body.result === "success";
|
|
4038
|
-
const canceled = body.result === "canceled";
|
|
4039
4217
|
const run = await getRun(lease.runId);
|
|
4040
4218
|
if (run?.phase === "canceled") return this.ignoreCanceledReport(runToken, lease, body);
|
|
4041
4219
|
if (run?.phase === "done") return {
|
|
@@ -4047,17 +4225,18 @@ var MachineGateway = class {
|
|
|
4047
4225
|
}
|
|
4048
4226
|
};
|
|
4049
4227
|
if (run?.phase === "error" && (lease.state === "terminal-grace" || lease.state === "reconciliation-only")) return this.reconcileReclaimedReport(runToken, lease, run, body);
|
|
4050
|
-
const
|
|
4051
|
-
const
|
|
4228
|
+
const { ok: effectiveOk, canceled, patch } = reportOutcome(body);
|
|
4229
|
+
const files = await this.terminalFiles(body);
|
|
4052
4230
|
const loopPatch = {};
|
|
4053
4231
|
let terminal;
|
|
4054
4232
|
try {
|
|
4055
4233
|
terminal = await finalizeRunningRun(lease.loopId, lease.runId, {
|
|
4056
4234
|
phase: canceled ? "canceled" : effectiveOk ? "done" : "error",
|
|
4057
4235
|
...coerceTelemetry(body),
|
|
4058
|
-
...
|
|
4236
|
+
...patch,
|
|
4237
|
+
fileNotices: files.notices,
|
|
4059
4238
|
ts: nowIso()
|
|
4060
|
-
}, loopPatch, sha256(runToken), AUTOPAUSE_STREAK, expected);
|
|
4239
|
+
}, loopPatch, sha256(runToken), AUTOPAUSE_STREAK, expected, files.updates);
|
|
4061
4240
|
} catch (error) {
|
|
4062
4241
|
const raced = await this.reportRaceResponse(runToken, lease, body, payloadDigest);
|
|
4063
4242
|
if (raced) return raced;
|
|
@@ -4068,7 +4247,7 @@ var MachineGateway = class {
|
|
|
4068
4247
|
if (!deleting) await this.captureRunSnapshot(lease.runId, lease.loopId);
|
|
4069
4248
|
this.scheduler.addLoop(terminal.loop);
|
|
4070
4249
|
this.applyAutopauseTimer(lease.loopId, terminal);
|
|
4071
|
-
log$
|
|
4250
|
+
log$1.info({
|
|
4072
4251
|
runId: lease.runId,
|
|
4073
4252
|
ok: effectiveOk
|
|
4074
4253
|
}, "report: finalized");
|
|
@@ -4182,157 +4361,9 @@ function nextFires(cron, timezone, n) {
|
|
|
4182
4361
|
}
|
|
4183
4362
|
}
|
|
4184
4363
|
/**
|
|
4185
|
-
*
|
|
4186
|
-
* `MachineGateway` (which keeps poll/report/CLI/retention). Same wire surface,
|
|
4187
|
-
* framework-agnostic like the rest of the gateway (`{ status, body }` results):
|
|
4188
|
-
*
|
|
4189
|
-
* POST /api/machine/sync (Bearer device token) → manifest reconcile
|
|
4190
|
-
* PUT /api/machine/blob/:hash (Bearer device token) → negotiated blob upload
|
|
4191
|
-
*
|
|
4192
|
-
* plus `readBlob` (the download seam the artifact/diff readers resolve bytes
|
|
4193
|
-
* through). Boot constructs ONE BlobStore and hands the SAME instance to this
|
|
4194
|
-
* class and to `MachineGateway` (whose `maintainStorage` GC deletes the bytes
|
|
4195
|
-
* written here).
|
|
4196
|
-
*/
|
|
4197
|
-
var log$1 = logger.child({ mod: "gateway" });
|
|
4198
|
-
var BLOB_PRESENCE_CONCURRENCY = 16;
|
|
4199
|
-
async function mapConcurrent(items, limit, visit) {
|
|
4200
|
-
let next = 0;
|
|
4201
|
-
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
4202
|
-
while (next < items.length) {
|
|
4203
|
-
const item = items[next++];
|
|
4204
|
-
await visit(item);
|
|
4205
|
-
}
|
|
4206
|
-
}));
|
|
4207
|
-
}
|
|
4208
|
-
var ArtifactSync = class {
|
|
4209
|
-
blobStore;
|
|
4210
|
-
constructor(blobStore = createBlobStore()) {
|
|
4211
|
-
this.blobStore = blobStore;
|
|
4212
|
-
}
|
|
4213
|
-
/**
|
|
4214
|
-
* Reconcile one complete configured-path manifest after a run. Missing hashes
|
|
4215
|
-
* are returned for upload through the raw blob PUT endpoint.
|
|
4216
|
-
*/
|
|
4217
|
-
async sync(deviceToken, rawBody) {
|
|
4218
|
-
const auth = await authenticateDeviceToken(deviceToken);
|
|
4219
|
-
if (!auth.ok) return auth.response;
|
|
4220
|
-
const { machineId } = auth;
|
|
4221
|
-
const body = parseArtifactSyncBody(rawBody);
|
|
4222
|
-
if (!body) return {
|
|
4223
|
-
status: 400,
|
|
4224
|
-
body: { error: "invalid sync body" }
|
|
4225
|
-
};
|
|
4226
|
-
const { loopId, manifest } = body;
|
|
4227
|
-
const loop = await getLoop(loopId);
|
|
4228
|
-
if (!loop || loop.machineId !== machineId) return {
|
|
4229
|
-
status: 404,
|
|
4230
|
-
body: { error: "no such loop on this machine" }
|
|
4231
|
-
};
|
|
4232
|
-
const allowedPaths = new Set(loop.artifacts);
|
|
4233
|
-
if (manifest.some((entry) => !allowedPaths.has(entry.path))) return {
|
|
4234
|
-
status: 400,
|
|
4235
|
-
body: { error: "manifest path is not configured on this loop" }
|
|
4236
|
-
};
|
|
4237
|
-
const keepPaths = [];
|
|
4238
|
-
const needHashes = /* @__PURE__ */ new Set();
|
|
4239
|
-
const storedState = /* @__PURE__ */ new Map();
|
|
4240
|
-
const stateFor = async (hash) => {
|
|
4241
|
-
const cached = storedState.get(hash);
|
|
4242
|
-
if (cached) return cached;
|
|
4243
|
-
const metadata = await blobExists(hash);
|
|
4244
|
-
const state = {
|
|
4245
|
-
metadata,
|
|
4246
|
-
bytes: metadata && await this.blobStore.has(hash)
|
|
4247
|
-
};
|
|
4248
|
-
storedState.set(hash, state);
|
|
4249
|
-
return state;
|
|
4250
|
-
};
|
|
4251
|
-
await mapConcurrent([...new Set(manifest.flatMap((entry) => entry.oversize ? [] : [entry.hash]))], BLOB_PRESENCE_CONCURRENCY, async (hash) => {
|
|
4252
|
-
await stateFor(hash);
|
|
4253
|
-
});
|
|
4254
|
-
for (const entry of manifest) {
|
|
4255
|
-
if (entry.oversize) {
|
|
4256
|
-
if (await upsertConfiguredArtifactFile({
|
|
4257
|
-
loopId,
|
|
4258
|
-
path: entry.path,
|
|
4259
|
-
hash: null,
|
|
4260
|
-
size: entry.size,
|
|
4261
|
-
binary: entry.binary,
|
|
4262
|
-
oversize: true
|
|
4263
|
-
})) keepPaths.push(entry.path);
|
|
4264
|
-
continue;
|
|
4265
|
-
}
|
|
4266
|
-
const hash = entry.hash;
|
|
4267
|
-
const stored = await stateFor(hash);
|
|
4268
|
-
if (!await upsertConfiguredArtifactFile({
|
|
4269
|
-
loopId,
|
|
4270
|
-
path: entry.path,
|
|
4271
|
-
hash,
|
|
4272
|
-
size: entry.size,
|
|
4273
|
-
binary: entry.binary,
|
|
4274
|
-
oversize: false
|
|
4275
|
-
})) continue;
|
|
4276
|
-
if (!stored.bytes) needHashes.add(hash);
|
|
4277
|
-
keepPaths.push(entry.path);
|
|
4278
|
-
}
|
|
4279
|
-
const tombstoned = await reconcileConfiguredArtifacts(loopId, [...allowedPaths], keepPaths);
|
|
4280
|
-
log$1.info({
|
|
4281
|
-
machineId,
|
|
4282
|
-
loopId,
|
|
4283
|
-
files: keepPaths.length,
|
|
4284
|
-
need: needHashes.size,
|
|
4285
|
-
tombstoned
|
|
4286
|
-
}, "sync: reconciled");
|
|
4287
|
-
return {
|
|
4288
|
-
status: 200,
|
|
4289
|
-
body: {
|
|
4290
|
-
ok: true,
|
|
4291
|
-
needHashes: [...needHashes]
|
|
4292
|
-
}
|
|
4293
|
-
};
|
|
4294
|
-
}
|
|
4295
|
-
/**
|
|
4296
|
-
* Upload one content-addressed blob's raw bytes (Bearer device token). The
|
|
4297
|
-
* server recomputes sha256(body) and rejects any mismatch before storing —
|
|
4298
|
-
* integrity + anti-poisoning, so a blob's bytes always match its key.
|
|
4299
|
-
*/
|
|
4300
|
-
async putBlob(deviceToken, hash, bytes) {
|
|
4301
|
-
const auth = await authenticateDeviceToken(deviceToken);
|
|
4302
|
-
if (!auth.ok) return auth.response;
|
|
4303
|
-
const { machineId } = auth;
|
|
4304
|
-
if (!isValidHash(hash)) return {
|
|
4305
|
-
status: 400,
|
|
4306
|
-
body: { error: "invalid hash (expect sha256 hex)" }
|
|
4307
|
-
};
|
|
4308
|
-
if (bytes.length > 10485760) return {
|
|
4309
|
-
status: 413,
|
|
4310
|
-
body: { error: "blob exceeds size cap" }
|
|
4311
|
-
};
|
|
4312
|
-
if (sha256Buf(bytes) !== hash) return {
|
|
4313
|
-
status: 400,
|
|
4314
|
-
body: { error: "hash mismatch (sha256(body) !== :hash)" }
|
|
4315
|
-
};
|
|
4316
|
-
if (!await machineReferencesBlob(machineId, hash)) return {
|
|
4317
|
-
status: 403,
|
|
4318
|
-
body: { error: "hash was not requested for this machine (sync a manifest first)" }
|
|
4319
|
-
};
|
|
4320
|
-
await this.blobStore.put(hash, bytes);
|
|
4321
|
-
await recordBlob(hash, bytes.length);
|
|
4322
|
-
return {
|
|
4323
|
-
status: 200,
|
|
4324
|
-
body: { ok: true }
|
|
4325
|
-
};
|
|
4326
|
-
}
|
|
4327
|
-
readBlob(hash) {
|
|
4328
|
-
if (!isValidHash(hash)) return Promise.resolve(null);
|
|
4329
|
-
return this.blobStore.get(hash);
|
|
4330
|
-
}
|
|
4331
|
-
};
|
|
4332
|
-
/**
|
|
4333
|
-
* Per-run artifact diff. Computed lazily on the server at read time
|
|
4364
|
+
* Per-run loop-file diff. Computed lazily on the server at read time
|
|
4334
4365
|
* (no stored diffs): load run N's snapshot + the previous run's snapshot and diff
|
|
4335
|
-
* the two
|
|
4366
|
+
* the two name → metadata maps. For changed TEXT files we load both blobs and
|
|
4336
4367
|
* compute a unified line diff with `diff` (jsdiff) — a pure-string library, no
|
|
4337
4368
|
* process execution, so the server's zero-exec invariant holds. Binary/oversize
|
|
4338
4369
|
* files emit a size-delta marker only (no inline diff).
|
|
@@ -4353,7 +4384,7 @@ function withinDiffCap(e) {
|
|
|
4353
4384
|
}
|
|
4354
4385
|
async function bytesOf(e) {
|
|
4355
4386
|
if (!e.hash) return null;
|
|
4356
|
-
return await (await
|
|
4387
|
+
return await (await getLoopFiles()).readBlob(e.hash) ?? null;
|
|
4357
4388
|
}
|
|
4358
4389
|
function unified(path, oldText, newText, maxEditLength) {
|
|
4359
4390
|
return maxEditLength === void 0 ? createTwoFilesPatch(path, path, oldText, newText, "previous run", "this run") : createTwoFilesPatch(path, path, oldText, newText, "previous run", "this run", { maxEditLength });
|
|
@@ -4366,7 +4397,7 @@ function expectedInputBytes(change, verifiedSizes) {
|
|
|
4366
4397
|
if (sizes.some((size) => size === void 0)) return null;
|
|
4367
4398
|
return sizes.reduce((sum, size) => sum + size, 0);
|
|
4368
4399
|
}
|
|
4369
|
-
/** Compute one run's
|
|
4400
|
+
/** Compute one run's loop-file diff. The default remains the web UI's complete
|
|
4370
4401
|
* view. Budgeted callers get explicit totals and stop before blob reads/jsdiff
|
|
4371
4402
|
* once any work limit is spent. */
|
|
4372
4403
|
async function computeRunDiff(runId, budget) {
|
|
@@ -4781,6 +4812,7 @@ async function detailHistory(loop, q) {
|
|
|
4781
4812
|
errorTruncated: error.truncated,
|
|
4782
4813
|
finalText: finalText.value,
|
|
4783
4814
|
finalTextTruncated: finalText.truncated,
|
|
4815
|
+
fileNotices: run.fileNotices ?? [],
|
|
4784
4816
|
diffAvailable: !!snapshot,
|
|
4785
4817
|
diff: q.diff ? await boundedDiff(run.id) : {
|
|
4786
4818
|
included: false,
|
|
@@ -4806,6 +4838,7 @@ async function detailHistory(loop, q) {
|
|
|
4806
4838
|
["errorTruncated", detail.errorTruncated],
|
|
4807
4839
|
["finalText", detail.finalText],
|
|
4808
4840
|
["finalTextTruncated", detail.finalTextTruncated],
|
|
4841
|
+
["fileNotices", { raw: JSON.stringify(detail.fileNotices) }],
|
|
4809
4842
|
["diffAvailable", detail.diffAvailable],
|
|
4810
4843
|
["diff", { raw: JSON.stringify(detail.diff) }]
|
|
4811
4844
|
]));
|
|
@@ -4830,8 +4863,8 @@ async function readLoopHistory(loop, flags) {
|
|
|
4830
4863
|
* The verb router keys authority on credential type first (`dk_` device prefix
|
|
4831
4864
|
* vs `rk_` run lease) and reuses the core gateway
|
|
4832
4865
|
* methods (`createLoop`/`editLoop`) through the injected `MachineGateway`,
|
|
4833
|
-
* plus the credential-neutral history module. Run credentials
|
|
4834
|
-
*
|
|
4866
|
+
* plus the credential-neutral history module. Run credentials authorize no CLI
|
|
4867
|
+
* verb: a run reports through its final response, not a callback.
|
|
4835
4868
|
*/
|
|
4836
4869
|
var cliLog = logger.child({ mod: "cli" });
|
|
4837
4870
|
var CliGateway = class {
|
|
@@ -4850,9 +4883,8 @@ var CliGateway = class {
|
|
|
4850
4883
|
* already exists — never a rewrite — that keys authority on the credential type:
|
|
4851
4884
|
* · DEVICE credential (`dk_`-prefixed) → owner authority over any loop bound to
|
|
4852
4885
|
* the machine: `new`→createLoop, `loops`→listLoops, `edit`→editLoop,
|
|
4853
|
-
* `log`→readLoopHistory, `show`→describe
|
|
4854
|
-
* · RUN credential (`rk_` run lease) →
|
|
4855
|
-
* Every owner verb is rejected.
|
|
4886
|
+
* `log`→readLoopHistory, `show`→describe, `file`→loop files.
|
|
4887
|
+
* · RUN credential (`rk_` run lease) → rejected; runs have no CLI.
|
|
4856
4888
|
*/
|
|
4857
4889
|
async cli(token, argv) {
|
|
4858
4890
|
return finalizeCli(token.startsWith("dk_") ? await this.deviceCli(token, argv) : await this.runCli(token, argv));
|
|
@@ -4943,13 +4975,10 @@ var CliGateway = class {
|
|
|
4943
4975
|
}
|
|
4944
4976
|
};
|
|
4945
4977
|
}
|
|
4946
|
-
case "
|
|
4947
|
-
status: 403,
|
|
4948
|
-
body: { error: "pievo: report is a run-only verb; the owner edits via edit" }
|
|
4949
|
-
};
|
|
4978
|
+
case "file": return this.ownerFile(machineId, argv);
|
|
4950
4979
|
default: return {
|
|
4951
4980
|
status: 400,
|
|
4952
|
-
body: { error: `pievo: unknown command "${verb}" for the device credential (try: new, loops, pause, start, stop, delete, run stop, edit, log, show)` }
|
|
4981
|
+
body: { error: `pievo: unknown command "${verb}" for the device credential (try: new, loops, pause, start, stop, delete, run stop, edit, log, show, file)` }
|
|
4953
4982
|
};
|
|
4954
4983
|
}
|
|
4955
4984
|
}
|
|
@@ -4998,7 +5027,7 @@ var CliGateway = class {
|
|
|
4998
5027
|
}
|
|
4999
5028
|
async requireStopProtocol(machineId, running) {
|
|
5000
5029
|
if (!running) return void 0;
|
|
5001
|
-
if ((await getMachine(machineId))?.daemonProtocol ===
|
|
5030
|
+
if ((await getMachine(machineId))?.daemonProtocol === 5) return void 0;
|
|
5002
5031
|
return {
|
|
5003
5032
|
status: 426,
|
|
5004
5033
|
body: {
|
|
@@ -5109,121 +5138,90 @@ var CliGateway = class {
|
|
|
5109
5138
|
body: { text: `run already finished: ${runResultToken(stopped)}` }
|
|
5110
5139
|
};
|
|
5111
5140
|
}
|
|
5112
|
-
async runCli(runToken,
|
|
5113
|
-
|
|
5114
|
-
if (!lease) return {
|
|
5141
|
+
async runCli(runToken, _argv) {
|
|
5142
|
+
if (!await resolveLease(runToken)) return {
|
|
5115
5143
|
status: 401,
|
|
5116
5144
|
body: {
|
|
5117
5145
|
text: errorBlock("invalid or expired token", "UNAUTHORIZED"),
|
|
5118
5146
|
exitCode: 1
|
|
5119
5147
|
}
|
|
5120
5148
|
};
|
|
5121
|
-
|
|
5122
|
-
status: 409,
|
|
5123
|
-
body: {
|
|
5124
|
-
text: errorBlock(TERMINAL_GRACE_MSG, "CONFLICT"),
|
|
5125
|
-
exitCode: 1
|
|
5126
|
-
}
|
|
5127
|
-
};
|
|
5128
|
-
const verb = argv[0] ?? "";
|
|
5129
|
-
const flags = parseFlags(argv.slice(1));
|
|
5130
|
-
if (DEVICE_ONLY_VERBS.has(verb)) return {
|
|
5131
|
-
status: 403,
|
|
5132
|
-
body: {
|
|
5133
|
-
text: errorBlock(`"${verb}" needs the device credential (owner authority); a run may only report`, "FORBIDDEN"),
|
|
5134
|
-
exitCode: 1
|
|
5135
|
-
}
|
|
5136
|
-
};
|
|
5137
|
-
if (flags.help === true && verb === "report") return {
|
|
5138
|
-
status: 200,
|
|
5139
|
-
body: {
|
|
5140
|
-
text: verbHelpText("report", lease),
|
|
5141
|
-
exitCode: 0
|
|
5142
|
-
}
|
|
5143
|
-
};
|
|
5144
|
-
if (verb !== "report" && verb !== "help" && verb !== "--help" && verb !== "-h" && verb !== "") return {
|
|
5149
|
+
return {
|
|
5145
5150
|
status: 403,
|
|
5146
5151
|
body: {
|
|
5147
|
-
text: errorBlock(
|
|
5152
|
+
text: errorBlock(RUN_HAS_NO_CLI, "FORBIDDEN"),
|
|
5148
5153
|
exitCode: 1
|
|
5149
5154
|
}
|
|
5150
5155
|
};
|
|
5151
|
-
const out = await this.dispatch(lease, createHash("sha256").update(runToken).digest("hex"), argv);
|
|
5152
|
-
return {
|
|
5153
|
-
status: out.code,
|
|
5154
|
-
body: {
|
|
5155
|
-
text: out.text,
|
|
5156
|
-
exitCode: out.code === 200 ? 0 : 1
|
|
5157
|
-
}
|
|
5158
|
-
};
|
|
5159
5156
|
}
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
const
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5157
|
+
/** `file list|get|set` over one loop's files. `set` takes base64 content so
|
|
5158
|
+
* arbitrary text (including leading `--`) survives argv transport. */
|
|
5159
|
+
async ownerFile(machineId, argv) {
|
|
5160
|
+
const [, sub, loopId, name] = argv;
|
|
5161
|
+
const loop = loopId ? await this.ownedLoop(machineId, loopId) : void 0;
|
|
5162
|
+
if (sub !== "list" && sub !== "get" && sub !== "set") return {
|
|
5163
|
+
status: 400,
|
|
5164
|
+
body: { error: `usage: ${FILE_USAGE}` }
|
|
5165
|
+
};
|
|
5166
|
+
if (!loop) return {
|
|
5167
|
+
status: 404,
|
|
5168
|
+
body: { error: "no such loop on this machine" }
|
|
5167
5169
|
};
|
|
5168
|
-
|
|
5169
|
-
if (
|
|
5170
|
-
|
|
5171
|
-
text:
|
|
5170
|
+
const names = loopFileNames(loop);
|
|
5171
|
+
if (sub === "list") return {
|
|
5172
|
+
status: 200,
|
|
5173
|
+
body: { text: doc(listBlock("files", [
|
|
5174
|
+
"name",
|
|
5175
|
+
"size",
|
|
5176
|
+
"limit",
|
|
5177
|
+
"updatedAt"
|
|
5178
|
+
], await Promise.all(names.map(async (file) => {
|
|
5179
|
+
const row = await getArtifactFile(loop.id, file);
|
|
5180
|
+
const live = row && !row.deleted && row.hash ? row : void 0;
|
|
5181
|
+
return [
|
|
5182
|
+
file,
|
|
5183
|
+
live?.size ?? 0,
|
|
5184
|
+
fileLimitLabel(file),
|
|
5185
|
+
live ? fmtTime(live.updatedAt) : null
|
|
5186
|
+
];
|
|
5187
|
+
}))), helpBlock([`Run \`pievo file get ${loop.id} <name>\` to print a file`, `Run \`pievo file set ${loop.id} <name> --from <path>\` to replace a file`])) }
|
|
5172
5188
|
};
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
const message = str("message")?.trim();
|
|
5177
|
-
if (!message) return derr(400, "report requires a non-empty --message", "VALIDATION_ERROR");
|
|
5178
|
-
const acceptedFlags = [
|
|
5179
|
-
"status",
|
|
5180
|
-
"message",
|
|
5181
|
-
"help"
|
|
5182
|
-
];
|
|
5183
|
-
const unknown = Object.keys(flags).filter((key) => !acceptedFlags.includes(key));
|
|
5184
|
-
if (unknown.length) return derr(400, `report does not accept --${unknown[0]}`, "VALIDATION_ERROR");
|
|
5185
|
-
if (lease.state === "retired") return {
|
|
5186
|
-
code: 200,
|
|
5187
|
-
text: "reported: run retired · result discarded"
|
|
5189
|
+
if (!name || !names.includes(name)) return {
|
|
5190
|
+
status: 404,
|
|
5191
|
+
body: { error: `no such loop file: ${name ?? ""} (files: ${names.join(", ")})` }
|
|
5188
5192
|
};
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
status,
|
|
5204
|
-
|
|
5205
|
-
});
|
|
5206
|
-
if (result.state === "applied") return {
|
|
5207
|
-
ok: true,
|
|
5208
|
-
detail: "reported"
|
|
5193
|
+
if (sub === "get") {
|
|
5194
|
+
const row = await getArtifactFile(loop.id, name);
|
|
5195
|
+
const bytes = await this.gateway.loopFiles.readBlob(row && !row.deleted && row.hash ? row.hash : EMPTY_SHA256);
|
|
5196
|
+
if (!bytes) return {
|
|
5197
|
+
status: 500,
|
|
5198
|
+
body: { error: "loop file bytes are missing" }
|
|
5199
|
+
};
|
|
5200
|
+
return {
|
|
5201
|
+
status: 200,
|
|
5202
|
+
body: { text: bytes.length ? bytes.toString("utf8") : "\n" }
|
|
5203
|
+
};
|
|
5204
|
+
}
|
|
5205
|
+
const encoded = parseFlags(argv.slice(4))["content-base64"];
|
|
5206
|
+
if (typeof encoded !== "string") return {
|
|
5207
|
+
status: 400,
|
|
5208
|
+
body: { error: `usage: ${FILE_USAGE}` }
|
|
5209
5209
|
};
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5210
|
+
const bytes = Buffer.from(encoded, "base64");
|
|
5211
|
+
const issue = loopFileIssue(name, bytes);
|
|
5212
|
+
if (issue) return {
|
|
5213
|
+
status: 413,
|
|
5214
|
+
body: { error: `${issue}; nothing was changed` }
|
|
5215
5215
|
};
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5216
|
+
const hash = await this.gateway.loopFiles.putBlob(bytes);
|
|
5217
|
+
if (hash !== "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") await recordBlob(hash, bytes.length);
|
|
5218
|
+
if (!await writeLoopFile(loop.id, name, hash, bytes.length)) return {
|
|
5219
|
+
status: 404,
|
|
5220
|
+
body: { error: "no such loop file" }
|
|
5221
5221
|
};
|
|
5222
5222
|
return {
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
code: "CONFLICT",
|
|
5226
|
-
status: 409
|
|
5223
|
+
status: 200,
|
|
5224
|
+
body: { text: `file: ${name}\nsize: ${bytes.length}\nupdated: true` }
|
|
5227
5225
|
};
|
|
5228
5226
|
}
|
|
5229
5227
|
async describe(loopId, opts = {}) {
|
|
@@ -5268,17 +5266,8 @@ var CliGateway = class {
|
|
|
5268
5266
|
})), scoped.elsewhere, await recentMachineRuns(loops, 3));
|
|
5269
5267
|
}
|
|
5270
5268
|
};
|
|
5271
|
-
var
|
|
5272
|
-
|
|
5273
|
-
var DEVICE_ONLY_VERBS = new Set([
|
|
5274
|
-
"new",
|
|
5275
|
-
"edit",
|
|
5276
|
-
"loops",
|
|
5277
|
-
"start",
|
|
5278
|
-
"stop",
|
|
5279
|
-
"delete",
|
|
5280
|
-
"run"
|
|
5281
|
-
]);
|
|
5269
|
+
var RUN_HAS_NO_CLI = "pievo commands are not available inside a run; end your final response with a summary and a last line `STATUS: keep|no-change|block`";
|
|
5270
|
+
var FILE_USAGE = "pievo file list <loop> | file get <loop> <name> | file set <loop> <name> --from <path>";
|
|
5282
5271
|
var PAUSED_FINISHING = "loop paused; current run is finishing";
|
|
5283
5272
|
var STOP_UPGRADE_REQUIRED = "Daemon upgrade required to stop a running process. Run `npm install -g @kky42/pievo@latest`, then `pievo daemon restart`.";
|
|
5284
5273
|
var FORCE_DELETE_CONFIRMATION = "delete-server-data-anyway";
|
|
@@ -5316,15 +5305,6 @@ function parseJsonFlag(raw) {
|
|
|
5316
5305
|
value: obj
|
|
5317
5306
|
};
|
|
5318
5307
|
}
|
|
5319
|
-
/** A structured `error:`/`code:` TOON result for stdout.
|
|
5320
|
-
* Mirrors the `{code, text}` shape `dispatch` returns; the slug defaults from the
|
|
5321
|
-
* HTTP status but a caller may pin it (e.g. CONFLICT). */
|
|
5322
|
-
function derr(code, message, slug) {
|
|
5323
|
-
return {
|
|
5324
|
-
code,
|
|
5325
|
-
text: errorBlock(message, slug ?? codeForStatus(code))
|
|
5326
|
-
};
|
|
5327
|
-
}
|
|
5328
5308
|
/** The structured keys a `/api/machine/cli` body may carry. The daemon renders
|
|
5329
5309
|
* `text` and exits with `exitCode`; two structured channels remain because the
|
|
5330
5310
|
* current daemon reads them as data:
|
|
@@ -5352,18 +5332,6 @@ function finalizeCli(res) {
|
|
|
5352
5332
|
}
|
|
5353
5333
|
return res;
|
|
5354
5334
|
}
|
|
5355
|
-
function renderReportedText(status, hasMessage) {
|
|
5356
|
-
const parts = [];
|
|
5357
|
-
if (status) parts.push(`status=${status}`);
|
|
5358
|
-
if (hasMessage) parts.push("message recorded");
|
|
5359
|
-
return `reported: ${parts.length ? parts.join(" · ") : "recorded"}`;
|
|
5360
|
-
}
|
|
5361
|
-
var RUN_VERB_HELP = { report: {
|
|
5362
|
-
syntax: "report --status keep|no-change|block --message <text>",
|
|
5363
|
-
summary: "record this run's required status and non-empty summary",
|
|
5364
|
-
avail: () => "always available",
|
|
5365
|
-
help: ["Run `pievo report --status no-change --message \"no actionable change\"` to close this run", "Run `pievo report --status keep --message \"completed the requested work\"` to retain a result"]
|
|
5366
|
-
} };
|
|
5367
5335
|
var DEVICE_VERB_HELP = {
|
|
5368
5336
|
new: {
|
|
5369
5337
|
syntax: "new --json '<config>' [--dry-run]",
|
|
@@ -5402,24 +5370,29 @@ var DEVICE_VERB_HELP = {
|
|
|
5402
5370
|
},
|
|
5403
5371
|
delete: {
|
|
5404
5372
|
syntax: "delete <id> [--force]",
|
|
5405
|
-
summary: "stop first, then delete server history and
|
|
5406
|
-
help: ["Stop this loop and delete its Pievo history and
|
|
5373
|
+
summary: "stop first, then delete server history, memory, and artifacts",
|
|
5374
|
+
help: ["Stop this loop and delete its Pievo history, memory, and artifacts? Local project files are not deleted.", "--force requires a prior Delete request and explicit local confirmation; the local process may still be running"]
|
|
5407
5375
|
},
|
|
5408
5376
|
run: {
|
|
5409
5377
|
syntax: "run stop <run-id>",
|
|
5410
5378
|
summary: "stop one pending or running run without pausing its loop",
|
|
5411
5379
|
help: ["A running run remains running until the daemon confirms cancellation"]
|
|
5412
5380
|
},
|
|
5381
|
+
file: {
|
|
5382
|
+
syntax: "file list <id> | file get <id> <name> | file set <id> <name> --from <path>",
|
|
5383
|
+
summary: "inspect or replace a loop's MEMORY.md and artifact files",
|
|
5384
|
+
help: ["Run `pievo file get <id> MEMORY.md` to read the loop's memory", "An edit made while a run is executing wins over that run's version of the file"]
|
|
5385
|
+
},
|
|
5413
5386
|
log: {
|
|
5414
5387
|
syntax: "log [<id>] [--limit 1..20] [--since ISO] [--until ISO] [--status keep|no-change|block] [--phase done|error|canceled] [--run <index|UUID> [--diff]] [--json]",
|
|
5415
5388
|
summary: "bounded terminal history or one detailed run",
|
|
5416
5389
|
help: ["Run `pievo log <id> --json` for structured history", "Run `pievo log <id> --run 12 --diff` for bounded run detail and artifact diff"]
|
|
5417
5390
|
}
|
|
5418
5391
|
};
|
|
5419
|
-
function verbHelpText(verb
|
|
5420
|
-
const spec =
|
|
5392
|
+
function verbHelpText(verb) {
|
|
5393
|
+
const spec = DEVICE_VERB_HELP[verb];
|
|
5421
5394
|
if (!spec) return void 0;
|
|
5422
|
-
return doc(kvLine("verb", verb), kvLine("syntax", spec.syntax), kvLine("summary", spec.summary),
|
|
5395
|
+
return doc(kvLine("verb", verb), kvLine("syntax", spec.syntax), kvLine("summary", spec.summary), helpBlock(spec.help));
|
|
5423
5396
|
}
|
|
5424
5397
|
function loopEnvelope(loop) {
|
|
5425
5398
|
return { ...canonicalLoopEnvelope(loop) };
|
|
@@ -5456,7 +5429,8 @@ function renderShowText(loop, env, totalRuns, lastRun, opts = {}) {
|
|
|
5456
5429
|
const help = [
|
|
5457
5430
|
`Run \`pievo show ${loop.id} --full\` to see the complete prompt`,
|
|
5458
5431
|
`Run \`pievo edit ${loop.id} --json '{"schedule":{"mode":"continuous","delayMinutes":5}}'\` to change the schedule`,
|
|
5459
|
-
`Run \`pievo log ${loop.id}\` to see recent run results
|
|
5432
|
+
`Run \`pievo log ${loop.id}\` to see recent run results`,
|
|
5433
|
+
`Run \`pievo file list ${loop.id}\` to see the loop's memory and artifact files`
|
|
5460
5434
|
];
|
|
5461
5435
|
return doc(block, kvLine("nextFire", nextFireDisplay(loop)), `lifecycle: ${loop.enabled ? "active" : "paused"}`, `runs: ${runsTally}`, !loop.enabled ? kvLine("pauseCause", loop.pauseCause?.kind === "failure-streak" ? `failure-streak (run ${loop.pauseCause.runId}, count ${loop.pauseCause.count})` : loop.pauseCause?.kind === "blocked" ? `blocked (run ${loop.pauseCause.runId})` : loop.pauseCause?.kind === "owner" ? "owner" : "unknown") : null, lastRun?.reportIncident ? kvLine("reportIncident", `${lastRun.reportIncident.code} · ${lastRun.reportIncident.faultDomain} · ${lastRun.reportIncident.reason}`) : null, helpBlock(help));
|
|
5462
5436
|
}
|
|
@@ -5544,9 +5518,6 @@ function renderHomeText(ctx, presence, here, elsewhere, recent) {
|
|
|
5544
5518
|
"Run `pievo new --json '{...}'` to create a loop"
|
|
5545
5519
|
]));
|
|
5546
5520
|
}
|
|
5547
|
-
function reportStatus(s) {
|
|
5548
|
-
return s === "keep" || s === "no-change" || s === "block" ? s : void 0;
|
|
5549
|
-
}
|
|
5550
5521
|
/** Tiny flag parser: `--k v` pairs, bare `--flag` → true, first positional under `_`.
|
|
5551
5522
|
* Every key/value is NUL-stripped at this wire boundary before validation or
|
|
5552
5523
|
* persistence because Postgres text rejects NUL. */
|
|
@@ -5687,9 +5658,9 @@ function msg(err) {
|
|
|
5687
5658
|
*/
|
|
5688
5659
|
var boot_exports = /* @__PURE__ */ __exportAll({
|
|
5689
5660
|
ensureServer: () => ensureServer,
|
|
5690
|
-
getArtifactSync: () => getArtifactSync,
|
|
5691
5661
|
getCliGateway: () => getCliGateway,
|
|
5692
|
-
getGateway: () => getGateway
|
|
5662
|
+
getGateway: () => getGateway,
|
|
5663
|
+
getLoopFiles: () => getLoopFiles
|
|
5693
5664
|
});
|
|
5694
5665
|
var g = globalThis;
|
|
5695
5666
|
function ensureServer() {
|
|
@@ -5714,9 +5685,7 @@ async function boot() {
|
|
|
5714
5685
|
};
|
|
5715
5686
|
let gateway;
|
|
5716
5687
|
const scheduler = new Scheduler({ dispatch: (loop) => gateway.dispatcher.dispatch(loop) });
|
|
5717
|
-
|
|
5718
|
-
gateway = new MachineGateway(scheduler, blobStore);
|
|
5719
|
-
const artifactSync = new ArtifactSync(blobStore);
|
|
5688
|
+
gateway = new MachineGateway(scheduler, createBlobStore());
|
|
5720
5689
|
const cliGateway = new CliGateway(gateway);
|
|
5721
5690
|
await scheduler.start(abort.signal);
|
|
5722
5691
|
const sweep = setInterval(() => void gateway.sweep().catch((err) => logger.error({ err: String(err) }, "sweep tick failed")), ONLINE_TTL_MS);
|
|
@@ -5729,7 +5698,6 @@ async function boot() {
|
|
|
5729
5698
|
return {
|
|
5730
5699
|
scheduler,
|
|
5731
5700
|
gateway,
|
|
5732
|
-
artifactSync,
|
|
5733
5701
|
cliGateway,
|
|
5734
5702
|
abort,
|
|
5735
5703
|
shutdown
|
|
@@ -5738,11 +5706,11 @@ async function boot() {
|
|
|
5738
5706
|
async function getGateway() {
|
|
5739
5707
|
return (await ensureServer()).gateway;
|
|
5740
5708
|
}
|
|
5741
|
-
async function
|
|
5742
|
-
return (await ensureServer()).
|
|
5709
|
+
async function getLoopFiles() {
|
|
5710
|
+
return (await ensureServer()).gateway.loopFiles;
|
|
5743
5711
|
}
|
|
5744
5712
|
async function getCliGateway() {
|
|
5745
5713
|
return (await ensureServer()).cliGateway;
|
|
5746
5714
|
}
|
|
5747
5715
|
//#endregion
|
|
5748
|
-
export {
|
|
5716
|
+
export { listRuns as A, requestDeleteLoop as B, getRun as C, listArtifacts as D, isOutdated as E, machinePresence as F, tryDeleteLoop as G, sha256 as H, mintDeviceToken as I, validateLoopTags as J, updateLoop as K, pauseLoop as L, loopTags_CnVLi70h_exports as M, loopsForMachine as N, listLoops as O, machineIdFromToken as P, recentLoopUsage as R, getMachine as S, hasRunningRun as T, startLoop as U, requestRunCancel as V, stopLoop as W, createMachine as _, canonicalLoopSummary as a, getArtifactFile as b, getLoopFiles as c, validOptionalPollString as d, validPollCurrentRun as f, countRuns as g, CODING_AGENTS as h, canonicalLoopEnvelope as i, listRunsBefore as j, listMachines as k, readByteBody as l, validateLoopEdit as m, POLL_HTTP_BODY_FIELDS as n, daemonNeedsUpdate as o, validPollWireId as p, updateMachine as q, boot_DmpKtb0A_exports as r, ensureServer as s, MIN_DAEMON_VERSION as t, readJsonBody as u, forceDeleteLoop as v, hasPendingRun as w, getLoop as x, forceDeleteMachine as y, reconciliationStatesForRuns as z };
|