toolroll 0.9.98 → 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 +16 -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/checkout-cleanup.js +2 -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/deploy-phase-data.json +21 -0
- package/dist/deploy-phase.d.ts +37 -0
- package/dist/deploy-phase.js +64 -0
- package/dist/deploy-rescue.d.ts +2 -1
- package/dist/deploy-rescue.js +6 -7
- package/dist/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-adapters.js +30 -14
- 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.d.ts +3 -3
- package/dist/release-gate.js +8 -5
- package/dist/release-next.d.ts +3 -3
- package/dist/release-next.js +13 -16
- 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/staged-runtimes.js +2 -1
- 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
package/dist/coordinator.js
CHANGED
|
@@ -21,6 +21,7 @@ import { canonicalRepos } from "./runner.js";
|
|
|
21
21
|
import { fileTaskProposal } from "./proposal.js";
|
|
22
22
|
import { describeScope } from "./scope.js";
|
|
23
23
|
import { reportSummaryFor } from "./lead-tools.js";
|
|
24
|
+
import { claimedNowSql, markedRunningSql } from "./task-moves.js";
|
|
24
25
|
import { taskWorkSummaryOf } from "./work-summary.js";
|
|
25
26
|
import { assignmentOf, assignmentBrief } from "./assignment.js";
|
|
26
27
|
/** Module-private: the ONLY maker, reachable only through the token
|
|
@@ -286,8 +287,7 @@ export function statusFor(store, who, now) {
|
|
|
286
287
|
// finding 6): a live, unexpired, newest-generation claim.
|
|
287
288
|
const running = one(`SELECT COUNT(DISTINCT claim.task_ref) AS n FROM claim
|
|
288
289
|
JOIN task_ref ON task_ref.id = claim.task_ref AND task_ref.backend = 'built-in'
|
|
289
|
-
WHERE task_ref.repo IN (${sql}) AND
|
|
290
|
-
AND claim.lease_generation = (SELECT MAX(newest.lease_generation) FROM claim AS newest WHERE newest.task_ref = claim.task_ref)`, [now.toISOString()]);
|
|
290
|
+
WHERE task_ref.repo IN (${sql}) AND ${claimedNowSql}`, [now.toISOString()]);
|
|
291
291
|
const incidents = one(`SELECT COUNT(*) AS n FROM incident JOIN run ON run.id = incident.run
|
|
292
292
|
JOIN task_ref ON task_ref.id = run.task_ref AND task_ref.backend = 'built-in'
|
|
293
293
|
WHERE task_ref.repo IN (${sql}) AND incident.resolved_at IS NULL`);
|
|
@@ -298,7 +298,7 @@ export function statusFor(store, who, now) {
|
|
|
298
298
|
const repos = store.handle
|
|
299
299
|
.prepare(`SELECT task_ref.repo AS repo,
|
|
300
300
|
SUM(CASE WHEN task.state = 'queued' THEN 1 ELSE 0 END) AS queued,
|
|
301
|
-
SUM(CASE WHEN
|
|
301
|
+
SUM(CASE WHEN ${markedRunningSql} THEN 1 ELSE 0 END) AS running
|
|
302
302
|
FROM task_ref JOIN task ON task.id = task_ref.external_id AND task_ref.backend = 'built-in'
|
|
303
303
|
WHERE task_ref.repo IN (${sql}) GROUP BY task_ref.repo ORDER BY task_ref.repo`)
|
|
304
304
|
.all(...args)
|
package/dist/demo.js
CHANGED
|
@@ -1342,7 +1342,7 @@ export function createDemoLead(input) {
|
|
|
1342
1342
|
return id;
|
|
1343
1343
|
}
|
|
1344
1344
|
};
|
|
1345
|
-
/** Each stage's store facts, written once, in order. */
|
|
1345
|
+
/** Each stage's store facts, written once, in order. False when the stage can't start (its task was closed). */
|
|
1346
1346
|
const enterStage = (exchange, stage, at) => {
|
|
1347
1347
|
const plan = exchange.plan;
|
|
1348
1348
|
const taskId = exchange.taskId;
|
|
@@ -1356,6 +1356,9 @@ export function createDemoLead(input) {
|
|
|
1356
1356
|
}
|
|
1357
1357
|
const route = store.approvedRouteOf(taskId);
|
|
1358
1358
|
const claimed = acquire(store, ref, CREW, { now: at, token: crewSeen(at), ttlMs: 10 * 60_000 });
|
|
1359
|
+
// A task closed meanwhile (cancelled, say) is not built: no run starts and the story stops here.
|
|
1360
|
+
if (!claimed.ok && (claimed.reason === "closed" || claimed.reason === "no-task"))
|
|
1361
|
+
return false;
|
|
1359
1362
|
const leaseId = claimed.ok ? claimed.claim.leaseId : `demo-lease-${taskId}`;
|
|
1360
1363
|
const run = store.startRun({
|
|
1361
1364
|
taskRef: ref,
|
|
@@ -1379,6 +1382,7 @@ export function createDemoLead(input) {
|
|
|
1379
1382
|
}
|
|
1380
1383
|
if (stage === 3)
|
|
1381
1384
|
finish(exchange, at);
|
|
1385
|
+
return true;
|
|
1382
1386
|
};
|
|
1383
1387
|
const finish = (exchange, at) => {
|
|
1384
1388
|
const plan = exchange.plan;
|
|
@@ -1454,7 +1458,8 @@ export function createDemoLead(input) {
|
|
|
1454
1458
|
const current = exchange.stage === null ? -1 : DEMO_STAGES.indexOf(exchange.stage);
|
|
1455
1459
|
for (let stage = current + 1; stage <= target; stage++) {
|
|
1456
1460
|
const at = new Date(exchange.approvedAt + [0, 1, 3, 4][stage] * stepMs);
|
|
1457
|
-
enterStage(exchange, stage, at)
|
|
1461
|
+
if (!enterStage(exchange, stage, at))
|
|
1462
|
+
break;
|
|
1458
1463
|
if (stage < 3)
|
|
1459
1464
|
exchange.stage = DEMO_STAGES[stage];
|
|
1460
1465
|
else {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"phases": [
|
|
3
|
+
{ "phase": "preparing", "action": "lift-pause", "flags": ["beforeSwap", "beforePause"] },
|
|
4
|
+
{ "phase": "admission-paused", "action": "lift-pause", "flags": ["beforeSwap"] },
|
|
5
|
+
{ "phase": "frozen", "action": "lift-pause", "flags": ["beforeSwap"] },
|
|
6
|
+
{ "phase": "backup-verified", "action": "lift-pause", "flags": ["beforeSwap"] },
|
|
7
|
+
{ "phase": "rehearsed", "action": "lift-pause", "flags": ["beforeSwap"] },
|
|
8
|
+
{ "phase": "stopping", "action": "roll-back", "flags": [] },
|
|
9
|
+
{ "phase": "stopped", "action": "roll-back", "flags": ["restartable"] },
|
|
10
|
+
{ "phase": "migrating", "action": "roll-back", "flags": ["migrationStarted", "restartable"] },
|
|
11
|
+
{ "phase": "migrated", "action": "roll-back", "flags": ["migrationStarted", "restartable"] },
|
|
12
|
+
{ "phase": "starting", "action": "forward", "flags": [] },
|
|
13
|
+
{ "phase": "started", "action": "forward", "flags": [] },
|
|
14
|
+
{ "phase": "healthy", "action": "forward", "flags": [] },
|
|
15
|
+
{ "phase": "start-failed", "action": "roll-back", "flags": ["migrationStarted", "restartable"] },
|
|
16
|
+
{ "phase": "backup-restored", "action": "roll-back", "flags": ["restartable"] },
|
|
17
|
+
{ "phase": "restored", "action": "roll-back", "flags": [] },
|
|
18
|
+
{ "phase": "released", "action": "none", "flags": [] },
|
|
19
|
+
{ "phase": "deployed", "action": "none", "flags": ["deployed"] }
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** What `toolroll release` does with a deployment that stopped at a phase; "refuse" for a word that is no phase. */
|
|
2
|
+
export type DeployAction = "none" | "lift-pause" | "forward" | "roll-back" | "refuse";
|
|
3
|
+
type RowAction = Exclude<DeployAction, "refuse">;
|
|
4
|
+
type Flag = "beforeSwap" | "beforePause" | "migrationStarted" | "restartable" | "deployed";
|
|
5
|
+
type Row = {
|
|
6
|
+
readonly phase: string;
|
|
7
|
+
readonly action: RowAction;
|
|
8
|
+
readonly flags: readonly Flag[];
|
|
9
|
+
};
|
|
10
|
+
/** The rows of a parsed data file, or an Error naming what is wrong with it. Never a partial table. */
|
|
11
|
+
export declare function deployPhaseRows(data: unknown): readonly Row[];
|
|
12
|
+
/** Every phase, in journal order. */
|
|
13
|
+
export declare const DEPLOY_PHASES: readonly string[];
|
|
14
|
+
export declare const isDeployPhase: (phase: unknown) => phase is string;
|
|
15
|
+
/** The one action a journal's phase calls for; "refuse" for anything that is not a phase. */
|
|
16
|
+
export declare const deployActionOf: (phase: unknown) => DeployAction;
|
|
17
|
+
/** The word itself, or an Error: a journal is never written with a word the table does not hold. */
|
|
18
|
+
export declare function assertDeployPhase(phase: string): string;
|
|
19
|
+
/** The service was never stopped. */
|
|
20
|
+
export declare const BEFORE_SWAP: readonly string[];
|
|
21
|
+
/** Before the swap, with the pause in place. */
|
|
22
|
+
export declare const PAUSED_BEFORE_SWAP: readonly string[];
|
|
23
|
+
/** The candidate's store opened the live database, or the new service ran on it. */
|
|
24
|
+
export declare const MIGRATION_STARTED: readonly string[];
|
|
25
|
+
/** The old service is proved stopped: the previous definition may be started again. */
|
|
26
|
+
export declare const RESTARTABLE: readonly string[];
|
|
27
|
+
/** The new service may be running: never rolled back by a journal alone. */
|
|
28
|
+
export declare const FORWARD_ONLY: readonly string[];
|
|
29
|
+
/** Nothing is left to recover. Not the same as deployed: a released deployment never installed its runtime. */
|
|
30
|
+
export declare const NEEDS_NO_RECOVERY: readonly string[];
|
|
31
|
+
/** The deployment finished and its runtime is the installed one. */
|
|
32
|
+
export declare const isDeployed: (phase: unknown) => boolean;
|
|
33
|
+
/** Stopped where nothing was changed (before its pause), or with nothing left to do. */
|
|
34
|
+
export declare const stoppedWhereNothingChanged: (phase: unknown) => boolean;
|
|
35
|
+
/** The phase a lifted pause leaves behind (release-gate.ts). */
|
|
36
|
+
export declare const RELEASED: string;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The phases of a browser deployment's journal, and what a stopped deployment at each one calls for.
|
|
3
|
+
*
|
|
4
|
+
* The rows are data: deploy-phase-data.json beside this file, the one authored list (the build copies it into dist,
|
|
5
|
+
* scripts/postbuild.mjs). The deploy scripts read the same file through scripts/deploy-phase-table.mjs, where each
|
|
6
|
+
* column is described. Both sides check its shape the same way; deploy-phase.test.ts holds them to each other and to
|
|
7
|
+
* its own hand-written expectations.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
const ACTIONS = ["none", "lift-pause", "forward", "roll-back"];
|
|
12
|
+
const FLAGS = ["beforeSwap", "beforePause", "migrationStarted", "restartable", "deployed"];
|
|
13
|
+
/** The rows of a parsed data file, or an Error naming what is wrong with it. Never a partial table. */
|
|
14
|
+
export function deployPhaseRows(data) {
|
|
15
|
+
const rows = data?.phases;
|
|
16
|
+
if (!Array.isArray(rows) || rows.length === 0)
|
|
17
|
+
throw new Error("The deployment phase table has no rows.");
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
return Object.freeze(rows.map((row) => {
|
|
20
|
+
const { phase, action, flags } = (row ?? {});
|
|
21
|
+
if (typeof phase !== "string" || !/^[a-z]+(-[a-z]+)*$/.test(phase) || seen.has(phase))
|
|
22
|
+
throw new Error(`The deployment phase table has a bad or repeated phase: ${String(phase)}.`);
|
|
23
|
+
if (typeof action !== "string" || !ACTIONS.includes(action))
|
|
24
|
+
throw new Error(`The deployment phase ${phase} has no known action.`);
|
|
25
|
+
if (!Array.isArray(flags) || flags.some(flag => typeof flag !== "string" || !FLAGS.includes(flag)) || new Set(flags).size !== flags.length)
|
|
26
|
+
throw new Error(`The deployment phase ${phase} has an unknown or repeated flag.`);
|
|
27
|
+
seen.add(phase);
|
|
28
|
+
return Object.freeze({ phase, action: action, flags: Object.freeze([...flags]) });
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
// By path, not by URL: a test environment that stands in for a browser replaces the global URL.
|
|
32
|
+
const ROWS = deployPhaseRows(JSON.parse(readFileSync(join(import.meta.dirname, "deploy-phase-data.json"), "utf8")));
|
|
33
|
+
const rowOf = (phase) => typeof phase === "string" ? ROWS.find(row => row.phase === phase) : undefined;
|
|
34
|
+
const withFlag = (flag) => Object.freeze(ROWS.filter(row => row.flags.includes(flag)).map(row => row.phase));
|
|
35
|
+
const withAction = (action) => Object.freeze(ROWS.filter(row => row.action === action).map(row => row.phase));
|
|
36
|
+
/** Every phase, in journal order. */
|
|
37
|
+
export const DEPLOY_PHASES = Object.freeze(ROWS.map(row => row.phase));
|
|
38
|
+
export const isDeployPhase = (phase) => rowOf(phase) !== undefined;
|
|
39
|
+
/** The one action a journal's phase calls for; "refuse" for anything that is not a phase. */
|
|
40
|
+
export const deployActionOf = (phase) => rowOf(phase)?.action ?? "refuse";
|
|
41
|
+
/** The word itself, or an Error: a journal is never written with a word the table does not hold. */
|
|
42
|
+
export function assertDeployPhase(phase) {
|
|
43
|
+
if (!isDeployPhase(phase))
|
|
44
|
+
throw new Error(`"${String(phase)}" is not a deployment phase; the journal was not written.`);
|
|
45
|
+
return phase;
|
|
46
|
+
}
|
|
47
|
+
/** The service was never stopped. */
|
|
48
|
+
export const BEFORE_SWAP = withFlag("beforeSwap");
|
|
49
|
+
/** Before the swap, with the pause in place. */
|
|
50
|
+
export const PAUSED_BEFORE_SWAP = Object.freeze(ROWS.filter(row => row.flags.includes("beforeSwap") && !row.flags.includes("beforePause")).map(row => row.phase));
|
|
51
|
+
/** The candidate's store opened the live database, or the new service ran on it. */
|
|
52
|
+
export const MIGRATION_STARTED = withFlag("migrationStarted");
|
|
53
|
+
/** The old service is proved stopped: the previous definition may be started again. */
|
|
54
|
+
export const RESTARTABLE = withFlag("restartable");
|
|
55
|
+
/** The new service may be running: never rolled back by a journal alone. */
|
|
56
|
+
export const FORWARD_ONLY = withAction("forward");
|
|
57
|
+
/** Nothing is left to recover. Not the same as deployed: a released deployment never installed its runtime. */
|
|
58
|
+
export const NEEDS_NO_RECOVERY = withAction("none");
|
|
59
|
+
/** The deployment finished and its runtime is the installed one. */
|
|
60
|
+
export const isDeployed = (phase) => rowOf(phase)?.flags.includes("deployed") === true;
|
|
61
|
+
/** Stopped where nothing was changed (before its pause), or with nothing left to do. */
|
|
62
|
+
export const stoppedWhereNothingChanged = (phase) => { const row = rowOf(phase); return row !== undefined && (row.flags.includes("beforePause") || row.action === "none"); };
|
|
63
|
+
/** The phase a lifted pause leaves behind (release-gate.ts). */
|
|
64
|
+
export const RELEASED = assertDeployPhase("released");
|
package/dist/deploy-rescue.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { FORWARD_ONLY } from "./deploy-phase.js";
|
|
1
2
|
/** Phases where the new service may be running: recovery leaves them to the release, which resumes forward. */
|
|
2
|
-
export
|
|
3
|
+
export { FORWARD_ONLY };
|
|
3
4
|
export type RescueWorld = {
|
|
4
5
|
/** The live database. */
|
|
5
6
|
database: string;
|
package/dist/deploy-rescue.js
CHANGED
|
@@ -18,10 +18,9 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { realpathSync } from "node:fs";
|
|
20
20
|
import { join } from "node:path";
|
|
21
|
-
import { BEFORE_SWAP } from "./
|
|
21
|
+
import { BEFORE_SWAP, FORWARD_ONLY, MIGRATION_STARTED, NEEDS_NO_RECOVERY, RESTARTABLE } from "./deploy-phase.js";
|
|
22
22
|
/** Phases where the new service may be running: recovery leaves them to the release, which resumes forward. */
|
|
23
|
-
export
|
|
24
|
-
const FINISHED = Object.freeze(["released", "deployed"]);
|
|
23
|
+
export { FORWARD_ONLY };
|
|
25
24
|
const real = (path) => { try {
|
|
26
25
|
return realpathSync(path);
|
|
27
26
|
}
|
|
@@ -39,8 +38,8 @@ export function rescuePlan(phase, journal) {
|
|
|
39
38
|
if (phase === "stopping") {
|
|
40
39
|
words.push("The old service must already be stopped. Recovery checks for remaining processes and refuses while any may still be running. Stop them through the service owner, then rerun recovery.");
|
|
41
40
|
}
|
|
42
|
-
if (
|
|
43
|
-
if (
|
|
41
|
+
if (phase === "stopping" || RESTARTABLE.includes(phase)) {
|
|
42
|
+
if (MIGRATION_STARTED.includes(phase))
|
|
44
43
|
words.push("Puts the verified backup back first when the previous runtime can't read the migrated database.");
|
|
45
44
|
words.push(`Starts the previous service (${prior}) again and waits for it to answer, past a stopped worker's heartbeat window; then lifts update pause ${id}.`);
|
|
46
45
|
}
|
|
@@ -56,7 +55,7 @@ export async function rescueDeployment(stageArg, yes, world) {
|
|
|
56
55
|
return { ok: false, reason: "usage", message: `${stageArg} is not a deployment stage of this installation (under ${join(world.stateDir, "staged-upgrades")}).` };
|
|
57
56
|
}
|
|
58
57
|
else {
|
|
59
|
-
const open = all.filter(one => one.journal === null || !
|
|
58
|
+
const open = all.filter(one => one.journal === null || !NEEDS_NO_RECOVERY.includes(String(one.journal["phase"])));
|
|
60
59
|
if (open.length === 0)
|
|
61
60
|
return { ok: true, acted: false, stage: null, phase: null, words: ["Nothing to recover: every deployment finished or was already put back."] };
|
|
62
61
|
if (open.length > 1)
|
|
@@ -74,7 +73,7 @@ export async function rescueDeployment(stageArg, yes, world) {
|
|
|
74
73
|
return { ok: false, reason: "conflict", message: `That deployment ran on ${String(journal["database"])}, not ${world.database}; nothing was changed.` };
|
|
75
74
|
if (!Number.isSafeInteger(run) || run < 1)
|
|
76
75
|
return { ok: false, reason: "damaged", message: `${join(stage, "deployment.json")} names no builder run, so its commit's recovery can't be found; nothing was changed.` };
|
|
77
|
-
if (
|
|
76
|
+
if (NEEDS_NO_RECOVERY.includes(phase))
|
|
78
77
|
return { ok: true, acted: false, stage, phase, words: [`Nothing to recover: that deployment is ${phase}.`] };
|
|
79
78
|
if (FORWARD_ONLY.includes(phase)) {
|
|
80
79
|
return { ok: false, reason: "forward", message: `That deployment is at ${phase}: the new service may be running, so nothing was stopped. Continue it forward with \`toolroll release <branch>\` (it resumes the deployment's finish).` };
|
package/dist/discord-chat.js
CHANGED
|
@@ -7,7 +7,7 @@ import { cardWithin } from "./chat-card.js";
|
|
|
7
7
|
import { chatFlowButtons } from "./chat-flow.js";
|
|
8
8
|
import { refuseResultShots, resultShotsPruned, shotWaitsUntil } from "./result-shots.js";
|
|
9
9
|
import { admitChatEvent } from "./chat-admission.js";
|
|
10
|
-
import { ChatState, ChatDeliveryError, chatHash, partContent, } from "./chat-delivery-state.js";
|
|
10
|
+
import { ChatState, ChatDeliveryError, chatHash, chatRetryAt, partContent, } from "./chat-delivery-state.js";
|
|
11
11
|
import { readChatPart } from "./contracts/chat-content.js";
|
|
12
12
|
import { readDiscordButtonToken, readDiscordComponentInteraction } from "./contracts/discord-callback.js";
|
|
13
13
|
import { processChatEvent, planChatNotifications, channelAccess, chatObject as object, planRoomMessages, heldForWatch, } from "./chat-delivery.js";
|
|
@@ -418,8 +418,7 @@ export async function deliverDiscordPart(options) {
|
|
|
418
418
|
const problem = error instanceof ChatDeliveryError
|
|
419
419
|
? error
|
|
420
420
|
: new DiscordError("Discord delivery is waiting to retry", 15_000, true);
|
|
421
|
-
const until =
|
|
422
|
-
Math.max(problem.retryMs, [5000, 15000, 60000, 300000][Math.min(row.attempts, 3)])).toISOString();
|
|
421
|
+
const until = chatRetryAt(options.clock?.() ?? new Date(), problem.retryMs, row.attempts);
|
|
423
422
|
state.partFailed(row.id, { problem: problem.message, until, uncertain: problem.uncertain });
|
|
424
423
|
state.setProblem(identity.installation, options.owner, problem.code === "ratelimited" ? "Discord asked us to wait. Saved replies will retry." : problem.message);
|
|
425
424
|
if (problem.code === "ratelimited")
|
package/dist/dispatch.js
CHANGED
|
@@ -19,6 +19,7 @@ import { failedAttemptSentence, latestFinishedAttempt } from "./needs-you.js";
|
|
|
19
19
|
import { buildReviewOf } from "./review-switch.js";
|
|
20
20
|
import { vanishedStopOf, vanishedStopSentence } from "./vanished-retry.js";
|
|
21
21
|
import { routeChangeOf } from "./route-change.js";
|
|
22
|
+
import { wasInterrupted } from "./run-ending.js";
|
|
22
23
|
import { BUILT_IN, refArchived, parseCapabilityKey } from "./store.js";
|
|
23
24
|
import { shortWhen } from "./when-html.js";
|
|
24
25
|
export const DEFAULT_MAX_OPEN_DECISIONS = 5;
|
|
@@ -157,7 +158,7 @@ function reviewViewOf(sourceRun, retry) {
|
|
|
157
158
|
retriesRemaining: retry.retriesRemaining,
|
|
158
159
|
latestRun: retry.latest?.runId ?? null,
|
|
159
160
|
latestReason: retry.latest?.reason ?? null,
|
|
160
|
-
interrupted: retry.latest !== null && (retry.latest
|
|
161
|
+
interrupted: retry.latest !== null && wasInterrupted(retry.latest),
|
|
161
162
|
queued: retry.openRequest === null ? null : { requestedBy: retry.openRequest.requestedBy, origin: retry.openRequest.origin },
|
|
162
163
|
};
|
|
163
164
|
}
|
package/dist/dispose.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { STORAGE_HOLD_PREFIX } from "./needs-you.js";
|
|
2
2
|
import { type FailureClass } from "./claim.js";
|
|
3
3
|
import type { BuildResult } from "./builder.js";
|
|
4
|
-
import type { Store } from "./store.js";
|
|
4
|
+
import type { ClosedState, Store } from "./store.js";
|
|
5
5
|
import { type ProofVerdict } from "./proof.js";
|
|
6
6
|
/**
|
|
7
7
|
* Which road is disposing. 'tick' = the unattended loop: full task
|
|
@@ -59,6 +59,9 @@ export type Disposition = {
|
|
|
59
59
|
disposition: string | null;
|
|
60
60
|
strikes: number | null;
|
|
61
61
|
sealed: boolean;
|
|
62
|
+
/** The state sealed when strikes ran out on a task a person had already closed (disposition "closed"): it is
|
|
63
|
+
* kept, with no incident, hold or page. Null for every other ending. */
|
|
64
|
+
closedState: ClosedState | null;
|
|
62
65
|
}
|
|
63
66
|
/** The standalone road's simple record: outcome written, nothing else. */
|
|
64
67
|
| {
|
package/dist/dispose.js
CHANGED
|
@@ -316,7 +316,7 @@ function disposeBuildOutcomeLocked(context, result) {
|
|
|
316
316
|
}
|
|
317
317
|
if (leaseId === undefined) {
|
|
318
318
|
store.finishRun(runId, { outcome: "failed", reason: result.reason, now: clock() });
|
|
319
|
-
return { kind: "failed", failureClass, disposition: null, strikes: null, sealed: false };
|
|
319
|
+
return { kind: "failed", failureClass, disposition: null, strikes: null, sealed: false, closedState: null };
|
|
320
320
|
}
|
|
321
321
|
const sealed = finalize(store, leaseId, {
|
|
322
322
|
kind: "failure",
|
|
@@ -333,6 +333,7 @@ function disposeBuildOutcomeLocked(context, result) {
|
|
|
333
333
|
disposition: sealed.ok ? sealed.disposition : null,
|
|
334
334
|
strikes: sealed.ok && "strikes" in sealed ? sealed.strikes : null,
|
|
335
335
|
sealed: sealed.ok,
|
|
336
|
+
closedState: sealed.ok && sealed.disposition === "closed" ? sealed.state : null,
|
|
336
337
|
};
|
|
337
338
|
}
|
|
338
339
|
if (result.reason === "protected-storage") {
|
package/dist/fallback.js
CHANGED
|
@@ -22,6 +22,7 @@ import { isProviderId, validateSpec } from "./provider.js";
|
|
|
22
22
|
import { providerName } from "./provider-auth.js";
|
|
23
23
|
import { PLAN_LOGIN_PROVABLE } from "./plan-login.js";
|
|
24
24
|
import { canonicalChainJson, chainDigestOf, LIVE_CHAIN_VERSION, liveChainFromJson, machineActing, profileDigestOf } from "./scope.js";
|
|
25
|
+
import { moveTask } from "./task-moves.js";
|
|
25
26
|
export const MAX_FALLBACK_ENTRIES = 3;
|
|
26
27
|
const tablesSeen = new WeakSet();
|
|
27
28
|
function hasTables(store) {
|
|
@@ -516,7 +517,7 @@ export function blockAfterSignedOut(store, runId, now) {
|
|
|
516
517
|
store.handle.prepare("UPDATE run SET reason = 'fallback-signed-out' WHERE id = ? AND outcome = 'refused'").run(runId);
|
|
517
518
|
const ref = store.refForId(run.taskRef);
|
|
518
519
|
if (ref !== null)
|
|
519
|
-
store.handle.
|
|
520
|
+
moveTask(store.handle, ref.externalId, "giveUp", now);
|
|
520
521
|
return true;
|
|
521
522
|
});
|
|
522
523
|
}
|
|
@@ -568,7 +569,7 @@ export function moveOnIfExhausted(store, runId, taskId, now) {
|
|
|
568
569
|
store.handle.prepare("INSERT INTO fallback_move (walk, to_index, from_run, terminal_class, paid, moved_at) VALUES (?, ?, ?, ?, ?, ?)")
|
|
569
570
|
.run(walk.id, run.chainIndex + 1, run.id, evidence.terminalClass, paid ? 1 : 0, now.toISOString());
|
|
570
571
|
store.handle.prepare("UPDATE fallback_walk SET cursor = ? WHERE id = ?").run(run.chainIndex + 1, walk.id);
|
|
571
|
-
store.handle
|
|
572
|
+
moveTask(store.handle, taskId, "requeue", now);
|
|
572
573
|
store.recordAction({ at: now.toISOString(), actor: "toolroll", repo: store.refForId(run.taskRef)?.repo ?? null, taskId, runId: run.id, action: "work moved to fallback agent", outcome: paid ? "paid" : "plan", source: "work", detail: entryWords(to) });
|
|
573
574
|
tell(store, run, taskId, "moved", to, now);
|
|
574
575
|
store.bumpWake();
|
package/dist/fast-start.js
CHANGED
|
@@ -31,7 +31,7 @@ import { fileTaskProposal } from "./proposal.js";
|
|
|
31
31
|
import { releaseCiHoldOf } from "./release-ci-hold.js";
|
|
32
32
|
import { COMPLETION_ACTION, COMPLETION_BY_PERSON } from "./result-completion.js";
|
|
33
33
|
import { deployGateScope, machineActing, propose, releaseCheckScope } from "./scope.js";
|
|
34
|
-
import { BUILT_IN } from "./store.js";
|
|
34
|
+
import { BUILT_IN, withdrawApproval } from "./store.js";
|
|
35
35
|
import { classifyTask, filingSizer, heuristicSizing } from "./task-sizing.js";
|
|
36
36
|
import { issueImported } from "./task-source.js";
|
|
37
37
|
import { titleWithin } from "./task-text.js";
|
|
@@ -295,7 +295,7 @@ export function expireFastStarts(store, now) {
|
|
|
295
295
|
store.transact(() => {
|
|
296
296
|
if (store.currentLiveLease(ref, now) !== null)
|
|
297
297
|
return;
|
|
298
|
-
store
|
|
298
|
+
withdrawApproval(store, { where: "WHERE task_id = ?", args: [task] }, { basisAfter: null, clearRoute: true, clearModeDigest: false });
|
|
299
299
|
store.handle.prepare("UPDATE task_ref SET fast_start_until = NULL WHERE id = ?").run(ref);
|
|
300
300
|
store.recordAction({ at: stamp, actor: "system", repo: row["repo"] === null ? null : String(row["repo"]), taskId: task, runId: null, action: "fast start expired", outcome: "seal cleared", source: "work", detail: `no build started within ${FAST_WINDOW_MS / 60_000} minutes` });
|
|
301
301
|
cleared++;
|
package/dist/monitoring.js
CHANGED
|
@@ -19,6 +19,7 @@ import { createHash, createHmac, randomUUID } from "node:crypto";
|
|
|
19
19
|
import { closeSync, constants, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, writeSync } from "node:fs";
|
|
20
20
|
import { hostname } from "node:os";
|
|
21
21
|
import { basename, join } from "node:path";
|
|
22
|
+
import { wasInterrupted } from "./run-ending.js";
|
|
22
23
|
import { COMPLETION_ACTION } from "./result-completion.js";
|
|
23
24
|
export const SIGNATURE_HEADER = "x-standing-orders-signature";
|
|
24
25
|
const BATCH = 200;
|
|
@@ -164,7 +165,7 @@ export function spansFor(store, entries) {
|
|
|
164
165
|
continue;
|
|
165
166
|
const root = rootOf(taskId);
|
|
166
167
|
const ref = store.refForId(run.taskRef);
|
|
167
|
-
const failed = run.outcome === "failed" || run.outcome === "refused" || run
|
|
168
|
+
const failed = run.outcome === "failed" || run.outcome === "refused" || wasInterrupted(run);
|
|
168
169
|
spans.push({
|
|
169
170
|
traceId: hex(`standing-orders/trace/${root}`, 32), spanId: hex(`standing-orders/run/${run.id}`, 16), parentSpanId: hex(`standing-orders/task/${root}`, 16),
|
|
170
171
|
name: `${run.role} · ${run.provider}`, kind: 1, startTimeUnixNano: nanos(run.startedAt), endTimeUnixNano: nanos(run.finishedAt),
|
package/dist/operate.js
CHANGED
|
@@ -123,7 +123,7 @@ import { homedir, hostname } from "node:os";
|
|
|
123
123
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
124
124
|
import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
|
|
125
125
|
import { dur } from "./design/kit.js";
|
|
126
|
-
import { checkoutIdle, setCheckoutIdle, leadQuickAnswersOn, setLeadQuickAnswers, openStore, openStoreNoMigrate, initLocalMode, enrollOwnerPassword, installationCreatedBy, laptopPortOf, recordLaptopPort, localModeOf, localOwnerBinding, archiveTaskFamily, restoreTaskFamily, shipChecksForTask, elapsedSeconds, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, parseCapabilityKey, verifiedAuthor, isDigestTime, predecessorWatches, RESULT_SCREENSHOTS, lifecycleOf, reportLifecycle, beatLifecycle, } from "./store.js";
|
|
126
|
+
import { requeueEligibility, checkoutIdle, setCheckoutIdle, leadQuickAnswersOn, setLeadQuickAnswers, openStore, openStoreNoMigrate, initLocalMode, enrollOwnerPassword, installationCreatedBy, laptopPortOf, recordLaptopPort, localModeOf, localOwnerBinding, archiveTaskFamily, restoreTaskFamily, shipChecksForTask, elapsedSeconds, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, parseCapabilityKey, verifiedAuthor, isDigestTime, predecessorWatches, RESULT_SCREENSHOTS, lifecycleOf, reportLifecycle, beatLifecycle, } from "./store.js";
|
|
127
127
|
import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
128
128
|
import { claimInstance, clearInstance, openInstanceSocket } from "./console-instance.js";
|
|
129
129
|
import { backgroundOff, backgroundOn, backgroundShow, laptopStatus, laptopStop, restartOwnBuilder } from "./laptop-background.js";
|
|
@@ -220,6 +220,7 @@ import { describePublicationGrant, observeChecks, publishPass, sweepMerges, } fr
|
|
|
220
220
|
import { checkPublishing, publishingOf, savePublishing, followPullRequests, newestPullRequestOf } from "./pull-request-flow.js";
|
|
221
221
|
import { starterOf, startersFor, switchOnStarter } from "./flow-starters.js";
|
|
222
222
|
import { acquire, acquireIfReady, heartbeat, release, reap, currentClaim, DEFAULT_LEASE_MS, SYNC_MAX_AGE_MS, finalize, } from "./claim.js";
|
|
223
|
+
import { moveTask } from "./task-moves.js";
|
|
223
224
|
import { disposeBuildOutcome, holdStaleApproval, regateTask } from "./dispose.js";
|
|
224
225
|
import { proposeGrant, describeGrant, describeWithheld, permits, MUTATION_CLASSES, DEFAULT_MUTATIONS, } from "./grant.js";
|
|
225
226
|
import { builtIn, guarded } from "./backend.js";
|
|
@@ -296,6 +297,7 @@ import { machineActing } from "./scope.js";
|
|
|
296
297
|
import { reevaluateResultRecords } from "./route-results.js";
|
|
297
298
|
import { holdRuleOff, routeRulesPass, taskEvidenceWords } from "./route-rules.js";
|
|
298
299
|
import { ruleHoldOf } from "./route-store.js";
|
|
300
|
+
import { wasInterrupted } from "./run-ending.js";
|
|
299
301
|
import { loginFileOf, migrateLegacyLogin, savedLoginFile, writableLoginFile } from "./login-secrets.js";
|
|
300
302
|
/**
|
|
301
303
|
* 0 done · 1 broke · 2 bad usage · 3 ran fine, the answer is no.
|
|
@@ -1672,12 +1674,9 @@ function claimCommand(positional, flags, context) {
|
|
|
1672
1674
|
return fail(write, json, "claim", verdict.reason, verdict.message, EXIT.refused);
|
|
1673
1675
|
}
|
|
1674
1676
|
const ref = store.refFor(backend, id);
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
ttlMs: ttl,
|
|
1679
|
-
mutation: mutationFrom(flags, now),
|
|
1680
|
-
});
|
|
1677
|
+
// The grant and the `running` it makes are one transaction (closed-task-claims.md §3.5): nobody can close the task
|
|
1678
|
+
// between them, and a refusal writes neither.
|
|
1679
|
+
const result = claimAndMarkRunning(store, backend, id, ref.id, runner, { now, token, ttlMs: ttl, mutation: mutationFrom(flags, now) });
|
|
1681
1680
|
if (!result.ok) {
|
|
1682
1681
|
// Two distinct refusals: somebody HOLDS it right now, or it is
|
|
1683
1682
|
// RESERVED for a specific worker whoever asks (queue columns, v19).
|
|
@@ -1712,24 +1711,12 @@ function claimCommand(positional, flags, context) {
|
|
|
1712
1711
|
return fail(write, json, "claim", "archived", `${id} is put away — restore it with \`toolroll task restore ${id}\` first`, EXIT.refused);
|
|
1713
1712
|
if (result.reason === "draining")
|
|
1714
1713
|
return fail(write, json, "claim", "draining", `Toolroll is restarting (draining): ${runner} takes no new work until it is back (by ${result.until})`, EXIT.refused, { until: result.until });
|
|
1714
|
+
if (result.reason === "closed")
|
|
1715
|
+
return fail(write, json, "claim", "closed", closedClaimWords(store, id, result.state, now), EXIT.refused, { state: result.state });
|
|
1716
|
+
if (result.reason === "no-task")
|
|
1717
|
+
return fail(write, json, "claim", "no-task", `no task \`${id}\``, EXIT.refused);
|
|
1715
1718
|
return fail(write, json, "claim", result.reason, `held by ${result.by} until ${result.until}`, EXIT.refused, { holder: result.by, until: result.until });
|
|
1716
1719
|
}
|
|
1717
|
-
// Taking a task is what makes it running; leaving that to the caller would
|
|
1718
|
-
// let a claimed task keep showing up as queued to anything reading state.
|
|
1719
|
-
// On an idempotent REPLAY the transition runs only as the ONE narrow
|
|
1720
|
-
// repair (external dispatch, finding 36): the stored claim is still the
|
|
1721
|
-
// live lease and the task never left queued — anything else (cancelled,
|
|
1722
|
-
// done, reopened elsewhere) mutates nothing.
|
|
1723
|
-
if (result.replayed === true) {
|
|
1724
|
-
const task = store.getTask(id);
|
|
1725
|
-
const ref = store.lookupRef(id);
|
|
1726
|
-
const stillMine = ref !== null && store.currentLiveLease(ref.id, now) === result.claim.leaseId;
|
|
1727
|
-
if (task !== null && task.state === "queued" && stillMine)
|
|
1728
|
-
store.setTaskState(id, "running", now);
|
|
1729
|
-
}
|
|
1730
|
-
else {
|
|
1731
|
-
store.setTaskState(id, "running", now);
|
|
1732
|
-
}
|
|
1733
1720
|
return succeed(write, json, "claim", { lease: result.claim, reclaimed: result.reclaimed }, () => [
|
|
1734
1721
|
`Claimed ${id} as ${runner}.`,
|
|
1735
1722
|
` lease ${result.claim.leaseId}`,
|
|
@@ -1751,20 +1738,18 @@ function leaseCommand(command, positional, flags, context) {
|
|
|
1751
1738
|
const ttl = readTtl(flags);
|
|
1752
1739
|
if (ttl === null)
|
|
1753
1740
|
return fail(write, json, name, "usage", "--ttl takes whole seconds", EXIT.usage);
|
|
1754
|
-
|
|
1741
|
+
// Releasing says the runner is finished with it, not that it succeeded, so a task left running is put back rather
|
|
1742
|
+
// than marked done, in the release's own transaction (releaseToQueue).
|
|
1743
|
+
const result = command === "heartbeat" ? heartbeat(store, lease, now, ttl) : releaseToQueue(store, lease, now);
|
|
1755
1744
|
if (!result.ok) {
|
|
1745
|
+
if (result.reason === "closed") {
|
|
1746
|
+
return fail(write, json, name, "closed", `this lease ran out and its task is ${result.state} now, so it can't be renewed; stop rather than retry`, EXIT.refused, { state: result.state });
|
|
1747
|
+
}
|
|
1756
1748
|
const message = result.reason === "fenced"
|
|
1757
1749
|
? "superseded — another runner holds this task now; stop rather than retry"
|
|
1758
1750
|
: "no such lease";
|
|
1759
1751
|
return fail(write, json, name, result.reason, message, EXIT.refused);
|
|
1760
1752
|
}
|
|
1761
|
-
if (command === "release") {
|
|
1762
|
-
const task = store.getTask(String(refExternalId(store, result.claim.taskRef)));
|
|
1763
|
-
// Releasing says the runner is finished with it, not that it succeeded, so
|
|
1764
|
-
// a task left running is put back rather than marked done.
|
|
1765
|
-
if (task !== null && task.state === "running")
|
|
1766
|
-
store.setTaskState(task.id, "queued", now);
|
|
1767
|
-
}
|
|
1768
1753
|
return succeed(write, json, name, { lease: result.claim }, () => [
|
|
1769
1754
|
command === "heartbeat"
|
|
1770
1755
|
? `Still yours until ${result.claim.expiresAt}.`
|
|
@@ -3512,7 +3497,7 @@ async function tickCommand(flags, context) {
|
|
|
3512
3497
|
? "auth-expired — requeued, sign-in needed"
|
|
3513
3498
|
: disposition.disposition === "fallback"
|
|
3514
3499
|
? `out of plan — moved to the next approved agent`
|
|
3515
|
-
: `${disposition.failureClass}${disposition.disposition === "backoff" ? ` — retry ${disposition.strikes}/3` : disposition.disposition === "stalled" ? " — stalled" : ""}`
|
|
3500
|
+
: `${disposition.failureClass}${disposition.disposition === "backoff" ? ` — retry ${disposition.strikes}/3` : disposition.disposition === "stalled" ? " — stalled" : disposition.closedState !== null ? ` — the task was already ${disposition.closedState}, so it stays ${disposition.closedState}` : ""}`
|
|
3516
3501
|
: "fenced",
|
|
3517
3502
|
...(result.ok ? {} : { detail: result.message.slice(0, 200) }),
|
|
3518
3503
|
worktree: leased.worktree.path,
|
|
@@ -9543,7 +9528,7 @@ function reviewStatusLines(review, sourceRun) {
|
|
|
9543
9528
|
queued: "legacy request retained; the worker will close it without running a model review",
|
|
9544
9529
|
running: `running — attempt ${review.live?.attempt ?? review.attempts.length} of ${review.cap}`,
|
|
9545
9530
|
succeeded: `succeeded on attempt ${review.succeeded?.attempt ?? 1} of ${review.cap}`,
|
|
9546
|
-
retryable: `${review.latest
|
|
9531
|
+
retryable: `${review.latest !== null && wasInterrupted(review.latest) ? "interrupted" : "failed"} on attempt ${review.latest?.attempt ?? review.attempts.length} of ${review.cap} — inspect the saved result; model reviews no longer retry`,
|
|
9547
9532
|
exhausted: `exhausted — all ${review.cap} attempts ended without a review; nothing retries a fourth time`,
|
|
9548
9533
|
};
|
|
9549
9534
|
const first = review.state === "queued" && review.attempts.length === 0 ? "queued — waiting for a worker" : word[review.state];
|
|
@@ -12066,9 +12051,6 @@ function slug(title, now) {
|
|
|
12066
12051
|
const stamp = now.toISOString().slice(11, 19).replace(/:/g, "");
|
|
12067
12052
|
return words === "" ? `task-${stamp}` : `${words}-${stamp}`;
|
|
12068
12053
|
}
|
|
12069
|
-
function refExternalId(store, taskRef) {
|
|
12070
|
-
return store.externalIdFor(taskRef);
|
|
12071
|
-
}
|
|
12072
12054
|
function describe(error) {
|
|
12073
12055
|
return error instanceof Error ? error.message : String(error);
|
|
12074
12056
|
}
|
|
@@ -14299,3 +14281,60 @@ function startServiceBeats(store) {
|
|
|
14299
14281
|
void delivery.runNow();
|
|
14300
14282
|
return beats;
|
|
14301
14283
|
}
|
|
14284
|
+
/**
|
|
14285
|
+
* The CLI `claim`'s grant and its `running`, one transaction (docs/plans/closed-task-claims.md §3.5). A fresh grant of
|
|
14286
|
+
* a built-in task moves it to running through the narrow `claim` move; a replay of the same key repairs `running` only
|
|
14287
|
+
* when the task is still queued and the replayed lease is still the live one, as before. A task of another backend is
|
|
14288
|
+
* never written in the built-in table.
|
|
14289
|
+
*/
|
|
14290
|
+
function claimAndMarkRunning(store, backend, id, taskRef, runner, options) {
|
|
14291
|
+
const { now } = options;
|
|
14292
|
+
return store.transact(() => {
|
|
14293
|
+
const result = acquire(store, taskRef, runner, options);
|
|
14294
|
+
if (!result.ok || backend !== BUILT_IN)
|
|
14295
|
+
return result;
|
|
14296
|
+
if (result.replayed === true) {
|
|
14297
|
+
const task = store.getTask(id);
|
|
14298
|
+
const stillMine = store.currentLiveLease(taskRef, now) === result.claim.leaseId;
|
|
14299
|
+
if (task !== null && task.state === "queued" && stillMine)
|
|
14300
|
+
store.setTaskState(id, "running", now);
|
|
14301
|
+
return result;
|
|
14302
|
+
}
|
|
14303
|
+
// The grant just read this row open in this transaction, so the move cannot miss; if it ever did, throwing rolls
|
|
14304
|
+
// the grant back with it.
|
|
14305
|
+
if (!moveTask(store.handle, id, "claim", now))
|
|
14306
|
+
throw new Error(`claim: ${id} could not be marked running`);
|
|
14307
|
+
store.bumpWake();
|
|
14308
|
+
return result;
|
|
14309
|
+
});
|
|
14310
|
+
}
|
|
14311
|
+
/**
|
|
14312
|
+
* The refusal words for a claim on a closed task: what state it is in, and only a next step that works for this task
|
|
14313
|
+
* (a requeue the store would accept, a cancelled task reopened through the state verb, otherwise the task itself).
|
|
14314
|
+
*/
|
|
14315
|
+
function closedClaimWords(store, id, state, now) {
|
|
14316
|
+
const head = `${id} is ${state}, so it can't be claimed.`;
|
|
14317
|
+
if (state === "cancelled")
|
|
14318
|
+
return `${head} To run it again, reopen it: \`toolroll task state ${id} queued\``;
|
|
14319
|
+
if (requeueEligibility(store, id, now).ok)
|
|
14320
|
+
return `${head} To run it again: \`toolroll task requeue ${id}\``;
|
|
14321
|
+
return `${head} See its result: \`toolroll task show ${id}\``;
|
|
14322
|
+
}
|
|
14323
|
+
/**
|
|
14324
|
+
* `worker release`: hand the lease back and, when this release is the one that ended the claim, put a task left
|
|
14325
|
+
* running back in the queue, in one transaction, so no one can claim it in between (closed-task-claims.md §5). A
|
|
14326
|
+
* duplicate release changes nothing: by then another runner may hold the task.
|
|
14327
|
+
*/
|
|
14328
|
+
function releaseToQueue(store, lease, now) {
|
|
14329
|
+
return store.transact(() => {
|
|
14330
|
+
const result = release(store, lease, now);
|
|
14331
|
+
if (!result.ok || result.duplicate === true)
|
|
14332
|
+
return result;
|
|
14333
|
+
const ref = store.refForId(result.claim.taskRef);
|
|
14334
|
+
if (ref === null || ref.backend !== BUILT_IN)
|
|
14335
|
+
return result;
|
|
14336
|
+
if (store.getTask(ref.externalId)?.state === "running")
|
|
14337
|
+
store.setTaskState(ref.externalId, "queued", now);
|
|
14338
|
+
return result;
|
|
14339
|
+
});
|
|
14340
|
+
}
|
package/dist/plane-review.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import { scanForSecrets } from "./evidence.js";
|
|
31
31
|
import { scrubIntegrationText } from "./integrations.js";
|
|
32
32
|
import { taskWaitSnapshot } from "./lead-status.js";
|
|
33
|
+
import { wasInterrupted } from "./run-ending.js";
|
|
33
34
|
export const REVIEW_HOURS = 24;
|
|
34
35
|
const EVIDENCE_LINES = 5;
|
|
35
36
|
const WAITING_DAYS = 1;
|
|
@@ -73,7 +74,7 @@ export function runCause(run) {
|
|
|
73
74
|
return "timeout";
|
|
74
75
|
if (reason === "no-handoff")
|
|
75
76
|
return "no-handoff";
|
|
76
|
-
if (
|
|
77
|
+
if (wasInterrupted(run))
|
|
77
78
|
return run.stopped ? null : "orphaned";
|
|
78
79
|
if (PROVIDER_REASONS.has(reason) || run.terminalClass === "transient-throttle")
|
|
79
80
|
return "provider";
|
|
@@ -22,18 +22,17 @@ export type KnowledgeView = ReturnType<typeof knowledgeView>;
|
|
|
22
22
|
export declare function knowledgeVersion(store: Store, repo: string, actor: string, revision: number): Knowledge;
|
|
23
23
|
/** Chat reads only an admitted project. Keep the full source in the project UI. */
|
|
24
24
|
export declare function conversationKnowledge(store: Store, repo: string, actor: string, reference?: string, decision?: number): {
|
|
25
|
-
content?: never;
|
|
26
25
|
history?: never;
|
|
27
26
|
revision: number;
|
|
28
27
|
stale: boolean;
|
|
29
28
|
notice: string;
|
|
30
29
|
title?: never;
|
|
30
|
+
content?: never;
|
|
31
31
|
instructions?: never;
|
|
32
32
|
references?: never;
|
|
33
33
|
decisions?: never;
|
|
34
34
|
waiting?: never;
|
|
35
35
|
} | {
|
|
36
|
-
content?: never;
|
|
37
36
|
revision?: never;
|
|
38
37
|
history?: never;
|
|
39
38
|
stale?: never;
|
|
@@ -54,6 +53,7 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
54
53
|
afterwards?: string;
|
|
55
54
|
notice: string;
|
|
56
55
|
title?: never;
|
|
56
|
+
content?: never;
|
|
57
57
|
instructions?: never;
|
|
58
58
|
references?: never;
|
|
59
59
|
decisions?: never;
|
|
@@ -70,9 +70,9 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
70
70
|
decisions?: never;
|
|
71
71
|
waiting?: never;
|
|
72
72
|
} | {
|
|
73
|
-
content?: never;
|
|
74
73
|
stale?: never;
|
|
75
74
|
title?: never;
|
|
75
|
+
content?: never;
|
|
76
76
|
revision: number;
|
|
77
77
|
history: {
|
|
78
78
|
revision: number;
|