@edgehero/pi-dispatch 4.3.1 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/.env.example +84 -3
  2. package/README.md +20 -7
  3. package/deploy/console.service +52 -0
  4. package/deploy/docker-compose.yml +3 -1
  5. package/deploy/pi-dispatch-valkey.container +4 -2
  6. package/deploy/sandbox-gateway.service +23 -0
  7. package/deploy/worker-env-wrapper.cmd +6 -0
  8. package/deploy/worker-env-wrapper.sh +10 -1
  9. package/package.json +117 -19
  10. package/src/agent-snapshot.mjs +49 -0
  11. package/src/answerer-runtime.mjs +194 -0
  12. package/src/answerer.mjs +178 -0
  13. package/src/approval-policy.mjs +76 -0
  14. package/src/approval-runtime.mjs +188 -0
  15. package/src/approval-session.mjs +137 -0
  16. package/src/approval-state.mjs +170 -0
  17. package/src/auth-cli.mjs +86 -0
  18. package/src/auth-connections.mjs +145 -0
  19. package/src/auth-policy.mjs +32 -0
  20. package/src/auth-store.mjs +97 -0
  21. package/src/azure-host.mjs +66 -4
  22. package/src/azure-prompt.mjs +5 -4
  23. package/src/backend-conformance.mjs +34 -2
  24. package/src/backend-local.mjs +103 -34
  25. package/src/backend-openshell.mjs +1540 -0
  26. package/src/backend-podman.mjs +63 -2
  27. package/src/backend-registry.mjs +61 -4
  28. package/src/backends.mjs +395 -6
  29. package/src/bullmq-internals.mjs +208 -0
  30. package/src/canary-cli.mjs +28 -0
  31. package/src/canary-lock.mjs +41 -0
  32. package/src/canary-runtime.mjs +237 -0
  33. package/src/canary.mjs +53 -0
  34. package/src/cancel-cli.mjs +47 -34
  35. package/src/cancel-state.mjs +213 -15
  36. package/src/capabilities.mjs +36 -1
  37. package/src/capacity-cli.mjs +28 -8
  38. package/src/capacity-records.mjs +75 -16
  39. package/src/capacity.mjs +65 -0
  40. package/src/chat-cli.mjs +288 -0
  41. package/src/chat-fork.mjs +547 -0
  42. package/src/chat-mode.mjs +156 -0
  43. package/src/chat-view.mjs +720 -0
  44. package/src/checkpoint-paths.mjs +113 -0
  45. package/src/cli.mjs +128 -41
  46. package/src/comment-tables.mjs +322 -0
  47. package/src/community-auth-adapters.mjs +87 -0
  48. package/src/community-auth-helper.mjs +76 -0
  49. package/src/community-auth-metadata.mjs +6 -0
  50. package/src/community-auth-output-pins.mjs +38 -0
  51. package/src/community-auth-patches.mjs +168 -0
  52. package/src/community-auth-pins.mjs +38 -0
  53. package/src/config.mjs +190 -29
  54. package/src/connection.mjs +83 -7
  55. package/src/console-answer.mjs +113 -0
  56. package/src/console-audit.mjs +604 -0
  57. package/src/console-bootstrap.mjs +159 -0
  58. package/src/console-client.mjs +848 -0
  59. package/src/console-probe.mjs +106 -0
  60. package/src/console-tokens.mjs +459 -0
  61. package/src/container-names.mjs +51 -0
  62. package/src/container-spec.mjs +20 -1
  63. package/src/cpu-reserve.mjs +4 -1
  64. package/src/defer-reasons.mjs +200 -0
  65. package/src/defer-write.mjs +208 -0
  66. package/src/deployment-venue.mjs +23 -6
  67. package/src/docker-run.mjs +11 -0
  68. package/src/doctor-bundles.mjs +130 -0
  69. package/src/doctor-model-proposals.mjs +109 -0
  70. package/src/doctor-openshell.mjs +238 -0
  71. package/src/doctor-results.mjs +602 -0
  72. package/src/doctor-spawner.mjs +215 -0
  73. package/src/doctor.mjs +1639 -196
  74. package/src/dollar-budget.mjs +212 -3
  75. package/src/endpoint-status.mjs +100 -0
  76. package/src/env-allowlist.mjs +96 -6
  77. package/src/env-file.mjs +6 -4
  78. package/src/file-expect.mjs +47 -0
  79. package/src/forgejo-host.mjs +42 -5
  80. package/src/forgejo-prompt.mjs +5 -4
  81. package/src/forges.mjs +33 -3
  82. package/src/fork-job.mjs +58 -0
  83. package/src/fork-policy.mjs +23 -0
  84. package/src/fork-store.mjs +338 -0
  85. package/src/get-token.mjs +1 -1
  86. package/src/git-dirty.mjs +119 -14
  87. package/src/git-hardening.mjs +85 -0
  88. package/src/github-host.mjs +31 -6
  89. package/src/github-prompt.mjs +7 -16
  90. package/src/gitlab-host.mjs +42 -3
  91. package/src/gitlab-prompt.mjs +5 -4
  92. package/src/held-document.mjs +23 -0
  93. package/src/held-replies.mjs +325 -0
  94. package/src/held-settle.mjs +266 -0
  95. package/src/hook-runs.mjs +188 -0
  96. package/src/host-budget.mjs +8 -5
  97. package/src/host-clone.mjs +72 -0
  98. package/src/host-loads.mjs +557 -0
  99. package/src/host-registry.mjs +66 -6
  100. package/src/host-telemetry.mjs +92 -0
  101. package/src/http-body.mjs +78 -0
  102. package/src/image-preflight.mjs +156 -13
  103. package/src/import-pi.mjs +60 -172
  104. package/src/inbox-relay.mjs +482 -0
  105. package/src/inbox-settle.mjs +196 -0
  106. package/src/index.mjs +734 -129
  107. package/src/init.mjs +35 -5
  108. package/src/job-data-version.mjs +75 -0
  109. package/src/job-id.mjs +30 -15
  110. package/src/job-layout.mjs +22 -0
  111. package/src/job-size.mjs +16 -0
  112. package/src/live-jobs.mjs +14 -6
  113. package/src/live-probes.mjs +6 -2
  114. package/src/live-transcript.mjs +872 -0
  115. package/src/loaded-record.mjs +609 -0
  116. package/src/mcp-config.mjs +1 -1
  117. package/src/mcp-preflight.mjs +36 -5
  118. package/src/model-attempts.mjs +84 -0
  119. package/src/model-catalog.mjs +83 -0
  120. package/src/model-cooldowns.mjs +22 -0
  121. package/src/model-failover-protocol.mjs +11 -0
  122. package/src/model-failover.mjs +134 -0
  123. package/src/model-observation.mjs +50 -0
  124. package/src/model-proposal-protocol.mjs +21 -0
  125. package/src/model-proposals.mjs +75 -0
  126. package/src/model-ref.mjs +67 -10
  127. package/src/model-roles-file.mjs +24 -0
  128. package/src/model-roles.mjs +73 -0
  129. package/src/model-status.mjs +34 -0
  130. package/src/models-json.mjs +4 -1
  131. package/src/money.mjs +1 -1
  132. package/src/oauth-openai.mjs +121 -0
  133. package/src/older-state.mjs +249 -0
  134. package/src/on-failure.mjs +17 -13
  135. package/src/open-browser.mjs +31 -2
  136. package/src/opener-cli.mjs +135 -0
  137. package/src/opener-protocol.mjs +27 -0
  138. package/src/opener-terminal.mjs +128 -0
  139. package/src/openshell-cli.mjs +155 -0
  140. package/src/openshell-policy.mjs +163 -0
  141. package/src/outbox.mjs +14 -0
  142. package/src/output-cap.mjs +1 -0
  143. package/src/package-canary-record.mjs +26 -0
  144. package/src/package-canary.mjs +135 -0
  145. package/src/package-names.mjs +9 -0
  146. package/src/package-stage.mjs +418 -0
  147. package/src/packages.mjs +24 -16
  148. package/src/parked.mjs +2 -1
  149. package/src/pause-windows.mjs +136 -17
  150. package/src/podman-stack.mjs +40 -14
  151. package/src/portfolio-snapshot.mjs +26 -7
  152. package/src/prepare-github.mjs +41 -77
  153. package/src/prepare-local.mjs +32 -2
  154. package/src/prepare.mjs +45 -11
  155. package/src/private-store-boundary.mjs +70 -0
  156. package/src/processor.mjs +1550 -173
  157. package/src/projects.mjs +23 -0
  158. package/src/prompt-fence.mjs +18 -0
  159. package/src/provider-key.mjs +7 -0
  160. package/src/provider-steering.mjs +1 -0
  161. package/src/question-context.mjs +35 -0
  162. package/src/question-group-runtime.mjs +91 -0
  163. package/src/question-groups.mjs +171 -0
  164. package/src/queue.mjs +264 -55
  165. package/src/reason-catalog.mjs +448 -0
  166. package/src/receiver-status.mjs +230 -0
  167. package/src/record-reason.mjs +37 -0
  168. package/src/recovery-queue.mjs +566 -0
  169. package/src/redact-secrets.mjs +219 -0
  170. package/src/reserve-estimate.mjs +101 -0
  171. package/src/reserved-env.mjs +36 -1
  172. package/src/retention.mjs +67 -0
  173. package/src/run-budget-ledger.mjs +283 -0
  174. package/src/run-checkpoint.mjs +627 -0
  175. package/src/run-container.mjs +749 -33
  176. package/src/run-history.mjs +791 -45
  177. package/src/run-mirror.mjs +75 -7
  178. package/src/run-progress.mjs +751 -0
  179. package/src/runtime-observations.mjs +3 -1
  180. package/src/runtime-settings.mjs +272 -37
  181. package/src/sandbox-cli.mjs +78 -2
  182. package/src/sandbox-deployment.mjs +51 -0
  183. package/src/sandbox-gateway-protocol.mjs +378 -0
  184. package/src/sandbox-hosts.mjs +100 -0
  185. package/src/sandbox-inventory.mjs +83 -0
  186. package/src/sandbox-policy.mjs +225 -0
  187. package/src/sandbox-prepare-requests.mjs +145 -0
  188. package/src/sandbox-prepare.mjs +285 -0
  189. package/src/sandbox-refs.mjs +70 -0
  190. package/src/sandbox-session-monitor.mjs +4 -0
  191. package/src/sandbox-session-watch.mjs +124 -0
  192. package/src/sandbox-sessions.mjs +161 -0
  193. package/src/sandbox-store.mjs +63 -6
  194. package/src/sandbox-workspace-guard.mjs +39 -0
  195. package/src/sandbox.mjs +27 -200
  196. package/src/scheduler-stall-guard.mjs +14 -8
  197. package/src/schedules.mjs +19 -6
  198. package/src/secret-bundles.mjs +245 -0
  199. package/src/secret-profiles.mjs +103 -3
  200. package/src/secrets.mjs +19 -9
  201. package/src/service-env.mjs +38 -5
  202. package/src/service.mjs +638 -106
  203. package/src/session-key.mjs +60 -0
  204. package/src/session-store.mjs +168 -10
  205. package/src/setup-contract.mjs +40 -0
  206. package/src/setup-egress.mjs +113 -0
  207. package/src/setup-host-entry.mjs +33 -0
  208. package/src/setup-host.mjs +242 -0
  209. package/src/setup-lock.mjs +41 -0
  210. package/src/size-records.mjs +15 -3
  211. package/src/start.mjs +1353 -135
  212. package/src/status-comment.mjs +415 -0
  213. package/src/stderr-transcript.mjs +318 -0
  214. package/src/steer-ack.mjs +122 -0
  215. package/src/steer-cli.mjs +213 -0
  216. package/src/steer-framing.mjs +185 -0
  217. package/src/steer-inbox.mjs +693 -0
  218. package/src/store-version.mjs +139 -0
  219. package/src/subscription-config.mjs +56 -0
  220. package/src/subscription-limits.mjs +3 -0
  221. package/src/subscription-quota.mjs +259 -0
  222. package/src/subscription-runtime.mjs +123 -0
  223. package/src/subscription-status.mjs +42 -0
  224. package/src/subscription-wait.mjs +11 -0
  225. package/src/subscriptions.mjs +7 -1
  226. package/src/thinking-level.mjs +19 -0
  227. package/src/touch-outbox.mjs +61 -0
  228. package/src/touch-state.mjs +133 -0
  229. package/src/triggers-file.mjs +299 -39
  230. package/src/triggers.mjs +780 -38
  231. package/src/trust-paths.mjs +171 -0
  232. package/src/up.mjs +398 -51
  233. package/src/updates-cli.mjs +61 -0
  234. package/src/updates-look.mjs +127 -0
  235. package/src/updates-reconcile.mjs +182 -0
  236. package/src/updates-subscription-adapters.mjs +8 -0
  237. package/src/updates.mjs +252 -0
  238. package/src/valkey-auth.mjs +32 -9
  239. package/src/valkey-endpoint.mjs +14 -4
  240. package/src/wait-for.mjs +1 -1
  241. package/src/wait-state.mjs +62 -3
  242. package/src/watch-board.mjs +38 -0
  243. package/src/watch-closer.mjs +1 -1
  244. package/src/watch-contract.mjs +102 -0
  245. package/src/watch-decision.mjs +78 -0
  246. package/src/watch-drainer.mjs +14 -0
  247. package/src/watch-issue-adapter.mjs +44 -0
  248. package/src/watch-issue.mjs +111 -0
  249. package/src/watch-record.mjs +10 -0
  250. package/src/watch-runtime.mjs +167 -0
  251. package/src/watch-store.mjs +183 -0
  252. package/src/watch-trace.mjs +34 -0
  253. package/src/workspace-checkpoint.mjs +1750 -0
  254. package/src/zero-touch-fixes.mjs +99 -0
  255. package/src/zero-touch.mjs +225 -0
  256. package/src/repeat-slot.mjs +0 -13
@@ -1,4 +1,5 @@
1
1
  import { UNREADABLE_RECORD } from "./run-history.mjs";
2
+ import { RUN_MIRROR_MAX_RUNS_DEFAULT } from "./retention.mjs";
2
3
 
3
4
  /**
4
5
  * A fleet-visible copy of the run history (issue #57, Gap 3).
@@ -201,13 +202,17 @@ export const runRecordKey = (sanitizedJobId) => `runs:rec:${sanitizedJobId}`;
201
202
  export const MIRROR_MAX_DAYS = 92;
202
203
 
203
204
  /**
204
- * A hard ceiling on index members, independent of the time window.
205
+ * A hard ceiling on index members, independent of the time window: the DEFAULT of `PI_RUN_MIRROR_MAX_RUNS`
206
+ * (issue #761), which the worker hands `makeRunMirror` as `indexMax`. Kept under this name for its importers (a
207
+ * reader's default, the admin's local index).
205
208
  *
206
209
  * The window alone does not bound memory: a deployment running thousands of jobs a day would hold a
207
210
  * quarter of a million members for ninety-two days. This caps what the fleet view can cost at roughly the
208
- * depth a panel can display, and the file on disk remains the complete history either way.
211
+ * depth a panel can display, and the file on disk remains the complete history either way. Every writer trims
212
+ * the SHARED index to its own cap, so in a fleet the lowest setting wins; each count trim raises the horizon, so
213
+ * a reader sees the cut, never idle time.
209
214
  */
210
- export const RUNS_INDEX_MAX = 5_000;
215
+ export const RUNS_INDEX_MAX = RUN_MIRROR_MAX_RUNS_DEFAULT;
211
216
 
212
217
  const DAY_MS = 24 * 60 * 60 * 1000;
213
218
  const OP_TIMEOUT_MS = 2_000;
@@ -290,8 +295,13 @@ export function makeRunMirror({ redis, retentionDays, now = () => Date.now(), lo
290
295
  *
291
296
  * Two round trips regardless of how many runs come back: one `ZREVRANGEBYSCORE` for the ids, one `MGET`
292
297
  * for the bodies. Never one read per run.
298
+ *
299
+ * `prune: false` makes the read READ-ONLY (issue #641): the console's pages read the mirror on every request, and a
300
+ * read that writes is a read that needs a writer's care (a page refresh that moves the horizon is a page refresh that
301
+ * changes what capacity reports). An expired body is then skipped and left in the index for a pruning reader or the
302
+ * writer's own trim; the row it stood for is not shown either way.
293
303
  */
294
- export async function readMirroredRuns(redis, { limit = 50, sinceMs = 0, now = () => Date.now(), timeoutMs = OP_TIMEOUT_MS } = {}) {
304
+ export async function readMirroredRuns(redis, { limit = 50, sinceMs = 0, prune = true, now = () => Date.now(), timeoutMs = OP_TIMEOUT_MS } = {}) {
295
305
  if (!redis) return { runs: [], degraded: "off" };
296
306
  let ids;
297
307
  try {
@@ -326,7 +336,7 @@ export async function readMirroredRuns(redis, { limit = 50, sinceMs = 0, now = (
326
336
  stale.push(ids[i]); // unparseable is indistinguishable from gone, and equally not showable
327
337
  }
328
338
  }
329
- if (stale.length > 0) {
339
+ if (stale.length > 0 && prune !== false) {
330
340
  try {
331
341
  // Removed with the cut recorded (`PRUNE_SCRIPT`): the time up to a pruned run is no longer whole.
332
342
  await bounded(redis.eval(PRUNE_SCRIPT, 3, RUNS_INDEX, RUNS_HORIZON, RUNS_SINCE, mirrorWindowMs(0), RUNS_HORIZON_MEMBER, ...stale), timeoutMs);
@@ -337,6 +347,60 @@ export async function readMirroredRuns(redis, { limit = 50, sinceMs = 0, now = (
337
347
  return { runs, degraded: runs.length >= limit ? "truncated" : "ok" };
338
348
  }
339
349
 
350
+ /**
351
+ * One PAGE of the index below a score cursor, READ-ONLY (issue #641): the console's runs screen. Returns
352
+ * `{ runs, fetched, frontier, degraded }` and never throws.
353
+ *
354
+ * `before` is an EXCLUSIVE score (millis) or null for the newest; `offset` skips that many members under the same
355
+ * cursor, so a caller fetching in batches under ONE cursor reads a stable sequence while new runs land above it (a new
356
+ * run scores at its own end, which is after any cursor a page handed out). `fetched` is how many members the range
357
+ * returned, expired ones included, and `frontier` the lowest score among them (null when none): together they tell a
358
+ * caller how deep it has read, which the runs alone cannot, since an expired body leaves no run behind. Nothing is
359
+ * pruned and nothing is written: two reads, the range WITHSCORES and one MGET.
360
+ */
361
+ export async function readMirroredPage(redis, { before = null, offset = 0, limit = 50, timeoutMs = OP_TIMEOUT_MS } = {}) {
362
+ if (!redis) return { runs: [], fetched: 0, frontier: null, degraded: "off" };
363
+ const max = Number.isFinite(before) ? `(${before}` : "+inf";
364
+ let flat;
365
+ try {
366
+ flat = await bounded(redis.zrevrangebyscore(RUNS_INDEX, max, "-inf", "WITHSCORES", "LIMIT", Math.max(0, Math.floor(offset) || 0), Math.max(1, Math.floor(limit) || 1)), timeoutMs);
367
+ } catch (err) {
368
+ return { runs: [], fetched: 0, frontier: null, degraded: `unreachable (${err?.message ?? "?"})` };
369
+ }
370
+ const ids = [];
371
+ let frontier = null;
372
+ for (let i = 0; Array.isArray(flat) && i + 1 < flat.length; i += 2) {
373
+ ids.push(flat[i]);
374
+ const score = Number(flat[i + 1]);
375
+ if (Number.isFinite(score)) frontier = frontier === null ? score : Math.min(frontier, score);
376
+ }
377
+ if (ids.length === 0) return { runs: [], fetched: 0, frontier: null, degraded: "ok" };
378
+ let bodies;
379
+ try {
380
+ bodies = await bounded(redis.mget(...ids.map(runRecordKey)), timeoutMs);
381
+ } catch (err) {
382
+ return { runs: [], fetched: 0, frontier: null, degraded: `unreachable (${err?.message ?? "?"})` };
383
+ }
384
+ const runs = [];
385
+ for (let i = 0; i < ids.length; i++) {
386
+ const raw = bodies?.[i];
387
+ if (typeof raw !== "string" || raw === "") continue;
388
+ try {
389
+ const rec = JSON.parse(raw);
390
+ if (rec && typeof rec === "object" && !Array.isArray(rec)) runs.push(rec);
391
+ } catch {
392
+ // unparseable is not showable, and this read prunes nothing
393
+ }
394
+ }
395
+ return { runs, fetched: ids.length, frontier, degraded: "ok" };
396
+ }
397
+
398
+ /**
399
+ * What `readMirroredRecord` answers, when asked to tell it (`tellUnread`), for a GET that did not complete: the server
400
+ * unreachable, or no answer within the bound. Nothing was read, which is not the same as no record.
401
+ */
402
+ export const UNREAD_RECORD = Object.freeze({ unread: true });
403
+
340
404
  /**
341
405
  * One run's mirrored record, by its sanitized id: the parsed object, `null`, or `UNREADABLE_RECORD`. Never throws,
342
406
  * never rejects.
@@ -346,8 +410,12 @@ export async function readMirroredRuns(redis, { limit = 50, sinceMs = 0, now = (
346
410
  * is there but is not a record (unparseable, empty, not an object) is `UNREADABLE_RECORD`, exactly as the local
347
411
  * reader says it, so the check can log `unreadable` from `mirror`. Neither is ever accepted: the caller keeps
348
412
  * today's path.
413
+ *
414
+ * `tellUnread: true` answers `UNREAD_RECORD` for a GET that did not complete instead of `null`, for a caller that must
415
+ * not take an unread record for an absent one: the console's run page, whose "no run with this id" is a claim that
416
+ * every place a run is kept was read. The worker's own reads keep `null`, which fails toward today's path.
349
417
  */
350
- export async function readMirroredRecord(redis, sanitizedJobId, { timeoutMs = OP_TIMEOUT_MS } = {}) {
418
+ export async function readMirroredRecord(redis, sanitizedJobId, { timeoutMs = OP_TIMEOUT_MS, tellUnread = false } = {}) {
351
419
  if (!redis || !sanitizedJobId) return null;
352
420
  try {
353
421
  const raw = await bounded(redis.get(runRecordKey(sanitizedJobId)), timeoutMs);
@@ -361,7 +429,7 @@ export async function readMirroredRecord(redis, sanitizedJobId, { timeoutMs = OP
361
429
  }
362
430
  return rec !== null && typeof rec === "object" && !Array.isArray(rec) ? rec : UNREADABLE_RECORD;
363
431
  } catch {
364
- return null; // unreachable or timed out: nothing was read
432
+ return tellUnread === true ? UNREAD_RECORD : null; // unreachable or timed out: nothing was read
365
433
  }
366
434
  }
367
435