@hank-warren/pi-loop 0.2.0 → 0.2.1

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
@@ -1,5 +1,11 @@
1
1
  # @hank-warren/pi-loop
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 2b22273: Stop the loop with "goal completed" when pi-goal clears its state entry after completion. pi-goal persists the finished goal (status complete) and then writes a clear (goal: null), so by the loop's next tick the last goal-state entry was the clear and the loop paused as goal-missing instead of stopping as goal-complete. readGoalSnapshot now reads a completed goal through its completion clear; a clear over any non-complete goal (user /goal clear mid-flight) still pauses the loop.
8
+
3
9
  ## 0.2.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hank-warren/pi-loop",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Interval wakeups for Pi: recurring prompt re-runs, stall rescue toward an active pi-goal goal, and loop-aware compaction that survives long sessions.",
5
5
  "type": "module",
6
6
  "keywords": [
package/src/state.ts CHANGED
@@ -87,11 +87,17 @@ interface SessionEntryLike {
87
87
  }
88
88
 
89
89
  function lastCustomEntryData(entries: unknown[], customType: string): unknown {
90
- for (let index = entries.length - 1; index >= 0; index -= 1) {
90
+ return lastCustomEntryDatas(entries, customType, 1)[0];
91
+ }
92
+
93
+ /** Newest-first data of the last `limit` custom entries of `customType`. */
94
+ function lastCustomEntryDatas(entries: unknown[], customType: string, limit: number): unknown[] {
95
+ const datas: unknown[] = [];
96
+ for (let index = entries.length - 1; index >= 0 && datas.length < limit; index -= 1) {
91
97
  const entry = entries[index] as SessionEntryLike | undefined;
92
- if (entry?.type === "custom" && entry.customType === customType) return entry.data;
98
+ if (entry?.type === "custom" && entry.customType === customType) datas.push(entry.data);
93
99
  }
94
- return undefined;
100
+ return datas;
95
101
  }
96
102
 
97
103
  /** Restore the persisted loop state from a session branch, fail-open. */
@@ -120,11 +126,26 @@ export interface GoalSnapshot {
120
126
  * entry shape is not recognizably a goal. Only fields pi-loop consumes are
121
127
  * extracted; unknown statuses are preserved verbatim so the caller can treat
122
128
  * anything outside its known sets conservatively.
129
+ *
130
+ * Completion race: pi-goal persists the finished goal (status "complete") and
131
+ * then clears the entry (goal: null), so by the loop's next tick the last
132
+ * entry is the clear. A clear whose immediately preceding entry is a complete
133
+ * goal therefore reports that complete goal — the loop must stop with
134
+ * "goal completed", not pause as goal-missing. A clear over any other status
135
+ * (user /goal clear mid-flight) still reads as no goal.
123
136
  */
124
137
  export function readGoalSnapshot(entries: unknown[]): GoalSnapshot | undefined {
125
- const data = ownRecord(lastCustomEntryData(entries, GOAL_STATE_ENTRY_TYPE));
126
- if (!data) return undefined;
127
- const goal = ownRecord(data.goal);
138
+ const datas = lastCustomEntryDatas(entries, GOAL_STATE_ENTRY_TYPE, 2);
139
+ const newest = ownRecord(datas[0]);
140
+ if (!newest) return undefined;
141
+ const goal = parseGoalSnapshot(newest.goal);
142
+ if (goal) return goal;
143
+ const cleared = parseGoalSnapshot(ownRecord(datas[1])?.goal);
144
+ return cleared?.status === "complete" ? cleared : undefined;
145
+ }
146
+
147
+ function parseGoalSnapshot(value: unknown): GoalSnapshot | undefined {
148
+ const goal = ownRecord(value);
128
149
  if (!goal) return undefined;
129
150
  const status = typeof goal.status === "string" ? goal.status : undefined;
130
151
  const text = typeof goal.text === "string" ? goal.text.trim() : "";