pi-advisor-flow 0.9.0 → 0.9.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
@@ -4,6 +4,23 @@ All notable changes to this project are documented here.
4
4
 
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0/).
6
6
 
7
+ ## 0.9.1 - 2026-09-30
8
+
9
+ ### Fixed
10
+
11
+ - Overrode transitive `brace-expansion` 5.0.12 and `undici` 8.11.2 to clear four high-severity audit advisories (stack-exhaustion DoS, WebSocket subprotocol DoS, TLS validation bypass).
12
+
13
+ - Fixed manual Advisor consultations crashing non-interactive (`--print`) sessions on Pi 0.99: UI status updates and scout lifecycle reads now no-op safely once the session context is retired instead of throwing, `/advisor-manual` completes within the command handler in print mode, and streaming tool-result repaint timers stop once their renderer is gone.
14
+
15
+ ### Changed
16
+
17
+ - Added Pi 0.99.x support (peer range `^0.87.1 || ^0.99.1`); Pi 0.99's context-lifecycle enforcement is now honored. Manual consultation results no longer trigger a follow-up turn in print mode.
18
+ - Updated the dev toolchain: oxlint 1.86.0, @oxlint/plugins 1.86.0, oxfmt 0.71.0, ultracite 7.12.2, knip 6.39.0.
19
+
20
+ ### Added
21
+
22
+ - Regression tests covering stale-context resilience of the guarded UI helpers.
23
+
7
24
  ## 0.9.0 - 2026-09-28
8
25
 
9
26
  ### Added
package/README.md CHANGED
@@ -41,7 +41,7 @@ Regular consultations never block execution. Automatic loop gates are different:
41
41
 
42
42
  ## Install
43
43
 
44
- Requires Pi 0.84.1 or later. Pi 0.85.0 is not supported (broken upstream release); use Pi 0.85.1 or later instead.
44
+ Requires Pi 0.87.1+ (0.87.x) or 0.99.1+ (0.99.x).
45
45
 
46
46
  ```bash
47
47
  pi install npm:pi-advisor-flow
package/dist/index.js CHANGED
@@ -3884,6 +3884,30 @@ var runAdvisorGate = async (ctx, question, trigger = "repeated-tool-call", signa
3884
3884
  // src/tools/scout-status.ts
3885
3885
  import { Text as Text2 } from "@earendil-works/pi-tui";
3886
3886
 
3887
+ // src/ui-guard.ts
3888
+ var isStaleContext = (error) => error instanceof Error && /stale/iu.test(error.message);
3889
+ var uiAvailable = (ctx) => {
3890
+ try {
3891
+ return ctx.hasUI;
3892
+ } catch (error) {
3893
+ if (isStaleContext(error)) {
3894
+ return false;
3895
+ }
3896
+ throw error;
3897
+ }
3898
+ };
3899
+ var uiAction = (ctx, action) => {
3900
+ try {
3901
+ if (ctx.hasUI) {
3902
+ action(ctx.ui);
3903
+ }
3904
+ } catch (error) {
3905
+ if (!isStaleContext(error)) {
3906
+ throw error;
3907
+ }
3908
+ }
3909
+ };
3910
+
3887
3911
  // src/tools/render-common.ts
3888
3912
  import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
3889
3913
  import {
@@ -4071,14 +4095,14 @@ class ScoutStatusManager {
4071
4095
  }
4072
4096
  }
4073
4097
  update(ctx, token, event) {
4074
- if (this.#retired.has(token) || !ctx.hasUI) {
4098
+ if (this.#retired.has(token) || !uiAvailable(ctx)) {
4075
4099
  return;
4076
4100
  }
4077
4101
  this.#known.add(token);
4078
4102
  if (event.type === "call" || event.type === "chunk") {
4079
4103
  this.#active.add(token);
4080
4104
  if (this.showStatus) {
4081
- ctx.ui.setStatus("advisor-scout", "Scout curating…");
4105
+ uiAction(ctx, (ui) => ui.setStatus("advisor-scout", "Scout curating…"));
4082
4106
  }
4083
4107
  return;
4084
4108
  }
@@ -4088,10 +4112,10 @@ class ScoutStatusManager {
4088
4112
  this.#active.delete(token);
4089
4113
  this.#known.delete(token);
4090
4114
  this.#retired.add(token);
4091
- if (!(ctx.hasUI && this.showStatus)) {
4115
+ if (!(uiAvailable(ctx) && this.showStatus)) {
4092
4116
  return;
4093
4117
  }
4094
- ctx.ui.setStatus("advisor-scout", this.#active.size > 0 ? "Scout curating…" : undefined);
4118
+ uiAction(ctx, (ui) => ui.setStatus("advisor-scout", this.#active.size > 0 ? "Scout curating…" : undefined));
4095
4119
  }
4096
4120
  clear(ctx) {
4097
4121
  for (const token of this.#known) {
@@ -4099,8 +4123,8 @@ class ScoutStatusManager {
4099
4123
  }
4100
4124
  this.#known.clear();
4101
4125
  this.#active.clear();
4102
- if (ctx.hasUI && this.showStatus) {
4103
- ctx.ui.setStatus("advisor-scout", undefined);
4126
+ if (this.showStatus) {
4127
+ uiAction(ctx, (ui) => ui.setStatus("advisor-scout", undefined));
4104
4128
  }
4105
4129
  }
4106
4130
  }
@@ -4638,9 +4662,7 @@ var advisorSessionState = new AdvisorSessionState;
4638
4662
 
4639
4663
  // src/commands/runtime.ts
4640
4664
  var notify = (ctx, message, level) => {
4641
- if (ctx.hasUI) {
4642
- ctx.ui.notify(message, level);
4643
- }
4665
+ uiAction(ctx, (ui) => ui.notify(message, level));
4644
4666
  };
4645
4667
  var reportManualBudgetExhausted = (ctx) => {
4646
4668
  const message = "Advisor call budget exhausted for this session.";
@@ -4648,9 +4670,7 @@ var reportManualBudgetExhausted = (ctx) => {
4648
4670
  notifyHerdrAdvisorFailure("Advisor budget exhausted", message);
4649
4671
  };
4650
4672
  var requestManualRender = (ctx) => {
4651
- if (ctx.hasUI) {
4652
- ctx.ui.setStatus("advisor-manual", undefined);
4653
- }
4673
+ uiAction(ctx, (ui) => ui.setStatus("advisor-manual", undefined));
4654
4674
  };
4655
4675
 
4656
4676
  class CommandRuntime {
@@ -4706,9 +4726,7 @@ class CommandRuntime {
4706
4726
  }
4707
4727
  }
4708
4728
  updateAdvisorUsageStatus(ctx) {
4709
- if (ctx.hasUI) {
4710
- ctx.ui.setStatus("advisor-usage", getAdvisorSettings().showUsageFooter ? this.advisorSessionState.usageStatus() : undefined);
4711
- }
4729
+ uiAction(ctx, (ui) => ui.setStatus("advisor-usage", getAdvisorSettings().showUsageFooter ? this.advisorSessionState.usageStatus() : undefined));
4712
4730
  }
4713
4731
  }
4714
4732
  var createCommandRuntime = (pi, dependencies = {}) => new CommandRuntime(pi, dependencies);
@@ -5849,9 +5867,10 @@ var screeningSkipText = (outcome) => outcome.kind === "repeat" && outcome.reatta
5849
5867
 
5850
5868
  // src/commands/manual-consultation.ts
5851
5869
  var startManualConsultation = async (runtime, ctx, question, controller, scoutStatusToken, progress, gitContext) => {
5870
+ const isPrintMode = ctx.mode === "print";
5852
5871
  progress.phase = "preparing";
5853
5872
  runtime.requestManualRender(ctx);
5854
- if (ctx.hasUI) {
5873
+ if (uiAvailable(ctx)) {
5855
5874
  const timer = setInterval(() => {
5856
5875
  if (controller.signal.aborted || runtime.manualConsultations.get(controller) !== scoutStatusToken) {
5857
5876
  clearInterval(timer);
@@ -5916,7 +5935,7 @@ ${markdown}`,
5916
5935
  display: true
5917
5936
  }, {
5918
5937
  deliverAs: "steer",
5919
- triggerTurn: true
5938
+ triggerTurn: !isPrintMode
5920
5939
  });
5921
5940
  } catch (error) {
5922
5941
  if (controller.signal.aborted) {
@@ -5941,7 +5960,7 @@ ${markdown}`,
5941
5960
  text: `**Advisor consultation failed:** ${message}`
5942
5961
  },
5943
5962
  display: true
5944
- }, { deliverAs: "steer", triggerTurn: true });
5963
+ }, { deliverAs: "steer", triggerTurn: !isPrintMode });
5945
5964
  notify(ctx, `Advisor consultation failed: ${message}`, "error");
5946
5965
  notifyHerdrAdvisorFailure("Advisor consultation failed", message);
5947
5966
  } finally {
@@ -6035,7 +6054,15 @@ var registerManualCommand = (runtime) => {
6035
6054
  runtime.scoutStatus.register(scoutStatusToken);
6036
6055
  runtime.manualConsultations.set(controller, scoutStatusToken);
6037
6056
  runtime.pi.appendEntry?.("advisor-manual-call", { progressId, question });
6038
- startManualConsultation(runtime, ctx, question, controller, scoutStatusToken, progress, gitContext);
6057
+ if (ctx.mode === "print") {
6058
+ await startManualConsultation(runtime, ctx, question, controller, scoutStatusToken, progress, gitContext);
6059
+ return;
6060
+ }
6061
+ (async () => {
6062
+ try {
6063
+ await startManualConsultation(runtime, ctx, question, controller, scoutStatusToken, progress, gitContext);
6064
+ } catch {}
6065
+ })();
6039
6066
  }
6040
6067
  });
6041
6068
  };
@@ -7769,7 +7796,14 @@ var renderPartialAdvisorResult = (box, result, expanded, theme, context) => {
7769
7796
  const scoutActive = scout?.status === "calling" || scout?.status === "streaming";
7770
7797
  syncRenderPhase(context, scoutActive ? "scout" : "advisor");
7771
7798
  if (!context.state.timerId) {
7772
- context.state.timerId = setInterval(() => context.invalidate(), 80);
7799
+ context.state.timerId = setInterval(() => {
7800
+ try {
7801
+ context.invalidate();
7802
+ } catch {
7803
+ clearInterval(context.state.timerId);
7804
+ context.state.timerId = undefined;
7805
+ }
7806
+ }, 80);
7773
7807
  }
7774
7808
  if (scout) {
7775
7809
  renderScoutDetails(box, scout, expanded, theme);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-advisor-flow",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Advanced Executor/Advisor flow for Pi, fully configurable and extendable.",
5
5
  "keywords": [
6
6
  "advisor",
@@ -51,26 +51,26 @@
51
51
  "format:check": "oxfmt --check"
52
52
  },
53
53
  "devDependencies": {
54
- "@earendil-works/pi-ai": "0.87.1",
55
- "@earendil-works/pi-coding-agent": "0.87.1",
56
- "@earendil-works/pi-tui": "0.87.1",
57
- "@oxlint/plugins": "1.85.0",
54
+ "@earendil-works/pi-ai": "0.99.1",
55
+ "@earendil-works/pi-coding-agent": "0.99.1",
56
+ "@earendil-works/pi-tui": "0.99.1",
57
+ "@oxlint/plugins": "1.86.0",
58
58
  "@types/node": "^26.6.3",
59
59
  "@typesafe-ai/sdk": "^0.6.0",
60
60
  "bun-types": "1.4.2",
61
61
  "husky": "^9.1.7",
62
- "knip": "^6.38.0",
62
+ "knip": "6.39.0",
63
63
  "lint-staged": "^17.6.0",
64
- "oxfmt": "0.70.0",
65
- "oxlint": "1.85.0",
64
+ "oxfmt": "0.71.0",
65
+ "oxlint": "1.86.0",
66
66
  "typebox": "^1.3.34",
67
67
  "typescript": "^7.0.2",
68
- "ultracite": "7.12.0"
68
+ "ultracite": "7.12.2"
69
69
  },
70
70
  "peerDependencies": {
71
- "@earendil-works/pi-ai": "^0.87.1",
72
- "@earendil-works/pi-coding-agent": "^0.87.1",
73
- "@earendil-works/pi-tui": "^0.87.1",
71
+ "@earendil-works/pi-ai": "^0.87.1 || ^0.99.1",
72
+ "@earendil-works/pi-coding-agent": "^0.87.1 || ^0.99.1",
73
+ "@earendil-works/pi-tui": "^0.87.1 || ^0.99.1",
74
74
  "typebox": "^1.1.38"
75
75
  },
76
76
  "peerDependenciesMeta": {
@@ -92,10 +92,10 @@
92
92
  "safe-buffer": "npm:@socketregistry/safe-buffer@1.0.9"
93
93
  },
94
94
  "overrides": {
95
- "brace-expansion": "5.0.9",
95
+ "brace-expansion": "5.0.12",
96
96
  "is-unicode-supported": "npm:@socketregistry/is-unicode-supported@1.0.5",
97
97
  "safe-buffer": "npm:@socketregistry/safe-buffer@1.0.9",
98
- "undici": "8.9.0"
98
+ "undici": "8.11.2"
99
99
  },
100
100
  "lint-staged": {
101
101
  "*.{ts,tsx}": [
@@ -112,18 +112,38 @@ export const registerManualCommand = (runtime: CommandRuntime) => {
112
112
  runtime.scoutStatus.register(scoutStatusToken);
113
113
  runtime.manualConsultations.set(controller, scoutStatusToken);
114
114
  runtime.pi.appendEntry?.("advisor-manual-call", { progressId, question });
115
+ // Print mode tears the session down when the handler returns, so the
116
+ // consultation must complete inside the handler to keep its ctx valid.
117
+ if (ctx.mode === "print") {
118
+ await startManualConsultation(
119
+ runtime,
120
+ ctx,
121
+ question,
122
+ controller,
123
+ scoutStatusToken,
124
+ progress,
125
+ gitContext
126
+ );
127
+ return;
128
+ }
115
129
  // Intentional fire-and-forget: the consultation streams after the command
116
- // handler returns. void satisfies noFloatingPromises; noVoid is ignored here
117
- // because this Biome version offers no ignoreVoidAsExpression option.
118
- void startManualConsultation(
119
- runtime,
120
- ctx,
121
- question,
122
- controller,
123
- scoutStatusToken,
124
- progress,
125
- gitContext
126
- );
130
+ // handler returns. A rejected consultation must not escape as an
131
+ // unhandled rejection; its error paths already notify and record.
132
+ void (async () => {
133
+ try {
134
+ await startManualConsultation(
135
+ runtime,
136
+ ctx,
137
+ question,
138
+ controller,
139
+ scoutStatusToken,
140
+ progress,
141
+ gitContext
142
+ );
143
+ } catch {
144
+ // Consultation error paths already notify and record; nothing to add.
145
+ }
146
+ })();
127
147
  },
128
148
  });
129
149
  };
@@ -7,6 +7,7 @@ import { notifyHerdrAdvisorFailure } from "../herdr.ts";
7
7
  import { normalizeScreeningQuestion } from "../tools/jev-filter.ts";
8
8
  import { appendScoutLifecycleEntry } from "../tools/scout-status.ts";
9
9
  import type { ScoutToolDetails } from "../tools/types.ts";
10
+ import { uiAvailable } from "../ui-guard.ts";
10
11
  import { advisorUsageCost, snapshotAdvisorUsage } from "../usage.ts";
11
12
  import type { AdvisorUsageSnapshot } from "../usage.ts";
12
13
  import { notify } from "./runtime.ts";
@@ -28,9 +29,10 @@ export const startManualConsultation = async (
28
29
  progress: ManualAdvisorProgressState,
29
30
  gitContext?: GitContextLevel
30
31
  ) => {
32
+ const isPrintMode = ctx.mode === "print";
31
33
  progress.phase = "preparing";
32
34
  runtime.requestManualRender(ctx);
33
- if (ctx.hasUI) {
35
+ if (uiAvailable(ctx)) {
34
36
  const timer = setInterval(() => {
35
37
  if (
36
38
  controller.signal.aborted ||
@@ -118,8 +120,10 @@ export const startManualConsultation = async (
118
120
  {
119
121
  // Steer lets the current turn finish its active work; the Executor sees
120
122
  // the result before its next model call rather than being interrupted.
123
+ // Print mode has no follow-up turn; triggering one trips pi's turn_end
124
+ // entry resolution during teardown.
121
125
  deliverAs: "steer",
122
- triggerTurn: true,
126
+ triggerTurn: !isPrintMode,
123
127
  }
124
128
  );
125
129
  } catch (error) {
@@ -147,7 +151,7 @@ export const startManualConsultation = async (
147
151
  },
148
152
  display: true,
149
153
  },
150
- { deliverAs: "steer", triggerTurn: true }
154
+ { deliverAs: "steer", triggerTurn: !isPrintMode }
151
155
  );
152
156
  notify(ctx, `Advisor consultation failed: ${message}`, "error");
153
157
  notifyHerdrAdvisorFailure("Advisor consultation failed", message);
@@ -13,6 +13,7 @@ import {
13
13
  } from "../tools/model-access.ts";
14
14
  import { ScoutStatusManager } from "../tools/scout-status.ts";
15
15
  import { advisorSessionState as defaultAdvisorSessionState } from "../tools/session.ts";
16
+ import { uiAction } from "../ui-guard.ts";
16
17
  import type {
17
18
  CommandDependencies,
18
19
  CommandRuntime as CommandRuntimeContract,
@@ -25,9 +26,7 @@ export const notify = (
25
26
  message: string,
26
27
  level: "error" | "info" | "warning"
27
28
  ) => {
28
- if (ctx.hasUI) {
29
- ctx.ui.notify(message, level);
30
- }
29
+ uiAction(ctx, (ui) => ui.notify(message, level));
31
30
  };
32
31
 
33
32
  const reportManualBudgetExhausted = (ctx: ExtensionContext) => {
@@ -37,9 +36,7 @@ const reportManualBudgetExhausted = (ctx: ExtensionContext) => {
37
36
  };
38
37
 
39
38
  const requestManualRender = (ctx: ExtensionContext) => {
40
- if (ctx.hasUI) {
41
- ctx.ui.setStatus("advisor-manual", undefined);
42
- }
39
+ uiAction(ctx, (ui) => ui.setStatus("advisor-manual", undefined));
43
40
  };
44
41
 
45
42
  class CommandRuntime implements CommandRuntimeContract {
@@ -130,14 +127,14 @@ class CommandRuntime implements CommandRuntimeContract {
130
127
  }
131
128
 
132
129
  updateAdvisorUsageStatus(ctx: ExtensionContext) {
133
- if (ctx.hasUI) {
134
- ctx.ui.setStatus(
130
+ uiAction(ctx, (ui) =>
131
+ ui.setStatus(
135
132
  "advisor-usage",
136
133
  getAdvisorSettings().showUsageFooter
137
134
  ? this.advisorSessionState.usageStatus()
138
135
  : undefined
139
- );
140
- }
136
+ )
137
+ );
141
138
  }
142
139
  }
143
140
 
@@ -100,7 +100,15 @@ const renderPartialAdvisorResult = (
100
100
  scout?.status === "calling" || scout?.status === "streaming";
101
101
  syncRenderPhase(context, scoutActive ? "scout" : "advisor");
102
102
  if (!context.state.timerId) {
103
- context.state.timerId = setInterval(() => context.invalidate(), 80);
103
+ context.state.timerId = setInterval(() => {
104
+ try {
105
+ context.invalidate();
106
+ } catch {
107
+ // Renderer context is gone; stop repainting and drop the timer.
108
+ clearInterval(context.state.timerId);
109
+ context.state.timerId = undefined;
110
+ }
111
+ }, 80);
104
112
  }
105
113
  if (scout) {
106
114
  renderScoutDetails(box, scout, expanded, theme);
@@ -8,6 +8,7 @@ import type { Box } from "@earendil-works/pi-tui";
8
8
 
9
9
  import { executorRef, getAdvisorSettings } from "../config/state.ts";
10
10
  import type { ScoutLifecycleEvent } from "../scout.ts";
11
+ import { uiAction, uiAvailable } from "../ui-guard.ts";
11
12
  import { formatAdvisorUsage, snapshotAdvisorUsage } from "../usage.ts";
12
13
  import { renderThinkingMarkdown, SPINNER_FRAMES } from "./render-common.ts";
13
14
  import type { ScoutToolDetails } from "./types.ts";
@@ -91,14 +92,14 @@ export class ScoutStatusManager {
91
92
  }
92
93
 
93
94
  update(ctx: ExtensionContext, token: symbol, event: ScoutLifecycleEvent) {
94
- if (this.#retired.has(token) || !ctx.hasUI) {
95
+ if (this.#retired.has(token) || !uiAvailable(ctx)) {
95
96
  return;
96
97
  }
97
98
  this.#known.add(token);
98
99
  if (event.type === "call" || event.type === "chunk") {
99
100
  this.#active.add(token);
100
101
  if (this.showStatus) {
101
- ctx.ui.setStatus("advisor-scout", "Scout curating…");
102
+ uiAction(ctx, (ui) => ui.setStatus("advisor-scout", "Scout curating…"));
102
103
  }
103
104
  return;
104
105
  }
@@ -109,12 +110,14 @@ export class ScoutStatusManager {
109
110
  this.#active.delete(token);
110
111
  this.#known.delete(token);
111
112
  this.#retired.add(token);
112
- if (!(ctx.hasUI && this.showStatus)) {
113
+ if (!(uiAvailable(ctx) && this.showStatus)) {
113
114
  return;
114
115
  }
115
- ctx.ui.setStatus(
116
- "advisor-scout",
117
- this.#active.size > 0 ? "Scout curating…" : undefined
116
+ uiAction(ctx, (ui) =>
117
+ ui.setStatus(
118
+ "advisor-scout",
119
+ this.#active.size > 0 ? "Scout curating…" : undefined
120
+ )
118
121
  );
119
122
  }
120
123
 
@@ -124,8 +127,8 @@ export class ScoutStatusManager {
124
127
  }
125
128
  this.#known.clear();
126
129
  this.#active.clear();
127
- if (ctx.hasUI && this.showStatus) {
128
- ctx.ui.setStatus("advisor-scout", undefined);
130
+ if (this.showStatus) {
131
+ uiAction(ctx, (ui) => ui.setStatus("advisor-scout", undefined));
129
132
  }
130
133
  }
131
134
  }
@@ -0,0 +1,31 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ // Pi 0.99 asserts ctx liveness on UI access; post-session reads must no-op.
4
+ const isStaleContext = (error: unknown): error is Error =>
5
+ error instanceof Error && /stale/iu.test(error.message);
6
+
7
+ export const uiAvailable = (ctx: ExtensionContext): boolean => {
8
+ try {
9
+ return ctx.hasUI;
10
+ } catch (error) {
11
+ if (isStaleContext(error)) {
12
+ return false;
13
+ }
14
+ throw error;
15
+ }
16
+ };
17
+
18
+ export const uiAction = (
19
+ ctx: ExtensionContext,
20
+ action: (ui: ExtensionContext["ui"]) => void
21
+ ): void => {
22
+ try {
23
+ if (ctx.hasUI) {
24
+ action(ctx.ui);
25
+ }
26
+ } catch (error) {
27
+ if (!isStaleContext(error)) {
28
+ throw error;
29
+ }
30
+ }
31
+ };