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,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PowerShell's own parser as a service: one long-lived parse server per
|
|
3
|
+
* process, run on the PowerShell the tool itself runs, so the gate reads a
|
|
4
|
+
* line exactly as it will execute (decisions/windows.md "The PowerShell
|
|
5
|
+
* pre-gate reads PowerShell's own parse"; the tree-sitter grammars it replaces
|
|
6
|
+
* are measured in findings §41).
|
|
7
|
+
*
|
|
8
|
+
* {@link PowerShellParser.parse} never rejects. Anything short of a clean
|
|
9
|
+
* answer, whether the server could not start, timed out, crashed, runs in
|
|
10
|
+
* Constrained Language Mode or stopped for good after too many crashes,
|
|
11
|
+
* resolves to `{ ok: false, reason }`, and the caller treats the line as
|
|
12
|
+
* unparsed (the classifier decides). A parse with errors is still `ok: true`:
|
|
13
|
+
* the tree and its `errors` are PowerShell's answer; judging them is the
|
|
14
|
+
* caller's job.
|
|
15
|
+
*
|
|
16
|
+
* Lifecycle. The server starts on the first request and stops after
|
|
17
|
+
* `idleMs` without one; the next request starts it again. A crash, or a
|
|
18
|
+
* request that times out (the server is then killed), counts toward
|
|
19
|
+
* `maxRestarts`; past it the parser stays unavailable for the life of the
|
|
20
|
+
* process. The child, its pipes and every timer are unref'd, so an idle
|
|
21
|
+
* server never holds a finished one-shot run open; a caller that awaits a
|
|
22
|
+
* parse in one holds its own ref (`withKeepAlive`). The server also exits on
|
|
23
|
+
* its own when this process ends, because its stdin closes.
|
|
24
|
+
*
|
|
25
|
+
* Pure: no pi imports. The caller supplies `spawnServer`, which starts the
|
|
26
|
+
* right executable with the bootstrap after `-Command`, so the BI fork can use
|
|
27
|
+
* its own launcher.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type { ChildProcess } from "node:child_process";
|
|
31
|
+
import { POWERSHELL_PARSE_BOOTSTRAP, POWERSHELL_PARSE_WALKER } from "./powershell-parse-script.ts";
|
|
32
|
+
import { unrefHandle } from "./process-tree.ts";
|
|
33
|
+
|
|
34
|
+
/** One syntax-tree node, as the walker reports it (powershell-parse-script.ts). */
|
|
35
|
+
export interface PowerShellAstNode {
|
|
36
|
+
/** The .NET type name: `CommandAst`, `StringConstantExpressionAst`, … */
|
|
37
|
+
type: string;
|
|
38
|
+
/** Index of the parent node in `nodes`; -1 for the root. */
|
|
39
|
+
parent: number;
|
|
40
|
+
/** Extent as UTF-16 offsets into the command (JS string indices). */
|
|
41
|
+
start: number;
|
|
42
|
+
end: number;
|
|
43
|
+
/** CommandAst: the static command name (null when not static). CommandParameterAst: the parameter. VariableExpressionAst: the variable path. Type nodes: the type's full name. */
|
|
44
|
+
name?: string | null;
|
|
45
|
+
/** String and constant nodes: the value PowerShell resolved (quotes and escapes applied). */
|
|
46
|
+
value?: string | null;
|
|
47
|
+
/** String nodes: `BareWord`, `SingleQuoted`, `DoubleQuoted`, `SingleQuotedHereString`, `DoubleQuotedHereString`. */
|
|
48
|
+
kind?: string;
|
|
49
|
+
/** CommandAst: the invocation operator (`Unknown`, `Ampersand`, `Dot`). Operator nodes: the operator. */
|
|
50
|
+
operator?: string;
|
|
51
|
+
/** VariableExpressionAst: `@name` splatting. */
|
|
52
|
+
splatted?: boolean;
|
|
53
|
+
/** Redirections: the stream redirected (`Output`, `Error`, `All`, …) and, for a merge, where to. */
|
|
54
|
+
from?: string;
|
|
55
|
+
to?: string;
|
|
56
|
+
/** FileRedirectionAst: `>>`. */
|
|
57
|
+
append?: boolean;
|
|
58
|
+
/** Pipelines and pipeline chains: a trailing `&` (PowerShell 7). */
|
|
59
|
+
background?: boolean;
|
|
60
|
+
/** Member access: `::`. */
|
|
61
|
+
static?: boolean;
|
|
62
|
+
/** CommandAst with a static name that resolves in this PowerShell: the resolved command's type (`Cmdlet`, `Application`, `Function`, …), after an alias. */
|
|
63
|
+
commandType?: string;
|
|
64
|
+
/** CommandAst: the name was an alias. */
|
|
65
|
+
alias?: boolean;
|
|
66
|
+
/** CommandAst: the resolved cmdlet's name, or an application's full path. */
|
|
67
|
+
resolvedName?: string;
|
|
68
|
+
/** CommandAst: the resolved command's module (empty for an application). */
|
|
69
|
+
module?: string;
|
|
70
|
+
/**
|
|
71
|
+
* CommandAst of a cmdlet: PowerShell's static parameter binding. `value` is
|
|
72
|
+
* the bound argument's node index, -1 for a switch given alone, and -2 for a
|
|
73
|
+
* value the binder built itself: the arguments a remaining-arguments
|
|
74
|
+
* parameter collects (`Write-Output a b`), whose node indices are
|
|
75
|
+
* `elements` (-1 for one that matched no argument).
|
|
76
|
+
*/
|
|
77
|
+
bindings?: Array<{ parameter: string; value: number; elements?: number[] }>;
|
|
78
|
+
/** CommandAst of a cmdlet: the arguments or parameters the binder could not bind (an unknown or ambiguous name, a surplus positional, a repeat). */
|
|
79
|
+
bindingErrors?: string[];
|
|
80
|
+
/** CommandAst of a cmdlet: the static binder is missing or threw. */
|
|
81
|
+
bindingUnavailable?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface PowerShellParse {
|
|
85
|
+
/** The tree in pre-order, root first. */
|
|
86
|
+
nodes: PowerShellAstNode[];
|
|
87
|
+
/** Each parse error's ErrorId. PowerShell runs nothing from a line that has one. */
|
|
88
|
+
errors: string[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type PowerShellParseOutcome = { ok: true; parse: PowerShellParse } | { ok: false; reason: string };
|
|
92
|
+
|
|
93
|
+
export interface PowerShellParserOptions {
|
|
94
|
+
/** Start the server: the PowerShell executable with `bootstrap` as the argument after `-Command`, stdio all piped. */
|
|
95
|
+
spawnServer: (bootstrap: string) => ChildProcess;
|
|
96
|
+
/** How long the first request waits while the server starts. */
|
|
97
|
+
startTimeoutMs?: number;
|
|
98
|
+
/** How long a request waits once the server is ready. */
|
|
99
|
+
requestTimeoutMs?: number;
|
|
100
|
+
/** Crashes and timeouts tolerated before the parser stays unavailable. */
|
|
101
|
+
maxRestarts?: number;
|
|
102
|
+
/** Stop the server after this long without a request. */
|
|
103
|
+
idleMs?: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const POWERSHELL_PARSE_START_TIMEOUT_MS = 5_000;
|
|
107
|
+
export const POWERSHELL_PARSE_REQUEST_TIMEOUT_MS = 2_000;
|
|
108
|
+
export const POWERSHELL_PARSE_MAX_RESTARTS = 3;
|
|
109
|
+
export const POWERSHELL_PARSE_IDLE_MS = 10 * 60_000;
|
|
110
|
+
|
|
111
|
+
interface Pending {
|
|
112
|
+
resolve: (outcome: PowerShellParseOutcome) => void;
|
|
113
|
+
timer: ReturnType<typeof setTimeout>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface Server {
|
|
117
|
+
child: ChildProcess;
|
|
118
|
+
ready: boolean;
|
|
119
|
+
/** Set once the server is gone or being stopped; its late output and exit are ignored. */
|
|
120
|
+
stopped: boolean;
|
|
121
|
+
/** Why it stopped. */
|
|
122
|
+
reason?: string;
|
|
123
|
+
buffer: string;
|
|
124
|
+
pending: Map<number, Pending>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const encode = (text: string) => Buffer.from(text, "utf8").toString("base64");
|
|
128
|
+
|
|
129
|
+
export class PowerShellParser {
|
|
130
|
+
private readonly options: Required<PowerShellParserOptions>;
|
|
131
|
+
private server: Server | undefined;
|
|
132
|
+
private nextId = 1;
|
|
133
|
+
private failures = 0;
|
|
134
|
+
/** Why the parser is unavailable for good; set past `maxRestarts`, or when the server runs in a restricted language mode. */
|
|
135
|
+
private disabled: string | undefined;
|
|
136
|
+
private idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
137
|
+
|
|
138
|
+
constructor(options: PowerShellParserOptions) {
|
|
139
|
+
this.options = {
|
|
140
|
+
startTimeoutMs: POWERSHELL_PARSE_START_TIMEOUT_MS,
|
|
141
|
+
requestTimeoutMs: POWERSHELL_PARSE_REQUEST_TIMEOUT_MS,
|
|
142
|
+
maxRestarts: POWERSHELL_PARSE_MAX_RESTARTS,
|
|
143
|
+
idleMs: POWERSHELL_PARSE_IDLE_MS,
|
|
144
|
+
...options,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Why the parser cannot answer at all, or undefined while it can (or can still start). */
|
|
149
|
+
unavailable(): string | undefined {
|
|
150
|
+
return this.disabled;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Parse `command` with PowerShell's own parser. Never rejects. */
|
|
154
|
+
parse(command: string): Promise<PowerShellParseOutcome> {
|
|
155
|
+
if (this.disabled) return Promise.resolve({ ok: false, reason: this.disabled });
|
|
156
|
+
let server: Server;
|
|
157
|
+
try {
|
|
158
|
+
server = this.server ?? this.start();
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return Promise.resolve({ ok: false, reason: this.fail(`PowerShell could not start: ${message(error)}`) });
|
|
161
|
+
}
|
|
162
|
+
// A server can fail while it starts (a spawn error, a restricted language
|
|
163
|
+
// mode) before this request is registered with it.
|
|
164
|
+
if (server.stopped) return Promise.resolve({ ok: false, reason: this.disabled ?? server.reason ?? "the PowerShell parser stopped" });
|
|
165
|
+
this.touch();
|
|
166
|
+
const id = this.nextId++;
|
|
167
|
+
return new Promise((resolve) => {
|
|
168
|
+
const timeoutMs = server.ready ? this.options.requestTimeoutMs : this.options.startTimeoutMs;
|
|
169
|
+
const timer = setTimeout(() => {
|
|
170
|
+
server.pending.delete(id);
|
|
171
|
+
resolve({ ok: false, reason: `the PowerShell parser did not answer within ${timeoutMs} ms` });
|
|
172
|
+
this.crashed(server, `the PowerShell parser timed out after ${timeoutMs} ms`);
|
|
173
|
+
}, timeoutMs);
|
|
174
|
+
unrefHandle(timer);
|
|
175
|
+
server.pending.set(id, { resolve, timer });
|
|
176
|
+
server.child.stdin?.write(`${id} ${encode(command)}\n`);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Stop the server. The next request starts a new one. */
|
|
181
|
+
stop(): void {
|
|
182
|
+
const server = this.server;
|
|
183
|
+
if (!server) return;
|
|
184
|
+
this.server = undefined;
|
|
185
|
+
this.clearIdle();
|
|
186
|
+
this.settleAll(server, "the PowerShell parser was stopped");
|
|
187
|
+
server.stopped = true;
|
|
188
|
+
server.child.stdin?.end();
|
|
189
|
+
server.child.kill();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private start(): Server {
|
|
193
|
+
const child = this.options.spawnServer(POWERSHELL_PARSE_BOOTSTRAP);
|
|
194
|
+
const server: Server = { child, ready: false, stopped: false, buffer: "", pending: new Map() };
|
|
195
|
+
this.server = server;
|
|
196
|
+
child.stdin?.on("error", () => {});
|
|
197
|
+
child.stdout?.setEncoding("ascii");
|
|
198
|
+
child.stdout?.on("data", (chunk: string) => this.receive(server, chunk));
|
|
199
|
+
// PowerShell's own error output is not part of the protocol; drain it so
|
|
200
|
+
// the pipe never fills.
|
|
201
|
+
child.stderr?.on("data", () => {});
|
|
202
|
+
child.on("error", (error) => this.crashed(server, `PowerShell could not start: ${message(error)}`));
|
|
203
|
+
child.on("exit", (code, signal) => this.crashed(server, `the PowerShell parser exited (${signal ?? `code ${code}`})`));
|
|
204
|
+
unrefHandle(child);
|
|
205
|
+
unrefHandle(child.stdin);
|
|
206
|
+
unrefHandle(child.stdout);
|
|
207
|
+
unrefHandle(child.stderr);
|
|
208
|
+
child.stdin?.write(`${encode(POWERSHELL_PARSE_WALKER)}\n`);
|
|
209
|
+
return server;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private receive(server: Server, chunk: string): void {
|
|
213
|
+
if (server.stopped) return;
|
|
214
|
+
server.buffer += chunk;
|
|
215
|
+
for (let newline = server.buffer.indexOf("\n"); newline >= 0; newline = server.buffer.indexOf("\n")) {
|
|
216
|
+
const line = server.buffer.slice(0, newline).trim();
|
|
217
|
+
server.buffer = server.buffer.slice(newline + 1);
|
|
218
|
+
if (line) this.record(server, line);
|
|
219
|
+
if (server.stopped) return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private record(server: Server, line: string): void {
|
|
224
|
+
let record: Record<string, unknown>;
|
|
225
|
+
try {
|
|
226
|
+
record = JSON.parse(Buffer.from(line, "base64").toString("utf8")) as Record<string, unknown>;
|
|
227
|
+
} catch {
|
|
228
|
+
this.crashed(server, "the PowerShell parser wrote a line that is not part of its protocol");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (record.id === 0) {
|
|
232
|
+
if (record.language !== "FullLanguage") {
|
|
233
|
+
// Constrained Language Mode (or stricter) is a machine policy, so a
|
|
234
|
+
// restart cannot change it.
|
|
235
|
+
this.disabled = `PowerShell runs in ${String(record.language)} mode, where the parser is not available`;
|
|
236
|
+
this.crashed(server, this.disabled);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Failures are not reset here: a line that hangs the walker would
|
|
240
|
+
// otherwise restart it forever.
|
|
241
|
+
server.ready = true;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const pending = typeof record.id === "number" ? server.pending.get(record.id) : undefined;
|
|
245
|
+
if (!pending) return;
|
|
246
|
+
server.pending.delete(record.id as number);
|
|
247
|
+
clearTimeout(pending.timer);
|
|
248
|
+
if (typeof record.failure === "string") {
|
|
249
|
+
pending.resolve({ ok: false, reason: `the PowerShell parser failed: ${record.failure}` });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
pending.resolve({ ok: true, parse: { nodes: asArray(record.nodes).map(normalizeNode), errors: asArray(record.errors).map(String) } });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The server is gone or unusable: settle what waits on it and count the failure. */
|
|
256
|
+
private crashed(server: Server, reason: string): void {
|
|
257
|
+
if (server.stopped) return;
|
|
258
|
+
server.stopped = true;
|
|
259
|
+
server.reason = reason;
|
|
260
|
+
if (this.server === server) this.server = undefined;
|
|
261
|
+
this.clearIdle();
|
|
262
|
+
this.settleAll(server, reason);
|
|
263
|
+
server.child.stdin?.end();
|
|
264
|
+
server.child.kill();
|
|
265
|
+
this.fail(reason);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private fail(reason: string): string {
|
|
269
|
+
this.failures++;
|
|
270
|
+
if (!this.disabled && this.failures > this.options.maxRestarts) {
|
|
271
|
+
this.disabled = `the PowerShell parser is off after ${this.failures} failures (last: ${reason})`;
|
|
272
|
+
}
|
|
273
|
+
return reason;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private settleAll(server: Server, reason: string): void {
|
|
277
|
+
for (const pending of server.pending.values()) {
|
|
278
|
+
clearTimeout(pending.timer);
|
|
279
|
+
pending.resolve({ ok: false, reason });
|
|
280
|
+
}
|
|
281
|
+
server.pending.clear();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private touch(): void {
|
|
285
|
+
this.clearIdle();
|
|
286
|
+
this.idleTimer = setTimeout(() => this.stop(), this.options.idleMs);
|
|
287
|
+
unrefHandle(this.idleTimer);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private clearIdle(): void {
|
|
291
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
292
|
+
this.idleTimer = undefined;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* A node with its list fields made lists: ConvertTo-Json may write a
|
|
298
|
+
* one-element list as the element. A binding list that is not one after
|
|
299
|
+
* that marks the binding unavailable, which the gate refuses.
|
|
300
|
+
*/
|
|
301
|
+
function normalizeNode(value: unknown): PowerShellAstNode {
|
|
302
|
+
const node = { ...(value as PowerShellAstNode) };
|
|
303
|
+
if (node.bindingErrors !== undefined) node.bindingErrors = asArray(node.bindingErrors).map(String);
|
|
304
|
+
if (node.bindings !== undefined) {
|
|
305
|
+
const bindings = asArray(node.bindings) as Array<{ parameter?: unknown; value?: unknown; elements?: unknown }>;
|
|
306
|
+
if (bindings.every((b) => b !== null && typeof b === "object" && typeof b.parameter === "string" && typeof b.value === "number")) {
|
|
307
|
+
node.bindings = bindings.map((b) => ({ parameter: b.parameter as string, value: b.value as number, ...(b.elements === undefined ? {} : { elements: asArray(b.elements).map(Number) }) }));
|
|
308
|
+
} else {
|
|
309
|
+
delete node.bindings;
|
|
310
|
+
node.bindingUnavailable = true;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return node;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function asArray(value: unknown): unknown[] {
|
|
317
|
+
if (Array.isArray(value)) return value;
|
|
318
|
+
// ConvertTo-Json writes a one-element array as that element on some
|
|
319
|
+
// PowerShell versions, and an empty one as null.
|
|
320
|
+
return value === undefined || value === null ? [] : [value];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function message(error: unknown): string {
|
|
324
|
+
return error instanceof Error ? error.message : String(error);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const STATE_KEY = Symbol.for("one-code.powershell-parser");
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The process-wide parser for one PowerShell executable. It lives on
|
|
331
|
+
* `globalThis`, not in module state: jiti gives each extension, and each
|
|
332
|
+
* in-process child session, its own instance of this module (findings §3), and
|
|
333
|
+
* one server per process is the point.
|
|
334
|
+
*/
|
|
335
|
+
export function sharedPowerShellParser(executable: string, create: () => PowerShellParser): PowerShellParser {
|
|
336
|
+
const store = globalThis as { [STATE_KEY]?: Map<string, PowerShellParser> };
|
|
337
|
+
const parsers = (store[STATE_KEY] ??= new Map());
|
|
338
|
+
let parser = parsers.get(executable);
|
|
339
|
+
if (!parser) {
|
|
340
|
+
parser = create();
|
|
341
|
+
parsers.set(executable, parser);
|
|
342
|
+
}
|
|
343
|
+
return parser;
|
|
344
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep the previous request's conversation breakpoint (pure).
|
|
3
|
+
*
|
|
4
|
+
* Anthropic finds an earlier cache entry only a limited way back from a
|
|
5
|
+
* breakpoint, about 20 content blocks, and pi moves the one conversation
|
|
6
|
+
* breakpoint to each request's tail. A request that adds many blocks since the
|
|
7
|
+
* last one (a large parallel tool batch), or a long run of `tool_addition`
|
|
8
|
+
* blocks after the session-facts system message, then cannot reach the entry
|
|
9
|
+
* the previous request wrote and re-caches the whole conversation (findings
|
|
10
|
+
* §72: a live pair read 11,707 of 24,837 tokens, and 24,837 with this mark).
|
|
11
|
+
* Marking the block the previous request marked again lets the server match
|
|
12
|
+
* that entry directly. It costs nothing: the entry exists, so the mark reads.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { appendPoint } from "./ledger/wire.ts";
|
|
16
|
+
import { MAX_CACHE_MARKERS } from "./request-replay.ts";
|
|
17
|
+
|
|
18
|
+
type Block = Record<string, unknown> & { cache_control?: unknown };
|
|
19
|
+
type Message = { role?: unknown; content?: unknown };
|
|
20
|
+
|
|
21
|
+
/** Where a request's last conversation breakpoint sat, and the block it marked. */
|
|
22
|
+
export interface MessageMark {
|
|
23
|
+
message: number;
|
|
24
|
+
block: number;
|
|
25
|
+
marker: unknown;
|
|
26
|
+
/** The marked block itself: pi builds each request afresh but reuses large strings (an image's data), so a structural compare short-circuits on them. */
|
|
27
|
+
content: Block;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const blocksOf = (message: unknown): Block[] =>
|
|
31
|
+
Array.isArray((message as Message | null)?.content) ? ((message as Message).content as Block[]) : [];
|
|
32
|
+
|
|
33
|
+
/** Equal JSON values, ignoring a top-level `cache_control`. */
|
|
34
|
+
function sameBlock(a: Block, b: Block): boolean {
|
|
35
|
+
const keys = (block: Block) => Object.keys(block).filter((key) => key !== "cache_control");
|
|
36
|
+
const left = keys(a);
|
|
37
|
+
return left.length === keys(b).length && left.every((key) => sameValue(a[key], b[key]));
|
|
38
|
+
}
|
|
39
|
+
function sameValue(a: unknown, b: unknown): boolean {
|
|
40
|
+
if (a === b) return true;
|
|
41
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null || Array.isArray(a) !== Array.isArray(b)) return false;
|
|
42
|
+
const left = Object.keys(a);
|
|
43
|
+
return left.length === Object.keys(b).length && left.every((key) => sameValue((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The last breakpoint in `messages`, or undefined when none carries one. */
|
|
47
|
+
export function lastMessageMark(messages: unknown): MessageMark | undefined {
|
|
48
|
+
if (!Array.isArray(messages)) return undefined;
|
|
49
|
+
for (let message = messages.length - 1; message >= 0; message--) {
|
|
50
|
+
const blocks = blocksOf(messages[message]);
|
|
51
|
+
for (let block = blocks.length - 1; block >= 0; block--) {
|
|
52
|
+
const marker = blocks[block]?.cache_control;
|
|
53
|
+
if (marker !== undefined) return { message, block, marker, content: blocks[block] };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function marksIn(value: unknown): number {
|
|
60
|
+
if (!Array.isArray(value)) return 0;
|
|
61
|
+
return value.reduce((sum: number, entry) => {
|
|
62
|
+
const own = (entry as Block | null)?.cache_control !== undefined ? 1 : 0;
|
|
63
|
+
return sum + own + blocksOf(entry).filter((block) => block?.cache_control !== undefined).length;
|
|
64
|
+
}, 0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The payload with `previous` marked again: only when that block is still
|
|
69
|
+
* there with the same bytes, no later block already took its place as the
|
|
70
|
+
* tail mark, and a breakpoint slot is free. Otherwise the payload unchanged.
|
|
71
|
+
*/
|
|
72
|
+
export function withPreviousMark<T extends Record<string, unknown>>(payload: T, previous: MessageMark | undefined): T {
|
|
73
|
+
if (!previous || !Array.isArray(payload.messages)) return payload;
|
|
74
|
+
const messages = payload.messages as Message[];
|
|
75
|
+
const blocks = blocksOf(messages[previous.message]);
|
|
76
|
+
const target = blocks[previous.block];
|
|
77
|
+
if (!target || target.cache_control !== undefined || !sameBlock(target, previous.content)) return payload;
|
|
78
|
+
if (marksIn(payload.tools) + marksIn(payload.system) + marksIn(messages) >= MAX_CACHE_MARKERS) return payload;
|
|
79
|
+
const copy = [...messages];
|
|
80
|
+
const content = [...blocks];
|
|
81
|
+
content[previous.block] = { ...target, cache_control: previous.marker };
|
|
82
|
+
copy[previous.message] = { ...messages[previous.message], content };
|
|
83
|
+
return { ...payload, messages: copy };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The breakpoint the base request carried, located in `messages` (the request
|
|
88
|
+
* that extends it, spliced): the last block of the base's last element before
|
|
89
|
+
* its trailing directives, with the marker this request's own tail mark uses.
|
|
90
|
+
* Undefined when the request has no tail mark or the base is empty.
|
|
91
|
+
*/
|
|
92
|
+
export function baseMark(messages: unknown, baseList: readonly unknown[], api: string): MessageMark | undefined {
|
|
93
|
+
if (!Array.isArray(messages)) return undefined;
|
|
94
|
+
const end = appendPoint(baseList, api);
|
|
95
|
+
const tail = lastMessageMark(messages);
|
|
96
|
+
if (end === 0 || !tail || tail.message < end) return undefined;
|
|
97
|
+
const blocks = blocksOf(messages[end - 1]);
|
|
98
|
+
const block = blocks.length - 1;
|
|
99
|
+
if (block < 0) return undefined;
|
|
100
|
+
return { message: end - 1, block, marker: tail.marker, content: blocks[block] };
|
|
101
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process state: what lives as long as the One Code process and no longer
|
|
3
|
+
* (lifecycle-grid-hunt plan, part D). Every piece of state an extension holds
|
|
4
|
+
* is one of three kinds:
|
|
5
|
+
*
|
|
6
|
+
* - branch state follows the conversation: a resume, `/reload`, `/fork` and
|
|
7
|
+
* `/tree` each read it back from the branch (a `one-code:state` entry,
|
|
8
|
+
* lib/ledger/state.ts, or a fold of the branch's own entries);
|
|
9
|
+
* - process state survives `/reload` and the session swaps of `/clear`,
|
|
10
|
+
* `/new`, `/resume` and `/fork`, and ends with the process (a grant the
|
|
11
|
+
* user gave "for this session", a notice owed to the next session): this
|
|
12
|
+
* module;
|
|
13
|
+
* - resources (a timer, a child process, a connection) end with the instance
|
|
14
|
+
* that opened them, in `session_shutdown`.
|
|
15
|
+
*
|
|
16
|
+
* Module scope is none of them: pi re-evaluates every extension module at a
|
|
17
|
+
* `/reload` and at a `/resume` into another directory (findings §8), so a
|
|
18
|
+
* module-level value is lost there and kept everywhere else. The store here
|
|
19
|
+
* is a `globalThis` slot, which no re-evaluation resets.
|
|
20
|
+
*
|
|
21
|
+
* Pure: no pi imports.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const SLOT = Symbol.for("one-code:process-state");
|
|
25
|
+
|
|
26
|
+
type Store = Map<string, unknown>;
|
|
27
|
+
|
|
28
|
+
function store(): Store {
|
|
29
|
+
const slot = globalThis as { [SLOT]?: Store };
|
|
30
|
+
return (slot[SLOT] ??= new Map());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The process's value for `key`, created by `init` on first use. Keys are
|
|
35
|
+
* namespaced by their owner (`"permissions:grants"`); the value is shared by
|
|
36
|
+
* every instance of that owner in the process, so it must be mutated, not
|
|
37
|
+
* replaced.
|
|
38
|
+
*/
|
|
39
|
+
export function processState<T>(key: string, init: () => T): T {
|
|
40
|
+
const values = store();
|
|
41
|
+
if (!values.has(key)) values.set(key, init());
|
|
42
|
+
return values.get(key) as T;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Forget every process value: a test's fresh process. */
|
|
46
|
+
export function resetProcessStateForTest(): void {
|
|
47
|
+
store().clear();
|
|
48
|
+
}
|
|
@@ -14,9 +14,22 @@
|
|
|
14
14
|
* The producer spawns `detached` (own process group; `detachedSpawnOptions`)
|
|
15
15
|
* and stops it with `killProcessTree`, which signals the negative pid — the
|
|
16
16
|
* whole group — falling back to the child alone when the group is already gone.
|
|
17
|
-
* `stopProcessTree` adds the grace period: SIGTERM now, SIGKILL
|
|
18
|
-
*
|
|
19
|
-
* which
|
|
17
|
+
* `stopProcessTree` adds the grace period: SIGTERM now, SIGKILL to the group
|
|
18
|
+
* when it ends, whether or not the leader is still alive. The leader is often
|
|
19
|
+
* only the `bash -c` wrapper, which dies on TERM at once while the command it
|
|
20
|
+
* forked (a dev server in its graceful-shutdown handler) keeps the group and
|
|
21
|
+
* its port; a leader-only check skipped the SIGKILL exactly then
|
|
22
|
+
* (LIFECYCLE-BACKGROUND-REVIEW-2026-09-26 M1, measured). A group whose SIGKILL
|
|
23
|
+
* is still pending when the process exits (pi's quit calls `process.exit`,
|
|
24
|
+
* and the grace timer is unref'd) is SIGKILLed from a `process.on("exit")`
|
|
25
|
+
* hook, so nothing One Code stopped outlives it.
|
|
26
|
+
*
|
|
27
|
+
* `rememberProcessGroup` owns a background shell's group for the lifetime of
|
|
28
|
+
* this process, including descendants left by a successfully exited leader.
|
|
29
|
+
* Session replacement must not kill those intentionally launched daemons.
|
|
30
|
+
* Each module instance keeps its own groups through process listeners (never
|
|
31
|
+
* through a session context or cross-extension module state), reaps empty
|
|
32
|
+
* groups, and sends TERM synchronously on process exit or termination signals.
|
|
20
33
|
*
|
|
21
34
|
* `waitForChildExit` is the counterpart on the waiting side: it settles on
|
|
22
35
|
* `exit` plus a short stdio grace, never on `close` alone — a descendant the
|
|
@@ -37,6 +50,16 @@ export const EXIT_STDIO_GRACE_MS = 200;
|
|
|
37
50
|
/** ...and the most it may keep arriving in total: a straggler writing continuously must not keep re-arming the grace. */
|
|
38
51
|
export const EXIT_STDIO_MAX_MS = 2_000;
|
|
39
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Unref a child process, one of its stdio streams or a timer, so it never
|
|
55
|
+
* holds the event loop open on its own (a finished one-shot run can exit);
|
|
56
|
+
* a caller awaiting it holds its own ref. Child pipes are sockets at run
|
|
57
|
+
* time and carry `unref()` even though their stream types do not declare it.
|
|
58
|
+
*/
|
|
59
|
+
export function unrefHandle(handle: unknown): void {
|
|
60
|
+
(handle as { unref?: () => void } | null | undefined)?.unref?.();
|
|
61
|
+
}
|
|
62
|
+
|
|
40
63
|
/**
|
|
41
64
|
* `detached` everywhere but Windows, where process groups do not exist — except
|
|
42
65
|
* for a child that must outlive an exiting parent (a fire-and-forget hook),
|
|
@@ -95,20 +118,121 @@ export function killProcessTree(child: ChildProcess, signal: NodeJS.Signals = "S
|
|
|
95
118
|
}
|
|
96
119
|
}
|
|
97
120
|
|
|
121
|
+
/** Groups owned by this producer, retained by process listeners across session replacement. */
|
|
122
|
+
const rememberedGroups = new Set<ChildProcess>();
|
|
123
|
+
let groupReaper: NodeJS.Timeout | undefined;
|
|
124
|
+
const terminationSignals = ["SIGINT", "SIGTERM", "SIGHUP"] as const;
|
|
125
|
+
const groupSignalHandlers = new Map<NodeJS.Signals, () => void>();
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Whether a remembered tree still needs killing at exit. Windows has no
|
|
129
|
+
* process groups, so only a live leader is worth a `taskkill /T`; it is judged
|
|
130
|
+
* by the child's own exit state, because once the leader exits Windows may
|
|
131
|
+
* hand its pid to an unrelated process that a pid probe would then find. On
|
|
132
|
+
* POSIX a group id stays reserved while any member lives, and the reaper
|
|
133
|
+
* forgets an emptied group within a second, far sooner than sequential pid
|
|
134
|
+
* allocation wraps around to it.
|
|
135
|
+
*/
|
|
136
|
+
export function rememberedGroupAlive(child: ChildProcess): boolean {
|
|
137
|
+
if (child.pid == null) return false;
|
|
138
|
+
if (process.platform === "win32") return child.exitCode === null && child.signalCode === null;
|
|
139
|
+
try {
|
|
140
|
+
process.kill(-child.pid, 0);
|
|
141
|
+
return true;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return (error as NodeJS.ErrnoException).code !== "ESRCH";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function releaseGroupListeners(): void {
|
|
148
|
+
if (groupReaper) clearInterval(groupReaper);
|
|
149
|
+
groupReaper = undefined;
|
|
150
|
+
process.removeListener("exit", terminateRememberedGroups);
|
|
151
|
+
for (const [signal, handler] of groupSignalHandlers) process.removeListener(signal, handler);
|
|
152
|
+
groupSignalHandlers.clear();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Exit handlers cannot await a grace period. Existing explicit stops retain their TERM/KILL escalation below. */
|
|
156
|
+
function terminateRememberedGroups(): void {
|
|
157
|
+
for (const child of rememberedGroups) {
|
|
158
|
+
if (rememberedGroupAlive(child)) killProcessTree(child, "SIGTERM");
|
|
159
|
+
}
|
|
160
|
+
rememberedGroups.clear();
|
|
161
|
+
releaseGroupListeners();
|
|
162
|
+
}
|
|
163
|
+
|
|
98
164
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
165
|
+
* Remember a detached background tree from spawn until its group becomes empty.
|
|
166
|
+
* Register before awaiting its leader, so even immediate process exit covers
|
|
167
|
+
* running work. The unref'd reaper cannot keep a print run alive. No session
|
|
168
|
+
* shutdown handler touches this set; the process listeners keep this module
|
|
169
|
+
* instance alive, and reach its set, even after pi evaluates the module again
|
|
170
|
+
* (a /reload, a /resume into another directory: findings §8).
|
|
171
|
+
*/
|
|
172
|
+
export function rememberProcessGroup(child: ChildProcess): void {
|
|
173
|
+
if (child.pid == null || rememberedGroups.has(child)) return;
|
|
174
|
+
rememberedGroups.add(child);
|
|
175
|
+
if (groupReaper) return;
|
|
176
|
+
process.on("exit", terminateRememberedGroups);
|
|
177
|
+
for (const signal of terminationSignals) {
|
|
178
|
+
const handler = () => {
|
|
179
|
+
terminateRememberedGroups();
|
|
180
|
+
// Do not swallow Node's default signal termination when there is no
|
|
181
|
+
// pi handler. With isolated producers, the last listener re-raises it.
|
|
182
|
+
if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
|
|
183
|
+
};
|
|
184
|
+
groupSignalHandlers.set(signal, handler);
|
|
185
|
+
// Run before signal-exit observers: after we remove ourselves they must
|
|
186
|
+
// still see the original listener count and preserve default termination.
|
|
187
|
+
process.prependOnceListener(signal, handler);
|
|
188
|
+
}
|
|
189
|
+
groupReaper = setInterval(() => {
|
|
190
|
+
for (const child of rememberedGroups) {
|
|
191
|
+
if (!rememberedGroupAlive(child)) rememberedGroups.delete(child);
|
|
192
|
+
}
|
|
193
|
+
if (rememberedGroups.size === 0) releaseGroupListeners();
|
|
194
|
+
}, 1_000);
|
|
195
|
+
groupReaper.unref?.();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Trees sent SIGTERM whose SIGKILL has not run yet. Keyed by the child, not
|
|
200
|
+
* its pid, so a pid the OS hands to a new child during the grace does not
|
|
201
|
+
* make that child look already scheduled. Per module instance on purpose
|
|
202
|
+
* (each extension force-kills its own children); one `exit` listener per
|
|
203
|
+
* instance, installed on first use.
|
|
204
|
+
*/
|
|
205
|
+
const pendingKills = new Set<ChildProcess>();
|
|
206
|
+
let exitHookInstalled = false;
|
|
207
|
+
|
|
208
|
+
/** The process is exiting: the grace timers will never fire, so force-kill every tree still pending. */
|
|
209
|
+
function killPendingAtExit(): void {
|
|
210
|
+
for (const child of pendingKills) killProcessTree(child, "SIGKILL");
|
|
211
|
+
pendingKills.clear();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* SIGTERM the tree, then SIGKILL the whole group after `graceMs`, whatever
|
|
216
|
+
* the leader's state: a group already gone makes that an ESRCH, and a live
|
|
217
|
+
* group keeps its id reserved, so the id cannot name anyone else. The timer
|
|
218
|
+
* is unref'd (it must never be what keeps a one-shot process alive); a
|
|
219
|
+
* process exit before it fires runs the SIGKILL from the `exit` hook.
|
|
101
220
|
*/
|
|
102
221
|
export function stopProcessTree(child: ChildProcess, graceMs: number): void {
|
|
103
222
|
killProcessTree(child, "SIGTERM");
|
|
104
223
|
// taskkill /F is already forceful; there is no gentler first signal to grace.
|
|
105
224
|
if (process.platform === "win32") return;
|
|
106
|
-
if (child.
|
|
225
|
+
if (child.pid == null || pendingKills.has(child)) return;
|
|
226
|
+
pendingKills.add(child);
|
|
227
|
+
if (!exitHookInstalled) {
|
|
228
|
+
exitHookInstalled = true;
|
|
229
|
+
process.on("exit", killPendingAtExit);
|
|
230
|
+
}
|
|
107
231
|
const timer = setTimeout(() => {
|
|
108
|
-
if (
|
|
232
|
+
if (!pendingKills.delete(child)) return;
|
|
233
|
+
killProcessTree(child, "SIGKILL");
|
|
109
234
|
}, graceMs);
|
|
110
235
|
timer.unref?.();
|
|
111
|
-
child.once("exit", () => clearTimeout(timer));
|
|
112
236
|
}
|
|
113
237
|
|
|
114
238
|
export interface ChildExit {
|