@oh-my-pi/pi-coding-agent 15.12.3 → 15.13.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.
- package/CHANGELOG.md +347 -7
- package/dist/cli.js +1615 -1231
- package/dist/types/async/job-manager.d.ts +15 -0
- package/dist/types/autolearn/controller.d.ts +25 -0
- package/dist/types/autolearn/managed-skills.d.ts +45 -0
- package/dist/types/autoresearch/state.d.ts +1 -1
- package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/log-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/run-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/update-notes.d.ts +1 -1
- package/dist/types/autoresearch/types.d.ts +1 -1
- package/dist/types/cli/args.d.ts +19 -2
- package/dist/types/cli/models-cli.d.ts +49 -0
- package/dist/types/cli/session-picker.d.ts +1 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/cli/setup-model-picker.d.ts +14 -0
- package/dist/types/collab/protocol.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +0 -3
- package/dist/types/commands/models.d.ts +33 -0
- package/dist/types/commands/say.d.ts +24 -0
- package/dist/types/commands/token.d.ts +25 -0
- package/dist/types/commit/agentic/tools/analyze-file.d.ts +1 -1
- package/dist/types/commit/agentic/tools/git-file-diff.d.ts +1 -1
- package/dist/types/commit/agentic/tools/git-hunk.d.ts +1 -1
- package/dist/types/commit/agentic/tools/git-overview.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-changelog.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +1 -1
- package/dist/types/commit/agentic/tools/recent-commits.d.ts +1 -1
- package/dist/types/commit/agentic/tools/schemas.d.ts +1 -1
- package/dist/types/commit/agentic/tools/split-commit.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +1 -1
- package/dist/types/commit/shared-llm.d.ts +1 -1
- package/dist/types/config/keybindings.d.ts +3 -3
- package/dist/types/config/model-registry.d.ts +17 -0
- package/dist/types/config/models-config-schema.d.ts +13 -1
- package/dist/types/config/models-config.d.ts +8 -2
- package/dist/types/config/settings-schema.d.ts +281 -58
- package/dist/types/edit/hashline/params.d.ts +1 -1
- package/dist/types/edit/modes/apply-patch.d.ts +1 -1
- package/dist/types/edit/modes/patch.d.ts +1 -1
- package/dist/types/edit/modes/replace.d.ts +1 -1
- package/dist/types/export/html/index.d.ts +2 -1
- package/dist/types/extensibility/custom-commands/types.d.ts +2 -2
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +49 -3
- package/dist/types/extensibility/hooks/index.d.ts +2 -1
- package/dist/types/extensibility/hooks/types.d.ts +2 -2
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
- package/dist/types/extensibility/plugins/loader.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/extensibility/skills.d.ts +10 -0
- package/dist/types/goals/guided-setup.d.ts +18 -0
- package/dist/types/goals/state.d.ts +1 -1
- package/dist/types/goals/tools/goal-tool.d.ts +1 -1
- package/dist/types/hindsight/transcript.d.ts +1 -1
- package/dist/types/index.d.ts +5 -0
- package/dist/types/internal-urls/local-protocol.d.ts +4 -2
- package/dist/types/lsp/types.d.ts +1 -1
- package/dist/types/main.d.ts +4 -3
- package/dist/types/mcp/manager.d.ts +8 -0
- package/dist/types/mcp/startup-events.d.ts +11 -0
- package/dist/types/memories/index.d.ts +7 -0
- package/dist/types/memory-backend/local-backend.d.ts +4 -3
- package/dist/types/mnemopi/config.d.ts +28 -0
- package/dist/types/modes/acp/acp-agent.d.ts +1 -2
- package/dist/types/modes/components/agent-hub.d.ts +6 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
- package/dist/types/modes/components/custom-editor.d.ts +39 -1
- package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/logout-account-selector.d.ts +8 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/component.d.ts +9 -5
- package/dist/types/modes/components/status-line/types.d.ts +2 -1
- package/dist/types/modes/components/tool-execution.d.ts +26 -16
- package/dist/types/modes/components/transcript-container.d.ts +23 -2
- package/dist/types/modes/components/tree-selector.d.ts +1 -1
- package/dist/types/modes/components/usage-row.d.ts +3 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -2
- package/dist/types/modes/controllers/event-controller.d.ts +0 -17
- package/dist/types/modes/controllers/input-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/gradient-highlight.d.ts +9 -4
- package/dist/types/modes/image-references.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +27 -6
- package/dist/types/modes/magic-keywords.d.ts +13 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
- package/dist/types/modes/runtime-init.d.ts +4 -0
- package/dist/types/modes/theme/theme.d.ts +13 -2
- package/dist/types/modes/types.d.ts +8 -7
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/registry/agent-registry.d.ts +17 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +28 -35
- package/dist/types/session/agent-storage.d.ts +2 -1
- package/dist/types/session/indexed-session-storage.d.ts +3 -3
- package/dist/types/session/messages.d.ts +8 -10
- package/dist/types/session/session-context.d.ts +39 -0
- package/dist/types/session/session-entries.d.ts +159 -0
- package/dist/types/session/session-listing.d.ts +69 -0
- package/dist/types/session/session-loader.d.ts +16 -0
- package/dist/types/session/session-manager.d.ts +85 -462
- package/dist/types/session/session-migrations.d.ts +12 -0
- package/dist/types/session/session-paths.d.ts +25 -0
- package/dist/types/session/session-persistence.d.ts +8 -0
- package/dist/types/session/session-storage.d.ts +11 -7
- package/dist/types/session/snapcompact-inline.d.ts +12 -1
- package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
- package/dist/types/session/tool-choice-queue.d.ts +6 -6
- package/dist/types/slash-commands/helpers/logout.d.ts +15 -0
- package/dist/types/stt/asr-client.d.ts +90 -0
- package/dist/types/stt/asr-protocol.d.ts +97 -0
- package/dist/types/stt/asr-worker.d.ts +2 -0
- package/dist/types/stt/downloader.d.ts +38 -0
- package/dist/types/stt/endpointer.d.ts +59 -0
- package/dist/types/stt/index.d.ts +5 -1
- package/dist/types/stt/models.d.ts +120 -0
- package/dist/types/stt/recorder.d.ts +17 -0
- package/dist/types/stt/stt-controller.d.ts +6 -0
- package/dist/types/stt/transcriber.d.ts +5 -7
- package/dist/types/stt/wav.d.ts +29 -0
- package/dist/types/system-prompt.d.ts +4 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/task/index.d.ts +9 -1
- package/dist/types/task/types.d.ts +37 -1
- package/dist/types/tools/ask.d.ts +1 -1
- package/dist/types/tools/ast-edit.d.ts +1 -1
- package/dist/types/tools/ast-grep.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +3 -3
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +202 -0
- package/dist/types/tools/browser/cmux/rpc.d.ts +70 -0
- package/dist/types/tools/browser/cmux/socket-client.d.ts +19 -0
- package/dist/types/tools/browser/registry.d.ts +16 -3
- package/dist/types/tools/browser/render.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +16 -4
- package/dist/types/tools/browser.d.ts +3 -1
- package/dist/types/tools/checkpoint.d.ts +1 -1
- package/dist/types/tools/debug.d.ts +1 -1
- package/dist/types/tools/eval-render.d.ts +1 -1
- package/dist/types/tools/eval.d.ts +1 -1
- package/dist/types/tools/find.d.ts +1 -1
- package/dist/types/tools/gh.d.ts +1 -1
- package/dist/types/tools/image-gen.d.ts +1 -1
- package/dist/types/tools/index.d.ts +14 -2
- package/dist/types/tools/inspect-image.d.ts +1 -1
- package/dist/types/tools/irc.d.ts +2 -1
- package/dist/types/tools/job.d.ts +1 -1
- package/dist/types/tools/learn.d.ts +51 -0
- package/dist/types/tools/manage-skill.d.ts +40 -0
- package/dist/types/tools/memory-edit.d.ts +1 -1
- package/dist/types/tools/memory-recall.d.ts +1 -1
- package/dist/types/tools/memory-reflect.d.ts +1 -1
- package/dist/types/tools/memory-retain.d.ts +1 -1
- package/dist/types/tools/plan-mode-guard.d.ts +10 -0
- package/dist/types/tools/read.d.ts +1 -1
- package/dist/types/tools/render-mermaid.d.ts +1 -1
- package/dist/types/tools/renderers.d.ts +7 -11
- package/dist/types/tools/resolve.d.ts +1 -1
- package/dist/types/tools/review.d.ts +1 -1
- package/dist/types/tools/search-tool-bm25.d.ts +1 -1
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/tools/ssh.d.ts +2 -2
- package/dist/types/tools/todo.d.ts +2 -2
- package/dist/types/tools/tts.d.ts +26 -1
- package/dist/types/tools/write.d.ts +2 -2
- package/dist/types/tts/downloader.d.ts +20 -0
- package/dist/types/tts/index.d.ts +8 -0
- package/dist/types/tts/models.d.ts +82 -0
- package/dist/types/tts/player.d.ts +32 -0
- package/dist/types/tts/runtime.d.ts +6 -0
- package/dist/types/tts/streaming-player.d.ts +41 -0
- package/dist/types/tts/tts-client.d.ts +93 -0
- package/dist/types/tts/tts-protocol.d.ts +95 -0
- package/dist/types/tts/tts-worker.d.ts +2 -0
- package/dist/types/tts/vocalizer.d.ts +41 -0
- package/dist/types/tts/wav.d.ts +8 -0
- package/dist/types/utils/clipboard.d.ts +4 -3
- package/dist/types/utils/image-loading.d.ts +18 -1
- package/dist/types/utils/thinking-display.d.ts +17 -0
- package/dist/types/utils/tool-choice.d.ts +8 -0
- package/dist/types/utils/tools-manager.d.ts +2 -1
- package/dist/types/utils/tools-manager.test.d.ts +1 -0
- package/dist/types/web/scrapers/github.d.ts +1 -1
- package/dist/types/web/search/index.d.ts +1 -1
- package/package.json +17 -16
- package/src/async/job-manager.ts +49 -0
- package/src/autolearn/controller.ts +139 -0
- package/src/autolearn/managed-skills.ts +257 -0
- package/src/autoresearch/state.ts +1 -1
- package/src/autoresearch/storage.ts +2 -1
- package/src/autoresearch/tools/init-experiment.ts +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/autoresearch/tools/run-experiment.ts +1 -1
- package/src/autoresearch/tools/update-notes.ts +1 -1
- package/src/autoresearch/types.ts +1 -1
- package/src/cli/args.ts +56 -10
- package/src/cli/auth-gateway-cli.ts +1 -1
- package/src/cli/bench-cli.ts +1 -1
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/models-cli.ts +427 -0
- package/src/cli/session-picker.ts +2 -1
- package/src/cli/setup-cli.ts +148 -47
- package/src/cli/setup-model-picker.ts +43 -0
- package/src/cli-commands.ts +3 -0
- package/src/cli.ts +45 -13
- package/src/collab/host.ts +10 -13
- package/src/collab/protocol.ts +1 -1
- package/src/commands/launch.ts +0 -3
- package/src/commands/models.ts +61 -0
- package/src/commands/say.ts +102 -0
- package/src/commands/setup.ts +1 -1
- package/src/commands/token.ts +89 -0
- package/src/commit/agentic/tools/analyze-file.ts +4 -1
- package/src/commit/agentic/tools/git-file-diff.ts +1 -1
- package/src/commit/agentic/tools/git-hunk.ts +1 -1
- package/src/commit/agentic/tools/git-overview.ts +1 -1
- package/src/commit/agentic/tools/propose-changelog.ts +1 -1
- package/src/commit/agentic/tools/propose-commit.ts +1 -1
- package/src/commit/agentic/tools/recent-commits.ts +1 -1
- package/src/commit/agentic/tools/schemas.ts +1 -1
- package/src/commit/agentic/tools/split-commit.ts +1 -1
- package/src/commit/analysis/summary.ts +1 -1
- package/src/commit/changelog/generate.ts +1 -1
- package/src/commit/shared-llm.ts +1 -1
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-discovery.ts +11 -5
- package/src/config/model-registry.ts +79 -21
- package/src/config/model-resolver.ts +2 -2
- package/src/config/models-config-schema.ts +5 -2
- package/src/config/models-config.ts +2 -1
- package/src/config/settings-schema.ts +266 -32
- package/src/config/settings.ts +10 -0
- package/src/discovery/builtin.ts +23 -1
- package/src/discovery/claude-plugins.ts +44 -5
- package/src/discovery/helpers.ts +41 -1
- package/src/edit/hashline/params.ts +1 -1
- package/src/edit/modes/apply-patch.ts +1 -1
- package/src/edit/modes/patch.ts +1 -1
- package/src/edit/modes/replace.ts +1 -1
- package/src/eval/__tests__/budget-bridge.test.ts +1 -1
- package/src/eval/agent-bridge.ts +1 -1
- package/src/eval/completion-bridge.ts +1 -1
- package/src/eval/js/shared/prelude.txt +69 -17
- package/src/export/html/index.ts +3 -6
- package/src/export/html/template.js +24 -2
- package/src/export/html/tool-views.generated.js +2 -2
- package/src/extensibility/custom-commands/loader.ts +1 -1
- package/src/extensibility/custom-commands/types.ts +2 -2
- package/src/extensibility/custom-tools/loader.ts +1 -1
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/loader.ts +2 -2
- package/src/extensibility/extensions/model-api.ts +41 -0
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +54 -3
- package/src/extensibility/extensions/wrapper.ts +41 -5
- package/src/extensibility/hooks/index.ts +2 -1
- package/src/extensibility/hooks/loader.ts +1 -1
- package/src/extensibility/hooks/types.ts +2 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
- package/src/extensibility/plugins/loader.ts +30 -19
- package/src/extensibility/plugins/manager.ts +221 -90
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +101 -5
- package/src/goals/guided-setup.ts +133 -0
- package/src/goals/state.ts +1 -1
- package/src/goals/tools/goal-tool.ts +1 -1
- package/src/hindsight/transcript.ts +1 -1
- package/src/index.ts +5 -0
- package/src/internal-urls/docs-index.generated.ts +13 -10
- package/src/internal-urls/history-protocol.ts +1 -1
- package/src/internal-urls/local-protocol.ts +29 -7
- package/src/lsp/types.ts +1 -1
- package/src/main.ts +27 -32
- package/src/mcp/config-writer.ts +7 -3
- package/src/mcp/manager.ts +11 -0
- package/src/mcp/startup-events.ts +21 -0
- package/src/mcp/transports/stdio.ts +2 -1
- package/src/memories/index.ts +149 -12
- package/src/memories/storage.ts +2 -1
- package/src/memory-backend/local-backend.ts +11 -5
- package/src/mnemopi/backend.ts +1 -0
- package/src/mnemopi/config.ts +112 -12
- package/src/modes/acp/acp-agent.ts +8 -53
- package/src/modes/acp/acp-event-mapper.ts +5 -1
- package/src/modes/components/agent-hub.ts +51 -5
- package/src/modes/components/assistant-message.ts +12 -44
- package/src/modes/components/compaction-summary-message.ts +125 -26
- package/src/modes/components/custom-editor.test.ts +96 -0
- package/src/modes/components/custom-editor.ts +164 -8
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/logout-account-selector.ts +130 -0
- package/src/modes/components/mcp-add-wizard.ts +1 -1
- package/src/modes/components/model-selector.ts +2 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/status-line/component.ts +54 -157
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line/types.ts +2 -1
- package/src/modes/components/tool-execution.ts +82 -43
- package/src/modes/components/transcript-container.ts +70 -1
- package/src/modes/components/tree-selector.ts +1 -1
- package/src/modes/components/usage-row.ts +18 -0
- package/src/modes/components/user-message.ts +4 -2
- package/src/modes/controllers/command-controller.ts +14 -16
- package/src/modes/controllers/event-controller.ts +101 -73
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/input-controller.ts +311 -57
- package/src/modes/controllers/mcp-command-controller.ts +44 -3
- package/src/modes/controllers/selector-controller.ts +68 -12
- package/src/modes/controllers/streaming-reveal.ts +4 -3
- package/src/modes/gradient-highlight.ts +21 -9
- package/src/modes/image-references.ts +20 -0
- package/src/modes/interactive-mode.ts +288 -48
- package/src/modes/magic-keywords.ts +27 -5
- package/src/modes/rpc/rpc-mode.ts +146 -14
- package/src/modes/rpc/rpc-subagents.ts +2 -2
- package/src/modes/rpc/rpc-types.ts +8 -2
- package/src/modes/runtime-init.ts +28 -3
- package/src/modes/theme/theme.ts +99 -51
- package/src/modes/types.ts +6 -7
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +36 -7
- package/src/priority.json +5 -1
- package/src/prompts/agents/task.md +1 -0
- package/src/prompts/goals/guided-goal-interview.md +8 -0
- package/src/prompts/goals/guided-goal-system.md +12 -0
- package/src/prompts/memories/read-path.md +6 -0
- package/src/prompts/system/autolearn-guidance-learn.md +1 -0
- package/src/prompts/system/autolearn-guidance.md +7 -0
- package/src/prompts/system/autolearn-nudge.md +3 -0
- package/src/prompts/system/eager-task.md +7 -0
- package/src/prompts/system/eager-todo.md +11 -6
- package/src/prompts/system/empty-stop-retry.md +4 -6
- package/src/prompts/system/subagent-system-prompt.md +4 -0
- package/src/prompts/system/system-prompt.md +10 -5
- package/src/prompts/system/title-marker-instruction.md +1 -0
- package/src/prompts/system/title-system-marker.md +16 -0
- package/src/prompts/tools/job.md +1 -0
- package/src/prompts/tools/learn.md +7 -0
- package/src/prompts/tools/manage-skill.md +9 -0
- package/src/prompts/tools/task.md +3 -0
- package/src/registry/agent-registry.ts +30 -0
- package/src/sdk.ts +103 -43
- package/src/secrets/obfuscator.ts +1 -1
- package/src/session/agent-session.ts +331 -318
- package/src/session/agent-storage.ts +18 -9
- package/src/session/history-storage.ts +3 -2
- package/src/session/indexed-session-storage.ts +7 -10
- package/src/session/messages.ts +9 -11
- package/src/session/session-context.ts +352 -0
- package/src/session/session-dump-format.ts +4 -2
- package/src/session/session-entries.ts +194 -0
- package/src/session/session-listing.ts +588 -0
- package/src/session/session-loader.ts +106 -0
- package/src/session/session-manager.ts +968 -3064
- package/src/session/session-migrations.ts +78 -0
- package/src/session/session-paths.ts +193 -0
- package/src/session/session-persistence.ts +131 -0
- package/src/session/session-storage.ts +91 -30
- package/src/session/snapcompact-inline.ts +21 -1
- package/src/session/snapcompact-savings-journal.ts +113 -0
- package/src/session/tool-choice-queue.ts +23 -11
- package/src/slash-commands/builtin-registry.ts +40 -4
- package/src/slash-commands/helpers/logout.ts +88 -0
- package/src/stt/asr-client.ts +520 -0
- package/src/stt/asr-protocol.ts +65 -0
- package/src/stt/asr-worker.ts +790 -0
- package/src/stt/downloader.ts +107 -47
- package/src/stt/endpointer.ts +259 -0
- package/src/stt/index.ts +5 -1
- package/src/stt/models.ts +150 -0
- package/src/stt/recorder.ts +247 -60
- package/src/stt/stt-controller.ts +201 -22
- package/src/stt/transcriber.ts +37 -68
- package/src/stt/wav.ts +173 -0
- package/src/system-prompt.ts +8 -0
- package/src/task/agents.ts +1 -2
- package/src/task/executor.ts +49 -15
- package/src/task/index.ts +60 -6
- package/src/task/render.ts +83 -8
- package/src/task/types.ts +54 -1
- package/src/tools/ask.ts +9 -1
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/ast-grep.ts +1 -1
- package/src/tools/bash.ts +5 -4
- package/src/tools/browser/cmux/cmux-tab.ts +1264 -0
- package/src/tools/browser/cmux/rpc.ts +156 -0
- package/src/tools/browser/cmux/socket-client.ts +309 -0
- package/src/tools/browser/registry.ts +37 -3
- package/src/tools/browser/render.ts +6 -1
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +189 -18
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/browser.ts +16 -1
- package/src/tools/checkpoint.ts +1 -1
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval-render.ts +4 -3
- package/src/tools/eval.ts +11 -6
- package/src/tools/fetch.ts +13 -2
- package/src/tools/find.ts +1 -1
- package/src/tools/gh.ts +1 -1
- package/src/tools/github-cache.ts +2 -1
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +43 -5
- package/src/tools/inspect-image.ts +3 -1
- package/src/tools/irc.ts +11 -3
- package/src/tools/job.ts +15 -3
- package/src/tools/learn.ts +144 -0
- package/src/tools/manage-skill.ts +104 -0
- package/src/tools/memory-edit.ts +1 -1
- package/src/tools/memory-recall.ts +1 -1
- package/src/tools/memory-reflect.ts +1 -1
- package/src/tools/memory-retain.ts +1 -1
- package/src/tools/plan-mode-guard.ts +53 -19
- package/src/tools/read.ts +8 -2
- package/src/tools/render-mermaid.ts +1 -1
- package/src/tools/renderers.ts +7 -11
- package/src/tools/report-tool-issue.ts +3 -2
- package/src/tools/resolve.ts +1 -1
- package/src/tools/review.ts +1 -1
- package/src/tools/search-tool-bm25.ts +1 -1
- package/src/tools/search.ts +1 -1
- package/src/tools/ssh.ts +5 -4
- package/src/tools/todo.ts +2 -2
- package/src/tools/tts.ts +204 -93
- package/src/tools/write.ts +19 -3
- package/src/tts/downloader.ts +64 -0
- package/src/tts/index.ts +8 -0
- package/src/tts/models.ts +137 -0
- package/src/tts/player.ts +137 -0
- package/src/tts/runtime.ts +21 -0
- package/src/tts/streaming-player.ts +266 -0
- package/src/tts/tts-client.ts +647 -0
- package/src/tts/tts-protocol.ts +60 -0
- package/src/tts/tts-worker.ts +497 -0
- package/src/tts/vocalizer.ts +162 -0
- package/src/tts/wav.ts +58 -0
- package/src/utils/clipboard.ts +35 -18
- package/src/utils/image-loading.ts +35 -4
- package/src/utils/thinking-display.ts +37 -0
- package/src/utils/title-generator.ts +48 -5
- package/src/utils/tool-choice.ts +16 -0
- package/src/utils/tools-manager.test.ts +25 -0
- package/src/utils/tools-manager.ts +19 -1
- package/src/web/scrapers/github.ts +96 -0
- package/src/web/search/index.ts +14 -1
- package/src/web/search/providers/searxng.ts +13 -1
- package/dist/types/cli/list-models.d.ts +0 -30
- package/dist/types/stt/setup.d.ts +0 -18
- package/src/cli/list-models.ts +0 -194
- package/src/stt/setup.ts +0 -52
- package/src/stt/transcribe.py +0 -70
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Snowflake } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { type CompactionEntry, CURRENT_SESSION_VERSION, type FileEntry, type SessionHeader } from "./session-entries";
|
|
3
|
+
|
|
4
|
+
/** Generate a unique short ID (8 hex chars, collision-checked) */
|
|
5
|
+
export function generateId(byId: { has(id: string): boolean }): string {
|
|
6
|
+
for (let i = 0; i < 100; i++) {
|
|
7
|
+
const id = crypto.randomUUID().slice(-8);
|
|
8
|
+
if (!byId.has(id)) return id;
|
|
9
|
+
}
|
|
10
|
+
return Snowflake.next(); // fallback to full snowflake id
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Migrate v1 → v2: add id/parentId tree structure. Mutates in place. */
|
|
14
|
+
function migrateV1ToV2(entries: FileEntry[]): void {
|
|
15
|
+
const ids = new Set<string>();
|
|
16
|
+
let prevId: string | null = null;
|
|
17
|
+
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (entry.type === "session") {
|
|
20
|
+
entry.version = 2;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
entry.id = generateId(ids);
|
|
25
|
+
entry.parentId = prevId;
|
|
26
|
+
prevId = entry.id;
|
|
27
|
+
|
|
28
|
+
// Convert firstKeptEntryIndex to firstKeptEntryId for compaction
|
|
29
|
+
if (entry.type === "compaction") {
|
|
30
|
+
const comp = entry as CompactionEntry & { firstKeptEntryIndex?: number };
|
|
31
|
+
if (typeof comp.firstKeptEntryIndex === "number") {
|
|
32
|
+
const targetEntry = entries[comp.firstKeptEntryIndex];
|
|
33
|
+
if (targetEntry && targetEntry.type !== "session") {
|
|
34
|
+
comp.firstKeptEntryId = targetEntry.id;
|
|
35
|
+
}
|
|
36
|
+
delete comp.firstKeptEntryIndex;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Migrate v2 → v3: rename hookMessage role to custom. Mutates in place. */
|
|
43
|
+
function migrateV2ToV3(entries: FileEntry[]): void {
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (entry.type === "session") {
|
|
46
|
+
entry.version = 3;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (entry.type === "message") {
|
|
51
|
+
const msg = entry.message as { role?: string };
|
|
52
|
+
if (msg.role === "hookMessage") {
|
|
53
|
+
(entry.message as { role: string }).role = "custom";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Run all necessary migrations to bring entries to current version.
|
|
61
|
+
* Mutates entries in place. Returns true if any migration was applied.
|
|
62
|
+
*/
|
|
63
|
+
export function migrateToCurrentVersion(entries: FileEntry[]): boolean {
|
|
64
|
+
const header = entries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
65
|
+
const version = header?.version ?? 1;
|
|
66
|
+
|
|
67
|
+
if (version >= CURRENT_SESSION_VERSION) return false;
|
|
68
|
+
|
|
69
|
+
if (version < 2) migrateV1ToV2(entries);
|
|
70
|
+
if (version < 3) migrateV2ToV3(entries);
|
|
71
|
+
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Exported for testing */
|
|
76
|
+
export function migrateSessionEntries(entries: FileEntry[]): void {
|
|
77
|
+
migrateToCurrentVersion(entries);
|
|
78
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { getTerminalId } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import { getSessionsDir, getTerminalSessionsDir, isEnoent, logger, resolveEquivalentPath } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import type { SessionStorage } from "./session-storage";
|
|
7
|
+
|
|
8
|
+
const migratedSessionRoots = new Set<string>();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Merge or rename a legacy session directory into its canonical target.
|
|
12
|
+
* Best effort: callers decide whether migration failures should surface.
|
|
13
|
+
*/
|
|
14
|
+
function migrateSessionDirPath(oldPath: string, newPath: string): void {
|
|
15
|
+
const existing = fs.statSync(newPath, { throwIfNoEntry: false });
|
|
16
|
+
if (existing?.isDirectory()) {
|
|
17
|
+
for (const file of fs.readdirSync(oldPath)) {
|
|
18
|
+
const src = path.join(oldPath, file);
|
|
19
|
+
const dst = path.join(newPath, file);
|
|
20
|
+
if (!fs.existsSync(dst)) {
|
|
21
|
+
fs.renameSync(src, dst);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
fs.rmSync(oldPath, { recursive: true, force: true });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (existing) {
|
|
28
|
+
fs.rmSync(newPath, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
fs.renameSync(oldPath, newPath);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function encodeLegacyAbsoluteSessionDirName(cwd: string): string {
|
|
34
|
+
const resolvedCwd = path.resolve(cwd);
|
|
35
|
+
return `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function encodeRelativeSessionDirName(prefix: string, relative: string): string {
|
|
39
|
+
const encoded = relative.replace(/[/\\:]/g, "-");
|
|
40
|
+
return encoded ? (prefix.endsWith("-") ? `${prefix}${encoded}` : `${prefix}-${encoded}`) : prefix;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getDefaultSessionDirName(cwd: string): { encodedDirName: string; resolvedCwd: string } {
|
|
44
|
+
const resolvedCwd = path.resolve(cwd);
|
|
45
|
+
const canonicalCwd = resolveEquivalentPath(resolvedCwd);
|
|
46
|
+
const home = os.homedir();
|
|
47
|
+
const canonicalHome = resolveEquivalentPath(home);
|
|
48
|
+
const tempRoot = os.tmpdir();
|
|
49
|
+
const canonicalTempRoot = resolveEquivalentPath(tempRoot);
|
|
50
|
+
const homeRelative = path.relative(canonicalHome, canonicalCwd);
|
|
51
|
+
const tempRelative = path.relative(canonicalTempRoot, canonicalCwd);
|
|
52
|
+
const encodedDirName =
|
|
53
|
+
homeRelative === "" || (!homeRelative.startsWith("..") && !path.isAbsolute(homeRelative))
|
|
54
|
+
? encodeRelativeSessionDirName("-", homeRelative)
|
|
55
|
+
: tempRelative === "" || (!tempRelative.startsWith("..") && !path.isAbsolute(tempRelative))
|
|
56
|
+
? encodeRelativeSessionDirName("-tmp", tempRelative)
|
|
57
|
+
: encodeLegacyAbsoluteSessionDirName(canonicalCwd);
|
|
58
|
+
return { encodedDirName, resolvedCwd };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Migrate old `--<home-encoded>-*--` session dirs to the new `-*` format.
|
|
63
|
+
* Runs once per sessions root on first access, best-effort.
|
|
64
|
+
*/
|
|
65
|
+
function migrateHomeSessionDirs(sessionsRoot: string): void {
|
|
66
|
+
if (migratedSessionRoots.has(sessionsRoot)) return;
|
|
67
|
+
migratedSessionRoots.add(sessionsRoot);
|
|
68
|
+
|
|
69
|
+
const home = os.homedir();
|
|
70
|
+
const homeEncoded = home.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-");
|
|
71
|
+
const oldPrefix = `--${homeEncoded}-`;
|
|
72
|
+
const oldExact = `--${homeEncoded}--`;
|
|
73
|
+
|
|
74
|
+
let entries: string[];
|
|
75
|
+
try {
|
|
76
|
+
entries = fs.readdirSync(sessionsRoot);
|
|
77
|
+
} catch {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
let remainder: string;
|
|
83
|
+
if (entry === oldExact) {
|
|
84
|
+
remainder = "";
|
|
85
|
+
} else if (entry.startsWith(oldPrefix) && entry.endsWith("--")) {
|
|
86
|
+
remainder = entry.slice(oldPrefix.length, -2);
|
|
87
|
+
} else {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const newName = remainder ? `-${remainder}` : "-";
|
|
92
|
+
const oldPath = path.join(sessionsRoot, entry);
|
|
93
|
+
const newPath = path.join(sessionsRoot, newName);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
migrateSessionDirPath(oldPath, newPath);
|
|
97
|
+
} catch {
|
|
98
|
+
// Best effort
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function migrateLegacyAbsoluteSessionDir(cwd: string, sessionDir: string, sessionsRoot: string): void {
|
|
104
|
+
const legacyDir = path.join(sessionsRoot, encodeLegacyAbsoluteSessionDirName(cwd));
|
|
105
|
+
if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) return;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
migrateSessionDirPath(legacyDir, sessionDir);
|
|
109
|
+
} catch {
|
|
110
|
+
// Best effort
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function resolveManagedSessionRoot(sessionDir: string, cwd: string): string | undefined {
|
|
115
|
+
const currentDirName = path.basename(sessionDir);
|
|
116
|
+
const { encodedDirName } = getDefaultSessionDirName(cwd);
|
|
117
|
+
if (currentDirName !== encodedDirName && currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd)) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
return path.dirname(sessionDir);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Compute the default session directory for a cwd.
|
|
125
|
+
* Classifies cwd by canonical location so symlink/alias paths resolve to the
|
|
126
|
+
* same home-relative or temp-root directory names as their real targets.
|
|
127
|
+
*/
|
|
128
|
+
export function computeDefaultSessionDir(
|
|
129
|
+
cwd: string,
|
|
130
|
+
storage: SessionStorage,
|
|
131
|
+
sessionsRoot: string = getSessionsDir(),
|
|
132
|
+
): string {
|
|
133
|
+
const { encodedDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
|
|
134
|
+
migrateHomeSessionDirs(sessionsRoot);
|
|
135
|
+
const sessionDir = path.join(sessionsRoot, encodedDirName);
|
|
136
|
+
migrateLegacyAbsoluteSessionDir(resolvedCwd, sessionDir, sessionsRoot);
|
|
137
|
+
storage.ensureDirSync(sessionDir);
|
|
138
|
+
return sessionDir;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// =============================================================================
|
|
142
|
+
// Terminal breadcrumbs: maps terminal (TTY) -> last session file for --continue
|
|
143
|
+
// =============================================================================
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Write a breadcrumb linking the current terminal to a session file.
|
|
147
|
+
* The breadcrumb contains the cwd and session path so --continue can
|
|
148
|
+
* find "this terminal's last session" even when running concurrent instances.
|
|
149
|
+
*/
|
|
150
|
+
export function writeTerminalBreadcrumb(cwd: string, sessionFile: string): void {
|
|
151
|
+
const terminalId = getTerminalId();
|
|
152
|
+
if (!terminalId) return;
|
|
153
|
+
|
|
154
|
+
const breadcrumbDir = getTerminalSessionsDir();
|
|
155
|
+
const breadcrumbFile = path.join(breadcrumbDir, terminalId);
|
|
156
|
+
const content = `${cwd}\n${sessionFile}\n`;
|
|
157
|
+
// Best-effort — don't break session creation if breadcrumb fails
|
|
158
|
+
Bun.write(breadcrumbFile, content).catch(() => {});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface TerminalBreadcrumb {
|
|
162
|
+
cwd: string;
|
|
163
|
+
sessionFile: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Read the raw terminal breadcrumb for the current terminal.
|
|
168
|
+
* Returns the recorded cwd + session file (verified to exist) regardless of
|
|
169
|
+
* whether the recorded cwd still matches the current one. Callers decide how
|
|
170
|
+
* to interpret a cwd mismatch (e.g. a moved/renamed worktree).
|
|
171
|
+
*/
|
|
172
|
+
export async function readTerminalBreadcrumbEntry(): Promise<TerminalBreadcrumb | null> {
|
|
173
|
+
const terminalId = getTerminalId();
|
|
174
|
+
if (!terminalId) return null;
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const breadcrumbFile = path.join(getTerminalSessionsDir(), terminalId);
|
|
178
|
+
const content = await Bun.file(breadcrumbFile).text();
|
|
179
|
+
const lines = content.trim().split("\n");
|
|
180
|
+
if (lines.length < 2) return null;
|
|
181
|
+
|
|
182
|
+
const breadcrumbCwd = lines[0];
|
|
183
|
+
const sessionFile = lines[1];
|
|
184
|
+
|
|
185
|
+
// Verify the session file still exists
|
|
186
|
+
const stat = fs.statSync(sessionFile, { throwIfNoEntry: false });
|
|
187
|
+
if (stat?.isFile()) return { cwd: breadcrumbCwd, sessionFile };
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (!isEnoent(err)) logger.debug("Terminal breadcrumb read failed", { err });
|
|
190
|
+
// Breadcrumb doesn't exist or is corrupt — fall through
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type BlobStore,
|
|
3
|
+
externalizeImageDataSync,
|
|
4
|
+
externalizeImageDataUrlSync,
|
|
5
|
+
isBlobRef,
|
|
6
|
+
isImageDataUrl,
|
|
7
|
+
} from "./blob-store";
|
|
8
|
+
import type { FileEntry } from "./session-entries";
|
|
9
|
+
|
|
10
|
+
const MAX_PERSIST_CHARS = 500_000;
|
|
11
|
+
const TRUNCATION_NOTICE = "\n\n[Session persistence truncated large content]";
|
|
12
|
+
/** Minimum base64 length to externalize to blob store (skip tiny inline images) */
|
|
13
|
+
const BLOB_EXTERNALIZE_THRESHOLD = 1024;
|
|
14
|
+
const TEXT_CONTENT_KEY = "content";
|
|
15
|
+
|
|
16
|
+
function truncateString(value: string, maxLength: number): string {
|
|
17
|
+
if (value.length <= maxLength) return value;
|
|
18
|
+
let truncated = value.slice(0, maxLength);
|
|
19
|
+
if (truncated.length > 0) {
|
|
20
|
+
const last = truncated.charCodeAt(truncated.length - 1);
|
|
21
|
+
if (last >= 0xd800 && last <= 0xdbff) {
|
|
22
|
+
truncated = truncated.slice(0, -1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return truncated;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isImageBlock(value: unknown): value is { type: "image"; data: string; mimeType?: string } {
|
|
29
|
+
return (
|
|
30
|
+
typeof value === "object" &&
|
|
31
|
+
value !== null &&
|
|
32
|
+
"type" in value &&
|
|
33
|
+
(value as { type?: string }).type === "image" &&
|
|
34
|
+
"data" in value &&
|
|
35
|
+
typeof (value as { data?: string }).data === "string"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Recursively truncate large strings in an object for session persistence.
|
|
41
|
+
* - Truncates any oversized string fields (key-agnostic)
|
|
42
|
+
* - Replaces oversized image blocks with text notices
|
|
43
|
+
* - Updates lineCount when content is truncated
|
|
44
|
+
* - Returns original object if no changes needed (structural sharing)
|
|
45
|
+
*
|
|
46
|
+
* Runs in one synchronous tick so an OOM/SIGKILL landing right after a persist
|
|
47
|
+
* call returns cannot lose the entry. Image externalization happens via the
|
|
48
|
+
* synchronous blob-store path (`fs.writeFileSync`), so blob bytes are in the
|
|
49
|
+
* kernel page cache before the JSONL line referencing them is written.
|
|
50
|
+
*/
|
|
51
|
+
function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?: string): unknown {
|
|
52
|
+
if (obj === null || obj === undefined) return obj;
|
|
53
|
+
|
|
54
|
+
if (typeof obj === "string") {
|
|
55
|
+
if (key === "image_url" && isImageDataUrl(obj)) {
|
|
56
|
+
return externalizeImageDataUrlSync(blobStore, obj);
|
|
57
|
+
}
|
|
58
|
+
if (obj.length > MAX_PERSIST_CHARS) {
|
|
59
|
+
// Cryptographic signatures must be preserved exactly or cleared entirely — never truncated.
|
|
60
|
+
// Truncation would produce an invalid signature that the API rejects.
|
|
61
|
+
if (key === "thinkingSignature" || key === "thoughtSignature" || key === "textSignature") {
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
const limit = Math.max(0, MAX_PERSIST_CHARS - TRUNCATION_NOTICE.length);
|
|
65
|
+
return `${truncateString(obj, limit)}${TRUNCATION_NOTICE}`;
|
|
66
|
+
}
|
|
67
|
+
return obj;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (Array.isArray(obj)) {
|
|
71
|
+
let changed = false;
|
|
72
|
+
const result: unknown[] = new Array(obj.length);
|
|
73
|
+
for (let i = 0; i < obj.length; i++) {
|
|
74
|
+
const item = obj[i];
|
|
75
|
+
if (
|
|
76
|
+
key === TEXT_CONTENT_KEY &&
|
|
77
|
+
isImageBlock(item) &&
|
|
78
|
+
!isBlobRef(item.data) &&
|
|
79
|
+
item.data.length >= BLOB_EXTERNALIZE_THRESHOLD
|
|
80
|
+
) {
|
|
81
|
+
changed = true;
|
|
82
|
+
result[i] = { ...item, data: externalizeImageDataSync(blobStore, item.data, item.mimeType) };
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const newItem = truncateForPersistence(item, blobStore, key);
|
|
86
|
+
if (newItem !== item) changed = true;
|
|
87
|
+
result[i] = newItem;
|
|
88
|
+
}
|
|
89
|
+
return changed ? result : obj;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (typeof obj === "object") {
|
|
93
|
+
let changed = false;
|
|
94
|
+
const entries: Array<readonly [string, unknown]> = [];
|
|
95
|
+
for (const [childKey, value] of Object.entries(obj)) {
|
|
96
|
+
// Strip transient/redundant properties that shouldn't be persisted.
|
|
97
|
+
// - partialJson: streaming accumulator for tool call JSON parsing
|
|
98
|
+
// - jsonlEvents: raw subprocess streaming events (already saved to artifact files)
|
|
99
|
+
if (childKey === "partialJson" || childKey === "jsonlEvents") {
|
|
100
|
+
changed = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const newValue = truncateForPersistence(value, blobStore, childKey);
|
|
104
|
+
if (newValue !== value) changed = true;
|
|
105
|
+
entries.push([childKey, newValue]);
|
|
106
|
+
}
|
|
107
|
+
if (!changed) return obj;
|
|
108
|
+
|
|
109
|
+
const contentEntry = entries.find(([childKey]) => childKey === "content");
|
|
110
|
+
const lineCountEntry = entries.find(([childKey]) => childKey === "lineCount");
|
|
111
|
+
if (
|
|
112
|
+
contentEntry &&
|
|
113
|
+
typeof contentEntry[1] === "string" &&
|
|
114
|
+
lineCountEntry &&
|
|
115
|
+
typeof lineCountEntry[1] === "number"
|
|
116
|
+
) {
|
|
117
|
+
const content = contentEntry[1];
|
|
118
|
+
const updatedEntries = entries.map(([childKey, value]) =>
|
|
119
|
+
childKey === "lineCount" ? ([childKey, content.split("\n").length] as const) : ([childKey, value] as const),
|
|
120
|
+
);
|
|
121
|
+
return Object.fromEntries(updatedEntries);
|
|
122
|
+
}
|
|
123
|
+
return Object.fromEntries(entries);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return obj;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function prepareEntryForPersistence(entry: FileEntry, blobStore: BlobStore): FileEntry {
|
|
130
|
+
return truncateForPersistence(entry, blobStore) as FileEntry;
|
|
131
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as fsp from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { isEnoent, peekFileEnds, toError } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { hasFsCode, isEnoent, logger, peekFileEnds, Snowflake, toError } from "@oh-my-pi/pi-utils";
|
|
5
5
|
|
|
6
6
|
const utf8Decoder = new TextDecoder("utf-8");
|
|
7
7
|
|
|
@@ -12,17 +12,17 @@ export interface SessionStorageStat {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface SessionStorageWriter {
|
|
15
|
-
writeLine(line: string): Promise<void>;
|
|
16
15
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* even though it has not yet been fsynced to the underlying disk.
|
|
16
|
+
* Append one newline-terminated line. File and memory storage perform the
|
|
17
|
+
* write synchronously in-body; indexed backends queue in call order.
|
|
20
18
|
*
|
|
21
|
-
* `line` MUST
|
|
19
|
+
* `line` MUST include the trailing newline.
|
|
22
20
|
*/
|
|
23
|
-
|
|
21
|
+
append(line: string): Promise<void>;
|
|
22
|
+
/** Resolve once all queued appends complete. No fsync. */
|
|
24
23
|
flush(): Promise<void>;
|
|
25
|
-
|
|
24
|
+
/** False once close() has begun/finished. */
|
|
25
|
+
isOpen(): boolean;
|
|
26
26
|
close(): Promise<void>;
|
|
27
27
|
getError(): Error | undefined;
|
|
28
28
|
}
|
|
@@ -39,6 +39,7 @@ export interface SessionStorage {
|
|
|
39
39
|
/** Read the requested UTF-8 byte windows from the head and tail of the file. */
|
|
40
40
|
readTextSlices(path: string, prefixBytes: number, suffixBytes: number): Promise<[string, string]>;
|
|
41
41
|
writeText(path: string, content: string): Promise<void>;
|
|
42
|
+
writeTextAtomic(path: string, content: string): Promise<void>;
|
|
42
43
|
rename(path: string, nextPath: string): Promise<void>;
|
|
43
44
|
unlink(path: string): Promise<void>;
|
|
44
45
|
deleteSessionWithArtifacts(sessionPath: string): Promise<void>;
|
|
@@ -81,7 +82,7 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
81
82
|
return error;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
async append(line: string): Promise<void> {
|
|
85
86
|
if (this.#closed) throw new Error("Writer closed");
|
|
86
87
|
if (this.#error) throw this.#error;
|
|
87
88
|
try {
|
|
@@ -99,23 +100,12 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
async writeLine(line: string): Promise<void> {
|
|
103
|
-
this.writeLineSync(line);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
103
|
async flush(): Promise<void> {
|
|
107
104
|
if (this.#error) throw this.#error;
|
|
108
|
-
// OS buffers are flushed on fsync, nothing to do here
|
|
109
105
|
}
|
|
110
106
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (this.#error) throw this.#error;
|
|
114
|
-
try {
|
|
115
|
-
fs.fsyncSync(this.#fd);
|
|
116
|
-
} catch (err) {
|
|
117
|
-
throw this.#recordError(err);
|
|
118
|
-
}
|
|
107
|
+
isOpen(): boolean {
|
|
108
|
+
return !this.#closed;
|
|
119
109
|
}
|
|
120
110
|
|
|
121
111
|
async close(): Promise<void> {
|
|
@@ -189,6 +179,77 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
189
179
|
await Bun.write(path, content, { createPath: true });
|
|
190
180
|
}
|
|
191
181
|
|
|
182
|
+
async writeTextAtomic(fpath: string, content: string): Promise<void> {
|
|
183
|
+
const dir = path.resolve(fpath, "..");
|
|
184
|
+
const tempPath = path.join(dir, `.${path.basename(fpath)}.${Snowflake.next()}.tmp`);
|
|
185
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
186
|
+
try {
|
|
187
|
+
await fs.promises.writeFile(tempPath, content);
|
|
188
|
+
try {
|
|
189
|
+
await this.rename(tempPath, fpath);
|
|
190
|
+
return;
|
|
191
|
+
} catch (err) {
|
|
192
|
+
if (!hasFsCode(err, "EPERM")) throw toError(err);
|
|
193
|
+
await this.#replaceSessionFileAfterEperm(tempPath, fpath, err);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
} catch (err) {
|
|
197
|
+
try {
|
|
198
|
+
await this.unlink(tempPath);
|
|
199
|
+
} catch (cleanupErr) {
|
|
200
|
+
if (!isEnoent(cleanupErr)) {
|
|
201
|
+
logger.warn("Failed to remove session rewrite temp file", {
|
|
202
|
+
sessionFile: fpath,
|
|
203
|
+
tempPath,
|
|
204
|
+
error: toError(cleanupErr).message,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
throw toError(err);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async #replaceSessionFileAfterEperm(tempPath: string, targetPath: string, renameError: unknown): Promise<void> {
|
|
213
|
+
const dir = path.resolve(targetPath, "..");
|
|
214
|
+
const backupPath = path.join(dir, `${path.basename(targetPath)}.${Snowflake.next()}.bak`);
|
|
215
|
+
try {
|
|
216
|
+
await this.rename(targetPath, backupPath);
|
|
217
|
+
} catch (moveAsideError) {
|
|
218
|
+
if (isEnoent(moveAsideError)) {
|
|
219
|
+
await this.rename(tempPath, targetPath);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
throw toError(renameError);
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
await this.rename(tempPath, targetPath);
|
|
226
|
+
} catch (replaceError) {
|
|
227
|
+
try {
|
|
228
|
+
await this.rename(backupPath, targetPath);
|
|
229
|
+
} catch (rollbackErr) {
|
|
230
|
+
const rollbackError = toError(rollbackErr);
|
|
231
|
+
throw new Error(
|
|
232
|
+
`Failed to replace session file after EPERM (original: ${toError(renameError).message}; retry: ${
|
|
233
|
+
toError(replaceError).message
|
|
234
|
+
}; rollback: ${rollbackError.message})`,
|
|
235
|
+
{ cause: toError(renameError) },
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
throw toError(replaceError);
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
await this.unlink(backupPath);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
if (!isEnoent(err)) {
|
|
244
|
+
logger.warn("Failed to remove session rewrite backup", {
|
|
245
|
+
sessionFile: targetPath,
|
|
246
|
+
backupPath,
|
|
247
|
+
error: toError(err).message,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
192
253
|
async rename(path: string, nextPath: string): Promise<void> {
|
|
193
254
|
try {
|
|
194
255
|
await fs.promises.rename(path, nextPath);
|
|
@@ -267,7 +328,7 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
|
|
|
267
328
|
return error;
|
|
268
329
|
}
|
|
269
330
|
|
|
270
|
-
|
|
331
|
+
async append(line: string): Promise<void> {
|
|
271
332
|
if (this.#closed) throw new Error("Writer closed");
|
|
272
333
|
if (this.#error) throw this.#error;
|
|
273
334
|
try {
|
|
@@ -278,17 +339,12 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
|
|
|
278
339
|
}
|
|
279
340
|
}
|
|
280
341
|
|
|
281
|
-
async writeLine(line: string): Promise<void> {
|
|
282
|
-
this.writeLineSync(line);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
342
|
async flush(): Promise<void> {
|
|
286
343
|
if (this.#error) throw this.#error;
|
|
287
344
|
}
|
|
288
345
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (this.#error) throw this.#error;
|
|
346
|
+
isOpen(): boolean {
|
|
347
|
+
return !this.#closed;
|
|
292
348
|
}
|
|
293
349
|
|
|
294
350
|
async close(): Promise<void> {
|
|
@@ -507,6 +563,11 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
507
563
|
return Promise.resolve();
|
|
508
564
|
}
|
|
509
565
|
|
|
566
|
+
writeTextAtomic(path: string, content: string): Promise<void> {
|
|
567
|
+
this.writeTextSync(path, content);
|
|
568
|
+
return Promise.resolve();
|
|
569
|
+
}
|
|
570
|
+
|
|
510
571
|
rename(path: string, nextPath: string): Promise<void> {
|
|
511
572
|
const entry = this.#files.get(path);
|
|
512
573
|
if (!entry) return Promise.reject(new Error(`File not found: ${path}`));
|
|
@@ -32,6 +32,17 @@ export interface SnapcompactInlineOptions {
|
|
|
32
32
|
shape?: snapcompact.ShapeVariantName | "auto";
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Reports the per-tool-result tokens kept off the wire when a swap is applied.
|
|
37
|
+
* `savedTokens` is `textTokens - frames * shape.frameTokenEstimate` for each
|
|
38
|
+
* imaged tool result (always > 0; the savings gate guarantees it). Wired to the
|
|
39
|
+
* append-only savings journal; never throws into the request path.
|
|
40
|
+
*/
|
|
41
|
+
export type SnapcompactSavingsSink = (
|
|
42
|
+
savings: ReadonlyArray<{ toolCallId: string; savedTokens: number }>,
|
|
43
|
+
model: Model,
|
|
44
|
+
) => void;
|
|
45
|
+
|
|
35
46
|
// Per-provider image-count budgets live in @oh-my-pi/snapcompact
|
|
36
47
|
// (`providerImageBudget`): snapcompact frames are 1568px (<2000px) so
|
|
37
48
|
// dimension/size limits never bind; only COUNT does. Once the budget is
|
|
@@ -398,7 +409,10 @@ export class SnapcompactInlineTransformer {
|
|
|
398
409
|
#toolCache = new Map<string, FrameCacheEntry>();
|
|
399
410
|
#systemCache?: FrameCacheEntry;
|
|
400
411
|
|
|
401
|
-
constructor(
|
|
412
|
+
constructor(
|
|
413
|
+
private readonly options: SnapcompactInlineOptions,
|
|
414
|
+
private readonly onToolResultSavings?: SnapcompactSavingsSink,
|
|
415
|
+
) {}
|
|
402
416
|
|
|
403
417
|
transform(context: Context, model: Model): Context {
|
|
404
418
|
// Vision gate: providers silently DROP images on text-only models —
|
|
@@ -464,13 +478,19 @@ export class SnapcompactInlineTransformer {
|
|
|
464
478
|
});
|
|
465
479
|
|
|
466
480
|
let changed = false;
|
|
481
|
+
const savings: Array<{ toolCallId: string; savedTokens: number }> = [];
|
|
467
482
|
for (const swap of plan.toolResults) {
|
|
468
483
|
const target = targets.get(swap.id);
|
|
469
484
|
if (!target) continue;
|
|
470
485
|
const frames = this.#framesFor(this.#toolCache, swap.id, target.text, shape);
|
|
471
486
|
messages[target.index] = { ...target.message, content: [{ type: "text", text: toolResultNote }, ...frames] };
|
|
472
487
|
changed = true;
|
|
488
|
+
savings.push({
|
|
489
|
+
toolCallId: swap.id,
|
|
490
|
+
savedTokens: Math.max(0, swap.textTokens - swap.frames * shape.frameTokenEstimate),
|
|
491
|
+
});
|
|
473
492
|
}
|
|
493
|
+
if (savings.length > 0) this.onToolResultSavings?.(savings, model);
|
|
474
494
|
if (this.options.renderToolResults) {
|
|
475
495
|
// Drop cache entries for tool calls no longer in the context
|
|
476
496
|
// (compacted away) so the cache stays bounded by live history.
|