pi-sessions 0.14.0 → 0.14.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.
@@ -76,6 +76,7 @@ export function createSessionReachableTool(deps: SessionReachableDeps): ToolDefi
76
76
  promptGuidelines: [
77
77
  "Before session_send_message, use session_reachable to find the target session id.",
78
78
  ],
79
+ annotations: { readOnlyHint: true, openWorldHint: false },
79
80
  parameters: subagentsAvailable ? SESSION_REACHABLE_PARAMS : SESSION_REACHABLE_USER_PARAMS,
80
81
  outputSchema: SESSION_REACHABLE_TOOL_DETAILS_SCHEMA,
81
82
  renderResult(result, options, theme, context) {
@@ -638,7 +638,7 @@ function extractMessageFileTouches(
638
638
  toolCalls = message.content.filter(isToolCallBlock);
639
639
  } else if (message.role === "toolResult" && message.nestedCalls) {
640
640
  // Calls made through ctx.executeTool(), e.g. from a codemode script, only appear here.
641
- toolCalls = message.nestedCalls.calls;
641
+ toolCalls = message.nestedCalls.calls.filter((call) => call.status !== "error");
642
642
  } else {
643
643
  return [];
644
644
  }
@@ -2,6 +2,7 @@ import type { ExecOptions, ExecResult } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  const TMUX_COMMAND = "tmux";
4
4
  const TMUX_TIMEOUT_MS = 15_000;
5
+ const CREATE_WINDOW_ATTEMPTS = 3;
5
6
  const SESSION_ID_OPTION = "@pi_session_id";
6
7
  const WINDOW_FORMAT = "#{window_id}\t#{window_name}\t#{@pi_session_id}";
7
8
 
@@ -103,15 +104,31 @@ export async function createTmuxWindow(
103
104
  }
104
105
  }
105
106
 
107
+ // The server can exit between any two commands below; the next attempt starts a fresh one.
106
108
  async function createWindow(
107
109
  executor: TmuxExecutor,
108
110
  options: CreateTmuxWindowOptions,
109
111
  ): Promise<TmuxWindow> {
112
+ for (let attempt = 1; ; attempt += 1) {
113
+ const outcome = await attemptCreateWindow(executor, options);
114
+ if ("window" in outcome) {
115
+ return outcome.window;
116
+ }
117
+ if (attempt === CREATE_WINDOW_ATTEMPTS || !isMissingSession(outcome.result)) {
118
+ throw tmuxError(outcome.action, outcome.result);
119
+ }
120
+ }
121
+ }
122
+
123
+ async function attemptCreateWindow(
124
+ executor: TmuxExecutor,
125
+ options: CreateTmuxWindowOptions,
126
+ ): Promise<{ window: TmuxWindow } | { action: string; result: ExecResult }> {
110
127
  const existing = (await listTmuxWindows(executor, options.tmuxSession)).find(
111
128
  (window) => window.piSessionId === options.piSessionId,
112
129
  );
113
130
  if (existing) {
114
- return existing;
131
+ return { window: existing };
115
132
  }
116
133
 
117
134
  const sessionExists = await hasTmuxSession(executor, options.tmuxSession);
@@ -149,7 +166,7 @@ async function createWindow(
149
166
  timeout: TMUX_TIMEOUT_MS,
150
167
  });
151
168
  if (created.code !== 0) {
152
- throw tmuxError("create window", created);
169
+ return { action: "create window", result: created };
153
170
  }
154
171
 
155
172
  const windowId = created.stdout.trim();
@@ -165,10 +182,10 @@ async function createWindow(
165
182
  await executor.exec(TMUX_COMMAND, ["kill-window", "-t", windowId], {
166
183
  timeout: TMUX_TIMEOUT_MS,
167
184
  });
168
- throw tmuxError("stamp window", stamped);
185
+ return { action: "stamp window", result: stamped };
169
186
  }
170
187
 
171
- return { windowId, name: options.name, piSessionId: options.piSessionId };
188
+ return { window: { windowId, name: options.name, piSessionId: options.piSessionId } };
172
189
  }
173
190
 
174
191
  export async function killTmuxWindow(
@@ -215,8 +232,10 @@ function parseWindow(line: string): TmuxWindow | undefined {
215
232
  return { windowId, name, piSessionId };
216
233
  }
217
234
 
235
+ // A server that is shutting down answers in-flight commands with `no current target` (its last
236
+ // session is gone but it has not exited yet) or drops them with `server exited unexpectedly`.
218
237
  function isMissingSession(result: ExecResult): boolean {
219
- return /can't find session|no server running|failed to connect to server|error connecting to .*no such file/i.test(
238
+ return /can't find session|no current target|no server running|server exited unexpectedly|failed to connect to server|error connecting to .*no such file/i.test(
220
239
  `${result.stderr}\n${result.stdout}`,
221
240
  );
222
241
  }
@@ -1,4 +1,8 @@
1
- import type { ExtensionAPI, SessionTreeNode } from "@earendil-works/pi-coding-agent";
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionUIContext,
4
+ SessionTreeNode,
5
+ } from "@earendil-works/pi-coding-agent";
2
6
  import type { HandoffLaunchTarget } from "../session-handoff/launch-target.ts";
3
7
  import type {
4
8
  MessagingHandle,
@@ -23,7 +27,7 @@ import {
23
27
  SUBAGENT_REPORT_MESSAGE_SCHEMA,
24
28
  SUBAGENT_REPORT_RECEIVED_CUSTOM_TYPE,
25
29
  } from "./ledger.ts";
26
- import { openReconcileSession, SubagentReconciler } from "./reconcile.ts";
30
+ import { openReconcileSession, type ReconcileResult, SubagentReconciler } from "./reconcile.ts";
27
31
  import {
28
32
  buildIncomingSubagentReport,
29
33
  createSubmitTaskReportTool,
@@ -51,6 +55,7 @@ interface ParentSessionState extends SubagentParentSession {
51
55
  getTree(): SessionTreeNode[];
52
56
  hasPendingMessages(): boolean;
53
57
  shutdown(): void;
58
+ reportError(error: unknown): void;
54
59
  }
55
60
 
56
61
  interface CurrentSubagentSession {
@@ -178,9 +183,15 @@ export function installSubagents(
178
183
  if (!session || session.parent.sessionId !== ctx.sessionManager.getSessionId()) {
179
184
  return;
180
185
  }
181
- const reconciliation = hasSubagentLaunchEntries(session.parent.getBranch())
182
- ? await reconciler.reconcile()
183
- : undefined;
186
+ let reconciliation: ReconcileResult | undefined;
187
+ try {
188
+ reconciliation = hasSubagentLaunchEntries(session.parent.getBranch())
189
+ ? await reconciler.reconcile()
190
+ : undefined;
191
+ } catch (error) {
192
+ notifySubagentFailure(ctx.ui, error);
193
+ return;
194
+ }
184
195
  if (session.child) {
185
196
  await contextLimit.compactIfOverLimit(ctx);
186
197
  await settledChildLifecycle.settle(session.parent, session.child, reconciliation);
@@ -233,11 +244,15 @@ export function installSubagents(
233
244
  );
234
245
  };
235
246
 
236
- pi.on("session_tree", async () => {
247
+ pi.on("session_tree", async (_event, ctx) => {
237
248
  settledChildLifecycle.cancel();
238
249
  refreshChildSession();
239
250
  registerMessagingTools();
240
- await reconciler.reconcileAndRestoreSuspended();
251
+ try {
252
+ await reconciler.reconcileAndRestoreSuspended();
253
+ } catch (error) {
254
+ notifySubagentFailure(ctx.ui, error);
255
+ }
241
256
  });
242
257
 
243
258
  const handle: SubagentsHandle = {
@@ -283,6 +298,7 @@ export function installSubagents(
283
298
  epoch,
284
299
  launchState: { sessionId, depth: identity?.depth ?? 0, epoch },
285
300
  tmuxInstalled: await isTmuxInstalled(pi, ctx.cwd),
301
+ reportError: (error) => notifySubagentFailure(ctx.ui, error),
286
302
  };
287
303
  reconciler.beginSession();
288
304
  current = { parent };
@@ -290,16 +306,24 @@ export function installSubagents(
290
306
  if (identity) {
291
307
  registerMessagingTools();
292
308
  }
293
- if (_event.reason === "reload") {
294
- await reconciler.reconcile();
295
- } else {
296
- await reconciler.reconcileAndRestoreSuspended();
309
+ try {
310
+ if (_event.reason === "reload") {
311
+ await reconciler.reconcile();
312
+ } else {
313
+ await reconciler.reconcileAndRestoreSuspended();
314
+ }
315
+ } catch (error) {
316
+ notifySubagentFailure(ctx.ui, error);
297
317
  }
298
318
  },
299
- async onSessionShutdown(event) {
319
+ async onSessionShutdown(event, ctx) {
300
320
  settledChildLifecycle.cancel();
301
321
  if (current && event.reason !== "reload") {
302
- await reconciler.suspendForShutdown();
322
+ try {
323
+ await reconciler.suspendForShutdown();
324
+ } catch (error) {
325
+ notifySubagentFailure(ctx.ui, error);
326
+ }
303
327
  }
304
328
  epoch += 1;
305
329
  current = undefined;
@@ -307,3 +331,12 @@ export function installSubagents(
307
331
  };
308
332
  return handle;
309
333
  }
334
+
335
+ // Lifecycle hooks and timers run outside any tool call. Pi renders a thrown error's full stack
336
+ // over the conversation, so report only the message.
337
+ function notifySubagentFailure(ui: ExtensionUIContext, error: unknown): void {
338
+ ui.notify(
339
+ `Subagent lifecycle failed: ${error instanceof Error ? error.message : String(error)}`,
340
+ "error",
341
+ );
342
+ }
@@ -25,6 +25,7 @@ export interface SettledChildParentSession {
25
25
  hasPendingMessages(): boolean;
26
26
  isIdle(): boolean;
27
27
  shutdown(): void;
28
+ reportError(error: unknown): void;
28
29
  }
29
30
 
30
31
  export interface SettledChildLifecycle {
@@ -72,7 +73,11 @@ export function createSettledChildLifecycle(
72
73
  const schedule = (delayMs: number): void => {
73
74
  timer = setTimeout(() => {
74
75
  timer = undefined;
75
- void advance();
76
+ advance().catch((error: unknown) => {
77
+ if (isCurrentSession(parent.epoch)) {
78
+ parent.reportError(error);
79
+ }
80
+ });
76
81
  }, delayMs);
77
82
  };
78
83
  const advance = async (): Promise<void> => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sessions",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "type": "module",
5
5
  "description": "Pi session search, ask, handoff, messaging, auto-titling, and indexing tools",
6
6
  "license": "MIT",
@@ -38,15 +38,15 @@
38
38
  },
39
39
  "devDependencies": {
40
40
  "@biomejs/biome": "^2.5.10",
41
- "@earendil-works/pi-agent-core": "^0.99.1",
42
- "@earendil-works/pi-ai": "^0.99.1",
43
- "@earendil-works/pi-coding-agent": "^0.99.1",
44
- "@earendil-works/pi-tui": "^0.99.1",
41
+ "@earendil-works/pi-agent-core": "^1.0.2",
42
+ "@earendil-works/pi-ai": "^1.0.2",
43
+ "@earendil-works/pi-coding-agent": "^1.0.2",
44
+ "@earendil-works/pi-tui": "^1.0.2",
45
45
  "@types/node": "^24.13.3",
46
46
  "strip-ansi": "^7.2.0",
47
47
  "typebox": "1.3.27",
48
48
  "typescript": "^7.0.0",
49
- "vitest": "^4.1.11"
49
+ "vitest": "^5.0.0"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "@earendil-works/pi-agent-core": "*",