@edgehero/pi-dispatch 4.3.1 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/.env.example +84 -3
  2. package/README.md +21 -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 +118 -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 +136 -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 +325 -0
  58. package/src/console-client.mjs +848 -0
  59. package/src/console-devices.mjs +372 -0
  60. package/src/console-install.mjs +239 -0
  61. package/src/console-open.mjs +99 -0
  62. package/src/console-probe.mjs +106 -0
  63. package/src/console-tokens.mjs +459 -0
  64. package/src/container-names.mjs +51 -0
  65. package/src/container-spec.mjs +20 -1
  66. package/src/cpu-reserve.mjs +4 -1
  67. package/src/defer-reasons.mjs +200 -0
  68. package/src/defer-write.mjs +208 -0
  69. package/src/deployment-venue.mjs +23 -6
  70. package/src/docker-run.mjs +11 -0
  71. package/src/doctor-bundles.mjs +130 -0
  72. package/src/doctor-model-proposals.mjs +109 -0
  73. package/src/doctor-openshell.mjs +238 -0
  74. package/src/doctor-results.mjs +602 -0
  75. package/src/doctor-spawner.mjs +215 -0
  76. package/src/doctor.mjs +1657 -196
  77. package/src/dollar-budget.mjs +212 -3
  78. package/src/endpoint-status.mjs +100 -0
  79. package/src/env-allowlist.mjs +96 -6
  80. package/src/env-file.mjs +6 -4
  81. package/src/envelope.mjs +5 -3
  82. package/src/file-expect.mjs +47 -0
  83. package/src/forgejo-host.mjs +42 -5
  84. package/src/forgejo-prompt.mjs +5 -4
  85. package/src/forges.mjs +33 -3
  86. package/src/fork-job.mjs +58 -0
  87. package/src/fork-policy.mjs +23 -0
  88. package/src/fork-store.mjs +338 -0
  89. package/src/get-token.mjs +1 -1
  90. package/src/git-dirty.mjs +119 -14
  91. package/src/git-hardening.mjs +85 -0
  92. package/src/github-host.mjs +31 -6
  93. package/src/github-prompt.mjs +7 -16
  94. package/src/gitlab-host.mjs +42 -3
  95. package/src/gitlab-prompt.mjs +5 -4
  96. package/src/held-document.mjs +23 -0
  97. package/src/held-replies.mjs +325 -0
  98. package/src/held-settle.mjs +266 -0
  99. package/src/hook-runs.mjs +188 -0
  100. package/src/host-budget.mjs +8 -5
  101. package/src/host-clone.mjs +72 -0
  102. package/src/host-loads.mjs +557 -0
  103. package/src/host-registry.mjs +66 -6
  104. package/src/host-telemetry.mjs +92 -0
  105. package/src/http-body.mjs +78 -0
  106. package/src/image-preflight.mjs +156 -13
  107. package/src/import-pi.mjs +60 -172
  108. package/src/inbox-relay.mjs +482 -0
  109. package/src/inbox-settle.mjs +196 -0
  110. package/src/index.mjs +734 -129
  111. package/src/init.mjs +35 -5
  112. package/src/job-data-version.mjs +75 -0
  113. package/src/job-id.mjs +30 -15
  114. package/src/job-layout.mjs +22 -0
  115. package/src/job-size.mjs +16 -0
  116. package/src/live-jobs.mjs +14 -6
  117. package/src/live-probes.mjs +6 -2
  118. package/src/live-transcript.mjs +872 -0
  119. package/src/loaded-record.mjs +609 -0
  120. package/src/mcp-config.mjs +4 -2
  121. package/src/mcp-preflight.mjs +36 -5
  122. package/src/model-attempts.mjs +84 -0
  123. package/src/model-catalog.mjs +83 -0
  124. package/src/model-cooldowns.mjs +22 -0
  125. package/src/model-failover-protocol.mjs +11 -0
  126. package/src/model-failover.mjs +134 -0
  127. package/src/model-observation.mjs +50 -0
  128. package/src/model-proposal-protocol.mjs +21 -0
  129. package/src/model-proposals.mjs +75 -0
  130. package/src/model-ref.mjs +67 -10
  131. package/src/model-roles-file.mjs +24 -0
  132. package/src/model-roles.mjs +73 -0
  133. package/src/model-status.mjs +34 -0
  134. package/src/models-json.mjs +12 -2
  135. package/src/money.mjs +1 -1
  136. package/src/oauth-openai.mjs +121 -0
  137. package/src/older-state.mjs +249 -0
  138. package/src/on-failure.mjs +17 -13
  139. package/src/open-browser.mjs +31 -2
  140. package/src/opener-cli.mjs +135 -0
  141. package/src/opener-protocol.mjs +27 -0
  142. package/src/opener-terminal.mjs +128 -0
  143. package/src/openshell-cli.mjs +155 -0
  144. package/src/openshell-policy.mjs +163 -0
  145. package/src/outbox.mjs +14 -0
  146. package/src/output-cap.mjs +1 -0
  147. package/src/package-canary-record.mjs +26 -0
  148. package/src/package-canary.mjs +135 -0
  149. package/src/package-names.mjs +9 -0
  150. package/src/package-stage.mjs +418 -0
  151. package/src/packages.mjs +24 -16
  152. package/src/parked.mjs +2 -1
  153. package/src/pause-windows.mjs +136 -17
  154. package/src/podman-stack.mjs +40 -14
  155. package/src/portfolio-snapshot.mjs +26 -7
  156. package/src/prepare-github.mjs +41 -77
  157. package/src/prepare-local.mjs +32 -2
  158. package/src/prepare.mjs +45 -11
  159. package/src/private-store-boundary.mjs +70 -0
  160. package/src/processor.mjs +1550 -173
  161. package/src/projects.mjs +23 -0
  162. package/src/prompt-fence.mjs +18 -0
  163. package/src/provider-key.mjs +7 -0
  164. package/src/provider-steering.mjs +1 -0
  165. package/src/question-context.mjs +35 -0
  166. package/src/question-group-runtime.mjs +91 -0
  167. package/src/question-groups.mjs +171 -0
  168. package/src/queue.mjs +264 -55
  169. package/src/reason-catalog.mjs +448 -0
  170. package/src/receiver-status.mjs +230 -0
  171. package/src/record-reason.mjs +37 -0
  172. package/src/recovery-queue.mjs +566 -0
  173. package/src/redact-secrets.mjs +219 -0
  174. package/src/reserve-estimate.mjs +101 -0
  175. package/src/reserved-env.mjs +36 -1
  176. package/src/retention.mjs +67 -0
  177. package/src/run-budget-ledger.mjs +283 -0
  178. package/src/run-checkpoint.mjs +627 -0
  179. package/src/run-container.mjs +749 -33
  180. package/src/run-history.mjs +791 -45
  181. package/src/run-mirror.mjs +75 -7
  182. package/src/run-progress.mjs +751 -0
  183. package/src/runtime-observations.mjs +3 -1
  184. package/src/runtime-settings.mjs +272 -37
  185. package/src/sandbox-cli.mjs +78 -2
  186. package/src/sandbox-deployment.mjs +51 -0
  187. package/src/sandbox-gateway-protocol.mjs +378 -0
  188. package/src/sandbox-hosts.mjs +100 -0
  189. package/src/sandbox-inventory.mjs +83 -0
  190. package/src/sandbox-policy.mjs +225 -0
  191. package/src/sandbox-prepare-requests.mjs +145 -0
  192. package/src/sandbox-prepare.mjs +285 -0
  193. package/src/sandbox-refs.mjs +70 -0
  194. package/src/sandbox-session-monitor.mjs +4 -0
  195. package/src/sandbox-session-watch.mjs +124 -0
  196. package/src/sandbox-sessions.mjs +161 -0
  197. package/src/sandbox-store.mjs +63 -6
  198. package/src/sandbox-workspace-guard.mjs +39 -0
  199. package/src/sandbox.mjs +27 -200
  200. package/src/scheduler-stall-guard.mjs +14 -8
  201. package/src/schedules.mjs +19 -6
  202. package/src/secret-bundles.mjs +245 -0
  203. package/src/secret-profiles.mjs +103 -3
  204. package/src/secrets.mjs +19 -9
  205. package/src/service-env.mjs +38 -5
  206. package/src/service.mjs +638 -106
  207. package/src/session-key.mjs +60 -0
  208. package/src/session-store.mjs +168 -10
  209. package/src/setup-contract.mjs +40 -0
  210. package/src/setup-egress.mjs +113 -0
  211. package/src/setup-host-entry.mjs +36 -0
  212. package/src/setup-host.mjs +242 -0
  213. package/src/setup-lock.mjs +41 -0
  214. package/src/size-records.mjs +15 -3
  215. package/src/start.mjs +1353 -135
  216. package/src/status-comment.mjs +415 -0
  217. package/src/stderr-transcript.mjs +318 -0
  218. package/src/steer-ack.mjs +122 -0
  219. package/src/steer-cli.mjs +213 -0
  220. package/src/steer-framing.mjs +185 -0
  221. package/src/steer-inbox.mjs +693 -0
  222. package/src/store-version.mjs +139 -0
  223. package/src/subscription-config.mjs +56 -0
  224. package/src/subscription-limits.mjs +3 -0
  225. package/src/subscription-quota.mjs +259 -0
  226. package/src/subscription-runtime.mjs +123 -0
  227. package/src/subscription-status.mjs +42 -0
  228. package/src/subscription-wait.mjs +11 -0
  229. package/src/subscriptions.mjs +7 -1
  230. package/src/thinking-level.mjs +19 -0
  231. package/src/touch-outbox.mjs +61 -0
  232. package/src/touch-state.mjs +133 -0
  233. package/src/triggers-file.mjs +299 -39
  234. package/src/triggers.mjs +780 -38
  235. package/src/trust-paths.mjs +171 -0
  236. package/src/up.mjs +402 -53
  237. package/src/updates-cli.mjs +61 -0
  238. package/src/updates-look.mjs +127 -0
  239. package/src/updates-reconcile.mjs +182 -0
  240. package/src/updates-subscription-adapters.mjs +8 -0
  241. package/src/updates.mjs +252 -0
  242. package/src/valkey-auth.mjs +32 -9
  243. package/src/valkey-endpoint.mjs +14 -4
  244. package/src/wait-for.mjs +1 -1
  245. package/src/wait-state.mjs +62 -3
  246. package/src/watch-board.mjs +38 -0
  247. package/src/watch-closer.mjs +1 -1
  248. package/src/watch-contract.mjs +102 -0
  249. package/src/watch-decision.mjs +78 -0
  250. package/src/watch-drainer.mjs +14 -0
  251. package/src/watch-issue-adapter.mjs +44 -0
  252. package/src/watch-issue.mjs +111 -0
  253. package/src/watch-record.mjs +10 -0
  254. package/src/watch-runtime.mjs +167 -0
  255. package/src/watch-store.mjs +183 -0
  256. package/src/watch-trace.mjs +34 -0
  257. package/src/workspace-checkpoint.mjs +1750 -0
  258. package/src/zero-touch-fixes.mjs +99 -0
  259. package/src/zero-touch.mjs +225 -0
  260. package/src/repeat-slot.mjs +0 -13
@@ -0,0 +1,156 @@
1
+ /**
2
+ * chat-mode.mjs -- which kind of chat a run can have right now (issue #647, terminal chat).
3
+ *
4
+ * ONE RULE, SHARED. `pi-dispatch chat`, the console's `/api/run` and its run page's Chat panel, and the admin's
5
+ * `/dispatch chat` all ask the same question of the same facts, and each would otherwise grow its own table. Three of
6
+ * them drifting apart is how an operator gets offered a reply box for a run that is still running, or a fork of a run
7
+ * whose answer another run already took. So the table lives here, once:
8
+ *
9
+ * | the run | mode |
10
+ * |------------------------------------------------------|---------|
11
+ * | its job is active, or its run goes on as an active | live |
12
+ * | #645 recovery job (`activeJobId`) | |
13
+ * | its job is queued, or its run goes on as a queued | refused |
14
+ * | #645 recovery job (`queuedJobId`) | |
15
+ * | parked: completed, awaiting-input, no later run on | reply |
16
+ * | its target took the answer, and none is queued or | |
17
+ * | running on it now | |
18
+ * | finished (any other record, job no longer queued) | fork |
19
+ * | unknown, answered, or being answered (a queue too | refused |
20
+ * | deep to check counts as unknown: `pendingTooMany`) | |
21
+ *
22
+ * A refusal is `{ mode: null, says }`, with a sentence an operator can act on, never a bare null (the project's fail
23
+ * loudly rule). PURE and TOTAL: no I/O, no clock, no throw; the caller reads the queue, the record and the parked fold,
24
+ * and hands the facts in. Nothing here looks at free text: the record fields read are the worker's own closed words.
25
+ *
26
+ * WHY ACTIVE WINS OVER A RECORD. An infra retry keeps the job id, so attempt 1's record can exist while attempt 2 runs;
27
+ * the run is live, and a fork of attempt 1's session would race the job that is writing it. And WHY QUEUED WINS too: a
28
+ * retry waiting to start is about to become that writer, and so is a #645 recovery job waiting to start (its base
29
+ * attempt's record says `recovery-enqueued`, which on its own would read as finished and offer a fork).
30
+ *
31
+ * WHY A RUN ON THE TARGET THAT HAS NO RECORD YET ANSWERS A PARK. The answered rule (`readParkedRuns`) looks for a later
32
+ * RECORD, and a record is written at a run's end, so the answer's own resume run, queued or running, would leave the
33
+ * park offering a second reply that resumes the same transcript twice. The caller looks for a job on the same target in
34
+ * the queue (`parked.pending`), and a park with one is being answered.
35
+ *
36
+ * The fork mode is the run's MODE, not a promise that a fork will work: whether a session exists on this host, whether
37
+ * the run was a pull request, and the pi-version stamp are the fork's own checks (`chat-fork.mjs`), made where the
38
+ * session store is.
39
+ */
40
+
41
+ /** The chat modes, in the table's order. */
42
+ export const CHAT_MODES = Object.freeze(["live", "reply", "fork"]);
43
+
44
+ /** The run states `runStateOf` answers, which `/api/run` carries as `state`. */
45
+ export const RUN_STATES = Object.freeze(["active", "queued", "parked", "answered", "finished", "unknown"]);
46
+
47
+ /**
48
+ * BullMQ's words for a job that has not started (or is waiting to start again). `paused` is an older BullMQ's word for
49
+ * a waiting job on a paused queue; it is kept so a mixed fleet still reads as queued. Exported so the readers that look
50
+ * for a run's queued recovery job and for a park's pending answer judge "queued" by this one list.
51
+ */
52
+ export const QUEUED_STATES = Object.freeze(["waiting", "delayed", "prioritized", "waiting-children", "paused"]);
53
+
54
+ /** A run id as the worker mints them (the console's `RUN_ID_RE`); anything else is no active job. */
55
+ const RUN_ID_RE = /^[A-Za-z0-9._:-]{1,200}$/;
56
+
57
+ /** The sentences a refusal says, by why. Exported so a surface can test for one without restating it. */
58
+ export const CHAT_REFUSALS = Object.freeze({
59
+ queued: "This run has not started yet (its job is queued), so there is nothing to chat with until it runs.",
60
+ recoveryQueued: "This run goes on as a recovery job that is queued and has not started yet, so there is nothing to chat with until it runs.",
61
+ answered: "This run asked a question and a later run on the same target took the answer; open that run's chat instead.",
62
+ answering: "This run asked a question and a run on the same target is queued or running now, which takes the answer; open that run's chat instead.",
63
+ parkedUnknown: "This run asked a question, but whether it still waits for an answer cannot be told right now (the run history or the queue could not be read); try again.",
64
+ parkedTooMany: "This run asked a question, but the queue holds too many jobs to check whether an answer is already on its way; answer it on the forge, or wait until the queue is shorter.",
65
+ endedNoRecord: "This run's job has ended but its record is not in this host's history or the fleet's run mirror, so there is no session to open.",
66
+ unknown: "No run with that id is in this host's history, the fleet's run mirror or the queue.",
67
+ });
68
+
69
+ /** Whether a record says its run parked on a question (REQ-AWAIT-INPUT): the worker's own closed words. */
70
+ function isParkedRecord(record) {
71
+ return record?.outcome === "completed" && record?.reason === "awaiting-input";
72
+ }
73
+
74
+ /**
75
+ * The run's state and its chat, from the facts the caller read:
76
+ *
77
+ * @param {object} facts
78
+ * @param {string|null} facts.state - The BullMQ state of the job named (`job.getState()`), or null when the queue no
79
+ * longer holds it (removed on completion, or never this queue's).
80
+ * @param {object|null} facts.record - The job's run record, or null when there is none yet (a record is written at a
81
+ * run's end) or none this host can read.
82
+ * @param {string|null} facts.activeJobId - The run's ACTIVE job: the job itself when it is active, or the #645 recovery
83
+ * job its run goes on as (`continuesAs`); null when nothing of the run is running.
84
+ * @param {string|null} facts.queuedJobId - The #645 recovery job the run goes on as when that job is QUEUED (waiting,
85
+ * delayed, prioritized), or null. Ignored when the run is active.
86
+ * @param {{answered: boolean, pending?: boolean|null, pendingTooMany?: boolean}|null} facts.parked - For a parked record:
87
+ * whether a later run on its target took the answer (`readParkedRuns`'s rule) and whether a job on its target is
88
+ * queued or running now (`pending`, absent when not looked for, null when the scan could not tell, then
89
+ * `pendingTooMany` true when that was the queue's depth: `pendingFactsOf`), or null when either could not be read.
90
+ * @returns {{state: string, mode: string|null, says: string|null}}
91
+ */
92
+ function judge({ state = null, record = null, activeJobId = null, queuedJobId = null, parked = null } = {}) {
93
+ const active = (typeof activeJobId === "string" && RUN_ID_RE.test(activeJobId)) || state === "active";
94
+ if (active) return { state: "active", mode: "live", says: null };
95
+ if (typeof state === "string" && QUEUED_STATES.includes(state)) return { state: "queued", mode: null, says: CHAT_REFUSALS.queued };
96
+ if (typeof queuedJobId === "string" && RUN_ID_RE.test(queuedJobId)) return { state: "queued", mode: null, says: CHAT_REFUSALS.recoveryQueued };
97
+ const rec = record !== null && typeof record === "object" && !Array.isArray(record) ? record : null;
98
+ if (rec === null) {
99
+ // A job the queue holds as ended with no record is a record this host cannot read (another host's, with no mirror),
100
+ // which is not the same sentence as a run nobody knows.
101
+ const ended = state === "completed" || state === "failed";
102
+ return { state: "unknown", mode: null, says: ended ? CHAT_REFUSALS.endedNoRecord : CHAT_REFUSALS.unknown };
103
+ }
104
+ if (isParkedRecord(rec)) {
105
+ const answered = parked !== null && typeof parked === "object" ? parked.answered : null;
106
+ if (answered === true) return { state: "answered", mode: null, says: CHAT_REFUSALS.answered };
107
+ if (answered === false && parked.pending === true) return { state: "answered", mode: null, says: CHAT_REFUSALS.answering };
108
+ if (answered === false && (parked.pending === false || parked.pending === undefined)) return { state: "parked", mode: "reply", says: null };
109
+ // FAIL LOUDLY when the queue scan could not tell (`pendingTooMany`, review round 2). The scan gives up only on a
110
+ // queue deeper than its budget, and on such a queue a queued answer can wait longer than the ten-minute semantic
111
+ // window (`repo#n:flow`) that would otherwise swallow a second answer: the backstop that would make failing open
112
+ // safe has lapsed exactly when this case arises. So the park is refused with a sentence that says why, and the
113
+ // forge, whose reply goes through the receiver's own path, stays the way to answer it.
114
+ if (answered === false && parked.pendingTooMany === true) return { state: "unknown", mode: null, says: CHAT_REFUSALS.parkedTooMany };
115
+ return { state: "unknown", mode: null, says: CHAT_REFUSALS.parkedUnknown };
116
+ }
117
+ return { state: "finished", mode: "fork", says: null };
118
+ }
119
+
120
+ /**
121
+ * A pending-answer scan's result (the console's `readPendingAnswer`: `{ jobId }` with an id or null, `{ unknown: true,
122
+ * tooMany? }` or `{ unreachable: true }`) as the `parked` facts `chatModeOf` reads: `{ pending, pendingTooMany,
123
+ * pendingJobId }`. ONE mapping, so the run page's Chat panel, `/api/run`, the parked inbox and the planner cannot
124
+ * disagree about what a scan that could not tell means (review round 2: the inbox drew a form where the panel refused).
125
+ */
126
+ export function pendingFactsOf(scan) {
127
+ const id = typeof scan?.jobId === "string" && RUN_ID_RE.test(scan.jobId) ? scan.jobId : null;
128
+ if (id !== null) return { pending: true, pendingTooMany: false, pendingJobId: id };
129
+ if (scan !== null && typeof scan === "object" && Object.hasOwn(scan, "jobId") && scan.jobId === null) return { pending: false, pendingTooMany: false, pendingJobId: null };
130
+ return { pending: null, pendingTooMany: scan?.tooMany === true && scan?.unknown === true, pendingJobId: null };
131
+ }
132
+
133
+ /**
134
+ * The verdict on a park no later RECORD answered, from its pending-answer scan alone (`pendingFactsOf`, then this
135
+ * module's table): `{ mode: "reply" | null, says, answeringJobId }`. The parked inbox asks this per row, the Chat panel
136
+ * asks `chatModeOf` with the same facts, so the two screens draw a form for exactly the same parks.
137
+ */
138
+ export function parkedVerdictOf(scan) {
139
+ const facts = pendingFactsOf(scan);
140
+ const { mode, says } = judge({ record: { outcome: "completed", reason: "awaiting-input" }, parked: { answered: false, ...facts } });
141
+ return { mode, says, answeringJobId: facts.pendingJobId };
142
+ }
143
+
144
+ /** The run's state word (one of `RUN_STATES`), from the same facts `chatModeOf` takes. */
145
+ export function runStateOf(facts) {
146
+ return judge(facts).state;
147
+ }
148
+
149
+ /**
150
+ * The chat this run can have: `{ mode: "live" | "reply" | "fork", says: null }`, or `{ mode: null, says }` with the
151
+ * sentence that says why not. Facts as `runStateOf`'s.
152
+ */
153
+ export function chatModeOf(facts) {
154
+ const { mode, says } = judge(facts);
155
+ return { mode, says };
156
+ }