@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.
@@ -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;
@@ -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().catch((error) => {
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);