@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.0

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 (285) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +4659 -4261
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/model-registry.d.ts +3 -1
  10. package/dist/types/config/models-config-schema.d.ts +18 -6
  11. package/dist/types/config/models-config.d.ts +14 -5
  12. package/dist/types/config/provider-globals.d.ts +2 -2
  13. package/dist/types/config/service-tier.d.ts +5 -1
  14. package/dist/types/config/settings-schema.d.ts +316 -47
  15. package/dist/types/config/settings.d.ts +7 -0
  16. package/dist/types/dap/client.d.ts +20 -0
  17. package/dist/types/exec/bash-executor.d.ts +38 -0
  18. package/dist/types/exec/direnv.d.ts +33 -0
  19. package/dist/types/export/html/args.d.ts +15 -0
  20. package/dist/types/export/html/index.d.ts +11 -26
  21. package/dist/types/export/html/web-palette.d.ts +55 -116
  22. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  23. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  24. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
  25. package/dist/types/live/controller.d.ts +40 -0
  26. package/dist/types/live/protocol.d.ts +85 -0
  27. package/dist/types/live/protocol.test.d.ts +1 -0
  28. package/dist/types/live/transport.d.ts +48 -0
  29. package/dist/types/live/visualizer.d.ts +33 -0
  30. package/dist/types/lsp/client.d.ts +16 -1
  31. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  32. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  33. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  34. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  35. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  36. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  37. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  38. package/dist/types/modes/components/settings-defs.d.ts +7 -1
  39. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  40. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  41. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  43. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  44. package/dist/types/modes/interactive-mode.d.ts +5 -0
  45. package/dist/types/modes/theme/theme.d.ts +1 -1
  46. package/dist/types/modes/types.d.ts +3 -1
  47. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  48. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  49. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  50. package/dist/types/registry/agent-registry.d.ts +11 -4
  51. package/dist/types/sdk.d.ts +15 -1
  52. package/dist/types/secrets/index.d.ts +15 -2
  53. package/dist/types/secrets/obfuscator.d.ts +113 -15
  54. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  55. package/dist/types/session/agent-session-events.d.ts +84 -0
  56. package/dist/types/session/agent-session-types.d.ts +314 -0
  57. package/dist/types/session/agent-session.d.ts +118 -755
  58. package/dist/types/session/async-job-delivery.d.ts +28 -0
  59. package/dist/types/session/bash-runner.d.ts +75 -0
  60. package/dist/types/session/blob-store.d.ts +11 -1
  61. package/dist/types/session/blob-store.test.d.ts +1 -0
  62. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  63. package/dist/types/session/eval-runner.d.ts +49 -0
  64. package/dist/types/session/irc-bridge.d.ts +46 -0
  65. package/dist/types/session/messages.d.ts +22 -0
  66. package/dist/types/session/model-controls.d.ts +180 -0
  67. package/dist/types/session/prewalk.d.ts +46 -0
  68. package/dist/types/session/queued-messages.d.ts +22 -0
  69. package/dist/types/session/redis-session-storage.d.ts +1 -0
  70. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  71. package/dist/types/session/role-models.d.ts +15 -0
  72. package/dist/types/session/session-advisors.d.ts +228 -0
  73. package/dist/types/session/session-entries.d.ts +8 -0
  74. package/dist/types/session/session-handoff.d.ts +70 -0
  75. package/dist/types/session/session-history-format.d.ts +6 -1
  76. package/dist/types/session/session-maintenance.d.ts +246 -0
  77. package/dist/types/session/session-manager.d.ts +41 -0
  78. package/dist/types/session/session-memory.d.ts +56 -0
  79. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  80. package/dist/types/session/session-stats.d.ts +48 -0
  81. package/dist/types/session/session-tools.d.ts +143 -0
  82. package/dist/types/session/session-workspace.d.ts +32 -0
  83. package/dist/types/session/stream-guards.d.ts +44 -0
  84. package/dist/types/session/todo-tracker.d.ts +56 -0
  85. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  86. package/dist/types/session/turn-recovery.d.ts +225 -0
  87. package/dist/types/system-prompt.d.ts +2 -0
  88. package/dist/types/task/executor.d.ts +4 -1
  89. package/dist/types/task/output-manager.d.ts +3 -1
  90. package/dist/types/tools/approval.d.ts +1 -0
  91. package/dist/types/tools/browser/launch.d.ts +5 -0
  92. package/dist/types/tools/image-gen.d.ts +4 -4
  93. package/dist/types/tools/image-providers.d.ts +38 -0
  94. package/dist/types/tools/index.d.ts +5 -0
  95. package/dist/types/tools/path-utils.d.ts +8 -0
  96. package/dist/types/tools/todo.d.ts +10 -4
  97. package/dist/types/tools/xdev.d.ts +5 -1
  98. package/dist/types/tts/streaming-player.d.ts +14 -0
  99. package/dist/types/tts/vocalizer.d.ts +5 -0
  100. package/dist/types/utils/jj.d.ts +29 -0
  101. package/dist/types/utils/lang-from-path.d.ts +1 -0
  102. package/dist/types/utils/title-generator.d.ts +30 -0
  103. package/dist/types/vibe/runtime.d.ts +36 -8
  104. package/dist/types/web/search/index.d.ts +1 -1
  105. package/dist/types/web/search/provider.d.ts +15 -5
  106. package/dist/types/web/search/types.d.ts +94 -0
  107. package/package.json +12 -12
  108. package/scripts/generate-share-viewer.ts +4 -6
  109. package/src/advisor/__tests__/advisor.test.ts +643 -0
  110. package/src/advisor/runtime.ts +144 -43
  111. package/src/async/job-manager.ts +90 -2
  112. package/src/cli/args.ts +2 -0
  113. package/src/cli/auth-gateway-cli.ts +18 -3
  114. package/src/cli/bench-cli.ts +395 -29
  115. package/src/cli/flag-tables.ts +3 -0
  116. package/src/cli/gc-cli.ts +2 -2
  117. package/src/cli/usage-cli.ts +10 -1
  118. package/src/collab/host.ts +3 -2
  119. package/src/commands/bench.ts +18 -4
  120. package/src/commands/launch.ts +4 -0
  121. package/src/config/model-registry.ts +10 -2
  122. package/src/config/models-config-schema.ts +15 -4
  123. package/src/config/provider-globals.ts +9 -9
  124. package/src/config/service-tier.ts +26 -1
  125. package/src/config/settings-schema.ts +209 -51
  126. package/src/config/settings.ts +81 -2
  127. package/src/dap/client.ts +37 -4
  128. package/src/edit/index.ts +49 -12
  129. package/src/eval/py/executor.ts +103 -33
  130. package/src/exec/bash-executor.ts +89 -6
  131. package/src/exec/direnv.ts +145 -0
  132. package/src/export/html/args.ts +20 -0
  133. package/src/export/html/index.ts +45 -47
  134. package/src/export/html/template.css +19 -1
  135. package/src/export/html/template.html +6 -0
  136. package/src/export/html/template.js +21 -0
  137. package/src/export/html/tool-views.generated.js +31 -31
  138. package/src/export/html/web-palette.ts +116 -132
  139. package/src/export/share.ts +249 -49
  140. package/src/extensibility/extensions/loader.ts +29 -1
  141. package/src/extensibility/extensions/runner.ts +6 -0
  142. package/src/extensibility/extensions/types.ts +33 -2
  143. package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
  144. package/src/hindsight/state.ts +64 -6
  145. package/src/internal-urls/local-protocol.ts +2 -1
  146. package/src/internal-urls/memory-protocol.ts +2 -2
  147. package/src/internal-urls/skill-protocol.ts +2 -2
  148. package/src/internal-urls/ssh-protocol.ts +2 -1
  149. package/src/internal-urls/vault-protocol.ts +2 -1
  150. package/src/live/audio-worklet.txt +59 -0
  151. package/src/live/browser-runtime.txt +221 -0
  152. package/src/live/controller.ts +464 -0
  153. package/src/live/prompts/agent-final-message.md +3 -0
  154. package/src/live/prompts/live-instructions.md +23 -0
  155. package/src/live/protocol.test.ts +140 -0
  156. package/src/live/protocol.ts +233 -0
  157. package/src/live/transport.ts +490 -0
  158. package/src/live/visualizer.ts +234 -0
  159. package/src/lsp/client.ts +29 -9
  160. package/src/lsp/index.ts +19 -4
  161. package/src/main.ts +8 -1
  162. package/src/mcp/config-writer.test.ts +43 -0
  163. package/src/mcp/config-writer.ts +109 -82
  164. package/src/mcp/oauth-discovery.ts +10 -2
  165. package/src/mcp/smithery-auth.test.ts +29 -0
  166. package/src/mcp/smithery-auth.ts +3 -2
  167. package/src/memories/index.ts +40 -20
  168. package/src/memory-backend/tool-names.ts +2 -0
  169. package/src/mnemopi/backend.ts +24 -7
  170. package/src/modes/acp/acp-agent.ts +56 -10
  171. package/src/modes/acp/acp-event-mapper.ts +8 -1
  172. package/src/modes/components/agent-hub.ts +1 -1
  173. package/src/modes/components/login-dialog.ts +14 -4
  174. package/src/modes/components/plan-review-overlay.ts +350 -35
  175. package/src/modes/components/settings-defs.ts +23 -1
  176. package/src/modes/components/settings-selector.ts +169 -1
  177. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  178. package/src/modes/components/status-line/component.ts +123 -2
  179. package/src/modes/components/tool-execution.ts +11 -2
  180. package/src/modes/controllers/command-controller.ts +56 -15
  181. package/src/modes/controllers/event-controller.ts +111 -16
  182. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  183. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  184. package/src/modes/controllers/live-command-controller.ts +178 -0
  185. package/src/modes/controllers/mcp-command-controller.ts +10 -1
  186. package/src/modes/controllers/omfg-controller.ts +44 -40
  187. package/src/modes/controllers/selector-controller.ts +18 -10
  188. package/src/modes/controllers/session-focus-controller.ts +6 -1
  189. package/src/modes/interactive-mode.ts +157 -38
  190. package/src/modes/runtime-init.ts +2 -0
  191. package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
  192. package/src/modes/theme/theme.ts +1 -1
  193. package/src/modes/types.ts +3 -1
  194. package/src/modes/utils/context-usage.ts +23 -7
  195. package/src/modes/utils/ui-helpers.ts +10 -5
  196. package/src/plan-mode/plan-files.ts +40 -0
  197. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  198. package/src/prompts/bench/cache-prefix.md +3 -0
  199. package/src/prompts/bench/cache-suffix.md +1 -0
  200. package/src/prompts/system/custom-system-prompt.md +1 -1
  201. package/src/prompts/system/project-prompt.md +8 -1
  202. package/src/prompts/system/subagent-async-pending.md +6 -0
  203. package/src/prompts/system/system-prompt.md +1 -1
  204. package/src/prompts/system/workflow-notice.md +48 -40
  205. package/src/prompts/system/xdev-mount-notice.md +4 -0
  206. package/src/prompts/tools/todo.md +3 -1
  207. package/src/registry/agent-lifecycle.ts +116 -46
  208. package/src/registry/agent-registry.ts +35 -8
  209. package/src/registry/persisted-agents.ts +26 -2
  210. package/src/sdk.ts +210 -136
  211. package/src/secrets/index.ts +127 -4
  212. package/src/secrets/obfuscator.ts +2328 -105
  213. package/src/session/acp-permission-gate.ts +165 -0
  214. package/src/session/agent-session-error-log.test.ts +1 -1
  215. package/src/session/agent-session-events.ts +66 -0
  216. package/src/session/agent-session-types.ts +334 -0
  217. package/src/session/agent-session.ts +1892 -12226
  218. package/src/session/artifacts.ts +6 -5
  219. package/src/session/async-job-delivery.ts +74 -0
  220. package/src/session/bash-runner.ts +326 -0
  221. package/src/session/blob-store.test.ts +56 -0
  222. package/src/session/blob-store.ts +18 -2
  223. package/src/session/checkpoint-entries.ts +81 -0
  224. package/src/session/eval-runner.ts +212 -0
  225. package/src/session/indexed-session-storage.ts +9 -2
  226. package/src/session/irc-bridge.ts +203 -0
  227. package/src/session/messages.ts +263 -1
  228. package/src/session/model-controls.ts +714 -0
  229. package/src/session/prewalk.ts +252 -0
  230. package/src/session/queued-messages.ts +93 -0
  231. package/src/session/redis-session-storage.ts +51 -11
  232. package/src/session/retry-fallback-chains.ts +455 -0
  233. package/src/session/role-models.ts +85 -0
  234. package/src/session/session-advisors.ts +1679 -0
  235. package/src/session/session-context.ts +6 -3
  236. package/src/session/session-entries.ts +8 -0
  237. package/src/session/session-handoff.ts +308 -0
  238. package/src/session/session-history-format.ts +20 -9
  239. package/src/session/session-loader.ts +0 -46
  240. package/src/session/session-maintenance.ts +2983 -0
  241. package/src/session/session-manager.ts +390 -30
  242. package/src/session/session-memory.ts +222 -0
  243. package/src/session/session-provider-boundary.ts +307 -0
  244. package/src/session/session-stats.ts +293 -0
  245. package/src/session/session-tools.ts +943 -0
  246. package/src/session/session-workspace.ts +53 -0
  247. package/src/session/stream-guards.ts +417 -0
  248. package/src/session/todo-tracker.ts +380 -0
  249. package/src/session/ttsr-coordinator.ts +496 -0
  250. package/src/session/turn-recovery.ts +1629 -0
  251. package/src/slash-commands/builtin-registry.ts +92 -11
  252. package/src/slash-commands/helpers/usage-report.ts +22 -3
  253. package/src/system-prompt.ts +41 -17
  254. package/src/task/executor.ts +285 -70
  255. package/src/task/index.ts +98 -105
  256. package/src/task/output-manager.ts +25 -3
  257. package/src/task/persisted-revive.ts +4 -3
  258. package/src/task/structured-subagent.ts +1 -0
  259. package/src/tools/approval.ts +55 -10
  260. package/src/tools/bash-interactive.ts +26 -0
  261. package/src/tools/bash-skill-urls.ts +28 -2
  262. package/src/tools/bash.ts +339 -123
  263. package/src/tools/browser/launch.ts +9 -1
  264. package/src/tools/browser/tab-supervisor.ts +8 -5
  265. package/src/tools/browser.ts +94 -47
  266. package/src/tools/image-gen.ts +20 -28
  267. package/src/tools/image-providers.ts +50 -0
  268. package/src/tools/index.ts +9 -0
  269. package/src/tools/path-utils.ts +49 -2
  270. package/src/tools/read.ts +156 -88
  271. package/src/tools/renderers.ts +7 -1
  272. package/src/tools/todo.ts +101 -11
  273. package/src/tools/vibe.ts +1 -2
  274. package/src/tools/write.ts +51 -1
  275. package/src/tools/xdev.ts +67 -3
  276. package/src/tts/streaming-player.ts +62 -4
  277. package/src/tts/vocalizer.ts +18 -1
  278. package/src/utils/jj.ts +125 -4
  279. package/src/utils/lang-from-path.ts +7 -0
  280. package/src/utils/title-generator.ts +137 -1
  281. package/src/vibe/runtime.ts +852 -72
  282. package/src/web/search/index.ts +3 -6
  283. package/src/web/search/provider.ts +31 -16
  284. package/src/web/search/providers/xai.ts +7 -1
  285. package/src/web/search/types.ts +3 -0
@@ -18,6 +18,8 @@ import { type CompactionEntry, EPHEMERAL_MODEL_CHANGE_ROLE, type SessionEntry }
18
18
  const LEGACY_SNAPCOMPACT_FRAME_COUNT_GUARD = 16;
19
19
  const LEGACY_SNAPCOMPACT_ARCHIVE_TEXT_GUARD = 250_000;
20
20
  const LEGACY_SNAPCOMPACT_TRUNCATED_CHARS_GUARD = 1_000_000;
21
+ const SUPERSEDED_COMPACTION_SUMMARY = "[Superseded compaction summary elided after a newer compaction]";
22
+ const SUPERSEDED_COMPACTION_SHORT_SUMMARY = "Superseded compaction elided";
21
23
 
22
24
  function hasLegacySnapcompactFrames(archive: snapcompact.Archive): boolean {
23
25
  return archive.frames.some(frame => frame.font === undefined && frame.variant === undefined);
@@ -344,13 +346,14 @@ export function buildSessionContext(
344
346
  for (const entry of path) {
345
347
  handleEntryResetTracking(entry);
346
348
  if (entry.type === "compaction") {
347
- const snapcompactArchive = snapcompact.getPreservedArchive(entry.preserveData);
349
+ const active = entry.id === compaction?.id;
350
+ const snapcompactArchive = active ? snapcompact.getPreservedArchive(entry.preserveData) : undefined;
348
351
  pushMessage(
349
352
  createCompactionSummaryMessage(
350
- entry.summary,
353
+ active ? entry.summary : SUPERSEDED_COMPACTION_SUMMARY,
351
354
  entry.tokensBefore,
352
355
  entry.timestamp,
353
- entry.shortSummary,
356
+ active ? entry.shortSummary : SUPERSEDED_COMPACTION_SHORT_SUMMARY,
354
357
  undefined,
355
358
  undefined,
356
359
  snapcompactHistoryBlocksForContext(snapcompactArchive, options),
@@ -32,6 +32,12 @@ export interface SessionHeader {
32
32
  titleSource?: SessionTitleSource;
33
33
  timestamp: string;
34
34
  cwd: string;
35
+ /**
36
+ * Additional workspace directories beyond `cwd` (multi-root workspace).
37
+ * Absolute, normalized, deduplicated. Absent on legacy single-cwd sessions.
38
+ * See {@link SessionWorkspace} in `./session-workspace`.
39
+ */
40
+ additionalDirectories?: string[];
35
41
  parentSession?: string;
36
42
  /** Provider prompt-cache identity inherited by exact-route full forks. */
37
43
  providerPromptCacheKey?: string;
@@ -43,6 +49,8 @@ export interface NewSessionOptions {
43
49
  providerPromptCacheKey?: string;
44
50
  /** Skip flushing the current session and delete it instead of saving. */
45
51
  drop?: boolean;
52
+ /** Additional workspace directories to seed on the new session. */
53
+ additionalDirectories?: string[];
46
54
  }
47
55
 
48
56
  export interface SessionEntryBase {
@@ -0,0 +1,308 @@
1
+ /** Handoff generation and session transition orchestration. */
2
+
3
+ import * as path from "node:path";
4
+ import {
5
+ type Agent,
6
+ type AgentMessage,
7
+ resolveTelemetry,
8
+ type StreamFn,
9
+ type ThinkingLevel,
10
+ } from "@oh-my-pi/pi-agent-core";
11
+ import { generateHandoffFromContext, renderHandoffPrompt } from "@oh-my-pi/pi-agent-core/compaction";
12
+ import type { Message, Model, ServiceTier, SimpleStreamOptions } from "@oh-my-pi/pi-ai";
13
+ import { logger, Snowflake } from "@oh-my-pi/pi-utils";
14
+ import type { ModelRegistry } from "../config/model-registry";
15
+ import type { Settings } from "../config/settings";
16
+ import type { ExtensionRunner, SessionBeforeSwitchResult } from "../extensibility/extensions";
17
+ import { obfuscateProviderContext, type SecretObfuscator } from "../secrets/obfuscator";
18
+ import type { HandoffResult, SessionHandoffOptions } from "./agent-session-types";
19
+ import type { BashSessionTransition } from "./bash-runner";
20
+ import type { SessionContext } from "./session-context";
21
+ import type { SessionManager } from "./session-manager";
22
+
23
+ function createHandoffContext(document: string): string {
24
+ return `<handoff-context>\n${document}\n</handoff-context>\n\nThe above is a handoff document from a previous session. Use this context to continue the work seamlessly.`;
25
+ }
26
+
27
+ function createHandoffFileName(date = new Date()): string {
28
+ const fileTimestamp = date.toISOString().replace(/[:.]/g, "-");
29
+ return `handoff-${fileTimestamp}.md`;
30
+ }
31
+
32
+ /** Capabilities borrowed from the owning AgentSession. */
33
+ export interface SessionHandoffHost {
34
+ agent: Agent;
35
+ sessionManager: SessionManager;
36
+ settings: Settings;
37
+ modelRegistry: ModelRegistry;
38
+ extensionRunner: ExtensionRunner | undefined;
39
+ sideStreamFn: StreamFn;
40
+ obfuscator: SecretObfuscator | undefined;
41
+ model(): Model | undefined;
42
+ thinkingLevel(): ThinkingLevel | undefined;
43
+ sessionId(): string;
44
+ sessionFile(): string | undefined;
45
+ baseSystemPrompt(): string[];
46
+ assertVibeSessionTransitionAllowed(action: string): void;
47
+ setSkipPostTurnMaintenance(timestamp: number | undefined): void;
48
+ obfuscateTextForProvider(text: string | undefined): string | undefined;
49
+ deobfuscateFromProvider(text: string): string;
50
+ convertMessagesToLlm(messages: AgentMessage[], signal?: AbortSignal): Promise<Message[]>;
51
+ prepareSimpleStreamOptions(options: SimpleStreamOptions, provider?: string): SimpleStreamOptions;
52
+ effectiveServiceTier(model: Model | undefined): ServiceTier | undefined;
53
+ flushPendingBash(): Promise<void>;
54
+ beginBashSessionTransition(): BashSessionTransition;
55
+ markBashSessionTransition(transition: BashSessionTransition): void;
56
+ finishBashSessionTransition(transition: BashSessionTransition, success: boolean): void;
57
+ cancelOwnAsyncJobs(): void;
58
+ clearCheckpointRuntimeState(): void;
59
+ clearSessionScopedToolState(): void;
60
+ clearFreshProviderSessionId(): void;
61
+ syncAgentSessionId(): void;
62
+ rekeyMemoryForCurrentSessionId(): void;
63
+ resetMemoryContextForNewTranscript(): Promise<void>;
64
+ clearPendingNextTurnMessages(): void;
65
+ resetTodoCycle(): void;
66
+ buildDisplaySessionContext(): SessionContext;
67
+ resetAdvisorRuntimes(): void;
68
+ syncTodoPhasesFromBranch(): void;
69
+ }
70
+
71
+ /** Generates handoff documents and owns the handoff session transition. */
72
+ export class SessionHandoff {
73
+ #handoffAbortController: AbortController | undefined;
74
+ readonly #host: SessionHandoffHost;
75
+
76
+ constructor(host: SessionHandoffHost) {
77
+ this.#host = host;
78
+ }
79
+ /**
80
+ * Cancel in-progress handoff generation.
81
+ */
82
+ abortHandoff(): void {
83
+ this.#handoffAbortController?.abort();
84
+ }
85
+
86
+ /**
87
+ * Check if handoff generation is in progress.
88
+ */
89
+ get isGeneratingHandoff(): boolean {
90
+ return this.#handoffAbortController !== undefined;
91
+ }
92
+
93
+ /**
94
+ * Generate a handoff document with a oneshot LLM call, then start a new session with it.
95
+ *
96
+ * @param customInstructions Optional focus for the handoff document
97
+ * @param options Handoff execution options
98
+ * @returns The handoff document text, or undefined if cancelled/failed
99
+ */
100
+ async handoff(customInstructions?: string, options?: SessionHandoffOptions): Promise<HandoffResult | undefined> {
101
+ this.#host.assertVibeSessionTransitionAllowed("handoff to a new session");
102
+ const entries = this.#host.sessionManager.getBranch();
103
+ const messageCount = entries.filter(e => e.type === "message").length;
104
+
105
+ if (messageCount < 2) {
106
+ throw new Error("Nothing to hand off (no messages yet)");
107
+ }
108
+
109
+ this.#host.setSkipPostTurnMaintenance(undefined);
110
+
111
+ this.#handoffAbortController = new AbortController();
112
+ const handoffAbortController = this.#handoffAbortController;
113
+ const handoffSignal = handoffAbortController.signal;
114
+ const sourceSignal = options?.signal;
115
+ const onSourceAbort = () => {
116
+ if (!handoffSignal.aborted) {
117
+ handoffAbortController.abort();
118
+ }
119
+ };
120
+ if (sourceSignal) {
121
+ sourceSignal.addEventListener("abort", onSourceAbort, { once: true });
122
+ if (sourceSignal.aborted) {
123
+ onSourceAbort();
124
+ }
125
+ }
126
+
127
+ try {
128
+ if (handoffSignal.aborted) {
129
+ throw new Error("Handoff cancelled");
130
+ }
131
+
132
+ const model = this.#host.model();
133
+ if (!model) {
134
+ throw new Error("No model selected for handoff");
135
+ }
136
+ const apiKey = await this.#host.modelRegistry.getApiKey(model, this.#host.sessionId());
137
+ if (!apiKey) {
138
+ throw new Error(`No API key for ${model.provider}`);
139
+ }
140
+
141
+ // Build the handoff request through the SAME pipeline a live turn uses
142
+ // (`runEphemeralTurn` / `/btw` share it) so the oneshot reads the
143
+ // provider prompt cache the main turn populated instead of cold-missing
144
+ // the whole prefix: identical system prompt, normalized tools, and
145
+ // transform-/obfuscation-matched message history via
146
+ // `convertMessagesToLlm` + `buildSideRequestContext`, plus the live turn's
147
+ // effective provider cache key with a unique side `sessionId` so
148
+ // OpenAI/Codex append-only state never mixes with the live turn.
149
+ const cacheSessionId = this.#host.sessionId();
150
+ // The loop sends `promptCacheKey` (providerPromptCacheKey) and falls back to
151
+ // the provider session id; providers route on `promptCacheKey ?? sessionId`.
152
+ // Both can diverge from this.#host.sessionId() (tan/subagent/shared sessions), so
153
+ // mirror exactly what the live turn populated the cache under.
154
+ const handoffPromptCacheKey = this.#host.agent.promptCacheKey ?? this.#host.agent.sessionId;
155
+ const handoffPromptText = renderHandoffPrompt(this.#host.obfuscateTextForProvider(customInstructions));
156
+ const handoffSnapshot: AgentMessage[] = [
157
+ ...this.#host.agent.state.messages,
158
+ {
159
+ role: "user",
160
+ content: [{ type: "text", text: handoffPromptText }],
161
+ attribution: "agent",
162
+ timestamp: Date.now(),
163
+ },
164
+ ];
165
+ const handoffLlmMessages = await this.#host.convertMessagesToLlm(handoffSnapshot, handoffSignal);
166
+ // Base system prompt, not a per-turn `before_agent_start` hook override —
167
+ // the handoff seeds a fresh session and must not carry prompt-specific
168
+ // hook state. Matches the prompt the old handoff path sent.
169
+ const handoffContext = await this.#host.agent.buildSideRequestContext(
170
+ handoffLlmMessages,
171
+ this.#host.baseSystemPrompt(),
172
+ );
173
+ const handoffStreamOptions = this.#host.prepareSimpleStreamOptions(
174
+ {
175
+ apiKey: this.#host.modelRegistry.resolver(model, cacheSessionId),
176
+ sessionId: `${cacheSessionId}:side:${Snowflake.next()}`,
177
+ promptCacheKey: handoffPromptCacheKey,
178
+ preferWebsockets: false,
179
+ serviceTier: this.#host.effectiveServiceTier(model),
180
+ hideThinkingSummary: this.#host.agent.hideThinkingSummary,
181
+ initiatorOverride: "agent",
182
+ signal: handoffSignal,
183
+ },
184
+ model.provider,
185
+ );
186
+ const rawHandoffText = await generateHandoffFromContext(
187
+ obfuscateProviderContext(this.#host.obfuscator, handoffContext),
188
+ model,
189
+ {
190
+ streamOptions: handoffStreamOptions,
191
+ completeImpl: async (requestModel, requestContext, requestOptions) => {
192
+ const stream = await this.#host.sideStreamFn(requestModel, requestContext, requestOptions);
193
+ return stream.result();
194
+ },
195
+ telemetry: resolveTelemetry(this.#host.agent.telemetry, this.#host.sessionId()),
196
+ // Honor the user's /model thinking selection on the handoff path.
197
+ // Clamped per-model inside generateHandoffFromContext via
198
+ // resolveCompactionEffort so unsupported-effort models don't trip
199
+ // requireSupportedEffort.
200
+ thinkingLevel: this.#host.thinkingLevel(),
201
+ },
202
+ );
203
+ const handoffText = this.#host.deobfuscateFromProvider(rawHandoffText);
204
+
205
+ if (handoffSignal.aborted) {
206
+ throw new Error("Handoff cancelled");
207
+ }
208
+ if (!handoffText) {
209
+ return undefined;
210
+ }
211
+
212
+ // Start a new session
213
+ const previousSessionFile = this.#host.sessionFile();
214
+ if (this.#host.extensionRunner?.hasHandlers("session_before_switch")) {
215
+ const result = (await this.#host.extensionRunner.emit({
216
+ type: "session_before_switch",
217
+ reason: "handoff",
218
+ })) as SessionBeforeSwitchResult | undefined;
219
+
220
+ if (result?.cancel) {
221
+ options?.onSwitchCancelled?.();
222
+ return undefined;
223
+ }
224
+ }
225
+ await this.#host.flushPendingBash();
226
+ await this.#host.sessionManager.flush();
227
+ const bashTransition = this.#host.beginBashSessionTransition();
228
+ this.#host.cancelOwnAsyncJobs();
229
+ let sessionTransitioned = false;
230
+ try {
231
+ await this.#host.sessionManager.newSession(
232
+ previousSessionFile ? { parentSession: previousSessionFile } : undefined,
233
+ );
234
+ this.#host.markBashSessionTransition(bashTransition);
235
+ sessionTransitioned = true;
236
+ } finally {
237
+ this.#host.finishBashSessionTransition(bashTransition, sessionTransitioned);
238
+ }
239
+
240
+ this.#host.clearSessionScopedToolState();
241
+
242
+ this.#host.clearCheckpointRuntimeState();
243
+ // agent.reset() clears the core steering/follow-up queues. Preserve any queued
244
+ // steers/follow-ups (RPC/SDK steer()/followUp() issued during the handoff, or a
245
+ // pre-loader TUI steer) so they survive into the post-handoff session instead of
246
+ // being silently dropped. Capture is synchronous immediately before reset and
247
+ // restore is synchronous immediately after — no await gap — so a steer arriving
248
+ // later (during ensureOnDisk/Bun.write below) appends to the restored queue
249
+ // rather than being clobbered.
250
+ const preservedSteering = this.#host.agent.peekSteeringQueue().slice();
251
+ const preservedFollowUp = this.#host.agent.peekFollowUpQueue().slice();
252
+ this.#host.agent.reset();
253
+ this.#host.agent.replaceQueues(preservedSteering, preservedFollowUp);
254
+ this.#host.clearFreshProviderSessionId();
255
+ this.#host.syncAgentSessionId();
256
+ this.#host.rekeyMemoryForCurrentSessionId();
257
+ await this.#host.resetMemoryContextForNewTranscript();
258
+ this.#host.clearPendingNextTurnMessages();
259
+ this.#host.resetTodoCycle();
260
+
261
+ // Inject the handoff document as a custom message
262
+ const handoffContent = createHandoffContext(handoffText);
263
+ this.#host.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true, undefined, "agent");
264
+ await this.#host.sessionManager.ensureOnDisk();
265
+ let savedPath: string | undefined;
266
+ if (options?.autoTriggered && this.#host.settings.get("compaction.handoffSaveToDisk")) {
267
+ const artifactsDir = this.#host.sessionManager.getArtifactsDir();
268
+ if (artifactsDir) {
269
+ const handoffFilePath = path.join(artifactsDir, createHandoffFileName());
270
+ try {
271
+ await Bun.write(handoffFilePath, `${handoffText}\n`);
272
+ savedPath = handoffFilePath;
273
+ } catch (error) {
274
+ logger.warn("Failed to save handoff document to disk", {
275
+ path: handoffFilePath,
276
+ error: error instanceof Error ? error.message : String(error),
277
+ });
278
+ }
279
+ } else {
280
+ logger.debug("Skipping handoff document save because session is not persisted");
281
+ }
282
+ }
283
+
284
+ // Rebuild agent messages from session
285
+ const sessionContext = this.#host.buildDisplaySessionContext();
286
+ this.#host.agent.replaceMessages(sessionContext.messages);
287
+ this.#host.resetAdvisorRuntimes();
288
+ this.#host.syncTodoPhasesFromBranch();
289
+ if (this.#host.extensionRunner) {
290
+ await this.#host.extensionRunner.emit({
291
+ type: "session_switch",
292
+ reason: "handoff",
293
+ previousSessionFile,
294
+ });
295
+ }
296
+
297
+ return { document: handoffText, savedPath };
298
+ } catch (error) {
299
+ if (handoffSignal.aborted || (error instanceof Error && error.name === "AbortError")) {
300
+ throw new Error("Handoff cancelled");
301
+ }
302
+ throw error;
303
+ } finally {
304
+ sourceSignal?.removeEventListener("abort", onSourceAbort);
305
+ this.#handoffAbortController = undefined;
306
+ }
307
+ }
308
+ }
@@ -85,6 +85,10 @@ function oneLine(text: string, max = PRIMARY_ARG_MAX): string {
85
85
  return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
86
86
  }
87
87
 
88
+ export function formatExecutionSourcePreview(source: string): string {
89
+ return oneLine(source);
90
+ }
91
+
88
92
  /** Join the text blocks of a string-or-blocks content field. Images become `[image]`. */
89
93
  function contentToText(content: string | readonly (TextContent | ImageContent)[]): string {
90
94
  if (typeof content === "string") return content;
@@ -110,7 +114,7 @@ function primaryArgValue(value: unknown): string {
110
114
  }
111
115
 
112
116
  /** Pick the most informative scalar argument of a tool call. */
113
- function primaryArg(name: string, args: Record<string, unknown> | undefined): string {
117
+ export function formatToolCallPrimaryArg(name: string, args: Record<string, unknown> | undefined): string {
114
118
  if (!args || typeof args !== "object") return "";
115
119
  // Advisor note is the most informative summary; preserve severity too.
116
120
  if (name === "advise") {
@@ -158,6 +162,15 @@ function primaryArg(name: string, args: Record<string, unknown> | undefined): st
158
162
  }
159
163
  }
160
164
 
165
+ export function formatToolCallIntentPreview(args: Record<string, unknown> | undefined): string | undefined {
166
+ const intent = args?.[INTENT_FIELD];
167
+ return typeof intent === "string" && intent.trim() ? oneLine(intent, 80) : undefined;
168
+ }
169
+
170
+ export function formatToolResultErrorPreview(content: string | readonly (TextContent | ImageContent)[]): string {
171
+ return oneLine(contentToText(content).split("\n", 1)[0] ?? "");
172
+ }
173
+
161
174
  /**
162
175
  * Wrap a diff body in a backtick fence sized to outlast the longest backtick
163
176
  * run inside it, so a diff that touches markdown (triple backticks) can't break
@@ -177,7 +190,7 @@ function toolCallLine(
177
190
  includeToolIntent?: boolean,
178
191
  expandEditDiffs?: boolean,
179
192
  ): string {
180
- const head = `→ ${name}(${primaryArg(name, args)})`;
193
+ const head = `→ ${name}(${formatToolCallPrimaryArg(name, args)})`;
181
194
  let base: string;
182
195
  if (!result) {
183
196
  base = `${head} ⇒ pending`;
@@ -186,7 +199,7 @@ function toolCallLine(
186
199
  const lines = lineCount(text);
187
200
  const count = `${lines} ${lines === 1 ? "line" : "lines"}`;
188
201
  if (result.isError) {
189
- const firstLine = oneLine(text.split("\n", 1)[0] ?? "");
202
+ const firstLine = formatToolResultErrorPreview(result.content);
190
203
  base = firstLine ? `${head} ⇒ error · ${count} — ${firstLine}` : `${head} ⇒ error · ${count}`;
191
204
  } else {
192
205
  base = `${head} ⇒ ok · ${count}`;
@@ -200,11 +213,8 @@ function toolCallLine(
200
213
  }
201
214
  }
202
215
 
203
- const intent = includeToolIntent ? args?.[INTENT_FIELD] : undefined;
204
- if (typeof intent === "string" && intent.trim()) {
205
- const formattedIntent = oneLine(intent, 80);
206
- return `// ${formattedIntent}\n${base}`;
207
- }
216
+ const formattedIntent = includeToolIntent ? formatToolCallIntentPreview(args) : undefined;
217
+ if (formattedIntent) return `// ${formattedIntent}\n${base}`;
208
218
  return base;
209
219
  }
210
220
 
@@ -220,7 +230,8 @@ function executionLine(
220
230
  ? `error · exit ${msg.exitCode}`
221
231
  : "ok";
222
232
  const lines = lineCount(msg.output);
223
- return `→ ${kind}! ${oneLine(source)} ⇒ ${status} · ${lines} ${lines === 1 ? "line" : "lines"}`;
233
+ const sourcePreview = formatExecutionSourcePreview(source);
234
+ return `→ ${kind}! ${sourcePreview} ⇒ ${status} · ${lines} ${lines === 1 ? "line" : "lines"}`;
224
235
  }
225
236
 
226
237
  /**
@@ -3,7 +3,6 @@ import { getBlobsDir, isEnoent, parseJsonlLenient } from "@oh-my-pi/pi-utils";
3
3
  import { BlobStore, isBlobRef, resolveImageData, resolveImageDataUrl } from "./blob-store";
4
4
  import { buildSessionContext } from "./session-context";
5
5
  import {
6
- type CompactionEntry,
7
6
  type FileEntry,
8
7
  type RawFileEntry,
9
8
  SESSION_TITLE_SLOT_BYTES,
@@ -22,8 +21,6 @@ import {
22
21
  } from "./session-title-slot";
23
22
 
24
23
  const STREAM_LOAD_THRESHOLD_BYTES = 8 * 1024 * 1024;
25
- const ELIDED_COMPACTION_SUMMARY = "[Superseded compaction summary elided during session load]";
26
- const ELIDED_COMPACTION_SHORT_SUMMARY = "Superseded compaction elided";
27
24
 
28
25
  function splitTitleSlot(content: string): { body: string; slot: SessionTitleUpdate | undefined } {
29
26
  const slot = titleUpdateFromSlot(parseTitleSlotFromContent(content));
@@ -59,48 +56,6 @@ export function parseSessionContent(content: string): {
59
56
  return { entries: foldTitleSlot(entries, slot), titleSlot: slot };
60
57
  }
61
58
 
62
- function elideCompactionSummary(entry: CompactionEntry | undefined): boolean {
63
- if (!entry) return false;
64
- if (
65
- entry.summary === ELIDED_COMPACTION_SUMMARY &&
66
- entry.shortSummary === ELIDED_COMPACTION_SHORT_SUMMARY &&
67
- entry.preserveData === undefined
68
- ) {
69
- return false;
70
- }
71
- entry.summary = ELIDED_COMPACTION_SUMMARY;
72
- entry.shortSummary = ELIDED_COMPACTION_SHORT_SUMMARY;
73
- entry.preserveData = undefined;
74
- return true;
75
- }
76
-
77
- function collectActiveBranchIds(entries: FileEntry[]): Set<string> {
78
- const byId = new Map<string, SessionEntry>();
79
- for (const entry of entries) {
80
- const id = (entry as SessionEntry).id;
81
- if (typeof id === "string") byId.set(id, entry as SessionEntry);
82
- }
83
- const branchIds = new Set<string>();
84
- let cursor = entries[entries.length - 1] as SessionEntry | undefined;
85
- while (cursor && typeof cursor.id === "string" && !branchIds.has(cursor.id)) {
86
- branchIds.add(cursor.id);
87
- const parentId = cursor.parentId;
88
- cursor = parentId ? byId.get(parentId) : undefined;
89
- }
90
- return branchIds;
91
- }
92
-
93
- function elideSupersededCompactionEntries(entries: FileEntry[]): void {
94
- const branchIds = collectActiveBranchIds(entries);
95
- let previousCompaction: CompactionEntry | undefined;
96
- for (const entry of entries) {
97
- if (entry.type !== "compaction") continue;
98
- if (!branchIds.has(entry.id)) continue;
99
- elideCompactionSummary(previousCompaction);
100
- previousCompaction = entry;
101
- }
102
- }
103
-
104
59
  /** Exported for testing — the ≥8MiB streaming path (works on any file size). */
105
60
  export async function loadEntriesFromFileStream(filePath: string): Promise<{
106
61
  entries: FileEntry[];
@@ -215,7 +170,6 @@ export async function loadEntriesFromFile(
215
170
  throw err;
216
171
  }
217
172
  const { entries } = loaded;
218
- elideSupersededCompactionEntries(entries);
219
173
 
220
174
  // Validate session header
221
175
  if (entries.length === 0) return entries;