pi-better-goal 0.2.0 → 0.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-goal",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Pi extension for goal tracking with background-aware continuation.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -162,6 +162,7 @@ function formatSnapshot(snapshot: ActivitySnapshot): string {
162
162
 
163
163
  export default function (pi: ExtensionAPI): void {
164
164
  const providers = new Map<string, BackgroundActivityProvider>();
165
+ const providerUnsubscribers = new Map<string, () => void>();
165
166
  providers.set("subagents", {
166
167
  id: "subagents",
167
168
  label: "Subagents",
@@ -172,6 +173,7 @@ export default function (pi: ExtensionAPI): void {
172
173
  let foregroundRunning = false;
173
174
  let pollTimer: ReturnType<typeof setInterval> | undefined;
174
175
  let collecting = false;
176
+ let collectionPending = false;
175
177
  let latestSnapshot: ActivitySnapshot | null = null;
176
178
  let backgroundDrainTracker: BackgroundDrainTracker | null = null;
177
179
  let lastWakeSignature = "";
@@ -230,6 +232,7 @@ export default function (pi: ExtensionAPI): void {
230
232
  continuationQueuedFor = null;
231
233
  backgroundDrainTracker = null;
232
234
  clearIdleContinuation();
235
+ syncPollingState();
233
236
  // Force a full redraw when the dock height changes (absent ↔ visible clock).
234
237
  refreshGoalWidget?.(!wasVisible || !isGoalClockVisible(goal));
235
238
  };
@@ -241,6 +244,7 @@ export default function (pi: ExtensionAPI): void {
241
244
  continuationQueuedFor = null;
242
245
  backgroundDrainTracker = null;
243
246
  clearIdleContinuation();
247
+ syncPollingState();
244
248
  refreshGoalWidget?.(wasVisible);
245
249
  };
246
250
 
@@ -409,12 +413,22 @@ export default function (pi: ExtensionAPI): void {
409
413
 
410
414
  const collectIfPossible = (): void => {
411
415
  const ctx = currentCtx;
412
- if (!ctx || collecting) {
416
+ if (!ctx) {
417
+ return;
418
+ }
419
+ if (collecting) {
420
+ collectionPending = true;
413
421
  return;
414
422
  }
415
423
  collecting = true;
416
424
  void publishSnapshot(ctx).finally(() => {
417
425
  collecting = false;
426
+ if (collectionPending) {
427
+ collectionPending = false;
428
+ collectIfPossible();
429
+ } else {
430
+ syncPollingState();
431
+ }
418
432
  });
419
433
  };
420
434
 
@@ -424,7 +438,6 @@ export default function (pi: ExtensionAPI): void {
424
438
  }
425
439
  pollTimer = setInterval(collectIfPossible, POLL_INTERVAL_MS);
426
440
  pollTimer.unref?.();
427
- collectIfPossible();
428
441
  };
429
442
 
430
443
  const stopPolling = (): void => {
@@ -435,6 +448,16 @@ export default function (pi: ExtensionAPI): void {
435
448
  pollTimer = undefined;
436
449
  };
437
450
 
451
+ const syncPollingState = (): void => {
452
+ const ctx = currentCtx;
453
+ const goal = ctx ? currentGoalSnapshot(ctx) : null;
454
+ if (ctx && (isPokeable(goal) || latestSnapshot?.backgroundRunning === true)) {
455
+ startPolling();
456
+ } else {
457
+ stopPolling();
458
+ }
459
+ };
460
+
438
461
  const installGoalWidget = (ctx: ExtensionContext): void => {
439
462
  if (!ctx.hasUI) {
440
463
  return;
@@ -494,8 +517,14 @@ export default function (pi: ExtensionAPI): void {
494
517
  if (!candidate || typeof candidate.id !== "string" || typeof candidate.getActivity !== "function") {
495
518
  return;
496
519
  }
497
- providers.set(candidate.id, candidate as BackgroundActivityProvider);
498
- collectIfPossible();
520
+ const accepted = candidate as BackgroundActivityProvider;
521
+ providerUnsubscribers.get(accepted.id)?.();
522
+ providerUnsubscribers.delete(accepted.id);
523
+ providers.set(accepted.id, accepted);
524
+ if (typeof accepted.onActivityChanged === "function") {
525
+ providerUnsubscribers.set(accepted.id, accepted.onActivityChanged(collectIfPossible));
526
+ }
527
+ if (latestSnapshot) collectIfPossible();
499
528
  });
500
529
 
501
530
  pi.registerCommand("goal", {
@@ -661,7 +690,8 @@ export default function (pi: ExtensionAPI): void {
661
690
  foregroundRunning = !ctx.isIdle();
662
691
  pi.events.emit(EVENT_READY, { version: EXTENSION_VERSION });
663
692
  installGoalWidget(ctx);
664
- startPolling();
693
+ latestSnapshot = await publishSnapshot(ctx);
694
+ syncPollingState();
665
695
  });
666
696
 
667
697
  pi.on("input", async (event, ctx) => {
@@ -683,7 +713,7 @@ export default function (pi: ExtensionAPI): void {
683
713
  }
684
714
 
685
715
  const backgroundInstruction = snapshot.backgroundRunning
686
- ? ` The goal still has delegated background work running (${summarizeActiveBackground(snapshot)}). Foreground idleness alone is not goal completion; do not mark the goal complete until delegated background work reaches a terminal state and its result or failure has been inspected.`
716
+ ? ` The goal still has delegated background work running (${summarizeActiveBackground(snapshot)}). Foreground idleness alone is not goal completion; keep any structured plan current and do not mark verification, the plan, or the goal complete until every relevant delegated task reaches a terminal state and its result or failure has been inspected and integrated.`
687
717
  : "";
688
718
  return {
689
719
  systemPrompt:
@@ -762,6 +792,8 @@ export default function (pi: ExtensionAPI): void {
762
792
  foregroundRunning = false;
763
793
  clearIdleContinuation();
764
794
  currentCtx = undefined;
795
+ latestSnapshot = null;
796
+ collectionPending = false;
765
797
  try {
766
798
  ctx.ui.setStatus(EXTENSION_NAME, undefined);
767
799
  ctx.ui.setWidget(EXTENSION_NAME, undefined);
package/src/subagents.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, readFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
@@ -82,6 +82,46 @@ export function readSubagentMetas(baseDir = defaultSubagentBaseDir()): SubagentR
82
82
  return metas.sort((left, right) => right.startedAt - left.startedAt);
83
83
  }
84
84
 
85
+ export function readParentSubagentMetas(
86
+ baseDir = defaultSubagentBaseDir(),
87
+ parentPid = process.pid,
88
+ ): SubagentRunMeta[] {
89
+ const directory = join(baseDir, "by-parent", String(parentPid));
90
+ ensureParentIndex(baseDir, directory, parentPid);
91
+ let ids: string[];
92
+ try {
93
+ ids = readdirSync(directory).filter((id) => id !== ".initialized");
94
+ } catch {
95
+ return [];
96
+ }
97
+ return ids
98
+ .map((id) => readSubagentMeta(baseDir, id))
99
+ .filter((meta): meta is SubagentRunMeta => meta !== undefined && meta.spawnPid === parentPid)
100
+ .sort((left, right) => right.startedAt - left.startedAt);
101
+ }
102
+
103
+ function readSubagentMeta(baseDir: string, id: string): SubagentRunMeta | undefined {
104
+ try {
105
+ const meta = JSON.parse(readFileSync(join(baseDir, "runs", id, "meta.json"), "utf8")) as SubagentRunMeta;
106
+ return typeof meta.id === "string" && typeof meta.status === "string" ? meta : undefined;
107
+ } catch {
108
+ return undefined;
109
+ }
110
+ }
111
+
112
+ function ensureParentIndex(baseDir: string, directory: string, parentPid: number): void {
113
+ try {
114
+ readFileSync(join(directory, ".initialized"));
115
+ return;
116
+ } catch {
117
+ // Existing registries are backfilled once for this parent process.
118
+ }
119
+ const owned = readSubagentMetas(baseDir).filter((meta) => meta.spawnPid === parentPid);
120
+ mkdirSync(directory, { recursive: true });
121
+ for (const meta of owned) writeFileSync(join(directory, meta.id), "");
122
+ writeFileSync(join(directory, ".initialized"), "1");
123
+ }
124
+
85
125
  export function subagentStatusToItem(
86
126
  meta: SubagentRunMeta,
87
127
  status: SubagentEffectiveStatus,
@@ -122,9 +162,7 @@ export function collectSubagentActivity(
122
162
  ): BackgroundProviderSnapshot {
123
163
  const parentPid = options.parentPid ?? process.pid;
124
164
  const processExists = options.processExists ?? defaultProcessExists;
125
- const metas = readSubagentMetas(options.baseDir).filter((meta) =>
126
- isCurrentParentSubagent(meta, parentPid),
127
- );
165
+ const metas = readParentSubagentMetas(options.baseDir, parentPid);
128
166
 
129
167
  return {
130
168
  providerId: "subagents",
package/src/types.ts CHANGED
@@ -47,6 +47,7 @@ export interface BackgroundActivityProvider {
47
47
  id: string;
48
48
  label?: string;
49
49
  getActivity(ctx: ExtensionContext): BackgroundProviderSnapshot | Promise<BackgroundProviderSnapshot>;
50
+ onActivityChanged?(notify: () => void): () => void;
50
51
  }
51
52
 
52
53
  export interface ActivitySnapshot {