toolroll 0.9.99 → 0.9.101
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 +18 -0
- package/dist/accent-colors.js +1 -1
- package/dist/acp-check.js +4 -2
- package/dist/activity-line.d.ts +1 -1
- package/dist/activity-line.js +1 -1
- package/dist/agent-state.js +2 -1
- package/dist/agents-board.js +3 -2
- package/dist/agents-live.js +3 -2
- package/dist/approval-authority.d.ts +2 -1
- package/dist/approval-authority.js +2 -1
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +32 -32
- package/dist/browser-workspace.d.ts +31 -2
- package/dist/chat-ask.js +4 -2
- package/dist/chat-core.js +4 -2
- package/dist/chat-decide.js +1 -2
- package/dist/chat-delivery-state.d.ts +29 -6
- package/dist/chat-delivery-state.js +43 -13
- package/dist/chat-delivery.js +12 -5
- package/dist/chat-flow.js +15 -13
- package/dist/chat-live.js +5 -2
- package/dist/chat-message-states.d.ts +75 -0
- package/dist/chat-message-states.js +74 -0
- package/dist/chat-messages.d.ts +10 -4
- package/dist/chat-messages.js +12 -7
- package/dist/check-failure.js +2 -1
- package/dist/check-report.js +1 -1
- package/dist/claim.d.ts +40 -2
- package/dist/claim.js +92 -19
- package/dist/contracts/chat-tables.js +3 -0
- package/dist/coordinator.js +3 -3
- package/dist/demo.js +7 -2
- package/dist/design/kit.css +94 -2
- package/dist/design/kit.d.ts +12 -4
- package/dist/design/kit.js +8 -2
- package/dist/design/tokens.css +5 -5
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-chat.js +2 -3
- package/dist/dispatch.js +2 -1
- package/dist/dispose.d.ts +4 -1
- package/dist/dispose.js +2 -1
- package/dist/fallback.js +3 -2
- package/dist/fast-start.js +2 -2
- package/dist/first-run.d.ts +10 -6
- package/dist/first-run.js +19 -18
- package/dist/guides.js +1 -1
- package/dist/monitoring.js +2 -1
- package/dist/operate.js +77 -36
- package/dist/plane-review.js +2 -1
- package/dist/project-knowledge.d.ts +3 -3
- package/dist/release-authority.d.ts +9 -0
- package/dist/release-authority.js +13 -0
- package/dist/release-auto.js +2 -3
- package/dist/release-gate.js +1 -1
- package/dist/release-view.js +2 -1
- package/dist/release.js +18 -30
- package/dist/result-shots.js +1 -1
- package/dist/route-results.js +2 -1
- package/dist/run-ending.d.ts +21 -0
- package/dist/run-ending.js +19 -0
- package/dist/scope.d.ts +5 -0
- package/dist/scope.js +10 -3
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +1 -1
- package/dist/server/console.css +46 -17
- package/dist/server/kit.d.ts +36 -3
- package/dist/server/kit.js +33 -4
- package/dist/server/render-chat.d.ts +2 -0
- package/dist/server/render-chat.js +1 -0
- package/dist/server/render-pages.js +1 -1
- package/dist/server/render-results.js +6 -5
- package/dist/server/render-tasks.d.ts +10 -0
- package/dist/server/render-tasks.js +234 -64
- package/dist/server/task-views.js +5 -1
- package/dist/settings-groups-ui.js +1 -1
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-chat.js +2 -3
- package/dist/store.d.ts +61 -2
- package/dist/store.js +141 -80
- package/dist/subagent-question.js +8 -6
- package/dist/surface.js +2 -2
- package/dist/task-archive.d.ts +3 -0
- package/dist/task-archive.js +5 -0
- package/dist/task-moves.d.ts +90 -0
- package/dist/task-moves.js +68 -0
- package/dist/task-status.d.ts +2 -2
- package/dist/task-status.js +3 -3
- package/dist/teams-chat.js +2 -2
- package/dist/telegram-flow.d.ts +1 -1
- package/dist/telegram-flow.js +1 -1
- package/dist/telegram-lead.d.ts +1 -2
- package/dist/telegram-lead.js +18 -23
- package/dist/telegram-progress.js +2 -1
- package/dist/telegram.d.ts +0 -1
- package/dist/telegram.js +6 -7
- package/dist/tui.js +1 -1
- package/dist/vanished-retry.js +3 -2
- package/dist/workspace-ui.d.ts +69 -0
- package/dist/workspace-ui.js +105 -1
- package/dist/worktree.js +3 -2
- package/package.json +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one door for writing a task's state (docs/plans/task-state-moves.md).
|
|
3
|
+
*
|
|
4
|
+
* A task is in one of five states: queued, running, done, failed, cancelled. TASK_MOVES lists every way code moves
|
|
5
|
+
* a task between them today: the state it writes and the states it may move from. moveTask writes one move as ONE
|
|
6
|
+
* statement, so the from-check and the write happen together inside whatever transaction the caller is in.
|
|
7
|
+
*
|
|
8
|
+
* Each from-list is copied from the statement it replaced, so this table records current behaviour exactly, except
|
|
9
|
+
* where docs/plans/closed-task-claims.md changed it (exhaust narrowed, claim added). The two moves marked
|
|
10
|
+
* `from: "any"` (complete, set) have no from-check and callers rely on that; making them narrower is a behaviour
|
|
11
|
+
* change with its own plan.
|
|
12
|
+
*
|
|
13
|
+
* Cancellation is not a move here: it has its own door, Store.applyCancellation, which also records why and who.
|
|
14
|
+
* Task creation (INSERT ... 'queued') is not a move either.
|
|
15
|
+
*
|
|
16
|
+
* This file imports nothing from store.ts at runtime (store.ts calls it), only types.
|
|
17
|
+
*/
|
|
18
|
+
import type { TaskState } from "./store.js";
|
|
19
|
+
export declare const TASK_MOVES: {
|
|
20
|
+
/** Back to the queue from a running attempt that ended without finishing (stop, service stop, sign-in expired,
|
|
21
|
+
* vanished builder, move to a fallback agent). */
|
|
22
|
+
readonly requeue: {
|
|
23
|
+
readonly to: "queued";
|
|
24
|
+
readonly from: readonly ["running"];
|
|
25
|
+
};
|
|
26
|
+
/** An unfinished task that cannot go on (retries stopped, fallback agent signed out). */
|
|
27
|
+
readonly giveUp: {
|
|
28
|
+
readonly to: "failed";
|
|
29
|
+
readonly from: readonly ["queued", "running"];
|
|
30
|
+
};
|
|
31
|
+
/** A person's Retry. */
|
|
32
|
+
readonly retry: {
|
|
33
|
+
readonly to: "queued";
|
|
34
|
+
readonly from: readonly ["failed", "queued", "done"];
|
|
35
|
+
};
|
|
36
|
+
/** The tracker item was opened again. */
|
|
37
|
+
readonly reopen: {
|
|
38
|
+
readonly to: "queued";
|
|
39
|
+
readonly from: readonly ["cancelled", "failed", "queued"];
|
|
40
|
+
};
|
|
41
|
+
/** The fenced completion of an attempt. Wide today: a late result still writes over a closed task. */
|
|
42
|
+
readonly complete: {
|
|
43
|
+
readonly to: readonly ["done", "failed"];
|
|
44
|
+
readonly from: "any";
|
|
45
|
+
};
|
|
46
|
+
/** Strikes used up (build and scouting). Only an open task: one a person already closed keeps its state
|
|
47
|
+
* (docs/plans/closed-task-claims.md, decision B). */
|
|
48
|
+
readonly exhaust: {
|
|
49
|
+
readonly to: "failed";
|
|
50
|
+
readonly from: readonly ["queued", "running"];
|
|
51
|
+
};
|
|
52
|
+
/** The CLI `claim`: taking a task makes it running, in the grant's own transaction. `running` is listed because
|
|
53
|
+
* taking over an expired claim on a task a CLI runner left running keeps it running. */
|
|
54
|
+
readonly claim: {
|
|
55
|
+
readonly to: "running";
|
|
56
|
+
readonly from: readonly ["queued", "running"];
|
|
57
|
+
};
|
|
58
|
+
/** Store.setTaskState (the state verb, backend.setState, release and others). Wide by design: it is the
|
|
59
|
+
* general-purpose write; cancelled goes through applyCancellation instead. */
|
|
60
|
+
readonly set: {
|
|
61
|
+
readonly to: readonly ["queued", "running", "done", "failed"];
|
|
62
|
+
readonly from: "any";
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
export type TaskMoveName = keyof typeof TASK_MOVES;
|
|
66
|
+
/** The smallest database handle moveTask needs (Store.handle fits). */
|
|
67
|
+
export type MoveDatabase = {
|
|
68
|
+
prepare(sql: string): {
|
|
69
|
+
run(...params: unknown[]): {
|
|
70
|
+
changes: number | bigint;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Move one task. One statement: the from-check and the write together. True when the row changed (false when the
|
|
76
|
+
* task does not exist or is not in one of the move's from-states). `to` is required for complete and set, and
|
|
77
|
+
* refused for every other move.
|
|
78
|
+
*/
|
|
79
|
+
export declare function moveTask(db: MoveDatabase, id: string, move: TaskMoveName, now: Date, to?: TaskState): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* SQL condition, true while a runner holds a live claim on the task: the claim is not released, has not expired, and
|
|
82
|
+
* is the newest generation for its task. It needs the `claim` table in scope under that name, and binds one
|
|
83
|
+
* parameter: the current time as an ISO string. This is ownership, not proof that a process is alive.
|
|
84
|
+
*/
|
|
85
|
+
export declare const claimedNowSql = "claim.released_at IS NULL AND claim.expires_at > ? AND claim.lease_generation = (SELECT MAX(newest.lease_generation) FROM claim AS newest WHERE newest.task_ref = claim.task_ref)";
|
|
86
|
+
/**
|
|
87
|
+
* SQL condition, true when the task's stored state is running (the `task` table in scope under that name). A task
|
|
88
|
+
* can be marked running with no live claim (see claimedNowSql), so the two can count differently.
|
|
89
|
+
*/
|
|
90
|
+
export declare const markedRunningSql = "task.state = 'running'";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const ALL_BUT_CANCELLED = ["queued", "running", "done", "failed"];
|
|
2
|
+
export const TASK_MOVES = {
|
|
3
|
+
/** Back to the queue from a running attempt that ended without finishing (stop, service stop, sign-in expired,
|
|
4
|
+
* vanished builder, move to a fallback agent). */
|
|
5
|
+
requeue: { to: "queued", from: ["running"] },
|
|
6
|
+
/** An unfinished task that cannot go on (retries stopped, fallback agent signed out). */
|
|
7
|
+
giveUp: { to: "failed", from: ["queued", "running"] },
|
|
8
|
+
/** A person's Retry. */
|
|
9
|
+
retry: { to: "queued", from: ["failed", "queued", "done"] },
|
|
10
|
+
/** The tracker item was opened again. */
|
|
11
|
+
reopen: { to: "queued", from: ["cancelled", "failed", "queued"] },
|
|
12
|
+
/** The fenced completion of an attempt. Wide today: a late result still writes over a closed task. */
|
|
13
|
+
complete: { to: ["done", "failed"], from: "any" },
|
|
14
|
+
/** Strikes used up (build and scouting). Only an open task: one a person already closed keeps its state
|
|
15
|
+
* (docs/plans/closed-task-claims.md, decision B). */
|
|
16
|
+
exhaust: { to: "failed", from: ["queued", "running"] },
|
|
17
|
+
/** The CLI `claim`: taking a task makes it running, in the grant's own transaction. `running` is listed because
|
|
18
|
+
* taking over an expired claim on a task a CLI runner left running keeps it running. */
|
|
19
|
+
claim: { to: "running", from: ["queued", "running"] },
|
|
20
|
+
/** Store.setTaskState (the state verb, backend.setState, release and others). Wide by design: it is the
|
|
21
|
+
* general-purpose write; cancelled goes through applyCancellation instead. */
|
|
22
|
+
set: { to: ALL_BUT_CANCELLED, from: "any" },
|
|
23
|
+
};
|
|
24
|
+
/** The SQL for one move: the guard is built from the table's own constants, never from caller input. */
|
|
25
|
+
function moveSql(rule) {
|
|
26
|
+
const base = "UPDATE task SET state = ?, updated_at = ? WHERE id = ?";
|
|
27
|
+
if (rule.from === "any")
|
|
28
|
+
return base;
|
|
29
|
+
return `${base} AND state IN (${rule.from.map(state => `'${state}'`).join(", ")})`;
|
|
30
|
+
}
|
|
31
|
+
/** The state a move writes; throws on a call that does not fit the table (a programming error, not a refusal). */
|
|
32
|
+
function targetOf(name, rule, to) {
|
|
33
|
+
if (to === "cancelled")
|
|
34
|
+
throw new Error("moveTask never writes cancelled: use applyCancellation");
|
|
35
|
+
if (typeof rule.to === "string") {
|
|
36
|
+
if (to !== undefined)
|
|
37
|
+
throw new Error(`the ${name} move always writes ${rule.to}; do not pass a target`);
|
|
38
|
+
return rule.to;
|
|
39
|
+
}
|
|
40
|
+
if (to === undefined)
|
|
41
|
+
throw new Error(`the ${name} move needs a target (${rule.to.join(" or ")})`);
|
|
42
|
+
if (!rule.to.includes(to))
|
|
43
|
+
throw new Error(`the ${name} move cannot write ${to}`);
|
|
44
|
+
return to;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Move one task. One statement: the from-check and the write together. True when the row changed (false when the
|
|
48
|
+
* task does not exist or is not in one of the move's from-states). `to` is required for complete and set, and
|
|
49
|
+
* refused for every other move.
|
|
50
|
+
*/
|
|
51
|
+
export function moveTask(db, id, move, now, to) {
|
|
52
|
+
const rule = Object.hasOwn(TASK_MOVES, move) ? TASK_MOVES[move] : undefined;
|
|
53
|
+
if (rule === undefined)
|
|
54
|
+
throw new Error(`unknown task move: ${String(move)}`);
|
|
55
|
+
const state = targetOf(move, rule, to);
|
|
56
|
+
return Number(db.prepare(moveSql(rule)).run(state, now.toISOString(), id).changes) > 0;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* SQL condition, true while a runner holds a live claim on the task: the claim is not released, has not expired, and
|
|
60
|
+
* is the newest generation for its task. It needs the `claim` table in scope under that name, and binds one
|
|
61
|
+
* parameter: the current time as an ISO string. This is ownership, not proof that a process is alive.
|
|
62
|
+
*/
|
|
63
|
+
export const claimedNowSql = "claim.released_at IS NULL AND claim.expires_at > ? AND claim.lease_generation = (SELECT MAX(newest.lease_generation) FROM claim AS newest WHERE newest.task_ref = claim.task_ref)";
|
|
64
|
+
/**
|
|
65
|
+
* SQL condition, true when the task's stored state is running (the `task` table in scope under that name). A task
|
|
66
|
+
* can be marked running with no live claim (see claimedNowSql), so the two can count differently.
|
|
67
|
+
*/
|
|
68
|
+
export const markedRunningSql = "task.state = 'running'";
|
package/dist/task-status.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export declare const OPEN_RESULT = "Open result";
|
|
|
18
18
|
export declare const MACHINE_CHECK_RUNNING = "Running the check on the prepared version.";
|
|
19
19
|
export type DetailKey = "checks" | "pull-request" | "requirements" | "evidence";
|
|
20
20
|
/** The row's small icon: the only coloured part of a detail. `failed` is red
|
|
21
|
-
* and only ever appears under the Failed headline; `note` is
|
|
21
|
+
* and only ever appears under the Failed headline; `note` is ink, with a caution mark. */
|
|
22
22
|
export type DetailMark = "ok" | "running" | "none" | "note" | "failed";
|
|
23
23
|
export type StatusAction = {
|
|
24
24
|
label: string;
|
|
@@ -302,7 +302,7 @@ export declare function statusWhyHtml(status: TaskStatus, extra?: readonly strin
|
|
|
302
302
|
detail: string;
|
|
303
303
|
}[]): Html;
|
|
304
304
|
/** Shared CSS for the server-rendered status: neutral rows, coloured icons. */
|
|
305
|
-
export declare const TASK_STATUS_CSS = ".status-headline{display:flex;align-items:center;gap:.6rem;margin:0;font-size:1.125rem;font-weight:600;color:var(--foreground)}.status-headline i{width:.625rem;height:.625rem;border-radius:999px;flex-shrink:0;background:var(--muted-foreground)}[data-headline-tone=live] .status-headline i{background:var(--so-info,#0d74ce)}[data-headline-tone=attention] .status-headline i{background:var(--so-attention,var(--attention))}[data-headline-tone=ready] .status-headline i{background:transparent;box-shadow:inset 0 0 0 2px var(--so-success,#218358)}[data-headline-tone=success] .status-headline i{background:var(--so-success,#218358)}[data-headline-tone=danger] .status-headline i{background:var(--so-danger,#c4320a)}.status-sentence{margin:.35rem 0 0;color:var(--muted-foreground)}.status-details{list-style:none;margin:.75rem 0 0;padding:.25rem 0 0;border-top:1px solid var(--so-line,var(--border))}.status-detail{display:grid;grid-template-columns:14px 7.5rem minmax(0,1fr);align-items:center;gap:.6rem;padding:.4rem 0;font-size:.8125rem;color:var(--foreground)}.status-detail .status-icon{align-self:center;color:var(--muted-foreground)}.status-detail--ok .status-icon{color:var(--so-success,#218358)}.status-detail--running .status-icon{color:var(--so-info,#0d74ce)}.status-detail--note .status-icon{color:var(--so-
|
|
305
|
+
export declare const TASK_STATUS_CSS = ".status-headline{display:flex;align-items:center;gap:.6rem;margin:0;font-size:1.125rem;font-weight:600;color:var(--foreground)}.status-headline i{width:.625rem;height:.625rem;border-radius:999px;flex-shrink:0;background:var(--muted-foreground)}[data-headline-tone=live] .status-headline i{background:var(--so-info,#0d74ce)}[data-headline-tone=attention] .status-headline i{background:var(--so-attention,var(--attention))}[data-headline-tone=ready] .status-headline i{background:transparent;box-shadow:inset 0 0 0 2px var(--so-success,#218358)}[data-headline-tone=success] .status-headline i{background:var(--so-success,#218358)}[data-headline-tone=danger] .status-headline i{background:var(--so-danger,#c4320a)}.status-sentence{margin:.35rem 0 0;color:var(--muted-foreground)}.status-details{list-style:none;margin:.75rem 0 0;padding:.25rem 0 0;border-top:1px solid var(--so-line,var(--border))}.status-detail{display:grid;grid-template-columns:14px 7.5rem minmax(0,1fr);align-items:center;gap:.6rem;padding:.4rem 0;font-size:.8125rem;color:var(--foreground)}.status-detail .status-icon{align-self:center;color:var(--muted-foreground)}.status-detail--ok .status-icon{color:var(--so-success,#218358)}.status-detail--running .status-icon{color:var(--so-info,#0d74ce)}.status-detail--note .status-icon{color:var(--so-ink)}.status-detail--failed .status-icon{color:var(--so-danger,#c4320a)}.status-detail-label{color:var(--muted-foreground)}.status-detail-text{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 .6rem;min-width:0;overflow-wrap:anywhere}.status-detail-text a{color:inherit}.status-detail-act{font-weight:600;white-space:nowrap}.status-detail--note .status-detail-act{color:var(--so-ink)}@media(max-width:480px){.status-detail{grid-template-columns:14px 6.5rem minmax(0,1fr);min-height:2.75rem;padding:.25rem 0}.status-detail-text a{display:inline-block;padding:.75rem 0;margin:-.75rem 0}}.status-why{border:0;padding:0;margin:.25rem 0 0;background:transparent;box-shadow:none}.status-why>summary{cursor:pointer;min-height:2.75rem;display:list-item;align-content:center;font-size:.8125rem;color:var(--muted-foreground)}.status-why p{margin:.25rem 0 .5rem}.receipt-note{display:flex;align-items:center;gap:.45rem;font-size:.8125rem;color:var(--foreground)}.receipt-note .status-icon{color:var(--so-ink)}";
|
|
306
306
|
/** A dispatch diagnosis as its stage, for the phone and CLI reads that hold
|
|
307
307
|
* no assignment. A done task is Complete once a person marked its result;
|
|
308
308
|
* a refuted result whose project check failed is Failed. */
|
package/dist/task-status.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Severity: red belongs to the Failed headline alone. A detail problem that
|
|
10
10
|
* doesn't undo the outcome (a pull request that couldn't open, shortened
|
|
11
|
-
* output) is an
|
|
11
|
+
* output) is an ink note on its own row with one action; the exact
|
|
12
12
|
* technical reason stays one tap away in `why`. */
|
|
13
13
|
import { DEFAULT_LEAD_NAME } from "./lead-identity.js";
|
|
14
14
|
import { html } from "./html.js";
|
|
@@ -209,7 +209,7 @@ function sentenceOf(headline, facts) {
|
|
|
209
209
|
case "Stopped": return reason ?? "Stopped by a person. The work so far is kept.";
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
-
/** A detail problem: red only under Failed, otherwise an
|
|
212
|
+
/** A detail problem: red only under Failed, otherwise an ink note. */
|
|
213
213
|
const problem = (headline) => headline === "Failed" ? "failed" : "note";
|
|
214
214
|
function detailsOf(headline, facts) {
|
|
215
215
|
const rows = [];
|
|
@@ -627,7 +627,7 @@ export function statusWhyHtml(status, extra = [], diagnostics = []) {
|
|
|
627
627
|
return html `<details class="status-why"><summary>${STATUS_MORE}</summary>${lines.map(one => html `<p class="meta">${one}</p>`)}${diagnostics.map(one => html `<p class="meta" data-work-diagnostic="${one.token}">${one.label} · ${one.detail}</p>`)}</details>`;
|
|
628
628
|
}
|
|
629
629
|
/** Shared CSS for the server-rendered status: neutral rows, coloured icons. */
|
|
630
|
-
export const TASK_STATUS_CSS = `.status-headline{display:flex;align-items:center;gap:.6rem;margin:0;font-size:1.125rem;font-weight:600;color:var(--foreground)}.status-headline i{width:.625rem;height:.625rem;border-radius:999px;flex-shrink:0;background:var(--muted-foreground)}[data-headline-tone=live] .status-headline i{background:var(--so-info,#0d74ce)}[data-headline-tone=attention] .status-headline i{background:var(--so-attention,var(--attention))}[data-headline-tone=ready] .status-headline i{background:transparent;box-shadow:inset 0 0 0 2px var(--so-success,#218358)}[data-headline-tone=success] .status-headline i{background:var(--so-success,#218358)}[data-headline-tone=danger] .status-headline i{background:var(--so-danger,#c4320a)}.status-sentence{margin:.35rem 0 0;color:var(--muted-foreground)}.status-details{list-style:none;margin:.75rem 0 0;padding:.25rem 0 0;border-top:1px solid var(--so-line,var(--border))}.status-detail{display:grid;grid-template-columns:14px 7.5rem minmax(0,1fr);align-items:center;gap:.6rem;padding:.4rem 0;font-size:.8125rem;color:var(--foreground)}.status-detail .status-icon{align-self:center;color:var(--muted-foreground)}.status-detail--ok .status-icon{color:var(--so-success,#218358)}.status-detail--running .status-icon{color:var(--so-info,#0d74ce)}.status-detail--note .status-icon{color:var(--so-
|
|
630
|
+
export const TASK_STATUS_CSS = `.status-headline{display:flex;align-items:center;gap:.6rem;margin:0;font-size:1.125rem;font-weight:600;color:var(--foreground)}.status-headline i{width:.625rem;height:.625rem;border-radius:999px;flex-shrink:0;background:var(--muted-foreground)}[data-headline-tone=live] .status-headline i{background:var(--so-info,#0d74ce)}[data-headline-tone=attention] .status-headline i{background:var(--so-attention,var(--attention))}[data-headline-tone=ready] .status-headline i{background:transparent;box-shadow:inset 0 0 0 2px var(--so-success,#218358)}[data-headline-tone=success] .status-headline i{background:var(--so-success,#218358)}[data-headline-tone=danger] .status-headline i{background:var(--so-danger,#c4320a)}.status-sentence{margin:.35rem 0 0;color:var(--muted-foreground)}.status-details{list-style:none;margin:.75rem 0 0;padding:.25rem 0 0;border-top:1px solid var(--so-line,var(--border))}.status-detail{display:grid;grid-template-columns:14px 7.5rem minmax(0,1fr);align-items:center;gap:.6rem;padding:.4rem 0;font-size:.8125rem;color:var(--foreground)}.status-detail .status-icon{align-self:center;color:var(--muted-foreground)}.status-detail--ok .status-icon{color:var(--so-success,#218358)}.status-detail--running .status-icon{color:var(--so-info,#0d74ce)}.status-detail--note .status-icon{color:var(--so-ink)}.status-detail--failed .status-icon{color:var(--so-danger,#c4320a)}.status-detail-label{color:var(--muted-foreground)}.status-detail-text{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 .6rem;min-width:0;overflow-wrap:anywhere}.status-detail-text a{color:inherit}.status-detail-act{font-weight:600;white-space:nowrap}.status-detail--note .status-detail-act{color:var(--so-ink)}@media(max-width:480px){.status-detail{grid-template-columns:14px 6.5rem minmax(0,1fr);min-height:2.75rem;padding:.25rem 0}.status-detail-text a{display:inline-block;padding:.75rem 0;margin:-.75rem 0}}.status-why{border:0;padding:0;margin:.25rem 0 0;background:transparent;box-shadow:none}.status-why>summary{cursor:pointer;min-height:2.75rem;display:list-item;align-content:center;font-size:.8125rem;color:var(--muted-foreground)}.status-why p{margin:.25rem 0 .5rem}.receipt-note{display:flex;align-items:center;gap:.45rem;font-size:.8125rem;color:var(--foreground)}.receipt-note .status-icon{color:var(--so-ink)}`;
|
|
631
631
|
/** A dispatch diagnosis as its stage, for the phone and CLI reads that hold
|
|
632
632
|
* no assignment. A done task is Complete once a person marked its result;
|
|
633
633
|
* a refuted result whose project check failed is Failed. */
|
package/dist/teams-chat.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { resultShotsPruned } from "./result-shots.js";
|
|
4
4
|
import { chatQuestionButtons } from "./subagent-question.js";
|
|
5
5
|
import { chatAskButtons } from "./chat-ask.js";
|
|
6
|
-
import { ChatDeliveryError, ChatState, chatHash, partContent } from "./chat-delivery-state.js";
|
|
6
|
+
import { ChatDeliveryError, ChatState, chatHash, chatRetryAt, partContent } from "./chat-delivery-state.js";
|
|
7
7
|
import { isLiveMessage, liveSpacingOf, underCooldown } from "./chat-live.js";
|
|
8
8
|
import { channelAccess, chatObject as object, heldForWatch, planChatNotifications, planRoomMessages, processChatEvent, splitChatText } from "./chat-delivery.js";
|
|
9
9
|
import { PLATFORM_LIMITS } from "./text-limits.js";
|
|
@@ -260,7 +260,7 @@ export async function deliverTeamsPart(options) {
|
|
|
260
260
|
return true;
|
|
261
261
|
}
|
|
262
262
|
const problem = error instanceof TeamsError ? error : new TeamsError("Teams delivery failed", 15_000, true);
|
|
263
|
-
state.partFailed(row.id, { problem: problem.message, until:
|
|
263
|
+
state.partFailed(row.id, { problem: problem.message, until: chatRetryAt(now, problem.retryMs, row.attempts), uncertain: problem.uncertain });
|
|
264
264
|
return true;
|
|
265
265
|
}
|
|
266
266
|
}
|
package/dist/telegram-flow.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type FlowChoiceVisit, type FlowSendContent } from "./flow-send.js";
|
|
2
2
|
import { type FlowStage } from "./flows.js";
|
|
3
3
|
import { type InlineButton } from "./telegram-lead.js";
|
|
4
|
-
import type
|
|
4
|
+
import { type FlowCardRow, type FlowRow, type Store, type TelegramBinding, type TelegramFlowAction, type TelegramFlowChoice, type TelegramFlowPrompt } from "./store.js";
|
|
5
5
|
/** A flow decision's notice: one per visit, and again (`:ask:<ms>`) when the owner asks the lead to approve it. */
|
|
6
6
|
export declare const FLOW_DECIDE_KEY: RegExp;
|
|
7
7
|
type Waiting = {
|
package/dist/telegram-flow.js
CHANGED
|
@@ -26,6 +26,7 @@ import { decideFlowCard, draftFor, flowCardHref, flowDefinitionOf } from "./flow
|
|
|
26
26
|
import { chooseFlowCard, flowChoiceAt, flowPersonOf, flowSendPaths, readFlowSend } from "./flow-send.js";
|
|
27
27
|
import { deciderOf, FLOW_END, replyTarget } from "./flows.js";
|
|
28
28
|
import { phoneLinkButton } from "./telegram-lead.js";
|
|
29
|
+
import { CONFIRM_TTL_MS } from "./store.js";
|
|
29
30
|
import { telegramButton } from "./contracts/telegram-callback.js";
|
|
30
31
|
/** A flow decision's notice: one per visit, and again (`:ask:<ms>`) when the owner asks the lead to approve it. */
|
|
31
32
|
export const FLOW_DECIDE_KEY = /^flow-decide:([1-9][0-9]{0,14}):([1-9][0-9]{0,9})(?::ask:[0-9]{1,15})?$/;
|
|
@@ -132,7 +133,6 @@ function freshButtons(store, binding, waiting, now) {
|
|
|
132
133
|
const minted = flowButtons(store, binding, waiting, now);
|
|
133
134
|
return { keyboard: minted.keyboard.map(row => row.flatMap(one => "callback_data" in one && typeof one.callback_data === "string" ? [{ label: one.text, token: one.callback_data }] : [])), place: minted.tokens };
|
|
134
135
|
}
|
|
135
|
-
const CONFIRM_TTL_MS = 10 * 60_000;
|
|
136
136
|
const FLOW_QUESTION = /\n\nApprove “[^\n]*”\? [^\n]*$/;
|
|
137
137
|
/** Where a card is now, for a message whose buttons no longer act: named and linked only when it is in one of the
|
|
138
138
|
* viewer's projects, and linked only while it still waits on someone. Never a live button. */
|
package/dist/telegram-lead.d.ts
CHANGED
|
@@ -13,9 +13,8 @@ export declare const CONVERSATION_MAX_AGE_MS: number;
|
|
|
13
13
|
/** Waiting for the lead itself (it is answering, or earlier messages wait in the person's lead queue) is bounded longer:
|
|
14
14
|
* two hours from the message's arrival, never reset as it moves from waiting behind the queue to waiting on a turn. */
|
|
15
15
|
export declare const CONVERSATION_WAIT_MAX_AGE_MS: number;
|
|
16
|
-
/** A card's buttons
|
|
16
|
+
/** A card's buttons live this long; its armed Yes and Cancel last CONFIRM_TTL_MS. */
|
|
17
17
|
export declare const CARD_TTL_MS: number;
|
|
18
|
-
export declare const CHALLENGE_TTL_MS: number;
|
|
19
18
|
/** Telegram's own message ceiling, with room for our part headers (the same bound telegram.ts splits at). */
|
|
20
19
|
export declare const PART_CAP = 3900;
|
|
21
20
|
export type TelegramConversationOptions = {
|
package/dist/telegram-lead.js
CHANGED
|
@@ -29,7 +29,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
29
29
|
import { leadFailureText, runLeadTurn } from "./lead.js";
|
|
30
30
|
import { confirmLeadProposal, confirmTypedStart, dismissLeadProposal, TYPED_START_STARTED } from "./lead-doors.js";
|
|
31
31
|
import { ceilingDigestOf, verifyApproverStanding } from "./principal.js";
|
|
32
|
-
import { LEAD_ASK_OTHER } from "./store.js";
|
|
32
|
+
import { CONFIRM_TTL_MS, LEAD_ASK_OTHER } from "./store.js";
|
|
33
33
|
import { phoneText } from "./telegram-status.js";
|
|
34
34
|
import { shapeReplyParts, telegramReply } from "./reply-shape.js";
|
|
35
35
|
import { createTelegramLiveDraft, forgetWaitingDraft, telegramAck, telegramWaitingDraft } from "./telegram-live.js";
|
|
@@ -37,6 +37,7 @@ import { TeamLeads } from "./team-leads.js";
|
|
|
37
37
|
import { chatResultHref } from "./chat-controls.js";
|
|
38
38
|
import { resultImageFileName, safeResultImageCaption, verifyResultImage } from "./chat-evidence.js";
|
|
39
39
|
import { telegramButton } from "./contracts/telegram-callback.js";
|
|
40
|
+
import { chatOutcome, isAdmittedWait, isLeadWait } from "./chat-message-states.js";
|
|
40
41
|
/** How long one claimed turn may go without a heartbeat before another poller may take it over. */
|
|
41
42
|
export const CONVERSATION_CLAIM_MS = 2 * 60_000;
|
|
42
43
|
/** A busy engine (one turn at a time) defers a queued message this long, up to the age bound below. */
|
|
@@ -45,11 +46,8 @@ export const CONVERSATION_MAX_AGE_MS = 10 * 60_000;
|
|
|
45
46
|
/** Waiting for the lead itself (it is answering, or earlier messages wait in the person's lead queue) is bounded longer:
|
|
46
47
|
* two hours from the message's arrival, never reset as it moves from waiting behind the queue to waiting on a turn. */
|
|
47
48
|
export const CONVERSATION_WAIT_MAX_AGE_MS = 2 * 3_600_000;
|
|
48
|
-
/** A
|
|
49
|
-
const ADMITTED_REGISTRY_WAIT = "registry-unavailable:admitted";
|
|
50
|
-
/** A card's buttons and an armed irreversible challenge live this long. */
|
|
49
|
+
/** A card's buttons live this long; its armed Yes and Cancel last CONFIRM_TTL_MS. */
|
|
51
50
|
export const CARD_TTL_MS = 24 * 3_600_000;
|
|
52
|
-
export const CHALLENGE_TTL_MS = 10 * 60_000;
|
|
53
51
|
/** Telegram's own message ceiling, with room for our part headers (the same bound telegram.ts splits at). */
|
|
54
52
|
export const PART_CAP = 3_900;
|
|
55
53
|
// ---- the ceiling and the principal --------------------------------------------
|
|
@@ -261,8 +259,8 @@ export function applyProposalTap(store, binding, token, message, repos, options,
|
|
|
261
259
|
const yes = randomBytes(16).toString("hex");
|
|
262
260
|
const cancel = randomBytes(16).toString("hex");
|
|
263
261
|
const placedOn = String(message.message_id);
|
|
264
|
-
store.createTelegramProposalAction({ token: yes, binding: binding.id, proposal: proposal.id, phase: "yes", chatId: tapChatId, messageId: placedOn, ttlMs:
|
|
265
|
-
store.createTelegramProposalAction({ token: cancel, binding: binding.id, proposal: proposal.id, phase: "cancel", chatId: tapChatId, messageId: placedOn, ttlMs:
|
|
262
|
+
store.createTelegramProposalAction({ token: yes, binding: binding.id, proposal: proposal.id, phase: "yes", chatId: tapChatId, messageId: placedOn, ttlMs: CONFIRM_TTL_MS }, now);
|
|
263
|
+
store.createTelegramProposalAction({ token: cancel, binding: binding.id, proposal: proposal.id, phase: "cancel", chatId: tapChatId, messageId: placedOn, ttlMs: CONFIRM_TTL_MS }, now);
|
|
266
264
|
const body = preview.text.split("\n\nConfirm or Dismiss below")[0];
|
|
267
265
|
if (challenged) {
|
|
268
266
|
ack("confirm it");
|
|
@@ -358,8 +356,6 @@ export async function processTelegramConversations(args) {
|
|
|
358
356
|
}
|
|
359
357
|
class PlanRefused extends Error {
|
|
360
358
|
}
|
|
361
|
-
/** The outcome a planned reply waits under until the bridge's cycle sends it. */
|
|
362
|
-
const ANSWERED_SENDING = "answered:sending";
|
|
363
359
|
/**
|
|
364
360
|
* One claimed message, in order of what is already known about it:
|
|
365
361
|
*
|
|
@@ -401,11 +397,10 @@ async function runTelegramConversation(row, args) {
|
|
|
401
397
|
const requeue = async (outcome) => {
|
|
402
398
|
// A turn already admitted for it waits for that turn's own outcome (bounded by the turn's deadline); waiting for the
|
|
403
399
|
// lead gets two hours from arrival; anything else, ten minutes.
|
|
404
|
-
const limit = outcome
|
|
405
|
-
: outcome === "busy" || outcome === "backlog" ? CONVERSATION_WAIT_MAX_AGE_MS : CONVERSATION_MAX_AGE_MS;
|
|
400
|
+
const limit = isAdmittedWait(outcome) ? Number.POSITIVE_INFINITY : isLeadWait(outcome) ? CONVERSATION_WAIT_MAX_AGE_MS : CONVERSATION_MAX_AGE_MS;
|
|
406
401
|
if (ageMs > limit) {
|
|
407
402
|
await notify(couldNotAnswerText(outcome === "registry-unavailable" ? "your projects couldn't be read" : "the assistant stayed busy with another message"));
|
|
408
|
-
finish({ state: "failed", outcome:
|
|
403
|
+
finish({ state: "failed", outcome: chatOutcome.gaveUp(outcome) });
|
|
409
404
|
report.refused++;
|
|
410
405
|
return;
|
|
411
406
|
}
|
|
@@ -413,7 +408,7 @@ async function runTelegramConversation(row, args) {
|
|
|
413
408
|
if (outcome === "backlog" && row.outcome !== "backlog")
|
|
414
409
|
await notify("Queued behind your earlier messages. It goes to the lead once they're answered.");
|
|
415
410
|
// Waiting for the lead: a draft that says so, refreshed while it waits (never over this chat's running turn).
|
|
416
|
-
if (outcome
|
|
411
|
+
if (isLeadWait(outcome)) {
|
|
417
412
|
await telegramWaitingDraft({ store, botId, transport, clock, chatId: row.chatId, binding: row.binding, draftId: row.id, allowed: () => stillPaired() && claimed(), ...(options.liveTiming === undefined ? {} : { timing: options.liveTiming }) });
|
|
418
413
|
}
|
|
419
414
|
finish({ state: "queued", outcome, nextAttemptAt: new Date(clock().getTime() + CONVERSATION_RETRY_MS).toISOString() });
|
|
@@ -483,7 +478,7 @@ async function runTelegramConversation(row, args) {
|
|
|
483
478
|
if (now.getTime() - Date.parse(part.createdAt) > DELIVERY_MAX_AGE_MS) {
|
|
484
479
|
report.problems.push(`telegram chat reply for update ${row.updateId} was not sent within a day: ${part.lastError ?? "unsent"}`);
|
|
485
480
|
await notify(couldNotAnswerText("my reply couldn't be sent for a day"));
|
|
486
|
-
finish({ state: "failed", outcome:
|
|
481
|
+
finish({ state: "failed", outcome: chatOutcome.unsentGaveUp(part.lastError) });
|
|
487
482
|
report.refused++;
|
|
488
483
|
return;
|
|
489
484
|
}
|
|
@@ -510,7 +505,7 @@ async function runTelegramConversation(row, args) {
|
|
|
510
505
|
if (problem !== null) {
|
|
511
506
|
report.problems.push(`telegram chat reply for update ${row.updateId} was not sent: ${problem}`);
|
|
512
507
|
await notify(`${problem.charAt(0).toUpperCase()}${problem.slice(1)}, so the assistant's reply was not sent. Nothing was changed. Send your message again if you still want it.`);
|
|
513
|
-
finish({ state: "failed", outcome:
|
|
508
|
+
finish({ state: "failed", outcome: chatOutcome.unsent(problem) });
|
|
514
509
|
report.refused++;
|
|
515
510
|
return;
|
|
516
511
|
}
|
|
@@ -708,7 +703,7 @@ async function runTelegramConversation(row, args) {
|
|
|
708
703
|
}
|
|
709
704
|
if (turn.state !== "answered") {
|
|
710
705
|
await notify(leadFailureText(turn.failureReason, store.leadTurnCostUnconfirmed(turn.id), turn.failureDetail ?? null));
|
|
711
|
-
finish({ state: "failed", outcome:
|
|
706
|
+
finish({ state: "failed", outcome: chatOutcome.replayedFailed(turn.failureReason) });
|
|
712
707
|
report.refused++;
|
|
713
708
|
return;
|
|
714
709
|
}
|
|
@@ -724,11 +719,11 @@ async function runTelegramConversation(row, args) {
|
|
|
724
719
|
await deliver(outcomeWord);
|
|
725
720
|
return;
|
|
726
721
|
}
|
|
727
|
-
finish({ state: "queued", outcome: outcomeWord === "answered" ?
|
|
722
|
+
finish({ state: "queued", outcome: outcomeWord === "answered" ? "answered:sending" : "delivering", nextAttemptAt: clock().toISOString() });
|
|
728
723
|
};
|
|
729
724
|
// 2. Parts already planned: the turn answered; only the sending remains.
|
|
730
725
|
if (store.listTelegramConversationParts(row.id).length > 0) {
|
|
731
|
-
await deliver(row.outcome ===
|
|
726
|
+
await deliver(row.outcome === "answered:sending" ? "answered" : "replayed");
|
|
732
727
|
return;
|
|
733
728
|
}
|
|
734
729
|
// 3. A turn already admitted for it and still working: wait for that turn's own outcome. Nothing here needs the
|
|
@@ -755,7 +750,7 @@ async function runTelegramConversation(row, args) {
|
|
|
755
750
|
// A turn already took it (its receipt is in the bound session): its answer exists or will, and is delivered once
|
|
756
751
|
// the projects can be read again — never abandoned to the outage.
|
|
757
752
|
const admitted = row.session !== null && store.leadRequestReceipt(row.session, row.request) !== null;
|
|
758
|
-
await requeue(admitted ?
|
|
753
|
+
await requeue(admitted ? "registry-unavailable:admitted" : "registry-unavailable");
|
|
759
754
|
return;
|
|
760
755
|
}
|
|
761
756
|
const repos = telegramConversationRepos(store, binding.approver, registry);
|
|
@@ -826,7 +821,7 @@ async function runTelegramConversation(row, args) {
|
|
|
826
821
|
}
|
|
827
822
|
// It waited (for the lead, or behind earlier messages), was never admitted, and is past the wait from its arrival:
|
|
828
823
|
// it is never sent this late, even when the lead is free now.
|
|
829
|
-
if ((row.outcome
|
|
824
|
+
if (isLeadWait(row.outcome) && ageMs > CONVERSATION_WAIT_MAX_AGE_MS) {
|
|
830
825
|
await requeue(row.outcome);
|
|
831
826
|
return;
|
|
832
827
|
}
|
|
@@ -839,7 +834,7 @@ async function runTelegramConversation(row, args) {
|
|
|
839
834
|
if (!resolved.ok) {
|
|
840
835
|
if (resolved.said !== null)
|
|
841
836
|
await notify(resolved.said);
|
|
842
|
-
finish({ state: "failed", outcome:
|
|
837
|
+
finish({ state: "failed", outcome: chatOutcome.refused(resolved.reason) });
|
|
843
838
|
report.refused++;
|
|
844
839
|
return;
|
|
845
840
|
}
|
|
@@ -924,7 +919,7 @@ async function runTelegramConversation(row, args) {
|
|
|
924
919
|
return;
|
|
925
920
|
}
|
|
926
921
|
await notify(phoneText(outcome.message, 1_000));
|
|
927
|
-
finish({ state: "failed", outcome:
|
|
922
|
+
finish({ state: "failed", outcome: chatOutcome.refused(outcome.refused) });
|
|
928
923
|
report.refused++;
|
|
929
924
|
return;
|
|
930
925
|
}
|
|
@@ -932,7 +927,7 @@ async function runTelegramConversation(row, args) {
|
|
|
932
927
|
// Truthful: what failed, that nothing was kept, and that a new message
|
|
933
928
|
// is a new turn. A failed turn's drafts are already gone.
|
|
934
929
|
await notify(phoneText(outcome.message, 1_000));
|
|
935
|
-
finish({ state: "failed", outcome:
|
|
930
|
+
finish({ state: "failed", outcome: chatOutcome.failed(outcome.failed) });
|
|
936
931
|
report.refused++;
|
|
937
932
|
return;
|
|
938
933
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* once the lead or a person has marked that exact result. Slack, Discord and
|
|
6
6
|
* Teams send this same card. */
|
|
7
7
|
import { machineChecked } from "./machine-check.js";
|
|
8
|
+
import { wasInterrupted } from "./run-ending.js";
|
|
8
9
|
import { assignmentOf } from "./assignment.js";
|
|
9
10
|
import { chatResultHref, chatControlHref } from "./chat-controls.js";
|
|
10
11
|
import { phoneText, projectLabel } from "./telegram-status.js";
|
|
@@ -72,7 +73,7 @@ export function telegramProgressCard(store, run, taskId, project, now = new Date
|
|
|
72
73
|
else if (run.outcome === "parked") {
|
|
73
74
|
facts = { stage: "needs-you", need: "answer" };
|
|
74
75
|
}
|
|
75
|
-
else if (run
|
|
76
|
+
else if (wasInterrupted(run)) {
|
|
76
77
|
facts = { stage: "stopped", reason: "The attempt was interrupted. Inspect the saved work, then resume." };
|
|
77
78
|
}
|
|
78
79
|
else {
|
package/dist/telegram.d.ts
CHANGED
|
@@ -24,7 +24,6 @@ export type TelegramReadProjects = () => Promise<readonly string[]>;
|
|
|
24
24
|
/** The environment name — and therefore the name the builder strips from agents (both names; see names.ts). */
|
|
25
25
|
export { TELEGRAM_TOKEN_ENV as TOKEN_ENV, TELEGRAM_TOKEN_ENVS as TOKEN_ENVS } from "./names.js";
|
|
26
26
|
export declare const PAIRING_TTL_MS: number;
|
|
27
|
-
export declare const CONFIRM_TTL_MS: number;
|
|
28
27
|
export declare const BRIDGE_LEASE_MS: number;
|
|
29
28
|
export declare const DELIVERY_CLAIM_MS: number;
|
|
30
29
|
/** Where Telegram pushes this bot's updates: under the public hooks address (Tailscale Funnel covers /hooks). */
|
package/dist/telegram.js
CHANGED
|
@@ -30,7 +30,7 @@ import { chmodSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
|
30
30
|
import { join } from "node:path";
|
|
31
31
|
import { closedQuestionWords, validateNote } from "./decision.js";
|
|
32
32
|
import { isShotsKind, resultShotsFor } from "./result-shots.js";
|
|
33
|
-
import { isLifecycleNotification, isTelegramProgressNotification, TELEGRAM_HOLD_REASONS } from "./store.js";
|
|
33
|
+
import { CONFIRM_TTL_MS, isLifecycleNotification, isTelegramProgressNotification, TELEGRAM_HOLD_REASONS } from "./store.js";
|
|
34
34
|
import { telegramProgressCard } from "./telegram-progress.js";
|
|
35
35
|
import { enqueueEveningDigests, finishedView, isTaskFact, joinsBatch, needsPerson, quietCardView } from "./chat-quiet.js";
|
|
36
36
|
import { BATCH_MS, chatText, chatTitle, factLinkLabel, mentions, nameTelegramBot } from "./chat-voice.js";
|
|
@@ -59,7 +59,6 @@ export { TELEGRAM_TOKEN_ENV as TOKEN_ENV, TELEGRAM_TOKEN_ENVS as TOKEN_ENVS } fr
|
|
|
59
59
|
/** BotFather's shape: numeric bot id, colon, secret. */
|
|
60
60
|
const TOKEN_SHAPE = /^(\d+):[A-Za-z0-9_-]{20,}$/;
|
|
61
61
|
export const PAIRING_TTL_MS = 10 * 60_000;
|
|
62
|
-
export const CONFIRM_TTL_MS = 10 * 60_000;
|
|
63
62
|
export const BRIDGE_LEASE_MS = 2 * 60_000;
|
|
64
63
|
export const DELIVERY_CLAIM_MS = 2 * 60_000;
|
|
65
64
|
/** Telegram's own message ceiling, with room for our part headers. */
|
|
@@ -2575,7 +2574,7 @@ function applyCallback(context, update, effects) {
|
|
|
2575
2574
|
const draft = store.liveNoteDraft(binding.id, decision.id, clock());
|
|
2576
2575
|
const digest = draft === null ? undefined : noteDigestOf(draft.note);
|
|
2577
2576
|
if (draft !== null)
|
|
2578
|
-
store.setNoteDraftState(draft.id, "armed");
|
|
2577
|
+
store.setNoteDraftState(draft.id, ["pending", "armed"], "armed");
|
|
2579
2578
|
store.createTelegramAction({ token: confirm, binding: binding.id, decision: decision.id, optionId: option.id, phase: "confirm", chatId: binding.chatId, messageId: placedOn, ttlMs: CONFIRM_TTL_MS, ...(digest === undefined ? {} : { noteDigest: digest }) }, clock());
|
|
2580
2579
|
store.createTelegramAction({ token: cancel, binding: binding.id, decision: decision.id, optionId: option.id, phase: "cancel", chatId: binding.chatId, messageId: placedOn, ttlMs: CONFIRM_TTL_MS }, clock());
|
|
2581
2580
|
ack("irreversible — confirm it");
|
|
@@ -2620,7 +2619,7 @@ function applyCallback(context, update, effects) {
|
|
|
2620
2619
|
{
|
|
2621
2620
|
const draft = store.liveNoteDraft(binding.id, decision.id, clock());
|
|
2622
2621
|
if (draft !== null)
|
|
2623
|
-
store.setNoteDraftState(draft.id, "discarded");
|
|
2622
|
+
store.setNoteDraftState(draft.id, ["pending", "armed"], "discarded");
|
|
2624
2623
|
}
|
|
2625
2624
|
const fresh = decision.options.map(option => ({ option, token: randomBytes(16).toString("hex") }));
|
|
2626
2625
|
for (const { option, token: choose } of fresh) {
|
|
@@ -2642,7 +2641,7 @@ function answerNow(context, decision, choice, binding, ack, editText) {
|
|
|
2642
2641
|
const answered = store.answerDecisionLocked({ id: decision.id, choice, by: binding.approver, via: "telegram", ...(draft === null ? {} : { note: draft.note }) }, clock());
|
|
2643
2642
|
if (answered.ok) {
|
|
2644
2643
|
if (draft !== null)
|
|
2645
|
-
store.setNoteDraftState(draft.id, "consumed");
|
|
2644
|
+
store.setNoteDraftState(draft.id, ["pending", "armed"], "consumed");
|
|
2646
2645
|
report.answered++;
|
|
2647
2646
|
ack(`✓ ${choice}${draft === null ? "" : " — with your note"}`);
|
|
2648
2647
|
editText(answeredText(store, answered.decision));
|
|
@@ -2650,7 +2649,7 @@ function answerNow(context, decision, choice, binding, ack, editText) {
|
|
|
2650
2649
|
}
|
|
2651
2650
|
if (answered.reason === "already-answered" || answered.reason === "closed") {
|
|
2652
2651
|
if (draft !== null)
|
|
2653
|
-
store.setNoteDraftState(draft.id, "discarded");
|
|
2652
|
+
store.setNoteDraftState(draft.id, ["pending", "armed"], "discarded");
|
|
2654
2653
|
const settled = store.getDecision(decision.id);
|
|
2655
2654
|
ack(`${settled?.supersededReason != null ? closedQuestionWords(settled.supersededReason) : `already answered: ${settled?.choice ?? "?"}`}${draft === null ? "" : " — your note did NOT travel"}`);
|
|
2656
2655
|
if (settled !== null)
|
|
@@ -2702,7 +2701,7 @@ function expiredDraftGuard(store, binding, decisionId, now) {
|
|
|
2702
2701
|
.get(binding.id, decisionId, now.toISOString());
|
|
2703
2702
|
if (expired === undefined)
|
|
2704
2703
|
return false;
|
|
2705
|
-
store.setNoteDraftState(Number(expired["id"]), "discarded");
|
|
2704
|
+
store.setNoteDraftState(Number(expired["id"]), ["pending", "armed"], "discarded");
|
|
2706
2705
|
return true;
|
|
2707
2706
|
}
|
|
2708
2707
|
/** Bridges in this process waiting out their push idle, by bot: a kept push ends the wait early. */
|