pi-shadow-mind 0.1.8 → 0.1.9

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/README.md CHANGED
@@ -23,7 +23,7 @@ npm pack
23
23
  The generated `pi-shadow-mind-<version>.tgz` contains the compiled `dist/` extension. Publish it to npm for normal Pi installation, or unpack the standalone ZIP release and install that directory:
24
24
 
25
25
  ```powershell
26
- pi install npm:pi-shadow-mind@0.1.8
26
+ pi install npm:pi-shadow-mind@0.1.9
27
27
  # or, after unpacking the ZIP
28
28
  pi install ./pi-shadow-mind-0.1.0
29
29
  ```
package/dist/index.js CHANGED
@@ -7918,6 +7918,35 @@ function sample(values, count, random) {
7918
7918
  return copy.slice(0, count);
7919
7919
  }
7920
7920
 
7921
+ // src/session-lifetime.ts
7922
+ var STALE_CONTEXT_MARKER = "This extension ctx is stale after session replacement or reload";
7923
+ var SessionLifetime = class {
7924
+ active = false;
7925
+ activate() {
7926
+ this.active = true;
7927
+ }
7928
+ deactivate() {
7929
+ this.active = false;
7930
+ }
7931
+ get isActive() {
7932
+ return this.active;
7933
+ }
7934
+ run(action) {
7935
+ if (!this.active) return false;
7936
+ try {
7937
+ action();
7938
+ return true;
7939
+ } catch (error) {
7940
+ if (!isStaleExtensionContextError(error)) throw error;
7941
+ this.active = false;
7942
+ return false;
7943
+ }
7944
+ }
7945
+ };
7946
+ function isStaleExtensionContextError(error) {
7947
+ return error instanceof Error && error.message.includes(STALE_CONTEXT_MARKER);
7948
+ }
7949
+
7921
7950
  // src/shadow-runner.ts
7922
7951
  import { mkdir as mkdir3 } from "node:fs/promises";
7923
7952
  import { fileURLToPath } from "node:url";
@@ -8317,6 +8346,7 @@ var ShadowMindRuntime = class {
8317
8346
  active = /* @__PURE__ */ new Map();
8318
8347
  recentEvents = [];
8319
8348
  batcher;
8349
+ sessionLifetime = new SessionLifetime();
8320
8350
  epoch = 0;
8321
8351
  modelCalls = 0;
8322
8352
  paused = false;
@@ -8332,6 +8362,7 @@ var ShadowMindRuntime = class {
8332
8362
  }
8333
8363
  registerEvents() {
8334
8364
  this.pi.on("session_start", async (_event, ctx) => {
8365
+ this.sessionLifetime.activate();
8335
8366
  this.latestContext = ctx;
8336
8367
  this.modelCalls = 0;
8337
8368
  await this.configStore.initialize();
@@ -8364,6 +8395,7 @@ var ShadowMindRuntime = class {
8364
8395
  this.abortAll("session-shutdown");
8365
8396
  ctx.ui.setStatus("shadow-mind", void 0);
8366
8397
  ctx.ui.setWidget("shadow-mind-panel", void 0);
8398
+ this.sessionLifetime.deactivate();
8367
8399
  });
8368
8400
  }
8369
8401
  registerUi() {
@@ -8456,12 +8488,20 @@ var ShadowMindRuntime = class {
8456
8488
  modelAuthOk: (model) => ctx.modelRegistry.hasConfiguredAuth(model) || ctx.modelRegistry.isUsingOAuth(model),
8457
8489
  mainThinkingLevel: ctx.thinkingLevel,
8458
8490
  onReport: (report) => this.acceptReport(report)
8459
- }).then((result) => this.handleRunEnd(runId, shadow, result));
8491
+ }).then((result) => this.handleRunEnd(runId, shadow, result)).catch((error) => {
8492
+ this.active.delete(runId);
8493
+ this.record("run-end", {
8494
+ runId,
8495
+ shadowId: shadow.id,
8496
+ reason: "error",
8497
+ error: error instanceof Error ? error.message : String(error)
8498
+ });
8499
+ });
8460
8500
  }
8461
8501
  handleRunEnd(runId, shadow, result) {
8462
8502
  this.active.delete(runId);
8463
8503
  this.record("run-end", { runId, shadowId: shadow.id, ...result });
8464
- if (this.latestContext) this.updateStatus(this.latestContext);
8504
+ if (this.latestContext && this.sessionLifetime.isActive) this.updateStatus(this.latestContext);
8465
8505
  }
8466
8506
  acceptReport(report) {
8467
8507
  if (report.epoch !== this.epoch) return;
@@ -8473,12 +8513,14 @@ var ShadowMindRuntime = class {
8473
8513
  const content = formatReportBatch(current);
8474
8514
  this.record("report-delivered", { runIds: current.map((report) => report.runId), count: current.length });
8475
8515
  const idle = this.latestContext?.isIdle() ?? true;
8476
- this.pi.sendMessage({
8477
- customType: "shadow-report",
8478
- content,
8479
- display: true,
8480
- details: { reports: current.map(({ shadowId, runId }) => ({ shadowId, runId })) }
8481
- }, { triggerTurn: true, deliverAs: idle ? "followUp" : "steer" });
8516
+ this.sessionLifetime.run(() => {
8517
+ this.pi.sendMessage({
8518
+ customType: "shadow-report",
8519
+ content,
8520
+ display: true,
8521
+ details: { reports: current.map(({ shadowId, runId }) => ({ shadowId, runId })) }
8522
+ }, { triggerTurn: true, deliverAs: idle ? "followUp" : "steer" });
8523
+ });
8482
8524
  }
8483
8525
  async refresh(ctx) {
8484
8526
  const config = await this.configStore.reload();
@@ -8514,11 +8556,13 @@ var ShadowMindRuntime = class {
8514
8556
  const event = { at: (/* @__PURE__ */ new Date()).toISOString(), kind, epoch: this.epoch, data };
8515
8557
  this.recentEvents.push(event);
8516
8558
  if (this.recentEvents.length > 20) this.recentEvents.shift();
8517
- this.pi.appendEntry("shadow-mind-event", event);
8559
+ this.sessionLifetime.run(() => this.pi.appendEntry("shadow-mind-event", event));
8518
8560
  }
8519
8561
  updateStatus(ctx) {
8520
- ctx.ui.setStatus("shadow-mind", `\u{1F419} ${this.active.size}${this.paused ? " paused" : ""}${this.diagnostics.length || this.hasRecentRunErrors() ? " !" : ""}`);
8521
- if (this.panelVisible) ctx.ui.setWidget("shadow-mind-panel", this.statusLines(), { placement: "aboveEditor" });
8562
+ this.sessionLifetime.run(() => {
8563
+ ctx.ui.setStatus("shadow-mind", `\u{1F419} ${this.active.size}${this.paused ? " paused" : ""}${this.diagnostics.length || this.hasRecentRunErrors() ? " !" : ""}`);
8564
+ if (this.panelVisible) ctx.ui.setWidget("shadow-mind-panel", this.statusLines(), { placement: "aboveEditor" });
8565
+ });
8522
8566
  }
8523
8567
  hasRecentRunErrors() {
8524
8568
  return this.recentEvents.slice(-3).some((event) => event.kind === "run-end" && (event.data?.reason === "error" || event.data?.reason === "timeout"));