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,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every fixed result word a message row can be written with. These are stored in `chat_event.result`, so each one
|
|
3
|
+
* must stay byte-identical: renaming one would change what older rows mean.
|
|
4
|
+
*/
|
|
5
|
+
export const CHAT_MESSAGE_OUTCOMES = [
|
|
6
|
+
// done: the reply was sent (Telegram)
|
|
7
|
+
"answered", // a fresh turn's reply
|
|
8
|
+
"replayed", // the reply of a turn found again after a restart, or sent on a later pass
|
|
9
|
+
"typed-start", // Toolroll's own reply to a typed yes on a Start card
|
|
10
|
+
// queued: waiting for the lead, with no age bound or the bounds in telegram-lead.ts
|
|
11
|
+
"busy", // the lead is answering something else
|
|
12
|
+
"backlog", // the person's own earlier messages wait first
|
|
13
|
+
"replayed:running", // a turn was already admitted for it and is still working
|
|
14
|
+
"registry-unavailable", // the project records could not be read
|
|
15
|
+
"registry-unavailable:admitted", // the same, but a turn already took the message
|
|
16
|
+
// queued: the reply is planned and only sending remains
|
|
17
|
+
"delivering",
|
|
18
|
+
"answered:sending", // a fresh turn's reply, handed to the bridge's cycle to send
|
|
19
|
+
// failed
|
|
20
|
+
"unpaired", // the chat's pairing changed before the turn
|
|
21
|
+
"unsent:no-session", // the session ended before the reply went out
|
|
22
|
+
"replayed:missing", // the turn its receipt names is gone
|
|
23
|
+
"typed-start:not-counted", // a typed yes whose card's turn is gone
|
|
24
|
+
// dropped: the chat was disconnected while the message waited (chat-core.ts)
|
|
25
|
+
"revoked",
|
|
26
|
+
];
|
|
27
|
+
/** Words a message is queued under while it waits for another attempt (telegram-lead.ts `requeue`). */
|
|
28
|
+
export const CHAT_WAIT_OUTCOMES = ["busy", "backlog", "replayed:running", "registry-unavailable", "registry-unavailable:admitted"];
|
|
29
|
+
/** Waiting for the lead itself (it is answering, or earlier messages wait first): the two-hour bound applies. */
|
|
30
|
+
export const CHAT_LEAD_WAITS = ["busy", "backlog"];
|
|
31
|
+
/** Waiting on a turn that was already admitted: no age bound (the turn's own deadline ends it). */
|
|
32
|
+
export const CHAT_ADMITTED_WAITS = ["replayed:running", "registry-unavailable:admitted"];
|
|
33
|
+
/** Builders for the result words that carry a detail. The stored text is exactly what the old string joins wrote. */
|
|
34
|
+
export const chatOutcome = {
|
|
35
|
+
/** A wait that ran past its age bound. */
|
|
36
|
+
gaveUp: (wait) => `${wait}:gave-up`,
|
|
37
|
+
/** The lead refused the turn, or no session could be resolved for it. */
|
|
38
|
+
refused: (reason) => `refused:${reason}`,
|
|
39
|
+
/** The turn ran and failed. */
|
|
40
|
+
failed: (reason) => `failed:${reason}`,
|
|
41
|
+
/** The recovered turn had ended without an answer: its failure reason as the turn row stores it ("failed" when none). */
|
|
42
|
+
replayedFailed: (reason) => `replayed:${reason ?? "failed"}`,
|
|
43
|
+
/** The reply could not go out: the chat or its projects changed (the problem in words). */
|
|
44
|
+
unsent: (problem) => `unsent:${problem}`,
|
|
45
|
+
/** The reply was still unsent a day after it was planned (the last send error, or "unsent"). */
|
|
46
|
+
unsentGaveUp: (error) => `unsent:gave-up:${error ?? "unsent"}`,
|
|
47
|
+
};
|
|
48
|
+
/** Is this word one of the waits for the lead itself (busy or backlog)? */
|
|
49
|
+
export function isLeadWait(outcome) {
|
|
50
|
+
return CHAT_LEAD_WAITS.includes(outcome ?? "");
|
|
51
|
+
}
|
|
52
|
+
/** Is this word a wait on a turn that was already admitted (no age bound)? */
|
|
53
|
+
export function isAdmittedWait(outcome) {
|
|
54
|
+
return CHAT_ADMITTED_WAITS.includes(outcome ?? "");
|
|
55
|
+
}
|
|
56
|
+
const SQL_ALIAS = /^[a-z_][a-z0-9_]*$/;
|
|
57
|
+
function alias(name) {
|
|
58
|
+
if (!SQL_ALIAS.test(name))
|
|
59
|
+
throw new Error(`not a table alias: ${name}`);
|
|
60
|
+
return name;
|
|
61
|
+
}
|
|
62
|
+
/** SQL: this `chat_part` row (by its alias) has not reached the person: planned and neither sent nor dropped. */
|
|
63
|
+
export function unsentPart(part) {
|
|
64
|
+
return `${alias(part)}.state = 'pending'`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* SQL: the `chat_event` row (by its alias or table name) has a reply part that has not reached the person yet.
|
|
68
|
+
* The one answer to "is anything of this reply unsent?" on every app; wrap it in NOT for "everything reached them
|
|
69
|
+
* or was dropped". `part` names the inner alias when the caller's query already uses the default.
|
|
70
|
+
*/
|
|
71
|
+
export function unsent(event, part = "unsent_part") {
|
|
72
|
+
const e = alias(event), p = alias(part);
|
|
73
|
+
return `EXISTS (SELECT 1 FROM chat_part ${p} WHERE ${p}.provider = ${e}.provider AND ${p}.event = ${e}.id AND ${unsentPart(p)})`;
|
|
74
|
+
}
|
package/dist/chat-messages.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ChatCore } from "./chat-core.js";
|
|
2
|
+
import { type ChatMessageOutcome } from "./chat-message-states.js";
|
|
2
3
|
import type { Notification, NotificationReceipt, Run, TelegramAction, TelegramBinding, TelegramConversation, TelegramConversationPart, TelegramConversationPartPlan, TelegramDelivery, TelegramDigest, TelegramFlowAction, TelegramFlowChoice, TelegramFlowPrompt, TelegramProposalAction } from "./store.js";
|
|
3
4
|
/** v83: a notification for another person, settled for this destination without sending. */
|
|
4
5
|
export declare const TELEGRAM_SKIPPED_ELSEWHERE = "skipped:for-another-person";
|
|
@@ -32,6 +33,8 @@ export declare const TELEGRAM_HOLD_REASONS: Readonly<{
|
|
|
32
33
|
order: "Earlier task notification is still undelivered";
|
|
33
34
|
disabled: "Telegram delivery is disabled";
|
|
34
35
|
}>;
|
|
36
|
+
/** A note draft's states: `pending` and `armed` are live; the other three are final. */
|
|
37
|
+
export type NoteDraftState = "pending" | "armed" | "superseded" | "consumed" | "discarded";
|
|
35
38
|
export declare class ChatMessages extends ChatCore {
|
|
36
39
|
createDecisionAction(action: {
|
|
37
40
|
token: string;
|
|
@@ -72,8 +75,9 @@ export declare class ChatMessages extends ChatCore {
|
|
|
72
75
|
replyTo: string;
|
|
73
76
|
note: string;
|
|
74
77
|
}, now: Date, ttlMs?: number): boolean;
|
|
75
|
-
/** Move a draft
|
|
76
|
-
|
|
78
|
+
/** Move a draft to `to`, only from one of the states in `from` (the check and the write are one statement). True when
|
|
79
|
+
* it moved; false when the draft was in none of them. The caller owns the transaction story. */
|
|
80
|
+
setNoteDraftState(id: number, from: readonly NoteDraftState[], to: NoteDraftState): boolean;
|
|
77
81
|
createFlowActions(at: {
|
|
78
82
|
binding: number;
|
|
79
83
|
chatId: string;
|
|
@@ -202,14 +206,16 @@ export declare class ChatMessages extends ChatCore {
|
|
|
202
206
|
* Settle a claimed row: done or failed with its outcome word, or back to
|
|
203
207
|
* queued for a later attempt (a busy engine). Only the claim holder may;
|
|
204
208
|
* a lapsed claim settles nothing, so a reclaimer's outcome stands.
|
|
209
|
+
* Telegram writes done here only once every part of the reply was sent
|
|
210
|
+
* or dropped; what done means on each app is in chat-message-states.ts.
|
|
205
211
|
*/
|
|
206
212
|
finishMessage(id: number, owner: string, result: {
|
|
207
213
|
state: "done" | "failed";
|
|
208
|
-
outcome:
|
|
214
|
+
outcome: ChatMessageOutcome;
|
|
209
215
|
replyMessageId?: string | null;
|
|
210
216
|
} | {
|
|
211
217
|
state: "queued";
|
|
212
|
-
outcome:
|
|
218
|
+
outcome: ChatMessageOutcome;
|
|
213
219
|
nextAttemptAt: string;
|
|
214
220
|
}, now: Date): boolean;
|
|
215
221
|
/**
|
package/dist/chat-messages.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { neverHeld, watchedPing } from "./agent-board.js";
|
|
12
12
|
import { ChatCore } from "./chat-core.js";
|
|
13
|
+
import { unsentPart, unsent } from "./chat-message-states.js";
|
|
13
14
|
import { readChatMessageBody, readChatPart, savedChatEventBody, savedChatPart } from "./contracts/chat-content.js";
|
|
14
15
|
import { isTelegramProgressNotification, proposalTaskOf, readNotification, readNotificationReceipt } from "./notification-rows.js";
|
|
15
16
|
/** v83: a notification for another person, settled for this destination without sending. */
|
|
@@ -195,7 +196,7 @@ export class ChatMessages extends ChatCore {
|
|
|
195
196
|
if (live !== undefined) {
|
|
196
197
|
if (Number(live["event"]) >= draft.updateId)
|
|
197
198
|
return false;
|
|
198
|
-
chat.
|
|
199
|
+
chat.setNoteDraftState(Number(live["id"]), ["pending", "armed"], "superseded");
|
|
199
200
|
}
|
|
200
201
|
chat
|
|
201
202
|
.prepare(`INSERT INTO chat_note_draft (provider, id, binding, decision, event, message, reply_to, note, state, created, expires)
|
|
@@ -204,9 +205,11 @@ export class ChatMessages extends ChatCore {
|
|
|
204
205
|
return true;
|
|
205
206
|
});
|
|
206
207
|
}
|
|
207
|
-
/** Move a draft
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
/** Move a draft to `to`, only from one of the states in `from` (the check and the write are one statement). True when
|
|
209
|
+
* it moved; false when the draft was in none of them. The caller owns the transaction story. */
|
|
210
|
+
setNoteDraftState(id, from, to) {
|
|
211
|
+
return Number(this.prepare("UPDATE chat_note_draft SET state = ? WHERE provider = :provider AND id = ? AND state IN (SELECT value FROM json_each(?))")
|
|
212
|
+
.run(to, id, JSON.stringify(from)).changes) === 1;
|
|
210
213
|
}
|
|
211
214
|
createFlowActions(at, actions, now, days = 7) {
|
|
212
215
|
const stamp = now.toISOString(), expires = new Date(now.getTime() + days * 86_400_000).toISOString();
|
|
@@ -454,6 +457,8 @@ export class ChatMessages extends ChatCore {
|
|
|
454
457
|
* Settle a claimed row: done or failed with its outcome word, or back to
|
|
455
458
|
* queued for a later attempt (a busy engine). Only the claim holder may;
|
|
456
459
|
* a lapsed claim settles nothing, so a reclaimer's outcome stands.
|
|
460
|
+
* Telegram writes done here only once every part of the reply was sent
|
|
461
|
+
* or dropped; what done means on each app is in chat-message-states.ts.
|
|
457
462
|
*/
|
|
458
463
|
finishMessage(id, owner, result, now) {
|
|
459
464
|
const stamp = now.toISOString(), chat = this;
|
|
@@ -478,9 +483,9 @@ export class ChatMessages extends ChatCore {
|
|
|
478
483
|
unsentReplies(botId, approver, since) {
|
|
479
484
|
return this
|
|
480
485
|
.prepare(`SELECT c.id, b.approver, MIN(p.created) AS since,
|
|
481
|
-
(SELECT q.problem FROM chat_part q WHERE q.provider = c.provider AND q.event = c.id AND q
|
|
486
|
+
(SELECT q.problem FROM chat_part q WHERE q.provider = c.provider AND q.event = c.id AND ${unsentPart("q")} ORDER BY q.ordinal LIMIT 1) AS error
|
|
482
487
|
FROM chat_event c JOIN chat_binding b ON b.provider = c.provider AND b.id = c.binding
|
|
483
|
-
JOIN chat_part p ON p.provider = c.provider AND p.event = c.id AND p
|
|
488
|
+
JOIN chat_part p ON p.provider = c.provider AND p.event = c.id AND ${unsentPart("p")}
|
|
484
489
|
WHERE c.provider = :provider AND c.kind = 'message' AND c.state IN ('queued','running') AND (? IS NULL OR c.installation = ?) AND (? IS NULL OR b.approver = ?)
|
|
485
490
|
GROUP BY c.id HAVING MIN(p.created) <= ? ORDER BY ${TELEGRAM_CONVERSATION_ORDER.replaceAll("e.", "c.")}`)
|
|
486
491
|
.all(botId, botId, approver, approver, since.toISOString())
|
|
@@ -492,7 +497,7 @@ export class ChatMessages extends ChatCore {
|
|
|
492
497
|
.prepare(`UPDATE chat_event SET next_at = ?
|
|
493
498
|
WHERE provider = :provider AND kind = 'message' AND state = 'queued'
|
|
494
499
|
AND (? IS NULL OR binding IN (SELECT id FROM chat_binding WHERE provider = :provider AND approver = ?))
|
|
495
|
-
AND
|
|
500
|
+
AND ${unsent("chat_event")}`)
|
|
496
501
|
.run(now.toISOString(), approver, approver);
|
|
497
502
|
return Number(changes);
|
|
498
503
|
}
|
package/dist/check-failure.js
CHANGED
|
@@ -27,6 +27,7 @@ import { INFRA_RERUNS_PER_CANDIDATE, INFRA_RERUNS_PER_INCIDENT } from "./release
|
|
|
27
27
|
import { releaseCheckScope } from "./scope.js";
|
|
28
28
|
import { isVerificationReceipt, verificationEvidence } from "./verification-evidence.js";
|
|
29
29
|
import { checkAttemptOf } from "./store.js";
|
|
30
|
+
import { endedInterrupted } from "./run-ending.js";
|
|
30
31
|
/** The plane's limits for confirmed infrastructure failures (one rerun per incident, two per candidate). */
|
|
31
32
|
export { INFRA_RERUNS_PER_CANDIDATE, INFRA_RERUNS_PER_INCIDENT };
|
|
32
33
|
/** The ledger action that records each rerun the release asked for, and why. */
|
|
@@ -208,7 +209,7 @@ export function setupReplayWords(first) {
|
|
|
208
209
|
* got as far as its check keeps every rule below.
|
|
209
210
|
*/
|
|
210
211
|
export function ranNoCheck(store, row) {
|
|
211
|
-
if (row
|
|
212
|
+
if (!(endedInterrupted(row) || (row.outcome === "failed" && row.reason === "no-op")) || row.head !== null || row.checkRow !== null)
|
|
212
213
|
return false;
|
|
213
214
|
if (checkAttemptOf(store, row.id) !== null)
|
|
214
215
|
return false;
|
package/dist/check-report.js
CHANGED
|
@@ -36,7 +36,7 @@ export const CHECK_ARTIFACT_CAP = CHECK_REPORT_MAX_BYTES;
|
|
|
36
36
|
/** The release check's harness, named: the orchestrator, its runners and helpers, Vitest's configuration and setup, and
|
|
37
37
|
* the reporter. These never carry (release-carry.ts). The execution context digests far more (inHarnessDigest). */
|
|
38
38
|
export const HARNESS_FILES = Object.freeze([
|
|
39
|
-
"scripts/release-check.mjs", "scripts/release-check-ci.mjs", "scripts/suite-lifecycle.mjs", "scripts/provider-gate.mjs",
|
|
39
|
+
"scripts/release-check.mjs", "scripts/release-check-ci.mjs", "scripts/upgrade-rule.mjs", "scripts/suite-lifecycle.mjs", "scripts/provider-gate.mjs",
|
|
40
40
|
"scripts/e2e-parallel.mjs", "scripts/e2e-kit.mjs", "scripts/upgrade-path.mjs", "scripts/check-results.mjs",
|
|
41
41
|
"vitest.config.ts", "test/attacks/vitest.config.ts",
|
|
42
42
|
"test/setup-state.ts", "test/fresh-store.ts", "test/temp-root.ts", "test/ensure-build.ts", "test/release-reporter.ts",
|
package/dist/claim.d.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* untestable, and a lease is exactly the kind of thing that has to be provable
|
|
28
28
|
* rather than probable.
|
|
29
29
|
*/
|
|
30
|
-
import { type Store, type Mutation, type TaskState } from "./store.js";
|
|
30
|
+
import { type Store, type Mutation, type TaskState, type ClosedState } from "./store.js";
|
|
31
31
|
import type { ParsedDecision, Problem } from "./decision.js";
|
|
32
32
|
import type { ParsedPlan } from "./plan.js";
|
|
33
33
|
import { type PlannerSource } from "./planner-source.js";
|
|
@@ -115,6 +115,18 @@ export type AcquireResult = {
|
|
|
115
115
|
ok: false;
|
|
116
116
|
reason: "draining";
|
|
117
117
|
until: string;
|
|
118
|
+
}
|
|
119
|
+
/** The task is done, failed or cancelled (docs/plans/closed-task-claims.md): nothing claims a closed task, except
|
|
120
|
+
* check cleanup's own door (acquireForCheckCleanup). */
|
|
121
|
+
| {
|
|
122
|
+
ok: false;
|
|
123
|
+
reason: "closed";
|
|
124
|
+
state: ClosedState;
|
|
125
|
+
}
|
|
126
|
+
/** A built-in reference with no task row behind it: nothing to claim. */
|
|
127
|
+
| {
|
|
128
|
+
ok: false;
|
|
129
|
+
reason: "no-task";
|
|
118
130
|
};
|
|
119
131
|
/** `fenced` means the lease was superseded; `unknown` means it never existed. */
|
|
120
132
|
export type FenceResult = {
|
|
@@ -125,6 +137,12 @@ export type FenceResult = {
|
|
|
125
137
|
ok: false;
|
|
126
138
|
reason: "fenced" | "unknown" | "stopped";
|
|
127
139
|
};
|
|
140
|
+
/** A heartbeat's answer: the fence, or `closed` when an expired claim's task was closed meanwhile (it is not renewed). */
|
|
141
|
+
export type HeartbeatResult = FenceResult | {
|
|
142
|
+
ok: false;
|
|
143
|
+
reason: "closed";
|
|
144
|
+
state: ClosedState;
|
|
145
|
+
};
|
|
128
146
|
/**
|
|
129
147
|
* Completion's OWN result (external dispatch, finding 33): `completed` is
|
|
130
148
|
* today's world — the task is done/failed as asked; `disowned` means the
|
|
@@ -225,7 +243,7 @@ export declare function acquireIfReady(store: Store, taskRef: number, runner: st
|
|
|
225
243
|
* has been superseded — which is the cheapest moment for it to find out, well
|
|
226
244
|
* before it has finished work nobody will accept.
|
|
227
245
|
*/
|
|
228
|
-
export declare function heartbeat(store: Store, leaseId: string, now: Date, ttlMs?: number):
|
|
246
|
+
export declare function heartbeat(store: Store, leaseId: string, now: Date, ttlMs?: number): HeartbeatResult;
|
|
229
247
|
/**
|
|
230
248
|
* Hand the task back.
|
|
231
249
|
*
|
|
@@ -354,6 +372,13 @@ export type FailureDisposition = {
|
|
|
354
372
|
ok: true;
|
|
355
373
|
disposition: "fallback";
|
|
356
374
|
provider: string;
|
|
375
|
+
}
|
|
376
|
+
/** Strikes used up on a task a person had already closed: its state is kept, and no incident, hold or page. */
|
|
377
|
+
| {
|
|
378
|
+
ok: true;
|
|
379
|
+
disposition: "closed";
|
|
380
|
+
strikes: number;
|
|
381
|
+
state: ClosedState;
|
|
357
382
|
} | {
|
|
358
383
|
ok: false;
|
|
359
384
|
reason: "fenced" | "unknown" | "stopped";
|
|
@@ -557,6 +582,11 @@ export type ScoutFailureDisposition = {
|
|
|
557
582
|
disposition: "stalled";
|
|
558
583
|
incidentId: number;
|
|
559
584
|
strikes: number;
|
|
585
|
+
} | {
|
|
586
|
+
ok: true;
|
|
587
|
+
disposition: "closed";
|
|
588
|
+
strikes: number;
|
|
589
|
+
state: ClosedState;
|
|
560
590
|
} | {
|
|
561
591
|
ok: true;
|
|
562
592
|
disposition: "auth-expired";
|
|
@@ -587,3 +617,11 @@ export declare function currentClaim(store: Store, taskRef: number, now: Date):
|
|
|
587
617
|
* that recovers and a system that appears to have lost the work.
|
|
588
618
|
*/
|
|
589
619
|
export declare function reap(store: Store, now: Date): Claim[];
|
|
620
|
+
/**
|
|
621
|
+
* Check cleanup's claim (acp-check.ts settleChecks): the one road that may claim a closed task. Only the task bound to
|
|
622
|
+
* check `checkId`, when that check is this runner's, finished and unsettled, held right now by this cleanup `attempt`,
|
|
623
|
+
* and its task is the built-in one named from the check row's own id (store.ts checkCleanupTaskOf) — proved here, in
|
|
624
|
+
* the same transaction as the grant. Every other gate of the claim still applies. When the proof fails, the claim
|
|
625
|
+
* answers exactly as an ordinary one would: never wider.
|
|
626
|
+
*/
|
|
627
|
+
export declare function acquireForCheckCleanup(store: Store, checkId: number, attempt: string, runner: string, options: AcquireOptions): AcquireResult;
|
package/dist/claim.js
CHANGED
|
@@ -31,7 +31,7 @@ import { randomUUID } from "node:crypto";
|
|
|
31
31
|
import { authenticate, drainOf } from "./runner.js";
|
|
32
32
|
import { pauseForAuth } from "./provider-auth.js";
|
|
33
33
|
import { DEFAULT_MAX_OPEN_DECISIONS, missingCapability, scopeApprovedForDispatch, taskReadinessBlocker, } from "./dispatch.js";
|
|
34
|
-
import { BUILT_IN, refArchived, } from "./store.js";
|
|
34
|
+
import { BUILT_IN, checkCleanupTaskOf, isClosedState, refArchived, taskStateForClaim, } from "./store.js";
|
|
35
35
|
import { digestOf } from "./scope.js";
|
|
36
36
|
import { parseExecutionPlanDocument } from "./plan.js";
|
|
37
37
|
import { sealUnchangedPlanUnderMode } from "./plan-auto.js";
|
|
@@ -39,6 +39,7 @@ import { routeFromJson } from "./phase-routing.js";
|
|
|
39
39
|
import { contractChangesOf, decodePlannerSource, encodePlannerSource, canonicalContractJson, PLANNER_SOURCE_LIMITS, describeContractChanges, encodePlanContractRecord, plannerContractOf, plannerSourceDigest, } from "./planner-source.js";
|
|
40
40
|
import { storeEvidence, readVerifiedArtifact, EVIDENCE_CAPS } from "./evidence.js";
|
|
41
41
|
import { moveOnIfExhausted } from "./fallback.js";
|
|
42
|
+
import { moveTask } from "./task-moves.js";
|
|
42
43
|
/** How stale a mirror's last complete sync may be before admission refuses. */
|
|
43
44
|
export const SYNC_MAX_AGE_MS = 15 * 60_000;
|
|
44
45
|
/**
|
|
@@ -60,7 +61,7 @@ export function acquire(store, taskRef, runner, options) {
|
|
|
60
61
|
// copy, so a second replay cannot double-stamp, and Store.replay's T→T
|
|
61
62
|
// contract holds for every other user.
|
|
62
63
|
const replayed = store.hasMutationRecord(options.mutation ?? {});
|
|
63
|
-
const result = inTransaction(store, () => acquireLocked(store, taskRef, runner, options));
|
|
64
|
+
const result = inTransaction(store, () => acquireLocked(store, taskRef, runner, options, "refuse"));
|
|
64
65
|
return replayed && result.ok ? { ...result, replayed: true } : result;
|
|
65
66
|
}
|
|
66
67
|
/**
|
|
@@ -68,7 +69,10 @@ export function acquire(store, taskRef, runner, options) {
|
|
|
68
69
|
* `acquireIfReady` needs its readiness check and the CAS to be one atomic
|
|
69
70
|
* step, and SQLite does not nest transactions.
|
|
70
71
|
*/
|
|
71
|
-
function acquireLocked(store, taskRef, runner, options
|
|
72
|
+
function acquireLocked(store, taskRef, runner, options,
|
|
73
|
+
/** Not an AcquireOptions field, so no options object (CLI flag, HTTP body, chat, MCP) can carry it. Only
|
|
74
|
+
* acquireForCheckCleanup passes "check-cleanup", after proving its check row in this transaction. */
|
|
75
|
+
closed) {
|
|
72
76
|
const { now, ttlMs = DEFAULT_LEASE_MS, newLeaseId = randomUUID, mutation = {} } = options;
|
|
73
77
|
const db = store.handle;
|
|
74
78
|
// Idempotency wraps the CAS, not the other way round: a retried
|
|
@@ -158,6 +162,18 @@ function acquireLocked(store, taskRef, runner, options) {
|
|
|
158
162
|
until: String(existing["expires_at"]),
|
|
159
163
|
};
|
|
160
164
|
}
|
|
165
|
+
// A CLOSED TASK claims for nobody (docs/plans/closed-task-claims.md, decision A), read here in the same write
|
|
166
|
+
// transaction as the insert below. Placed after every older refusal, so each of those answers as before; this
|
|
167
|
+
// only replaces what would have been a grant. A built-in reference with no task row has nothing to claim. A
|
|
168
|
+
// task of another backend keeps today's answer: its state is in that tracker, not in this database.
|
|
169
|
+
if (placedRef !== null && placedRef.backend === BUILT_IN) {
|
|
170
|
+
const known = taskStateForClaim(store, taskRef);
|
|
171
|
+
if (known.kind === "missing")
|
|
172
|
+
return { ok: false, reason: "no-task" };
|
|
173
|
+
if (known.kind === "built-in" && isClosedState(known.state) && closed === "refuse") {
|
|
174
|
+
return { ok: false, reason: "closed", state: known.state };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
161
177
|
const generation = existing === undefined ? 1 : Number(existing["lease_generation"]) + 1;
|
|
162
178
|
const claim = {
|
|
163
179
|
taskRef,
|
|
@@ -227,7 +243,7 @@ export function acquireForCheck(store, taskRef, runner, options) {
|
|
|
227
243
|
if (registered !== null && store.liveClaimCount(runner, options.now) >= registered.runner.capacity) {
|
|
228
244
|
return { ok: false, reason: "capacity", message: `${runner} has no free slot for a check` };
|
|
229
245
|
}
|
|
230
|
-
return acquireLocked(store, taskRef, runner, options);
|
|
246
|
+
return acquireLocked(store, taskRef, runner, options, "refuse");
|
|
231
247
|
});
|
|
232
248
|
}
|
|
233
249
|
export function acquireIfReady(store, taskRef, runner, options) {
|
|
@@ -344,7 +360,7 @@ export function acquireIfReady(store, taskRef, runner, options) {
|
|
|
344
360
|
};
|
|
345
361
|
}
|
|
346
362
|
}
|
|
347
|
-
const taken = acquireLocked(store, taskRef, runner, options);
|
|
363
|
+
const taken = acquireLocked(store, taskRef, runner, options, "refuse");
|
|
348
364
|
// The half-open probe slot is consumed only WITH the claim it admits —
|
|
349
365
|
// an attention refusal or a lost CAS must not re-arm the quota as
|
|
350
366
|
// exhausted with no probe in flight (finding 3). Same transaction, so
|
|
@@ -364,6 +380,12 @@ export function heartbeat(store, leaseId, now, ttlMs = DEFAULT_LEASE_MS) {
|
|
|
364
380
|
const db = store.handle;
|
|
365
381
|
const expiresAt = new Date(now.getTime() + ttlMs).toISOString();
|
|
366
382
|
return inTransaction(store, () => {
|
|
383
|
+
// An expired claim is not brought back on a task closed meanwhile (closed-task-claims.md, finding 1): that would
|
|
384
|
+
// be a new claim on a closed task by another road. A live claim, and an expired one on an open task, renew as
|
|
385
|
+
// always. Read in this write transaction, before the renewal.
|
|
386
|
+
const closedNow = expiredOnClosedTask(store, leaseId, now);
|
|
387
|
+
if (closedNow !== null)
|
|
388
|
+
return { ok: false, reason: "closed", state: closedNow };
|
|
367
389
|
const { changes } = db
|
|
368
390
|
.prepare(`UPDATE claim SET heartbeat_at = ?, expires_at = ?
|
|
369
391
|
WHERE lease_id = ? AND released_at IS NULL AND ${NOT_SUPERSEDED}`)
|
|
@@ -582,7 +604,7 @@ function sealComplete(store, leaseId, args) {
|
|
|
582
604
|
store.applyCancellation(String(taskId["external_id"]), { kind: "machine", code: "disowned-completion" }, now, null);
|
|
583
605
|
}
|
|
584
606
|
else {
|
|
585
|
-
db
|
|
607
|
+
moveTask(db, String(taskId["external_id"]), "complete", now, state);
|
|
586
608
|
}
|
|
587
609
|
return true;
|
|
588
610
|
});
|
|
@@ -789,11 +811,9 @@ function sealInterrupted(store, leaseId, args) {
|
|
|
789
811
|
return { ok: true, stopRun, requeued: false, fenced: true };
|
|
790
812
|
// Back to the queue, where the stop's hold keeps it — a paused task
|
|
791
813
|
// reads as queued-and-held, exactly like an operator pause.
|
|
792
|
-
const requeued = db
|
|
793
|
-
.prepare("UPDATE task SET state = 'queued', updated_at = ? WHERE id = ? AND state = 'running'")
|
|
794
|
-
.run(now.toISOString(), taskId);
|
|
814
|
+
const requeued = moveTask(db, taskId, "requeue", now);
|
|
795
815
|
store.bumpWake();
|
|
796
|
-
return { ok: true, stopRun, requeued
|
|
816
|
+
return { ok: true, stopRun, requeued, fenced: false };
|
|
797
817
|
}
|
|
798
818
|
/**
|
|
799
819
|
* Hand an attempt back because the SERVICE stopped (its builder is
|
|
@@ -824,11 +844,9 @@ function sealHandBack(store, leaseId, args) {
|
|
|
824
844
|
store.finishRun(runId, { outcome: "failed", reason: "interrupted", now });
|
|
825
845
|
if (fenced)
|
|
826
846
|
return { ok: true, requeued: false, fenced: true };
|
|
827
|
-
const requeued = db
|
|
828
|
-
.prepare("UPDATE task SET state = 'queued', updated_at = ? WHERE id = ? AND state = 'running'")
|
|
829
|
-
.run(now.toISOString(), taskId);
|
|
847
|
+
const requeued = moveTask(db, taskId, "requeue", now);
|
|
830
848
|
store.bumpWake();
|
|
831
|
-
return { ok: true, requeued
|
|
849
|
+
return { ok: true, requeued, fenced: false };
|
|
832
850
|
}
|
|
833
851
|
/**
|
|
834
852
|
* THE STOP FENCE at settlement (v52): when a stop applies to the run —
|
|
@@ -868,7 +886,6 @@ export function interruptIfStopped(store, args) {
|
|
|
868
886
|
*/
|
|
869
887
|
function sealFailure(store, leaseId, args) {
|
|
870
888
|
const { runId, taskId, failureClass, message, now } = args;
|
|
871
|
-
const db = store.handle;
|
|
872
889
|
const run = openAttempt(store, leaseId, runId, "a failure", "builder");
|
|
873
890
|
const stopped = interruptIfStopped(store, { leaseId, runId, taskId, now });
|
|
874
891
|
if (stopped !== null)
|
|
@@ -916,6 +933,11 @@ function sealFailure(store, leaseId, args) {
|
|
|
916
933
|
}
|
|
917
934
|
const strikes = store.addStrike(run.taskRef);
|
|
918
935
|
if (strikes >= MAX_STRIKES) {
|
|
936
|
+
// The state first: a task a person already closed keeps its state, and then no incident, hold or page is
|
|
937
|
+
// written either (closed-task-claims.md, decision B). The run's outcome, the release and the strike stand.
|
|
938
|
+
const closedState = exhaustUnlessClosed(store, run, taskId, now);
|
|
939
|
+
if (closedState !== null)
|
|
940
|
+
return { ok: true, disposition: "closed", strikes, state: closedState };
|
|
919
941
|
const incidentId = store.createIncident({ run: runId, kind: "attempts-exhausted" }, now);
|
|
920
942
|
store.holdOwned({
|
|
921
943
|
taskRef: run.taskRef,
|
|
@@ -924,7 +946,6 @@ function sealFailure(store, leaseId, args) {
|
|
|
924
946
|
reason: `attempts-exhausted — ${strikes} consecutive failures, last: ${failureClass}`,
|
|
925
947
|
until: null,
|
|
926
948
|
}, now);
|
|
927
|
-
db.prepare("UPDATE task SET state = 'failed', updated_at = ? WHERE id = ?").run(now.toISOString(), taskId);
|
|
928
949
|
store.enqueueNotification({
|
|
929
950
|
source: { run: runId },
|
|
930
951
|
dedupeKey: `stalled:${run.taskRef}`,
|
|
@@ -970,7 +991,7 @@ function sealFailure(store, leaseId, args) {
|
|
|
970
991
|
*/
|
|
971
992
|
function sealAuthExpired(store, run, taskId, now) {
|
|
972
993
|
store.finishRun(run.id, { outcome: "failed", reason: "auth-expired", now });
|
|
973
|
-
store.handle
|
|
994
|
+
moveTask(store.handle, taskId, "requeue", now);
|
|
974
995
|
pauseForAuth(store, { provider: run.provider, authMode: run.authMode ?? null, runId: run.id, taskRef: run.taskRef, now });
|
|
975
996
|
store.bumpWake();
|
|
976
997
|
return { disposition: "auth-expired", provider: run.provider };
|
|
@@ -1385,7 +1406,6 @@ function sealScout(store, leaseId, args) {
|
|
|
1385
1406
|
*/
|
|
1386
1407
|
function sealScoutFailure(store, leaseId, args) {
|
|
1387
1408
|
const { runId, taskId, failure: kind, message, now } = args;
|
|
1388
|
-
const db = store.handle;
|
|
1389
1409
|
const run = openAttempt(store, leaseId, runId, "a failure", "scout");
|
|
1390
1410
|
const stopped = interruptIfStopped(store, { leaseId, runId, taskId, now });
|
|
1391
1411
|
if (stopped !== null)
|
|
@@ -1419,6 +1439,10 @@ function sealScoutFailure(store, leaseId, args) {
|
|
|
1419
1439
|
}
|
|
1420
1440
|
const strikes = store.addStrike(run.taskRef);
|
|
1421
1441
|
if (strikes >= MAX_STRIKES) {
|
|
1442
|
+
// Same as the build: a task a person already closed keeps its state, with no incident, hold or page.
|
|
1443
|
+
const closedState = exhaustUnlessClosed(store, run, taskId, now);
|
|
1444
|
+
if (closedState !== null)
|
|
1445
|
+
return { ok: true, disposition: "closed", strikes, state: closedState };
|
|
1422
1446
|
const incidentId = store.createIncident({ run: runId, kind: "attempts-exhausted" }, now);
|
|
1423
1447
|
store.holdOwned({
|
|
1424
1448
|
taskRef: run.taskRef,
|
|
@@ -1427,7 +1451,6 @@ function sealScoutFailure(store, leaseId, args) {
|
|
|
1427
1451
|
reason: `attempts-exhausted — ${strikes} consecutive scouting failures`,
|
|
1428
1452
|
until: null,
|
|
1429
1453
|
}, now);
|
|
1430
|
-
db.prepare("UPDATE task SET state = 'failed', updated_at = ? WHERE id = ?").run(now.toISOString(), taskId);
|
|
1431
1454
|
store.enqueueNotification({
|
|
1432
1455
|
source: { run: runId },
|
|
1433
1456
|
dedupeKey: `stalled:${run.taskRef}`,
|
|
@@ -1512,3 +1535,53 @@ function inTransaction(store, body) {
|
|
|
1512
1535
|
// intent) joins one transaction instead of dying on a nested BEGIN.
|
|
1513
1536
|
return store.transact(body);
|
|
1514
1537
|
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Whether `leaseId` is an expired claim, still unreleased and the newest for its task, on a built-in task that is now
|
|
1540
|
+
* done, failed or cancelled: that closed state, else null. heartbeat refuses to renew exactly this case.
|
|
1541
|
+
*/
|
|
1542
|
+
function expiredOnClosedTask(store, leaseId, now) {
|
|
1543
|
+
const row = store.handle
|
|
1544
|
+
.prepare(`SELECT task_ref FROM claim WHERE lease_id = ? AND released_at IS NULL AND expires_at <= ? AND ${NOT_SUPERSEDED}`)
|
|
1545
|
+
.get(leaseId, now.toISOString());
|
|
1546
|
+
if (row === undefined)
|
|
1547
|
+
return null;
|
|
1548
|
+
const known = taskStateForClaim(store, Number(row["task_ref"]));
|
|
1549
|
+
return known.kind === "built-in" && isClosedState(known.state) ? known.state : null;
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* The stall's state write (build and scouting), inside finalize's transaction. The task is the run's own: a supplied
|
|
1553
|
+
* id that names another task is a caller defect, thrown like openAttempt's. An open built-in task is marked failed
|
|
1554
|
+
* (null back, the stall goes on as before). One a person already closed keeps its state: that state comes back and
|
|
1555
|
+
* the caller writes no incident, hold or page. A reference with no built-in task row behind it (a missing row, or a
|
|
1556
|
+
* task of another backend) writes no state and answers null, so the stall goes on as it did before.
|
|
1557
|
+
*/
|
|
1558
|
+
function exhaustUnlessClosed(store, run, taskId, now) {
|
|
1559
|
+
const ref = store.refForId(run.taskRef);
|
|
1560
|
+
if (ref === null || ref.externalId !== taskId) {
|
|
1561
|
+
throw new Error(`run ${run.id} belongs to task reference ${run.taskRef}, not ${taskId} — a stall is sealed on the run's own task`);
|
|
1562
|
+
}
|
|
1563
|
+
if (ref.backend !== BUILT_IN)
|
|
1564
|
+
return null;
|
|
1565
|
+
if (moveTask(store.handle, taskId, "exhaust", now))
|
|
1566
|
+
return null;
|
|
1567
|
+
const state = store.getTask(taskId)?.state;
|
|
1568
|
+
return state !== undefined && isClosedState(state) ? state : null;
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Check cleanup's claim (acp-check.ts settleChecks): the one road that may claim a closed task. Only the task bound to
|
|
1572
|
+
* check `checkId`, when that check is this runner's, finished and unsettled, held right now by this cleanup `attempt`,
|
|
1573
|
+
* and its task is the built-in one named from the check row's own id (store.ts checkCleanupTaskOf) — proved here, in
|
|
1574
|
+
* the same transaction as the grant. Every other gate of the claim still applies. When the proof fails, the claim
|
|
1575
|
+
* answers exactly as an ordinary one would: never wider.
|
|
1576
|
+
*/
|
|
1577
|
+
export function acquireForCheckCleanup(store, checkId, attempt, runner, options) {
|
|
1578
|
+
return inTransaction(store, () => {
|
|
1579
|
+
const proved = checkCleanupTaskOf(store, checkId, runner, attempt, options.now);
|
|
1580
|
+
if (proved !== null)
|
|
1581
|
+
return acquireLocked(store, proved, runner, options, "check-cleanup");
|
|
1582
|
+
const bound = store.acpCheck(checkId)?.taskRef ?? null;
|
|
1583
|
+
if (bound === null)
|
|
1584
|
+
return { ok: false, reason: "no-task" };
|
|
1585
|
+
return acquireLocked(store, bound, runner, options, "refuse");
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
@@ -26,6 +26,9 @@ CREATE TABLE IF NOT EXISTS chat_pair (
|
|
|
26
26
|
);
|
|
27
27
|
-- Everything an app sent us, once each (the key is the idempotency). kind 'update' is an applied (or pushed and
|
|
28
28
|
-- waiting) Telegram update; 'message' is a message the lead answers, queued to running to done or failed.
|
|
29
|
+
-- done means "no more work for the lead", not "the person has the reply": Telegram writes it once every part is sent,
|
|
30
|
+
-- Slack, Discord and Teams when the reply is planned. result is a word from chat-message-states.ts; whether a reply
|
|
31
|
+
-- reached the person is answered there by unsent(), from chat_part.
|
|
29
32
|
-- session: the lead session its turn was dispatched under, a pointer to its receipt (never an authority).
|
|
30
33
|
CREATE TABLE IF NOT EXISTS chat_event (
|
|
31
34
|
${PROVIDER}, id TEXT NOT NULL, installation TEXT NOT NULL, binding INTEGER,
|
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 {
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -97,13 +97,13 @@ export declare function sqliteLock(file: string, busyTimeoutMs?: number): Databa
|
|
|
97
97
|
export declare function lockHeld(file: string): boolean;
|
|
98
98
|
export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
|
|
99
99
|
export declare function desktopUpdateStatus(stateDir: string): {
|
|
100
|
-
error?: never;
|
|
101
100
|
active: boolean;
|
|
102
101
|
phase: string;
|
|
103
102
|
detail: string;
|
|
104
103
|
running: boolean;
|
|
105
104
|
canResume: boolean;
|
|
106
105
|
canCancel: boolean;
|
|
106
|
+
error?: never;
|
|
107
107
|
automaticRecovery?: never;
|
|
108
108
|
recoveryAttempts?: never;
|
|
109
109
|
recoveryRepairs?: never;
|