toolroll 0.9.97 โ 0.9.98
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -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/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/desktop-update.d.ts +1 -1
- 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/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/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/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;
|
package/dist/telegram.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* BotFather token plus `bridge telegram unpair` is the recovery, and both
|
|
19
19
|
* are one command.
|
|
20
20
|
*/
|
|
21
|
+
import { recordTypedStart, typedStartCard, typedYes } from "./typed-start.js";
|
|
21
22
|
import { telegramCardMessage } from "./card-render.js";
|
|
22
23
|
import { flowDoneCard } from "./flow-result-card.js";
|
|
23
24
|
import { applyTelegramQuestionReply, applyTelegramQuestionTap, openQuestionOf, telegramQuestionButtons } from "./subagent-question.js";
|
|
@@ -352,6 +353,8 @@ export const MAX_POLL_SECONDS = 25;
|
|
|
352
353
|
const FOLLOW_BACKOFF_MS = [1_000, 2_000, 4_000, 8_000, 16_000, 30_000];
|
|
353
354
|
/** A cycle that returned instantly with nothing is padded to this โ a scripted or broken server must not spin the loop hot. */
|
|
354
355
|
const FOLLOW_IDLE_FLOOR_MS = 1_000;
|
|
356
|
+
/** While a long poll is open, how often the follower looks for a message due to go out (live-wakeups.md). */
|
|
357
|
+
const DELIVERY_CHECK_MS = 1_000;
|
|
355
358
|
/** v98: how often the bridge checks Telegram still pushes to us; how long pushes may keep failing before it asks
|
|
356
359
|
* for updates itself; and how long it asks before trying pushes again. */
|
|
357
360
|
const PUSH_CHECK_MS = 60_000, PUSH_GIVE_UP_MS = 5 * 60_000, PUSH_RETRY_MS = 15 * 60_000, PUSH_IDLE_MS = 2_000;
|
|
@@ -532,6 +535,8 @@ export async function followBridge(store, options) {
|
|
|
532
535
|
const startedAt = Date.now();
|
|
533
536
|
const report = { sent: 0, answered: 0, paired: 0, ignored: 0, backlog: false, problems: [] };
|
|
534
537
|
await managePush(report);
|
|
538
|
+
// What this cycle's delivery below covers: anything newer may end the long poll early.
|
|
539
|
+
const deliveredThrough = newestNotificationIdOf(store);
|
|
535
540
|
if (options.deliver !== false) {
|
|
536
541
|
await deliverOutbox(store, botId, transport, owner, clock, report, options.readProjects, options.canDeliver, options.conversation?.phoneOrigin, options.conversation?.evidenceRoot);
|
|
537
542
|
await deliverTeam(store, botId, transport, clock, report, options.readProjects, options.canDeliver, options.conversation?.phoneOrigin);
|
|
@@ -547,7 +552,43 @@ export async function followBridge(store, options) {
|
|
|
547
552
|
report.pushed = pushed;
|
|
548
553
|
if (!pushing()) {
|
|
549
554
|
// While a turn is running, poll briefly so its reply is sent within a second or two of being written.
|
|
550
|
-
|
|
555
|
+
const seconds = inFlight === null ? pollSeconds : 1;
|
|
556
|
+
// A DELIVERY WAKE (live-wakeups.md): while the long poll is open, a message written for this bot since this
|
|
557
|
+
// cycle began delivering ends the poll, once per cycle, so it goes out now instead of after the poll. Same
|
|
558
|
+
// owner, same generation, one connection; the cursor moves only past updates already processed.
|
|
559
|
+
const poll = new AbortController();
|
|
560
|
+
const endPoll = () => poll.abort();
|
|
561
|
+
if (signal.aborted)
|
|
562
|
+
poll.abort();
|
|
563
|
+
else
|
|
564
|
+
signal.addEventListener("abort", endPoll, { once: true });
|
|
565
|
+
let woke = false;
|
|
566
|
+
const checker = options.deliver !== false && seconds > 1
|
|
567
|
+
? setInterval(() => {
|
|
568
|
+
if (woke || poll.signal.aborted)
|
|
569
|
+
return;
|
|
570
|
+
let due = false;
|
|
571
|
+
try {
|
|
572
|
+
due = telegramOutboxDue(store, botId, deliveredThrough, clock());
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
due = false;
|
|
576
|
+
}
|
|
577
|
+
if (!due)
|
|
578
|
+
return;
|
|
579
|
+
woke = true;
|
|
580
|
+
poll.abort();
|
|
581
|
+
}, options.deliveryCheckMs ?? DELIVERY_CHECK_MS)
|
|
582
|
+
: null;
|
|
583
|
+
checker?.unref?.();
|
|
584
|
+
try {
|
|
585
|
+
await drainUpdates(store, botId, transport, owner, lease.generation, lease.cursor, clock, report, seconds, poll.signal, options.readProjects, options.conversation, () => woke && !signal.aborted);
|
|
586
|
+
}
|
|
587
|
+
finally {
|
|
588
|
+
if (checker !== null)
|
|
589
|
+
clearInterval(checker);
|
|
590
|
+
signal.removeEventListener("abort", endPoll);
|
|
591
|
+
}
|
|
551
592
|
// A push address nobody here wants any more (the public address was removed): take it down, so asking works again.
|
|
552
593
|
if (report.webhookActive === true && push === null)
|
|
553
594
|
await transport("deleteWebhook", { drop_pending_updates: false }, signal);
|
|
@@ -587,7 +628,8 @@ export async function followBridge(store, options) {
|
|
|
587
628
|
const took = Date.now() - startedAt;
|
|
588
629
|
// While Telegram pushes (v98) there is no long poll to wait on: a pushed update waits at most this long.
|
|
589
630
|
const floor = pushing() ? PUSH_IDLE_MS : FOLLOW_IDLE_FLOOR_MS;
|
|
590
|
-
|
|
631
|
+
// A delivery wake goes straight back round to send: no padding.
|
|
632
|
+
if (!signal.aborted && report.deliveryWake !== true && report.sent === 0 && report.answered === 0 && (report.chatAnswered ?? 0) === 0 && took < floor) {
|
|
591
633
|
// While Telegram pushes, a push kept by this process's hook ends the idle at once.
|
|
592
634
|
await (pushing() ? idle(floor - took) : wait(floor - took));
|
|
593
635
|
}
|
|
@@ -1484,7 +1526,9 @@ function taskOf(store, decision) {
|
|
|
1484
1526
|
const run = store.getRun(decision.run);
|
|
1485
1527
|
return run === null ? "?" : store.externalIdFor(run.taskRef) ?? "?";
|
|
1486
1528
|
}
|
|
1487
|
-
async function drainUpdates(store, botId, transport, owner, generation, cursor, clock, report, pollSeconds = 0, signal, readProjects, conversation
|
|
1529
|
+
async function drainUpdates(store, botId, transport, owner, generation, cursor, clock, report, pollSeconds = 0, signal, readProjects, conversation,
|
|
1530
|
+
/** True when the poll was ended on purpose for a delivery: an answer that failed then is that abort, not the wire. */
|
|
1531
|
+
deliveryWake) {
|
|
1488
1532
|
const context = { store, botId, transport, clock, report, readProjects, conversation, projects: null };
|
|
1489
1533
|
let offset = cursor + 1;
|
|
1490
1534
|
for (let page = 0; page < PAGE_BUDGET; page++) {
|
|
@@ -1495,6 +1539,12 @@ async function drainUpdates(store, botId, transport, owner, generation, cursor,
|
|
|
1495
1539
|
allowed_updates: ["message", "callback_query"],
|
|
1496
1540
|
}, signal);
|
|
1497
1541
|
if (!answer.ok) {
|
|
1542
|
+
// Ended on purpose so a due message goes out now: no problem, no backoff; the next cycle asks again from the
|
|
1543
|
+
// same cursor, so nothing is lost.
|
|
1544
|
+
if (deliveryWake?.() === true) {
|
|
1545
|
+
report.deliveryWake = true;
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1498
1548
|
const said = answer.description ?? "failed";
|
|
1499
1549
|
// v98: another program asked Telegram for this bot's updates, and Telegram ended our
|
|
1500
1550
|
// request in its favour. Nothing is lost: an update is only gone once someone
|
|
@@ -1723,7 +1773,14 @@ function applyMessage(context, update, effects) {
|
|
|
1723
1773
|
return;
|
|
1724
1774
|
// Replies to decisions retain their existing note meaning, even if the
|
|
1725
1775
|
// note starts with a slash. New read commands are direct messages only.
|
|
1726
|
-
|
|
1776
|
+
// A bare "start" as the very next message after a Start card is that card's yes (docs/plans/one-step-chat.md C5),
|
|
1777
|
+
// not the help command. Only the routing is decided here, by the same ids the message path reads again when it
|
|
1778
|
+
// records the yes; "/start", and "start" anywhere else, stay help.
|
|
1779
|
+
const yesSeat = message.reply_to_message === undefined && chat.type === "private" && typeof message.text === "string" && typedYes(message.text)
|
|
1780
|
+
&& context.conversation !== undefined && context.readProjects !== undefined ? store.liveTelegramBindingFor(botId, String(from.id)) : null;
|
|
1781
|
+
const answersStartCard = yesSeat !== null && String(chat.id) === yesSeat.chatId
|
|
1782
|
+
&& typedStartCard(store, { binding: yesSeat.id, chatId: yesSeat.chatId, approver: yesSeat.approver }, { id: String(message.message_id), replyTo: null }) !== null;
|
|
1783
|
+
if (message.reply_to_message === undefined && !answersStartCard && applyPhoneRead(context, update, effects))
|
|
1727
1784
|
return;
|
|
1728
1785
|
// A reply to a decision message this bot sent is a note, exactly as
|
|
1729
1786
|
// before. Everything else that is ordinary text talks to the shared
|
|
@@ -1902,10 +1959,19 @@ function applyConversation(context, update, effects) {
|
|
|
1902
1959
|
report.ignored++;
|
|
1903
1960
|
return;
|
|
1904
1961
|
}
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1962
|
+
// A typed yes to a Start card (docs/plans/one-step-chat.md C5) is read here, before any model runs, and recorded with
|
|
1963
|
+
// the message in the same transaction: the fixed words only, tied to one exact card by message ids and Toolroll's own
|
|
1964
|
+
// delivery record. The worker acts on it (confirmTypedStart) and no lead turn runs for it. Anything else is queued for
|
|
1965
|
+
// the lead exactly as before.
|
|
1966
|
+
store.transact(() => {
|
|
1967
|
+
const typed = typedYes(text) ? typedStartCard(store, { binding: binding.id, chatId: binding.chatId, approver: binding.approver }, { id: String(message.message_id), replyTo }) : null;
|
|
1968
|
+
store.enqueueTelegramConversation({
|
|
1969
|
+
binding, updateId: update.update_id, messageId: String(message.message_id), replyTo,
|
|
1970
|
+
request: telegramRequestId(botId, binding.id, update.update_id), text, context: context_, taskId, sourceRun,
|
|
1971
|
+
}, clock());
|
|
1972
|
+
if (typed !== null)
|
|
1973
|
+
recordTypedStart(store, `m${update.update_id}`, typed, clock());
|
|
1974
|
+
});
|
|
1909
1975
|
report.chatQueued = (report.chatQueued ?? 0) + 1;
|
|
1910
1976
|
// Saved: ๐ on the owner's message right away (docs/plans/responsive-chat.md ยง3.1), once this update's transaction
|
|
1911
1977
|
// has committed. It comes off when the answer or the failure notice is delivered. Never awaited: a burst of messages
|
|
@@ -2653,3 +2719,21 @@ export function wakeTelegramBridge(botId) {
|
|
|
2653
2719
|
for (const wake of wakers)
|
|
2654
2720
|
wake();
|
|
2655
2721
|
}
|
|
2722
|
+
/** The newest notification id now: a follower cycle's mark for what its delivery covers. */
|
|
2723
|
+
function newestNotificationIdOf(store) {
|
|
2724
|
+
return Number(store.handle.prepare("SELECT COALESCE(MAX(id), 0) AS id FROM notification").get()?.["id"] ?? 0);
|
|
2725
|
+
}
|
|
2726
|
+
/**
|
|
2727
|
+
* Whether a message for this bot was written after `mark` and is still due: unresolved, addressed to everyone or to a
|
|
2728
|
+
* person paired with this bot, and the bot not held back by Telegram's rate limit. Read-only; delivery itself still
|
|
2729
|
+
* decides each row under its own fences.
|
|
2730
|
+
*/
|
|
2731
|
+
export function telegramOutboxDue(store, botId, mark, now) {
|
|
2732
|
+
const approvers = [...new Set(store.liveTelegramBindings(botId).map(binding => binding.approver))];
|
|
2733
|
+
if (approvers.length === 0)
|
|
2734
|
+
return false;
|
|
2735
|
+
if (store.telegramRetryAt(botId) > now.toISOString())
|
|
2736
|
+
return false;
|
|
2737
|
+
return store.handle.prepare(`SELECT 1 AS hit FROM notification WHERE id > ? AND resolved_at IS NULL
|
|
2738
|
+
AND (recipient IS NULL OR recipient IN (SELECT value FROM json_each(?))) LIMIT 1`).get(mark, JSON.stringify(approvers)) !== undefined;
|
|
2739
|
+
}
|
package/dist/toolroll-update.js
CHANGED
|
@@ -44,6 +44,7 @@ import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES,
|
|
|
44
44
|
import { V152_ADDED_COLUMNS } from "./code-map-store.js";
|
|
45
45
|
import { V153_ADDED_COLUMNS } from "./plain-schema.js";
|
|
46
46
|
import { V158_ADDED_COLUMNS } from "./lead-latency-schema.js";
|
|
47
|
+
import { V159_ADDED_COLUMNS } from "./one-step-chat-schema.js";
|
|
47
48
|
import { isNewer, REGISTRY } from "./releases.js";
|
|
48
49
|
import { markNeverIndex } from "./never-index.js";
|
|
49
50
|
import { readRuntimeUpdateJournal, RUNTIME_PHASES, RUNTIME_UPDATE_STEPS, stagedStartedAt, updaterStartingOf } from "./contracts/update-journal.js";
|
|
@@ -432,7 +433,8 @@ const HISTORY_RULES = {
|
|
|
432
433
|
// v156 adds tables only (fallback chains): likewise.
|
|
433
434
|
// v158 (after v157's chat-app tables): a saved lead step reads no tool names and no failure detail, and a saved reply no route and no failure detail
|
|
434
435
|
// (NULL); the quick-answer switch is a new table.
|
|
435
|
-
|
|
436
|
+
// v159: a saved fast-start preview reads no lead turn (NULL); a typed yes's record is a new table.
|
|
437
|
+
...Object.fromEntries(Object.entries(addedColumns(V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS, V138_ADDED_COLUMNS, V143_ADDED_COLUMNS, V147_ADDED_COLUMNS, V148_ADDED_COLUMNS, V150_ADDED_COLUMNS, V151_ADDED_COLUMNS, V152_ADDED_COLUMNS, V153_ADDED_COLUMNS, V158_ADDED_COLUMNS, V159_ADDED_COLUMNS))
|
|
436
438
|
.map(([table, rule]) => [table, { ...EARLIER_RULES[table], ...rule }])),
|
|
437
439
|
};
|
|
438
440
|
/** The tables a migration may remove whole (v115's removed features). */
|
package/dist/tui-contract.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export type TuiRow = {
|
|
|
17
17
|
/** The attempt running now, if any: what Stop names. */
|
|
18
18
|
readonly liveRunId: number | null;
|
|
19
19
|
readonly repo: string | null;
|
|
20
|
+
/** Home's group (home-groups.ts homeGroupOf): waits on you, working, done, or none (cancelled, replaced). */
|
|
21
|
+
readonly group: "you" | "working" | "done" | null;
|
|
20
22
|
};
|
|
21
23
|
/** A parked question on a task, answered by choosing one of its options. */
|
|
22
24
|
export type TuiDecision = {
|
package/dist/tui-source.js
CHANGED
|
@@ -18,6 +18,7 @@ import { localModeOf, openStoreReadOnly } from "./store.js";
|
|
|
18
18
|
import { routeChangeOf } from "./route-change.js";
|
|
19
19
|
import { flakyCount } from "./flaky.js";
|
|
20
20
|
import { memoryWaiting } from "./lesson-notes.js";
|
|
21
|
+
import { homeGroupOf } from "./home-groups.js";
|
|
21
22
|
import { terminalText } from "./terminal-text.js";
|
|
22
23
|
import { welcomeHere, welcomeLines } from "./welcome-facts.js";
|
|
23
24
|
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
@@ -74,6 +75,8 @@ export function createTuiSource(options) {
|
|
|
74
75
|
return {
|
|
75
76
|
id: words(row["id"], words(row["rootId"])), title: shown(row["title"]), status: shown(status["label"], words(row["state"])), detail: shown(status["detail"]),
|
|
76
77
|
needsYou: views.includes("needs-you"), liveRunId: typeof row["liveRunId"] === "number" ? row["liveRunId"] : null, repo: typeof row["repo"] === "string" ? row["repo"] : null,
|
|
78
|
+
// The same rule as Home's groups, over the same work index item.
|
|
79
|
+
group: homeGroupOf({ status: { views: views.filter((one) => typeof one === "string") } }),
|
|
77
80
|
};
|
|
78
81
|
});
|
|
79
82
|
const waiting = rows.filter(one => one.needsYou);
|