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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An incoming chat message (a `chat_event` row of kind 'message'): what its states mean, the result words it is
|
|
3
|
+
* settled with, and the one question "which replies have not reached the person yet".
|
|
4
|
+
*
|
|
5
|
+
* States (contracts/chat-tables.ts): queued, running, done, failed, dropped.
|
|
6
|
+
*
|
|
7
|
+
* What `done` means differs by app, and nothing should read more into it than this:
|
|
8
|
+
* - Telegram: done after every planned part of the reply was sent (or dropped as moot, such as a card whose
|
|
9
|
+
* proposal was already answered elsewhere). It is written by `ChatMessages.finishMessage`, only by the claim
|
|
10
|
+
* holder and only from 'running'.
|
|
11
|
+
* - Slack, Discord and Teams: done as soon as the reply is planned, before any of it is sent. `ChatState.plan`
|
|
12
|
+
* writes the parts and then `ChatState.finish` marks the row done in the same transaction; the parts go out
|
|
13
|
+
* later, part by part, through `chat_part`.
|
|
14
|
+
* So `done` means "the message needs no more work from the lead", never "the person has the reply". To ask whether
|
|
15
|
+
* a reply has reached the person, use `unsent()` / `unsentPart()` below, which look at the parts themselves and
|
|
16
|
+
* give the same answer on every app.
|
|
17
|
+
*
|
|
18
|
+
* `failed` is used by Telegram only. `dropped` means the chat was disconnected (or an app gave the event up) while
|
|
19
|
+
* it waited; Telegram rows read a dropped row as failed. Dropped is final: `ChatState.finish` never writes over it,
|
|
20
|
+
* and `ChatState.plan` saves no reply for it.
|
|
21
|
+
*/
|
|
22
|
+
import type { ResolvedLead } from "./chat-channel.js";
|
|
23
|
+
import type { LeadFailure, LeadRefusal } from "./lead.js";
|
|
24
|
+
/**
|
|
25
|
+
* Every fixed result word a message row can be written with. These are stored in `chat_event.result`, so each one
|
|
26
|
+
* must stay byte-identical: renaming one would change what older rows mean.
|
|
27
|
+
*/
|
|
28
|
+
export declare const CHAT_MESSAGE_OUTCOMES: readonly ["answered", "replayed", "typed-start", "busy", "backlog", "replayed:running", "registry-unavailable", "registry-unavailable:admitted", "delivering", "answered:sending", "unpaired", "unsent:no-session", "replayed:missing", "typed-start:not-counted", "revoked"];
|
|
29
|
+
export type ChatMessageFixedOutcome = (typeof CHAT_MESSAGE_OUTCOMES)[number];
|
|
30
|
+
/** Words a message is queued under while it waits for another attempt (telegram-lead.ts `requeue`). */
|
|
31
|
+
export declare const CHAT_WAIT_OUTCOMES: readonly ["busy", "backlog", "replayed:running", "registry-unavailable", "registry-unavailable:admitted"];
|
|
32
|
+
export type ChatWaitOutcome = (typeof CHAT_WAIT_OUTCOMES)[number];
|
|
33
|
+
/** Waiting for the lead itself (it is answering, or earlier messages wait first): the two-hour bound applies. */
|
|
34
|
+
export declare const CHAT_LEAD_WAITS: readonly ["busy", "backlog"];
|
|
35
|
+
export type ChatLeadWait = (typeof CHAT_LEAD_WAITS)[number];
|
|
36
|
+
/** Waiting on a turn that was already admitted: no age bound (the turn's own deadline ends it). */
|
|
37
|
+
export declare const CHAT_ADMITTED_WAITS: readonly ["replayed:running", "registry-unavailable:admitted"];
|
|
38
|
+
/** The words a row is marked done with (the reply was sent). */
|
|
39
|
+
export type ChatDoneOutcome = "answered" | "replayed" | "typed-start";
|
|
40
|
+
type RefusedReason = LeadRefusal | Extract<ResolvedLead, {
|
|
41
|
+
ok: false;
|
|
42
|
+
}>["reason"];
|
|
43
|
+
/**
|
|
44
|
+
* Every result word, including the forms that carry a detail after a prefix or a suffix. Build the detailed forms
|
|
45
|
+
* with `chatOutcome` below, never by joining strings at the call site.
|
|
46
|
+
*/
|
|
47
|
+
export type ChatMessageOutcome = ChatMessageFixedOutcome | `${ChatWaitOutcome}:gave-up` | `refused:${RefusedReason}` | `failed:${LeadFailure}` | `replayed:${string}` | `unsent:gave-up:${string}` | `unsent:${string}`;
|
|
48
|
+
/** Builders for the result words that carry a detail. The stored text is exactly what the old string joins wrote. */
|
|
49
|
+
export declare const chatOutcome: {
|
|
50
|
+
/** A wait that ran past its age bound. */
|
|
51
|
+
readonly gaveUp: <W extends ChatWaitOutcome>(wait: W) => `${W}:gave-up`;
|
|
52
|
+
/** The lead refused the turn, or no session could be resolved for it. */
|
|
53
|
+
readonly refused: <R extends RefusedReason>(reason: R) => `refused:${R}`;
|
|
54
|
+
/** The turn ran and failed. */
|
|
55
|
+
readonly failed: <R extends LeadFailure>(reason: R) => `failed:${R}`;
|
|
56
|
+
/** The recovered turn had ended without an answer: its failure reason as the turn row stores it ("failed" when none). */
|
|
57
|
+
readonly replayedFailed: (reason: string | null) => `replayed:${string}`;
|
|
58
|
+
/** The reply could not go out: the chat or its projects changed (the problem in words). */
|
|
59
|
+
readonly unsent: (problem: string) => `unsent:${string}`;
|
|
60
|
+
/** The reply was still unsent a day after it was planned (the last send error, or "unsent"). */
|
|
61
|
+
readonly unsentGaveUp: (error: string | null) => `unsent:gave-up:${string}`;
|
|
62
|
+
};
|
|
63
|
+
/** Is this word one of the waits for the lead itself (busy or backlog)? */
|
|
64
|
+
export declare function isLeadWait(outcome: string | null): outcome is ChatLeadWait;
|
|
65
|
+
/** Is this word a wait on a turn that was already admitted (no age bound)? */
|
|
66
|
+
export declare function isAdmittedWait(outcome: string | null): boolean;
|
|
67
|
+
/** SQL: this `chat_part` row (by its alias) has not reached the person: planned and neither sent nor dropped. */
|
|
68
|
+
export declare function unsentPart(part: string): string;
|
|
69
|
+
/**
|
|
70
|
+
* SQL: the `chat_event` row (by its alias or table name) has a reply part that has not reached the person yet.
|
|
71
|
+
* The one answer to "is anything of this reply unsent?" on every app; wrap it in NOT for "everything reached them
|
|
72
|
+
* or was dropped". `part` names the inner alias when the caller's query already uses the default.
|
|
73
|
+
*/
|
|
74
|
+
export declare function unsent(event: string, part?: string): string;
|
|
75
|
+
export {};
|
|
@@ -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/checkout-cleanup.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { createHash } from "node:crypto";
|
|
28
28
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
29
29
|
import { basename, dirname, join } from "node:path";
|
|
30
|
+
import { isDeployed } from "./deploy-phase.js";
|
|
30
31
|
import { taskBranches } from "./names.js";
|
|
31
32
|
import { COMPLETION_ACTION } from "./result-completion.js";
|
|
32
33
|
import { CLEANUP_CHOICES, IDLE_CHOICES, bytesWords, diskBytes, diskBytesAsync } from "./storage.js";
|
|
@@ -59,7 +60,7 @@ export function deployedRelease(stateDir) {
|
|
|
59
60
|
catch {
|
|
60
61
|
continue;
|
|
61
62
|
}
|
|
62
|
-
if (journal["phase"]
|
|
63
|
+
if (!isDeployed(journal["phase"]))
|
|
63
64
|
continue;
|
|
64
65
|
const at = typeof journal["deployedAt"] === "string" ? journal["deployedAt"] : typeof journal["updatedAt"] === "string" ? journal["updatedAt"] : "";
|
|
65
66
|
if (newest !== null && newest.at >= at)
|
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,
|