pi-ui-extend 0.1.34 → 0.1.36

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 (73) hide show
  1. package/README.md +20 -0
  2. package/dist/app/app.d.ts +1 -0
  3. package/dist/app/app.js +12 -2
  4. package/dist/app/commands/command-host.d.ts +1 -0
  5. package/dist/app/commands/command-model-actions.d.ts +1 -0
  6. package/dist/app/commands/command-model-actions.js +32 -0
  7. package/dist/app/commands/command-navigation-actions.js +3 -0
  8. package/dist/app/commands/command-session-actions.js +2 -0
  9. package/dist/app/constants.d.ts +2 -1
  10. package/dist/app/constants.js +6 -1
  11. package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
  12. package/dist/app/extensions/extension-actions-controller.js +4 -0
  13. package/dist/app/input/input-controller.d.ts +5 -1
  14. package/dist/app/input/input-controller.js +122 -16
  15. package/dist/app/input/input-paste-handler.js +3 -1
  16. package/dist/app/input/terminal-edit-shortcuts.d.ts +21 -0
  17. package/dist/app/input/terminal-edit-shortcuts.js +92 -16
  18. package/dist/app/popup/popup-action-controller.d.ts +1 -0
  19. package/dist/app/popup/popup-action-controller.js +1 -0
  20. package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
  21. package/dist/app/rendering/conversation-entry-renderer.js +1 -1
  22. package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
  23. package/dist/app/rendering/conversation-tool-renderer.js +21 -0
  24. package/dist/app/rendering/conversation-viewport.d.ts +3 -0
  25. package/dist/app/rendering/conversation-viewport.js +41 -5
  26. package/dist/app/rendering/editor-layout-renderer.js +3 -2
  27. package/dist/app/rendering/editor-panels.js +27 -10
  28. package/dist/app/runtime.d.ts +1 -0
  29. package/dist/app/runtime.js +33 -14
  30. package/dist/app/session/session-event-controller.d.ts +7 -0
  31. package/dist/app/session/session-event-controller.js +78 -0
  32. package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
  33. package/dist/app/session/session-lifecycle-controller.js +7 -0
  34. package/dist/app/session/tabs-controller.d.ts +1 -0
  35. package/dist/app/session/tabs-controller.js +4 -1
  36. package/dist/app/subagents/subagents-widget-controller.d.ts +10 -2
  37. package/dist/app/subagents/subagents-widget-controller.js +141 -70
  38. package/dist/app/terminal/terminal-controller.d.ts +10 -0
  39. package/dist/app/terminal/terminal-controller.js +91 -2
  40. package/dist/app/todo/todo-model.js +2 -0
  41. package/dist/app/todo/todo-widget-controller.d.ts +2 -0
  42. package/dist/app/todo/todo-widget-controller.js +17 -7
  43. package/dist/app/types.d.ts +4 -0
  44. package/dist/app/workspace/workspace-actions-controller.d.ts +1 -0
  45. package/dist/app/workspace/workspace-actions-controller.js +1 -0
  46. package/dist/bundled-extensions/question/tui.js +8 -1
  47. package/dist/bundled-extensions/session-title/index.js +65 -14
  48. package/dist/input-editor-files.js +23 -4
  49. package/dist/markdown-format.d.ts +4 -1
  50. package/dist/markdown-format.js +76 -9
  51. package/external/pi-tools-suite/README.md +71 -1
  52. package/external/pi-tools-suite/package.json +5 -5
  53. package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -6
  54. package/external/pi-tools-suite/src/async-subagents/index.ts +133 -37
  55. package/external/pi-tools-suite/src/context-usage.ts +6 -1
  56. package/external/pi-tools-suite/src/dcp/commands.ts +3 -2
  57. package/external/pi-tools-suite/src/dcp/compress-tool.ts +9 -4
  58. package/external/pi-tools-suite/src/dcp/config.ts +142 -6
  59. package/external/pi-tools-suite/src/dcp/index.ts +20 -8
  60. package/external/pi-tools-suite/src/dcp/prompts.ts +17 -9
  61. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +59 -15
  62. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +6 -8
  63. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
  64. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +51 -1
  65. package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +16 -11
  66. package/external/pi-tools-suite/src/model-tools/index.ts +24 -12
  67. package/external/pi-tools-suite/src/prompt-commands/index.ts +11 -2
  68. package/external/pi-tools-suite/src/telegram-mirror/index.ts +66 -27
  69. package/external/pi-tools-suite/src/todo/index.ts +87 -16
  70. package/external/pi-tools-suite/src/todo/state/store.ts +41 -10
  71. package/external/pi-tools-suite/src/todo/todo.ts +49 -6
  72. package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
  73. package/package.json +7 -5
@@ -68,12 +68,62 @@ DCP settings are stored only under `dcp` in the user shared config file `~/.conf
68
68
  "nudgeFrequency": 1,
69
69
  "iterationNudgeThreshold": 6,
70
70
  "protectedTools": ["compress", "write", "edit", "subagents"]
71
+ },
72
+ "modelOverrides": {
73
+ "openai-codex/gpt-5.5": {
74
+ "compress": {
75
+ "minContextPercent": "28%",
76
+ "maxContextPercent": "48%"
77
+ }
78
+ },
79
+ "openai-codex/gpt-5.4-mini": {
80
+ "compress": {
81
+ "minContextPercent": "20%",
82
+ "maxContextPercent": "38%"
83
+ }
84
+ },
85
+ "zai/*": {
86
+ "compress": {
87
+ "minContextPercent": "16%",
88
+ "maxContextPercent": "30%"
89
+ }
90
+ },
91
+ "antigravity/*sonnet*": {
92
+ "compress": {
93
+ "minContextPercent": "22%",
94
+ "maxContextPercent": "40%"
95
+ }
96
+ },
97
+ "antigravity/gemini-3.1-pro*": {
98
+ "compress": {
99
+ "minContextPercent": "24%",
100
+ "maxContextPercent": "42%"
101
+ }
102
+ },
103
+ "antigravity/gemini-3-flash*": {
104
+ "compress": {
105
+ "minContextPercent": "18%",
106
+ "maxContextPercent": "34%"
107
+ }
108
+ },
109
+ "antigravity/gemini-2.5-flash*": {
110
+ "compress": {
111
+ "minContextPercent": "18%",
112
+ "maxContextPercent": "32%"
113
+ }
114
+ },
115
+ "antigravity/antigravity-claude-opus-4-6-thinking": {
116
+ "compress": {
117
+ "minContextPercent": "26%",
118
+ "maxContextPercent": "44%"
119
+ }
120
+ }
71
121
  }
72
122
  }
73
123
  }
74
124
  ```
75
125
 
76
- `minContextPercent` / `maxContextPercent` accept legacy fractions (`0.25`), percent strings (`"25%"`), or absolute token counts when Pi knows the current model context window. `minContextLimit` / `maxContextLimit` and `modelMinContextLimits` / `modelMaxContextLimits` are explicit absolute-or-percent aliases. If `compress.protectUserMessages` is enabled, range compression appends selected user messages verbatim instead of rejecting the range; individual message compression still skips protected raw user messages. Protected tool outputs are copied into summaries for tools protected by name or `protectedFilePatterns`; protected `subagents` result reads also try to include the saved `result.md` artifact when available.
126
+ `minContextPercent` / `maxContextPercent` accept legacy fractions (`0.25`), percent strings (`"25%"`), or absolute token counts when Pi knows the current model context window. `minContextLimit` / `maxContextLimit` and `modelMinContextLimits` / `modelMaxContextLimits` are explicit absolute-or-percent aliases. `modelOverrides` and the `modelMin*` / `modelMax*` maps support exact model keys plus `*` / `?` wildcard patterns; matching is applied from generic to specific so exact bare-model matches override bare wildcards, and exact `provider/model` matches override provider wildcards. Array fields are union-merged, so model-specific `protectedTools` extend the defaults instead of replacing them. If `compress.protectUserMessages` is enabled, range compression appends selected user messages verbatim instead of rejecting the range; individual message compression still skips protected raw user messages. Protected tool outputs are copied into summaries for tools protected by name or `protectedFilePatterns`; protected `subagents` result reads also try to include the saved `result.md` artifact when available.
77
127
 
78
128
  ## LSP setup
79
129
 
@@ -271,6 +321,26 @@ npm run test:e2e
271
321
 
272
322
  Supporting docs and historical standalone README content are kept in `docs/`; third-party license texts are kept in `licenses/`.
273
323
 
324
+ ## SDK pin
325
+
326
+ This suite runs inside the Pi host process, so its `@earendil-works/*`
327
+ peerDependencies (`pi-ai`, `pi-coding-agent`, `pi-tui`) must match the host Pi
328
+ SDK version exactly. Otherwise npm can resolve a stale copy in this package's
329
+ own `node_modules` and cause a double-load (e.g. `0.75.4` here vs `0.79.4` in
330
+ the host).
331
+
332
+ The host repo keeps these aligned: `npm run sync:sdk-pin` rewrites these
333
+ peerDeps to the host version, and `npm run sync:sdk-pin:check` reports drift
334
+ (non-zero exit). When you bump the Pi SDK in the host `package.json`, the host
335
+ runs `sync:sdk-pin` and then you reinstall here:
336
+
337
+ ```bash
338
+ npm install --ignore-scripts
339
+ ```
340
+
341
+ The suite deliberately does not bump its own `version` field for SDK changes;
342
+ its peerDeps carry the version.
343
+
274
344
  ## Third-party notices
275
345
 
276
346
  Parts of this extension suite are based on or adapted from code by other vendors and projects. The corresponding license texts and notices are included in `licenses/`.
@@ -22,13 +22,13 @@
22
22
  "smoke:tools": "PI_OFFLINE=1 pi --no-session -p \"ping\"",
23
23
  "smoke": "npm run smoke:explicit && npm run smoke:auto && npm run smoke:tools",
24
24
  "test": "bun test test",
25
- "test:async-subagents-e2e": "ASYNC_SUBAGENTS_E2E=1 ASYNC_SUBAGENTS_MODEL=zai/glm-5-turbo bun test --concurrent --max-concurrency=30 test/async-subagents",
25
+ "test:async-subagents-e2e": "ASYNC_SUBAGENTS_E2E=1 ASYNC_SUBAGENTS_DEBUG_LOGS=1 ASYNC_SUBAGENTS_MODEL=zai/glm-5-turbo bun test --concurrent --max-concurrency=30 test/async-subagents",
26
26
  "test:async-subagents-selection-e2e": "ASYNC_SUBAGENTS_SELECTION_E2E=1 ASYNC_SUBAGENTS_MODEL=zai/glm-5-turbo bun test --concurrent --max-concurrency=30 test/async-subagents/selection-e2e.test.ts",
27
27
  "bench:locate": "PI_LOCATE_BENCH_ITERATIONS=5 PI_LOCATE_BENCH_FAKE_IDX=0 PI_LOCATE_BENCH_MODEL=zai/glm-5-turbo PI_LOCATE_BENCH_MODES=direct-read-grep,ast-structural,repo-search-hybrid,repo-discovery,subagent-search,unrestricted-suite node test/fixtures/hard-to-find-project/benchmark/run-locate-benchmark.mjs",
28
28
  "bench:locate:analyze": "node test/fixtures/hard-to-find-project/benchmark/analyze-locate-benchmark.mjs",
29
29
  "test:locate-benchmark-e2e": "PI_LOCATE_BENCH_E2E=1 PI_LOCATE_BENCH_MODEL=zai/glm-5-turbo bun test test/locate-benchmark-e2e.test.ts",
30
30
  "test:tool-selection-e2e": "TOOL_SELECTION_E2E=1 TOOL_SELECTION_E2E_MODEL=zai/glm-5-turbo bun test --concurrent --max-concurrency=30 test/tool-selection-e2e.test.ts",
31
- "test:e2e": "TOOL_SELECTION_E2E=1 TOOL_SELECTION_E2E_MODEL=zai/glm-5-turbo ASYNC_SUBAGENTS_E2E=1 ASYNC_SUBAGENTS_MODEL=zai/glm-5-turbo bun test --concurrent --max-concurrency=5 test/tool-selection-e2e.test.ts test/async-subagents/e2e.test.ts test/async-subagents/selection-e2e.test.ts test/todo-persistence-e2e.test.ts",
31
+ "test:e2e": "TOOL_SELECTION_E2E=1 TOOL_SELECTION_E2E_MODEL=zai/glm-5-turbo ASYNC_SUBAGENTS_E2E=1 ASYNC_SUBAGENTS_DEBUG_LOGS=1 ASYNC_SUBAGENTS_MODEL=zai/glm-5-turbo bun test --concurrent --max-concurrency=5 test/tool-selection-e2e.test.ts test/async-subagents/e2e.test.ts test/async-subagents/selection-e2e.test.ts test/todo-persistence-e2e.test.ts",
32
32
  "typecheck:async-subagents": "bash scripts/typecheck-source.sh",
33
33
  "check": "npm run smoke"
34
34
  },
@@ -38,9 +38,9 @@
38
38
  "vscode-languageserver-protocol": "^3.17.5"
39
39
  },
40
40
  "peerDependencies": {
41
- "@earendil-works/pi-ai": "*",
42
- "@earendil-works/pi-coding-agent": "*",
43
- "@earendil-works/pi-tui": "*",
41
+ "@earendil-works/pi-ai": "0.79.4",
42
+ "@earendil-works/pi-coding-agent": "0.79.4",
43
+ "@earendil-works/pi-tui": "0.79.4",
44
44
  "typebox": "*"
45
45
  },
46
46
  "devDependencies": {
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
+ import { ignoreStaleExtensionContextError } from "../context-usage.js";
4
5
  import {
5
6
  copySubagentConfigSample,
6
7
  ensureSessionFileLink,
@@ -172,12 +173,17 @@ async function triggerOrchestrationPrompt(
172
173
  ? `${basePrompt}\n\nObjective:\n${objective}`
173
174
  : basePrompt;
174
175
 
175
- if (typeof pi.sendUserMessage === "function") {
176
- pi.sendUserMessage(prompt);
177
- } else if (typeof pi.sendMessage === "function") {
178
- pi.sendMessage({ customType: `async-subagents-${modeName}`, content: prompt, display: false }, { triggerTurn: true, deliverAs: "followUp" });
179
- } else {
180
- ctx.ui.notify(`Cannot trigger /${modeName}: this Pi runtime does not expose sendUserMessage/sendMessage.`, "error");
176
+ try {
177
+ if (typeof pi.sendUserMessage === "function") {
178
+ pi.sendUserMessage(prompt);
179
+ } else if (typeof pi.sendMessage === "function") {
180
+ pi.sendMessage({ customType: `async-subagents-${modeName}`, content: prompt, display: false }, { triggerTurn: true, deliverAs: "followUp" });
181
+ } else {
182
+ ctx.ui.notify(`Cannot trigger /${modeName}: this Pi runtime does not expose sendUserMessage/sendMessage.`, "error");
183
+ return;
184
+ }
185
+ } catch (error) {
186
+ ignoreStaleExtensionContextError(error);
181
187
  return;
182
188
  }
183
189
 
@@ -9,12 +9,13 @@ import {
9
9
  getSubagentRegistryPath,
10
10
  isBlindModelRef,
11
11
  loadSubagentConfig,
12
- listRunDirs,
12
+ listSubagentSessionRecords,
13
13
  loadSubagentRegistry,
14
14
  removeSubagentRunsFromRegistry,
15
15
  stopAgents,
16
16
  type AgentCompletionHandler,
17
17
  type StopSignal,
18
+ type SubagentSessionRecord,
18
19
  } from "./lib.js";
19
20
  import { buildUltraworkPrompt, isUltraworkEnvEnabled, registerCommands } from "./commands.js";
20
21
  import { agentStrategyPrompt, appendAgentStrategyPrompt } from "./core/agent-strategy.js";
@@ -46,6 +47,11 @@ interface ShutdownTarget {
46
47
  agentIds?: string[];
47
48
  }
48
49
 
50
+ interface ShutdownPlan {
51
+ targets: ShutdownTarget[];
52
+ runDirsToDelete: string[];
53
+ }
54
+
49
55
  function createLiveStatePayload(
50
56
  liveAgents: Map<string, Map<string, LiveAgent>>,
51
57
  sessionFile: string | undefined,
@@ -81,6 +87,14 @@ function agentMatchesSession(agent: LiveAgent, sessionFile: string | undefined):
81
87
  return pathsEqual(sessionFile, agent.parentSession);
82
88
  }
83
89
 
90
+ function isStaleExtensionContextError(error: unknown): boolean {
91
+ return error instanceof Error && /ctx is stale|stale ctx|stale after session replacement|stale after.*reload/i.test(error.message);
92
+ }
93
+
94
+ function ignoreStaleExtensionContextError(error: unknown): void {
95
+ if (!isStaleExtensionContextError(error)) throw error;
96
+ }
97
+
84
98
  export default function (pi: ExtensionAPI) {
85
99
  const liveAgents = new Map<string, Map<string, LiveAgent>>();
86
100
  const subagentOverlay = new SubagentOverlay(liveAgents);
@@ -90,11 +104,15 @@ export default function (pi: ExtensionAPI) {
90
104
  publishSubagentPresetsStartupSection();
91
105
 
92
106
  function refreshSubagentOverlay(): void {
93
- reconcileLiveAgentCompletions();
94
- const liveState = createLiveStatePayload(liveAgents, currentSessionFile);
95
- pi.events?.emit?.(SUBAGENTS_LIVE_COUNT_EVENT, { count: liveState.count });
96
- pi.events?.emit?.(SUBAGENTS_LIVE_STATE_EVENT, liveState);
97
- updateCompletionWatcher();
107
+ try {
108
+ reconcileLiveAgentCompletions();
109
+ const liveState = createLiveStatePayload(liveAgents, currentSessionFile);
110
+ pi.events?.emit?.(SUBAGENTS_LIVE_COUNT_EVENT, { count: liveState.count });
111
+ pi.events?.emit?.(SUBAGENTS_LIVE_STATE_EVENT, liveState);
112
+ updateCompletionWatcher();
113
+ } catch (error) {
114
+ ignoreStaleExtensionContextError(error);
115
+ }
98
116
  }
99
117
 
100
118
  function removeLiveAgent(runDir: string, agentId: string): void {
@@ -147,10 +165,14 @@ export default function (pi: ExtensionAPI) {
147
165
  registerCommands(pi);
148
166
 
149
167
  pi.on("session_start", async (_event, ctx) => {
150
- sawAutoUltraworkCandidate = false;
151
- currentSessionFile = sessionFileFromContext(ctx);
152
- subagentOverlay.restoreRunningAgents(ctx.cwd, currentSessionFile);
153
- refreshSubagentOverlay();
168
+ try {
169
+ sawAutoUltraworkCandidate = false;
170
+ currentSessionFile = sessionFileFromContext(ctx);
171
+ subagentOverlay.restoreRunningAgents(ctx.cwd, currentSessionFile);
172
+ refreshSubagentOverlay();
173
+ } catch (error) {
174
+ ignoreStaleExtensionContextError(error);
175
+ }
154
176
  });
155
177
 
156
178
  pi.on("tool_execution_end", async (event) => {
@@ -207,18 +229,23 @@ export default function (pi: ExtensionAPI) {
207
229
  });
208
230
 
209
231
  pi.on("session_shutdown", async (event, ctx) => {
210
- subagentOverlay.dispose();
211
- if (completionWatchTimer) {
212
- clearInterval(completionWatchTimer);
213
- completionWatchTimer = undefined;
214
- }
215
- if (event?.reason === "reload" || event?.reason === "fork") return;
216
232
  try {
217
- await cleanupProjectSubagentState(ctx.cwd, liveAgents);
218
- liveAgents.clear();
219
- refreshSubagentOverlay();
220
- } catch {
221
- // Shutdown cleanup is best-effort and must never block the main session from closing.
233
+ subagentOverlay.dispose();
234
+ if (completionWatchTimer) {
235
+ clearInterval(completionWatchTimer);
236
+ completionWatchTimer = undefined;
237
+ }
238
+ if (event?.reason === "reload" || event?.reason === "fork") return;
239
+ try {
240
+ const shutdownSessionFile = sessionFileFromContext(ctx) ?? currentSessionFile;
241
+ await cleanupProjectSubagentState(ctx.cwd, liveAgents, { parentSession: shutdownSessionFile });
242
+ liveAgents.clear();
243
+ refreshSubagentOverlay();
244
+ } catch {
245
+ // Shutdown cleanup is best-effort and must never block the main session from closing.
246
+ }
247
+ } catch (error) {
248
+ ignoreStaleExtensionContextError(error);
222
249
  }
223
250
  });
224
251
  }
@@ -394,31 +421,100 @@ function selectedToolsInclude(event: unknown, toolName: string): boolean {
394
421
  return !Array.isArray(selectedTools) || selectedTools.includes(toolName);
395
422
  }
396
423
 
397
- async function cleanupProjectSubagentState(cwd: string, liveAgents: Map<string, Map<string, LiveAgent>>): Promise<void> {
398
- const shutdownTargets = collectShutdownTargets(cwd, liveAgents);
399
- const signaled = signalShutdownTargets(shutdownTargets, "SIGTERM");
424
+ async function cleanupProjectSubagentState(
425
+ cwd: string,
426
+ liveAgents: Map<string, Map<string, LiveAgent>>,
427
+ options: { parentSession?: string } = {},
428
+ ): Promise<void> {
429
+ const shutdownPlan = collectShutdownPlan(cwd, liveAgents, options.parentSession);
430
+ const signaled = signalShutdownTargets(shutdownPlan.targets, "SIGTERM");
400
431
  if (signaled > 0) await sleep(SESSION_SHUTDOWN_KILL_GRACE_MS);
401
- signalShutdownTargets(shutdownTargets, "SIGKILL");
432
+ signalShutdownTargets(shutdownPlan.targets, "SIGKILL");
402
433
 
403
- const runDirs = listRunDirs(cwd);
404
- for (const runDir of runDirs) stopRunBestEffort(runDir, undefined, "SIGKILL");
405
- deleteRunDirs(runDirs);
406
- removeSubagentRunsFromRegistry(cwd, runDirs);
434
+ for (const target of shutdownPlan.targets) stopRunBestEffort(target.runDir, target.agentIds, "SIGKILL");
435
+ deleteRunDirs(shutdownPlan.runDirsToDelete);
436
+ removeSubagentRunsFromRegistry(cwd, shutdownPlan.runDirsToDelete);
407
437
  removeEmptySubagentState(cwd);
408
438
  }
409
439
 
410
- function collectShutdownTargets(cwd: string, liveAgents: Map<string, Map<string, LiveAgent>>): ShutdownTarget[] {
411
- const targets = new Map<string, Set<string> | undefined>();
412
- for (const runDir of listRunDirs(cwd)) targets.set(runDir, undefined);
440
+
441
+ function collectShutdownPlan(
442
+ cwd: string,
443
+ liveAgents: Map<string, Map<string, LiveAgent>>,
444
+ parentSession: string | undefined,
445
+ ): ShutdownPlan {
446
+ if (!parentSession) return collectLiveShutdownPlan(liveAgents);
447
+
448
+ const targets = new Map<string, Set<string>>();
449
+ const runDirsToDelete = new Set<string>();
450
+ const recordsByRun = groupRecordsByRun(listSubagentSessionRecords(cwd));
451
+
452
+ for (const [runDir, records] of recordsByRun) {
453
+ const matchingRecords = records.filter((record) => recordMatchesSession(record, parentSession));
454
+ if (matchingRecords.length === 0) continue;
455
+ mergeTargetIds(targets, runDir, matchingRecords.map((record) => record.agentId));
456
+ const liveRun = liveAgents.get(runDir);
457
+ if (records.every((record) => recordMatchesSession(record, parentSession)) && liveRunMatchesSession(liveRun, parentSession)) {
458
+ runDirsToDelete.add(runDir);
459
+ }
460
+ }
461
+
413
462
  for (const [runDir, liveRun] of liveAgents) {
414
- const existing = targets.get(runDir);
415
- if (existing === undefined && targets.has(runDir)) continue;
416
- targets.set(runDir, new Set(liveRun.keys()));
463
+ const matchingIds = [...liveRun.values()]
464
+ .filter((agent) => liveAgentMatchesSession(agent, parentSession))
465
+ .map((agent) => agent.agentId);
466
+ if (matchingIds.length === 0) continue;
467
+ mergeTargetIds(targets, runDir, matchingIds);
468
+ const records = recordsByRun.get(runDir) ?? [];
469
+ if (records.every((record) => recordMatchesSession(record, parentSession)) && liveRunMatchesSession(liveRun, parentSession)) {
470
+ runDirsToDelete.add(runDir);
471
+ }
417
472
  }
418
- return [...targets].map(([runDir, agentIds]) => ({
473
+
474
+ return {
475
+ targets: targetMapToTargets(targets),
476
+ runDirsToDelete: [...runDirsToDelete],
477
+ };
478
+ }
479
+
480
+ function collectLiveShutdownPlan(liveAgents: Map<string, Map<string, LiveAgent>>): ShutdownPlan {
481
+ const targets = [...liveAgents.entries()].map(([runDir, liveRun]) => ({
419
482
  runDir,
420
- ...(agentIds ? { agentIds: [...agentIds] } : {}),
483
+ agentIds: [...liveRun.keys()],
421
484
  }));
485
+ return { targets, runDirsToDelete: targets.map((target) => target.runDir) };
486
+ }
487
+
488
+ function groupRecordsByRun(records: SubagentSessionRecord[]): Map<string, SubagentSessionRecord[]> {
489
+ const grouped = new Map<string, SubagentSessionRecord[]>();
490
+ for (const record of records) {
491
+ const existing = grouped.get(record.runDir) ?? [];
492
+ existing.push(record);
493
+ grouped.set(record.runDir, existing);
494
+ }
495
+ return grouped;
496
+ }
497
+
498
+ function recordMatchesSession(record: SubagentSessionRecord, sessionFile: string): boolean {
499
+ return Boolean(record.parentSession && pathsEqual(record.parentSession, sessionFile));
500
+ }
501
+
502
+ function liveAgentMatchesSession(agent: LiveAgent, sessionFile: string): boolean {
503
+ return Boolean(agent.parentSession && pathsEqual(agent.parentSession, sessionFile));
504
+ }
505
+
506
+ function liveRunMatchesSession(liveRun: Map<string, LiveAgent> | undefined, sessionFile: string): boolean {
507
+ return !liveRun || [...liveRun.values()].every((agent) => liveAgentMatchesSession(agent, sessionFile));
508
+ }
509
+
510
+ function mergeTargetIds(targets: Map<string, Set<string>>, runDir: string, agentIds: string[]): void {
511
+ const target = targets.get(runDir) ?? new Set<string>();
512
+ for (const agentId of agentIds) target.add(agentId);
513
+ targets.set(runDir, target);
514
+ }
515
+
516
+ function targetMapToTargets(targets: Map<string, Set<string>>): ShutdownTarget[] {
517
+ return [...targets.entries()].map(([runDir, agentIds]) => ({ runDir, agentIds: [...agentIds] }));
422
518
  }
423
519
 
424
520
  function signalShutdownTargets(targets: ShutdownTarget[], signal: StopSignal): number {
@@ -10,13 +10,18 @@ export interface ContextUsageProvider {
10
10
 
11
11
  export function isStaleExtensionContextError(error: unknown): boolean {
12
12
  if (!(error instanceof Error)) return false
13
- return /ctx is stale|stale after session replacement|stale after.*reload/i.test(error.message)
13
+ return /ctx is stale|stale ctx|stale after session replacement|stale after.*reload/i.test(error.message)
14
14
  }
15
15
 
16
16
  export function ignoreStaleExtensionContextError(error: unknown): void {
17
17
  if (!isStaleExtensionContextError(error)) throw error
18
18
  }
19
19
 
20
+ export function isAgentBusyRaceError(error: unknown): boolean {
21
+ if (!(error instanceof Error)) return false
22
+ return /Agent is already processing(?: a prompt)?\.|Wait for completion before continuing|Specify streamingBehavior/i.test(error.message)
23
+ }
24
+
20
25
  // Fork/newSession/switchSession/reload can invalidate a runner while late UI or
21
26
  // context events from the old runner are still unwinding. In that race,
22
27
  // ctx.getContextUsage() throws the Pi stale-ctx guard; treat it as unavailable
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"
2
2
  import type { AutocompleteItem } from "@earendil-works/pi-tui"
3
3
  import type { DcpState } from "./state.js"
4
- import type { DcpConfig } from "./config.js"
4
+ import { modelKeysFromContext, resolveModelConfig, type DcpConfig } from "./config.js"
5
5
  import type { DcpNudgeType } from "./pruner-types.js"
6
6
  import { isToolRecordProtected, markToolPruned } from "./pruner.js"
7
7
  import { safeGetContextUsage } from "../context-usage.js"
@@ -540,6 +540,7 @@ export function registerCommands(
540
540
  async handler(args: string, ctx: ExtensionCommandContext): Promise<void> {
541
541
  const parts = args.trim().split(/\s+/).filter(Boolean)
542
542
  const sub = parts[0] ?? ""
543
+ const effectiveConfig = resolveModelConfig(config, modelKeysFromContext(ctx))
543
544
 
544
545
  try {
545
546
  switch (sub) {
@@ -559,7 +560,7 @@ export function registerCommands(
559
560
  case "sweep": {
560
561
  const rawN = parts[1] !== undefined ? parseInt(parts[1], 10) : 0
561
562
  const n = isNaN(rawN) || rawN < 0 ? 0 : rawN
562
- await handleSweep(ctx, state, config, n)
563
+ await handleSweep(ctx, state, effectiveConfig, n)
563
564
  break
564
565
  }
565
566
 
@@ -5,7 +5,7 @@
5
5
  import { Type } from "typebox"
6
6
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
7
7
  import type { DcpState } from "./state.js"
8
- import type { DcpConfig } from "./config.js"
8
+ import { modelKeysFromContext, resolveModelConfig, type DcpConfig } from "./config.js"
9
9
  import { clearDcpNudgeAnchors } from "./pruner.js"
10
10
  import type { DcpCompressionVisualDetails } from "./ui.js"
11
11
  import { normalizeDcpContextUsage } from "./ui.js"
@@ -149,6 +149,11 @@ export function registerCompressTool(
149
149
  }),
150
150
 
151
151
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
152
+ const effectiveConfig = resolveModelConfig(config, modelKeysFromContext(ctx))
153
+ if (!effectiveConfig.enabled) {
154
+ throw new Error("DCP is disabled for the active model")
155
+ }
156
+
152
157
  const newBlockIds: number[] = []
153
158
  const ranges = Array.isArray(params.ranges) ? params.ranges : []
154
159
  const messages = Array.isArray(params.messages) ? params.messages : []
@@ -215,7 +220,7 @@ export function registerCompressTool(
215
220
  anchorMessageId: anchor.stableId,
216
221
  createdByToolCallId: _toolCallId,
217
222
  state,
218
- config,
223
+ config: effectiveConfig,
219
224
  mode: "range",
220
225
  })
221
226
  const block = created.block
@@ -260,7 +265,7 @@ export function registerCompressTool(
260
265
  skippedMessageIssues.push({ kind: "non-finite", messageId })
261
266
  continue
262
267
  }
263
- if (config.compress.protectUserMessages && meta.role === "user") {
268
+ if (effectiveConfig.compress.protectUserMessages && meta.role === "user") {
264
269
  skippedMessageIssues.push({ kind: "protected-user", messageId })
265
270
  continue
266
271
  }
@@ -291,7 +296,7 @@ export function registerCompressTool(
291
296
  anchorMessageId: anchor.stableId,
292
297
  createdByToolCallId: _toolCallId,
293
298
  state,
294
- config,
299
+ config: effectiveConfig,
295
300
  mode: "message",
296
301
  validatePlaceholders: false,
297
302
  expandPlaceholders: false,