pi-observational-memory-extension 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.
Files changed (2) hide show
  1. package/extensions/index.ts +105 -15
  2. package/package.json +1 -1
@@ -378,9 +378,28 @@ export default function (pi: ExtensionAPI) {
378
378
  pi.on("session_shutdown", async (_event, ctx) => {
379
379
  if (runtime.statusTimer) clearInterval(runtime.statusTimer);
380
380
  runtime.currentOperation?.abort();
381
- if (runtime.state) await saveState(runtime.state);
382
- ctx.ui.setStatus("om", undefined);
383
- ctx.ui.setWidget("om", undefined);
381
+ if (runtime.state) {
382
+ try {
383
+ let isStale = false;
384
+ try {
385
+ const _ = ctx.cwd;
386
+ } catch {
387
+ isStale = true;
388
+ }
389
+ if (!isStale && runtime.state.enabled && runtime.state.pendingMessageTokens > 0) {
390
+ await observeNow(ctx, { force: true, reason: "session_shutdown" });
391
+ }
392
+ } catch (error) {
393
+ // Safe catch on shutdown
394
+ }
395
+ await saveState(runtime.state);
396
+ }
397
+ try {
398
+ ctx.ui.setStatus("om", undefined);
399
+ ctx.ui.setWidget("om", undefined);
400
+ } catch (e) {
401
+ // Ignore if UI or context is already stale
402
+ }
384
403
  });
385
404
 
386
405
  pi.on("model_select", async (event, ctx) => {
@@ -559,6 +578,14 @@ export default function (pi: ExtensionAPI) {
559
578
  }
560
579
 
561
580
  async function safeBoundary(ctx: any, boundary: string): Promise<void> {
581
+ let isStale = false;
582
+ try {
583
+ const _ = ctx.cwd;
584
+ } catch {
585
+ isStale = true;
586
+ }
587
+ if (isStale) return;
588
+
562
589
  const state = await ensureState(ctx);
563
590
  if (!state.enabled || state.status === "failed") return;
564
591
  await refreshCounts(ctx);
@@ -577,17 +604,40 @@ async function safeBoundary(ctx: any, boundary: string): Promise<void> {
577
604
  }
578
605
  if (shouldBuffer(state)) {
579
606
  void bufferObservation(ctx, `${boundary}:buffer`).catch(async (error) => {
580
- const s = await ensureState(ctx);
581
- s.status = "failed";
582
- s.lastError = `OM buffer failed: ${errorMessage(error)}`;
583
- await saveState(s);
584
- updateStatus(ctx);
585
- ctx.ui.notify(s.lastError, "error");
607
+ try {
608
+ let innerStale = false;
609
+ try {
610
+ const _ = ctx.cwd;
611
+ } catch {
612
+ innerStale = true;
613
+ }
614
+ if (innerStale) return;
615
+
616
+ const s = await ensureState(ctx);
617
+ s.status = "failed";
618
+ s.lastError = `OM buffer failed: ${errorMessage(error)}`;
619
+ await saveState(s);
620
+ updateStatus(ctx);
621
+ ctx.ui.notify(s.lastError, "error");
622
+ } catch (innerError) {
623
+ // Prevent any uncaught exceptions from background handler
624
+ }
586
625
  });
587
626
  }
588
627
  }
589
628
 
590
629
  async function ensureState(ctx: any): Promise<PiOMRecord> {
630
+ let isStale = false;
631
+ try {
632
+ const _ = ctx.cwd;
633
+ } catch {
634
+ isStale = true;
635
+ }
636
+ if (isStale) {
637
+ if (runtime.state) return runtime.state;
638
+ throw new Error("Cannot ensure state because the extension context is stale.");
639
+ }
640
+
591
641
  if (runtime.state && runtime.state.cwd === ctx.cwd && runtime.state.sessionFile === ctx.sessionManager.getSessionFile()) return runtime.state;
592
642
  const sessionFile = ctx.sessionManager.getSessionFile?.();
593
643
  const sessionId = ctx.sessionManager.getSessionId?.() || (sessionFile ? basename(sessionFile, ".jsonl") : "in-memory");
@@ -642,6 +692,14 @@ async function saveState(state: PiOMRecord): Promise<void> {
642
692
  }
643
693
 
644
694
  async function refreshCounts(ctx: any): Promise<void> {
695
+ let isStale = false;
696
+ try {
697
+ const _ = ctx.cwd;
698
+ } catch {
699
+ isStale = true;
700
+ }
701
+ if (isStale) return;
702
+
645
703
  const state = await ensureState(ctx);
646
704
  const branch = ctx.sessionManager.getBranch() as SessionEntry[];
647
705
  const pending = entriesAfter(branch, state.lastObservedEntryId).filter(isMessageLikeEntry);
@@ -653,11 +711,23 @@ async function refreshCounts(ctx: any): Promise<void> {
653
711
  function updateStatus(ctx: any): void {
654
712
  const state = runtime.state;
655
713
  if (!state) return;
656
- ctx.ui.setStatus("om", formatShortStatusColored(state));
714
+ try {
715
+ ctx.ui.setStatus("om", formatShortStatusColored(state));
716
+ } catch (e) {
717
+ // Context might be stale or UI disposed
718
+ }
657
719
  runtime.overlayHandle?.requestRender();
658
720
  }
659
721
 
660
722
  async function observeNow(ctx: any, opts: { force: boolean; reason: string; signal?: AbortSignal; manualText?: string }): Promise<void> {
723
+ let isStale = false;
724
+ try {
725
+ const _ = ctx.cwd;
726
+ } catch {
727
+ isStale = true;
728
+ }
729
+ if (isStale) return;
730
+
661
731
  const state = await ensureState(ctx);
662
732
  assertNoOperation(state);
663
733
  const branch = ctx.sessionManager.getBranch() as SessionEntry[];
@@ -749,6 +819,14 @@ async function bufferObservation(ctx: any, reason: string): Promise<void> {
749
819
  }
750
820
 
751
821
  async function activateBuffered(ctx: any, reason: string): Promise<void> {
822
+ let isStale = false;
823
+ try {
824
+ const _ = ctx.cwd;
825
+ } catch {
826
+ isStale = true;
827
+ }
828
+ if (isStale) return;
829
+
752
830
  const state = await ensureState(ctx);
753
831
  if (state.buffered.observations.length === 0) return;
754
832
  assertNoOperation(state);
@@ -785,6 +863,14 @@ async function activateBuffered(ctx: any, reason: string): Promise<void> {
785
863
  }
786
864
 
787
865
  async function reflectNow(ctx: any, opts: { reason: string; signal?: AbortSignal; manualPrompt?: string }): Promise<void> {
866
+ let isStale = false;
867
+ try {
868
+ const _ = ctx.cwd;
869
+ } catch {
870
+ isStale = true;
871
+ }
872
+ if (isStale) return;
873
+
788
874
  const state = await ensureState(ctx);
789
875
  assertNoOperation(state);
790
876
  if (!state.observations.trim()) throw new Error("No observations to reflect");
@@ -1311,11 +1397,15 @@ function formatTokens(tokens: number): string {
1311
1397
  }
1312
1398
 
1313
1399
  async function writeDebug(ctx: any, name: string, payload: unknown): Promise<void> {
1314
- const state = await ensureState(ctx);
1315
- const dir = runtime.debugDir ?? join(ctx.cwd, CONFIG_DIR_NAME, "om", "debug");
1316
- await mkdir(dir, { recursive: true });
1317
- const file = join(dir, `${new Date().toISOString().replace(/[:.]/g, "-")}-${sanitizeFileName(name)}.json`);
1318
- await writeFile(file, JSON.stringify({ extension: EXTENSION_ID, sessionId: state.sessionId, ...payload as any }, null, 2) + "\n", "utf8");
1400
+ try {
1401
+ const state = await ensureState(ctx);
1402
+ const dir = runtime.debugDir || join(ctx.cwd, CONFIG_DIR_NAME, "om", "debug");
1403
+ await mkdir(dir, { recursive: true });
1404
+ const file = join(dir, `${new Date().toISOString().replace(/[:.]/g, "-")}-${sanitizeFileName(name)}.json`);
1405
+ await writeFile(file, JSON.stringify({ extension: EXTENSION_ID, sessionId: state.sessionId, ...payload as any }, null, 2) + "\n", "utf8");
1406
+ } catch (error) {
1407
+ // Ignore debug write errors if context is stale
1408
+ }
1319
1409
  }
1320
1410
 
1321
1411
  function sanitizeFileName(name: string): string {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-observational-memory-extension",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Mastra-style Observational Memory extension for Pi compaction and runtime context.",
5
5
  "license": "MIT",
6
6
  "author": "Nikita Nosov <20nik.nosov21@gmail.com>",