pi-graft 0.1.0 → 0.1.2

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.
@@ -138,6 +138,10 @@ interface Stats {
138
138
  totalCount: number; readyCount: number;
139
139
  staleCount: number; dirty: boolean; syncing: boolean;
140
140
  syncedAt: string | null; lastFile: string | null;
141
+ /** When the in-flight background sync started (null when idle). A syncing
142
+ * flag older than BUILD_TIMEOUT_MS is orphaned (pi quit / crash / sleep
143
+ * killed the detached build) and must be treated as not-syncing. */
144
+ syncStartedAt?: string | null;
141
145
  }
142
146
 
143
147
  interface SessionState {
@@ -157,6 +161,7 @@ function emptyStats(): Stats {
157
161
  return {
158
162
  nodeCount: 0, edgeCount: 0, languages: [], totalCount: 0, readyCount: 0,
159
163
  staleCount: 0, dirty: false, syncing: false, syncedAt: null, lastFile: null,
164
+ syncStartedAt: null,
160
165
  };
161
166
  }
162
167
 
@@ -193,6 +198,23 @@ function patchStats(d: string, patch: Partial<Stats>): Stats {
193
198
  return next;
194
199
  }
195
200
 
201
+ /** True when the syncing flag is orphaned: set, but its build can no longer
202
+ * be alive (older than the rebuild budget, or timestamp missing from an
203
+ * older version). Callers should clear it instead of showing `syncing…`. */
204
+ function isSyncStale(s: Stats | null): boolean {
205
+ if (!s?.syncing) return false;
206
+ if (!s.syncStartedAt) return true;
207
+ const age = Date.now() - Date.parse(s.syncStartedAt);
208
+ return Number.isNaN(age) || age > BUILD_TIMEOUT_MS;
209
+ }
210
+
211
+ /** Clear an orphaned syncing flag (persisted so every session sees it). */
212
+ function healStaleSync(d: string): Stats | null {
213
+ const s = readStats(d);
214
+ if (!isSyncStale(s)) return s;
215
+ return patchStats(d, { syncing: false, syncStartedAt: null });
216
+ }
217
+
196
218
  function readSession(d: string, id: string): SessionState {
197
219
  return readJson<SessionState>(sessionPath(d, id)) ?? emptySession();
198
220
  }
@@ -233,7 +255,7 @@ function resolveStats(d: string): Stats | null {
233
255
  // ── formatting (mirror claude/format) ───────────────────────────────────────
234
256
 
235
257
  function freshnessSegment(s: Stats): string {
236
- if (s.syncing) return "syncing…";
258
+ if (s.syncing && !isSyncStale(s)) return "syncing…";
237
259
  if (s.dirty && s.staleCount > 0) return `⚠ ${s.staleCount} stale`;
238
260
  if (s.dirty) return "⚠ stale";
239
261
  return "✓ synced";
@@ -500,6 +522,9 @@ export default function (pi: ExtensionAPI) {
500
522
  ctx.ui.setStatus("graft", "graft: not installed (npm i -g @nanonets/graft)");
501
523
  return;
502
524
  }
525
+ // Self-heal here (not just at settle): a restarted session must not
526
+ // display another process's orphaned `syncing…` for even one turn.
527
+ healStaleSync(cwd);
503
528
  const stats = resolveStats(cwd);
504
529
  if (!stats) {
505
530
  ctx.ui.setStatus("graft", `graft ${version}: no graph — run graft build`);
@@ -622,6 +647,9 @@ export default function (pi: ExtensionAPI) {
622
647
  const staleCount =
623
648
  (g.changed?.length ?? 0) + (g.added?.length ?? 0) + (g.removed?.length ?? 0);
624
649
  patchStats(cwd, { dirty: true, staleCount, lastFile: basename(abs) });
650
+ // A stale syncing flag from a killed build must not survive an edit:
651
+ // the footer should show the real ⚠ stale state, not phantom syncing.
652
+ healStaleSync(cwd);
625
653
  await refreshStatus(ctx, cwd);
626
654
  } catch { /* post-edit work is advisory */ }
627
655
 
@@ -660,11 +688,14 @@ export default function (pi: ExtensionAPI) {
660
688
  pi.on("agent_settled", async (_event, ctx) => {
661
689
  const cwd = ctx.cwd;
662
690
  if (!hasGraph(cwd) || syncing.has(cwd)) return;
691
+ // Orphaned flag from a previous process that died mid-build: clear it so
692
+ // the footer is truthful even when this turn makes no edits (!dirty).
693
+ healStaleSync(cwd);
663
694
  let stats: Stats | null = null;
664
695
  try { stats = readStats(cwd); } catch { return; }
665
696
  if (!stats?.dirty) return;
666
697
  syncing.add(cwd);
667
- try { patchStats(cwd, { syncing: true }); } catch { /* ignore */ }
698
+ try { patchStats(cwd, { syncing: true, syncStartedAt: new Date().toISOString() }); } catch { /* ignore */ }
668
699
  await refreshStatus(ctx, cwd);
669
700
  const dir = cwd;
670
701
  const bin = GRAFT_BIN;
@@ -688,11 +719,11 @@ const writeAtomic = (p, v) => {
688
719
  try {
689
720
  execFileSync(bin, ["build", "."], { cwd: dir, stdio: "ignore", timeout: ${BUILD_TIMEOUT_MS} });
690
721
  const w = readJ(wiringP);
691
- if (!w) { writeAtomic(statsP, { ...(readJ(statsP) ?? {}), syncing: false }); process.exit(0); }
722
+ if (!w) { writeAtomic(statsP, { ...(readJ(statsP) ?? {}), syncing: false, syncStartedAt: null }); process.exit(0); }
692
723
  const nodes = w.nodes ?? [], edges = w.edges ?? [];
693
724
  writeAtomic(statsP, {
694
725
  ...(readJ(statsP) ?? {}),
695
- dirty: false, staleCount: 0, syncing: false, syncedAt: new Date().toISOString(),
726
+ dirty: false, staleCount: 0, syncing: false, syncStartedAt: null, syncedAt: new Date().toISOString(),
696
727
  nodeCount: (w.meta && w.meta.nodeCount) || nodes.length,
697
728
  edgeCount: (w.meta && w.meta.edgeCount) || edges.length,
698
729
  languages: (w.meta && w.meta.languages) || [],
@@ -700,7 +731,7 @@ try {
700
731
  readyCount: nodes.filter((n) => n.summary_state === "ready").length,
701
732
  });
702
733
  } catch {
703
- try { writeAtomic(statsP, { ...(readJ(statsP) ?? {}), syncing: false }); } catch {}
734
+ try { writeAtomic(statsP, { ...(readJ(statsP) ?? {}), syncing: false, syncStartedAt: null }); } catch {}
704
735
  }
705
736
  `;
706
737
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-graft",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Graft context-graph support for the pi coding harness — Claude-parity hooks, CLI-backed tools, skill, statusline and commands",
5
5
  "license": "MIT",
6
6
  "keywords": ["pi-package", "graft", "context-graph"],
@@ -16,6 +16,7 @@
16
16
  "skills": ["./skills/graft"]
17
17
  },
18
18
  "peerDependencies": {
19
- "@earendil-works/pi-coding-agent": "*"
19
+ "@earendil-works/pi-coding-agent": "*",
20
+ "typebox": "*"
20
21
  }
21
22
  }