toolroll 0.9.97 → 0.9.99
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 +15 -0
- package/dist/approval-authority.d.ts +10 -1
- package/dist/approval-authority.js +18 -2
- package/dist/browser/workspace.js +1 -1
- package/dist/browser-workspace.d.ts +4 -3
- package/dist/chat-actions.d.ts +40 -5
- package/dist/chat-actions.js +124 -13
- package/dist/chat-card.js +4 -1
- package/dist/chat-channel.d.ts +3 -0
- package/dist/chat-channel.js +24 -7
- package/dist/chat-decide.js +4 -2
- package/dist/chat-task-actions.js +1 -1
- package/dist/chat-terms.d.ts +8 -0
- package/dist/chat-terms.js +14 -0
- package/dist/chat-voice.js +1 -1
- package/dist/checkout-cleanup.js +2 -1
- package/dist/contracts/chat-actions.d.ts +6 -0
- package/dist/contracts/chat-actions.js +2 -0
- package/dist/contracts/cli.d.ts +6 -0
- package/dist/contracts/cli.js +3 -0
- package/dist/contracts/lead-tools.d.ts +17 -0
- package/dist/contracts/lead-tools.js +4 -1
- package/dist/contracts/store-json.d.ts +1 -0
- package/dist/deploy-phase-data.json +21 -0
- package/dist/deploy-phase.d.ts +37 -0
- package/dist/deploy-phase.js +64 -0
- package/dist/deploy-rescue.d.ts +2 -1
- package/dist/deploy-rescue.js +6 -7
- package/dist/desktop-update.d.ts +2 -2
- package/dist/fast-start.d.ts +83 -6
- package/dist/fast-start.js +112 -15
- package/dist/home-groups.d.ts +39 -0
- package/dist/home-groups.js +33 -0
- package/dist/lead-answer-eval.d.ts +7 -0
- package/dist/lead-answer-eval.js +6 -2
- package/dist/lead-capabilities.js +1 -1
- package/dist/lead-context.js +2 -2
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +10 -7
- package/dist/lead-doors.d.ts +32 -0
- package/dist/lead-doors.js +80 -14
- package/dist/lead-status.d.ts +3 -0
- package/dist/lead-status.js +8 -3
- package/dist/lead-tools.d.ts +20 -0
- package/dist/lead-tools.js +66 -3
- package/dist/lead-voice.js +1 -1
- package/dist/lead.js +18 -2
- package/dist/local-owner.js +2 -0
- package/dist/one-step-chat-schema.d.ts +22 -0
- package/dist/one-step-chat-schema.js +37 -0
- package/dist/operate.d.ts +27 -1
- package/dist/operate.js +206 -32
- package/dist/project-delete.js +9 -0
- package/dist/project-knowledge.d.ts +7 -7
- package/dist/publish.d.ts +2 -0
- package/dist/publish.js +1 -1
- package/dist/pull-request-flow.d.ts +4 -0
- package/dist/pull-request-flow.js +16 -1
- package/dist/release-adapters.js +30 -14
- package/dist/release-gate.d.ts +3 -3
- package/dist/release-gate.js +7 -4
- package/dist/release-next.d.ts +3 -3
- package/dist/release-next.js +13 -16
- package/dist/reply-voice.d.ts +23 -0
- package/dist/reply-voice.js +37 -0
- package/dist/scope.d.ts +6 -1
- package/dist/scope.js +3 -2
- package/dist/server/chat.js +4 -4
- package/dist/server/chrome.js +2 -2
- package/dist/server/home.d.ts +4 -3
- package/dist/server/home.js +33 -28
- package/dist/server/render-chat.js +11 -6
- package/dist/server/render-pages.js +2 -2
- package/dist/setup-guide.d.ts +2 -2
- package/dist/staged-runtimes.js +2 -1
- package/dist/store.d.ts +4 -2
- package/dist/store.js +35 -12
- package/dist/telegram-lead.js +69 -5
- package/dist/telegram-status.js +9 -4
- package/dist/telegram.d.ts +10 -0
- package/dist/telegram.js +92 -8
- package/dist/toolroll-update.js +3 -1
- package/dist/tui-contract.d.ts +2 -0
- package/dist/tui-source.js +3 -0
- package/dist/tui.js +41 -41
- package/dist/typed-start.d.ts +51 -0
- package/dist/typed-start.js +98 -0
- package/dist/wake-hub.d.ts +72 -0
- package/dist/wake-hub.js +151 -0
- package/package.json +1 -1
package/dist/server/home.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { browserWorkActionHref } from "../browser-crew.js";
|
|
2
|
-
import {
|
|
2
|
+
import { HOME_GROUP_LABEL, homeGroupOf } from "../home-groups.js";
|
|
3
3
|
import { projectName } from "../project.js";
|
|
4
4
|
import { workIndexPage } from "../work-index.js";
|
|
5
5
|
import { relativeAge } from "./chrome.js";
|
|
6
6
|
import { taskHref } from "./http.js";
|
|
7
7
|
const TONE = { attention: "needs", ready: "needs", live: "live", done: "done", problem: "failed", muted: "", neutral: "" };
|
|
8
|
-
const ASKS = ["decide", "review", "unblock"];
|
|
9
8
|
const ASK_WORD = { decide: "Decision", review: "Result", unblock: "Blocked" };
|
|
10
9
|
/** How many waiting rows Home lists; the rest open in Tasks. */
|
|
11
10
|
export const HOME_NEEDS_SHOWN = 30;
|
|
12
11
|
const HOME_RUNNING_SHOWN = 8;
|
|
12
|
+
/** How many rows finished in the last 7 days Home lists; the rest open in Tasks. */
|
|
13
|
+
const HOME_DONE_SHOWN = 8;
|
|
13
14
|
/** "7 need you across 2 projects." — the sentence and the figures read the same totals. */
|
|
14
15
|
export function homeSentence(needs, perProject, scope, any) {
|
|
15
16
|
if (!any)
|
|
@@ -51,50 +52,54 @@ function rowOf(store, item, now, multi, canAnswer, closedDoor) {
|
|
|
51
52
|
: href === null || action === null ? null : { label: action.label, href }, question,
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
|
-
/** Home's view for one person and scope, from
|
|
55
|
+
/** Home's view for one person and scope, from four reads of the work index (what waits on you, what runs, what waits on
|
|
56
|
+
* Toolroll, what was done), grouped by the one rule every surface shares (home-groups.ts). */
|
|
55
57
|
export function homeView(facts) {
|
|
56
58
|
const { store, now, access, project } = facts;
|
|
57
59
|
const read = (view, limit) => workIndexPage(store, now, access, { view, limit, project, ...(facts.evidenceRoot === undefined ? {} : { root: facts.evidenceRoot }) });
|
|
58
60
|
const needs = read("needs-you", HOME_NEEDS_SHOWN);
|
|
59
61
|
const running = read("running", HOME_RUNNING_SHOWN);
|
|
60
|
-
//
|
|
61
|
-
const waiting =
|
|
62
|
+
// Queued: what waits its turn, or what Toolroll or another task moves on, with no person asked.
|
|
63
|
+
const waiting = read("queued", HOME_RUNNING_SHOWN);
|
|
64
|
+
// Done: the Completed list is ordered by completion time, newest first, so its first rows are the newest finished;
|
|
65
|
+
// only those finished in the last 7 days are listed, and that filter runs before the cap.
|
|
66
|
+
const completed = read("completed", HOME_DONE_SHOWN);
|
|
62
67
|
const totals = needs.totals;
|
|
63
68
|
// Queued: the Tasks list's own Queued count (the same predicate its view=queued filter lists).
|
|
64
69
|
const queued = totals.queued;
|
|
65
70
|
// Every project is said by name (scope=all): /tasks alone would narrow to the session's open project.
|
|
66
71
|
const scopeQuery = project === null ? "scope=all&" : `project=${encodeURIComponent(project)}&`;
|
|
72
|
+
// Each figure keeps the name of the list it opens; Done is the whole Completed list, not just this week.
|
|
67
73
|
const counts = [
|
|
68
|
-
{ key: "needs", label:
|
|
74
|
+
{ key: "needs", label: HOME_GROUP_LABEL.you, value: totals["needs-you"], href: `/tasks?${scopeQuery}view=needs-you` },
|
|
69
75
|
{ key: "running", label: "Running", value: totals.running, href: `/tasks?${scopeQuery}view=running` },
|
|
70
76
|
{ key: "queued", label: "Queued", value: queued, href: `/tasks?${scopeQuery}view=queued` },
|
|
71
|
-
{ key: "finished", label:
|
|
77
|
+
{ key: "finished", label: HOME_GROUP_LABEL.done, value: totals.completed, href: `/tasks?${scopeQuery}view=completed` },
|
|
72
78
|
];
|
|
73
|
-
|
|
79
|
+
// One row per family, in the first group that claims it.
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
const take = (items, group) => items.filter(one => {
|
|
82
|
+
if (homeGroupOf(one) !== group || seen.has(one.rootId))
|
|
83
|
+
return false;
|
|
84
|
+
seen.add(one.rootId);
|
|
85
|
+
return true;
|
|
86
|
+
});
|
|
87
|
+
// Waiting on you, in today's order of what it asks: decide (with any row whose ask the index couldn't name, which
|
|
88
|
+
// still waits on this person and is never lost), then review, then unblock. Each row's chip names its ask.
|
|
89
|
+
const yours = take(["decide", null, "review", "unblock"].flatMap(ask => needs.items.filter(one => one.ask === ask)), "you");
|
|
90
|
+
// Working: what runs, then what is queued (waits its turn, or on Toolroll or another task), each read with its own bound.
|
|
91
|
+
const working = [...take(running.items, "working"), ...take(waiting.items, "working")];
|
|
92
|
+
const weekAgo = new Date(now.getTime() - 7 * 86_400_000).toISOString();
|
|
93
|
+
const done = take(completed.items.filter(one => (one.completion?.at ?? one.updatedAt) >= weekAgo), "done").slice(0, HOME_DONE_SHOWN);
|
|
94
|
+
const multi = project === null && new Set([...yours, ...working, ...done].map(one => one.repo ?? "")).size > 1;
|
|
74
95
|
const groups = [];
|
|
75
|
-
for (const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
groups.push({ key: ask, label: ASK_LABEL[ask], rows });
|
|
96
|
+
for (const [key, items] of [["you", yours], ["working", working], ["done", done]]) {
|
|
97
|
+
if (items.length > 0)
|
|
98
|
+
groups.push({ key, label: HOME_GROUP_LABEL[key], rows: items.map(one => rowOf(store, one, now, multi, facts.canAnswer, facts.closedDoor)) });
|
|
79
99
|
}
|
|
80
|
-
// A waiting row whose ask the index couldn't name still waits on this person: it goes under Decide, never lost.
|
|
81
|
-
const unnamed = needs.items.filter(one => one.ask === null).map(one => rowOf(store, one, now, multi, facts.canAnswer, facts.closedDoor));
|
|
82
|
-
if (unnamed.length > 0) {
|
|
83
|
-
const decide = groups.find(one => one.key === "decide");
|
|
84
|
-
if (decide)
|
|
85
|
-
decide.rows.push(...unnamed);
|
|
86
|
-
else
|
|
87
|
-
groups.unshift({ key: "decide", label: ASK_LABEL.decide, rows: unnamed });
|
|
88
|
-
}
|
|
89
|
-
const live = running.items.filter(one => !one.status.views.includes("needs-you")).map(one => rowOf(store, one, now, multi, facts.canAnswer, facts.closedDoor));
|
|
90
|
-
if (live.length > 0)
|
|
91
|
-
groups.push({ key: "running", label: "Running", rows: live });
|
|
92
|
-
const waits = waiting.items.filter(one => one.status.label === "Waiting" && !one.status.views.includes("needs-you")).map(one => rowOf(store, one, now, multi, facts.canAnswer, facts.closedDoor));
|
|
93
|
-
if (waits.length > 0)
|
|
94
|
-
groups.push({ key: "waiting", label: "Waiting", rows: waits });
|
|
95
100
|
const perProject = needs.projects.map(one => ({ name: one.repo === null ? "Unplaced" : projectName(one.repo), needs: one.totals["needs-you"] }));
|
|
96
101
|
const any = totals.all > 0 || totals.completed > 0 || (totals.archived ?? 0) > 0;
|
|
97
|
-
const shown =
|
|
102
|
+
const shown = yours.length;
|
|
98
103
|
return {
|
|
99
104
|
kind: "home",
|
|
100
105
|
say: homeSentence(totals["needs-you"], perProject, project === null ? null : projectName(project), any),
|
|
@@ -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
|
|
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 & 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: "
|
|
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
|
-
|
|
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)}"
|
|
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:
|
|
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
|
}
|
|
@@ -981,7 +986,7 @@ export function leadPage(chrome, data) {
|
|
|
981
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>`,
|
|
982
987
|
html `<details class="chat-limits chat-session-details"><summary>Conversation details<span class="meta">${subscription ? "membership" : data.config.provider}</span></summary>\n${joinHtml([
|
|
983
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
|
|
984
|
-
? html `<span>membership login · no dollar
|
|
989
|
+
? html `<span>membership login · no dollar limit</span>`
|
|
985
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>`,
|
|
986
991
|
data.session === null
|
|
987
992
|
? html `<p class="meta">No conversation yet. Send a message to start one.</p>`
|
|
@@ -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', '
|
|
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', '
|
|
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
|
}
|
package/dist/setup-guide.d.ts
CHANGED
|
@@ -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/staged-runtimes.js
CHANGED
|
@@ -12,6 +12,7 @@ import { lstatSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSyn
|
|
|
12
12
|
import { execFileSync } from "node:child_process";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
15
|
+
import { isDeployed } from "./deploy-phase.js";
|
|
15
16
|
import { quarantine, quarantineEntries, claimQuarantine, releaseQuarantine, restoreQuarantine, deleteQuarantineSync } from "./sweep-quarantine.js";
|
|
16
17
|
import { processCensus } from "./run-closeout.js";
|
|
17
18
|
const DAY_MS = 86_400_000;
|
|
@@ -84,7 +85,7 @@ function readStage(path, name, fallbackAt) {
|
|
|
84
85
|
at = 0;
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
|
-
const finished = deploy !== null ? deploy["phase"]
|
|
88
|
+
const finished = deploy !== null ? isDeployed(deploy["phase"]) : update !== null ? typeof update["finishedAt"] === "string" : false;
|
|
88
89
|
const from = update?.["from"];
|
|
89
90
|
const prior = typeof deploy?.["priorRuntime"] === "string" ? deploy["priorRuntime"] : typeof from?.dist === "string" ? from.dist : null;
|
|
90
91
|
let keptAside = false;
|
package/dist/store.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { V155_STEPS } from "./fleet-schema.js";
|
|
|
10
10
|
import { V156_STEPS } from "./fallback-schema.js";
|
|
11
11
|
import { V158_STEPS, type LeadFailureDetail, type LeadRoute } from "./lead-latency-schema.js";
|
|
12
12
|
import { V157_STEPS } from "./chat-live-schema.js";
|
|
13
|
+
import { V159_STEPS } from "./one-step-chat-schema.js";
|
|
13
14
|
export { refArchived, taskArchived } from "./task-archive.js";
|
|
14
15
|
import { type ChatBinding } from "./chat-core.js";
|
|
15
16
|
import { ChatMessages, TELEGRAM_HOLD_REASONS, TELEGRAM_SKIPPED_ELSEWHERE, TELEGRAM_SKIPPED_OTHER_CHAT, TELEGRAM_SKIPPED_QUIET } from "./chat-messages.js";
|
|
@@ -250,7 +251,7 @@ export type SealedLedgerEntry = LedgerEntry & {
|
|
|
250
251
|
hash: string;
|
|
251
252
|
} | null;
|
|
252
253
|
};
|
|
253
|
-
export declare const SCHEMA_VERSION =
|
|
254
|
+
export declare const SCHEMA_VERSION = 159;
|
|
254
255
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
255
256
|
export declare const V118_MERGED_TABLES: Readonly<Record<string, CredentialKind>>;
|
|
256
257
|
/**
|
|
@@ -442,7 +443,7 @@ export declare const V147_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record
|
|
|
442
443
|
}>>>>;
|
|
443
444
|
/** The v147 migration's steps after the epoch, in order: a test seam (OpenOptions.migrationStep) may stop after each. */
|
|
444
445
|
export declare const V147_STEPS: readonly ["epoch", "local-mode", "password-set", "web-session", "console-instance"];
|
|
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];
|
|
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];
|
|
446
447
|
/**
|
|
447
448
|
* v148 (docs/plans/results-routing.md): every table the results-routing arc uses, landed at once because a current
|
|
448
449
|
* store runs no DDL. Slice 1 writes run_cohort, route_record and models_generation; route_rule, route_exposure* and
|
|
@@ -532,6 +533,7 @@ export { V155_STEPS };
|
|
|
532
533
|
export { V156_STEPS };
|
|
533
534
|
export { V157_STEPS };
|
|
534
535
|
export { V158_STEPS };
|
|
536
|
+
export { V159_STEPS };
|
|
535
537
|
/**
|
|
536
538
|
* v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
|
|
537
539
|
* release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
|
package/dist/store.js
CHANGED
|
@@ -15,6 +15,7 @@ import { FINDING_SOURCES, FINDING_SOURCES_BEFORE_LEAD, V155_SCHEMA, V155_STEPS }
|
|
|
15
15
|
import { V156_SCHEMA, V156_STEPS } from "./fallback-schema.js";
|
|
16
16
|
import { V158_ADDED_COLUMNS, V158_SCHEMA, V158_STEPS, LEAD_FAILURE_DETAILS, LEAD_ROUTES } from "./lead-latency-schema.js";
|
|
17
17
|
import { V157_SCHEMA, V157_STEPS } from "./chat-live-schema.js";
|
|
18
|
+
import { V159_ADDED_COLUMNS, V159_SCHEMA, V159_STEPS } from "./one-step-chat-schema.js";
|
|
18
19
|
import { refArchived, taskArchived } from "./task-archive.js";
|
|
19
20
|
export { refArchived, taskArchived } from "./task-archive.js";
|
|
20
21
|
import { CHAT_PROVIDERS, CHAT_SCHEMA } from "./contracts/chat-tables.js";
|
|
@@ -1148,7 +1149,10 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
1148
1149
|
// v158 adds, and changes no saved row (docs/plans/responsive-chat.md, slice 5): the tools each lead step asked for, an
|
|
1149
1150
|
// exact failure detail on a step and a reply, which model answered a reply (mate_turn.route), and the quick-answer
|
|
1150
1151
|
// switch (lead_quick_setting). V158_SCHEMA, V158_ADDED_COLUMNS (lead-latency-schema.ts): saved rows read NULL.
|
|
1151
|
-
|
|
1152
|
+
// v159 adds, and changes no saved row (docs/plans/one-step-chat.md): the lead turn a chat start's preview was prepared
|
|
1153
|
+
// in (fast_preview.turn) and a typed yes's record (chat_typed_start, a new table). V159_SCHEMA, V159_ADDED_COLUMNS
|
|
1154
|
+
// (one-step-chat-schema.ts): saved rows read NULL.
|
|
1155
|
+
export const SCHEMA_VERSION = 159;
|
|
1152
1156
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
1153
1157
|
export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
|
|
1154
1158
|
/** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
|
|
@@ -1281,7 +1285,7 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
|
|
|
1281
1285
|
* unchanged), and adds the nullable column in V125_ADDED_COLUMNS, checked the same way. v126 only adds a new table
|
|
1282
1286
|
* (task_source) and the columns in V126_ADDED_COLUMNS (null or 0), checked the same way.
|
|
1283
1287
|
*/
|
|
1284
|
-
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158]);
|
|
1288
|
+
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159]);
|
|
1285
1289
|
/** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
|
|
1286
1290
|
export const V119_ADDED_COLUMNS = Object.freeze({
|
|
1287
1291
|
task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
@@ -1944,6 +1948,7 @@ export { V155_STEPS };
|
|
|
1944
1948
|
export { V156_STEPS };
|
|
1945
1949
|
export { V157_STEPS };
|
|
1946
1950
|
export { V158_STEPS };
|
|
1951
|
+
export { V159_STEPS };
|
|
1947
1952
|
/**
|
|
1948
1953
|
* v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
|
|
1949
1954
|
* release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
|
|
@@ -5293,6 +5298,13 @@ CREATE INDEX IF NOT EXISTS run_checkpoint_by_task ON run_checkpoint (task_ref, i
|
|
|
5293
5298
|
for (const [column, { definition }] of Object.entries(columns))
|
|
5294
5299
|
addColumn(db, table, column, definition);
|
|
5295
5300
|
step("lead-latency-columns");
|
|
5301
|
+
// v159: one-step start from chat (V159_SCHEMA, V159_ADDED_COLUMNS: saved rows read NULL). Idempotent, as v158's are.
|
|
5302
|
+
db.exec(V159_SCHEMA);
|
|
5303
|
+
step("one-step-chat-tables");
|
|
5304
|
+
for (const [table, columns] of Object.entries(V159_ADDED_COLUMNS))
|
|
5305
|
+
for (const [column, { definition }] of Object.entries(columns))
|
|
5306
|
+
addColumn(db, table, column, definition);
|
|
5307
|
+
step("one-step-chat-columns");
|
|
5296
5308
|
// Keep the epoch until reclamation succeeds, including a retry after compaction already committed. A new file has
|
|
5297
5309
|
// nothing to reclaim.
|
|
5298
5310
|
if (!fresh)
|
|
@@ -8244,6 +8256,9 @@ export class Store {
|
|
|
8244
8256
|
throw new ReleaseSealRefused();
|
|
8245
8257
|
}
|
|
8246
8258
|
if (changed.changes > 0) {
|
|
8259
|
+
// A real seal is work a builder can take now: wake the loops (live-wakeups.md). Inside the seal's own
|
|
8260
|
+
// transaction, so a seal that rolls back wakes nothing. The claim re-proves the approval regardless.
|
|
8261
|
+
this.bumpWake();
|
|
8247
8262
|
// The project's tools this yes saw (v80): a later addition needs a later yes.
|
|
8248
8263
|
this.sealProjectTools(taskId, String(working["digest"]), now);
|
|
8249
8264
|
// A fresh yes lifts the stale-approval hold and closes its page
|
|
@@ -11754,17 +11769,23 @@ export class Store {
|
|
|
11754
11769
|
* enough, and neither is a stale yes).
|
|
11755
11770
|
*/
|
|
11756
11771
|
markCapability(repo, kind, name, outcome, now, expiresAt = null) {
|
|
11757
|
-
|
|
11758
|
-
|
|
11772
|
+
return this.transact(() => {
|
|
11773
|
+
// The status before this answer, read in the same transaction: only a gap that fills wakes the loop. A
|
|
11774
|
+
// repeated passing probe refreshes the timestamp and says nothing new, so it must not wake (live-wakeups.md).
|
|
11775
|
+
const prior = this.db.prepare("SELECT status FROM capability WHERE repo = ? AND kind = ? AND name = ?").get(repo, kind, name);
|
|
11776
|
+
const { changes } = this.db
|
|
11777
|
+
.prepare(`UPDATE capability SET status = ?, last_verified_at = ?, verified_by = ?, last_result = ?, expires_at = COALESCE(?, expires_at)
|
|
11759
11778
|
WHERE repo = ? AND kind = ? AND name = ?`)
|
|
11760
|
-
|
|
11761
|
-
|
|
11762
|
-
|
|
11763
|
-
|
|
11764
|
-
|
|
11765
|
-
|
|
11766
|
-
|
|
11767
|
-
|
|
11779
|
+
.run(outcome.status, outcome.status === "verified" ? now.toISOString() : null, outcome.by, outcome.detail ?? null, expiresAt, repo, kind, name);
|
|
11780
|
+
// A gap that just filled ends its notification episode: the next failure
|
|
11781
|
+
// is a new fact, and must be allowed to say so.
|
|
11782
|
+
if (Number(changes) > 0 && outcome.status === "verified") {
|
|
11783
|
+
this.clearGapEpisode(repo, kind, name);
|
|
11784
|
+
if (prior?.["status"] !== "verified")
|
|
11785
|
+
this.bumpWake();
|
|
11786
|
+
}
|
|
11787
|
+
return Number(changes) > 0;
|
|
11788
|
+
});
|
|
11768
11789
|
}
|
|
11769
11790
|
/** Set what a task needs, as `kind:name` keys, replacing what it needed before. */
|
|
11770
11791
|
setRequirements(taskRef, keys, mutation = {}) {
|
|
@@ -16452,6 +16473,8 @@ export class Store {
|
|
|
16452
16473
|
generation = generation + 1, last_error = NULL
|
|
16453
16474
|
WHERE id = ? AND state = 'waiting-human'`)
|
|
16454
16475
|
.run(id);
|
|
16476
|
+
// The person's yes makes the merge actionable now: wake the loops (live-wakeups.md). The firing CAS re-proves it.
|
|
16477
|
+
this.bumpWake();
|
|
16455
16478
|
return { ok: true };
|
|
16456
16479
|
});
|
|
16457
16480
|
}
|
package/dist/telegram-lead.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { telegramCardMessage } from "./card-render.js";
|
|
2
2
|
import { cardOfProposal, cardPlainOf, cardsFirst, cardWithin, isCardProposal } from "./chat-card.js";
|
|
3
3
|
import { CHAT_ACTIONS, sharedActionAllowsChallenge, sharedActionPayload } from "./chat-actions.js";
|
|
4
|
-
import {
|
|
4
|
+
import { typedStartOf } from "./typed-start.js";
|
|
5
|
+
import { armedQuestion, taskInCeiling, mirrorToTaskChat, couldNotAnswerText, channelRepos as telegramConversationRepos, resolveChannelLead as resolveTelegramLead, NO_PHONE_LINK, NO_TASK_LINK, phoneProposalLink, confirmedLink, startedTaskLink, phoneProposalPreview, proposalOutcomeText, handoffCardText, confirmedCardText } from "./chat-channel.js";
|
|
5
6
|
export { channelRepos as telegramConversationRepos, resolveChannelLead as resolveTelegramLead, tooLongText, whichTaskText, replyContextFor, NO_PHONE_LINK, NO_TASK_LINK, proposalLink, confirmedLink, proposalPreview, proposalOutcomeText, handoffCardText, confirmedCardText, CHAT_ACTION_PARITY as TELEGRAM_ACTION_PARITY, parityGaps } from "./chat-channel.js";
|
|
6
7
|
/**
|
|
7
8
|
* The paired phone as one more way to use the same assistant.
|
|
@@ -26,7 +27,7 @@ export { channelRepos as telegramConversationRepos, resolveChannelLead as resolv
|
|
|
26
27
|
*/
|
|
27
28
|
import { createHash, randomBytes } from "node:crypto";
|
|
28
29
|
import { leadFailureText, runLeadTurn } from "./lead.js";
|
|
29
|
-
import { confirmLeadProposal, dismissLeadProposal } from "./lead-doors.js";
|
|
30
|
+
import { confirmLeadProposal, confirmTypedStart, dismissLeadProposal, TYPED_START_STARTED } from "./lead-doors.js";
|
|
30
31
|
import { ceilingDigestOf, verifyApproverStanding } from "./principal.js";
|
|
31
32
|
import { LEAD_ASK_OTHER } from "./store.js";
|
|
32
33
|
import { phoneText } from "./telegram-status.js";
|
|
@@ -129,7 +130,10 @@ export function mintCardTokens(store, binding, proposal, now, messageId, chatId
|
|
|
129
130
|
for (const [token, phase] of [[confirm, "confirm"], [dismiss, "dismiss"]]) {
|
|
130
131
|
store.createTelegramProposalAction({ token, binding: binding.id, proposal, phase, chatId, ttlMs: CARD_TTL_MS, ...(messageId === undefined ? {} : { messageId }) }, now);
|
|
131
132
|
}
|
|
132
|
-
|
|
133
|
+
// A Start card's button says what it does: the card's last line tells the person to tap it (or reply yes).
|
|
134
|
+
const saved = store.getLeadProposal(proposal);
|
|
135
|
+
const label = saved?.kind === "action" && sharedActionPayload(saved.payload)?.operation === "task_start" ? "Start work" : "Confirm";
|
|
136
|
+
return { keyboard: [[telegramButton(label, confirm), telegramButton("Dismiss", dismiss)]], tokens: [confirm, dismiss] };
|
|
133
137
|
}
|
|
134
138
|
/**
|
|
135
139
|
* One tap on a proposal card, applied inside the update's own transaction
|
|
@@ -249,7 +253,7 @@ export function applyProposalTap(store, binding, token, message, repos, options,
|
|
|
249
253
|
}
|
|
250
254
|
const irreversible = proposal.kind === "answer" && proposal.payload["reversible"] === false;
|
|
251
255
|
const sharedAction = proposal.kind === "action" ? sharedActionPayload(proposal.payload) : null;
|
|
252
|
-
const challenged = sharedAction !== null && sharedActionAllowsChallenge(sharedAction);
|
|
256
|
+
const challenged = sharedAction !== null && sharedActionAllowsChallenge(sharedAction, "telegram");
|
|
253
257
|
// The arm: two fresh one-time tokens make a real challenge, exactly as a
|
|
254
258
|
// decision button does. Nothing is answered or recorded here.
|
|
255
259
|
const arm = () => {
|
|
@@ -573,7 +577,11 @@ async function runTelegramConversation(row, args) {
|
|
|
573
577
|
const proposal = part.kind === "card" && part.proposal !== null ? store.getLeadProposal(part.proposal) : null;
|
|
574
578
|
const origin = proposal === null ? null : options.phoneOrigin?.() ?? null;
|
|
575
579
|
const wanted = proposal === null ? null : phoneProposalLink(store, proposal, repos);
|
|
576
|
-
|
|
580
|
+
// A typed yes that started a task (one-step-chat.md C5): Toolroll's reply carries the task's link, as a tapped
|
|
581
|
+
// card does. From the record and the origin read now, like every link.
|
|
582
|
+
const yes = part.kind === "reply" ? typedStartOf(store, "telegram", `m${row.id}`) : null;
|
|
583
|
+
const started = yes !== null && yes.outcome === "started" && yes.taskId !== null ? phoneLinkButton(options.phoneOrigin?.() ?? null, startedTaskLink(store, yes.taskId, repos)) : null;
|
|
584
|
+
const keyboard = keyboardWith(part.keyboard, started ?? phoneLinkButton(origin, wanted));
|
|
577
585
|
// A Toolroll card: read again now, shown only where every project behind it is still reachable. An unreadable
|
|
578
586
|
// one goes out as its saved plain words.
|
|
579
587
|
const isCard = proposal !== null && isCardProposal(proposal);
|
|
@@ -751,6 +759,62 @@ async function runTelegramConversation(row, args) {
|
|
|
751
759
|
return;
|
|
752
760
|
}
|
|
753
761
|
const repos = telegramConversationRepos(store, binding.approver, registry);
|
|
762
|
+
// A typed yes to a Start card (docs/plans/one-step-chat.md C5), recorded when the message arrived: Toolroll acts on it
|
|
763
|
+
// here and no lead turn runs for it. Its outcome and Toolroll's own reply are written in ONE transaction (the claim
|
|
764
|
+
// fences the reply; a lapsed claim takes the whole act back), then sent like any reply. A settled record returns its
|
|
765
|
+
// first outcome, so a redelivery or a crash starts nothing more.
|
|
766
|
+
const typed = typedStartOf(store, "telegram", `m${row.id}`);
|
|
767
|
+
if (typed !== null) {
|
|
768
|
+
const verified = verifyApproverStanding(store, binding.approver, binding.approverGeneration, repos);
|
|
769
|
+
// The card's turn is gone when the chat was ended (its proposals are deleted) or its project deleted: there is then
|
|
770
|
+
// no session to send a reply under, and nothing can have started. Toolroll says so itself. The yes is never passed on.
|
|
771
|
+
const unanchored = { said: null };
|
|
772
|
+
try {
|
|
773
|
+
store.transact(() => {
|
|
774
|
+
const now = clock();
|
|
775
|
+
const answered = confirmTypedStart(store, verified.ok ? verified.who : null, { provider: "telegram", event: typed.event }, now, { binding: binding.id, evidenceRoot: options.evidenceRoot });
|
|
776
|
+
// The reply is fenced on the session the card was composed under.
|
|
777
|
+
const card = store.getLeadProposal(typed.proposal);
|
|
778
|
+
const turn = card === null ? null : store.getLeadTurn(card.turn);
|
|
779
|
+
if (turn === null) {
|
|
780
|
+
// A start without its card can't happen (the door re-reads the card); if it ever did, take it back.
|
|
781
|
+
if (answered.ok)
|
|
782
|
+
throw new PlanRefused();
|
|
783
|
+
unanchored.said = answered.said;
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
// The card is answered: its buttons are spent with it.
|
|
787
|
+
if (answered.ok)
|
|
788
|
+
store.consumeTelegramProposalActions(typed.proposal, now);
|
|
789
|
+
if (!store.planTelegramConversationParts(row.id, owner, { session: turn.session, turn: turn.id }, [{ kind: "reply", text: answered.said, replyTo: row.messageId }], now))
|
|
790
|
+
throw new PlanRefused();
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
if (error instanceof PlanRefused)
|
|
795
|
+
return;
|
|
796
|
+
throw error;
|
|
797
|
+
}
|
|
798
|
+
if (unanchored.said !== null) {
|
|
799
|
+
await notify(unanchored.said);
|
|
800
|
+
finish({ state: "failed", outcome: "typed-start:not-counted" });
|
|
801
|
+
report.refused++;
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
// The card it answered is repainted as a tap's would be, best effort: its buttons are already spent, and the
|
|
805
|
+
// reply below says the same words with the same link.
|
|
806
|
+
const settled = typedStartOf(store, "telegram", typed.event);
|
|
807
|
+
if (settled?.outcome === "started" && settled.taskId !== null && pairingProblem() === null && held()) {
|
|
808
|
+
const link = phoneLinkButton(options.phoneOrigin?.() ?? null, startedTaskLink(store, settled.taskId, repos));
|
|
809
|
+
try {
|
|
810
|
+
await transport("editMessageText", { chat_id: chatId, message_id: Number(typed.cardMessage), text: `✓ ${TYPED_START_STARTED}`, link_preview_options: { is_disabled: true },
|
|
811
|
+
reply_markup: { inline_keyboard: link === null ? [] : [link] } });
|
|
812
|
+
}
|
|
813
|
+
catch { /* the reply below still says it */ }
|
|
814
|
+
}
|
|
815
|
+
await sendOrHandOff("typed-start");
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
754
818
|
// 4. A session bound before an earlier dispatch: the receipt lives there,
|
|
755
819
|
// whatever session is live today.
|
|
756
820
|
if (row.session !== null) {
|
package/dist/telegram-status.js
CHANGED
|
@@ -8,6 +8,8 @@ import { familyCompleted } from "./result-completion.js";
|
|
|
8
8
|
import { approvedCheckOf, approvedCheckStateOf, assignmentStatusFacts, checksFactOf, plainReasonOf, stageOfDispatch, taskStatusOf } from "./task-status.js";
|
|
9
9
|
import { assignmentChecksForRun, assignmentOf } from "./assignment.js";
|
|
10
10
|
import { CHAT_CONTROLS, chatControlHref, chatResultHref } from "./chat-controls.js";
|
|
11
|
+
import { homeFiguresLine, homeFiguresOf } from "./home-groups.js";
|
|
12
|
+
import { workIndexPage } from "./work-index.js";
|
|
11
13
|
export function phoneCommand(text) {
|
|
12
14
|
// Slash forms everywhere; the bare words too, because Slack and Discord
|
|
13
15
|
// keep unregistered slash text for themselves. Only a whole message that
|
|
@@ -106,10 +108,13 @@ function nextStep(d) {
|
|
|
106
108
|
* history. This snapshot is intentionally bounded and advertises that bound. */
|
|
107
109
|
export function phoneStatus(store, repos, now, focused = null, root) {
|
|
108
110
|
if (repos.length === 0)
|
|
109
|
-
return "No
|
|
111
|
+
return "No projects are connected here yet. Add a project in Toolroll, then send /status again.";
|
|
110
112
|
return store.transact(() => {
|
|
111
113
|
const snapshot = withDispatchDiagnoses(store, store.chatSnapshot(repos, now), now);
|
|
112
|
-
|
|
114
|
+
// It opens with Home's figures, from the work index totals over these projects (the Tasks lists' own counts);
|
|
115
|
+
// the groups below are dispatch diagnoses, which are not the work index's families, so they keep their headings.
|
|
116
|
+
const figures = homeFiguresOf(workIndexPage(store, now, { principal: "operator", repos, includeUnplaced: false }, { limit: 1, ...(root === undefined ? {} : { root }) }).totals);
|
|
117
|
+
const lines = [homeFiguresLine(figures), "", ...(focused === null ? [] : [`Talking about: ${focused} · /lead to switch back`, ""]), "Recent work", `As of ${now.toISOString().replace("T", " ").slice(0, 19)} UTC · ${repos.length} project(s)`, ""];
|
|
113
118
|
if (snapshot.tasksSaturated)
|
|
114
119
|
lines.push("Newest 60 tasks only — older work may still need attention.", "");
|
|
115
120
|
if (snapshot.tasks.length === 0)
|
|
@@ -126,7 +131,7 @@ export function phoneStatus(store, repos, now, focused = null, root) {
|
|
|
126
131
|
lines.push(` +${rows.length - 2} more in the console`);
|
|
127
132
|
lines.push("");
|
|
128
133
|
}
|
|
129
|
-
lines.push("Send /tasks to pick one, or /task <name> for its next step.
|
|
134
|
+
lines.push("Send /tasks to pick one, or /task <name> for its next step. This is how things stand right now.");
|
|
130
135
|
return lines.join("\n");
|
|
131
136
|
});
|
|
132
137
|
}
|
|
@@ -251,7 +256,7 @@ export function phoneTaskView(store, repos, id, now, root) {
|
|
|
251
256
|
lines.push(`Attempt ended: ${plain(latest.finishedAt, 40)}`);
|
|
252
257
|
else if (d.condition === "running") {
|
|
253
258
|
const checkpoint = store.latestCheckpointForRun(latest.id);
|
|
254
|
-
lines.push(checkpoint === null ? "No milestone progress has been recorded for this attempt." : `Latest agent
|
|
259
|
+
lines.push(checkpoint === null ? "No milestone progress has been recorded for this attempt." : `Latest update from the agent, not verified: ${plain(checkpoint.createdAt, 40)}.`);
|
|
255
260
|
}
|
|
256
261
|
}
|
|
257
262
|
let link = null;
|
package/dist/telegram.d.ts
CHANGED
|
@@ -148,6 +148,8 @@ export type BridgeReport = {
|
|
|
148
148
|
contention?: number;
|
|
149
149
|
/** v98: Telegram answered that it pushes this bot's updates to a webhook. */
|
|
150
150
|
webhookActive?: boolean;
|
|
151
|
+
/** The long poll was ended on purpose because a message was due to go out (live-wakeups.md): not a failure. */
|
|
152
|
+
deliveryWake?: boolean;
|
|
151
153
|
/** v98: pushed updates applied this pass. */
|
|
152
154
|
pushed?: number;
|
|
153
155
|
};
|
|
@@ -242,6 +244,8 @@ export declare function followBridge(store: Store, options: {
|
|
|
242
244
|
} | null;
|
|
243
245
|
/** v98: how often to check Telegram still pushes to us (tests shorten it). */
|
|
244
246
|
pushCheckMs?: number;
|
|
247
|
+
/** While a long poll is open, how often to look for a message due to go out (tests shorten it). */
|
|
248
|
+
deliveryCheckMs?: number;
|
|
245
249
|
}): Promise<FollowReport>;
|
|
246
250
|
/** The digest text: a header with the count and the window, then one
|
|
247
251
|
* fact per entry — its subject, then its body's first line, indented.
|
|
@@ -260,3 +264,9 @@ export declare function digestText(rows: readonly Notification[], since: string
|
|
|
260
264
|
export declare const TELEGRAM_MAX_WAITING = 10;
|
|
261
265
|
/** A pushed update was kept for this bot: wake its bridge if it runs in this process (docs/plans/responsive-chat.md §3.1). */
|
|
262
266
|
export declare function wakeTelegramBridge(botId: string): void;
|
|
267
|
+
/**
|
|
268
|
+
* Whether a message for this bot was written after `mark` and is still due: unresolved, addressed to everyone or to a
|
|
269
|
+
* person paired with this bot, and the bot not held back by Telegram's rate limit. Read-only; delivery itself still
|
|
270
|
+
* decides each row under its own fences.
|
|
271
|
+
*/
|
|
272
|
+
export declare function telegramOutboxDue(store: Store, botId: string, mark: number, now: Date): boolean;
|