one-code-extension 0.4.1 → 0.6.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/README.md +16 -5
- package/agents/general-purpose.md +2 -2
- package/docs/guide/README.md +2 -1
- package/docs/guide/bring-your-claude-code-setup.md +60 -9
- package/docs/guide/configuration.md +48 -14
- package/docs/guide/differences-from-claude-code.md +40 -28
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +26 -4
- package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
- package/docs/guide/providers-and-models.md +137 -54
- package/docs/guide/reference.md +27 -15
- package/docs/guide/sessions-and-context.md +69 -16
- package/docs/guide/skills-plugins-and-mcp.md +57 -20
- package/docs/guide/subagents-and-workflows.md +95 -29
- package/docs/guide/tasks-and-background-work.md +101 -26
- package/docs/guide/terminal-interface.md +24 -12
- package/docs/guide/tools.md +46 -5
- package/docs/guide/troubleshooting.md +13 -9
- package/docs/guide/windows.md +58 -31
- package/extensions/artifacts/gallery.ts +227 -0
- package/extensions/artifacts/index.ts +288 -0
- package/extensions/artifacts/store.ts +570 -0
- package/extensions/artifacts/viewer.ts +199 -0
- package/extensions/ask-user/description.ts +67 -0
- package/extensions/ask-user/dialogs.ts +80 -0
- package/extensions/ask-user/index.ts +37 -33
- package/extensions/ask-user/questions.ts +32 -7
- package/extensions/ask-user/widget.ts +45 -25
- package/extensions/auto-mode/actions.ts +6 -0
- package/extensions/auto-mode/cache.ts +88 -0
- package/extensions/auto-mode/classifier-prompt.ts +5 -4
- package/extensions/auto-mode/classifier.ts +290 -65
- package/extensions/auto-mode/config.ts +72 -20
- package/extensions/auto-mode/decision-log.ts +2 -0
- package/extensions/auto-mode/defaults.ts +1 -1
- package/extensions/auto-mode/denials.ts +124 -0
- package/extensions/auto-mode/git-checkout-programs.ts +187 -0
- package/extensions/auto-mode/git-status-meta.ts +159 -0
- package/extensions/auto-mode/history.ts +189 -0
- package/extensions/auto-mode/instructions.ts +58 -52
- package/extensions/auto-mode/model-select.ts +159 -134
- package/extensions/auto-mode/paths.ts +57 -5
- package/extensions/auto-mode/pause.ts +57 -2
- package/extensions/auto-mode/prompt.ts +37 -21
- package/extensions/auto-mode/read-only-options.ts +724 -0
- package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
- package/extensions/auto-mode/rules.ts +18 -1
- package/extensions/auto-mode/safety-floor.ts +361 -18
- package/extensions/auto-mode/setup-run.ts +20 -14
- package/extensions/auto-mode/shell-analysis.ts +764 -490
- package/extensions/auto-mode/shell-parse.ts +885 -0
- package/extensions/auto-mode/system-note.ts +33 -0
- package/extensions/auto-mode/transcript.ts +80 -51
- package/extensions/background/cron.ts +642 -0
- package/extensions/background/index.ts +614 -248
- package/extensions/background/loop-fire.ts +173 -0
- package/extensions/background/loop-preamble.ts +59 -0
- package/extensions/background/loop-skill.ts +163 -0
- package/extensions/background/monitor-batch.ts +53 -0
- package/extensions/background/noop-fold.ts +128 -0
- package/extensions/background/switch-guard.ts +56 -0
- package/extensions/background/wakeup.ts +178 -55
- package/extensions/bash/background.ts +53 -17
- package/extensions/bash/child.ts +7 -0
- package/extensions/bash/description.ts +188 -0
- package/extensions/bash/guards.ts +15 -67
- package/extensions/bash/index.ts +66 -35
- package/extensions/branding/compaction-keep.ts +63 -0
- package/extensions/branding/declined.ts +22 -0
- package/extensions/branding/index.ts +133 -9
- package/extensions/branding/prompt-editor.ts +53 -6
- package/extensions/branding/queued-edit.ts +31 -0
- package/extensions/branding/replaced-builtins.ts +68 -0
- package/extensions/branding/startup.ts +33 -71
- package/extensions/btw/index.ts +250 -78
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/cache-marks/index.ts +42 -0
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +436 -63
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +219 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/budget.ts +16 -2
- package/extensions/context-budget/index.ts +35 -15
- package/extensions/context-management/index.ts +36 -28
- package/extensions/doctor/build.ts +59 -8
- package/extensions/doctor/builtins.ts +84 -0
- package/extensions/doctor/cli.ts +7 -1
- package/extensions/doctor/compat.ts +88 -23
- package/extensions/doctor/dependencies.ts +41 -5
- package/extensions/doctor/fix-prompt.ts +9 -2
- package/extensions/doctor/index.ts +102 -58
- package/extensions/doctor/models.ts +102 -63
- package/extensions/doctor/presets.ts +42 -27
- package/extensions/doctor/report.ts +19 -2
- package/extensions/doctor/viewer.ts +49 -27
- package/extensions/effort/index.ts +141 -25
- package/extensions/effort/settings.ts +21 -0
- package/extensions/effort/slider.ts +24 -20
- package/extensions/exit/index.ts +32 -1
- package/extensions/file-tracker/index.ts +420 -42
- package/extensions/file-tracker/replay.ts +137 -24
- package/extensions/file-tracker/restore.ts +66 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +45 -15
- package/extensions/footer/footer-line.ts +45 -24
- package/extensions/footer/index.ts +47 -2
- package/extensions/footer/pr.ts +34 -5
- package/extensions/hooks/compaction.ts +7 -0
- package/extensions/hooks/executor.ts +32 -30
- package/extensions/hooks/index.ts +209 -58
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/prompt-context.ts +59 -0
- package/extensions/hooks/protocol.ts +41 -3
- package/extensions/hooks/settings.ts +50 -19
- package/extensions/hooks/subagent-bridge.ts +2 -0
- package/extensions/hooks/trust.ts +26 -12
- package/extensions/idle-compact/index.ts +119 -0
- package/extensions/idle-compact/policy.ts +143 -0
- package/extensions/init/index.ts +11 -10
- package/extensions/init/prompt.ts +25 -32
- package/extensions/interrupted/index.ts +13 -1
- package/extensions/kept-thinking/index.ts +27 -0
- package/extensions/ledger/index.ts +46 -0
- package/extensions/ledger/wire.ts +154 -0
- package/extensions/lib/agent-cron.ts +146 -0
- package/extensions/lib/agent-loader.ts +18 -3
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-auth.ts +98 -0
- package/extensions/lib/anthropic-payload.ts +82 -1
- package/extensions/lib/anthropic-server-call.ts +230 -0
- package/extensions/lib/anthropic-server-tools.ts +485 -0
- package/extensions/lib/app-launch.mjs +94 -0
- package/extensions/lib/argument-hints.ts +190 -0
- package/extensions/lib/atomic-write.ts +6 -2
- package/extensions/lib/auto-memory.ts +75 -0
- package/extensions/lib/bash-parser.ts +84 -0
- package/extensions/lib/btw-fork.ts +93 -0
- package/extensions/lib/child-extensions.ts +52 -0
- package/extensions/lib/child-writes.ts +45 -0
- package/extensions/lib/claude-context.ts +515 -100
- package/extensions/lib/claude-external-includes.ts +68 -0
- package/extensions/lib/claude-id.ts +22 -0
- package/extensions/lib/claude-rules.ts +323 -0
- package/extensions/lib/claude-settings.ts +76 -4
- package/extensions/lib/compaction-boundary.ts +36 -0
- package/extensions/lib/compaction-keep.mjs +83 -0
- package/extensions/lib/config-mode.ts +112 -0
- package/extensions/lib/consent-dialogs.ts +63 -0
- package/extensions/lib/consent-preview.ts +5 -1
- package/extensions/lib/consent-stores.ts +48 -0
- package/extensions/lib/context-facts.ts +97 -0
- package/extensions/lib/covered-messages.ts +23 -0
- package/extensions/lib/deferred.ts +359 -25
- package/extensions/lib/dock-rows.ts +60 -0
- package/extensions/lib/empty-tool-result.ts +27 -0
- package/extensions/lib/environment-block.ts +76 -0
- package/extensions/lib/fetch-timeout.ts +24 -6
- package/extensions/{system-prompt → lib}/git-status.ts +51 -13
- package/extensions/lib/git.ts +75 -13
- package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
- package/extensions/lib/grammars/README.md +2 -0
- package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
- package/extensions/lib/guide-docs.ts +45 -0
- package/extensions/lib/headless-output.ts +46 -0
- package/extensions/lib/hook-gate.ts +7 -7
- package/extensions/lib/interrupt.ts +20 -4
- package/extensions/lib/key-input.ts +455 -0
- package/extensions/lib/keys.ts +14 -1
- package/extensions/lib/ledger/fold-memo.ts +49 -0
- package/extensions/lib/ledger/fold.ts +265 -0
- package/extensions/lib/ledger/layout.ts +72 -0
- package/extensions/lib/ledger/plan.ts +102 -0
- package/extensions/lib/ledger/records.ts +276 -0
- package/extensions/lib/ledger/state.ts +47 -0
- package/extensions/lib/ledger/validate.ts +141 -0
- package/extensions/lib/ledger/wire.ts +632 -0
- package/extensions/lib/local-command.ts +71 -19
- package/extensions/lib/mcp-overrides.ts +1 -1
- package/extensions/lib/mcp-share.ts +46 -0
- package/extensions/lib/memory-content.ts +146 -0
- package/extensions/lib/memory-index.ts +18 -0
- package/extensions/lib/memory.ts +134 -123
- package/extensions/lib/model-catalog/huggingface.json +178 -0
- package/extensions/lib/model-catalog/models-dev.json +18533 -0
- package/extensions/lib/model-catalog/openrouter.json +5050 -0
- package/extensions/lib/model-catalog-data.ts +513 -0
- package/extensions/lib/model-catalog.ts +462 -0
- package/extensions/lib/model-choice-warnings.ts +137 -0
- package/extensions/lib/model-pairings.ts +152 -0
- package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
- package/extensions/lib/model-policy.ts +112 -15
- package/extensions/lib/model-tier.ts +283 -355
- package/extensions/lib/model-unusable.ts +1 -1
- package/extensions/lib/newer-model.ts +100 -0
- package/extensions/lib/notices.ts +87 -0
- package/extensions/lib/notifications.ts +357 -73
- package/extensions/lib/one-code-settings.ts +74 -8
- package/extensions/lib/open-browser.ts +63 -0
- package/extensions/lib/openrouter-generation.ts +94 -0
- package/extensions/lib/operation-deadline.ts +30 -0
- package/extensions/lib/original-command.ts +4 -4
- package/extensions/lib/package-version.ts +1 -1
- package/extensions/lib/panel-focus.ts +134 -0
- package/extensions/lib/paths.ts +17 -2
- package/extensions/lib/permission-gate.ts +72 -6
- package/extensions/lib/persisted-output.ts +60 -8
- package/extensions/lib/pi-ai-estimate.ts +19 -3
- package/extensions/lib/pi-version.ts +38 -2
- package/extensions/lib/plan-mode-channels.ts +42 -5
- package/extensions/lib/plugins.ts +26 -17
- package/extensions/lib/powershell-parse-script.ts +208 -0
- package/extensions/lib/powershell-parser.ts +344 -0
- package/extensions/lib/previous-mark.ts +101 -0
- package/extensions/lib/process-state.ts +48 -0
- package/extensions/lib/process-tree.ts +132 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +370 -358
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +547 -0
- package/extensions/lib/scratchpad.ts +81 -14
- package/extensions/lib/send-now-channel.ts +7 -0
- package/extensions/lib/session-model-tier.ts +51 -0
- package/extensions/lib/session-work.ts +64 -0
- package/extensions/lib/settings-channels.ts +4 -4
- package/extensions/lib/shell-exit-semantics.ts +170 -0
- package/extensions/lib/shell-quote.ts +11 -0
- package/extensions/lib/shell-result.ts +70 -0
- package/extensions/lib/shell-spawn.ts +70 -6
- package/extensions/lib/shell-tool.ts +75 -25
- package/extensions/lib/side-call-cache.ts +124 -0
- package/extensions/lib/side-call-run.ts +96 -0
- package/extensions/lib/side-call-usage.ts +38 -0
- package/extensions/lib/side-call.ts +24 -1
- package/extensions/lib/single-flight.ts +18 -0
- package/extensions/lib/skill-body.ts +42 -0
- package/extensions/lib/skill-scan.ts +63 -24
- package/extensions/lib/spill-file.ts +140 -0
- package/extensions/lib/subagent-handback.ts +112 -0
- package/extensions/lib/system-role.ts +156 -0
- package/extensions/lib/terminal-text.ts +114 -0
- package/extensions/lib/text-width.ts +47 -16
- package/extensions/lib/timer-ops.ts +20 -0
- package/extensions/lib/tool-additions.ts +129 -0
- package/extensions/lib/tool-call-corruption.ts +134 -0
- package/extensions/lib/tool-path.ts +66 -0
- package/extensions/lib/tool-variants.ts +69 -0
- package/extensions/lib/tui-render.ts +255 -27
- package/extensions/lib/turn-budget-layout.ts +159 -0
- package/extensions/lib/update-check.mjs +15 -2
- package/extensions/lib/usage-bus.ts +1 -1
- package/extensions/lib/user-denial.ts +38 -0
- package/extensions/lib/workspace-channel.ts +16 -0
- package/extensions/lib/worktree-channel.ts +31 -3
- package/extensions/lib/worktree-isolation.ts +40 -10
- package/extensions/lsp/client.ts +8 -9
- package/extensions/lsp/index.ts +76 -12
- package/extensions/lsp/keep-alive.ts +1 -1
- package/extensions/lsp/trust.ts +119 -0
- package/extensions/lsp/watcher.ts +1 -1
- package/extensions/mcp/announce.ts +90 -43
- package/extensions/mcp/client.ts +161 -48
- package/extensions/mcp/config.ts +132 -8
- package/extensions/mcp/discovery.ts +19 -0
- package/extensions/mcp/index.ts +220 -121
- package/extensions/mcp/oauth/browser.ts +38 -17
- package/extensions/mcp/oauth/flow.ts +25 -17
- package/extensions/mcp/oauth/provider.ts +14 -8
- package/extensions/mcp/oauth/store.ts +34 -16
- package/extensions/mcp/panel/keys.ts +12 -11
- package/extensions/mcp/panel/render.ts +9 -6
- package/extensions/mcp/schema.ts +104 -5
- package/extensions/mcp/trust.ts +164 -73
- package/extensions/memory/entries.ts +23 -14
- package/extensions/memory/index.ts +134 -28
- package/extensions/memory/open-external.ts +33 -18
- package/extensions/memory/panel.ts +135 -26
- package/extensions/model-default/index.ts +86 -19
- package/extensions/newer-model/index.ts +31 -0
- package/extensions/notebook/description.ts +18 -0
- package/extensions/notebook/index.ts +6 -5
- package/extensions/notebook/notebook.ts +1 -1
- package/extensions/permissions/index.ts +1426 -398
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +11 -4
- package/extensions/permissions/panel/host.ts +130 -0
- package/extensions/permissions/panel/keys.ts +48 -0
- package/extensions/permissions/panel/render.ts +450 -0
- package/extensions/permissions/panel/state.ts +550 -0
- package/extensions/permissions/powershell-cmdlets.ts +198 -0
- package/extensions/permissions/powershell-paths.ts +197 -0
- package/extensions/permissions/powershell-rules.ts +207 -280
- package/extensions/permissions/powershell-tree.ts +557 -0
- package/extensions/permissions/project-trust.ts +27 -11
- package/extensions/permissions/protected-paths.ts +14 -2
- package/extensions/permissions/session-grant.ts +6 -0
- package/extensions/permissions/settings.ts +166 -31
- package/extensions/permissions/subagent-gate.ts +25 -0
- package/extensions/permissions/workspace.ts +78 -0
- package/extensions/plan-mode/index.ts +276 -92
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/run-state.ts +26 -0
- package/extensions/plan-mode/texts.ts +177 -0
- package/extensions/plan-mode/viewer.ts +92 -27
- package/extensions/plugins/counts.ts +3 -3
- package/extensions/plugins/index.ts +64 -46
- package/extensions/plugins/install/install.ts +16 -0
- package/extensions/plugins/marketplace/sync.ts +9 -3
- package/extensions/plugins/marketplace/types.ts +3 -1
- package/extensions/plugins/panel/keys.ts +21 -24
- package/extensions/plugins/panel/render.ts +12 -6
- package/extensions/plugins/shell-expand.ts +107 -0
- package/extensions/plugins/template.ts +32 -15
- package/extensions/powershell/child.ts +7 -0
- package/extensions/powershell/description.ts +38 -6
- package/extensions/powershell/guards.ts +1 -1
- package/extensions/powershell/index.ts +14 -7
- package/extensions/recap/index.ts +73 -71
- package/extensions/recap/prompt.ts +2 -2
- package/extensions/recap/scheduler.ts +3 -5
- package/extensions/search-tools/grep-merge.ts +39 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/send-now/chord.ts +62 -0
- package/extensions/send-now/index.ts +133 -0
- package/extensions/session-title/index.ts +3 -0
- package/extensions/session-title/title.ts +4 -1
- package/extensions/skill/announce.ts +30 -0
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +469 -132
- package/extensions/skill/invoke.ts +118 -75
- package/extensions/skill/listing.ts +133 -9
- package/extensions/skill/panel/keys.ts +15 -16
- package/extensions/skill/panel/render.ts +5 -2
- package/extensions/skill/panel/state.ts +12 -1
- package/extensions/subagents/agent-args.ts +39 -0
- package/extensions/subagents/agent-description.ts +132 -0
- package/extensions/subagents/agents.ts +146 -31
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +82 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1594 -852
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +5 -2
- package/extensions/subagents/panel-keys.ts +73 -31
- package/extensions/subagents/panel-render.ts +120 -96
- package/extensions/subagents/panel-widget.ts +60 -89
- package/extensions/subagents/parent-mode.ts +50 -0
- package/extensions/subagents/pending-claim.ts +18 -1
- package/extensions/subagents/runner.ts +135 -80
- package/extensions/subagents/runs.ts +44 -5
- package/extensions/subagents/send-to-main-tool.ts +1 -1
- package/extensions/subagents/session-turns.ts +32 -8
- package/extensions/subagents/shell-panel.ts +36 -178
- package/extensions/subagents/tasks-dialog.ts +170 -0
- package/extensions/subagents/viewport-mirror.ts +95 -0
- package/extensions/subagents/worktree.ts +57 -13
- package/extensions/system-prompt/environment.ts +19 -19
- package/extensions/system-prompt/idle-turn.ts +65 -0
- package/extensions/system-prompt/index.ts +165 -47
- package/extensions/system-prompt/template.ts +34 -51
- package/extensions/system-prompt/tiers/common.ts +23 -64
- package/extensions/system-prompt/tiers/frontier.ts +32 -11
- package/extensions/system-prompt/tiers/low.ts +67 -45
- package/extensions/system-prompt/tiers/mid.ts +96 -38
- package/extensions/system-reminder/index.ts +204 -70
- package/extensions/tasks/descriptions.ts +190 -0
- package/extensions/tasks/index.ts +120 -41
- package/extensions/tasks/store.ts +83 -16
- package/extensions/tool-call-corruption/child.ts +7 -0
- package/extensions/tool-call-corruption/index.ts +237 -0
- package/extensions/tool-search/index.ts +366 -144
- package/extensions/tool-style/index.ts +68 -10
- package/extensions/tool-style/write-description.ts +40 -0
- package/extensions/tool-style/write-render.ts +237 -0
- package/extensions/turn-duration/index.ts +7 -3
- package/extensions/turn-duration/line.ts +47 -4
- package/extensions/web/backends.ts +6 -9
- package/extensions/web/description.ts +50 -0
- package/extensions/web/index.ts +118 -53
- package/extensions/web/native-call.ts +15 -0
- package/extensions/web/native-results.ts +41 -0
- package/extensions/web-fetch/description.ts +42 -0
- package/extensions/web-fetch/extract.ts +53 -6
- package/extensions/web-fetch/index.ts +125 -76
- package/extensions/web-fetch/page-content.ts +19 -0
- package/extensions/web-fetch/response.ts +49 -0
- package/extensions/web-fetch/summarize.ts +9 -10
- package/extensions/workflow/agent-session.ts +108 -18
- package/extensions/workflow/args.ts +24 -0
- package/extensions/workflow/description.ts +67 -0
- package/extensions/workflow/globals.ts +17 -3
- package/extensions/workflow/index.ts +157 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +33 -6
- package/extensions/workflow/saved-workflows.ts +3 -2
- package/extensions/workflow/types.ts +22 -1
- package/extensions/workflow/viewer.ts +54 -24
- package/extensions/workflow/widget.ts +20 -0
- package/extensions/worktree/descriptions.ts +91 -0
- package/extensions/worktree/guards.ts +316 -54
- package/extensions/worktree/index.ts +162 -44
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +21 -7
- package/skills/artifact-design/SKILL.md +85 -0
- package/skills/code-review/SKILL.md +20 -20
- package/skills/code-review/cells/arguments-and-flags.md +19 -0
- package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
- package/skills/code-review/cells/high-8-inline.md +122 -0
- package/skills/code-review/cells/low-1-pass-max4.md +27 -0
- package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
- package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
- package/skills/code-review/cells/medium-3x5-verify.md +132 -0
- package/skills/code-review/cells/minimal-prompt.md +32 -0
- package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
- package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
- package/skills/fewer-permission-prompts/SKILL.md +1 -1
- package/skills/loop/SKILL.md +11 -0
- package/skills/security-review/SKILL.md +2 -5
- package/skills/simplify/SKILL.md +7 -5
- package/skills/workflow-authoring/SKILL.md +143 -0
- package/themes/onecode-light.json +1 -1
- package/themes/onecode.json +1 -1
- package/types/app.d.ts +58 -0
- package/types/community-packages.d.ts +26 -1
- package/extensions/auto-mode/recoverability.ts +0 -229
- package/extensions/lib/capability-index.ts +0 -466
- package/extensions/lib/model-facts.generated.json +0 -2675
- package/extensions/lib/model-facts.ts +0 -162
- package/extensions/tool-search/announce.ts +0 -42
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replaying the session's last provider request with a few messages appended,
|
|
3
|
+
* so a side call (btw, recap, compaction) reads the prompt cache the session
|
|
4
|
+
* wrote. Claude Code sends these calls the same way: the main request's
|
|
5
|
+
* `system`, `tools` and `messages` unchanged, the final assistant reply, then
|
|
6
|
+
* the side prompt (working-docs/decisions/caching.md "Side calls replay the last
|
|
7
|
+
* request").
|
|
8
|
+
*
|
|
9
|
+
* The request cannot be rebuilt from pi's APIs: tool-search reshapes the wire
|
|
10
|
+
* `tools` array and context-management edits the body inside pi's request
|
|
11
|
+
* pipeline, which `completeSimple` bypasses, and one differing byte in `tools`
|
|
12
|
+
* or `system` misses the whole cache. So the compaction extension (the last to
|
|
13
|
+
* see `before_provider_request`) publishes the final payload on
|
|
14
|
+
* LAST_REQUEST_CHANNEL, and a side call sends it back with its tail spliced on
|
|
15
|
+
* through `completeSimple`'s `onPayload`. pi-ai converts the tail itself (the
|
|
16
|
+
* call's own context is just the tail), so its wire shape matches the rest by
|
|
17
|
+
* construction.
|
|
18
|
+
*
|
|
19
|
+
* Pure: no pi extension API; only pi-ai types and the vendored estimator.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
23
|
+
import { settleSystemMessages } from "./ledger/layout.ts";
|
|
24
|
+
import { isObject } from "./ledger/records.ts";
|
|
25
|
+
import { appendPoint, isToolsItem, leadingToolsItem, listKey, loadedInList, preludeLength, responsesDeclarations, stripMarkers, toolName, toolsItemFor } from "./ledger/wire.ts";
|
|
26
|
+
import { estimateMessageTokens } from "./pi-ai-estimate.ts";
|
|
27
|
+
|
|
28
|
+
/** The compaction extension publishes a RequestCapture here, or undefined when none is valid. */
|
|
29
|
+
export const LAST_REQUEST_CHANNEL = "one-code:last-request";
|
|
30
|
+
|
|
31
|
+
/** The provider APIs whose request body can be extended by appending to one list. */
|
|
32
|
+
export type ReplayApi =
|
|
33
|
+
| "anthropic-messages"
|
|
34
|
+
| "openai-completions"
|
|
35
|
+
| "openai-responses"
|
|
36
|
+
| "azure-openai-responses"
|
|
37
|
+
| "openai-codex-responses"
|
|
38
|
+
| "bedrock-converse-stream"
|
|
39
|
+
| "google-generative-ai"
|
|
40
|
+
| "google-vertex";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Where the output cap lives, first match in the captured body wins: a
|
|
44
|
+
* top-level field, or one level down (Bedrock's `inferenceConfig.maxTokens`,
|
|
45
|
+
* Google's `config.maxOutputTokens`). Codex sends none.
|
|
46
|
+
*/
|
|
47
|
+
const CAP_PATHS: Record<ReplayApi, ReadonlyArray<readonly string[]>> = {
|
|
48
|
+
"anthropic-messages": [["max_tokens"]],
|
|
49
|
+
"openai-completions": [["max_completion_tokens"], ["max_tokens"]],
|
|
50
|
+
"openai-responses": [["max_output_tokens"]],
|
|
51
|
+
"azure-openai-responses": [["max_output_tokens"]],
|
|
52
|
+
"openai-codex-responses": [],
|
|
53
|
+
"bedrock-converse-stream": [["inferenceConfig", "maxTokens"]],
|
|
54
|
+
"google-generative-ai": [["config", "maxOutputTokens"]],
|
|
55
|
+
"google-vertex": [["config", "maxOutputTokens"]],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const REPLAY_APIS: ReadonlySet<string> = new Set<ReplayApi>([
|
|
59
|
+
"anthropic-messages",
|
|
60
|
+
"openai-completions",
|
|
61
|
+
"openai-responses",
|
|
62
|
+
"azure-openai-responses",
|
|
63
|
+
"openai-codex-responses",
|
|
64
|
+
"bedrock-converse-stream",
|
|
65
|
+
"google-generative-ai",
|
|
66
|
+
"google-vertex",
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
/** The value at `path` in `payload`, when it is a number. */
|
|
70
|
+
function capAt(payload: Record<string, unknown>, path: readonly string[]): number | undefined {
|
|
71
|
+
let value: unknown = payload;
|
|
72
|
+
for (const key of path) value = isObject(value) ? value[key] : undefined;
|
|
73
|
+
return typeof value === "number" ? value : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The path of the cap `payload` carries for `api`, or the first one the API takes. */
|
|
77
|
+
function capPath(api: ReplayApi, payload: Record<string, unknown>): readonly string[] | undefined {
|
|
78
|
+
const paths = CAP_PATHS[api];
|
|
79
|
+
return paths.find((path) => capAt(payload, path) !== undefined) ?? paths[0];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** `payload` with `value` at `path`, copying only the objects on the way. */
|
|
83
|
+
function withCap(payload: Record<string, unknown>, path: readonly string[], value: number): Record<string, unknown> {
|
|
84
|
+
const [head, ...rest] = path;
|
|
85
|
+
if (rest.length === 0) return { ...payload, [head]: value };
|
|
86
|
+
const inner = isObject(payload[head]) ? (payload[head] as Record<string, unknown>) : {};
|
|
87
|
+
return { ...payload, [head]: withCap(inner, rest, value) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The session's last provider request body, and the model it went to. */
|
|
91
|
+
export interface RequestCapture {
|
|
92
|
+
api: ReplayApi;
|
|
93
|
+
provider: string;
|
|
94
|
+
modelId: string;
|
|
95
|
+
payload: Record<string, unknown>;
|
|
96
|
+
/** The last message of the context the body was built from: where a fork's own messages start. */
|
|
97
|
+
covers?: CoveredThrough;
|
|
98
|
+
/** The session the body went out for: its id is the routing affinity a fork must send too. */
|
|
99
|
+
sessionId?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A transcript message named by what survives a session fork: its role and timestamp. */
|
|
103
|
+
export interface CoveredThrough {
|
|
104
|
+
role: string;
|
|
105
|
+
timestamp: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface ModelRef {
|
|
109
|
+
api?: string;
|
|
110
|
+
provider?: string;
|
|
111
|
+
id?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A capture of `payload` for `model`, or undefined when its API cannot be replayed. */
|
|
115
|
+
export function captureRequest(model: ModelRef | undefined, payload: unknown, covers?: CoveredThrough, sessionId?: string): RequestCapture | undefined {
|
|
116
|
+
const api = model?.api;
|
|
117
|
+
if (!api || !REPLAY_APIS.has(api)) return undefined;
|
|
118
|
+
if (!model?.provider || !model.id || !isObject(payload) || !Array.isArray(payload[listKey(api)])) return undefined;
|
|
119
|
+
return { api: api as ReplayApi, provider: model.provider, modelId: model.id, payload, ...(covers ? { covers } : {}), ...(sessionId ? { sessionId } : {}) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The model fields pi-ai reads to choose its session-affinity headers. */
|
|
123
|
+
export interface AffinityModel extends ModelRef {
|
|
124
|
+
baseUrl?: string;
|
|
125
|
+
compat?: { sendSessionAffinityHeaders?: boolean; sessionAffinityFormat?: string };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The session-affinity headers pi-ai sends with `sessionId` for `model`, so a
|
|
130
|
+
* fork can send its parent's: a gateway routes on them (OpenRouter spreads a
|
|
131
|
+
* model across hosts, each with its own cache), and the fork is a different
|
|
132
|
+
* session. Extension headers merge after pi-ai's, so these override. A copy
|
|
133
|
+
* of pi-ai's `createClient` rules for the three replayable APIs, unchanged
|
|
134
|
+
* from 0.87.1 to 0.99.2 (re-check at a pi bump:
|
|
135
|
+
* features/pi-adaptation/plan.md §3 step 9).
|
|
136
|
+
*/
|
|
137
|
+
export function affinityHeaders(model: AffinityModel | undefined, sessionId: string): Record<string, string> {
|
|
138
|
+
if (!model) return {};
|
|
139
|
+
const openRouter = model.provider === "openrouter" || (model.baseUrl ?? "").includes("openrouter.ai");
|
|
140
|
+
const send = model.compat?.sendSessionAffinityHeaders ?? openRouter;
|
|
141
|
+
if (model.api === "anthropic-messages") {
|
|
142
|
+
if (!send) return {};
|
|
143
|
+
const format = model.compat?.sessionAffinityFormat ?? (openRouter ? "openrouter" : undefined);
|
|
144
|
+
return { [format === "openrouter" ? "x-session-id" : "x-session-affinity"]: sessionId };
|
|
145
|
+
}
|
|
146
|
+
if (model.api !== "openai-completions" && model.api !== "openai-responses") return {};
|
|
147
|
+
if (model.api === "openai-completions" && !send) return {};
|
|
148
|
+
const format = model.compat?.sessionAffinityFormat ?? (openRouter ? "openrouter" : "openai");
|
|
149
|
+
if (format === "openrouter") return { "x-session-id": sessionId };
|
|
150
|
+
return {
|
|
151
|
+
...(format === "openai" ? { session_id: sessionId } : {}),
|
|
152
|
+
"x-client-request-id": sessionId,
|
|
153
|
+
...(model.api === "openai-completions" ? { "x-session-affinity": sessionId } : {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The last non-system message of a context, as a fork will find it again; undefined without one. */
|
|
158
|
+
export function lastCovered(messages: readonly { role: string; timestamp?: number }[]): CoveredThrough | undefined {
|
|
159
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
160
|
+
const { role, timestamp } = messages[index];
|
|
161
|
+
if (role === "system") continue;
|
|
162
|
+
return typeof timestamp === "number" ? { role, timestamp } : undefined;
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** True when `model` is the one `capture` went to: a different model has a different cache. */
|
|
168
|
+
export function captureMatches(capture: RequestCapture, model: ModelRef | undefined): boolean {
|
|
169
|
+
return capture.api === model?.api && capture.provider === model?.provider && capture.modelId === model?.id;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
interface MessageLike {
|
|
173
|
+
role: string;
|
|
174
|
+
content?: unknown;
|
|
175
|
+
stopReason?: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The last captured request plus the assistant reply that answered it, as one
|
|
180
|
+
* consumer extension sees them. The reply is kept only when it closes the
|
|
181
|
+
* exchange cleanly: an errored or aborted reply is omitted from pi's context,
|
|
182
|
+
* and one that called tools is still waiting on their results (appending it
|
|
183
|
+
* without them is invalid on every provider), so both leave the tail empty.
|
|
184
|
+
*/
|
|
185
|
+
export class LastExchange<Reply extends MessageLike = MessageLike> {
|
|
186
|
+
private capture: RequestCapture | undefined;
|
|
187
|
+
private reply: Reply | undefined;
|
|
188
|
+
|
|
189
|
+
/** A new request (or undefined: none valid) replaces the old one and its reply. */
|
|
190
|
+
setCapture(capture: RequestCapture | undefined): void {
|
|
191
|
+
this.capture = capture;
|
|
192
|
+
this.reply = undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Feed every main-session `message_end`; only assistant messages matter. */
|
|
196
|
+
noteMessage(message: Reply): void {
|
|
197
|
+
if (!this.capture || message.role !== "assistant") return;
|
|
198
|
+
const content = Array.isArray(message.content) ? (message.content as { type?: string }[]) : [];
|
|
199
|
+
const clean = message.stopReason !== "error" && message.stopReason !== "aborted" && !content.some((block) => block.type === "toolCall");
|
|
200
|
+
this.reply = clean ? message : undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The capture and reply when the capture went to `model`; a switched model has its own cache. */
|
|
204
|
+
forModel(model: ModelRef | undefined): { capture: RequestCapture; reply: Reply | undefined } | undefined {
|
|
205
|
+
const capture = this.capture;
|
|
206
|
+
if (!capture || capture.provider !== model?.provider || capture.modelId !== model?.id) return undefined;
|
|
207
|
+
return { capture, reply: this.reply };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The captured body with `tail`'s message list (a body pi-ai built for the tail
|
|
213
|
+
* alone) appended, every other field kept byte for byte. `maxTokens` replaces
|
|
214
|
+
* the output cap when given.
|
|
215
|
+
*
|
|
216
|
+
* Anthropic cache breakpoints: the tail's own markers are dropped, and when the
|
|
217
|
+
* tail starts with the assistant reply, the markers on the captured request's
|
|
218
|
+
* last message move to the reply's last text block (Claude Code's placement).
|
|
219
|
+
* The marker count never grows (the API allows four), the prefix the session
|
|
220
|
+
* cached is read through the lookback, and the reply gets written for the
|
|
221
|
+
* user's next turn. A tail that starts with a user message is merged into a
|
|
222
|
+
* trailing user message, so the roles still alternate.
|
|
223
|
+
*/
|
|
224
|
+
export function extendPayload(capture: RequestCapture, tail: Record<string, unknown>, maxTokens?: number): Record<string, unknown> {
|
|
225
|
+
const key = listKey(capture.api);
|
|
226
|
+
const base = capture.payload[key] as unknown[];
|
|
227
|
+
// The tail's own markers are dropped: the capture's mark the entry the session wrote.
|
|
228
|
+
const extra = stripMarkers(Array.isArray(tail[key]) ? (tail[key] as unknown[]) : []);
|
|
229
|
+
const list =
|
|
230
|
+
capture.api === "anthropic-messages"
|
|
231
|
+
? settleSystemMessages(appendAnthropic(base, extra), capture.api, base.length)
|
|
232
|
+
: capture.api === "bedrock-converse-stream" || capture.api.startsWith("google-")
|
|
233
|
+
? joinTurns(base, extra, capture.api.startsWith("google-") ? "parts" : "content")
|
|
234
|
+
: [...base, ...extra];
|
|
235
|
+
let out: Record<string, unknown> = { ...capture.payload, [key]: list };
|
|
236
|
+
// Google's config carries the request's live abort signal: the side call's own, never the session's.
|
|
237
|
+
if (capture.api.startsWith("google-") && isObject(out.config)) {
|
|
238
|
+
const { abortSignal: _session, ...config } = out.config;
|
|
239
|
+
const own = isObject(tail.config) ? tail.config.abortSignal : undefined;
|
|
240
|
+
out = { ...out, config: own === undefined ? config : { ...config, abortSignal: own } };
|
|
241
|
+
}
|
|
242
|
+
const path = maxTokens === undefined ? undefined : capPath(capture.api, capture.payload);
|
|
243
|
+
return path && maxTokens !== undefined ? withCap(out, path, maxTokens) : out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The tail appended so turns still alternate: a tail that opens with a user
|
|
248
|
+
* turn joins a captured trailing user turn (Bedrock and Google reject two in
|
|
249
|
+
* a row). `field` is where a turn keeps its blocks (`parts` on Google).
|
|
250
|
+
*/
|
|
251
|
+
function joinTurns(base: unknown[], tail: unknown[], field: "content" | "parts"): unknown[] {
|
|
252
|
+
const last = base.at(-1) as Record<string, unknown> | undefined;
|
|
253
|
+
const first = tail[0] as Record<string, unknown> | undefined;
|
|
254
|
+
if (last?.role === "user" && first?.role === "user" && Array.isArray(last[field]) && Array.isArray(first[field])) {
|
|
255
|
+
return [...base.slice(0, -1), { ...last, [field]: [...(last[field] as unknown[]), ...(first[field] as unknown[])] }, ...tail.slice(1)];
|
|
256
|
+
}
|
|
257
|
+
return [...base, ...tail];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The output cap the captured body carries, if any. */
|
|
261
|
+
function capturedMaxTokens(capture: RequestCapture): number | undefined {
|
|
262
|
+
for (const path of CAP_PATHS[capture.api]) {
|
|
263
|
+
const value = capAt(capture.payload, path);
|
|
264
|
+
if (value !== undefined) return value;
|
|
265
|
+
}
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** A replay that would leave less output room than this is not sent as a replay. */
|
|
270
|
+
export const MIN_REPLAY_OUTPUT_TOKENS = 1024;
|
|
271
|
+
/** The output cap when neither the captured body nor the caller names one. */
|
|
272
|
+
const DEFAULT_REPLAY_OUTPUT_TOKENS = 8192;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The messages a side call appends: the reply that closed the exchange, if
|
|
276
|
+
* any, then `before` (a side session's earlier exchanges), then its prompt.
|
|
277
|
+
*/
|
|
278
|
+
export function replayTail(reply: Message | undefined, prompt: string, timestamp = Date.now(), before: readonly Message[] = []): Message[] {
|
|
279
|
+
const question: Message = { role: "user", content: [{ type: "text", text: prompt }], timestamp };
|
|
280
|
+
return [...(reply ? [reply] : []), ...before, question];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The output cap for a replay, or undefined when too little room is left. The
|
|
285
|
+
* captured cap was clamped to what that request left of the window, and the
|
|
286
|
+
* tail spends some of it; `limit` caps it further.
|
|
287
|
+
*/
|
|
288
|
+
export function replayOutputCap(capture: RequestCapture, tail: readonly Message[], limit?: number): number | undefined {
|
|
289
|
+
const captured = capturedMaxTokens(capture);
|
|
290
|
+
const room = captured === undefined ? (limit ?? DEFAULT_REPLAY_OUTPUT_TOKENS) : captured - tail.reduce((sum, message) => sum + estimateMessageTokens(message), 0);
|
|
291
|
+
const cap = limit === undefined ? room : Math.min(limit, room);
|
|
292
|
+
return cap >= MIN_REPLAY_OUTPUT_TOKENS ? cap : undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// Forks: every request of a child session that inherits the transcript
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* A fork's context with the messages its parent's captured request already
|
|
301
|
+
* carries removed: everything up to and including `covers`, system messages
|
|
302
|
+
* kept (pi requires the leading one). What is left is the fork's own tail: the
|
|
303
|
+
* reply that answered the capture, the fork's task, and its own turns. A
|
|
304
|
+
* session fork copies the entries with their timestamps, so the boundary is
|
|
305
|
+
* found by role and timestamp; undefined when it is not there (the fork
|
|
306
|
+
* compacted, or the capture belongs to another branch).
|
|
307
|
+
*/
|
|
308
|
+
export function uncoveredMessages<M extends { role: string; timestamp?: number }>(messages: readonly M[], covers: CoveredThrough): M[] | undefined {
|
|
309
|
+
const boundary = messages.findLastIndex((message) => message.role === covers.role && message.timestamp === covers.timestamp);
|
|
310
|
+
if (boundary === -1) return undefined;
|
|
311
|
+
return [...messages.slice(0, boundary + 1).filter((message) => message.role === "system"), ...messages.slice(boundary + 1)];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Anthropic's limit on `cache_control` breakpoints in one request. */
|
|
315
|
+
export const MAX_CACHE_MARKERS = 4;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* A fork's request: the parent's captured body byte for byte (system prompt,
|
|
319
|
+
* tools, messages, `prompt_cache_key`, every other field), then the fork's own
|
|
320
|
+
* tail from `child` (pi's body for the fork's uncovered messages), so the fork
|
|
321
|
+
* reads the prefix the parent cached (decisions/caching.md, "A call that
|
|
322
|
+
* inherits the session's context reads the session's cache").
|
|
323
|
+
*
|
|
324
|
+
* - Tools stay the parent's. A tool the fork gained after its first request
|
|
325
|
+
* (not in `baseline`, the first request's names), or one a tail
|
|
326
|
+
* `tool_reference` names, is appended so it stays callable; that breaks the
|
|
327
|
+
* cache from there on, as the same load does in the parent.
|
|
328
|
+
* - The output cap is the smaller of the fork's and the parent's cap less the
|
|
329
|
+
* tail, since pi clamped the fork's to a context without the prefix.
|
|
330
|
+
* - Cache markers: the parent's stay where the parent wrote its entry, the
|
|
331
|
+
* tail's mark the fork's own entry, and the earliest are dropped past four.
|
|
332
|
+
*/
|
|
333
|
+
export function forkRequestPayload(capture: RequestCapture, child: Record<string, unknown>, baseline: ReadonlySet<string>): Record<string, unknown> {
|
|
334
|
+
const key = listKey(capture.api);
|
|
335
|
+
const base = capture.payload[key] as unknown[];
|
|
336
|
+
const childList = Array.isArray(child[key]) ? (child[key] as WireMessage[]) : [];
|
|
337
|
+
const parentDeclared = captureDeclarations(capture);
|
|
338
|
+
// The child's own prelude and system messages go; a Responses load item its
|
|
339
|
+
// tool_search placed in the tail stays, less what the capture declares.
|
|
340
|
+
const tail = childList.slice(preludeLength(childList, capture.api)).flatMap((entry): WireMessage[] => {
|
|
341
|
+
if (isToolsItem(entry)) {
|
|
342
|
+
const tools = entry.tools.filter((tool) => !parentDeclared.has(toolName(tool) ?? ""));
|
|
343
|
+
return tools.length > 0 ? [{ ...entry, tools } as WireMessage] : [];
|
|
344
|
+
}
|
|
345
|
+
return entry.role !== "system" && entry.role !== "developer" ? [entry] : [];
|
|
346
|
+
});
|
|
347
|
+
const childTools = [...(Array.isArray(child.tools) ? child.tools : []), ...(leadingToolsItem(child.tools, childList)?.tools ?? [])];
|
|
348
|
+
const inTail = loadedInList(tail);
|
|
349
|
+
const gained = forkGainedTools(parentDeclared, childTools.filter((tool) => !inTail.has(toolName(tool) ?? "")), baseline, tail);
|
|
350
|
+
// A gained tool goes where the capture keeps its tools: in an item at the
|
|
351
|
+
// start of the fork's tail when the capture declares in a leading item
|
|
352
|
+
// (after the parent's prefix, which it then keeps), else appended to the
|
|
353
|
+
// top-level array.
|
|
354
|
+
const parentItem = leadingToolsItem(capture.payload.tools, base);
|
|
355
|
+
const ownTail = gained.length > 0 && parentItem ? [toolsItemFor(gained, parentItem) as WireMessage, ...tail] : tail;
|
|
356
|
+
const out: Record<string, unknown> = { ...capture.payload, [key]: capture.api === "anthropic-messages" ? settleSystemMessages(appendTurns(base, ownTail), capture.api, base.length) : [...base, ...ownTail] };
|
|
357
|
+
// A capture that declares no tools at its head takes the child's own,
|
|
358
|
+
// wherever the child declared them.
|
|
359
|
+
const parentTools = capture.payload.tools;
|
|
360
|
+
const appended = !parentItem && Array.isArray(parentTools) ? gained : [];
|
|
361
|
+
if (!parentItem) {
|
|
362
|
+
const tools = Array.isArray(parentTools) ? (appended.length > 0 ? [...parentTools, ...appended] : parentTools) : childTools.length > 0 ? stripMarkers(childTools) : undefined;
|
|
363
|
+
if (tools) out.tools = tools;
|
|
364
|
+
}
|
|
365
|
+
const capKey = CAP_PATHS[capture.api].find((path) => capAt(capture.payload, path) !== undefined);
|
|
366
|
+
// Appended tool schemas spend room too; the parent's own tools are already in its cap.
|
|
367
|
+
const tailRoom = forkOutputRoom(capture, ownTail);
|
|
368
|
+
const room = tailRoom === undefined ? undefined : tailRoom - (appended.length > 0 ? Math.ceil(JSON.stringify(appended).length / 4) : 0);
|
|
369
|
+
let result = out;
|
|
370
|
+
if (capKey && room !== undefined) {
|
|
371
|
+
// Never above the room: the fork chose the fallback before this when too little was left.
|
|
372
|
+
const own = capAt(child, capKey) ?? room;
|
|
373
|
+
result = withCap(out, capKey, Math.max(1, Math.min(own, room)));
|
|
374
|
+
}
|
|
375
|
+
capCacheMarkers(result, key, capture, ownTail);
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** The tool names a fork's first request declares, wherever it keeps them: the baseline its later gains are measured from. */
|
|
380
|
+
export function forkBaselineNames(payload: Record<string, unknown>, api: string): Set<string> {
|
|
381
|
+
return new Set(api.endsWith("-responses") ? responsesDeclarations(payload, api).keys() : toolNames(payload.tools));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** What each capture declares, computed once: every request of a fork splices the same capture. */
|
|
385
|
+
const declaredByCapture = new WeakMap<RequestCapture, Set<string>>();
|
|
386
|
+
|
|
387
|
+
function captureDeclarations(capture: RequestCapture): Set<string> {
|
|
388
|
+
let names = declaredByCapture.get(capture);
|
|
389
|
+
if (!names) declaredByCapture.set(capture, (names = forkBaselineNames(capture.payload, capture.api)));
|
|
390
|
+
return names;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* The output room a fork request leaves: the captured cap (pi clamped it to
|
|
395
|
+
* what the parent's context left of the window) less a rough size of `tail`.
|
|
396
|
+
* Undefined when the capture names no cap. A fork whose room is below
|
|
397
|
+
* `MIN_REPLAY_OUTPUT_TOKENS` must not be spliced: it goes out whole instead.
|
|
398
|
+
*/
|
|
399
|
+
export function forkOutputRoom(capture: RequestCapture, tail: unknown): number | undefined {
|
|
400
|
+
const cap = capturedMaxTokens(capture);
|
|
401
|
+
return cap === undefined ? undefined : cap - Math.ceil(JSON.stringify(tail).length / 4);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** The names a body's tool list declares, in any API's shape (lib/ledger/wire.ts toolName). */
|
|
405
|
+
export function toolNames(tools: unknown): string[] {
|
|
406
|
+
return Array.isArray(tools) ? tools.map(toolName).filter((name): name is string => name !== undefined) : [];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* The child's tools the capture does not declare and the fork needs: one it
|
|
411
|
+
* gained after its first request (not in `baseline`), or one a tail
|
|
412
|
+
* `tool_reference` names. Markers stripped: the capture's own mark stays.
|
|
413
|
+
*/
|
|
414
|
+
function forkGainedTools(declared: ReadonlySet<string>, child: readonly unknown[], baseline: ReadonlySet<string>, tail: unknown[]): unknown[] {
|
|
415
|
+
const referenced = referencedTools(tail);
|
|
416
|
+
return stripMarkers(
|
|
417
|
+
child.filter((tool) => {
|
|
418
|
+
const name = toolName(tool);
|
|
419
|
+
return name !== undefined && !declared.has(name) && (!baseline.has(name) || referenced.has(name));
|
|
420
|
+
}),
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Tool names the tail's `tool_reference` blocks load (Anthropic tool search). */
|
|
425
|
+
function referencedTools(value: unknown, found = new Set<string>()): Set<string> {
|
|
426
|
+
if (Array.isArray(value)) for (const entry of value) referencedTools(entry, found);
|
|
427
|
+
else if (isObject(value)) {
|
|
428
|
+
if (value.type === "tool_reference" && typeof value.tool_name === "string") found.add(value.tool_name);
|
|
429
|
+
for (const entry of Object.values(value)) referencedTools(entry, found);
|
|
430
|
+
}
|
|
431
|
+
return found;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* `tail` after a captured Anthropic list, with the capture kept a byte
|
|
436
|
+
* prefix. A tail that opens with a user turn joins the captured last one, so
|
|
437
|
+
* the roles alternate. A capture that ends in a content system run (a
|
|
438
|
+
* `<total_tokens>` countdown, a context message) took no clean reply: a user
|
|
439
|
+
* turn after it would strand the run, and the layout pass would move the run
|
|
440
|
+
* behind it, rewriting what the session cached there (findings §73). A text
|
|
441
|
+
* turn goes in as a system message instead, so the run stays where its cache
|
|
442
|
+
* entry was written. A side prompt opens its tail that way, and so does a fork
|
|
443
|
+
* taken before the capture's reply landed (a `/btw` fork while it streams).
|
|
444
|
+
*/
|
|
445
|
+
function appendTurns(base: unknown[], tail: unknown[]): unknown[] {
|
|
446
|
+
const last = base.at(-1) as WireMessage | undefined;
|
|
447
|
+
const first = tail[0] as WireMessage | undefined;
|
|
448
|
+
if (last?.role === "user" && first?.role === "user") {
|
|
449
|
+
return [...base.slice(0, -1), { ...last, content: [...blocksOf(last.content), ...blocksOf(first.content)] }, ...tail.slice(1)];
|
|
450
|
+
}
|
|
451
|
+
if (first?.role === "user" && endsInSystemRun(base) && blocksOf(first.content).every((block) => block.type === "text")) {
|
|
452
|
+
return [...base, { role: "system", content: blocksOf(first.content) }, ...tail.slice(1)];
|
|
453
|
+
}
|
|
454
|
+
return [...base, ...tail];
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Drop the earliest `cache_control` markers (tools, then system, then
|
|
459
|
+
* messages) past the limit. Copy-on-write: the captured body is shared by
|
|
460
|
+
* every fork, so only the path to a dropped marker is copied.
|
|
461
|
+
*/
|
|
462
|
+
function capCacheMarkers(payload: Record<string, unknown>, listKey: string, base: RequestCapture, tail: unknown[]): void {
|
|
463
|
+
const keys = ["tools", "system", listKey].filter((key) => key in payload);
|
|
464
|
+
// Appended tools carry no markers, so the body holds the capture's and the tail's.
|
|
465
|
+
const total = captureMarkers(base) + countMarkers(tail);
|
|
466
|
+
const budget = { drop: total - MAX_CACHE_MARKERS };
|
|
467
|
+
for (const key of keys) {
|
|
468
|
+
if (budget.drop <= 0) break;
|
|
469
|
+
payload[key] = dropFirstMarkers(payload[key], budget);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** The captured body's marker count, walked once per capture: the list can hold a 1M-token transcript. */
|
|
474
|
+
const capturedMarkerCounts = new WeakMap<RequestCapture, number>();
|
|
475
|
+
function captureMarkers(capture: RequestCapture): number {
|
|
476
|
+
let count = capturedMarkerCounts.get(capture);
|
|
477
|
+
if (count === undefined) {
|
|
478
|
+
const payload = capture.payload;
|
|
479
|
+
count = countMarkers(payload.tools) + countMarkers(payload.system) + countMarkers(payload[listKey(capture.api)]);
|
|
480
|
+
capturedMarkerCounts.set(capture, count);
|
|
481
|
+
}
|
|
482
|
+
return count;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function countMarkers(value: unknown): number {
|
|
486
|
+
if (Array.isArray(value)) return value.reduce((sum: number, entry) => sum + countMarkers(entry), 0);
|
|
487
|
+
if (!isObject(value)) return 0;
|
|
488
|
+
let count = "cache_control" in value ? 1 : 0;
|
|
489
|
+
for (const [key, entry] of Object.entries(value)) if (key !== "cache_control") count += countMarkers(entry);
|
|
490
|
+
return count;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function dropFirstMarkers(value: unknown, budget: { drop: number }): unknown {
|
|
494
|
+
if (budget.drop <= 0) return value;
|
|
495
|
+
if (Array.isArray(value)) {
|
|
496
|
+
let changed = false;
|
|
497
|
+
const out = value.map((entry) => {
|
|
498
|
+
const next = dropFirstMarkers(entry, budget);
|
|
499
|
+
if (next !== entry) changed = true;
|
|
500
|
+
return next;
|
|
501
|
+
});
|
|
502
|
+
return changed ? out : value;
|
|
503
|
+
}
|
|
504
|
+
if (!isObject(value)) return value;
|
|
505
|
+
let out: Record<string, unknown> | undefined;
|
|
506
|
+
if ("cache_control" in value) {
|
|
507
|
+
const { cache_control: _, ...rest } = value;
|
|
508
|
+
out = rest;
|
|
509
|
+
budget.drop--;
|
|
510
|
+
}
|
|
511
|
+
for (const [key, entry] of Object.entries(out ?? value)) {
|
|
512
|
+
if (budget.drop <= 0) break;
|
|
513
|
+
const next = dropFirstMarkers(entry, budget);
|
|
514
|
+
if (next !== entry) out = { ...(out ?? value), [key]: next };
|
|
515
|
+
}
|
|
516
|
+
return out ?? value;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
type Block = Record<string, unknown>;
|
|
520
|
+
type WireMessage = { role?: string; content?: unknown } & Record<string, unknown>;
|
|
521
|
+
|
|
522
|
+
function appendAnthropic(base: unknown[], extra: unknown[]): unknown[] {
|
|
523
|
+
if (extra.length === 0) return base;
|
|
524
|
+
const last = base.at(-1) as WireMessage | undefined;
|
|
525
|
+
const first = extra[0] as WireMessage;
|
|
526
|
+
if (last && first.role === "assistant") {
|
|
527
|
+
const markers = blocksOf(last.content).map((block) => block.cache_control).filter((marker) => marker !== undefined);
|
|
528
|
+
const replyBlocks = blocksOf(first.content);
|
|
529
|
+
const anchor = replyBlocks.findLastIndex((block) => block.type === "text");
|
|
530
|
+
if (markers.length === 0 || anchor === -1) return [...base, ...extra];
|
|
531
|
+
const lastUnmarked = { ...last, content: blocksOf(last.content).map(({ cache_control: _, ...rest }) => rest) };
|
|
532
|
+
const content = replyBlocks.map((block, index) => (index === anchor ? { ...block, cache_control: markers.at(-1) } : block));
|
|
533
|
+
return [...base.slice(0, -1), lastUnmarked, { ...first, content }, ...extra.slice(1)];
|
|
534
|
+
}
|
|
535
|
+
return appendTurns(base, extra);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** Whether the list, past its trailing directives, ends in a system message with content. */
|
|
539
|
+
function endsInSystemRun(list: unknown[]): boolean {
|
|
540
|
+
const last = list[appendPoint(list, "anthropic-messages") - 1] as WireMessage | undefined;
|
|
541
|
+
return last?.role === "system" && blocksOf(last.content).length > 0;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function blocksOf(content: unknown): Block[] {
|
|
545
|
+
if (typeof content === "string") return [{ type: "text", text: content }];
|
|
546
|
+
return Array.isArray(content) ? (content as Block[]) : [];
|
|
547
|
+
}
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* module state — so each re-derives it through `sessionScratchpadDir`.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { chmodSync, lstatSync, mkdirSync } from "node:fs";
|
|
17
18
|
import os from "node:os";
|
|
18
|
-
import { join } from "node:path";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
19
20
|
import { findGitRoot } from "./git.ts";
|
|
20
21
|
import { projectSlug } from "./memory.ts";
|
|
21
22
|
import { tryRealpath } from "./paths.ts";
|
|
@@ -52,21 +53,87 @@ export function sessionScratchpadDir(cwd: string, sessionId: string): string {
|
|
|
52
53
|
return scratchpadDir(resolveTmpRoot(), uid, findGitRoot(cwd) ?? cwd, sessionId);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
return
|
|
56
|
+
/** The owner-level directory of a scratchpad path (`<tmp>/onecode-<uid>`). */
|
|
57
|
+
function ownerDirOf(scratchpad: string): string {
|
|
58
|
+
return dirname(dirname(dirname(scratchpad)));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** What `ownerDirProblem` needs from an `lstat`. */
|
|
62
|
+
export interface OwnerDirStat {
|
|
63
|
+
isSymbolicLink: boolean;
|
|
64
|
+
isDirectory: boolean;
|
|
65
|
+
uid: number;
|
|
66
|
+
mode: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Why the owner directory cannot hold this user's scratchpads, or undefined.
|
|
71
|
+
* On a shared `/tmp` the name is predictable from the uid, so another user
|
|
72
|
+
* can create it first: a symlink, a directory they own, or one anyone may
|
|
73
|
+
* write lets them read, rename or replace every session's files under it
|
|
74
|
+
* (the sticky bit on `/tmp` does not protect entries of their directory).
|
|
75
|
+
* Pure.
|
|
76
|
+
*/
|
|
77
|
+
export function ownerDirProblem(stat: OwnerDirStat, uid: number): string | undefined {
|
|
78
|
+
if (stat.isSymbolicLink) return "it is a symlink";
|
|
79
|
+
if (!stat.isDirectory) return "it is not a directory";
|
|
80
|
+
if (stat.uid !== uid) return `it is owned by uid ${stat.uid}`;
|
|
81
|
+
if (stat.mode & 0o022) return "others may write to it";
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
58
84
|
|
|
59
|
-
|
|
60
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Create the scratchpad private to this user, or return false. The owner
|
|
87
|
+
* directory is created 0700 and must then be a real directory this user owns
|
|
88
|
+
* that nobody else may write (`ownerDirProblem`); one created before this
|
|
89
|
+
* check with a looser mode is tightened to 0700, so other users can neither
|
|
90
|
+
* list the project paths in it nor read what the model writes. Every level
|
|
91
|
+
* below is created 0700 too. Windows has no shared temp root (`%TEMP%` is per
|
|
92
|
+
* user), so there it only creates the directory.
|
|
93
|
+
*/
|
|
94
|
+
export function ensurePrivateScratchpad(dir: string, uid: number | undefined = typeof process.getuid === "function" ? process.getuid() : undefined): boolean {
|
|
95
|
+
try {
|
|
96
|
+
if (uid !== undefined) {
|
|
97
|
+
const owner = ownerDirOf(dir);
|
|
98
|
+
mkdirSync(owner, { recursive: true, mode: 0o700 });
|
|
99
|
+
const stat = lstatSync(owner);
|
|
100
|
+
if (ownerDirProblem({ isSymbolicLink: stat.isSymbolicLink(), isDirectory: stat.isDirectory(), uid: stat.uid, mode: stat.mode }, uid)) return false;
|
|
101
|
+
if (stat.mode & 0o077) chmodSync(owner, 0o700);
|
|
102
|
+
}
|
|
103
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
104
|
+
return true;
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
61
109
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Whether an existing scratchpad sits under a private owner directory, with
|
|
112
|
+
* no side effects: for a gate that trusts a scratchpad another extension
|
|
113
|
+
* created.
|
|
114
|
+
*/
|
|
115
|
+
export function isPrivateScratchpad(dir: string, uid: number | undefined = typeof process.getuid === "function" ? process.getuid() : undefined): boolean {
|
|
116
|
+
if (uid === undefined) return true;
|
|
117
|
+
try {
|
|
118
|
+
const stat = lstatSync(ownerDirOf(dir));
|
|
119
|
+
return ownerDirProblem({ isSymbolicLink: stat.isSymbolicLink(), isDirectory: stat.isDirectory(), uid: stat.uid, mode: stat.mode }, uid) === undefined && (stat.mode & 0o077) === 0;
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
68
124
|
|
|
69
|
-
|
|
125
|
+
/** The session's scratchpad, created private to this user, or undefined when it cannot be. */
|
|
126
|
+
export function privateSessionScratchpadDir(cwd: string, sessionId: string): string | undefined {
|
|
127
|
+
const dir = sessionScratchpadDir(cwd, sessionId);
|
|
128
|
+
return ensurePrivateScratchpad(dir) ? dir : undefined;
|
|
129
|
+
}
|
|
70
130
|
|
|
71
|
-
|
|
131
|
+
/**
|
|
132
|
+
* A session's own private temp directory, the scratchpad's parent
|
|
133
|
+
* (`<tmp>/onecode-<uid>/<project-slug>/<session-id>`), created private to
|
|
134
|
+
* this user, or undefined when it cannot be.
|
|
135
|
+
*/
|
|
136
|
+
export function privateSessionTempDir(cwd: string, sessionId: string): string | undefined {
|
|
137
|
+
const scratchpad = sessionScratchpadDir(cwd, sessionId);
|
|
138
|
+
return ensurePrivateScratchpad(scratchpad) ? dirname(scratchpad) : undefined;
|
|
72
139
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emitted by the send-now extension just before it aborts a turn to deliver
|
|
3
|
+
* the queued messages, so the interrupted extension skips its "Interrupted ·
|
|
4
|
+
* What should One Code do instead?" note for that abort: the user has already
|
|
5
|
+
* said what to do instead.
|
|
6
|
+
*/
|
|
7
|
+
export const SEND_NOW_CHANNEL = "one-code:send-now";
|