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
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* background extension — Claude Code's background-task surface:
|
|
3
|
-
* monitor, task_output, task_stop, schedule_wakeup
|
|
3
|
+
* monitor, task_output, task_stop, schedule_wakeup, cron_create, cron_list,
|
|
4
|
+
* cron_delete, and /loop.
|
|
4
5
|
*
|
|
5
6
|
* Owns the BackgroundRegistry. Other extensions (subagents) register their
|
|
6
7
|
* long-running work over TASK_REGISTER_CHANNEL at runtime, so task_output and
|
|
7
8
|
* task_stop address every background task in the session regardless of which
|
|
8
9
|
* extension started it. Events and completions are delivered as steered
|
|
9
10
|
* task notifications (lib/notifications.ts), never as user input; a fired
|
|
10
|
-
* wakeup or
|
|
11
|
+
* wakeup or cron job re-invokes the session with its prompt verbatim.
|
|
12
|
+
*
|
|
13
|
+
* Every scheduled prompt lives in one CronStore (cron.ts): cron_create jobs
|
|
14
|
+
* and schedule_wakeup's one-shot wakeups (wakeup.ts). One timer is armed to
|
|
15
|
+
* the store's next fire; jobs fire only while the agent is idle, so a job
|
|
16
|
+
* that came due during a turn fires once at agent_settled. `/loop` is
|
|
17
|
+
* Claude Code's skill (skills/loop, its body built by loop-skill.ts): the
|
|
18
|
+
* model schedules the loop itself.
|
|
11
19
|
*/
|
|
12
20
|
|
|
13
21
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
22
|
import { Type } from "typebox";
|
|
15
23
|
import { whenAborted } from "../lib/abort.ts";
|
|
16
24
|
import { DEFER_CHANNEL } from "../lib/deferred.ts";
|
|
17
|
-
import { persistIfLarge, sessionResultsDir } from "../lib/persisted-output.ts";
|
|
18
|
-
import { detachedSpawnOptions, KILL_GRACE_MS, stopProcessTree, waitForChildExit } from "../lib/process-tree.ts";
|
|
25
|
+
import { persistedFilePreview, persistIfLarge, sessionResultsDir } from "../lib/persisted-output.ts";
|
|
26
|
+
import { detachedSpawnOptions, KILL_GRACE_MS, rememberProcessGroup, stopProcessTree, waitForChildExit } from "../lib/process-tree.ts";
|
|
19
27
|
import { sessionAlive } from "../lib/session-lifecycle.ts";
|
|
20
28
|
import { bashSpawn, spawnShellCommand } from "../lib/shell-spawn.ts";
|
|
21
|
-
import { ccToolRenderers, customMessageText, liveUiCtx, notificationComponent, scheduledTaskComponent } from "../lib/tui-render.ts";
|
|
29
|
+
import { ccToolRenderers, customMessageText, formatFireTime, liveUiCtx, notificationComponent, notificationUserMarkdown, scheduledTaskComponent } from "../lib/tui-render.ts";
|
|
22
30
|
import {
|
|
23
31
|
type BackgroundTask,
|
|
24
32
|
BackgroundRegistry,
|
|
@@ -26,16 +34,35 @@ import {
|
|
|
26
34
|
generateTaskId,
|
|
27
35
|
TASK_REGISTER_CHANNEL,
|
|
28
36
|
} from "./registry.ts";
|
|
37
|
+
import { SWITCH_CANCEL, SWITCH_STOP, switchWarning, workWidgetLine } from "./switch-guard.ts";
|
|
29
38
|
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
parseLoopArgs,
|
|
39
|
+
AGED_OUT_RESULT,
|
|
40
|
+
DynamicLoop,
|
|
41
|
+
formatScheduled,
|
|
42
|
+
formatStopped,
|
|
43
|
+
SCHEDULE_WAKEUP_DESCRIPTION,
|
|
44
|
+
SCHEDULE_WAKEUP_PARAMS,
|
|
45
|
+
WAKEUP_ERRORS,
|
|
38
46
|
} from "./wakeup.ts";
|
|
47
|
+
import { freshDeliveryState, type LoopDeliveryState, readLoopFile, resolveLoopFire } from "./loop-fire.ts";
|
|
48
|
+
import { autonomousPreamble, loopSkillPrompt } from "./loop-skill.ts";
|
|
49
|
+
import {
|
|
50
|
+
CRON_CREATE_DESCRIPTION,
|
|
51
|
+
CRON_CREATE_PARAMETERS,
|
|
52
|
+
CRON_DELETE_DESCRIPTION,
|
|
53
|
+
CRON_DELETE_PARAMETERS,
|
|
54
|
+
CRON_LIST_DESCRIPTION,
|
|
55
|
+
CRON_LIST_PARAMETERS,
|
|
56
|
+
CronStore,
|
|
57
|
+
type CronFire,
|
|
58
|
+
type CronJob,
|
|
59
|
+
describeCadence,
|
|
60
|
+
formatCannotFire,
|
|
61
|
+
formatCreateResult,
|
|
62
|
+
formatDeleteResult,
|
|
63
|
+
formatJobList,
|
|
64
|
+
formatUnknownJob,
|
|
65
|
+
} from "./cron.ts";
|
|
39
66
|
import {
|
|
40
67
|
createTaskNotifier,
|
|
41
68
|
monitorEndedSummary,
|
|
@@ -55,15 +82,34 @@ import {
|
|
|
55
82
|
MONITOR_BATCH_MAX_CHARS,
|
|
56
83
|
MONITOR_BATCH_IDLE_MS,
|
|
57
84
|
type MonitorBatch,
|
|
85
|
+
MonitorLineSplitter,
|
|
58
86
|
pushEvent,
|
|
59
87
|
} from "./monitor-batch.ts";
|
|
60
88
|
import { registerLocalCommand } from "../lib/local-command.ts";
|
|
61
|
-
|
|
62
|
-
|
|
89
|
+
import { applyNoopFolds, decideFold, type FoldDetails, type FoldEntry, foldSuffix } from "./noop-fold.ts";
|
|
90
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
91
|
+
import { AGENT_CRON_CHANNEL, AGENT_CRON_FIRE_CHANNEL, type AgentCronFire, type AgentCronRequest, formatNotOwner } from "../lib/agent-cron.ts";
|
|
92
|
+
import { SKILL_BODY_CHANNEL, type SkillBodyQuery, SLASH_EXPAND_CHANNEL, type SlashExpandQuery } from "../lib/skill-body.ts";
|
|
93
|
+
import { BUNDLED_SKILLS_DIR } from "../lib/skill-scan.ts";
|
|
94
|
+
import { createWriteStream, mkdirSync, type WriteStream } from "node:fs";
|
|
95
|
+
import { join, resolve } from "node:path";
|
|
96
|
+
import { StringDecoder } from "node:string_decoder";
|
|
97
|
+
import { SESSION_WORK_CHANNEL, sessionBackgroundTask, sessionCron, type SessionWorkQuery } from "../lib/session-work.ts";
|
|
98
|
+
import { WORKTREE_CHANNEL, type WorktreeLocation } from "../lib/worktree-channel.ts";
|
|
99
|
+
import { queueNotice } from "../lib/notices.ts";
|
|
100
|
+
|
|
101
|
+
/** Claude Code's `task_output` limit (`TASK_MAX_OUTPUT_DEFAULT`). */
|
|
102
|
+
const TASK_OUTPUT_MAX_CHARS = 32_000;
|
|
63
103
|
const STORED_OUTPUT_CAP = 200_000;
|
|
64
104
|
const DEFAULT_MONITOR_TIMEOUT_MS = 300_000;
|
|
65
105
|
const MAX_MONITOR_TIMEOUT_MS = 3_600_000;
|
|
66
106
|
const MAX_BLOCK_TIMEOUT_MS = 600_000;
|
|
107
|
+
/** A finished monitor that saw nothing says so, never a blank a model reads as "still running" (bash's EMPTY_OUTPUT_MARKER). */
|
|
108
|
+
const MONITOR_EMPTY_OUTPUT = "(no output — the monitor received no events and nothing on stderr)";
|
|
109
|
+
/** Heads a monitor's output when the start is gone: past the in-memory cap with no spool file to hold it. */
|
|
110
|
+
const SPOOL_LOST_NOTE = `[The monitor's spool file could not be written, so only the last ${STORED_OUTPUT_CAP.toLocaleString("en-US")} characters of its output were kept.]\n`;
|
|
111
|
+
/** setTimeout's longest delay; a later fire re-arms when this one wakes. */
|
|
112
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
67
113
|
function tail(text: string, cap: number): string {
|
|
68
114
|
return text.length <= cap ? text : `… (earlier output truncated)\n${text.slice(-cap)}`;
|
|
69
115
|
}
|
|
@@ -79,6 +125,58 @@ function tail(text: string, cap: number): string {
|
|
|
79
125
|
*/
|
|
80
126
|
let pendingShutdownNotice: string | undefined;
|
|
81
127
|
|
|
128
|
+
/**
|
|
129
|
+
* `<sessionDir>/monitor/<taskId>/output.log`, beside background shells'
|
|
130
|
+
* `bash/<taskId>/`. A session-less run spools under the session's private
|
|
131
|
+
* results dir (`sessionResultsDir`), so its output is never kept only as the
|
|
132
|
+
* in-memory tail. Undefined when the folder cannot be made.
|
|
133
|
+
*/
|
|
134
|
+
function monitorLogPath(ctx: ExtensionContext, taskId: string): string | undefined {
|
|
135
|
+
try {
|
|
136
|
+
const dir = join(sessionResultsDir(ctx), "monitor", taskId);
|
|
137
|
+
mkdirSync(dir, { recursive: true });
|
|
138
|
+
return join(dir, "output.log");
|
|
139
|
+
} catch {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* `task_output`'s body. Past `TASK_OUTPUT_MAX_CHARS` a task with a spool file
|
|
146
|
+
* gets Claude Code's clip, `[Truncated. Full output: <path>]` and the tail
|
|
147
|
+
* that fits, since the file holds the full text; a task without one (a
|
|
148
|
+
* subagent's report) is persisted whole to the session's tool-results
|
|
149
|
+
* folder, never cut (decisions/tools.md).
|
|
150
|
+
*/
|
|
151
|
+
function taskOutputBody(task: BackgroundTask, ctx: ExtensionContext): string {
|
|
152
|
+
const output = task.output();
|
|
153
|
+
if (output.length <= TASK_OUTPUT_MAX_CHARS) return output;
|
|
154
|
+
if (task.logPath) {
|
|
155
|
+
const header = `[Truncated. Full output: ${task.logPath}]\n\n`;
|
|
156
|
+
return `${header}${output.slice(-(TASK_OUTPUT_MAX_CHARS - header.length))}`;
|
|
157
|
+
}
|
|
158
|
+
return persistIfLarge(output, { dir: sessionResultsDir(ctx), id: `task-output-${task.id}`, maxBytes: TASK_OUTPUT_MAX_CHARS });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** A session entry as the no-op fold reads it (noop-fold.ts). */
|
|
162
|
+
function foldEntry(entry: { type: string; timestamp?: string; customType?: string; details?: unknown; message?: unknown }): FoldEntry {
|
|
163
|
+
if (entry.type === "compaction") return { kind: "compaction" };
|
|
164
|
+
if (entry.type === "custom_message") {
|
|
165
|
+
if (entry.customType === "wakeup") return { kind: "wakeup", timestamp: Date.parse(entry.timestamp ?? "") || 0, details: entry.details as FoldDetails | undefined };
|
|
166
|
+
return entry.customType === "cron" ? { kind: "fire" } : { kind: "other" };
|
|
167
|
+
}
|
|
168
|
+
if (entry.type !== "message") return { kind: "other" };
|
|
169
|
+
const message = entry.message as { role?: string; content?: unknown; stopReason?: string; toolCallId?: string };
|
|
170
|
+
if (message.role === "user") return { kind: "user" };
|
|
171
|
+
if (message.role === "toolResult") return { kind: "toolResult", toolCallId: message.toolCallId ?? "" };
|
|
172
|
+
if (message.role === "assistant") {
|
|
173
|
+
const blocks = Array.isArray(message.content) ? (message.content as Array<{ type?: string; id?: string; name?: string; arguments?: { noop?: unknown } }>) : [];
|
|
174
|
+
const toolCalls = blocks.filter((b) => b.type === "toolCall").map((b) => ({ id: b.id ?? "", name: b.name ?? "", noop: b.arguments?.noop }));
|
|
175
|
+
return { kind: "assistant", toolCalls, aborted: message.stopReason === "aborted" };
|
|
176
|
+
}
|
|
177
|
+
return { kind: "other" };
|
|
178
|
+
}
|
|
179
|
+
|
|
82
180
|
export default function backgroundExtension(pi: ExtensionAPI) {
|
|
83
181
|
// After session_shutdown the captured ctx throws on every access and the
|
|
84
182
|
// notifier is inert; a monitor whose command ends later (its shell was
|
|
@@ -88,32 +186,78 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
88
186
|
const alive = sessionAlive(pi);
|
|
89
187
|
const registry = new BackgroundRegistry();
|
|
90
188
|
let lastCtx: ExtensionContext | undefined;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
189
|
+
const cron = new CronStore();
|
|
190
|
+
const dynamicLoop = new DynamicLoop(cron);
|
|
191
|
+
// What the no-prompt /loop sentinels already delivered, per recipient (the
|
|
192
|
+
// main session, or a subagent by task id): each conversation has its own
|
|
193
|
+
// record. The main session's compaction resets its own (loop-fire.ts).
|
|
194
|
+
const MAIN_OWNER = "main";
|
|
195
|
+
const loopDelivery = new Map<string, LoopDeliveryState>();
|
|
196
|
+
const deliveryFor = (owner: string): LoopDeliveryState => {
|
|
197
|
+
let state = loopDelivery.get(owner);
|
|
198
|
+
if (!state) loopDelivery.set(owner, (state = freshDeliveryState()));
|
|
199
|
+
return state;
|
|
200
|
+
};
|
|
201
|
+
let sessionCwd = process.cwd();
|
|
202
|
+
// The main session's worktree while it is in one (enter_worktree), where its fires resolve.
|
|
203
|
+
let worktreeCwd: string | undefined;
|
|
204
|
+
pi.events.on(WORKTREE_CHANNEL, (data) => {
|
|
205
|
+
worktreeCwd = (data as WorktreeLocation | null)?.path;
|
|
206
|
+
});
|
|
207
|
+
let cronTimer: NodeJS.Timeout | undefined;
|
|
208
|
+
// True between agent_start and agent_settled, so a due cron job waits for the
|
|
209
|
+
// turn to end instead of injecting one mid-run. agent_settled, not agent_end:
|
|
210
|
+
// agent_end can precede an internal retry / auto-compaction / queued
|
|
211
|
+
// continuation, and a fire landing in that gap would inject a turn while the
|
|
212
|
+
// prior one is still about to resume.
|
|
100
213
|
let agentBusy = false;
|
|
101
|
-
// Set when a dynamic /loop force-activated schedule_wakeup, so /loop stop can
|
|
102
|
-
// restore the lean tool surface it changed.
|
|
103
|
-
let activatedWakeupTool = false;
|
|
104
214
|
|
|
105
|
-
pi.events.on(TASK_REGISTER_CHANNEL, (
|
|
215
|
+
pi.events.on(TASK_REGISTER_CHANNEL, (data) => {
|
|
216
|
+
const task = data as BackgroundTask;
|
|
217
|
+
registry.register(task);
|
|
218
|
+
// Shells and subagent runs count toward the widget's warning line too.
|
|
219
|
+
updateWidget();
|
|
220
|
+
task.finished?.then(updateWidget, updateWidget);
|
|
221
|
+
});
|
|
222
|
+
// A subagent's cron tools (lib/agent-cron.ts): its jobs, its view, its deletes.
|
|
223
|
+
pi.events.on(AGENT_CRON_CHANNEL, (data) => {
|
|
224
|
+
const request = data as AgentCronRequest;
|
|
225
|
+
if (request.op === "create") {
|
|
226
|
+
const created = cron.create({ cron: request.cron, prompt: request.prompt, recurring: request.recurring, agentId: request.agentId, cwd: request.cwd }, Date.now());
|
|
227
|
+
request.result = created.ok
|
|
228
|
+
? { text: formatCreateResult(created.job), details: { jobId: created.job.id } }
|
|
229
|
+
: { text: created.error, isError: true };
|
|
230
|
+
if (created.ok) runCron();
|
|
231
|
+
} else if (request.op === "list") {
|
|
232
|
+
const jobs = cron.list().filter((job) => job.agentId === request.agentId);
|
|
233
|
+
request.result = { text: formatJobList(jobs), details: { jobCount: jobs.length } };
|
|
234
|
+
} else {
|
|
235
|
+
const job = cron.get(request.id);
|
|
236
|
+
if (!job) request.result = { text: formatUnknownJob(request.id), isError: true };
|
|
237
|
+
else if (job.agentId !== request.agentId) request.result = { text: formatNotOwner(request.id), isError: true };
|
|
238
|
+
else {
|
|
239
|
+
cron.delete(request.id);
|
|
240
|
+
runCron();
|
|
241
|
+
request.result = { text: formatDeleteResult(request.id) };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
// The Stop hook's session_crons / background_tasks (lib/session-work.ts).
|
|
246
|
+
pi.events.on(SESSION_WORK_CHANNEL, (data) => {
|
|
247
|
+
const query = data as SessionWorkQuery;
|
|
248
|
+
query.crons.push(...cron.list().map(sessionCron));
|
|
249
|
+
query.tasks.push(...registry.running().map(sessionBackgroundTask));
|
|
250
|
+
});
|
|
106
251
|
|
|
107
252
|
const updateWidget = () => {
|
|
108
253
|
// liveUiCtx: a stale ctx (session replaced) reads as "no UI", never a throw;
|
|
109
254
|
// session_shutdown also drops lastCtx, so a late repaint is a no-op.
|
|
110
255
|
const live = liveUiCtx(lastCtx);
|
|
111
256
|
if (!live) return;
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
live.ui.setWidget("cc-background", running > 0 ? [` background tasks: ${running} running`] : undefined);
|
|
257
|
+
// Every running task and scheduled job, panel-owned ones included: the
|
|
258
|
+
// line is the warning that /clear, /reload or quitting stops them (switch-guard.ts).
|
|
259
|
+
const line = workWidgetLine(registry.running().length, cron.list().length);
|
|
260
|
+
live.ui.setWidget("cc-background", line ? [line] : undefined);
|
|
117
261
|
};
|
|
118
262
|
|
|
119
263
|
// The wire frames are model-facing; the transcript shows a compact headline
|
|
@@ -123,20 +267,88 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
123
267
|
pi.registerMessageRenderer("task-notification", (message, { expanded }, theme) =>
|
|
124
268
|
notificationComponent(theme, customMessageText(message.content), expanded),
|
|
125
269
|
);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
pi.registerMessageRenderer("loop-start", (message, { expanded }, theme) =>
|
|
133
|
-
notificationComponent(theme, customMessageText(message.content), expanded),
|
|
270
|
+
// A notification that opens the session's first turn is a user message
|
|
271
|
+
// (lib/notifications.ts, "First turn of a session"); pi draws those as
|
|
272
|
+
// markdown, so the same headline replaces the frames here. Every notifier's
|
|
273
|
+
// messages pass through this one transformer.
|
|
274
|
+
pi.registerMarkdownTransformer((markdown, context) =>
|
|
275
|
+
context.messageType === "user" ? (notificationUserMarkdown(markdown) ?? markdown) : markdown,
|
|
134
276
|
);
|
|
277
|
+
for (const customType of ["wakeup", "cron"]) {
|
|
278
|
+
// Claude Code: "Claude resuming /loop wakeup (…)" for a wakeup; the model here may not be Claude.
|
|
279
|
+
const label = customType === "wakeup" ? "Resuming /loop wakeup" : undefined;
|
|
280
|
+
pi.registerMessageRenderer(customType, (message, { expanded }, theme) => {
|
|
281
|
+
const fold = message.details as FoldDetails | undefined;
|
|
282
|
+
const suffix = fold?.noOpStreak ? foldSuffix(fold.noOpStreak, formatFireTime(fold.streakStartedAt ?? Date.now())) : "";
|
|
283
|
+
return scheduledTaskComponent(theme, customMessageText(message.content), (message as { timestamp?: number }).timestamp ?? Date.now(), expanded, label, suffix);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
135
286
|
|
|
136
287
|
// A monitor's end the model already read through task_output is withdrawn
|
|
137
288
|
// like a shell's (lib/notifications.ts).
|
|
138
289
|
const notify = createTaskNotifier(pi, { withdrawOnDelivery: true });
|
|
139
290
|
|
|
291
|
+
/**
|
|
292
|
+
* The text a fired prompt delivers, as Claude Code's queue would run it: a
|
|
293
|
+
* no-prompt /loop sentinel expands (loop-fire.ts), and a slash command that
|
|
294
|
+
* names a skill runs that skill (lib/skill-body.ts); anything else is the
|
|
295
|
+
* prompt verbatim. Both resolve in the job owner's directory: a subagent's
|
|
296
|
+
* own, or the main session's worktree while it is in one.
|
|
297
|
+
*/
|
|
298
|
+
const fireText = (job: CronJob): string => {
|
|
299
|
+
const { prompt } = job;
|
|
300
|
+
const cwd = job.cwd ?? worktreeCwd ?? sessionCwd;
|
|
301
|
+
const resolved = resolveLoopFire(deliveryFor(job.agentId ?? MAIN_OWNER), prompt, cwd, autonomousPreamble());
|
|
302
|
+
if (resolved !== prompt || !prompt.startsWith("/")) return resolved;
|
|
303
|
+
const query: SlashExpandQuery = { text: prompt, cwd };
|
|
304
|
+
pi.events.emit(SLASH_EXPAND_CHANNEL, query);
|
|
305
|
+
return query.expanded ?? prompt;
|
|
306
|
+
};
|
|
307
|
+
/** Claude Code's no-op fold for the wakeup firing now (noop-fold.ts), read off the session branch. */
|
|
308
|
+
const foldDetails = (): FoldDetails => {
|
|
309
|
+
let entries: FoldEntry[];
|
|
310
|
+
try {
|
|
311
|
+
entries = (lastCtx?.sessionManager.getBranch() ?? []).map(foldEntry);
|
|
312
|
+
} catch {
|
|
313
|
+
return {};
|
|
314
|
+
}
|
|
315
|
+
const decision = decideFold(entries);
|
|
316
|
+
return decision.kind === "fold" ? { noOpStreak: decision.priorStreak + 1, streakStartedAt: decision.since } : {};
|
|
317
|
+
};
|
|
318
|
+
const fireCron = (fire: CronFire) => {
|
|
319
|
+
const { job, final } = fire;
|
|
320
|
+
if (job.agentId !== undefined) {
|
|
321
|
+
// A subagent's job goes to that agent, or is dropped once it has ended.
|
|
322
|
+
const delivery: AgentCronFire = { agentId: job.agentId, jobId: job.id, prompt: fireText(job) };
|
|
323
|
+
pi.events.emit(AGENT_CRON_FIRE_CHANNEL, delivery);
|
|
324
|
+
if (!delivery.delivered) {
|
|
325
|
+
// The agent has ended: its job and what it was sent go with it.
|
|
326
|
+
cron.delete(job.id);
|
|
327
|
+
loopDelivery.delete(job.agentId);
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const fold = job.source === "wakeup" ? foldDetails() : {};
|
|
332
|
+
if (job.source === "wakeup") dynamicLoop.inFlight = job.prompt;
|
|
333
|
+
notify(job.source === "wakeup" ? "wakeup" : "cron", fireText(job), { jobId: job.id, cron: job.cron, source: job.source, final, ...fold });
|
|
334
|
+
};
|
|
335
|
+
/** Fire every due job (only while idle), then arm the one timer to the next fire. */
|
|
336
|
+
const runCron = () => {
|
|
337
|
+
if (cronTimer) clearTimeout(cronTimer);
|
|
338
|
+
cronTimer = undefined;
|
|
339
|
+
// A busy agent re-runs this at agent_settled; a dead session never does.
|
|
340
|
+
if (!alive() || agentBusy) {
|
|
341
|
+
updateWidget();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
for (const fire of cron.takeDue(Date.now())) fireCron(fire);
|
|
345
|
+
updateWidget();
|
|
346
|
+
const next = cron.nextFireAt();
|
|
347
|
+
if (next === undefined) return;
|
|
348
|
+
cronTimer = setTimeout(runCron, Math.min(Math.max(0, next - Date.now()), MAX_TIMER_MS));
|
|
349
|
+
cronTimer.unref?.();
|
|
350
|
+
};
|
|
351
|
+
|
|
140
352
|
pi.registerTool({
|
|
141
353
|
name: "monitor",
|
|
142
354
|
label: "Monitor",
|
|
@@ -180,6 +392,32 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
180
392
|
const id = generateTaskId();
|
|
181
393
|
let status: BackgroundTask["status"] = "running";
|
|
182
394
|
let stored = "";
|
|
395
|
+
// The whole stream, spooled like a background shell's, so the batches'
|
|
396
|
+
// "task_output has the full stream" and task_output's clip header point
|
|
397
|
+
// at something true, and a one-shot result past the in-memory cap can
|
|
398
|
+
// still persist every line. Opened on the first output.
|
|
399
|
+
let log: WriteStream | undefined;
|
|
400
|
+
let spoolFailed = false;
|
|
401
|
+
let overflowed = false;
|
|
402
|
+
const record = (text: string) => {
|
|
403
|
+
if (stored.length + text.length > STORED_OUTPUT_CAP) overflowed = true;
|
|
404
|
+
stored = tail(`${stored}${text}`, STORED_OUTPUT_CAP);
|
|
405
|
+
if (!text || spoolFailed) return;
|
|
406
|
+
if (!log) {
|
|
407
|
+
const path = monitorLogPath(ctx, id);
|
|
408
|
+
if (!path) return;
|
|
409
|
+
log = createWriteStream(path, { flags: "a" });
|
|
410
|
+
log.on("error", () => {
|
|
411
|
+
// Best-effort, as bash's spool: the in-memory tail stays
|
|
412
|
+
// authoritative, and nothing names a missing or partial file as
|
|
413
|
+
// the full output any more.
|
|
414
|
+
spoolFailed = true;
|
|
415
|
+
task.logPath = undefined;
|
|
416
|
+
});
|
|
417
|
+
task.logPath = path;
|
|
418
|
+
}
|
|
419
|
+
log.write(text);
|
|
420
|
+
};
|
|
183
421
|
let eventCount = 0;
|
|
184
422
|
let pending: MonitorBatch = emptyBatch();
|
|
185
423
|
let flushTimer: NodeJS.Timeout | undefined;
|
|
@@ -188,9 +426,16 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
188
426
|
finish = resolve;
|
|
189
427
|
});
|
|
190
428
|
|
|
191
|
-
const flush = () => {
|
|
429
|
+
const flush = (force = false) => {
|
|
192
430
|
flushTimer = undefined;
|
|
193
431
|
if (batchSize(pending) === 0) return;
|
|
432
|
+
if (!force && notify.holding()) {
|
|
433
|
+
// Held after an interrupt: one bounded batch accumulates (its
|
|
434
|
+
// overflow counted) instead of one held message per flush, and
|
|
435
|
+
// goes out once the next turn starts.
|
|
436
|
+
flushTimer = setTimeout(() => flush(), MONITOR_BATCH_IDLE_MS);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
194
439
|
const batch = pending;
|
|
195
440
|
pending = emptyBatch();
|
|
196
441
|
// CC's mid-run monitor batch: no status, the lines in `<event>`.
|
|
@@ -201,48 +446,71 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
201
446
|
);
|
|
202
447
|
};
|
|
203
448
|
|
|
449
|
+
// Set by stop(): what the command prints while it is torn down (a shell
|
|
450
|
+
// running the rest of `sleep 30; echo done` before the SIGKILL grace ends)
|
|
451
|
+
// is not an event, so the count says what happened before the stop.
|
|
452
|
+
let stopRequested = false;
|
|
204
453
|
const onEvent = (line: string) => {
|
|
454
|
+
if (stopRequested) return;
|
|
205
455
|
eventCount++;
|
|
206
|
-
|
|
456
|
+
record(`${line}\n`);
|
|
207
457
|
if (oneShot) return; // collected into the tool result instead
|
|
208
458
|
pushEvent(pending, line);
|
|
209
459
|
// Bounded batches, and a wider window mid-turn so a chatty stream
|
|
210
460
|
// coalesces instead of steering one notification per second
|
|
211
461
|
// (monitor-batch.ts).
|
|
212
|
-
flushTimer ??= setTimeout(flush, agentBusy ? MONITOR_BATCH_BUSY_MS : MONITOR_BATCH_IDLE_MS);
|
|
462
|
+
flushTimer ??= setTimeout(() => flush(), agentBusy ? MONITOR_BATCH_BUSY_MS : MONITOR_BATCH_IDLE_MS);
|
|
213
463
|
};
|
|
214
464
|
|
|
215
465
|
const end = (finalStatus: BackgroundTask["status"], note?: string) => {
|
|
216
466
|
if (status !== "running") return;
|
|
217
467
|
status = finalStatus;
|
|
218
|
-
task.status = finalStatus;
|
|
219
|
-
task.finishedAt = Date.now();
|
|
220
468
|
if (flushTimer) clearTimeout(flushTimer);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
469
|
+
const complete = () => {
|
|
470
|
+
if (task.status !== "running") return;
|
|
471
|
+
// Terminal status and its notification promise readable final output.
|
|
472
|
+
task.status = finalStatus;
|
|
473
|
+
task.finishedAt = Date.now();
|
|
474
|
+
finish();
|
|
475
|
+
// After shutdown or inside a one-shot run, report through `finished` alone.
|
|
476
|
+
if (!alive() || oneShot) return;
|
|
477
|
+
flush(true);
|
|
478
|
+
updateWidget();
|
|
479
|
+
// CC's monitor end: the recent tail rides <event> under the ended summary.
|
|
480
|
+
const ccStatus = taskStatusOf(finalStatus);
|
|
481
|
+
const recent = tail(stored, MONITOR_BATCH_MAX_CHARS).trim();
|
|
482
|
+
notify(
|
|
483
|
+
"task-notification",
|
|
484
|
+
taskNotification({
|
|
485
|
+
kind: "monitor",
|
|
486
|
+
taskId: id,
|
|
487
|
+
toolUseId: toolCallId,
|
|
488
|
+
status: ccStatus,
|
|
489
|
+
summary: monitorEndedSummary(params.description, ccStatus, eventCount > 0, note),
|
|
490
|
+
result: recent || undefined,
|
|
491
|
+
}),
|
|
492
|
+
{ taskId: id, status: finalStatus },
|
|
493
|
+
);
|
|
494
|
+
};
|
|
495
|
+
if (log && !log.closed) {
|
|
496
|
+
// `close` follows a clean end and an error alike. A stuck stream
|
|
497
|
+
// must not hold a blocking tool forever or advertise a partial file.
|
|
498
|
+
const fallback = setTimeout(() => {
|
|
499
|
+
spoolFailed = true;
|
|
500
|
+
task.logPath = undefined;
|
|
501
|
+
log?.destroy();
|
|
502
|
+
complete();
|
|
503
|
+
}, 1_000);
|
|
504
|
+
log.once("close", () => {
|
|
505
|
+
clearTimeout(fallback);
|
|
506
|
+
complete();
|
|
507
|
+
});
|
|
508
|
+
log.end();
|
|
509
|
+
} else {
|
|
510
|
+
complete();
|
|
511
|
+
}
|
|
243
512
|
};
|
|
244
513
|
|
|
245
|
-
let stopRequested = false;
|
|
246
514
|
let stop: () => void;
|
|
247
515
|
if (params.command) {
|
|
248
516
|
// Own process group: stop() must end the COMMAND, not just the `$SHELL -c`
|
|
@@ -263,27 +531,32 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
263
531
|
...detachedSpawnOptions(),
|
|
264
532
|
stdio: ["ignore", "pipe", "pipe"],
|
|
265
533
|
});
|
|
266
|
-
|
|
534
|
+
rememberProcessGroup(child);
|
|
535
|
+
const lines = new MonitorLineSplitter();
|
|
536
|
+
// Streaming decoders: a UTF-8 character split across reads stays whole.
|
|
537
|
+
const stdoutText = new StringDecoder("utf8");
|
|
538
|
+
const stderrText = new StringDecoder("utf8");
|
|
267
539
|
child.stdout?.on("data", (chunk: Buffer) => {
|
|
268
|
-
|
|
269
|
-
let idx: number;
|
|
270
|
-
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
271
|
-
const line = buffer.slice(0, idx).trimEnd();
|
|
272
|
-
buffer = buffer.slice(idx + 1);
|
|
273
|
-
if (line) onEvent(line);
|
|
274
|
-
}
|
|
540
|
+
for (const line of lines.push(stdoutText.write(chunk))) onEvent(line);
|
|
275
541
|
});
|
|
276
542
|
child.stderr?.on("data", (chunk: Buffer) => {
|
|
277
|
-
|
|
543
|
+
record(stderrText.write(chunk));
|
|
278
544
|
});
|
|
279
545
|
// Exit plus a short stdio grace, not `close` (lib/process-tree.ts).
|
|
546
|
+
// The last line may have no newline; it is an event all the same.
|
|
547
|
+
const drain = () => {
|
|
548
|
+
for (const line of [...lines.push(stdoutText.end()), ...lines.end()]) onEvent(line);
|
|
549
|
+
record(stderrText.end());
|
|
550
|
+
};
|
|
280
551
|
waitForChildExit(child).then(
|
|
281
|
-
({ code }) =>
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
552
|
+
({ code }) => {
|
|
553
|
+
drain();
|
|
554
|
+
end(stopRequested ? "stopped" : code === 0 ? "completed" : "failed", code !== null && code !== 0 ? `exit code ${code}` : undefined);
|
|
555
|
+
},
|
|
556
|
+
(error: Error) => {
|
|
557
|
+
drain();
|
|
558
|
+
end("failed", error.message);
|
|
559
|
+
},
|
|
287
560
|
);
|
|
288
561
|
stop = () => {
|
|
289
562
|
stopRequested = true;
|
|
@@ -322,7 +595,7 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
322
595
|
description: params.description,
|
|
323
596
|
status,
|
|
324
597
|
startedAt: Date.now(),
|
|
325
|
-
output: () => stored,
|
|
598
|
+
output: () => (overflowed && !task.logPath ? `${SPOOL_LOST_NOTE}${stored}` : stored || (task.status === "running" ? "" : MONITOR_EMPTY_OUTPUT)),
|
|
326
599
|
stop,
|
|
327
600
|
finished,
|
|
328
601
|
};
|
|
@@ -349,7 +622,11 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
349
622
|
} finally {
|
|
350
623
|
unhook();
|
|
351
624
|
}
|
|
352
|
-
|
|
625
|
+
// Past the in-memory cap the spool holds every line: point at it rather
|
|
626
|
+
// than persist the tail. Without one, say what was lost.
|
|
627
|
+
const spooled = overflowed && task.logPath ? persistedFilePreview(task.logPath) : undefined;
|
|
628
|
+
const kept = overflowed && !spooled ? `${SPOOL_LOST_NOTE}${stored.trim()}` : stored.trim() || "(no events)";
|
|
629
|
+
const output = spooled ?? persistIfLarge(kept, { dir: sessionResultsDir(ctx), id: `monitor-${id}` });
|
|
353
630
|
const finalStatus = task.status;
|
|
354
631
|
let how: string = finalStatus;
|
|
355
632
|
if (finalStatus === "stopped") {
|
|
@@ -407,22 +684,26 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
407
684
|
|
|
408
685
|
if (params.block !== false && task.status === "running") {
|
|
409
686
|
const timeoutMs = Math.min(params.timeout ?? 30_000, MAX_BLOCK_TIMEOUT_MS);
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
resolve
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
687
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
688
|
+
let unhook = () => {};
|
|
689
|
+
try {
|
|
690
|
+
await Promise.race([
|
|
691
|
+
task.finished,
|
|
692
|
+
new Promise<void>((resolve) => {
|
|
693
|
+
// The tool awaits this deadline: keep it referenced in one-shot runs.
|
|
694
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
695
|
+
unhook = whenAborted(signal, resolve);
|
|
696
|
+
}),
|
|
697
|
+
]);
|
|
698
|
+
} finally {
|
|
699
|
+
clearTimeout(timer);
|
|
700
|
+
unhook();
|
|
701
|
+
}
|
|
421
702
|
}
|
|
422
703
|
|
|
423
704
|
updateWidget();
|
|
424
705
|
const header = formatTaskLine(task);
|
|
425
|
-
const body =
|
|
706
|
+
const body = taskOutputBody(task, ctx) || "(no output yet)";
|
|
426
707
|
// The model now holds a finished task's output: a completion notification
|
|
427
708
|
// still waiting to go out for it is redundant (lib/notifications.ts).
|
|
428
709
|
if (task.status !== "running") pi.events.emit(TASK_OUTPUT_DELIVERED_CHANNEL, { taskId: task.id } satisfies TaskOutputDelivered);
|
|
@@ -437,9 +718,12 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
437
718
|
name: "task_stop",
|
|
438
719
|
label: "Stop Task",
|
|
439
720
|
...ccToolRenderers("Stop Task"),
|
|
440
|
-
|
|
721
|
+
// Claude Code's TaskStop text without its agent-team and agent-name clauses:
|
|
722
|
+
// a task is stopped by its id here.
|
|
723
|
+
description:
|
|
724
|
+
"\n- Stops a running background task by its ID\n- Takes a task_id parameter identifying the task to stop\n- Returns a success or failure status\n- Use this tool when you need to terminate a long-running task\n",
|
|
441
725
|
parameters: Type.Object({
|
|
442
|
-
task_id: Type.String({ description: "The
|
|
726
|
+
task_id: Type.String({ description: "The ID of the background task to stop." }),
|
|
443
727
|
}),
|
|
444
728
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
445
729
|
lastCtx = ctx;
|
|
@@ -463,7 +747,7 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
463
747
|
{
|
|
464
748
|
type: "text",
|
|
465
749
|
text: wasResident
|
|
466
|
-
? `Terminated the resident agent behind ${task.id} (${task.description}); it can no longer be messaged live (
|
|
750
|
+
? `Terminated the resident agent behind ${task.id} (${task.description}); it can no longer be messaged live (SendMessage will resume it from its session file).`
|
|
467
751
|
: `Stop requested for ${task.id} (${task.description}).`,
|
|
468
752
|
},
|
|
469
753
|
],
|
|
@@ -478,61 +762,91 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
478
762
|
...ccToolRenderers<{ delaySeconds?: number; stop?: boolean }>("Schedule Wakeup", {
|
|
479
763
|
title: (a) => (a?.stop ? "stop" : a?.delaySeconds !== undefined ? `${a.delaySeconds}s` : undefined),
|
|
480
764
|
}),
|
|
481
|
-
description:
|
|
482
|
-
"Schedule when to resume work on a self-paced recurring task — the dynamic mode of the /loop command. After `delaySeconds` (clamped to [60, 3600]) the harness re-invokes you with the given prompt, verbatim, as the next turn's input. One wakeup is pending at a time — scheduling again replaces it; {stop: true} ends the loop and cancels any pending wakeup.\n\nPass the same task back via `prompt` each turn so the next firing repeats it. Set `noop: true` when nothing changed this tick (you checked and there's nothing to report); `noop: false` when something happened worth keeping. Pick `delaySeconds` from what you're actually waiting for: poll external state (a CI run, a deploy) at the rate it changes; for a quiet idle heartbeat prefer a long delay (1200s+). Do NOT schedule a short wakeup just to poll background work you started here — its completion already notifies you.",
|
|
765
|
+
description: SCHEDULE_WAKEUP_DESCRIPTION,
|
|
483
766
|
parameters: Type.Object({
|
|
484
|
-
delaySeconds: Type.Optional(Type.Number({ description:
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
767
|
+
delaySeconds: Type.Optional(Type.Number({ description: SCHEDULE_WAKEUP_PARAMS.delaySeconds })),
|
|
768
|
+
reason: Type.Optional(Type.String({ description: SCHEDULE_WAKEUP_PARAMS.reason })),
|
|
769
|
+
prompt: Type.Optional(Type.String({ description: SCHEDULE_WAKEUP_PARAMS.prompt })),
|
|
770
|
+
stop: Type.Optional(Type.Boolean({ description: SCHEDULE_WAKEUP_PARAMS.stop })),
|
|
771
|
+
noop: Type.Optional(Type.Boolean({ description: SCHEDULE_WAKEUP_PARAMS.noop })),
|
|
489
772
|
}),
|
|
490
773
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
491
774
|
lastCtx = ctx;
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
};
|
|
498
|
-
if (params.stop) {
|
|
499
|
-
clearPending();
|
|
500
|
-
return { content: [{ type: "text", text: "Wakeup loop stopped; no further wakeups will fire." }], details: {} };
|
|
775
|
+
const fail = (text: string) => ({ content: [{ type: "text" as const, text }], details: {}, isError: true });
|
|
776
|
+
if (params.stop === true) {
|
|
777
|
+
const cancelled = dynamicLoop.stop();
|
|
778
|
+
runCron();
|
|
779
|
+
return { content: [{ type: "text", text: formatStopped(cancelled) }], details: { stopped: true, cancelledWakeups: cancelled } };
|
|
501
780
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
if (params.
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
781
|
+
if (params.delaySeconds === undefined || params.reason === undefined) return fail(WAKEUP_ERRORS.delayAndReason);
|
|
782
|
+
if (params.prompt === undefined) return fail(WAKEUP_ERRORS.prompt);
|
|
783
|
+
if (params.noop === undefined) return fail(WAKEUP_ERRORS.noop);
|
|
784
|
+
const now = Date.now();
|
|
785
|
+
const scheduled = dynamicLoop.schedule(params.delaySeconds, params.prompt, now);
|
|
786
|
+
runCron();
|
|
787
|
+
if (!scheduled) return { content: [{ type: "text", text: AGED_OUT_RESULT }], details: { scheduledFor: 0 } };
|
|
788
|
+
const text = formatScheduled(scheduled, now);
|
|
789
|
+
return {
|
|
790
|
+
content: [{ type: "text", text: sessionOutlivesTurn(ctx.mode) ? text : `${text} ${formatCannotFire()}` }],
|
|
791
|
+
details: { ...scheduled, reason: params.reason, noop: params.noop },
|
|
792
|
+
};
|
|
793
|
+
},
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
pi.registerTool({
|
|
797
|
+
name: "cron_create",
|
|
798
|
+
label: "Cron Create",
|
|
799
|
+
...ccToolRenderers<{ cron?: string }>("Cron Create", { title: (a) => a?.cron }),
|
|
800
|
+
description: CRON_CREATE_DESCRIPTION,
|
|
801
|
+
parameters: CRON_CREATE_PARAMETERS,
|
|
802
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
803
|
+
lastCtx = ctx;
|
|
804
|
+
const created = cron.create({ cron: params.cron, prompt: params.prompt, recurring: params.recurring }, Date.now());
|
|
805
|
+
if (!created.ok) return { content: [{ type: "text", text: created.error }], details: {}, isError: true };
|
|
806
|
+
runCron();
|
|
807
|
+
const text = formatCreateResult(created.job);
|
|
808
|
+
return {
|
|
809
|
+
content: [{ type: "text", text: sessionOutlivesTurn(ctx.mode) ? text : `${text} ${formatCannotFire()}` }],
|
|
810
|
+
details: { jobId: created.job.id, cron: created.job.cron, recurring: created.job.recurring, nextFireAt: created.job.nextFireAt },
|
|
811
|
+
};
|
|
528
812
|
},
|
|
529
813
|
});
|
|
530
814
|
|
|
815
|
+
pi.registerTool({
|
|
816
|
+
name: "cron_list",
|
|
817
|
+
label: "Cron List",
|
|
818
|
+
...ccToolRenderers("Cron List"),
|
|
819
|
+
description: CRON_LIST_DESCRIPTION,
|
|
820
|
+
parameters: CRON_LIST_PARAMETERS,
|
|
821
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
822
|
+
lastCtx = ctx;
|
|
823
|
+
const jobs = cron.list();
|
|
824
|
+
return { content: [{ type: "text", text: formatJobList(jobs) }], details: { count: jobs.length } };
|
|
825
|
+
},
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
pi.registerTool({
|
|
829
|
+
name: "cron_delete",
|
|
830
|
+
label: "Cron Delete",
|
|
831
|
+
...ccToolRenderers<{ id?: string }>("Cron Delete", { title: (a) => a?.id }),
|
|
832
|
+
description: CRON_DELETE_DESCRIPTION,
|
|
833
|
+
parameters: CRON_DELETE_PARAMETERS,
|
|
834
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
835
|
+
lastCtx = ctx;
|
|
836
|
+
if (!cron.delete(params.id)) return { content: [{ type: "text", text: formatUnknownJob(params.id) }], details: {}, isError: true };
|
|
837
|
+
runCron();
|
|
838
|
+
return { content: [{ type: "text", text: formatDeleteResult(params.id) }], details: { jobId: params.id } };
|
|
839
|
+
},
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
// schedule_wakeup stays eager, as Claude Code's ScheduleWakeup is.
|
|
531
843
|
for (const [name, keywords] of Object.entries({
|
|
532
844
|
monitor: ["monitor", "watch", "background", "tail", "events", "stream", "websocket"],
|
|
533
845
|
task_output: ["task", "background", "output", "status", "wait"],
|
|
534
846
|
task_stop: ["task", "background", "stop", "kill", "cancel"],
|
|
535
|
-
|
|
847
|
+
cron_create: ["cron", "schedule", "recurring", "remind", "timer", "every", "later"],
|
|
848
|
+
cron_list: ["cron", "schedule", "list", "jobs", "scheduled"],
|
|
849
|
+
cron_delete: ["cron", "schedule", "cancel", "delete", "stop"],
|
|
536
850
|
})) {
|
|
537
851
|
pi.events.emit(DEFER_CHANNEL, { name, keywords });
|
|
538
852
|
}
|
|
@@ -545,100 +859,23 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
545
859
|
},
|
|
546
860
|
});
|
|
547
861
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
clearTimeout(wakeup.timer);
|
|
557
|
-
wakeup = undefined;
|
|
558
|
-
}
|
|
559
|
-
};
|
|
560
|
-
|
|
561
|
-
registerLocalCommand(pi, "loop", {
|
|
562
|
-
description: "Repeat a task on a loop: `/loop 5m <task>` (fixed interval) or `/loop <task>` (self-paced). `/loop stop` ends it.",
|
|
563
|
-
handler: async (args, ctx) => {
|
|
564
|
-
lastCtx = ctx;
|
|
565
|
-
const raw = (args ?? "").trim();
|
|
566
|
-
const keyword = raw.toLowerCase();
|
|
567
|
-
|
|
568
|
-
if (keyword === "stop") {
|
|
569
|
-
const had = Boolean(loop || wakeup);
|
|
570
|
-
clearLoop();
|
|
571
|
-
clearWakeup();
|
|
572
|
-
if (activatedWakeupTool) {
|
|
573
|
-
const active = pi.getActiveTools();
|
|
574
|
-
if (active.includes("schedule_wakeup")) pi.setActiveTools(active.filter((t) => t !== "schedule_wakeup"));
|
|
575
|
-
activatedWakeupTool = false;
|
|
576
|
-
}
|
|
577
|
-
ctx.ui.notify(had ? "Loop stopped; no further iterations will fire." : "No loop was running.", "info");
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
if (raw === "" || keyword === "status") {
|
|
581
|
-
const parts: string[] = [];
|
|
582
|
-
if (loop) parts.push(`fixed interval every ${loop.intervalSeconds}s — ${loop.task}`);
|
|
583
|
-
if (wakeup) parts.push(`self-paced wakeup pending — ${wakeup.reason}`);
|
|
584
|
-
ctx.ui.notify(
|
|
585
|
-
parts.length
|
|
586
|
-
? `Running:\n- ${parts.join("\n- ")}\nStop with /loop stop.`
|
|
587
|
-
: "No loop running. Start with `/loop [interval] <task>`, e.g. `/loop 5m check the build`.",
|
|
588
|
-
"info",
|
|
589
|
-
);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const parsed = parseLoopArgs(raw);
|
|
594
|
-
if (!parsed.task) {
|
|
595
|
-
ctx.ui.notify("Give a task to loop, e.g. `/loop 10m check for new PRs` or `/loop watch the deploy`.", "info");
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// One loop at a time — replace any existing interval loop or pending wakeup.
|
|
600
|
-
clearLoop();
|
|
601
|
-
clearWakeup();
|
|
602
|
-
|
|
603
|
-
if (parsed.intervalSeconds !== undefined) {
|
|
604
|
-
// Fixed interval, harness-driven: re-arm automatically and fire the first tick now.
|
|
605
|
-
const seconds = clampDelaySeconds(parsed.intervalSeconds);
|
|
606
|
-
const adjusted =
|
|
607
|
-
seconds !== parsed.intervalSeconds
|
|
608
|
-
? ` (adjusted from ${parsed.intervalSeconds}s; allowed range ${MIN_DELAY_SECONDS}-${MAX_DELAY_SECONDS}s)`
|
|
609
|
-
: "";
|
|
610
|
-
// Skip (don't queue) a tick while the previous tick's turn is still
|
|
611
|
-
// running, so a slow task can't build a backlog of stale iterations.
|
|
612
|
-
const tick = () => {
|
|
613
|
-
if (agentBusy) return;
|
|
614
|
-
notify("loop", buildLoopMessage(parsed.task), { intervalSeconds: seconds });
|
|
615
|
-
};
|
|
616
|
-
const timer = setInterval(tick, seconds * 1000);
|
|
617
|
-
timer.unref?.();
|
|
618
|
-
loop = { timer, intervalSeconds: seconds, task: parsed.task };
|
|
619
|
-
ctx.ui.notify(`Looping every ${seconds}s${adjusted}: ${parsed.task}. Stop with /loop stop.`, "info");
|
|
620
|
-
tick(); // fire the first iteration now
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Dynamic (self-paced): the model drives schedule_wakeup, so make sure it is
|
|
625
|
-
// active — remembering we did, so /loop stop can restore the lean surface.
|
|
626
|
-
const active = pi.getActiveTools();
|
|
627
|
-
if (!active.includes("schedule_wakeup")) {
|
|
628
|
-
pi.setActiveTools([...active, "schedule_wakeup"]);
|
|
629
|
-
activatedWakeupTool = true;
|
|
630
|
-
}
|
|
631
|
-
ctx.ui.notify(`Self-paced loop started: ${parsed.task}. Stop with /loop stop.`, "info");
|
|
632
|
-
notify("loop-start", buildDynamicLoopPrompt(parsed.task), {});
|
|
633
|
-
},
|
|
862
|
+
// `/loop` is a bundled skill whose body depends on its arguments; build it
|
|
863
|
+
// for the skill extension (lib/skill-body.ts). Only our own SKILL.md: a
|
|
864
|
+
// project skill named "loop" keeps its file's body.
|
|
865
|
+
const loopSkillPath = join(BUNDLED_SKILLS_DIR, "loop", "SKILL.md");
|
|
866
|
+
pi.events.on(SKILL_BODY_CHANNEL, (data) => {
|
|
867
|
+
const query = data as SkillBodyQuery;
|
|
868
|
+
if (query.skill !== "loop" || resolve(query.path) !== loopSkillPath) return;
|
|
869
|
+
query.body = loopSkillPrompt(query.args, readLoopFile(query.cwd));
|
|
634
870
|
});
|
|
635
871
|
|
|
636
872
|
pi.on("session_start", (_event, ctx) => {
|
|
637
873
|
lastCtx = ctx;
|
|
874
|
+
sessionCwd = ctx.cwd;
|
|
638
875
|
if (pendingShutdownNotice) {
|
|
639
876
|
const notice = pendingShutdownNotice;
|
|
640
877
|
pendingShutdownNotice = undefined;
|
|
641
|
-
if (ctx.hasUI)
|
|
878
|
+
if (ctx.hasUI) queueNotice(pi.events, ctx, "info", notice);
|
|
642
879
|
}
|
|
643
880
|
});
|
|
644
881
|
pi.on("agent_start", () => {
|
|
@@ -646,22 +883,63 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
646
883
|
});
|
|
647
884
|
pi.on("agent_settled", () => {
|
|
648
885
|
agentBusy = false;
|
|
886
|
+
// Claude Code's keepalive: a wakeup's turn that scheduled no next one gets one fallback.
|
|
887
|
+
dynamicLoop.settle(Date.now());
|
|
888
|
+
runCron();
|
|
889
|
+
});
|
|
890
|
+
// Folded no-op ticks leave the model's context (noop-fold.ts); the note stands in for them.
|
|
891
|
+
pi.on("context", (event) => {
|
|
892
|
+
const messages = applyNoopFolds(event.messages as Array<AgentMessage & { role: string }>, (text, timestamp) => ({ role: "user", content: [{ type: "text", text }], timestamp }) as AgentMessage & { role: string });
|
|
893
|
+
return messages ? { messages } : undefined;
|
|
894
|
+
});
|
|
895
|
+
// The first fire after a compaction resends the full loop instructions, as in Claude Code.
|
|
896
|
+
pi.on("session_compact", () => {
|
|
897
|
+
loopDelivery.delete(MAIN_OWNER);
|
|
649
898
|
});
|
|
650
899
|
|
|
900
|
+
// A switch stops everything below, so ask first. pi can cancel a switch but
|
|
901
|
+
// not a quit; the widget line covers quitting (switch-guard.ts).
|
|
902
|
+
const confirmSwitch = async (ctx: ExtensionContext, action: string): Promise<{ cancel: true } | undefined> => {
|
|
903
|
+
if (!ctx.hasUI) return undefined;
|
|
904
|
+
const title = switchWarning(
|
|
905
|
+
registry.running().map((t) => ({ id: t.id, label: t.description })),
|
|
906
|
+
cron.list().map((j) => ({ id: j.id, label: describeCadence(j.cron) })),
|
|
907
|
+
action,
|
|
908
|
+
);
|
|
909
|
+
if (!title) return undefined;
|
|
910
|
+
const choice = await ctx.ui.select(title, [SWITCH_CANCEL, SWITCH_STOP]);
|
|
911
|
+
return choice === SWITCH_STOP ? undefined : { cancel: true };
|
|
912
|
+
};
|
|
913
|
+
pi.on("session_before_switch", (event, ctx) =>
|
|
914
|
+
confirmSwitch(ctx, event.reason === "resume" ? "Resuming another session" : "Starting a new session"),
|
|
915
|
+
);
|
|
916
|
+
pi.on("session_before_fork", (_event, ctx) => confirmSwitch(ctx, "Forking the session"));
|
|
917
|
+
|
|
651
918
|
pi.on("session_shutdown", (event) => {
|
|
652
919
|
// Fires on /clear, /new, /resume and /reload too (findings §8). Everything
|
|
653
920
|
// running dies with this instance — the replacement instance has no handle
|
|
654
921
|
// on it — and the replacement is told (see pendingShutdownNotice); a quit
|
|
655
922
|
// needs no note. A reload keeps the conversation, so its note says so.
|
|
656
923
|
const running = registry.running();
|
|
657
|
-
|
|
658
|
-
|
|
924
|
+
const jobs = cron.list();
|
|
925
|
+
if ((running.length > 0 || jobs.length > 0) && event.reason !== "quit") {
|
|
659
926
|
const when = event.reason === "reload" ? "on reload" : "with the previous session";
|
|
660
|
-
|
|
927
|
+
const notices: string[] = [];
|
|
928
|
+
if (running.length > 0) {
|
|
929
|
+
const names = running.map((t) => `${t.id} (${t.description})`).join(", ");
|
|
930
|
+
notices.push(`Stopped ${running.length} background task${running.length === 1 ? "" : "s"} ${when}: ${names}.`);
|
|
931
|
+
}
|
|
932
|
+
if (jobs.length > 0) {
|
|
933
|
+
const names = jobs.map((j) => `${j.id} (${describeCadence(j.cron)})`).join(", ");
|
|
934
|
+
notices.push(`Cancelled ${jobs.length} scheduled job${jobs.length === 1 ? "" : "s"} ${when}: ${names}.`);
|
|
935
|
+
}
|
|
936
|
+
pendingShutdownNotice = notices.join(" ");
|
|
661
937
|
}
|
|
662
938
|
registry.stopAll();
|
|
663
|
-
|
|
664
|
-
|
|
939
|
+
cron.clear();
|
|
940
|
+
dynamicLoop.clear();
|
|
941
|
+
if (cronTimer) clearTimeout(cronTimer);
|
|
942
|
+
cronTimer = undefined;
|
|
665
943
|
lastCtx = undefined;
|
|
666
944
|
});
|
|
667
945
|
}
|