@docyrus/docyrus 0.0.66 → 0.0.67

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": "@docyrus/docyrus",
3
- "version": "0.0.66",
3
+ "version": "0.0.67",
4
4
  "private": false,
5
5
  "description": "Docyrus API CLI",
6
6
  "main": "./main.js",
@@ -13,6 +13,7 @@ import { DynamicBorder, getAgentDir as piGetAgentDir, loadProjectContextFiles as
13
13
  import { Container, Key, Text, matchesKey, type Component, type TUI } from "@mariozechner/pi-tui";
14
14
  import os from "node:os";
15
15
  import path from "node:path";
16
+ import { isStaleExtensionError } from "../shared/extensionLifecycle";
16
17
 
17
18
  function formatUsd(cost: number): string {
18
19
  if (!Number.isFinite(cost) || cost <= 0) {return "$0.00";}
@@ -390,22 +391,27 @@ export default function contextExtension(pi: ExtensionAPI) {
390
391
  };
391
392
 
392
393
  pi.on("tool_result", (event: ToolResultEvent, ctx: ExtensionContext) => {
393
- // Only count successful reads.
394
- if ((event as any).toolName !== "read") {return;}
395
- if ((event as any).isError) {return;}
396
-
397
- const input = (event as any).input as { path?: unknown } | undefined;
398
- const p = typeof input?.path === "string" ? input.path : "";
399
- if (!p) {return;}
400
-
401
- ensureCaches(ctx);
402
- const abs = normalizeReadPath(p, ctx.cwd);
403
- const skillName = matchSkillForPath(abs);
404
- if (!skillName) {return;}
405
-
406
- if (!cachedLoadedSkills.has(skillName)) {
407
- cachedLoadedSkills.add(skillName);
408
- pi.appendEntry<SkillLoadedEntryData>(SKILL_LOADED_ENTRY, { name: skillName, path: abs });
394
+ try {
395
+ // Only count successful reads.
396
+ if ((event as any).toolName !== "read") {return;}
397
+ if ((event as any).isError) {return;}
398
+
399
+ const input = (event as any).input as { path?: unknown } | undefined;
400
+ const p = typeof input?.path === "string" ? input.path : "";
401
+ if (!p) {return;}
402
+
403
+ ensureCaches(ctx);
404
+ const abs = normalizeReadPath(p, ctx.cwd);
405
+ const skillName = matchSkillForPath(abs);
406
+ if (!skillName) {return;}
407
+
408
+ if (!cachedLoadedSkills.has(skillName)) {
409
+ cachedLoadedSkills.add(skillName);
410
+ pi.appendEntry<SkillLoadedEntryData>(SKILL_LOADED_ENTRY, { name: skillName, path: abs });
411
+ }
412
+ }
413
+ catch (error: unknown) {
414
+ if (!isStaleExtensionError(error)) {throw error;}
409
415
  }
410
416
  });
411
417
 
@@ -52,6 +52,7 @@ import { promises as fs } from "node:fs";
52
52
  import * as net from "node:net";
53
53
  import * as os from "node:os";
54
54
  import * as path from "node:path";
55
+ import { isStaleExtensionError } from "../shared/extensionLifecycle";
55
56
 
56
57
  const CONTROL_FLAG = "session-control";
57
58
  const CONTROL_TARGET_FLAG = "control-session";
@@ -1080,42 +1081,57 @@ export default function(pi: ExtensionAPI) {
1080
1081
  };
1081
1082
 
1082
1083
  pi.on("session_start", async(_event, ctx) => {
1083
- await refreshServer(ctx);
1084
- if (!cliSendHandled) {
1085
- cliSendHandled = true;
1086
- await maybeHandleStartupControlSend(pi, ctx);
1084
+ try {
1085
+ await refreshServer(ctx);
1086
+ if (!cliSendHandled) {
1087
+ cliSendHandled = true;
1088
+ await maybeHandleStartupControlSend(pi, ctx);
1089
+ }
1090
+ }
1091
+ catch (error: unknown) {
1092
+ if (!isStaleExtensionError(error)) {throw error;}
1087
1093
  }
1088
1094
  });
1089
1095
 
1090
1096
  pi.on("session_shutdown", async() => {
1091
- if (state.aliasTimer) {
1092
- clearInterval(state.aliasTimer);
1093
- state.aliasTimer = null;
1097
+ try {
1098
+ if (state.aliasTimer) {
1099
+ clearInterval(state.aliasTimer);
1100
+ state.aliasTimer = null;
1101
+ }
1102
+ updateStatus(state.context, false);
1103
+ updateSessionEnv(state.context, false);
1104
+ await stopControlServer(state);
1105
+ }
1106
+ catch (error: unknown) {
1107
+ if (!isStaleExtensionError(error)) {throw error;}
1094
1108
  }
1095
- updateStatus(state.context, false);
1096
- updateSessionEnv(state.context, false);
1097
- await stopControlServer(state);
1098
1109
  });
1099
1110
 
1100
1111
  // Fire turn_end events to subscribers
1101
1112
  pi.on("turn_end", (event: TurnEndEvent, ctx: ExtensionContext) => {
1102
- if (state.turnEndSubscriptions.length === 0) {return;}
1103
-
1104
- void syncAlias(state, ctx);
1105
- const lastMessage = getLastAssistantMessage(ctx);
1106
- const eventData = { message: lastMessage, turnIndex: event.turnIndex };
1107
-
1108
- // Fire to all subscribers (one-shot)
1109
- const subscriptions = [...state.turnEndSubscriptions];
1110
- state.turnEndSubscriptions = [];
1111
-
1112
- for (const sub of subscriptions) {
1113
- writeEvent(sub.socket, {
1114
- type: "event",
1115
- event: "turn_end",
1116
- data: eventData,
1117
- subscriptionId: sub.subscriptionId,
1118
- });
1113
+ try {
1114
+ if (state.turnEndSubscriptions.length === 0) {return;}
1115
+
1116
+ void syncAlias(state, ctx);
1117
+ const lastMessage = getLastAssistantMessage(ctx);
1118
+ const eventData = { message: lastMessage, turnIndex: event.turnIndex };
1119
+
1120
+ // Fire to all subscribers (one-shot)
1121
+ const subscriptions = [...state.turnEndSubscriptions];
1122
+ state.turnEndSubscriptions = [];
1123
+
1124
+ for (const sub of subscriptions) {
1125
+ writeEvent(sub.socket, {
1126
+ type: "event",
1127
+ event: "turn_end",
1128
+ data: eventData,
1129
+ subscriptionId: sub.subscriptionId,
1130
+ });
1131
+ }
1132
+ }
1133
+ catch (error: unknown) {
1134
+ if (!isStaleExtensionError(error)) {throw error;}
1119
1135
  }
1120
1136
  });
1121
1137
  }
@@ -10,6 +10,7 @@ import type {
10
10
  } from "@mariozechner/pi-coding-agent";
11
11
  import { getMarkdownTheme, keyHint } from "@mariozechner/pi-coding-agent";
12
12
  import { Box, Markdown, Text } from "@mariozechner/pi-tui";
13
+ import { isStaleExtensionError } from "../shared/extensionLifecycle";
13
14
 
14
15
  const KNOWLEDGE_STATE_TYPE = "knowledge-session";
15
16
  const KNOWLEDGE_WIDGET_KEY = "knowledge";
@@ -458,35 +459,60 @@ export default function(pi: ExtensionAPI) {
458
459
  });
459
460
 
460
461
  pi.on("tool_result", (event: ToolResultEvent, ctx: ExtensionContext) => {
461
- trackToolResult(pi, ctx, event);
462
+ try {
463
+ trackToolResult(pi, ctx, event);
464
+ }
465
+ catch (error: unknown) {
466
+ if (!isStaleExtensionError(error)) {throw error;}
467
+ }
462
468
  });
463
469
 
464
470
  pi.on("session_start", async(_event, ctx) => {
465
- await refreshKnowledgeState(pi, ctx, {
466
- runCheck: false,
467
- allowReminder: true,
468
- });
471
+ try {
472
+ await refreshKnowledgeState(pi, ctx, {
473
+ runCheck: false,
474
+ allowReminder: true,
475
+ });
476
+ }
477
+ catch (error: unknown) {
478
+ if (!isStaleExtensionError(error)) {throw error;}
479
+ }
469
480
  });
470
481
 
471
482
  pi.on("turn_end", async(_event: TurnEndEvent, ctx) => {
472
- await refreshKnowledgeState(pi, ctx, {
473
- runCheck: false,
474
- allowReminder: true,
475
- });
483
+ try {
484
+ await refreshKnowledgeState(pi, ctx, {
485
+ runCheck: false,
486
+ allowReminder: true,
487
+ });
488
+ }
489
+ catch (error: unknown) {
490
+ if (!isStaleExtensionError(error)) {throw error;}
491
+ }
476
492
  });
477
493
 
478
494
  pi.on("agent_end", async(_event, ctx) => {
479
- const state = await refreshKnowledgeState(pi, ctx, {
480
- runCheck: true,
481
- allowReminder: true,
482
- });
483
- if (ctx.hasUI && state.commitWouldFail) {
484
- ctx.ui.notify("Knowledge drift is severe or integrity checks failed. A commit would likely be blocked until knowledge is updated.", "warning");
495
+ try {
496
+ const state = await refreshKnowledgeState(pi, ctx, {
497
+ runCheck: true,
498
+ allowReminder: true,
499
+ });
500
+ if (ctx.hasUI && state.commitWouldFail) {
501
+ ctx.ui.notify("Knowledge drift is severe or integrity checks failed. A commit would likely be blocked until knowledge is updated.", "warning");
502
+ }
503
+ }
504
+ catch (error: unknown) {
505
+ if (!isStaleExtensionError(error)) {throw error;}
485
506
  }
486
507
  });
487
508
 
488
509
  pi.on("session_shutdown", async(_event, ctx) => {
489
- updateKnowledgeWidget(ctx, defaultKnowledgeState());
510
+ try {
511
+ updateKnowledgeWidget(ctx, defaultKnowledgeState());
512
+ }
513
+ catch (error: unknown) {
514
+ if (!isStaleExtensionError(error)) {throw error;}
515
+ }
490
516
  });
491
517
 
492
518
  pi.registerMessageRenderer("knowledge-reminder", (message, { expanded }, theme) => {
@@ -12,6 +12,7 @@ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-age
12
12
  import { compact } from "@mariozechner/pi-coding-agent";
13
13
  import { Container, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
14
14
  import { DynamicBorder } from "@mariozechner/pi-coding-agent";
15
+ import { isStaleExtensionError } from "../shared/extensionLifecycle";
15
16
 
16
17
  type LoopMode = "tests" | "custom" | "self";
17
18
 
@@ -385,39 +386,50 @@ export default function loopExtension(pi: ExtensionAPI): void {
385
386
  });
386
387
 
387
388
  pi.on("agent_end", async(event, ctx) => {
388
- if (!loopState.active) {return;}
389
-
390
- if (ctx.hasUI && wasLastAssistantAborted(event.messages)) {
391
- const confirm = await ctx.ui.confirm(
392
- "Break active loop?",
393
- "Operation aborted. Break out of the loop?",
394
- );
395
- if (confirm) {
396
- breakLoop(ctx);
397
- return;
389
+ try {
390
+ if (!loopState.active) {return;}
391
+
392
+ if (ctx.hasUI && wasLastAssistantAborted(event.messages)) {
393
+ const confirm = await ctx.ui.confirm(
394
+ "Break active loop?",
395
+ "Operation aborted. Break out of the loop?",
396
+ );
397
+ if (confirm) {
398
+ breakLoop(ctx);
399
+ return;
400
+ }
398
401
  }
399
- }
400
402
 
401
- triggerLoopPrompt(ctx);
403
+ triggerLoopPrompt(ctx);
404
+ }
405
+ catch (error: unknown) {
406
+ if (!isStaleExtensionError(error)) {throw error;}
407
+ }
402
408
  });
403
409
 
404
410
  pi.on("session_before_compact", async(event, ctx) => {
405
- if (!loopState.active || !loopState.mode || !ctx.model) {return;}
406
- const apiKey = await ctx.modelRegistry.getApiKey(ctx.model);
407
- if (!apiKey) {return;}
408
-
409
- const instructionParts = [event.customInstructions, getCompactionInstructions(loopState.mode, loopState.condition)]
410
- .filter(Boolean)
411
- .join("\n\n");
412
-
413
411
  try {
414
- const compaction = await compact(event.preparation, ctx.model, apiKey, instructionParts, event.signal);
415
- return { compaction };
416
- } catch (error) {
417
- if (ctx.hasUI) {
418
- const message = error instanceof Error ? error.message : String(error);
419
- ctx.ui.notify(`Loop compaction failed: ${message}`, "warning");
412
+ if (!loopState.active || !loopState.mode || !ctx.model) {return;}
413
+ const apiKey = await ctx.modelRegistry.getApiKey(ctx.model);
414
+ if (!apiKey) {return;}
415
+
416
+ const instructionParts = [event.customInstructions, getCompactionInstructions(loopState.mode, loopState.condition)]
417
+ .filter(Boolean)
418
+ .join("\n\n");
419
+
420
+ try {
421
+ const compaction = await compact(event.preparation, ctx.model, apiKey, instructionParts, event.signal);
422
+ return { compaction };
423
+ } catch (error) {
424
+ if (ctx.hasUI) {
425
+ const message = error instanceof Error ? error.message : String(error);
426
+ ctx.ui.notify(`Loop compaction failed: ${message}`, "warning");
427
+ }
428
+ return;
420
429
  }
430
+ }
431
+ catch (error: unknown) {
432
+ if (!isStaleExtensionError(error)) {throw error;}
421
433
  return;
422
434
  }
423
435
  });
@@ -440,6 +452,11 @@ export default function loopExtension(pi: ExtensionAPI): void {
440
452
  }
441
453
 
442
454
  pi.on("session_start", async(_event, ctx) => {
443
- await restoreLoopState(ctx);
455
+ try {
456
+ await restoreLoopState(ctx);
457
+ }
458
+ catch (error: unknown) {
459
+ if (!isStaleExtensionError(error)) {throw error;}
460
+ }
444
461
  });
445
462
  }
@@ -21,6 +21,7 @@ import {
21
21
  type IAskUserQuestion,
22
22
  type IAskUserResponse,
23
23
  } from "../shared/askUserProtocol";
24
+ import { isStaleExtensionError } from "../shared/extensionLifecycle";
24
25
 
25
26
  const PLAN_OUTPUT_ROOT_SEGMENTS = [".docyrus", "plans"] as const;
26
27
  const PLAN_STATE_TYPE = "plan-session";
@@ -1360,71 +1361,109 @@ export default function planExtension(pi: ExtensionAPI) {
1360
1361
  });
1361
1362
 
1362
1363
  pi.on("before_agent_start", (event, ctx) => {
1363
- const planState = getPlanState(ctx);
1364
- const readOnlyState = getReadOnlyState(ctx);
1365
- const overlays: string[] = [];
1364
+ try {
1365
+ const planState = getPlanState(ctx);
1366
+ const readOnlyState = getReadOnlyState(ctx);
1367
+ const overlays: string[] = [];
1366
1368
 
1367
- if (planState?.active) {
1368
- overlays.push(buildPlanPromptOverlay(planState));
1369
- }
1369
+ if (planState?.active) {
1370
+ overlays.push(buildPlanPromptOverlay(planState));
1371
+ }
1370
1372
 
1371
- if (readOnlyState?.active) {
1372
- overlays.push(READONLY_MODE_SYSTEM_PROMPT);
1373
- }
1373
+ if (readOnlyState?.active) {
1374
+ overlays.push(READONLY_MODE_SYSTEM_PROMPT);
1375
+ }
1374
1376
 
1375
- if (overlays.length === 0) {
1377
+ if (overlays.length === 0) {
1378
+ return;
1379
+ }
1380
+
1381
+ return {
1382
+ systemPrompt: [event.systemPrompt, ...overlays].filter(Boolean).join("\n\n"),
1383
+ };
1384
+ }
1385
+ catch (error: unknown) {
1386
+ if (!isStaleExtensionError(error)) {throw error;}
1376
1387
  return;
1377
1388
  }
1378
-
1379
- return {
1380
- systemPrompt: [event.systemPrompt, ...overlays].filter(Boolean).join("\n\n"),
1381
- };
1382
1389
  });
1383
1390
 
1384
1391
  pi.on("tool_call", (event, ctx) => {
1385
- return handleToolCallDuringPlan(event, ctx) ?? handleToolCallDuringReadOnly(event, ctx);
1392
+ try {
1393
+ return handleToolCallDuringPlan(event, ctx) ?? handleToolCallDuringReadOnly(event, ctx);
1394
+ }
1395
+ catch (error: unknown) {
1396
+ if (!isStaleExtensionError(error)) {throw error;}
1397
+ return;
1398
+ }
1386
1399
  });
1387
1400
 
1388
1401
  pi.on("user_bash", (event, ctx) => {
1389
- return handleUserBashDuringPlan(event, ctx) ?? handleUserBashDuringReadOnly(event, ctx);
1402
+ try {
1403
+ return handleUserBashDuringPlan(event, ctx) ?? handleUserBashDuringReadOnly(event, ctx);
1404
+ }
1405
+ catch (error: unknown) {
1406
+ if (!isStaleExtensionError(error)) {throw error;}
1407
+ return;
1408
+ }
1390
1409
  });
1391
1410
 
1392
1411
  pi.on("agent_end", async(event, ctx) => {
1393
- const state = getPlanState(ctx);
1394
- if (state?.active) {
1395
- const text = extractLastAssistantText(event.messages ?? []);
1396
- const askUserRequest = text ? parseAskUserRequestFromText(text) : undefined;
1397
- if (askUserRequest && ctx.hasUI) {
1398
- const response = await collectAskUserResponse(ctx, askUserRequest);
1399
- if (response) {
1400
- pi.sendUserMessage(formatAskUserResponsePrompt(response));
1412
+ try {
1413
+ const state = getPlanState(ctx);
1414
+ if (state?.active) {
1415
+ const text = extractLastAssistantText(event.messages ?? []);
1416
+ const askUserRequest = text ? parseAskUserRequestFromText(text) : undefined;
1417
+ if (askUserRequest && ctx.hasUI) {
1418
+ const response = await collectAskUserResponse(ctx, askUserRequest);
1419
+ if (response) {
1420
+ pi.sendUserMessage(formatAskUserResponsePrompt(response));
1421
+ }
1422
+ return;
1401
1423
  }
1402
- return;
1403
1424
  }
1404
- }
1405
1425
 
1406
- await writePlanArtifactFromEvent(event, ctx);
1407
- await writeArchitectArtifactFromEvent(event, ctx);
1426
+ await writePlanArtifactFromEvent(event, ctx);
1427
+ await writeArchitectArtifactFromEvent(event, ctx);
1428
+ }
1429
+ catch (error: unknown) {
1430
+ if (!isStaleExtensionError(error)) {throw error;}
1431
+ }
1408
1432
  });
1409
1433
 
1410
1434
  pi.on("session_start", async(_event, ctx) => {
1411
- await syncPlanState(pi, ctx);
1412
- const readOnlyState = getReadOnlyState(ctx);
1413
- currentReadOnlyState = readOnlyState;
1414
- setReadOnlyWidget(ctx, readOnlyState);
1435
+ try {
1436
+ await syncPlanState(pi, ctx);
1437
+ const readOnlyState = getReadOnlyState(ctx);
1438
+ currentReadOnlyState = readOnlyState;
1439
+ setReadOnlyWidget(ctx, readOnlyState);
1440
+ }
1441
+ catch (error: unknown) {
1442
+ if (!isStaleExtensionError(error)) {throw error;}
1443
+ }
1415
1444
  });
1416
1445
 
1417
1446
  pi.on("session_tree", async(_event, ctx) => {
1418
- await syncPlanState(pi, ctx);
1419
- const readOnlyState = getReadOnlyState(ctx);
1420
- currentReadOnlyState = readOnlyState;
1421
- setReadOnlyWidget(ctx, readOnlyState);
1447
+ try {
1448
+ await syncPlanState(pi, ctx);
1449
+ const readOnlyState = getReadOnlyState(ctx);
1450
+ currentReadOnlyState = readOnlyState;
1451
+ setReadOnlyWidget(ctx, readOnlyState);
1452
+ }
1453
+ catch (error: unknown) {
1454
+ if (!isStaleExtensionError(error)) {throw error;}
1455
+ }
1422
1456
  });
1423
1457
 
1424
1458
  pi.on("session_shutdown", async(_event, ctx) => {
1425
- currentPlanState = undefined;
1426
- setPlanWidget(ctx, undefined);
1427
- currentReadOnlyState = undefined;
1428
- setReadOnlyWidget(ctx, undefined);
1459
+ try {
1460
+ currentPlanState = undefined;
1461
+ setPlanWidget(ctx, undefined);
1462
+ currentReadOnlyState = undefined;
1463
+ setReadOnlyWidget(ctx, undefined);
1464
+ }
1465
+ catch (error: unknown) {
1466
+ if (!isStaleExtensionError(error)) {throw error;}
1467
+ }
1429
1468
  });
1430
1469
  }
@@ -1,5 +1,6 @@
1
1
  import { DynamicBorder, type ExtensionAPI, type ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import { Container, Text } from "@mariozechner/pi-tui";
3
+ import { isStaleExtensionError } from "../shared/extensionLifecycle";
3
4
 
4
5
  const PR_PROMPT_PATTERN = /^\s*You are given one or more GitHub PR URLs:\s*(\S+)/im;
5
6
  const ISSUE_PROMPT_PATTERN = /^\s*Analyze GitHub issue\(s\):\s*(\S+)/im;
@@ -92,20 +93,30 @@ export default function promptUrlWidgetExtension(pi: ExtensionAPI) {
92
93
  };
93
94
 
94
95
  pi.on("before_agent_start", async(event, ctx) => {
95
- if (!ctx.hasUI) {return;}
96
- const match = extractPromptMatch(event.prompt);
97
- if (!match) {
98
- return;
96
+ try {
97
+ if (!ctx.hasUI) {return;}
98
+ const match = extractPromptMatch(event.prompt);
99
+ if (!match) {
100
+ return;
101
+ }
102
+
103
+ setWidget(ctx, match);
104
+ applySessionName(ctx, match);
105
+ void fetchGhMetadata(pi, match.kind, match.url).then((meta) => {
106
+ try {
107
+ const title = meta?.title?.trim();
108
+ const authorText = formatAuthor(meta?.author);
109
+ setWidget(ctx, match, title, authorText);
110
+ applySessionName(ctx, match, title);
111
+ }
112
+ catch (error: unknown) {
113
+ if (!isStaleExtensionError(error)) {throw error;}
114
+ }
115
+ });
116
+ }
117
+ catch (error: unknown) {
118
+ if (!isStaleExtensionError(error)) {throw error;}
99
119
  }
100
-
101
- setWidget(ctx, match);
102
- applySessionName(ctx, match);
103
- void fetchGhMetadata(pi, match.kind, match.url).then((meta) => {
104
- const title = meta?.title?.trim();
105
- const authorText = formatAuthor(meta?.author);
106
- setWidget(ctx, match, title, authorText);
107
- applySessionName(ctx, match, title);
108
- });
109
120
  });
110
121
 
111
122
  const getUserText = (content: string | { type: string; text?: string }[] | undefined): string => {
@@ -141,14 +152,24 @@ export default function promptUrlWidgetExtension(pi: ExtensionAPI) {
141
152
  setWidget(ctx, match);
142
153
  applySessionName(ctx, match);
143
154
  void fetchGhMetadata(pi, match.kind, match.url).then((meta) => {
144
- const title = meta?.title?.trim();
145
- const authorText = formatAuthor(meta?.author);
146
- setWidget(ctx, match, title, authorText);
147
- applySessionName(ctx, match, title);
155
+ try {
156
+ const title = meta?.title?.trim();
157
+ const authorText = formatAuthor(meta?.author);
158
+ setWidget(ctx, match, title, authorText);
159
+ applySessionName(ctx, match, title);
160
+ }
161
+ catch (error: unknown) {
162
+ if (!isStaleExtensionError(error)) {throw error;}
163
+ }
148
164
  });
149
165
  };
150
166
 
151
167
  pi.on("session_start", async(_event, ctx) => {
152
- rebuildFromSession(ctx);
168
+ try {
169
+ rebuildFromSession(ctx);
170
+ }
171
+ catch (error: unknown) {
172
+ if (!isStaleExtensionError(error)) {throw error;}
173
+ }
153
174
  });
154
175
  }
@@ -42,6 +42,7 @@ import {
42
42
  } from "@mariozechner/pi-tui";
43
43
  import path from "node:path";
44
44
  import { promises as fs } from "node:fs";
45
+ import { isStaleExtensionError } from "../shared/extensionLifecycle";
45
46
 
46
47
  // State to track fresh session review (where we branched from).
47
48
  // Module-level state means only one review can be active at a time.
@@ -935,11 +936,21 @@ export default function reviewExtension(pi: ExtensionAPI) {
935
936
  }
936
937
 
937
938
  pi.on("session_start", (_event, ctx) => {
938
- applyAllReviewState(ctx);
939
+ try {
940
+ applyAllReviewState(ctx);
941
+ }
942
+ catch (error: unknown) {
943
+ if (!isStaleExtensionError(error)) {throw error;}
944
+ }
939
945
  });
940
946
 
941
947
  pi.on("session_tree", (_event, ctx) => {
942
- applyAllReviewState(ctx);
948
+ try {
949
+ applyAllReviewState(ctx);
950
+ }
951
+ catch (error: unknown) {
952
+ if (!isStaleExtensionError(error)) {throw error;}
953
+ }
943
954
  });
944
955
 
945
956
  /**
@@ -0,0 +1,11 @@
1
+ /**
2
+ * pi invalidates the captured `pi` / `ctx` held by an extension factory closure
3
+ * after session replacement (`/new`, `/clear`, `/resume`, `/reload`, etc.).
4
+ * Async handlers that started before the replacement resume against the stale
5
+ * runtime and throw — the runner catches and rethrows via `emitError`, which
6
+ * surfaces as a visible TUI error. Use this helper to ignore only that one
7
+ * well-known error and let every other failure bubble.
8
+ */
9
+ export function isStaleExtensionError(error: unknown): boolean {
10
+ return error instanceof Error && /stale after session replacement or reload/i.test(error.message);
11
+ }