toolroll 0.9.99 → 0.9.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/acp-check.js +4 -2
- package/dist/agent-state.js +2 -1
- package/dist/agents-board.js +3 -2
- package/dist/agents-live.js +3 -2
- package/dist/approval-authority.d.ts +2 -1
- package/dist/approval-authority.js +2 -1
- package/dist/chat-ask.js +4 -2
- package/dist/chat-core.js +4 -2
- package/dist/chat-decide.js +1 -2
- package/dist/chat-delivery-state.d.ts +29 -6
- package/dist/chat-delivery-state.js +43 -13
- package/dist/chat-delivery.js +12 -5
- package/dist/chat-flow.js +15 -13
- package/dist/chat-live.js +5 -2
- package/dist/chat-message-states.d.ts +75 -0
- package/dist/chat-message-states.js +74 -0
- package/dist/chat-messages.d.ts +10 -4
- package/dist/chat-messages.js +12 -7
- package/dist/check-failure.js +2 -1
- package/dist/check-report.js +1 -1
- package/dist/claim.d.ts +40 -2
- package/dist/claim.js +92 -19
- package/dist/contracts/chat-tables.js +3 -0
- package/dist/coordinator.js +3 -3
- package/dist/demo.js +7 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-chat.js +2 -3
- package/dist/dispatch.js +2 -1
- package/dist/dispose.d.ts +4 -1
- package/dist/dispose.js +2 -1
- package/dist/fallback.js +3 -2
- package/dist/fast-start.js +2 -2
- package/dist/monitoring.js +2 -1
- package/dist/operate.js +75 -36
- package/dist/plane-review.js +2 -1
- package/dist/project-knowledge.d.ts +3 -3
- package/dist/release-authority.d.ts +9 -0
- package/dist/release-authority.js +13 -0
- package/dist/release-auto.js +2 -3
- package/dist/release-gate.js +1 -1
- package/dist/release-view.js +2 -1
- package/dist/release.js +18 -30
- package/dist/result-shots.js +1 -1
- package/dist/route-results.js +2 -1
- package/dist/run-ending.d.ts +21 -0
- package/dist/run-ending.js +19 -0
- package/dist/scope.js +2 -2
- package/dist/server/render-results.js +3 -2
- package/dist/server/render-tasks.js +3 -2
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-chat.js +2 -3
- package/dist/store.d.ts +61 -2
- package/dist/store.js +127 -77
- package/dist/subagent-question.js +8 -6
- package/dist/surface.js +2 -2
- package/dist/task-moves.d.ts +90 -0
- package/dist/task-moves.js +68 -0
- package/dist/teams-chat.js +2 -2
- package/dist/telegram-flow.d.ts +1 -1
- package/dist/telegram-flow.js +1 -1
- package/dist/telegram-lead.d.ts +1 -2
- package/dist/telegram-lead.js +18 -23
- package/dist/telegram-progress.js +2 -1
- package/dist/telegram.d.ts +0 -1
- package/dist/telegram.js +6 -7
- package/dist/vanished-retry.js +3 -2
- package/dist/workspace-ui.js +2 -1
- package/dist/worktree.js +3 -2
- package/package.json +1 -1
package/dist/discord-chat.js
CHANGED
|
@@ -7,7 +7,7 @@ import { cardWithin } from "./chat-card.js";
|
|
|
7
7
|
import { chatFlowButtons } from "./chat-flow.js";
|
|
8
8
|
import { refuseResultShots, resultShotsPruned, shotWaitsUntil } from "./result-shots.js";
|
|
9
9
|
import { admitChatEvent } from "./chat-admission.js";
|
|
10
|
-
import { ChatState, ChatDeliveryError, chatHash, partContent, } from "./chat-delivery-state.js";
|
|
10
|
+
import { ChatState, ChatDeliveryError, chatHash, chatRetryAt, partContent, } from "./chat-delivery-state.js";
|
|
11
11
|
import { readChatPart } from "./contracts/chat-content.js";
|
|
12
12
|
import { readDiscordButtonToken, readDiscordComponentInteraction } from "./contracts/discord-callback.js";
|
|
13
13
|
import { processChatEvent, planChatNotifications, channelAccess, chatObject as object, planRoomMessages, heldForWatch, } from "./chat-delivery.js";
|
|
@@ -418,8 +418,7 @@ export async function deliverDiscordPart(options) {
|
|
|
418
418
|
const problem = error instanceof ChatDeliveryError
|
|
419
419
|
? error
|
|
420
420
|
: new DiscordError("Discord delivery is waiting to retry", 15_000, true);
|
|
421
|
-
const until =
|
|
422
|
-
Math.max(problem.retryMs, [5000, 15000, 60000, 300000][Math.min(row.attempts, 3)])).toISOString();
|
|
421
|
+
const until = chatRetryAt(options.clock?.() ?? new Date(), problem.retryMs, row.attempts);
|
|
423
422
|
state.partFailed(row.id, { problem: problem.message, until, uncertain: problem.uncertain });
|
|
424
423
|
state.setProblem(identity.installation, options.owner, problem.code === "ratelimited" ? "Discord asked us to wait. Saved replies will retry." : problem.message);
|
|
425
424
|
if (problem.code === "ratelimited")
|
package/dist/dispatch.js
CHANGED
|
@@ -19,6 +19,7 @@ import { failedAttemptSentence, latestFinishedAttempt } from "./needs-you.js";
|
|
|
19
19
|
import { buildReviewOf } from "./review-switch.js";
|
|
20
20
|
import { vanishedStopOf, vanishedStopSentence } from "./vanished-retry.js";
|
|
21
21
|
import { routeChangeOf } from "./route-change.js";
|
|
22
|
+
import { wasInterrupted } from "./run-ending.js";
|
|
22
23
|
import { BUILT_IN, refArchived, parseCapabilityKey } from "./store.js";
|
|
23
24
|
import { shortWhen } from "./when-html.js";
|
|
24
25
|
export const DEFAULT_MAX_OPEN_DECISIONS = 5;
|
|
@@ -157,7 +158,7 @@ function reviewViewOf(sourceRun, retry) {
|
|
|
157
158
|
retriesRemaining: retry.retriesRemaining,
|
|
158
159
|
latestRun: retry.latest?.runId ?? null,
|
|
159
160
|
latestReason: retry.latest?.reason ?? null,
|
|
160
|
-
interrupted: retry.latest !== null && (retry.latest
|
|
161
|
+
interrupted: retry.latest !== null && wasInterrupted(retry.latest),
|
|
161
162
|
queued: retry.openRequest === null ? null : { requestedBy: retry.openRequest.requestedBy, origin: retry.openRequest.origin },
|
|
162
163
|
};
|
|
163
164
|
}
|
package/dist/dispose.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { STORAGE_HOLD_PREFIX } from "./needs-you.js";
|
|
2
2
|
import { type FailureClass } from "./claim.js";
|
|
3
3
|
import type { BuildResult } from "./builder.js";
|
|
4
|
-
import type { Store } from "./store.js";
|
|
4
|
+
import type { ClosedState, Store } from "./store.js";
|
|
5
5
|
import { type ProofVerdict } from "./proof.js";
|
|
6
6
|
/**
|
|
7
7
|
* Which road is disposing. 'tick' = the unattended loop: full task
|
|
@@ -59,6 +59,9 @@ export type Disposition = {
|
|
|
59
59
|
disposition: string | null;
|
|
60
60
|
strikes: number | null;
|
|
61
61
|
sealed: boolean;
|
|
62
|
+
/** The state sealed when strikes ran out on a task a person had already closed (disposition "closed"): it is
|
|
63
|
+
* kept, with no incident, hold or page. Null for every other ending. */
|
|
64
|
+
closedState: ClosedState | null;
|
|
62
65
|
}
|
|
63
66
|
/** The standalone road's simple record: outcome written, nothing else. */
|
|
64
67
|
| {
|
package/dist/dispose.js
CHANGED
|
@@ -316,7 +316,7 @@ function disposeBuildOutcomeLocked(context, result) {
|
|
|
316
316
|
}
|
|
317
317
|
if (leaseId === undefined) {
|
|
318
318
|
store.finishRun(runId, { outcome: "failed", reason: result.reason, now: clock() });
|
|
319
|
-
return { kind: "failed", failureClass, disposition: null, strikes: null, sealed: false };
|
|
319
|
+
return { kind: "failed", failureClass, disposition: null, strikes: null, sealed: false, closedState: null };
|
|
320
320
|
}
|
|
321
321
|
const sealed = finalize(store, leaseId, {
|
|
322
322
|
kind: "failure",
|
|
@@ -333,6 +333,7 @@ function disposeBuildOutcomeLocked(context, result) {
|
|
|
333
333
|
disposition: sealed.ok ? sealed.disposition : null,
|
|
334
334
|
strikes: sealed.ok && "strikes" in sealed ? sealed.strikes : null,
|
|
335
335
|
sealed: sealed.ok,
|
|
336
|
+
closedState: sealed.ok && sealed.disposition === "closed" ? sealed.state : null,
|
|
336
337
|
};
|
|
337
338
|
}
|
|
338
339
|
if (result.reason === "protected-storage") {
|
package/dist/fallback.js
CHANGED
|
@@ -22,6 +22,7 @@ import { isProviderId, validateSpec } from "./provider.js";
|
|
|
22
22
|
import { providerName } from "./provider-auth.js";
|
|
23
23
|
import { PLAN_LOGIN_PROVABLE } from "./plan-login.js";
|
|
24
24
|
import { canonicalChainJson, chainDigestOf, LIVE_CHAIN_VERSION, liveChainFromJson, machineActing, profileDigestOf } from "./scope.js";
|
|
25
|
+
import { moveTask } from "./task-moves.js";
|
|
25
26
|
export const MAX_FALLBACK_ENTRIES = 3;
|
|
26
27
|
const tablesSeen = new WeakSet();
|
|
27
28
|
function hasTables(store) {
|
|
@@ -516,7 +517,7 @@ export function blockAfterSignedOut(store, runId, now) {
|
|
|
516
517
|
store.handle.prepare("UPDATE run SET reason = 'fallback-signed-out' WHERE id = ? AND outcome = 'refused'").run(runId);
|
|
517
518
|
const ref = store.refForId(run.taskRef);
|
|
518
519
|
if (ref !== null)
|
|
519
|
-
store.handle.
|
|
520
|
+
moveTask(store.handle, ref.externalId, "giveUp", now);
|
|
520
521
|
return true;
|
|
521
522
|
});
|
|
522
523
|
}
|
|
@@ -568,7 +569,7 @@ export function moveOnIfExhausted(store, runId, taskId, now) {
|
|
|
568
569
|
store.handle.prepare("INSERT INTO fallback_move (walk, to_index, from_run, terminal_class, paid, moved_at) VALUES (?, ?, ?, ?, ?, ?)")
|
|
569
570
|
.run(walk.id, run.chainIndex + 1, run.id, evidence.terminalClass, paid ? 1 : 0, now.toISOString());
|
|
570
571
|
store.handle.prepare("UPDATE fallback_walk SET cursor = ? WHERE id = ?").run(run.chainIndex + 1, walk.id);
|
|
571
|
-
store.handle
|
|
572
|
+
moveTask(store.handle, taskId, "requeue", now);
|
|
572
573
|
store.recordAction({ at: now.toISOString(), actor: "toolroll", repo: store.refForId(run.taskRef)?.repo ?? null, taskId, runId: run.id, action: "work moved to fallback agent", outcome: paid ? "paid" : "plan", source: "work", detail: entryWords(to) });
|
|
573
574
|
tell(store, run, taskId, "moved", to, now);
|
|
574
575
|
store.bumpWake();
|
package/dist/fast-start.js
CHANGED
|
@@ -31,7 +31,7 @@ import { fileTaskProposal } from "./proposal.js";
|
|
|
31
31
|
import { releaseCiHoldOf } from "./release-ci-hold.js";
|
|
32
32
|
import { COMPLETION_ACTION, COMPLETION_BY_PERSON } from "./result-completion.js";
|
|
33
33
|
import { deployGateScope, machineActing, propose, releaseCheckScope } from "./scope.js";
|
|
34
|
-
import { BUILT_IN } from "./store.js";
|
|
34
|
+
import { BUILT_IN, withdrawApproval } from "./store.js";
|
|
35
35
|
import { classifyTask, filingSizer, heuristicSizing } from "./task-sizing.js";
|
|
36
36
|
import { issueImported } from "./task-source.js";
|
|
37
37
|
import { titleWithin } from "./task-text.js";
|
|
@@ -295,7 +295,7 @@ export function expireFastStarts(store, now) {
|
|
|
295
295
|
store.transact(() => {
|
|
296
296
|
if (store.currentLiveLease(ref, now) !== null)
|
|
297
297
|
return;
|
|
298
|
-
store
|
|
298
|
+
withdrawApproval(store, { where: "WHERE task_id = ?", args: [task] }, { basisAfter: null, clearRoute: true, clearModeDigest: false });
|
|
299
299
|
store.handle.prepare("UPDATE task_ref SET fast_start_until = NULL WHERE id = ?").run(ref);
|
|
300
300
|
store.recordAction({ at: stamp, actor: "system", repo: row["repo"] === null ? null : String(row["repo"]), taskId: task, runId: null, action: "fast start expired", outcome: "seal cleared", source: "work", detail: `no build started within ${FAST_WINDOW_MS / 60_000} minutes` });
|
|
301
301
|
cleared++;
|
package/dist/monitoring.js
CHANGED
|
@@ -19,6 +19,7 @@ import { createHash, createHmac, randomUUID } from "node:crypto";
|
|
|
19
19
|
import { closeSync, constants, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, writeSync } from "node:fs";
|
|
20
20
|
import { hostname } from "node:os";
|
|
21
21
|
import { basename, join } from "node:path";
|
|
22
|
+
import { wasInterrupted } from "./run-ending.js";
|
|
22
23
|
import { COMPLETION_ACTION } from "./result-completion.js";
|
|
23
24
|
export const SIGNATURE_HEADER = "x-standing-orders-signature";
|
|
24
25
|
const BATCH = 200;
|
|
@@ -164,7 +165,7 @@ export function spansFor(store, entries) {
|
|
|
164
165
|
continue;
|
|
165
166
|
const root = rootOf(taskId);
|
|
166
167
|
const ref = store.refForId(run.taskRef);
|
|
167
|
-
const failed = run.outcome === "failed" || run.outcome === "refused" || run
|
|
168
|
+
const failed = run.outcome === "failed" || run.outcome === "refused" || wasInterrupted(run);
|
|
168
169
|
spans.push({
|
|
169
170
|
traceId: hex(`standing-orders/trace/${root}`, 32), spanId: hex(`standing-orders/run/${run.id}`, 16), parentSpanId: hex(`standing-orders/task/${root}`, 16),
|
|
170
171
|
name: `${run.role} · ${run.provider}`, kind: 1, startTimeUnixNano: nanos(run.startedAt), endTimeUnixNano: nanos(run.finishedAt),
|
package/dist/operate.js
CHANGED
|
@@ -123,7 +123,7 @@ import { homedir, hostname } from "node:os";
|
|
|
123
123
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
124
124
|
import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
|
|
125
125
|
import { dur } from "./design/kit.js";
|
|
126
|
-
import { checkoutIdle, setCheckoutIdle, leadQuickAnswersOn, setLeadQuickAnswers, openStore, openStoreNoMigrate, initLocalMode, enrollOwnerPassword, installationCreatedBy, laptopPortOf, recordLaptopPort, localModeOf, localOwnerBinding, archiveTaskFamily, restoreTaskFamily, shipChecksForTask, elapsedSeconds, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, parseCapabilityKey, verifiedAuthor, isDigestTime, predecessorWatches, RESULT_SCREENSHOTS, lifecycleOf, reportLifecycle, beatLifecycle, } from "./store.js";
|
|
126
|
+
import { requeueEligibility, checkoutIdle, setCheckoutIdle, leadQuickAnswersOn, setLeadQuickAnswers, openStore, openStoreNoMigrate, initLocalMode, enrollOwnerPassword, installationCreatedBy, laptopPortOf, recordLaptopPort, localModeOf, localOwnerBinding, archiveTaskFamily, restoreTaskFamily, shipChecksForTask, elapsedSeconds, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, parseCapabilityKey, verifiedAuthor, isDigestTime, predecessorWatches, RESULT_SCREENSHOTS, lifecycleOf, reportLifecycle, beatLifecycle, } from "./store.js";
|
|
127
127
|
import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
128
128
|
import { claimInstance, clearInstance, openInstanceSocket } from "./console-instance.js";
|
|
129
129
|
import { backgroundOff, backgroundOn, backgroundShow, laptopStatus, laptopStop, restartOwnBuilder } from "./laptop-background.js";
|
|
@@ -220,6 +220,7 @@ import { describePublicationGrant, observeChecks, publishPass, sweepMerges, } fr
|
|
|
220
220
|
import { checkPublishing, publishingOf, savePublishing, followPullRequests, newestPullRequestOf } from "./pull-request-flow.js";
|
|
221
221
|
import { starterOf, startersFor, switchOnStarter } from "./flow-starters.js";
|
|
222
222
|
import { acquire, acquireIfReady, heartbeat, release, reap, currentClaim, DEFAULT_LEASE_MS, SYNC_MAX_AGE_MS, finalize, } from "./claim.js";
|
|
223
|
+
import { moveTask } from "./task-moves.js";
|
|
223
224
|
import { disposeBuildOutcome, holdStaleApproval, regateTask } from "./dispose.js";
|
|
224
225
|
import { proposeGrant, describeGrant, describeWithheld, permits, MUTATION_CLASSES, DEFAULT_MUTATIONS, } from "./grant.js";
|
|
225
226
|
import { builtIn, guarded } from "./backend.js";
|
|
@@ -296,6 +297,7 @@ import { machineActing } from "./scope.js";
|
|
|
296
297
|
import { reevaluateResultRecords } from "./route-results.js";
|
|
297
298
|
import { holdRuleOff, routeRulesPass, taskEvidenceWords } from "./route-rules.js";
|
|
298
299
|
import { ruleHoldOf } from "./route-store.js";
|
|
300
|
+
import { wasInterrupted } from "./run-ending.js";
|
|
299
301
|
import { loginFileOf, migrateLegacyLogin, savedLoginFile, writableLoginFile } from "./login-secrets.js";
|
|
300
302
|
/**
|
|
301
303
|
* 0 done · 1 broke · 2 bad usage · 3 ran fine, the answer is no.
|
|
@@ -1672,12 +1674,9 @@ function claimCommand(positional, flags, context) {
|
|
|
1672
1674
|
return fail(write, json, "claim", verdict.reason, verdict.message, EXIT.refused);
|
|
1673
1675
|
}
|
|
1674
1676
|
const ref = store.refFor(backend, id);
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
ttlMs: ttl,
|
|
1679
|
-
mutation: mutationFrom(flags, now),
|
|
1680
|
-
});
|
|
1677
|
+
// The grant and the `running` it makes are one transaction (closed-task-claims.md §3.5): nobody can close the task
|
|
1678
|
+
// between them, and a refusal writes neither.
|
|
1679
|
+
const result = claimAndMarkRunning(store, backend, id, ref.id, runner, { now, token, ttlMs: ttl, mutation: mutationFrom(flags, now) });
|
|
1681
1680
|
if (!result.ok) {
|
|
1682
1681
|
// Two distinct refusals: somebody HOLDS it right now, or it is
|
|
1683
1682
|
// RESERVED for a specific worker whoever asks (queue columns, v19).
|
|
@@ -1712,24 +1711,12 @@ function claimCommand(positional, flags, context) {
|
|
|
1712
1711
|
return fail(write, json, "claim", "archived", `${id} is put away — restore it with \`toolroll task restore ${id}\` first`, EXIT.refused);
|
|
1713
1712
|
if (result.reason === "draining")
|
|
1714
1713
|
return fail(write, json, "claim", "draining", `Toolroll is restarting (draining): ${runner} takes no new work until it is back (by ${result.until})`, EXIT.refused, { until: result.until });
|
|
1714
|
+
if (result.reason === "closed")
|
|
1715
|
+
return fail(write, json, "claim", "closed", closedClaimWords(store, id, result.state, now), EXIT.refused, { state: result.state });
|
|
1716
|
+
if (result.reason === "no-task")
|
|
1717
|
+
return fail(write, json, "claim", "no-task", `no task \`${id}\``, EXIT.refused);
|
|
1715
1718
|
return fail(write, json, "claim", result.reason, `held by ${result.by} until ${result.until}`, EXIT.refused, { holder: result.by, until: result.until });
|
|
1716
1719
|
}
|
|
1717
|
-
// Taking a task is what makes it running; leaving that to the caller would
|
|
1718
|
-
// let a claimed task keep showing up as queued to anything reading state.
|
|
1719
|
-
// On an idempotent REPLAY the transition runs only as the ONE narrow
|
|
1720
|
-
// repair (external dispatch, finding 36): the stored claim is still the
|
|
1721
|
-
// live lease and the task never left queued — anything else (cancelled,
|
|
1722
|
-
// done, reopened elsewhere) mutates nothing.
|
|
1723
|
-
if (result.replayed === true) {
|
|
1724
|
-
const task = store.getTask(id);
|
|
1725
|
-
const ref = store.lookupRef(id);
|
|
1726
|
-
const stillMine = ref !== null && store.currentLiveLease(ref.id, now) === result.claim.leaseId;
|
|
1727
|
-
if (task !== null && task.state === "queued" && stillMine)
|
|
1728
|
-
store.setTaskState(id, "running", now);
|
|
1729
|
-
}
|
|
1730
|
-
else {
|
|
1731
|
-
store.setTaskState(id, "running", now);
|
|
1732
|
-
}
|
|
1733
1720
|
return succeed(write, json, "claim", { lease: result.claim, reclaimed: result.reclaimed }, () => [
|
|
1734
1721
|
`Claimed ${id} as ${runner}.`,
|
|
1735
1722
|
` lease ${result.claim.leaseId}`,
|
|
@@ -1751,20 +1738,18 @@ function leaseCommand(command, positional, flags, context) {
|
|
|
1751
1738
|
const ttl = readTtl(flags);
|
|
1752
1739
|
if (ttl === null)
|
|
1753
1740
|
return fail(write, json, name, "usage", "--ttl takes whole seconds", EXIT.usage);
|
|
1754
|
-
|
|
1741
|
+
// Releasing says the runner is finished with it, not that it succeeded, so a task left running is put back rather
|
|
1742
|
+
// than marked done, in the release's own transaction (releaseToQueue).
|
|
1743
|
+
const result = command === "heartbeat" ? heartbeat(store, lease, now, ttl) : releaseToQueue(store, lease, now);
|
|
1755
1744
|
if (!result.ok) {
|
|
1745
|
+
if (result.reason === "closed") {
|
|
1746
|
+
return fail(write, json, name, "closed", `this lease ran out and its task is ${result.state} now, so it can't be renewed; stop rather than retry`, EXIT.refused, { state: result.state });
|
|
1747
|
+
}
|
|
1756
1748
|
const message = result.reason === "fenced"
|
|
1757
1749
|
? "superseded — another runner holds this task now; stop rather than retry"
|
|
1758
1750
|
: "no such lease";
|
|
1759
1751
|
return fail(write, json, name, result.reason, message, EXIT.refused);
|
|
1760
1752
|
}
|
|
1761
|
-
if (command === "release") {
|
|
1762
|
-
const task = store.getTask(String(refExternalId(store, result.claim.taskRef)));
|
|
1763
|
-
// Releasing says the runner is finished with it, not that it succeeded, so
|
|
1764
|
-
// a task left running is put back rather than marked done.
|
|
1765
|
-
if (task !== null && task.state === "running")
|
|
1766
|
-
store.setTaskState(task.id, "queued", now);
|
|
1767
|
-
}
|
|
1768
1753
|
return succeed(write, json, name, { lease: result.claim }, () => [
|
|
1769
1754
|
command === "heartbeat"
|
|
1770
1755
|
? `Still yours until ${result.claim.expiresAt}.`
|
|
@@ -3512,7 +3497,7 @@ async function tickCommand(flags, context) {
|
|
|
3512
3497
|
? "auth-expired — requeued, sign-in needed"
|
|
3513
3498
|
: disposition.disposition === "fallback"
|
|
3514
3499
|
? `out of plan — moved to the next approved agent`
|
|
3515
|
-
: `${disposition.failureClass}${disposition.disposition === "backoff" ? ` — retry ${disposition.strikes}/3` : disposition.disposition === "stalled" ? " — stalled" : ""}`
|
|
3500
|
+
: `${disposition.failureClass}${disposition.disposition === "backoff" ? ` — retry ${disposition.strikes}/3` : disposition.disposition === "stalled" ? " — stalled" : disposition.closedState !== null ? ` — the task was already ${disposition.closedState}, so it stays ${disposition.closedState}` : ""}`
|
|
3516
3501
|
: "fenced",
|
|
3517
3502
|
...(result.ok ? {} : { detail: result.message.slice(0, 200) }),
|
|
3518
3503
|
worktree: leased.worktree.path,
|
|
@@ -9543,7 +9528,7 @@ function reviewStatusLines(review, sourceRun) {
|
|
|
9543
9528
|
queued: "legacy request retained; the worker will close it without running a model review",
|
|
9544
9529
|
running: `running — attempt ${review.live?.attempt ?? review.attempts.length} of ${review.cap}`,
|
|
9545
9530
|
succeeded: `succeeded on attempt ${review.succeeded?.attempt ?? 1} of ${review.cap}`,
|
|
9546
|
-
retryable: `${review.latest
|
|
9531
|
+
retryable: `${review.latest !== null && wasInterrupted(review.latest) ? "interrupted" : "failed"} on attempt ${review.latest?.attempt ?? review.attempts.length} of ${review.cap} — inspect the saved result; model reviews no longer retry`,
|
|
9547
9532
|
exhausted: `exhausted — all ${review.cap} attempts ended without a review; nothing retries a fourth time`,
|
|
9548
9533
|
};
|
|
9549
9534
|
const first = review.state === "queued" && review.attempts.length === 0 ? "queued — waiting for a worker" : word[review.state];
|
|
@@ -12066,9 +12051,6 @@ function slug(title, now) {
|
|
|
12066
12051
|
const stamp = now.toISOString().slice(11, 19).replace(/:/g, "");
|
|
12067
12052
|
return words === "" ? `task-${stamp}` : `${words}-${stamp}`;
|
|
12068
12053
|
}
|
|
12069
|
-
function refExternalId(store, taskRef) {
|
|
12070
|
-
return store.externalIdFor(taskRef);
|
|
12071
|
-
}
|
|
12072
12054
|
function describe(error) {
|
|
12073
12055
|
return error instanceof Error ? error.message : String(error);
|
|
12074
12056
|
}
|
|
@@ -14299,3 +14281,60 @@ function startServiceBeats(store) {
|
|
|
14299
14281
|
void delivery.runNow();
|
|
14300
14282
|
return beats;
|
|
14301
14283
|
}
|
|
14284
|
+
/**
|
|
14285
|
+
* The CLI `claim`'s grant and its `running`, one transaction (docs/plans/closed-task-claims.md §3.5). A fresh grant of
|
|
14286
|
+
* a built-in task moves it to running through the narrow `claim` move; a replay of the same key repairs `running` only
|
|
14287
|
+
* when the task is still queued and the replayed lease is still the live one, as before. A task of another backend is
|
|
14288
|
+
* never written in the built-in table.
|
|
14289
|
+
*/
|
|
14290
|
+
function claimAndMarkRunning(store, backend, id, taskRef, runner, options) {
|
|
14291
|
+
const { now } = options;
|
|
14292
|
+
return store.transact(() => {
|
|
14293
|
+
const result = acquire(store, taskRef, runner, options);
|
|
14294
|
+
if (!result.ok || backend !== BUILT_IN)
|
|
14295
|
+
return result;
|
|
14296
|
+
if (result.replayed === true) {
|
|
14297
|
+
const task = store.getTask(id);
|
|
14298
|
+
const stillMine = store.currentLiveLease(taskRef, now) === result.claim.leaseId;
|
|
14299
|
+
if (task !== null && task.state === "queued" && stillMine)
|
|
14300
|
+
store.setTaskState(id, "running", now);
|
|
14301
|
+
return result;
|
|
14302
|
+
}
|
|
14303
|
+
// The grant just read this row open in this transaction, so the move cannot miss; if it ever did, throwing rolls
|
|
14304
|
+
// the grant back with it.
|
|
14305
|
+
if (!moveTask(store.handle, id, "claim", now))
|
|
14306
|
+
throw new Error(`claim: ${id} could not be marked running`);
|
|
14307
|
+
store.bumpWake();
|
|
14308
|
+
return result;
|
|
14309
|
+
});
|
|
14310
|
+
}
|
|
14311
|
+
/**
|
|
14312
|
+
* The refusal words for a claim on a closed task: what state it is in, and only a next step that works for this task
|
|
14313
|
+
* (a requeue the store would accept, a cancelled task reopened through the state verb, otherwise the task itself).
|
|
14314
|
+
*/
|
|
14315
|
+
function closedClaimWords(store, id, state, now) {
|
|
14316
|
+
const head = `${id} is ${state}, so it can't be claimed.`;
|
|
14317
|
+
if (state === "cancelled")
|
|
14318
|
+
return `${head} To run it again, reopen it: \`toolroll task state ${id} queued\``;
|
|
14319
|
+
if (requeueEligibility(store, id, now).ok)
|
|
14320
|
+
return `${head} To run it again: \`toolroll task requeue ${id}\``;
|
|
14321
|
+
return `${head} See its result: \`toolroll task show ${id}\``;
|
|
14322
|
+
}
|
|
14323
|
+
/**
|
|
14324
|
+
* `worker release`: hand the lease back and, when this release is the one that ended the claim, put a task left
|
|
14325
|
+
* running back in the queue, in one transaction, so no one can claim it in between (closed-task-claims.md §5). A
|
|
14326
|
+
* duplicate release changes nothing: by then another runner may hold the task.
|
|
14327
|
+
*/
|
|
14328
|
+
function releaseToQueue(store, lease, now) {
|
|
14329
|
+
return store.transact(() => {
|
|
14330
|
+
const result = release(store, lease, now);
|
|
14331
|
+
if (!result.ok || result.duplicate === true)
|
|
14332
|
+
return result;
|
|
14333
|
+
const ref = store.refForId(result.claim.taskRef);
|
|
14334
|
+
if (ref === null || ref.backend !== BUILT_IN)
|
|
14335
|
+
return result;
|
|
14336
|
+
if (store.getTask(ref.externalId)?.state === "running")
|
|
14337
|
+
store.setTaskState(ref.externalId, "queued", now);
|
|
14338
|
+
return result;
|
|
14339
|
+
});
|
|
14340
|
+
}
|
package/dist/plane-review.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import { scanForSecrets } from "./evidence.js";
|
|
31
31
|
import { scrubIntegrationText } from "./integrations.js";
|
|
32
32
|
import { taskWaitSnapshot } from "./lead-status.js";
|
|
33
|
+
import { wasInterrupted } from "./run-ending.js";
|
|
33
34
|
export const REVIEW_HOURS = 24;
|
|
34
35
|
const EVIDENCE_LINES = 5;
|
|
35
36
|
const WAITING_DAYS = 1;
|
|
@@ -73,7 +74,7 @@ export function runCause(run) {
|
|
|
73
74
|
return "timeout";
|
|
74
75
|
if (reason === "no-handoff")
|
|
75
76
|
return "no-handoff";
|
|
76
|
-
if (
|
|
77
|
+
if (wasInterrupted(run))
|
|
77
78
|
return run.stopped ? null : "orphaned";
|
|
78
79
|
if (PROVIDER_REASONS.has(reason) || run.terminalClass === "transient-throttle")
|
|
79
80
|
return "provider";
|
|
@@ -22,18 +22,17 @@ export type KnowledgeView = ReturnType<typeof knowledgeView>;
|
|
|
22
22
|
export declare function knowledgeVersion(store: Store, repo: string, actor: string, revision: number): Knowledge;
|
|
23
23
|
/** Chat reads only an admitted project. Keep the full source in the project UI. */
|
|
24
24
|
export declare function conversationKnowledge(store: Store, repo: string, actor: string, reference?: string, decision?: number): {
|
|
25
|
-
content?: never;
|
|
26
25
|
history?: never;
|
|
27
26
|
revision: number;
|
|
28
27
|
stale: boolean;
|
|
29
28
|
notice: string;
|
|
30
29
|
title?: never;
|
|
30
|
+
content?: never;
|
|
31
31
|
instructions?: never;
|
|
32
32
|
references?: never;
|
|
33
33
|
decisions?: never;
|
|
34
34
|
waiting?: never;
|
|
35
35
|
} | {
|
|
36
|
-
content?: never;
|
|
37
36
|
revision?: never;
|
|
38
37
|
history?: never;
|
|
39
38
|
stale?: never;
|
|
@@ -54,6 +53,7 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
54
53
|
afterwards?: string;
|
|
55
54
|
notice: string;
|
|
56
55
|
title?: never;
|
|
56
|
+
content?: never;
|
|
57
57
|
instructions?: never;
|
|
58
58
|
references?: never;
|
|
59
59
|
decisions?: never;
|
|
@@ -70,9 +70,9 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
70
70
|
decisions?: never;
|
|
71
71
|
waiting?: never;
|
|
72
72
|
} | {
|
|
73
|
-
content?: never;
|
|
74
73
|
stale?: never;
|
|
75
74
|
title?: never;
|
|
75
|
+
content?: never;
|
|
76
76
|
revision: number;
|
|
77
77
|
history: {
|
|
78
78
|
revision: number;
|
|
@@ -7,6 +7,15 @@ import { type ReleasePolicy, type ReleaseTerms } from "./contracts/release-terms
|
|
|
7
7
|
import type { Store } from "./store.js";
|
|
8
8
|
export type ReleaseState = "open" | "deploying" | "merge-reserved" | "merged" | "revoked" | "expired" | "superseded";
|
|
9
9
|
export declare const LIVE_RELEASE_STATES: readonly ReleaseState[];
|
|
10
|
+
/**
|
|
11
|
+
* Whether an authority's approval has run out at `now`. Nothing writes the `expired` state: expiry is worked out from
|
|
12
|
+
* `expires_at` here, and every reader that asks whether an authority is still good asks through this (or
|
|
13
|
+
* releaseStateAt). The one SQL reader, approval-authority.ts releaseStandsSql, states the same rule in SQL.
|
|
14
|
+
*/
|
|
15
|
+
export declare function releaseExpiredAt(a: Pick<ReleaseAuthority, "state" | "expiresAt">, now: Date): boolean;
|
|
16
|
+
/** An authority's state as read at `now`: a live one (open, deploying, merge-reserved) past its time reads as `expired`;
|
|
17
|
+
* an ended one keeps the state it ended in. */
|
|
18
|
+
export declare function releaseStateAt(a: Pick<ReleaseAuthority, "state" | "expiresAt">, now: Date): ReleaseState;
|
|
10
19
|
/** One generation of a release a person approved: the single source of truth for it. */
|
|
11
20
|
export type ReleaseAuthority = {
|
|
12
21
|
id: number;
|
|
@@ -5,6 +5,19 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { readReleasePolicy } from "./contracts/release-terms.js";
|
|
7
7
|
export const LIVE_RELEASE_STATES = ["open", "deploying", "merge-reserved"];
|
|
8
|
+
/**
|
|
9
|
+
* Whether an authority's approval has run out at `now`. Nothing writes the `expired` state: expiry is worked out from
|
|
10
|
+
* `expires_at` here, and every reader that asks whether an authority is still good asks through this (or
|
|
11
|
+
* releaseStateAt). The one SQL reader, approval-authority.ts releaseStandsSql, states the same rule in SQL.
|
|
12
|
+
*/
|
|
13
|
+
export function releaseExpiredAt(a, now) {
|
|
14
|
+
return a.state === "expired" || a.expiresAt <= now.toISOString();
|
|
15
|
+
}
|
|
16
|
+
/** An authority's state as read at `now`: a live one (open, deploying, merge-reserved) past its time reads as `expired`;
|
|
17
|
+
* an ended one keeps the state it ended in. */
|
|
18
|
+
export function releaseStateAt(a, now) {
|
|
19
|
+
return LIVE_RELEASE_STATES.includes(a.state) && releaseExpiredAt(a, now) ? "expired" : a.state;
|
|
20
|
+
}
|
|
8
21
|
function readReleaseAuthority(row) {
|
|
9
22
|
const text = (key) => row[key] == null ? null : String(row[key]);
|
|
10
23
|
let policy = null;
|
package/dist/release-auto.js
CHANGED
|
@@ -26,7 +26,7 @@ import { releaseCheckStanding } from "./check-failure.js";
|
|
|
26
26
|
import { CARRY_RULES, rulePath } from "./release-carry.js";
|
|
27
27
|
import { AUTO_EVIDENCE_ACTION, AUTO_WITHDRAWN_ACTION, LIVE_RELEASE_STATES, RELEASE_APPROVAL_ACTION, releaseAuthorityOfTask, releaseAutoApproves, releaseApprovalSetting } from "./release-authority.js";
|
|
28
28
|
import { autoReleaseApprovalOf, personApprovalOf } from "./scope.js";
|
|
29
|
-
import { releaseOperations, revokeRelease } from "./store.js";
|
|
29
|
+
import { releaseOperations, revokeRelease, withdrawApproval } from "./store.js";
|
|
30
30
|
import { RELEASE_CI_HELD_ACTION, RELEASE_CI_PASSED_ACTION, RELEASE_CI_RUNNING_WORDS, releaseCiHoldOf, releaseCiRunningOf } from "./release-ci-hold.js";
|
|
31
31
|
import { verificationEvidence } from "./verification-evidence.js";
|
|
32
32
|
import { nightlyDebtHold } from "./nightly-github.js";
|
|
@@ -87,8 +87,7 @@ export function setReleaseApproval(store, repo, auto, by, now) {
|
|
|
87
87
|
* marked withdrawn, so no later reading — the setting turned on again included — lets it stand. */
|
|
88
88
|
function unseal(store, taskId, now) {
|
|
89
89
|
withdraw(store, taskId, now);
|
|
90
|
-
store
|
|
91
|
-
approved_route_json = NULL, approval_basis = NULL, mode_digest = NULL WHERE task_id = ? AND approval_basis IN ('auto-check', 'auto-green')`).run(taskId);
|
|
90
|
+
withdrawApproval(store, { where: "WHERE task_id = ? AND approval_basis IN ('auto-check', 'auto-green')", args: [taskId] }, { basisAfter: null, clearRoute: true, clearModeDigest: true });
|
|
92
91
|
}
|
|
93
92
|
/** Mark a task's automatic seal's evidence withdrawn for good (its seal row may stay while an issued merge reconciles). */
|
|
94
93
|
function withdraw(store, taskId, now) {
|
package/dist/release-gate.js
CHANGED
|
@@ -27,7 +27,7 @@ import { BEFORE_SWAP, RELEASED } from "./deploy-phase.js";
|
|
|
27
27
|
/** The journal phases a deployment passes before it stops the old service (deploy-phase.ts, the one table). */
|
|
28
28
|
export { BEFORE_SWAP };
|
|
29
29
|
/** Journal fields that only the swap and later phases write. */
|
|
30
|
-
const SWAP_FIELDS = ["stoppingService", "codingBackupBeforeSwap", "migration", "newService", "deployedAt"];
|
|
30
|
+
const SWAP_FIELDS = ["stoppingService", "codingBackupBeforeSwap", "migration", "newService", "deployedAt", "launchd", "newServiceStop"];
|
|
31
31
|
const GATE_ID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
32
32
|
const refuse = (reason, message) => ({ ok: false, reason, message: `${message} The update pause was left as it is.` });
|
|
33
33
|
/** The owner id written into the live pause's triggers, when there is one pause and it names one id. */
|
package/dist/release-view.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { liveReleaseOf } from "./scope.js";
|
|
7
7
|
import { nightlyReleaseLines } from "./nightly-github.js";
|
|
8
8
|
import { releaseAuthorityOfTask, releaseCarries } from "./store.js";
|
|
9
|
+
import { releaseStateAt } from "./release-authority.js";
|
|
9
10
|
export function releaseViewOf(store, taskId, now, obligations = () => []) {
|
|
10
11
|
const found = releaseAuthorityOfTask(store, taskId);
|
|
11
12
|
if (found === null)
|
|
@@ -18,7 +19,7 @@ export function releaseViewOf(store, taskId, now, obligations = () => []) {
|
|
|
18
19
|
return {
|
|
19
20
|
kind: found.carry === null ? "anchor" : "carried",
|
|
20
21
|
authority: a.id, version: a.version, pr: a.pr, state: a.state,
|
|
21
|
-
carrying: a
|
|
22
|
+
carrying: releaseStateAt(a, now) === "open" && live.applies && live.ok, why: live.applies && !live.ok ? live.why : null,
|
|
22
23
|
approver: anchor?.approvedBy ?? null, anchorTask: a.anchorTask, tipTask: a.tipTask, expiresAt: a.expiresAt,
|
|
23
24
|
landed: releaseCarries(store, a.id).flatMap(carry => carry.commits.map(commit => ({ sha: commit.sha, task: carry.taskId, files: commit.files }))),
|
|
24
25
|
obligations: obligations(a.id),
|
package/dist/release.js
CHANGED
|
@@ -667,6 +667,17 @@ export async function runRelease(options, adapters) {
|
|
|
667
667
|
return head !== null && head !== j.candidate.sha ? head : null;
|
|
668
668
|
});
|
|
669
669
|
};
|
|
670
|
+
/**
|
|
671
|
+
* Move the release back to an earlier step: write the fields the caller names (and only those), drop the named steps'
|
|
672
|
+
* intents, set the step, and save the journal. Every step-back goes through here.
|
|
673
|
+
*/
|
|
674
|
+
const rewindTo = (step, fields, dropIntents = []) => {
|
|
675
|
+
Object.assign(j, fields);
|
|
676
|
+
for (const one of dropIntents)
|
|
677
|
+
delete j.intents[one];
|
|
678
|
+
j.step = step;
|
|
679
|
+
save();
|
|
680
|
+
};
|
|
670
681
|
const restart = async (head, updated = false) => {
|
|
671
682
|
// The step this commit was at ends here: the release goes on with the new commit (and, filed again, a new task).
|
|
672
683
|
mark(j.step, "failed", `${j.branch} moved to ${head.slice(0, 7)}`);
|
|
@@ -678,16 +689,10 @@ export async function runRelease(options, adapters) {
|
|
|
678
689
|
const carried = await rel.carry(j.authority.id, head, randomUUID(), epoch);
|
|
679
690
|
if (carried.ok) {
|
|
680
691
|
const view = await rel.authority(carried.taskId);
|
|
692
|
+
// The candidate is written before the gate is read, as it always was: a gate that fails leaves it moved.
|
|
681
693
|
j.candidate = { sha: head, tree };
|
|
682
|
-
|
|
683
|
-
j.
|
|
684
|
-
j.completion = null;
|
|
685
|
-
j.landed = view?.landed ?? j.landed ?? [];
|
|
686
|
-
delete j.intents.check;
|
|
687
|
-
delete j.intents.complete;
|
|
688
|
-
j.step = "check";
|
|
689
|
-
j.stopped = null;
|
|
690
|
-
save();
|
|
694
|
+
const task = { id: carried.taskId, scopeDigest: (await adapters.toolroll.gate(carried.taskId)).scopeDigest };
|
|
695
|
+
rewindTo("check", { task, check: null, completion: null, landed: view?.landed ?? j.landed ?? [], stopped: null }, ["check", "complete"]);
|
|
691
696
|
say(` carried the approval to ${head.slice(0, 7)} (task ${carried.taskId})`);
|
|
692
697
|
return;
|
|
693
698
|
}
|
|
@@ -695,30 +700,16 @@ export async function runRelease(options, adapters) {
|
|
|
695
700
|
}
|
|
696
701
|
else
|
|
697
702
|
say(` ${j.branch} moved to ${head.slice(0, 7)}: filing it for approval`);
|
|
698
|
-
j.candidate = { sha: head, tree };
|
|
699
|
-
j.task = { id: gateTaskId(j.packageVersion, head), scopeDigest: null };
|
|
700
|
-
j.approval = null;
|
|
701
|
-
j.check = null;
|
|
702
|
-
j.completion = null;
|
|
703
|
-
j.authority = null;
|
|
704
|
-
j.auto = null;
|
|
705
703
|
epoch = null;
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
delete j.intents.complete;
|
|
709
|
-
j.step = "gate";
|
|
710
|
-
j.stopped = null;
|
|
711
|
-
save();
|
|
704
|
+
rewindTo("gate", { candidate: { sha: head, tree }, task: { id: gateTaskId(j.packageVersion, head), scopeDigest: null },
|
|
705
|
+
approval: null, check: null, completion: null, authority: null, auto: null, stopped: null }, ["gate", "check", "complete"]);
|
|
712
706
|
};
|
|
713
707
|
/**
|
|
714
708
|
* The automatic road ended without an approval: the plane takes its `auto-check` back (the task asks a person, its
|
|
715
709
|
* passing check kept) and tells the owner; the release waits at approval for a person, as without the setting.
|
|
716
710
|
*/
|
|
717
711
|
const autoRefused = async (why) => {
|
|
718
|
-
|
|
719
|
-
j.check = null;
|
|
720
|
-
j.step = "approval";
|
|
721
|
-
save();
|
|
712
|
+
rewindTo("approval", { auto: { state: "refused", why }, check: null });
|
|
722
713
|
await rel?.auto?.giveUp(j.task.id, why);
|
|
723
714
|
throw new Stop("approval", `Not approved automatically: ${why}. A person approves task ${j.task.id} (\`toolroll task approve ${j.task.id} --digest ${j.task.scopeDigest}\`, or the console), then rerun \`${resume}\`.`);
|
|
724
715
|
};
|
|
@@ -1053,10 +1044,7 @@ export async function runRelease(options, adapters) {
|
|
|
1053
1044
|
}
|
|
1054
1045
|
catch (error) {
|
|
1055
1046
|
if (error instanceof Stop && error.reason === "ci-failed") {
|
|
1056
|
-
|
|
1057
|
-
j.check = null;
|
|
1058
|
-
j.step = "approval";
|
|
1059
|
-
save();
|
|
1047
|
+
rewindTo("approval", { auto: { state: "refused", why: error.message }, check: null });
|
|
1060
1048
|
await rel?.auto?.giveUp(j.task.id, error.message.replace(/\.$/, ""));
|
|
1061
1049
|
}
|
|
1062
1050
|
throw error;
|
package/dist/result-shots.js
CHANGED
|
@@ -169,6 +169,6 @@ export function refuseResultShots(state, store, part, content, app, now) {
|
|
|
169
169
|
const title = chatTitle(store, content.image?.taskId ?? content.task ?? "");
|
|
170
170
|
const line = { text: `Screenshots for ${title} weren't sent: the ${app} app isn't allowed to upload files here.`,
|
|
171
171
|
...(content.task === undefined ? {} : { task: content.task }), ...(content.run === undefined ? {} : { run: content.run }) };
|
|
172
|
-
state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL,problem=NULL,file=NULL,uploaded=0,uncertain=0,created=? WHERE provider=:provider AND id=?").run(savedChatPart(line), now.toISOString(), part.id);
|
|
172
|
+
state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL,problem=NULL,file=NULL,uploaded=0,uncertain=0,created=? WHERE provider=:provider AND id=? AND state<>'dropped'").run(savedChatPart(line), now.toISOString(), part.id);
|
|
173
173
|
state.prepare("UPDATE chat_part SET state='dropped',problem='Uploads are not allowed here' WHERE provider=:provider AND event=? AND id<>? AND state='pending'").run(part.event, part.id);
|
|
174
174
|
}
|
package/dist/route-results.js
CHANGED
|
@@ -14,6 +14,7 @@ import { INELIGIBLE_REASONS, REASON_WORDS, selectionSchema, } from "./contracts/
|
|
|
14
14
|
import { changedFilesOf, COMPLETION_ACTION } from "./result-completion.js";
|
|
15
15
|
import { runAccounting } from "./store.js";
|
|
16
16
|
import { INFRASTRUCTURE_TERMINALS, saveResultRecord } from "./route-store.js";
|
|
17
|
+
import { wasInterrupted } from "./run-ending.js";
|
|
17
18
|
// The execution class and the cohort snapshot live with their writer (route-store.ts), which store.ts imports: kept
|
|
18
19
|
// light so the store's module graph doesn't reach the evaluator's readers.
|
|
19
20
|
export { cohortOf, EXEC_CLASS_OF_REASON, execClassOf } from "./route-store.js";
|
|
@@ -49,7 +50,7 @@ function classOfRun(run, kept) {
|
|
|
49
50
|
return kept;
|
|
50
51
|
if (run.terminalClass != null && INFRASTRUCTURE_TERMINALS.has(run.terminalClass))
|
|
51
52
|
return "infrastructure";
|
|
52
|
-
if ((run
|
|
53
|
+
if (wasInterrupted(run) && run.providerStartedAt === null)
|
|
53
54
|
return "infrastructure";
|
|
54
55
|
return "model";
|
|
55
56
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a run ended, asked one way.
|
|
3
|
+
*
|
|
4
|
+
* "Interrupted" is stored in two spellings. Every ending written today is the outcome `failed` with the reason
|
|
5
|
+
* `interrupted`. Rows written between schema 25 and 114 carry the outcome `interrupted` itself (a held session cut
|
|
6
|
+
* down mid-flight) and still read. A check that knows only one spelling treats the same event two ways, so every check
|
|
7
|
+
* asks here.
|
|
8
|
+
*/
|
|
9
|
+
export declare function wasInterrupted(run: {
|
|
10
|
+
outcome: string | null;
|
|
11
|
+
reason?: string | null;
|
|
12
|
+
}): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* The spelling written today, and only it. The roads that hand an interrupted attempt's draft to the next attempt
|
|
15
|
+
* (salvage in the checkout, the recovery grant, the check a run never reached) ask this one: they were built for the
|
|
16
|
+
* runs that write it, and reading the old spelling there would widen what they recover.
|
|
17
|
+
*/
|
|
18
|
+
export declare function endedInterrupted(run: {
|
|
19
|
+
outcome: unknown;
|
|
20
|
+
reason?: unknown;
|
|
21
|
+
}): boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a run ended, asked one way.
|
|
3
|
+
*
|
|
4
|
+
* "Interrupted" is stored in two spellings. Every ending written today is the outcome `failed` with the reason
|
|
5
|
+
* `interrupted`. Rows written between schema 25 and 114 carry the outcome `interrupted` itself (a held session cut
|
|
6
|
+
* down mid-flight) and still read. A check that knows only one spelling treats the same event two ways, so every check
|
|
7
|
+
* asks here.
|
|
8
|
+
*/
|
|
9
|
+
export function wasInterrupted(run) {
|
|
10
|
+
return run.outcome === "interrupted" || (run.outcome === "failed" && run.reason === "interrupted");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The spelling written today, and only it. The roads that hand an interrupted attempt's draft to the next attempt
|
|
14
|
+
* (salvage in the checkout, the recovery grant, the check a run never reached) ask this one: they were built for the
|
|
15
|
+
* runs that write it, and reading the old spelling there would widen what they recover.
|
|
16
|
+
*/
|
|
17
|
+
export function endedInterrupted(run) {
|
|
18
|
+
return run.outcome === "failed" && run.reason === "interrupted";
|
|
19
|
+
}
|