toolroll 0.9.60 → 0.9.61
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 +15 -0
- package/dist/agent-board.d.ts +208 -0
- package/dist/agent-board.js +454 -0
- package/dist/agent-fence.d.ts +19 -0
- package/dist/agent-fence.js +100 -8
- package/dist/agent-pulse.d.ts +43 -0
- package/dist/agent-pulse.js +214 -0
- package/dist/agent-turns.d.ts +52 -0
- package/dist/agent-turns.js +137 -0
- package/dist/agents-board.d.ts +101 -0
- package/dist/agents-board.js +225 -0
- package/dist/assignment-adapters.d.ts +1 -0
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment.d.ts +4 -0
- package/dist/assignment.js +8 -6
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +29 -25
- package/dist/browser-crew.js +2 -0
- package/dist/browser-workspace.d.ts +79 -1
- package/dist/browser-workspace.js +1 -0
- package/dist/builder.d.ts +8 -0
- package/dist/builder.js +113 -13
- package/dist/chat-channel.js +9 -0
- package/dist/chat-core.d.ts +6 -0
- package/dist/chat-core.js +8 -0
- package/dist/chat-delivery.js +22 -0
- package/dist/chat-messages.js +8 -1
- package/dist/chat-quiet.js +1 -0
- package/dist/chat-voice.d.ts +2 -0
- package/dist/chat-voice.js +2 -0
- package/dist/checkout-cleanup.d.ts +3 -0
- package/dist/checkout-cleanup.js +11 -2
- package/dist/contracts/console-api.d.ts +3 -1
- package/dist/contracts/console-api.js +5 -1
- package/dist/contracts/lead-tools.d.ts +16 -2
- package/dist/contracts/lead-tools.js +5 -1
- package/dist/desktop-update.d.ts +1 -1
- package/dist/evidence-pack.js +1 -1
- package/dist/evidence.d.ts +6 -0
- package/dist/evidence.js +14 -3
- package/dist/exec.d.ts +32 -0
- package/dist/exec.js +229 -10
- package/dist/invoke.js +110 -18
- package/dist/landed-cleanup.d.ts +157 -0
- package/dist/landed-cleanup.js +707 -0
- package/dist/lead-cli.js +22 -19
- package/dist/lead-contract.js +1 -1
- package/dist/lead-doors.js +45 -1
- package/dist/lead-tools.js +40 -7
- package/dist/live-input.d.ts +68 -0
- package/dist/live-input.js +254 -0
- package/dist/live.d.ts +1 -1
- package/dist/live.js +4 -2
- package/dist/maintenance.d.ts +2 -0
- package/dist/maintenance.js +5 -2
- package/dist/operate-remote-arguments.js +5 -1
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +188 -16
- package/dist/orphan-sweep.d.ts +2 -0
- package/dist/orphan-sweep.js +5 -3
- package/dist/project-delete-ui.d.ts +11 -0
- package/dist/project-delete-ui.js +29 -2
- package/dist/project-tools.d.ts +7 -11
- package/dist/project-tools.js +13 -37
- package/dist/provider.d.ts +65 -8
- package/dist/provider.js +80 -21
- package/dist/result-completion.d.ts +7 -0
- package/dist/result-completion.js +9 -0
- package/dist/runner.d.ts +4 -2
- package/dist/runner.js +3 -3
- package/dist/serve.js +19 -1
- package/dist/server/live.d.ts +3 -0
- package/dist/server/live.js +35 -2
- package/dist/server/pages.js +59 -35
- package/dist/server/render-chat.js +12 -0
- package/dist/server/render-tasks.d.ts +12 -2
- package/dist/server/render-tasks.js +27 -7
- package/dist/server/route-table.js +4 -1
- package/dist/server/settings.js +2 -1
- package/dist/server/task-views.js +9 -0
- package/dist/server/tasks.js +63 -0
- package/dist/shared-deps.d.ts +9 -4
- package/dist/shared-deps.js +89 -13
- package/dist/staged-runtimes.d.ts +16 -4
- package/dist/staged-runtimes.js +160 -38
- package/dist/steer-live.d.ts +19 -0
- package/dist/steer-live.js +93 -0
- package/dist/steer-status.d.ts +8 -2
- package/dist/steer-status.js +19 -5
- package/dist/storage-sweep-child.d.ts +1 -0
- package/dist/storage-sweep-child.js +76 -0
- package/dist/storage-sweep-process.d.ts +41 -0
- package/dist/storage-sweep-process.js +187 -0
- package/dist/storage-sweep.d.ts +30 -6
- package/dist/storage-sweep.js +214 -28
- package/dist/storage-ui.d.ts +2 -0
- package/dist/storage-ui.js +16 -4
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +35 -0
- package/dist/store.d.ts +163 -2
- package/dist/store.js +350 -15
- package/dist/surface.js +4 -2
- package/dist/sweep-quarantine.d.ts +28 -0
- package/dist/sweep-quarantine.js +153 -0
- package/dist/task-status.d.ts +2 -0
- package/dist/task-status.js +5 -1
- package/dist/test-temp.d.ts +6 -3
- package/dist/test-temp.js +15 -9
- package/dist/toolroll-update.js +3 -2
- package/dist/work-index.d.ts +1 -0
- package/dist/work-index.js +32 -12
- package/dist/work-summary.d.ts +2 -2
- package/dist/workspace-revision.js +3 -1
- package/dist/worktree.d.ts +3 -1
- package/dist/worktree.js +93 -10
- package/package.json +1 -1
- package/scripts/provider-canary.mjs +235 -147
package/dist/browser-crew.js
CHANGED
|
@@ -12,6 +12,8 @@ export function browserWorkActionHref(item) {
|
|
|
12
12
|
return '/';
|
|
13
13
|
if (action.code === 'sign-in')
|
|
14
14
|
return '/settings#providers';
|
|
15
|
+
if (action.code === 'answer-agent' && runId !== null)
|
|
16
|
+
return `/agents?run=${runId}`;
|
|
15
17
|
if (action.code === 'confirm-stopped')
|
|
16
18
|
return `/t/${encodeURIComponent(item.rootId)}#confirm-stopped`;
|
|
17
19
|
if (action.code === 'open-result' && runId !== null)
|
|
@@ -440,6 +440,16 @@ export type BrowserTaskView = {
|
|
|
440
440
|
action: string;
|
|
441
441
|
run: number;
|
|
442
442
|
} | null;
|
|
443
|
+
/** Pause and Pause and steer, beside Stop: the same exact live Claude build, one request id per form (Pause, the note,
|
|
444
|
+
* Resume), where its latest pause stands (null: running), and until when a paused build can wait before it ends. */
|
|
445
|
+
pause?: {
|
|
446
|
+
action: string;
|
|
447
|
+
run: number;
|
|
448
|
+
state: "pausing" | "paused" | null;
|
|
449
|
+
requests: [string, string, string];
|
|
450
|
+
until?: string | null;
|
|
451
|
+
limitMs?: number | null;
|
|
452
|
+
} | null;
|
|
443
453
|
/** The Building card's link to the live build's own record ("Build #N record", /r/<id>). */
|
|
444
454
|
record?: BrowserLink | null;
|
|
445
455
|
earlier?: {
|
|
@@ -1118,7 +1128,75 @@ export type BrowserFlowView = {
|
|
|
1118
1128
|
}[];
|
|
1119
1129
|
colors: string[];
|
|
1120
1130
|
};
|
|
1121
|
-
|
|
1131
|
+
/** The Agents board's groups, in order (agents-board.ts). */
|
|
1132
|
+
export type AgentGroupKey = 'needs-you' | 'working' | 'stuck' | 'done' | 'idle';
|
|
1133
|
+
/** One agent: its task, project, last activity line, elapsed time and diff size. */
|
|
1134
|
+
export type BrowserAgentRow = {
|
|
1135
|
+
runId: number;
|
|
1136
|
+
taskId: string;
|
|
1137
|
+
title: string;
|
|
1138
|
+
project: string | null;
|
|
1139
|
+
href: string;
|
|
1140
|
+
/** Its one line: what it is doing, its question, or why it ended; null when there is nothing the state chip doesn't say. */
|
|
1141
|
+
group: AgentGroupKey;
|
|
1142
|
+
label: string;
|
|
1143
|
+
line: string | null;
|
|
1144
|
+
quiet: boolean;
|
|
1145
|
+
/** When it started, and when it ended (null: still running): the row counts the time between. */
|
|
1146
|
+
startedAt: string;
|
|
1147
|
+
endedAt: string | null;
|
|
1148
|
+
/** "3 files", "+12 −4 · 3 files"; null when nothing could be measured. */
|
|
1149
|
+
diff: string | null;
|
|
1150
|
+
agent: string;
|
|
1151
|
+
};
|
|
1152
|
+
export type BrowserAgentQuestion = {
|
|
1153
|
+
id: number;
|
|
1154
|
+
key: string;
|
|
1155
|
+
text: string;
|
|
1156
|
+
action: string;
|
|
1157
|
+
};
|
|
1158
|
+
export type BrowserAgentDetail = BrowserAgentRow & {
|
|
1159
|
+
live: boolean;
|
|
1160
|
+
question: BrowserAgentQuestion | null;
|
|
1161
|
+
/** The live transcript's JSON window (/r/<run>?fragment=transcript); null when this console can't read it. */
|
|
1162
|
+
transcript: string | null;
|
|
1163
|
+
/** The live changes fragment (/r/<run>?fragment=peek) while it runs; the saved changes once it has finished. */
|
|
1164
|
+
peek: string | null;
|
|
1165
|
+
files: {
|
|
1166
|
+
path: string;
|
|
1167
|
+
added: number | null;
|
|
1168
|
+
removed: number | null;
|
|
1169
|
+
}[];
|
|
1170
|
+
filesMore: number;
|
|
1171
|
+
/** Steer the running agent (POST /t/<task>/steer): null once it has finished. */
|
|
1172
|
+
steer: {
|
|
1173
|
+
action: string;
|
|
1174
|
+
run: number;
|
|
1175
|
+
} | null;
|
|
1176
|
+
/** Whether this person may reply or steer (an approver, outside the demo); a viewer sees the question but no composer. */
|
|
1177
|
+
canAct: boolean;
|
|
1178
|
+
/** The task, and once finished the run's own page (its result page when it built one). */
|
|
1179
|
+
taskHref: string;
|
|
1180
|
+
resultHref: string | null;
|
|
1181
|
+
/** The exact-run live room: updates, and a watch that keeps this person's pings quiet. */
|
|
1182
|
+
room: string;
|
|
1183
|
+
said: {
|
|
1184
|
+
tone: 'info' | 'error';
|
|
1185
|
+
text: string;
|
|
1186
|
+
} | null;
|
|
1187
|
+
};
|
|
1188
|
+
export type BrowserAgentsView = {
|
|
1189
|
+
kind: 'agents';
|
|
1190
|
+
groups: {
|
|
1191
|
+
key: AgentGroupKey;
|
|
1192
|
+
label: string;
|
|
1193
|
+
rows: BrowserAgentRow[];
|
|
1194
|
+
}[];
|
|
1195
|
+
selected: BrowserAgentDetail | null;
|
|
1196
|
+
/** Shown when nothing is on the board. */
|
|
1197
|
+
empty: string | null;
|
|
1198
|
+
};
|
|
1199
|
+
export type BrowserView = BrowserTasksView | BrowserSettingsView | BrowserTaskView | BrowserProjectsView | BrowserResultView | BrowserFlowView | BrowserAgentsView;
|
|
1122
1200
|
export type BrowserWorkspace = {
|
|
1123
1201
|
version: 1;
|
|
1124
1202
|
path: string;
|
|
@@ -42,6 +42,7 @@ export function browserNavigationOf(path, project = null, needsYou = 0, tasksPro
|
|
|
42
42
|
{ label: 'Tasks', href: `/work${tasksProject === null ? '' : `?project=${encodeURIComponent(tasksProject)}`}`,
|
|
43
43
|
active: pathname === '/work' || pathname === '/tasks' || pathname.startsWith('/t/') || pathname.startsWith('/r/'),
|
|
44
44
|
...(needsYou > 0 ? { count: needsYou, ...(needsYouLabel === undefined ? {} : { countLabel: needsYouLabel }) } : {}) },
|
|
45
|
+
{ label: 'Agents', href: '/agents', active: pathname === '/agents' || pathname === '/peek' },
|
|
45
46
|
{ label: 'Flows', href: '/flows', active: pathname === '/flows' || pathname.startsWith('/flows/') },
|
|
46
47
|
{ label: 'Projects', href: '/projects', active: pathname === '/projects' },
|
|
47
48
|
{ label: 'Knowledge', href: `/settings/knowledge${project === null ? '' : `?repo=${encodeURIComponent(project)}`}`, active: knowledge },
|
package/dist/builder.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Decision, Store } from "./store.js";
|
|
|
4
4
|
import { type ExecutionProfile, type Scope } from "./scope.js";
|
|
5
5
|
import { type ParsedDecision, type Problem } from "./decision.js";
|
|
6
6
|
import { type AgentOutcome } from "./invoke.js";
|
|
7
|
+
import { type LiveCloseReason } from "./live-input.js";
|
|
7
8
|
import { type DiffStatFacts } from "./proof.js";
|
|
8
9
|
import { type AuthoritySnapshot, type MilestoneState } from "./plan.js";
|
|
9
10
|
export type Runner = (file: string, args: readonly string[], options?: RunOptions) => Promise<ExecResult>;
|
|
@@ -55,6 +56,8 @@ export type BuildRequest = {
|
|
|
55
56
|
clock?: () => Date;
|
|
56
57
|
/** How often the pulse beats while the agent runs. 0 disables it. */
|
|
57
58
|
pulseMs?: number;
|
|
59
|
+
/** Live steering's dispatcher interval (steer-live.ts); tests shorten it. */
|
|
60
|
+
steerTickMs?: number;
|
|
58
61
|
worktree: string;
|
|
59
62
|
branch: string;
|
|
60
63
|
now: Date;
|
|
@@ -268,6 +271,11 @@ export type CapturedBuild = {
|
|
|
268
271
|
* one) settles exactly as it always has, and a caller assembling a
|
|
269
272
|
* capture by hand need not know this protocol exists. */
|
|
270
273
|
plan?: PlanBinding;
|
|
274
|
+
/** A Claude build's live input (live-input.ts): why Toolroll closed its stdin, and its total turn budget. */
|
|
275
|
+
live?: {
|
|
276
|
+
close: LiveCloseReason | null;
|
|
277
|
+
maxTurns: number;
|
|
278
|
+
};
|
|
271
279
|
};
|
|
272
280
|
/** What settlement needs to know about the plan this attempt built against. */
|
|
273
281
|
export type PlanBinding = {
|
package/dist/builder.js
CHANGED
|
@@ -42,7 +42,7 @@ import { PREPARED_EVIDENCE_FILE, PREPARED_EVIDENCE_GIT, preparedScreenshotMatche
|
|
|
42
42
|
import { existsSync, unlinkSync, writeFileSync } from "node:fs";
|
|
43
43
|
import { homedir } from "node:os";
|
|
44
44
|
import { join } from "node:path";
|
|
45
|
-
import { run, runOwnerTag } from "./exec.js";
|
|
45
|
+
import { run, runClaudeStreamJsonl, runOwnerTag, streamsInput } from "./exec.js";
|
|
46
46
|
import { stopRequestedFor, stopWords, underStopWatch } from "./task-control.js";
|
|
47
47
|
import { witnessedRunner } from "./process-custody.js";
|
|
48
48
|
import { runWithIsolatedDatabase } from "./child-database.js";
|
|
@@ -61,10 +61,15 @@ import { TELEGRAM_TOKEN_ENVS } from "./names.js";
|
|
|
61
61
|
import { OPENROUTER_ENV_KEY, auditOf, ALL_CREDENTIAL_ENV } from "./provider.js";
|
|
62
62
|
import { openLiveLog } from "./live.js";
|
|
63
63
|
import { fence } from "./brief-fence.js";
|
|
64
|
-
import { closeSteerMailbox, openSteerMailbox,
|
|
64
|
+
import { closeSteerMailbox, openSteerMailbox, STEER_HANDLED } from "./steer-mailbox.js";
|
|
65
|
+
import { LiveInput } from "./live-input.js";
|
|
66
|
+
import { LIVE_TICK_MS, tickLiveSteer } from "./steer-live.js";
|
|
65
67
|
import { withRunActivity } from "./task-activity.js";
|
|
66
68
|
import { CheckProgressTracker } from "./check-progress.js";
|
|
67
|
-
import { captureParkEvidence, captureTerminalDiff, captureBaseTree, evidenceRoot, handoffName, isImagePath, WORKTREE_EVIDENCE_DIR, storeHandoffArtifact, looksLikeProtocolFile, mailboxName, progressFileName, steerFolderName, proofFileName, rubricFileName, proposalFileName, quarantineMailboxes, readMailbox, readVerifiedArtifact, scanForSecrets, redactSecretLines, storeEvidence, validateScreenshotBytes, imageDimensions, SCREENSHOT_BYTE_CAP, boundStreamHeadTail } from "./evidence.js";
|
|
69
|
+
import { captureParkEvidence, captureTerminalDiff, captureBaseTree, evidenceRoot, handoffName, isImagePath, WORKTREE_EVIDENCE_DIR, storeHandoffArtifact, looksLikeProtocolFile, mailboxName, progressFileName, steerFolderName, statusFileName, proofFileName, rubricFileName, proposalFileName, quarantineMailboxes, readMailbox, readVerifiedArtifact, scanForSecrets, redactSecretLines, storeEvidence, validateScreenshotBytes, imageDimensions, SCREENSHOT_BYTE_CAP, boundStreamHeadTail } from "./evidence.js";
|
|
70
|
+
import { attemptKeyOf, closeRunQuestions, noteOpenTool, recordAgentTurn } from "./agent-board.js";
|
|
71
|
+
import { openStatusFile, pulseAgentBoard, removeStatusFile, statusBrief } from "./agent-pulse.js";
|
|
72
|
+
import { withAgentTurns } from "./agent-turns.js";
|
|
68
73
|
import { PROOF_LIMITS, parseProof, serializeProof, adjudicate, artifactManifestOnly, sameDiffStatFacts } from "./proof.js";
|
|
69
74
|
import { parseHandoffArtifact } from "./contracts/handoff.js";
|
|
70
75
|
import { captureReviewContext } from "./review-context.js";
|
|
@@ -931,7 +936,36 @@ export async function build(store, request) {
|
|
|
931
936
|
// Notes are mailed only while the agent process runs: one written after
|
|
932
937
|
// it exits could never be read, and would only wait for the next attempt.
|
|
933
938
|
let agentLive = false;
|
|
939
|
+
// A Claude build's live input (live-input.ts): the brief goes in as the
|
|
940
|
+
// first stdin message, and notes and pauses follow it in seconds. The
|
|
941
|
+
// gateway attaches it only through a transport that keeps stdin open.
|
|
942
|
+
let liveSession = null;
|
|
943
|
+
let liveTimer;
|
|
944
|
+
// The Agents board rides the same pulse (agent-pulse.ts): the agent's status file, its transcript's size, the
|
|
945
|
+
// changed-file count and the Stuck mark. Null until the agent is about to start, and after it has exited.
|
|
946
|
+
let agentPulse = null;
|
|
947
|
+
const closeAgentPulse = () => {
|
|
948
|
+
if (agentPulse === null)
|
|
949
|
+
return;
|
|
950
|
+
const last = agentPulse;
|
|
951
|
+
agentPulse = null;
|
|
952
|
+
// One last read of what the agent wrote, then its status file goes, like the steering folder.
|
|
953
|
+
try {
|
|
954
|
+
pulseAgentBoard(store, { ...last, looking: true }, clock());
|
|
955
|
+
}
|
|
956
|
+
catch { /* display state */ }
|
|
957
|
+
removeStatusFile(last.status);
|
|
958
|
+
// Its questions close unanswered: a reply now could only reach a later attempt, as ordinary steering does.
|
|
959
|
+
try {
|
|
960
|
+
closeRunQuestions(store, last.run, clock());
|
|
961
|
+
}
|
|
962
|
+
catch { /* the run's ending closes them too */ }
|
|
963
|
+
};
|
|
934
964
|
const closeSteering = () => {
|
|
965
|
+
if (liveTimer !== undefined)
|
|
966
|
+
clearInterval(liveTimer);
|
|
967
|
+
liveTimer = undefined;
|
|
968
|
+
liveSession?.detach();
|
|
935
969
|
if (steerBox === null)
|
|
936
970
|
return;
|
|
937
971
|
const box = steerBox;
|
|
@@ -940,6 +974,10 @@ export async function build(store, request) {
|
|
|
940
974
|
store.markSteerFolderClosed(request.runId, clock());
|
|
941
975
|
}
|
|
942
976
|
catch { /* display state; the run's outcome ends it */ }
|
|
977
|
+
try {
|
|
978
|
+
store.markLiveInputClosed(request.runId, clock());
|
|
979
|
+
}
|
|
980
|
+
catch { /* display state; the run's outcome ends it */ }
|
|
943
981
|
closeSteerMailbox(store, box, clock());
|
|
944
982
|
};
|
|
945
983
|
if (request.leaseId !== undefined && pulseMs > 0) {
|
|
@@ -978,14 +1016,14 @@ export async function build(store, request) {
|
|
|
978
1016
|
catch {
|
|
979
1017
|
// A checkpoint is never worth an attempt. The next beat retries.
|
|
980
1018
|
}
|
|
981
|
-
// Steering
|
|
982
|
-
//
|
|
983
|
-
if (
|
|
1019
|
+
// Steering has its own faster beat (steer-live.ts tickLiveSteer, below), which mails and delivers notes —
|
|
1020
|
+
// an Agents-board Reply among them, as an ordinary steering note. The board's display state rides this one.
|
|
1021
|
+
if (agentPulse !== null) {
|
|
984
1022
|
try {
|
|
985
|
-
|
|
1023
|
+
pulseAgentBoard(store, agentPulse, clock());
|
|
986
1024
|
}
|
|
987
1025
|
catch {
|
|
988
|
-
//
|
|
1026
|
+
// The board is display state: the next beat reads again.
|
|
989
1027
|
}
|
|
990
1028
|
}
|
|
991
1029
|
}
|
|
@@ -1076,13 +1114,48 @@ export async function build(store, request) {
|
|
|
1076
1114
|
// The steering folder exists before the brief names it, and only on the
|
|
1077
1115
|
// agent road: a prepared candidate spawns no agent to read it.
|
|
1078
1116
|
steerBox = steerable && (scope?.candidate ?? null) === null ? openSteerMailbox(worktree, steerFolder, request.runId, taskRef, randomBytes(8).toString("hex")) : null;
|
|
1079
|
-
if (steerBox !== null)
|
|
1117
|
+
if (steerBox !== null) {
|
|
1080
1118
|
store.markSteerFolderOpened(request.runId, clock());
|
|
1119
|
+
// Only through a transport that keeps stdin open: an injected runner opts in (exec.ts STREAM_INPUT).
|
|
1120
|
+
if (provider === "claude" && streamsInput(agent ?? runClaudeStreamJsonl)) {
|
|
1121
|
+
const handoffFiles = [done, mailbox, proposal];
|
|
1122
|
+
liveSession = new LiveInput({
|
|
1123
|
+
maxTurns: effective.maxTurns ?? maxTurns,
|
|
1124
|
+
finished: () => handoffFiles.some(name => existsSync(join(worktree, name))),
|
|
1125
|
+
// A paused build can wait as long as the idle limit allows (live-input.ts idleLimit); a wall-clock profile has none.
|
|
1126
|
+
onOpen: () => store.markLiveInputOpened(request.runId, clock(), effective.profile.timeoutKind === "idle" ? effective.timeoutMs : null),
|
|
1127
|
+
onClose: () => store.markLiveInputClosed(request.runId, clock()),
|
|
1128
|
+
onPaused: () => store.markRunPaused(request.runId, clock()),
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
// Steering's own beat, independent of the lease pulse: a note is in the
|
|
1132
|
+
// folder (and on a Claude build's stdin) within a tick of being filed.
|
|
1133
|
+
// It rides the pulse's rule: a fenced build writes nothing more.
|
|
1134
|
+
const steer = { box: steerBox, session: liveSession };
|
|
1135
|
+
liveTimer = setInterval(() => {
|
|
1136
|
+
if (fencedMidBuild || steerBox === null)
|
|
1137
|
+
return;
|
|
1138
|
+
try {
|
|
1139
|
+
tickLiveSteer(store, steer, clock(), agentLive);
|
|
1140
|
+
}
|
|
1141
|
+
catch {
|
|
1142
|
+
// Unread notes stay pending; the next tick, or the next attempt, carries them.
|
|
1143
|
+
}
|
|
1144
|
+
}, request.steerTickMs ?? LIVE_TICK_MS);
|
|
1145
|
+
liveTimer.unref?.();
|
|
1146
|
+
}
|
|
1147
|
+
// The Agents board (agent-board.ts): turn states recorded under this run's attempt key, and a status file the
|
|
1148
|
+
// agent appends to, read on the pulse. A legacy run with no key records nothing; a prepared candidate runs no agent.
|
|
1149
|
+
const attemptKey = attemptKeyOf(store, request.runId);
|
|
1150
|
+
const statusName = attemptKey !== null && steerable && (scope?.candidate ?? null) === null ? statusFileName() : null;
|
|
1151
|
+
const statusPath = statusName === null ? null : openStatusFile(worktree, statusName);
|
|
1081
1152
|
// The live window (arc 1): display state beside the run, never evidence.
|
|
1082
1153
|
// Every streaming transport emits events now (peek); a file that cannot
|
|
1083
1154
|
// open is a null, and a null never costs a build.
|
|
1084
1155
|
// What the agent did last rides the same stream into one row (live tasks).
|
|
1085
|
-
const liveLog = withRunActivity(openLiveLog(root, request.runId), store, request.runId, clock)
|
|
1156
|
+
const liveLog = withAgentTurns(withRunActivity(openLiveLog(root, request.runId), store, request.runId, clock), (state, at) => { if (attemptKey !== null)
|
|
1157
|
+
recordAgentTurn(store, { run: request.runId, attemptKey, state, at }); }, clock, (open, at) => { if (attemptKey !== null)
|
|
1158
|
+
noteOpenTool(store, request.runId, attemptKey, open, at); });
|
|
1086
1159
|
// The retry base (steering fix for run 1465's proof): the SAME pinned
|
|
1087
1160
|
// base the machine will use to capture the sealed diff (settleProof
|
|
1088
1161
|
// below, mirroring store.firstBuilderBase's own doc). Null on a first
|
|
@@ -1101,7 +1174,8 @@ export async function build(store, request) {
|
|
|
1101
1174
|
// replacement for.
|
|
1102
1175
|
milestones.length === 0 || planRevisionHash === null
|
|
1103
1176
|
? null
|
|
1104
|
-
: { revision: planRevisionNumber, hash: planRevisionHash, milestones, progress, proposal }, scope === null ? [] : flowGoalCuts(store, scope.taskId, scope.goal), steerBox === null ? null : { folder: steerFolder, key: steerBox.key }) + `\nCanonical signed rubric: ${rubric}. Its statement fields are exact; evidence requirements are separate fields. Do not edit this input. The lead or user reads these criteria directly; no restatement is needed.\n` + sharedDepsBrief(store, worktree)
|
|
1177
|
+
: { revision: planRevisionNumber, hash: planRevisionHash, milestones, progress, proposal }, scope === null ? [] : flowGoalCuts(store, scope.taskId, scope.goal), steerBox === null ? null : { folder: steerFolder, key: steerBox.key, live: liveSession !== null }) + `\nCanonical signed rubric: ${rubric}. Its statement fields are exact; evidence requirements are separate fields. Do not edit this input. The lead or user reads these criteria directly; no restatement is needed.\n` + sharedDepsBrief(store, worktree)
|
|
1178
|
+
+ (statusPath === null || statusName === null ? "" : statusBrief(statusName, steerBox === null ? null : steerFolder));
|
|
1105
1179
|
// THE PREPARED-CANDIDATE ROAD (v69): the scope names a commit, proved
|
|
1106
1180
|
// above before anything moved. The machine brings the worktree to its exact
|
|
1107
1181
|
// tree — uncommitted, as an agent would leave it — writes the handoff
|
|
@@ -1187,6 +1261,9 @@ export async function build(store, request) {
|
|
|
1187
1261
|
}
|
|
1188
1262
|
}
|
|
1189
1263
|
let invoked;
|
|
1264
|
+
let spawned = false;
|
|
1265
|
+
if (attemptKey !== null && statusPath !== null)
|
|
1266
|
+
agentPulse = { run: request.runId, attemptKey, root, worktree, status: statusPath, base: undefined, looking: false };
|
|
1190
1267
|
try {
|
|
1191
1268
|
agentLive = true;
|
|
1192
1269
|
invoked = await invokeAgent(store, request.runId,
|
|
@@ -1211,6 +1288,7 @@ export async function build(store, request) {
|
|
|
1211
1288
|
...(steerBox === null ? {} : { steerFolder: { dir: steerBox.dir, name: steerFolder } }),
|
|
1212
1289
|
}, {
|
|
1213
1290
|
cwd: worktree,
|
|
1291
|
+
...(liveSession === null ? {} : { liveInput: liveSession }),
|
|
1214
1292
|
// New approvals bind a no-progress watchdog, not a deadline. Legacy
|
|
1215
1293
|
// snapshots carry no timeoutKind and retain their wall-clock terms.
|
|
1216
1294
|
...(effective.profile.timeoutKind === "idle"
|
|
@@ -1218,7 +1296,7 @@ export async function build(store, request) {
|
|
|
1218
1296
|
: { timeoutMs: effective.timeoutMs }),
|
|
1219
1297
|
omitEnv: AGENT_ENV_DENYLIST,
|
|
1220
1298
|
...(agent === undefined ? {} : { runner: agent }),
|
|
1221
|
-
|
|
1299
|
+
onSpawn: (pid) => { spawned = true; request.onProviderSpawn?.(pid); },
|
|
1222
1300
|
onStreamEvent: (event) => liveLog.observe(event),
|
|
1223
1301
|
// The receipt (finding 8): the stream proved the prompt reached the
|
|
1224
1302
|
// agent — settle delivery NOW, durably, whatever happens to the run
|
|
@@ -1230,9 +1308,12 @@ export async function build(store, request) {
|
|
|
1230
1308
|
}
|
|
1231
1309
|
catch (error) {
|
|
1232
1310
|
agentLive = false;
|
|
1311
|
+
if (spawned)
|
|
1312
|
+
liveLog.ended();
|
|
1233
1313
|
if (pulseTimer !== undefined)
|
|
1234
1314
|
clearInterval(pulseTimer);
|
|
1235
1315
|
closeSteering();
|
|
1316
|
+
closeAgentPulse();
|
|
1236
1317
|
liveLog?.close();
|
|
1237
1318
|
try {
|
|
1238
1319
|
unlinkSync(join(worktree, rubric));
|
|
@@ -1244,7 +1325,11 @@ export async function build(store, request) {
|
|
|
1244
1325
|
// the folder goes — before settlement, whose stop and timeout roads sweep
|
|
1245
1326
|
// protocol files. Anything unread stays pending for the next attempt.
|
|
1246
1327
|
agentLive = false;
|
|
1328
|
+
// A process that ended mid-turn, without its result, crashed (the board says so; the outcome is settlement's).
|
|
1329
|
+
if (spawned)
|
|
1330
|
+
liveLog.ended();
|
|
1247
1331
|
closeSteering();
|
|
1332
|
+
closeAgentPulse();
|
|
1248
1333
|
try {
|
|
1249
1334
|
// The gateway's value-shaped refusals (Phase 3 B5): a race past the
|
|
1250
1335
|
// pre-claim skip, or the harness breaking its own protocol. Both dispose
|
|
@@ -1266,6 +1351,7 @@ export async function build(store, request) {
|
|
|
1266
1351
|
runner, provider, scope, effective, answers, timeoutMs, root, mailbox, done, proof, rubric,
|
|
1267
1352
|
clock, fenced: () => fencedMidBuild,
|
|
1268
1353
|
plan: { proposal, revision: planRevisionNumber, authority, progress: progressState },
|
|
1354
|
+
...(liveSession === null ? {} : { live: { close: liveSession.closeReason, maxTurns: liveSession.maxTurns } }),
|
|
1269
1355
|
};
|
|
1270
1356
|
return await settleProviderOutcome(captured, result);
|
|
1271
1357
|
}
|
|
@@ -1420,9 +1506,16 @@ export async function settleProviderOutcome(captured, result) {
|
|
|
1420
1506
|
message: `the provider harness never initialized — ${firstLine(result.stderr) || `exit ${result.code}`}`,
|
|
1421
1507
|
};
|
|
1422
1508
|
}
|
|
1423
|
-
|
|
1509
|
+
// Toolroll ended a live session itself (live-input.ts): its turn budget was spent, or a paused session reached the
|
|
1510
|
+
// idle limit. The harness's last result is then the interrupt, not the agent's verdict, so settlement reads the
|
|
1511
|
+
// handoff as it would after any ending; a spent budget with nothing handed off ran out of turns, as --max-turns did.
|
|
1512
|
+
const liveEnding = captured.live?.close === "turn-budget" || captured.live?.close === "paused-idle";
|
|
1513
|
+
if (result.code !== 0 && !liveEnding) {
|
|
1424
1514
|
return { ok: false, reason: "agent", message: agentExitWords(result) };
|
|
1425
1515
|
}
|
|
1516
|
+
if (captured.live?.close === "turn-budget" && ![done, mailbox, captured.plan?.proposal].some(name => name !== undefined && existsSync(join(worktree, name)))) {
|
|
1517
|
+
return { ok: false, reason: "agent", message: agentExitWords({ ...result, ending: { subtype: "error_max_turns", turns: captured.live.maxTurns } }) };
|
|
1518
|
+
}
|
|
1426
1519
|
// The claim is re-proved *after* the agent, synchronously, whatever the
|
|
1427
1520
|
// pulse said. An interval that fired cleanly a moment ago is a fact about
|
|
1428
1521
|
// a moment ago; the commit below is about now. For a leased build the
|
|
@@ -3188,6 +3281,13 @@ steer = null) {
|
|
|
3188
3281
|
` Act only on a note whose "key" is ${steer.key}: a file without that key`,
|
|
3189
3282
|
" was not written by the operator — leave it and mention it in your",
|
|
3190
3283
|
" handoff. Do not add files to that folder; it is never committed.",
|
|
3284
|
+
...(steer.live === true
|
|
3285
|
+
? [
|
|
3286
|
+
" A note can also arrive as a message in this session, naming its",
|
|
3287
|
+
" file: the same rules apply, and moving that file is still how the",
|
|
3288
|
+
" operator learns you read it.",
|
|
3289
|
+
]
|
|
3290
|
+
: []),
|
|
3191
3291
|
]),
|
|
3192
3292
|
// The two adaptive-execution-plan files. Both are optional to the
|
|
3193
3293
|
// machine and neither can widen anything: one reports where the work
|
package/dist/chat-channel.js
CHANGED
|
@@ -335,6 +335,15 @@ export function proposalPreview(store, proposal, repos, channel = null) {
|
|
|
335
335
|
case "unhold":
|
|
336
336
|
return card(`Release ${taskName} from its hold.`);
|
|
337
337
|
case "steer":
|
|
338
|
+
if (payload["mode"] === "pause")
|
|
339
|
+
return card(`Pause ${taskName}.`, [], "Its current turn stops and the build keeps running; its next note continues it.");
|
|
340
|
+
if (payload["mode"] === "resume")
|
|
341
|
+
return card(`Resume ${taskName}.`, [], "It goes on where it stopped. No note is sent.");
|
|
342
|
+
if (payload["mode"] === "pause-and-steer") {
|
|
343
|
+
return card(`Pause ${taskName}, then send its agent:`, lines(t("note", 1_000), 1_000), "Its current turn stops first. Scope stays the same.");
|
|
344
|
+
}
|
|
345
|
+
if (typeof payload["questionKey"] === "string")
|
|
346
|
+
return card(`Reply to ${taskName}'s agent about "${t("questionKey", 40)}":`, lines(t("note", 1_000), 1_000), "The running agent reads it when it next checks for notes, and the question closes. Scope stays the same.");
|
|
338
347
|
return card(`Guidance for ${taskName}'s agent:`, lines(t("note", 1_000), 1_000), "A running agent reads it when it next checks for notes; otherwise the next attempt does. Scope stays the same.");
|
|
339
348
|
case "repair": {
|
|
340
349
|
const blocker = t("blockerTitle", 120) || t("blocker", 64);
|
package/dist/chat-core.d.ts
CHANGED
|
@@ -167,5 +167,11 @@ export declare class ChatCore {
|
|
|
167
167
|
} | null;
|
|
168
168
|
/** Small transport facts an app must remember per chat (a Teams service URL). Never credentials. */
|
|
169
169
|
meta(installation: string, key: string): string | null;
|
|
170
|
+
/** Every small fact whose key starts with this prefix (a fixed, LIKE-safe word). */
|
|
171
|
+
metaStartingWith(installation: string, prefix: string): {
|
|
172
|
+
key: string;
|
|
173
|
+
value: string;
|
|
174
|
+
}[];
|
|
175
|
+
deleteMeta(installation: string, key: string): void;
|
|
170
176
|
setMeta(installation: string, key: string, value: string, now: Date): void;
|
|
171
177
|
}
|
package/dist/chat-core.js
CHANGED
|
@@ -281,6 +281,14 @@ export class ChatCore {
|
|
|
281
281
|
meta(installation, key) {
|
|
282
282
|
return str(this.prepare("SELECT value FROM chat_meta WHERE provider = :provider AND installation = ? AND key = ?").get(installation, key)?.["value"]);
|
|
283
283
|
}
|
|
284
|
+
/** Every small fact whose key starts with this prefix (a fixed, LIKE-safe word). */
|
|
285
|
+
metaStartingWith(installation, prefix) {
|
|
286
|
+
return this.prepare("SELECT key, value FROM chat_meta WHERE provider = :provider AND installation = ? AND substr(key, 1, ?) = ? ORDER BY key")
|
|
287
|
+
.all(installation, prefix.length, prefix).map(row => ({ key: String(row["key"]), value: String(row["value"]) }));
|
|
288
|
+
}
|
|
289
|
+
deleteMeta(installation, key) {
|
|
290
|
+
this.prepare("DELETE FROM chat_meta WHERE provider = :provider AND installation = ? AND key = ?").run(installation, key);
|
|
291
|
+
}
|
|
284
292
|
setMeta(installation, key, value, now) {
|
|
285
293
|
this.prepare(`INSERT INTO chat_meta (provider, installation, key, value, updated) VALUES (:provider, ?, ?, ?, ?)
|
|
286
294
|
ON CONFLICT (provider, installation, key) DO UPDATE SET value = excluded.value, updated = excluded.updated`).run(installation, key, value, now.toISOString());
|
package/dist/chat-delivery.js
CHANGED
|
@@ -26,6 +26,7 @@ import { isShotsKind, resultShotsFor } from "./result-shots.js";
|
|
|
26
26
|
import { CHAT_APP_NAMES } from "./flow-triggers.js";
|
|
27
27
|
import { answersPrompt, applyDecideFeedback, applyDecideTap, decideFallbackLink, decideOffer, decideTargetOf, hasLiveDecideTokens, linksFor, mergedText, mintDecideButtons, offerFingerprint, openPromptFor, recordChatMerge } from "./chat-decide.js";
|
|
28
28
|
import { mergePullRequest } from "./pull-request-flow.js";
|
|
29
|
+
import { watchedPing } from "./agent-board.js";
|
|
29
30
|
export const chatObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v)
|
|
30
31
|
? v
|
|
31
32
|
: {};
|
|
@@ -861,6 +862,16 @@ export async function planChatNotifications(options) {
|
|
|
861
862
|
offerNotice(options, notification, binding, registry);
|
|
862
863
|
state.setMeta(identity.installation, owed, "offered", nowOf(options));
|
|
863
864
|
}
|
|
865
|
+
// A ping held while its person watched the run (agent-board.ts watchedPing) is offered again once the watch lapses.
|
|
866
|
+
for (const held of state.metaStartingWith(identity.installation, WATCH_HELD)) {
|
|
867
|
+
if (held.value > nowOf(options).toISOString())
|
|
868
|
+
continue;
|
|
869
|
+
state.deleteMeta(identity.installation, held.key);
|
|
870
|
+
const [bindingId, notificationId] = held.key.slice(WATCH_HELD.length).split(":").map(Number);
|
|
871
|
+
const binding = bindings.find(one => one.id === bindingId), notification = store.notificationById(notificationId ?? 0);
|
|
872
|
+
if (binding !== undefined && notification !== null)
|
|
873
|
+
offerNotice(options, notification, binding, registry);
|
|
874
|
+
}
|
|
864
875
|
// Every paired person is a destination of their own, under their own ceiling.
|
|
865
876
|
for (const notification of store.notificationsAfter(cursor, 100)) {
|
|
866
877
|
for (const binding of bindings)
|
|
@@ -870,6 +881,8 @@ export async function planChatNotifications(options) {
|
|
|
870
881
|
.run(notification.id, identity.installation);
|
|
871
882
|
}
|
|
872
883
|
}
|
|
884
|
+
/** A held ping's small fact: `watch-held:<binding>:<notification>` → when the watch lapses. */
|
|
885
|
+
const WATCH_HELD = "watch-held:";
|
|
873
886
|
/** One notification for one paired person: planned as their parts, or nothing. Never twice: each plan's event id is
|
|
874
887
|
* the notification's for this binding. */
|
|
875
888
|
function offerNotice(options, notification, binding, registry) {
|
|
@@ -893,6 +906,15 @@ function offerNotice(options, notification, binding, registry) {
|
|
|
893
906
|
(personal
|
|
894
907
|
? notification.recipient === binding.approver && (notification.project === null || repos.includes(notification.project))
|
|
895
908
|
: (notification.taskId || notification.kind === "flow-decision") && notification.project !== null && repos.includes(notification.project))) {
|
|
909
|
+
// Watching the run on the Agents board holds this ping (re-offered above once the watch lapses); a finished
|
|
910
|
+
// run they have since opened needs no ping at all.
|
|
911
|
+
const watched = watchedPing(store, notification, binding.approver, nowOf(options));
|
|
912
|
+
if (watched === "skip")
|
|
913
|
+
return;
|
|
914
|
+
if (watched !== "send") {
|
|
915
|
+
state.setMeta(identity.installation, `${WATCH_HELD}${binding.id}:${notification.id}`, watched.hold, nowOf(options));
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
896
918
|
const run = store.telegramProgressRun(notification);
|
|
897
919
|
const id = chatHash(`${options.state.provider}:notice:${binding.id}:${notification.id}`), now = nowOf(options);
|
|
898
920
|
// Only when I'm needed (the default): the task's one card is edited in place, and a new message
|
package/dist/chat-messages.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Imports the store's types only (no runtime cycle): the store builds its chats on it.
|
|
10
10
|
*/
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
+
import { watchedPing } from "./agent-board.js";
|
|
12
13
|
import { ChatCore } from "./chat-core.js";
|
|
13
14
|
import { readChatMessageBody, readChatPart, savedChatEventBody, savedChatPart } from "./contracts/chat-content.js";
|
|
14
15
|
import { isTelegramProgressNotification, proposalTaskOf, readNotification, readNotificationReceipt } from "./notification-rows.js";
|
|
@@ -704,9 +705,15 @@ export class ChatMessages extends ChatCore {
|
|
|
704
705
|
for (const raw of this.db.prepare(`SELECT n.* FROM notification n JOIN notification_delivery d ON d.notification = n.id AND d.destination = ?
|
|
705
706
|
WHERE n.resolved_at IS NULL AND d.delivered_at IS NULL AND d.receipt IS NULL AND (d.claim_owner IS NULL OR d.claim_expires_at <= ?)`).all(destination, now.toISOString())) {
|
|
706
707
|
const row = readNotification(raw);
|
|
707
|
-
|
|
708
|
+
// A ping about a run this person is watching stays unsettled until the watch lapses, then is decided again.
|
|
709
|
+
const watched = this.store.pingAllowed(row, binding.approver) ? watchedPing(this.store, row, binding.approver, now) : "skip";
|
|
710
|
+
if (watched === "skip") {
|
|
708
711
|
this.db.prepare(`UPDATE notification_delivery SET receipt = '${TELEGRAM_SKIPPED_QUIET}' WHERE notification = ? AND destination = ?`).run(row.id, destination);
|
|
709
712
|
}
|
|
713
|
+
else if (watched !== "send") {
|
|
714
|
+
this.db.prepare("UPDATE notification_delivery SET next_attempt_at = ? WHERE notification = ? AND destination = ? AND (next_attempt_at IS NULL OR next_attempt_at < ?)")
|
|
715
|
+
.run(watched.hold, row.id, destination, watched.hold);
|
|
716
|
+
}
|
|
710
717
|
}
|
|
711
718
|
if (this.store.telegramRetryAt(binding.botId) > now.toISOString())
|
|
712
719
|
return [];
|
package/dist/chat-quiet.js
CHANGED
|
@@ -155,6 +155,7 @@ function lineOf(store, item, now, root, viewer) {
|
|
|
155
155
|
: checks.status === "passed" || checks.status === "failed" || checks.status === "not-run" ? checks.status : null,
|
|
156
156
|
report: card.facts.report === true,
|
|
157
157
|
completedBy: card.facts.completedBy ?? null,
|
|
158
|
+
...(card.facts.landed ? { landed: card.facts.landed } : {}),
|
|
158
159
|
pullRequest,
|
|
159
160
|
// The lead speaks as "I" for what it did: the work it filed or approved, or the completion it recorded.
|
|
160
161
|
lead: card.status.headline === "Complete" ? card.completedByLead : store.leadWorkOf(item.taskRef) !== null,
|
package/dist/chat-voice.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export type FinishedFact = {
|
|
|
19
19
|
checks: "passed" | "failed" | "not-run" | "off" | null;
|
|
20
20
|
report: boolean;
|
|
21
21
|
completedBy: string | null;
|
|
22
|
+
/** Toolroll marked it complete because its work landed: why ("its pull request merged"). */
|
|
23
|
+
landed?: string | null;
|
|
22
24
|
/** An open pull request carries this result: the ask is "Merge it?" rather than "Accept and finish it?". */
|
|
23
25
|
pullRequest?: boolean;
|
|
24
26
|
/** The lead did this (built it, or marked it complete), so the lead says "I". */
|
package/dist/chat-voice.js
CHANGED
|
@@ -82,6 +82,8 @@ export function finishedLine(fact) {
|
|
|
82
82
|
? `I couldn't finish ${lower}. The work so far is kept; retry when you're ready.`
|
|
83
83
|
: `${name} stopped before it finished. The work so far is kept; retry when you're ready.`;
|
|
84
84
|
case "Complete":
|
|
85
|
+
if (fact.landed)
|
|
86
|
+
return `${name} is done: ${fact.landed}.`;
|
|
85
87
|
if (fact.lead === true && fact.completedBy)
|
|
86
88
|
return `I marked ${lower} complete.`;
|
|
87
89
|
return fact.completedBy ? `${name} is done. ${fact.completedBy} marked it complete.` : `${name} is done.`;
|
|
@@ -64,6 +64,8 @@ export declare function finishedCheckouts(store: Store, now: Date, repo?: string
|
|
|
64
64
|
taskId: string | null;
|
|
65
65
|
finishedAt: string;
|
|
66
66
|
}[];
|
|
67
|
+
/** Why a checkout stays as a clean-up by hand reads it (in use, or its task not done with it), before its files; null: it may go. */
|
|
68
|
+
export declare function checkoutKeepWhy(pool: WorktreePool, row: WorktreeRow, statuses: ReturnType<typeof taskStatuses>, now: Date): KeepWhy | null;
|
|
67
69
|
/**
|
|
68
70
|
* What a clean-up would remove now, how much space it would free, and what stays and why. `manual` is a clean-up by
|
|
69
71
|
* hand (it doesn't wait for the setting); otherwise it is the worker's pass under the setting. Reads only.
|
|
@@ -92,6 +94,7 @@ export declare function cleanCheckouts(store: Store, pool: WorktreePool, now: ()
|
|
|
92
94
|
actor: string;
|
|
93
95
|
repo?: string;
|
|
94
96
|
only?: ReadonlySet<string>;
|
|
97
|
+
spare?: (path: string) => boolean;
|
|
95
98
|
}): Promise<CleanResult>;
|
|
96
99
|
export type SlimResult = {
|
|
97
100
|
slimmed: {
|
package/dist/checkout-cleanup.js
CHANGED
|
@@ -242,6 +242,10 @@ function keptFor(pool, row, statuses, now, cleanup) {
|
|
|
242
242
|
const dueAt = new Date(Date.parse(since) + days * DAY_MS).toISOString();
|
|
243
243
|
return dueAt > now.toISOString() ? { why: "not due yet", taskId, dueAt } : { why: null, taskId };
|
|
244
244
|
}
|
|
245
|
+
/** Why a checkout stays as a clean-up by hand reads it (in use, or its task not done with it), before its files; null: it may go. */
|
|
246
|
+
export function checkoutKeepWhy(pool, row, statuses, now) {
|
|
247
|
+
return keptFor(pool, row, statuses, now, "manual").why;
|
|
248
|
+
}
|
|
245
249
|
async function eachAtMost(items, limit, work) {
|
|
246
250
|
const out = new Array(items.length);
|
|
247
251
|
let next = 0;
|
|
@@ -298,11 +302,16 @@ export async function cleanCheckouts(store, pool, now, options) {
|
|
|
298
302
|
const sizes = new Map();
|
|
299
303
|
const wanted = (row, fresh) => {
|
|
300
304
|
// Right before a checkout goes, where its task stands is read afresh, and its size taken for the ledger.
|
|
305
|
+
// `spare`: live work uses it (storage-sweep.ts), asked at both looks.
|
|
301
306
|
if (fresh)
|
|
302
307
|
statuses = taskStatuses(store, now());
|
|
303
|
-
const go = keptFor(pool, row, statuses, now(), options.manual ? "manual" : cleanup).why === null;
|
|
304
|
-
if (go && fresh)
|
|
308
|
+
const go = keptFor(pool, row, statuses, now(), options.manual ? "manual" : cleanup).why === null && options.spare?.(row.path) !== true;
|
|
309
|
+
if (go && fresh) {
|
|
305
310
|
sizes.set(row.path, diskBytes([row.path]).get(row.path) ?? 0);
|
|
311
|
+
// du can be slow. The reservation excludes a concurrent checkout lease, and this last read also catches
|
|
312
|
+
// an open run/claim or task change that arrived while size was measured in the sweep child.
|
|
313
|
+
return keptFor(pool, row, taskStatuses(store, now()), now(), options.manual ? "manual" : cleanup).why === null && options.spare?.(row.path) !== true;
|
|
314
|
+
}
|
|
306
315
|
return go;
|
|
307
316
|
};
|
|
308
317
|
const pruned = await pool.prune(rows, wanted, options.manual ? Infinity : AUTO_MAX);
|
|
@@ -156,7 +156,8 @@ export declare const CONSOLE_FORMS: {
|
|
|
156
156
|
readonly revise: FormContract<"batch" | "return" | "source">;
|
|
157
157
|
readonly followUp: FormContract<"level" | "return">;
|
|
158
158
|
readonly resolveIncident: FormContract<"return">;
|
|
159
|
-
readonly taskSteer: FormContract<"note">;
|
|
159
|
+
readonly taskSteer: FormContract<"note" | "return" | "run">;
|
|
160
|
+
readonly taskReply: FormContract<"answer" | "question">;
|
|
160
161
|
readonly taskHold: FormContract<"reason">;
|
|
161
162
|
readonly taskBlock: FormContract<"on">;
|
|
162
163
|
readonly taskRepairDependency: FormContract<"blocker" | "operation" | "replacement">;
|
|
@@ -173,6 +174,7 @@ export declare const CONSOLE_FORMS: {
|
|
|
173
174
|
readonly taskAcceptProof: FormContract<"note" | "return" | "run">;
|
|
174
175
|
readonly taskComplete: FormContract<"accept" | "note" | "publish" | "receipt" | "return" | "run">;
|
|
175
176
|
readonly taskStop: FormContract<"return" | "run">;
|
|
177
|
+
readonly taskPause: FormContract<"mode" | "note" | "request" | "return" | "run">;
|
|
176
178
|
readonly taskResume: FormContract<"nonce" | "return" | "run" | "token">;
|
|
177
179
|
};
|
|
178
180
|
export type ConsoleFormName = keyof typeof CONSOLE_FORMS;
|
|
@@ -167,7 +167,8 @@ export const CONSOLE_FORMS = {
|
|
|
167
167
|
followUp: formContract("POST /r/<run>/checks|add-tests", ["return", "level"]),
|
|
168
168
|
resolveIncident: formContract("POST /i/<id>/resolve", ["return"]),
|
|
169
169
|
// A task's verbs (/t/<task>/<verb>), one body each.
|
|
170
|
-
taskSteer: formContract("POST /t/<task>/steer", ["note"]),
|
|
170
|
+
taskSteer: formContract("POST /t/<task>/steer", ["note", "return", "run"]),
|
|
171
|
+
taskReply: formContract("POST /t/<task>/reply", ["question", "answer"]),
|
|
171
172
|
taskHold: formContract("POST /t/<task>/hold", ["reason"]),
|
|
172
173
|
taskBlock: formContract("POST /t/<task>/block|unblock", ["on"]),
|
|
173
174
|
taskRepairDependency: formContract("POST /t/<task>/repair-dependency", ["blocker", "operation", "replacement"]),
|
|
@@ -184,6 +185,9 @@ export const CONSOLE_FORMS = {
|
|
|
184
185
|
taskAcceptProof: formContract("POST /t/<task>/accept-proof", ["run", "note", "return"]),
|
|
185
186
|
taskComplete: formContract("POST /t/<task>/complete", ["receipt", "run", "accept", "note", "publish", "return"]),
|
|
186
187
|
taskStop: formContract("POST /t/<task>/stop", ["run", "return"]),
|
|
188
|
+
// Pause, or Pause and steer when a note rides along, or Resume (mode=resume): the exact run shown, and the page's own
|
|
189
|
+
// request id.
|
|
190
|
+
taskPause: formContract("POST /t/<task>/pause", ["run", "request", "note", "mode", "return"]),
|
|
187
191
|
taskResume: formContract("POST /t/<task>/resume|resume-arm", ["run", "token", "nonce", "return"]),
|
|
188
192
|
};
|
|
189
193
|
/** The console POSTs whose handlers read no field (sign-out not even the shared guard): their contract is the empty form. */
|