@alphacoder-v0/pi-loops 0.22.7 → 0.22.8

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 CHANGED
@@ -5,6 +5,23 @@ All notable changes to pi-loops are documented here. The format follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.22.8] - 2026-10-28
9
+
10
+ ### Fixed
11
+ - **A slot is not run twice when the clock changes hands.** A headless host that read `jobs.json`
12
+ just before an interactive pi took the clock could still dispatch from that snapshot. The pi
13
+ had already run the slot and cleared `running`, so the host claimed and ran it a second time,
14
+ and in one variant wrote an older `lastDueAt` back so a newer slot was owed again. A claim now
15
+ fails when the job's `lastDueAt` or `lastFiredAt` moved since the snapshot. Found with a TLA+
16
+ model of the scheduler. [#51](https://github.com/alphacoder-v0/pi-loops/issues/51),
17
+ [#87](https://github.com/alphacoder-v0/pi-loops/pull/87)
18
+ - **A host that learns from its own tick that it lost the clock leaves cleanly.** It awaited
19
+ `exit()` inside the tick, and `exit()` stops the scheduler, which waits for that same tick. The
20
+ host then left only through the 10 s `process.exit` fallback: its runs in flight wrote no
21
+ record and no `run_end` event, and `host.json` was left behind, so the next pi reported a
22
+ crashed host. [#52](https://github.com/alphacoder-v0/pi-loops/issues/52),
23
+ [#87](https://github.com/alphacoder-v0/pi-loops/pull/87)
24
+
8
25
  ## [0.22.7] - 2026-10-28
9
26
 
10
27
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alphacoder-v0/pi-loops",
3
- "version": "0.22.7",
3
+ "version": "0.22.8",
4
4
  "description": "An automation layer for pi: cron + stateful loops with a triage inbox, maker/checker verification, dynamic triggers, MCP notifications and tools, lifecycle hooks, session archives. A non-invasive pi extension.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -139,7 +139,8 @@ export function createHostRuntime(deps: HostRuntimeDeps): HostRuntime {
139
139
  if (exiting) return;
140
140
  exiting = true;
141
141
  log("an interactive pi owns the clock; exiting");
142
- await deps.exit(0);
142
+ // Not awaited: exit() stops the scheduler, and scheduler.stop() waits for this tick.
143
+ Promise.resolve(deps.exit(0)).catch((err: any) => log(`exit failed: ${err?.message ?? err}`));
143
144
  return;
144
145
  }
145
146
  await triggers.tick(now, leader);
package/src/scheduler.ts CHANGED
@@ -776,6 +776,9 @@ export class LoopScheduler {
776
776
  const priorFiredAt = job.lastFiredAt;
777
777
  const claimed = await this.store.update(job.id, (j) => {
778
778
  if (j.running) return;
779
+ // The slot was decided from a snapshot. If another process ran or skipped this job since,
780
+ // its stamps moved: that slot is spent, and running it again would run it twice.
781
+ if (j.lastDueAt !== priorDueAt || j.lastFiredAt !== priorFiredAt) return;
779
782
  j.running = { runId, pid: process.pid, host: this.self.host, startedAt };
780
783
  if (dueIso) j.lastDueAt = dueIso;
781
784
  j.lastFiredAt = startedAt;