@kky42/pievo-server 3.0.0 → 4.2.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-C9-AzIkl.js +66 -0
- package/.output/public/assets/{actionUi-BZrLpgHT.js → actionUi-D47jwMfh.js} +1 -1
- package/.output/public/assets/app-C5xfpeJe.css +2 -0
- package/.output/public/assets/index-BgNjFXWE.js +10 -0
- package/.output/public/assets/loops._loopId-Bo63FXPQ.js +2 -0
- package/.output/public/assets/loops._loopId_.runs._runId-DW2HPjtj.js +3 -0
- package/.output/public/assets/{preload-helper-hRTYB_ju.js → preload-helper-CbYqBpY9.js} +2 -2
- package/.output/public/assets/routes-C88oSApK.js +1 -0
- package/.output/public/assets/routes-CTCh8xYm.js +1 -0
- package/.output/server/_ssr/{MachinesModal-CmVMec6B.mjs → MachinesModal-DwHy2cis.mjs} +44 -25
- package/.output/server/_ssr/MarkdownView-BOe75tli.mjs +67 -0
- package/.output/server/_ssr/{actionUi-DE-Pb2tv.mjs → actionUi-CszQXDSN.mjs} +20 -2
- package/.output/server/_ssr/artifactFiles-DJiK99sF.mjs +67 -0
- package/.output/server/_ssr/{artifactKind-DqpmtxDU.mjs → artifactKind-DYMU0q3N.mjs} +2 -7
- package/.output/server/_ssr/{boot-CYRi-wez.mjs → boot-CecCDRoH.mjs} +950 -968
- package/.output/server/_ssr/{lifecycleUi-BsbUsi5K.mjs → lifecycleUi-Bd7_sthp.mjs} +12 -19
- package/.output/server/_ssr/{loopApi-Bez1rHV0.mjs → loopApi-uEiBQ5YX.mjs} +17 -23
- package/.output/server/_ssr/{loopProjection-esCjyZJg.mjs → loopProjection-Bo54jjGd.mjs} +5 -3
- package/.output/server/_ssr/{loops._loopId-KUezNY69.mjs → loops._loopId-c1oRP7cr.mjs} +37 -117
- package/.output/server/_ssr/{loops._loopId_.runs._runId-DOkUEesG.mjs → loops._loopId_.runs._runId-Dl0SwOlC.mjs} +43 -23
- package/.output/server/_ssr/loops._loopId_.runs._runId-czMVvNsk.mjs +11 -0
- package/.output/server/_ssr/{machineFns-CniGcY8s.mjs → machineFns-aw9xe5oz.mjs} +6 -20
- package/.output/server/_ssr/{router-BFIH2UAE.mjs → router-3OQ849sN.mjs} +96 -112
- package/.output/server/_ssr/{routes-BSVBANI3.mjs → routes-CL4y6AEv.mjs} +2 -2
- package/.output/server/_ssr/{routes-DnE7XDe_.mjs → routes-CT5IdoiP.mjs} +6 -6
- package/.output/server/_ssr/{routes-ngpX7h8D.mjs → routes-nU2PDK3W.mjs} +2 -2
- package/.output/server/_ssr/ssr.mjs +22 -22
- package/.output/server/{_tanstack-start-manifest_v-DnwAupbZ.mjs → _tanstack-start-manifest_v-_nMS2ozp.mjs} +9 -10
- package/.output/server/drizzle/0001_loop_files.sql +9 -0
- package/.output/server/drizzle/0002_run_attention.sql +1 -0
- package/.output/server/drizzle/meta/_journal.json +15 -1
- package/.output/server/index.mjs +52 -45
- package/drizzle/0001_loop_files.sql +9 -0
- package/drizzle/0002_run_attention.sql +1 -0
- package/drizzle/meta/_journal.json +15 -1
- package/package.json +1 -1
- package/.output/public/assets/app-DXLuQNb9.css +0 -2
- package/.output/public/assets/index-Di_wpAtL.js +0 -10
- 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-BIOJZsig.js +0 -1
- package/.output/public/assets/routes-K8D5uBiV.js +0 -1
- package/.output/server/_ssr/artifactFiles-Di1xDKvX.mjs +0 -57
- package/.output/server/_ssr/loops._loopId_.runs._runId-BFP9oMrp.mjs +0 -16
|
@@ -11,114 +11,118 @@ 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-Bi7Bm7v8.js
|
|
15
|
+
var loopTags_Bi7Bm7v8_exports = /* @__PURE__ */ __exportAll$1({
|
|
16
|
+
$: () => loopIdsWithSnapshots,
|
|
17
|
+
$t: () => logger_exports,
|
|
18
|
+
A: () => enqueueRun,
|
|
19
|
+
At: () => updateMachine,
|
|
20
|
+
B: () => getRun,
|
|
21
|
+
Bt: () => loopFileIssue,
|
|
22
|
+
C: () => buildLoopManifest,
|
|
23
|
+
Ct: () => setMachineOnline,
|
|
24
|
+
D: () => createLoop,
|
|
25
|
+
Dt: () => store_exports,
|
|
26
|
+
E: () => countRuns,
|
|
27
|
+
Et: () => stopLoop,
|
|
28
|
+
F: () => getArtifactFile,
|
|
29
|
+
Ft: () => LoopFiles,
|
|
30
|
+
G: () => initializeCronCadence,
|
|
31
|
+
Gt: () => createBlobStore,
|
|
32
|
+
H: () => getTerminalReportIncidents,
|
|
33
|
+
Ht: () => loopFiles_exports,
|
|
34
|
+
I: () => getExactTerminalReportIncident,
|
|
35
|
+
It: () => MEMORY_CHAR_CAP,
|
|
36
|
+
J: () => listLoops,
|
|
37
|
+
Jt: () => runMigrations,
|
|
38
|
+
K: () => lastRun,
|
|
39
|
+
Kt: () => closeClient,
|
|
40
|
+
L: () => getLoop,
|
|
41
|
+
Lt: () => MEMORY_FILE,
|
|
42
|
+
M: () => finalizeRunningRun,
|
|
43
|
+
Mt: () => ARTIFACT_EXTENSIONS,
|
|
44
|
+
N: () => forceDeleteLoop,
|
|
45
|
+
Nt: () => EMPTY_SHA256,
|
|
46
|
+
O: () => createMachine,
|
|
47
|
+
Ot: () => tryDeleteLoop,
|
|
48
|
+
P: () => forceDeleteMachine,
|
|
49
|
+
Pt: () => LOOP_FILE_BYTE_CAP,
|
|
50
|
+
Q: () => liveBlobRefs,
|
|
51
|
+
Qt: () => logger,
|
|
52
|
+
R: () => getMachine,
|
|
53
|
+
Rt: () => fileLimitLabel,
|
|
54
|
+
S: () => blobSizes,
|
|
55
|
+
St: () => runningRunsForMachine,
|
|
56
|
+
T: () => countRunSnapshots,
|
|
57
|
+
Tt: () => startLoop,
|
|
58
|
+
U: () => hasPendingRun,
|
|
59
|
+
Ut: () => sha256Buf,
|
|
60
|
+
V: () => getRunSnapshot,
|
|
61
|
+
Vt: () => loopFileNames,
|
|
62
|
+
W: () => hasRunningRun,
|
|
63
|
+
Wt: () => validArtifactName,
|
|
64
|
+
X: () => listRuns,
|
|
65
|
+
Xt: () => CODING_AGENTS,
|
|
66
|
+
Y: () => listMachines,
|
|
67
|
+
Yt: () => runs,
|
|
68
|
+
Z: () => listRunsBefore,
|
|
69
|
+
Zt: () => coerceCodingAgent,
|
|
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: () => BLOB_GC_GRACE_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
|
-
|
|
99
|
+
nn: () => __exportAll,
|
|
100
|
+
nt: () => pauseLoop,
|
|
98
101
|
o: () => connectKeyValid,
|
|
99
|
-
ot: () =>
|
|
102
|
+
ot: () => pruneRunSnapshots,
|
|
100
103
|
p: () => resolveLease,
|
|
101
|
-
pt: () =>
|
|
102
|
-
q: () =>
|
|
103
|
-
qt: () =>
|
|
104
|
+
pt: () => reconciliationStatesForRuns,
|
|
105
|
+
q: () => listArtifacts,
|
|
106
|
+
qt: () => db,
|
|
104
107
|
r: () => isValidSemver,
|
|
105
|
-
rt: () =>
|
|
108
|
+
rt: () => pauseLoopState,
|
|
106
109
|
s: () => isDeviceTokenShape,
|
|
107
|
-
st: () =>
|
|
110
|
+
st: () => putReportReceiptIfAbsent,
|
|
108
111
|
t: () => validateLoopTags,
|
|
109
|
-
|
|
112
|
+
tn: () => GC_INTERVAL_MS,
|
|
113
|
+
tt: () => openRuns,
|
|
110
114
|
u: () => pruneExpiredLeases,
|
|
111
|
-
ut: () =>
|
|
112
|
-
v: () =>
|
|
113
|
-
vt: () =>
|
|
114
|
-
w: () =>
|
|
115
|
-
wt: () =>
|
|
116
|
-
x: () =>
|
|
117
|
-
xt: () =>
|
|
118
|
-
y: () =>
|
|
119
|
-
yt: () =>
|
|
120
|
-
z: () =>
|
|
121
|
-
zt: () =>
|
|
115
|
+
ut: () => recentLoopUsage,
|
|
116
|
+
v: () => acknowledgeRetiredTerminalIncident,
|
|
117
|
+
vt: () => repairTerminalRunLeases,
|
|
118
|
+
w: () => claimReadyRunForMachine,
|
|
119
|
+
wt: () => snapshotBlobRefs,
|
|
120
|
+
x: () => blobHashesOlderThan,
|
|
121
|
+
xt: () => runningRunForLoop,
|
|
122
|
+
y: () => advanceDueSchedules,
|
|
123
|
+
yt: () => requestDeleteLoop,
|
|
124
|
+
z: () => getReportReceipt,
|
|
125
|
+
zt: () => isValidHash
|
|
122
126
|
});
|
|
123
127
|
var import_pino = /* @__PURE__ */ __toESM(require_pino());
|
|
124
128
|
var import_pino_pretty = /* @__PURE__ */ __toESM(require_pino_pretty());
|
|
@@ -133,7 +137,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
133
137
|
return target;
|
|
134
138
|
};
|
|
135
139
|
/**
|
|
136
|
-
* Server data directory: the embedded database (`pgdata/`),
|
|
140
|
+
* Server data directory: the embedded database (`pgdata/`), loop-file bytes
|
|
137
141
|
* (`blobs/`), and the launcher's pid/lock/log files. Kept apart from the
|
|
138
142
|
* daemon's `~/.pievo` so a device running both never mixes their state.
|
|
139
143
|
* Override with `PIEVO_DATA_DIR` (absolute; `~` is not expanded).
|
|
@@ -150,9 +154,9 @@ var BLOB_GC_GRACE_MS = 3600 * 1e3;
|
|
|
150
154
|
/** Storage-maintenance cadence (prune snapshots, then GC blobs). 15 minutes. */
|
|
151
155
|
var GC_INTERVAL_MS = 900 * 1e3;
|
|
152
156
|
/**
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
157
|
+
* pino-pretty as a synchronous destination stream (not a worker-thread transport)
|
|
158
|
+
* so logs flush before process.exit() in the one-shot CLIs. Writes to stderr so
|
|
159
|
+
* stdout carries only CLI output.
|
|
156
160
|
*/
|
|
157
161
|
var logger_exports = /* @__PURE__ */ __exportAll({ logger: () => logger });
|
|
158
162
|
var level = (process.env.PIEVO_LOG_LEVEL || "info").toLowerCase();
|
|
@@ -163,17 +167,9 @@ var stream = (0, import_pino_pretty.default)({
|
|
|
163
167
|
ignore: "pid,hostname"
|
|
164
168
|
});
|
|
165
169
|
var logger = (0, import_pino.default)({ level }, stream);
|
|
166
|
-
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* integration lands; execution itself is real for every value.
|
|
170
|
-
*
|
|
171
|
-
* Runtime SINGLE SOURCE (anti-drift): every server consumer DERIVES from this
|
|
172
|
-
* array — the `CodingAgent` type here, the `db/schema.ts` `CodingAgent` type AND
|
|
173
|
-
* the `loops.agent` column enum, the edit validator (`coerceCodingAgent`), and the
|
|
174
|
-
* web agent `<select>` (LoopForm). So widening the set is a one-line edit HERE with
|
|
175
|
-
* no other server change (the daemon's own enum in `packages/daemon/src/create.ts`
|
|
176
|
-
* is a separate package, widened alongside). */
|
|
170
|
+
/** Single source for coding agents: server types, the `loops.agent` column enum,
|
|
171
|
+
* validators, and the dashboard select all derive from this array. The daemon
|
|
172
|
+
* keeps its own copy in `packages/daemon/src/create.ts`; widen both together. */
|
|
177
173
|
var CODING_AGENTS = [
|
|
178
174
|
"claude-code",
|
|
179
175
|
"codex",
|
|
@@ -193,10 +189,6 @@ var RESERVED_LOOP_TAGS = [
|
|
|
193
189
|
* Pievo is single-user: no row carries an owner. Timestamp values are ISO
|
|
194
190
|
* strings; JSON columns use `jsonb().$type<>()`.
|
|
195
191
|
*/
|
|
196
|
-
/** `skipped`: a pending run retired without executing (pause/retention cleanup,
|
|
197
|
-
* queue coalescing, or the offline catch-up window elapsed).
|
|
198
|
-
* Neither success nor failure — excluded from the failure streak (it rides
|
|
199
|
-
* phase `canceled`, and the streak counts only phase `error`). */
|
|
200
192
|
var machines = pgTable("machines", {
|
|
201
193
|
id: text("id").primaryKey(),
|
|
202
194
|
name: text("name").notNull(),
|
|
@@ -228,7 +220,8 @@ var loops = pgTable("loops", {
|
|
|
228
220
|
statusKeep: text("status_keep").notNull(),
|
|
229
221
|
statusNoChange: text("status_no_change").notNull(),
|
|
230
222
|
statusBlock: text("status_block").notNull(),
|
|
231
|
-
/**
|
|
223
|
+
/** Configured artifact names, in addition to the always-present MEMORY.md.
|
|
224
|
+
* See `gateway/loopFiles.ts`. */
|
|
232
225
|
artifacts: jsonb("artifacts").$type().notNull().default([]),
|
|
233
226
|
cron: text("cron").notNull(),
|
|
234
227
|
scheduleMode: text("schedule_mode", { enum: ["cron", "continuous"] }).notNull(),
|
|
@@ -245,11 +238,8 @@ var loops = pgTable("loops", {
|
|
|
245
238
|
pauseCause: jsonb("pause_cause").$type(),
|
|
246
239
|
model: text("model"),
|
|
247
240
|
reasoningEffort: text("reasoning_effort"),
|
|
248
|
-
/**
|
|
249
|
-
*
|
|
250
|
-
* `codex` → `codex exec`, `pi` → Pi print mode). Measured from the creating CLI's env when
|
|
251
|
-
* detectable, else the declared/selected value. The text column carries
|
|
252
|
-
* both a TypeScript enum and a database CHECK constraint. */
|
|
241
|
+
/** The daemon launches this agent for every run (`buildAgentSpawn`). Enforced by
|
|
242
|
+
* both the TypeScript enum and a database CHECK constraint. */
|
|
253
243
|
agent: text("agent", { enum: CODING_AGENTS }).notNull(),
|
|
254
244
|
enabled: boolean("enabled").notNull().default(true),
|
|
255
245
|
nextRunAt: text("next_run_at"),
|
|
@@ -277,6 +267,9 @@ var runs = pgTable("runs", {
|
|
|
277
267
|
model: text("model"),
|
|
278
268
|
reasoningEffort: text("reasoning_effort"),
|
|
279
269
|
runIndex: integer("run_index"),
|
|
270
|
+
/** A pending run retired without executing (pause, retention cleanup, queue
|
|
271
|
+
* coalescing, or an elapsed offline catch-up window) is `canceled`, not `error`,
|
|
272
|
+
* so it never counts toward the failure streak. */
|
|
280
273
|
phase: text("phase", { enum: [
|
|
281
274
|
"pending",
|
|
282
275
|
"running",
|
|
@@ -296,6 +289,9 @@ var runs = pgTable("runs", {
|
|
|
296
289
|
"block"
|
|
297
290
|
] }),
|
|
298
291
|
message: text("message"),
|
|
292
|
+
/** Owner-facing reason from the final response's ATTENTION line. Independent
|
|
293
|
+
* of status: it never pauses the loop. */
|
|
294
|
+
attention: text("attention"),
|
|
299
295
|
durationMs: integer("duration_ms"),
|
|
300
296
|
exitCode: integer("exit_code"),
|
|
301
297
|
finalText: text("final_text"),
|
|
@@ -308,7 +304,11 @@ var runs = pgTable("runs", {
|
|
|
308
304
|
mode: "string"
|
|
309
305
|
}),
|
|
310
306
|
heartbeatAt: text("heartbeat_at"),
|
|
311
|
-
reportIncident: jsonb("report_incident").$type()
|
|
307
|
+
reportIncident: jsonb("report_incident").$type(),
|
|
308
|
+
/** Loop-file versions this run produced but that were not kept (over limit,
|
|
309
|
+
* owner edited during the run, upload lost). Shown on the run and repeated in
|
|
310
|
+
* the next run's prompt so the agent can correct course. */
|
|
311
|
+
fileNotices: jsonb("file_notices").$type()
|
|
312
312
|
}, (t) => [
|
|
313
313
|
index("runs_loop_idx").on(t.loopId),
|
|
314
314
|
index("runs_phase_idx").on(t.phase),
|
|
@@ -387,21 +387,22 @@ var blobs = pgTable("blobs", {
|
|
|
387
387
|
createdAt: text("created_at").notNull()
|
|
388
388
|
});
|
|
389
389
|
/**
|
|
390
|
-
* The current
|
|
391
|
-
*
|
|
392
|
-
* (
|
|
393
|
-
*
|
|
390
|
+
* The current file set of each loop — one row per live or tombstoned name.
|
|
391
|
+
* `hash` → `blobs.hash`; null for a tombstoned (deleted or unconfigured) file.
|
|
392
|
+
* The unique (loopId, path) index is the upsert key owner writes and run
|
|
393
|
+
* finalization share.
|
|
394
394
|
*/
|
|
395
395
|
var artifactFiles = pgTable("artifact_files", {
|
|
396
396
|
id: text("id").primaryKey(),
|
|
397
397
|
loopId: text("loop_id").notNull(),
|
|
398
|
-
/** Normalized workdir-relative path (never absolute or escaping the workdir). */
|
|
399
398
|
path: text("path").notNull(),
|
|
400
399
|
hash: text("hash"),
|
|
401
400
|
size: integer("size"),
|
|
402
|
-
/**
|
|
401
|
+
/** Legacy snapshot/diff metadata. Loop-file ingress rejects NUL, so new rows
|
|
402
|
+
* always store false. */
|
|
403
403
|
binary: boolean("binary").notNull().default(false),
|
|
404
|
-
/**
|
|
404
|
+
/** Legacy snapshot/diff metadata. Loop-file ingress rejects over-limit versions,
|
|
405
|
+
* so new rows always store false. */
|
|
405
406
|
oversize: boolean("oversize").notNull().default(false),
|
|
406
407
|
deleted: boolean("deleted").notNull().default(false),
|
|
407
408
|
updatedAt: text("updated_at").notNull()
|
|
@@ -411,7 +412,7 @@ var artifactFiles = pgTable("artifact_files", {
|
|
|
411
412
|
index("artifact_files_hash_idx").on(t.hash)
|
|
412
413
|
]);
|
|
413
414
|
/**
|
|
414
|
-
* The loop's full
|
|
415
|
+
* The loop's full loop-file manifest captured at each run's finalize — the input
|
|
415
416
|
* to the per-run diff. Written cheaply on report (no diff computed on write);
|
|
416
417
|
* `getRunDiff` lazily compares run N's snapshot with the preceding snapshot.
|
|
417
418
|
* One row per run (runId primary key).
|
|
@@ -476,7 +477,6 @@ function migrationsDir() {
|
|
|
476
477
|
if (fs.existsSync(builtDir)) return builtDir;
|
|
477
478
|
return devDir;
|
|
478
479
|
}
|
|
479
|
-
/** Apply generated migrations in-process (idempotent). */
|
|
480
480
|
async function runMigrations() {
|
|
481
481
|
const dir = migrationsDir();
|
|
482
482
|
if (!fs.existsSync(dir)) return;
|
|
@@ -496,16 +496,310 @@ async function closeClient() {
|
|
|
496
496
|
} catch {}
|
|
497
497
|
}
|
|
498
498
|
/**
|
|
499
|
-
*
|
|
500
|
-
*
|
|
499
|
+
* Content-addressed blob byte store for loop files.
|
|
500
|
+
*
|
|
501
|
+
* Bytes are keyed by their sha256 hash (content-addressed ⇒ deduped across every
|
|
502
|
+
* loop/run). The store ONLY writes/reads bytes — it never executes or interprets
|
|
503
|
+
* them — preserving the server's zero-exec invariant.
|
|
504
|
+
*
|
|
505
|
+
* `LocalBlobStore` is the durable store under `<dataDir>/blobs`;
|
|
506
|
+
* `MemoryBlobStore` is the injected test fake.
|
|
507
|
+
*/
|
|
508
|
+
var log$3 = logger.child({ mod: "blobstore" });
|
|
509
|
+
var SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
510
|
+
/** Durable filesystem adapter. Hash sharding bounds entries per directory; atomic
|
|
511
|
+
* temp-file renames ensure readers see either complete bytes or no object. */
|
|
512
|
+
var LocalBlobStore = class {
|
|
513
|
+
root;
|
|
514
|
+
initialized;
|
|
515
|
+
constructor(root = path.join(dataDir(), "blobs")) {
|
|
516
|
+
this.root = root;
|
|
517
|
+
}
|
|
518
|
+
file(hash) {
|
|
519
|
+
if (!SHA256_HEX.test(hash)) throw new Error("invalid sha256 blob key");
|
|
520
|
+
return path.join(this.root, hash.slice(0, 2), hash);
|
|
521
|
+
}
|
|
522
|
+
async has(hash) {
|
|
523
|
+
const target = this.file(hash);
|
|
524
|
+
await this.ensureInitialized();
|
|
525
|
+
try {
|
|
526
|
+
return (await fs$1.stat(target)).isFile();
|
|
527
|
+
} catch (err) {
|
|
528
|
+
if (err.code === "ENOENT") return false;
|
|
529
|
+
throw err;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
async syncDir(dir) {
|
|
533
|
+
let handle;
|
|
534
|
+
try {
|
|
535
|
+
handle = await fs$1.open(dir, "r");
|
|
536
|
+
await handle.sync();
|
|
537
|
+
} catch (err) {
|
|
538
|
+
const code = err.code;
|
|
539
|
+
if (![
|
|
540
|
+
"EINVAL",
|
|
541
|
+
"ENOTSUP",
|
|
542
|
+
"EBADF",
|
|
543
|
+
"EPERM",
|
|
544
|
+
"ENOENT"
|
|
545
|
+
].includes(code)) throw err;
|
|
546
|
+
} finally {
|
|
547
|
+
await handle?.close().catch(() => {});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
ensureInitialized() {
|
|
551
|
+
return this.initialized ??= (async () => {
|
|
552
|
+
await fs$1.mkdir(this.root, { recursive: true });
|
|
553
|
+
await this.syncDir(path.dirname(path.dirname(this.root)));
|
|
554
|
+
await this.syncDir(path.dirname(this.root));
|
|
555
|
+
await this.syncDir(this.root);
|
|
556
|
+
for (const shard of await fs$1.readdir(this.root, { withFileTypes: true })) {
|
|
557
|
+
if (!shard.isDirectory() || !/^[a-f0-9]{2}$/.test(shard.name)) continue;
|
|
558
|
+
const dir = path.join(this.root, shard.name);
|
|
559
|
+
let removed = false;
|
|
560
|
+
for (const name of await fs$1.readdir(dir)) {
|
|
561
|
+
if (!/^\.[a-f0-9]{64}\.\d+\.[0-9a-f-]+\.tmp$/.test(name)) continue;
|
|
562
|
+
await fs$1.rm(path.join(dir, name), { force: true });
|
|
563
|
+
removed = true;
|
|
564
|
+
}
|
|
565
|
+
if (removed) await this.syncDir(dir);
|
|
566
|
+
}
|
|
567
|
+
})();
|
|
568
|
+
}
|
|
569
|
+
async put(hash, bytes) {
|
|
570
|
+
const target = this.file(hash);
|
|
571
|
+
await this.ensureInitialized();
|
|
572
|
+
const dir = path.dirname(target);
|
|
573
|
+
await fs$1.mkdir(dir, { recursive: true });
|
|
574
|
+
await this.syncDir(this.root);
|
|
575
|
+
const temp = path.join(dir, `.${hash}.${process.pid}.${randomUUID()}.tmp`);
|
|
576
|
+
let handle;
|
|
577
|
+
try {
|
|
578
|
+
handle = await fs$1.open(temp, "wx", 384);
|
|
579
|
+
await handle.writeFile(bytes);
|
|
580
|
+
await handle.sync();
|
|
581
|
+
await handle.close();
|
|
582
|
+
handle = void 0;
|
|
583
|
+
try {
|
|
584
|
+
await fs$1.rename(temp, target);
|
|
585
|
+
await this.syncDir(dir);
|
|
586
|
+
} catch (err) {
|
|
587
|
+
if ([
|
|
588
|
+
"EEXIST",
|
|
589
|
+
"EACCES",
|
|
590
|
+
"EPERM"
|
|
591
|
+
].includes(err.code ?? "") && await this.has(hash)) {
|
|
592
|
+
await fs$1.rm(temp, { force: true });
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
throw err;
|
|
596
|
+
}
|
|
597
|
+
} catch (err) {
|
|
598
|
+
await handle?.close().catch(() => {});
|
|
599
|
+
await fs$1.rm(temp, { force: true }).catch(() => {});
|
|
600
|
+
throw err;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
async get(hash) {
|
|
604
|
+
const target = this.file(hash);
|
|
605
|
+
await this.ensureInitialized();
|
|
606
|
+
try {
|
|
607
|
+
return await fs$1.readFile(target);
|
|
608
|
+
} catch (err) {
|
|
609
|
+
if (err.code === "ENOENT") return null;
|
|
610
|
+
throw err;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
async delete(hash) {
|
|
614
|
+
const target = this.file(hash);
|
|
615
|
+
await this.ensureInitialized();
|
|
616
|
+
await fs$1.rm(target, { force: true });
|
|
617
|
+
await this.syncDir(path.dirname(target));
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
/** The one local blob store shared by loop-file writes/reads and GC. */
|
|
621
|
+
function createBlobStore() {
|
|
622
|
+
const local = new LocalBlobStore();
|
|
623
|
+
log$3.info({ root: local.root }, "blob store: local filesystem");
|
|
624
|
+
return local;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Loop files: a loop's server-owned memory and display artifacts.
|
|
628
|
+
*
|
|
629
|
+
* Every loop has the required `MEMORY.md` plus up to MAX_ARTIFACTS configured
|
|
630
|
+
* text/code artifact names. The server is authoritative: bytes live in the
|
|
631
|
+
* content-addressed BlobStore and the current version of each name is an
|
|
632
|
+
* `artifact_files` row. For each run the daemon downloads the current files into
|
|
633
|
+
* a private run directory, and the terminal report carries the changed versions
|
|
634
|
+
* back with the hash each edit started from, so an owner edit made during the run
|
|
635
|
+
* wins over the agent's (base-hash check under the loop lock).
|
|
636
|
+
*
|
|
637
|
+
* Limits are part of the agent-facing prompt contract and are mirrored in the
|
|
638
|
+
* daemon (`packages/daemon/src/loop-files.ts`); change both together. An over-limit
|
|
639
|
+
* version is rejected and the previous version kept, never truncated, because a cut
|
|
640
|
+
* HTML file renders broken and a cut memory file silently loses its newest notes.
|
|
501
641
|
*/
|
|
642
|
+
var loopFiles_exports = /* @__PURE__ */ __exportAll({
|
|
643
|
+
ARTIFACT_BYTE_CAP: () => ARTIFACT_BYTE_CAP,
|
|
644
|
+
ARTIFACT_EXTENSIONS: () => ARTIFACT_EXTENSIONS,
|
|
645
|
+
EMPTY_SHA256: () => EMPTY_SHA256,
|
|
646
|
+
LOOP_FILE_BYTE_CAP: () => LOOP_FILE_BYTE_CAP,
|
|
647
|
+
LoopFiles: () => LoopFiles,
|
|
648
|
+
MAX_ARTIFACTS: () => 4,
|
|
649
|
+
MEMORY_CHAR_CAP: () => MEMORY_CHAR_CAP,
|
|
650
|
+
MEMORY_FILE: () => MEMORY_FILE,
|
|
651
|
+
fileLimitLabel: () => fileLimitLabel,
|
|
652
|
+
isValidHash: () => isValidHash,
|
|
653
|
+
loopFileIssue: () => loopFileIssue,
|
|
654
|
+
loopFileNames: () => loopFileNames,
|
|
655
|
+
sha256Buf: () => sha256Buf,
|
|
656
|
+
validArtifactName: () => validArtifactName
|
|
657
|
+
});
|
|
658
|
+
var MEMORY_FILE = "MEMORY.md";
|
|
659
|
+
/** Memory is re-read by every run, so it is bounded in characters (Unicode code
|
|
660
|
+
* points), roughly 2k tokens of prompt context per run. */
|
|
661
|
+
var MEMORY_CHAR_CAP = 8e3;
|
|
662
|
+
/** 256 KB bounds a run's upload and keeps dashboard rendering smooth. */
|
|
663
|
+
var ARTIFACT_BYTE_CAP = 256 * 1024;
|
|
664
|
+
var ARTIFACT_LIMIT_LABEL = `${ARTIFACT_BYTE_CAP / 1024} KB`;
|
|
665
|
+
/** sha256 of zero bytes: the version of a loop file that has never been written. */
|
|
666
|
+
var EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
667
|
+
/** Text/code formats accepted as artifacts. Loop files are NUL-free text, so
|
|
668
|
+
* binary formats (images, PDFs, archives) are deliberately absent; `.svg` is
|
|
669
|
+
* text and renders through the hardened inline image route. */
|
|
670
|
+
var ARTIFACT_EXTENSIONS = [
|
|
671
|
+
"md",
|
|
672
|
+
"markdown",
|
|
673
|
+
"mdx",
|
|
674
|
+
"html",
|
|
675
|
+
"htm",
|
|
676
|
+
"txt",
|
|
677
|
+
"text",
|
|
678
|
+
"rst",
|
|
679
|
+
"adoc",
|
|
680
|
+
"org",
|
|
681
|
+
"sh",
|
|
682
|
+
"bash",
|
|
683
|
+
"zsh",
|
|
684
|
+
"fish",
|
|
685
|
+
"ps1",
|
|
686
|
+
"bat",
|
|
687
|
+
"cmd",
|
|
688
|
+
"py",
|
|
689
|
+
"rb",
|
|
690
|
+
"pl",
|
|
691
|
+
"lua",
|
|
692
|
+
"r",
|
|
693
|
+
"js",
|
|
694
|
+
"mjs",
|
|
695
|
+
"cjs",
|
|
696
|
+
"ts",
|
|
697
|
+
"tsx",
|
|
698
|
+
"jsx",
|
|
699
|
+
"go",
|
|
700
|
+
"rs",
|
|
701
|
+
"java",
|
|
702
|
+
"kt",
|
|
703
|
+
"kts",
|
|
704
|
+
"scala",
|
|
705
|
+
"c",
|
|
706
|
+
"h",
|
|
707
|
+
"cc",
|
|
708
|
+
"cpp",
|
|
709
|
+
"hpp",
|
|
710
|
+
"cs",
|
|
711
|
+
"php",
|
|
712
|
+
"swift",
|
|
713
|
+
"dart",
|
|
714
|
+
"ex",
|
|
715
|
+
"exs",
|
|
716
|
+
"erl",
|
|
717
|
+
"hs",
|
|
718
|
+
"clj",
|
|
719
|
+
"cljs",
|
|
720
|
+
"json",
|
|
721
|
+
"jsonl",
|
|
722
|
+
"yaml",
|
|
723
|
+
"yml",
|
|
724
|
+
"toml",
|
|
725
|
+
"xml",
|
|
726
|
+
"csv",
|
|
727
|
+
"tsv",
|
|
728
|
+
"svg",
|
|
729
|
+
"css",
|
|
730
|
+
"scss",
|
|
731
|
+
"sass",
|
|
732
|
+
"less",
|
|
733
|
+
"sql",
|
|
734
|
+
"ini",
|
|
735
|
+
"conf",
|
|
736
|
+
"cfg",
|
|
737
|
+
"env",
|
|
738
|
+
"properties",
|
|
739
|
+
"diff",
|
|
740
|
+
"patch",
|
|
741
|
+
"tex",
|
|
742
|
+
"log"
|
|
743
|
+
];
|
|
744
|
+
var ARTIFACT_EXTENSION_SET = new Set(ARTIFACT_EXTENSIONS);
|
|
745
|
+
var ARTIFACT_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}\.([A-Za-z0-9]{1,16})$/;
|
|
746
|
+
function sha256Buf(bytes) {
|
|
747
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
748
|
+
}
|
|
749
|
+
function isValidHash(hash) {
|
|
750
|
+
return typeof hash === "string" && /^[0-9a-f]{64}$/.test(hash);
|
|
751
|
+
}
|
|
752
|
+
/** A plain text/code file name (no directories) other than MEMORY.md. */
|
|
753
|
+
function validArtifactName(name) {
|
|
754
|
+
if (typeof name !== "string" || name.toLowerCase() === "MEMORY.md".toLowerCase()) return false;
|
|
755
|
+
const match = ARTIFACT_NAME_RE.exec(name);
|
|
756
|
+
return match !== null && ARTIFACT_EXTENSION_SET.has(match[1].toLowerCase());
|
|
757
|
+
}
|
|
758
|
+
function loopFileNames(loop) {
|
|
759
|
+
return [MEMORY_FILE, ...loop.artifacts];
|
|
760
|
+
}
|
|
761
|
+
function fileLimitLabel(name) {
|
|
762
|
+
return name === "MEMORY.md" ? `${MEMORY_CHAR_CAP} characters` : ARTIFACT_LIMIT_LABEL;
|
|
763
|
+
}
|
|
764
|
+
/** Why `bytes` cannot become the next version of `name`, or undefined if it can. */
|
|
765
|
+
function loopFileIssue(name, bytes) {
|
|
766
|
+
if (bytes.includes(0)) return `${name}: binary content is not allowed`;
|
|
767
|
+
if (name === "MEMORY.md") {
|
|
768
|
+
const chars = Array.from(bytes.toString("utf8")).length;
|
|
769
|
+
if (chars > 8e3) return `${name}: ${chars} characters exceeds the ${MEMORY_CHAR_CAP}-character limit`;
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (bytes.length > 262144) return `${name}: ${(bytes.length / 1024).toFixed(1)} KB exceeds the ${ARTIFACT_LIMIT_LABEL} limit`;
|
|
773
|
+
}
|
|
774
|
+
/** The largest byte size any loop file can have (a code point is at most 4 bytes). */
|
|
775
|
+
var LOOP_FILE_BYTE_CAP = Math.max(ARTIFACT_BYTE_CAP, MEMORY_CHAR_CAP * 4);
|
|
776
|
+
var LoopFiles = class {
|
|
777
|
+
blobStore;
|
|
778
|
+
constructor(blobStore = createBlobStore()) {
|
|
779
|
+
this.blobStore = blobStore;
|
|
780
|
+
}
|
|
781
|
+
readBlob(hash) {
|
|
782
|
+
if (!isValidHash(hash)) return Promise.resolve(null);
|
|
783
|
+
if (hash === "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") return Promise.resolve(Buffer.alloc(0));
|
|
784
|
+
return this.blobStore.get(hash);
|
|
785
|
+
}
|
|
786
|
+
/** Persist bytes under their own hash. Idempotent; callers record the metadata row. */
|
|
787
|
+
async putBlob(bytes) {
|
|
788
|
+
const hash = sha256Buf(bytes);
|
|
789
|
+
if (hash !== "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" && !await this.blobStore.has(hash)) await this.blobStore.put(hash, bytes);
|
|
790
|
+
return hash;
|
|
791
|
+
}
|
|
792
|
+
async hasBlob(hash) {
|
|
793
|
+
return hash === "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" || this.blobStore.has(hash);
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
/** Data-access layer over Drizzle. Loop-locked state transitions live here. */
|
|
502
797
|
var store_exports = /* @__PURE__ */ __exportAll({
|
|
503
798
|
TERMINAL_REPORT_GRACE_MS: () => TERMINAL_REPORT_GRACE_MS,
|
|
504
799
|
acknowledgeRetiredReport: () => acknowledgeRetiredReport,
|
|
505
800
|
acknowledgeRetiredTerminalIncident: () => acknowledgeRetiredTerminalIncident,
|
|
506
801
|
advanceDueSchedules: () => advanceDueSchedules,
|
|
507
802
|
artifactFileReferencesHash: () => artifactFileReferencesHash,
|
|
508
|
-
blobExists: () => blobExists,
|
|
509
803
|
blobHashesOlderThan: () => blobHashesOlderThan,
|
|
510
804
|
blobSizes: () => blobSizes,
|
|
511
805
|
buildLoopManifest: () => buildLoopManifest,
|
|
@@ -540,24 +834,22 @@ var store_exports = /* @__PURE__ */ __exportAll({
|
|
|
540
834
|
liveBlobRefs: () => liveBlobRefs,
|
|
541
835
|
loopIdsWithSnapshots: () => loopIdsWithSnapshots,
|
|
542
836
|
loopsForMachine: () => loopsForMachine,
|
|
543
|
-
machineReferencesBlob: () => machineReferencesBlob,
|
|
544
837
|
newLoopId: () => newLoopId,
|
|
545
838
|
nextCronAt: () => nextCronAt,
|
|
546
839
|
openRuns: () => openRuns,
|
|
547
840
|
pauseLoop: () => pauseLoop,
|
|
548
841
|
pauseLoopState: () => pauseLoopState,
|
|
549
842
|
prevRunSnapshot: () => prevRunSnapshot,
|
|
843
|
+
previousRunFileNotices: () => previousRunFileNotices,
|
|
550
844
|
pruneRunSnapshots: () => pruneRunSnapshots,
|
|
551
845
|
putReportReceiptIfAbsent: () => putReportReceiptIfAbsent,
|
|
552
846
|
putRunSnapshot: () => putRunSnapshot,
|
|
553
847
|
putTerminalReportIncidentIfAbsent: () => putTerminalReportIncidentIfAbsent,
|
|
554
848
|
recentLoopUsage: () => recentLoopUsage,
|
|
555
849
|
reclaimRun: () => reclaimRun,
|
|
556
|
-
reconcileConfiguredArtifacts: () => reconcileConfiguredArtifacts,
|
|
557
850
|
reconcileReclaimedRun: () => reconcileReclaimedRun,
|
|
558
851
|
reconciliationStatesForRuns: () => reconciliationStatesForRuns,
|
|
559
852
|
recordBlob: () => recordBlob,
|
|
560
|
-
recordRunReportOnce: () => recordRunReportOnce,
|
|
561
853
|
refreshRunHeartbeats: () => refreshRunHeartbeats,
|
|
562
854
|
rejectTerminalReport: () => rejectTerminalReport,
|
|
563
855
|
releaseAbsentReconciliations: () => releaseAbsentReconciliations,
|
|
@@ -574,7 +866,7 @@ var store_exports = /* @__PURE__ */ __exportAll({
|
|
|
574
866
|
tryDeleteLoop: () => tryDeleteLoop,
|
|
575
867
|
updateLoop: () => updateLoop,
|
|
576
868
|
updateMachine: () => updateMachine,
|
|
577
|
-
|
|
869
|
+
writeLoopFile: () => writeLoopFile
|
|
578
870
|
});
|
|
579
871
|
function newLoopId() {
|
|
580
872
|
return `loop-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
|
@@ -697,7 +989,7 @@ async function updateLoopTx(tx, current, patch, at) {
|
|
|
697
989
|
}).where(eq(loops.id, current.id)).returning())[0];
|
|
698
990
|
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
991
|
if (patch.artifacts !== void 0) {
|
|
700
|
-
const allowed = new Set(patch.artifacts ?? []);
|
|
992
|
+
const allowed = new Set([MEMORY_FILE, ...patch.artifacts ?? []]);
|
|
701
993
|
const removed = (await tx.select({
|
|
702
994
|
id: artifactFiles.id,
|
|
703
995
|
path: artifactFiles.path
|
|
@@ -1040,26 +1332,32 @@ async function activeRunForReportTx(tx, loopId, runId, leaseTokenHash) {
|
|
|
1040
1332
|
run
|
|
1041
1333
|
} : { state: "run-not-running" };
|
|
1042
1334
|
}
|
|
1043
|
-
/**
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1046
|
-
async function
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
if (
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1335
|
+
/** Apply a run's loop-file versions under the loop lock. A file whose current
|
|
1336
|
+
* version is no longer the one the run started from was changed by the owner (or a
|
|
1337
|
+
* newer run) meanwhile; that version wins and the agent's edit becomes a notice. */
|
|
1338
|
+
async function applyLoopFileUpdatesTx(tx, loop, updates, at) {
|
|
1339
|
+
const notices = [];
|
|
1340
|
+
const configured = new Set(loopFileNames(loop));
|
|
1341
|
+
for (const update of updates) {
|
|
1342
|
+
if (!configured.has(update.name)) {
|
|
1343
|
+
notices.push(`${update.name}: no longer configured on this loop; this run's version was discarded`);
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
const row = (await tx.select().from(artifactFiles).where(and(eq(artifactFiles.loopId, loop.id), eq(artifactFiles.path, update.name))).limit(1).for("update"))[0];
|
|
1347
|
+
if ((row && !row.deleted && row.hash ? row.hash : "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") !== update.baseHash) {
|
|
1348
|
+
notices.push(`${update.name}: changed outside this run while it was running; that version was kept and this run's edit was discarded`);
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
await writeConfiguredArtifactTx(tx, {
|
|
1352
|
+
loopId: loop.id,
|
|
1353
|
+
path: update.name,
|
|
1354
|
+
hash: update.hash,
|
|
1355
|
+
size: update.size,
|
|
1356
|
+
binary: false,
|
|
1357
|
+
oversize: false
|
|
1358
|
+
}, at);
|
|
1359
|
+
}
|
|
1360
|
+
return notices;
|
|
1063
1361
|
}
|
|
1064
1362
|
async function finalizeTerminalRunTx(tx, input) {
|
|
1065
1363
|
if (input.receipt) await lockReportIdTx(tx, input.receipt.reportId);
|
|
@@ -1074,8 +1372,10 @@ async function finalizeTerminalRunTx(tx, input) {
|
|
|
1074
1372
|
if (!target) return void 0;
|
|
1075
1373
|
const runIndex = await ensureRunIndexTx(tx, target);
|
|
1076
1374
|
const at = typeof input.runPatch.ts === "string" ? input.runPatch.ts : nowIso$1();
|
|
1375
|
+
const fileNotices = [...input.runPatch.fileNotices ?? [], ...input.fileUpdates?.length ? await applyLoopFileUpdatesTx(tx, current, input.fileUpdates, at) : []];
|
|
1077
1376
|
const run = (await tx.update(runs).set({
|
|
1078
1377
|
...input.runPatch,
|
|
1378
|
+
fileNotices: fileNotices.length ? fileNotices : null,
|
|
1079
1379
|
runIndex,
|
|
1080
1380
|
updatedAt: at
|
|
1081
1381
|
}).where(and(eq(runs.id, input.runId), eq(runs.loopId, input.loopId), eq(runs.phase, input.runPhase))).returning())[0];
|
|
@@ -1207,12 +1507,13 @@ async function rejectTerminalReport(input) {
|
|
|
1207
1507
|
}
|
|
1208
1508
|
/** Atomically CAS running→terminal and apply the report's loop patch.
|
|
1209
1509
|
* The loop patch is unreachable when cancel/reclaim/another report won. */
|
|
1210
|
-
async function finalizeRunningRun(loopId, runId, runPatch, loopPatch = {}, leaseTokenHash, failureAutopauseStreak = 0, receipt) {
|
|
1510
|
+
async function finalizeRunningRun(loopId, runId, runPatch, loopPatch = {}, leaseTokenHash, failureAutopauseStreak = 0, receipt, fileUpdates) {
|
|
1211
1511
|
return db.transaction(async (tx) => {
|
|
1212
1512
|
return finalizeTerminalRunTx(tx, {
|
|
1213
1513
|
loopId,
|
|
1214
1514
|
runId,
|
|
1215
1515
|
runPhase: "running",
|
|
1516
|
+
fileUpdates,
|
|
1216
1517
|
runPatch,
|
|
1217
1518
|
loopPatch,
|
|
1218
1519
|
leaseTokenHash,
|
|
@@ -1271,12 +1572,13 @@ async function releaseAbsentReconciliations(machineId, recoverableRunIds, at = n
|
|
|
1271
1572
|
* exactly once. Keeping the lease check/delete beside the run + loop writes closes the
|
|
1272
1573
|
* error→error double-report hole where a phase CAS alone cannot distinguish the
|
|
1273
1574
|
* first real failure from a second concurrent one. */
|
|
1274
|
-
async function reconcileReclaimedRun(loopId, runId, leaseTokenHash, runPatch, loopPatch = {}, failureAutopauseStreak = 0, receipt) {
|
|
1575
|
+
async function reconcileReclaimedRun(loopId, runId, leaseTokenHash, runPatch, loopPatch = {}, failureAutopauseStreak = 0, receipt, fileUpdates) {
|
|
1275
1576
|
return db.transaction(async (tx) => {
|
|
1276
1577
|
return finalizeTerminalRunTx(tx, {
|
|
1277
1578
|
loopId,
|
|
1278
1579
|
runId,
|
|
1279
1580
|
runPhase: "error",
|
|
1581
|
+
fileUpdates,
|
|
1280
1582
|
runPatch,
|
|
1281
1583
|
loopPatch,
|
|
1282
1584
|
leaseTokenHash,
|
|
@@ -1521,9 +1823,6 @@ async function setMachineOnline(id, online) {
|
|
|
1521
1823
|
lastSeen: nowIso$1()
|
|
1522
1824
|
}).where(eq(machines.id, id));
|
|
1523
1825
|
}
|
|
1524
|
-
async function blobExists(hash) {
|
|
1525
|
-
return !!(await db.select({ hash: blobs.hash }).from(blobs).where(eq(blobs.hash, hash)))[0];
|
|
1526
|
-
}
|
|
1527
1826
|
/** Verified byte lengths recorded at blob ingress, used to budget reads before
|
|
1528
1827
|
* touching the byte store. Missing metadata is intentionally not estimated. */
|
|
1529
1828
|
async function blobSizes(hashes) {
|
|
@@ -1541,13 +1840,6 @@ async function recordBlob(hash, size) {
|
|
|
1541
1840
|
createdAt: nowIso$1()
|
|
1542
1841
|
}).onConflictDoNothing();
|
|
1543
1842
|
}
|
|
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
1843
|
async function writeConfiguredArtifactTx(tx, input, ts) {
|
|
1552
1844
|
await tx.insert(artifactFiles).values({
|
|
1553
1845
|
id: randomUUID(),
|
|
@@ -1571,36 +1863,32 @@ async function writeConfiguredArtifactTx(tx, input, ts) {
|
|
|
1571
1863
|
}
|
|
1572
1864
|
});
|
|
1573
1865
|
}
|
|
1574
|
-
/**
|
|
1575
|
-
*
|
|
1576
|
-
async function
|
|
1866
|
+
/** Owner replacement of one loop file. Serialized with run finalization by the
|
|
1867
|
+
* loop lock, so a run that started from the previous version cannot overwrite it. */
|
|
1868
|
+
async function writeLoopFile(loopId, name, hash, size) {
|
|
1577
1869
|
return db.transaction(async (tx) => {
|
|
1578
|
-
const loop = (await tx.select(
|
|
1579
|
-
if (!loop || !loop.
|
|
1580
|
-
await writeConfiguredArtifactTx(tx,
|
|
1870
|
+
const loop = (await tx.select().from(loops).where(eq(loops.id, loopId)).for("update"))[0];
|
|
1871
|
+
if (!loop || !loopFileNames(loop).includes(name)) return false;
|
|
1872
|
+
await writeConfiguredArtifactTx(tx, {
|
|
1873
|
+
loopId,
|
|
1874
|
+
path: name,
|
|
1875
|
+
hash,
|
|
1876
|
+
size,
|
|
1877
|
+
binary: false,
|
|
1878
|
+
oversize: false
|
|
1879
|
+
}, nowIso$1());
|
|
1581
1880
|
return true;
|
|
1582
1881
|
});
|
|
1583
1882
|
}
|
|
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
|
-
});
|
|
1883
|
+
/** File notices of the immediately previous terminal run, for the next prompt.
|
|
1884
|
+
* Canceled runs are terminal too: their notices (edits discarded) are repeated,
|
|
1885
|
+
* and an older run's notice must not masquerade as the previous run's. */
|
|
1886
|
+
async function previousRunFileNotices(loopId, beforeRunIndex) {
|
|
1887
|
+
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, [
|
|
1888
|
+
"done",
|
|
1889
|
+
"error",
|
|
1890
|
+
"canceled"
|
|
1891
|
+
]))).orderBy(desc(runs.runIndex)).limit(1))[0]?.fileNotices ?? [];
|
|
1604
1892
|
}
|
|
1605
1893
|
async function listArtifacts(loopId) {
|
|
1606
1894
|
return db.select().from(artifactFiles).where(and(eq(artifactFiles.loopId, loopId), eq(artifactFiles.deleted, false))).orderBy(artifactFiles.path);
|
|
@@ -1618,8 +1906,8 @@ async function buildLoopManifest(loopId) {
|
|
|
1618
1906
|
};
|
|
1619
1907
|
return manifest;
|
|
1620
1908
|
}
|
|
1621
|
-
/** Write/overwrite a run's snapshot (
|
|
1622
|
-
* so a re-report of the same run just refreshes the captured
|
|
1909
|
+
/** Write/overwrite a run's snapshot (name → file metadata). Idempotent on runId
|
|
1910
|
+
* so a re-report of the same run just refreshes the captured loop-file state. */
|
|
1623
1911
|
async function putRunSnapshot(runId, loopId, manifest) {
|
|
1624
1912
|
await db.insert(runSnapshots).values({
|
|
1625
1913
|
runId,
|
|
@@ -1708,103 +1996,16 @@ async function countRunSnapshots() {
|
|
|
1708
1996
|
const r = (await db.select({ n: sql`count(*)` }).from(runSnapshots))[0];
|
|
1709
1997
|
return Number(r?.n ?? 0);
|
|
1710
1998
|
}
|
|
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
1999
|
/**
|
|
1746
|
-
*
|
|
1747
|
-
*
|
|
1748
|
-
*
|
|
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
|
-
/**
|
|
1795
|
-
* Machine + run credential helpers. Device tokens (`dk_…`) identify a machine
|
|
1796
|
-
* (its id is derived from the token: `m-sha256(token)[:16]`). A run
|
|
1797
|
-
* LEASE (`rk_…`) is minted per delivery, bound to one run, and carries the
|
|
1798
|
-
* run's least-privilege caps — the CLI dispatch authorizes the `pievo` shim
|
|
1799
|
-
* against it. Its lifecycle is a small state machine (`active` →
|
|
1800
|
-
* `terminal-grace` → `reconciliation-only`/`retired`), not a mint→revoke pair;
|
|
1801
|
-
* see `RunLease` below.
|
|
2000
|
+
* Machine and run credentials. A device token (`dk_…`) identifies a machine whose
|
|
2001
|
+
* id is derived from it (`m-sha256(token)[:16]`). A run lease (`rk_…`) is minted per
|
|
2002
|
+
* delivery and authorizes only that run's loop-file transfer and terminal report;
|
|
2003
|
+
* its lifecycle is the state machine on `RunLease`.
|
|
1802
2004
|
*
|
|
1803
|
-
* Leases and connect-key bindings are
|
|
1804
|
-
*
|
|
1805
|
-
*
|
|
1806
|
-
*
|
|
1807
|
-
* losing it only degrades retry deduplication, never persisted data.
|
|
2005
|
+
* Leases and connect-key bindings are durable so a restart neither rejects
|
|
2006
|
+
* in-flight reports nor blocks a just-connected machine from enrolling. The
|
|
2007
|
+
* 15-minute `new` idempotency window stays in memory; losing it on restart only
|
|
2008
|
+
* weakens retry deduplication.
|
|
1808
2009
|
*/
|
|
1809
2010
|
var tokens_exports = /* @__PURE__ */ __exportAll({
|
|
1810
2011
|
CONNECT_KEY_TTL_MS: () => CONNECT_KEY_TTL_MS,
|
|
@@ -2067,8 +2268,8 @@ function validateLoopTags(value) {
|
|
|
2067
2268
|
};
|
|
2068
2269
|
}
|
|
2069
2270
|
//#endregion
|
|
2070
|
-
//#region node_modules/.nitro/vite/services/ssr/assets/boot-
|
|
2071
|
-
var
|
|
2271
|
+
//#region node_modules/.nitro/vite/services/ssr/assets/boot-CecCDRoH.js
|
|
2272
|
+
var boot_CecCDRoH_exports = /* @__PURE__ */ __exportAll$1({
|
|
2072
2273
|
_: () => validOptionalPollString,
|
|
2073
2274
|
a: () => POLL_HTTP_BODY_FIELDS,
|
|
2074
2275
|
c: () => MIN_DAEMON_VERSION,
|
|
@@ -2082,24 +2283,11 @@ var boot_CYRi_wez_exports = /* @__PURE__ */ __exportAll$1({
|
|
|
2082
2283
|
n: () => ensureServer,
|
|
2083
2284
|
o: () => validPollCurrentRun,
|
|
2084
2285
|
p: () => MACHINE_BODY_CAP,
|
|
2085
|
-
r: () =>
|
|
2286
|
+
r: () => getLoopFiles,
|
|
2086
2287
|
s: () => validPollWireId,
|
|
2087
2288
|
t: () => boot_exports,
|
|
2088
2289
|
u: () => canonicalLoopEnvelope
|
|
2089
2290
|
});
|
|
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
2291
|
/** Read raw request bytes under a hard cap. Declared oversize requests fail
|
|
2104
2292
|
* before reading; chunked/lying streams are canceled on the first chunk that
|
|
2105
2293
|
* takes the observed count over the cap. */
|
|
@@ -2164,208 +2352,116 @@ async function readJsonBody(request, maxBytes) {
|
|
|
2164
2352
|
} catch {
|
|
2165
2353
|
return { kind: "invalid" };
|
|
2166
2354
|
}
|
|
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
|
-
/**
|
|
2177
|
-
*
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
function
|
|
2182
|
-
return s.
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
*
|
|
2217
|
-
*
|
|
2218
|
-
*
|
|
2219
|
-
*
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
const
|
|
2239
|
-
|
|
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));
|
|
2355
|
+
}
|
|
2356
|
+
var WIRE_TEXT_CAP = 512 * 1024;
|
|
2357
|
+
/** Validate an optional poll string without coercion, clipping, or NUL removal. */
|
|
2358
|
+
function validOptionalPollString(value, cap) {
|
|
2359
|
+
return value === void 0 || typeof value === "string" && value.length <= cap && !value.includes("\0");
|
|
2360
|
+
}
|
|
2361
|
+
function nowIso() {
|
|
2362
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2363
|
+
}
|
|
2364
|
+
/** Postgres text/jsonb columns reject NUL, so a daemon-supplied string carrying
|
|
2365
|
+
* one would fail mid-finalize. The single sanitizer behind `clipText` and `cli.ts`. */
|
|
2366
|
+
function stripNul(s) {
|
|
2367
|
+
return s.replace(/\u0000/g, "");
|
|
2368
|
+
}
|
|
2369
|
+
function clipText(s, cap) {
|
|
2370
|
+
return stripNul(s.slice(0, cap));
|
|
2371
|
+
}
|
|
2372
|
+
/** Literal placeholder the daemon replaces with the run's absolute loop-file
|
|
2373
|
+
* directory before launch (and exports as an env var of the same name). */
|
|
2374
|
+
var LOOP_DIR_PLACEHOLDER = "$PIEVO_LOOP_DIR";
|
|
2375
|
+
var STATUS_LINE_RE = /^STATUS:\s*(keep|no-change|block)\.?$/i;
|
|
2376
|
+
var ATTENTION_LINE_RE = /^ATTENTION:\s*(.*)$/i;
|
|
2377
|
+
/**
|
|
2378
|
+
* Deliver the user's prompt unchanged, followed by the status definitions, the
|
|
2379
|
+
* loop-file contract, and the final-line status contract. The run index
|
|
2380
|
+
* intentionally remains an internal history fact.
|
|
2381
|
+
*/
|
|
2382
|
+
function buildRunTask(loop, previousNotices = []) {
|
|
2383
|
+
const files = [`- ${MEMORY_FILE}: this loop's long-term experience (max ${MEMORY_CHAR_CAP} characters). Read it first and follow it. Before finishing, record only lessons this run learned the hard way that make later runs faster or safer: a pitfall hit, a workaround found, an environment fact discovered. One line each; correct or delete lessons that proved wrong. Never restate the task or these instructions, and never store state, results, baselines, trends, or run logs (those go in an artifact). Most runs add nothing.`, ...loop.artifacts.map((name) => `- ${name}: artifact shown on the Pievo dashboard (max ${fileLimitLabel(name)}). Update it only as the task above asks.`)];
|
|
2384
|
+
const notices = previousNotices.length ? `\n\nNotices from the previous run:\n${previousNotices.map((notice) => `- ${notice}`).join("\n")}` : "";
|
|
2385
|
+
return `${loop.prompt}
|
|
2386
|
+
|
|
2387
|
+
Status definitions:
|
|
2388
|
+
- keep: ${loop.statusKeep}
|
|
2389
|
+
- no-change: ${loop.statusNoChange}
|
|
2390
|
+
- block: ${loop.statusBlock}
|
|
2391
|
+
A block pauses the loop until the owner restarts it.
|
|
2392
|
+
|
|
2393
|
+
Loop files in ${LOOP_DIR_PLACEHOLDER}:
|
|
2394
|
+
${files.join("\n")}
|
|
2395
|
+
A file over its limit is rejected and its previous version kept. Other files in that directory are discarded.${notices}
|
|
2396
|
+
|
|
2397
|
+
Be concise in everything you write: lead with what changed or needs action; prefer tables and short lists; no filler or restated instructions.
|
|
2398
|
+
|
|
2399
|
+
Your final response is only: a summary of at most five short lines (no headings, no repeated tables or file contents); if the owner should look at something, one line \`ATTENTION: <reason>\`; then exactly one last line:
|
|
2400
|
+
STATUS: keep|no-change|block`;
|
|
2401
|
+
}
|
|
2402
|
+
var bareLine = (line) => line.trim().replace(/^[*_`>\s]+|[*_`\s]+$/g, "");
|
|
2403
|
+
/**
|
|
2404
|
+
* Split a provider's final response into the reported status, an optional
|
|
2405
|
+
* attention reason from the ATTENTION lines just above it, and the summary. The final
|
|
2406
|
+
* non-empty line must be the status line; markdown emphasis or code ticks around
|
|
2407
|
+
* either line are tolerated because models add them unprompted.
|
|
2408
|
+
*/
|
|
2409
|
+
function parseFinalStatus(finalText) {
|
|
2410
|
+
if (!finalText) return void 0;
|
|
2411
|
+
const lines = finalText.split(/\r?\n/);
|
|
2412
|
+
let index = lines.length - 1;
|
|
2413
|
+
while (index >= 0 && lines[index].trim() === "") index--;
|
|
2414
|
+
if (index < 0) return void 0;
|
|
2415
|
+
const match = STATUS_LINE_RE.exec(bareLine(lines[index]));
|
|
2416
|
+
if (!match) return void 0;
|
|
2417
|
+
const status = match[1].toLowerCase();
|
|
2418
|
+
const reasons = [];
|
|
2419
|
+
let end = index;
|
|
2420
|
+
for (;;) {
|
|
2421
|
+
let previous = end;
|
|
2422
|
+
while (previous > 0 && lines[previous - 1].trim() === "") previous--;
|
|
2423
|
+
const attentionMatch = previous > 0 ? ATTENTION_LINE_RE.exec(bareLine(lines[previous - 1])) : null;
|
|
2424
|
+
if (!attentionMatch) break;
|
|
2425
|
+
end = previous - 1;
|
|
2426
|
+
const reason = attentionMatch[1].trim();
|
|
2427
|
+
if (reason && !/^(none|n\/a|-)$/i.test(reason)) reasons.unshift(reason);
|
|
2333
2428
|
}
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2429
|
+
const joined = reasons.join("; ");
|
|
2430
|
+
const attention = joined ? joined.length > 200 ? `${joined.slice(0, 199)}…` : joined : null;
|
|
2431
|
+
return {
|
|
2432
|
+
status,
|
|
2433
|
+
message: lines.slice(0, end).join("\n").trim(),
|
|
2434
|
+
attention
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
async function buildDelivery(loop, queuedRun, runToken, roots) {
|
|
2438
|
+
if (queuedRun.runIndex == null) throw new Error(`claimed run ${queuedRun.id} has no history index`);
|
|
2439
|
+
const notices = await previousRunFileNotices(loop.id, queuedRun.runIndex);
|
|
2440
|
+
return {
|
|
2441
|
+
runId: queuedRun.id,
|
|
2442
|
+
runIndex: queuedRun.runIndex,
|
|
2443
|
+
runToken,
|
|
2444
|
+
roots,
|
|
2445
|
+
loop: {
|
|
2446
|
+
id: loop.id,
|
|
2447
|
+
name: loop.name,
|
|
2448
|
+
workdir: loop.workdir,
|
|
2449
|
+
model: loop.model ?? null,
|
|
2450
|
+
reasoningEffort: loop.reasoningEffort ?? null,
|
|
2451
|
+
agent: loop.agent
|
|
2452
|
+
},
|
|
2453
|
+
task: buildRunTask(loop, notices),
|
|
2454
|
+
files: loopFileNames(loop)
|
|
2455
|
+
};
|
|
2340
2456
|
}
|
|
2341
2457
|
/**
|
|
2342
|
-
*
|
|
2343
|
-
*
|
|
2344
|
-
*
|
|
2345
|
-
*
|
|
2346
|
-
*
|
|
2347
|
-
* the OLD blob is no longer pointed at by the current file row — but nothing was
|
|
2348
|
-
* reclaiming it, so the byte store grew forever. This module closes that gap with two pieces:
|
|
2349
|
-
*
|
|
2350
|
-
* 1. pruneSnapshots() — bound the per-loop run-snapshot history to a window, so
|
|
2351
|
-
* old snapshots stop pinning the blobs they referenced for diffs.
|
|
2352
|
-
* 2. gcBlobs() — delete blob bytes no LIVE row needs: a blob is collected
|
|
2353
|
-
* only when NO artifact_files row AND NO retained run_snapshot references its
|
|
2354
|
-
* hash. CORRECTNESS over aggressiveness — a still-referenced blob is never
|
|
2355
|
-
* deleted; a leaked blob is only a cost bug the next pass reclaims.
|
|
2356
|
-
*
|
|
2357
|
-
* Concurrency: gcBlobs is safe to run alongside active syncs. Three guards combine —
|
|
2358
|
-
* a grace window (a blob whose metadata row is younger than the window is never
|
|
2359
|
-
* collected, so a blob a sync just wrote/referenced is untouchable), a point
|
|
2360
|
-
* re-check of each candidate immediately before its bytes are deleted (so a blob
|
|
2361
|
-
* re-referenced before we touch it keeps both its bytes and metadata), and a
|
|
2362
|
-
* bytes-before-metadata delete ordering: each candidate's bytes are reclaimed
|
|
2363
|
-
* FIRST, then its metadata row is dropped unconditionally — so blobExists() goes
|
|
2364
|
-
* false and any sync that re-references the hash (even one that raced the byte
|
|
2365
|
-
* delete) re-uploads the bytes (self-healing). We never leave a live blobs row
|
|
2366
|
-
* pointing at deleted bytes.
|
|
2458
|
+
* Loop-file storage retention. Blob bytes are content-addressed and shared, so
|
|
2459
|
+
* replacing a file never frees its old blob by itself. pruneSnapshots() bounds
|
|
2460
|
+
* per-loop snapshot history; gcBlobs() then deletes bytes no file row and no
|
|
2461
|
+
* retained snapshot references. Correctness wins over aggressiveness: a leaked
|
|
2462
|
+
* blob is a storage cost the next pass reclaims, a deleted live blob is data loss.
|
|
2367
2463
|
*/
|
|
2368
|
-
var log$
|
|
2464
|
+
var log$2 = logger.child({ mod: "retention" });
|
|
2369
2465
|
/**
|
|
2370
2466
|
* Prune every loop's run snapshots down to the configured retention window
|
|
2371
2467
|
* (most-recent-N). Returns the total number pruned. This is what makes an old
|
|
@@ -2377,28 +2473,14 @@ async function pruneSnapshots(keep = 20) {
|
|
|
2377
2473
|
return pruned;
|
|
2378
2474
|
}
|
|
2379
2475
|
/**
|
|
2380
|
-
* Reclaim unreferenced blob bytes
|
|
2381
|
-
*
|
|
2382
|
-
*
|
|
2383
|
-
*
|
|
2384
|
-
*
|
|
2385
|
-
*
|
|
2386
|
-
*
|
|
2387
|
-
*
|
|
2388
|
-
* a. Pre-delete guard: if a sync re-referenced it since the keep-set was
|
|
2389
|
-
* computed, skip it — leaving both its bytes AND metadata row intact
|
|
2390
|
-
* (a consistent live blob, nothing to heal). The guard is two O(1) checks:
|
|
2391
|
-
* an indexed artifact_files point query (always fresh) plus membership in a
|
|
2392
|
-
* snapshot-hash Set deserialized ONCE per pass and rebuilt only when the
|
|
2393
|
-
* snapshot row count changes (a report() raced us) — never the old
|
|
2394
|
-
* per-candidate full snapshot scan (which was O(garbage × snapshots), since
|
|
2395
|
-
* genuine garbage always misses the file check and fell through to it).
|
|
2396
|
-
* b. Otherwise delete the BYTES (the await), then drop the metadata row
|
|
2397
|
-
* UNCONDITIONALLY. Bytes-before-metadata is the data-loss fix: if a sync
|
|
2398
|
-
* raced the byte delete (re-referencing the hash + recreating the blobs
|
|
2399
|
-
* row mid-await), dropping the metadata still leaves blobExists()=false,
|
|
2400
|
-
* so that file re-uploads its bytes on the next sync. We never leave a
|
|
2401
|
-
* live blobs row pointing at deleted bytes.
|
|
2476
|
+
* Reclaim unreferenced blob bytes; returns the number collected. Safe alongside
|
|
2477
|
+
* owner writes and run uploads because of three guards:
|
|
2478
|
+
* - grace window: blobs whose metadata row is younger than `graceMs` are skipped;
|
|
2479
|
+
* - per-candidate re-check right before deletion: an indexed file-row lookup plus
|
|
2480
|
+
* a snapshot-hash set rebuilt only when the snapshot count changes (a concurrent
|
|
2481
|
+
* report), instead of an O(garbage x snapshots) scan;
|
|
2482
|
+
* - bytes before metadata: the metadata row is dropped after the bytes even if a
|
|
2483
|
+
* writer raced the delete, so blobExists() is false and that writer re-uploads.
|
|
2402
2484
|
*/
|
|
2403
2485
|
async function gcBlobs(blobStore, graceMs = BLOB_GC_GRACE_MS) {
|
|
2404
2486
|
const refs = await liveBlobRefs();
|
|
@@ -2418,7 +2500,7 @@ async function gcBlobs(blobStore, graceMs = BLOB_GC_GRACE_MS) {
|
|
|
2418
2500
|
await deleteBlob(hash);
|
|
2419
2501
|
reclaimed++;
|
|
2420
2502
|
} catch (err) {
|
|
2421
|
-
log$
|
|
2503
|
+
log$2.warn({
|
|
2422
2504
|
hash,
|
|
2423
2505
|
err: err instanceof Error ? err.message : String(err)
|
|
2424
2506
|
}, "gc: blob byte-delete failed");
|
|
@@ -2429,7 +2511,7 @@ async function gcBlobs(blobStore, graceMs = BLOB_GC_GRACE_MS) {
|
|
|
2429
2511
|
async function maintainStorage(blobStore, options = {}) {
|
|
2430
2512
|
const snapshotsPruned = await pruneSnapshots(options.keep);
|
|
2431
2513
|
const blobsReclaimed = await gcBlobs(blobStore, options.graceMs);
|
|
2432
|
-
if (snapshotsPruned || blobsReclaimed) log$
|
|
2514
|
+
if (snapshotsPruned || blobsReclaimed) log$2.info({
|
|
2433
2515
|
snapshotsPruned,
|
|
2434
2516
|
blobsReclaimed
|
|
2435
2517
|
}, "storage maintenance");
|
|
@@ -2764,44 +2846,37 @@ function validateStatusDefinitions(value) {
|
|
|
2764
2846
|
}
|
|
2765
2847
|
};
|
|
2766
2848
|
}
|
|
2767
|
-
function
|
|
2849
|
+
function validateArtifactNames(value) {
|
|
2768
2850
|
if (value === void 0 || value === null) return {
|
|
2769
2851
|
ok: true,
|
|
2770
2852
|
value: []
|
|
2771
2853
|
};
|
|
2772
2854
|
if (!Array.isArray(value)) return {
|
|
2773
2855
|
ok: false,
|
|
2774
|
-
detail: "artifacts must be an array of
|
|
2856
|
+
detail: "artifacts must be an array of file names"
|
|
2857
|
+
};
|
|
2858
|
+
if (value.length > 4) return {
|
|
2859
|
+
ok: false,
|
|
2860
|
+
detail: `at most 4 artifacts are allowed`
|
|
2775
2861
|
};
|
|
2776
|
-
const
|
|
2862
|
+
const names = [];
|
|
2777
2863
|
const seen = /* @__PURE__ */ new Set();
|
|
2778
2864
|
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 {
|
|
2865
|
+
if (!validArtifactName(entry)) return {
|
|
2792
2866
|
ok: false,
|
|
2793
|
-
detail: `artifact
|
|
2867
|
+
detail: `artifact must be a plain text/code file name other than ${MEMORY_FILE} (supported extensions: ${ARTIFACT_EXTENSIONS.join(", ")}; letters, digits, . _ -; at most 64 characters before the extension): ${JSON.stringify(entry)}`
|
|
2794
2868
|
};
|
|
2795
|
-
|
|
2869
|
+
const key = entry.toLowerCase();
|
|
2870
|
+
if (seen.has(key)) return {
|
|
2796
2871
|
ok: false,
|
|
2797
|
-
detail: `duplicate artifact
|
|
2872
|
+
detail: `duplicate artifact name: ${entry}`
|
|
2798
2873
|
};
|
|
2799
|
-
seen.add(
|
|
2800
|
-
|
|
2874
|
+
seen.add(key);
|
|
2875
|
+
names.push(entry);
|
|
2801
2876
|
}
|
|
2802
2877
|
return {
|
|
2803
2878
|
ok: true,
|
|
2804
|
-
value:
|
|
2879
|
+
value: names
|
|
2805
2880
|
};
|
|
2806
2881
|
}
|
|
2807
2882
|
function rowSchedule(schedule, retainedCron = SAFE_RETAINED_CRON) {
|
|
@@ -2886,7 +2961,7 @@ function validateLoopCreate(value) {
|
|
|
2886
2961
|
if (!schedule.ok) return schedule;
|
|
2887
2962
|
const statusDefinitions = validateStatusDefinitions(raw.statusDefinitions);
|
|
2888
2963
|
if (!statusDefinitions.ok) return statusDefinitions;
|
|
2889
|
-
const artifacts =
|
|
2964
|
+
const artifacts = validateArtifactNames(raw.artifacts);
|
|
2890
2965
|
if (!artifacts.ok) return artifacts;
|
|
2891
2966
|
const model = optionalProvider(raw.model, "model");
|
|
2892
2967
|
if (!model.ok) return model;
|
|
@@ -2981,7 +3056,7 @@ function validateLoopEdit(loop, value) {
|
|
|
2981
3056
|
update.statusBlock = result.value.block;
|
|
2982
3057
|
}
|
|
2983
3058
|
if (raw.artifacts !== void 0) {
|
|
2984
|
-
const result =
|
|
3059
|
+
const result = validateArtifactNames(raw.artifacts);
|
|
2985
3060
|
if (!result.ok) return result;
|
|
2986
3061
|
update.artifacts = result.value;
|
|
2987
3062
|
}
|
|
@@ -3017,7 +3092,7 @@ function validateLoopEdit(loop, value) {
|
|
|
3017
3092
|
value: update
|
|
3018
3093
|
};
|
|
3019
3094
|
}
|
|
3020
|
-
var MIN_DAEMON_VERSION = "
|
|
3095
|
+
var MIN_DAEMON_VERSION = "4.0.0";
|
|
3021
3096
|
function daemonNeedsUpdate(current, required = MIN_DAEMON_VERSION) {
|
|
3022
3097
|
if (!current || !isValidSemver(current) || !isValidSemver(required)) return true;
|
|
3023
3098
|
if (current === required) return false;
|
|
@@ -3050,23 +3125,18 @@ var validPollWireId = (value) => typeof value === "string" && value.length <= 20
|
|
|
3050
3125
|
var validPollCurrentRun = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 2 && Object.prototype.hasOwnProperty.call(value, "runId") && Object.prototype.hasOwnProperty.call(value, "stage") && validPollWireId(value.runId) && (value.stage === "executing" || value.stage === "reporting");
|
|
3051
3126
|
var validPollInfo = (value) => value === void 0 || value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).every((key) => POLL_INFO_FIELDS.has(key)) && validOptionalPollString(value.host, 524288) && validOptionalPollString(value.platform, 524288) && validOptionalPollString(value.arch, 524288) && validOptionalPollString(value.version, 64);
|
|
3052
3127
|
/**
|
|
3053
|
-
* Machine gateway
|
|
3054
|
-
*
|
|
3055
|
-
* long-poll while idle). Framework-agnostic like the rest of the gateway
|
|
3056
|
-
* (return `{ status, body }` so the methods can be mounted on a plain http
|
|
3057
|
-
* server or TanStack server routes):
|
|
3128
|
+
* Machine gateway: the run-lifecycle core the daemon talks to. Methods return
|
|
3129
|
+
* transport-free `{ status, body }` results for the thin route shells:
|
|
3058
3130
|
*
|
|
3059
|
-
* POST /api/machine/poll
|
|
3060
|
-
* POST /machine/report
|
|
3131
|
+
* POST /api/machine/poll (device token) claim and deliver runs
|
|
3132
|
+
* POST /machine/report (run token) finalize a run
|
|
3133
|
+
* GET|PUT /api/machine/run-files/:name (run token) loop-file transfer
|
|
3061
3134
|
*
|
|
3062
|
-
*
|
|
3063
|
-
*
|
|
3064
|
-
*
|
|
3065
|
-
* CLI verb dispatch at `/api/machine/cli` lives in `gateway/cli.ts`
|
|
3066
|
-
* (`CliGateway`), which reuses this class's owner methods;
|
|
3067
|
-
* canonical loop configuration validation lives in `gateway/loopConfig.ts`.
|
|
3135
|
+
* Idle polls may be held open (long-poll) until the scheduler's dispatcher wakes
|
|
3136
|
+
* them. `sweep()` marks stale machines offline and reclaims silent runs. CLI verbs
|
|
3137
|
+
* live in `cli.ts`; loop config validation lives in `loopConfig.ts`.
|
|
3068
3138
|
*/
|
|
3069
|
-
var log$
|
|
3139
|
+
var log$1 = logger.child({ mod: "gateway" });
|
|
3070
3140
|
var ONLINE_TTL_MS = 3e4;
|
|
3071
3141
|
/** Auto-pause after this many consecutive errors. Canceled rows are transparent;
|
|
3072
3142
|
* zero disables the breaker. */
|
|
@@ -3081,10 +3151,15 @@ var RUN_TIMEOUT_MS = Number.isFinite(configuredRunTimeoutMs) && configuredRunTim
|
|
|
3081
3151
|
* patched over the device-token edit surface. Exported for `cli.ts`
|
|
3082
3152
|
* (the `new`/`edit` verb help lists these keys). */
|
|
3083
3153
|
var EDITABLE_LOOP_FIELDS = LOOP_EDIT_FIELDS;
|
|
3084
|
-
/**
|
|
3085
|
-
*
|
|
3086
|
-
* this cap. Exported for `cli.ts` so the report verb uses the same budget. */
|
|
3154
|
+
/** Run summary (the final response minus its STATUS line) and run errors share
|
|
3155
|
+
* this cap; the full final response is stored separately as finalText. */
|
|
3087
3156
|
var MESSAGE_CAP = 2e3;
|
|
3157
|
+
/** A run reports at most one version per loop file (MEMORY.md + 4 artifacts). */
|
|
3158
|
+
var REPORT_FILES_MAX = 5;
|
|
3159
|
+
/** One notice per file plus a few upload/rejection reasons; bounds the prompt echo. */
|
|
3160
|
+
var REPORT_NOTICES_MAX = 10;
|
|
3161
|
+
/** One short sentence (name + reason); 500 chars keeps the next prompt small. */
|
|
3162
|
+
var NOTICE_CAP = 500;
|
|
3088
3163
|
/** Provider session IDs are opaque short tokens; this rejects malformed payloads
|
|
3089
3164
|
* without constraining known provider formats. */
|
|
3090
3165
|
var SESSION_ID_CAP = 200;
|
|
@@ -3099,7 +3174,7 @@ var HEARTBEAT_STAMP_REFRESH_MS = heartbeatRefreshMs(RUN_TIMEOUT_MS);
|
|
|
3099
3174
|
* and an idle poll becomes read-only, with worst-case staleness well inside
|
|
3100
3175
|
* the 30s TTL (max stamp gap = refresh + one poll interval). */
|
|
3101
3176
|
var LAST_SEEN_REFRESH_MS = 1e4;
|
|
3102
|
-
var
|
|
3177
|
+
var MACHINE_REPORT_FIELDS = [
|
|
3103
3178
|
"reportId",
|
|
3104
3179
|
"runId",
|
|
3105
3180
|
"result",
|
|
@@ -3108,8 +3183,17 @@ var MACHINE_REPORT_FIELD_SET = new Set([
|
|
|
3108
3183
|
"sessionId",
|
|
3109
3184
|
"usage",
|
|
3110
3185
|
"error",
|
|
3111
|
-
"finalText"
|
|
3112
|
-
|
|
3186
|
+
"finalText",
|
|
3187
|
+
"files",
|
|
3188
|
+
"fileNotices"
|
|
3189
|
+
];
|
|
3190
|
+
var REPORT_FILE_FIELDS = [
|
|
3191
|
+
"baseHash",
|
|
3192
|
+
"hash",
|
|
3193
|
+
"name",
|
|
3194
|
+
"size"
|
|
3195
|
+
];
|
|
3196
|
+
var MACHINE_REPORT_FIELD_SET = new Set(MACHINE_REPORT_FIELDS);
|
|
3113
3197
|
var MACHINE_REPORT_USAGE_FIELDS = new Set([
|
|
3114
3198
|
"inputTokens",
|
|
3115
3199
|
"outputTokens",
|
|
@@ -3138,7 +3222,7 @@ function receiptFor(body, runId, ackStatus = 200, ackBody) {
|
|
|
3138
3222
|
}
|
|
3139
3223
|
function receiptResponse(receipt, expected) {
|
|
3140
3224
|
if (!receipt || receipt.runId !== expected.runId) return void 0;
|
|
3141
|
-
if (receipt.payloadDigest !== expected.payloadDigest) log$
|
|
3225
|
+
if (receipt.payloadDigest !== expected.payloadDigest) log$1.warn({
|
|
3142
3226
|
reportId: expected.reportId,
|
|
3143
3227
|
runId: expected.runId
|
|
3144
3228
|
}, "report: same-run payload changed after commit; replaying authoritative ACK");
|
|
@@ -3166,7 +3250,7 @@ async function committedReportEvidence(reportId, payloadDigest, authoritativeRun
|
|
|
3166
3250
|
foreignRun: false
|
|
3167
3251
|
};
|
|
3168
3252
|
if (normal?.runId === authoritativeRunId) {
|
|
3169
|
-
if (normal.payloadDigest !== payloadDigest) log$
|
|
3253
|
+
if (normal.payloadDigest !== payloadDigest) log$1.warn({
|
|
3170
3254
|
reportId,
|
|
3171
3255
|
runId: authoritativeRunId
|
|
3172
3256
|
}, "report: same-run payload changed after commit; replaying authoritative ACK");
|
|
@@ -3200,14 +3284,37 @@ function incidentDiagnosis(code, issues, reportId, payloadDigest) {
|
|
|
3200
3284
|
recommendedAction
|
|
3201
3285
|
};
|
|
3202
3286
|
}
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
*
|
|
3206
|
-
function
|
|
3207
|
-
const
|
|
3208
|
-
if (
|
|
3209
|
-
|
|
3210
|
-
|
|
3287
|
+
var MISSING_STATUS_ERROR = "run protocol incomplete: the final response has no \"STATUS: keep|no-change|block\" line";
|
|
3288
|
+
/** The run outcome a terminal report establishes. Only a successful provider
|
|
3289
|
+
* exit is read for a status; a successful exit without one is a protocol error. */
|
|
3290
|
+
function reportOutcome(body) {
|
|
3291
|
+
const canceled = body.result === "canceled";
|
|
3292
|
+
if (canceled) return {
|
|
3293
|
+
ok: false,
|
|
3294
|
+
canceled,
|
|
3295
|
+
patch: { error: "stopped by user" }
|
|
3296
|
+
};
|
|
3297
|
+
if (body.result !== "success") return {
|
|
3298
|
+
ok: false,
|
|
3299
|
+
canceled,
|
|
3300
|
+
patch: { error: typeof body.error === "string" ? clipText(body.error, MESSAGE_CAP) : "run failed on machine" }
|
|
3301
|
+
};
|
|
3302
|
+
const parsed = parseFinalStatus(body.finalText);
|
|
3303
|
+
if (!parsed) return {
|
|
3304
|
+
ok: false,
|
|
3305
|
+
canceled,
|
|
3306
|
+
patch: { error: MISSING_STATUS_ERROR }
|
|
3307
|
+
};
|
|
3308
|
+
return {
|
|
3309
|
+
ok: true,
|
|
3310
|
+
canceled,
|
|
3311
|
+
patch: {
|
|
3312
|
+
status: parsed.status,
|
|
3313
|
+
message: clipText(parsed.message, 2e3) || null,
|
|
3314
|
+
attention: parsed.attention,
|
|
3315
|
+
error: null
|
|
3316
|
+
}
|
|
3317
|
+
};
|
|
3211
3318
|
}
|
|
3212
3319
|
function validateTerminalReport(body) {
|
|
3213
3320
|
const issues = [];
|
|
@@ -3227,6 +3334,25 @@ function validateTerminalReport(body) {
|
|
|
3227
3334
|
"error",
|
|
3228
3335
|
"finalText"
|
|
3229
3336
|
]) if (has(key) && typeof body[key] !== "string") issues.push(`${key} must be a string`);
|
|
3337
|
+
if (has("files")) {
|
|
3338
|
+
const files = body.files;
|
|
3339
|
+
if (!Array.isArray(files) || files.length > REPORT_FILES_MAX) issues.push(`files must be an array of at most ${REPORT_FILES_MAX} entries`);
|
|
3340
|
+
else {
|
|
3341
|
+
const names = /* @__PURE__ */ new Set();
|
|
3342
|
+
for (const entry of files) {
|
|
3343
|
+
const file = entry;
|
|
3344
|
+
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)) {
|
|
3345
|
+
issues.push("files entries must be exactly {name, baseHash, hash, size} with unique names, sha256 hashes, and an in-cap size");
|
|
3346
|
+
break;
|
|
3347
|
+
}
|
|
3348
|
+
names.add(file.name);
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
if (has("fileNotices")) {
|
|
3353
|
+
const notices = body.fileNotices;
|
|
3354
|
+
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`);
|
|
3355
|
+
}
|
|
3230
3356
|
if (has("usage")) if (body.usage === null || typeof body.usage !== "object" || Array.isArray(body.usage)) issues.push("usage must be an object");
|
|
3231
3357
|
else {
|
|
3232
3358
|
const usage = body.usage;
|
|
@@ -3267,7 +3393,9 @@ var MachineGateway = class {
|
|
|
3267
3393
|
constructor(scheduler, blobStore = createBlobStore()) {
|
|
3268
3394
|
this.scheduler = scheduler;
|
|
3269
3395
|
this.blobStore = blobStore;
|
|
3396
|
+
this.loopFiles = new LoopFiles(blobStore);
|
|
3270
3397
|
}
|
|
3398
|
+
loopFiles;
|
|
3271
3399
|
/** In-flight latch: the maintenance pass is sequential and the first post-deploy
|
|
3272
3400
|
* backlog reclamation can overrun the interval, so a fresh tick skips rather than
|
|
3273
3401
|
* running a second pass concurrently (idempotent but wasteful + double-counts). */
|
|
@@ -3275,7 +3403,7 @@ var MachineGateway = class {
|
|
|
3275
3403
|
applyAutopauseTimer(loopId, terminal) {
|
|
3276
3404
|
if (!terminal.autoPaused) return;
|
|
3277
3405
|
this.scheduler.removeLoop(loopId);
|
|
3278
|
-
log$
|
|
3406
|
+
log$1.warn({
|
|
3279
3407
|
loopId,
|
|
3280
3408
|
streak: terminal.failureStreak
|
|
3281
3409
|
}, "loop auto-paused after block or consecutive errors");
|
|
@@ -3344,20 +3472,12 @@ var MachineGateway = class {
|
|
|
3344
3472
|
await pruneExpiredLeases(now);
|
|
3345
3473
|
for (const loop of await listLoops()) if (loop.deleteRequestedAt) await tryDeleteLoop(loop.id);
|
|
3346
3474
|
}
|
|
3347
|
-
/**
|
|
3348
|
-
*
|
|
3349
|
-
*
|
|
3350
|
-
*
|
|
3351
|
-
*
|
|
3352
|
-
*
|
|
3353
|
-
* (often SUCCESSFUL) result. Retiring the lease here would 401 that late report
|
|
3354
|
-
* and strand the run as a permanent false failure with its message lost (the
|
|
3355
|
-
* investigated bug). So the lease survives a bounded grace window
|
|
3356
|
-
* (`TERMINAL_GRACE_MS`) during which exactly ONE late wake-report may reconcile
|
|
3357
|
-
* the run — see `report()`'s terminal-grace branch. The credential is still
|
|
3358
|
-
* bounded: CLI mutations are refused while terminal-grace, and the
|
|
3359
|
-
* reconciliation retires the lease single-shot. Pending rows are durable inbox
|
|
3360
|
-
* entries and are never reclaimed by this path. */
|
|
3475
|
+
/** Reclaim one silent running run as an error, but terminalize its lease instead
|
|
3476
|
+
* of retiring it. The usual cause is a laptop that fell asleep mid-run; on wake
|
|
3477
|
+
* the daemon delivers the real (often successful) result, and a retired lease
|
|
3478
|
+
* would 401 it and leave a false failure. During TERMINAL_GRACE_MS exactly one
|
|
3479
|
+
* late report may reconcile the run (see `report()`); CLI mutations stay refused.
|
|
3480
|
+
* Pending rows are durable queue entries and never reach this path. */
|
|
3361
3481
|
async reclaimRun(run, reason) {
|
|
3362
3482
|
if (run.phase !== "running") return;
|
|
3363
3483
|
const at = nowIso();
|
|
@@ -3371,13 +3491,14 @@ var MachineGateway = class {
|
|
|
3371
3491
|
* Periodic storage maintenance: prune each loop's run snapshots to the
|
|
3372
3492
|
* retention window, then GC blob bytes no live row needs. Wired to its own
|
|
3373
3493
|
* 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
|
|
3494
|
+
* tests / on-demand triggers. Safe to run concurrently with owner writes and
|
|
3495
|
+
* run uploads (a grace window + final re-check protect freshly-written or
|
|
3496
|
+
* referenced blobs) and idempotent with no garbage. Best-effort — never throws
|
|
3497
|
+
* into the caller.
|
|
3377
3498
|
*/
|
|
3378
3499
|
async maintainStorage(options) {
|
|
3379
3500
|
if (this.maintenanceRunning) {
|
|
3380
|
-
log$
|
|
3501
|
+
log$1.info("storage maintenance already in progress — skipping this tick");
|
|
3381
3502
|
return {
|
|
3382
3503
|
snapshotsPruned: 0,
|
|
3383
3504
|
blobsReclaimed: 0
|
|
@@ -3387,7 +3508,7 @@ var MachineGateway = class {
|
|
|
3387
3508
|
try {
|
|
3388
3509
|
return await maintainStorage(this.blobStore, options);
|
|
3389
3510
|
} catch (err) {
|
|
3390
|
-
log$
|
|
3511
|
+
log$1.warn({ err: err instanceof Error ? err.message : String(err) }, "storage maintenance failed");
|
|
3391
3512
|
return {
|
|
3392
3513
|
snapshotsPruned: 0,
|
|
3393
3514
|
blobsReclaimed: 0
|
|
@@ -3413,7 +3534,7 @@ var MachineGateway = class {
|
|
|
3413
3534
|
token: deviceToken,
|
|
3414
3535
|
online: true
|
|
3415
3536
|
});
|
|
3416
|
-
log$
|
|
3537
|
+
log$1.info({
|
|
3417
3538
|
machineId,
|
|
3418
3539
|
host: info?.host
|
|
3419
3540
|
}, "poll: self-registered machine");
|
|
@@ -3445,7 +3566,7 @@ var MachineGateway = class {
|
|
|
3445
3566
|
const claimed = await claimReadyRunForMachine(machineId, void 0, currentRunIds ?? []);
|
|
3446
3567
|
if (claimed) delivery = await buildDelivery(claimed.loop, claimed.run, claimed.runToken, machine.roots ?? []);
|
|
3447
3568
|
}
|
|
3448
|
-
if (delivery) log$
|
|
3569
|
+
if (delivery) log$1.info({
|
|
3449
3570
|
machineId,
|
|
3450
3571
|
runId: delivery.runId
|
|
3451
3572
|
}, "poll: delivered");
|
|
@@ -3463,7 +3584,7 @@ var MachineGateway = class {
|
|
|
3463
3584
|
code: "VALIDATION_ERROR"
|
|
3464
3585
|
}
|
|
3465
3586
|
};
|
|
3466
|
-
if (request.protocolVersion !==
|
|
3587
|
+
if (request.protocolVersion !== 5) {
|
|
3467
3588
|
const auth = await authenticateDeviceToken(deviceToken, { allowUnknown: true });
|
|
3468
3589
|
if (!auth.ok && auth.reason === "mismatch") return auth.response;
|
|
3469
3590
|
if (auth.ok && auth.machine) {
|
|
@@ -3475,7 +3596,7 @@ var MachineGateway = class {
|
|
|
3475
3596
|
body: {
|
|
3476
3597
|
error: "daemon upgrade required; run `npm install -g @kky42/pievo@latest`, then `pievo daemon restart`",
|
|
3477
3598
|
code: "UPGRADE_REQUIRED",
|
|
3478
|
-
requiredProtocol:
|
|
3599
|
+
requiredProtocol: 5
|
|
3479
3600
|
}
|
|
3480
3601
|
};
|
|
3481
3602
|
}
|
|
@@ -3507,7 +3628,7 @@ var MachineGateway = class {
|
|
|
3507
3628
|
const needsUpdate = daemonNeedsUpdate(reportedVersion);
|
|
3508
3629
|
const base = await this.pollCore(deviceToken, request.info, currentIds, !needsUpdate, true);
|
|
3509
3630
|
if (base.status !== 200) return base;
|
|
3510
|
-
if ((await getMachine(machineId))?.daemonProtocol !==
|
|
3631
|
+
if ((await getMachine(machineId))?.daemonProtocol !== 5) await updateMachine(machineId, { daemonProtocol: 5 });
|
|
3511
3632
|
const running = await runningRunsForMachine(machineId);
|
|
3512
3633
|
const currentSet = new Set(currentIds);
|
|
3513
3634
|
return {
|
|
@@ -3524,7 +3645,7 @@ var MachineGateway = class {
|
|
|
3524
3645
|
};
|
|
3525
3646
|
}
|
|
3526
3647
|
async pollV4Wait(deviceToken, request, waitMs = LONG_POLL_WAIT_MS) {
|
|
3527
|
-
if (request.currentRuns?.length || request.protocolVersion !==
|
|
3648
|
+
if (request.currentRuns?.length || request.protocolVersion !== 5) return this.pollV4(deviceToken, request);
|
|
3528
3649
|
const auth = await authenticateDeviceToken(deviceToken, { allowUnknown: true });
|
|
3529
3650
|
if (!auth.ok) return auth.response;
|
|
3530
3651
|
const machineId = auth.machineId;
|
|
@@ -3542,11 +3663,8 @@ var MachineGateway = class {
|
|
|
3542
3663
|
waiter.cancel();
|
|
3543
3664
|
}
|
|
3544
3665
|
}
|
|
3545
|
-
/**
|
|
3546
|
-
*
|
|
3547
|
-
* Claude Code can avoid starting a duplicate. `online` is fresh-checked against
|
|
3548
|
-
* the poll TTL, not just the stored flag.
|
|
3549
|
-
*/
|
|
3666
|
+
/** Whether this machine has a live daemon. `online` is fresh-checked against the
|
|
3667
|
+
* poll TTL, not just the stored flag. */
|
|
3550
3668
|
async status(deviceToken) {
|
|
3551
3669
|
const auth = await authenticateDeviceToken(deviceToken, { allowUnknown: true });
|
|
3552
3670
|
if (!auth.ok) return auth.response;
|
|
@@ -3632,7 +3750,7 @@ var MachineGateway = class {
|
|
|
3632
3750
|
});
|
|
3633
3751
|
this.scheduler.addLoop(loop);
|
|
3634
3752
|
recordNewIdempotency(idempotencyKey, machineId, loop.id);
|
|
3635
|
-
log$
|
|
3753
|
+
log$1.info({
|
|
3636
3754
|
machineId,
|
|
3637
3755
|
loopId: loop.id,
|
|
3638
3756
|
agent: loop.agent
|
|
@@ -3846,7 +3964,7 @@ var MachineGateway = class {
|
|
|
3846
3964
|
}
|
|
3847
3965
|
const response = receiptResponse(stored, expected);
|
|
3848
3966
|
if (response.status < 300) await retireLease(runToken);
|
|
3849
|
-
log$
|
|
3967
|
+
log$1.info({ runId: lease.runId }, "report: ignored (run was canceled)");
|
|
3850
3968
|
return response;
|
|
3851
3969
|
}
|
|
3852
3970
|
async reportRaceResponse(runToken, lease, body, payloadDigest) {
|
|
@@ -3870,7 +3988,7 @@ var MachineGateway = class {
|
|
|
3870
3988
|
const refreshedLease = await resolveLease(runToken);
|
|
3871
3989
|
if (refreshedLease?.state === "retired") return this.retiredReport(runToken, body, lease.runId);
|
|
3872
3990
|
if (retryReconcile && fresh?.phase === "error" && refreshedLease?.state === "terminal-grace") return this.reconcileReclaimedReport(runToken, refreshedLease, fresh, body);
|
|
3873
|
-
log$
|
|
3991
|
+
log$1.info({
|
|
3874
3992
|
runId: lease.runId,
|
|
3875
3993
|
phase: fresh?.phase
|
|
3876
3994
|
}, logMessage);
|
|
@@ -3883,12 +4001,108 @@ var MachineGateway = class {
|
|
|
3883
4001
|
}
|
|
3884
4002
|
};
|
|
3885
4003
|
}
|
|
4004
|
+
/** Reported loop-file versions whose uploaded bytes are present. A canceled run
|
|
4005
|
+
* was killed mid-edit, so none of its files are applied. */
|
|
4006
|
+
async terminalFiles(body) {
|
|
4007
|
+
const notices = (body.fileNotices ?? []).map((notice) => clipText(notice, NOTICE_CAP));
|
|
4008
|
+
const updates = [];
|
|
4009
|
+
if (body.result === "canceled") return {
|
|
4010
|
+
updates,
|
|
4011
|
+
notices
|
|
4012
|
+
};
|
|
4013
|
+
const files = body.files ?? [];
|
|
4014
|
+
const sizes = await blobSizes(files.map((file) => file.hash).filter((hash) => hash !== EMPTY_SHA256));
|
|
4015
|
+
for (const file of files) {
|
|
4016
|
+
if (!await this.loopFiles.hasBlob(file.hash)) {
|
|
4017
|
+
notices.push(`${file.name}: the upload was lost; the previous version was kept`);
|
|
4018
|
+
continue;
|
|
4019
|
+
}
|
|
4020
|
+
const size = file.hash === "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" ? 0 : sizes.get(file.hash);
|
|
4021
|
+
if (size === void 0 || size !== file.size) {
|
|
4022
|
+
notices.push(`${file.name}: reported size did not match the stored version; the previous version was kept`);
|
|
4023
|
+
continue;
|
|
4024
|
+
}
|
|
4025
|
+
updates.push({
|
|
4026
|
+
name: file.name,
|
|
4027
|
+
baseHash: file.baseHash,
|
|
4028
|
+
hash: file.hash,
|
|
4029
|
+
size
|
|
4030
|
+
});
|
|
4031
|
+
}
|
|
4032
|
+
return {
|
|
4033
|
+
updates,
|
|
4034
|
+
notices
|
|
4035
|
+
};
|
|
4036
|
+
}
|
|
4037
|
+
/** Current bytes of one loop file for an executing run (the run's starting version). */
|
|
4038
|
+
async readRunFile(runToken, name) {
|
|
4039
|
+
const lease = await resolveLease(runToken);
|
|
4040
|
+
if (!lease) return {
|
|
4041
|
+
status: 401,
|
|
4042
|
+
body: { error: "invalid or expired token" }
|
|
4043
|
+
};
|
|
4044
|
+
if (lease.state !== "active") return {
|
|
4045
|
+
status: 409,
|
|
4046
|
+
body: { error: "run is no longer executing" }
|
|
4047
|
+
};
|
|
4048
|
+
const loop = await getLoop(lease.loopId);
|
|
4049
|
+
if (!loop || !loopFileNames(loop).includes(name)) return {
|
|
4050
|
+
status: 404,
|
|
4051
|
+
body: { error: "no such loop file" }
|
|
4052
|
+
};
|
|
4053
|
+
const row = await getArtifactFile(loop.id, name);
|
|
4054
|
+
const hash = row && !row.deleted && row.hash ? row.hash : EMPTY_SHA256;
|
|
4055
|
+
const bytes = await this.loopFiles.readBlob(hash);
|
|
4056
|
+
if (!bytes) return {
|
|
4057
|
+
status: 500,
|
|
4058
|
+
body: { error: "loop file bytes are missing" }
|
|
4059
|
+
};
|
|
4060
|
+
return {
|
|
4061
|
+
status: 200,
|
|
4062
|
+
body: {},
|
|
4063
|
+
bytes,
|
|
4064
|
+
hash
|
|
4065
|
+
};
|
|
4066
|
+
}
|
|
4067
|
+
/** Store one loop-file version produced by a run. Accepted until the run's
|
|
4068
|
+
* authority retires, so the durable report outbox can retry after a crash.
|
|
4069
|
+
* Applying it to the loop happens only in the terminal report transaction. */
|
|
4070
|
+
async storeRunFile(runToken, name, bytes, expectedHash) {
|
|
4071
|
+
const lease = await resolveLease(runToken);
|
|
4072
|
+
if (!lease || lease.state === "retired") return {
|
|
4073
|
+
status: 401,
|
|
4074
|
+
body: { error: "invalid or expired token" }
|
|
4075
|
+
};
|
|
4076
|
+
const loop = await getLoop(lease.loopId);
|
|
4077
|
+
if (!loop || !loopFileNames(loop).includes(name)) return {
|
|
4078
|
+
status: 404,
|
|
4079
|
+
body: { error: "no such loop file" }
|
|
4080
|
+
};
|
|
4081
|
+
const issue = loopFileIssue(name, bytes);
|
|
4082
|
+
if (issue) return {
|
|
4083
|
+
status: 413,
|
|
4084
|
+
body: { error: issue }
|
|
4085
|
+
};
|
|
4086
|
+
if (expectedHash !== null && expectedHash !== sha256Buf(bytes)) return {
|
|
4087
|
+
status: 400,
|
|
4088
|
+
body: { error: "hash mismatch" }
|
|
4089
|
+
};
|
|
4090
|
+
const hash = await this.loopFiles.putBlob(bytes);
|
|
4091
|
+
if (hash !== "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") await recordBlob(hash, bytes.length);
|
|
4092
|
+
return {
|
|
4093
|
+
status: 200,
|
|
4094
|
+
body: {
|
|
4095
|
+
ok: true,
|
|
4096
|
+
hash
|
|
4097
|
+
}
|
|
4098
|
+
};
|
|
4099
|
+
}
|
|
3886
4100
|
async captureRunSnapshot(runId, loopId) {
|
|
3887
4101
|
try {
|
|
3888
4102
|
await putRunSnapshot(runId, loopId, await buildLoopManifest(loopId));
|
|
3889
4103
|
await pruneRunSnapshots(loopId, 20);
|
|
3890
4104
|
} catch (err) {
|
|
3891
|
-
log$
|
|
4105
|
+
log$1.warn({
|
|
3892
4106
|
runId,
|
|
3893
4107
|
err: err instanceof Error ? err.message : String(err)
|
|
3894
4108
|
}, "snapshot capture failed");
|
|
@@ -3898,11 +4112,8 @@ var MachineGateway = class {
|
|
|
3898
4112
|
* same loop-lock transaction as the error→done/error patch and loop state, so
|
|
3899
4113
|
* concurrent late reports cannot both win (including error→error). */
|
|
3900
4114
|
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;
|
|
4115
|
+
const { ok, canceled, patch } = reportOutcome(body);
|
|
4116
|
+
const files = await this.terminalFiles(body);
|
|
3906
4117
|
const loopPatch = {};
|
|
3907
4118
|
const receipt = receiptFor(body, lease.runId);
|
|
3908
4119
|
const payloadDigest = sha256(JSON.stringify(body));
|
|
@@ -3911,10 +4122,10 @@ var MachineGateway = class {
|
|
|
3911
4122
|
reconciled = await reconcileReclaimedRun(lease.loopId, lease.runId, sha256(runToken), {
|
|
3912
4123
|
phase: canceled ? "canceled" : ok ? "done" : "error",
|
|
3913
4124
|
...coerceTelemetry(body),
|
|
3914
|
-
...
|
|
3915
|
-
|
|
4125
|
+
...patch,
|
|
4126
|
+
fileNotices: files.notices,
|
|
3916
4127
|
ts: nowIso()
|
|
3917
|
-
}, loopPatch, AUTOPAUSE_STREAK, receipt);
|
|
4128
|
+
}, loopPatch, AUTOPAUSE_STREAK, receipt, files.updates);
|
|
3918
4129
|
} catch (error) {
|
|
3919
4130
|
const raced = await this.reportRaceResponse(runToken, lease, body, payloadDigest);
|
|
3920
4131
|
if (raced) return raced;
|
|
@@ -3927,7 +4138,7 @@ var MachineGateway = class {
|
|
|
3927
4138
|
if (!reportOnly) this.scheduler.addLoop(reconciled.loop);
|
|
3928
4139
|
if (!reportOnly) this.applyAutopauseTimer(lease.loopId, reconciled);
|
|
3929
4140
|
if (deleting) await tryDeleteLoop(lease.loopId);
|
|
3930
|
-
log$
|
|
4141
|
+
log$1.info({
|
|
3931
4142
|
runId: lease.runId,
|
|
3932
4143
|
ok,
|
|
3933
4144
|
reclaimed: true
|
|
@@ -3980,7 +4191,7 @@ var MachineGateway = class {
|
|
|
3980
4191
|
this.scheduler.addLoop(rejected.loop);
|
|
3981
4192
|
if (rejected.state === "run-error") this.applyAutopauseTimer(lease.loopId, rejected);
|
|
3982
4193
|
if (rejected.loop.deleteRequestedAt) await tryDeleteLoop(lease.loopId);
|
|
3983
|
-
log$
|
|
4194
|
+
log$1.warn({
|
|
3984
4195
|
runId: lease.runId,
|
|
3985
4196
|
reportId,
|
|
3986
4197
|
code,
|
|
@@ -4034,8 +4245,6 @@ var MachineGateway = class {
|
|
|
4034
4245
|
if (evidence.foreignRun) return this.rejectTerminalAttempt(runToken, lease, body, payloadDigest, "REPORT_CONFLICT", ["reportId was already committed for another run"]);
|
|
4035
4246
|
if (issues.length) return this.rejectTerminalAttempt(runToken, lease, body, payloadDigest, "REPORT_INVALID", issues);
|
|
4036
4247
|
const expected = receiptFor(body, lease.runId);
|
|
4037
|
-
const ok = body.result === "success";
|
|
4038
|
-
const canceled = body.result === "canceled";
|
|
4039
4248
|
const run = await getRun(lease.runId);
|
|
4040
4249
|
if (run?.phase === "canceled") return this.ignoreCanceledReport(runToken, lease, body);
|
|
4041
4250
|
if (run?.phase === "done") return {
|
|
@@ -4047,17 +4256,18 @@ var MachineGateway = class {
|
|
|
4047
4256
|
}
|
|
4048
4257
|
};
|
|
4049
4258
|
if (run?.phase === "error" && (lease.state === "terminal-grace" || lease.state === "reconciliation-only")) return this.reconcileReclaimedReport(runToken, lease, run, body);
|
|
4050
|
-
const
|
|
4051
|
-
const
|
|
4259
|
+
const { ok: effectiveOk, canceled, patch } = reportOutcome(body);
|
|
4260
|
+
const files = await this.terminalFiles(body);
|
|
4052
4261
|
const loopPatch = {};
|
|
4053
4262
|
let terminal;
|
|
4054
4263
|
try {
|
|
4055
4264
|
terminal = await finalizeRunningRun(lease.loopId, lease.runId, {
|
|
4056
4265
|
phase: canceled ? "canceled" : effectiveOk ? "done" : "error",
|
|
4057
4266
|
...coerceTelemetry(body),
|
|
4058
|
-
...
|
|
4267
|
+
...patch,
|
|
4268
|
+
fileNotices: files.notices,
|
|
4059
4269
|
ts: nowIso()
|
|
4060
|
-
}, loopPatch, sha256(runToken), AUTOPAUSE_STREAK, expected);
|
|
4270
|
+
}, loopPatch, sha256(runToken), AUTOPAUSE_STREAK, expected, files.updates);
|
|
4061
4271
|
} catch (error) {
|
|
4062
4272
|
const raced = await this.reportRaceResponse(runToken, lease, body, payloadDigest);
|
|
4063
4273
|
if (raced) return raced;
|
|
@@ -4068,7 +4278,7 @@ var MachineGateway = class {
|
|
|
4068
4278
|
if (!deleting) await this.captureRunSnapshot(lease.runId, lease.loopId);
|
|
4069
4279
|
this.scheduler.addLoop(terminal.loop);
|
|
4070
4280
|
this.applyAutopauseTimer(lease.loopId, terminal);
|
|
4071
|
-
log$
|
|
4281
|
+
log$1.info({
|
|
4072
4282
|
runId: lease.runId,
|
|
4073
4283
|
ok: effectiveOk
|
|
4074
4284
|
}, "report: finalized");
|
|
@@ -4182,157 +4392,9 @@ function nextFires(cron, timezone, n) {
|
|
|
4182
4392
|
}
|
|
4183
4393
|
}
|
|
4184
4394
|
/**
|
|
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
|
|
4395
|
+
* Per-run loop-file diff. Computed lazily on the server at read time
|
|
4334
4396
|
* (no stored diffs): load run N's snapshot + the previous run's snapshot and diff
|
|
4335
|
-
* the two
|
|
4397
|
+
* the two name → metadata maps. For changed TEXT files we load both blobs and
|
|
4336
4398
|
* compute a unified line diff with `diff` (jsdiff) — a pure-string library, no
|
|
4337
4399
|
* process execution, so the server's zero-exec invariant holds. Binary/oversize
|
|
4338
4400
|
* files emit a size-delta marker only (no inline diff).
|
|
@@ -4353,7 +4415,7 @@ function withinDiffCap(e) {
|
|
|
4353
4415
|
}
|
|
4354
4416
|
async function bytesOf(e) {
|
|
4355
4417
|
if (!e.hash) return null;
|
|
4356
|
-
return await (await
|
|
4418
|
+
return await (await getLoopFiles()).readBlob(e.hash) ?? null;
|
|
4357
4419
|
}
|
|
4358
4420
|
function unified(path, oldText, newText, maxEditLength) {
|
|
4359
4421
|
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 +4428,7 @@ function expectedInputBytes(change, verifiedSizes) {
|
|
|
4366
4428
|
if (sizes.some((size) => size === void 0)) return null;
|
|
4367
4429
|
return sizes.reduce((sum, size) => sum + size, 0);
|
|
4368
4430
|
}
|
|
4369
|
-
/** Compute one run's
|
|
4431
|
+
/** Compute one run's loop-file diff. The default remains the web UI's complete
|
|
4370
4432
|
* view. Budgeted callers get explicit totals and stop before blob reads/jsdiff
|
|
4371
4433
|
* once any work limit is spent. */
|
|
4372
4434
|
async function computeRunDiff(runId, budget) {
|
|
@@ -4663,6 +4725,7 @@ function normalizedListRun(run) {
|
|
|
4663
4725
|
terminalAt: run.ts,
|
|
4664
4726
|
phase: run.phase,
|
|
4665
4727
|
status: run.status ?? null,
|
|
4728
|
+
attention: run.attention ?? null,
|
|
4666
4729
|
durationMs: run.durationMs ?? null,
|
|
4667
4730
|
tokenUsage: totalTokenUsage(run.usage ?? null),
|
|
4668
4731
|
message: message.value,
|
|
@@ -4717,6 +4780,7 @@ async function listHistory(loop, q) {
|
|
|
4717
4780
|
"terminal",
|
|
4718
4781
|
"phase",
|
|
4719
4782
|
"status",
|
|
4783
|
+
"attention",
|
|
4720
4784
|
"durationMs",
|
|
4721
4785
|
"tokenUsage",
|
|
4722
4786
|
"message",
|
|
@@ -4726,6 +4790,7 @@ async function listHistory(loop, q) {
|
|
|
4726
4790
|
fmt(run.ts),
|
|
4727
4791
|
run.phase,
|
|
4728
4792
|
run.status ?? null,
|
|
4793
|
+
run.attention ?? null,
|
|
4729
4794
|
run.durationMs ?? null,
|
|
4730
4795
|
totalTokenUsage(run.usage ?? null),
|
|
4731
4796
|
run.message ? truncate(run.message, 500, "use --run for detail").value : null,
|
|
@@ -4773,6 +4838,7 @@ async function detailHistory(loop, q) {
|
|
|
4773
4838
|
terminalAt: TERMINAL_PHASES.includes(run.phase) ? run.ts : null,
|
|
4774
4839
|
phase: run.phase,
|
|
4775
4840
|
status: run.status ?? null,
|
|
4841
|
+
attention: run.attention ?? null,
|
|
4776
4842
|
durationMs: run.durationMs ?? null,
|
|
4777
4843
|
tokenUsage: totalTokenUsage(run.usage ?? null),
|
|
4778
4844
|
message: message.value,
|
|
@@ -4781,6 +4847,7 @@ async function detailHistory(loop, q) {
|
|
|
4781
4847
|
errorTruncated: error.truncated,
|
|
4782
4848
|
finalText: finalText.value,
|
|
4783
4849
|
finalTextTruncated: finalText.truncated,
|
|
4850
|
+
fileNotices: run.fileNotices ?? [],
|
|
4784
4851
|
diffAvailable: !!snapshot,
|
|
4785
4852
|
diff: q.diff ? await boundedDiff(run.id) : {
|
|
4786
4853
|
included: false,
|
|
@@ -4798,6 +4865,7 @@ async function detailHistory(loop, q) {
|
|
|
4798
4865
|
["terminalAt", detail.terminalAt],
|
|
4799
4866
|
["phase", detail.phase],
|
|
4800
4867
|
["status", detail.status],
|
|
4868
|
+
["attention", detail.attention],
|
|
4801
4869
|
["durationMs", detail.durationMs],
|
|
4802
4870
|
["tokenUsage", detail.tokenUsage],
|
|
4803
4871
|
["message", detail.message],
|
|
@@ -4806,6 +4874,7 @@ async function detailHistory(loop, q) {
|
|
|
4806
4874
|
["errorTruncated", detail.errorTruncated],
|
|
4807
4875
|
["finalText", detail.finalText],
|
|
4808
4876
|
["finalTextTruncated", detail.finalTextTruncated],
|
|
4877
|
+
["fileNotices", { raw: JSON.stringify(detail.fileNotices) }],
|
|
4809
4878
|
["diffAvailable", detail.diffAvailable],
|
|
4810
4879
|
["diff", { raw: JSON.stringify(detail.diff) }]
|
|
4811
4880
|
]));
|
|
@@ -4820,18 +4889,10 @@ async function readLoopHistory(loop, flags) {
|
|
|
4820
4889
|
return parsed.value.mode === "detail" ? detailHistory(loop, parsed.value) : listHistory(loop, parsed.value);
|
|
4821
4890
|
}
|
|
4822
4891
|
/**
|
|
4823
|
-
*
|
|
4824
|
-
*
|
|
4825
|
-
*
|
|
4826
|
-
*
|
|
4827
|
-
*
|
|
4828
|
-
* POST /api/machine/cli (Bearer device OR run credential) → unified dispatch
|
|
4829
|
-
*
|
|
4830
|
-
* The verb router keys authority on credential type first (`dk_` device prefix
|
|
4831
|
-
* vs `rk_` run lease) and reuses the core gateway
|
|
4832
|
-
* methods (`createLoop`/`editLoop`) through the injected `MachineGateway`,
|
|
4833
|
-
* plus the credential-neutral history module. Run credentials expose only the
|
|
4834
|
-
* exactly-once report callback.
|
|
4892
|
+
* `/api/machine/cli` verb router. Authority keys on credential type: a device
|
|
4893
|
+
* (`dk_`) token gets owner verbs over loops bound to its machine, reusing
|
|
4894
|
+
* `MachineGateway`'s owner methods; a run (`rk_`) token gets no verb, because a
|
|
4895
|
+
* run reports only through its final response.
|
|
4835
4896
|
*/
|
|
4836
4897
|
var cliLog = logger.child({ mod: "cli" });
|
|
4837
4898
|
var CliGateway = class {
|
|
@@ -4845,15 +4906,6 @@ var CliGateway = class {
|
|
|
4845
4906
|
this.forceDeleteLoop = deps.forceDeleteLoop ?? forceDeleteLoop;
|
|
4846
4907
|
this.destructiveLog = deps.destructiveLog ?? ((event) => cliLog.warn(event, "force-delete: destructive server authority removal"));
|
|
4847
4908
|
}
|
|
4848
|
-
/**
|
|
4849
|
-
* The unified CLI endpoint. It is a ROUTER in front of the gateway logic that
|
|
4850
|
-
* already exists — never a rewrite — that keys authority on the credential type:
|
|
4851
|
-
* · DEVICE credential (`dk_`-prefixed) → owner authority over any loop bound to
|
|
4852
|
-
* the machine: `new`→createLoop, `loops`→listLoops, `edit`→editLoop,
|
|
4853
|
-
* `log`→readLoopHistory, `show`→describe. `report` is RUN-only (403).
|
|
4854
|
-
* · RUN credential (`rk_` run lease) → report and report help only.
|
|
4855
|
-
* Every owner verb is rejected.
|
|
4856
|
-
*/
|
|
4857
4909
|
async cli(token, argv) {
|
|
4858
4910
|
return finalizeCli(token.startsWith("dk_") ? await this.deviceCli(token, argv) : await this.runCli(token, argv));
|
|
4859
4911
|
}
|
|
@@ -4943,13 +4995,10 @@ var CliGateway = class {
|
|
|
4943
4995
|
}
|
|
4944
4996
|
};
|
|
4945
4997
|
}
|
|
4946
|
-
case "
|
|
4947
|
-
status: 403,
|
|
4948
|
-
body: { error: "pievo: report is a run-only verb; the owner edits via edit" }
|
|
4949
|
-
};
|
|
4998
|
+
case "file": return this.ownerFile(machineId, argv);
|
|
4950
4999
|
default: return {
|
|
4951
5000
|
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)` }
|
|
5001
|
+
body: { error: `pievo: unknown command "${verb}" for the device credential (try: new, loops, pause, start, stop, delete, run stop, edit, log, show, file)` }
|
|
4953
5002
|
};
|
|
4954
5003
|
}
|
|
4955
5004
|
}
|
|
@@ -4998,7 +5047,7 @@ var CliGateway = class {
|
|
|
4998
5047
|
}
|
|
4999
5048
|
async requireStopProtocol(machineId, running) {
|
|
5000
5049
|
if (!running) return void 0;
|
|
5001
|
-
if ((await getMachine(machineId))?.daemonProtocol ===
|
|
5050
|
+
if ((await getMachine(machineId))?.daemonProtocol === 5) return void 0;
|
|
5002
5051
|
return {
|
|
5003
5052
|
status: 426,
|
|
5004
5053
|
body: {
|
|
@@ -5109,121 +5158,90 @@ var CliGateway = class {
|
|
|
5109
5158
|
body: { text: `run already finished: ${runResultToken(stopped)}` }
|
|
5110
5159
|
};
|
|
5111
5160
|
}
|
|
5112
|
-
async runCli(runToken,
|
|
5113
|
-
|
|
5114
|
-
if (!lease) return {
|
|
5161
|
+
async runCli(runToken, _argv) {
|
|
5162
|
+
if (!await resolveLease(runToken)) return {
|
|
5115
5163
|
status: 401,
|
|
5116
5164
|
body: {
|
|
5117
5165
|
text: errorBlock("invalid or expired token", "UNAUTHORIZED"),
|
|
5118
5166
|
exitCode: 1
|
|
5119
5167
|
}
|
|
5120
5168
|
};
|
|
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 {
|
|
5169
|
+
return {
|
|
5145
5170
|
status: 403,
|
|
5146
5171
|
body: {
|
|
5147
|
-
text: errorBlock(
|
|
5172
|
+
text: errorBlock(RUN_HAS_NO_CLI, "FORBIDDEN"),
|
|
5148
5173
|
exitCode: 1
|
|
5149
5174
|
}
|
|
5150
5175
|
};
|
|
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
5176
|
}
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
const
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5177
|
+
/** `file list|get|set` over one loop's files. `set` takes base64 content so
|
|
5178
|
+
* arbitrary text (including leading `--`) survives argv transport. */
|
|
5179
|
+
async ownerFile(machineId, argv) {
|
|
5180
|
+
const [, sub, loopId, name] = argv;
|
|
5181
|
+
const loop = loopId ? await this.ownedLoop(machineId, loopId) : void 0;
|
|
5182
|
+
if (sub !== "list" && sub !== "get" && sub !== "set") return {
|
|
5183
|
+
status: 400,
|
|
5184
|
+
body: { error: `usage: ${FILE_USAGE}` }
|
|
5167
5185
|
};
|
|
5168
|
-
if (
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
text: verbHelpText("report", lease)
|
|
5186
|
+
if (!loop) return {
|
|
5187
|
+
status: 404,
|
|
5188
|
+
body: { error: "no such loop on this machine" }
|
|
5172
5189
|
};
|
|
5173
|
-
const
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5190
|
+
const names = loopFileNames(loop);
|
|
5191
|
+
if (sub === "list") return {
|
|
5192
|
+
status: 200,
|
|
5193
|
+
body: { text: doc(listBlock("files", [
|
|
5194
|
+
"name",
|
|
5195
|
+
"size",
|
|
5196
|
+
"limit",
|
|
5197
|
+
"updatedAt"
|
|
5198
|
+
], await Promise.all(names.map(async (file) => {
|
|
5199
|
+
const row = await getArtifactFile(loop.id, file);
|
|
5200
|
+
const live = row && !row.deleted && row.hash ? row : void 0;
|
|
5201
|
+
return [
|
|
5202
|
+
file,
|
|
5203
|
+
live?.size ?? 0,
|
|
5204
|
+
fileLimitLabel(file),
|
|
5205
|
+
live ? fmtTime(live.updatedAt) : null
|
|
5206
|
+
];
|
|
5207
|
+
}))), 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`])) }
|
|
5188
5208
|
};
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
text: renderReportedText(status, true)
|
|
5193
|
-
} : derr(applied.status ?? 409, applied.detail ?? "this run is no longer active", applied.code);
|
|
5194
|
-
}
|
|
5195
|
-
helpText(_lease) {
|
|
5196
|
-
return doc(listBlock("verbs", ["verb", "syntax"], [["report", "--status keep|no-change|block --message <summary>"]]), helpBlock(["Call exactly one pievo report before finishing", "No other command is accepted from a run credential"]));
|
|
5197
|
-
}
|
|
5198
|
-
async applyReportMutation(lease, leaseTokenHash, status, message) {
|
|
5199
|
-
const result = await recordRunReportOnce({
|
|
5200
|
-
loopId: lease.loopId,
|
|
5201
|
-
runId: lease.runId,
|
|
5202
|
-
leaseTokenHash,
|
|
5203
|
-
status,
|
|
5204
|
-
message
|
|
5205
|
-
});
|
|
5206
|
-
if (result.state === "applied") return {
|
|
5207
|
-
ok: true,
|
|
5208
|
-
detail: "reported"
|
|
5209
|
+
if (!name || !names.includes(name)) return {
|
|
5210
|
+
status: 404,
|
|
5211
|
+
body: { error: `no such loop file: ${name ?? ""} (files: ${names.join(", ")})` }
|
|
5209
5212
|
};
|
|
5210
|
-
if (
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5213
|
+
if (sub === "get") {
|
|
5214
|
+
const row = await getArtifactFile(loop.id, name);
|
|
5215
|
+
const bytes = await this.gateway.loopFiles.readBlob(row && !row.deleted && row.hash ? row.hash : EMPTY_SHA256);
|
|
5216
|
+
if (!bytes) return {
|
|
5217
|
+
status: 500,
|
|
5218
|
+
body: { error: "loop file bytes are missing" }
|
|
5219
|
+
};
|
|
5220
|
+
return {
|
|
5221
|
+
status: 200,
|
|
5222
|
+
body: { text: bytes.length ? bytes.toString("utf8") : "\n" }
|
|
5223
|
+
};
|
|
5224
|
+
}
|
|
5225
|
+
const encoded = parseFlags(argv.slice(4))["content-base64"];
|
|
5226
|
+
if (typeof encoded !== "string") return {
|
|
5227
|
+
status: 400,
|
|
5228
|
+
body: { error: `usage: ${FILE_USAGE}` }
|
|
5215
5229
|
};
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5230
|
+
const bytes = Buffer.from(encoded, "base64");
|
|
5231
|
+
const issue = loopFileIssue(name, bytes);
|
|
5232
|
+
if (issue) return {
|
|
5233
|
+
status: 413,
|
|
5234
|
+
body: { error: `${issue}; nothing was changed` }
|
|
5235
|
+
};
|
|
5236
|
+
const hash = await this.gateway.loopFiles.putBlob(bytes);
|
|
5237
|
+
if (hash !== "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") await recordBlob(hash, bytes.length);
|
|
5238
|
+
if (!await writeLoopFile(loop.id, name, hash, bytes.length)) return {
|
|
5239
|
+
status: 404,
|
|
5240
|
+
body: { error: "no such loop file" }
|
|
5221
5241
|
};
|
|
5222
5242
|
return {
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
code: "CONFLICT",
|
|
5226
|
-
status: 409
|
|
5243
|
+
status: 200,
|
|
5244
|
+
body: { text: `file: ${name}\nsize: ${bytes.length}\nupdated: true` }
|
|
5227
5245
|
};
|
|
5228
5246
|
}
|
|
5229
5247
|
async describe(loopId, opts = {}) {
|
|
@@ -5268,17 +5286,8 @@ var CliGateway = class {
|
|
|
5268
5286
|
})), scoped.elsewhere, await recentMachineRuns(loops, 3));
|
|
5269
5287
|
}
|
|
5270
5288
|
};
|
|
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
|
-
]);
|
|
5289
|
+
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`";
|
|
5290
|
+
var FILE_USAGE = "pievo file list <loop> | file get <loop> <name> | file set <loop> <name> --from <path>";
|
|
5282
5291
|
var PAUSED_FINISHING = "loop paused; current run is finishing";
|
|
5283
5292
|
var STOP_UPGRADE_REQUIRED = "Daemon upgrade required to stop a running process. Run `npm install -g @kky42/pievo@latest`, then `pievo daemon restart`.";
|
|
5284
5293
|
var FORCE_DELETE_CONFIRMATION = "delete-server-data-anyway";
|
|
@@ -5316,15 +5325,6 @@ function parseJsonFlag(raw) {
|
|
|
5316
5325
|
value: obj
|
|
5317
5326
|
};
|
|
5318
5327
|
}
|
|
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
5328
|
/** The structured keys a `/api/machine/cli` body may carry. The daemon renders
|
|
5329
5329
|
* `text` and exits with `exitCode`; two structured channels remain because the
|
|
5330
5330
|
* current daemon reads them as data:
|
|
@@ -5352,18 +5352,6 @@ function finalizeCli(res) {
|
|
|
5352
5352
|
}
|
|
5353
5353
|
return res;
|
|
5354
5354
|
}
|
|
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
5355
|
var DEVICE_VERB_HELP = {
|
|
5368
5356
|
new: {
|
|
5369
5357
|
syntax: "new --json '<config>' [--dry-run]",
|
|
@@ -5402,24 +5390,29 @@ var DEVICE_VERB_HELP = {
|
|
|
5402
5390
|
},
|
|
5403
5391
|
delete: {
|
|
5404
5392
|
syntax: "delete <id> [--force]",
|
|
5405
|
-
summary: "stop first, then delete server history and
|
|
5406
|
-
help: ["Stop this loop and delete its Pievo history and
|
|
5393
|
+
summary: "stop first, then delete server history, memory, and artifacts",
|
|
5394
|
+
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
5395
|
},
|
|
5408
5396
|
run: {
|
|
5409
5397
|
syntax: "run stop <run-id>",
|
|
5410
5398
|
summary: "stop one pending or running run without pausing its loop",
|
|
5411
5399
|
help: ["A running run remains running until the daemon confirms cancellation"]
|
|
5412
5400
|
},
|
|
5401
|
+
file: {
|
|
5402
|
+
syntax: "file list <id> | file get <id> <name> | file set <id> <name> --from <path>",
|
|
5403
|
+
summary: "inspect or replace a loop's MEMORY.md and artifact files",
|
|
5404
|
+
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"]
|
|
5405
|
+
},
|
|
5413
5406
|
log: {
|
|
5414
5407
|
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
5408
|
summary: "bounded terminal history or one detailed run",
|
|
5416
5409
|
help: ["Run `pievo log <id> --json` for structured history", "Run `pievo log <id> --run 12 --diff` for bounded run detail and artifact diff"]
|
|
5417
5410
|
}
|
|
5418
5411
|
};
|
|
5419
|
-
function verbHelpText(verb
|
|
5420
|
-
const spec =
|
|
5412
|
+
function verbHelpText(verb) {
|
|
5413
|
+
const spec = DEVICE_VERB_HELP[verb];
|
|
5421
5414
|
if (!spec) return void 0;
|
|
5422
|
-
return doc(kvLine("verb", verb), kvLine("syntax", spec.syntax), kvLine("summary", spec.summary),
|
|
5415
|
+
return doc(kvLine("verb", verb), kvLine("syntax", spec.syntax), kvLine("summary", spec.summary), helpBlock(spec.help));
|
|
5423
5416
|
}
|
|
5424
5417
|
function loopEnvelope(loop) {
|
|
5425
5418
|
return { ...canonicalLoopEnvelope(loop) };
|
|
@@ -5456,7 +5449,8 @@ function renderShowText(loop, env, totalRuns, lastRun, opts = {}) {
|
|
|
5456
5449
|
const help = [
|
|
5457
5450
|
`Run \`pievo show ${loop.id} --full\` to see the complete prompt`,
|
|
5458
5451
|
`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
|
|
5452
|
+
`Run \`pievo log ${loop.id}\` to see recent run results`,
|
|
5453
|
+
`Run \`pievo file list ${loop.id}\` to see the loop's memory and artifact files`
|
|
5460
5454
|
];
|
|
5461
5455
|
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
5456
|
}
|
|
@@ -5544,9 +5538,6 @@ function renderHomeText(ctx, presence, here, elsewhere, recent) {
|
|
|
5544
5538
|
"Run `pievo new --json '{...}'` to create a loop"
|
|
5545
5539
|
]));
|
|
5546
5540
|
}
|
|
5547
|
-
function reportStatus(s) {
|
|
5548
|
-
return s === "keep" || s === "no-change" || s === "block" ? s : void 0;
|
|
5549
|
-
}
|
|
5550
5541
|
/** Tiny flag parser: `--k v` pairs, bare `--flag` → true, first positional under `_`.
|
|
5551
5542
|
* Every key/value is NUL-stripped at this wire boundary before validation or
|
|
5552
5543
|
* persistence because Postgres text rejects NUL. */
|
|
@@ -5677,19 +5668,13 @@ var Scheduler = class {
|
|
|
5677
5668
|
function msg(err) {
|
|
5678
5669
|
return err instanceof Error ? err.message : String(err);
|
|
5679
5670
|
}
|
|
5680
|
-
/**
|
|
5681
|
-
*
|
|
5682
|
-
* migrations, wire the machine gateway's dispatcher into the Scheduler, start
|
|
5683
|
-
* the Scheduler + an offline-sweep interval. Guarded on `globalThis` so it
|
|
5684
|
-
* survives dev HMR and runs at most once per process. Call `ensureServer()`
|
|
5685
|
-
* from any server-side entry (the standalone machine server / TanStack server
|
|
5686
|
-
* fns); the first call boots, the rest share the same instance.
|
|
5687
|
-
*/
|
|
5671
|
+
/** One-time, idempotent in-process backend boot (migrations, scheduler, sweep and
|
|
5672
|
+
* storage-maintenance intervals). Guarded on `globalThis` to survive dev HMR. */
|
|
5688
5673
|
var boot_exports = /* @__PURE__ */ __exportAll({
|
|
5689
5674
|
ensureServer: () => ensureServer,
|
|
5690
|
-
getArtifactSync: () => getArtifactSync,
|
|
5691
5675
|
getCliGateway: () => getCliGateway,
|
|
5692
|
-
getGateway: () => getGateway
|
|
5676
|
+
getGateway: () => getGateway,
|
|
5677
|
+
getLoopFiles: () => getLoopFiles
|
|
5693
5678
|
});
|
|
5694
5679
|
var g = globalThis;
|
|
5695
5680
|
function ensureServer() {
|
|
@@ -5714,9 +5699,7 @@ async function boot() {
|
|
|
5714
5699
|
};
|
|
5715
5700
|
let gateway;
|
|
5716
5701
|
const scheduler = new Scheduler({ dispatch: (loop) => gateway.dispatcher.dispatch(loop) });
|
|
5717
|
-
|
|
5718
|
-
gateway = new MachineGateway(scheduler, blobStore);
|
|
5719
|
-
const artifactSync = new ArtifactSync(blobStore);
|
|
5702
|
+
gateway = new MachineGateway(scheduler, createBlobStore());
|
|
5720
5703
|
const cliGateway = new CliGateway(gateway);
|
|
5721
5704
|
await scheduler.start(abort.signal);
|
|
5722
5705
|
const sweep = setInterval(() => void gateway.sweep().catch((err) => logger.error({ err: String(err) }, "sweep tick failed")), ONLINE_TTL_MS);
|
|
@@ -5729,7 +5712,6 @@ async function boot() {
|
|
|
5729
5712
|
return {
|
|
5730
5713
|
scheduler,
|
|
5731
5714
|
gateway,
|
|
5732
|
-
artifactSync,
|
|
5733
5715
|
cliGateway,
|
|
5734
5716
|
abort,
|
|
5735
5717
|
shutdown
|
|
@@ -5738,11 +5720,11 @@ async function boot() {
|
|
|
5738
5720
|
async function getGateway() {
|
|
5739
5721
|
return (await ensureServer()).gateway;
|
|
5740
5722
|
}
|
|
5741
|
-
async function
|
|
5742
|
-
return (await ensureServer()).
|
|
5723
|
+
async function getLoopFiles() {
|
|
5724
|
+
return (await ensureServer()).gateway.loopFiles;
|
|
5743
5725
|
}
|
|
5744
5726
|
async function getCliGateway() {
|
|
5745
5727
|
return (await ensureServer()).cliGateway;
|
|
5746
5728
|
}
|
|
5747
5729
|
//#endregion
|
|
5748
|
-
export {
|
|
5730
|
+
export { listRuns as A, reconciliationStatesForRuns as B, getRun as C, listArtifacts as D, isOutdated as E, machineIdFromToken as F, stopLoop as G, requestRunCancel as H, machinePresence as I, updateMachine as J, tryDeleteLoop as K, mintDeviceToken as L, loopFileNames as M, loopTags_Bi7Bm7v8_exports as N, listLoops as O, loopsForMachine as P, pauseLoop as R, getMachine as S, hasRunningRun as T, sha256 as U, requestDeleteLoop as V, startLoop as W, validateLoopTags as Y, 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, updateLoop as q, boot_CecCDRoH_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, recentLoopUsage as z };
|