one-code-extension 0.4.1 → 0.5.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 +32 -23
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +27 -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 +123 -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 +515 -0
- package/extensions/background/index.ts +514 -236
- package/extensions/background/loop-fire.ts +136 -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 +52 -0
- package/extensions/background/wakeup.ts +159 -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 +88 -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 +229 -71
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +446 -58
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +216 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/index.ts +24 -5
- package/extensions/context-management/index.ts +25 -0
- 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 +72 -19
- 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 +337 -26
- package/extensions/file-tracker/replay.ts +96 -10
- package/extensions/file-tracker/restore.ts +63 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +32 -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 +198 -31
- package/extensions/hooks/matcher.ts +3 -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 +13 -5
- 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/lib/agent-cron.ts +130 -0
- package/extensions/lib/agent-loader.ts +15 -1
- package/extensions/lib/agent-view.ts +85 -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 +42 -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 +78 -0
- package/extensions/lib/context-stack.ts +81 -0
- package/extensions/lib/deferred.ts +219 -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/local-command.ts +47 -13
- 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 +174 -0
- package/extensions/lib/model-catalog/models-dev.json +18297 -0
- package/extensions/lib/model-catalog/openrouter.json +5252 -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 +308 -64
- 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 +21 -2
- package/extensions/lib/plan-mode-channels.ts +20 -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/process-tree.ts +131 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +364 -58
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +439 -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 +13 -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 +113 -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 +149 -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 +143 -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 +70 -4
- 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 +87 -5
- 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 +209 -71
- 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 +151 -67
- 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/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 +1300 -381
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +6 -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 +156 -69
- package/extensions/plan-mode/reminder.ts +89 -21
- 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 +61 -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 +12 -7
- package/extensions/recap/index.ts +65 -60
- 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/code-review.ts +97 -0
- package/extensions/skill/index.ts +421 -101
- package/extensions/skill/invoke.ts +128 -18
- 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 +116 -23
- package/extensions/subagents/announce.ts +69 -0
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +76 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1493 -824
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +2 -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/runner.ts +108 -80
- package/extensions/subagents/runs.ts +39 -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 +131 -47
- package/extensions/system-prompt/template.ts +24 -50
- 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 +175 -13
- 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/announce.ts +16 -0
- package/extensions/tool-search/index.ts +189 -41
- 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 +105 -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 +145 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +29 -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 +121 -39
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +18 -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
|
@@ -7,20 +7,42 @@
|
|
|
7
7
|
* cannot overwrite someone else's change.
|
|
8
8
|
* - Files that change out of band are reported in a `<system-reminder>` with the
|
|
9
9
|
* new content around the change, line-numbered.
|
|
10
|
+
* - pi's read, write and edit results read as Claude Code's (`results.ts`).
|
|
11
|
+
* - After a compaction, the files read or written most recently come back as
|
|
12
|
+
* Claude Code brings them back: the contents of a small one, a note to read
|
|
13
|
+
* a large one again (`restore.ts`), and the read state is cleared.
|
|
10
14
|
*
|
|
11
15
|
* Tracking is by content, not by tool call, which is what makes it catch writes
|
|
12
16
|
* that went through bash and never touched an intercepted tool.
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
19
|
+
import { CHILD_WROTE_CHANNEL, type ChildWrote } from "../lib/child-writes.ts";
|
|
20
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { createReadToolDefinition, type ExtensionAPI, type ExtensionContext, type SessionCompactEvent } from "@earendil-works/pi-coding-agent";
|
|
18
24
|
import { pathArgument } from "../auto-mode/paths.ts";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
25
|
+
import { bashParserReady } from "../lib/bash-parser.ts";
|
|
26
|
+
import { discoverContextFiles, instructionRule } from "../lib/claude-context.ts";
|
|
27
|
+
import { readExternalIncludesApproval } from "../lib/claude-external-includes.ts";
|
|
28
|
+
import { inContextEntries, latestCompaction } from "../lib/compaction-boundary.ts";
|
|
29
|
+
import { autoMemoryEnabled } from "../lib/auto-memory.ts";
|
|
30
|
+
import { projectMemoryDir } from "../lib/memory.ts";
|
|
31
|
+
import { claudeConfigDir, comparablePath, isPathAtOrUnder, tryRealpath } from "../lib/paths.ts";
|
|
32
|
+
import { estimateTextTokens } from "../lib/pi-ai-estimate.ts";
|
|
33
|
+
import { planFileOnBranch } from "../lib/plan-mode-channels.ts";
|
|
34
|
+
import { REMINDER_CHANNEL, type ReminderPayload } from "../lib/reminders.ts";
|
|
35
|
+
import { resolveToolPath } from "../lib/tool-path.ts";
|
|
36
|
+
import { parseRules, ruleMatches } from "../permissions/matcher.ts";
|
|
37
|
+
import { loadPermissionSettings } from "../permissions/settings.ts";
|
|
38
|
+
import { keptReadPaths, keptShellReadPaths, lastTouchesOnBranch, pathsReadOnBranch, shellCallsOnBranch } from "./replay.ts";
|
|
39
|
+
import { type RestoredRead, restoreBlocks, restoreCandidates } from "./restore.ts";
|
|
40
|
+
import { fileToolResultContent } from "./results.ts";
|
|
41
|
+
import { shellReadPaths, shownInFull } from "./shell-reads.ts";
|
|
42
|
+
import { sessionWorkCwd, WORKTREE_CHANNEL, type WorktreeLocation } from "../lib/worktree-channel.ts";
|
|
21
43
|
import {
|
|
22
44
|
describeChanges,
|
|
23
|
-
EXTERNAL_CHANGE_REMINDER,
|
|
45
|
+
CHILD_CHANGE_REMINDER, EXTERNAL_CHANGE_REMINDER,
|
|
24
46
|
type FileStamp,
|
|
25
47
|
FileTracker,
|
|
26
48
|
STALE_REASON,
|
|
@@ -46,6 +68,15 @@ function readIfPresent(path: string): string | undefined {
|
|
|
46
68
|
}
|
|
47
69
|
}
|
|
48
70
|
|
|
71
|
+
/** Whether `path` is a regular file (symlinks followed): never a device, FIFO or directory. */
|
|
72
|
+
function isRegularFile(path: string): boolean {
|
|
73
|
+
try {
|
|
74
|
+
return statSync(path).isFile();
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
49
80
|
/** The file's disk stamp, or undefined when it is gone (or not a plain file we could stat). */
|
|
50
81
|
function statIfPresent(path: string): FileStamp | undefined {
|
|
51
82
|
try {
|
|
@@ -56,12 +87,84 @@ function statIfPresent(path: string): FileStamp | undefined {
|
|
|
56
87
|
}
|
|
57
88
|
}
|
|
58
89
|
|
|
59
|
-
|
|
60
|
-
|
|
90
|
+
function commandOf(input: unknown): string {
|
|
91
|
+
const command = (input as { command?: unknown } | undefined)?.command;
|
|
92
|
+
return typeof command === "string" ? command : "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function textOf(content: unknown): string {
|
|
96
|
+
if (!Array.isArray(content)) return "";
|
|
97
|
+
return content.map((block) => (block as { type?: string; text?: string })?.type === "text" ? ((block as { text?: string }).text ?? "") : "").join("\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A file's identity and version, from metadata only: `ctimeMs` changes on any
|
|
102
|
+
* write, rename or metadata change, and cannot be set back by the writer.
|
|
103
|
+
*/
|
|
104
|
+
function versionOf(path: string): string | undefined {
|
|
105
|
+
try {
|
|
106
|
+
const stat = statSync(path);
|
|
107
|
+
return stat.isFile() ? `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}` : undefined;
|
|
108
|
+
} catch {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The files a shell command may print, with each one's version as the command
|
|
115
|
+
* starts (`shell-reads.ts` paths, resolved as the shell resolves them, from
|
|
116
|
+
* the directory a literal `cd` earlier in the command moved to). Taken before
|
|
117
|
+
* the command runs, so a file the command itself rewrites (`cat x; cp y x`)
|
|
118
|
+
* is not certified with its new content (found by a GPT-6 Sol review,
|
|
119
|
+
* 2026-10-04). Metadata only: this runs before the permission gate decides
|
|
120
|
+
* the call, so a file the user then denies is never opened.
|
|
121
|
+
*/
|
|
122
|
+
function shellReadSnapshot(command: string, cwd: string): Map<string, string> {
|
|
123
|
+
const snapshot = new Map<string, string>();
|
|
124
|
+
if (!command) return snapshot;
|
|
125
|
+
for (const path of shellReadPaths(command, cwd, homedir(), (dir) => readdirSync(dir))) {
|
|
126
|
+
// Regular files only: a device or FIFO (`head -c 1 /dev/zero`) reports size 0
|
|
127
|
+
// and a read of it never ends (found by a GPT-6 Astra review).
|
|
128
|
+
if (!isRegularFile(path)) continue;
|
|
129
|
+
const stamp = statIfPresent(path);
|
|
130
|
+
if (!stamp || stamp.size > MAX_SHELL_READ_BYTES) continue;
|
|
131
|
+
const version = versionOf(path);
|
|
132
|
+
if (version !== undefined) snapshot.set(path, version);
|
|
133
|
+
}
|
|
134
|
+
return snapshot;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Count as read each file the command printed in full and did not change:
|
|
139
|
+
* its version before and after the command is the same, and the output holds
|
|
140
|
+
* all of it. Observed, not touched: the post-compaction restore follows the
|
|
141
|
+
* file tools, as Claude Code's does. Returns the files it observed.
|
|
142
|
+
*/
|
|
143
|
+
function observeShellReads(tracker: FileTracker, before: Map<string, string>, output: string): string[] {
|
|
144
|
+
const observed: string[] = [];
|
|
145
|
+
if (!output) return observed;
|
|
146
|
+
for (const [path, version] of before) {
|
|
147
|
+
const stamp = statIfPresent(path);
|
|
148
|
+
if (!stamp || !isRegularFile(path) || versionOf(path) !== version) continue;
|
|
149
|
+
const content = readIfPresent(path);
|
|
150
|
+
if (content === undefined || versionOf(path) !== version) continue;
|
|
151
|
+
if (!shownInFull(output, content)) continue;
|
|
152
|
+
tracker.observe(path, content, Date.now(), stamp);
|
|
153
|
+
observed.push(path);
|
|
154
|
+
}
|
|
155
|
+
return observed;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Above this a shell read's output is persisted, not shown, so the file cannot have been seen whole. */
|
|
159
|
+
const MAX_SHELL_READ_BYTES = 256 * 1024;
|
|
160
|
+
|
|
161
|
+
/** Observe a file's current content together with the stamp it was read at, and return that stamp. */
|
|
162
|
+
function observeFromDisk(tracker: FileTracker, path: string): FileStamp | undefined {
|
|
61
163
|
const stamp = statIfPresent(path);
|
|
62
164
|
const current = readIfPresent(path);
|
|
63
165
|
if (current === undefined) tracker.forget(path);
|
|
64
166
|
else tracker.observe(path, current, Date.now(), stamp);
|
|
167
|
+
return stamp;
|
|
65
168
|
}
|
|
66
169
|
|
|
67
170
|
function pathOf(input: unknown, cwd: string): string | undefined {
|
|
@@ -71,40 +174,165 @@ function pathOf(input: unknown, cwd: string): string | undefined {
|
|
|
71
174
|
// read-before-write guard (code-review).
|
|
72
175
|
const raw = pathArgument(input as Record<string, unknown> | undefined);
|
|
73
176
|
if (typeof raw !== "string" || !raw.trim()) return undefined;
|
|
74
|
-
|
|
177
|
+
// pi's own resolution (`~`, `@`, `file://`, …): the guard must check the
|
|
178
|
+
// file the tool will actually touch (lib/tool-path.ts).
|
|
179
|
+
return resolveToolPath(raw, cwd);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The files the context still shows after a compaction, as comparable paths:
|
|
184
|
+
* the context stack (the CLAUDE.md family as claude-context discovers it, what
|
|
185
|
+
* they `@`-import, and the auto-memory index), the plan file, the files a
|
|
186
|
+
* `read` in the kept turns returned (`kept`, as written) and the files a kept
|
|
187
|
+
* bash call showed in full (`keptShell`, absolute). Claude Code's restore
|
|
188
|
+
* skips them; they stay read.
|
|
189
|
+
*/
|
|
190
|
+
function inContextFiles(ctx: ExtensionContext, branch: readonly { type: string; customType?: string; data?: unknown }[], kept: string[], keptShell: string[]): Set<string> {
|
|
191
|
+
const home = homedir();
|
|
192
|
+
// Use the context loader's import confinement and approval: a literal
|
|
193
|
+
// unapproved @import is not evidence that its file is still in context.
|
|
194
|
+
const stack = discoverContextFiles({ cwd: ctx.cwd, home, homeClaudeDir: claudeConfigDir(), rule: instructionRule(home),
|
|
195
|
+
includeExternal: readExternalIncludesApproval(ctx.cwd, home).approved,
|
|
196
|
+
}).flatMap((file) => [file.path, ...(file.imported ?? [])]);
|
|
197
|
+
return new Set(
|
|
198
|
+
[...stack, autoMemoryEnabled(ctx.cwd) ? join(projectMemoryDir(ctx.cwd, home), "MEMORY.md") : undefined, planFileOnBranch(branch), ...kept.map((path) => resolveToolPath(path, ctx.cwd)), ...keptShell]
|
|
199
|
+
.filter((path): path is string => typeof path === "string")
|
|
200
|
+
.map(comparablePath),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Whether a file's contents may come back without the permission gate that a
|
|
206
|
+
* `read` call passes: only a file that resolves (symlinks followed) inside the
|
|
207
|
+
* working directory and that no read deny or ask rule covers now. Any other
|
|
208
|
+
* file, one read outside the project after a question, say, is not restored;
|
|
209
|
+
* the model reads it again through the gate if it needs it. Nothing passes
|
|
210
|
+
* when the settings cannot be read (the gate reports them).
|
|
211
|
+
*/
|
|
212
|
+
function restoreAllowed(ctx: ExtensionContext): (path: string) => boolean {
|
|
213
|
+
try {
|
|
214
|
+
const settings = loadPermissionSettings(ctx.cwd, homedir());
|
|
215
|
+
const rules = parseRules([...settings.deny, ...settings.ask]);
|
|
216
|
+
const root = tryRealpath(ctx.cwd) ?? ctx.cwd;
|
|
217
|
+
return (path) => {
|
|
218
|
+
const real = tryRealpath(path);
|
|
219
|
+
if (!real || !isPathAtOrUnder(real, root)) return false;
|
|
220
|
+
return !rules.some((rule) => ruleMatches(rule, "read", path, ctx.cwd) || ruleMatches(rule, "read", real, ctx.cwd));
|
|
221
|
+
};
|
|
222
|
+
} catch {
|
|
223
|
+
return () => false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** A file read again through pi's own read tool, as the model would see it; undefined when gone, unreadable or an image. */
|
|
228
|
+
async function readAgain(read: ReturnType<typeof createReadToolDefinition>, path: string, ctx: ExtensionContext): Promise<string | undefined> {
|
|
229
|
+
try {
|
|
230
|
+
const result = await read.execute("restore", { path }, undefined, undefined, ctx as never);
|
|
231
|
+
if (result.content.some((block) => block.type !== "text")) return undefined;
|
|
232
|
+
return result.content.map((block) => (block as { text: string }).text).join("\n");
|
|
233
|
+
} catch {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
75
236
|
}
|
|
76
237
|
|
|
77
238
|
export default function fileTrackerExtension(pi: ExtensionAPI) {
|
|
78
239
|
let tracker = new FileTracker();
|
|
240
|
+
/**
|
|
241
|
+
* The files each bash call showed in full, by call id, decided where the
|
|
242
|
+
* call ran: a compaction keeps the reads of the calls it keeps from this,
|
|
243
|
+
* since re-parsing a command later resolves it from today's directory.
|
|
244
|
+
*/
|
|
245
|
+
const shellReadsByCall = new Map<string, string[]>();
|
|
246
|
+
const recordShellReads = (callId: string, paths: string[]) => {
|
|
247
|
+
if (paths.length > 0) shellReadsByCall.set(callId, paths);
|
|
248
|
+
};
|
|
249
|
+
/** The entered worktree, where the shell runs while one is active (lib/worktree-channel.ts). */
|
|
250
|
+
let entered: WorktreeLocation | undefined;
|
|
251
|
+
pi.events.on(WORKTREE_CHANNEL, (data) => {
|
|
252
|
+
const location = data as WorktreeLocation | null | undefined;
|
|
253
|
+
entered = location?.path ? location : undefined;
|
|
254
|
+
});
|
|
79
255
|
let sessionId: string | undefined;
|
|
256
|
+
/**
|
|
257
|
+
* Every file a read or write touched on this branch, with its modification
|
|
258
|
+
* time as stat'ed then (the transcript time for one replayed after a resume):
|
|
259
|
+
* the post-compaction restore's order.
|
|
260
|
+
*/
|
|
261
|
+
let touched = new Map<string, number>();
|
|
262
|
+
/**
|
|
263
|
+
* Files this session's own child agents changed since the parent last saw
|
|
264
|
+
* them (lib/child-writes.ts). Every way the parent sees a file clears its
|
|
265
|
+
* entry: a file tool, a shell read, a rebuild from disk, the change scan.
|
|
266
|
+
*/
|
|
267
|
+
const childWrites = new Map<string, string | undefined>();
|
|
268
|
+
pi.events.on(CHILD_WROTE_CHANNEL, (data) => {
|
|
269
|
+
const wrote = data as ChildWrote | undefined;
|
|
270
|
+
if (wrote?.path) childWrites.set(wrote.path, wrote.agent);
|
|
271
|
+
});
|
|
272
|
+
/** Observe a file the model read or wrote, and record it as touched at its current modification time. */
|
|
273
|
+
const touch = (path: string) => {
|
|
274
|
+
childWrites.delete(path);
|
|
275
|
+
const stamp = observeFromDisk(tracker, path);
|
|
276
|
+
if (stamp) touched.set(path, stamp.mtimeMs);
|
|
277
|
+
};
|
|
80
278
|
|
|
81
279
|
// A resumed session (or a session_tree switch) carries reads the model did
|
|
82
|
-
// earlier in the transcript; rebuild the tracker from the
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
280
|
+
// earlier in the transcript; rebuild the tracker from the part of the branch
|
|
281
|
+
// still in context (since the latest compaction's kept tail) so post-resume
|
|
282
|
+
// edits are not refused as "never read" (replay.ts). A new session id starts
|
|
283
|
+
// from an empty tracker.
|
|
284
|
+
const reconstruct = (ctx: ExtensionContext, fresh = false) => {
|
|
86
285
|
const id = ctx.sessionManager.getSessionId?.() ?? undefined;
|
|
87
|
-
if (id !== sessionId) {
|
|
286
|
+
if (fresh || id !== sessionId) {
|
|
88
287
|
sessionId = id;
|
|
89
288
|
tracker = new FileTracker();
|
|
90
289
|
}
|
|
290
|
+
// Tracked files are re-read from disk below: what a child wrote is now the baseline.
|
|
291
|
+
childWrites.clear();
|
|
91
292
|
let entries: unknown[] = [];
|
|
92
293
|
try {
|
|
93
|
-
entries = ctx.sessionManager.getBranch() as unknown[];
|
|
294
|
+
entries = inContextEntries(ctx.sessionManager.getBranch() as unknown[]);
|
|
94
295
|
} catch {
|
|
95
296
|
return;
|
|
96
297
|
}
|
|
97
298
|
for (const raw of pathsReadOnBranch(entries)) {
|
|
98
|
-
observeFromDisk(tracker,
|
|
299
|
+
observeFromDisk(tracker, resolveToolPath(raw, ctx.cwd));
|
|
99
300
|
}
|
|
301
|
+
// Shell reads need the bash grammar, which may still be loading this early.
|
|
302
|
+
const target = tracker;
|
|
303
|
+
shellReadsByCall.clear();
|
|
304
|
+
const shellCalls = [...shellCallsOnBranch(entries)];
|
|
305
|
+
if (shellCalls.length > 0) {
|
|
306
|
+
void bashParserReady().then(() => {
|
|
307
|
+
if (target !== tracker) return;
|
|
308
|
+
// A replayed result has no before-state: the files as they are now stand in for it.
|
|
309
|
+
for (const call of shellCalls) recordShellReads(call.id, observeShellReads(target, shellReadSnapshot(call.command, sessionWorkCwd(entered, ctx.cwd)), call.output));
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
touched = new Map([...lastTouchesOnBranch(entries)].map(([raw, at]) => [resolveToolPath(raw, ctx.cwd), at]));
|
|
100
313
|
};
|
|
101
314
|
pi.on("session_start", (_event, ctx) => reconstruct(ctx));
|
|
102
|
-
|
|
315
|
+
// A branch switch keeps the session id, but the reads of the branch left
|
|
316
|
+
// behind are not in context any more: start from an empty tracker, as for a
|
|
317
|
+
// new session (found by an adversarial GPT-6 Astra run, 2026-10-04).
|
|
318
|
+
pi.on("session_tree", (_event, ctx) => reconstruct(ctx, true));
|
|
319
|
+
|
|
320
|
+
/** Whether a write's file existed when the call was made, by call id: its result says "created" or "updated". */
|
|
321
|
+
const writeTargetExisted = new Map<string, boolean>();
|
|
103
322
|
|
|
104
|
-
|
|
323
|
+
/** Each running bash call's candidate files as they were when it started (`shellReadSnapshot`). */
|
|
324
|
+
const shellSnapshots = new Map<string, Map<string, string>>();
|
|
325
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
326
|
+
if (event.toolName === "bash") {
|
|
327
|
+
await bashParserReady();
|
|
328
|
+
// Relative paths in the command resolve where the shell runs: the entered worktree, if any.
|
|
329
|
+
shellSnapshots.set(event.toolCallId, shellReadSnapshot(commandOf(event.input), sessionWorkCwd(entered, ctx.cwd)));
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
105
332
|
if (!GUARDED_TOOLS.has(event.toolName)) return undefined;
|
|
106
333
|
const path = pathOf(event.input, ctx.cwd);
|
|
107
334
|
if (!path) return undefined;
|
|
335
|
+
if (event.toolName === "write") writeTargetExisted.set(event.toolCallId, existsSync(path));
|
|
108
336
|
|
|
109
337
|
const current = readIfPresent(path);
|
|
110
338
|
const status = tracker.status(path, current);
|
|
@@ -115,8 +343,19 @@ export default function fileTrackerExtension(pi: ExtensionAPI) {
|
|
|
115
343
|
return { block: true, reason: STALE_REASON(path) };
|
|
116
344
|
});
|
|
117
345
|
|
|
118
|
-
pi.on("tool_result", (event, ctx) => {
|
|
119
|
-
|
|
346
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
347
|
+
const existedBefore = writeTargetExisted.get(event.toolCallId);
|
|
348
|
+
writeTargetExisted.delete(event.toolCallId);
|
|
349
|
+
if (event.toolName === "bash") {
|
|
350
|
+
const before = shellSnapshots.get(event.toolCallId);
|
|
351
|
+
shellSnapshots.delete(event.toolCallId);
|
|
352
|
+
if (before) {
|
|
353
|
+
const observed = observeShellReads(tracker, before, textOf(event.content));
|
|
354
|
+
recordShellReads(event.toolCallId, observed);
|
|
355
|
+
for (const path of observed) childWrites.delete(path);
|
|
356
|
+
}
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
120
359
|
if (!READ_TOOLS.has(event.toolName) && !GUARDED_TOOLS.has(event.toolName)) return undefined;
|
|
121
360
|
|
|
122
361
|
const path = pathOf(event.input, ctx.cwd);
|
|
@@ -124,8 +363,69 @@ export default function fileTrackerExtension(pi: ExtensionAPI) {
|
|
|
124
363
|
|
|
125
364
|
// After a read we know the file; after our own write we know it again, so a
|
|
126
365
|
// successful edit does not make the file look stale to the next edit.
|
|
127
|
-
|
|
128
|
-
|
|
366
|
+
if (!event.isError) touch(path);
|
|
367
|
+
const content = fileToolResultContent({
|
|
368
|
+
toolName: event.toolName,
|
|
369
|
+
isError: event.isError,
|
|
370
|
+
content: event.content,
|
|
371
|
+
path,
|
|
372
|
+
cwd: ctx.cwd,
|
|
373
|
+
existedBefore,
|
|
374
|
+
isEmptyFile: () => statIfPresent(path)?.size === 0,
|
|
375
|
+
});
|
|
376
|
+
return content ? { content } : undefined;
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Claude Code's post-compaction file restore (`restore.ts`). The
|
|
381
|
+
* blocks open the next user message (`user-prepend`); an overflow compaction
|
|
382
|
+
* that retries the turn has no such message, so they ride the request's tail
|
|
383
|
+
* instead.
|
|
384
|
+
*
|
|
385
|
+
* Like Claude Code, the compaction also clears what the session has read:
|
|
386
|
+
* the summary no longer holds those files, so an edit needs a fresh read.
|
|
387
|
+
* A file the context still shows stays read (a kept turn's read or whole-file
|
|
388
|
+
* shell read, the context stack, the plan file), and a restored file counts
|
|
389
|
+
* as read and as touched for the next compaction's restore; a note never
|
|
390
|
+
* marks its file read. Contents come back only for a file the gate would
|
|
391
|
+
* let through without a question (`restoreAllowed`).
|
|
392
|
+
*/
|
|
393
|
+
pi.on("session_compact", async (event: SessionCompactEvent, ctx) => {
|
|
394
|
+
const before = touched;
|
|
395
|
+
try {
|
|
396
|
+
const branch = ctx.sessionManager.getBranch();
|
|
397
|
+
const compaction = latestCompaction(branch);
|
|
398
|
+
const keptTurns = compaction ? branch.slice(compaction.keptStart, compaction.index) : [];
|
|
399
|
+
// The kept shell reads: what each kept bash call certified as it ran.
|
|
400
|
+
const keptShell = keptShellReadPaths(keptTurns, (id) => shellReadsByCall.get(id), (path) => tracker.lastSeen(path));
|
|
401
|
+
// A call the summary took never comes back: only the kept ones need their record.
|
|
402
|
+
const keptCalls = new Set([...shellCallsOnBranch(keptTurns)].map((call) => call.id));
|
|
403
|
+
for (const id of shellReadsByCall.keys()) if (!keptCalls.has(id)) shellReadsByCall.delete(id);
|
|
404
|
+
const inContext = inContextFiles(ctx, branch, keptReadPaths(keptTurns), keptShell);
|
|
405
|
+
// The loader lists imports by real path; a tracked path may be spelled through a symlink.
|
|
406
|
+
const shown = (path: string) => inContext.has(comparablePath(path)) || inContext.has(comparablePath(tryRealpath(path) ?? path));
|
|
407
|
+
// A file the context still shows keeps its read state (what the model
|
|
408
|
+
// saw, not what is on disk now, so the stale-edit guard still holds)
|
|
409
|
+
// and its touch time, for the next compaction's restore.
|
|
410
|
+
for (const path of tracker.tracked) if (!shown(path)) tracker.forget(path);
|
|
411
|
+
touched = new Map([...before].filter(([path]) => shown(path)));
|
|
412
|
+
// Opting out of One Code's restore does not disable pi's compaction:
|
|
413
|
+
// reads discarded by either summarizer still need a fresh read.
|
|
414
|
+
if (process.env.CC_COMPACTION === "0" || before.size === 0) return;
|
|
415
|
+
const allowed = restoreAllowed(ctx);
|
|
416
|
+
const candidates = restoreCandidates(before, (path) => shown(path) || !allowed(path));
|
|
417
|
+
const read = createReadToolDefinition(ctx.cwd);
|
|
418
|
+
const reads: RestoredRead[] = await Promise.all(candidates.map(async (path) => ({ path, text: await readAgain(read, path, ctx) })));
|
|
419
|
+
const { blocks, restored } = restoreBlocks(reads, estimateTextTokens);
|
|
420
|
+
const placement = event.willRetry ? "last-append" : "user-prepend";
|
|
421
|
+
for (const text of blocks) pi.events.emit(REMINDER_CHANNEL, { text, placement } satisfies ReminderPayload);
|
|
422
|
+
for (const path of restored) touch(path);
|
|
423
|
+
} catch {
|
|
424
|
+
// The restore is best effort: nothing comes back, and an edit needs a fresh read.
|
|
425
|
+
tracker = new FileTracker();
|
|
426
|
+
touched = new Map();
|
|
427
|
+
shellReadsByCall.clear();
|
|
428
|
+
}
|
|
129
429
|
});
|
|
130
430
|
|
|
131
431
|
/**
|
|
@@ -166,14 +466,22 @@ export default function fileTrackerExtension(pi: ExtensionAPI) {
|
|
|
166
466
|
const detailed: string[] = [];
|
|
167
467
|
const overflow: string[] = [];
|
|
168
468
|
for (const path of tracker.tracked) {
|
|
469
|
+
// Undefined for a file tracked without content (oversized); an empty
|
|
470
|
+
// file is compared like any other.
|
|
169
471
|
const previous = tracker.lastSeen(path);
|
|
170
|
-
if (previous === undefined
|
|
472
|
+
if (previous === undefined) continue;
|
|
171
473
|
const stamp = statIfPresent(path);
|
|
172
474
|
if (stamp && tracker.unchangedOnDisk(path, stamp)) continue;
|
|
475
|
+
// This scan accounts for the child's write whatever it finds (same
|
|
476
|
+
// content, already notified, overflow), so a later change by someone
|
|
477
|
+
// else is never credited to that agent.
|
|
478
|
+
const byChild = childWrites.has(path);
|
|
479
|
+
const childAgent = childWrites.get(path);
|
|
480
|
+
childWrites.delete(path);
|
|
173
481
|
const current = stamp ? readIfPresent(path) : undefined;
|
|
174
482
|
if (stamp === undefined || current === undefined) {
|
|
175
483
|
tracker.forget(path);
|
|
176
|
-
detailed.push(`${path} no longer exists; it was deleted or moved after you read it.`);
|
|
484
|
+
detailed.push(`${path} no longer exists; it was deleted or moved after you last read or wrote it.`);
|
|
177
485
|
continue;
|
|
178
486
|
}
|
|
179
487
|
tracker.recordStamp(path, stamp);
|
|
@@ -183,7 +491,7 @@ export default function fileTrackerExtension(pi: ExtensionAPI) {
|
|
|
183
491
|
|
|
184
492
|
if (detailed.length < DETAILED_CHANGE_REMINDERS_PER_TURN) {
|
|
185
493
|
const excerpt = describeChanges(previous, current);
|
|
186
|
-
if (excerpt) detailed.push(EXTERNAL_CHANGE_REMINDER(path, excerpt));
|
|
494
|
+
if (excerpt) detailed.push(byChild ? CHILD_CHANGE_REMINDER(path, childAgent, excerpt) : EXTERNAL_CHANGE_REMINDER(path, excerpt));
|
|
187
495
|
} else {
|
|
188
496
|
overflow.push(path);
|
|
189
497
|
}
|
|
@@ -212,8 +520,11 @@ export default function fileTrackerExtension(pi: ExtensionAPI) {
|
|
|
212
520
|
pi.on("agent_start", () => {
|
|
213
521
|
// A turn aborted mid-batch can leave ids behind; each turn starts from a
|
|
214
522
|
// clean set so the mid-turn scan cannot be wedged off for the rest of the
|
|
215
|
-
// session.
|
|
523
|
+
// session. A write blocked before it ran left its existence entry behind,
|
|
524
|
+
// and a blocked or aborted bash call its snapshot of file versions.
|
|
216
525
|
executing.clear();
|
|
526
|
+
writeTargetExisted.clear();
|
|
527
|
+
shellSnapshots.clear();
|
|
217
528
|
reportExternalChanges();
|
|
218
529
|
return undefined;
|
|
219
530
|
});
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
* guard still catches anything that changes from this point on.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { pathArgument } from "../auto-mode/paths.ts";
|
|
15
|
+
import { shownInFull } from "./shell-reads.ts";
|
|
16
|
+
|
|
14
17
|
const READ_TOOLS = new Set(["read", "notebook_edit"]);
|
|
15
18
|
const WRITE_TOOLS = new Set(["edit", "write", "notebook_edit"]);
|
|
16
19
|
|
|
@@ -21,26 +24,109 @@ interface ToolCallBlock {
|
|
|
21
24
|
arguments?: Record<string, unknown>;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
type BranchMessage = { role?: string; content?: unknown; toolCallId?: string; isError?: boolean; timestamp?: number };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Every tool call on the branch that names a path (`path`, `file_path` or
|
|
31
|
+
* `notebook_path`, as the gates read it) and got a successful result, in
|
|
32
|
+
* result order, with the result message's time.
|
|
33
|
+
*/
|
|
34
|
+
export function* successfulPathCalls(entries: readonly unknown[]): Generator<{ name: string; path: string; timestamp: number | undefined }> {
|
|
26
35
|
const calls = new Map<string, { name: string; path: string }>();
|
|
27
|
-
const seen: string[] = [];
|
|
28
36
|
for (const entry of entries) {
|
|
29
|
-
const e = entry as { type?: string; message?:
|
|
37
|
+
const e = entry as { type?: string; message?: BranchMessage };
|
|
30
38
|
if (e?.type !== "message" || !e.message) continue;
|
|
31
39
|
const m = e.message;
|
|
32
40
|
if (m.role === "assistant" && Array.isArray(m.content)) {
|
|
33
41
|
for (const block of m.content as ToolCallBlock[]) {
|
|
34
42
|
if (block?.type !== "toolCall" || !block.id) continue;
|
|
35
|
-
const path = block.arguments
|
|
36
|
-
if (
|
|
43
|
+
const path = pathArgument(block.arguments);
|
|
44
|
+
if (path?.trim()) calls.set(block.id, { name: block.name, path });
|
|
37
45
|
}
|
|
38
46
|
} else if (m.role === "toolResult" && m.toolCallId && !m.isError) {
|
|
39
47
|
const call = calls.get(m.toolCallId);
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
if (call) yield { ...call, timestamp: m.timestamp };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const touchesFile = (name: string) => READ_TOOLS.has(name) || WRITE_TOOLS.has(name);
|
|
54
|
+
|
|
55
|
+
/** Paths (as written in the tool input) of every successful read/edit/write on the branch, in order, deduplicated. */
|
|
56
|
+
export function pathsReadOnBranch(entries: readonly unknown[]): string[] {
|
|
57
|
+
const seen = new Set<string>();
|
|
58
|
+
for (const call of successfulPathCalls(entries)) if (touchesFile(call.name)) seen.add(call.path);
|
|
59
|
+
return [...seen];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Each path (as written in the tool input) a successful read/edit/write on the
|
|
64
|
+
* branch touched, with the time of its last such result message: the order
|
|
65
|
+
* the post-compaction file restore uses after a resume, when the modification
|
|
66
|
+
* times stat'ed at those reads are gone (Claude Code uses the transcript time
|
|
67
|
+
* the same way).
|
|
68
|
+
*/
|
|
69
|
+
export function lastTouchesOnBranch(entries: readonly unknown[]): Map<string, number> {
|
|
70
|
+
const touches = new Map<string, number>();
|
|
71
|
+
for (const call of successfulPathCalls(entries)) {
|
|
72
|
+
if (touchesFile(call.name) && call.timestamp !== undefined) touches.set(call.path, call.timestamp);
|
|
73
|
+
}
|
|
74
|
+
return touches;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The paths (as written in the tool input) that a successful `read` in the
|
|
79
|
+
* kept turns returned: those files are in context already, so the
|
|
80
|
+
* post-compaction restore skips them.
|
|
81
|
+
*/
|
|
82
|
+
export function keptReadPaths(kept: readonly unknown[]): string[] {
|
|
83
|
+
return [...successfulPathCalls(kept)].filter((call) => call.name === "read").map((call) => call.path);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The tracked files that a bash call in the kept turns showed in full: those
|
|
88
|
+
* the call certified when it ran (`readsOf`, by call id: the directory it ran
|
|
89
|
+
* in decided, never today's) whose content as the tracker last saw it (`seen`)
|
|
90
|
+
* is still all in its kept output. Like a kept `read`, they are in context
|
|
91
|
+
* still, so they stay read; a file changed since is then stale, not unread. A
|
|
92
|
+
* call needs its result in the kept turns too, and one with no record
|
|
93
|
+
* certifies nothing (a miss costs one re-read).
|
|
94
|
+
*/
|
|
95
|
+
export function keptShellReadPaths(kept: readonly unknown[], readsOf: (callId: string) => readonly string[] | undefined, seen: (path: string) => string | undefined): string[] {
|
|
96
|
+
const shown = new Set<string>();
|
|
97
|
+
for (const { id, output } of shellCallsOnBranch(kept)) {
|
|
98
|
+
for (const path of readsOf(id) ?? []) {
|
|
99
|
+
if (shown.has(path)) continue;
|
|
100
|
+
const content = seen(path);
|
|
101
|
+
if (content !== undefined && shownInFull(output, content)) shown.add(path);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return [...shown];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Every bash call on the branch with its command and the text of its result,
|
|
109
|
+
* in result order: a resumed session re-checks which files those outputs
|
|
110
|
+
* showed in full (`shell-reads.ts`), against the files' current content.
|
|
111
|
+
*/
|
|
112
|
+
export function* shellCallsOnBranch(entries: readonly unknown[]): Generator<{ id: string; command: string; output: string }> {
|
|
113
|
+
const commands = new Map<string, string>();
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const e = entry as { type?: string; message?: BranchMessage };
|
|
116
|
+
if (e?.type !== "message" || !e.message) continue;
|
|
117
|
+
const m = e.message;
|
|
118
|
+
if (m.role === "assistant" && Array.isArray(m.content)) {
|
|
119
|
+
for (const block of m.content as ToolCallBlock[]) {
|
|
120
|
+
const command = block?.type === "toolCall" && block.name === "bash" ? block.arguments?.command : undefined;
|
|
121
|
+
if (typeof command === "string" && block.id) commands.set(block.id, command);
|
|
122
|
+
}
|
|
123
|
+
} else if (m.role === "toolResult" && m.toolCallId) {
|
|
124
|
+
const command = commands.get(m.toolCallId);
|
|
125
|
+
if (command === undefined || !Array.isArray(m.content)) continue;
|
|
126
|
+
const output = (m.content as { type?: string; text?: string }[])
|
|
127
|
+
.map((block) => (block?.type === "text" ? (block.text ?? "") : ""))
|
|
128
|
+
.join("\n");
|
|
129
|
+
yield { id: m.toolCallId, command, output };
|
|
43
130
|
}
|
|
44
131
|
}
|
|
45
|
-
return seen;
|
|
46
132
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files that come back after a compaction, and how. Pure: no pi imports,
|
|
3
|
+
* no I/O.
|
|
4
|
+
*
|
|
5
|
+
* As in Claude Code: the five files the session read, edited or wrote with
|
|
6
|
+
* the newest modification time (as stat'ed at that read or write), minus the
|
|
7
|
+
* files the context still shows, are read again. A file within 5,000 tokens
|
|
8
|
+
* comes back as a read call and its result, a larger one as a note to read it
|
|
9
|
+
* again, while the total stays within 50,000 tokens (findings §47). Each is
|
|
10
|
+
* its own `<system-reminder>` block at the start of the next user message,
|
|
11
|
+
* worded as Claude Code words them with One Code's tool name and argument
|
|
12
|
+
* (`read`, `path`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const RESTORE_MAX_FILES = 5;
|
|
16
|
+
export const RESTORE_FILE_MAX_TOKENS = 5_000;
|
|
17
|
+
export const RESTORE_TOTAL_MAX_TOKENS = 50_000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The files to restore, newest first: the touched files (path to modification
|
|
21
|
+
* time) minus `skip`, at most five. A file that cannot be read again is
|
|
22
|
+
* dropped afterwards, not replaced, as in Claude Code.
|
|
23
|
+
*/
|
|
24
|
+
export function restoreCandidates(touched: ReadonlyMap<string, number>, skip: (path: string) => boolean): string[] {
|
|
25
|
+
const candidates: string[] = [];
|
|
26
|
+
for (const [path] of [...touched].sort((a, b) => b[1] - a[1])) {
|
|
27
|
+
if (candidates.length === RESTORE_MAX_FILES) break;
|
|
28
|
+
if (!skip(path)) candidates.push(path);
|
|
29
|
+
}
|
|
30
|
+
return candidates;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What reading a candidate again gave: its text as the read tool returns it, or nothing (gone, unreadable, an image). */
|
|
34
|
+
export type RestoredRead = { path: string; text: string | undefined };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The reminder texts (one per `<system-reminder>` block) for the reads, in
|
|
38
|
+
* order: two blocks for a file within the per-file cap, one note for a larger
|
|
39
|
+
* one, nothing for a file that could not be read. A file's blocks are kept only
|
|
40
|
+
* while the running total stays within the overall cap. `restored` lists the
|
|
41
|
+
* files whose contents went back into context.
|
|
42
|
+
*/
|
|
43
|
+
export function restoreBlocks(reads: RestoredRead[], estimateTokens: (text: string) => number): { blocks: string[]; restored: string[] } {
|
|
44
|
+
const blocks: string[] = [];
|
|
45
|
+
const restored: string[] = [];
|
|
46
|
+
let total = 0;
|
|
47
|
+
for (const { path, text } of reads) {
|
|
48
|
+
if (text === undefined) continue;
|
|
49
|
+
const fits = estimateTokens(text) <= RESTORE_FILE_MAX_TOKENS;
|
|
50
|
+
const fileBlocks = fits ? [readCallBlock(path), readResultBlock(text)] : [tooLargeNote(path)];
|
|
51
|
+
const tokens = fileBlocks.reduce((sum, block) => sum + estimateTokens(block), 0);
|
|
52
|
+
if (total + tokens > RESTORE_TOTAL_MAX_TOKENS) continue;
|
|
53
|
+
total += tokens;
|
|
54
|
+
blocks.push(...fileBlocks);
|
|
55
|
+
if (fits) restored.push(path);
|
|
56
|
+
}
|
|
57
|
+
return { blocks, restored };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const readCallBlock = (path: string) => `Called the read tool with the following input: ${JSON.stringify({ path })}`;
|
|
61
|
+
export const readResultBlock = (text: string) => `Result of calling the read tool:\n${text}`;
|
|
62
|
+
export const tooLargeNote = (path: string) =>
|
|
63
|
+
`Note: ${path} was read before the last conversation was summarized, but the contents are too large to include. Use read tool if you need to access it.`;
|