@osolmaz/pi-workflows 0.16.4 → 0.16.5
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/dist/server/server.d.ts +13 -0
- package/dist/server/server.js +138 -6
- package/dist/server/server.js.map +1 -1
- package/dist/state/prune.d.ts +28 -0
- package/dist/state/prune.js +278 -76
- package/dist/state/prune.js.map +1 -1
- package/docs/SQLITE_STATE.md +21 -3
- package/docs/WORKFLOW_SERVER.md +29 -1
- package/docs/plans/2026-09-04-automatic-state-retention-plan.md +283 -0
- package/docs/workflows.md +16 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/src/server/server.ts +156 -7
- package/src/state/prune.ts +409 -83
package/dist/server/server.d.ts
CHANGED
|
@@ -60,6 +60,12 @@ export declare class WorkflowServer {
|
|
|
60
60
|
private decisionChannelConfig;
|
|
61
61
|
private decisionChannelError;
|
|
62
62
|
private channelReloading;
|
|
63
|
+
private automaticStatePruneTask;
|
|
64
|
+
private automaticStatePruneTimer;
|
|
65
|
+
private automaticStatePruneScheduled;
|
|
66
|
+
private automaticStatePruneDue;
|
|
67
|
+
private lastAutomaticStatePruneAt;
|
|
68
|
+
private nextAutomaticStatePruneAttemptAt;
|
|
63
69
|
private nextTerminalMessageReconciliationAt;
|
|
64
70
|
constructor(options?: WorkflowServerOptions);
|
|
65
71
|
get endpoint(): string;
|
|
@@ -94,6 +100,13 @@ export declare class WorkflowServer {
|
|
|
94
100
|
private executeResourceManagerOperation;
|
|
95
101
|
private statusReceipt;
|
|
96
102
|
private activeRunnerContentHashes;
|
|
103
|
+
private requestAutomaticStatePrune;
|
|
104
|
+
private resumeAutomaticStatePruneIfDue;
|
|
105
|
+
private startAutomaticStatePrune;
|
|
106
|
+
private runAutomaticStatePrune;
|
|
107
|
+
private scheduleAutomaticStatePruneTimer;
|
|
108
|
+
private automaticStatePruneCanContinue;
|
|
109
|
+
private waitForAutomaticStatePrune;
|
|
97
110
|
private stateStatusReceipt;
|
|
98
111
|
private answerCheckpoint;
|
|
99
112
|
private answerDecision;
|
package/dist/server/server.js
CHANGED
|
@@ -15,7 +15,7 @@ import { ResourceManagerWorkflowCoordinator, } from "../resource-managers/workfl
|
|
|
15
15
|
import { StateDatabase, workflowStatePath } from "../state/database.js";
|
|
16
16
|
import { canonicalJson } from "../state/json.js";
|
|
17
17
|
import { resourceIdFor } from "../state/mutation.js";
|
|
18
|
-
import { pruneState } from "../state/prune.js";
|
|
18
|
+
import { AUTOMATIC_STATE_PRUNE_INTERVAL_MS, pruneState, pruneStateAutomatically, } from "../state/prune.js";
|
|
19
19
|
import { recordViewerDeltas } from "../state/viewer.js";
|
|
20
20
|
import { workflowMessageIdFor } from "../state/workflow-messages.js";
|
|
21
21
|
import { humanDecisionChannelRequest } from "../workflows/decision-presentation.js";
|
|
@@ -38,6 +38,7 @@ const SERVER_RENEW_MS = 10_000;
|
|
|
38
38
|
const PACKAGE_VERSION = runtimePackageVersion();
|
|
39
39
|
const CLAIM_POLL_MS = 2_000;
|
|
40
40
|
const TERMINAL_MESSAGE_RECONCILE_MS = 1_000;
|
|
41
|
+
const AUTOMATIC_STATE_PRUNE_IDLE_RETRY_MS = 5 * 60 * 1_000;
|
|
41
42
|
const RUN_CLAIM_LEASE_MS = 30_000;
|
|
42
43
|
const RESOURCE_MANAGER_CLAIM_LEASE_MS = 120_000;
|
|
43
44
|
const RESOURCE_MANAGER_RENEW_MS = 30_000;
|
|
@@ -86,6 +87,12 @@ export class WorkflowServer {
|
|
|
86
87
|
decisionChannelConfig = null;
|
|
87
88
|
decisionChannelError = null;
|
|
88
89
|
channelReloading = false;
|
|
90
|
+
automaticStatePruneTask = null;
|
|
91
|
+
automaticStatePruneTimer = null;
|
|
92
|
+
automaticStatePruneScheduled = false;
|
|
93
|
+
automaticStatePruneDue = true;
|
|
94
|
+
lastAutomaticStatePruneAt = null;
|
|
95
|
+
nextAutomaticStatePruneAttemptAt = 0;
|
|
89
96
|
nextTerminalMessageReconciliationAt = 0;
|
|
90
97
|
constructor(options = {}) {
|
|
91
98
|
this.options = options;
|
|
@@ -159,13 +166,16 @@ export class WorkflowServer {
|
|
|
159
166
|
this.startTimers();
|
|
160
167
|
this.started = true;
|
|
161
168
|
this.log(`ready on ${this.socketPath} at epoch ${this.claim.epoch}`);
|
|
169
|
+
this.requestAutomaticStatePrune();
|
|
162
170
|
this.expireTimedOutInteraction();
|
|
163
|
-
void this.expireTimedOutDecision();
|
|
164
|
-
void this.claimOne();
|
|
165
|
-
void this.claimResourceManagerOne();
|
|
166
|
-
void this.reloadDecisionChannels()
|
|
171
|
+
void this.expireTimedOutDecision().finally(() => this.resumeAutomaticStatePruneIfDue());
|
|
172
|
+
void this.claimOne().finally(() => this.resumeAutomaticStatePruneIfDue());
|
|
173
|
+
void this.claimResourceManagerOne().finally(() => this.resumeAutomaticStatePruneIfDue());
|
|
174
|
+
void this.reloadDecisionChannels()
|
|
175
|
+
.catch((error) => {
|
|
167
176
|
this.log(`decision channel startup failed: ${errorMessage(error)}`);
|
|
168
|
-
})
|
|
177
|
+
})
|
|
178
|
+
.finally(() => this.resumeAutomaticStatePruneIfDue());
|
|
169
179
|
}
|
|
170
180
|
catch (error) {
|
|
171
181
|
const server = this.server;
|
|
@@ -196,9 +206,13 @@ export class WorkflowServer {
|
|
|
196
206
|
clearInterval(this.pollTimer);
|
|
197
207
|
if (this.viewTimer !== null)
|
|
198
208
|
clearInterval(this.viewTimer);
|
|
209
|
+
if (this.automaticStatePruneTimer !== null)
|
|
210
|
+
clearTimeout(this.automaticStatePruneTimer);
|
|
199
211
|
this.heartbeatTimer = null;
|
|
200
212
|
this.pollTimer = null;
|
|
201
213
|
this.viewTimer = null;
|
|
214
|
+
this.automaticStatePruneTimer = null;
|
|
215
|
+
this.automaticStatePruneScheduled = false;
|
|
202
216
|
const server = this.server;
|
|
203
217
|
this.server = null;
|
|
204
218
|
this.detachSessionCoordinators();
|
|
@@ -227,6 +241,9 @@ export class WorkflowServer {
|
|
|
227
241
|
this.activeChannels.clear();
|
|
228
242
|
await Promise.allSettled(this.activationTasks.values());
|
|
229
243
|
await Promise.allSettled(this.maintenanceCommands.values());
|
|
244
|
+
if (this.automaticStatePruneTask !== null) {
|
|
245
|
+
await Promise.allSettled([this.automaticStatePruneTask]);
|
|
246
|
+
}
|
|
230
247
|
this.registry.killAll();
|
|
231
248
|
if (this.claim !== null)
|
|
232
249
|
this.serverState.releaseServer(this.claim);
|
|
@@ -524,6 +541,7 @@ export class WorkflowServer {
|
|
|
524
541
|
});
|
|
525
542
|
case "state.prune":
|
|
526
543
|
return await this.executeMaintenanceCommand(request, async () => {
|
|
544
|
+
await this.waitForAutomaticStatePrune();
|
|
527
545
|
const payload = requireRecord(request.payload, "state.prune payload");
|
|
528
546
|
const before = requireString(payload.before, "before");
|
|
529
547
|
const apply = requireBoolean(payload.apply, "apply");
|
|
@@ -578,6 +596,7 @@ export class WorkflowServer {
|
|
|
578
596
|
if (this.maintenanceCommands.get(key) === execution) {
|
|
579
597
|
this.maintenanceCommands.delete(key);
|
|
580
598
|
}
|
|
599
|
+
this.requestAutomaticStatePrune();
|
|
581
600
|
}
|
|
582
601
|
}
|
|
583
602
|
addSubscription(connection, request, kind, target) {
|
|
@@ -1521,6 +1540,116 @@ export class WorkflowServer {
|
|
|
1521
1540
|
}
|
|
1522
1541
|
return [...digests].map((digest) => Buffer.from(digest, "hex"));
|
|
1523
1542
|
}
|
|
1543
|
+
requestAutomaticStatePrune() {
|
|
1544
|
+
this.automaticStatePruneDue = true;
|
|
1545
|
+
if (!this.started ||
|
|
1546
|
+
this.stopping ||
|
|
1547
|
+
this.automaticStatePruneScheduled ||
|
|
1548
|
+
this.automaticStatePruneTask !== null) {
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
this.automaticStatePruneScheduled = true;
|
|
1552
|
+
setImmediate(() => {
|
|
1553
|
+
if (!this.automaticStatePruneScheduled)
|
|
1554
|
+
return;
|
|
1555
|
+
this.automaticStatePruneScheduled = false;
|
|
1556
|
+
this.startAutomaticStatePrune();
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
resumeAutomaticStatePruneIfDue() {
|
|
1560
|
+
if (this.automaticStatePruneDue)
|
|
1561
|
+
this.requestAutomaticStatePrune();
|
|
1562
|
+
}
|
|
1563
|
+
startAutomaticStatePrune() {
|
|
1564
|
+
if (this.automaticStatePruneTask !== null || this.stopping || !this.started)
|
|
1565
|
+
return;
|
|
1566
|
+
const task = this.runAutomaticStatePrune();
|
|
1567
|
+
this.automaticStatePruneTask = task;
|
|
1568
|
+
void task.finally(() => {
|
|
1569
|
+
if (this.automaticStatePruneTask === task)
|
|
1570
|
+
this.automaticStatePruneTask = null;
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
async runAutomaticStatePrune() {
|
|
1574
|
+
if (!this.automaticStatePruneDue)
|
|
1575
|
+
return;
|
|
1576
|
+
if (!this.automaticStatePruneCanContinue()) {
|
|
1577
|
+
this.scheduleAutomaticStatePruneTimer(AUTOMATIC_STATE_PRUNE_IDLE_RETRY_MS);
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
const now = Date.now();
|
|
1581
|
+
if (now < this.nextAutomaticStatePruneAttemptAt) {
|
|
1582
|
+
this.scheduleAutomaticStatePruneTimer(this.nextAutomaticStatePruneAttemptAt - now);
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
if (this.lastAutomaticStatePruneAt !== null &&
|
|
1586
|
+
now - this.lastAutomaticStatePruneAt < AUTOMATIC_STATE_PRUNE_INTERVAL_MS) {
|
|
1587
|
+
this.scheduleAutomaticStatePruneTimer(this.lastAutomaticStatePruneAt + AUTOMATIC_STATE_PRUNE_INTERVAL_MS - now);
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
try {
|
|
1591
|
+
const report = await pruneStateAutomatically(this.state, this.databasePath, {
|
|
1592
|
+
now,
|
|
1593
|
+
activeBlobHashes: () => this.activeRunnerContentHashes(),
|
|
1594
|
+
shouldContinue: () => this.automaticStatePruneCanContinue(),
|
|
1595
|
+
});
|
|
1596
|
+
if (!report.completed) {
|
|
1597
|
+
this.automaticStatePruneDue = true;
|
|
1598
|
+
this.scheduleAutomaticStatePruneTimer(AUTOMATIC_STATE_PRUNE_IDLE_RETRY_MS);
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
this.lastAutomaticStatePruneAt = now;
|
|
1602
|
+
this.nextAutomaticStatePruneAttemptAt = 0;
|
|
1603
|
+
this.automaticStatePruneDue = false;
|
|
1604
|
+
this.scheduleAutomaticStatePruneTimer(AUTOMATIC_STATE_PRUNE_INTERVAL_MS);
|
|
1605
|
+
this.log(`automatic state prune completed: ${report.selectedRuns} run(s) selected, ` +
|
|
1606
|
+
`${report.blockedTrees} tree(s) blocked, ${report.deletedBlobs} blob(s) and ` +
|
|
1607
|
+
`${report.deletedBlobBytes} byte(s) removed`);
|
|
1608
|
+
if (report.compactionError !== undefined) {
|
|
1609
|
+
this.log(`automatic state compaction failed: ${report.compactionError}`);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
catch (error) {
|
|
1613
|
+
this.automaticStatePruneDue = true;
|
|
1614
|
+
this.nextAutomaticStatePruneAttemptAt = Date.now() + AUTOMATIC_STATE_PRUNE_IDLE_RETRY_MS;
|
|
1615
|
+
this.scheduleAutomaticStatePruneTimer(AUTOMATIC_STATE_PRUNE_IDLE_RETRY_MS);
|
|
1616
|
+
this.log(`automatic state prune failed: ${errorMessage(error)}`);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
scheduleAutomaticStatePruneTimer(delayMs) {
|
|
1620
|
+
if (this.stopping || !this.started)
|
|
1621
|
+
return;
|
|
1622
|
+
if (this.automaticStatePruneTimer !== null)
|
|
1623
|
+
clearTimeout(this.automaticStatePruneTimer);
|
|
1624
|
+
this.automaticStatePruneTimer = setTimeout(() => {
|
|
1625
|
+
this.automaticStatePruneTimer = null;
|
|
1626
|
+
this.requestAutomaticStatePrune();
|
|
1627
|
+
}, Math.max(1, delayMs));
|
|
1628
|
+
this.automaticStatePruneTimer.unref?.();
|
|
1629
|
+
}
|
|
1630
|
+
automaticStatePruneCanContinue() {
|
|
1631
|
+
return (this.started &&
|
|
1632
|
+
!this.stopping &&
|
|
1633
|
+
this.activeRuns.size === 0 &&
|
|
1634
|
+
this.activeResourceManagers.size === 0 &&
|
|
1635
|
+
this.activationTasks.size === 0 &&
|
|
1636
|
+
this.maintenanceCommands.size === 0 &&
|
|
1637
|
+
this.pendingStarts.size === 0 &&
|
|
1638
|
+
this.pendingRunClaims.size === 0 &&
|
|
1639
|
+
this.pendingResumes.size === 0 &&
|
|
1640
|
+
this.controlClaims.size === 0 &&
|
|
1641
|
+
this.pendingTerminalMessageReconciliations.size === 0 &&
|
|
1642
|
+
!this.resourceManagerPollActive &&
|
|
1643
|
+
!this.decisionTimeoutActive &&
|
|
1644
|
+
!this.channelReloading &&
|
|
1645
|
+
[...this.activeChannels.values()].every((channel) => channel.inFlight.size === 0));
|
|
1646
|
+
}
|
|
1647
|
+
async waitForAutomaticStatePrune() {
|
|
1648
|
+
this.automaticStatePruneScheduled = false;
|
|
1649
|
+
const task = this.automaticStatePruneTask;
|
|
1650
|
+
if (task !== null)
|
|
1651
|
+
await task;
|
|
1652
|
+
}
|
|
1524
1653
|
stateStatusReceipt() {
|
|
1525
1654
|
const count = (sql, ...params) => {
|
|
1526
1655
|
const row = this.state.connection.prepare(sql).get(...params);
|
|
@@ -2485,6 +2614,7 @@ export class WorkflowServer {
|
|
|
2485
2614
|
finally {
|
|
2486
2615
|
clearInterval(renewTimer);
|
|
2487
2616
|
this.activeResourceManagers.delete(key);
|
|
2617
|
+
this.requestAutomaticStatePrune();
|
|
2488
2618
|
if (!this.stopping)
|
|
2489
2619
|
setImmediate(() => void this.claimResourceManagerOne());
|
|
2490
2620
|
}
|
|
@@ -3098,6 +3228,7 @@ export class WorkflowServer {
|
|
|
3098
3228
|
finally {
|
|
3099
3229
|
this.reapRunnerDescendants(envelope.runnerEpoch);
|
|
3100
3230
|
this.activeRuns.delete(runId);
|
|
3231
|
+
this.requestAutomaticStatePrune();
|
|
3101
3232
|
}
|
|
3102
3233
|
}
|
|
3103
3234
|
async resumePendingRun(runId) {
|
|
@@ -3566,6 +3697,7 @@ export class WorkflowServer {
|
|
|
3566
3697
|
WHERE resource_id = (SELECT resource_id FROM runs WHERE run_id = ?)
|
|
3567
3698
|
AND generation = ?`)
|
|
3568
3699
|
.run(context.runId, active.generation);
|
|
3700
|
+
this.queue.settleRunEffect(context.runId, context.state.status === "waiting" ? "run.park_queue" : "run.settle_queue");
|
|
3569
3701
|
}
|
|
3570
3702
|
ensureTerminalWorkflowMessage(runId, now = Date.now(), stateOverride) {
|
|
3571
3703
|
const queue = this.queue.getWorkflowRun(runId);
|