toolroll 0.9.62 → 0.9.65

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 (345) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +66 -8
  3. package/dist/acp-agents.d.ts +43 -0
  4. package/dist/acp-agents.js +62 -0
  5. package/dist/acp-attest.d.ts +44 -0
  6. package/dist/acp-attest.js +269 -0
  7. package/dist/acp-check.d.ts +28 -0
  8. package/dist/acp-check.js +410 -0
  9. package/dist/acp-client.d.ts +163 -0
  10. package/dist/acp-client.js +738 -0
  11. package/dist/acp-inspect.d.ts +23 -0
  12. package/dist/acp-inspect.js +109 -0
  13. package/dist/agent-board.d.ts +19 -6
  14. package/dist/agent-board.js +30 -9
  15. package/dist/agent-fence.d.ts +203 -10
  16. package/dist/agent-fence.js +623 -41
  17. package/dist/agent-pulse.js +9 -2
  18. package/dist/agent-state-words.d.ts +19 -0
  19. package/dist/agent-state-words.js +20 -0
  20. package/dist/agent-state.d.ts +65 -0
  21. package/dist/agent-state.js +229 -0
  22. package/dist/agentconfig.js +36 -13
  23. package/dist/agents-board.d.ts +34 -2
  24. package/dist/agents-board.js +110 -1
  25. package/dist/approval-rules-ui.js +2 -2
  26. package/dist/assignment-delivery.d.ts +14 -0
  27. package/dist/assignment-delivery.js +13 -0
  28. package/dist/assignment.js +13 -0
  29. package/dist/attest.js +7 -0
  30. package/dist/backup-ui.js +2 -2
  31. package/dist/browser/THIRD_PARTY_NOTICES.txt +48 -0
  32. package/dist/browser/terminal.css +37 -0
  33. package/dist/browser/terminal.js +117 -0
  34. package/dist/browser/workspace.css +2 -2
  35. package/dist/browser/workspace.js +42 -38
  36. package/dist/browser-shell.js +6 -3
  37. package/dist/browser-workspace.d.ts +263 -1
  38. package/dist/browser-workspace.js +60 -0
  39. package/dist/builder.d.ts +6 -1
  40. package/dist/builder.js +34 -13
  41. package/dist/chat-actions.d.ts +13 -1
  42. package/dist/chat-actions.js +199 -17
  43. package/dist/chat-ask.d.ts +4 -0
  44. package/dist/chat-ask.js +14 -0
  45. package/dist/chat-channel.js +20 -0
  46. package/dist/chat-continuity.js +5 -4
  47. package/dist/chat-decide.d.ts +0 -14
  48. package/dist/chat-decide.js +4 -1
  49. package/dist/chat-delivery.d.ts +9 -0
  50. package/dist/chat-delivery.js +45 -6
  51. package/dist/chat-messages.d.ts +8 -0
  52. package/dist/chat-messages.js +36 -4
  53. package/dist/check-levels-ui.js +2 -2
  54. package/dist/check-levels.js +3 -0
  55. package/dist/checkout-proof.d.ts +28 -0
  56. package/dist/checkout-proof.js +62 -0
  57. package/dist/claim.d.ts +12 -0
  58. package/dist/claim.js +20 -1
  59. package/dist/cli-help.d.ts +1 -1
  60. package/dist/cli-help.js +5 -1
  61. package/dist/cli.d.ts +3 -0
  62. package/dist/cli.js +24 -2
  63. package/dist/coding-ui.js +6 -4
  64. package/dist/console-instance.d.ts +124 -0
  65. package/dist/console-instance.js +337 -0
  66. package/dist/contracts/chat-actions.d.ts +16 -0
  67. package/dist/contracts/chat-actions.js +4 -0
  68. package/dist/contracts/chat-content.d.ts +19 -0
  69. package/dist/contracts/chat-content.js +6 -0
  70. package/dist/contracts/cli.d.ts +8 -0
  71. package/dist/contracts/cli.js +3 -1
  72. package/dist/contracts/console-api.d.ts +30 -4
  73. package/dist/contracts/console-api.js +13 -4
  74. package/dist/contracts/flow-card.d.ts +4 -0
  75. package/dist/contracts/flow-card.js +3 -0
  76. package/dist/contracts/flow.d.ts +46 -0
  77. package/dist/contracts/flow.js +11 -2
  78. package/dist/contracts/integration-metadata.d.ts +2 -1
  79. package/dist/contracts/integration-metadata.js +2 -1
  80. package/dist/contracts/lead-tools.d.ts +92 -0
  81. package/dist/contracts/lead-tools.js +13 -0
  82. package/dist/contracts/provider.d.ts +11 -1
  83. package/dist/contracts/provider.js +1 -1
  84. package/dist/contracts/route.d.ts +76 -1
  85. package/dist/contracts/route.js +1 -1
  86. package/dist/contracts/store-json.d.ts +2 -0
  87. package/dist/control-setup.d.ts +1 -1
  88. package/dist/control-ui.js +2 -2
  89. package/dist/converse.d.ts +16 -0
  90. package/dist/converse.js +32 -4
  91. package/dist/demo-chat.js +4 -4
  92. package/dist/demo.js +2 -2
  93. package/dist/desktop-update-gate.js +6 -0
  94. package/dist/discord-chat.js +4 -1
  95. package/dist/dispatch.js +7 -4
  96. package/dist/doctor.d.ts +53 -0
  97. package/dist/doctor.js +427 -0
  98. package/dist/egress-bridge.d.ts +17 -0
  99. package/dist/egress-bridge.js +111 -0
  100. package/dist/egress.d.ts +119 -0
  101. package/dist/egress.js +423 -0
  102. package/dist/evidence-pack.d.ts +1 -1
  103. package/dist/evidence-pack.js +2 -2
  104. package/dist/exec.d.ts +81 -0
  105. package/dist/exec.js +368 -9
  106. package/dist/export-ui.js +2 -2
  107. package/dist/flow-code.d.ts +2 -2
  108. package/dist/flow-code.js +4 -15
  109. package/dist/flow-engine.js +35 -11
  110. package/dist/flow-gallery-ui.js +2 -2
  111. package/dist/flow-gallery.js +27 -8
  112. package/dist/flow-share.js +16 -5
  113. package/dist/flow-triggers.d.ts +84 -11
  114. package/dist/flow-triggers.js +554 -87
  115. package/dist/flow-unattended-words.d.ts +28 -0
  116. package/dist/flow-unattended-words.js +50 -0
  117. package/dist/flow-unattended.d.ts +227 -0
  118. package/dist/flow-unattended.js +776 -0
  119. package/dist/flows-cli.d.ts +28 -1
  120. package/dist/flows-cli.js +63 -12
  121. package/dist/flows-ui.d.ts +2 -3
  122. package/dist/flows-ui.js +56 -5
  123. package/dist/flows.d.ts +4 -0
  124. package/dist/flows.js +9 -1
  125. package/dist/google-mail.d.ts +1 -0
  126. package/dist/google-mail.js +61 -18
  127. package/dist/html.d.ts +9 -0
  128. package/dist/html.js +11 -0
  129. package/dist/integrations-ui.d.ts +2 -1
  130. package/dist/integrations-ui.js +6 -3
  131. package/dist/integrations.d.ts +17 -3
  132. package/dist/integrations.js +38 -18
  133. package/dist/invoke.d.ts +17 -0
  134. package/dist/invoke.js +510 -182
  135. package/dist/keys.d.ts +18 -14
  136. package/dist/keys.js +5 -0
  137. package/dist/landed-cleanup.d.ts +3 -2
  138. package/dist/landed-cleanup.js +12 -5
  139. package/dist/lead-capabilities.js +3 -2
  140. package/dist/lead-contract.js +2 -0
  141. package/dist/lead-doors.d.ts +2 -0
  142. package/dist/lead-doors.js +7 -0
  143. package/dist/lead-follow.d.ts +2 -0
  144. package/dist/lead-follow.js +58 -2
  145. package/dist/lead-progress.js +1 -0
  146. package/dist/lead-queue-runtime.d.ts +46 -0
  147. package/dist/lead-queue-runtime.js +191 -0
  148. package/dist/lead-queue.d.ts +104 -0
  149. package/dist/lead-queue.js +187 -0
  150. package/dist/lead-sweep.d.ts +67 -0
  151. package/dist/lead-sweep.js +99 -0
  152. package/dist/lead-tools.d.ts +22 -0
  153. package/dist/lead-tools.js +163 -4
  154. package/dist/lead.d.ts +13 -5
  155. package/dist/lead.js +89 -29
  156. package/dist/limits-ui.d.ts +8 -1
  157. package/dist/limits-ui.js +26 -0
  158. package/dist/live-bus.d.ts +8 -0
  159. package/dist/live-bus.js +40 -4
  160. package/dist/live-detail.d.ts +27 -0
  161. package/dist/live-detail.js +57 -0
  162. package/dist/live-notes.d.ts +20 -0
  163. package/dist/live-notes.js +47 -0
  164. package/dist/live-records.d.ts +140 -0
  165. package/dist/live-records.js +935 -0
  166. package/dist/live-terminal.d.ts +258 -0
  167. package/dist/live-terminal.js +649 -0
  168. package/dist/live.d.ts +38 -27
  169. package/dist/live.js +142 -124
  170. package/dist/mcp-connect.d.ts +8 -7
  171. package/dist/mcp-connect.js +49 -51
  172. package/dist/mcp-oauth.d.ts +6 -3
  173. package/dist/mcp-oauth.js +8 -4
  174. package/dist/memory-pass.js +2 -2
  175. package/dist/mobile-viewport.d.ts +5 -1
  176. package/dist/mobile-viewport.js +18 -2
  177. package/dist/model-catalog.js +4 -0
  178. package/dist/monitoring-ui.js +2 -2
  179. package/dist/needs-you.d.ts +1 -1
  180. package/dist/needs-you.js +11 -4
  181. package/dist/oauth-run-token.d.ts +51 -0
  182. package/dist/oauth-run-token.js +127 -0
  183. package/dist/operate.d.ts +2 -0
  184. package/dist/operate.js +282 -18
  185. package/dist/passwordless.d.ts +23 -0
  186. package/dist/passwordless.js +26 -0
  187. package/dist/peek-cli.d.ts +12 -4
  188. package/dist/peek-cli.js +19 -11
  189. package/dist/phase-routing.js +10 -3
  190. package/dist/plan-auto.d.ts +11 -0
  191. package/dist/plan-auto.js +62 -11
  192. package/dist/planner-source.d.ts +3 -0
  193. package/dist/planner-source.js +14 -0
  194. package/dist/planner.d.ts +2 -1
  195. package/dist/planner.js +7 -1
  196. package/dist/policy-ui.js +2 -2
  197. package/dist/policy.d.ts +1 -20
  198. package/dist/policy.js +29 -2
  199. package/dist/process-recovery-eligibility.d.ts +1 -1
  200. package/dist/project-access.d.ts +3 -0
  201. package/dist/project-access.js +3 -0
  202. package/dist/project-delete-ui.d.ts +12 -0
  203. package/dist/project-delete-ui.js +41 -9
  204. package/dist/project-delete.d.ts +3 -1
  205. package/dist/project-delete.js +12 -6
  206. package/dist/project-file.d.ts +3 -0
  207. package/dist/project-file.js +16 -0
  208. package/dist/project-memory.d.ts +3 -1
  209. package/dist/project-memory.js +57 -25
  210. package/dist/project-tools.d.ts +103 -27
  211. package/dist/project-tools.js +399 -100
  212. package/dist/provider-auth.d.ts +4 -3
  213. package/dist/provider-auth.js +5 -4
  214. package/dist/provider-connection.d.ts +2 -1
  215. package/dist/provider-connection.js +5 -0
  216. package/dist/provider-home.d.ts +22 -0
  217. package/dist/provider-home.js +99 -0
  218. package/dist/provider.d.ts +42 -5
  219. package/dist/provider.js +97 -19
  220. package/dist/pull-request-ui.js +2 -2
  221. package/dist/push.js +1 -1
  222. package/dist/quick-start.d.ts +24 -0
  223. package/dist/quick-start.js +127 -0
  224. package/dist/release.d.ts +3 -0
  225. package/dist/release.js +14 -8
  226. package/dist/remote-main.d.ts +4 -0
  227. package/dist/remote-main.js +3 -3
  228. package/dist/request-budget-ui.js +2 -2
  229. package/dist/result-review.d.ts +1 -1
  230. package/dist/result-review.js +9 -3
  231. package/dist/retention-ui.js +2 -2
  232. package/dist/retention.js +10 -1
  233. package/dist/run-efficiency.d.ts +73 -0
  234. package/dist/run-efficiency.js +228 -0
  235. package/dist/runner.d.ts +5 -1
  236. package/dist/runner.js +22 -1
  237. package/dist/scope.d.ts +171 -6
  238. package/dist/scope.js +251 -31
  239. package/dist/scout-net.d.ts +4 -1
  240. package/dist/scout-net.js +37 -12
  241. package/dist/scout.d.ts +20 -6
  242. package/dist/scout.js +27 -3
  243. package/dist/serve.js +258 -43
  244. package/dist/server/chat.d.ts +1 -1
  245. package/dist/server/chat.js +122 -31
  246. package/dist/server/chrome.d.ts +6 -1
  247. package/dist/server/chrome.js +18 -7
  248. package/dist/server/console.css +52 -38
  249. package/dist/server/flows.js +65 -13
  250. package/dist/server/guards.d.ts +1 -0
  251. package/dist/server/guards.js +23 -9
  252. package/dist/server/http.d.ts +7 -0
  253. package/dist/server/http.js +3 -2
  254. package/dist/server/live.d.ts +34 -1
  255. package/dist/server/live.js +39 -17
  256. package/dist/server/pages-assets.js +5 -0
  257. package/dist/server/pages.js +90 -13
  258. package/dist/server/people-tokens.d.ts +3 -0
  259. package/dist/server/people-tokens.js +55 -17
  260. package/dist/server/remote-hooks.js +12 -6
  261. package/dist/server/remote.d.ts +2 -1
  262. package/dist/server/remote.js +14 -5
  263. package/dist/server/render-chat.d.ts +12 -3
  264. package/dist/server/render-chat.js +47 -16
  265. package/dist/server/render-pages.d.ts +4 -1
  266. package/dist/server/render-pages.js +5 -4
  267. package/dist/server/render-people.d.ts +3 -2
  268. package/dist/server/render-people.js +3 -2
  269. package/dist/server/render-results.d.ts +18 -2
  270. package/dist/server/render-results.js +37 -28
  271. package/dist/server/render-settings.js +6 -5
  272. package/dist/server/render-tasks.d.ts +35 -10
  273. package/dist/server/render-tasks.js +110 -27
  274. package/dist/server/request-context.d.ts +2 -0
  275. package/dist/server/request-context.js +2 -1
  276. package/dist/server/route-table.d.ts +3 -0
  277. package/dist/server/route-table.js +49 -5
  278. package/dist/server/runtime.d.ts +34 -3
  279. package/dist/server/session.d.ts +25 -1
  280. package/dist/server/session.js +18 -4
  281. package/dist/server/settings.js +132 -13
  282. package/dist/server/sweeps.d.ts +4 -0
  283. package/dist/server/sweeps.js +58 -0
  284. package/dist/server/task-views.d.ts +2 -1
  285. package/dist/server/task-views.js +96 -6
  286. package/dist/server/tasks.js +320 -74
  287. package/dist/server/terminals.d.ts +31 -0
  288. package/dist/server/terminals.js +365 -0
  289. package/dist/setup-guide.js +3 -2
  290. package/dist/slack-chat.js +4 -1
  291. package/dist/spend-ui.js +3 -3
  292. package/dist/spend.js +2 -1
  293. package/dist/steer-capabilities.d.ts +32 -0
  294. package/dist/steer-capabilities.js +60 -0
  295. package/dist/steer-controls.d.ts +14 -0
  296. package/dist/steer-controls.js +36 -0
  297. package/dist/steer-status.js +2 -2
  298. package/dist/storage-sweep-child.js +3 -1
  299. package/dist/storage-sweep-process.d.ts +12 -5
  300. package/dist/storage-sweep-process.js +23 -6
  301. package/dist/storage-ui.js +4 -4
  302. package/dist/store-baseline.d.ts +1 -1
  303. package/dist/store-baseline.js +1 -1
  304. package/dist/store.d.ts +466 -19
  305. package/dist/store.js +1185 -63
  306. package/dist/subagent-desk.js +1 -1
  307. package/dist/subagent-tools.d.ts +1 -1
  308. package/dist/summary.js +3 -0
  309. package/dist/surface.js +6 -4
  310. package/dist/task-source.d.ts +58 -0
  311. package/dist/task-source.js +93 -0
  312. package/dist/team-leads.js +1 -1
  313. package/dist/teams-chat.js +4 -1
  314. package/dist/teardown-git.d.ts +14 -0
  315. package/dist/teardown-git.js +119 -0
  316. package/dist/telegram-lead.d.ts +3 -0
  317. package/dist/telegram-lead.js +44 -7
  318. package/dist/telegram.d.ts +11 -2
  319. package/dist/telegram.js +51 -17
  320. package/dist/terminal-catalog.d.ts +11 -0
  321. package/dist/terminal-catalog.js +129 -0
  322. package/dist/text-limits.d.ts +2 -0
  323. package/dist/text-limits.js +2 -0
  324. package/dist/tool-broker.d.ts +39 -0
  325. package/dist/tool-broker.js +370 -0
  326. package/dist/tool-launcher.js +5 -4
  327. package/dist/tool-secret-store.d.ts +170 -0
  328. package/dist/tool-secret-store.js +834 -0
  329. package/dist/toolroll-update-ui.js +2 -2
  330. package/dist/toolroll-update.js +14 -3
  331. package/dist/tools-ui.d.ts +7 -1
  332. package/dist/tools-ui.js +19 -10
  333. package/dist/webhook-limits.d.ts +23 -0
  334. package/dist/webhook-limits.js +30 -0
  335. package/dist/webhook-readers.d.ts +56 -0
  336. package/dist/webhook-readers.js +138 -0
  337. package/dist/webhook-signing.d.ts +27 -0
  338. package/dist/webhook-signing.js +119 -0
  339. package/dist/work-index.d.ts +29 -0
  340. package/dist/work-index.js +21 -5
  341. package/dist/workspace-revision.js +3 -1
  342. package/dist/worktree.d.ts +10 -4
  343. package/dist/worktree.js +70 -29
  344. package/package.json +13 -2
  345. package/scripts/provider-canary.mjs +210 -31
@@ -74,7 +74,7 @@ export function askSubagent(store, given, from, text, now) {
74
74
  return { ok: false, said: `${name}'s desk can't take cards right now.` };
75
75
  const firstLine = said.split("\n")[0].trim();
76
76
  const title = firstLine.length <= 100 ? firstLine : `${firstLine.slice(0, 99)}…`;
77
- const card = store.addFlowCard({ flow: desk.id, title, description: said === title ? null : said, stage: zone.id, by: from.who, source: { kind: "message", label: `Asked through ${from.via}`, url: null } }, now);
77
+ const card = store.addFlowCard({ flow: desk.id, title, description: said === title ? null : said, stage: zone.id, by: from.who, byKind: "person", source: { kind: "message", label: `Asked through ${from.via}`, url: null } }, now);
78
78
  store.setFlowCardWatcher(card, from.who, true, now);
79
79
  return { ok: true, said: `${name} has it. The answer comes to you when it's done.`, card, link: { label: "Open the card", path: flowCardHref(desk.id, card) } };
80
80
  }
@@ -12,7 +12,7 @@ export type ToolLister = (spec: ToolSpec, values: Record<string, string>) => Pro
12
12
  export type ToolIo = {
13
13
  callTool?: ToolCaller;
14
14
  listTools?: ToolLister;
15
- toolHome?: string;
15
+ toolHome?: string | undefined;
16
16
  };
17
17
  /** Actions whose names say they only read start as "do it"; everything else asks first until its manager says otherwise. */
18
18
  export declare const READ_VERB: RegExp;
package/dist/summary.js CHANGED
@@ -32,6 +32,9 @@ export function tally(runs) {
32
32
  * key spend and must never be presented as a separate charge.
33
33
  */
34
34
  export function runCostWords(run, live = false) {
35
+ // An ACP agent signs in its own way and reports no price Toolroll trusts.
36
+ if (run.authMode === "agent-login")
37
+ return live ? "the agent's own sign-in · in progress" : "the agent's own sign-in · cost not measured";
35
38
  if (run.authMode === "subscription") {
36
39
  return run.costUsd === null
37
40
  ? live
package/dist/surface.js CHANGED
@@ -116,8 +116,9 @@ const GUIDE_ROWS = [
116
116
  ...KNOWLEDGE_DESCRIPTORS.map(spec => ({ invocation: `knowledge ${spec.action}`, synopsis: spec.synopsis, audience: "audience" in spec ? spec.audience : "agent", agentMayInvoke: !("audience" in spec), mutation: spec.mutation, flags: spec.flags, ...(spec.takesQuery ? { positionals: [{ name: "query", required: true, meaning: "search text or source file for impact" }] } : {}) })),
117
117
  ...MEMORY_DESCRIPTORS.map(spec => ({ invocation: `memory ${spec.action}`, synopsis: spec.synopsis, audience: "agent", agentMayInvoke: true, mutation: spec.mutation, flags: spec.flags, ...(spec.takesQuery ? { positionals: [{ name: "query", required: true, meaning: "search text, a decision id, or the decision sentence" }] } : {}) })),
118
118
  // Flow writes carry an approver's credential: an agent runs them only as the person asked, after showing the preview.
119
- ...FLOWS_DESCRIPTORS.map(spec => ({ invocation: `flows ${spec.action}`, synopsis: spec.synopsis, audience: "agent", agentMayInvoke: true, mutation: spec.mutation, flags: spec.flags,
120
- ...("positionals" in spec ? { positionals: spec.positionals.map(name => ({ name, required: true, meaning: name === "flow" ? "a flow id from flows list" : name === "card" ? "a card id from flows show" : "a trigger id from flows show, or for trigger add its settings as JSON, a JSON file, or -" })) } : {}) })),
119
+ // Letting a flow run on its own raises authority: the owner's own password ceremony, never an agent's act.
120
+ ...FLOWS_DESCRIPTORS.map(spec => ({ invocation: `flows ${spec.action}`, synopsis: spec.synopsis, audience: "audience" in spec ? spec.audience : "agent", agentMayInvoke: !("audience" in spec), mutation: spec.mutation, flags: spec.flags,
121
+ ...("positionals" in spec ? { positionals: spec.positionals.map(name => ({ name, required: true, meaning: name === "flow" ? "a flow id from flows list" : name === "card" ? "a card id from flows show" : name === "on|off" ? "on signs the grant, off revokes it" : "a trigger id from flows show, or for trigger add its settings as JSON, a JSON file, or -" })) } : {}) })),
121
122
  ...MODELS_DESCRIPTORS.map(spec => ({ invocation: `models ${spec.action}`, synopsis: spec.synopsis, audience: "agent", agentMayInvoke: true, mutation: spec.mutation, flags: spec.flags, ...(spec.takesQuery ? { positionals: [{ name: "target", required: true, meaning: "the CLI to update, or on/off" }] } : {}) })),
122
123
  // ---- the queue (agent surface) ----
123
124
  { invocation: "status", synopsis: "running work, queued reasons, results to review, the latest release check and plan windows", audience: "agent", agentMayInvoke: true, mutation: "none", flags: [jsonFlag, dbFlag] },
@@ -411,6 +412,7 @@ const GUIDE_ROWS = [
411
412
  operator("up", "console + worker + browser, one command"),
412
413
  operator("onboard", "add this repository, report signed-in agents, install or --remove the operator skill (--yes), offer the starter flows (--starter <ids>), and print the handoff"),
413
414
  operator("daemon", "install the loop under the OS service manager"),
415
+ { ...operatorRead("doctor", "check that Toolroll's service holds no key, token or password a build could read; names what it finds and the steps to fix it, never a value, and changes nothing"), flags: [jsonFlag] },
414
416
  ];
415
417
  /**
416
418
  * Whether each row may run on a central server for a person signed in with their own API token (`runOperateAs` in
@@ -429,7 +431,7 @@ const REMOTE_NO = [
429
431
  // Workers, the loop and the services that run here.
430
432
  "claim", "heartbeat", "worker release", "reap", "tick", "build", "reconcile",
431
433
  "runner register", "runner retire", "runner bind", "runner capacity", "coordinator mint", "coordinator revoke", "mcp", "outbox deliver", "peek",
432
- "serve", "serve check-public", "watch", "up", "onboard", "daemon", "bridge", "setup show", "setup clear",
434
+ "serve", "serve check-public", "watch", "up", "onboard", "daemon", "doctor", "bridge", "setup show", "setup clear",
433
435
  // Provider keys and model tools on this machine.
434
436
  "providers", "keys status", "keys set", "keys clear", "keys verify", "keys auth", "models check", "models update", "models watch",
435
437
  // Publication and release.
@@ -443,7 +445,7 @@ const REMOTE_NO = [
443
445
  ];
444
446
  const REMOTE_STEP_UP = [
445
447
  "proposals", "decide", "task regate", "task approve", "task accept", "task merge", "run settle", "knowledge apply", "memory apply",
446
- "flows card approve", "flows card send-back", "flows script approve",
448
+ "flows card approve", "flows card send-back", "flows script approve", "flows unattended",
447
449
  "approver list", "approver add", "people list", "people invite", "people projects", "people revoke",
448
450
  // A person's own API tokens: a password step-up on the server's machine, or the console. Never with a token.
449
451
  "tokens create", "tokens list", "tokens revoke", "tokens rotate",
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Where a task came from (docs/plans/webhook-tasks.md §5): an outside event's delivery, kept with the task. A card's
3
+ * source is written once at receipt (who proved it, the outside thing's identity and link); the task filed from that
4
+ * card keeps a snapshot of it (task_source), which its revisions copy, so the reference outlives the card's own
5
+ * task link and zone. Every word shown comes from that snapshot — never from today's trigger settings.
6
+ */
7
+ import type { FlowCardSource, Store } from "./store.js";
8
+ /** What a task keeps of the card it was filed from. */
9
+ export type TaskSourceSnapshot = {
10
+ label: string;
11
+ verified: string;
12
+ external: {
13
+ id: string | null;
14
+ url: string | null;
15
+ };
16
+ receipt: string | null;
17
+ };
18
+ /** As much of a card's source as a reference reads. */
19
+ export type SourceLike = Pick<FlowCardSource, "label"> & Partial<Pick<FlowCardSource, "verified" | "external" | "receipt">>;
20
+ /** The snapshot an outside event's card gives its task, or null when the card didn't come from one (a person's, a poll's). */
21
+ export declare function snapshotOf(source: SourceLike | null): TaskSourceSnapshot | null;
22
+ /** A snapshot as saved, read back strictly: anything else is no snapshot at all. */
23
+ export declare function readSnapshot(json: string): TaskSourceSnapshot | null;
24
+ /** The host a link names, for words that always show where it goes. */
25
+ export declare function hostOf(url: string | null): string | null;
26
+ /** Where a link goes, as it is shown: its host and path (no scheme), the middle elided when long, so the destination is
27
+ * always visible beside the label. */
28
+ export declare function linkWords(url: string, max?: number): string;
29
+ /** How a snapshot's delivery was proved, in words: "signed by Sentry", or "unsigned". */
30
+ export declare function provedWords(snapshot: TaskSourceSnapshot): string;
31
+ /**
32
+ * The reference a task's brief carries: what the outside thing is, from where, and that it is data, not instructions.
33
+ * Bounded (an identity is at most 120 characters and a link 2,000), and said plainly when it was unsigned.
34
+ */
35
+ /** A reference's words, at most: every reader of them (the planner's source, the scout's brief) accepts exactly this. The
36
+ * largest there can be (a 253-character host, 120-character label and identity, a 2,000-character link) fits. */
37
+ export declare const REFERENCE_LIMIT = 3000;
38
+ export declare function referenceWords(snapshot: TaskSourceSnapshot): string;
39
+ /** The same reference for a card that hasn't filed its task yet ({{card.source}}), or "" for a card from no outside event. */
40
+ export declare function cardReference(source: SourceLike | null | undefined): string;
41
+ /** Where a task came from, in the words a page shows: the flow (named and linked only for a viewer who can see it) and
42
+ * the outside event (its label, how it was proved, its link and that link's host), all from what was kept. */
43
+ export type TaskOrigin = {
44
+ flow: {
45
+ name: string;
46
+ href: string;
47
+ } | "hidden" | "removed" | null;
48
+ source: {
49
+ label: string;
50
+ proved: string;
51
+ url: string | null;
52
+ host: string | null;
53
+ } | null;
54
+ };
55
+ /** A task's origin, or null when it came from neither a flow nor an outside event. */
56
+ export declare function originOf(store: Store, taskId: string, canSee: (repo: string) => boolean): TaskOrigin | null;
57
+ /** The one line an approval shows about an outside event that started its work: context, never a term agreed to. */
58
+ export declare function originNote(origin: TaskOrigin | null | undefined): string | null;
@@ -0,0 +1,93 @@
1
+ import { SENDER_WORDS } from "./webhook-signing.js";
2
+ /** The snapshot an outside event's card gives its task, or null when the card didn't come from one (a person's, a poll's). */
3
+ export function snapshotOf(source) {
4
+ if (source === null || typeof source.verified !== "string")
5
+ return null;
6
+ return { label: source.label.slice(0, 120), verified: source.verified, external: { id: source.external?.id ?? null, url: source.external?.url ?? null }, receipt: source.receipt ?? null };
7
+ }
8
+ /** A snapshot as saved, read back strictly: anything else is no snapshot at all. */
9
+ export function readSnapshot(json) {
10
+ try {
11
+ const raw = JSON.parse(json);
12
+ const external = raw["external"];
13
+ const optional = (value) => typeof value === "string" ? value : null;
14
+ if (typeof raw["label"] !== "string" || typeof raw["verified"] !== "string" || external === undefined || external === null || typeof external !== "object")
15
+ return null;
16
+ return { label: raw["label"], verified: raw["verified"], external: { id: optional(external["id"]), url: optional(external["url"]) }, receipt: optional(raw["receipt"]) };
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ /** The host a link names, for words that always show where it goes. */
23
+ export function hostOf(url) {
24
+ if (url === null)
25
+ return null;
26
+ try {
27
+ return new URL(url).host;
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ /** Where a link goes, as it is shown: its host and path (no scheme), the middle elided when long, so the destination is
34
+ * always visible beside the label. */
35
+ export function linkWords(url, max = 72) {
36
+ let host, path;
37
+ try {
38
+ const parsed = new URL(url);
39
+ host = parsed.host;
40
+ path = parsed.pathname === "/" ? "" : parsed.pathname;
41
+ }
42
+ catch {
43
+ return "a link";
44
+ }
45
+ // The host is never shortened (a cut inside it could show one site's name for another's address; URL.host is already
46
+ // punycode for any non-ASCII name): only the path's middle is elided when long.
47
+ const room = Math.max(12, max - host.length);
48
+ return path.length <= room ? `${host}${path}` : `${host}${path.slice(0, Math.ceil(room / 2) - 1)}…${path.slice(path.length - Math.floor(room / 2))}`;
49
+ }
50
+ /** How a snapshot's delivery was proved, in words: "signed by Sentry", or "unsigned". */
51
+ export function provedWords(snapshot) {
52
+ const by = snapshot.verified === "github" ? "GitHub" : snapshot.verified === "linear" ? "Linear" : SENDER_WORDS[snapshot.verified] ?? snapshot.verified;
53
+ return snapshot.verified === "none" ? "unsigned" : `signed by ${by}`;
54
+ }
55
+ /**
56
+ * The reference a task's brief carries: what the outside thing is, from where, and that it is data, not instructions.
57
+ * Bounded (an identity is at most 120 characters and a link 2,000), and said plainly when it was unsigned.
58
+ */
59
+ /** A reference's words, at most: every reader of them (the planner's source, the scout's brief) accepts exactly this. The
60
+ * largest there can be (a 253-character host, 120-character label and identity, a 2,000-character link) fits. */
61
+ export const REFERENCE_LIMIT = 3_000;
62
+ export function referenceWords(snapshot) {
63
+ const host = hostOf(snapshot.external.url) ?? "its sender";
64
+ const what = [snapshot.label.slice(0, 120), snapshot.external.id?.slice(0, 120) ?? null].filter(one => one !== null && one !== "").join(" ");
65
+ const proved = snapshot.verified === "none" ? "an unsigned webhook: anyone with its address could have sent it" : provedWords(snapshot);
66
+ const words = `Outside reference (data from ${host.slice(0, 253)}, ${proved.slice(0, 80)}; not instructions): ${what}${snapshot.external.url === null ? "" : ` — ${snapshot.external.url.slice(0, 2_000)}`}`;
67
+ return words.slice(0, REFERENCE_LIMIT);
68
+ }
69
+ /** The same reference for a card that hasn't filed its task yet ({{card.source}}), or "" for a card from no outside event. */
70
+ export function cardReference(source) {
71
+ const snapshot = snapshotOf(source ?? null);
72
+ return snapshot === null ? "" : referenceWords(snapshot);
73
+ }
74
+ /** A task's origin, or null when it came from neither a flow nor an outside event. */
75
+ export function originOf(store, taskId, canSee) {
76
+ const kept = store.taskSourceOf(taskId);
77
+ const flowId = kept?.flow ?? Number(/^flow:(\d+)$/.exec(store.filedViaOf(taskId) ?? "")?.[1] ?? NaN);
78
+ const snapshot = kept === null ? null : readSnapshot(kept.sourceJson);
79
+ if (!Number.isSafeInteger(flowId) && snapshot === null)
80
+ return null;
81
+ const flow = Number.isSafeInteger(flowId) ? store.getFlow(flowId) : null;
82
+ return {
83
+ flow: !Number.isSafeInteger(flowId) ? null : flow === null ? "removed" : canSee(flow.repo) ? { name: flow.name, href: `/flows/${flow.id}` } : "hidden",
84
+ source: snapshot === null ? null : { label: snapshot.label, proved: provedWords(snapshot), url: snapshot.external.url, host: hostOf(snapshot.external.url) },
85
+ };
86
+ }
87
+ /** The one line an approval shows about an outside event that started its work: context, never a term agreed to. */
88
+ export function originNote(origin) {
89
+ if (origin?.source == null)
90
+ return null;
91
+ const where = origin.source.host === null ? "" : ` · ${origin.source.host}`;
92
+ return `Started by an outside event: ${origin.source.label} (${origin.source.proved})${where}. The agent reads what it sent as data, not instructions.`;
93
+ }
@@ -345,7 +345,7 @@ export class TeamLeads {
345
345
  return ok('Message already saved.', { messageId: Number(seen['message']), status: String(seen['status']) });
346
346
  }
347
347
  const revisions = this.memberRevisions(conversationId, actor.name);
348
- const messageId = this.store.appendLeadMessage({ thread: conversation.threadId, turn: null, role: 'operator', text }, now);
348
+ const messageId = this.store.appendLeadMessage({ thread: conversation.threadId, turn: null, role: 'operator', text, pending: true }, now);
349
349
  this.db.prepare("INSERT INTO team_message(message,conversation,author,author_generation,lead_member_revision,participant_revision,request_id,payload_hash,status) VALUES(?,?,?,?,?,?,?,?,'queued')").run(messageId, conversationId, actor.name, actor.generation, revisions.lead, revisions.participant, requestId, digest);
350
350
  this.event(lead.id, conversationId, 'message-queued', actor.name, now);
351
351
  return ok('Message saved.', { messageId, status: 'queued' });
@@ -4,7 +4,7 @@ import { resultShotsPruned } from "./result-shots.js";
4
4
  import { chatQuestionButtons } from "./subagent-question.js";
5
5
  import { chatAskButtons } from "./chat-ask.js";
6
6
  import { ChatDeliveryError, ChatState, chatHash, partContent } from "./chat-delivery-state.js";
7
- import { channelAccess, chatObject as object, planChatNotifications, planRoomMessages, processChatEvent, splitChatText } from "./chat-delivery.js";
7
+ import { channelAccess, chatObject as object, heldForWatch, planChatNotifications, planRoomMessages, processChatEvent, splitChatText } from "./chat-delivery.js";
8
8
  import { PLATFORM_LIMITS } from "./text-limits.js";
9
9
  import { roomCommand } from "./chat-rooms.js";
10
10
  import { LEAD_MESSAGE_MAX_CHARS } from "./lead.js";
@@ -236,6 +236,9 @@ export async function deliverTeamsPart(options) {
236
236
  const bodies = teamsMessages(voiced ?? text, voiced !== null, one => actions.length || content.proposal ? teamsCard(one, actions) : { type: "message", text: one, textFormat: voiced !== null ? "markdown" : "plain" });
237
237
  const activities = `/v3/conversations/${encodeURIComponent(destination)}/activities`;
238
238
  // An edit replaces its one message with the first part; any more follow it as new messages.
239
+ // The watch, at the last moment before the network (cockpit §3).
240
+ if (heldForWatch(state, store, row.id, event, binding.approver, content, now))
241
+ return true;
239
242
  let answer = await options.api(target ? "PUT" : "POST", serviceUrl, target ? `${activities}/${encodeURIComponent(target)}` : activities, bodies[0]);
240
243
  for (const more of bodies.slice(1))
241
244
  answer = await options.api("POST", serviceUrl, activities, more);
@@ -0,0 +1,14 @@
1
+ import { type ExecResult } from "./exec.js";
2
+ import { type Runner } from "./worktree.js";
3
+ export type Git = (cwd: string, args: readonly string[]) => Promise<ExecResult>;
4
+ /** Fixed on every teardown command, ahead of its own arguments. */
5
+ export declare const TEARDOWN_FLAGS: readonly string[];
6
+ /** Exit code of a refused command (git's own fatal code, so no caller reads it as "not found" or "no"). */
7
+ export declare const REFUSED_CODE = 128;
8
+ /** The command-running setting a `git config --list --show-scope -z` listing sets, or null. Only local and worktree
9
+ * scope count: teardown commands never read system or global config. */
10
+ export declare function commandSetting(listing: string): string | null;
11
+ /** Why a teardown command was refused, or null when `result` isn't a refusal. */
12
+ export declare function refusal(result: ExecResult): string | null;
13
+ /** The one way teardown runs git, including the target checkout immediately before removal. */
14
+ export declare function teardownGit(runner?: Runner): Git;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Git for teardown (landed-cleanup.ts and the checkout removal it drives in worktree.ts), run by Toolroll's own
3
+ * process inside the shared repository a build can write to. A build can plant hooks in .git/hooks and settings in
4
+ * .git/config (or a worktree's config.worktree), so every teardown command runs hardened: an allowlisted environment
5
+ * with no system or global config (remote-main.ts's), no hooks, no fsmonitor, no ext transport, no prompts and no
6
+ * automatic maintenance.
7
+ *
8
+ * Settings that name a command, and includes (which could bring them in from a file nobody looked at), are not
9
+ * overridden but refused: before each command the repository's own config is read with includes off, and if it sets
10
+ * any of them the command never runs. The refusal is a failed git result, so every caller's existing doubt applies and
11
+ * nothing is removed.
12
+ */
13
+ import { existsSync, readdirSync } from "node:fs";
14
+ import { join, resolve } from "node:path";
15
+ import { run } from "./exec.js";
16
+ import { ENV, ENV_ALLOWLIST } from "./remote-main.js";
17
+ import { WORKTREE_TIMEOUT_MS } from "./worktree.js";
18
+ /** Fixed on every teardown command, ahead of its own arguments. */
19
+ export const TEARDOWN_FLAGS = [
20
+ "--no-optional-locks", "-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", "-c", "protocol.ext.allow=never",
21
+ "-c", "credential.helper=", "-c", "gc.auto=0", "-c", "maintenance.auto=false", "-c", "submodule.recurse=false",
22
+ ];
23
+ /** Exit code of a refused command (git's own fatal code, so no caller reads it as "not found" or "no"). */
24
+ export const REFUSED_CODE = 128;
25
+ const REFUSED_PREFIX = "Toolroll won't run git here: ";
26
+ /** Config keys that run a command, bring in unread config, or redirect inspection away from the checked path.
27
+ * `*` is one subsection, any name. */
28
+ const COMMAND_KEYS = [
29
+ "core.sshcommand", "core.gitproxy", "core.askpass", "core.pager", "core.editor", "core.alternaterefscommand",
30
+ "core.worktree", "gpg.ssh.defaultkeycommand", "trailer.*.command", "trailer.*.cmd", "submodule.*.update", "sequence.editor", "diff.external", "credential.helper", "credential.*.helper", "filter.*.clean", "filter.*.smudge", "filter.*.process",
31
+ "diff.*.command", "diff.*.textconv", "merge.*.driver", "gpg.program", "gpg.*.program", "uploadpack.packobjectshook",
32
+ "remote.*.uploadpack", "remote.*.receivepack", "remote.*.vcs", "pager.*", "include.path", "includeif.*.path",
33
+ ].map(key => new RegExp(`^${key.replace(/\./g, "\\.").replace(/\*/g, ".+")}$`));
34
+ /** core.fsmonitor as a boolean is git's own daemon; anything else is a command to run. */
35
+ const BOOLEAN = /^(|true|false|yes|no|on|off|-?\d+)$/i;
36
+ /** The command-running setting a `git config --list --show-scope -z` listing sets, or null. Only local and worktree
37
+ * scope count: teardown commands never read system or global config. */
38
+ export function commandSetting(listing) {
39
+ const fields = listing.split("\0");
40
+ for (let at = 0; at + 1 < fields.length; at += 2) {
41
+ const scope = fields[at];
42
+ const entry = fields[at + 1];
43
+ if (scope !== "local" && scope !== "worktree")
44
+ continue;
45
+ const newline = entry.indexOf("\n");
46
+ const key = (newline < 0 ? entry : entry.slice(0, newline)).toLowerCase();
47
+ const value = newline < 0 ? "" : entry.slice(newline + 1);
48
+ if (key === "core.fsmonitor" ? !BOOLEAN.test(value.trim()) : COMMAND_KEYS.some(pattern => pattern.test(key)))
49
+ return key;
50
+ }
51
+ return null;
52
+ }
53
+ /** Why a teardown command was refused, or null when `result` isn't a refusal. */
54
+ export function refusal(result) {
55
+ return result.code === REFUSED_CODE && result.stderr.startsWith(REFUSED_PREFIX) ? result.stderr.slice(REFUSED_PREFIX.length).trim() : null;
56
+ }
57
+ const refused = (why) => ({ code: REFUSED_CODE, stdout: "", stderr: `${REFUSED_PREFIX}${why}\n`, timedOut: false, notFound: false });
58
+ /** Refuse nested repositories without asking Git to enter them or evaluate their config.
59
+ * Includes ignored/untracked repositories and bare repositories; never follows symlinks. */
60
+ function nestedRepository(path) {
61
+ const pending = [path];
62
+ while (pending.length > 0) {
63
+ const dir = pending.pop();
64
+ const entries = readdirSync(dir, { withFileTypes: true });
65
+ if (dir !== path && (entries.some(entry => entry.name === ".git") ||
66
+ ["HEAD", "objects", "config"].every(name => entries.some(entry => entry.name === name))))
67
+ return true;
68
+ for (const entry of entries)
69
+ if (entry.isDirectory() && entry.name !== ".git")
70
+ pending.push(join(dir, entry.name));
71
+ }
72
+ return false;
73
+ }
74
+ /** The one way teardown runs git, including the target checkout immediately before removal. */
75
+ export function teardownGit(runner = run) {
76
+ const options = (cwd) => ({ cwd, envAllowlist: ENV_ALLOWLIST, env: ENV, timeoutMs: WORKTREE_TIMEOUT_MS, maxBuffer: 512 * 1024 * 1024 });
77
+ const configProblem = async (cwd) => {
78
+ const config = await runner("git", ["--no-optional-locks", "config", "--list", "--no-includes", "--show-scope", "-z"], options(cwd));
79
+ if (config.code !== 0)
80
+ return refused("its config couldn't be read");
81
+ const key = commandSetting(config.stdout);
82
+ return key === null ? null : refused(`the project's git config sets ${key}, which can run a command`);
83
+ };
84
+ return async (cwd, args) => {
85
+ const problem = await configProblem(cwd);
86
+ if (problem !== null)
87
+ return problem;
88
+ const removal = args[0] === "worktree" && args[1] === "remove";
89
+ const inspection = args.includes("status") || args.includes("diff");
90
+ const target = removal ? resolve(cwd, args.at(-1)) : cwd;
91
+ if (removal) {
92
+ const targetProblem = await configProblem(target);
93
+ if (targetProblem !== null)
94
+ return targetProblem;
95
+ }
96
+ if (removal || inspection || args.includes("checkout") || args.includes("clean")) {
97
+ try {
98
+ if (existsSync(target) && nestedRepository(target))
99
+ return refused("the checkout contains a nested repository");
100
+ }
101
+ catch {
102
+ return refused("the checkout couldn't be inspected for nested repositories");
103
+ }
104
+ // ls-files reads index entries only; it cannot run submodule filters. Even an uninitialized
105
+ // gitlink is kept, so worktree remove's internal status can never traverse it.
106
+ const index = await runner("git", [...TEARDOWN_FLAGS, "ls-files", "--stage", "-z"], options(target));
107
+ if (index.code !== 0)
108
+ return refused("the checkout's index couldn't be read");
109
+ if (index.stdout.split("\0").some(entry => entry.startsWith("160000 ")))
110
+ return refused("the checkout contains a gitlink");
111
+ }
112
+ const safeArgs = [...args];
113
+ if (inspection && !safeArgs.includes("--ignore-submodules=all")) {
114
+ const command = safeArgs.findIndex(arg => arg === "status" || arg === "diff");
115
+ safeArgs.splice(command + 1, 0, "--ignore-submodules=all");
116
+ }
117
+ return runner("git", [...TEARDOWN_FLAGS, ...safeArgs], options(cwd));
118
+ };
119
+ }
@@ -9,6 +9,9 @@ export declare const CONVERSATION_CLAIM_MS: number;
9
9
  /** A busy engine (one turn at a time) defers a queued message this long, up to the age bound below. */
10
10
  export declare const CONVERSATION_RETRY_MS = 5000;
11
11
  export declare const CONVERSATION_MAX_AGE_MS: number;
12
+ /** Waiting for the lead itself (it is answering, or earlier messages wait in the person's lead queue) is bounded longer:
13
+ * two hours from the message's arrival, never reset as it moves from waiting behind the queue to waiting on a turn. */
14
+ export declare const CONVERSATION_WAIT_MAX_AGE_MS: number;
12
15
  /** A card's buttons and an armed irreversible challenge live this long. */
13
16
  export declare const CARD_TTL_MS: number;
14
17
  export declare const CHALLENGE_TTL_MS: number;
@@ -39,6 +39,11 @@ export const CONVERSATION_CLAIM_MS = 2 * 60_000;
39
39
  /** A busy engine (one turn at a time) defers a queued message this long, up to the age bound below. */
40
40
  export const CONVERSATION_RETRY_MS = 5_000;
41
41
  export const CONVERSATION_MAX_AGE_MS = 10 * 60_000;
42
+ /** Waiting for the lead itself (it is answering, or earlier messages wait in the person's lead queue) is bounded longer:
43
+ * two hours from the message's arrival, never reset as it moves from waiting behind the queue to waiting on a turn. */
44
+ export const CONVERSATION_WAIT_MAX_AGE_MS = 2 * 3_600_000;
45
+ /** A message whose turn was admitted, waiting for the project records to deliver its answer (no age bound). */
46
+ const ADMITTED_REGISTRY_WAIT = "registry-unavailable:admitted";
42
47
  /** A card's buttons and an armed irreversible challenge live this long. */
43
48
  export const CARD_TTL_MS = 24 * 3_600_000;
44
49
  export const CHALLENGE_TTL_MS = 10 * 60_000;
@@ -367,12 +372,19 @@ async function runTelegramConversation(row, args) {
367
372
  const held = () => store.renewTelegramConversation(row.id, owner, CONVERSATION_CLAIM_MS, clock());
368
373
  const ageMs = clock().getTime() - Date.parse(row.createdAt);
369
374
  const requeue = async (outcome) => {
370
- if (ageMs > CONVERSATION_MAX_AGE_MS) {
375
+ // A turn already admitted for it waits for that turn's own outcome (bounded by the turn's deadline); waiting for the
376
+ // lead gets two hours from arrival; anything else, ten minutes.
377
+ const limit = outcome === "replayed:running" || outcome === ADMITTED_REGISTRY_WAIT ? Number.POSITIVE_INFINITY
378
+ : outcome === "busy" || outcome === "backlog" ? CONVERSATION_WAIT_MAX_AGE_MS : CONVERSATION_MAX_AGE_MS;
379
+ if (ageMs > limit) {
371
380
  await notify(couldNotAnswerText(outcome === "registry-unavailable" ? "your projects couldn't be read" : "the assistant stayed busy with another message"));
372
381
  finish({ state: "failed", outcome: `${outcome}:gave-up` });
373
382
  report.refused++;
374
383
  return;
375
384
  }
385
+ // Behind the person's own earlier messages: said once, then it waits quietly.
386
+ if (outcome === "backlog" && row.outcome !== "backlog")
387
+ await notify("Queued behind your earlier messages. It goes to the lead once they're answered.");
376
388
  finish({ state: "queued", outcome, nextAttemptAt: new Date(clock().getTime() + CONVERSATION_RETRY_MS).toISOString() });
377
389
  };
378
390
  // 1. The exact binding the message arrived under must still be the live one.
@@ -435,7 +447,9 @@ async function runTelegramConversation(row, args) {
435
447
  continue;
436
448
  }
437
449
  }
438
- if (now.getTime() - Date.parse(row.createdAt) > DELIVERY_MAX_AGE_MS) {
450
+ // A day from when this part was planned (its card's tokens were minted then), not from the message's arrival: an
451
+ // answer that waited out a registry outage still gets its full day to go out.
452
+ if (now.getTime() - Date.parse(part.createdAt) > DELIVERY_MAX_AGE_MS) {
439
453
  report.problems.push(`telegram chat reply for update ${row.updateId} was not sent within a day: ${part.lastError ?? "unsent"}`);
440
454
  await notify(couldNotAnswerText("my reply couldn't be sent for a day"));
441
455
  finish({ state: "failed", outcome: `unsent:gave-up:${part.lastError ?? "unsent"}` });
@@ -644,7 +658,7 @@ async function runTelegramConversation(row, args) {
644
658
  return;
645
659
  }
646
660
  if (turn.state !== "answered") {
647
- await notify(leadFailureText(turn.failureReason));
661
+ await notify(leadFailureText(turn.failureReason, store.leadTurnCostUnconfirmed(turn.id)));
648
662
  finish({ state: "failed", outcome: `replayed:${turn.failureReason ?? "failed"}` });
649
663
  report.refused++;
650
664
  return;
@@ -668,14 +682,31 @@ async function runTelegramConversation(row, args) {
668
682
  await deliver(row.outcome === ANSWERED_SENDING ? "answered" : "replayed");
669
683
  return;
670
684
  }
671
- // 3. The ceiling, reloaded now.
685
+ // 3. A turn already admitted for it and still working: wait for that turn's own outcome. Nothing here needs the
686
+ // project records, so a registry that can't be read never fails a message whose turn is running.
687
+ if (row.session !== null) {
688
+ const admitted = store.leadRequestReceipt(row.session, row.request);
689
+ if (admitted !== null) {
690
+ store.sweepStaleLeadTurns(clock());
691
+ const turn = store.getLeadTurn(admitted.turn);
692
+ if (turn !== null && (turn.state === "queued" || turn.state === "running")) {
693
+ store.bindTelegramConversationTurn(row.id, owner, row.session, admitted.turn);
694
+ await requeue("replayed:running");
695
+ return;
696
+ }
697
+ }
698
+ }
699
+ // The ceiling, reloaded now.
672
700
  let registry;
673
701
  try {
674
702
  registry = await readProjects();
675
703
  }
676
704
  catch {
677
705
  report.problems.push("telegram chat could not read the current project records");
678
- await requeue("registry-unavailable");
706
+ // A turn already took it (its receipt is in the bound session): its answer exists or will, and is delivered once
707
+ // the projects can be read again — never abandoned to the outage.
708
+ const admitted = row.session !== null && store.leadRequestReceipt(row.session, row.request) !== null;
709
+ await requeue(admitted ? ADMITTED_REGISTRY_WAIT : "registry-unavailable");
679
710
  return;
680
711
  }
681
712
  const repos = telegramConversationRepos(store, binding.approver, registry);
@@ -688,6 +719,12 @@ async function runTelegramConversation(row, args) {
688
719
  return;
689
720
  }
690
721
  }
722
+ // It waited (for the lead, or behind earlier messages), was never admitted, and is past the wait from its arrival:
723
+ // it is never sent this late, even when the lead is free now.
724
+ if ((row.outcome === "busy" || row.outcome === "backlog") && ageMs > CONVERSATION_WAIT_MAX_AGE_MS) {
725
+ await requeue(row.outcome);
726
+ return;
727
+ }
691
728
  // 5. The session and thread — shared with the console and the CLI.
692
729
  const resolved = resolveTelegramLead(store, binding, repos, clock());
693
730
  if (!resolved.ok && resolved.reason === "busy") {
@@ -743,8 +780,8 @@ async function runTelegramConversation(row, args) {
743
780
  if (receipt !== null)
744
781
  store.bindTelegramConversationTurn(row.id, owner, session.id, receipt.turn);
745
782
  if (!outcome.ok && "refused" in outcome) {
746
- if (outcome.refused === "concurrent") {
747
- await requeue("busy");
783
+ if (outcome.refused === "concurrent" || outcome.refused === "backlog") {
784
+ await requeue(outcome.refused === "backlog" ? "backlog" : "busy");
748
785
  return;
749
786
  }
750
787
  await notify(phoneText(outcome.message, 1_000));
@@ -20,8 +20,6 @@
20
20
  */
21
21
  import { type Store, type Notification } from "./store.js";
22
22
  import { type TelegramConversationOptions } from "./telegram-lead.js";
23
- /** Read the enrolled project list on demand. No callback means no task data,
24
- * never an implicit all-database ceiling. Shared by pass and embedded follower. */
25
23
  export type TelegramReadProjects = () => Promise<readonly string[]>;
26
24
  /** The environment name — and therefore the name the builder strips from agents (both names; see names.ts). */
27
25
  export { TELEGRAM_TOKEN_ENV as TOKEN_ENV, TELEGRAM_TOKEN_ENVS as TOKEN_ENVS } from "./names.js";
@@ -244,3 +242,14 @@ export declare function followBridge(store: Store, options: {
244
242
  * fact per entry — its subject, then its body's first line, indented.
245
243
  * Plain text, no buttons: nothing in a digest is tappable. */
246
244
  export declare function digestText(rows: readonly Notification[], since: string | null, now: Date, titleOf?: (taskId: string) => string): string;
245
+ /**
246
+ * An ordinary message from the paired person: persisted for the shared
247
+ * assistant in THIS transaction — before the cursor moves — with the exact
248
+ * binding, sender, update, message and the request identity the engine
249
+ * receipts its turn under. Two things are answered here and now without a
250
+ * model: text over the engine's bound (never truncated), and a reply to a
251
+ * message that carried several tasks (never guessed). Everything hostile is
252
+ * silence, as for every other inbound shape.
253
+ */
254
+ /** At most this many of a chat's messages wait for the lead at once (the personal lead queue's bound). */
255
+ export declare const TELEGRAM_MAX_WAITING = 10;