toolroll 0.9.53 → 0.9.55

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 (254) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +5 -4
  3. package/dist/agent-onboard.js +4 -1
  4. package/dist/approval-rules-ui.d.ts +3 -2
  5. package/dist/approval-rules-ui.js +15 -20
  6. package/dist/assignment-ui.d.ts +3 -2
  7. package/dist/assignment-ui.js +4 -11
  8. package/dist/backup-ui.d.ts +2 -1
  9. package/dist/backup-ui.js +16 -27
  10. package/dist/brand-mark.d.ts +3 -2
  11. package/dist/brand-mark.js +4 -4
  12. package/dist/browser/workspace.css +1 -1
  13. package/dist/browser/workspace.js +25 -25
  14. package/dist/browser-shell.d.ts +2 -1
  15. package/dist/browser-shell.js +9 -9
  16. package/dist/browser-workspace.d.ts +2 -3
  17. package/dist/browser-workspace.js +0 -3
  18. package/dist/chat-ask.js +8 -8
  19. package/dist/chat-core.d.ts +171 -0
  20. package/dist/chat-core.js +288 -0
  21. package/dist/chat-decide.d.ts +10 -8
  22. package/dist/chat-decide.js +26 -21
  23. package/dist/chat-delivery-state.d.ts +51 -75
  24. package/dist/chat-delivery-state.js +140 -170
  25. package/dist/chat-delivery.d.ts +21 -2
  26. package/dist/chat-delivery.js +313 -194
  27. package/dist/chat-flow.d.ts +29 -1
  28. package/dist/chat-flow.js +24 -50
  29. package/dist/chat-messages.d.ts +319 -0
  30. package/dist/chat-messages.js +841 -0
  31. package/dist/chat-migration.d.ts +30 -0
  32. package/dist/chat-migration.js +802 -0
  33. package/dist/chat-polish.d.ts +5 -4
  34. package/dist/chat-polish.js +9 -10
  35. package/dist/chat-quiet.d.ts +3 -0
  36. package/dist/chat-quiet.js +5 -0
  37. package/dist/check-levels-ui.d.ts +2 -1
  38. package/dist/check-levels-ui.js +26 -53
  39. package/dist/cli.d.ts +3 -1
  40. package/dist/cli.js +9 -3
  41. package/dist/coding-shipping-ui.d.ts +2 -1
  42. package/dist/coding-shipping-ui.js +6 -6
  43. package/dist/coding-ui.d.ts +3 -1
  44. package/dist/coding-ui.js +30 -29
  45. package/dist/contracts/chat-content.d.ts +71 -2
  46. package/dist/contracts/chat-content.js +22 -3
  47. package/dist/contracts/chat-tables.d.ts +11 -4
  48. package/dist/contracts/chat-tables.js +165 -87
  49. package/dist/contracts/cli.d.ts +1 -3
  50. package/dist/contracts/console-api.d.ts +1 -1
  51. package/dist/contracts/console-api.js +6 -5
  52. package/dist/contracts/discord-callback.js +2 -1
  53. package/dist/contracts/health.d.ts +1 -3
  54. package/dist/contracts/slack-callback.js +3 -2
  55. package/dist/contracts/store-json.d.ts +1 -5
  56. package/dist/contracts/store-json.js +1 -4
  57. package/dist/contracts/teams-callback.js +2 -1
  58. package/dist/control-ui.d.ts +5 -4
  59. package/dist/control-ui.js +9 -34
  60. package/dist/credentials-ui.d.ts +5 -4
  61. package/dist/credentials-ui.js +23 -24
  62. package/dist/demo-chat.d.ts +3 -3
  63. package/dist/demo-chat.js +29 -76
  64. package/dist/discord-chat.js +19 -38
  65. package/dist/discord-settings.d.ts +3 -2
  66. package/dist/discord-settings.js +21 -55
  67. package/dist/discord.js +4 -14
  68. package/dist/envelope.js +10 -0
  69. package/dist/evidence-pack.d.ts +2 -1
  70. package/dist/evidence-pack.js +45 -49
  71. package/dist/export-ui.d.ts +7 -2
  72. package/dist/export-ui.js +4 -10
  73. package/dist/export.js +4 -4
  74. package/dist/flow-gallery-ui.d.ts +4 -3
  75. package/dist/flow-gallery-ui.js +28 -43
  76. package/dist/flow-starters-ui.d.ts +2 -1
  77. package/dist/flow-starters-ui.js +12 -15
  78. package/dist/flow-triggers.d.ts +1 -0
  79. package/dist/flow-triggers.js +16 -14
  80. package/dist/flows-ui.d.ts +4 -3
  81. package/dist/flows-ui.js +19 -30
  82. package/dist/flows.js +2 -1
  83. package/dist/guides.js +108 -3
  84. package/dist/html.d.ts +76 -0
  85. package/dist/html.js +173 -0
  86. package/dist/integrations-ui.d.ts +2 -1
  87. package/dist/integrations-ui.js +19 -23
  88. package/dist/kits-ui.d.ts +5 -4
  89. package/dist/kits-ui.js +18 -20
  90. package/dist/knowledge-ui.d.ts +6 -5
  91. package/dist/knowledge-ui.js +26 -25
  92. package/dist/lead-context.d.ts +11 -1
  93. package/dist/lead-context.js +6 -8
  94. package/dist/ledger-view.d.ts +2 -1
  95. package/dist/ledger-view.js +27 -29
  96. package/dist/limits-ui.d.ts +2 -1
  97. package/dist/limits-ui.js +7 -7
  98. package/dist/live-bus.d.ts +50 -8
  99. package/dist/live-bus.js +133 -50
  100. package/dist/mate-tools.js +3 -2
  101. package/dist/mcp-oauth.js +35 -36
  102. package/dist/models-ui.d.ts +2 -1
  103. package/dist/models-ui.js +25 -35
  104. package/dist/modes.d.ts +13 -1
  105. package/dist/modes.js +17 -1
  106. package/dist/monitoring-ui.d.ts +4 -3
  107. package/dist/monitoring-ui.js +13 -26
  108. package/dist/notification-rows.d.ts +30 -0
  109. package/dist/notification-rows.js +102 -0
  110. package/dist/openrouter-models.d.ts +2 -1
  111. package/dist/openrouter-models.js +5 -9
  112. package/dist/operate.js +16 -9
  113. package/dist/people-audit-ui.d.ts +2 -1
  114. package/dist/people-audit-ui.js +13 -13
  115. package/dist/plane-review.js +5 -11
  116. package/dist/policy-ui.d.ts +3 -2
  117. package/dist/policy-ui.js +17 -23
  118. package/dist/project-delete-ui.d.ts +4 -3
  119. package/dist/project-delete-ui.js +15 -27
  120. package/dist/project-delete.js +21 -26
  121. package/dist/pull-request-ui.d.ts +2 -1
  122. package/dist/pull-request-ui.js +17 -32
  123. package/dist/recipe-ui.d.ts +8 -7
  124. package/dist/recipe-ui.js +39 -32
  125. package/dist/release-adapters.d.ts +60 -0
  126. package/dist/release-adapters.js +435 -0
  127. package/dist/release-cli.d.ts +24 -0
  128. package/dist/release-cli.js +181 -0
  129. package/dist/release-gate.d.ts +80 -0
  130. package/dist/release-gate.js +150 -0
  131. package/dist/release-links.d.ts +29 -0
  132. package/dist/release-links.js +102 -0
  133. package/dist/release.d.ts +331 -0
  134. package/dist/release.js +550 -0
  135. package/dist/reply-shape.d.ts +2 -1
  136. package/dist/reply-shape.js +5 -5
  137. package/dist/repository-context-ui.d.ts +2 -1
  138. package/dist/repository-context-ui.js +2 -7
  139. package/dist/request-budget-ui.d.ts +2 -1
  140. package/dist/request-budget-ui.js +16 -16
  141. package/dist/result-review.d.ts +2 -3
  142. package/dist/result-review.js +3 -9
  143. package/dist/result-shots.js +2 -2
  144. package/dist/retention-ui.d.ts +7 -1
  145. package/dist/retention-ui.js +7 -11
  146. package/dist/retention.js +1 -1
  147. package/dist/serve.d.ts +11 -2
  148. package/dist/serve.js +69 -58
  149. package/dist/server/chat.d.ts +3 -6
  150. package/dist/server/chat.js +969 -1006
  151. package/dist/server/chrome.d.ts +291 -0
  152. package/dist/server/chrome.js +747 -0
  153. package/dist/server/console.css +2819 -0
  154. package/dist/server/flows.d.ts +2 -6
  155. package/dist/server/flows.js +840 -766
  156. package/dist/server/guards.d.ts +1 -1
  157. package/dist/server/guards.js +5 -2
  158. package/dist/server/handler-context.d.ts +1 -1
  159. package/dist/server/handler-context.js +1 -1
  160. package/dist/server/handler-registry.d.ts +8 -6
  161. package/dist/server/handler-registry.js +14 -2
  162. package/dist/server/http.d.ts +274 -0
  163. package/dist/server/http.js +239 -0
  164. package/dist/server/live.d.ts +34 -0
  165. package/dist/server/live.js +309 -0
  166. package/dist/server/pages.d.ts +2 -6
  167. package/dist/server/pages.js +808 -789
  168. package/dist/server/people-tokens.d.ts +1 -6
  169. package/dist/server/people-tokens.js +580 -572
  170. package/dist/server/remote.d.ts +4 -7
  171. package/dist/server/remote.js +37 -70
  172. package/dist/server/render-chat.d.ts +402 -0
  173. package/dist/server/render-chat.js +927 -0
  174. package/dist/server/render-pages.d.ts +535 -0
  175. package/dist/server/render-pages.js +1277 -0
  176. package/dist/server/render-people.d.ts +32 -0
  177. package/dist/server/render-people.js +121 -0
  178. package/dist/server/render-results.d.ts +744 -0
  179. package/dist/server/render-results.js +2237 -0
  180. package/dist/server/render-settings.d.ts +147 -0
  181. package/dist/server/render-settings.js +577 -0
  182. package/dist/server/render-tasks.d.ts +632 -0
  183. package/dist/server/render-tasks.js +2345 -0
  184. package/dist/server/request-context.d.ts +46 -0
  185. package/dist/server/request-context.js +17 -0
  186. package/dist/server/route-table.d.ts +25 -3
  187. package/dist/server/route-table.js +163 -75
  188. package/dist/server/runtime.d.ts +15 -7
  189. package/dist/server/runtime.js +7 -1
  190. package/dist/server/session.d.ts +138 -0
  191. package/dist/server/session.js +94 -0
  192. package/dist/server/settings.d.ts +1 -5
  193. package/dist/server/settings.js +2182 -2117
  194. package/dist/server/task-views.d.ts +11 -4
  195. package/dist/server/task-views.js +26 -22
  196. package/dist/server/tasks.d.ts +9 -9
  197. package/dist/server/tasks.js +1486 -1418
  198. package/dist/server-telemetry.d.ts +4 -3
  199. package/dist/server-telemetry.js +4 -8
  200. package/dist/skills-ui.d.ts +4 -3
  201. package/dist/skills-ui.js +14 -17
  202. package/dist/skills.d.ts +1 -1
  203. package/dist/skills.js +2 -1
  204. package/dist/slack-api.d.ts +5 -1
  205. package/dist/slack-api.js +2 -2
  206. package/dist/slack-chat.d.ts +9 -4
  207. package/dist/slack-chat.js +29 -52
  208. package/dist/slack-settings.d.ts +3 -2
  209. package/dist/slack-settings.js +23 -57
  210. package/dist/slack.js +6 -16
  211. package/dist/spend-ui.d.ts +2 -1
  212. package/dist/spend-ui.js +9 -32
  213. package/dist/sso-ui.d.ts +3 -2
  214. package/dist/sso-ui.js +26 -27
  215. package/dist/storage-ui.d.ts +2 -1
  216. package/dist/storage-ui.js +14 -33
  217. package/dist/store.d.ts +97 -364
  218. package/dist/store.js +207 -1531
  219. package/dist/surface.js +14 -3
  220. package/dist/task-composer.d.ts +2 -1
  221. package/dist/task-composer.js +7 -7
  222. package/dist/task-status.d.ts +4 -3
  223. package/dist/task-status.js +15 -16
  224. package/dist/team-http.d.ts +5 -9
  225. package/dist/team-http.js +5 -63
  226. package/dist/team-ui.d.ts +2 -1
  227. package/dist/team-ui.js +2 -5
  228. package/dist/teammate-question.js +12 -12
  229. package/dist/teammates-ui.d.ts +5 -4
  230. package/dist/teammates-ui.js +61 -90
  231. package/dist/teams-chat.js +12 -9
  232. package/dist/teams-settings.d.ts +3 -2
  233. package/dist/teams-settings.js +22 -33
  234. package/dist/teams.js +4 -5
  235. package/dist/telegram-flow.js +5 -5
  236. package/dist/telegram-settings.d.ts +3 -2
  237. package/dist/telegram-settings.js +14 -25
  238. package/dist/telegram.js +4 -4
  239. package/dist/toolroll-update-ui.d.ts +3 -2
  240. package/dist/toolroll-update-ui.js +35 -39
  241. package/dist/toolroll-update.d.ts +10 -0
  242. package/dist/toolroll-update.js +50 -3
  243. package/dist/tools-ui.d.ts +5 -2
  244. package/dist/tools-ui.js +31 -31
  245. package/dist/when-html.d.ts +11 -3
  246. package/dist/when-html.js +5 -6
  247. package/dist/workspace-revision.js +3 -1
  248. package/dist/workspace-ui.d.ts +4 -2
  249. package/dist/workspace-ui.js +10 -10
  250. package/package.json +1 -1
  251. package/dist/server/shared.d.ts +0 -3151
  252. package/dist/server/shared.js +0 -12001
  253. package/dist/slack-state.d.ts +0 -13
  254. package/dist/slack-state.js +0 -10
@@ -1,5 +1,5 @@
1
+ import { html, postForm } from "./html.js";
1
2
  import { effectiveLimits, limitOverrides, PER_DAY_MAX, PER_MINUTE_MAX, REQUEST_BUDGET_DEFAULTS } from "./request-budget.js";
2
- const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
3
3
  const n = (value) => value.toLocaleString("en-US");
4
4
  export const REQUEST_LIMITS_CSS = `.credentials .limits{display:grid;gap:14px}.credentials .limits form{display:grid;gap:10px}.credentials .limits label{display:grid;gap:6px}` +
5
5
  `.credentials .limits input,.credentials .limits select{box-sizing:border-box;width:100%;min-width:0}.credentials .limits .pair{display:grid;grid-template-columns:1fr 1fr;gap:10px}.credentials .limits .actions{display:flex;flex-wrap:wrap;gap:8px}` +
@@ -10,22 +10,22 @@ export function tokenLimitWords(db, token) {
10
10
  return `${n(limits.perMinute)} requests a minute · ${n(limits.perDay)} a day`;
11
11
  }
12
12
  /** The operator's disclosure: everyone's tokens, or one token. Blank fields use the default shown. */
13
- export function requestLimitsHtml(db, tokens, csrf, now) {
13
+ export function requestLimitsHtml(db, tokens, now) {
14
14
  const overrides = limitOverrides(db);
15
15
  const everyone = overrides.get("*") ?? { readPerMinute: null, actPerMinute: null, perDay: null };
16
- const field = (name, label, value, placeholder, max) => `<label>${label}<input type="number" name="${name}" min="1" max="${max}" step="1" inputmode="numeric" value="${value === null ? "" : value}" placeholder="${placeholder}"></label>`;
17
- const password = `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>`;
18
- const buttons = `<div class="actions"><button name="action" value="save">Save limits</button><button class="secondary" name="action" value="clear">Use defaults</button></div>`;
16
+ const field = (name, label, value, placeholder, max) => html `<label>${label}<input type="number" name="${name}" min="1" max="${max}" step="1" inputmode="numeric" value="${value === null ? "" : value}" placeholder="${placeholder}"></label>`;
17
+ const password = html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>`;
18
+ const buttons = html `<div class="actions"><button name="action" value="save">Save limits</button><button class="secondary" name="action" value="clear">Use defaults</button></div>`;
19
19
  const live = tokens.filter(one => one.revokedAt === null && Date.parse(one.expiresAt) > now);
20
- const installation = `<form method="post" action="/settings/request-limits"><input type="hidden" name="csrf" value="${e(csrf)}"><input type="hidden" name="target" value="*">` +
21
- `<h3>Everyone's tokens</h3><div class="pair">${field("read-per-minute", "Read tokens, a minute", everyone.readPerMinute, String(REQUEST_BUDGET_DEFAULTS.read), PER_MINUTE_MAX)}` +
22
- `${field("act-per-minute", "Act tokens, a minute", everyone.actPerMinute, String(REQUEST_BUDGET_DEFAULTS.act), PER_MINUTE_MAX)}</div>` +
23
- `${field("per-day", "Each token, a day", everyone.perDay, String(REQUEST_BUDGET_DEFAULTS.day), PER_DAY_MAX)}${password}${buttons}</form>`;
24
- const one = live.length === 0 ? "" : `<form method="post" action="/settings/request-limits"><input type="hidden" name="csrf" value="${e(csrf)}">` +
25
- `<h3>One token</h3><label>Token<select name="target">${live.map(row => {
26
- const set = overrides.get(row.id);
27
- return `<option value="${e(row.id)}">${e(row.name)} (${e(row.account)}, ${row.access === "act" ? "act" : "read"})${set === undefined ? "" : " · own limits"}</option>`;
28
- }).join("")}</select></label>` +
29
- `<div class="pair">${field("per-minute", "A minute", null, "Everyone's", PER_MINUTE_MAX)}${field("per-day", "A day", null, "Everyone's", PER_DAY_MAX)}</div>${password}${buttons}</form>`;
30
- return `<details class="card"><summary>Request limits</summary><div class="limits">${installation}${one}</div></details>`;
20
+ const installation = postForm("/settings/request-limits", html `\
21
+ <h3>Everyone's tokens</h3><div class="pair">${field("read-per-minute", "Read tokens, a minute", everyone.readPerMinute, String(REQUEST_BUDGET_DEFAULTS.read), PER_MINUTE_MAX)}\
22
+ ${field("act-per-minute", "Act tokens, a minute", everyone.actPerMinute, String(REQUEST_BUDGET_DEFAULTS.act), PER_MINUTE_MAX)}</div>\
23
+ ${field("per-day", "Each token, a day", everyone.perDay, String(REQUEST_BUDGET_DEFAULTS.day), PER_DAY_MAX)}${password}${buttons}`, { hidden: { target: "*" } });
24
+ const one = live.length === 0 ? "" : postForm("/settings/request-limits", html `\
25
+ <h3>One token</h3><label>Token<select name="target">${live.map(row => {
26
+ const set = overrides.get(row.id);
27
+ return html `<option value="${row.id}">${row.name} (${row.account}, ${row.access === "act" ? "act" : "read"})${set === undefined ? "" : " · own limits"}</option>`;
28
+ })}</select></label>\
29
+ <div class="pair">${field("per-minute", "A minute", null, "Everyone's", PER_MINUTE_MAX)}${field("per-day", "A day", null, "Everyone's", PER_DAY_MAX)}</div>${password}${buttons}`);
30
+ return html `<details class="card"><summary>Request limits</summary><div class="limits">${installation}${one}</div></details>`;
31
31
  }
@@ -5,7 +5,7 @@
5
5
  * Nothing here reads a database or renders a full page. The server
6
6
  * assembles the same verified records it already reads (handoff, proof,
7
7
  * diff, matrix, publication, report) into `SharedResultFacts`; every
8
- * surface prints those facts through `resultFactsAttributes`, so a test —
8
+ * surface prints those facts through `resultFactsAttributeMap` (as html attributes), so a test —
9
9
  * or a person — can check that chat, run detail, and review agree on the
10
10
  * run, its head, its checks, its caveats, and its publication.
11
11
  *
@@ -141,8 +141,7 @@ export type SharedResultFacts = {
141
141
  /** The shared facts as the panel's data attributes, unescaped (the rebuilt
142
142
  * result page sets them through React). */
143
143
  export declare function resultFactsAttributeMap(facts: SharedResultFacts): Record<string, string>;
144
- export declare function resultFactsAttributes(facts: SharedResultFacts): string;
145
- /** The shared fact names, in the order `resultFactsAttributes` writes them. */
144
+ /** The shared fact names, in the order `resultFactsAttributeMap` lists them. */
146
145
  export declare const RESULT_FACT_KEYS: readonly ["run", "head", "base", "head-source", "checks", "caveats", "evidence", "publication"];
147
146
  /** Parse the shared facts back — the test-side and proof-side reader.
148
147
  * Only the shared keys are read; a panel's own presentation attributes
@@ -5,7 +5,7 @@
5
5
  * Nothing here reads a database or renders a full page. The server
6
6
  * assembles the same verified records it already reads (handoff, proof,
7
7
  * diff, matrix, publication, report) into `SharedResultFacts`; every
8
- * surface prints those facts through `resultFactsAttributes`, so a test —
8
+ * surface prints those facts through `resultFactsAttributeMap` (as html attributes), so a test —
9
9
  * or a person — can check that chat, run detail, and review agree on the
10
10
  * run, its head, its checks, its caveats, and its publication.
11
11
  *
@@ -126,10 +126,7 @@ export function resultFactsAttributeMap(facts) {
126
126
  "data-result-publication": facts.publicationState,
127
127
  };
128
128
  }
129
- export function resultFactsAttributes(facts) {
130
- return Object.entries(resultFactsAttributeMap(facts)).map(([name, value]) => ` ${name}="${escapeAttribute(value)}"`).join("");
131
- }
132
- /** The shared fact names, in the order `resultFactsAttributes` writes them. */
129
+ /** The shared fact names, in the order `resultFactsAttributeMap` lists them. */
133
130
  export const RESULT_FACT_KEYS = ["run", "head", "base", "head-source", "checks", "caveats", "evidence", "publication"];
134
131
  /** Parse the shared facts back — the test-side and proof-side reader.
135
132
  * Only the shared keys are read; a panel's own presentation attributes
@@ -426,9 +423,6 @@ export const RESULT_REVIEW_SCRIPT = String.raw `
426
423
  }
427
424
  }
428
425
  })();`;
429
- function escapeAttribute(text) {
430
- return text.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
431
- }
432
426
  function unescapeAttribute(text) {
433
- return text.replace(/&quot;/g, '"').replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
427
+ return text.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
434
428
  }
@@ -169,6 +169,6 @@ export function refuseResultShots(state, store, part, content, app, now) {
169
169
  const title = chatTitle(store, content.image?.taskId ?? content.task ?? "");
170
170
  const line = { text: `Screenshots for ${title} weren't sent: the ${app} app isn't allowed to upload files here.`,
171
171
  ...(content.task === undefined ? {} : { task: content.task }), ...(content.run === undefined ? {} : { run: content.run }) };
172
- state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL,problem=NULL,file=NULL,uploaded=0,uncertain=0,created=? WHERE id=?").run(savedChatPart(line), now.toISOString(), part.id);
173
- state.prepare("UPDATE chat_part SET state='dropped',problem='Uploads are not allowed here' WHERE event=? AND id<>? AND state='pending'").run(part.event, part.id);
172
+ state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL,problem=NULL,file=NULL,uploaded=0,uncertain=0,created=? WHERE provider=:provider AND id=?").run(savedChatPart(line), now.toISOString(), part.id);
173
+ state.prepare("UPDATE chat_part SET state='dropped',problem='Uploads are not allowed here' WHERE provider=:provider AND event=? AND id<>? AND state='pending'").run(part.event, part.id);
174
174
  }
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Settings → Retention (v105): how long each kind of data is kept, and what
3
+ * the next daily sweep would remove. An instance operator's page; changes
4
+ * take a step-up.
5
+ */
6
+ import { type Html } from "./html.js";
1
7
  import { type RetentionCount, type RetentionKind, type RetentionPeriods } from "./retention.js";
2
8
  export declare const RETENTION_CSS: string;
3
9
  /** `chosen`: the kinds someone chose a period for; the rest keep their default. */
@@ -11,4 +17,4 @@ export type RetentionView = {
11
17
  export declare function retentionHtml(view: RetentionView, notice: {
12
18
  said?: string | null;
13
19
  problem?: string | null;
14
- }): string;
20
+ }): Html;
@@ -3,9 +3,9 @@
3
3
  * the next daily sweep would remove. An instance operator's page; changes
4
4
  * take a step-up.
5
5
  */
6
+ import { html, postForm } from "./html.js";
6
7
  import { bytesWords } from "./storage.js";
7
8
  import { RETENTION_KINDS, countWords, periodChoices, periodLabel } from "./retention.js";
8
- const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
9
9
  export const RETENTION_CSS = `.retention{max-width:720px;min-width:0}.retention-next{margin:0 0 16px}` +
10
10
  `.retention form{display:grid;gap:0}.retention .kind{display:grid;grid-template-columns:minmax(0,1fr) 11rem;gap:4px 16px;align-items:center;padding:12px 0;border-bottom:1px solid var(--border)}` +
11
11
  `.retention .kind .name{font-weight:500;font-size:.875rem}.retention .kind .meta{grid-column:1;margin:0}.retention .kind select{grid-column:2;grid-row:1/span 2;width:100%;min-height:44px}` +
@@ -23,17 +23,13 @@ function nextWords(view) {
23
23
  return `The next daily sweep removes ${listWords(due.map(countWords))} (about ${bytesWords(bytes)}).`;
24
24
  }
25
25
  export function retentionHtml(view, notice) {
26
- const note = notice.problem ? `<p class="problem" role="alert">${e(notice.problem)}</p>` : notice.said ? `<p role="status">${e(notice.said)}</p>` : "";
26
+ const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
27
27
  const rows = RETENTION_KINDS.map(({ kind, label, detail }) => {
28
28
  const current = view.periods[kind];
29
29
  const chosen = view.chosen.includes(kind);
30
- const options = periodChoices(kind, current).map(days => `<option value="${days === null ? "forever" : days}"${days === current ? " selected" : ""}>${e(periodLabel(kind, days, chosen || days !== current))}</option>`).join("");
31
- return `<div class="kind" data-kind="${kind}"><label class="name" for="keep-${kind}">${e(label)}</label><p class="meta">${e(detail)}</p><select id="keep-${kind}" name="${kind}">${options}</select></div>`;
32
- }).join("");
33
- const last = view.lastSweep === null ? "" : ` Last sweep ${e(view.lastSweep.slice(0, 10))}.`;
34
- return `<article class="retention">${note}<p class="retention-next" data-retention-next>${e(nextWords(view))}${last}</p>` +
35
- `<form method="post" action="/settings/retention"><input type="hidden" name="csrf" value="${e(view.csrf)}">${rows}` +
36
- `<label class="step-up">Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>` +
37
- `<button type="submit">Save</button></form>` +
38
- `<p class="meta">Never removed: the action ledger, and anything a task still needs (unfinished tasks, results Ready for review, anything on hold).</p></article>`;
30
+ const options = periodChoices(kind, current).map(days => html `<option value="${days === null ? "forever" : days}"${days === current ? html ` selected` : ""}>${periodLabel(kind, days, chosen || days !== current)}</option>`);
31
+ return html `<div class="kind" data-kind="${kind}"><label class="name" for="keep-${kind}">${label}</label><p class="meta">${detail}</p><select id="keep-${kind}" name="${kind}">${options}</select></div>`;
32
+ });
33
+ const last = view.lastSweep === null ? "" : ` Last sweep ${view.lastSweep.slice(0, 10)}.`;
34
+ return html `<article class="retention">${note}<p class="retention-next" data-retention-next>${nextWords(view)}${last}</p>${postForm("/settings/retention", html `${rows}<label class="step-up">Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label><button type="submit">Save</button>`)}<p class="meta">Never removed: the action ledger, and anything a task still needs (unfinished tasks, results Ready for review, anything on hold).</p></article>`;
39
35
  }
package/dist/retention.js CHANGED
@@ -280,7 +280,7 @@ export function sweepRetention(store, evidenceRoot, now, actor = "worker") {
280
280
  }
281
281
  if (periods.notifications !== null) {
282
282
  const deliveries = db.prepare("DELETE FROM notification_delivery WHERE notification = ?");
283
- const outbound = db.prepare("DELETE FROM telegram_outbound_message WHERE notification = ?");
283
+ const outbound = db.prepare("DELETE FROM chat_message_ref WHERE notification = ?");
284
284
  const pushes = db.prepare("DELETE FROM push_delivery WHERE notification = ?");
285
285
  const notification = db.prepare("DELETE FROM notification WHERE id = ?");
286
286
  for (const one of plan.items.notifications) {
package/dist/serve.d.ts CHANGED
@@ -1,3 +1,12 @@
1
- import { type DecisionServer, type ServeOptions } from "./server/shared.js";
1
+ import { type ServeOptions } from "./server/http.js";
2
+ import { type DecisionServer } from "./server/session.js";
2
3
  export declare function createDecisionServer(options: ServeOptions): DecisionServer;
3
- export { decisionAnswerScript, diffFileAnchor, earlierVersionsWords, editorFileHref, inboxFingerprints, needActionOf, orderChangedFiles, PAGE_CSS, parseInboxTab, pinnedTheme, plainConclusionOf, queueScript, rankReviewQueue, redactedPath, reviewFilePriority, reviewPriorityOf, revisionLineageWords, runFactsFragment, SENSITIVE_INPUT, settingsGroups, settingsTiles, SHUTDOWN_WAIT_MS, SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH, ssoStepUps, TASK_COMPOSER_MODES, withinSignedTouches, wrongHostPage, type DecisionServer, type InboxTab, type ReviewFileRow, type ReviewPriority, type ReviewQueueFacts, type RouteView, type ServeOptions, type TaskComposerMode } from "./server/shared.js";
4
+ export { PAGE_CSS, pinnedTheme, SENSITIVE_INPUT } from "./server/chrome.js";
5
+ export { redactedPath, SHUTDOWN_WAIT_MS, type ServeOptions } from "./server/http.js";
6
+ export { TASK_COMPOSER_MODES, type TaskComposerMode } from "./server/render-chat.js";
7
+ export { inboxFingerprints, parseInboxTab, queueScript, type InboxTab } from "./server/render-pages.js";
8
+ export { ssoStepUps, wrongHostPage } from "./server/render-people.js";
9
+ export { diffFileAnchor, editorFileHref, needActionOf, orderChangedFiles, plainConclusionOf, rankReviewQueue, reviewFilePriority, reviewPriorityOf, runFactsFragment, withinSignedTouches, type ReviewFileRow, type ReviewPriority, type ReviewQueueFacts } from "./server/render-results.js";
10
+ export { settingsGroups, settingsTiles } from "./server/render-settings.js";
11
+ export { decisionAnswerScript, earlierVersionsWords, revisionLineageWords, type RouteView } from "./server/render-tasks.js";
12
+ export { SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH, type DecisionServer } from "./server/session.js";
package/dist/serve.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { html, htmlString, jsonScript, textHtml } from "./html.js";
1
2
  import { browserAssetsAvailable, browserWorkspaceDocument } from './browser-shell.js';
2
3
  import { browserCrewFromIndex, browserCrewOf, browserNavigationOf, browserProjectsOf, needsYouLabelOf } from './browser-workspace.js';
3
4
  import { sharedActionPayload } from './chat-actions.js';
@@ -111,11 +112,18 @@ import { isAlive as runnerAlive } from "./runner.js";
111
112
  import { freshIdentitySignIn } from "./scope.js";
112
113
  import { instrumentRequest } from "./server-telemetry.js";
113
114
  import { createChatHandlers } from './server/chat.js';
115
+ import { createLiveHandlers } from './server/live.js';
114
116
  import { createFlowsHandlers } from './server/flows.js';
115
117
  import { createPagesHandlers } from './server/pages.js';
116
118
  import { createPeopleHandlers } from './server/people-tokens.js';
117
119
  import { createSettingsHandlers } from './server/settings.js';
118
- import { BODY_CAP, chromeScript, decisionsFor, DEMO_BANNER, DEMO_BANNER_SHORT, escape, focusDocument, form, KBD_HELP, LEAD_BY_DEFAULT_FACT, loginHref, matchTaskPath, mateBrowserMessages, mateChatVersion, NO_PROJECT, NO_TOUCH_FRAGMENTS, NONCE_CAP, NONCE_TTL_MS, page, PersistentSessions, pinnedTheme, projectChatHref, QUEUE_VIEW, redactedPath, redirect, refuse, requestContext, respond, safeReturn, screen, SENSITIVE_INPUT, SESSION_ABSOLUTE_MS, SESSION_IDLE_MS, shell, SHUTDOWN_WAIT_MS, sidebarScript, SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH, ssoStepUps, TASK_FORM_BODY_CAP, taskChatHref, teamProposalCardParts, wrongHostPage } from "./server/shared.js";
120
+ import { requestContext } from "./server/request-context.js";
121
+ import { chromeScript, DEMO_BANNER, DEMO_BANNER_SHORT, focusDocument, KBD_HELP, pinnedTheme, QUEUE_VIEW, refuse, screen, SENSITIVE_INPUT, shell, sidebarScript } from "./server/chrome.js";
122
+ import { BODY_CAP, form, loginHref, matchTaskPath, page, projectChatHref, redactedPath, redirect, respond, safeReturn, SHUTDOWN_WAIT_MS, taskChatHref } from "./server/http.js";
123
+ import { decisionsFor, LEAD_BY_DEFAULT_FACT, mateBrowserMessages, mateChatVersion, teamProposalCardParts } from "./server/render-chat.js";
124
+ import {} from "./server/render-pages.js";
125
+ import { ssoStepUps, wrongHostPage } from "./server/render-people.js";
126
+ import { NO_PROJECT, NO_TOUCH_FRAGMENTS, NONCE_CAP, NONCE_TTL_MS, PersistentSessions, SESSION_ABSOLUTE_MS, SESSION_IDLE_MS, SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH } from "./server/session.js";
119
127
  import { createTasksHandlers } from './server/tasks.js';
120
128
  import { DEFAULT_GUARD_POLICY, passwordGuardOf, provenPasswordAccount, SourceBudget, withPasswordSource } from "./sign-in-guard.js";
121
129
  import { sourceKey } from "./source-key.js";
@@ -421,9 +429,8 @@ export function createDecisionServer(options) {
421
429
  };
422
430
  };
423
431
  const codingProjects = () => [...new Set([...managedRepos(), ...store.listProjects().map(project => project.path)])].filter(repo => rowVisible(liveCeiling(), repo));
424
- const teamStreams = new Set();
425
432
  /** Open chat streams (live replies), ended when the server closes. */
426
- const chatStreams = new Set();
433
+ let liveTurnStarted = () => { };
427
434
  /** Google sign-ins in progress (v89): each consent visit's state, for 10 minutes. */
428
435
  const googleVisits = new Map();
429
436
  /** One-click connections on their way: the service's sign-in page and back, 15 minutes at most. */
@@ -625,12 +632,13 @@ export function createDecisionServer(options) {
625
632
  }
626
633
  if (edgeRoute === null && isEdgeAddress(url.pathname))
627
634
  return refuseEdge(request, response, url.pathname, wrongMethodEdge);
628
- if (edgeRoute !== null && edgeRoute.domain !== 'people')
635
+ // Live rooms share the browser identity/context below; their adapter carries token limits into each room.
636
+ if (edgeRoute !== null && edgeRoute.domain !== 'people' && edgeRoute.domain !== 'live')
629
637
  return dispatchEdge(edgeRoute, { url, who: null, request, response, method });
630
638
  // Passive polls authenticate without extending the browser session.
631
639
  const fragmentName = url.searchParams.get("fragment");
632
640
  const workspaceRead = method === "GET" && url.searchParams.get('format') === 'workspace';
633
- const touch = !(method === "GET" && (workspaceRead || (fragmentName !== null && NO_TOUCH_FRAGMENTS.has(fragmentName)) || /^\/code\/[a-f0-9]{32}\/state$/.test(url.pathname)));
641
+ const touch = !(method === "GET" && (edgeRoute?.domain === 'live' || workspaceRead || (fragmentName !== null && NO_TOUCH_FRAGMENTS.has(fragmentName)) || /^\/code\/[a-f0-9]{32}\/state$/.test(url.pathname)));
634
642
  const refuseBudget = (admitted) => {
635
643
  if (admitted.ok)
636
644
  return false;
@@ -644,7 +652,7 @@ export function createDecisionServer(options) {
644
652
  if (refuseBudget(admitPasswordSource(request)))
645
653
  return;
646
654
  const refusedToken = {};
647
- const who = identify(request, touch, false, refusedToken);
655
+ const who = identify(request, touch, edgeRoute?.domain === 'live', refusedToken);
648
656
  if (refusedToken.principal !== undefined) {
649
657
  if (refuseBudget(requestBudget.admit(refusedToken.principal.tokenId, "api")))
650
658
  return;
@@ -685,8 +693,9 @@ export function createDecisionServer(options) {
685
693
  // An empty password stands for a fresh identity-provider sign-in, for this person only (v100).
686
694
  if (method === "GET" || method === "POST")
687
695
  return freshIdentitySignIn.run({ actor: requestFacts.sso?.fresh === true ? who.name : null }, () => provenPasswordAccount.run(passwordProof, () => requestContext.run(requestFacts, async () => {
688
- const taskTextForm = url.pathname === "/tasks/add" || /^\/t\/[^/]+\/scope$/.test(url.pathname);
689
- const body = method === "POST" ? await form(request, url.pathname === "/settings/skills/import" ? 2 * 1024 * 1024 : url.pathname === "/flows/import" ? 1024 * 1024 : taskTextForm ? TASK_FORM_BODY_CAP : BODY_CAP) : null;
696
+ if (edgeRoute?.domain === 'live')
697
+ return dispatchEdge(edgeRoute, { url, who, request, response, method });
698
+ const body = method === "POST" ? await form(request, (matchedRoute?.stage === "console" ? matchedRoute.bodyCap : undefined) ?? BODY_CAP) : null;
690
699
  const target = actionTarget(url, who, request, body === null ? null : readForm(body, CONSOLE_FORMS.ledgerTarget));
691
700
  const execute = async () => {
692
701
  // THE ROUTE TABLE (server/route-table.ts): nothing reaches a handler unless a row declares this method and
@@ -717,7 +726,7 @@ export function createDecisionServer(options) {
717
726
  // Authorize every read before considering a conditional response.
718
727
  // Task/result opening and receipt reconciliation always re-read work.
719
728
  const facts = requestContext.getStore();
720
- if (facts?.workspaceRead && who.via === 'cookie' && url.pathname === '/chat' &&
729
+ if (facts?.workspaceRead && who.via === 'cookie' && route.id === 'chat.page' &&
721
730
  !url.searchParams.has('task') && !url.searchParams.has('result') && !url.searchParams.has('request')) {
722
731
  const key = createHash('sha256').update(JSON.stringify([workspaceIncarnation, who.name,
723
732
  who.session.generation, who.session.csrf, who.session.project, who.session.projectRevision,
@@ -819,7 +828,7 @@ export function createDecisionServer(options) {
819
828
  // A project link is a read context, not a session-changing operation.
820
829
  // Prove it against both admission and the known project catalog before
821
830
  // collection queries; unknown/foreign paths reveal no work.
822
- if ((url.pathname === '/work' || url.pathname === '/system') && url.searchParams.has('project')) {
831
+ if ((route.id === 'work' || route.id === 'system') && url.searchParams.has('project')) {
823
832
  const wanted = url.searchParams.get('project') ?? '';
824
833
  if (url.searchParams.getAll('project').length !== 1 || !visible(wanted) ||
825
834
  ![...managedRepos(), ...store.listProjects().map(one => one.path)].includes(wanted)) {
@@ -836,7 +845,7 @@ export function createDecisionServer(options) {
836
845
  // viewing a result never changes the session's selected project, and
837
846
  // the page's project switch keeps showing the person's own choice.
838
847
  const chosenProject = project;
839
- if (url.pathname === "/review" && url.searchParams.has("result")) {
848
+ if (route.id === "review" && url.searchParams.has("result")) {
840
849
  const wanted = url.searchParams.get("result") ?? "";
841
850
  const ref = wanted.length > 0 && wanted.length <= 64 && !hasForbiddenControls(wanted) ? store.lookupRef(wanted) : null;
842
851
  const namedProject = url.searchParams.get("project");
@@ -867,8 +876,8 @@ export function createDecisionServer(options) {
867
876
  // and the all-projects board carry their own read context.
868
877
  const needsProject = who.via === "cookie" && project === null && !unscopedMode &&
869
878
  route.needsProject &&
870
- !(url.pathname === "/review" && url.searchParams.has("result")) &&
871
- !(url.pathname === "/board" && url.searchParams.get("scope") === "all");
879
+ !(route.id === "review" && url.searchParams.has("result")) &&
880
+ !(route.id === "board" && url.searchParams.get("scope") === "all");
872
881
  if (needsProject)
873
882
  return redirect(response, `/projects?return=${encodeURIComponent(safeReturn(url.pathname + url.search))}`);
874
883
  return dispatchConsole(route, { url, who, request, response, route, now, project, chosenProject, posted: new URLSearchParams() });
@@ -1039,7 +1048,7 @@ export function createDecisionServer(options) {
1039
1048
  const rows = mateConversationRows(who, principal, focusTask, now, chatProject);
1040
1049
  return {
1041
1050
  sessionId: session.id, user: session.approver, version: mateChatVersion({ ...rows, focusTask }),
1042
- messages: mateBrowserMessages(rows, who.session.csrf, back, { task: focusTask?.id ?? null, project: chatProject }),
1051
+ messages: mateBrowserMessages(rows, back, { task: focusTask?.id ?? null, project: chatProject }),
1043
1052
  pendingTurnId: rows.pending?.id ?? null, requestId: randomBytes(16).toString("hex"), maxChars: MATE_MESSAGE_MAX_CHARS,
1044
1053
  taskId: focusTask?.id ?? null, resultRunId, project: focusTask === null ? chatProject : null,
1045
1054
  };
@@ -1187,9 +1196,7 @@ export function createDecisionServer(options) {
1187
1196
  }
1188
1197
  const saturated = open.length > 200;
1189
1198
  const safeEntries = restricted() ? entries.filter(one => ["/work", "/work?view=needs-you", "/", "/board", "/flows", "/done", "/review", "/tasks", "/runs", "/projects", "/ledger"].includes(one.href) || one.href.startsWith("/t/")) : entries;
1190
- const json = JSON.stringify(saturated ? [...safeEntries, { label: "… more in the task list", href: "/tasks" }] : safeEntries)
1191
- .replace(/</g, "\\u003c");
1192
- return `<script type="application/json" id="palette-index">${json}</script>`;
1199
+ return jsonScript(saturated ? [...safeEntries, { label: "… more in the task list", href: "/tasks" }] : safeEntries, { id: "palette-index" });
1193
1200
  }
1194
1201
  /**
1195
1202
  * The palette cache (arc 4, finding 6), and its honest contract: a
@@ -1232,13 +1239,13 @@ export function createDecisionServer(options) {
1232
1239
  }
1233
1240
  const teamEntry = requestContext.getStore()?.returnTo;
1234
1241
  if (teamEntry?.startsWith('/chat') && !s.workspace?.team && !teamEntry.includes('task=') && !teamEntry.includes('proposal=') && demoLeadHere() === null) {
1235
- s.body = '<p class="team-entry"><a href="/chat?team=1">Open team chat</a></p>' + s.body;
1242
+ s.body = html `<p class="team-entry"><a href="/chat?team=1">Open team chat</a></p>${s.body}`;
1236
1243
  if (s.workspace?.conversation)
1237
- s.workspace.controlsHtml = '<p><a href="/chat?team=1">Open team chat</a></p>' + (s.workspace.controlsHtml ?? '');
1244
+ s.workspace.controlsHtml = html `<p><a href="/chat?team=1">Open team chat</a></p>${s.workspace.controlsHtml}`;
1238
1245
  }
1239
1246
  const sensitive = s.forceSensitive === true ||
1240
- SENSITIVE_INPUT.test(s.body) ||
1241
- (s.chrome?.listPane !== undefined && SENSITIVE_INPUT.test(s.chrome.listPane));
1247
+ SENSITIVE_INPUT.test(htmlString(s.body)) ||
1248
+ (s.chrome?.listPane !== undefined && SENSITIVE_INPUT.test(htmlString(s.chrome.listPane)));
1242
1249
  const requestFacts = requestContext.getStore();
1243
1250
  if (requestFacts?.browser && s.chrome && !s.forceSensitive && (requestFacts.workspaceRead || browserAssetsAvailable())) {
1244
1251
  const path = new URL(requestFacts.returnTo, 'http://standing-orders.local');
@@ -1248,7 +1255,7 @@ export function createDecisionServer(options) {
1248
1255
  path.searchParams.set('format', 'workspace');
1249
1256
  // The legacy list pane (every task or build as links) duplicates the
1250
1257
  // Crew panel and Tasks page; inside the workspace the page stands alone.
1251
- const pageHtml = s.body;
1258
+ const pageHtml = htmlString(s.body);
1252
1259
  const extras = s.workspace ?? {};
1253
1260
  const notices = [...(extras.notices ?? [])];
1254
1261
  if (s.chrome.modeBanner)
@@ -1315,9 +1322,9 @@ export function createDecisionServer(options) {
1315
1322
  ? { request, received: store.mateRequestReceipt(conversation.sessionId, request) !== null } : null,
1316
1323
  projects: browserProjectsOf(s.chrome.projects ?? []), ...crew,
1317
1324
  conversation, ...(extras.team ? { team: extras.team } : {}), focus: extras.focus ?? null, result: extras.result ?? null,
1318
- catchUpHtml: extras.catchUpHtml ?? '', controlsHtml: extras.controlsHtml ?? '', notices, view: extras.view ?? null,
1325
+ catchUpHtml: extras.catchUpHtml === undefined ? '' : htmlString(extras.catchUpHtml), controlsHtml: extras.controlsHtml === undefined ? '' : htmlString(extras.controlsHtml), notices, view: extras.view ?? null,
1319
1326
  ...(s.chrome.demo ? { demo: { text: DEMO_BANNER, short: DEMO_BANNER_SHORT } } : {}),
1320
- pageHtml: extras.pageHtml === undefined ? (conversation === null ? pageHtml : null) : extras.pageHtml,
1327
+ pageHtml: extras.pageHtml === undefined ? (conversation === null ? pageHtml : null) : extras.pageHtml === null ? null : htmlString(extras.pageHtml),
1321
1328
  navigation: [...browserNavigationOf(currentPath, s.chrome.project, needsYou, tasksProject, s.chrome.inboxLabel), { label: 'Workspace tools', href: '/menu', active: path.pathname === '/menu' }],
1322
1329
  chats,
1323
1330
  ...(s.refreshSeconds === undefined ? {} : { refreshSeconds: Math.max(5, Math.floor(s.refreshSeconds)) }),
@@ -1355,9 +1362,9 @@ export function createDecisionServer(options) {
1355
1362
  const script = functional + (chromeLayer ? chromeScript() + WORKSPACE_MOTION_SCRIPT : sensitiveChrome);
1356
1363
  const nonce = script === "" ? undefined : randomBytes(16).toString("base64");
1357
1364
  const body = chromeLayer
1358
- ? `${s.body}\n${paletteTagCached(s.chrome?.project ?? null)}\n${KBD_HELP}`
1365
+ ? html `${s.body}\n${paletteTagCached(s.chrome?.project ?? null)}\n${KBD_HELP}`
1359
1366
  : s.body;
1360
- const html = shell(s.title, body, {
1367
+ const document = shell(s.title, body, {
1361
1368
  ...(s.chrome === undefined ? {} : { chrome: s.chrome }),
1362
1369
  ...(sensitive ? { sensitive: true } : {}),
1363
1370
  ...(s.chrome !== undefined ? { sidebarToggle: true } : {}),
@@ -1367,7 +1374,7 @@ export function createDecisionServer(options) {
1367
1374
  // Pages that ship the chrome layer keep connect-src (v28 granted it for
1368
1375
  // the since-removed watched-session beat), beside pages whose own
1369
1376
  // functional script polls.
1370
- return page(response, status, html, nonce, s.functional?.fetches === true || chromeLayer || sensitiveChrome !== "");
1377
+ return page(response, status, document, nonce, s.functional?.fetches === true || chromeLayer || sensitiveChrome !== "");
1371
1378
  }
1372
1379
  /** The needs-you count every surface wears, read from this request's own
1373
1380
  * Work counts (one query per request, never a cached earlier one), so a
@@ -1513,41 +1520,30 @@ export function createDecisionServer(options) {
1513
1520
  if (peekCache.size <= PEEK_CACHE_ENTRIES && peekCacheBytes <= PEEK_CACHE_BYTES)
1514
1521
  break;
1515
1522
  peekCache.delete(key);
1516
- peekCacheBytes -= Buffer.byteLength(entry.fragment);
1523
+ peekCacheBytes -= entry.bytes;
1517
1524
  }
1518
1525
  };
1519
1526
  /** One typed sentence inside the region — plain words, no raw errors.
1520
1527
  * `final` marks conditions that cannot heal for this run (finished,
1521
1528
  * superseded, wrong machine): the region poller reads the marker and
1522
1529
  * stops, instead of refetching a dead build every beat forever. */
1523
- const peekSay = (message, final = false) => `<p class="meta"${final ? " data-region-stop" : ""}>${escape(message)}</p>`;
1530
+ const peekSay = (message, final = false) => html `<p class="meta"${final ? html ` data-region-stop` : ""}>${message}</p>`;
1524
1531
  /** The sanitize pipeline (finding 30/35): normalize → mask → escape. */
1525
1532
  const peekName = (path) => {
1526
1533
  const normalized = path.replace(/[\u0000-\u001f\u007f]/g, "");
1527
1534
  const masked = scanForSecrets(normalized).length > 0 ? "[redacted: a credential-shaped name]" : normalized;
1528
- return escape(masked);
1535
+ return textHtml(masked);
1529
1536
  };
1530
1537
  async function handlePost(route, url, who, request, response, posted) {
1531
- const early = () => dispatchConsole(route, { url, who, request, response, route, now: clock(), project: projectOf(who, request) ?? null, chosenProject: projectOf(who, request) ?? null, posted });
1538
+ // Every POST passes the shared mutation guard. Caller, scope, project (including a limited account's posted repo)
1539
+ // and role were admitted by the route table.
1532
1540
  const denied = authorizeMutation(request, who, readForm(posted, CONSOLE_FORMS.mutationGuard));
1533
1541
  if (denied !== null)
1534
1542
  return refuse(response, who, denied.status, denied.message);
1535
- if (["chat.demo", "provider.resume", "settings.updates-dismiss", "settings.updates-checks", "code.act"].includes(route.id))
1536
- return early();
1537
- if (restricted()) {
1538
- const repos = readForm(posted, CONSOLE_FORMS.projectCeiling).getAll("repo");
1539
- if (repos.length > 1 || repos.some(repo => repo.trim() !== "" && !visible(repo.trim())))
1540
- return refuse(response, who, 403, "That project is outside your access.", "/projects");
1541
- }
1542
- // THE CENTRAL VIEWER GATE (modes chain, D2/E2): consequential POSTs
1543
- // require ACTIVE approver standing — cookie and bearer alike — with
1544
- // an EXACT allowlist of session-local acts, never a prefix. /login,
1545
- // /logout and /join answer before this handler.
1546
- if (who.role === "viewer") {
1547
- if (route.viewer !== true) {
1548
- return refuse(response, who, 403, "your login can watch, not act — ask an approver to upgrade you");
1549
- }
1550
- }
1543
+ // Standing can change after a token was minted: a viewer's act stays refused, cookie and bearer alike, except
1544
+ // the row's session-local acts.
1545
+ if (who.role === "viewer" && !route.viewer)
1546
+ return refuse(response, who, 403, "your login can watch, not act — ask an approver to upgrade you");
1551
1547
  const now = clock();
1552
1548
  // Any accepted mutation may change what the inbox owes; the badge
1553
1549
  // re-counts within five seconds either way, this just makes it exact.
@@ -1588,14 +1584,9 @@ export function createDecisionServer(options) {
1588
1584
  requestBudget.flush();
1589
1585
  }
1590
1586
  catch { /* saved again on the close event when it can be */ }
1591
- for (const stream of teamStreams)
1592
- stream.end();
1593
- teamStreams.clear();
1594
- for (const stream of chatStreams)
1595
- stream.end();
1596
- chatStreams.clear();
1597
1587
  flowRooms.close();
1598
1588
  taskRooms.close();
1589
+ liveHandlers.close();
1599
1590
  detachReads();
1600
1591
  liveFollower.close();
1601
1592
  void Promise.all([closeCoding(), bounded(team.close()), bounded(leadMaintenance?.stop()), bounded(reads?.close())]).then(() => closeServer(callback)).catch(error => {
@@ -1657,7 +1648,7 @@ export function createDecisionServer(options) {
1657
1648
  get matePrincipal() { return matePrincipal; },
1658
1649
  get chatScopeOf() { return chatScopeOf; },
1659
1650
  get liveTurns() { return liveTurns; },
1660
- get chatStreams() { return chatStreams; },
1651
+ get liveTurnStarted() { return liveTurnStarted; },
1661
1652
  get mateConversationRows() { return mateConversationRows; },
1662
1653
  get demoLeadHere() { return demoLeadHere; },
1663
1654
  get teamBrowserReply() { return teamBrowserReply; },
@@ -1763,7 +1754,6 @@ export function createDecisionServer(options) {
1763
1754
  get allowedHost() { return allowedHost; },
1764
1755
  get team() { return team; },
1765
1756
  get teamBrowserReply() { return teamBrowserReply; },
1766
- get teamStreams() { return teamStreams; },
1767
1757
  get requestBudget() { return requestBudget; },
1768
1758
  get options() { return options; },
1769
1759
  get coding() { return coding; },
@@ -1781,13 +1771,26 @@ export function createDecisionServer(options) {
1781
1771
  get teamsSourceBudget() { return teamsSourceBudget; },
1782
1772
  get teamsTenantBudget() { return teamsTenantBudget; },
1783
1773
  };
1774
+ const liveHandlers = createLiveHandlers(handlerRuntime, { bus: liveBus, workspaceRevision, chatProjectOf: chatHandlers.chatProjectOf });
1775
+ liveTurnStarted = liveHandlers.turnStarted;
1784
1776
  const remoteHandlers = createRemoteHandlers(remoteRuntime);
1785
1777
  const registry = createHandlerRegistry([
1786
1778
  ...tasksHandlers.registrations, ...flowsHandlers.registrations, ...chatHandlers.registrations,
1787
1779
  ...settingsHandlers.registrations, ...peopleHandlers.registrations, ...pagesHandlers.registrations,
1788
- ...remoteHandlers.registrations,
1780
+ ...remoteHandlers.registrations, ...liveHandlers.registrations,
1789
1781
  ]);
1790
- function dispatchConsole(route, ctx) { return registry.console(route, ctx); }
1782
+ /** The row's role, after admission and before its one handler. */
1783
+ function dispatchConsole(route, ctx) {
1784
+ const { who, response } = ctx;
1785
+ const held = route.role === "any" || ((!route.roleBrowser || who.via === "cookie") &&
1786
+ (route.role === "approver" ? who.role === "approver" : store.isInstanceOperator(who.name)));
1787
+ if (!held) {
1788
+ ctx.request.resume();
1789
+ const refusal = route.roleRefusal;
1790
+ return Promise.resolve("message" in refusal ? refuse(response, who, 403, refusal.message, refusal.back) : respond(response, refusal.status, refusal.type, refusal.body));
1791
+ }
1792
+ return registry.console(route, ctx);
1793
+ }
1791
1794
  async function dispatchEdge(route, ctx) {
1792
1795
  if (route.callers.length === 1 && route.callers[0] === 'cookie' && ctx.request.headers.authorization !== undefined) {
1793
1796
  return respond(ctx.response, 403, 'text/plain; charset=utf-8', 'This address needs a browser sign-in.');
@@ -1801,4 +1804,12 @@ export function createDecisionServer(options) {
1801
1804
  }
1802
1805
  return Object.assign(server, { mintSignInLink, closeCoding });
1803
1806
  }
1804
- export { decisionAnswerScript, diffFileAnchor, earlierVersionsWords, editorFileHref, inboxFingerprints, needActionOf, orderChangedFiles, PAGE_CSS, parseInboxTab, pinnedTheme, plainConclusionOf, queueScript, rankReviewQueue, redactedPath, reviewFilePriority, reviewPriorityOf, revisionLineageWords, runFactsFragment, SENSITIVE_INPUT, settingsGroups, settingsTiles, SHUTDOWN_WAIT_MS, SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH, ssoStepUps, TASK_COMPOSER_MODES, withinSignedTouches, wrongHostPage } from "./server/shared.js";
1807
+ export { PAGE_CSS, pinnedTheme, SENSITIVE_INPUT } from "./server/chrome.js";
1808
+ export { redactedPath, SHUTDOWN_WAIT_MS } from "./server/http.js";
1809
+ export { TASK_COMPOSER_MODES } from "./server/render-chat.js";
1810
+ export { inboxFingerprints, parseInboxTab, queueScript } from "./server/render-pages.js";
1811
+ export { ssoStepUps, wrongHostPage } from "./server/render-people.js";
1812
+ export { diffFileAnchor, editorFileHref, needActionOf, orderChangedFiles, plainConclusionOf, rankReviewQueue, reviewFilePriority, reviewPriorityOf, runFactsFragment, withinSignedTouches } from "./server/render-results.js";
1813
+ export { settingsGroups, settingsTiles } from "./server/render-settings.js";
1814
+ export { decisionAnswerScript, earlierVersionsWords, revisionLineageWords } from "./server/render-tasks.js";
1815
+ export { SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH } from "./server/session.js";
@@ -1,17 +1,14 @@
1
- import type { Registration } from './handler-registry.js';
2
1
  import { type BrowserFirstRun, type BrowserHome, type BrowserPhoneCard } from '../browser-workspace.js';
3
2
  import { type DemoResultView } from "../demo-chat.js";
4
3
  import type { DemoExchange } from "../demo.js";
5
4
  import { type FirstTaskSuggestion } from "../first-run.js";
6
5
  import type { MateProgress } from "../mate-progress.js";
7
6
  import type { DirectChatProviderId } from "../store.js";
8
- import type { HandlerContext } from './handler-context.js';
9
7
  import type { ServerRuntime } from './runtime.js';
10
- import { type ChatEnablement, type Session, type Who } from "./shared.js";
8
+ import { type ChatEnablement } from "./render-chat.js";
9
+ import { type Session, type Who } from "./session.js";
11
10
  export declare function createChatHandlers(runtime: ServerRuntime): {
12
- registrations: Registration[];
13
- get: (ctx: HandlerContext) => Promise<void>;
14
- post: (ctx: HandlerContext) => Promise<void>;
11
+ registrations: import("./handler-registry.js").Registration[];
15
12
  chatFirstRun: (now: Date, project: string | null) => BrowserFirstRun | undefined;
16
13
  phoneCard: (who: Who & {
17
14
  via: "cookie";