one-code-extension 0.4.0 → 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 +219 -219
- 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 +66 -34
- 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 +19 -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
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* preamble, then every report line indented two spaces (a frame-like line at
|
|
13
13
|
* column zero inside the report is thereby forged). Its completion is a
|
|
14
14
|
* separate kind=agent `<task-notification>` whose `<result>` only points at
|
|
15
|
-
* that message — CC's two-message split (docs/decisions/subagents-workflows.md).
|
|
15
|
+
* that message — CC's two-message split (working-docs/decisions/subagents-workflows.md).
|
|
16
16
|
*
|
|
17
17
|
* Wakeups are neither: CC re-invokes the session with the scheduled prompt
|
|
18
18
|
* verbatim (findings §21), so `background/wakeup.ts` sends the prompt as-is.
|
|
@@ -25,13 +25,17 @@
|
|
|
25
25
|
* applies it at dispatch (`frameForDelivery`), never the producers.
|
|
26
26
|
*
|
|
27
27
|
* Every literal below was read out of real CC transcripts and the shipped
|
|
28
|
-
* 2.1.278 binary (docs/findings/24-claude-code-notifications.md) and is locked
|
|
28
|
+
* 2.1.278 binary (working-docs/findings/24-claude-code-notifications.md) and is locked
|
|
29
29
|
* by test/unit/notification-fidelity.test.ts. The delivery engine
|
|
30
30
|
* (`createTaskNotifier`, further down) is unchanged by the frame shape.
|
|
31
31
|
*/
|
|
32
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
33
|
+
import { randomUUID } from "node:crypto";
|
|
32
34
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
33
|
-
import {
|
|
35
|
+
import { withKeepAlive } from "../lsp/keep-alive.ts";
|
|
36
|
+
import { ONE_SHOT_COMMAND_FAILED_CHANNEL, RunOutcomeLatch } from "./interrupt.ts";
|
|
34
37
|
import { escapeXml } from "./local-command.ts";
|
|
38
|
+
import { isUserRun, PROMPT_OPTIONS_CHANNEL } from "./prompt-options.ts";
|
|
35
39
|
import { wrapReminder } from "./reminders.ts";
|
|
36
40
|
|
|
37
41
|
/** pi's session-mode union, derived from the context (the package root does not export ExtensionMode itself). */
|
|
@@ -154,7 +158,7 @@ export type HandBackVerdict = { kind: "blocked"; reason: string } | { kind: "una
|
|
|
154
158
|
* provider body, a stack), and this is the choke point where it enters the
|
|
155
159
|
* model-facing warning, so it must not go in unbounded. The clip is a bound on
|
|
156
160
|
* a diagnostic clause, not on output the model works from, so it is not
|
|
157
|
-
* persisted (docs/decisions/subagents-workflows.md, 2026-09-21); the
|
|
161
|
+
* persisted (working-docs/decisions/subagents-workflows.md, 2026-09-21); the
|
|
158
162
|
* `unavailable` variant marks a clipped reason with an ellipsis, the `blocked`
|
|
159
163
|
* one stays byte-exact with CC.
|
|
160
164
|
*/
|
|
@@ -278,7 +282,7 @@ export function monitorEndedSummary(description: string, status: TaskStatus, pro
|
|
|
278
282
|
* `Workflow "NAME" finished` — UNVERIFIED: the 2.1.278 binary holds no local
|
|
279
283
|
* workflow completion summary (only remote/error variants) and no transcript
|
|
280
284
|
* on this machine carries one, so this mirrors the agent shape
|
|
281
|
-
* (docs/features/notifications/plan.md, Deviations).
|
|
285
|
+
* (working-docs/features/notifications/plan.md, Deviations).
|
|
282
286
|
*/
|
|
283
287
|
export function workflowSummary(name: string, status: TaskStatus, reason?: string): string {
|
|
284
288
|
const suffix = reason ? `: ${reason}` : "";
|
|
@@ -362,6 +366,8 @@ const TAG_RE = {
|
|
|
362
366
|
summary: /<summary>([\s\S]*?)<\/summary>/,
|
|
363
367
|
result: /<result>([\s\S]*?)<\/result>/,
|
|
364
368
|
event: /<event>([\s\S]*?)<\/event>/,
|
|
369
|
+
status: /<status>([\s\S]*?)<\/status>/,
|
|
370
|
+
durationMs: /<duration_ms>(\d+)<\/duration_ms>/,
|
|
365
371
|
};
|
|
366
372
|
|
|
367
373
|
function tagBody(block: string, tag: keyof typeof TAG_RE): string | undefined {
|
|
@@ -371,7 +377,16 @@ function tagBody(block: string, tag: keyof typeof TAG_RE): string | undefined {
|
|
|
371
377
|
/** One frame of a notification's wire text, for display. */
|
|
372
378
|
export type NotificationFrame =
|
|
373
379
|
| { kind: "agent-message"; from: string; body: string; handBack: boolean }
|
|
374
|
-
| {
|
|
380
|
+
| {
|
|
381
|
+
kind: "task";
|
|
382
|
+
summary: string;
|
|
383
|
+
body: string;
|
|
384
|
+
pointer: boolean;
|
|
385
|
+
/** `<status>`, when the frame has one (an agent or shell completion). */
|
|
386
|
+
status?: string;
|
|
387
|
+
/** `<usage>`'s `<duration_ms>`: an agent frame's run time. */
|
|
388
|
+
durationMs?: number;
|
|
389
|
+
}
|
|
375
390
|
| { kind: "text"; text: string };
|
|
376
391
|
|
|
377
392
|
/**
|
|
@@ -393,11 +408,14 @@ export function parseNotificationFrames(wire: string): NotificationFrame[] {
|
|
|
393
408
|
const block = match[1];
|
|
394
409
|
const body = tagBody(block, "result") ?? tagBody(block, "event");
|
|
395
410
|
const result = body === undefined ? "" : unescapeTaskText(body).trim();
|
|
411
|
+
const durationMs = tagBody(block, "durationMs");
|
|
396
412
|
frames.push({
|
|
397
413
|
kind: "task",
|
|
398
414
|
summary: unescapeTaskText(tagBody(block, "summary") ?? ""),
|
|
399
415
|
body: result,
|
|
400
416
|
pointer: result.startsWith(HAND_BACK_POINTER_PREFIX),
|
|
417
|
+
status: tagBody(block, "status"),
|
|
418
|
+
durationMs: durationMs === undefined ? undefined : Number(durationMs),
|
|
401
419
|
});
|
|
402
420
|
} else {
|
|
403
421
|
frames.push({ kind: "agent-message", from: match[2], body: match[3], handBack: match[3].includes(HAND_BACK_PREAMBLE) });
|
|
@@ -463,8 +481,22 @@ export const NOTIFICATION_ID_KEY = "notificationId";
|
|
|
463
481
|
/** `details` key on a coalesced message: the `{customType, details}` of each notification it carries, in arrival order. */
|
|
464
482
|
export const NOTIFICATION_BATCH_KEY = "notifications";
|
|
465
483
|
|
|
484
|
+
/** True for the `details` of a message the notifier sent (one notification or a coalesced batch). */
|
|
485
|
+
export function isNotificationDetails(details: unknown): boolean {
|
|
486
|
+
return typeof details === "object" && details !== null && (NOTIFICATION_ID_KEY in details || NOTIFICATION_BATCH_KEY in details);
|
|
487
|
+
}
|
|
488
|
+
|
|
466
489
|
/** Inject a harness notification (a task/agent completion, a monitor event, a review note) into the conversation. */
|
|
467
|
-
export
|
|
490
|
+
export interface TaskNotifier {
|
|
491
|
+
(customType: string, text: string, details?: Record<string, unknown>): void;
|
|
492
|
+
/**
|
|
493
|
+
* True while notifications are held after an interrupted turn (see
|
|
494
|
+
* `createTaskNotifier`). A producer that emits a stream of batches (a
|
|
495
|
+
* monitor) keeps accumulating its own bounded batch while this is true,
|
|
496
|
+
* instead of adding one held message per flush.
|
|
497
|
+
*/
|
|
498
|
+
holding(): boolean;
|
|
499
|
+
}
|
|
468
500
|
|
|
469
501
|
/**
|
|
470
502
|
* `task_output` announces here that it returned a FINISHED task's output in a
|
|
@@ -505,12 +537,12 @@ export interface TaskNotifierOptions {
|
|
|
505
537
|
|
|
506
538
|
/**
|
|
507
539
|
* Custom types that are a turn's INPUT rather than a harness frame (a fired
|
|
508
|
-
* wakeup or
|
|
540
|
+
* wakeup or cron job delivers the model's prompt verbatim). Such a message is
|
|
509
541
|
* never merged with frames — it goes out on its own, in arrival order — or the
|
|
510
542
|
* prompt would abut a `<task-notification>` with nothing marking the switch,
|
|
511
543
|
* and the wrong renderer would draw the pair.
|
|
512
544
|
*/
|
|
513
|
-
export const REINVOCATION_TYPES: ReadonlySet<string> = new Set(["wakeup", "
|
|
545
|
+
export const REINVOCATION_TYPES: ReadonlySet<string> = new Set(["wakeup", "cron"]);
|
|
514
546
|
|
|
515
547
|
/** Default coalescing window (ms). */
|
|
516
548
|
export const DEFAULT_COALESCE_MS = 250;
|
|
@@ -556,11 +588,27 @@ export const DEFAULT_COALESCE_MS = 250;
|
|
|
556
588
|
* `triggerTurn` restarted, within milliseconds, the very work the user had just
|
|
557
589
|
* interrupted (review M2). So once a turn has settled `aborted` or `error`
|
|
558
590
|
* (a turn that died on a provider error should not be restarted by a
|
|
559
|
-
* notification either), every notification — the re-sends and
|
|
560
|
-
*
|
|
561
|
-
* and
|
|
562
|
-
*
|
|
563
|
-
*
|
|
591
|
+
* notification either), every notification — the re-sends and any new one
|
|
592
|
+
* arriving while idle — is held by the notifier itself, not started as a turn.
|
|
593
|
+
* The hold is bounded and ends with the next turn, however it starts:
|
|
594
|
+
*
|
|
595
|
+
* - The user's next prompt (`before_agent_start`) takes everything held as
|
|
596
|
+
* `nextTurn` messages, which pi appends to that prompt
|
|
597
|
+
* (`_pendingNextTurnMessages`, filled synchronously before pi reads it),
|
|
598
|
+
* exactly Claude Code's behaviour after an interrupt. The held frames merge
|
|
599
|
+
* into one message, as a coalescing window would merge them, so a long
|
|
600
|
+
* absence costs one message, not one per arrival.
|
|
601
|
+
* - Any other turn start (`agent_start`: a typed skill such as `/loop`, which
|
|
602
|
+
* pi runs through `sendMessage` and never through `prompt()`, or a scheduled
|
|
603
|
+
* fire) ends the hold too, and what was held is steered into that turn. A
|
|
604
|
+
* turn that then settles cleanly leaves no hold behind; before, only a typed
|
|
605
|
+
* prompt ended it, so one Esc early in a session silenced every later
|
|
606
|
+
* `/loop` tick.
|
|
607
|
+
* - A fresh re-invocation (a cron or wakeup fire, `REINVOCATION_TYPES`) is
|
|
608
|
+
* never held: it is the turn's input, scheduled by the user or the loop, so
|
|
609
|
+
* it opens its turn as it would have without the interrupt. Only a re-send
|
|
610
|
+
* of one Esc discarded waits. Producers that stream (a monitor) ask
|
|
611
|
+
* `holding()` and keep their own bounded batch meanwhile.
|
|
564
612
|
*
|
|
565
613
|
* Dead sessions: a producer's callback can outlive the session it belongs to
|
|
566
614
|
* (a background shell finishing after `/clear` replaced the session, or after
|
|
@@ -578,9 +626,16 @@ export const DEFAULT_COALESCE_MS = 250;
|
|
|
578
626
|
* pi's stock system prompt with no reminder stack (review H1, measured). Until
|
|
579
627
|
* no `prompt()` has run in this process, an idle notification is delivered as a
|
|
580
628
|
* user message (`sendUserMessage`, source "extension"), which takes the full
|
|
581
|
-
* prompt path; the cost is that this one notification renders as a user bubble
|
|
582
|
-
*
|
|
583
|
-
*
|
|
629
|
+
* prompt path; the cost is that this one notification renders as a user bubble
|
|
630
|
+
* (the background extension's markdown transformer shows only its headline).
|
|
631
|
+
* The usual first-turn case avoids it: the command that started the work (the
|
|
632
|
+
* `/btw` fork) announces pi's prompt options (lib/prompt-options.ts), which
|
|
633
|
+
* system-prompt builds from, and the notification goes the custom-message way
|
|
634
|
+
* with Claude Code's one-line render. Every later notification goes that way. Its turn skips
|
|
635
|
+
* `before_agent_start` too, and pi drops a run's prompt when the run settles,
|
|
636
|
+
* so the system-prompt extension installs the prompt for it from
|
|
637
|
+
* `context_with_system` (`system-prompt/idle-turn.ts`). Upstream ask:
|
|
638
|
+
* working-docs/upstream_prs.md #17.
|
|
584
639
|
*/
|
|
585
640
|
export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOptions = {}): TaskNotifier {
|
|
586
641
|
interface Pending {
|
|
@@ -588,6 +643,8 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
|
|
|
588
643
|
text: string;
|
|
589
644
|
details: Record<string, unknown>;
|
|
590
645
|
resent: boolean;
|
|
646
|
+
/** Unmerged arrivals so a held group can withdraw one task without losing its peers. */
|
|
647
|
+
items: Incoming[];
|
|
591
648
|
}
|
|
592
649
|
interface Incoming {
|
|
593
650
|
customType: string;
|
|
@@ -596,15 +653,24 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
|
|
|
596
653
|
}
|
|
597
654
|
const coalesceMs = options.coalesceMs ?? DEFAULT_COALESCE_MS;
|
|
598
655
|
const pending = new Map<string, Pending>();
|
|
656
|
+
// Every notifier hears every message_end; simultaneous producers must not share an outbox id.
|
|
657
|
+
const producerId = randomUUID();
|
|
599
658
|
let seq = 0;
|
|
659
|
+
const nextId = () => `${producerId}-${++seq}`;
|
|
600
660
|
/** False once the session this notifier belongs to has shut down. */
|
|
601
661
|
let active = true;
|
|
602
|
-
/**
|
|
662
|
+
/**
|
|
663
|
+
* True once an idle turn can be built without `prompt()`: a prompt has run
|
|
664
|
+
* (the only path that emits before_agent_start), or a command announced
|
|
665
|
+
* pi's prompt options.
|
|
666
|
+
*/
|
|
603
667
|
let prompted = false;
|
|
604
668
|
/** True between agent_start and agent_settled. */
|
|
605
669
|
let busy = false;
|
|
606
|
-
/** True from a turn settling aborted/errored until the
|
|
670
|
+
/** True from a turn settling aborted/errored until the next turn starts: hold, do not start turns. */
|
|
607
671
|
let interrupted = false;
|
|
672
|
+
/** What the hold kept, in arrival order; released as one merged message (see the header). */
|
|
673
|
+
let held: Incoming[] = [];
|
|
608
674
|
const outcome = new RunOutcomeLatch();
|
|
609
675
|
/** Notifications waiting for the coalescing window to close. */
|
|
610
676
|
let batch: Incoming[] = [];
|
|
@@ -615,29 +681,23 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
|
|
|
615
681
|
pending.delete(id);
|
|
616
682
|
return;
|
|
617
683
|
}
|
|
684
|
+
// Held after an interrupt (see the header): kept here, out of reach of
|
|
685
|
+
// Esc's queue clearing, so it leaves the outbox now; a re-send at a
|
|
686
|
+
// later settle would duplicate it. A fresh re-invocation is the one
|
|
687
|
+
// exception: it opens its own turn.
|
|
688
|
+
if (interrupted && !busy && (entry.resent || !REINVOCATION_TYPES.has(entry.customType))) {
|
|
689
|
+
pending.delete(id);
|
|
690
|
+
held.push(...entry.items);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
618
693
|
try {
|
|
619
|
-
if (interrupted && !busy) {
|
|
620
|
-
// Held for the user's next prompt (see the header). pi keeps a nextTurn
|
|
621
|
-
// message on the session itself, out of reach of Esc's queue clearing,
|
|
622
|
-
// so it counts as delivered now: a re-send at a later settle would
|
|
623
|
-
// duplicate it.
|
|
624
|
-
pending.delete(id);
|
|
625
|
-
pi.sendMessage(
|
|
626
|
-
{
|
|
627
|
-
customType: entry.customType,
|
|
628
|
-
content: [{ type: "text", text: frameForDelivery(entry.text, "with-user-prompt") }],
|
|
629
|
-
display: true,
|
|
630
|
-
details: { ...entry.details, [NOTIFICATION_ID_KEY]: id },
|
|
631
|
-
},
|
|
632
|
-
{ deliverAs: "nextTurn" },
|
|
633
|
-
);
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
694
|
if (!prompted && !busy) {
|
|
637
695
|
// No confirmation possible for a user-role message (no details), and
|
|
638
696
|
// prompt() cannot be cleared by Esc before it starts: count it delivered.
|
|
639
697
|
pending.delete(id);
|
|
640
|
-
pi.
|
|
698
|
+
// followUp: pi 1.2.0 counts a starting run as busy and rejects a
|
|
699
|
+
// bare send there; when idle, the option changes nothing.
|
|
700
|
+
pi.sendUserMessage(frameForDelivery(entry.text, "opens-turn"), { deliverAs: "followUp" });
|
|
641
701
|
return;
|
|
642
702
|
}
|
|
643
703
|
pi.sendMessage(
|
|
@@ -656,43 +716,56 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
|
|
|
656
716
|
}
|
|
657
717
|
};
|
|
658
718
|
|
|
659
|
-
/** One
|
|
719
|
+
/** One entry for a group: a lone notification as it came, several merged under one text. */
|
|
720
|
+
const merge = (group: Incoming[]): Incoming =>
|
|
721
|
+
group.length === 1
|
|
722
|
+
? group[0]
|
|
723
|
+
: {
|
|
724
|
+
customType: group[0].customType,
|
|
725
|
+
text: mergeNotificationTexts(group.map((n) => n.text)),
|
|
726
|
+
// Keep any pre-batched metadata flat.
|
|
727
|
+
details: {
|
|
728
|
+
[NOTIFICATION_BATCH_KEY]: group.flatMap(
|
|
729
|
+
(n) => (n.details[NOTIFICATION_BATCH_KEY] as unknown[] | undefined) ?? [{ customType: n.customType, details: n.details }],
|
|
730
|
+
),
|
|
731
|
+
},
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
/** One outbox entry per group. */
|
|
660
735
|
const enqueue = (group: Incoming[]) => {
|
|
661
736
|
if (group.length === 0) return;
|
|
662
|
-
const id =
|
|
663
|
-
const entry: Pending =
|
|
664
|
-
group.length === 1
|
|
665
|
-
? { ...group[0], resent: false }
|
|
666
|
-
: {
|
|
667
|
-
customType: group[0].customType,
|
|
668
|
-
text: mergeNotificationTexts(group.map((n) => n.text)),
|
|
669
|
-
details: { [NOTIFICATION_BATCH_KEY]: group.map((n) => ({ customType: n.customType, details: n.details })) },
|
|
670
|
-
resent: false,
|
|
671
|
-
};
|
|
737
|
+
const id = nextId();
|
|
738
|
+
const entry: Pending = { ...merge(group), resent: false, items: group };
|
|
672
739
|
pending.set(id, entry);
|
|
673
740
|
dispatch(id, entry);
|
|
674
741
|
};
|
|
675
742
|
|
|
676
743
|
/**
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
* order is kept across the two.
|
|
744
|
+
* Split arrivals into delivery groups: frames merge, a standalone
|
|
745
|
+
* (turn-input) message stands alone, arrival order is kept across the two.
|
|
680
746
|
*/
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
const incoming = batch;
|
|
684
|
-
batch = [];
|
|
747
|
+
const groups = (incoming: Incoming[]): Incoming[][] => {
|
|
748
|
+
const out: Incoming[][] = [];
|
|
685
749
|
let frames: Incoming[] = [];
|
|
686
750
|
for (const item of incoming) {
|
|
687
751
|
if (!REINVOCATION_TYPES.has(item.customType)) {
|
|
688
752
|
frames.push(item);
|
|
689
753
|
continue;
|
|
690
754
|
}
|
|
691
|
-
|
|
755
|
+
if (frames.length > 0) out.push(frames);
|
|
692
756
|
frames = [];
|
|
693
|
-
|
|
757
|
+
out.push([item]);
|
|
694
758
|
}
|
|
695
|
-
|
|
759
|
+
if (frames.length > 0) out.push(frames);
|
|
760
|
+
return out;
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
/** Close the coalescing window. */
|
|
764
|
+
const flush = () => {
|
|
765
|
+
batchTimer = undefined;
|
|
766
|
+
const incoming = batch;
|
|
767
|
+
batch = [];
|
|
768
|
+
for (const group of groups(incoming)) enqueue(group);
|
|
696
769
|
};
|
|
697
770
|
const clearBatch = () => {
|
|
698
771
|
if (batchTimer !== undefined) clearTimeout(batchTimer);
|
|
@@ -700,6 +773,37 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
|
|
|
700
773
|
batch = [];
|
|
701
774
|
};
|
|
702
775
|
|
|
776
|
+
/** End the hold. `withPrompt`: the user's prompt is being built, so ride it as nextTurn; else steer into the turn that started. */
|
|
777
|
+
const release = (withPrompt: boolean) => {
|
|
778
|
+
interrupted = false;
|
|
779
|
+
const released = held;
|
|
780
|
+
held = [];
|
|
781
|
+
if (!active || released.length === 0) return;
|
|
782
|
+
for (const group of groups(released)) {
|
|
783
|
+
if (!withPrompt) {
|
|
784
|
+
enqueue(group);
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
const entry = merge(group);
|
|
788
|
+
try {
|
|
789
|
+
// Synchronous up to pi's push onto `_pendingNextTurnMessages`, which
|
|
790
|
+
// the prompt being built reads right after this hook; pi keeps it out
|
|
791
|
+
// of reach of Esc's queue clearing, so no outbox entry.
|
|
792
|
+
pi.sendMessage(
|
|
793
|
+
{
|
|
794
|
+
customType: entry.customType,
|
|
795
|
+
content: [{ type: "text", text: frameForDelivery(entry.text, "with-user-prompt") }],
|
|
796
|
+
display: true,
|
|
797
|
+
details: { ...entry.details, [NOTIFICATION_ID_KEY]: nextId() },
|
|
798
|
+
},
|
|
799
|
+
{ deliverAs: "nextTurn" },
|
|
800
|
+
);
|
|
801
|
+
} catch {
|
|
802
|
+
// Disposed under us (see dispatch).
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
|
|
703
807
|
pi.on("message_end", (event) => {
|
|
704
808
|
const message = event.message as { role?: string; details?: unknown };
|
|
705
809
|
if (message.role !== "custom") return;
|
|
@@ -724,15 +828,24 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
|
|
|
724
828
|
});
|
|
725
829
|
pi.on("agent_start", () => {
|
|
726
830
|
busy = true;
|
|
831
|
+
// A turn the user did not type (a skill such as /loop, a scheduled fire):
|
|
832
|
+
// the session is working again, so what was held steers into it.
|
|
833
|
+
if (interrupted) release(false);
|
|
727
834
|
});
|
|
728
|
-
pi.on("before_agent_start", () => {
|
|
835
|
+
pi.on("before_agent_start", (event) => {
|
|
836
|
+
prompted = true;
|
|
837
|
+
// A run an extension opened (pi 1.2.0 fires this for it too) has no user
|
|
838
|
+
// message to ride; agent_start releases into it as before.
|
|
839
|
+
if (isUserRun(event)) release(true);
|
|
840
|
+
});
|
|
841
|
+
pi.events?.on(PROMPT_OPTIONS_CHANNEL, () => {
|
|
729
842
|
prompted = true;
|
|
730
|
-
interrupted = false;
|
|
731
843
|
});
|
|
732
844
|
// A replaced session (/clear, /new, resume) has no use for the old one's undelivered notices.
|
|
733
845
|
pi.on("session_start", () => {
|
|
734
846
|
pending.clear();
|
|
735
847
|
clearBatch();
|
|
848
|
+
held = [];
|
|
736
849
|
active = true;
|
|
737
850
|
prompted = false;
|
|
738
851
|
busy = false;
|
|
@@ -741,16 +854,28 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
|
|
|
741
854
|
pi.on("session_shutdown", () => {
|
|
742
855
|
active = false;
|
|
743
856
|
clearBatch();
|
|
857
|
+
held = [];
|
|
744
858
|
});
|
|
745
859
|
if (options.withdrawOnDelivery) {
|
|
746
860
|
pi.events?.on(TASK_OUTPUT_DELIVERED_CHANNEL, (data) => {
|
|
747
861
|
const taskId = (data as TaskOutputDelivered | undefined)?.taskId;
|
|
748
862
|
if (taskId === undefined) return;
|
|
749
|
-
|
|
863
|
+
const keep = (item: Incoming) => item.details.taskId !== taskId;
|
|
864
|
+
batch = batch.filter(keep);
|
|
865
|
+
held = held.filter(keep);
|
|
866
|
+
// A sent entry still awaiting confirmation is resent (or held) when an
|
|
867
|
+
// interrupt discards pi's queue: drop the task from it too, re-merging
|
|
868
|
+
// what remains, so the model is not told of output it already has.
|
|
869
|
+
for (const [id, entry] of [...pending]) {
|
|
870
|
+
const items = entry.items.filter(keep);
|
|
871
|
+
if (items.length === entry.items.length) continue;
|
|
872
|
+
if (items.length === 0) pending.delete(id);
|
|
873
|
+
else Object.assign(entry, merge(items), { items });
|
|
874
|
+
}
|
|
750
875
|
});
|
|
751
876
|
}
|
|
752
877
|
|
|
753
|
-
|
|
878
|
+
const notify = (customType: string, text: string, details: Record<string, unknown> = {}) => {
|
|
754
879
|
if (!active) return;
|
|
755
880
|
batch.push({ customType, text, details });
|
|
756
881
|
if (coalesceMs <= 0) {
|
|
@@ -762,6 +887,7 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
|
|
|
762
887
|
(batchTimer as { unref?: () => void }).unref?.();
|
|
763
888
|
}
|
|
764
889
|
};
|
|
890
|
+
return Object.assign(notify, { holding: () => active && interrupted && !busy });
|
|
765
891
|
}
|
|
766
892
|
|
|
767
893
|
/**
|
|
@@ -786,10 +912,128 @@ export function sessionOutlivesTurn(mode: ExtensionMode): boolean {
|
|
|
786
912
|
}
|
|
787
913
|
}
|
|
788
914
|
|
|
915
|
+
/**
|
|
916
|
+
* What a one-shot session (`-p`, `--mode json`) changes for the model, said
|
|
917
|
+
* before its first call. Without it the tool descriptions promise detached
|
|
918
|
+
* work, notifications and questions the session cannot deliver, and the model
|
|
919
|
+
* learns otherwise only from a result: in the 2026-10-04 Codex battery both
|
|
920
|
+
* GPT-6 models planned around background shells, agents, monitors, cron jobs
|
|
921
|
+
* and the question dialog, then had to retract. One Code's own text; Claude
|
|
922
|
+
* Code sends no such block.
|
|
923
|
+
*/
|
|
924
|
+
export function oneShotSessionNote(): string {
|
|
925
|
+
return [
|
|
926
|
+
"This is a non-interactive one-shot session: it ends when this turn ends, and nobody can reply before then.",
|
|
927
|
+
"- Work you start in the background (a shell command, an agent, a monitor) runs to completion before its call returns, and no notification follows. To overlap independent work, issue those calls together in one batch.",
|
|
928
|
+
"- Scheduled and recurring jobs never fire.",
|
|
929
|
+
"- No user can answer a question or approve a plan. State your assumption and proceed, or ask in your final reply and stop.",
|
|
930
|
+
].join("\n");
|
|
931
|
+
}
|
|
932
|
+
|
|
789
933
|
function assertNever(mode: never): never {
|
|
790
934
|
throw new Error(`Unhandled session mode: ${String(mode)}`);
|
|
791
935
|
}
|
|
792
936
|
|
|
937
|
+
/**
|
|
938
|
+
* Submit a user turn and keep a one-shot command alive through preflight AND
|
|
939
|
+
* settlement. Create once at extension factory scope, not once per command.
|
|
940
|
+
*
|
|
941
|
+
* Unlike sendMessage, sendUserMessage awaits input hooks/auth/before_agent_start
|
|
942
|
+
* before marking the run active. waitForIdle alone therefore returns too soon.
|
|
943
|
+
* Serialize one-shot commands across extensions and wait out any active run
|
|
944
|
+
* before sending: a queued message need not emit agent_start. Correlate that
|
|
945
|
+
* event with the sending async context, never the next unrelated start. Then
|
|
946
|
+
* waitForIdle covers tools, follow-ups and retries. TUI/RPC stay fire-and-forget.
|
|
947
|
+
*
|
|
948
|
+
* pi's void API reports preflight errors separately, without a completion event.
|
|
949
|
+
* Bound the start wait so an error or an input hook consuming the prompt cannot
|
|
950
|
+
* hang a print process forever. The timeout applies only to startup, not the run.
|
|
951
|
+
*/
|
|
952
|
+
export function createUserMessageSender(pi: Pick<ExtensionAPI, "on" | "sendUserMessage" | "events">) {
|
|
953
|
+
type Waiter = { start(): void; stop(): void };
|
|
954
|
+
const pending = new Set<Waiter>();
|
|
955
|
+
const sending = new AsyncLocalStorage<Waiter>();
|
|
956
|
+
let active = true;
|
|
957
|
+
let previous = Promise.resolve();
|
|
958
|
+
// Each extension has its own module instance. Reserve the same position in
|
|
959
|
+
// all senders through the session bus, not shared module state.
|
|
960
|
+
pi.events.on("one-code:user-turn-queue", (data) => {
|
|
961
|
+
if (!active) return;
|
|
962
|
+
const reservation = data as { before: Promise<void>[]; done: Promise<void> };
|
|
963
|
+
reservation.before.push(previous);
|
|
964
|
+
previous = reservation.done;
|
|
965
|
+
});
|
|
966
|
+
pi.on("agent_start", () => {
|
|
967
|
+
// Async preflight preserves the sending call's context, including input
|
|
968
|
+
// transformations. An unrelated turn must not release this command.
|
|
969
|
+
sending.getStore()?.start();
|
|
970
|
+
});
|
|
971
|
+
pi.on("session_shutdown", () => {
|
|
972
|
+
active = false;
|
|
973
|
+
for (const waiter of pending) waiter.stop();
|
|
974
|
+
pending.clear();
|
|
975
|
+
sending.disable();
|
|
976
|
+
});
|
|
977
|
+
// A replacement session (/new, /resume) reuses this extension instance:
|
|
978
|
+
// its commands report their failures again, and never queue behind a
|
|
979
|
+
// reservation the old session left unreleased.
|
|
980
|
+
pi.on("session_start", () => {
|
|
981
|
+
active = true;
|
|
982
|
+
previous = Promise.resolve();
|
|
983
|
+
});
|
|
984
|
+
return async (
|
|
985
|
+
ctx: Parameters<typeof awaitOneShotTurn>[0],
|
|
986
|
+
content: Parameters<ExtensionAPI["sendUserMessage"]>[0],
|
|
987
|
+
options?: Parameters<ExtensionAPI["sendUserMessage"]>[1],
|
|
988
|
+
): Promise<void> => {
|
|
989
|
+
if (!active) throw new Error("Session shut down before the command's user turn completed.");
|
|
990
|
+
if (sessionOutlivesTurn(ctx.mode)) {
|
|
991
|
+
// followUp by default: pi 1.2.0 counts a starting run as busy and
|
|
992
|
+
// rejects a send without deliverAs there; when idle it changes nothing.
|
|
993
|
+
pi.sendUserMessage(content, { ...options, deliverAs: options?.deliverAs ?? "followUp" });
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
let release!: () => void;
|
|
997
|
+
const reservation = { before: [] as Promise<void>[], done: new Promise<void>((resolve) => { release = resolve; }) };
|
|
998
|
+
pi.events.emit("one-code:user-turn-queue", reservation);
|
|
999
|
+
await withKeepAlive(async () => {
|
|
1000
|
+
let start!: () => void;
|
|
1001
|
+
let fail!: (error: Error) => void;
|
|
1002
|
+
const started = new Promise<void>((resolve) => { start = resolve; });
|
|
1003
|
+
const stopped = new Promise<never>((_resolve, reject) => { fail = reject; });
|
|
1004
|
+
const waiter = { start, stop: () => fail(new Error("Session shut down before the command's user turn completed.")) };
|
|
1005
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1006
|
+
pending.add(waiter);
|
|
1007
|
+
try {
|
|
1008
|
+
await Promise.race([Promise.all(reservation.before), stopped]);
|
|
1009
|
+
// A queued user message can be consumed in an existing run without
|
|
1010
|
+
// another agent_start. One-shot commands run after that run settles.
|
|
1011
|
+
if (!ctx.isIdle()) await Promise.race([awaitOneShotTurn(ctx), stopped]);
|
|
1012
|
+
if (!active) throw new Error("Session shut down before the command's user turn completed.");
|
|
1013
|
+
timer = setTimeout(() => fail(new Error("Command's user turn did not start within 30 seconds. Check model authentication and extension input hooks, then retry.")), 30_000);
|
|
1014
|
+
// A raw extension send can still start during preflight. Without a
|
|
1015
|
+
// per-send queue acknowledgement, reject that race rather than run
|
|
1016
|
+
// a queued command we would then incorrectly report as not started.
|
|
1017
|
+
sending.run(waiter, () => pi.sendUserMessage(content, options ? { ...options, deliverAs: undefined } : undefined));
|
|
1018
|
+
await Promise.race([started, stopped]);
|
|
1019
|
+
clearTimeout(timer);
|
|
1020
|
+
await Promise.race([awaitOneShotTurn(ctx), stopped]);
|
|
1021
|
+
} finally {
|
|
1022
|
+
clearTimeout(timer);
|
|
1023
|
+
pending.delete(waiter);
|
|
1024
|
+
release();
|
|
1025
|
+
}
|
|
1026
|
+
}).catch((error) => {
|
|
1027
|
+
// pi logs command-handler rejections but runPrintMode still returns
|
|
1028
|
+
// success. Tell the session's exit extension without touching the
|
|
1029
|
+
// process-wide code here (a later prompt may succeed, or this may be
|
|
1030
|
+
// a child). A shutdown rejection must not act on a disposed session.
|
|
1031
|
+
if (active) pi.events.emit(ONE_SHOT_COMMAND_FAILED_CHANNEL, {});
|
|
1032
|
+
throw error;
|
|
1033
|
+
});
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
|
|
793
1037
|
/**
|
|
794
1038
|
* Block until the current agent run settles, but only in one-shot modes.
|
|
795
1039
|
*
|
|
@@ -803,8 +1047,8 @@ function assertNever(mode: never): never {
|
|
|
803
1047
|
*
|
|
804
1048
|
* `waitForIdle()` exists on a command context; a plain event-handler context
|
|
805
1049
|
* (e.g. the `input` hook) only exposes `isIdle()`, so poll that as the fallback.
|
|
806
|
-
* `pi.sendMessage` marks the run active synchronously
|
|
807
|
-
*
|
|
1050
|
+
* `pi.sendMessage` marks the run active synchronously. `sendUserMessage` does
|
|
1051
|
+
* not: use createUserMessageSender for that API so its preflight finishes first.
|
|
808
1052
|
*/
|
|
809
1053
|
export async function awaitOneShotTurn(ctx: {
|
|
810
1054
|
mode: ExtensionMode;
|