toolroll 0.9.96 → 0.9.98

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 (131) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/approval-authority.d.ts +10 -1
  3. package/dist/approval-authority.js +18 -2
  4. package/dist/browser/workspace.css +2 -2
  5. package/dist/browser/workspace.js +32 -32
  6. package/dist/browser-workspace.d.ts +6 -3
  7. package/dist/chat-actions.d.ts +40 -5
  8. package/dist/chat-actions.js +124 -13
  9. package/dist/chat-card.js +4 -1
  10. package/dist/chat-channel.d.ts +3 -0
  11. package/dist/chat-channel.js +24 -7
  12. package/dist/chat-decide.js +4 -2
  13. package/dist/chat-delivery.d.ts +5 -1
  14. package/dist/chat-delivery.js +74 -25
  15. package/dist/chat-follow.d.ts +6 -3
  16. package/dist/chat-follow.js +23 -7
  17. package/dist/chat-live-schema.d.ts +17 -0
  18. package/dist/chat-live-schema.js +32 -0
  19. package/dist/chat-live.d.ts +161 -0
  20. package/dist/chat-live.js +491 -0
  21. package/dist/chat-task-actions.js +1 -1
  22. package/dist/chat-terms.d.ts +8 -0
  23. package/dist/chat-terms.js +14 -0
  24. package/dist/chat-voice.js +1 -1
  25. package/dist/contracts/chat-actions.d.ts +6 -0
  26. package/dist/contracts/chat-actions.js +2 -0
  27. package/dist/contracts/cli.d.ts +6 -0
  28. package/dist/contracts/cli.js +3 -0
  29. package/dist/contracts/console-api.d.ts +1 -0
  30. package/dist/contracts/console-api.js +1 -0
  31. package/dist/contracts/lead-tools.d.ts +17 -0
  32. package/dist/contracts/lead-tools.js +4 -1
  33. package/dist/contracts/store-json.d.ts +1 -0
  34. package/dist/discord-api.d.ts +3 -1
  35. package/dist/discord-api.js +9 -3
  36. package/dist/discord-chat.d.ts +13 -3
  37. package/dist/discord-chat.js +77 -21
  38. package/dist/discord.js +8 -1
  39. package/dist/fast-start.d.ts +83 -6
  40. package/dist/fast-start.js +112 -15
  41. package/dist/home-groups.d.ts +39 -0
  42. package/dist/home-groups.js +33 -0
  43. package/dist/lead-answer-eval.d.ts +93 -0
  44. package/dist/lead-answer-eval.js +259 -0
  45. package/dist/lead-capabilities.js +1 -1
  46. package/dist/lead-cli-live.d.ts +56 -0
  47. package/dist/lead-cli-live.js +200 -0
  48. package/dist/lead-cli.d.ts +3 -0
  49. package/dist/lead-cli.js +34 -4
  50. package/dist/lead-context.js +2 -2
  51. package/dist/lead-contract.d.ts +1 -1
  52. package/dist/lead-contract.js +10 -7
  53. package/dist/lead-doors.d.ts +32 -0
  54. package/dist/lead-doors.js +80 -14
  55. package/dist/lead-latency-schema.d.ts +24 -0
  56. package/dist/lead-latency-schema.js +36 -0
  57. package/dist/lead-progress.d.ts +10 -1
  58. package/dist/lead-progress.js +1 -1
  59. package/dist/lead-queue-runtime.d.ts +1 -1
  60. package/dist/lead-queue-runtime.js +1 -1
  61. package/dist/lead-route.d.ts +56 -0
  62. package/dist/lead-route.js +104 -0
  63. package/dist/lead-status.d.ts +3 -0
  64. package/dist/lead-status.js +8 -3
  65. package/dist/lead-tools.d.ts +20 -0
  66. package/dist/lead-tools.js +66 -3
  67. package/dist/lead-voice.js +1 -1
  68. package/dist/lead.d.ts +4 -2
  69. package/dist/lead.js +97 -28
  70. package/dist/live-reply.d.ts +83 -0
  71. package/dist/live-reply.js +302 -0
  72. package/dist/local-owner.js +2 -0
  73. package/dist/one-step-chat-schema.d.ts +22 -0
  74. package/dist/one-step-chat-schema.js +37 -0
  75. package/dist/operate.d.ts +27 -1
  76. package/dist/operate.js +245 -33
  77. package/dist/project-delete.js +9 -0
  78. package/dist/project-knowledge.d.ts +10 -10
  79. package/dist/publish.d.ts +2 -0
  80. package/dist/publish.js +1 -1
  81. package/dist/pull-request-flow.d.ts +4 -0
  82. package/dist/pull-request-flow.js +16 -1
  83. package/dist/reply-shape.d.ts +9 -0
  84. package/dist/reply-shape.js +13 -0
  85. package/dist/reply-voice.d.ts +23 -0
  86. package/dist/reply-voice.js +37 -0
  87. package/dist/scope.d.ts +6 -1
  88. package/dist/scope.js +3 -2
  89. package/dist/serve.js +11 -4
  90. package/dist/server/chat.d.ts +2 -1
  91. package/dist/server/chat.js +22 -10
  92. package/dist/server/chrome.js +2 -2
  93. package/dist/server/home.d.ts +4 -3
  94. package/dist/server/home.js +33 -28
  95. package/dist/server/live.js +18 -21
  96. package/dist/server/render-chat.d.ts +5 -0
  97. package/dist/server/render-chat.js +15 -8
  98. package/dist/server/render-pages.js +2 -2
  99. package/dist/server/render-settings.d.ts +8 -0
  100. package/dist/server/render-settings.js +13 -2
  101. package/dist/server/route-table.js +2 -0
  102. package/dist/server/settings.js +14 -1
  103. package/dist/setup-guide.d.ts +2 -2
  104. package/dist/slack-api.js +11 -0
  105. package/dist/slack-chat.d.ts +11 -1
  106. package/dist/slack-chat.js +50 -5
  107. package/dist/slack-settings.js +8 -1
  108. package/dist/slack.js +10 -3
  109. package/dist/store.d.ts +33 -2
  110. package/dist/store.js +92 -16
  111. package/dist/subscription-chat.d.ts +2 -0
  112. package/dist/subscription-chat.js +3 -1
  113. package/dist/teams-chat.d.ts +9 -2
  114. package/dist/teams-chat.js +50 -2
  115. package/dist/teams.js +5 -2
  116. package/dist/telegram-lead.d.ts +3 -0
  117. package/dist/telegram-lead.js +132 -14
  118. package/dist/telegram-live.d.ts +116 -0
  119. package/dist/telegram-live.js +224 -0
  120. package/dist/telegram-status.js +9 -4
  121. package/dist/telegram.d.ts +12 -0
  122. package/dist/telegram.js +142 -10
  123. package/dist/toolroll-update.js +6 -1
  124. package/dist/tui-contract.d.ts +2 -0
  125. package/dist/tui-source.js +3 -0
  126. package/dist/tui.js +41 -41
  127. package/dist/typed-start.d.ts +51 -0
  128. package/dist/typed-start.js +98 -0
  129. package/dist/wake-hub.d.ts +72 -0
  130. package/dist/wake-hub.js +151 -0
  131. package/package.json +1 -1
@@ -396,7 +396,7 @@ export function chatLimitsHtml(facts) {
396
396
  html `<span>${facts.turnsToday} / ${facts.dailyTurns} turns today</span>`,
397
397
  ];
398
398
  if (facts.subscription)
399
- rows.push(html `<span>membership login · no dollar ceiling</span>`);
399
+ rows.push(html `<span>membership login · no dollar limit</span>`);
400
400
  else if (facts.weekly !== null)
401
401
  rows.push(html `<span>this week ${chatMoney(facts.weekly.spent)} of ${chatMoney(facts.weekly.ceiling)}</span>`);
402
402
  return html `<details class="chat-limits"><summary>Model &amp; limits<span class="meta">${facts.subscription ? "membership" : facts.provider}</span></summary><div class="chat-budget">${rows}</div></details>`;
@@ -564,7 +564,7 @@ export function proposalCardParts(view, inert, decision, returnTo = null) {
564
564
  hold: { label: "Pause work", action: "hold", icon: html `<rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/>` },
565
565
  unhold: { label: "Resume work", action: "release hold", icon: html `<path d="m7 4 13 8-13 8z"/>` },
566
566
  steer: { label: "Guidance for the agent", action: "add guidance", icon: html `<path d="M5 12h14"/><path d="m13 6 6 6-6 6"/>` },
567
- scope: { label: "Scope revision", action: "save scope", icon: html `<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L8 18l-4 1 1-4z"/>` },
567
+ scope: { label: "Plan change", action: "save plan", icon: html `<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L8 18l-4 1 1-4z"/>` },
568
568
  answer: { label: "Decision answer", action: "confirm answer", icon: html `<path d="M9.1 9a3 3 0 1 1 5.8 1c0 2-3 2-3 4"/><path d="M12 18h.01"/><circle cx="12" cy="12" r="9"/>` },
569
569
  cancel: { label: "Cancel task", action: "open task", icon: html `<path d="m15 9-6 6"/><path d="m9 9 6 6"/><circle cx="12" cy="12" r="9"/>` },
570
570
  repair: {
@@ -599,7 +599,8 @@ export function proposalCardParts(view, inert, decision, returnTo = null) {
599
599
  }
600
600
  else if (view.kind === "action") {
601
601
  const action = sharedActionPayload(payload);
602
- what = action === null ? html `<p>This action is unavailable.</p>` : (sharedActionNeedsReview(action) ? html `` : html `${action.terms.map(term => html `<p style="white-space:pre-wrap;overflow-wrap:anywhere">${term}</p>`)}`);
602
+ // A Start card (one-step-chat.md C2) names what starts and where; its whole terms and the confirm are on the review page.
603
+ what = action === null ? html `<p>This action is unavailable.</p>` : action.operation === "task_start" ? html `<h3>${action.title}</h3>${facts(["project", mono(action.repo.split(/[\\/]/).filter(Boolean).at(-1) ?? action.repo)])}` : (sharedActionNeedsReview(action) ? html `` : html `${action.terms.map(term => html `<p style="white-space:pre-wrap;overflow-wrap:anywhere">${term}</p>`)}`);
603
604
  }
604
605
  else if (view.kind === "task_action") {
605
606
  const operation = payload["operation"];
@@ -723,7 +724,7 @@ export function proposalCardParts(view, inert, decision, returnTo = null) {
723
724
  if (view.state === "pending" && !inert) {
724
725
  acts =
725
726
  view.kind === "action" && sharedActionPayload(payload) !== null && sharedActionNeedsReview(sharedActionPayload(payload))
726
- ? html `<a class="button-link" href="${sharedActionReviewPath(view.id)}">Review action</a>${dismissForm(html `<button class="quiet">Dismiss</button>`)}`
727
+ ? html `<a class="button-link" href="${sharedActionReviewPath(view.id)}">${reviewWords(sharedActionPayload(payload))}</a>${dismissForm(html `<button class="quiet">Dismiss</button>`)}`
727
728
  : view.kind === "control"
728
729
  ? (isChatControl(payload["control"]) ? html `<a class="button-link" href="${chatControlHref(payload["control"], task, payload["run"], payload["project"])}">${CHAT_CONTROLS[payload["control"]].label}</a>` : html `<p>Control unavailable.</p>`)
729
730
  : view.kind === "cancel"
@@ -758,7 +759,7 @@ export function proposalCardParts(view, inert, decision, returnTo = null) {
758
759
  const sentence = (words) => words.charAt(0).toUpperCase() + words.slice(1);
759
760
  const pending = view.state === "pending" && !inert;
760
761
  const primary = !pending ? null
761
- : reviewAction !== null && sharedActionNeedsReview(reviewAction) ? { kind: "link", label: "Review action", href: sharedActionReviewPath(view.id) }
762
+ : reviewAction !== null && sharedActionNeedsReview(reviewAction) ? { kind: "link", label: reviewWords(reviewAction), href: sharedActionReviewPath(view.id) }
762
763
  : view.kind === "control" ? (isChatControl(payload["control"]) ? { kind: "link", label: CHAT_CONTROLS[payload["control"]].label, href: chatControlHref(payload["control"], task, payload["run"], payload["project"]) } : null)
763
764
  : view.kind === "cancel" ? { kind: "link", label: "Open task", href: taskHref(task) }
764
765
  : { kind: "confirm", label: sentence(presentation.action), irreversible, native: view.kind === "task_action" && payload["operation"] === "resume" };
@@ -777,6 +778,10 @@ export function proposalCardParts(view, inert, decision, returnTo = null) {
777
778
  };
778
779
  return { html: cardHtml, card };
779
780
  }
781
+ /** The review link's words: a Start card's is "Review and start". */
782
+ function reviewWords(action) {
783
+ return action?.operation === "task_start" ? "Review and start" : "Review action";
784
+ }
780
785
  export function leadProposalCard(proposal, inert, decision, returnTo = null) {
781
786
  return leadProposalCardParts(proposal, inert, decision, returnTo).html;
782
787
  }
@@ -825,7 +830,7 @@ export function replacedThreadHtml(previous) {
825
830
  * A div, not a p: the Remove form can't sit inside a paragraph (the parser would close it first). */
826
831
  function queuedStateHtml(row, back) {
827
832
  if (row.status === "queued") {
828
- const waiting = row.error?.startsWith("Waiting: ") === true ? row.error.slice("Waiting: ".length) : "It goes to the lead when it finishes.";
833
+ const waiting = row.error?.startsWith("Waiting: ") === true ? row.error.slice("Waiting: ".length) : "Waiting for your earlier messages…";
829
834
  return html `<div class="so-queued" data-queued="${row.message}" data-queue-state="queued"><span class="so-queued-state">Queued</span> · ${waiting}${postForm(`/chat/queued/${row.message}/remove`, html `<button type="submit" class="so-queued-remove quiet">Remove</button>`, { attrs: { class: "inline" }, ...(back === null ? {} : { returnTo: back }) })}</div>`;
830
835
  }
831
836
  if (row.status === "running" && row.turnId === null)
@@ -949,6 +954,8 @@ export function leadThreadHtml(data) {
949
954
  const ask = message.turn === null ? undefined : data.asks?.get(message.turn);
950
955
  thread.push(html `<div class="msg mate" data-message-role="assistant" data-key="m${message.id}"${message.id === latestReply ? html ` id="latest"` : ""}>${renderChatText(message.text, asked)}${ask === undefined ? "" : leadAskHtml(ask, { task: data.focusTask?.id ?? null, project: data.chatProject ?? null }, "chat-message", data.pending === null && data.ask?.turn === ask.turn)}${cards.map(one => leadProposalCard(one, inert, data.decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, data.focusTask === null ? null : returnTo))}<div class="chat-message-foot">${chatActivity(message.activity)}<time datetime="${message.createdAt}">${relativeAge(message.createdAt, data.now)}</time></div></div>`);
951
956
  }
957
+ if (data.pending === null && data.failure)
958
+ thread.push(html `<div class="problem" data-key="failed" data-lead-failed role="alert">${data.failure}</div>`);
952
959
  parts.push(html `<div class="thread" data-key="thread" data-chat-list>\n${joinHtml(thread, "\n")}\n</div>`);
953
960
  if (data.pending !== null) {
954
961
  const subscription = data.pending.reservedMicrousd === 0;
@@ -979,7 +986,7 @@ export function leadPage(chrome, data) {
979
986
  data.session === null ? "" : html `<details class="lead-follow"><summary>Automatic crew updates${data.follow?.enabled ? ' · On' : ''}</summary><p class="meta">${data.follow?.detail ?? 'Automatic crew updates are off.'}</p>${postForm("/chat/mate/follow", html `${data.follow?.enabled ? '' : html `<p>The lead responds when results or decisions arrive. Uses this conversation’s ${subscription ? 'membership usage, with no dollar maximum' : 'remaining spend allowance'} and daily turn limit. Existing task approvals still apply.</p>`}<button type="submit" class="quiet">${data.follow?.enabled ? 'Pause updates' : 'Enable updates'}</button>`, { returnTo, hidden: { enabled: data.follow?.enabled ? 'no' : 'yes' } })}</details>`,
980
987
  html `<details class="chat-limits chat-session-details"><summary>Conversation details<span class="meta">${subscription ? "membership" : data.config.provider}</span></summary>\n${joinHtml([
981
988
  html `<div class="chat-budget"><span class="mono">${data.config.provider} · ${data.config.model}</span><span>${data.turnsToday} / ${data.config.dailyTurns} turns today</span>${subscription
982
- ? html `<span>membership login · no dollar ceiling</span>`
989
+ ? html `<span>membership login · no dollar limit</span>`
983
990
  : html `<span>this conversation: ${chatMoney(data.session?.spentMicrousd ?? 0)} of ${chatMoney(data.session?.ceilingMicrousd ?? 0)}</span><span>this week ${chatMoney(data.weeklySpent)} of ${chatMoney(data.config.weeklyCeilingMicrousd)}</span>`}</div>`,
984
991
  data.session === null
985
992
  ? html `<p class="meta">No conversation yet. Send a message to start one.</p>`
@@ -1033,7 +1040,7 @@ export function leadPage(chrome, data) {
1033
1040
  sessionId, user: approver, version: leadChatVersion(data),
1034
1041
  messages: leadBrowserMessages(data, data.focusTask === null && chatProject === null ? null : returnTo, { task: data.focusTask?.id ?? null, project: chatProject }),
1035
1042
  pendingTurnId: data.pending?.id ?? null, requestId: randomBytes(16).toString('hex'), maxChars: LEAD_MESSAGE_MAX_CHARS,
1036
- taskId: data.focusTask?.id ?? null, resultRunId: data.resultRunId ?? null, project: chatProject,
1043
+ taskId: data.focusTask?.id ?? null, resultRunId: data.resultRunId ?? null, project: chatProject, failure: data.pending === null ? data.failure ?? null : null,
1037
1044
  ...(data.previous == null ? {} : { previous: { messages: replacedBrowserMessages(data.previous) } }),
1038
1045
  },
1039
1046
  focus: data.focusTask === null ? null : { id: data.focusTask.id, title: data.focusTask.title,
@@ -439,7 +439,7 @@ export function workPage(chrome, data) {
439
439
  ['finished', 'Finished', data.board.finished, 'completed', 'Nothing has finished yet.'],
440
440
  ].map(([key, label, page, view, empty]) => ({ key, label, count: page.totals[view], href: query({ view, layout: 'list' }), empty, rows: page.items.map(rowOf) }));
441
441
  const putAwayLink = data.canPutAway === true && data.view !== 'archived' ? { label: 'Archive old tasks', href: '/tasks/archive' } : null;
442
- const toolLinks = [['/tasks?view=order', 'Order the queue'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/tasks?scope=all', 'Every project']]), ['/ledger', 'Action ledger']];
442
+ const toolLinks = [['/tasks?view=order', 'Order the queue'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/tasks?scope=all', 'Every project']]), ['/ledger', 'Audit log']];
443
443
  const newTaskHref = lens === null ? '/tasks/new' : `/tasks/new?project=${encodeURIComponent(lens)}`;
444
444
  const emptyNow = data.work.items.length > 0 ? null : filtered && !data.previous ? { text: `No tasks are ${data.state} in this view.`, action: { label: 'Show every state', href: href(data.view, undefined, null) } } : {
445
445
  text: data.previous ? 'There are no more tasks on this page.' : current.empty,
@@ -496,7 +496,7 @@ export function workPage(chrome, data) {
496
496
  ? html `<div class="work-empty" data-work-empty="${data.view}"><p>${data.previous ? 'There are no more tasks on this page.' : current.empty}</p>${data.view === 'all' && !data.previous ? html `<p><a class="button-link" href="${chrome.chat === true ? '/chat' : '/tasks/new'}">${chrome.chat === true ? 'Start in chat' : 'Add a task'}</a></p>` : html `<a href="${href('all')}">See all tasks →</a>`}</div>`
497
497
  : html `<div class="work-list">${data.work.items.map(rowHtml)}</div>`;
498
498
  const pages = !data.previous && data.work.nextCursor === null ? null : html `<nav class="row work-pagination" aria-label="Task pages">${data.previous ? html `<a class="button-link" href="${href(data.view)}">First page</a>` : null}${data.work.nextCursor === null ? null : html `<a class="button-link" rel="next" href="${href(data.view, data.work.nextCursor)}">Next page</a>`}</nav>`;
499
- const tools = html `<details class="work-tools"><summary>Work tools${CHEVRON_ICON}</summary><nav class="work-tools-menu">${[['/', 'Home'], ['/tasks?view=board', 'Board'], ['/tasks?view=order', 'Order'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/tasks?scope=all', 'Every project']]), ['/ledger', 'Action ledger']]
499
+ const tools = html `<details class="work-tools"><summary>Work tools${CHEVRON_ICON}</summary><nav class="work-tools-menu">${[['/', 'Home'], ['/tasks?view=board', 'Board'], ['/tasks?view=order', 'Order'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/tasks?scope=all', 'Every project']]), ['/ledger', 'Audit log']]
500
500
  .map(([path, label]) => html `<a href="${path}">${label}</a>`)}</nav></details>`;
501
501
  return screen('work', html `<div class="work-head"><h1>Tasks</h1>${putAwayLink === null ? null : html `<a class="button-link" href="${putAwayLink.href}" data-put-away-old>${putAwayLink.label}</a>`}${tools}</div>${limitsHtml(data.limits ?? null)}${legacyTabs}${stateLinks}${list}${pages}`, { chrome, workspace: { view } });
502
502
  }
@@ -41,6 +41,14 @@ export declare function leadSettingsHtml(data: {
41
41
  when: string;
42
42
  until: string;
43
43
  }[];
44
+ /** Quick answers (responsive-chat.md §8): whether they're on, the faster model's name, whether this person may change
45
+ * them (an instance operator), and whether the last save was this one. Absent: not shown. */
46
+ quick?: {
47
+ on: boolean;
48
+ model: string;
49
+ canChange: boolean;
50
+ saved: boolean;
51
+ };
44
52
  /** D5: the lead's subagents in the projects this person can use: name and role, project, and whether it's paused. */
45
53
  subagents?: {
46
54
  id: number;
@@ -11,7 +11,7 @@ import {} from "../telegram.js";
11
11
  import { chatMoney, screen, when } from "./chrome.js";
12
12
  import { taskHref } from "./http.js";
13
13
  import { html, joinHtml, postForm } from "../html.js";
14
- import { acts, btn, chip, codeLine, disclose, facts, formRow, input, note, notice, select, settingsLink, settingsRecord, settingsSection, textarea } from "./kit.js";
14
+ import { acts, btn, chip, codeLine, disclose, facts, formRow, input, note, notice, select, settingsLink, settingsRecord, settingsSection, switchInput, textarea } from "./kit.js";
15
15
  import { kitSectionForm, SECRET_OPT_OUTS } from "../settings-kit.js";
16
16
  import { settingsSubPage } from "../settings-groups-ui.js";
17
17
  /** Settings → Lead: one line for what runs the lead and one action; the full form, turning it off and stored keys under Advanced. */
@@ -43,6 +43,17 @@ export function leadSettingsHtml(data) {
43
43
  attrs: { maxlength: ABOUT_YOU_MAX_LINES * (ABOUT_YOU_LINE_MAX + 1), placeholder: "Keep copy terse.\nI test changes myself.", "aria-label": "What your lead knows about you" } }), "top"),
44
44
  html `<div class="k-ssave">${savedLine(data.aboutSaved === true)}${btn("Save", { sm: true, type: "submit" })}</div>`,
45
45
  ]), { attrs: { "data-lead-about": true } }) });
46
+ // Quick answers: plain status questions go to a faster model; anything that asks for a change stays on the main one.
47
+ const quick = data.quick === undefined || config === null ? "" : settingsSection({ id: "lead-quick", title: "Quick answers",
48
+ lead: config.provider === "claude-subscription"
49
+ ? `Short questions about what's going on, like "what's running?", are answered by ${data.quick.model}, a faster model. Anything that asks for a change still goes to the lead's main model.`
50
+ : "Short questions about what's going on can go to a faster model. This works only when the lead uses the Anthropic membership.",
51
+ body: data.quick.canChange
52
+ ? postForm("/settings/lead/quick", joinHtml([
53
+ switchInput({ id: "lead-quick", name: "enabled", label: "Quick answers use a faster model", on: data.quick.on }),
54
+ html `<div class="k-ssave">${savedLine(data.quick.saved)}${btn("Save", { sm: true, type: "submit" })}</div>`,
55
+ ]), { attrs: { "data-lead-quick": true } })
56
+ : note(data.quick.on ? "On." : "Off.") });
46
57
  // What the lead promised to follow up on; it reports each once, here in chat, and drops it after 7 days.
47
58
  const promises = (data.promises ?? []).length === 0 ? "" : settingsSection({ id: "lead-promises", title: "Promises", lead: "What the lead said it would follow up on. Each is reported once, then dropped after 7 days.",
48
59
  body: joinHtml(data.promises.map(one => settingsRecord({
@@ -69,7 +80,7 @@ export function leadSettingsHtml(data) {
69
80
  save: { label: "Turn the lead off", variant: "danger" }, confirm: "Turn the lead off", body: note("The lead stops answering until you turn it on again.") }) });
70
81
  return settingsSubPage("agents", "Lead", joinHtml([
71
82
  data.problem ? notice(data.problem.charAt(0).toUpperCase() + data.problem.slice(1)) : data.said === null ? "" : html `<p class="k-notice info" role="status">${data.said}</p>`,
72
- runs, identity, about, promises, subagents,
83
+ runs, identity, about, quick, promises, subagents,
73
84
  disclose("Advanced", joinHtml([leadConfigForm(config, facts), keys, off])),
74
85
  ], "\n"));
75
86
  }
@@ -619,6 +619,8 @@ post("chat", "onboarding.phone-dismiss", "/onboarding/phone/dismiss", approver("
619
619
  const LEAD_WORDS = { identity: "An approver names their lead.", about: "An approver edits what their lead knows about them.", "promise/cancel": "An approver manages the lead's promises.", on: "An approver turns the lead on." };
620
620
  for (const one of ["identity", "about", "promise/cancel", "on"])
621
621
  post("settings", `settings.lead-${one.replaceAll("/", "-")}`, `/settings/lead/${one}`, approver(LEAD_WORDS[one], "/settings/lead"));
622
+ // Quick answers is installation-wide: which model answers everyone's plain questions (responsive-chat.md §8).
623
+ post("settings", "settings.lead-quick", "/settings/lead/quick", { callers: ["cookie"], ...operator("An instance operator chooses how the lead answers.", "/settings/lead") });
622
624
  post("chat", "chat.config", "/chat/config", { scope: "step-up" });
623
625
  for (const one of ["mint", "follow", "end", "stop"])
624
626
  post("chat", `chat.mate-${one}`, `/chat/mate/${one}`, { callers: ["cookie"], ...(one === "mint" ? { scope: "step-up" } : {}) });
@@ -76,6 +76,7 @@ import { cleanUpHtml, decisionsHtml, framedSection, knowledgeHtml, MEMORY_INTRO,
76
76
  import { aboutYouOf, checkAboutYou, saveAboutYou } from "../lead-about.js";
77
77
  import { cancelCommitment, conditionWords, openCommitments } from '../lead-commitments.js';
78
78
  import { checkLeadIdentity, leadIdentityOf } from "../lead-identity.js";
79
+ import { modelName as leadModelName, quickLeadModel } from "../lead-route.js";
79
80
  import { mailboxAccess, readThroughImap } from "../mailbox.js";
80
81
  import { startConnect } from "../mcp-connect.js";
81
82
  import { decideProposal, defaultTidyAnalyzer, listProposals, memoryStatus } from "../memory-pass.js";
@@ -122,6 +123,7 @@ import { addProjectInstructions, ASSISTANTS, detectPreparation, modelChoices, pr
122
123
  import { skillsHtml, skillsScript } from "../skills-ui.js";
123
124
  import { checkSlackCredentials, clearSlackCredentials, loadSlackCredentials, saveSlackCredentials, SLACK_MANIFEST, SlackError } from "../slack-api.js";
124
125
  import { slackSettingsHtml } from "../slack-settings.js";
126
+ import { ACK_REFUSED_META } from "../chat-live.js";
125
127
  import { spendHtml, spendScript } from "../spend-ui.js";
126
128
  import { approvalFactsNow } from "./kit.js";
127
129
  import { spendReport, spendReportCsv, spendRows } from "../spend-report.js";
@@ -135,7 +137,7 @@ import { sweepSchedule } from "../storage-sweep-schedule.js";
135
137
  import { storageHtml } from "../storage-ui.js";
136
138
  import { landedSummary } from "../landed-cleanup.js";
137
139
  import { bytesWords, parseCleanup, parseIdle } from "../storage.js";
138
- import { checkoutIdle, isDigestTime, liveBackupState, setCheckoutIdle, localModeOf, RESULT_SCREENSHOTS, setLiveBackupSettings, setLocalModeOff } from "../store.js";
140
+ import { checkoutIdle, isDigestTime, leadQuickAnswersOn, liveBackupState, setCheckoutIdle, localModeOf, RESULT_SCREENSHOTS, setLeadQuickAnswers, setLiveBackupSettings, setLocalModeOff } from "../store.js";
139
141
  import { defaultLiveFolder, liveFolderOf, liveFolderProblem } from "../live-backup.js";
140
142
  import { LIVE_EVERY } from "../backup-ui.js";
141
143
  import { checkTeamsCredentials, clearTeamsCredentials, loadTeamsCredentials, saveTeamsCredentials, TeamsError } from "../teams-api.js";
@@ -673,6 +675,7 @@ ${restoreHtml(chosen, retired, undoableMerges(store, chosen), disabledLessons(st
673
675
  return sendScreen(response, 200, screen("Lead", leadSettingsHtml({ config, facts, words: leadWords(), signedIn, command: agentSignInCommand(),
674
676
  said: url.searchParams.get("said"), problem: url.searchParams.get("problem"), saved: url.searchParams.get("saved") === "1", identity: leadIdentityOf(store, who.name), promises,
675
677
  about: aboutYouOf(store, who.name), aboutSaved: url.searchParams.get("saved") === "about",
678
+ quick: { on: leadQuickAnswersOn(store), model: leadModelName(quickLeadModel(store)), canChange: who.role === "approver" && store.isInstanceOperator(who.name), saved: url.searchParams.get("saved") === "quick" },
676
679
  subagents: store.subagents(consoleProjects().filter(repo => store.accountCanAccess(who.name, repo))).map(one => ({ id: one.id, label: labelOf(one), project: projectName(one.repo), paused: one.state === "paused" })) }), { chrome: chromeFor(project, "settings") }));
677
680
  }
678
681
  async function telegramPage(ctx) {
@@ -2005,6 +2008,8 @@ ${restoreHtml(chosen, retired, undoableMerges(store, chosen), disabledLessons(st
2005
2008
  if (credentials || loadSlackCredentials(dir))
2006
2009
  return show("Disconnect the current Slack app before connecting another.", 409);
2007
2010
  saveSlackCredentials(dir, checked);
2011
+ // A fresh connection asks for the 👀 again at once (an app reinstalled with reactions:write).
2012
+ state.deleteMeta(checked.installation, ACK_REFUSED_META);
2008
2013
  }
2009
2014
  catch (error) {
2010
2015
  return show(error instanceof SlackError ? error.message : "Slack could not be connected. Check the tokens and try again.");
@@ -2906,6 +2911,7 @@ ${postForm("/settings/chat-approval/save", html `\
2906
2911
  "mode.revoke": modeRevoke,
2907
2912
  "settings.lead-identity": leadIdentity,
2908
2913
  "settings.lead-about": leadAbout,
2914
+ "settings.lead-quick": leadQuick,
2909
2915
  "settings.lead-promise-cancel": leadPromiseCancel,
2910
2916
  "settings.lead-on": leadOn,
2911
2917
  "settings.code-map": codeMapAct("switch"),
@@ -3223,5 +3229,12 @@ ${postForm("/settings/chat-approval/save", html `\
3223
3229
  });
3224
3230
  return done.ok ? back("said", "Saved. Tasks scoped from now on use it; tasks already approved keep the chain they were approved with.") : back("problem", `Nothing changed: ${done.problem}`);
3225
3231
  }
3232
+ // Settings → Lead: quick answers on or off (installation-wide; the route admits an instance operator only).
3233
+ async function leadQuick(ctx) {
3234
+ const { who, response, now, posted } = ctx;
3235
+ const body = readForm(posted, CONSOLE_FORMS.leadQuick);
3236
+ setLeadQuickAnswers(store, body.get("enabled") === "on", who.name, now);
3237
+ return redirect(response, "/settings/lead?saved=quick");
3238
+ }
3226
3239
  return { registrations, modelsView };
3227
3240
  }
@@ -15,10 +15,10 @@ export declare function detectPreparation(repo: string): {
15
15
  } | null;
16
16
  /** Browser installation may write only this known file inside its project. */
17
17
  export declare function previewProjectInstructions(repo: string): {
18
+ content?: never;
18
19
  ok: false;
19
20
  message: string;
20
21
  plan?: never;
21
- content?: never;
22
22
  fingerprint?: never;
23
23
  installed?: never;
24
24
  } | {
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
30
30
  installed: boolean;
31
31
  };
32
32
  export declare function addProjectInstructions(repo: string, fingerprint: string): {
33
+ content?: never;
33
34
  ok: false;
34
35
  message: string;
35
36
  plan?: never;
36
- content?: never;
37
37
  fingerprint?: never;
38
38
  installed?: never;
39
39
  } | import("./skills.js").InstallResult;
package/dist/slack-api.js CHANGED
@@ -19,6 +19,10 @@ export class SlackError extends ChatDeliveryError {
19
19
  channel_not_found: "The Slack conversation is unavailable. Pair your account again.",
20
20
  message_not_found: "The original Slack message was removed. Open the saved chat to recover it.",
21
21
  already_complete: "Waiting to confirm the uploaded file’s receipt.",
22
+ cant_update_message: "Slack wouldn't let that message be changed.",
23
+ edit_window_closed: "Slack wouldn't let that message be changed any more.",
24
+ already_reacted: "The reaction was already there.",
25
+ no_reaction: "The reaction was already gone.",
22
26
  }[code] ?? code;
23
27
  }
24
28
  }
@@ -36,6 +40,11 @@ const errors = new Set([
36
40
  "file_not_found",
37
41
  "already_complete",
38
42
  "not_in_channel",
43
+ // The 👀 and edits of the live message: named, so they are told apart from a real failure.
44
+ "already_reacted",
45
+ "no_reaction",
46
+ "cant_update_message",
47
+ "edit_window_closed",
39
48
  ]);
40
49
  export function slackApi(token, fetcher = fetch) {
41
50
  return async (method, args = {}) => {
@@ -208,6 +217,8 @@ export const SLACK_MANIFEST = {
208
217
  "users:read",
209
218
  "files:read",
210
219
  "files:write",
220
+ // The 👀 on a message when it arrives, and taking it off once the answer is delivered.
221
+ "reactions:write",
211
222
  ],
212
223
  },
213
224
  },
@@ -1,6 +1,7 @@
1
- import { ChatState } from "./chat-delivery-state.js";
1
+ import { ChatState, type ChatEvent } from "./chat-delivery-state.js";
2
2
  import { uploadSlackBytes, type SlackApi, type SlackIdentity } from "./slack-api.js";
3
3
  import { type Store } from "./store.js";
4
+ import { type ChatAckHooks, type ChatLiveTransport, type LiveReplies } from "./chat-live.js";
4
5
  import type { SubscriptionLeadRunner } from "./subscription-chat.js";
5
6
  export type SlackChatOptions = {
6
7
  store: Store;
@@ -15,6 +16,8 @@ export type SlackChatOptions = {
15
16
  canNotify?: () => boolean;
16
17
  clock?: () => Date;
17
18
  upload?: typeof uploadSlackBytes;
19
+ /** The live "Working on it…" message (chat-live.ts): on in the worker, off unless asked for. */
20
+ liveReplies?: LiveReplies;
18
21
  };
19
22
  /** Synchronous receipt before ACK. Never stores response_url, raw tokens, or a pairing code. */
20
23
  export declare function receiveSlack(state: ChatState, identity: SlackIdentity, type: string, raw: unknown, now: Date): boolean;
@@ -25,3 +28,10 @@ export declare function slackBlocks(text: string, buttons?: Record<string, unkno
25
28
  /** The lead's reply as mrkdwn sections: no header block, so its first line stays an ordinary line. */
26
29
  export declare function slackVoiceBlocks(mrkdwn: string, buttons?: Record<string, unknown>[]): Record<string, unknown>[];
27
30
  export declare function deliverSlackPart(options: SlackChatOptions): Promise<boolean>;
31
+ /** A live message's exact payload (§3.2): the rendered mrkdwn as the top-level text and as verbatim sections, every
32
+ * field sent on every edit, nothing parsed, linked or unfurled, no buttons. */
33
+ export declare function slackLivePayload(channel: string, mrkdwn: string): Record<string, unknown>;
34
+ /** The live message for one owner message, in its thread (where the reply goes). */
35
+ export declare function slackLive(options: Pick<SlackChatOptions, "api">, event: ChatEvent, spacingMs?: number): ChatLiveTransport;
36
+ /** The 👀 on the owner's message (reactions:write). An install without that scope is asked at most once a minute, quietly. */
37
+ export declare function slackAckHooks(options: Pick<SlackChatOptions, "api">): ChatAckHooks;
@@ -17,7 +17,9 @@ import { readSlackBlockActions, readSlackButton, SLACK_LINK_ACTION } from "./con
17
17
  import { object, slackId, slackTs, slackMember, SlackError, uploadSlackBytes, } from "./slack-api.js";
18
18
  import { resultImageFileName, safeResultImageCaption, verifyResultImage, } from "./chat-evidence.js";
19
19
  import {} from "./store.js";
20
+ import { isLiveMessage, liveSpacingOf } from "./chat-live.js";
20
21
  const SLACK_REFUSED_BLOCKS = new Set(["invalid_blocks", "invalid_blocks_format", "invalid_arguments", "msg_too_long", "msg_blocks_too_long"]);
22
+ const SLACK_UNEDITABLE = new Set(["message_not_found", "cant_update_message", "edit_window_closed"]);
21
23
  const nowOf = (options) => options.clock?.() ?? new Date();
22
24
  const split = (text, size = 2800) => {
23
25
  const parts = [];
@@ -125,10 +127,8 @@ const delivery = (options) => ({
125
127
  state: new ChatState(options.store, "slack"),
126
128
  label: "Slack",
127
129
  member: (member, channel) => slackMember(options.api, options.identity, member, channel),
128
- // A 👍 on the owner's message where the app has reactions:write; Slack shows bots no typing indicator.
129
- warm: (event) => ({
130
- react: () => options.api("reactions.add", { channel: event.channel, timestamp: event.ts, name: "thumbsup" }),
131
- }),
130
+ // Slack shows bots no typing indicator; the 👀 goes at arrival (slackAckHooks) and the live message shows the rest.
131
+ ...(options.liveReplies ? { live: (event) => slackLive(options, event, liveSpacingOf(options.liveReplies)) } : {}),
132
132
  });
133
133
  export const processSlackEvent = (options) => processChatEvent(delivery(options));
134
134
  export const planSlackNotifications = (options) => planChatNotifications(delivery(options));
@@ -157,7 +157,8 @@ export function slackBlocks(text, buttons = []) {
157
157
  export function slackVoiceBlocks(mrkdwn, buttons = []) {
158
158
  const blocks = split(mrkdwn).map((part) => ({
159
159
  type: "section",
160
- text: { type: "mrkdwn", text: part },
160
+ // verbatim: Slack never turns words of the reply into a mention, a channel or a link (renderReply escapes the rest).
161
+ text: { type: "mrkdwn", text: part, verbatim: true },
161
162
  }));
162
163
  if (buttons.length)
163
164
  blocks.push({ type: "actions", elements: buttons });
@@ -396,6 +397,7 @@ export async function deliverSlackPart(options) {
396
397
  blocks: content.rich ? slackCardBlocks(content.rich, buttons) : voiced === null ? slackBlocks(text, buttons) : slackVoiceBlocks(voiced, buttons),
397
398
  mrkdwn: voiced !== null,
398
399
  parse: "none",
400
+ link_names: false,
399
401
  unfurl_links: false,
400
402
  unfurl_media: false,
401
403
  };
@@ -423,6 +425,12 @@ export async function deliverSlackPart(options) {
423
425
  }
424
426
  // No permission to upload files here: one plain line instead of the result's screenshots.
425
427
  const saved = readChatPart(row.payload), content = saved.ok ? saved.value : null;
428
+ // The live message the reply was to replace is gone (deleted, or too old to edit): the reply goes out as a new one.
429
+ if (content?.edit !== undefined && error instanceof SlackError && SLACK_UNEDITABLE.has(error.code) && isLiveMessage(state, row.event, content.edit)) {
430
+ const { edit: _edit, ...asNew } = content;
431
+ state.repaint(row.id, asNew);
432
+ return true;
433
+ }
426
434
  // Slack refused the card's blocks: the part's own plain text goes instead, so the message is never lost.
427
435
  if (content?.rich && error instanceof SlackError && SLACK_REFUSED_BLOCKS.has(error.code)) {
428
436
  state.repaint(row.id, withoutCard(content));
@@ -444,3 +452,40 @@ export async function deliverSlackPart(options) {
444
452
  return true;
445
453
  }
446
454
  }
455
+ /** A live message's exact payload (§3.2): the rendered mrkdwn as the top-level text and as verbatim sections, every
456
+ * field sent on every edit, nothing parsed, linked or unfurled, no buttons. */
457
+ export function slackLivePayload(channel, mrkdwn) {
458
+ return { channel, text: mrkdwn, blocks: slackVoiceBlocks(mrkdwn), mrkdwn: true, parse: "none", link_names: false, unfurl_links: false, unfurl_media: false };
459
+ }
460
+ /** The live message for one owner message, in its thread (where the reply goes). */
461
+ export function slackLive(options, event, spacingMs) {
462
+ return {
463
+ platform: "slack",
464
+ ...(spacingMs === undefined ? {} : { spacingMs }),
465
+ limit: 2_800,
466
+ create: async (text) => {
467
+ const answer = await options.api("chat.postMessage", { ...slackLivePayload(event.channel, text), thread_ts: event.thread });
468
+ if (!slackTs(answer.ts))
469
+ throw new SlackError("Slack did not confirm the message identity", 15_000, true);
470
+ return answer.ts;
471
+ },
472
+ edit: async (message, text) => {
473
+ const answer = await options.api("chat.update", { ...slackLivePayload(event.channel, text), ts: message });
474
+ if (answer.ts !== message)
475
+ throw new SlackError("Slack did not confirm the message identity", 15_000, true);
476
+ },
477
+ rateLimited: error => error instanceof SlackError && error.code === "ratelimited" ? error.retryMs : null,
478
+ uncertain: error => !(error instanceof SlackError) || error.uncertain,
479
+ };
480
+ }
481
+ /** The 👀 on the owner's message (reactions:write). An install without that scope is asked at most once a minute, quietly. */
482
+ export function slackAckHooks(options) {
483
+ const code = (error) => error instanceof SlackError ? error.code : "";
484
+ return {
485
+ react: (event) => options.api("reactions.add", { channel: event.channel, timestamp: event.ts, name: "eyes" }),
486
+ unreact: (event) => options.api("reactions.remove", { channel: event.channel, timestamp: event.ts, name: "eyes" }),
487
+ refused: error => ["missing_scope", "no_permission", "not_in_channel", "channel_not_found"].includes(code(error)),
488
+ settled: error => ["already_reacted", "no_reaction", "message_not_found"].includes(code(error)),
489
+ rateLimited: error => code(error) === "ratelimited" ? error.retryMs : null,
490
+ };
491
+ }
@@ -1,11 +1,18 @@
1
1
  import { html, joinHtml } from "./html.js";
2
2
  import { loadSlackCredentials } from "./slack-api.js";
3
3
  import { chatSecretRow, chatSettingsHtml } from "./chat-settings-page.js";
4
+ import { ChatState } from "./chat-delivery-state.js";
5
+ import { ACK_REFUSED_META } from "./chat-live.js";
6
+ import { notice } from "./server/kit.js";
4
7
  export function slackSettingsHtml(store, dir, options = {}) {
8
+ const credentials = loadSlackCredentials(dir);
9
+ // Slack answered "missing scope" to the 👀: an app installed before reactions:write needs one reinstall.
10
+ const reinstall = credentials !== null && new ChatState(store, "slack").meta(credentials.installation, ACK_REFUSED_META) !== null;
5
11
  return chatSettingsHtml(store, dir, options, {
6
12
  provider: "slack",
7
13
  label: "Slack",
8
- credentials: loadSlackCredentials(dir),
14
+ credentials,
15
+ ...(reinstall ? { connection: notice(html `Reinstall needed for reactions. Update the app with the new <a class="k-link" href="/settings/slack/manifest">app manifest</a>, then reinstall it to your workspace.`) } : {}),
9
16
  intro: "Manage projects and review results in a private Slack conversation.",
10
17
  steps: html `<ol class="k-note"><li>Download the <a class="k-link" href="/settings/slack/manifest">app manifest</a>, then <a class="k-link" href="https://api.slack.com/apps" target="_blank" rel="noopener noreferrer">create an app</a> in your workspace using that manifest.</li><li>Under Basic Information, create an app-level token with <code class="k-mono">connections:write</code>.</li><li>Under OAuth &amp; Permissions, install the app and copy its bot token.</li></ol>`,
11
18
  fields: joinHtml([
package/dist/slack.js CHANGED
@@ -3,7 +3,8 @@ import { randomBytes } from "node:crypto";
3
3
  import { abortablePause, workChatQueue } from "./chat-follow.js";
4
4
  import { loadSlackCredentials, slackApi } from "./slack-api.js";
5
5
  import { ChatState } from "./chat-delivery-state.js";
6
- import { receiveSlack, processSlackEvent, deliverSlackPart, planSlackNotifications, planSlackRooms, } from "./slack-chat.js";
6
+ import { runChatAcks } from "./chat-live.js";
7
+ import { receiveSlack, processSlackEvent, deliverSlackPart, planSlackNotifications, planSlackRooms, slackAckHooks, } from "./slack-chat.js";
7
8
  export async function followSlack(options) {
8
9
  const owner = randomBytes(16).toString("hex"), state = new ChatState(options.store, "slack");
9
10
  const pause = abortablePause(options.signal);
@@ -74,11 +75,16 @@ export async function followSlack(options) {
74
75
  problem("Reconnecting to Slack…");
75
76
  });
76
77
  client.on("error", () => problem("Slack connection failed. Check the app tokens and reconnect."));
78
+ // The 👀 on each owner message as it arrives, beside the queue (a turn there can take a minute).
79
+ const api = slackApi(credentials.botToken);
80
+ const acks = runChatAcks({ state, installation: credentials.installation, owner, current: () => same() && connected,
81
+ hooks: slackAckHooks({ api }), same, signal: options.signal });
77
82
  client.on("slack_event", (envelope) => {
78
83
  if (!same())
79
84
  return;
80
85
  try {
81
- receiveSlack(state, credentials, envelope.type, envelope.body, new Date());
86
+ if (receiveSlack(state, credentials, envelope.type, envelope.body, new Date()))
87
+ acks.wake();
82
88
  void envelope
83
89
  .ack()
84
90
  .catch(() => problem("Slack acknowledgement was lost; a repeated event will be recognized."));
@@ -91,9 +97,10 @@ export async function followSlack(options) {
91
97
  ...options,
92
98
  identity: credentials,
93
99
  owner,
94
- api: slackApi(credentials.botToken),
100
+ api,
95
101
  canNotify: options.notifications,
96
102
  current: same,
103
+ liveReplies: true,
97
104
  };
98
105
  try {
99
106
  await client.start();
package/dist/store.d.ts CHANGED
@@ -8,6 +8,9 @@ import { type AcpLaunch } from "./acp-attest.js";
8
8
  import { type CheckStamp, type StampReading } from "./check-stamp.js";
9
9
  import { V155_STEPS } from "./fleet-schema.js";
10
10
  import { V156_STEPS } from "./fallback-schema.js";
11
+ import { V158_STEPS, type LeadFailureDetail, type LeadRoute } from "./lead-latency-schema.js";
12
+ import { V157_STEPS } from "./chat-live-schema.js";
13
+ import { V159_STEPS } from "./one-step-chat-schema.js";
11
14
  export { refArchived, taskArchived } from "./task-archive.js";
12
15
  import { type ChatBinding } from "./chat-core.js";
13
16
  import { ChatMessages, TELEGRAM_HOLD_REASONS, TELEGRAM_SKIPPED_ELSEWHERE, TELEGRAM_SKIPPED_OTHER_CHAT, TELEGRAM_SKIPPED_QUIET } from "./chat-messages.js";
@@ -248,7 +251,7 @@ export type SealedLedgerEntry = LedgerEntry & {
248
251
  hash: string;
249
252
  } | null;
250
253
  };
251
- export declare const SCHEMA_VERSION = 156;
254
+ export declare const SCHEMA_VERSION = 159;
252
255
  /** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
253
256
  export declare const V118_MERGED_TABLES: Readonly<Record<string, CredentialKind>>;
254
257
  /**
@@ -440,7 +443,7 @@ export declare const V147_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record
440
443
  }>>>>;
441
444
  /** The v147 migration's steps after the epoch, in order: a test seam (OpenOptions.migrationStep) may stop after each. */
442
445
  export declare const V147_STEPS: readonly ["epoch", "local-mode", "password-set", "web-session", "console-instance"];
443
- export type MigrationStep = (typeof V147_STEPS)[number] | (typeof V148_STEPS)[number] | (typeof V149_STEPS)[number] | (typeof V150_STEPS)[number] | (typeof V151_STEPS)[number] | (typeof V152_STEPS)[number] | (typeof V153_STEPS)[number] | (typeof V154_STEPS)[number] | (typeof V156_STEPS)[number] | (typeof V155_STEPS)[number];
446
+ export type MigrationStep = (typeof V147_STEPS)[number] | (typeof V148_STEPS)[number] | (typeof V149_STEPS)[number] | (typeof V150_STEPS)[number] | (typeof V151_STEPS)[number] | (typeof V152_STEPS)[number] | (typeof V153_STEPS)[number] | (typeof V154_STEPS)[number] | (typeof V156_STEPS)[number] | (typeof V155_STEPS)[number] | (typeof V157_STEPS)[number] | (typeof V158_STEPS)[number] | (typeof V159_STEPS)[number];
444
447
  /**
445
448
  * v148 (docs/plans/results-routing.md): every table the results-routing arc uses, landed at once because a current
446
449
  * store runs no DDL. Slice 1 writes run_cohort, route_record and models_generation; route_rule, route_exposure* and
@@ -528,6 +531,9 @@ export declare const V153_STEPS: readonly ["plain-tables", "plain-columns"];
528
531
  export declare const V154_STEPS: readonly ["check-stamps"];
529
532
  export { V155_STEPS };
530
533
  export { V156_STEPS };
534
+ export { V157_STEPS };
535
+ export { V158_STEPS };
536
+ export { V159_STEPS };
531
537
  /**
532
538
  * v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
533
539
  * release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
@@ -1188,6 +1194,10 @@ export type LeadTurn = {
1188
1194
  tokensIn: number | null;
1189
1195
  tokensOut: number | null;
1190
1196
  failureReason: string | null;
1197
+ /** v158: why it failed, more exactly (lead-latency-schema.ts); null when unknown or before v158. */
1198
+ failureDetail?: LeadFailureDetail | null;
1199
+ /** v158: which model answered (default, the quick model, or the main model after a quick step asked for more). */
1200
+ route?: LeadRoute | null;
1191
1201
  };
1192
1202
  /** Who placed a hold — and therefore who alone may lift it. */
1193
1203
  /** 'stop' (v52) = the pause one exact attempt's operator stop placed — lifted
@@ -4393,6 +4403,10 @@ export declare class Store {
4393
4403
  reportedCostUsd?: number | null;
4394
4404
  /** v141: how the request's own events said it was billed (a subscription harness); absent or null: unknown. */
4395
4405
  billingReported?: "subscription" | "api-key" | null;
4406
+ /** v158: the tool names a lead step asked for (names only, never arguments); absent: not recorded. */
4407
+ tools?: readonly string[];
4408
+ /** v158: why it failed, more exactly than failureReason; absent: unknown. */
4409
+ failureDetail?: LeadFailureDetail | null;
4396
4410
  }, now: Date): boolean;
4397
4411
  /**
4398
4412
  * The crash sweep: turns past their deadline become failed('crashed'),
@@ -6583,6 +6597,8 @@ export declare class Store {
6583
6597
  tokensIn: number;
6584
6598
  tokensOut: number;
6585
6599
  failureReason?: string;
6600
+ /** v158: why it failed, more exactly than failureReason (the words a later reader shows); absent: unknown. */
6601
+ failureDetail?: LeadFailureDetail | null;
6586
6602
  /** Answered turns: the assistant row, written in the same transaction as the promotion. A timed-out turn's notice. */
6587
6603
  message?: {
6588
6604
  text: string;
@@ -8521,3 +8537,18 @@ export declare function taskCheckStamp(store: Store, repo: string, taskId: strin
8521
8537
  runId: number;
8522
8538
  reading: StampReading;
8523
8539
  } | null;
8540
+ /** A thread's newest lead turn, whatever its state; null before its first. What a surface reads back to say how the
8541
+ * last reply ended after a restart (docs/plans/responsive-chat.md §3.4): the saved row, never process memory. */
8542
+ export declare function latestLeadTurnOfThread(store: Store, thread: number): LeadTurn | null;
8543
+ /** Settings → Lead's "Quick answers use a faster model" (v158): absent reads off, the shipped default
8544
+ * (docs/plans/responsive-chat.md §8: on only once the lead-answer eval passes). */
8545
+ export declare function leadQuickAnswersOn(store: Store): boolean;
8546
+ /** Turn quick answers on or off, recording who and when (the caller proved the person may change the lead's settings). */
8547
+ export declare function setLeadQuickAnswers(store: Store, enabled: boolean, by: string, now: Date): void;
8548
+ /** Which model answers a running reply (v158): written at admission and when a quick step hands over to the main model.
8549
+ * Only the turn's own running generation may write it. */
8550
+ export declare function setLeadTurnRoute(store: Store, turn: number, generation: number, route: LeadRoute): boolean;
8551
+ /** Whether a new message in this thread may be the answer to the lead's own question (ask_owner): the thread's newest
8552
+ * reply asked one, less than LEAD_ASK_TTL_MS ago. Conservative on purpose: the quick-answer router (§8) keeps such a
8553
+ * message on the main model. */
8554
+ export declare function leadAskAwaitsAnswer(store: Store, thread: number, now: Date): boolean;