toolroll 0.9.99 → 0.9.100
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 +11 -0
- package/dist/acp-check.js +4 -2
- 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/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/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/monitoring.js +2 -1
- package/dist/operate.js +75 -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.js +2 -2
- package/dist/server/render-results.js +3 -2
- package/dist/server/render-tasks.js +3 -2
- 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 +127 -77
- package/dist/subagent-question.js +8 -6
- package/dist/surface.js +2 -2
- package/dist/task-moves.d.ts +90 -0
- package/dist/task-moves.js +68 -0
- 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/vanished-retry.js +3 -2
- package/dist/workspace-ui.js +2 -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/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. */
|
package/dist/vanished-retry.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { authorityStandsSql } from "./approval-authority.js";
|
|
20
20
|
import { stopFactOf } from "./store.js";
|
|
21
21
|
import { refArchived } from "./task-archive.js";
|
|
22
|
+
import { moveTask } from "./task-moves.js";
|
|
22
23
|
/** The automatic-retry limits, in one place. */
|
|
23
24
|
export const RETRY_LIMIT = 3;
|
|
24
25
|
export const RETRY_SPEND_TIMES = 2;
|
|
@@ -48,7 +49,7 @@ function approvalStands(store, taskRef, now) {
|
|
|
48
49
|
WHERE task_scope.approved_digest = task_scope.digest AND task_scope.approved_at IS NOT NULL AND ${authorityStandsSql("task_scope", "task_ref.repo", "?")}`).get(taskRef, stamp, stamp, stamp, stamp) !== undefined;
|
|
49
50
|
}
|
|
50
51
|
function requeue(store, taskId, now) {
|
|
51
|
-
return
|
|
52
|
+
return moveTask(store.handle, taskId, "requeue", now);
|
|
52
53
|
}
|
|
53
54
|
function episodeOf(store, taskRef) {
|
|
54
55
|
const row = store.handle.prepare("SELECT retries, reserved_microusd FROM retry_episode WHERE task_ref = ?").get(taskRef);
|
|
@@ -59,7 +60,7 @@ function stopFor(store, input) {
|
|
|
59
60
|
store.handle.prepare(`INSERT INTO retry_episode (task_ref, retries, reserved_microusd, stopped, stopped_run, started_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
60
61
|
ON CONFLICT (task_ref) DO UPDATE SET stopped = excluded.stopped, stopped_run = excluded.stopped_run, reserved_microusd = excluded.reserved_microusd, updated_at = excluded.updated_at`)
|
|
61
62
|
.run(input.taskRef, input.retries, input.reservedMicrousd, input.why, input.run, stamp, stamp);
|
|
62
|
-
store.handle
|
|
63
|
+
moveTask(store.handle, input.taskId, "giveUp", input.now);
|
|
63
64
|
store.enqueueNotification({
|
|
64
65
|
source: { run: input.run },
|
|
65
66
|
dedupeKey: `vanished:${input.run}`,
|
package/dist/workspace-ui.js
CHANGED
|
@@ -23,6 +23,7 @@ import { manualReviewOnly, plainReasonWords } from "./proof.js";
|
|
|
23
23
|
import { CHECK_COMMAND_CHANGED, CHECK_UNREADABLE, OPEN_RESULT, approvedCheckOf, approvedCheckStateOf, plainReasonOf, stageOfCode, taskStatusOf, workToneOf } from "./task-status.js";
|
|
24
24
|
import { whenUtc } from "./when-html.js";
|
|
25
25
|
import { NEEDS } from "./needs-you.js";
|
|
26
|
+
import { wasInterrupted } from "./run-ending.js";
|
|
26
27
|
import { html, postForm } from "./html.js";
|
|
27
28
|
export const WORK_VIEWS = [
|
|
28
29
|
{ key: "all", label: "All", hint: "Every task in view, most urgent first.", empty: "Nothing is in progress. Describe work in chat or add a task, and it appears here." },
|
|
@@ -50,7 +51,7 @@ export function reviewFactsOf(retry, reviewerAlive, automaticPending = false) {
|
|
|
50
51
|
attempts: retry.attempts.length,
|
|
51
52
|
retriesRemaining: retry.retriesRemaining,
|
|
52
53
|
latestReason: retry.latest?.reason ?? null,
|
|
53
|
-
interrupted: retry.latest !== null && (retry.latest
|
|
54
|
+
interrupted: retry.latest !== null && wasInterrupted(retry.latest),
|
|
54
55
|
queuedBy: retry.openRequest?.requestedBy ?? null,
|
|
55
56
|
queuedOrigin: retry.openRequest?.origin ?? null,
|
|
56
57
|
reviewerAlive: retry.live !== null && reviewerAlive(retry.live.runner),
|
package/dist/worktree.js
CHANGED
|
@@ -34,6 +34,7 @@ import { HANDOFF_PREFIX, MAILBOX_SUFFIX, looksLikeProtocolFile, readMailbox } fr
|
|
|
34
34
|
import { parseHandoff } from "./decision.js";
|
|
35
35
|
import { diskBytes } from "./storage.js";
|
|
36
36
|
import { markNeverIndex } from "./never-index.js";
|
|
37
|
+
import { endedInterrupted } from "./run-ending.js";
|
|
37
38
|
import { checkoutGit, gitIncidentRecord, teardownGit } from "./teardown-git.js";
|
|
38
39
|
/**
|
|
39
40
|
* The note a lease leaves in the checkout, naming the process holding it.
|
|
@@ -598,7 +599,7 @@ export class WorktreePool {
|
|
|
598
599
|
// failed/interrupted source required here.
|
|
599
600
|
if (prior.parentRun !== null) {
|
|
600
601
|
const source = this.store.getRun(prior.parentRun);
|
|
601
|
-
if (source
|
|
602
|
+
if (source !== null && endedInterrupted(source)) {
|
|
602
603
|
return { runId: source.id, kind: "partial" };
|
|
603
604
|
}
|
|
604
605
|
}
|
|
@@ -619,7 +620,7 @@ export class WorktreePool {
|
|
|
619
620
|
if (unhanded !== null && unhanded.outcome === "failed" && unhanded.reason === "no-handoff") {
|
|
620
621
|
return brokeTree(attempt) || brokeTree(later) ? null : { runId: unhanded.id, kind: "partial" };
|
|
621
622
|
}
|
|
622
|
-
if (prior.outcome !== null && (prior
|
|
623
|
+
if (prior.outcome !== null && !endedInterrupted(prior))
|
|
623
624
|
return null;
|
|
624
625
|
let names;
|
|
625
626
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.100",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|