@osolmaz/pi-workflows 0.16.4 → 0.16.6
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/README.md +5 -5
- package/dist/builtins/autoimplement.workflow.d.ts +5 -1
- package/dist/builtins/autoimplement.workflow.js +29 -10
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/client/client.js +3 -1
- package/dist/client/client.js.map +1 -1
- package/dist/client/protocol.d.ts +1 -1
- package/dist/client/protocol.js +18 -1
- package/dist/client/protocol.js.map +1 -1
- package/dist/client/view.d.ts +7 -5
- package/dist/extension/index.d.ts +2 -1
- package/dist/extension/index.js +115 -165
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +1 -1
- package/dist/extension/recorder.js +2 -2
- package/dist/extension/recorder.js.map +1 -1
- package/dist/extension/step-message.d.ts +1 -1
- package/dist/extension/step-message.js +4 -4
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/workflow-message-coordinator.d.ts +12 -1
- package/dist/extension/workflow-message-coordinator.js +65 -54
- package/dist/extension/workflow-message-coordinator.js.map +1 -1
- package/dist/resource-managers/index.d.ts +1 -2
- package/dist/resource-managers/index.js +1 -2
- package/dist/resource-managers/index.js.map +1 -1
- package/dist/resource-managers/sqlite.d.ts +2 -295
- package/dist/resource-managers/sqlite.js +71 -1528
- package/dist/resource-managers/sqlite.js.map +1 -1
- package/dist/resource-managers/types.d.ts +2 -7
- package/dist/server/resource-runner-entry.js +2 -6
- package/dist/server/resource-runner-entry.js.map +1 -1
- package/dist/server/rpc-bridge.js +1 -1
- package/dist/server/rpc-bridge.js.map +1 -1
- package/dist/server/rpc-executor.js +2 -4
- package/dist/server/rpc-executor.js.map +1 -1
- package/dist/server/server-entry.js +3 -0
- package/dist/server/server-entry.js.map +1 -1
- package/dist/server/server.d.ts +22 -8
- package/dist/server/server.js +498 -543
- package/dist/server/server.js.map +1 -1
- package/dist/server/state.d.ts +16 -29
- package/dist/server/state.js +105 -319
- package/dist/server/state.js.map +1 -1
- package/dist/server/view.d.ts +4 -5
- package/dist/server/view.js +51 -51
- package/dist/server/view.js.map +1 -1
- package/dist/server/workflow-runner-entry.d.ts +1 -1
- package/dist/server/workflow-runner-entry.js +1 -49
- package/dist/server/workflow-runner-entry.js.map +1 -1
- package/dist/server/workflow-runner-protocol.d.ts +2 -7
- package/dist/server/workflow-runner-protocol.js +5 -10
- package/dist/server/workflow-runner-protocol.js.map +1 -1
- package/dist/server/workflow-runner-store.d.ts +5 -6
- package/dist/server/workflow-runner-store.js +7 -14
- package/dist/server/workflow-runner-store.js.map +1 -1
- package/dist/state/attempt-time.d.ts +21 -0
- package/dist/state/attempt-time.js +95 -0
- package/dist/state/attempt-time.js.map +1 -0
- package/dist/state/json.d.ts +2 -1
- package/dist/state/json.js.map +1 -1
- package/dist/state/project-store.d.ts +39 -0
- package/dist/state/project-store.js +154 -0
- package/dist/state/project-store.js.map +1 -0
- package/dist/state/prune.d.ts +28 -0
- package/dist/state/prune.js +278 -83
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +23 -23
- package/dist/state/workflow-messages.d.ts +1 -1
- package/dist/state/workflow-messages.js +3 -1
- package/dist/state/workflow-messages.js.map +1 -1
- package/dist/viewer/backup.js +5 -0
- package/dist/viewer/backup.js.map +1 -1
- package/dist/workflows/diagnostics.d.ts +8 -0
- package/dist/workflows/diagnostics.js +61 -0
- package/dist/workflows/diagnostics.js.map +1 -0
- package/dist/workflows/engine.d.ts +2 -15
- package/dist/workflows/engine.js +125 -245
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +3 -7
- package/dist/workflows/human-decision.js +2 -56
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +2 -1
- package/dist/workflows/index.js +1 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/queue.d.ts +267 -0
- package/dist/workflows/queue.js +1248 -0
- package/dist/workflows/queue.js.map +1 -0
- package/dist/workflows/requests.d.ts +47 -0
- package/dist/workflows/requests.js +158 -0
- package/dist/workflows/requests.js.map +1 -0
- package/dist/workflows/schema.js +2 -4
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +11 -16
- package/dist/workflows/store.js +174 -169
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +6 -9
- package/dist/workflows/tool-input.js +15 -12
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/transitions.d.ts +38 -0
- package/dist/workflows/transitions.js +276 -0
- package/dist/workflows/transitions.js.map +1 -0
- package/dist/workflows/types.d.ts +9 -42
- package/dist/workflows/workflow-message-content.d.ts +7 -1
- package/dist/workflows/workflow-message-content.js +22 -4
- package/dist/workflows/workflow-message-content.js.map +1 -1
- package/docs/2026-08-25-workflow-follow-ups.md +5 -5
- package/docs/2026-09-02-unify-workflow-messages-plan.md +2 -0
- package/docs/2026-09-04-workflow-run-state-plan.md +2 -0
- package/docs/2026-09-06-durable-execution-plan.md +176 -0
- package/docs/DEFERRED_TURNS.md +84 -105
- package/docs/HUMAN_DECISIONS.md +17 -19
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +1 -1
- package/docs/SQLITE_STATE.md +87 -30
- package/docs/WORKFLOW_SERVER.md +50 -11
- package/docs/WORKFLOW_STEP_MESSAGES.md +72 -54
- package/docs/plans/2026-08-04-controller-runtime-plan.md +2 -0
- package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +2 -0
- package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +2 -0
- package/docs/plans/2026-09-04-automatic-state-retention-plan.md +283 -0
- package/docs/workflows.md +109 -44
- package/examples/workflows/autoresearch.workflow.ts +0 -2
- package/examples/workflows/branch.workflow.ts +0 -4
- package/examples/workflows/echo.workflow.ts +0 -2
- package/examples/workflows/live-settings.workflow.ts +0 -2
- package/examples/workflows/two-turn.workflow.ts +0 -2
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/protocol/fixtures/client-v1.json +1 -1
- package/skills/pi-workflows/SKILL.md +8 -7
- package/src/builtins/autoimplement.workflow.ts +31 -10
- package/src/client/client.ts +3 -1
- package/src/client/protocol.ts +21 -1
- package/src/client/view.ts +9 -5
- package/src/extension/index.ts +117 -189
- package/src/extension/recorder.ts +2 -2
- package/src/extension/step-message.ts +4 -4
- package/src/extension/workflow-message-coordinator.ts +78 -50
- package/src/resource-managers/index.ts +1 -9
- package/src/resource-managers/sqlite.ts +330 -2655
- package/src/resource-managers/types.ts +2 -3
- package/src/server/resource-runner-entry.ts +2 -7
- package/src/server/rpc-bridge.ts +1 -1
- package/src/server/rpc-executor.ts +4 -9
- package/src/server/server-entry.ts +3 -0
- package/src/server/server.ts +568 -644
- package/src/server/state.ts +122 -466
- package/src/server/view.ts +56 -60
- package/src/server/workflow-runner-entry.ts +2 -55
- package/src/server/workflow-runner-protocol.ts +5 -15
- package/src/server/workflow-runner-store.ts +12 -27
- package/src/state/attempt-time.ts +122 -0
- package/src/state/json.ts +2 -1
- package/src/state/project-store.ts +218 -0
- package/src/state/prune.ts +408 -95
- package/src/state/schema.ts +23 -23
- package/src/state/workflow-messages.ts +4 -2
- package/src/viewer/backup.ts +7 -0
- package/src/workflows/diagnostics.ts +78 -0
- package/src/workflows/engine.ts +133 -291
- package/src/workflows/human-decision.ts +3 -81
- package/src/workflows/index.ts +8 -2
- package/src/workflows/queue.ts +2091 -0
- package/src/workflows/requests.ts +283 -0
- package/src/workflows/schema.ts +2 -6
- package/src/workflows/store.ts +223 -231
- package/src/workflows/tool-input.ts +20 -12
- package/src/workflows/transitions.ts +330 -0
- package/src/workflows/types.ts +9 -46
- package/src/workflows/workflow-message-content.ts +28 -4
- package/dist/resource-managers/runtime.d.ts +0 -59
- package/dist/resource-managers/runtime.js +0 -423
- package/dist/resource-managers/runtime.js.map +0 -1
- package/src/resource-managers/runtime.ts +0 -576
package/src/state/prune.ts
CHANGED
|
@@ -5,16 +5,38 @@ import { StateDatabase } from "./database.js";
|
|
|
5
5
|
|
|
6
6
|
const TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "timed_out", "cancelled"]);
|
|
7
7
|
const LIVE_QUEUE_STATUSES = ["queued", "starting", "running", "parked"];
|
|
8
|
+
const ACTIVE_RUNNER_STATUSES = ["starting", "ready", "running"];
|
|
9
|
+
const ACTIVE_ATTEMPT_STATUSES = ["pending", "running", "waiting", "interrupted"];
|
|
8
10
|
const UNSETTLED_EFFECT_STATUSES = ["pending", "applying", "ambiguous"];
|
|
9
11
|
|
|
12
|
+
export const AUTOMATIC_STATE_RETENTION_MS = 30 * 24 * 60 * 60 * 1_000;
|
|
13
|
+
export const AUTOMATIC_STATE_PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1_000;
|
|
14
|
+
export const AUTOMATIC_STATE_VACUUM_MIN_BYTES = 64 * 1024 * 1024;
|
|
15
|
+
export const AUTOMATIC_STATE_VACUUM_MIN_FREE_RATIO = 0.2;
|
|
16
|
+
|
|
10
17
|
type RunAgeRow = {
|
|
11
18
|
runId: string;
|
|
12
19
|
parentRunId: string | null;
|
|
20
|
+
rootRunId: string;
|
|
13
21
|
launchOptionsHash: Buffer;
|
|
14
22
|
status: string;
|
|
15
23
|
finishedAt: number | null;
|
|
16
24
|
};
|
|
17
25
|
|
|
26
|
+
type SelectedRunTree = {
|
|
27
|
+
rootRunId: string;
|
|
28
|
+
runIds: string[];
|
|
29
|
+
signature: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type RunTreeSelection = {
|
|
33
|
+
candidateTrees: number;
|
|
34
|
+
blockedTrees: number;
|
|
35
|
+
trees: SelectedRunTree[];
|
|
36
|
+
runIds: string[];
|
|
37
|
+
signature: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
18
40
|
export type StatePruneOptions = {
|
|
19
41
|
before: string;
|
|
20
42
|
apply: boolean;
|
|
@@ -34,6 +56,38 @@ export type StatePruneReport = {
|
|
|
34
56
|
applied: boolean;
|
|
35
57
|
};
|
|
36
58
|
|
|
59
|
+
export type AutomaticStatePruneOptions = {
|
|
60
|
+
now?: number;
|
|
61
|
+
activeBlobHashes?: () => readonly Buffer[];
|
|
62
|
+
shouldContinue?: () => boolean;
|
|
63
|
+
yieldControl?: () => Promise<void>;
|
|
64
|
+
vacuumMinBytes?: number;
|
|
65
|
+
vacuumMinFreeRatio?: number;
|
|
66
|
+
vacuum?: (database: Database.Database) => void;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type AutomaticStatePruneReport = StatePruneReport & {
|
|
70
|
+
completed: boolean;
|
|
71
|
+
compacted: boolean;
|
|
72
|
+
pageCount: number;
|
|
73
|
+
freePageCount: number;
|
|
74
|
+
pageSize: number;
|
|
75
|
+
reclaimableBytes: number;
|
|
76
|
+
freePageRatio: number;
|
|
77
|
+
compactionError?: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type DatabasePageMetrics = Pick<
|
|
81
|
+
AutomaticStatePruneReport,
|
|
82
|
+
"pageCount" | "freePageCount" | "pageSize" | "reclaimableBytes" | "freePageRatio"
|
|
83
|
+
>;
|
|
84
|
+
|
|
85
|
+
type DeletedState = {
|
|
86
|
+
deletedRows: number;
|
|
87
|
+
deletedBlobs: number;
|
|
88
|
+
deletedBlobBytes: number;
|
|
89
|
+
};
|
|
90
|
+
|
|
37
91
|
export function validateStatePruneOptions(options: StatePruneOptions): void {
|
|
38
92
|
parseCutoff(options.before);
|
|
39
93
|
if (options.apply && options.backupPath === undefined) {
|
|
@@ -57,13 +111,7 @@ export async function pruneState(
|
|
|
57
111
|
try {
|
|
58
112
|
const selection = selectRunTrees(state, cutoff);
|
|
59
113
|
const sizeBefore = databaseBytes(databasePath);
|
|
60
|
-
const base =
|
|
61
|
-
cutoff: new Date(cutoff).toISOString(),
|
|
62
|
-
candidateTrees: selection.candidateTrees,
|
|
63
|
-
blockedTrees: selection.blockedTrees,
|
|
64
|
-
selectedRuns: selection.runIds.length,
|
|
65
|
-
databaseBytesBefore: sizeBefore,
|
|
66
|
-
};
|
|
114
|
+
const base = reportBase(cutoff, selection, sizeBefore);
|
|
67
115
|
if (!options.apply) {
|
|
68
116
|
return {
|
|
69
117
|
...base,
|
|
@@ -81,46 +129,14 @@ export async function pruneState(
|
|
|
81
129
|
}
|
|
82
130
|
await state.backup(backupPath);
|
|
83
131
|
|
|
84
|
-
let
|
|
85
|
-
let deletedBlobs = 0;
|
|
86
|
-
let deletedBlobBytes = 0;
|
|
132
|
+
let deleted: DeletedState;
|
|
87
133
|
state.connection.exec("BEGIN EXCLUSIVE");
|
|
88
134
|
try {
|
|
89
135
|
const checked = selectRunTrees(state, cutoff);
|
|
90
|
-
if (
|
|
91
|
-
checked.runIds.join("\0") !== selection.runIds.join("\0") ||
|
|
92
|
-
checked.signature !== selection.signature
|
|
93
|
-
) {
|
|
136
|
+
if (!sameSelection(checked, selection)) {
|
|
94
137
|
throw new Error("Prune selection changed after backup; run the command again");
|
|
95
138
|
}
|
|
96
|
-
|
|
97
|
-
if (checked.runIds.length !== 0) {
|
|
98
|
-
deleteRunAggregates(state.connection, checked.runIds);
|
|
99
|
-
}
|
|
100
|
-
state.connection
|
|
101
|
-
.prepare(
|
|
102
|
-
`DELETE FROM workflow_definitions
|
|
103
|
-
WHERE NOT EXISTS (
|
|
104
|
-
SELECT 1 FROM runs WHERE runs.definition_digest = workflow_definitions.definition_digest
|
|
105
|
-
)`,
|
|
106
|
-
)
|
|
107
|
-
.run();
|
|
108
|
-
state.connection
|
|
109
|
-
.prepare(
|
|
110
|
-
`DELETE FROM projects
|
|
111
|
-
WHERE NOT EXISTS (SELECT 1 FROM runs WHERE runs.project_id = projects.project_id)
|
|
112
|
-
AND NOT EXISTS (
|
|
113
|
-
SELECT 1 FROM controller_resources
|
|
114
|
-
WHERE controller_resources.project_id = projects.project_id
|
|
115
|
-
)`,
|
|
116
|
-
)
|
|
117
|
-
.run();
|
|
118
|
-
const retainedBlobHashes = activeBlobHashes();
|
|
119
|
-
const blobStats = unreferencedBlobStats(state.connection, retainedBlobHashes);
|
|
120
|
-
deleteUnreferencedBlobs(state.connection, retainedBlobHashes);
|
|
121
|
-
deletedRows = totalChanges(state.connection) - beforeChanges;
|
|
122
|
-
deletedBlobs = blobStats.count;
|
|
123
|
-
deletedBlobBytes = blobStats.bytes;
|
|
139
|
+
deleted = deleteSelectedState(state.connection, checked.runIds, activeBlobHashes());
|
|
124
140
|
state.connection.exec("COMMIT");
|
|
125
141
|
} catch (error) {
|
|
126
142
|
if (state.connection.inTransaction) state.connection.exec("ROLLBACK");
|
|
@@ -132,25 +148,160 @@ export async function pruneState(
|
|
|
132
148
|
state.integrityCheck();
|
|
133
149
|
return {
|
|
134
150
|
...base,
|
|
151
|
+
...deleted,
|
|
152
|
+
databaseBytesAfter: databaseBytes(databasePath),
|
|
153
|
+
applied: true,
|
|
154
|
+
};
|
|
155
|
+
} finally {
|
|
156
|
+
if (lock !== undefined) releaseMaintenanceLock(lockPath, lock);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function pruneStateAutomatically(
|
|
161
|
+
state: StateDatabase,
|
|
162
|
+
databasePath: string,
|
|
163
|
+
options: AutomaticStatePruneOptions = {},
|
|
164
|
+
): Promise<AutomaticStatePruneReport> {
|
|
165
|
+
const now = options.now ?? Date.now();
|
|
166
|
+
const cutoff = now - AUTOMATIC_STATE_RETENTION_MS;
|
|
167
|
+
const shouldContinue = options.shouldContinue ?? (() => true);
|
|
168
|
+
const yieldControl = options.yieldControl ?? yieldToEventLoop;
|
|
169
|
+
const activeBlobHashes = options.activeBlobHashes ?? (() => []);
|
|
170
|
+
const sizeBefore = databaseBytes(databasePath);
|
|
171
|
+
const lockPath = `${databasePath}.maintenance.lock`;
|
|
172
|
+
const lock = acquireMaintenanceLock(lockPath);
|
|
173
|
+
let initial = emptySelection();
|
|
174
|
+
let deletedRows = 0;
|
|
175
|
+
let deletedBlobs = 0;
|
|
176
|
+
let deletedBlobBytes = 0;
|
|
177
|
+
let completed = true;
|
|
178
|
+
let compacted = false;
|
|
179
|
+
let compactionError: string | undefined;
|
|
180
|
+
let metrics: DatabasePageMetrics = {
|
|
181
|
+
pageCount: 0,
|
|
182
|
+
freePageCount: 0,
|
|
183
|
+
pageSize: 0,
|
|
184
|
+
reclaimableBytes: 0,
|
|
185
|
+
freePageRatio: 0,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
metrics = databasePageMetrics(state.connection);
|
|
190
|
+
initial = selectRunTrees(state, cutoff, now);
|
|
191
|
+
for (const selectedTree of initial.trees) {
|
|
192
|
+
if (!shouldContinue()) {
|
|
193
|
+
completed = false;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
state.connection.exec("BEGIN IMMEDIATE");
|
|
197
|
+
try {
|
|
198
|
+
const checked = selectRunTrees(state, cutoff, now).trees.find(
|
|
199
|
+
(tree) => tree.rootRunId === selectedTree.rootRunId,
|
|
200
|
+
);
|
|
201
|
+
if (checked === undefined || !sameTree(checked, selectedTree)) {
|
|
202
|
+
completed = false;
|
|
203
|
+
state.connection.exec("ROLLBACK");
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
const beforeChanges = totalChanges(state.connection);
|
|
207
|
+
deleteRunAggregates(state.connection, checked.runIds);
|
|
208
|
+
deleteUnusedDefinitionsAndProjects(state.connection);
|
|
209
|
+
deletedRows += totalChanges(state.connection) - beforeChanges;
|
|
210
|
+
state.connection.exec("COMMIT");
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (state.connection.inTransaction) state.connection.exec("ROLLBACK");
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
await yieldControl();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (completed && shouldContinue()) {
|
|
219
|
+
state.connection.exec("BEGIN IMMEDIATE");
|
|
220
|
+
try {
|
|
221
|
+
const beforeChanges = totalChanges(state.connection);
|
|
222
|
+
deleteUnusedDefinitionsAndProjects(state.connection);
|
|
223
|
+
const retainedBlobHashes = activeBlobHashes();
|
|
224
|
+
const blobStats = unreferencedBlobStats(state.connection, retainedBlobHashes);
|
|
225
|
+
deleteUnreferencedBlobs(state.connection, retainedBlobHashes);
|
|
226
|
+
deletedRows += totalChanges(state.connection) - beforeChanges;
|
|
227
|
+
deletedBlobs = blobStats.count;
|
|
228
|
+
deletedBlobBytes = blobStats.bytes;
|
|
229
|
+
state.connection.exec("COMMIT");
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (state.connection.inTransaction) state.connection.exec("ROLLBACK");
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
completed = false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (completed && shouldContinue()) {
|
|
239
|
+
try {
|
|
240
|
+
state.connection.pragma("wal_checkpoint(TRUNCATE)");
|
|
241
|
+
metrics = databasePageMetrics(state.connection);
|
|
242
|
+
if (
|
|
243
|
+
shouldContinue() &&
|
|
244
|
+
shouldVacuumStateAutomatically(
|
|
245
|
+
metrics,
|
|
246
|
+
options.vacuumMinBytes ?? AUTOMATIC_STATE_VACUUM_MIN_BYTES,
|
|
247
|
+
options.vacuumMinFreeRatio ?? AUTOMATIC_STATE_VACUUM_MIN_FREE_RATIO,
|
|
248
|
+
)
|
|
249
|
+
) {
|
|
250
|
+
(options.vacuum ?? vacuumDatabase)(state.connection);
|
|
251
|
+
compacted = true;
|
|
252
|
+
state.connection.pragma("wal_checkpoint(TRUNCATE)");
|
|
253
|
+
metrics = databasePageMetrics(state.connection);
|
|
254
|
+
}
|
|
255
|
+
} catch (error) {
|
|
256
|
+
compactionError = errorMessage(error);
|
|
257
|
+
metrics = databasePageMetrics(state.connection);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
...reportBase(cutoff, initial, sizeBefore),
|
|
135
263
|
deletedRows,
|
|
136
264
|
deletedBlobs,
|
|
137
265
|
deletedBlobBytes,
|
|
138
266
|
databaseBytesAfter: databaseBytes(databasePath),
|
|
139
267
|
applied: true,
|
|
268
|
+
completed,
|
|
269
|
+
compacted,
|
|
270
|
+
...metrics,
|
|
271
|
+
...(compactionError === undefined ? {} : { compactionError }),
|
|
140
272
|
};
|
|
141
273
|
} finally {
|
|
142
|
-
|
|
274
|
+
releaseMaintenanceLock(lockPath, lock);
|
|
143
275
|
}
|
|
144
276
|
}
|
|
145
277
|
|
|
278
|
+
export function shouldVacuumStateAutomatically(
|
|
279
|
+
metrics: DatabasePageMetrics,
|
|
280
|
+
minimumBytes: number = AUTOMATIC_STATE_VACUUM_MIN_BYTES,
|
|
281
|
+
minimumFreeRatio: number = AUTOMATIC_STATE_VACUUM_MIN_FREE_RATIO,
|
|
282
|
+
): boolean {
|
|
283
|
+
return metrics.reclaimableBytes >= minimumBytes && metrics.freePageRatio >= minimumFreeRatio;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function reportBase(cutoff: number, selection: RunTreeSelection, databaseBytesBefore: number) {
|
|
287
|
+
return {
|
|
288
|
+
cutoff: new Date(cutoff).toISOString(),
|
|
289
|
+
candidateTrees: selection.candidateTrees,
|
|
290
|
+
blockedTrees: selection.blockedTrees,
|
|
291
|
+
selectedRuns: selection.runIds.length,
|
|
292
|
+
databaseBytesBefore,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
146
296
|
function selectRunTrees(
|
|
147
297
|
state: StateDatabase,
|
|
148
298
|
cutoff: number,
|
|
149
|
-
|
|
299
|
+
observedAt: number = Date.now(),
|
|
300
|
+
): RunTreeSelection {
|
|
150
301
|
const { connection: database } = state;
|
|
151
302
|
const rows = database
|
|
152
303
|
.prepare(
|
|
153
|
-
`SELECT run_id AS runId, parent_run_id AS parentRunId,
|
|
304
|
+
`SELECT run_id AS runId, parent_run_id AS parentRunId, root_run_id AS rootRunId,
|
|
154
305
|
launch_options_hash AS launchOptionsHash, status, finished_at AS finishedAt
|
|
155
306
|
FROM runs ORDER BY created_at, run_id`,
|
|
156
307
|
)
|
|
@@ -166,47 +317,64 @@ function selectRunTrees(
|
|
|
166
317
|
)
|
|
167
318
|
.map((row) => row.runId),
|
|
168
319
|
);
|
|
169
|
-
const
|
|
170
|
-
const
|
|
320
|
+
const rowIds = new Set(rows.map((row) => row.runId));
|
|
321
|
+
const links = new Map(rows.map((row) => [row.runId, new Set<string>()]));
|
|
171
322
|
for (const row of rows) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
323
|
+
linkRuns(links, rowIds, row.runId, row.parentRunId);
|
|
324
|
+
linkRuns(links, rowIds, row.runId, row.rootRunId);
|
|
325
|
+
linkRuns(
|
|
326
|
+
links,
|
|
327
|
+
rowIds,
|
|
328
|
+
row.runId,
|
|
329
|
+
restartParentFromLaunchOptions(state.readJson(row.launchOptionsHash)),
|
|
176
330
|
);
|
|
177
|
-
if (restartParentRunId !== null) runParents.add(restartParentRunId);
|
|
178
|
-
parents.set(row.runId, [...runParents]);
|
|
179
|
-
for (const parentRunId of runParents) {
|
|
180
|
-
const values = children.get(parentRunId) ?? [];
|
|
181
|
-
values.push(row.runId);
|
|
182
|
-
children.set(parentRunId, values);
|
|
183
|
-
}
|
|
184
331
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
332
|
+
|
|
333
|
+
const components: string[][] = [];
|
|
334
|
+
const visited = new Set<string>();
|
|
335
|
+
for (const row of rows) {
|
|
336
|
+
if (visited.has(row.runId)) continue;
|
|
337
|
+
const component = descendants(row.runId, links);
|
|
338
|
+
for (const runId of component) visited.add(runId);
|
|
339
|
+
components.push(component.sort());
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const trees: SelectedRunTree[] = [];
|
|
343
|
+
let candidateTrees = 0;
|
|
191
344
|
let blockedTrees = 0;
|
|
192
|
-
for (const
|
|
193
|
-
|
|
194
|
-
|
|
345
|
+
for (const runIds of components) {
|
|
346
|
+
if (!runIds.some((runId) => eligible.has(runId))) continue;
|
|
347
|
+
candidateTrees += 1;
|
|
348
|
+
if (
|
|
349
|
+
runIds.some((runId) => !eligible.has(runId)) ||
|
|
350
|
+
treeHasBlocker(database, runIds, observedAt)
|
|
351
|
+
) {
|
|
195
352
|
blockedTrees += 1;
|
|
196
353
|
continue;
|
|
197
354
|
}
|
|
198
|
-
|
|
355
|
+
const rootRunId = chooseRootRunId(rows, runIds);
|
|
356
|
+
trees.push({
|
|
357
|
+
rootRunId,
|
|
358
|
+
runIds,
|
|
359
|
+
signature: selectionSignature(database, runIds),
|
|
360
|
+
});
|
|
199
361
|
}
|
|
200
|
-
|
|
362
|
+
trees.sort((left, right) => left.rootRunId.localeCompare(right.rootRunId));
|
|
363
|
+
const runIds = trees.flatMap((tree) => tree.runIds).sort();
|
|
201
364
|
return {
|
|
202
|
-
candidateTrees
|
|
365
|
+
candidateTrees,
|
|
203
366
|
blockedTrees,
|
|
367
|
+
trees,
|
|
204
368
|
runIds,
|
|
205
|
-
signature:
|
|
369
|
+
signature: JSON.stringify(trees),
|
|
206
370
|
};
|
|
207
371
|
}
|
|
208
372
|
|
|
209
|
-
function treeHasBlocker(
|
|
373
|
+
function treeHasBlocker(
|
|
374
|
+
database: Database.Database,
|
|
375
|
+
runIds: string[],
|
|
376
|
+
observedAt: number,
|
|
377
|
+
): boolean {
|
|
210
378
|
const values = placeholders(runIds);
|
|
211
379
|
if (
|
|
212
380
|
hasRow(
|
|
@@ -214,6 +382,55 @@ function treeHasBlocker(database: Database.Database, runIds: string[]): boolean
|
|
|
214
382
|
`SELECT 1 FROM run_queue WHERE run_id IN (${values}) AND status IN (${placeholders(LIVE_QUEUE_STATUSES)})`,
|
|
215
383
|
[...runIds, ...LIVE_QUEUE_STATUSES],
|
|
216
384
|
) ||
|
|
385
|
+
hasRow(
|
|
386
|
+
database,
|
|
387
|
+
`SELECT 1 FROM run_workers
|
|
388
|
+
WHERE run_id IN (${values}) AND status IN (${placeholders(ACTIVE_RUNNER_STATUSES)})`,
|
|
389
|
+
[...runIds, ...ACTIVE_RUNNER_STATUSES],
|
|
390
|
+
) ||
|
|
391
|
+
hasRow(
|
|
392
|
+
database,
|
|
393
|
+
`SELECT 1 FROM node_attempts
|
|
394
|
+
WHERE run_id IN (${values}) AND status IN (${placeholders(ACTIVE_ATTEMPT_STATUSES)})`,
|
|
395
|
+
[...runIds, ...ACTIVE_ATTEMPT_STATUSES],
|
|
396
|
+
) ||
|
|
397
|
+
hasRow(
|
|
398
|
+
database,
|
|
399
|
+
`SELECT 1 FROM session_segments WHERE run_id IN (${values}) AND status = 'recording'`,
|
|
400
|
+
runIds,
|
|
401
|
+
) ||
|
|
402
|
+
hasRow(
|
|
403
|
+
database,
|
|
404
|
+
`SELECT 1 FROM interactive_requests WHERE run_id IN (${values}) AND status = 'pending'`,
|
|
405
|
+
runIds,
|
|
406
|
+
) ||
|
|
407
|
+
hasRow(
|
|
408
|
+
database,
|
|
409
|
+
`SELECT 1 FROM human_decisions d
|
|
410
|
+
LEFT JOIN human_decision_resolutions r ON r.decision_id = d.decision_id
|
|
411
|
+
WHERE d.run_id IN (${values}) AND r.decision_id IS NULL`,
|
|
412
|
+
runIds,
|
|
413
|
+
) ||
|
|
414
|
+
hasRow(
|
|
415
|
+
database,
|
|
416
|
+
`SELECT 1 FROM workflow_messages WHERE run_id IN (${values}) AND status = 'pending'`,
|
|
417
|
+
runIds,
|
|
418
|
+
) ||
|
|
419
|
+
hasRow(
|
|
420
|
+
database,
|
|
421
|
+
`SELECT 1 FROM workflow_turns WHERE run_id IN (${values}) AND state = 'started'`,
|
|
422
|
+
runIds,
|
|
423
|
+
) ||
|
|
424
|
+
hasRow(
|
|
425
|
+
database,
|
|
426
|
+
`SELECT 1 FROM run_bindings b
|
|
427
|
+
WHERE b.run_id IN (${values}) AND b.execution_mode = 'interactive'
|
|
428
|
+
AND NOT EXISTS (
|
|
429
|
+
SELECT 1 FROM workflow_messages m
|
|
430
|
+
WHERE m.run_id = b.run_id AND m.kind = 'terminal' AND m.status = 'sent'
|
|
431
|
+
)`,
|
|
432
|
+
runIds,
|
|
433
|
+
) ||
|
|
217
434
|
hasRow(
|
|
218
435
|
database,
|
|
219
436
|
`SELECT 1 FROM workflow_follow_ups
|
|
@@ -229,14 +446,8 @@ function treeHasBlocker(database: Database.Database, runIds: string[]): boolean
|
|
|
229
446
|
hasRow(
|
|
230
447
|
database,
|
|
231
448
|
`SELECT 1 FROM run_steps s JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
232
|
-
WHERE a.run_id IN (${values}) AND s.run_id NOT IN (${values})
|
|
233
|
-
|
|
234
|
-
) ||
|
|
235
|
-
hasRow(
|
|
236
|
-
database,
|
|
237
|
-
`SELECT 1 FROM continuations
|
|
238
|
-
WHERE (parent_run_id IN (${values}) AND continuation_run_id NOT IN (${values}))
|
|
239
|
-
OR (continuation_run_id IN (${values}) AND parent_run_id NOT IN (${values}))`,
|
|
449
|
+
WHERE (a.run_id IN (${values}) AND s.run_id NOT IN (${values}))
|
|
450
|
+
OR (s.run_id IN (${values}) AND a.run_id NOT IN (${values}))`,
|
|
240
451
|
[...runIds, ...runIds, ...runIds, ...runIds],
|
|
241
452
|
) ||
|
|
242
453
|
hasRow(
|
|
@@ -251,12 +462,12 @@ function treeHasBlocker(database: Database.Database, runIds: string[]): boolean
|
|
|
251
462
|
}
|
|
252
463
|
const resources = relatedResourceIds(database, runIds);
|
|
253
464
|
const resourceValues = placeholders(resources);
|
|
254
|
-
|
|
465
|
+
return (
|
|
255
466
|
hasRow(
|
|
256
467
|
database,
|
|
257
468
|
`SELECT 1 FROM leases
|
|
258
469
|
WHERE resource_id IN (${resourceValues}) AND owner_id IS NOT NULL AND expires_at > ?`,
|
|
259
|
-
[...resources,
|
|
470
|
+
[...resources, observedAt],
|
|
260
471
|
) ||
|
|
261
472
|
hasRow(
|
|
262
473
|
database,
|
|
@@ -271,10 +482,45 @@ function treeHasBlocker(database: Database.Database, runIds: string[]): boolean
|
|
|
271
482
|
WHERE d.run_id IN (${values})`,
|
|
272
483
|
runIds,
|
|
273
484
|
)
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function deleteSelectedState(
|
|
489
|
+
database: Database.Database,
|
|
490
|
+
runIds: string[],
|
|
491
|
+
retainedBlobHashes: readonly Buffer[],
|
|
492
|
+
): DeletedState {
|
|
493
|
+
const beforeChanges = totalChanges(database);
|
|
494
|
+
if (runIds.length !== 0) deleteRunAggregates(database, runIds);
|
|
495
|
+
deleteUnusedDefinitionsAndProjects(database);
|
|
496
|
+
const blobStats = unreferencedBlobStats(database, retainedBlobHashes);
|
|
497
|
+
deleteUnreferencedBlobs(database, retainedBlobHashes);
|
|
498
|
+
return {
|
|
499
|
+
deletedRows: totalChanges(database) - beforeChanges,
|
|
500
|
+
deletedBlobs: blobStats.count,
|
|
501
|
+
deletedBlobBytes: blobStats.bytes,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function deleteUnusedDefinitionsAndProjects(database: Database.Database): void {
|
|
506
|
+
database
|
|
507
|
+
.prepare(
|
|
508
|
+
`DELETE FROM workflow_definitions
|
|
509
|
+
WHERE NOT EXISTS (
|
|
510
|
+
SELECT 1 FROM runs WHERE runs.definition_digest = workflow_definitions.definition_digest
|
|
511
|
+
)`,
|
|
512
|
+
)
|
|
513
|
+
.run();
|
|
514
|
+
database
|
|
515
|
+
.prepare(
|
|
516
|
+
`DELETE FROM projects
|
|
517
|
+
WHERE NOT EXISTS (SELECT 1 FROM runs WHERE runs.project_id = projects.project_id)
|
|
518
|
+
AND NOT EXISTS (
|
|
519
|
+
SELECT 1 FROM controller_resources
|
|
520
|
+
WHERE controller_resources.project_id = projects.project_id
|
|
521
|
+
)`,
|
|
522
|
+
)
|
|
523
|
+
.run();
|
|
278
524
|
}
|
|
279
525
|
|
|
280
526
|
function deleteRunAggregates(database: Database.Database, runIds: string[]): void {
|
|
@@ -292,12 +538,6 @@ function deleteRunAggregates(database: Database.Database, runIds: string[]): voi
|
|
|
292
538
|
const effectValues = placeholders(effectIds);
|
|
293
539
|
database.prepare(`DELETE FROM effects WHERE effect_id IN (${effectValues})`).run(...effectIds);
|
|
294
540
|
}
|
|
295
|
-
database
|
|
296
|
-
.prepare(
|
|
297
|
-
`DELETE FROM continuations
|
|
298
|
-
WHERE parent_run_id IN (${values}) OR continuation_run_id IN (${values})`,
|
|
299
|
-
)
|
|
300
|
-
.run(...runIds, ...runIds);
|
|
301
541
|
const allResources = [...new Set([...resources, ...effectRows.map((row) => row.resourceId)])];
|
|
302
542
|
if (allResources.length !== 0) {
|
|
303
543
|
database
|
|
@@ -408,7 +648,60 @@ function blobReferencePredicate(database: Database.Database, retainedBlobCount:
|
|
|
408
648
|
return `${unreferenced} AND blob_hash NOT IN (${retainedPlaceholders})`;
|
|
409
649
|
}
|
|
410
650
|
|
|
411
|
-
function
|
|
651
|
+
function vacuumDatabase(database: Database.Database): void {
|
|
652
|
+
database.exec("VACUUM");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function databasePageMetrics(database: Database.Database): DatabasePageMetrics {
|
|
656
|
+
const pageCount = pragmaNumber(database, "page_count");
|
|
657
|
+
const freePageCount = pragmaNumber(database, "freelist_count");
|
|
658
|
+
const pageSize = pragmaNumber(database, "page_size");
|
|
659
|
+
return {
|
|
660
|
+
pageCount,
|
|
661
|
+
freePageCount,
|
|
662
|
+
pageSize,
|
|
663
|
+
reclaimableBytes: freePageCount * pageSize,
|
|
664
|
+
freePageRatio: pageCount === 0 ? 0 : freePageCount / pageCount,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function pragmaNumber(database: Database.Database, name: string): number {
|
|
669
|
+
const value = database.pragma(name, { simple: true });
|
|
670
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
671
|
+
throw new Error(`SQLite ${name} returned an invalid value`);
|
|
672
|
+
}
|
|
673
|
+
return value;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function sameSelection(left: RunTreeSelection, right: RunTreeSelection): boolean {
|
|
677
|
+
return left.runIds.join("\0") === right.runIds.join("\0") && left.signature === right.signature;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function sameTree(left: SelectedRunTree, right: SelectedRunTree): boolean {
|
|
681
|
+
return left.runIds.join("\0") === right.runIds.join("\0") && left.signature === right.signature;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function chooseRootRunId(rows: RunAgeRow[], runIds: string[]): string {
|
|
685
|
+
const runIdSet = new Set(runIds);
|
|
686
|
+
const candidates = rows
|
|
687
|
+
.filter((row) => runIdSet.has(row.runId) && row.rootRunId === row.runId)
|
|
688
|
+
.map((row) => row.runId)
|
|
689
|
+
.sort();
|
|
690
|
+
return candidates[0] ?? runIds[0] ?? "";
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function linkRuns(
|
|
694
|
+
links: Map<string, Set<string>>,
|
|
695
|
+
rowIds: Set<string>,
|
|
696
|
+
left: string,
|
|
697
|
+
right: string | null,
|
|
698
|
+
): void {
|
|
699
|
+
if (right === null || right === left || !rowIds.has(right)) return;
|
|
700
|
+
links.get(left)?.add(right);
|
|
701
|
+
links.get(right)?.add(left);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function descendants(root: string, links: Map<string, Set<string>>): string[] {
|
|
412
705
|
const result: string[] = [];
|
|
413
706
|
const seen = new Set<string>();
|
|
414
707
|
const pending = [root];
|
|
@@ -417,7 +710,7 @@ function descendants(root: string, children: Map<string, string[]>): string[] {
|
|
|
417
710
|
if (seen.has(runId)) continue;
|
|
418
711
|
seen.add(runId);
|
|
419
712
|
result.push(runId);
|
|
420
|
-
pending.push(...(
|
|
713
|
+
pending.push(...(links.get(runId) ?? []));
|
|
421
714
|
}
|
|
422
715
|
return result;
|
|
423
716
|
}
|
|
@@ -444,6 +737,10 @@ function databaseBytes(databasePath: string): number {
|
|
|
444
737
|
.reduce((total, filePath) => total + fs.statSync(filePath).size, 0);
|
|
445
738
|
}
|
|
446
739
|
|
|
740
|
+
function emptySelection(): RunTreeSelection {
|
|
741
|
+
return { candidateTrees: 0, blockedTrees: 0, trees: [], runIds: [], signature: "" };
|
|
742
|
+
}
|
|
743
|
+
|
|
447
744
|
function acquireMaintenanceLock(lockPath: string): number {
|
|
448
745
|
try {
|
|
449
746
|
return fs.openSync(lockPath, "wx", 0o600);
|
|
@@ -478,12 +775,28 @@ function quoteIdentifier(value: string): string {
|
|
|
478
775
|
return `"${value.replaceAll('"', '""')}"`;
|
|
479
776
|
}
|
|
480
777
|
|
|
778
|
+
function errorMessage(error: unknown): string {
|
|
779
|
+
return error instanceof Error ? error.message : String(error);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async function yieldToEventLoop(): Promise<void> {
|
|
783
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
784
|
+
}
|
|
785
|
+
|
|
481
786
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
482
787
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
483
788
|
}
|
|
484
789
|
|
|
485
790
|
function isRunAgeRow(value: unknown): value is RunAgeRow {
|
|
486
|
-
return
|
|
791
|
+
return (
|
|
792
|
+
isRecord(value) &&
|
|
793
|
+
typeof value.runId === "string" &&
|
|
794
|
+
(value.parentRunId === null || typeof value.parentRunId === "string") &&
|
|
795
|
+
typeof value.rootRunId === "string" &&
|
|
796
|
+
Buffer.isBuffer(value.launchOptionsHash) &&
|
|
797
|
+
typeof value.status === "string" &&
|
|
798
|
+
(value.finishedAt === null || typeof value.finishedAt === "number")
|
|
799
|
+
);
|
|
487
800
|
}
|
|
488
801
|
|
|
489
802
|
function isResourceRow(value: unknown): value is { resourceId: string } {
|