@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/README.md +1 -1
- package/dist/cli.js +4685 -4614
- package/dist/types/async/job-manager.d.ts +37 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +41 -0
- package/dist/types/commands/bench.d.ts +15 -3
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/models-config-schema.d.ts +18 -6
- package/dist/types/config/models-config.d.ts +14 -5
- package/dist/types/config/provider-globals.d.ts +2 -2
- package/dist/types/config/service-tier.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +374 -47
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/dap/client.d.ts +20 -0
- package/dist/types/exec/bash-executor.d.ts +38 -0
- package/dist/types/exec/direnv.d.ts +33 -0
- package/dist/types/export/html/args.d.ts +15 -0
- package/dist/types/export/html/index.d.ts +11 -26
- package/dist/types/export/html/web-palette.d.ts +55 -116
- package/dist/types/extensibility/extensions/loader.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
- package/dist/types/live/attestation.d.ts +2 -0
- package/dist/types/live/controller.d.ts +48 -0
- package/dist/types/live/protocol.d.ts +85 -0
- package/dist/types/live/protocol.test.d.ts +1 -0
- package/dist/types/live/transport.d.ts +35 -0
- package/dist/types/live/visualizer.d.ts +30 -0
- package/dist/types/lsp/client.d.ts +16 -1
- package/dist/types/mcp/config-writer.test.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/smithery-auth.d.ts +1 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
- package/dist/types/memory-backend/tool-names.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/custom-message.d.ts +1 -1
- package/dist/types/modes/components/login-dialog.d.ts +2 -2
- package/dist/types/modes/components/message-frame.d.ts +8 -4
- package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
- package/dist/types/modes/components/session-account-selector.d.ts +11 -0
- package/dist/types/modes/components/settings-defs.d.ts +7 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +6 -1
- package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +6 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -1
- package/dist/types/modes/utils/context-usage.d.ts +12 -2
- package/dist/types/plan-mode/plan-files.d.ts +10 -0
- package/dist/types/registry/agent-lifecycle.d.ts +21 -9
- package/dist/types/registry/agent-registry.d.ts +11 -4
- package/dist/types/sdk.d.ts +15 -1
- package/dist/types/secrets/index.d.ts +15 -2
- package/dist/types/secrets/obfuscator.d.ts +113 -15
- package/dist/types/session/acp-permission-gate.d.ts +19 -0
- package/dist/types/session/agent-session-events.d.ts +84 -0
- package/dist/types/session/agent-session-types.d.ts +321 -0
- package/dist/types/session/agent-session.d.ts +135 -753
- package/dist/types/session/async-job-delivery.d.ts +28 -0
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/bash-runner.d.ts +75 -0
- package/dist/types/session/blob-store.d.ts +11 -1
- package/dist/types/session/blob-store.test.d.ts +1 -0
- package/dist/types/session/checkpoint-entries.d.ts +28 -0
- package/dist/types/session/eval-runner.d.ts +51 -0
- package/dist/types/session/irc-bridge.d.ts +46 -0
- package/dist/types/session/messages.d.ts +24 -0
- package/dist/types/session/model-controls.d.ts +180 -0
- package/dist/types/session/prewalk.d.ts +46 -0
- package/dist/types/session/queued-messages.d.ts +22 -0
- package/dist/types/session/redis-session-storage.d.ts +1 -0
- package/dist/types/session/retry-fallback-chains.d.ts +74 -0
- package/dist/types/session/role-models.d.ts +15 -0
- package/dist/types/session/session-advisors.d.ts +228 -0
- package/dist/types/session/session-entries.d.ts +8 -0
- package/dist/types/session/session-handoff.d.ts +70 -0
- package/dist/types/session/session-history-format.d.ts +6 -1
- package/dist/types/session/session-maintenance.d.ts +246 -0
- package/dist/types/session/session-manager.d.ts +41 -0
- package/dist/types/session/session-memory.d.ts +56 -0
- package/dist/types/session/session-provider-boundary.d.ts +62 -0
- package/dist/types/session/session-stats.d.ts +48 -0
- package/dist/types/session/session-tools.d.ts +158 -0
- package/dist/types/session/session-workspace.d.ts +32 -0
- package/dist/types/session/stream-guards.d.ts +44 -0
- package/dist/types/session/streaming-output.d.ts +8 -0
- package/dist/types/session/todo-tracker.d.ts +56 -0
- package/dist/types/session/ttsr-coordinator.d.ts +51 -0
- package/dist/types/session/turn-recovery.d.ts +225 -0
- package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
- package/dist/types/stt/index.d.ts +0 -2
- package/dist/types/stt/stt-controller.d.ts +7 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +4 -1
- package/dist/types/task/output-manager.d.ts +3 -1
- package/dist/types/tiny/title-client.d.ts +10 -0
- package/dist/types/tools/approval.d.ts +1 -0
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/computer/protocol.d.ts +43 -0
- package/dist/types/tools/computer/supervisor.d.ts +32 -0
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +15 -0
- package/dist/types/tools/computer-renderer.d.ts +22 -0
- package/dist/types/tools/computer.d.ts +71 -0
- package/dist/types/tools/context.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +21 -0
- package/dist/types/tools/image-gen.d.ts +4 -4
- package/dist/types/tools/image-providers.d.ts +38 -0
- package/dist/types/tools/index.d.ts +7 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/todo.d.ts +10 -4
- package/dist/types/tools/xdev.d.ts +5 -1
- package/dist/types/tts/streaming-player.d.ts +10 -29
- package/dist/types/tts/vocalizer.d.ts +5 -0
- package/dist/types/utils/jj.d.ts +29 -0
- package/dist/types/utils/lang-from-path.d.ts +1 -0
- package/dist/types/utils/title-generator.d.ts +30 -0
- package/dist/types/utils/tools-manager.d.ts +1 -2
- package/dist/types/vibe/runtime.d.ts +36 -8
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +15 -5
- package/dist/types/web/search/types.d.ts +94 -0
- package/package.json +12 -12
- package/scripts/generate-share-viewer.ts +4 -6
- package/src/advisor/__tests__/advisor.test.ts +643 -0
- package/src/advisor/runtime.ts +144 -43
- package/src/async/job-manager.ts +90 -2
- package/src/cli/args.ts +3 -0
- package/src/cli/auth-gateway-cli.ts +18 -3
- package/src/cli/bench-cli.ts +395 -29
- package/src/cli/flag-tables.ts +3 -0
- package/src/cli/gc-cli.ts +2 -2
- package/src/cli/setup-cli.ts +2 -14
- package/src/cli/usage-cli.ts +10 -1
- package/src/cli.ts +8 -0
- package/src/collab/host.ts +3 -2
- package/src/commands/bench.ts +18 -4
- package/src/commands/launch.ts +4 -0
- package/src/config/model-registry.ts +16 -2
- package/src/config/models-config-schema.ts +15 -4
- package/src/config/provider-globals.ts +9 -9
- package/src/config/service-tier.ts +26 -1
- package/src/config/settings-schema.ts +270 -51
- package/src/config/settings.ts +81 -2
- package/src/dap/client.ts +37 -4
- package/src/edit/index.ts +49 -12
- package/src/eval/executor-base.ts +1 -0
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/py/executor.ts +103 -33
- package/src/exec/bash-executor.ts +90 -6
- package/src/exec/direnv.ts +145 -0
- package/src/export/html/args.ts +20 -0
- package/src/export/html/index.ts +45 -47
- package/src/export/html/template.css +19 -1
- package/src/export/html/template.html +6 -0
- package/src/export/html/template.js +21 -0
- package/src/export/html/tool-views.generated.js +31 -31
- package/src/export/html/web-palette.ts +116 -132
- package/src/export/share.ts +249 -49
- package/src/extensibility/extensions/loader.ts +29 -1
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +33 -2
- package/src/extensibility/extensions/wrapper.ts +68 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
- package/src/hindsight/state.ts +64 -6
- package/src/internal-urls/local-protocol.ts +2 -1
- package/src/internal-urls/memory-protocol.ts +2 -2
- package/src/internal-urls/skill-protocol.ts +2 -2
- package/src/internal-urls/ssh-protocol.ts +2 -1
- package/src/internal-urls/vault-protocol.ts +2 -1
- package/src/live/attestation.ts +91 -0
- package/src/live/controller.ts +517 -0
- package/src/live/prompts/agent-final-message.md +3 -0
- package/src/live/prompts/live-instructions.md +23 -0
- package/src/live/protocol.test.ts +140 -0
- package/src/live/protocol.ts +233 -0
- package/src/live/transport.ts +422 -0
- package/src/live/visualizer.ts +214 -0
- package/src/lsp/client.ts +29 -9
- package/src/lsp/index.ts +19 -4
- package/src/main.ts +8 -1
- package/src/mcp/config-writer.test.ts +43 -0
- package/src/mcp/config-writer.ts +109 -82
- package/src/mcp/oauth-discovery.ts +10 -2
- package/src/mcp/smithery-auth.test.ts +29 -0
- package/src/mcp/smithery-auth.ts +3 -2
- package/src/memories/index.ts +40 -20
- package/src/memory-backend/tool-names.ts +2 -0
- package/src/mnemopi/backend.ts +24 -7
- package/src/modes/acp/acp-agent.ts +56 -10
- package/src/modes/acp/acp-event-mapper.ts +8 -1
- package/src/modes/components/agent-hub.ts +1 -1
- package/src/modes/components/assistant-message.ts +7 -2
- package/src/modes/components/custom-message.ts +4 -1
- package/src/modes/components/login-dialog.ts +14 -4
- package/src/modes/components/message-frame.ts +14 -8
- package/src/modes/components/plan-review-overlay.ts +350 -35
- package/src/modes/components/session-account-selector.ts +62 -0
- package/src/modes/components/settings-defs.ts +23 -1
- package/src/modes/components/settings-selector.ts +169 -1
- package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
- package/src/modes/components/status-line/component.ts +123 -2
- package/src/modes/components/tool-execution.ts +28 -112
- package/src/modes/controllers/command-controller.ts +56 -15
- package/src/modes/controllers/event-controller.ts +111 -16
- package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -4
- package/src/modes/controllers/input-controller.ts +47 -39
- package/src/modes/controllers/live-command-controller.ts +255 -0
- package/src/modes/controllers/mcp-command-controller.ts +10 -1
- package/src/modes/controllers/omfg-controller.ts +44 -40
- package/src/modes/controllers/selector-controller.ts +80 -10
- package/src/modes/controllers/session-focus-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +176 -38
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/context-usage.ts +23 -7
- package/src/modes/utils/ui-helpers.ts +10 -5
- package/src/plan-mode/plan-files.ts +40 -0
- package/src/prompts/bench/cache-prefix-chunk.md +1 -0
- package/src/prompts/bench/cache-prefix.md +3 -0
- package/src/prompts/bench/cache-suffix.md +1 -0
- package/src/prompts/system/computer-safety.md +14 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/project-prompt.md +8 -1
- package/src/prompts/system/subagent-async-pending.md +6 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/workflow-notice.md +48 -40
- package/src/prompts/system/xdev-mount-notice.md +4 -0
- package/src/prompts/tools/computer.md +26 -0
- package/src/prompts/tools/todo.md +3 -1
- package/src/registry/agent-lifecycle.ts +116 -46
- package/src/registry/agent-registry.ts +35 -8
- package/src/registry/persisted-agents.ts +26 -2
- package/src/sdk.ts +215 -136
- package/src/secrets/index.ts +127 -4
- package/src/secrets/obfuscator.ts +2328 -105
- package/src/session/acp-permission-gate.ts +165 -0
- package/src/session/agent-session-error-log.test.ts +1 -1
- package/src/session/agent-session-events.ts +66 -0
- package/src/session/agent-session-types.ts +343 -0
- package/src/session/agent-session.ts +1940 -12218
- package/src/session/artifacts.ts +6 -5
- package/src/session/async-job-delivery.ts +74 -0
- package/src/session/auth-storage.ts +1 -0
- package/src/session/bash-runner.ts +326 -0
- package/src/session/blob-store.test.ts +56 -0
- package/src/session/blob-store.ts +18 -2
- package/src/session/checkpoint-entries.ts +81 -0
- package/src/session/eval-runner.ts +217 -0
- package/src/session/indexed-session-storage.ts +9 -2
- package/src/session/irc-bridge.ts +203 -0
- package/src/session/messages.ts +266 -1
- package/src/session/model-controls.ts +714 -0
- package/src/session/prewalk.ts +252 -0
- package/src/session/queued-messages.ts +93 -0
- package/src/session/redis-session-storage.ts +51 -11
- package/src/session/retry-fallback-chains.ts +455 -0
- package/src/session/role-models.ts +85 -0
- package/src/session/session-advisors.ts +1679 -0
- package/src/session/session-context.ts +6 -3
- package/src/session/session-entries.ts +8 -0
- package/src/session/session-handoff.ts +308 -0
- package/src/session/session-history-format.ts +20 -9
- package/src/session/session-loader.ts +0 -46
- package/src/session/session-maintenance.ts +2983 -0
- package/src/session/session-manager.ts +390 -30
- package/src/session/session-memory.ts +222 -0
- package/src/session/session-provider-boundary.ts +307 -0
- package/src/session/session-stats.ts +293 -0
- package/src/session/session-tools.ts +980 -0
- package/src/session/session-workspace.ts +53 -0
- package/src/session/stream-guards.ts +417 -0
- package/src/session/streaming-output.ts +52 -5
- package/src/session/todo-tracker.ts +380 -0
- package/src/session/ttsr-coordinator.ts +496 -0
- package/src/session/turn-recovery.ts +1629 -0
- package/src/slash-commands/builtin-registry.ts +257 -20
- package/src/slash-commands/helpers/session-pin.ts +44 -0
- package/src/slash-commands/helpers/usage-report.ts +22 -3
- package/src/stt/downloader.ts +0 -2
- package/src/stt/index.ts +0 -2
- package/src/stt/stt-controller.ts +57 -146
- package/src/system-prompt.ts +45 -17
- package/src/task/executor.ts +285 -70
- package/src/task/index.ts +98 -105
- package/src/task/output-manager.ts +25 -3
- package/src/task/persisted-revive.ts +4 -3
- package/src/task/structured-subagent.ts +1 -0
- package/src/tiny/title-client.ts +22 -0
- package/src/tools/approval.ts +55 -10
- package/src/tools/bash-interactive.ts +116 -86
- package/src/tools/bash-skill-urls.ts +28 -2
- package/src/tools/bash.ts +339 -123
- package/src/tools/browser/launch.ts +9 -1
- package/src/tools/browser/tab-supervisor.ts +8 -5
- package/src/tools/browser.ts +94 -47
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/computer/protocol.ts +28 -0
- package/src/tools/computer/supervisor.ts +258 -0
- package/src/tools/computer/worker-entry.ts +25 -0
- package/src/tools/computer/worker.ts +135 -0
- package/src/tools/computer-renderer.ts +108 -0
- package/src/tools/computer.ts +433 -0
- package/src/tools/context.ts +2 -0
- package/src/tools/default-renderer.ts +139 -0
- package/src/tools/essential-tools.ts +1 -0
- package/src/tools/image-gen.ts +20 -28
- package/src/tools/image-providers.ts +50 -0
- package/src/tools/index.ts +14 -0
- package/src/tools/path-utils.ts +49 -2
- package/src/tools/read.ts +156 -88
- package/src/tools/renderers.ts +9 -1
- package/src/tools/todo.ts +101 -11
- package/src/tools/vibe.ts +1 -2
- package/src/tools/write.ts +51 -1
- package/src/tools/xdev.ts +121 -29
- package/src/tts/streaming-player.ts +82 -283
- package/src/tts/vocalizer.ts +18 -1
- package/src/utils/clipboard.ts +1 -30
- package/src/utils/jj.ts +125 -4
- package/src/utils/lang-from-path.ts +7 -0
- package/src/utils/mac-file-urls.applescript +37 -0
- package/src/utils/title-generator.ts +137 -1
- package/src/utils/tool-choice.ts +14 -0
- package/src/utils/tools-manager.ts +1 -19
- package/src/vibe/runtime.ts +852 -72
- package/src/web/search/index.ts +3 -6
- package/src/web/search/provider.ts +31 -16
- package/src/web/search/providers/xai.ts +7 -1
- package/src/web/search/types.ts +3 -0
- package/dist/types/stt/recorder.d.ts +0 -30
- package/dist/types/stt/transcriber.d.ts +0 -14
- package/dist/types/stt/wav.d.ts +0 -29
- package/dist/types/tts/player.d.ts +0 -32
- package/src/stt/recorder.ts +0 -551
- package/src/stt/transcriber.ts +0 -60
- package/src/stt/wav.ts +0 -173
- package/src/tts/player.ts +0 -137
|
@@ -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
|
|
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
|
|
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}(${
|
|
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 =
|
|
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
|
|
204
|
-
if (
|
|
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
|
-
|
|
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;
|