toolroll 0.9.95 β 0.9.97
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.
- package/CHANGELOG.md +13 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +32 -32
- package/dist/browser-workspace.d.ts +2 -0
- package/dist/card-render.d.ts +18 -1
- package/dist/card-render.js +40 -3
- package/dist/chat-delivery.d.ts +5 -1
- package/dist/chat-delivery.js +74 -25
- package/dist/chat-follow.d.ts +6 -3
- package/dist/chat-follow.js +23 -7
- package/dist/chat-live-schema.d.ts +17 -0
- package/dist/chat-live-schema.js +32 -0
- package/dist/chat-live.d.ts +161 -0
- package/dist/chat-live.js +491 -0
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-api.d.ts +3 -1
- package/dist/discord-api.js +9 -3
- package/dist/discord-chat.d.ts +13 -3
- package/dist/discord-chat.js +77 -21
- package/dist/discord.js +8 -1
- package/dist/lead-answer-eval.d.ts +86 -0
- package/dist/lead-answer-eval.js +255 -0
- package/dist/lead-cli-live.d.ts +56 -0
- package/dist/lead-cli-live.js +200 -0
- package/dist/lead-cli.d.ts +3 -0
- package/dist/lead-cli.js +34 -4
- package/dist/lead-latency-schema.d.ts +24 -0
- package/dist/lead-latency-schema.js +36 -0
- package/dist/lead-progress.d.ts +10 -1
- package/dist/lead-progress.js +1 -1
- package/dist/lead-queue-runtime.d.ts +1 -1
- package/dist/lead-queue-runtime.js +1 -1
- package/dist/lead-route.d.ts +56 -0
- package/dist/lead-route.js +104 -0
- package/dist/lead.d.ts +4 -2
- package/dist/lead.js +79 -26
- package/dist/live-reply.d.ts +83 -0
- package/dist/live-reply.js +302 -0
- package/dist/operate.js +39 -1
- package/dist/project-knowledge.d.ts +3 -3
- package/dist/reply-shape.d.ts +9 -0
- package/dist/reply-shape.js +13 -0
- package/dist/serve.js +11 -4
- package/dist/server/chat.d.ts +2 -1
- package/dist/server/chat.js +18 -6
- package/dist/server/live.js +18 -21
- package/dist/server/render-chat.d.ts +5 -0
- package/dist/server/render-chat.js +4 -2
- package/dist/server/render-settings.d.ts +8 -0
- package/dist/server/render-settings.js +13 -2
- package/dist/server/route-table.js +2 -0
- package/dist/server/settings.js +14 -1
- package/dist/slack-api.js +11 -0
- package/dist/slack-chat.d.ts +11 -1
- package/dist/slack-chat.js +50 -5
- package/dist/slack-settings.js +8 -1
- package/dist/slack.js +10 -3
- package/dist/store.d.ts +31 -2
- package/dist/store.js +59 -6
- package/dist/subscription-chat.d.ts +2 -0
- package/dist/subscription-chat.js +3 -1
- package/dist/teams-chat.d.ts +9 -2
- package/dist/teams-chat.js +50 -2
- package/dist/teams.js +5 -2
- package/dist/teardown-git.js +33 -3
- package/dist/telegram-lead.d.ts +3 -0
- package/dist/telegram-lead.js +63 -9
- package/dist/telegram-live.d.ts +116 -0
- package/dist/telegram-live.js +224 -0
- package/dist/telegram.d.ts +2 -0
- package/dist/telegram.js +50 -2
- package/dist/toolroll-update.js +4 -1
- package/package.json +1 -1
package/dist/server/live.js
CHANGED
|
@@ -4,6 +4,7 @@ import { agentRunFingerprint } from "../agents-board.js";
|
|
|
4
4
|
import { createLiveRooms, LiveConnection } from "../live-bus.js";
|
|
5
5
|
import { reproveRemote } from "../operate-remote.js";
|
|
6
6
|
import { rowVisible } from "../project.js";
|
|
7
|
+
import { createLiveSender, LIVE_STATUS } from "../live-reply.js";
|
|
7
8
|
import { TEAM_PASSWORD_REVERIFY_MS } from "../team-http.js";
|
|
8
9
|
import { handlersOf } from "./handler-registry.js";
|
|
9
10
|
import { respond } from "./http.js";
|
|
@@ -18,6 +19,11 @@ const ROOM_NAME = /^(workspace|task|flow|chat|team|agent|summary|agents-live)(?:
|
|
|
18
19
|
const WORKSPACE_SETTLE_MS = 1_000;
|
|
19
20
|
/** A running reply's preview: at most one snapshot every 80 ms; the last one always goes out. */
|
|
20
21
|
const TURN_SETTLE_MS = 80;
|
|
22
|
+
function chatSnapshot(turn) {
|
|
23
|
+
const view = turn.reply?.view() ?? { phase: "working", status: LIVE_STATUS.thinking };
|
|
24
|
+
const steps = (turn.reply?.tools() ?? []).map(calls => ({ tools: calls.map(call => call.label), toolCalls: calls }));
|
|
25
|
+
return { steps, phase: view.phase, status: turn.done ? "" : turn.reply?.status() ?? LIVE_STATUS.thinking, text: view.phase === "writing" ? view.text : "", done: turn.done, ok: turn.ok };
|
|
26
|
+
}
|
|
21
27
|
/** A room's name, read strictly: anything else is not a room. */
|
|
22
28
|
export function parseLiveRoom(name) {
|
|
23
29
|
const match = ROOM_NAME.exec(name);
|
|
@@ -114,39 +120,30 @@ export function createLiveHandlers(runtime, live) {
|
|
|
114
120
|
}
|
|
115
121
|
return valid;
|
|
116
122
|
}
|
|
117
|
-
/** A running reply for one viewer's thread: snapshots while it runs, then the last one.
|
|
123
|
+
/** A running reply for one viewer's thread: snapshots while it runs, then the last one. Every snapshot goes through
|
|
124
|
+
* one serialized sender (live-reply.ts): one in flight, the newest waiting, standing re-proved before each, and the
|
|
125
|
+
* last one only after the sender has closed, so no preview lands after it. */
|
|
118
126
|
function follow(viewer, thread) {
|
|
119
127
|
if (!admitted(viewer, thread))
|
|
120
128
|
return;
|
|
121
129
|
const turn = liveTurns.get(thread);
|
|
122
130
|
if (turn === undefined || turn.done || viewer.detach !== null)
|
|
123
131
|
return;
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
if (admitted(viewer, thread))
|
|
127
|
-
viewer.connection.send(viewer.room, "turn", { steps: turn.steps, done: turn.done, ok: turn.ok });
|
|
128
|
-
};
|
|
129
|
-
const flush = () => {
|
|
130
|
-
queued = null;
|
|
131
|
-
snapshot();
|
|
132
|
-
if (turn.done)
|
|
133
|
-
stop();
|
|
134
|
-
};
|
|
132
|
+
const send = (payload) => { viewer.connection.send(viewer.room, "turn", payload); };
|
|
133
|
+
const sender = createLiveSender({ send, standing: () => admitted(viewer, thread), spacingMs: TURN_SETTLE_MS });
|
|
135
134
|
const listener = () => {
|
|
136
|
-
if (turn.done) {
|
|
137
|
-
|
|
138
|
-
clearTimeout(queued);
|
|
139
|
-
flush();
|
|
135
|
+
if (!turn.done) {
|
|
136
|
+
sender.offer(chatSnapshot(turn));
|
|
140
137
|
return;
|
|
141
138
|
}
|
|
142
|
-
|
|
143
|
-
|
|
139
|
+
stop();
|
|
140
|
+
void sender.close().then(() => { if (admitted(viewer, thread))
|
|
141
|
+
send(chatSnapshot(turn)); });
|
|
144
142
|
};
|
|
145
|
-
const stop = () => {
|
|
146
|
-
clearTimeout(queued); turn.listeners.delete(listener); viewer.detach = null; };
|
|
143
|
+
const stop = () => { turn.listeners.delete(listener); viewer.detach = null; void sender.close(); };
|
|
147
144
|
turn.listeners.add(listener);
|
|
148
145
|
viewer.detach = stop;
|
|
149
|
-
|
|
146
|
+
sender.offer(chatSnapshot(turn));
|
|
150
147
|
}
|
|
151
148
|
/** A reply starts on a thread: whoever has that conversation open follows it. */
|
|
152
149
|
function turnStarted(thread) {
|
|
@@ -300,6 +300,8 @@ export declare function replacedBrowserMessages(previous: ReplacedThread): impor
|
|
|
300
300
|
/** The replaced thread for the server-rendered page: read only, then the divider. */
|
|
301
301
|
export declare function replacedThreadHtml(previous: ReplacedThread | null | undefined): Html;
|
|
302
302
|
export type LeadThreadRows = {
|
|
303
|
+
/** How the thread's last reply failed, from its saved turn (no message of its own); null otherwise. */
|
|
304
|
+
failure?: string | null;
|
|
303
305
|
/** The lead thread a change in reachable projects replaced; shown only. */
|
|
304
306
|
previous?: ReplacedThread | null;
|
|
305
307
|
messages: LeadMessage[];
|
|
@@ -404,6 +406,9 @@ export type LiveTurn = {
|
|
|
404
406
|
stepAt?: number[];
|
|
405
407
|
/** Per step: its text is the provider's finished answer, secret-checked whole (lead.ts `final`). */
|
|
406
408
|
confirmed?: boolean[];
|
|
409
|
+
/** The shared live-reply core (live-reply.ts): the only source of what the chat room shows β one status line, the
|
|
410
|
+
* tools behind a disclosure, and shaped, settled partial text. `steps[].text` above is raw and never sent. */
|
|
411
|
+
reply?: import("../live-reply.js").LiveReply;
|
|
407
412
|
};
|
|
408
413
|
export type ChatEnablement = {
|
|
409
414
|
ok: true;
|
|
@@ -825,7 +825,7 @@ export function replacedThreadHtml(previous) {
|
|
|
825
825
|
* A div, not a p: the Remove form can't sit inside a paragraph (the parser would close it first). */
|
|
826
826
|
function queuedStateHtml(row, back) {
|
|
827
827
|
if (row.status === "queued") {
|
|
828
|
-
const waiting = row.error?.startsWith("Waiting: ") === true ? row.error.slice("Waiting: ".length) : "
|
|
828
|
+
const waiting = row.error?.startsWith("Waiting: ") === true ? row.error.slice("Waiting: ".length) : "Waiting for your earlier messagesβ¦";
|
|
829
829
|
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
830
|
}
|
|
831
831
|
if (row.status === "running" && row.turnId === null)
|
|
@@ -949,6 +949,8 @@ export function leadThreadHtml(data) {
|
|
|
949
949
|
const ask = message.turn === null ? undefined : data.asks?.get(message.turn);
|
|
950
950
|
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
951
|
}
|
|
952
|
+
if (data.pending === null && data.failure)
|
|
953
|
+
thread.push(html `<div class="problem" data-key="failed" data-lead-failed role="alert">${data.failure}</div>`);
|
|
952
954
|
parts.push(html `<div class="thread" data-key="thread" data-chat-list>\n${joinHtml(thread, "\n")}\n</div>`);
|
|
953
955
|
if (data.pending !== null) {
|
|
954
956
|
const subscription = data.pending.reservedMicrousd === 0;
|
|
@@ -1033,7 +1035,7 @@ export function leadPage(chrome, data) {
|
|
|
1033
1035
|
sessionId, user: approver, version: leadChatVersion(data),
|
|
1034
1036
|
messages: leadBrowserMessages(data, data.focusTask === null && chatProject === null ? null : returnTo, { task: data.focusTask?.id ?? null, project: chatProject }),
|
|
1035
1037
|
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,
|
|
1038
|
+
taskId: data.focusTask?.id ?? null, resultRunId: data.resultRunId ?? null, project: chatProject, failure: data.pending === null ? data.failure ?? null : null,
|
|
1037
1039
|
...(data.previous == null ? {} : { previous: { messages: replacedBrowserMessages(data.previous) } }),
|
|
1038
1040
|
},
|
|
1039
1041
|
focus: data.focusTask === null ? null : { id: data.focusTask.id, title: data.focusTask.title,
|
|
@@ -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" } : {}) });
|
package/dist/server/settings.js
CHANGED
|
@@ -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
|
}
|
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
|
},
|
package/dist/slack-chat.d.ts
CHANGED
|
@@ -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;
|
package/dist/slack-chat.js
CHANGED
|
@@ -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
|
-
//
|
|
129
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/slack-settings.js
CHANGED
|
@@ -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
|
|
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 & 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 {
|
|
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
|
|
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,8 @@ 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";
|
|
11
13
|
export { refArchived, taskArchived } from "./task-archive.js";
|
|
12
14
|
import { type ChatBinding } from "./chat-core.js";
|
|
13
15
|
import { ChatMessages, TELEGRAM_HOLD_REASONS, TELEGRAM_SKIPPED_ELSEWHERE, TELEGRAM_SKIPPED_OTHER_CHAT, TELEGRAM_SKIPPED_QUIET } from "./chat-messages.js";
|
|
@@ -248,7 +250,7 @@ export type SealedLedgerEntry = LedgerEntry & {
|
|
|
248
250
|
hash: string;
|
|
249
251
|
} | null;
|
|
250
252
|
};
|
|
251
|
-
export declare const SCHEMA_VERSION =
|
|
253
|
+
export declare const SCHEMA_VERSION = 158;
|
|
252
254
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
253
255
|
export declare const V118_MERGED_TABLES: Readonly<Record<string, CredentialKind>>;
|
|
254
256
|
/**
|
|
@@ -440,7 +442,7 @@ export declare const V147_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record
|
|
|
440
442
|
}>>>>;
|
|
441
443
|
/** The v147 migration's steps after the epoch, in order: a test seam (OpenOptions.migrationStep) may stop after each. */
|
|
442
444
|
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];
|
|
445
|
+
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];
|
|
444
446
|
/**
|
|
445
447
|
* v148 (docs/plans/results-routing.md): every table the results-routing arc uses, landed at once because a current
|
|
446
448
|
* store runs no DDL. Slice 1 writes run_cohort, route_record and models_generation; route_rule, route_exposure* and
|
|
@@ -528,6 +530,8 @@ export declare const V153_STEPS: readonly ["plain-tables", "plain-columns"];
|
|
|
528
530
|
export declare const V154_STEPS: readonly ["check-stamps"];
|
|
529
531
|
export { V155_STEPS };
|
|
530
532
|
export { V156_STEPS };
|
|
533
|
+
export { V157_STEPS };
|
|
534
|
+
export { V158_STEPS };
|
|
531
535
|
/**
|
|
532
536
|
* v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
|
|
533
537
|
* release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
|
|
@@ -1188,6 +1192,10 @@ export type LeadTurn = {
|
|
|
1188
1192
|
tokensIn: number | null;
|
|
1189
1193
|
tokensOut: number | null;
|
|
1190
1194
|
failureReason: string | null;
|
|
1195
|
+
/** v158: why it failed, more exactly (lead-latency-schema.ts); null when unknown or before v158. */
|
|
1196
|
+
failureDetail?: LeadFailureDetail | null;
|
|
1197
|
+
/** v158: which model answered (default, the quick model, or the main model after a quick step asked for more). */
|
|
1198
|
+
route?: LeadRoute | null;
|
|
1191
1199
|
};
|
|
1192
1200
|
/** Who placed a hold β and therefore who alone may lift it. */
|
|
1193
1201
|
/** 'stop' (v52) = the pause one exact attempt's operator stop placed β lifted
|
|
@@ -4393,6 +4401,10 @@ export declare class Store {
|
|
|
4393
4401
|
reportedCostUsd?: number | null;
|
|
4394
4402
|
/** v141: how the request's own events said it was billed (a subscription harness); absent or null: unknown. */
|
|
4395
4403
|
billingReported?: "subscription" | "api-key" | null;
|
|
4404
|
+
/** v158: the tool names a lead step asked for (names only, never arguments); absent: not recorded. */
|
|
4405
|
+
tools?: readonly string[];
|
|
4406
|
+
/** v158: why it failed, more exactly than failureReason; absent: unknown. */
|
|
4407
|
+
failureDetail?: LeadFailureDetail | null;
|
|
4396
4408
|
}, now: Date): boolean;
|
|
4397
4409
|
/**
|
|
4398
4410
|
* The crash sweep: turns past their deadline become failed('crashed'),
|
|
@@ -6583,6 +6595,8 @@ export declare class Store {
|
|
|
6583
6595
|
tokensIn: number;
|
|
6584
6596
|
tokensOut: number;
|
|
6585
6597
|
failureReason?: string;
|
|
6598
|
+
/** v158: why it failed, more exactly than failureReason (the words a later reader shows); absent: unknown. */
|
|
6599
|
+
failureDetail?: LeadFailureDetail | null;
|
|
6586
6600
|
/** Answered turns: the assistant row, written in the same transaction as the promotion. A timed-out turn's notice. */
|
|
6587
6601
|
message?: {
|
|
6588
6602
|
text: string;
|
|
@@ -8521,3 +8535,18 @@ export declare function taskCheckStamp(store: Store, repo: string, taskId: strin
|
|
|
8521
8535
|
runId: number;
|
|
8522
8536
|
reading: StampReading;
|
|
8523
8537
|
} | null;
|
|
8538
|
+
/** A thread's newest lead turn, whatever its state; null before its first. What a surface reads back to say how the
|
|
8539
|
+
* last reply ended after a restart (docs/plans/responsive-chat.md Β§3.4): the saved row, never process memory. */
|
|
8540
|
+
export declare function latestLeadTurnOfThread(store: Store, thread: number): LeadTurn | null;
|
|
8541
|
+
/** Settings β Lead's "Quick answers use a faster model" (v158): absent reads off, the shipped default
|
|
8542
|
+
* (docs/plans/responsive-chat.md Β§8: on only once the lead-answer eval passes). */
|
|
8543
|
+
export declare function leadQuickAnswersOn(store: Store): boolean;
|
|
8544
|
+
/** Turn quick answers on or off, recording who and when (the caller proved the person may change the lead's settings). */
|
|
8545
|
+
export declare function setLeadQuickAnswers(store: Store, enabled: boolean, by: string, now: Date): void;
|
|
8546
|
+
/** Which model answers a running reply (v158): written at admission and when a quick step hands over to the main model.
|
|
8547
|
+
* Only the turn's own running generation may write it. */
|
|
8548
|
+
export declare function setLeadTurnRoute(store: Store, turn: number, generation: number, route: LeadRoute): boolean;
|
|
8549
|
+
/** Whether a new message in this thread may be the answer to the lead's own question (ask_owner): the thread's newest
|
|
8550
|
+
* reply asked one, less than LEAD_ASK_TTL_MS ago. Conservative on purpose: the quick-answer router (Β§8) keeps such a
|
|
8551
|
+
* message on the main model. */
|
|
8552
|
+
export declare function leadAskAwaitsAnswer(store: Store, thread: number, now: Date): boolean;
|