toolroll 0.9.63 → 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 (292) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +15 -5
  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 +50 -2
  16. package/dist/agent-fence.js +157 -10
  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 +235 -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 +197 -15
  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/checkout-proof.d.ts +28 -0
  55. package/dist/checkout-proof.js +62 -0
  56. package/dist/claim.d.ts +12 -0
  57. package/dist/claim.js +19 -0
  58. package/dist/cli-help.d.ts +1 -1
  59. package/dist/cli-help.js +4 -1
  60. package/dist/cli.d.ts +3 -0
  61. package/dist/cli.js +21 -2
  62. package/dist/coding-ui.js +6 -4
  63. package/dist/console-instance.d.ts +124 -0
  64. package/dist/console-instance.js +337 -0
  65. package/dist/contracts/chat-actions.d.ts +16 -0
  66. package/dist/contracts/chat-actions.js +4 -0
  67. package/dist/contracts/chat-content.d.ts +19 -0
  68. package/dist/contracts/chat-content.js +6 -0
  69. package/dist/contracts/console-api.d.ts +12 -4
  70. package/dist/contracts/console-api.js +12 -4
  71. package/dist/contracts/flow-card.d.ts +4 -0
  72. package/dist/contracts/flow-card.js +3 -0
  73. package/dist/contracts/flow.d.ts +46 -0
  74. package/dist/contracts/flow.js +11 -2
  75. package/dist/contracts/lead-tools.d.ts +92 -0
  76. package/dist/contracts/lead-tools.js +13 -0
  77. package/dist/contracts/provider.d.ts +11 -1
  78. package/dist/contracts/provider.js +1 -1
  79. package/dist/contracts/route.d.ts +76 -1
  80. package/dist/contracts/route.js +1 -1
  81. package/dist/contracts/store-json.d.ts +2 -0
  82. package/dist/control-setup.d.ts +1 -1
  83. package/dist/control-ui.js +2 -2
  84. package/dist/converse.d.ts +16 -0
  85. package/dist/converse.js +32 -4
  86. package/dist/demo-chat.js +4 -4
  87. package/dist/demo.js +2 -2
  88. package/dist/desktop-update-gate.js +6 -0
  89. package/dist/desktop-update.d.ts +1 -1
  90. package/dist/discord-chat.js +4 -1
  91. package/dist/doctor.js +1 -1
  92. package/dist/egress-bridge.d.ts +17 -0
  93. package/dist/egress-bridge.js +111 -0
  94. package/dist/egress.d.ts +119 -0
  95. package/dist/egress.js +423 -0
  96. package/dist/exec.d.ts +78 -0
  97. package/dist/exec.js +367 -9
  98. package/dist/export-ui.js +2 -2
  99. package/dist/flow-engine.js +24 -5
  100. package/dist/flow-gallery-ui.js +2 -2
  101. package/dist/flow-gallery.js +27 -8
  102. package/dist/flow-share.js +16 -5
  103. package/dist/flow-triggers.d.ts +77 -8
  104. package/dist/flow-triggers.js +517 -70
  105. package/dist/flows-cli.js +8 -2
  106. package/dist/flows-ui.d.ts +0 -2
  107. package/dist/flows-ui.js +52 -4
  108. package/dist/flows.d.ts +4 -0
  109. package/dist/flows.js +9 -1
  110. package/dist/html.d.ts +9 -0
  111. package/dist/html.js +11 -0
  112. package/dist/invoke.d.ts +17 -0
  113. package/dist/invoke.js +217 -21
  114. package/dist/keys.d.ts +18 -14
  115. package/dist/keys.js +5 -0
  116. package/dist/lead-capabilities.js +3 -2
  117. package/dist/lead-contract.js +2 -0
  118. package/dist/lead-doors.d.ts +2 -0
  119. package/dist/lead-doors.js +7 -0
  120. package/dist/lead-follow.d.ts +2 -0
  121. package/dist/lead-follow.js +58 -2
  122. package/dist/lead-progress.js +1 -0
  123. package/dist/lead-queue-runtime.d.ts +46 -0
  124. package/dist/lead-queue-runtime.js +191 -0
  125. package/dist/lead-queue.d.ts +104 -0
  126. package/dist/lead-queue.js +187 -0
  127. package/dist/lead-sweep.d.ts +67 -0
  128. package/dist/lead-sweep.js +99 -0
  129. package/dist/lead-tools.d.ts +22 -0
  130. package/dist/lead-tools.js +152 -1
  131. package/dist/lead.d.ts +13 -5
  132. package/dist/lead.js +89 -29
  133. package/dist/limits-ui.d.ts +8 -1
  134. package/dist/limits-ui.js +26 -0
  135. package/dist/live-bus.d.ts +8 -0
  136. package/dist/live-bus.js +40 -4
  137. package/dist/live-detail.d.ts +27 -0
  138. package/dist/live-detail.js +57 -0
  139. package/dist/live-notes.d.ts +20 -0
  140. package/dist/live-notes.js +47 -0
  141. package/dist/live-records.d.ts +140 -0
  142. package/dist/live-records.js +935 -0
  143. package/dist/live-terminal.d.ts +258 -0
  144. package/dist/live-terminal.js +649 -0
  145. package/dist/live.d.ts +38 -27
  146. package/dist/live.js +142 -124
  147. package/dist/mcp-oauth.d.ts +6 -3
  148. package/dist/mcp-oauth.js +8 -4
  149. package/dist/memory-pass.js +2 -2
  150. package/dist/mobile-viewport.d.ts +5 -1
  151. package/dist/mobile-viewport.js +18 -2
  152. package/dist/model-catalog.js +4 -0
  153. package/dist/monitoring-ui.js +2 -2
  154. package/dist/needs-you.d.ts +1 -1
  155. package/dist/needs-you.js +11 -4
  156. package/dist/operate.d.ts +2 -0
  157. package/dist/operate.js +275 -17
  158. package/dist/passwordless.d.ts +23 -0
  159. package/dist/passwordless.js +26 -0
  160. package/dist/peek-cli.d.ts +12 -4
  161. package/dist/peek-cli.js +19 -11
  162. package/dist/phase-routing.js +10 -3
  163. package/dist/planner-source.d.ts +3 -0
  164. package/dist/planner-source.js +14 -0
  165. package/dist/planner.d.ts +2 -1
  166. package/dist/planner.js +7 -1
  167. package/dist/policy-ui.js +2 -2
  168. package/dist/policy.d.ts +1 -20
  169. package/dist/policy.js +29 -2
  170. package/dist/project-delete-ui.d.ts +12 -0
  171. package/dist/project-delete-ui.js +41 -9
  172. package/dist/project-delete.js +2 -2
  173. package/dist/project-memory.d.ts +3 -1
  174. package/dist/project-memory.js +57 -25
  175. package/dist/project-tools.d.ts +5 -3
  176. package/dist/project-tools.js +12 -9
  177. package/dist/provider-auth.d.ts +4 -3
  178. package/dist/provider-auth.js +5 -4
  179. package/dist/provider-connection.d.ts +2 -1
  180. package/dist/provider-connection.js +5 -0
  181. package/dist/provider.d.ts +25 -2
  182. package/dist/provider.js +63 -9
  183. package/dist/pull-request-ui.js +2 -2
  184. package/dist/push.js +1 -1
  185. package/dist/quick-start.d.ts +24 -0
  186. package/dist/quick-start.js +127 -0
  187. package/dist/release.d.ts +3 -0
  188. package/dist/release.js +14 -8
  189. package/dist/request-budget-ui.js +2 -2
  190. package/dist/result-review.d.ts +1 -1
  191. package/dist/result-review.js +9 -3
  192. package/dist/retention-ui.js +2 -2
  193. package/dist/retention.js +10 -1
  194. package/dist/run-efficiency.d.ts +73 -0
  195. package/dist/run-efficiency.js +228 -0
  196. package/dist/runner.d.ts +5 -1
  197. package/dist/runner.js +22 -1
  198. package/dist/scope.d.ts +170 -5
  199. package/dist/scope.js +251 -31
  200. package/dist/scout-net.d.ts +4 -1
  201. package/dist/scout-net.js +37 -12
  202. package/dist/scout.d.ts +20 -6
  203. package/dist/scout.js +27 -3
  204. package/dist/serve.js +247 -40
  205. package/dist/server/chat.d.ts +1 -1
  206. package/dist/server/chat.js +122 -31
  207. package/dist/server/chrome.d.ts +6 -1
  208. package/dist/server/chrome.js +18 -7
  209. package/dist/server/console.css +52 -38
  210. package/dist/server/flows.js +38 -8
  211. package/dist/server/guards.d.ts +1 -0
  212. package/dist/server/guards.js +23 -9
  213. package/dist/server/http.d.ts +7 -0
  214. package/dist/server/http.js +3 -2
  215. package/dist/server/live.d.ts +34 -1
  216. package/dist/server/live.js +39 -17
  217. package/dist/server/pages-assets.js +5 -0
  218. package/dist/server/pages.js +90 -13
  219. package/dist/server/people-tokens.d.ts +3 -0
  220. package/dist/server/people-tokens.js +55 -17
  221. package/dist/server/remote-hooks.js +12 -6
  222. package/dist/server/remote.d.ts +2 -1
  223. package/dist/server/remote.js +14 -5
  224. package/dist/server/render-chat.d.ts +12 -3
  225. package/dist/server/render-chat.js +47 -16
  226. package/dist/server/render-pages.d.ts +4 -1
  227. package/dist/server/render-pages.js +5 -4
  228. package/dist/server/render-people.d.ts +3 -2
  229. package/dist/server/render-people.js +3 -2
  230. package/dist/server/render-results.d.ts +18 -2
  231. package/dist/server/render-results.js +37 -28
  232. package/dist/server/render-settings.js +6 -5
  233. package/dist/server/render-tasks.d.ts +35 -10
  234. package/dist/server/render-tasks.js +110 -27
  235. package/dist/server/request-context.d.ts +2 -0
  236. package/dist/server/request-context.js +2 -1
  237. package/dist/server/route-table.d.ts +3 -0
  238. package/dist/server/route-table.js +46 -5
  239. package/dist/server/runtime.d.ts +33 -2
  240. package/dist/server/session.d.ts +25 -1
  241. package/dist/server/session.js +18 -4
  242. package/dist/server/settings.js +121 -10
  243. package/dist/server/sweeps.d.ts +4 -0
  244. package/dist/server/sweeps.js +58 -0
  245. package/dist/server/task-views.d.ts +2 -1
  246. package/dist/server/task-views.js +96 -6
  247. package/dist/server/tasks.js +320 -74
  248. package/dist/server/terminals.d.ts +31 -0
  249. package/dist/server/terminals.js +365 -0
  250. package/dist/setup-guide.js +3 -2
  251. package/dist/slack-chat.js +4 -1
  252. package/dist/spend-ui.js +3 -3
  253. package/dist/spend.js +2 -1
  254. package/dist/steer-capabilities.d.ts +32 -0
  255. package/dist/steer-capabilities.js +60 -0
  256. package/dist/steer-controls.d.ts +14 -0
  257. package/dist/steer-controls.js +36 -0
  258. package/dist/steer-status.js +2 -2
  259. package/dist/storage-ui.js +4 -4
  260. package/dist/store-baseline.d.ts +1 -1
  261. package/dist/store-baseline.js +1 -1
  262. package/dist/store.d.ts +430 -15
  263. package/dist/store.js +1041 -52
  264. package/dist/subagent-desk.js +1 -1
  265. package/dist/summary.js +3 -0
  266. package/dist/task-source.d.ts +58 -0
  267. package/dist/task-source.js +93 -0
  268. package/dist/team-leads.js +1 -1
  269. package/dist/teams-chat.js +4 -1
  270. package/dist/telegram-lead.d.ts +3 -0
  271. package/dist/telegram-lead.js +44 -7
  272. package/dist/telegram.d.ts +11 -2
  273. package/dist/telegram.js +51 -17
  274. package/dist/terminal-catalog.d.ts +11 -0
  275. package/dist/terminal-catalog.js +129 -0
  276. package/dist/text-limits.d.ts +2 -0
  277. package/dist/text-limits.js +2 -0
  278. package/dist/tool-broker.d.ts +2 -0
  279. package/dist/tool-broker.js +1 -0
  280. package/dist/toolroll-update-ui.js +2 -2
  281. package/dist/toolroll-update.js +13 -3
  282. package/dist/tools-ui.js +2 -2
  283. package/dist/webhook-limits.d.ts +23 -0
  284. package/dist/webhook-limits.js +30 -0
  285. package/dist/webhook-readers.d.ts +56 -0
  286. package/dist/webhook-readers.js +138 -0
  287. package/dist/webhook-signing.d.ts +27 -0
  288. package/dist/webhook-signing.js +119 -0
  289. package/dist/work-index.d.ts +29 -0
  290. package/dist/work-index.js +16 -2
  291. package/dist/workspace-revision.js +3 -1
  292. package/package.json +11 -2
@@ -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
  }
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
@@ -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);
@@ -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;
package/dist/telegram.js CHANGED
@@ -46,6 +46,10 @@ import { LEAD_MESSAGE_MAX_CHARS } from "./lead.js";
46
46
  import { envValue } from "./names.js";
47
47
  import { applyProposalTap, processTelegramConversations, phoneLinkButton, replyContextFor, telegramConversationRepos, telegramRequestId, tooLongText, whichTaskText, } from "./telegram-lead.js";
48
48
  import { askOf, parseTelegramUpdate, pickOf, readTelegramButtonData, readTelegramUpdate, telegramButton } from "./contracts/telegram-callback.js";
49
+ /** Read the enrolled project list on demand. No callback means no task data,
50
+ * never an implicit all-database ceiling. Shared by pass and embedded follower. */
51
+ /** A delivery its person began watching after it was claimed: held or settled at the final fence (cockpit §3). */
52
+ const WATCHED_SINCE_CLAIM = "Held: its person is watching this agent now";
49
53
  /** The environment name — and therefore the name the builder strips from agents (both names; see names.ts). */
50
54
  export { TELEGRAM_TOKEN_ENV as TOKEN_ENV, TELEGRAM_TOKEN_ENVS as TOKEN_ENVS } from "./names.js";
51
55
  /** BotFather's shape: numeric bot id, colon, secret. */
@@ -732,6 +736,9 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
732
736
  const blocked = [];
733
737
  const preflight = (await readAccess()) ?? channelProblem();
734
738
  for (const row of group) {
739
+ // Watched since the claim (cockpit §3): held for later or settled quietly, never sent now, never a failure.
740
+ if (preflight === null && store.telegramWatchFence(row, binding, owner, clock()) !== "send")
741
+ continue;
735
742
  const problem = preflight ?? store.telegramDeliveryProblem(row, binding, owner, projects, clock(), [...rows.map(one => one.id), row.id]);
736
743
  if (problem === null)
737
744
  rows.push(row);
@@ -747,7 +754,18 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
747
754
  if (rows.length === 0)
748
755
  continue;
749
756
  const ids = rows.map(row => row.id);
750
- const fence = () => {
757
+ // Once any part of this delivery has reached Telegram it is committed: a watch opening between its parts never
758
+ // releases it (that would send the parts already accepted again). Every transport call below goes through `wire`.
759
+ let sentAny = false;
760
+ const wire = async (...args) => {
761
+ const answer = await transport(...args);
762
+ if (answer.ok === true)
763
+ sentAny = true;
764
+ return answer;
765
+ };
766
+ // `beforeSend`: the fence before a transport call also asks the watch — until something of this delivery went out;
767
+ // the one after a confirmed send never does, so a message Telegram accepted is recorded as delivered.
768
+ const fence = (beforeSend = !sentAny) => {
751
769
  const channel = channelProblem();
752
770
  if (channel !== null)
753
771
  return channel;
@@ -755,6 +773,10 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
755
773
  const problem = store.telegramDeliveryProblem(row, binding, owner, projects, clock(), ids);
756
774
  if (problem !== null)
757
775
  return problem;
776
+ // The watch, at the last moment before the network: a held row's claim is released (its finalize is then a
777
+ // no-op), so nothing is spent on it.
778
+ if (beforeSend && store.telegramWatchFence(row, binding, owner, clock()) !== "send")
779
+ return WATCHED_SINCE_CLAIM;
758
780
  }
759
781
  return null;
760
782
  };
@@ -763,7 +785,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
763
785
  const problem = (await readAccess()) ?? fence();
764
786
  if (problem !== null)
765
787
  return { ok: false, error: problem };
766
- const sent = await send(transport, binding.chatId, text, keyboard, entities, silent);
788
+ const sent = await send(wire, binding.chatId, text, keyboard, entities, silent);
767
789
  if (!sent.ok) {
768
790
  if (sent.retryAfter !== undefined)
769
791
  store.deferTelegram(botId, new Date(clock().getTime() + sent.retryAfter * 1_000).toISOString());
@@ -774,7 +796,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
774
796
  for (const row of messageRows)
775
797
  store.recordTelegramMessage(row, binding, sent.messageId, clock());
776
798
  // Keep the old message's history but never acknowledge a replaced pairing.
777
- const after = (await readAccess()) ?? fence();
799
+ const after = (await readAccess()) ?? fence(false);
778
800
  return after === null ? sent : { ok: false, error: after };
779
801
  };
780
802
  const imageSender = async (row) => {
@@ -803,7 +825,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
803
825
  // No await between the access fence, file validation and upload.
804
826
  let answer;
805
827
  try {
806
- answer = await transport("sendDocument", { chat_id: binding.chatId, caption: `${resultTaskLabel(row.taskId)} · result #${row.run} · screenshot for acceptance` }, undefined, {
828
+ answer = await wire("sendDocument", { chat_id: binding.chatId, caption: `${resultTaskLabel(row.taskId)} · result #${row.run} · screenshot for acceptance` }, undefined, {
807
829
  field: "document", bytes: verified.bytes, contentType: verified.format === "png" ? "image/png" : "image/jpeg", fileName: resultImageFileName(row.taskId, row.run, artifact, verified.format),
808
830
  });
809
831
  }
@@ -819,7 +841,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
819
841
  return { ok: false, error: answer.uncertain || (answer.ok && messageId == null) ? "Screenshot delivery is unconfirmed; retry may duplicate it" : "Telegram did not accept the screenshot" };
820
842
  }
821
843
  store.recordTelegramMessage(row, binding, String(messageId), clock());
822
- const after = (await readAccess()) ?? fence();
844
+ const after = (await readAccess()) ?? fence(false);
823
845
  return after === null ? { ok: true, receipt: receiptFor(botId, binding.chatId, String(messageId)) } : { ok: false, error: after };
824
846
  };
825
847
  /** Screenshots with a result (result-shots.ts): photos, several as one album, re-proved before every call. Each is
@@ -846,10 +868,10 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
846
868
  let answer;
847
869
  try {
848
870
  answer = batch.length > 1
849
- ? await transport("sendMediaGroup", { chat_id: binding.chatId, media: batch.map((one, index) => ({ type: "photo", media: `attach://shot${index}`, ...(index === 0 || one.numbered === true ? { caption: one.caption } : {}) })) }, undefined, upload(batch[0], "shot0"), batch.slice(1).map((one, index) => upload(one, `shot${index + 1}`)))
871
+ ? await wire("sendMediaGroup", { chat_id: binding.chatId, media: batch.map((one, index) => ({ type: "photo", media: `attach://shot${index}`, ...(index === 0 || one.numbered === true ? { caption: one.caption } : {}) })) }, undefined, upload(batch[0], "shot0"), batch.slice(1).map((one, index) => upload(one, `shot${index + 1}`)))
850
872
  : batch[0].photo
851
- ? await transport("sendPhoto", { chat_id: binding.chatId, caption: batch[0].caption }, undefined, upload(batch[0], "photo"))
852
- : await transport("sendDocument", { chat_id: binding.chatId, caption: batch[0].caption }, undefined, upload(batch[0], "document"));
873
+ ? await wire("sendPhoto", { chat_id: binding.chatId, caption: batch[0].caption }, undefined, upload(batch[0], "photo"))
874
+ : await wire("sendDocument", { chat_id: binding.chatId, caption: batch[0].caption }, undefined, upload(batch[0], "document"));
853
875
  }
854
876
  catch {
855
877
  answer = { ok: false, uncertain: true };
@@ -893,7 +915,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
893
915
  const keyboard = button === null ? [] : [button];
894
916
  if (messageId === null)
895
917
  return sender(card.text, keyboard, rows, card.entities);
896
- const edited = await editProgress(transport, binding.chatId, messageId, card.text, keyboard, card.entities);
918
+ const edited = await editProgress(wire, binding.chatId, messageId, card.text, keyboard, card.entities);
897
919
  if (!edited.ok) {
898
920
  if (edited.retryAfter !== undefined)
899
921
  store.deferTelegram(botId, new Date(clock().getTime() + edited.retryAfter * 1000).toISOString());
@@ -905,7 +927,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
905
927
  }
906
928
  if (!onlyExisting)
907
929
  store.recordTelegramMessage(row, binding, messageId, clock());
908
- const after = (await readAccess()) ?? fence();
930
+ const after = (await readAccess()) ?? fence(false);
909
931
  return after === null ? edited : { ok: false, error: after };
910
932
  };
911
933
  /** Quiet mode: the task's one card (or its group's), created silently and then only ever edited. */
@@ -929,11 +951,11 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
929
951
  return { ok: true, messageId: card.message };
930
952
  }
931
953
  if (card.message !== null) {
932
- const edited = await editProgress(transport, binding.chatId, card.message, view.text, keyboard, view.entities);
954
+ const edited = await editProgress(wire, binding.chatId, card.message, view.text, keyboard, view.entities);
933
955
  if (edited.ok) {
934
956
  store.setChatCardMessage(card.id, card.message, shown);
935
957
  store.recordTelegramMessage(fact, binding, card.message, clock());
936
- const after = (await readAccess()) ?? fence();
958
+ const after = (await readAccess()) ?? fence(false);
937
959
  return after === null ? edited : { ok: false, error: after };
938
960
  }
939
961
  if (edited.retryAfter !== undefined)
@@ -1003,14 +1025,14 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
1003
1025
  if (problem !== null)
1004
1026
  return { ok: false, error: problem };
1005
1027
  mintFor();
1006
- const edited = await editProgress(transport, binding.chatId, batch.message, text, keyboard);
1028
+ const edited = await editProgress(wire, binding.chatId, batch.message, text, keyboard);
1007
1029
  if (edited.ok) {
1008
1030
  // The repaint landed: the old words' buttons retire and the new ones ride the message.
1009
1031
  retireDecideTokens(store, "telegram", binding.chatId, batch.message, clock());
1010
1032
  placeDecideTokens(store, tokens, batch.message);
1011
1033
  store.setChatBatchMessage(batch.id, batch.message, shown);
1012
1034
  store.recordTelegramMessage(fact, binding, batch.message, clock());
1013
- const after = (await readAccess()) ?? fence();
1035
+ const after = (await readAccess()) ?? fence(false);
1014
1036
  return after === null ? { ok: true, receipt: receiptFor(botId, binding.chatId, batch.message) } : { ok: false, error: after };
1015
1037
  }
1016
1038
  // A failed edit: the buttons it carried never act, and the card's old ones keep working.
@@ -1049,10 +1071,10 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
1049
1071
  const problem = (await readAccess()) ?? fence();
1050
1072
  if (problem !== null)
1051
1073
  return { ok: false, error: problem };
1052
- const edited = await editProgress(transport, binding.chatId, earlier, words, keyboard);
1074
+ const edited = await editProgress(wire, binding.chatId, earlier, words, keyboard);
1053
1075
  if (edited.ok) {
1054
1076
  store.recordTelegramMessage(fact, binding, earlier, clock());
1055
- const after = (await readAccess()) ?? fence();
1077
+ const after = (await readAccess()) ?? fence(false);
1056
1078
  return after === null ? { ok: true, receipt: receiptFor(botId, binding.chatId, earlier) } : { ok: false, error: after };
1057
1079
  }
1058
1080
  if (edited.retryAfter !== undefined)
@@ -1082,7 +1104,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
1082
1104
  ? await deliverDigest(botId, binding, sender, rows, digest.lastSentAt, clock, taskId => chatTitle(store, taskId))
1083
1105
  : await deliverOne(store, botId, binding, sender, rows[0], clock, phoneOrigin, evidenceRoot, projects);
1084
1106
  }
1085
- const finalProblem = outcome.ok ? (await readAccess()) ?? fence() : null;
1107
+ const finalProblem = outcome.ok ? (await readAccess()) ?? fence(false) : null;
1086
1108
  const settled = finalProblem === null ? outcome : { ok: false, error: finalProblem };
1087
1109
  // A skipped screenshot settles its row but nothing reached the phone.
1088
1110
  const skipped = settled.ok && settled.receipt !== null && settled.receipt.startsWith("skipped:");
@@ -1748,6 +1770,8 @@ function applyMessage(context, update, effects) {
1748
1770
  * message that carried several tasks (never guessed). Everything hostile is
1749
1771
  * silence, as for every other inbound shape.
1750
1772
  */
1773
+ /** At most this many of a chat's messages wait for the lead at once (the personal lead queue's bound). */
1774
+ export const TELEGRAM_MAX_WAITING = 10;
1751
1775
  function applyConversation(context, update, effects) {
1752
1776
  const { store, botId, transport, clock, report } = context;
1753
1777
  const message = update.message;
@@ -1812,6 +1836,12 @@ function applyConversation(context, update, effects) {
1812
1836
  context_ = focusContextFor(focused);
1813
1837
  }
1814
1838
  }
1839
+ // The lead queue's bound, per chat: ten messages wait at most; the next is answered at once instead of queued.
1840
+ if (store.telegramMessagesWaiting(binding.id) >= TELEGRAM_MAX_WAITING) {
1841
+ say(`${TELEGRAM_MAX_WAITING} messages are already waiting for the lead. Wait for a reply, then send this again.`);
1842
+ report.ignored++;
1843
+ return;
1844
+ }
1815
1845
  store.enqueueTelegramConversation({
1816
1846
  binding, updateId: update.update_id, messageId: String(message.message_id), replyTo,
1817
1847
  request: telegramRequestId(botId, binding.id, update.update_id), text, context: context_, taskId, sourceRun,
@@ -2248,6 +2278,10 @@ function applyCallback(context, update, effects) {
2248
2278
  return;
2249
2279
  }
2250
2280
  const focused = store.chatFocus("telegram", binding.id);
2281
+ if (store.telegramMessagesWaiting(binding.id) >= TELEGRAM_MAX_WAITING) {
2282
+ ack(`${TELEGRAM_MAX_WAITING} messages are already waiting. Wait for a reply.`);
2283
+ return;
2284
+ }
2251
2285
  store.enqueueTelegramConversation({
2252
2286
  binding, updateId: update.update_id, messageId: String(message.message_id), replyTo: null,
2253
2287
  request: telegramRequestId(botId, binding.id, update.update_id), text: option,
@@ -0,0 +1,11 @@
1
+ import type { Database } from "./store.js";
2
+ import type { TerminalLedger } from "./live-terminal.js";
3
+ export declare function terminalCatalogPath(orders: Database): string | null;
4
+ /** The console's ledger: creates the catalog, drops rows of consoles that are gone, and writes its own. */
5
+ export declare function openTerminalLedger(orders: Database, alive?: (pid: number) => boolean): TerminalLedger | null;
6
+ export declare function installTerminalUpdateGate(orders: Database, id: string): void;
7
+ export declare function removeTerminalUpdateGate(orders: Database, id: string): void;
8
+ /** Terminals starting or running in a console that is still alive: active work an update waits for. */
9
+ export declare function activeTerminalUpdateWork(orders: Database, alive?: (pid: number) => boolean): {
10
+ terminals: number;
11
+ };