toolroll 0.9.99 → 0.9.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/accent-colors.js +1 -1
- package/dist/acp-check.js +4 -2
- package/dist/activity-line.d.ts +1 -1
- package/dist/activity-line.js +1 -1
- package/dist/agent-state.js +2 -1
- package/dist/agents-board.js +3 -2
- package/dist/agents-live.js +3 -2
- package/dist/approval-authority.d.ts +2 -1
- package/dist/approval-authority.js +2 -1
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +32 -32
- package/dist/browser-workspace.d.ts +31 -2
- package/dist/chat-ask.js +4 -2
- package/dist/chat-core.js +4 -2
- package/dist/chat-decide.js +1 -2
- package/dist/chat-delivery-state.d.ts +29 -6
- package/dist/chat-delivery-state.js +43 -13
- package/dist/chat-delivery.js +12 -5
- package/dist/chat-flow.js +15 -13
- package/dist/chat-live.js +5 -2
- package/dist/chat-message-states.d.ts +75 -0
- package/dist/chat-message-states.js +74 -0
- package/dist/chat-messages.d.ts +10 -4
- package/dist/chat-messages.js +12 -7
- package/dist/check-failure.js +2 -1
- package/dist/check-report.js +1 -1
- package/dist/claim.d.ts +40 -2
- package/dist/claim.js +92 -19
- package/dist/contracts/chat-tables.js +3 -0
- package/dist/coordinator.js +3 -3
- package/dist/demo.js +7 -2
- package/dist/design/kit.css +94 -2
- package/dist/design/kit.d.ts +12 -4
- package/dist/design/kit.js +8 -2
- package/dist/design/tokens.css +5 -5
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-chat.js +2 -3
- package/dist/dispatch.js +2 -1
- package/dist/dispose.d.ts +4 -1
- package/dist/dispose.js +2 -1
- package/dist/fallback.js +3 -2
- package/dist/fast-start.js +2 -2
- package/dist/first-run.d.ts +10 -6
- package/dist/first-run.js +19 -18
- package/dist/guides.js +1 -1
- package/dist/monitoring.js +2 -1
- package/dist/operate.js +77 -36
- package/dist/plane-review.js +2 -1
- package/dist/project-knowledge.d.ts +3 -3
- package/dist/release-authority.d.ts +9 -0
- package/dist/release-authority.js +13 -0
- package/dist/release-auto.js +2 -3
- package/dist/release-gate.js +1 -1
- package/dist/release-view.js +2 -1
- package/dist/release.js +18 -30
- package/dist/result-shots.js +1 -1
- package/dist/route-results.js +2 -1
- package/dist/run-ending.d.ts +21 -0
- package/dist/run-ending.js +19 -0
- package/dist/scope.d.ts +5 -0
- package/dist/scope.js +10 -3
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +1 -1
- package/dist/server/console.css +46 -17
- package/dist/server/kit.d.ts +36 -3
- package/dist/server/kit.js +33 -4
- package/dist/server/render-chat.d.ts +2 -0
- package/dist/server/render-chat.js +1 -0
- package/dist/server/render-pages.js +1 -1
- package/dist/server/render-results.js +6 -5
- package/dist/server/render-tasks.d.ts +10 -0
- package/dist/server/render-tasks.js +234 -64
- package/dist/server/task-views.js +5 -1
- package/dist/settings-groups-ui.js +1 -1
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-chat.js +2 -3
- package/dist/store.d.ts +61 -2
- package/dist/store.js +141 -80
- package/dist/subagent-question.js +8 -6
- package/dist/surface.js +2 -2
- package/dist/task-archive.d.ts +3 -0
- package/dist/task-archive.js +5 -0
- package/dist/task-moves.d.ts +90 -0
- package/dist/task-moves.js +68 -0
- package/dist/task-status.d.ts +2 -2
- package/dist/task-status.js +3 -3
- package/dist/teams-chat.js +2 -2
- package/dist/telegram-flow.d.ts +1 -1
- package/dist/telegram-flow.js +1 -1
- package/dist/telegram-lead.d.ts +1 -2
- package/dist/telegram-lead.js +18 -23
- package/dist/telegram-progress.js +2 -1
- package/dist/telegram.d.ts +0 -1
- package/dist/telegram.js +6 -7
- package/dist/tui.js +1 -1
- package/dist/vanished-retry.js +3 -2
- package/dist/workspace-ui.d.ts +69 -0
- package/dist/workspace-ui.js +105 -1
- package/dist/worktree.js +3 -2
- package/package.json +1 -1
|
@@ -148,7 +148,7 @@ export function generalGroup(facts, flags) {
|
|
|
148
148
|
formRow("Theme", "Light, dark, or whatever this device uses.", postForm("/settings/appearance", html `<div class="k-seg" role="group" aria-label="Theme">${themeButton("system", "Match device")}${themeButton("light", "Light")}${themeButton("dark", "Dark")}</div>`, { returnTo: "/settings/general" })),
|
|
149
149
|
kitSectionForm({ proof: "appearance", action: "/settings/appearance", id: "accent", gate: true, hidden: { return: "/settings/general" },
|
|
150
150
|
body: joinHtml([
|
|
151
|
-
formRow("Accent", "Buttons
|
|
151
|
+
formRow("Accent", "Buttons, focus and what needs you.", select({ name: "accent", label: "Accent", current: facts.accent, items: [...ACCENT_CHOICES.map(one => [one.hex, one.id === "ink" ? "Ink (default)" : one.name]), ...(custom === null ? [] : [[custom, `Your own colour (${custom})`]])] })),
|
|
152
152
|
accentNoteWords === null ? "" : note(accentNoteWords),
|
|
153
153
|
]) }),
|
|
154
154
|
disclose("Use your own colour", kitSectionForm({ proof: "appearance", action: "/settings/appearance", id: "accent-own", gate: true, hidden: { return: "/settings/general" },
|
package/dist/setup-guide.d.ts
CHANGED
|
@@ -15,12 +15,12 @@ export declare function detectPreparation(repo: string): {
|
|
|
15
15
|
} | null;
|
|
16
16
|
/** Browser installation may write only this known file inside its project. */
|
|
17
17
|
export declare function previewProjectInstructions(repo: string): {
|
|
18
|
-
content?: never;
|
|
19
18
|
ok: false;
|
|
20
19
|
message: string;
|
|
21
20
|
plan?: never;
|
|
22
21
|
fingerprint?: never;
|
|
23
22
|
installed?: never;
|
|
23
|
+
content?: never;
|
|
24
24
|
} | {
|
|
25
25
|
message?: never;
|
|
26
26
|
ok: true;
|
|
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
|
|
|
30
30
|
installed: boolean;
|
|
31
31
|
};
|
|
32
32
|
export declare function addProjectInstructions(repo: string, fingerprint: string): {
|
|
33
|
-
content?: never;
|
|
34
33
|
ok: false;
|
|
35
34
|
message: string;
|
|
36
35
|
plan?: never;
|
|
37
36
|
fingerprint?: never;
|
|
38
37
|
installed?: never;
|
|
38
|
+
content?: never;
|
|
39
39
|
} | import("./skills.js").InstallResult;
|
package/dist/slack-chat.js
CHANGED
|
@@ -11,7 +11,7 @@ import { processChatEvent, planChatNotifications, channelAccess, planRoomMessage
|
|
|
11
11
|
import { proposalPreview, proposalLink, proposalOutcomeText, armedCardText, armedYesLabel, } from "./chat-channel.js";
|
|
12
12
|
import { LEAD_MESSAGE_MAX_CHARS } from "./lead.js";
|
|
13
13
|
import { renderReply } from "./reply-shape.js";
|
|
14
|
-
import { ChatDeliveryError, ChatState, chatHash, partContent } from "./chat-delivery-state.js";
|
|
14
|
+
import { ChatDeliveryError, ChatState, chatHash, chatRetryAt, partContent } from "./chat-delivery-state.js";
|
|
15
15
|
import { readChatPart } from "./contracts/chat-content.js";
|
|
16
16
|
import { readSlackBlockActions, readSlackButton, SLACK_LINK_ACTION } from "./contracts/slack-callback.js";
|
|
17
17
|
import { object, slackId, slackTs, slackMember, SlackError, uploadSlackBytes, } from "./slack-api.js";
|
|
@@ -444,8 +444,7 @@ export async function deliverSlackPart(options) {
|
|
|
444
444
|
? error
|
|
445
445
|
: new SlackError("Slack delivery is waiting to retry", 15_000, true);
|
|
446
446
|
state.setProblem(identity.installation, options.owner, problem.message);
|
|
447
|
-
const until =
|
|
448
|
-
Math.max(problem.retryMs, [5000, 15000, 60000, 300000][Math.min(row.attempts, 3)])).toISOString();
|
|
447
|
+
const until = chatRetryAt(nowOf(options), problem.retryMs, row.attempts);
|
|
449
448
|
state.partFailed(row.id, { problem: problem.message, until, uncertain: problem.uncertain });
|
|
450
449
|
if (problem.code === "ratelimited")
|
|
451
450
|
state.deferUntil(identity.installation, until);
|
package/dist/store.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type WritesMode } from "./writes-mode.js";
|
|
|
3
3
|
import { type CheckAccess } from "./check-access.js";
|
|
4
4
|
import { type ProjectSandbox } from "./project-sandbox.js";
|
|
5
5
|
import type { FencePolicy } from "./agent-fence.js";
|
|
6
|
+
import type { ChatMessageOutcome } from "./chat-message-states.js";
|
|
6
7
|
import { type NetworkProfile, type ProjectNetwork, type RunNetworkRecord } from "./egress.js";
|
|
7
8
|
import { type AcpLaunch } from "./acp-attest.js";
|
|
8
9
|
import { type CheckStamp, type StampReading } from "./check-stamp.js";
|
|
@@ -1805,8 +1806,8 @@ export type TelegramConversation = {
|
|
|
1805
1806
|
nextAttemptAt: string | null;
|
|
1806
1807
|
session: number | null;
|
|
1807
1808
|
turn: number | null;
|
|
1808
|
-
/**
|
|
1809
|
-
outcome:
|
|
1809
|
+
/** Its result word (chat-message-states.ts); a row written by an older version may hold a word no longer listed. */
|
|
1810
|
+
outcome: ChatMessageOutcome | null;
|
|
1810
1811
|
replyMessageId: string | null;
|
|
1811
1812
|
createdAt: string;
|
|
1812
1813
|
startedAt: string | null;
|
|
@@ -8552,3 +8553,61 @@ export declare function setLeadTurnRoute(store: Store, turn: number, generation:
|
|
|
8552
8553
|
* reply asked one, less than LEAD_ASK_TTL_MS ago. Conservative on purpose: the quick-answer router (§8) keeps such a
|
|
8553
8554
|
* message on the main model. */
|
|
8554
8555
|
export declare function leadAskAwaitsAnswer(store: Store, thread: number, now: Date): boolean;
|
|
8556
|
+
/** When an invite can still be used: not revoked, not consumed, not expired, and fewer than ten admitted attempts.
|
|
8557
|
+
* One bound parameter: the current time as an ISO string. Every read and write that asks "is this invite usable" uses
|
|
8558
|
+
* this fragment, so the join page, the attempt counter, the join terms and the open-invites list agree. */
|
|
8559
|
+
export declare const INVITE_USABLE_SQL = "revoked_at IS NULL AND consumed_at IS NULL AND expires_at > ? AND attempts < 10";
|
|
8560
|
+
/** How long a confirm step stays open: the Yes and Cancel buttons shown after a first tap, in every chat app. */
|
|
8561
|
+
export declare const CONFIRM_TTL_MS: number;
|
|
8562
|
+
/**
|
|
8563
|
+
* Withdraw task approvals: the one statement every withdrawal runs. It always clears approved_at, approved_by,
|
|
8564
|
+
* approved_digest and approved_profile_json, and sets approval_basis to `basisAfter`. The callers differ in what else
|
|
8565
|
+
* they clear, so that stays theirs to say: `clearRoute` clears approved_route_json, `clearModeDigest` clears
|
|
8566
|
+
* mode_digest. `where` is the caller's own WHERE clause (starting with WHERE) with its bound values; it is always a
|
|
8567
|
+
* fixed string in our code, never built from input. The check and the write are one statement. Returns the rows changed.
|
|
8568
|
+
*/
|
|
8569
|
+
export declare function withdrawApproval(store: Store, rows: {
|
|
8570
|
+
where: string;
|
|
8571
|
+
args: readonly (string | number | null)[];
|
|
8572
|
+
}, writes: {
|
|
8573
|
+
basisAfter: "password" | null;
|
|
8574
|
+
clearRoute: boolean;
|
|
8575
|
+
clearModeDigest: boolean;
|
|
8576
|
+
}): number;
|
|
8577
|
+
/**
|
|
8578
|
+
* Whether `requeueTask` would run this task again, read without writing anything: the same checks in the same order,
|
|
8579
|
+
* so the requeue itself and every sentence that offers it agree. Read inside the caller's transaction.
|
|
8580
|
+
*/
|
|
8581
|
+
export declare function requeueEligibility(store: Store, taskId: string, now: Date): {
|
|
8582
|
+
ok: true;
|
|
8583
|
+
taskRef: number;
|
|
8584
|
+
incidents: number[];
|
|
8585
|
+
state: string;
|
|
8586
|
+
rejectedRun: number | null;
|
|
8587
|
+
} | {
|
|
8588
|
+
ok: false;
|
|
8589
|
+
reason: "unknown-task" | "not-stalled" | "claimed" | "accepted-result" | "published" | "archived";
|
|
8590
|
+
};
|
|
8591
|
+
/** The three closed states (docs/plans/closed-task-claims.md). */
|
|
8592
|
+
export type ClosedState = Extract<TaskState, "done" | "failed" | "cancelled">;
|
|
8593
|
+
export declare function isClosedState(state: string): state is ClosedState;
|
|
8594
|
+
/**
|
|
8595
|
+
* What this database knows about the task behind a reference, for a claim: a built-in task's row and state, a
|
|
8596
|
+
* built-in reference with no task row, or a task of another backend (its state lives in that tracker, not here).
|
|
8597
|
+
* Read inside the caller's transaction, so the answer holds until it commits.
|
|
8598
|
+
*/
|
|
8599
|
+
export declare function taskStateForClaim(store: Store, taskRef: number): {
|
|
8600
|
+
kind: "built-in";
|
|
8601
|
+
state: TaskState;
|
|
8602
|
+
} | {
|
|
8603
|
+
kind: "missing";
|
|
8604
|
+
} | {
|
|
8605
|
+
kind: "outside";
|
|
8606
|
+
};
|
|
8607
|
+
/**
|
|
8608
|
+
* Check cleanup's proof (acp-check.ts settleChecks, claim.ts acquireForCheckCleanup): the task reference of check
|
|
8609
|
+
* `checkId` when that check belongs to `runner`, has finished (passed or failed) and is not yet settled, is held right
|
|
8610
|
+
* now by exactly this cleanup `attempt`, and is bound to the built-in task named from the check row's own id. Null
|
|
8611
|
+
* otherwise. Every fact comes from rows only code writes; read inside the caller's transaction.
|
|
8612
|
+
*/
|
|
8613
|
+
export declare function checkCleanupTaskOf(store: Store, checkId: number, runner: string, attempt: string, now: Date): number | null;
|
package/dist/store.js
CHANGED
|
@@ -16,7 +16,7 @@ import { V156_SCHEMA, V156_STEPS } from "./fallback-schema.js";
|
|
|
16
16
|
import { V158_ADDED_COLUMNS, V158_SCHEMA, V158_STEPS, LEAD_FAILURE_DETAILS, LEAD_ROUTES } from "./lead-latency-schema.js";
|
|
17
17
|
import { V157_SCHEMA, V157_STEPS } from "./chat-live-schema.js";
|
|
18
18
|
import { V159_ADDED_COLUMNS, V159_SCHEMA, V159_STEPS } from "./one-step-chat-schema.js";
|
|
19
|
-
import { refArchived, taskArchived } from "./task-archive.js";
|
|
19
|
+
import { refArchived, taskArchived, taskCancelled } from "./task-archive.js";
|
|
20
20
|
export { refArchived, taskArchived } from "./task-archive.js";
|
|
21
21
|
import { CHAT_PROVIDERS, CHAT_SCHEMA } from "./contracts/chat-tables.js";
|
|
22
22
|
import {} from "./chat-core.js";
|
|
@@ -129,8 +129,10 @@ import { configBase, envValue, namedPath } from "./names.js";
|
|
|
129
129
|
import { PULL_REQUEST_SCHEMA } from "./pull-request-schema.js";
|
|
130
130
|
import { chatTurnAmountOf, combineReportedModels, providerTurnsOf, reportedCostUsdOf, reportedModelOf, runAmountOf } from "./reported-usage.js";
|
|
131
131
|
import { usageOf } from "./run-efficiency.js";
|
|
132
|
+
import { endedInterrupted, wasInterrupted } from "./run-ending.js";
|
|
132
133
|
import { YOUIYOUX_FILED_VIA } from "./contracts/youiyoux-request.js";
|
|
133
134
|
import { cardShowsScope, cardTermsDigest, shownTermsDigest } from "./chat-terms.js";
|
|
135
|
+
import { markedRunningSql, moveTask } from "./task-moves.js";
|
|
134
136
|
// PR #263 follow-up: the built-ins the Store money seal uses, captured as this module is evaluated, so code that later
|
|
135
137
|
// replaces `Object.defineProperty` and the like can't fool the seal or the lock (lockStoreMoneyMethods refuses when
|
|
136
138
|
// they've been replaced). The four sealed method references are recorded once, at the seal, in SEALED_MONEY_METHODS.
|
|
@@ -6768,10 +6770,7 @@ export class Store {
|
|
|
6768
6770
|
return { ok: true };
|
|
6769
6771
|
}
|
|
6770
6772
|
const prior = this.db.prepare("SELECT state FROM task WHERE id = ?").get(id);
|
|
6771
|
-
|
|
6772
|
-
.prepare("UPDATE task SET state = ?, updated_at = ? WHERE id = ?")
|
|
6773
|
-
.run(state, now.toISOString(), id);
|
|
6774
|
-
if (Number(changes) === 0)
|
|
6773
|
+
if (!moveTask(this.db, id, "set", now, state))
|
|
6775
6774
|
return { ok: false, reason: "unknown-task" };
|
|
6776
6775
|
if (state === "done" && prior?.["state"] !== "done") {
|
|
6777
6776
|
const done = this.lookupRef(id);
|
|
@@ -7588,9 +7587,7 @@ export class Store {
|
|
|
7588
7587
|
.prepare(`UPDATE external_mirror SET dispatch_ok = 1, close_generation = NULL, reopened_by = ?, reopened_at = ?
|
|
7589
7588
|
WHERE local_task_id = ?`)
|
|
7590
7589
|
.run(by, now.toISOString(), localTaskId);
|
|
7591
|
-
this.db
|
|
7592
|
-
.prepare("UPDATE task SET state = 'queued', updated_at = ? WHERE id = ?")
|
|
7593
|
-
.run(now.toISOString(), localTaskId);
|
|
7590
|
+
moveTask(this.db, localTaskId, "reopen", now);
|
|
7594
7591
|
this.noteLifecycle({ taskRef, kind: "task-queued", identity: `t${taskRef}`, subject: "Back in the queue", body: "The tracker item is open again; another attempt can start.", link: chatControlHref("task", localTaskId) }, now);
|
|
7595
7592
|
this.bumpWake();
|
|
7596
7593
|
return { ok: true };
|
|
@@ -8076,7 +8073,11 @@ export class Store {
|
|
|
8076
8073
|
* exactly what the signed digest was bound to. Returns false when the
|
|
8077
8074
|
* scope vanished mid-ceremony. */
|
|
8078
8075
|
sealScopeApproval(taskId, by, now, mutation = {}, basis, approverKind) {
|
|
8079
|
-
|
|
8076
|
+
// ONE TRANSACTION, the primitive's own (Codex review of the steps page, round 2): every check below is read in
|
|
8077
|
+
// the same write transaction as the seal and the release authority it opens, so a task cancelled or put away on
|
|
8078
|
+
// another connection can't slip between the check and the write, and a seal whose release authority can't open
|
|
8079
|
+
// rolls back whole even when the caller opened no transaction. Reentrant: inside a caller's transaction it joins.
|
|
8080
|
+
const seal = () => this.once(mutation, "sealScopeApproval", () => {
|
|
8080
8081
|
// ONLY A PERSON'S SEAL HAS NO BASIS (approval policy, phase 1): with none given this is the person's
|
|
8081
8082
|
// ceremony (scope.ts approve), so a machine — the lead's token, a remote API run — never mints one, whatever
|
|
8082
8083
|
// road it came by.
|
|
@@ -8085,6 +8086,10 @@ export class Store {
|
|
|
8085
8086
|
// A PUT-AWAY TASK (v145, task-tidy.md) is never approved, by any road: restore it first.
|
|
8086
8087
|
if (taskArchived(this, taskId))
|
|
8087
8088
|
return false;
|
|
8089
|
+
// A CANCELLED TASK the same (Codex review of the steps page, finding 2): no seal, and so no release authority,
|
|
8090
|
+
// is written for work that was cancelled — whichever road asks (a stale form, the CLI, a rule, the schedule).
|
|
8091
|
+
if (taskCancelled(this, taskId))
|
|
8092
|
+
return false;
|
|
8088
8093
|
// A RELEASE WAITING FOR CI (Codex review round 1, finding 4): filed while its pull request's checks still ran,
|
|
8089
8094
|
// only the automatic road (which proves CI itself) seals it until the plane read exact-head CI passing.
|
|
8090
8095
|
if (basis?.kind !== "auto-check" && basis?.kind !== "auto-green" && releaseCiHoldOf(this, taskId) !== null)
|
|
@@ -8311,7 +8316,10 @@ export class Store {
|
|
|
8311
8316
|
}
|
|
8312
8317
|
}
|
|
8313
8318
|
return changed.changes > 0;
|
|
8314
|
-
}) === true
|
|
8319
|
+
}) === true;
|
|
8320
|
+
// Already inside a transaction (a caller's transact(), or a savepoint opened outside one): it joins that one,
|
|
8321
|
+
// which owns the commit and the rollback. Otherwise it opens its own.
|
|
8322
|
+
return this.db.isTransaction === true ? seal() : this.transact(seal);
|
|
8315
8323
|
}
|
|
8316
8324
|
/** Whether this task's scope carries a LIVE seal: approved, and the
|
|
8317
8325
|
* approved digest is the current one (a rewritten scope unseals). The
|
|
@@ -8514,10 +8522,7 @@ export class Store {
|
|
|
8514
8522
|
WHERE tr.backend = 'built-in' AND tr.external_id = task_scope.task_id
|
|
8515
8523
|
AND claim.released_at IS NULL AND claim.expires_at > ?)`;
|
|
8516
8524
|
const demoted = this.db.prepare(`SELECT task_id FROM task_scope ${demotable}`).all(digest, now.toISOString()).map(row => String(row["task_id"]));
|
|
8517
|
-
this.
|
|
8518
|
-
.prepare(`UPDATE task_scope SET approved_at = NULL, approved_by = NULL, approved_digest = NULL,
|
|
8519
|
-
approved_profile_json = NULL, approval_basis = 'password', mode_digest = NULL ${demotable}`)
|
|
8520
|
-
.run(digest, now.toISOString());
|
|
8525
|
+
withdrawApproval(this, { where: demotable, args: [digest, now.toISOString()] }, { basisAfter: "password", clearRoute: false, clearModeDigest: true });
|
|
8521
8526
|
this.db.prepare("DELETE FROM plan_authorization WHERE basis = 'grant' AND mode_digest = ?").run(digest);
|
|
8522
8527
|
for (const taskId of demoted) {
|
|
8523
8528
|
const ref = this.lookupRef(taskId);
|
|
@@ -8562,12 +8567,7 @@ export class Store {
|
|
|
8562
8567
|
.prepare(`SELECT task_id FROM task_scope ${demotable}`)
|
|
8563
8568
|
.all(...demotableArgs)
|
|
8564
8569
|
.map(row => String(row["task_id"]));
|
|
8565
|
-
this
|
|
8566
|
-
.prepare(`UPDATE task_scope
|
|
8567
|
-
SET approved_at = NULL, approved_by = NULL, approved_digest = NULL,
|
|
8568
|
-
approved_profile_json = NULL, approval_basis = 'password', mode_digest = NULL
|
|
8569
|
-
${demotable}`)
|
|
8570
|
-
.run(...demotableArgs);
|
|
8570
|
+
withdrawApproval(this, { where: demotable, args: demotableArgs }, { basisAfter: "password", clearRoute: false, clearModeDigest: true });
|
|
8571
8571
|
for (const taskId of demoted) {
|
|
8572
8572
|
const ref = this.lookupRef(taskId);
|
|
8573
8573
|
if (ref === null)
|
|
@@ -9263,7 +9263,7 @@ export class Store {
|
|
|
9263
9263
|
* false: the disclosure boundary is one indistinguishable page (D6). */
|
|
9264
9264
|
inviteIsLive(tokenValue, now) {
|
|
9265
9265
|
const row = this.db
|
|
9266
|
-
.prepare(
|
|
9266
|
+
.prepare(`SELECT 1 AS hit FROM invite WHERE token_hash = ? AND ${INVITE_USABLE_SQL}`)
|
|
9267
9267
|
.get(createHash("sha256").update(tokenValue, "utf8").digest("hex"), now.toISOString());
|
|
9268
9268
|
return row !== undefined;
|
|
9269
9269
|
}
|
|
@@ -9276,7 +9276,7 @@ export class Store {
|
|
|
9276
9276
|
admitInviteAttempt(tokenValue, now) {
|
|
9277
9277
|
const row = this.db
|
|
9278
9278
|
.prepare(`UPDATE invite SET attempts = attempts + 1
|
|
9279
|
-
WHERE token_hash = ? AND
|
|
9279
|
+
WHERE token_hash = ? AND ${INVITE_USABLE_SQL}
|
|
9280
9280
|
RETURNING role, minted_by`)
|
|
9281
9281
|
.get(createHash("sha256").update(tokenValue, "utf8").digest("hex"), now.toISOString());
|
|
9282
9282
|
return row === undefined
|
|
@@ -9297,6 +9297,8 @@ export class Store {
|
|
|
9297
9297
|
// concurrent join must read as 'gone' — the indistinguishable dead
|
|
9298
9298
|
// shape — never as a name collision that discloses the winner.
|
|
9299
9299
|
const hash = createHash("sha256").update(args.tokenValue, "utf8").digest("hex");
|
|
9300
|
+
// Not INVITE_USABLE_SQL on purpose: admitInviteAttempt has already counted this submission, so the tenth
|
|
9301
|
+
// admitted attempt arrives here with attempts = 10 and must still be able to join.
|
|
9300
9302
|
const live = this.db
|
|
9301
9303
|
.prepare("SELECT 1 AS hit FROM invite WHERE token_hash = ? AND revoked_at IS NULL AND consumed_at IS NULL AND expires_at > ?")
|
|
9302
9304
|
.get(hash, now.toISOString());
|
|
@@ -9609,7 +9611,7 @@ export class Store {
|
|
|
9609
9611
|
* never leave the store; an invite is named by its row id. */
|
|
9610
9612
|
openInvites(now) {
|
|
9611
9613
|
return this.db
|
|
9612
|
-
.prepare(
|
|
9614
|
+
.prepare(`SELECT id, role, minted_by, minted_at, expires_at, attempts, projects_json FROM invite WHERE ${INVITE_USABLE_SQL} ORDER BY id`)
|
|
9613
9615
|
.all(now.toISOString())
|
|
9614
9616
|
.map(row => ({
|
|
9615
9617
|
id: Number(row["id"]),
|
|
@@ -11930,53 +11932,11 @@ export class Store {
|
|
|
11930
11932
|
* is still the one just approved, read inside this transaction ("moved" otherwise). */
|
|
11931
11933
|
expect) {
|
|
11932
11934
|
return this.transact(() => {
|
|
11933
|
-
|
|
11934
|
-
|
|
11935
|
-
|
|
11936
|
-
|
|
11937
|
-
|
|
11938
|
-
const taskRef = Number(ref["id"]);
|
|
11939
|
-
// A put-away task (v145) is restored before it runs again.
|
|
11940
|
-
if (refArchived(this, taskRef))
|
|
11941
|
-
return { ok: false, reason: "archived" };
|
|
11942
|
-
const stamp = now.toISOString();
|
|
11943
|
-
if (this.hasLiveClaim(taskRef, now))
|
|
11944
|
-
return { ok: false, reason: "claimed" };
|
|
11945
|
-
const incidents = this.db
|
|
11946
|
-
.prepare(`SELECT incident.id FROM incident
|
|
11947
|
-
JOIN run ON run.id = incident.run
|
|
11948
|
-
WHERE run.task_ref = ? AND incident.resolved_at IS NULL`)
|
|
11949
|
-
.all(taskRef)
|
|
11950
|
-
.map(row => Number(row["id"]));
|
|
11951
|
-
const state = String(this.db.prepare("SELECT state FROM task WHERE id = ?").get(taskId)?.["state"]);
|
|
11952
|
-
// A finished task whose last result was rejected — a failed gate, a
|
|
11953
|
-
// contradicting review, a refuted proof — runs again on the SAME
|
|
11954
|
-
// filing: the branch keeps the earlier attempt, the sealed diff still
|
|
11955
|
-
// spans from the original base, and a corrected candidate needs a
|
|
11956
|
-
// re-scope, not a new task. An accepted or published result is final
|
|
11957
|
-
// here; it is revised through a revision or its pull request.
|
|
11958
|
-
let rejectedRun = null;
|
|
11959
|
-
if (state === "done") {
|
|
11960
|
-
// The guards hold whether or not an incident is open: a finished
|
|
11961
|
-
// task's last result decides, and an accepted or published one is
|
|
11962
|
-
// never reopened by resolving an incident beside it.
|
|
11963
|
-
const last = this.db
|
|
11964
|
-
.prepare("SELECT id FROM run WHERE task_ref = ? AND role = 'builder' AND finished_at IS NOT NULL ORDER BY id DESC LIMIT 1")
|
|
11965
|
-
.get(taskRef);
|
|
11966
|
-
if (last === undefined) {
|
|
11967
|
-
if (incidents.length === 0)
|
|
11968
|
-
return { ok: false, reason: "not-stalled" };
|
|
11969
|
-
}
|
|
11970
|
-
else {
|
|
11971
|
-
rejectedRun = Number(last["id"]);
|
|
11972
|
-
const final = this.finalResultReason(rejectedRun);
|
|
11973
|
-
if (final !== null)
|
|
11974
|
-
return { ok: false, reason: final };
|
|
11975
|
-
}
|
|
11976
|
-
}
|
|
11977
|
-
else if (incidents.length === 0 && state !== "failed") {
|
|
11978
|
-
return { ok: false, reason: "not-stalled" };
|
|
11979
|
-
}
|
|
11935
|
+
// Who may run again is one read, shared with the claim refusal's words (requeueEligibility, end of file).
|
|
11936
|
+
const eligible = requeueEligibility(this, taskId, now);
|
|
11937
|
+
if (!eligible.ok)
|
|
11938
|
+
return eligible;
|
|
11939
|
+
const { taskRef, incidents, state, rejectedRun } = eligible;
|
|
11980
11940
|
if (expect !== undefined) {
|
|
11981
11941
|
const scope = this.db.prepare("SELECT digest, approved_digest FROM task_scope WHERE task_id = ?").get(taskId);
|
|
11982
11942
|
if (state !== "done" || rejectedRun !== expect.run || scope === undefined || String(scope["digest"]) !== expect.scopeDigest ||
|
|
@@ -11990,9 +11950,7 @@ export class Store {
|
|
|
11990
11950
|
// A person's Retry is a new person act: a fast start gets a new start window. A machine's never does.
|
|
11991
11951
|
if (!machineActing())
|
|
11992
11952
|
this.db.prepare("UPDATE task_ref SET fast_start_until = ? WHERE id = ? AND fast_start_until IS NOT NULL").run(new Date(now.getTime() + 30 * 60_000).toISOString(), taskRef);
|
|
11993
|
-
this.db
|
|
11994
|
-
.prepare("UPDATE task SET state = 'queued', updated_at = ? WHERE id = ? AND state IN ('failed','queued','done')")
|
|
11995
|
-
.run(stamp, taskId);
|
|
11953
|
+
moveTask(this.db, taskId, "retry", now);
|
|
11996
11954
|
// The stall's page is answered by the act itself.
|
|
11997
11955
|
this.resolveEpisode(`stalled:${taskRef}`, now);
|
|
11998
11956
|
this.noteLifecycle({
|
|
@@ -13511,7 +13469,7 @@ export class Store {
|
|
|
13511
13469
|
if (taskId === null)
|
|
13512
13470
|
return;
|
|
13513
13471
|
const role = String(prior["role"]);
|
|
13514
|
-
const stopped = result
|
|
13472
|
+
const stopped = wasInterrupted(result) && this.stopOf(id) !== null;
|
|
13515
13473
|
if (role === "builder") {
|
|
13516
13474
|
if (stopped) {
|
|
13517
13475
|
this.noteLifecycle({ taskRef, run: id, kind: "run-stopped", identity: `r${id}`, subject: `Build #${id} stopped`, body: "Its work is preserved. Resume this attempt from the task page when ready.", link: chatControlHref("recovery", taskId) }, now);
|
|
@@ -14079,7 +14037,7 @@ export class Store {
|
|
|
14079
14037
|
return `run #${parent.id} left its draft in ${parent.worktree ?? "no worktree"} on ${parent.branch ?? "no branch"}, not ${run.worktree} on ${run.branch}`;
|
|
14080
14038
|
// An attempt that stopped before its handoff (run 2085) left its work in
|
|
14081
14039
|
// place for exactly this: the retry inherits it, like an interrupted one's.
|
|
14082
|
-
const interrupted = parent.outcome === null || (parent
|
|
14040
|
+
const interrupted = parent.outcome === null || endedInterrupted(parent) || (parent.outcome === "failed" && parent.reason === "no-handoff");
|
|
14083
14041
|
if (!interrupted)
|
|
14084
14042
|
return `run #${parent.id} ended as ${parent.outcome}${parent.reason === null ? "" : ` (${parent.reason})`} — only an interrupted attempt's draft is recovered`;
|
|
14085
14043
|
if (parent.outcome === null && this.liveClaimByLease(parent.leaseId, now) !== null)
|
|
@@ -18731,7 +18689,7 @@ export class Store {
|
|
|
18731
18689
|
const q = this.db
|
|
18732
18690
|
.prepare(`SELECT
|
|
18733
18691
|
SUM(CASE WHEN task.state = 'queued' THEN 1 ELSE 0 END) AS queued,
|
|
18734
|
-
SUM(CASE WHEN
|
|
18692
|
+
SUM(CASE WHEN ${markedRunningSql} THEN 1 ELSE 0 END) AS running
|
|
18735
18693
|
FROM task JOIN task_ref ON task_ref.backend = 'built-in' AND task_ref.external_id = task.id
|
|
18736
18694
|
WHERE task_ref.repo = ? AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)`)
|
|
18737
18695
|
.get(repo);
|
|
@@ -21761,7 +21719,7 @@ export function serializeReleaseTerms(terms) {
|
|
|
21761
21719
|
}
|
|
21762
21720
|
// ---- one approval per release: the authority record (release-once.md, part 1) --------------------------------------
|
|
21763
21721
|
export { LIVE_RELEASE_STATES, belongsToRelease, newestReleaseGeneration, outstandingReleaseOperations, releaseAuthority, releaseAuthorityOfTask, releaseCarries, releaseCarryTo, releaseOperation, releaseOperations, releaseSealProblem, } from "./release-authority.js";
|
|
21764
|
-
import { LIVE_RELEASE_STATES, autoEvidenceOf, belongsToRelease, newestReleaseGeneration, outstandingReleaseOperations, releaseAuthority, releaseCarryTo, releaseOperation, releaseSealProblem, } from "./release-authority.js";
|
|
21722
|
+
import { LIVE_RELEASE_STATES, autoEvidenceOf, belongsToRelease, releaseExpiredAt, newestReleaseGeneration, outstandingReleaseOperations, releaseAuthority, releaseCarryTo, releaseOperation, releaseSealProblem, } from "./release-authority.js";
|
|
21765
21723
|
/**
|
|
21766
21724
|
* Open a release's authority for a person's seal of its terms (inside the seal's transaction): the next generation,
|
|
21767
21725
|
* superseding the live one. False when it can't (releaseSealProblem), which refuses the seal.
|
|
@@ -21836,7 +21794,7 @@ export function reserveRelease(store, input) {
|
|
|
21836
21794
|
return { ok: false, why: "This release still waits for GitHub CI. Resume it with `toolroll release`." };
|
|
21837
21795
|
const at = input.now.toISOString();
|
|
21838
21796
|
// Expiry stops every live state; an operation already reserved settles only through reconciliation.
|
|
21839
|
-
if (a.
|
|
21797
|
+
if (releaseExpiredAt(a, input.now))
|
|
21840
21798
|
return { ok: false, why: "The release approval expired." };
|
|
21841
21799
|
if (a.tipTask !== input.taskId || a.tipSha !== input.candidate)
|
|
21842
21800
|
return { ok: false, why: `Task ${input.taskId} is not the release's current commit.` };
|
|
@@ -21882,7 +21840,7 @@ export function runReleaseOperation(store, id, epoch, now, issuer = currentRelea
|
|
|
21882
21840
|
if (releaseCiRunningOf(store, op.taskId) !== null)
|
|
21883
21841
|
return false;
|
|
21884
21842
|
// Past the expiry nothing new starts; the reserved operation is settled by reconciliation instead.
|
|
21885
|
-
if (a
|
|
21843
|
+
if (releaseExpiredAt(a, now))
|
|
21886
21844
|
return false;
|
|
21887
21845
|
// The lease that reserved it still holds (a person's direct deploy: still no coordinator holding the release), and
|
|
21888
21846
|
// the live-release predicate still accepts it, at this moment (review round 1).
|
|
@@ -22644,7 +22602,7 @@ export function restoreTaskFamily(store, taskId, by, now, guard) {
|
|
|
22644
22602
|
*/
|
|
22645
22603
|
export function liveInviteTerms(store, tokenValue, now) {
|
|
22646
22604
|
const row = store.handle
|
|
22647
|
-
.prepare(
|
|
22605
|
+
.prepare(`SELECT role, minted_by, expires_at, projects_json FROM invite WHERE token_hash = ? AND ${INVITE_USABLE_SQL}`)
|
|
22648
22606
|
.get(createHash("sha256").update(tokenValue, "utf8").digest("hex"), now.toISOString());
|
|
22649
22607
|
if (row === undefined)
|
|
22650
22608
|
return null;
|
|
@@ -23249,3 +23207,106 @@ export function leadAskAwaitsAnswer(store, thread, now) {
|
|
|
23249
23207
|
WHERE m.id = (SELECT MAX(id) FROM lead_message WHERE thread = ? AND role = 'assistant') AND t.state = 'answered'`).get(thread);
|
|
23250
23208
|
return row !== undefined && Date.parse(String(row["at"])) + LEAD_ASK_TTL_MS > now.getTime();
|
|
23251
23209
|
}
|
|
23210
|
+
/** When an invite can still be used: not revoked, not consumed, not expired, and fewer than ten admitted attempts.
|
|
23211
|
+
* One bound parameter: the current time as an ISO string. Every read and write that asks "is this invite usable" uses
|
|
23212
|
+
* this fragment, so the join page, the attempt counter, the join terms and the open-invites list agree. */
|
|
23213
|
+
export const INVITE_USABLE_SQL = "revoked_at IS NULL AND consumed_at IS NULL AND expires_at > ? AND attempts < 10";
|
|
23214
|
+
/** How long a confirm step stays open: the Yes and Cancel buttons shown after a first tap, in every chat app. */
|
|
23215
|
+
export const CONFIRM_TTL_MS = 10 * 60_000;
|
|
23216
|
+
/**
|
|
23217
|
+
* Withdraw task approvals: the one statement every withdrawal runs. It always clears approved_at, approved_by,
|
|
23218
|
+
* approved_digest and approved_profile_json, and sets approval_basis to `basisAfter`. The callers differ in what else
|
|
23219
|
+
* they clear, so that stays theirs to say: `clearRoute` clears approved_route_json, `clearModeDigest` clears
|
|
23220
|
+
* mode_digest. `where` is the caller's own WHERE clause (starting with WHERE) with its bound values; it is always a
|
|
23221
|
+
* fixed string in our code, never built from input. The check and the write are one statement. Returns the rows changed.
|
|
23222
|
+
*/
|
|
23223
|
+
export function withdrawApproval(store, rows, writes) {
|
|
23224
|
+
const set = ["approved_at = NULL", "approved_by = NULL", "approved_digest = NULL", "approved_profile_json = NULL",
|
|
23225
|
+
...(writes.clearRoute ? ["approved_route_json = NULL"] : []), "approval_basis = ?", ...(writes.clearModeDigest ? ["mode_digest = NULL"] : [])];
|
|
23226
|
+
return Number(store.handle.prepare(`UPDATE task_scope SET ${set.join(", ")} ${rows.where}`).run(writes.basisAfter, ...rows.args).changes);
|
|
23227
|
+
}
|
|
23228
|
+
/**
|
|
23229
|
+
* Whether `requeueTask` would run this task again, read without writing anything: the same checks in the same order,
|
|
23230
|
+
* so the requeue itself and every sentence that offers it agree. Read inside the caller's transaction.
|
|
23231
|
+
*/
|
|
23232
|
+
export function requeueEligibility(store, taskId, now) {
|
|
23233
|
+
const db = store.handle;
|
|
23234
|
+
const ref = db.prepare("SELECT id FROM task_ref WHERE backend = ? AND external_id = ?").get(BUILT_IN, taskId);
|
|
23235
|
+
if (ref === undefined)
|
|
23236
|
+
return { ok: false, reason: "unknown-task" };
|
|
23237
|
+
const taskRef = Number(ref["id"]);
|
|
23238
|
+
// A put-away task (v145) is restored before it runs again.
|
|
23239
|
+
if (refArchived(store, taskRef))
|
|
23240
|
+
return { ok: false, reason: "archived" };
|
|
23241
|
+
if (store.hasLiveClaim(taskRef, now))
|
|
23242
|
+
return { ok: false, reason: "claimed" };
|
|
23243
|
+
const incidents = db
|
|
23244
|
+
.prepare(`SELECT incident.id FROM incident
|
|
23245
|
+
JOIN run ON run.id = incident.run
|
|
23246
|
+
WHERE run.task_ref = ? AND incident.resolved_at IS NULL`)
|
|
23247
|
+
.all(taskRef)
|
|
23248
|
+
.map(row => Number(row["id"]));
|
|
23249
|
+
const state = String(db.prepare("SELECT state FROM task WHERE id = ?").get(taskId)?.["state"]);
|
|
23250
|
+
// A finished task whose last result was rejected — a failed gate, a
|
|
23251
|
+
// contradicting review, a refuted proof — runs again on the SAME
|
|
23252
|
+
// filing: the branch keeps the earlier attempt, the sealed diff still
|
|
23253
|
+
// spans from the original base, and a corrected candidate needs a
|
|
23254
|
+
// re-scope, not a new task. An accepted or published result is final
|
|
23255
|
+
// here; it is revised through a revision or its pull request.
|
|
23256
|
+
let rejectedRun = null;
|
|
23257
|
+
if (state === "done") {
|
|
23258
|
+
// The guards hold whether or not an incident is open: a finished
|
|
23259
|
+
// task's last result decides, and an accepted or published one is
|
|
23260
|
+
// never reopened by resolving an incident beside it.
|
|
23261
|
+
const last = db
|
|
23262
|
+
.prepare("SELECT id FROM run WHERE task_ref = ? AND role = 'builder' AND finished_at IS NOT NULL ORDER BY id DESC LIMIT 1")
|
|
23263
|
+
.get(taskRef);
|
|
23264
|
+
if (last === undefined) {
|
|
23265
|
+
if (incidents.length === 0)
|
|
23266
|
+
return { ok: false, reason: "not-stalled" };
|
|
23267
|
+
}
|
|
23268
|
+
else {
|
|
23269
|
+
rejectedRun = Number(last["id"]);
|
|
23270
|
+
const final = store.finalResultReason(rejectedRun);
|
|
23271
|
+
if (final !== null)
|
|
23272
|
+
return { ok: false, reason: final };
|
|
23273
|
+
}
|
|
23274
|
+
}
|
|
23275
|
+
else if (incidents.length === 0 && state !== "failed") {
|
|
23276
|
+
return { ok: false, reason: "not-stalled" };
|
|
23277
|
+
}
|
|
23278
|
+
return { ok: true, taskRef, incidents, state, rejectedRun };
|
|
23279
|
+
}
|
|
23280
|
+
export function isClosedState(state) {
|
|
23281
|
+
return state === "done" || state === "failed" || state === "cancelled";
|
|
23282
|
+
}
|
|
23283
|
+
/**
|
|
23284
|
+
* What this database knows about the task behind a reference, for a claim: a built-in task's row and state, a
|
|
23285
|
+
* built-in reference with no task row, or a task of another backend (its state lives in that tracker, not here).
|
|
23286
|
+
* Read inside the caller's transaction, so the answer holds until it commits.
|
|
23287
|
+
*/
|
|
23288
|
+
export function taskStateForClaim(store, taskRef) {
|
|
23289
|
+
const row = store.handle.prepare(`SELECT task_ref.backend AS backend, task.state AS state FROM task_ref
|
|
23290
|
+
LEFT JOIN task ON task.id = task_ref.external_id AND task_ref.backend = ?
|
|
23291
|
+
WHERE task_ref.id = ?`).get(BUILT_IN, taskRef);
|
|
23292
|
+
if (row === undefined)
|
|
23293
|
+
return { kind: "missing" };
|
|
23294
|
+
if (String(row["backend"]) !== BUILT_IN)
|
|
23295
|
+
return { kind: "outside" };
|
|
23296
|
+
if (row["state"] === null || row["state"] === undefined)
|
|
23297
|
+
return { kind: "missing" };
|
|
23298
|
+
return { kind: "built-in", state: String(row["state"]) };
|
|
23299
|
+
}
|
|
23300
|
+
/**
|
|
23301
|
+
* Check cleanup's proof (acp-check.ts settleChecks, claim.ts acquireForCheckCleanup): the task reference of check
|
|
23302
|
+
* `checkId` when that check belongs to `runner`, has finished (passed or failed) and is not yet settled, is held right
|
|
23303
|
+
* now by exactly this cleanup `attempt`, and is bound to the built-in task named from the check row's own id. Null
|
|
23304
|
+
* otherwise. Every fact comes from rows only code writes; read inside the caller's transaction.
|
|
23305
|
+
*/
|
|
23306
|
+
export function checkCleanupTaskOf(store, checkId, runner, attempt, now) {
|
|
23307
|
+
const row = store.handle.prepare(`SELECT r.task_ref AS ref FROM acp_check_request r
|
|
23308
|
+
JOIN task_ref t ON t.id = r.task_ref AND t.backend = ? AND t.external_id = 'agent-check-' || r.id
|
|
23309
|
+
WHERE r.id = ? AND r.runner = ? AND r.state IN ('passed','failed') AND r.settled_at IS NULL
|
|
23310
|
+
AND r.settle_by = ? AND r.settle_until > ?`).get(BUILT_IN, checkId, runner, attempt, now.toISOString());
|
|
23311
|
+
return row === undefined ? null : Number(row["ref"]);
|
|
23312
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* the person it was asked of. Answered anywhere, every button retires.
|
|
12
12
|
*/
|
|
13
13
|
import { randomBytes } from "node:crypto";
|
|
14
|
-
import { partContent
|
|
14
|
+
import { partContent } from "./chat-delivery-state.js";
|
|
15
15
|
import { answerSubagentQuestion } from "./subagent-work.js";
|
|
16
16
|
import { labelOf } from "./subagent-admin.js";
|
|
17
17
|
import { telegramButton } from "./contracts/telegram-callback.js";
|
|
@@ -97,12 +97,14 @@ export function chatQuestionButtons(state, part, now) {
|
|
|
97
97
|
return state.prepare("SELECT token,choice FROM chat_question_action WHERE provider=:provider AND part=? AND consumed IS NULL AND expires>? ORDER BY rowid").all(part, now.toISOString())
|
|
98
98
|
.map(one => ({ token: String(one.token), label: (labels.get(one.choice ?? "") ?? IN_WORDS).slice(0, 75), words: one.choice === null }));
|
|
99
99
|
}
|
|
100
|
-
/** Show the tapped notice again with the answer, and without its buttons.
|
|
101
|
-
|
|
100
|
+
/** Show the tapped notice again with the answer, and without its buttons. A notice that was dropped isn't brought back:
|
|
101
|
+
* the line goes as a new message. */
|
|
102
|
+
function repaint(state, part, event, line, now) {
|
|
102
103
|
const row = state.prepare("SELECT payload FROM chat_part WHERE provider=:provider AND id=?").get(part);
|
|
103
104
|
const before = row === undefined ? { text: "" } : partContent(String(row["payload"]));
|
|
104
105
|
const content = { text: `${before.text}\n\n${line}`.slice(0, 3400), edit: event.ts, ...(before.link === undefined ? {} : { link: before.link }), ...(before.channel === undefined ? {} : { channel: before.channel }) };
|
|
105
|
-
state.
|
|
106
|
+
if (!state.repaint(part, content))
|
|
107
|
+
state.plan(event.id, [{ text: line }], now);
|
|
106
108
|
}
|
|
107
109
|
/** A tapped question button, inside the action's transaction; false when the token isn't one. */
|
|
108
110
|
export function applyChatQuestionTap(options, event, binding, token, now) {
|
|
@@ -120,7 +122,7 @@ export function applyChatQuestionTap(options, event, binding, token, now) {
|
|
|
120
122
|
const question = store.subagentQuestion(questionId);
|
|
121
123
|
if (action["consumed"] !== null || String(action["expires"]) <= now.toISOString() || question === null || question.state !== "open") {
|
|
122
124
|
retireEverywhere(store, questionId, now, state);
|
|
123
|
-
repaint(state, part, event, "This was already answered.");
|
|
125
|
+
repaint(state, part, event, "This was already answered.", now);
|
|
124
126
|
state.finish(event.id);
|
|
125
127
|
return true;
|
|
126
128
|
}
|
|
@@ -139,7 +141,7 @@ export function applyChatQuestionTap(options, event, binding, token, now) {
|
|
|
139
141
|
}
|
|
140
142
|
retireEverywhere(store, questionId, now, state);
|
|
141
143
|
const label = question.options.find(one => one.id === action["choice"])?.label ?? String(action["choice"]);
|
|
142
|
-
repaint(state, part, event, `✅ You answered: ${label}. ${answered.said}
|
|
144
|
+
repaint(state, part, event, `✅ You answered: ${label}. ${answered.said}`, now);
|
|
143
145
|
state.finish(event.id);
|
|
144
146
|
return true;
|
|
145
147
|
}
|
package/dist/surface.js
CHANGED
|
@@ -366,10 +366,10 @@ const GUIDE_ROWS = [
|
|
|
366
366
|
{ name: "runner", takesValue: true, meaning: "your registered worker name" },
|
|
367
367
|
{ name: "token", takesValue: true, meaning: "the worker's token" },
|
|
368
368
|
{ name: "ttl", takesValue: true, meaning: "lease seconds" }],
|
|
369
|
-
notableReasons: ["held", "fenced", "unapproved", "reserved", "external", "claimed", "not-ready"] },
|
|
369
|
+
notableReasons: ["held", "fenced", "unapproved", "reserved", "external", "claimed", "not-ready", "closed"] },
|
|
370
370
|
{ invocation: "heartbeat", synopsis: "still working; extends the lease", audience: "agent", agentMayInvoke: true, mutation: "identity-idempotent",
|
|
371
371
|
positionals: [{ name: "lease", required: true, meaning: "the lease id" }],
|
|
372
|
-
flags: [jsonFlag, dbFlag], notableReasons: ["not-leased", "fenced"] },
|
|
372
|
+
flags: [jsonFlag, dbFlag], notableReasons: ["not-leased", "fenced", "closed"] },
|
|
373
373
|
{ invocation: "worker release", synopsis: "done with it; fenced if superseded", audience: "agent", agentMayInvoke: true, mutation: "identity-idempotent",
|
|
374
374
|
positionals: [{ name: "lease", required: true, meaning: "the lease id" }],
|
|
375
375
|
flags: [jsonFlag, dbFlag], notableReasons: ["not-leased", "fenced"] },
|
package/dist/task-archive.d.ts
CHANGED
|
@@ -8,3 +8,6 @@ export declare function taskArchived(store: Store, taskId: string): boolean;
|
|
|
8
8
|
/** Whether a flow card's work is put away: its current task or its first build task (a later stage keeps the first
|
|
9
9
|
* and may clear the current one). Read from the card's own row now, so a put away that committed first holds it. */
|
|
10
10
|
export declare function cardTaskArchived(store: Store, cardId: number): boolean;
|
|
11
|
+
/** Whether the built-in task with this id is cancelled. Every door that approves, seals or carries a release asks
|
|
12
|
+
* this beside the put-away check: a cancelled task takes no new yes (fail closed), by any road. */
|
|
13
|
+
export declare function taskCancelled(store: Store, taskId: string): boolean;
|
package/dist/task-archive.js
CHANGED
|
@@ -14,3 +14,8 @@ export function cardTaskArchived(store, cardId) {
|
|
|
14
14
|
return store.handle.prepare(`SELECT 1 FROM flow_card c JOIN task_ref r ON r.backend = 'built-in' AND r.external_id IN (c.task, c.primary_task)
|
|
15
15
|
JOIN task_archive a ON a.task_ref = r.id WHERE c.id = ?`).get(cardId) !== undefined;
|
|
16
16
|
}
|
|
17
|
+
/** Whether the built-in task with this id is cancelled. Every door that approves, seals or carries a release asks
|
|
18
|
+
* this beside the put-away check: a cancelled task takes no new yes (fail closed), by any road. */
|
|
19
|
+
export function taskCancelled(store, taskId) {
|
|
20
|
+
return store.handle.prepare("SELECT 1 FROM task WHERE id = ? AND state = 'cancelled'").get(taskId) !== undefined;
|
|
21
|
+
}
|