@kky42/pievo-server 2.4.8 → 2.4.10
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/{MachinesModal-ClNm1IAU.js → MachinesModal-ByHejech.js} +1 -1
- package/.output/public/assets/app-IrlFuBXa.css +2 -0
- package/.output/public/assets/{index-DM5pYU2E.js → index-ia26HsP1.js} +3 -3
- package/.output/public/assets/{invite._token-CjcTZgvV.js → invite._token-tDfe3p0Z.js} +1 -1
- package/.output/public/assets/{loops._loopId-1WUV1NQv.js → loops._loopId-CuYq_XgT.js} +3 -3
- package/.output/public/assets/{loops._loopId_.runs._runId-BgUc4GIy.js → loops._loopId_.runs._runId-mOaF51Qu.js} +1 -1
- package/.output/public/assets/{routes-DTmJ-YqP.js → routes-Dbl_3lt_.js} +1 -1
- package/.output/public/assets/{t._teamId-BmUhbnOU.js → t._teamId-BVsl0EGm.js} +1 -1
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +1 -1
- package/.output/server/_ssr/{DashboardView-DTfEhixr.mjs → DashboardView-BMHMrOfC.mjs} +15 -8
- package/.output/server/_ssr/{MachinesModal-D54Uza6A.mjs → MachinesModal-BhRw7oOx.mjs} +22 -17
- package/.output/server/_ssr/{SignIn-C2JkrSfV.mjs → SignIn-BhoZIOUp.mjs} +3 -3
- package/.output/server/_ssr/{SocialLinks-dPCmZWvC.mjs → SocialLinks-DVCLmql0.mjs} +2 -11
- package/.output/server/_ssr/{actionUi-DCNmrAjw.mjs → actionUi-Cf6DuEC2.mjs} +2 -2
- package/.output/server/_ssr/{artifactFiles-DBu2yk9a.mjs → artifactFiles-ChuJ4PzC.mjs} +3 -4
- package/.output/server/_ssr/{artifactKind-CJMjNtSC.mjs → artifactKind-DXumOv7c.mjs} +3 -7
- package/.output/server/_ssr/{artifacts-8JYBE5zx.mjs → artifacts-DOLHJHcd.mjs} +1 -1
- package/.output/server/_ssr/{auth-b1o4lrGO.mjs → auth-LwPp_iim.mjs} +9 -13
- package/.output/server/_ssr/{boot-CEPRQU5M.mjs → boot-BIxtZHU-.mjs} +67 -212
- package/.output/server/_ssr/{boot-dZdGfkZm.mjs → boot-D8cj4uZj.mjs} +1 -1
- package/.output/server/_ssr/{bun-sqlite-dialect-BW9W1_Ps-D2QuIuwp.mjs → bun-sqlite-dialect-BW9W1_Ps-CHMDkKqA.mjs} +2 -2
- package/.output/server/_ssr/{d1-sqlite-dialect-BLC8LXE6-CdT8qARs.mjs → d1-sqlite-dialect-BLC8LXE6-DYum6qky.mjs} +2 -2
- package/.output/server/_ssr/db-BphSJs66.mjs +2 -0
- package/.output/server/_ssr/{dist-E-fSrrAE.mjs → dist-bWqs5sTy.mjs} +2 -2
- package/.output/server/_ssr/{factory-CzRRgvct.mjs → factory-pBwrjVTI.mjs} +2 -2
- package/.output/server/_ssr/{invite._token-DDDd3vNf.mjs → invite._token-CPSvdDox.mjs} +4 -4
- package/.output/server/_ssr/{invite._token-ZthOtmu9.mjs → invite._token-CRBsTSTZ.mjs} +6 -6
- package/.output/server/_ssr/{invite._token-CGzJGIfm.mjs → invite._token-CaTLt92a.mjs} +2 -2
- package/.output/server/_ssr/{kysely-adapter-cB-XpVsU.mjs → kysely-adapter-DMf5pFTf.mjs} +1 -1
- package/.output/server/_ssr/{lifecycleUi-DOr6Af13.mjs → lifecycleUi-DauKavgx.mjs} +1 -2
- package/.output/server/_ssr/logger-EinA2J6y.mjs +2 -0
- package/.output/server/_ssr/{loopApi-DxDYnPI7.mjs → loopApi-Im6JSK0p.mjs} +10 -11
- package/.output/server/_ssr/{loopProjection-CeH1HELf.mjs → loopProjection-DIpGR6rp.mjs} +2 -4
- package/.output/server/_ssr/{loops._loopId-BTAKWGvT.mjs → loops._loopId-B5iPseDG.mjs} +13 -27
- package/.output/server/_ssr/{loops._loopId_.runs._runId-BGl_4tUK.mjs → loops._loopId_.runs._runId-BA-P6sGA.mjs} +6 -44
- package/.output/server/_ssr/{loops._loopId_.runs._runId-BRzoEUe1.mjs → loops._loopId_.runs._runId-DVtG0xYb.mjs} +3 -3
- package/.output/server/_ssr/{machineFns-Bmh_4hnx.mjs → machineFns-Bb9KNXun.mjs} +3 -5
- package/.output/server/_ssr/{node-sqlite-dialect-C8O4zOiM.mjs → node-sqlite-dialect-Dho9YjXb.mjs} +2 -2
- package/.output/server/_ssr/{router-Vpcd2SDx.mjs → router-DPz3uoUr.mjs} +22 -30
- package/.output/server/_ssr/{routes-BczmJLuq.mjs → routes-B9zD2h4Y.mjs} +5 -7
- package/.output/server/_ssr/{routes-DBCYbmLW.mjs → routes-BuILEmHs.mjs} +5 -5
- package/.output/server/_ssr/{routes-D5GHah06.mjs → routes-FO0MFfYK.mjs} +2 -2
- package/.output/server/_ssr/runDiff-BLdot9mi.mjs +2 -0
- package/.output/server/_ssr/ssr.mjs +39 -39
- package/.output/server/_ssr/store-CFCp_ggC.mjs +2 -0
- package/.output/server/_ssr/{t._teamId-COKCNxP7.mjs → t._teamId-1t5wOXrV.mjs} +5 -5
- package/.output/server/_ssr/{t._teamId-D0Y3DqcE.mjs → t._teamId-A37pQ8Vp.mjs} +5 -5
- package/.output/server/_ssr/{t._teamId-CrZ7EkUL.mjs → t._teamId-D1BTwhWf.mjs} +2 -2
- package/.output/server/_ssr/{teamFns-DctV7gTn.mjs → teamFns-DfqtuBW2.mjs} +3 -7
- package/.output/server/_ssr/{teamFns-BMR5TRJw.mjs → teamFns-cI_tsiCv.mjs} +2 -3
- package/.output/server/_ssr/{tokens-BP8ltV6J.mjs → tokens-CUz4LDLm.mjs} +1 -1
- package/.output/server/_ssr/{ui-BEPYdOmJ.mjs → ui-DGz-q9MT.mjs} +1 -22
- package/.output/server/{_tanstack-start-manifest_v-CevOnXtc.mjs → _tanstack-start-manifest_v-CojB4Veh.mjs} +9 -9
- package/.output/server/drizzle/0001_add_pi_agent.sql +9 -0
- package/.output/server/drizzle/meta/_journal.json +7 -0
- package/.output/server/index.mjs +61 -61
- package/drizzle/0001_add_pi_agent.sql +9 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/.output/public/assets/app-CuZ0I3Mb.css +0 -2
- package/.output/server/_ssr/db-CJUmGnnY.mjs +0 -2
- package/.output/server/_ssr/logger-Co0_q7fy.mjs +0 -2
- package/.output/server/_ssr/runDiff-Ce1DhI7z.mjs +0 -2
- package/.output/server/_ssr/store-BpbKfvFY.mjs +0 -2
|
@@ -11,7 +11,7 @@ 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/boot-
|
|
14
|
+
//#region node_modules/.nitro/vite/services/ssr/assets/boot-BIxtZHU-.js
|
|
15
15
|
var import_pino = /* @__PURE__ */ __toESM(require_pino());
|
|
16
16
|
var import_pino_pretty = /* @__PURE__ */ __toESM(require_pino_pretty());
|
|
17
17
|
function defineErrorCodes(codes) {
|
|
@@ -1268,7 +1268,6 @@ function r2Config() {
|
|
|
1268
1268
|
* a deploy can tune them without a rebuild. Each has a safe, generous default —
|
|
1269
1269
|
* the bias is "keep storage bounded without ever surprising a healthy loop".
|
|
1270
1270
|
*/
|
|
1271
|
-
/** A positive env integer, or `fallback` when unset / unparseable / non-positive. */
|
|
1272
1271
|
function posIntEnv(name, fallback) {
|
|
1273
1272
|
const raw = process.env[name]?.trim();
|
|
1274
1273
|
if (!raw) return fallback;
|
|
@@ -1321,11 +1320,9 @@ function dbWatchdogEnabled() {
|
|
|
1321
1320
|
if (process.env.VITEST || false) return false;
|
|
1322
1321
|
return Boolean(databaseUrl());
|
|
1323
1322
|
}
|
|
1324
|
-
/** How often the DB watchdog pings `select 1`. Default 20s. */
|
|
1325
1323
|
function dbWatchdogIntervalMs() {
|
|
1326
1324
|
return posIntEnv("PIEVO_DB_WATCHDOG_INTERVAL_MS", 2e4);
|
|
1327
1325
|
}
|
|
1328
|
-
/** Hard per-ping deadline; a ping that exceeds it counts as a failure. Default 5s. */
|
|
1329
1326
|
function dbWatchdogTimeoutMs() {
|
|
1330
1327
|
return posIntEnv("PIEVO_DB_WATCHDOG_TIMEOUT_MS", 5e3);
|
|
1331
1328
|
}
|
|
@@ -1338,8 +1335,6 @@ function dbWatchdogFailureThreshold() {
|
|
|
1338
1335
|
return posIntEnv("PIEVO_DB_WATCHDOG_FAILURES", 3);
|
|
1339
1336
|
}
|
|
1340
1337
|
/**
|
|
1341
|
-
* Central logger (pino + pino-pretty).
|
|
1342
|
-
*
|
|
1343
1338
|
* Uses pino-pretty as a *synchronous* destination stream (not a worker-thread
|
|
1344
1339
|
* transport) so logs flush before process.exit() in the one-shot CLIs. Writes to
|
|
1345
1340
|
* stderr (fd 2) to keep stdout clean for QR codes and human CLI output.
|
|
@@ -1405,7 +1400,7 @@ function poolOptionsFor(url, transactionPooler = isTransactionPooler(url)) {
|
|
|
1405
1400
|
};
|
|
1406
1401
|
}
|
|
1407
1402
|
/** The coding agent a loop is bound to AND executed with (BYOA on the owner's
|
|
1408
|
-
* machine): `claude-code` → Claude Code, `codex` → `codex exec
|
|
1403
|
+
* machine): `claude-code` → Claude Code, `codex` → `codex exec`, `pi` → Pi.
|
|
1409
1404
|
* Non-Claude agents may still have thinner daemon telemetry until stream
|
|
1410
1405
|
* integration lands; execution itself is real for every value.
|
|
1411
1406
|
*
|
|
@@ -1415,9 +1410,11 @@ function poolOptionsFor(url, transactionPooler = isTransactionPooler(url)) {
|
|
|
1415
1410
|
* web agent `<select>` (LoopForm). So widening the set is a one-line edit HERE with
|
|
1416
1411
|
* no other server change (the daemon's own enum in `packages/daemon/src/create.ts`
|
|
1417
1412
|
* is a separate package, widened alongside). */
|
|
1418
|
-
var CODING_AGENTS = [
|
|
1419
|
-
|
|
1420
|
-
|
|
1413
|
+
var CODING_AGENTS = [
|
|
1414
|
+
"claude-code",
|
|
1415
|
+
"codex",
|
|
1416
|
+
"pi"
|
|
1417
|
+
];
|
|
1421
1418
|
function coerceCodingAgent(value) {
|
|
1422
1419
|
return typeof value === "string" && CODING_AGENTS.includes(value) ? value : null;
|
|
1423
1420
|
}
|
|
@@ -1434,23 +1431,16 @@ function coerceCodingAgent(value) {
|
|
|
1434
1431
|
* columns use `jsonb().$type<>()`; booleans use native `boolean()`.
|
|
1435
1432
|
*/
|
|
1436
1433
|
var machines = pgTable("machines", {
|
|
1437
|
-
/** m-sha256(deviceToken)[:16] */
|
|
1438
1434
|
id: text("id").primaryKey(),
|
|
1439
|
-
/** Owning user (Better Auth user.id) — creator attribution. */
|
|
1440
1435
|
userId: text("user_id").notNull(),
|
|
1441
|
-
/** Owning team — the scope machines and loops are listed by. */
|
|
1442
1436
|
teamId: text("team_id").notNull(),
|
|
1443
|
-
/** Friendly name (set AFTER the daemon connects; empty string = pending/unnamed). */
|
|
1444
1437
|
name: text("name").notNull(),
|
|
1445
|
-
/** Daemon-reported machine identity (captured on first connect). */
|
|
1446
1438
|
hostname: text("hostname"),
|
|
1447
1439
|
platform: text("platform"),
|
|
1448
1440
|
arch: text("arch"),
|
|
1449
|
-
/** Daemon package version reported on poll. Drives the web upgrade hint. */
|
|
1450
1441
|
daemonVersion: text("daemon_version"),
|
|
1451
1442
|
/** Breaking machine protocol last observed on poll. */
|
|
1452
1443
|
daemonProtocol: integer("daemon_protocol"),
|
|
1453
|
-
/** Hash of the device token (machine identity derives from the token). */
|
|
1454
1444
|
tokenHash: text("token_hash").notNull(),
|
|
1455
1445
|
/**
|
|
1456
1446
|
* Plaintext device token. Stored so the UI can re-show the connect command
|
|
@@ -1458,67 +1448,51 @@ var machines = pgTable("machines", {
|
|
|
1458
1448
|
* for a self-hosted team tool where the DB is already the trust root).
|
|
1459
1449
|
*/
|
|
1460
1450
|
token: text("token"),
|
|
1461
|
-
/** Workdir allowlist the daemon enforces as cwd jail; null/[] = unrestricted. */
|
|
1462
1451
|
roots: jsonb("roots").$type(),
|
|
1463
|
-
/** Last WS contact (ISO). */
|
|
1464
1452
|
lastSeen: text("last_seen"),
|
|
1465
|
-
/** Live WS connection state. */
|
|
1466
1453
|
online: boolean("online").notNull().default(false),
|
|
1467
1454
|
createdAt: text("created_at").notNull()
|
|
1468
1455
|
}, (t) => [index("machines_user_idx").on(t.userId), index("machines_team_idx").on(t.teamId)]);
|
|
1469
1456
|
var loops = pgTable("loops", {
|
|
1470
1457
|
id: text("id").primaryKey(),
|
|
1471
|
-
/** Owning user (creator attribution). */
|
|
1472
1458
|
userId: text("user_id").notNull(),
|
|
1473
|
-
/** Owning team — the scope loops are listed and authorized by. */
|
|
1474
1459
|
teamId: text("team_id").notNull(),
|
|
1475
1460
|
/** Execution machine (set at creation; no cross-machine fallback). */
|
|
1476
1461
|
machineId: text("machine_id").notNull(),
|
|
1477
1462
|
name: text("name").notNull(),
|
|
1478
|
-
/** The user's complete runtime prompt. Canonical writes always provide it. */
|
|
1479
1463
|
prompt: text("prompt").notNull(),
|
|
1480
|
-
/** User-authored meanings appended to every delivered prompt. */
|
|
1481
1464
|
statusKeep: text("status_keep").notNull(),
|
|
1482
1465
|
statusNoChange: text("status_no_change").notNull(),
|
|
1483
1466
|
statusBlock: text("status_block").notNull(),
|
|
1484
1467
|
/** Exact workdir-relative artifact paths collected by the daemon after provider exit. */
|
|
1485
1468
|
artifacts: jsonb("artifacts").$type().notNull().default([]),
|
|
1486
1469
|
cron: text("cron").notNull(),
|
|
1487
|
-
/** Cadence facts stay denormalized internally; external writes use LoopSchedule. */
|
|
1488
1470
|
scheduleMode: text("schedule_mode", { enum: ["cron", "continuous"] }).notNull(),
|
|
1489
1471
|
/** Cron-only overlap behavior. Continuous runs never overlap. */
|
|
1490
1472
|
cronOverlap: text("cron_overlap", { enum: ["skip", "queue-one"] }).notNull(),
|
|
1491
|
-
/** Delay used when a done/error terminal restores continuous cadence. */
|
|
1492
1473
|
continuousDelayMinutes: integer("continuous_delay_minutes").notNull(),
|
|
1493
|
-
/** IANA tz for cron rows; null for continuous rows. */
|
|
1494
1474
|
timezone: text("timezone"),
|
|
1495
|
-
/** Absolute project dir ON THE MACHINE the agent runs in (cwd). */
|
|
1496
1475
|
workdir: text("workdir").notNull(),
|
|
1497
1476
|
/** Durable Stop-before-delete marker. A deleting loop is never claimable. */
|
|
1498
1477
|
deleteRequestedAt: timestamp("delete_requested_at", {
|
|
1499
1478
|
withTimezone: true,
|
|
1500
1479
|
mode: "string"
|
|
1501
1480
|
}),
|
|
1502
|
-
/** Diagnostic annotation for a paused loop. */
|
|
1503
1481
|
pauseCause: jsonb("pause_cause").$type(),
|
|
1504
|
-
/** Optional provider model id. Null delegates model selection to the coding-agent CLI. */
|
|
1505
1482
|
model: text("model"),
|
|
1506
|
-
/** Optional provider reasoning-effort value. Null delegates to the coding-agent CLI. */
|
|
1507
1483
|
reasoningEffort: text("reasoning_effort"),
|
|
1508
1484
|
/** Coding agent this loop is BOUND TO and EXECUTED with: the daemon's
|
|
1509
1485
|
* `buildAgentSpawn` branches on this value (`claude-code` → claude,
|
|
1510
|
-
* `codex` → `codex exec`). Measured from the creating CLI's env when
|
|
1486
|
+
* `codex` → `codex exec`, `pi` → Pi print mode). Measured from the creating CLI's env when
|
|
1511
1487
|
* detectable, else the declared/selected value. The text column carries
|
|
1512
1488
|
* both a TypeScript enum and a database CHECK constraint. */
|
|
1513
1489
|
agent: text("agent", { enum: CODING_AGENTS }).notNull(),
|
|
1514
1490
|
enabled: boolean("enabled").notNull().default(true),
|
|
1515
|
-
/** One-shot override, independent of the recurring cadence (ISO). */
|
|
1516
1491
|
nextRunAt: text("next_run_at"),
|
|
1517
1492
|
/** The next recurring cadence occurrence not yet materialized as a run. Cron
|
|
1518
1493
|
* advances this fact after each due occurrence; continuous clears it on due
|
|
1519
1494
|
* and restores it from a successful/error terminal. */
|
|
1520
1495
|
nextCadenceAt: text("next_cadence_at"),
|
|
1521
|
-
/** Last history number assigned under this loop's row lock. */
|
|
1522
1496
|
lastRunIndex: integer("last_run_index").notNull().default(0),
|
|
1523
1497
|
createdAt: text("created_at").notNull(),
|
|
1524
1498
|
updatedAt: text("updated_at").notNull()
|
|
@@ -1527,7 +1501,7 @@ var loops = pgTable("loops", {
|
|
|
1527
1501
|
index("loops_team_idx").on(t.teamId),
|
|
1528
1502
|
index("loops_machine_idx").on(t.machineId),
|
|
1529
1503
|
index("delete_requested_loops").on(t.deleteRequestedAt).where(sql`${t.deleteRequestedAt} IS NOT NULL`),
|
|
1530
|
-
check("loops_agent_check", sql`${t.agent} IN ('claude-code', 'codex')`),
|
|
1504
|
+
check("loops_agent_check", sql`${t.agent} IN ('claude-code', 'codex', 'pi')`),
|
|
1531
1505
|
check("loops_schedule_mode_check", sql`${t.scheduleMode} IN ('cron', 'continuous')`),
|
|
1532
1506
|
check("loops_cron_overlap_check", sql`${t.cronOverlap} IN ('skip', 'queue-one')`)
|
|
1533
1507
|
]);
|
|
@@ -1535,11 +1509,9 @@ var runs = pgTable("runs", {
|
|
|
1535
1509
|
id: text("id").primaryKey(),
|
|
1536
1510
|
loopId: text("loop_id").notNull(),
|
|
1537
1511
|
machineId: text("machine_id").notNull(),
|
|
1538
|
-
/** Execution profile captured atomically at claim. Pending/unclaimed rows are null. */
|
|
1539
1512
|
agent: text("agent", { enum: CODING_AGENTS }),
|
|
1540
1513
|
model: text("model"),
|
|
1541
1514
|
reasoningEffort: text("reasoning_effort"),
|
|
1542
|
-
/** 1-based execution/terminal order, assigned at claim or unclaimed terminalization. */
|
|
1543
1515
|
runIndex: integer("run_index"),
|
|
1544
1516
|
phase: text("phase", { enum: [
|
|
1545
1517
|
"pending",
|
|
@@ -1551,12 +1523,9 @@ var runs = pgTable("runs", {
|
|
|
1551
1523
|
/** Durable queue authority. It only promotes system→owner; diagnostic trigger
|
|
1552
1524
|
* reasons, if added later, must never drive lifecycle state. */
|
|
1553
1525
|
requestedBy: text("requested_by", { enum: ["owner", "system"] }).notNull().default("system"),
|
|
1554
|
-
/** Current phase/event timestamp. */
|
|
1555
1526
|
ts: text("ts").notNull(),
|
|
1556
|
-
/** Immutable queue/history age anchor plus mutation timestamp. */
|
|
1557
1527
|
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP::text`),
|
|
1558
1528
|
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP::text`),
|
|
1559
|
-
/** Canonical report status. */
|
|
1560
1529
|
status: text("status", { enum: [
|
|
1561
1530
|
"keep",
|
|
1562
1531
|
"no-change",
|
|
@@ -1567,18 +1536,14 @@ var runs = pgTable("runs", {
|
|
|
1567
1536
|
exitCode: integer("exit_code"),
|
|
1568
1537
|
finalText: text("final_text"),
|
|
1569
1538
|
error: text("error"),
|
|
1570
|
-
/** Provider session id on the machine, retained for owner-side continuation. */
|
|
1571
1539
|
sessionId: text("session_id"),
|
|
1572
|
-
/** Provider-neutral token usage. Dollar cost is deliberately not stored. */
|
|
1573
1540
|
usage: jsonb("usage").$type(),
|
|
1574
1541
|
/** Monotonic cancellation intent. Running becomes canceled only on daemon report. */
|
|
1575
1542
|
cancelRequestedAt: timestamp("cancel_requested_at", {
|
|
1576
1543
|
withTimezone: true,
|
|
1577
1544
|
mode: "string"
|
|
1578
1545
|
}),
|
|
1579
|
-
/** Last poll where this machine declared the run active. Timeout authority. */
|
|
1580
1546
|
heartbeatAt: text("heartbeat_at"),
|
|
1581
|
-
/** Latest rejected terminal-report diagnosis, if any. */
|
|
1582
1547
|
reportIncident: jsonb("report_incident").$type()
|
|
1583
1548
|
}, (t) => [
|
|
1584
1549
|
index("runs_loop_idx").on(t.loopId),
|
|
@@ -1589,13 +1554,12 @@ var runs = pgTable("runs", {
|
|
|
1589
1554
|
index("runs_loop_terminal_history_idx").on(t.loopId, t.runIndex).where(sql`${t.runIndex} IS NOT NULL AND ${t.phase} IN ('done', 'error', 'canceled')`),
|
|
1590
1555
|
uniqueIndex("runs_loop_pending_idx").on(t.loopId).where(sql`${t.phase} = 'pending'`),
|
|
1591
1556
|
uniqueIndex("one_running_run_per_loop").on(t.loopId).where(sql`${t.phase} = 'running'`),
|
|
1592
|
-
check("runs_agent_check", sql`${t.agent} IS NULL OR ${t.agent} IN ('claude-code', 'codex')`),
|
|
1557
|
+
check("runs_agent_check", sql`${t.agent} IS NULL OR ${t.agent} IN ('claude-code', 'codex', 'pi')`),
|
|
1593
1558
|
check("runs_phase_check", sql`${t.phase} IN ('pending', 'running', 'done', 'error', 'canceled')`),
|
|
1594
1559
|
check("runs_requested_by_check", sql`${t.requestedBy} IN ('owner', 'system')`),
|
|
1595
1560
|
check("runs_status_check", sql`${t.status} IS NULL OR ${t.status} IN ('keep', 'no-change', 'block')`)
|
|
1596
1561
|
]);
|
|
1597
1562
|
var runLeases = pgTable("run_leases", {
|
|
1598
|
-
/** sha256 hex of the full `rk_…` wire token. */
|
|
1599
1563
|
tokenHash: text("token_hash").primaryKey(),
|
|
1600
1564
|
runId: text("run_id").notNull(),
|
|
1601
1565
|
loopId: text("loop_id").notNull(),
|
|
@@ -1645,11 +1609,8 @@ var terminalReportIncidents = pgTable("terminal_report_incidents", {
|
|
|
1645
1609
|
check("terminal_report_incidents_disposition_check", sql`${t.disposition} IN ('run-error', 'telemetry-rejected')`)
|
|
1646
1610
|
]);
|
|
1647
1611
|
var connectKeys = pgTable("connect_keys", {
|
|
1648
|
-
/** m-sha256(connectKey)[:16] — the machine id this key self-registers as. */
|
|
1649
1612
|
machineId: text("machine_id").primaryKey(),
|
|
1650
|
-
/** The user who minted the key (the authenticated dashboard session). */
|
|
1651
1613
|
userId: text("user_id").notNull(),
|
|
1652
|
-
/** The validated active team the key was minted under. */
|
|
1653
1614
|
teamId: text("team_id").notNull(),
|
|
1654
1615
|
/** Mint time (ISO) — drives the TTL. */
|
|
1655
1616
|
mintedAt: text("minted_at").notNull()
|
|
@@ -1657,7 +1618,6 @@ var connectKeys = pgTable("connect_keys", {
|
|
|
1657
1618
|
var teams = pgTable("teams", {
|
|
1658
1619
|
id: text("id").primaryKey(),
|
|
1659
1620
|
name: text("name").notNull(),
|
|
1660
|
-
/** The user whose personal team this is (null for the open-mode shared team). */
|
|
1661
1621
|
ownerUserId: text("owner_user_id"),
|
|
1662
1622
|
createdAt: text("created_at").notNull()
|
|
1663
1623
|
});
|
|
@@ -1673,18 +1633,13 @@ var teamMembers = pgTable("team_members", {
|
|
|
1673
1633
|
check("team_members_role_check", sql`${t.role} IN ('owner', 'member')`)
|
|
1674
1634
|
]);
|
|
1675
1635
|
var teamInvites = pgTable("team_invites", {
|
|
1676
|
-
/** The wire token the recipient presents (`/invite/<token>`). */
|
|
1677
1636
|
token: text("token").primaryKey(),
|
|
1678
1637
|
teamId: text("team_id").notNull(),
|
|
1679
|
-
/** Role granted on redeem (capped at the inviter's role at mint time). */
|
|
1680
1638
|
role: text("role", { enum: ["owner", "member"] }).notNull().default("member"),
|
|
1681
|
-
/** The owner who minted the invite (attribution). */
|
|
1682
1639
|
invitedByUserId: text("invited_by_user_id").notNull(),
|
|
1683
|
-
/** When the invite lapses (ISO). A redeem past this is refused. */
|
|
1684
1640
|
expiresAt: text("expires_at").notNull(),
|
|
1685
1641
|
/** Single-use stamp: set on redeem so the same link can't be reused. */
|
|
1686
1642
|
redeemedAt: text("redeemed_at"),
|
|
1687
|
-
/** The user who redeemed it (attribution). */
|
|
1688
1643
|
redeemedByUserId: text("redeemed_by_user_id"),
|
|
1689
1644
|
createdAt: text("created_at").notNull()
|
|
1690
1645
|
}, (t) => [index("team_invites_team_idx").on(t.teamId), check("team_invites_role_check", sql`${t.role} IN ('owner', 'member')`)]);
|
|
@@ -1693,7 +1648,6 @@ var teamInvites = pgTable("team_invites", {
|
|
|
1693
1648
|
* the byte store under the hash; this row records verified byte presence and size.
|
|
1694
1649
|
*/
|
|
1695
1650
|
var blobs = pgTable("blobs", {
|
|
1696
|
-
/** sha256 hex of the bytes (the byte-store key). */
|
|
1697
1651
|
hash: text("hash").primaryKey(),
|
|
1698
1652
|
size: integer("size").notNull(),
|
|
1699
1653
|
createdAt: text("created_at").notNull()
|
|
@@ -1709,14 +1663,12 @@ var artifactFiles = pgTable("artifact_files", {
|
|
|
1709
1663
|
loopId: text("loop_id").notNull(),
|
|
1710
1664
|
/** Normalized workdir-relative path (never absolute or escaping the workdir). */
|
|
1711
1665
|
path: text("path").notNull(),
|
|
1712
|
-
/** → blobs.hash. Null when deleted or oversize (no bytes stored). */
|
|
1713
1666
|
hash: text("hash"),
|
|
1714
1667
|
size: integer("size"),
|
|
1715
1668
|
/** Bytes contain a NUL (set even for oversize metadata-only files). */
|
|
1716
1669
|
binary: boolean("binary").notNull().default(false),
|
|
1717
1670
|
/** File exceeds the per-file byte cap → metadata-only (path + size), no blob. */
|
|
1718
1671
|
oversize: boolean("oversize").notNull().default(false),
|
|
1719
|
-
/** Tombstone: the file vanished from the loop's manifest (kept for future diffs). */
|
|
1720
1672
|
deleted: boolean("deleted").notNull().default(false),
|
|
1721
1673
|
updatedAt: text("updated_at").notNull()
|
|
1722
1674
|
}, (t) => [
|
|
@@ -1943,7 +1895,6 @@ function newLoopId() {
|
|
|
1943
1895
|
function nowIso$1() {
|
|
1944
1896
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1945
1897
|
}
|
|
1946
|
-
/** First cron occurrence strictly after `after`, interpreted in the loop's zone. */
|
|
1947
1898
|
function nextCronAt(cron, timezone, after) {
|
|
1948
1899
|
if (!timezone) throw new Error("invariant: cron schedule has no timezone");
|
|
1949
1900
|
const probe = new E(cron, {
|
|
@@ -1969,8 +1920,6 @@ async function listLoops(teamId) {
|
|
|
1969
1920
|
const q = db.select().from(loops);
|
|
1970
1921
|
return teamId ? await q.where(eq(loops.teamId, teamId)) : await q;
|
|
1971
1922
|
}
|
|
1972
|
-
/** Count a team's loops without materializing their (large) rows — the
|
|
1973
|
-
* delete-block guard + settings-detail badge only need the tally. */
|
|
1974
1923
|
async function countLoopsForTeam(teamId) {
|
|
1975
1924
|
const r = (await db.select({ n: sql`count(*)` }).from(loops).where(eq(loops.teamId, teamId)))[0];
|
|
1976
1925
|
return Number(r?.n ?? 0);
|
|
@@ -1978,7 +1927,6 @@ async function countLoopsForTeam(teamId) {
|
|
|
1978
1927
|
async function getLoop(id) {
|
|
1979
1928
|
return (await db.select().from(loops).where(eq(loops.id, id)))[0];
|
|
1980
1929
|
}
|
|
1981
|
-
/** Loops bound to a machine — used by machine cascade authorization and UI counts. */
|
|
1982
1930
|
async function loopsForMachine(machineId) {
|
|
1983
1931
|
return db.select().from(loops).where(eq(loops.machineId, machineId));
|
|
1984
1932
|
}
|
|
@@ -2080,7 +2028,6 @@ async function updateLoopTx(tx, current, patch, at) {
|
|
|
2080
2028
|
}
|
|
2081
2029
|
return loop;
|
|
2082
2030
|
}
|
|
2083
|
-
/** Partial owner/system update under the loop lifecycle lock. */
|
|
2084
2031
|
async function updateLoop(id, patch) {
|
|
2085
2032
|
return db.transaction(async (tx) => {
|
|
2086
2033
|
const current = (await tx.select().from(loops).where(eq(loops.id, id)).for("update"))[0];
|
|
@@ -2112,11 +2059,9 @@ async function pauseLoopState(id) {
|
|
|
2112
2059
|
};
|
|
2113
2060
|
});
|
|
2114
2061
|
}
|
|
2115
|
-
/** Pause a loop and return its updated row. */
|
|
2116
2062
|
async function pauseLoop(id) {
|
|
2117
2063
|
return (await pauseLoopState(id))?.loop;
|
|
2118
2064
|
}
|
|
2119
|
-
/** Start a paused loop unless deletion has begun. */
|
|
2120
2065
|
async function startLoop(id) {
|
|
2121
2066
|
return db.transaction(async (tx) => {
|
|
2122
2067
|
const current = (await tx.select().from(loops).where(eq(loops.id, id)).for("update"))[0];
|
|
@@ -2146,14 +2091,12 @@ async function stopLoopTx(tx, current, at) {
|
|
|
2146
2091
|
}).where(and(eq(runs.id, running.id), eq(runs.phase, "running"), isNull(runs.cancelRequestedAt))).returning())[0] ?? running
|
|
2147
2092
|
};
|
|
2148
2093
|
}
|
|
2149
|
-
/** Stop is one loop-locked write: Pause + cancel all queue + mark current run. */
|
|
2150
2094
|
async function stopLoop(id) {
|
|
2151
2095
|
return db.transaction(async (tx) => {
|
|
2152
2096
|
const current = (await tx.select().from(loops).where(eq(loops.id, id)).for("update"))[0];
|
|
2153
2097
|
return current ? stopLoopTx(tx, current, nowIso$1()) : void 0;
|
|
2154
2098
|
});
|
|
2155
2099
|
}
|
|
2156
|
-
/** Stop-run preserves loop lifecycle; running cancellation waits for daemon proof. */
|
|
2157
2100
|
async function requestRunCancel(loopId, runId) {
|
|
2158
2101
|
return db.transaction(async (tx) => {
|
|
2159
2102
|
if (!(await tx.select({ id: loops.id }).from(loops).where(eq(loops.id, loopId)).for("update"))[0]) return void 0;
|
|
@@ -2338,7 +2281,6 @@ async function runningRunForLoop(loopId) {
|
|
|
2338
2281
|
async function getReportReceipt(reportId) {
|
|
2339
2282
|
return (await db.select().from(runReportReceipts).where(eq(runReportReceipts.reportId, reportId)))[0];
|
|
2340
2283
|
}
|
|
2341
|
-
/** Deterministic primary key for exact rejected-payload replay evidence. */
|
|
2342
2284
|
function terminalIncidentReceiptId(reportId, payloadDigest) {
|
|
2343
2285
|
return createHash("sha256").update(reportId + payloadDigest).digest("hex");
|
|
2344
2286
|
}
|
|
@@ -2407,7 +2349,6 @@ async function refreshRunHeartbeats(machineId, runIds, at, staleBefore) {
|
|
|
2407
2349
|
updatedAt: at
|
|
2408
2350
|
}).where(and(eq(runs.machineId, machineId), eq(runs.phase, "running"), inArray(runs.id, runIds), or(isNull(runs.heartbeatAt), lt(runs.heartbeatAt, staleBefore)))).returning({ id: runs.id })).length;
|
|
2409
2351
|
}
|
|
2410
|
-
/** Validate the report-only authority for one currently running run. */
|
|
2411
2352
|
async function activeRunForReportTx(tx, loopId, runId, leaseTokenHash) {
|
|
2412
2353
|
const lease = (await tx.select().from(runLeases).where(and(eq(runLeases.tokenHash, leaseTokenHash), eq(runLeases.runId, runId), eq(runLeases.loopId, loopId), eq(runLeases.state, "active"))).limit(1).for("update"))[0];
|
|
2413
2354
|
if (!lease) return { state: "invalid-lease" };
|
|
@@ -2792,11 +2733,9 @@ async function claimReadyRunForMachine(machineId, at = nowIso$1(), excludeRunIds
|
|
|
2792
2733
|
};
|
|
2793
2734
|
});
|
|
2794
2735
|
}
|
|
2795
|
-
/** Newest-last run history for a loop (chronological), capped. */
|
|
2796
2736
|
async function listRuns(loopId, limit = 30) {
|
|
2797
2737
|
return (await db.select().from(runs).where(eq(runs.loopId, loopId)).orderBy(desc(runs.ts)).limit(limit)).reverse();
|
|
2798
2738
|
}
|
|
2799
|
-
/** Live reconciliation projection for dashboard/run-detail reads. */
|
|
2800
2739
|
async function reconciliationStatesForRuns(loopId, runIds, at = nowIso$1()) {
|
|
2801
2740
|
if (!runIds.length) return /* @__PURE__ */ new Map();
|
|
2802
2741
|
const rows = await db.select({
|
|
@@ -2818,7 +2757,6 @@ async function countRuns(loopId) {
|
|
|
2818
2757
|
const r = (await db.select({ n: sql`count(*)` }).from(runs).where(eq(runs.loopId, loopId)))[0];
|
|
2819
2758
|
return Number(r?.n ?? 0);
|
|
2820
2759
|
}
|
|
2821
|
-
/** Run count and reported input + output tokens in a bounded recent window. */
|
|
2822
2760
|
async function recentLoopUsage(loopId, since) {
|
|
2823
2761
|
const row = (await db.select({
|
|
2824
2762
|
runCount: sql`count(*)`,
|
|
@@ -2832,7 +2770,6 @@ async function recentLoopUsage(loopId, since) {
|
|
|
2832
2770
|
tokenCount: Number(row?.tokenCount ?? 0)
|
|
2833
2771
|
};
|
|
2834
2772
|
}
|
|
2835
|
-
/** Open runs (pending/running) — used by the timeout-reclaim sweep. */
|
|
2836
2773
|
async function openRuns() {
|
|
2837
2774
|
return db.select().from(runs).where(inArray(runs.phase, ["pending", "running"]));
|
|
2838
2775
|
}
|
|
@@ -2864,8 +2801,7 @@ async function expirePendingRun(runId, expected, at, maxLifetimeMs, message) {
|
|
|
2864
2801
|
});
|
|
2865
2802
|
}
|
|
2866
2803
|
async function listMachines(teamId) {
|
|
2867
|
-
|
|
2868
|
-
return teamId ? await q.where(eq(machines.teamId, teamId)) : await q;
|
|
2804
|
+
return teamId ? await db.select().from(machines).where(eq(machines.teamId, teamId)).orderBy(asc(machines.name), asc(machines.id)) : await db.select().from(machines).orderBy(asc(machines.name), asc(machines.id));
|
|
2869
2805
|
}
|
|
2870
2806
|
/**
|
|
2871
2807
|
* Machines usable/visible in a team, MEMBERSHIP-scoped: every machine whose owner
|
|
@@ -2874,7 +2810,7 @@ async function listMachines(teamId) {
|
|
|
2874
2810
|
* A user has at most one membership row per team, so no machine is duplicated.
|
|
2875
2811
|
*/
|
|
2876
2812
|
async function listMachinesForTeam(teamId) {
|
|
2877
|
-
return (await db.select({ m: machines }).from(machines).innerJoin(teamMembers, eq(machines.userId, teamMembers.userId)).where(eq(teamMembers.teamId, teamId))).map((r) => r.m);
|
|
2813
|
+
return (await db.select({ m: machines }).from(machines).innerJoin(teamMembers, eq(machines.userId, teamMembers.userId)).where(eq(teamMembers.teamId, teamId)).orderBy(asc(machines.name), asc(machines.id))).map((r) => r.m);
|
|
2878
2814
|
}
|
|
2879
2815
|
async function getMachine(id) {
|
|
2880
2816
|
return (await db.select().from(machines).where(eq(machines.id, id)))[0];
|
|
@@ -2915,7 +2851,6 @@ async function setMachineOnline(id, online) {
|
|
|
2915
2851
|
lastSeen: nowIso$1()
|
|
2916
2852
|
}).where(eq(machines.id, id));
|
|
2917
2853
|
}
|
|
2918
|
-
/** Deterministic personal-team id for a user (open mode ⇒ the shared "team-shared"). */
|
|
2919
2854
|
function teamIdForUser(userId) {
|
|
2920
2855
|
return `team-${userId ?? "shared"}`;
|
|
2921
2856
|
}
|
|
@@ -2949,7 +2884,6 @@ async function ensureTeam(id, name, ownerUserId) {
|
|
|
2949
2884
|
function newTeamId() {
|
|
2950
2885
|
return `team-${randomUUID().slice(0, 12)}`;
|
|
2951
2886
|
}
|
|
2952
|
-
/** Whether this is the user's renamable but undeletable personal team. */
|
|
2953
2887
|
function isPersonalTeam(team) {
|
|
2954
2888
|
return !!team.ownerUserId && team.id === teamIdForUser(team.ownerUserId);
|
|
2955
2889
|
}
|
|
@@ -2975,16 +2909,12 @@ async function createTeam(name, ownerUserId) {
|
|
|
2975
2909
|
return team;
|
|
2976
2910
|
});
|
|
2977
2911
|
}
|
|
2978
|
-
/** Rename a team (no name-sync fights this — see ensureTeam). */
|
|
2979
2912
|
async function renameTeam(teamId, name) {
|
|
2980
2913
|
await db.update(teams).set({ name }).where(eq(teams.id, teamId));
|
|
2981
2914
|
}
|
|
2982
|
-
/** A user's membership row in a team (undefined ⇒ not a member). */
|
|
2983
2915
|
async function getTeamMember(teamId, userId) {
|
|
2984
2916
|
return (await db.select().from(teamMembers).where(and(eq(teamMembers.teamId, teamId), eq(teamMembers.userId, userId))))[0];
|
|
2985
2917
|
}
|
|
2986
|
-
/** A team's members joined with their user email/name (owners first, then by
|
|
2987
|
-
* join order). The member-list surface in the team settings UI. */
|
|
2988
2918
|
async function listTeamMembers(teamId) {
|
|
2989
2919
|
return (await db.select({
|
|
2990
2920
|
id: teamMembers.id,
|
|
@@ -3000,7 +2930,6 @@ async function listTeamMembers(teamId) {
|
|
|
3000
2930
|
displayName: r.displayName ?? null
|
|
3001
2931
|
})).sort((a, b) => a.role === b.role ? a.createdAt < b.createdAt ? -1 : 1 : a.role === "owner" ? -1 : 1);
|
|
3002
2932
|
}
|
|
3003
|
-
/** Resolve a user by email for direct membership; undefined means no account yet. */
|
|
3004
2933
|
async function userByEmail(email) {
|
|
3005
2934
|
const r = (await db.select({
|
|
3006
2935
|
id: user.id,
|
|
@@ -3079,7 +3008,6 @@ async function deleteTeamCascade(teamId) {
|
|
|
3079
3008
|
if (outcome === "ok") ensuredTeams.delete(teamId);
|
|
3080
3009
|
return outcome;
|
|
3081
3010
|
}
|
|
3082
|
-
/** Mint an invite for a team. `token` is the caller-supplied wire token. */
|
|
3083
3011
|
async function createInvite(input) {
|
|
3084
3012
|
return (await db.insert(teamInvites).values({
|
|
3085
3013
|
...input,
|
|
@@ -3089,7 +3017,6 @@ async function createInvite(input) {
|
|
|
3089
3017
|
async function getInvite(token) {
|
|
3090
3018
|
return (await db.select().from(teamInvites).where(eq(teamInvites.token, token)))[0];
|
|
3091
3019
|
}
|
|
3092
|
-
/** A team's still-live invites (unredeemed, unexpired), newest first. */
|
|
3093
3020
|
async function listPendingInvites(teamId) {
|
|
3094
3021
|
const now = nowIso$1();
|
|
3095
3022
|
return db.select().from(teamInvites).where(and(eq(teamInvites.teamId, teamId), isNull(teamInvites.redeemedAt), gt(teamInvites.expiresAt, now))).orderBy(desc(teamInvites.createdAt));
|
|
@@ -3121,23 +3048,18 @@ async function redeemInviteAtomic(token, userId, grant) {
|
|
|
3121
3048
|
return true;
|
|
3122
3049
|
});
|
|
3123
3050
|
}
|
|
3124
|
-
/** Revoke a pending invite (owner action). */
|
|
3125
3051
|
async function deleteInvite(token) {
|
|
3126
3052
|
await db.delete(teamInvites).where(eq(teamInvites.token, token));
|
|
3127
3053
|
}
|
|
3128
3054
|
async function getTeam(id) {
|
|
3129
3055
|
return (await db.select().from(teams).where(eq(teams.id, id)))[0];
|
|
3130
3056
|
}
|
|
3131
|
-
/** Teams the user belongs to (membership join), newest first. Drives the team
|
|
3132
|
-
* switcher — a regular user has just their personal team (no dropdown). */
|
|
3133
3057
|
async function listTeamsForUser(userId) {
|
|
3134
3058
|
return (await db.select({ t: teams }).from(teamMembers).innerJoin(teams, eq(teamMembers.teamId, teams.id)).where(eq(teamMembers.userId, userId)).orderBy(desc(teams.createdAt))).map((r) => r.t);
|
|
3135
3059
|
}
|
|
3136
|
-
/** Whether the user is a member of the team (authorizes a team-switch request). */
|
|
3137
3060
|
async function isTeamMember(teamId, userId) {
|
|
3138
3061
|
return !!(await db.select({ id: teamMembers.id }).from(teamMembers).where(and(eq(teamMembers.teamId, teamId), eq(teamMembers.userId, userId))))[0];
|
|
3139
3062
|
}
|
|
3140
|
-
/** Does the server already have metadata for this blob hash? (drives needHashes). */
|
|
3141
3063
|
async function blobExists(hash) {
|
|
3142
3064
|
return !!(await db.select({ hash: blobs.hash }).from(blobs).where(eq(blobs.hash, hash)))[0];
|
|
3143
3065
|
}
|
|
@@ -3151,7 +3073,6 @@ async function blobSizes(hashes) {
|
|
|
3151
3073
|
}).from(blobs).where(inArray(blobs.hash, [...new Set(hashes)]));
|
|
3152
3074
|
return new Map(rows.map((row) => [row.hash, row.size]));
|
|
3153
3075
|
}
|
|
3154
|
-
/** Record verified blob metadata idempotently. */
|
|
3155
3076
|
async function recordBlob(hash, size) {
|
|
3156
3077
|
await db.insert(blobs).values({
|
|
3157
3078
|
hash,
|
|
@@ -3220,16 +3141,12 @@ async function reconcileConfiguredArtifacts(loopId, configuredAtStart, keepPaths
|
|
|
3220
3141
|
return stale.length;
|
|
3221
3142
|
});
|
|
3222
3143
|
}
|
|
3223
|
-
/** The loop's current (non-deleted) file set, path-sorted. */
|
|
3224
3144
|
async function listArtifacts(loopId) {
|
|
3225
3145
|
return db.select().from(artifactFiles).where(and(eq(artifactFiles.loopId, loopId), eq(artifactFiles.deleted, false))).orderBy(artifactFiles.path);
|
|
3226
3146
|
}
|
|
3227
|
-
/** One file row by loop + path (live or tombstoned). */
|
|
3228
3147
|
async function getArtifactFile(loopId, path) {
|
|
3229
3148
|
return (await db.select().from(artifactFiles).where(and(eq(artifactFiles.loopId, loopId), eq(artifactFiles.path, path))))[0];
|
|
3230
3149
|
}
|
|
3231
|
-
/** The loop's CURRENT live file set as a snapshot manifest (path → metadata) —
|
|
3232
|
-
* what report() captures as the terminal run's artifact state. */
|
|
3233
3150
|
async function buildLoopManifest(loopId) {
|
|
3234
3151
|
const manifest = {};
|
|
3235
3152
|
for (const f of await listArtifacts(loopId)) manifest[f.path] = {
|
|
@@ -3257,7 +3174,6 @@ async function putRunSnapshot(runId, loopId, manifest) {
|
|
|
3257
3174
|
}
|
|
3258
3175
|
});
|
|
3259
3176
|
}
|
|
3260
|
-
/** A run's captured snapshot, or undefined when the run predates the feature. */
|
|
3261
3177
|
async function getRunSnapshot(runId) {
|
|
3262
3178
|
return (await db.select().from(runSnapshots).where(eq(runSnapshots.runId, runId)))[0];
|
|
3263
3179
|
}
|
|
@@ -3278,7 +3194,6 @@ async function pruneRunSnapshots(loopId, keep) {
|
|
|
3278
3194
|
const pred = survivors.length ? and(eq(runSnapshots.loopId, loopId), notInArray(runSnapshots.runId, survivors)) : eq(runSnapshots.loopId, loopId);
|
|
3279
3195
|
return (await db.delete(runSnapshots).where(pred).returning({ id: runSnapshots.runId })).length;
|
|
3280
3196
|
}
|
|
3281
|
-
/** Distinct loop ids that currently have at least one run snapshot. */
|
|
3282
3197
|
async function loopIdsWithSnapshots() {
|
|
3283
3198
|
return (await db.selectDistinct({ loopId: runSnapshots.loopId }).from(runSnapshots)).map((r) => r.loopId);
|
|
3284
3199
|
}
|
|
@@ -3329,7 +3244,6 @@ async function countRunSnapshots() {
|
|
|
3329
3244
|
const r = (await db.select({ n: sql`count(*)` }).from(runSnapshots))[0];
|
|
3330
3245
|
return Number(r?.n ?? 0);
|
|
3331
3246
|
}
|
|
3332
|
-
/** Server-side artifact-sync helpers: path safety, byte caps, and hashing. */
|
|
3333
3247
|
/** Per-file byte cap. At or under ⇒ bytes sync; over ⇒ metadata-only (no bytes). */
|
|
3334
3248
|
var BLOB_CAP = 10 * 1024 * 1024;
|
|
3335
3249
|
/** Hard ceiling on the exact-manifest sync POST body. */
|
|
@@ -3353,7 +3267,6 @@ function exactRecord(value, fields) {
|
|
|
3353
3267
|
function sha256Buf(bytes) {
|
|
3354
3268
|
return createHash("sha256").update(bytes).digest("hex");
|
|
3355
3269
|
}
|
|
3356
|
-
/** A 64-char lowercase hex sha256. */
|
|
3357
3270
|
function isValidHash(hash) {
|
|
3358
3271
|
return typeof hash === "string" && /^[0-9a-f]{64}$/.test(hash);
|
|
3359
3272
|
}
|
|
@@ -3424,11 +3337,9 @@ function parseArtifactSyncBody(value) {
|
|
|
3424
3337
|
function sha256(s) {
|
|
3425
3338
|
return createHash("sha256").update(s).digest("hex");
|
|
3426
3339
|
}
|
|
3427
|
-
/** Mint a fresh device token (`dk_…`) — the one wire format `machineIdFromToken` consumes. */
|
|
3428
3340
|
function mintDeviceToken() {
|
|
3429
3341
|
return `dk_${randomBytes(15).toString("hex")}`;
|
|
3430
3342
|
}
|
|
3431
|
-
/** Derive the stable machine id from its device token. */
|
|
3432
3343
|
function machineIdFromToken(token) {
|
|
3433
3344
|
return `m-${sha256(token).slice(0, 16)}`;
|
|
3434
3345
|
}
|
|
@@ -3461,7 +3372,6 @@ async function rememberConnectKey(connectKey, intent) {
|
|
|
3461
3372
|
set: row
|
|
3462
3373
|
});
|
|
3463
3374
|
}
|
|
3464
|
-
/** Peek (NON-evicting) the team/minter a connect-key was minted under, if still live. */
|
|
3465
3375
|
async function readClaimIntent(connectKey, now = Date.now()) {
|
|
3466
3376
|
if (!connectKey) return void 0;
|
|
3467
3377
|
const row = (await db.select().from(connectKeys).where(eq(connectKeys.machineId, machineIdFromToken(connectKey))))[0];
|
|
@@ -3471,7 +3381,6 @@ async function readClaimIntent(connectKey, now = Date.now()) {
|
|
|
3471
3381
|
teamId: row.teamId
|
|
3472
3382
|
};
|
|
3473
3383
|
}
|
|
3474
|
-
/** The remembered owner of a self-registering machine, if any (still-live key). */
|
|
3475
3384
|
async function getDeviceOwner(machineId, now = Date.now()) {
|
|
3476
3385
|
const row = (await db.select().from(connectKeys).where(eq(connectKeys.machineId, machineId)))[0];
|
|
3477
3386
|
if (!row || !connectKeyFresh(row.mintedAt, now)) return void 0;
|
|
@@ -3534,7 +3443,6 @@ var newIdempotency = /* @__PURE__ */ new Map();
|
|
|
3534
3443
|
function pruneNewIdempotency(now) {
|
|
3535
3444
|
for (const [key, rec] of newIdempotency) if (now - rec.at > 9e5) newIdempotency.delete(key);
|
|
3536
3445
|
}
|
|
3537
|
-
/** Remember that `key` (from THIS machine) created `loopId`. Pruned on write. */
|
|
3538
3446
|
function recordNewIdempotency(key, machineId, loopId, now = Date.now()) {
|
|
3539
3447
|
pruneNewIdempotency(now);
|
|
3540
3448
|
newIdempotency.set(key, {
|
|
@@ -3596,8 +3504,6 @@ function validIdentifiers(value, rejectNumericLeadingZero) {
|
|
|
3596
3504
|
return !rejectNumericLeadingZero || !/^\d+$/.test(identifier) || identifier === "0" || !identifier.startsWith("0");
|
|
3597
3505
|
});
|
|
3598
3506
|
}
|
|
3599
|
-
/** Parse the leading numeric core `x.y.z` of a version, ignoring any pre-release
|
|
3600
|
-
* / build suffix. Returns null when it isn't a recognizable version. */
|
|
3601
3507
|
function core(v) {
|
|
3602
3508
|
const m = /^\s*v?(\d+)\.(\d+)\.(\d+)/.exec(v);
|
|
3603
3509
|
if (!m) return null;
|
|
@@ -3607,7 +3513,6 @@ function core(v) {
|
|
|
3607
3513
|
Number(m[3])
|
|
3608
3514
|
];
|
|
3609
3515
|
}
|
|
3610
|
-
/** Does `v` carry a pre-release suffix (e.g. "1.2.3-rc.1")? */
|
|
3611
3516
|
function isPre(v) {
|
|
3612
3517
|
return /^\s*v?\d+\.\d+\.\d+-/.test(v);
|
|
3613
3518
|
}
|
|
@@ -3707,7 +3612,6 @@ async function readJsonBody(request, maxBytes) {
|
|
|
3707
3612
|
return { kind: "invalid" };
|
|
3708
3613
|
}
|
|
3709
3614
|
}
|
|
3710
|
-
/** Shared clipping cap for large prompt and terminal telemetry strings. */
|
|
3711
3615
|
var WIRE_TEXT_CAP = 512 * 1024;
|
|
3712
3616
|
/** Validate an optional poll string without coercion, clipping, or NUL removal. */
|
|
3713
3617
|
function validOptionalPollString(value, cap) {
|
|
@@ -3724,7 +3628,6 @@ function nowIso() {
|
|
|
3724
3628
|
function stripNul(s) {
|
|
3725
3629
|
return s.replace(/\u0000/g, "");
|
|
3726
3630
|
}
|
|
3727
|
-
/** Clip a free-text wire field and strip NUL before persistence. */
|
|
3728
3631
|
function clipText(s, cap) {
|
|
3729
3632
|
return stripNul(s.slice(0, cap));
|
|
3730
3633
|
}
|
|
@@ -3881,7 +3784,6 @@ var LocalBlobStore = class {
|
|
|
3881
3784
|
await this.syncDir(path.dirname(target));
|
|
3882
3785
|
}
|
|
3883
3786
|
};
|
|
3884
|
-
/** In-memory blob store — injectable test adapter. */
|
|
3885
3787
|
var MemoryBlobStore = class {
|
|
3886
3788
|
map = /* @__PURE__ */ new Map();
|
|
3887
3789
|
async has(hash) {
|
|
@@ -3897,11 +3799,9 @@ var MemoryBlobStore = class {
|
|
|
3897
3799
|
this.map.delete(hash);
|
|
3898
3800
|
}
|
|
3899
3801
|
};
|
|
3900
|
-
/** Object key for a blob hash. Flat namespace under a prefix — hashes are unique. */
|
|
3901
3802
|
function blobKey(hash) {
|
|
3902
3803
|
return `blobs/${hash}`;
|
|
3903
3804
|
}
|
|
3904
|
-
/** Cloudflare R2 blob store over the S3-compatible API (AWS SDK v3). */
|
|
3905
3805
|
var R2BlobStore = class {
|
|
3906
3806
|
cfg;
|
|
3907
3807
|
client;
|
|
@@ -3968,7 +3868,6 @@ var R2BlobStore = class {
|
|
|
3968
3868
|
}));
|
|
3969
3869
|
}
|
|
3970
3870
|
};
|
|
3971
|
-
/** S3 "object absent" maps to a 404 / NoSuchKey / NotFound error shape. */
|
|
3972
3871
|
function isNotFound(err) {
|
|
3973
3872
|
const e = err;
|
|
3974
3873
|
return e?.name === "NotFound" || e?.name === "NoSuchKey" || e?.$metadata?.httpStatusCode === 404;
|
|
@@ -4159,35 +4058,21 @@ async function authenticateDeviceToken(deviceToken, options = {}) {
|
|
|
4159
4058
|
machine
|
|
4160
4059
|
};
|
|
4161
4060
|
}
|
|
4162
|
-
/** Whether a token needs quoting inside a key/value line or comma-delimited row. */
|
|
4163
4061
|
function needsQuote(s) {
|
|
4164
4062
|
return s === "" || /[\s,:"]/.test(s);
|
|
4165
4063
|
}
|
|
4166
|
-
/** Quote and escape a string so it remains on one line. */
|
|
4167
4064
|
function quote(s) {
|
|
4168
4065
|
return `"${s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\r?\n/g, "\\n")}"`;
|
|
4169
4066
|
}
|
|
4170
|
-
/** Render one scalar the TOON way: a finite number / boolean bare, null|undefined as
|
|
4171
|
-
* the em-dash placeholder, a string bare unless it needs quoting. */
|
|
4172
4067
|
function scalar(v) {
|
|
4173
4068
|
if (v === null || v === void 0) return "—";
|
|
4174
4069
|
if (typeof v === "number") return Number.isFinite(v) ? String(v) : "—";
|
|
4175
4070
|
if (typeof v === "boolean") return v ? "true" : "false";
|
|
4176
4071
|
return needsQuote(v) ? quote(v) : v;
|
|
4177
4072
|
}
|
|
4178
|
-
/** A single top-level `key: value` line (value scalar-rendered). */
|
|
4179
4073
|
function kvLine(key, v) {
|
|
4180
4074
|
return `${key}: ${scalar(v)}`;
|
|
4181
4075
|
}
|
|
4182
|
-
/**
|
|
4183
|
-
* A detail block: a top key, then each field indented two spaces as `key: value`.
|
|
4184
|
-
* The value may be a pre-rendered raw string (pass a `{ raw }`) so composite values
|
|
4185
|
-
* (e.g. a size hint or an already-quoted body) render verbatim.
|
|
4186
|
-
*
|
|
4187
|
-
* topKey:
|
|
4188
|
-
* name: "Docs Sweep"
|
|
4189
|
-
* cron: "0 6 * * 1"
|
|
4190
|
-
*/
|
|
4191
4076
|
function detailBlock(topKey, rows) {
|
|
4192
4077
|
const lines = [`${topKey}:`];
|
|
4193
4078
|
for (const [k, v] of rows) {
|
|
@@ -4196,44 +4081,25 @@ function detailBlock(topKey, rows) {
|
|
|
4196
4081
|
}
|
|
4197
4082
|
return lines.join("\n");
|
|
4198
4083
|
}
|
|
4199
|
-
/** Render a count, optionally with total or displayed-window context. */
|
|
4200
4084
|
function countLine(count, opts = {}) {
|
|
4201
4085
|
if (opts.total !== void 0) return `count: ${count} of ${opts.total} total`;
|
|
4202
4086
|
if (opts.showing !== void 0) return `count: ${count} (showing first ${opts.showing})`;
|
|
4203
4087
|
return `count: ${count}`;
|
|
4204
4088
|
}
|
|
4205
|
-
/**
|
|
4206
|
-
* A typed list block: the header row `name[N]{f1,f2}:` then each record as an
|
|
4207
|
-
* indented comma row (cells scalar-rendered, quoted only when needed). Does NOT emit
|
|
4208
|
-
* the `count:` line — compose it with `countLine` so the caller controls the
|
|
4209
|
-
* aggregate flavor.
|
|
4210
|
-
*
|
|
4211
|
-
* loops[2]{id,name,cron,enabled}:
|
|
4212
|
-
* loop-abc,"Docs Sweep","0 6 * * 1",on
|
|
4213
|
-
*/
|
|
4214
4089
|
function listBlock(name, fields, rows) {
|
|
4215
4090
|
const lines = [`${name}[${rows.length}]{${fields.join(",")}}:`];
|
|
4216
4091
|
for (const row of rows) lines.push(` ${row.map(scalar).join(",")}`);
|
|
4217
4092
|
return lines.join("\n");
|
|
4218
4093
|
}
|
|
4219
|
-
/** Render an empty named collection. */
|
|
4220
4094
|
function emptyList(name) {
|
|
4221
4095
|
return `${name}: []`;
|
|
4222
4096
|
}
|
|
4223
|
-
/** Render command templates as an indented help block. */
|
|
4224
4097
|
function helpBlock(lines) {
|
|
4225
4098
|
return [`help[${lines.length}]:`, ...lines.map((l) => ` ${l}`)].join("\n");
|
|
4226
4099
|
}
|
|
4227
|
-
/**
|
|
4228
|
-
* Render a quoted error plus a machine-readable code.
|
|
4229
|
-
*
|
|
4230
|
-
* error: "status must be keep|no-change|block (got \"wibble\")"
|
|
4231
|
-
* code: VALIDATION_ERROR
|
|
4232
|
-
*/
|
|
4233
4100
|
function errorBlock(message, code) {
|
|
4234
4101
|
return `error: ${quote(message)}\ncode: ${code}`;
|
|
4235
4102
|
}
|
|
4236
|
-
/** Map an HTTP status to its CLI error code. */
|
|
4237
4103
|
function codeForStatus(status) {
|
|
4238
4104
|
switch (status) {
|
|
4239
4105
|
case 400: return "VALIDATION_ERROR";
|
|
@@ -4245,7 +4111,6 @@ function codeForStatus(status) {
|
|
|
4245
4111
|
default: return "ERROR";
|
|
4246
4112
|
}
|
|
4247
4113
|
}
|
|
4248
|
-
/** Clip content at `cap` and append its original size plus recovery hint. */
|
|
4249
4114
|
function truncate(text, cap, tail = "use --full to see complete body") {
|
|
4250
4115
|
if (text.length <= cap) return {
|
|
4251
4116
|
value: text,
|
|
@@ -4256,8 +4121,6 @@ function truncate(text, cap, tail = "use --full to see complete body") {
|
|
|
4256
4121
|
truncated: true
|
|
4257
4122
|
};
|
|
4258
4123
|
}
|
|
4259
|
-
/** Join document sections with a single newline, dropping empty/blank sections so a
|
|
4260
|
-
* missing optional block (e.g. no rejections) leaves no stray blank line. */
|
|
4261
4124
|
function doc(...sections) {
|
|
4262
4125
|
return sections.filter((s) => typeof s === "string" && s.length > 0).join("\n");
|
|
4263
4126
|
}
|
|
@@ -4324,6 +4187,24 @@ function optionalProvider(value, field) {
|
|
|
4324
4187
|
value: normalizeProviderSetting(value)
|
|
4325
4188
|
};
|
|
4326
4189
|
}
|
|
4190
|
+
var PI_THINKING = new Set([
|
|
4191
|
+
"off",
|
|
4192
|
+
"minimal",
|
|
4193
|
+
"low",
|
|
4194
|
+
"medium",
|
|
4195
|
+
"high",
|
|
4196
|
+
"xhigh",
|
|
4197
|
+
"max"
|
|
4198
|
+
]);
|
|
4199
|
+
function validateReasoningEffort(value, agent) {
|
|
4200
|
+
const result = optionalProvider(value, "reasoningEffort");
|
|
4201
|
+
if (!result.ok) return result;
|
|
4202
|
+
if (agent === "pi" && result.value !== null && !PI_THINKING.has(result.value)) return {
|
|
4203
|
+
ok: false,
|
|
4204
|
+
detail: "reasoningEffort for pi must be one of off, minimal, low, medium, high, xhigh, max, or null"
|
|
4205
|
+
};
|
|
4206
|
+
return result;
|
|
4207
|
+
}
|
|
4327
4208
|
function validTimezone(timezone) {
|
|
4328
4209
|
try {
|
|
4329
4210
|
new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
|
|
@@ -4535,7 +4416,6 @@ function statusDefinitionsFromLoop(loop) {
|
|
|
4535
4416
|
block: loop.statusBlock
|
|
4536
4417
|
};
|
|
4537
4418
|
}
|
|
4538
|
-
/** The single owner-facing structured loop shape used by show/list JSON output. */
|
|
4539
4419
|
function canonicalLoopEnvelope(loop) {
|
|
4540
4420
|
return {
|
|
4541
4421
|
id: loop.id,
|
|
@@ -4576,13 +4456,13 @@ function validateLoopCreate(value) {
|
|
|
4576
4456
|
if (!artifacts.ok) return artifacts;
|
|
4577
4457
|
const model = optionalProvider(raw.model, "model");
|
|
4578
4458
|
if (!model.ok) return model;
|
|
4579
|
-
const reasoningEffort = optionalProvider(raw.reasoningEffort, "reasoningEffort");
|
|
4580
|
-
if (!reasoningEffort.ok) return reasoningEffort;
|
|
4581
4459
|
const agent = coerceCodingAgent(raw.agent);
|
|
4582
4460
|
if (!agent) return {
|
|
4583
4461
|
ok: false,
|
|
4584
4462
|
detail: `agent is required and must be one of ${CODING_AGENTS.join(", ")}`
|
|
4585
4463
|
};
|
|
4464
|
+
const reasoningEffort = validateReasoningEffort(raw.reasoningEffort, agent);
|
|
4465
|
+
if (!reasoningEffort.ok) return reasoningEffort;
|
|
4586
4466
|
if (raw.enabled !== void 0 && typeof raw.enabled !== "boolean") return {
|
|
4587
4467
|
ok: false,
|
|
4588
4468
|
detail: "enabled must be boolean"
|
|
@@ -4669,19 +4549,21 @@ function validateLoopEdit(loop, value) {
|
|
|
4669
4549
|
if (!result.ok) return result;
|
|
4670
4550
|
update.model = result.value;
|
|
4671
4551
|
}
|
|
4672
|
-
|
|
4673
|
-
const result = optionalProvider(raw.reasoningEffort, "reasoningEffort");
|
|
4674
|
-
if (!result.ok) return result;
|
|
4675
|
-
update.reasoningEffort = result.value;
|
|
4676
|
-
}
|
|
4552
|
+
let effectiveAgent = loop.agent;
|
|
4677
4553
|
if (raw.agent !== void 0) {
|
|
4678
4554
|
const agent = coerceCodingAgent(raw.agent);
|
|
4679
4555
|
if (!agent) return {
|
|
4680
4556
|
ok: false,
|
|
4681
4557
|
detail: `agent must be one of ${CODING_AGENTS.join(", ")}`
|
|
4682
4558
|
};
|
|
4559
|
+
effectiveAgent = agent;
|
|
4683
4560
|
update.agent = agent;
|
|
4684
4561
|
}
|
|
4562
|
+
if (raw.reasoningEffort !== void 0 || raw.agent !== void 0) {
|
|
4563
|
+
const result = validateReasoningEffort(raw.reasoningEffort !== void 0 ? raw.reasoningEffort : loop.reasoningEffort, effectiveAgent);
|
|
4564
|
+
if (!result.ok) return result;
|
|
4565
|
+
if (raw.reasoningEffort !== void 0) update.reasoningEffort = result.value;
|
|
4566
|
+
}
|
|
4685
4567
|
if (raw.enabled !== void 0) {
|
|
4686
4568
|
if (typeof raw.enabled !== "boolean") return {
|
|
4687
4569
|
ok: false,
|
|
@@ -4748,7 +4630,6 @@ var ONLINE_TTL_MS = 3e4;
|
|
|
4748
4630
|
/** Auto-pause after this many consecutive errors. Canceled rows are transparent;
|
|
4749
4631
|
* zero disables the breaker. */
|
|
4750
4632
|
var AUTOPAUSE_STREAK = Math.max(0, Number(process.env.PIEVO_FAILURE_AUTOPAUSE_STREAK ?? 3));
|
|
4751
|
-
/** How long pending system work on an offline machine stays claimable. */
|
|
4752
4633
|
var DEFERRED_MAX_MS = 7 * 864e5;
|
|
4753
4634
|
/** A claimed run that never reports within this window is reclaimed as timed out. */
|
|
4754
4635
|
var configuredRunTimeoutMs = Number(process.env.PIEVO_RUN_TIMEOUT_MS || 20 * 6e4);
|
|
@@ -4763,9 +4644,9 @@ var EDITABLE_LOOP_FIELDS = LOOP_EDIT_FIELDS;
|
|
|
4763
4644
|
* it never satisfies the successful-run reporting protocol. Run errors share
|
|
4764
4645
|
* this cap. Exported for `cli.ts` so the report verb uses the same budget. */
|
|
4765
4646
|
var MESSAGE_CAP = 2e3;
|
|
4766
|
-
/**
|
|
4647
|
+
/** Provider session IDs are opaque short tokens; this rejects malformed payloads
|
|
4648
|
+
* without constraining known provider formats. */
|
|
4767
4649
|
var SESSION_ID_CAP = 200;
|
|
4768
|
-
/** Keep heartbeat throttling safely inside custom short timeout windows. */
|
|
4769
4650
|
function heartbeatRefreshMs(runTimeoutMs) {
|
|
4770
4651
|
if (!Number.isFinite(runTimeoutMs) || runTimeoutMs <= 0) return 1;
|
|
4771
4652
|
return Math.max(1, Math.min(6e4, runTimeoutMs / 3));
|
|
@@ -4971,7 +4852,6 @@ var MachineGateway = class {
|
|
|
4971
4852
|
* process-local; unlike durable run leases, a deploy drops them and the daemon
|
|
4972
4853
|
* simply re-polls. */
|
|
4973
4854
|
pollWaiters = /* @__PURE__ */ new Map();
|
|
4974
|
-
/** Resolve (and disarm) a machine's parked long-poll waiter, if any. */
|
|
4975
4855
|
wakeMachine(machineId) {
|
|
4976
4856
|
this.pollWaiters.get(machineId)?.(true);
|
|
4977
4857
|
}
|
|
@@ -5139,7 +5019,6 @@ var MachineGateway = class {
|
|
|
5139
5019
|
body: { delivery }
|
|
5140
5020
|
};
|
|
5141
5021
|
}
|
|
5142
|
-
/** Protocol-v4 poll: plural local state, one new delivery per poll. */
|
|
5143
5022
|
async pollV4(deviceToken, request) {
|
|
5144
5023
|
const rawRequest = request;
|
|
5145
5024
|
if (request === null || typeof request !== "object" || Array.isArray(request) || Object.keys(rawRequest).some((key) => !POLL_V4_REQUEST_FIELDS.has(key)) || !validPollInfo(rawRequest?.info) || rawRequest.protocolVersion !== void 0 && (typeof rawRequest.protocolVersion !== "number" || !Number.isInteger(rawRequest.protocolVersion)) || rawRequest.currentRuns !== void 0 && (!Array.isArray(rawRequest.currentRuns) || rawRequest.currentRuns.some((run) => !validPollCurrentRun(run))) || rawRequest.daemonInstanceId !== void 0 && !validPollWireId(rawRequest.daemonInstanceId) || rawRequest.recoveryComplete !== void 0 && typeof rawRequest.recoveryComplete !== "boolean") return {
|
|
@@ -5209,7 +5088,6 @@ var MachineGateway = class {
|
|
|
5209
5088
|
}
|
|
5210
5089
|
};
|
|
5211
5090
|
}
|
|
5212
|
-
/** Idle v4 polls long-poll; active/reporting polls return immediately. */
|
|
5213
5091
|
async pollV4Wait(deviceToken, request, waitMs = LONG_POLL_WAIT_MS) {
|
|
5214
5092
|
if (request.currentRuns?.length || request.protocolVersion !== 4) return this.pollV4(deviceToken, request);
|
|
5215
5093
|
const auth = await authenticateDeviceToken(deviceToken, { allowUnknown: true });
|
|
@@ -5268,8 +5146,6 @@ var MachineGateway = class {
|
|
|
5268
5146
|
}
|
|
5269
5147
|
};
|
|
5270
5148
|
}
|
|
5271
|
-
/** Create a validated loop through device authority, bind it to that machine,
|
|
5272
|
-
* and schedule it immediately. */
|
|
5273
5149
|
async createLoop(deviceToken, body) {
|
|
5274
5150
|
const auth = await authenticateDeviceToken(deviceToken);
|
|
5275
5151
|
if (!auth.ok) return auth.response;
|
|
@@ -5362,9 +5238,6 @@ var MachineGateway = class {
|
|
|
5362
5238
|
};
|
|
5363
5239
|
}
|
|
5364
5240
|
}
|
|
5365
|
-
/** List loops bound to this machine. `--fields` extends the minimal default
|
|
5366
|
-
* columns and rejects unknown names. `--json` returns full structured records
|
|
5367
|
-
* (first byte `[`), mirroring `show --json` — the daemon prints `text` either way. */
|
|
5368
5241
|
async listLoops(deviceToken, fieldsFlag, json) {
|
|
5369
5242
|
const auth = await authenticateDeviceToken(deviceToken);
|
|
5370
5243
|
if (!auth.ok) return auth.response;
|
|
@@ -5410,7 +5283,6 @@ var MachineGateway = class {
|
|
|
5410
5283
|
}
|
|
5411
5284
|
};
|
|
5412
5285
|
}
|
|
5413
|
-
/** Edit a loop through owner authority, scoped to loops bound to this machine. */
|
|
5414
5286
|
async editLoop(deviceToken, id, patch, dryRun = false) {
|
|
5415
5287
|
const auth = await authenticateDeviceToken(deviceToken);
|
|
5416
5288
|
if (!auth.ok) return auth.response;
|
|
@@ -5432,20 +5304,30 @@ var MachineGateway = class {
|
|
|
5432
5304
|
};
|
|
5433
5305
|
const update = validated.value;
|
|
5434
5306
|
const applied = Object.keys(patch ?? {});
|
|
5435
|
-
if (dryRun)
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5307
|
+
if (dryRun) {
|
|
5308
|
+
const before = canonicalLoopEnvelope(loop);
|
|
5309
|
+
const after = canonicalLoopEnvelope({
|
|
5310
|
+
...loop,
|
|
5311
|
+
...update
|
|
5312
|
+
});
|
|
5313
|
+
const { id: _readOnlyId, ...config } = after;
|
|
5314
|
+
const preview = {
|
|
5440
5315
|
id: loop.id,
|
|
5441
5316
|
applied,
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5317
|
+
before,
|
|
5318
|
+
after
|
|
5319
|
+
};
|
|
5320
|
+
return {
|
|
5321
|
+
status: 200,
|
|
5322
|
+
body: {
|
|
5323
|
+
ok: true,
|
|
5324
|
+
dryRun: true,
|
|
5325
|
+
...preview,
|
|
5326
|
+
config,
|
|
5327
|
+
text: JSON.stringify(preview, null, 2)
|
|
5328
|
+
}
|
|
5329
|
+
};
|
|
5330
|
+
}
|
|
5449
5331
|
if (Object.keys(update).length === 0) return {
|
|
5450
5332
|
status: 200,
|
|
5451
5333
|
body: {
|
|
@@ -5851,7 +5733,6 @@ function fmtTimeZoned(iso, timezone, opts = {}) {
|
|
|
5851
5733
|
return fmtTime(iso);
|
|
5852
5734
|
}
|
|
5853
5735
|
}
|
|
5854
|
-
/** Minimal default columns for `pievo loops`. */
|
|
5855
5736
|
var LIST_DEFAULT_FIELDS = [
|
|
5856
5737
|
"id",
|
|
5857
5738
|
"name",
|
|
@@ -5859,8 +5740,6 @@ var LIST_DEFAULT_FIELDS = [
|
|
|
5859
5740
|
"enabled",
|
|
5860
5741
|
"nextFire"
|
|
5861
5742
|
];
|
|
5862
|
-
/** The optional columns `--fields` may add (the "available" set an unknown field is
|
|
5863
|
-
* measured against). `runs`/`lastResult` are derived per loop. */
|
|
5864
5743
|
var LIST_OPTIONAL_FIELDS = [
|
|
5865
5744
|
"timezone",
|
|
5866
5745
|
"model",
|
|
@@ -5868,7 +5747,6 @@ var LIST_OPTIONAL_FIELDS = [
|
|
|
5868
5747
|
"runs",
|
|
5869
5748
|
"lastResult"
|
|
5870
5749
|
];
|
|
5871
|
-
/** One `loops` cell for a named column (scalar-rendered by `listBlock`). */
|
|
5872
5750
|
function loopCell(rec, field) {
|
|
5873
5751
|
switch (field) {
|
|
5874
5752
|
case "id": return rec.id;
|
|
@@ -5884,13 +5762,10 @@ function loopCell(rec, field) {
|
|
|
5884
5762
|
default: return null;
|
|
5885
5763
|
}
|
|
5886
5764
|
}
|
|
5887
|
-
/** Render the typed loop list. Columns are the default set
|
|
5888
|
-
* plus any `--fields` extras (validated + resolved by `listLoops`). */
|
|
5889
5765
|
function renderLoopsText(loops, fields) {
|
|
5890
5766
|
if (!loops.length) return doc(countLine(0), emptyList("loops"), helpBlock(["Run `pievo new --json '{...}'` to create your first loop", "Run `pievo daemon connect` if this machine isn't connected yet"]));
|
|
5891
5767
|
return doc(countLine(loops.length), listBlock("loops", fields, loops.map((l) => fields.map((f) => loopCell(l, f)))), helpBlock(["Run `pievo show <id>` to see a loop's full config", "Run `pievo log <id>` to see a loop's recent runs"]));
|
|
5892
5768
|
}
|
|
5893
|
-
/** One run's result cell, derived from phase + status. */
|
|
5894
5769
|
function runResultToken(r) {
|
|
5895
5770
|
if (r.status === "block") return `block/${r.phase}`;
|
|
5896
5771
|
if (r.phase === "canceled") return "canceled";
|
|
@@ -6070,7 +5945,6 @@ var ArtifactSync = class {
|
|
|
6070
5945
|
body: { ok: true }
|
|
6071
5946
|
};
|
|
6072
5947
|
}
|
|
6073
|
-
/** Read stored content-addressed bytes, or null when absent. */
|
|
6074
5948
|
readBlob(hash) {
|
|
6075
5949
|
if (!isValidHash(hash)) return Promise.resolve(null);
|
|
6076
5950
|
return this.blobStore.get(hash);
|
|
@@ -6091,20 +5965,16 @@ var ArtifactSync = class {
|
|
|
6091
5965
|
* a changed text file degrades to the size-delta marker, like binary/oversize.
|
|
6092
5966
|
*/
|
|
6093
5967
|
var MAX_DIFF_BYTES = 512 * 1024;
|
|
6094
|
-
/** A file is text-diffable only when it has stored bytes and isn't binary/oversize. */
|
|
6095
5968
|
function isText(e) {
|
|
6096
5969
|
return !!e && !e.binary && !e.oversize && !!e.hash;
|
|
6097
5970
|
}
|
|
6098
|
-
/** Within the inline-diff size cap (unknown size ⇒ allowed). */
|
|
6099
5971
|
function withinDiffCap(e) {
|
|
6100
5972
|
return !!e && (e.size == null || e.size <= MAX_DIFF_BYTES);
|
|
6101
5973
|
}
|
|
6102
|
-
/** Load one snapshot blob, or null when recorded bytes are absent. */
|
|
6103
5974
|
async function bytesOf(e) {
|
|
6104
5975
|
if (!e.hash) return null;
|
|
6105
5976
|
return await (await getArtifactSync()).readBlob(e.hash) ?? null;
|
|
6106
5977
|
}
|
|
6107
|
-
/** Unified diff between two versions of a path (empty string ⇒ added/removed side). */
|
|
6108
5978
|
function unified(path, oldText, newText, maxEditLength) {
|
|
6109
5979
|
return maxEditLength === void 0 ? createTwoFilesPatch(path, path, oldText, newText, "previous run", "this run") : createTwoFilesPatch(path, path, oldText, newText, "previous run", "this run", { maxEditLength });
|
|
6110
5980
|
}
|
|
@@ -6607,7 +6477,6 @@ var CliGateway = class {
|
|
|
6607
6477
|
async cli(token, argv) {
|
|
6608
6478
|
return finalizeCli(token.startsWith("dk_") ? await this.deviceCli(token, argv) : await this.runCli(token, argv));
|
|
6609
6479
|
}
|
|
6610
|
-
/** DEVICE-credential branch of the unified CLI. */
|
|
6611
6480
|
async deviceCli(deviceToken, argv) {
|
|
6612
6481
|
const auth = await authenticateDeviceToken(deviceToken, { allowUnknown: true });
|
|
6613
6482
|
if (!auth.ok) return auth.response;
|
|
@@ -6866,7 +6735,6 @@ var CliGateway = class {
|
|
|
6866
6735
|
body: { text: `run already finished: ${runResultToken(stopped)}` }
|
|
6867
6736
|
};
|
|
6868
6737
|
}
|
|
6869
|
-
/** RUN-credential branch of the unified CLI: report and its help only. */
|
|
6870
6738
|
async runCli(runToken, argv) {
|
|
6871
6739
|
const lease = await resolveLease(runToken);
|
|
6872
6740
|
if (!lease) return {
|
|
@@ -6940,13 +6808,16 @@ var CliGateway = class {
|
|
|
6940
6808
|
];
|
|
6941
6809
|
const unknown = Object.keys(flags).filter((key) => !acceptedFlags.includes(key));
|
|
6942
6810
|
if (unknown.length) return derr(400, `report does not accept --${unknown[0]}`, "VALIDATION_ERROR");
|
|
6811
|
+
if (lease.state === "retired") return {
|
|
6812
|
+
code: 200,
|
|
6813
|
+
text: "reported: run retired · result discarded"
|
|
6814
|
+
};
|
|
6943
6815
|
const applied = await this.applyReportMutation(lease, leaseTokenHash, status, message.slice(0, MESSAGE_CAP));
|
|
6944
6816
|
return applied.ok ? {
|
|
6945
6817
|
code: 200,
|
|
6946
6818
|
text: renderReportedText(status, true)
|
|
6947
6819
|
} : derr(applied.status ?? 409, applied.detail ?? "this run is no longer active", applied.code);
|
|
6948
6820
|
}
|
|
6949
|
-
/** The complete run-token command surface: report plus help for report. */
|
|
6950
6821
|
helpText(_lease) {
|
|
6951
6822
|
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"]));
|
|
6952
6823
|
}
|
|
@@ -7023,7 +6894,6 @@ var CliGateway = class {
|
|
|
7023
6894
|
})), scoped.elsewhere, await recentMachineRuns(loops, 3));
|
|
7024
6895
|
}
|
|
7025
6896
|
};
|
|
7026
|
-
/** Shared refusal for terminal-report-only grace after reclaim. */
|
|
7027
6897
|
var TERMINAL_GRACE_MSG = "this run is terminal and no longer accepts commands; its final result is delivered via the terminal report";
|
|
7028
6898
|
/** Verbs that require device authority. `report` is run-only and rejected there. */
|
|
7029
6899
|
var DEVICE_ONLY_VERBS = new Set([
|
|
@@ -7108,21 +6978,18 @@ function finalizeCli(res) {
|
|
|
7108
6978
|
}
|
|
7109
6979
|
return res;
|
|
7110
6980
|
}
|
|
7111
|
-
/** Render the compact run-status confirmation. */
|
|
7112
6981
|
function renderReportedText(status, hasMessage) {
|
|
7113
6982
|
const parts = [];
|
|
7114
6983
|
if (status) parts.push(`status=${status}`);
|
|
7115
6984
|
if (hasMessage) parts.push("message recorded");
|
|
7116
6985
|
return `reported: ${parts.length ? parts.join(" · ") : "recorded"}`;
|
|
7117
6986
|
}
|
|
7118
|
-
/** RUN-credential verb help (in-run `rk_` lease). */
|
|
7119
6987
|
var RUN_VERB_HELP = { report: {
|
|
7120
6988
|
syntax: "report --status keep|no-change|block --message <text>",
|
|
7121
6989
|
summary: "record this run's required status and non-empty summary",
|
|
7122
6990
|
avail: () => "always available",
|
|
7123
6991
|
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"]
|
|
7124
6992
|
} };
|
|
7125
|
-
/** DEVICE-credential verb help (owner `dk_` device token). */
|
|
7126
6993
|
var DEVICE_VERB_HELP = {
|
|
7127
6994
|
new: {
|
|
7128
6995
|
syntax: "new --json '<config>' [--dry-run] [--connect-key <dk_…>] [--server-url <url>]",
|
|
@@ -7175,25 +7042,19 @@ var DEVICE_VERB_HELP = {
|
|
|
7175
7042
|
help: ["Run `pievo log <id> --json` for structured history", "Run `pievo log <id> --run 12 --diff` for bounded run detail and artifact diff"]
|
|
7176
7043
|
}
|
|
7177
7044
|
};
|
|
7178
|
-
/** Render per-verb help. A run lease selects the report-only help;
|
|
7179
|
-
* no lease means the device (owner) surface. Returns undefined for a verb
|
|
7180
|
-
* with no help spec, so the caller falls back to its unknown-command handling. */
|
|
7181
7045
|
function verbHelpText(verb, lease) {
|
|
7182
7046
|
const spec = lease ? RUN_VERB_HELP[verb] : DEVICE_VERB_HELP[verb];
|
|
7183
7047
|
if (!spec) return void 0;
|
|
7184
7048
|
return doc(kvLine("verb", verb), kvLine("syntax", spec.syntax), kvLine("summary", spec.summary), lease && spec.avail ? kvLine("availability", spec.avail(lease)) : null, helpBlock(spec.help));
|
|
7185
7049
|
}
|
|
7186
|
-
/** The canonical editable config emitted by `show --json`. */
|
|
7187
7050
|
function loopEnvelope(loop) {
|
|
7188
7051
|
return { ...canonicalLoopEnvelope(loop) };
|
|
7189
7052
|
}
|
|
7190
|
-
/** Render a large prompt in full or as a compact presence hint. */
|
|
7191
7053
|
function contentField(value, full) {
|
|
7192
7054
|
if (value == null) return "absent";
|
|
7193
7055
|
if (full) return value;
|
|
7194
7056
|
return { raw: `present, ${value.length} bytes — use --full to see` };
|
|
7195
7057
|
}
|
|
7196
|
-
/** The next cadence fire, formatted in the loop's timezone. */
|
|
7197
7058
|
function nextFireDisplay(loop) {
|
|
7198
7059
|
const schedule = scheduleFromLoop(loop);
|
|
7199
7060
|
if (schedule.mode === "continuous") return `after exec terminal + ${schedule.delayMinutes}m`;
|
|
@@ -7201,7 +7062,6 @@ function nextFireDisplay(loop) {
|
|
|
7201
7062
|
if (!iso) return "(never)";
|
|
7202
7063
|
return fmtTimeZoned(iso, schedule.timezone, { seconds: true });
|
|
7203
7064
|
}
|
|
7204
|
-
/** `pievo show` renders the editable envelope followed by derived read-only state. */
|
|
7205
7065
|
function renderShowText(loop, env, totalRuns, lastRun, opts = {}) {
|
|
7206
7066
|
const full = opts.full === true;
|
|
7207
7067
|
const block = detailBlock("loop", [
|
|
@@ -7225,14 +7085,12 @@ function renderShowText(loop, env, totalRuns, lastRun, opts = {}) {
|
|
|
7225
7085
|
];
|
|
7226
7086
|
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));
|
|
7227
7087
|
}
|
|
7228
|
-
/** The static one-line description in the home header. */
|
|
7229
7088
|
var HOME_DESCRIPTION = "Run your scheduled Pievo agent loops on this machine with your own coding agent.";
|
|
7230
7089
|
/** Expand a leading `~/` against the daemon-supplied home dir (the SERVER's own home
|
|
7231
7090
|
* is irrelevant — a loop's paths are the daemon machine's). Absent home ⇒ unchanged. */
|
|
7232
7091
|
function expandHome(p, home) {
|
|
7233
7092
|
return home && p.startsWith("~/") ? path.join(home, p.slice(2)) : p;
|
|
7234
7093
|
}
|
|
7235
|
-
/** A loop's folder on the daemon machine is its configured workdir. */
|
|
7236
7094
|
function scopeLoopDir(workdir, home) {
|
|
7237
7095
|
return path.resolve(expandHome(workdir, home));
|
|
7238
7096
|
}
|
|
@@ -7261,8 +7119,6 @@ function scopeLoopsByCwd(loops, cwd, home) {
|
|
|
7261
7119
|
elsewhere: loops.length - matched.length
|
|
7262
7120
|
};
|
|
7263
7121
|
}
|
|
7264
|
-
/** The most recent runs across ALL of a machine's loops, newest-first, for the home
|
|
7265
|
-
* `recent[]` block. Merges each loop's newest few then globally sorts by ts. */
|
|
7266
7122
|
async function recentMachineRuns(loops, n) {
|
|
7267
7123
|
const rows = [];
|
|
7268
7124
|
for (const l of loops) for (const r of await listRuns(l.id, n)) rows.push({
|
|
@@ -7393,7 +7249,6 @@ var Scheduler = class {
|
|
|
7393
7249
|
enqueueInitialExec(id) {
|
|
7394
7250
|
return this.enqueue(id, { requestedBy: "system" });
|
|
7395
7251
|
}
|
|
7396
|
-
/** Poll fallback and timer callback converge here. */
|
|
7397
7252
|
async advanceDueSchedules(machineId, loopId) {
|
|
7398
7253
|
const advanced = await advanceDueSchedules((/* @__PURE__ */ new Date()).toISOString(), {
|
|
7399
7254
|
machineId,
|