@gajae-code/coding-agent 0.11.1 → 0.11.3
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/CHANGELOG.md +64 -0
- package/dist/types/cli/config-cli.d.ts +11 -1
- package/dist/types/cli/launch-disposition.d.ts +23 -0
- package/dist/types/cli/notify-cli.d.ts +1 -0
- package/dist/types/cli/session-picker.d.ts +2 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/commands/mcp-serve.d.ts +33 -0
- package/dist/types/commands/mcp.d.ts +1 -1
- package/dist/types/commands/notify.d.ts +0 -2
- package/dist/types/commands/sdk.d.ts +1 -1
- package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
- package/dist/types/config/file-lock.d.ts +14 -4
- package/dist/types/config/keybindings.d.ts +42 -1
- package/dist/types/config/model-auth.d.ts +3 -0
- package/dist/types/config/model-bindings-applier.d.ts +13 -0
- package/dist/types/config/model-discovery-manager.d.ts +49 -0
- package/dist/types/config/model-equivalence.d.ts +20 -0
- package/dist/types/config/model-registry.d.ts +16 -15
- package/dist/types/config/model-resolver.d.ts +28 -18
- package/dist/types/config/models-config-schema.d.ts +11 -0
- package/dist/types/config/settings-schema.d.ts +141 -11
- package/dist/types/config/settings.d.ts +14 -11
- package/dist/types/coordinator-mcp/policy.d.ts +5 -0
- package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/discovery/helpers.d.ts +0 -6
- package/dist/types/edit/index.d.ts +3 -0
- package/dist/types/eval/py/env.d.ts +9 -0
- package/dist/types/extensibility/extensions/loader.d.ts +2 -1
- package/dist/types/extensibility/extensions/runner.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +34 -3
- package/dist/types/extensibility/hooks/runner.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
- package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
- package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
- package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
- package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
- package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
- package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
- package/dist/types/gjc-runtime/team-store.d.ts +160 -0
- package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
- package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
- package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/goals/state.d.ts +8 -0
- package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
- package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
- package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/state.d.ts +11 -4
- package/dist/types/lsp/lspmux.d.ts +5 -3
- package/dist/types/lsp/path-trust.d.ts +1 -0
- package/dist/types/main.d.ts +4 -2
- package/dist/types/memories/storage.d.ts +1 -0
- package/dist/types/modes/action-registry.d.ts +29 -0
- package/dist/types/modes/components/chrome.d.ts +14 -0
- package/dist/types/modes/components/command-palette.d.ts +27 -0
- package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
- package/dist/types/modes/components/index.d.ts +2 -1
- package/dist/types/modes/components/pet-selector.d.ts +1 -1
- package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
- package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
- package/dist/types/modes/components/queue-pane.d.ts +12 -0
- package/dist/types/modes/components/read-tool-group.d.ts +6 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
- package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
- package/dist/types/modes/components/settings-selector.d.ts +1 -1
- package/dist/types/modes/components/show-images-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +1 -94
- package/dist/types/modes/components/tasks-pane.d.ts +15 -0
- package/dist/types/modes/components/theme-selector.d.ts +1 -1
- package/dist/types/modes/components/tool-execution.d.ts +15 -0
- package/dist/types/modes/components/tool-status-header.d.ts +111 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
- package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
- package/dist/types/modes/controllers/input-controller.d.ts +8 -1
- package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
- package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
- package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
- package/dist/types/modes/interactive-mode.d.ts +19 -17
- package/dist/types/modes/jobs-observer.d.ts +2 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
- package/dist/types/modes/tasks-aggregator.d.ts +39 -0
- package/dist/types/modes/transcript-item-registry.d.ts +60 -0
- package/dist/types/modes/types.d.ts +20 -13
- package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +78 -12
- package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
- package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
- package/dist/types/sdk/broker/session-index.d.ts +0 -1
- package/dist/types/sdk/broker/state-version.d.ts +2 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
- package/dist/types/sdk/bus/config-commands.d.ts +1 -1
- package/dist/types/sdk/bus/config.d.ts +11 -0
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/html-format.d.ts +3 -2
- package/dist/types/sdk/bus/index.d.ts +29 -0
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/notification-service.d.ts +20 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
- package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
- package/dist/types/sdk/bus/rich-document.d.ts +7 -0
- package/dist/types/sdk/bus/rich-render.d.ts +18 -41
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
- package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/session-directory.d.ts +56 -0
- package/dist/types/sdk/session.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +82 -47
- package/dist/types/session/agent-storage.d.ts +7 -0
- package/dist/types/session/fallback-chain-controller.d.ts +10 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
- package/dist/types/session/messages.d.ts +16 -0
- package/dist/types/session/session-manager-internal.d.ts +14 -0
- package/dist/types/session/session-manager.d.ts +82 -12
- package/dist/types/session/session-storage.d.ts +43 -3
- package/dist/types/session/volatile-context-pruning.d.ts +15 -0
- package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
- package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
- package/dist/types/system-prompt.d.ts +2 -11
- package/dist/types/task/executor.d.ts +3 -1
- package/dist/types/thinking.d.ts +7 -0
- package/dist/types/tool-discovery/tool-index.d.ts +1 -50
- package/dist/types/tools/ask.d.ts +64 -2
- package/dist/types/tools/bash.d.ts +3 -0
- package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
- package/dist/types/tools/cron.d.ts +2 -0
- package/dist/types/tools/fetch.d.ts +1 -0
- package/dist/types/tools/index.d.ts +30 -15
- package/dist/types/tools/output-meta.d.ts +1 -0
- package/dist/types/tools/read.d.ts +5 -0
- package/dist/types/tools/search-tool-bm25.d.ts +3 -4
- package/dist/types/tools/subagent.d.ts +1 -0
- package/dist/types/utils/event-bus.d.ts +1 -1
- package/dist/types/utils/file-mentions.d.ts +3 -3
- package/dist/types/web/search/index.d.ts +3 -1
- package/package.json +21 -9
- package/scripts/build-sdk-package-smoke.ts +34 -1
- package/scripts/generate-hotkeys-docs.ts +74 -0
- package/scripts/generate-sdk-operation-inventory.ts +64 -20
- package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
- package/src/async/job-manager.ts +49 -6
- package/src/cli/config-cli.ts +75 -2
- package/src/cli/launch-disposition.ts +42 -0
- package/src/cli/notify-cli.ts +92 -26
- package/src/cli/session-picker.ts +20 -3
- package/src/cli/setup-cli.ts +1 -1
- package/src/cli.ts +1 -1
- package/src/commands/config.ts +1 -1
- package/src/commands/harness.ts +34 -106
- package/src/commands/launch.ts +6 -4
- package/src/commands/mcp-serve.ts +85 -8
- package/src/commands/mcp.ts +2 -2
- package/src/commands/notify.ts +8 -4
- package/src/commands/sdk.ts +61 -40
- package/src/commands/ultragoal.ts +4 -1
- package/src/config/atomic-yaml-patch.ts +80 -13
- package/src/config/file-lock.ts +105 -43
- package/src/config/keybindings.ts +64 -20
- package/src/config/model-auth.ts +9 -0
- package/src/config/model-bindings-applier.ts +139 -0
- package/src/config/model-discovery-manager.ts +224 -0
- package/src/config/model-equivalence.ts +66 -0
- package/src/config/model-registry.ts +169 -302
- package/src/config/model-resolver.ts +361 -369
- package/src/config/models-config-schema.ts +5 -4
- package/src/config/settings-schema.ts +214 -12
- package/src/config/settings.ts +458 -482
- package/src/coordinator-mcp/policy.ts +76 -11
- package/src/coordinator-mcp/question-gate-codec.ts +274 -0
- package/src/coordinator-mcp/question-state.ts +778 -0
- package/src/coordinator-mcp/server.ts +1702 -238
- package/src/debug/index.ts +1 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
- package/src/defaults/gjc/skills/team/SKILL.md +16 -0
- package/src/discovery/helpers.ts +1 -50
- package/src/edit/index.ts +64 -0
- package/src/eval/py/env.ts +26 -0
- package/src/eval/py/kernel.ts +5 -3
- package/src/extensibility/extensions/loader.ts +9 -1
- package/src/extensibility/extensions/runner.ts +12 -3
- package/src/extensibility/extensions/types.ts +39 -1
- package/src/extensibility/gjc-plugins/installer.ts +16 -16
- package/src/extensibility/hooks/runner.ts +2 -2
- package/src/extensibility/runtime-skill-discovery.ts +33 -13
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
- package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
- package/src/gjc-runtime/deep-interview-state.ts +317 -5
- package/src/gjc-runtime/goal-mode-request.ts +55 -8
- package/src/gjc-runtime/launch-tmux.ts +2 -5
- package/src/gjc-runtime/launch-worktree.ts +5 -5
- package/src/gjc-runtime/linux-proc.ts +71 -0
- package/src/gjc-runtime/ralplan-runtime.ts +74 -90
- package/src/gjc-runtime/session-resolution.ts +18 -3
- package/src/gjc-runtime/session-state-sidecar.ts +6 -107
- package/src/gjc-runtime/state-migrations.ts +1 -1
- package/src/gjc-runtime/state-runtime.ts +327 -249
- package/src/gjc-runtime/state-writer.ts +36 -40
- package/src/gjc-runtime/team-launch.ts +319 -0
- package/src/gjc-runtime/team-notify.ts +326 -0
- package/src/gjc-runtime/team-runtime.ts +1099 -1482
- package/src/gjc-runtime/team-store.ts +828 -0
- package/src/gjc-runtime/team-workers.ts +962 -0
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
- package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
- package/src/gjc-runtime/tmux-sessions.ts +4 -21
- package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
- package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
- package/src/gjc-runtime/ultragoal-guard.ts +29 -5
- package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
- package/src/gjc-runtime/workflow-cli-common.ts +30 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +18 -0
- package/src/goals/continuation-timeout-guard.ts +79 -0
- package/src/goals/runtime.ts +7 -5
- package/src/goals/state.ts +18 -0
- package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
- package/src/harness-control-plane/phase-rollup.ts +4 -7
- package/src/harness-control-plane/receipt-ingest.ts +5 -0
- package/src/harness-control-plane/receipt-spool.ts +51 -7
- package/src/harness-control-plane/session-lease.ts +12 -130
- package/src/hindsight/backend.ts +26 -27
- package/src/hindsight/state.ts +101 -32
- package/src/hooks/skill-state.ts +17 -11
- package/src/internal-urls/docs-index.generated.ts +21 -18
- package/src/lsp/client.ts +1 -1
- package/src/lsp/config.ts +145 -26
- package/src/lsp/index.ts +1 -1
- package/src/lsp/lspmux.ts +39 -10
- package/src/lsp/path-trust.ts +81 -0
- package/src/main.ts +127 -30
- package/src/memories/index.ts +1 -0
- package/src/memories/storage.ts +6 -2
- package/src/modes/action-registry.ts +165 -0
- package/src/modes/components/chrome.ts +40 -0
- package/src/modes/components/command-palette.ts +168 -0
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/custom-provider-wizard.ts +51 -14
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/pet-selector.ts +11 -17
- package/src/modes/components/plan-preview-overlay.ts +295 -0
- package/src/modes/components/queue-mode-selector.ts +10 -27
- package/src/modes/components/queue-pane.ts +79 -0
- package/src/modes/components/read-tool-group.ts +13 -0
- package/src/modes/components/session-observer-overlay.ts +207 -772
- package/src/modes/components/sessions-dashboard.ts +181 -0
- package/src/modes/components/settings-selector.ts +1 -1
- package/src/modes/components/show-images-selector.ts +10 -24
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +1 -969
- package/src/modes/components/tasks-pane.ts +73 -0
- package/src/modes/components/theme-selector.ts +11 -31
- package/src/modes/components/tool-execution.ts +22 -0
- package/src/modes/components/tool-status-header.ts +1056 -0
- package/src/modes/components/transcript-viewer-overlay.ts +384 -0
- package/src/modes/controllers/btw-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +24 -1
- package/src/modes/controllers/goal-mode-controller.ts +474 -0
- package/src/modes/controllers/input-controller.ts +764 -229
- package/src/modes/controllers/mode-gate.ts +24 -0
- package/src/modes/controllers/plan-mode-controller.ts +517 -0
- package/src/modes/controllers/selector-controller.ts +225 -10
- package/src/modes/controllers/stt-controller.ts +125 -0
- package/src/modes/interactive-mode.ts +289 -1150
- package/src/modes/jobs-observer.ts +3 -0
- package/src/modes/prompt-action-autocomplete.ts +7 -0
- package/src/modes/runtime-init.ts +8 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
- package/src/modes/tasks-aggregator.ts +199 -0
- package/src/modes/transcript-item-registry.ts +229 -0
- package/src/modes/types.ts +45 -18
- package/src/modes/utils/ui-helpers.ts +17 -9
- package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
- package/src/prompts/agent-fragments/restricted-bash.md +1 -0
- package/src/prompts/agents/architect.md +2 -8
- package/src/prompts/agents/critic.md +2 -8
- package/src/prompts/agents/executor.md +3 -0
- package/src/prompts/agents/planner.md +4 -12
- package/src/prompts/system/custom-system-prompt.md +1 -23
- package/src/prompts/system/plan-mode-approved.md +8 -0
- package/src/prompts/system/system-prompt.md +17 -96
- package/src/prompts/tools/bash.md +4 -4
- package/src/prompts/tools/browser.md +15 -52
- package/src/prompts/tools/find.md +2 -2
- package/src/prompts/tools/hashline.md +13 -55
- package/src/prompts/tools/read.md +3 -6
- package/src/prompts/tools/search-tool-bm25.md +0 -2
- package/src/prompts/tools/search.md +2 -4
- package/src/prompts/tools/skill-discovery.md +1 -2
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-mcp/manager.ts +1 -1
- package/src/sdk/broker/broker.ts +206 -78
- package/src/sdk/broker/discovery.ts +1 -1
- package/src/sdk/broker/ensure.ts +3 -1
- package/src/sdk/broker/lifecycle-codec.ts +11 -0
- package/src/sdk/broker/lifecycle-ledger.ts +371 -74
- package/src/sdk/broker/lifecycle.ts +1399 -142
- package/src/sdk/broker/process-incarnation.ts +5 -13
- package/src/sdk/broker/session-index.ts +145 -56
- package/src/sdk/broker/state-version.ts +21 -0
- package/src/sdk/broker/transport.ts +1 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -0
- package/src/sdk/bus/chat-daemon-control.ts +8 -28
- package/src/sdk/bus/chat-effect-journal.ts +10 -2
- package/src/sdk/bus/config-commands.ts +5 -4
- package/src/sdk/bus/config.ts +98 -0
- package/src/sdk/bus/conversation-store.ts +5 -29
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/html-format.ts +20 -3
- package/src/sdk/bus/index.ts +782 -94
- package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
- package/src/sdk/bus/natives-augment.d.ts +26 -1
- package/src/sdk/bus/notification-service.ts +168 -20
- package/src/sdk/bus/rate-limit-pool.ts +58 -14
- package/src/sdk/bus/recent-activity.ts +188 -74
- package/src/sdk/bus/rich-document.ts +258 -0
- package/src/sdk/bus/rich-render.ts +62 -70
- package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
- package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
- package/src/sdk/bus/telegram-daemon.ts +1389 -237
- package/src/sdk/bus/threaded-inbound.ts +8 -3
- package/src/sdk/bus/threaded-render.ts +57 -1
- package/src/sdk/bus/topic-registry.ts +120 -13
- package/src/sdk/host/control/dispatch.ts +14 -2
- package/src/sdk/host/control/operations.ts +6 -1
- package/src/sdk/host/host.ts +44 -2
- package/src/sdk/host/query/handlers.ts +20 -22
- package/src/sdk/host/query/revision-store.ts +80 -57
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
- package/src/sdk/session-directory.ts +141 -0
- package/src/sdk/session.ts +462 -480
- package/src/session/agent-session.ts +1700 -583
- package/src/session/agent-storage.ts +18 -0
- package/src/session/cache-economics.ts +25 -8
- package/src/session/fallback-chain-controller.ts +53 -3
- package/src/session/internal/managed-session-scope.ts +2226 -0
- package/src/session/internal/managed-session-storage.ts +453 -0
- package/src/session/messages.ts +91 -2
- package/src/session/session-manager-internal.ts +33 -0
- package/src/session/session-manager.ts +1112 -333
- package/src/session/session-storage.ts +444 -39
- package/src/session/volatile-context-pruning.ts +78 -0
- package/src/setup/credential-auto-import.ts +2 -1
- package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
- package/src/setup/provider-onboarding.ts +36 -61
- package/src/setup/provider-presets.json +5 -0
- package/src/skill-state/active-state.ts +3 -13
- package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
- package/src/skill-state/workflow-state-contract.ts +6 -41
- package/src/slash-commands/builtin-registry.ts +46 -11
- package/src/system-prompt.ts +37 -44
- package/src/task/agents.ts +19 -1
- package/src/task/executor.ts +90 -6
- package/src/task/index.ts +36 -21
- package/src/thinking.ts +21 -6
- package/src/tool-discovery/tool-index.ts +2 -134
- package/src/tools/ask.ts +157 -21
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/bash.ts +38 -0
- package/src/tools/browser/dead-tab-recovery.ts +78 -0
- package/src/tools/browser/tab-supervisor.ts +282 -36
- package/src/tools/computer.ts +10 -3
- package/src/tools/cron.ts +30 -16
- package/src/tools/eval.ts +1 -2
- package/src/tools/fetch.ts +11 -4
- package/src/tools/index.ts +76 -24
- package/src/tools/output-meta.ts +19 -7
- package/src/tools/read.ts +219 -64
- package/src/tools/resource-gc.ts +2 -2
- package/src/tools/search-tool-bm25.ts +11 -74
- package/src/tools/subagent.ts +43 -2
- package/src/utils/event-bus.ts +9 -4
- package/src/utils/file-mentions.ts +4 -4
- package/src/web/search/index.ts +3 -3
- package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
- package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
- package/src/discovery/plugin-dir-roots.ts +0 -28
- package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
|
@@ -5,6 +5,13 @@ import type { ToolSession } from "../../sdk";
|
|
|
5
5
|
import { expandPath } from "../path-utils";
|
|
6
6
|
import { ToolAbortError, ToolError } from "../tool-errors";
|
|
7
7
|
import { pickElectronTarget } from "./attach";
|
|
8
|
+
import {
|
|
9
|
+
consumeDeadTabRecovery,
|
|
10
|
+
discardDeadTabRecovery,
|
|
11
|
+
isDeadTabRecoveryLive,
|
|
12
|
+
peekDeadTabRecovery,
|
|
13
|
+
registerDeadTabRecovery,
|
|
14
|
+
} from "./dead-tab-recovery";
|
|
8
15
|
import { type BrowserHandle, type BrowserKindTag, holdBrowser, releaseBrowser } from "./registry";
|
|
9
16
|
import type {
|
|
10
17
|
ReadyInfo,
|
|
@@ -55,6 +62,8 @@ export interface TabSession {
|
|
|
55
62
|
pending: Map<string, PendingRun>;
|
|
56
63
|
dialogPolicy?: DialogPolicy;
|
|
57
64
|
kindTag: BrowserKindTag;
|
|
65
|
+
/** Immutable inputs needed to recreate this worker after an unexpected exit. */
|
|
66
|
+
recoveryOpts: AcquireTabOptions;
|
|
58
67
|
/** Session that acquired this tab; used for session-scoped teardown (F13). */
|
|
59
68
|
ownerId?: string;
|
|
60
69
|
/** Unix-ms timestamp of the last acquire/run activity; drives GC idle + LRU ordering. */
|
|
@@ -73,6 +82,10 @@ export interface AcquireTabOptions {
|
|
|
73
82
|
dialogs?: DialogPolicy;
|
|
74
83
|
/** Owning session id so dispose can release only this session's tabs (F13). */
|
|
75
84
|
ownerId?: string;
|
|
85
|
+
/** Internal recovery-only target id; makes a headless replacement attach, not open a page. */
|
|
86
|
+
recoveryTargetId?: string;
|
|
87
|
+
/** Recovery fence: replacement must not overwrite a tab installed after dead teardown. */
|
|
88
|
+
requireVacantName?: boolean;
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
export interface AcquireTabResult {
|
|
@@ -98,8 +111,20 @@ export interface ReleaseTabOptions {
|
|
|
98
111
|
}
|
|
99
112
|
|
|
100
113
|
const tabs = new Map<string, TabSession>();
|
|
114
|
+
const recoveringTabs = new Map<string, { ownerId: string | undefined; promise: Promise<TabSession> }>();
|
|
115
|
+
let afterWorkerInitializationForTest: ((name: string) => void) | undefined;
|
|
116
|
+
let workerFactoryForTest: (() => Promise<WorkerHandle>) | undefined;
|
|
117
|
+
let workerInitializerForTest:
|
|
118
|
+
| ((worker: WorkerHandle, payload: WorkerInitPayload, timeoutMs: number) => Promise<ReadyInfo>)
|
|
119
|
+
| undefined;
|
|
101
120
|
const GRACE_MS = 750;
|
|
102
121
|
|
|
122
|
+
interface DeadTabDescriptor {
|
|
123
|
+
tab: TabSession;
|
|
124
|
+
browser: BrowserHandle;
|
|
125
|
+
opts: AcquireTabOptions;
|
|
126
|
+
}
|
|
127
|
+
|
|
103
128
|
/**
|
|
104
129
|
* Remaining time (ms) until an absolute teardown deadline, or +Infinity when no deadline
|
|
105
130
|
* is set (the GC / session-teardown callers). A non-positive result means the shared
|
|
@@ -156,6 +181,46 @@ function beginRelease(tab: TabSession): boolean {
|
|
|
156
181
|
return true;
|
|
157
182
|
}
|
|
158
183
|
|
|
184
|
+
async function withTemporaryBrowserHold<T>(browser: BrowserHandle, work: () => Promise<T>): Promise<T> {
|
|
185
|
+
holdBrowser(browser);
|
|
186
|
+
try {
|
|
187
|
+
return await work();
|
|
188
|
+
} finally {
|
|
189
|
+
await releaseBrowser(browser, { kill: false });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Test-only: exercise the same hold-transfer primitive used by replacement recovery. */
|
|
194
|
+
export async function withTemporaryBrowserHoldForTest<T>(browser: BrowserHandle, work: () => Promise<T>): Promise<T> {
|
|
195
|
+
return await withTemporaryBrowserHold(browser, work);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Test-only: close exactly the target belonging to the supplied dead session. */
|
|
199
|
+
export async function closeOrphanTargetForTest(tab: TabSession): Promise<void> {
|
|
200
|
+
await closeOrphanTarget(tab);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function recoveryPromiseForOwner(name: string, ownerId: string | undefined): Promise<TabSession> | undefined {
|
|
204
|
+
const existing = recoveringTabs.get(name);
|
|
205
|
+
if (!existing || existing.ownerId !== ownerId) return undefined;
|
|
206
|
+
return existing.promise;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Test-only: install an in-flight recovery and observe owner isolation. */
|
|
210
|
+
export function __setRecoveringTabForTest(
|
|
211
|
+
name: string,
|
|
212
|
+
ownerId: string | undefined,
|
|
213
|
+
promise: Promise<TabSession>,
|
|
214
|
+
): void {
|
|
215
|
+
recoveringTabs.set(name, { ownerId, promise });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function recoveryPromiseForOwnerForTest(
|
|
219
|
+
name: string,
|
|
220
|
+
ownerId: string | undefined,
|
|
221
|
+
): Promise<TabSession> | undefined {
|
|
222
|
+
return recoveryPromiseForOwner(name, ownerId);
|
|
223
|
+
}
|
|
159
224
|
/** Read-only, GC-facing projection of a live tab. Never exposes the mutable `tabs` map. */
|
|
160
225
|
export interface TabGcSnapshot {
|
|
161
226
|
name: string;
|
|
@@ -193,14 +258,14 @@ export function listTabsForGc(): TabGcSnapshot[] {
|
|
|
193
258
|
*/
|
|
194
259
|
export async function releaseTabIfGcEligible(name: string, policy: BrowserGcEligibilityPolicy): Promise<boolean> {
|
|
195
260
|
const tab = tabs.get(name);
|
|
196
|
-
if (!tab) return false;
|
|
197
|
-
if (tab.
|
|
198
|
-
|
|
261
|
+
if (!tab || tab.releasing) return false;
|
|
262
|
+
if (tab.state === "dead") {
|
|
263
|
+
if (recoveringTabs.has(name) || isDeadTabRecoveryLive(name, policy.now())) return false;
|
|
264
|
+
return await releaseDeadTabForRecovery(name, tab, tab.ownerId);
|
|
265
|
+
}
|
|
199
266
|
if (tab.pending.size !== 0) return false;
|
|
200
267
|
if (tab.kindTag !== "headless" && tab.kindTag !== "spawned") return false;
|
|
201
268
|
if (policy.now() - tab.lastUsedAt <= policy.idleMs) return false;
|
|
202
|
-
// All predicates passed synchronously; releaseTab applies the shared begin-release guard
|
|
203
|
-
// on the same microtask, so no other code runs between the check and the state transition.
|
|
204
269
|
return await releaseTab(name, { kill: false });
|
|
205
270
|
}
|
|
206
271
|
|
|
@@ -212,6 +277,36 @@ export function setTabForTest(tab: TabSession): void {
|
|
|
212
277
|
/** Test-only: clear the tab registry between cases. */
|
|
213
278
|
export function clearTabsForTest(): void {
|
|
214
279
|
tabs.clear();
|
|
280
|
+
recoveringTabs.clear();
|
|
281
|
+
afterWorkerInitializationForTest = undefined;
|
|
282
|
+
workerFactoryForTest = undefined;
|
|
283
|
+
workerInitializerForTest = undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function __setAfterWorkerInitializationForTest(callback: ((name: string) => void) | undefined): void {
|
|
287
|
+
afterWorkerInitializationForTest = callback;
|
|
288
|
+
}
|
|
289
|
+
export function __setAcquireTabWorkerDepsForTest(
|
|
290
|
+
factory: (() => Promise<WorkerHandle>) | undefined,
|
|
291
|
+
initializer:
|
|
292
|
+
| ((worker: WorkerHandle, payload: WorkerInitPayload, timeoutMs: number) => Promise<ReadyInfo>)
|
|
293
|
+
| undefined,
|
|
294
|
+
): void {
|
|
295
|
+
workerFactoryForTest = factory;
|
|
296
|
+
workerInitializerForTest = initializer;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function initializeAcquireWorker(
|
|
300
|
+
worker: WorkerHandle,
|
|
301
|
+
payload: WorkerInitPayload,
|
|
302
|
+
timeoutMs: number,
|
|
303
|
+
): Promise<ReadyInfo> {
|
|
304
|
+
return await (workerInitializerForTest?.(worker, payload, timeoutMs) ??
|
|
305
|
+
initializeTabWorker(worker, payload, timeoutMs));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function spawnAcquireWorker(): Promise<WorkerHandle> {
|
|
309
|
+
return await (workerFactoryForTest?.() ?? spawnTabWorker());
|
|
215
310
|
}
|
|
216
311
|
|
|
217
312
|
export async function acquireTab(
|
|
@@ -220,6 +315,9 @@ export async function acquireTab(
|
|
|
220
315
|
opts: AcquireTabOptions,
|
|
221
316
|
): Promise<AcquireTabResult> {
|
|
222
317
|
const existing = tabs.get(name);
|
|
318
|
+
if (opts.requireVacantName && existing)
|
|
319
|
+
throw new ToolError(`Tab ${JSON.stringify(name)} was replaced during recovery.`);
|
|
320
|
+
let replacementHold = false;
|
|
223
321
|
if (existing) {
|
|
224
322
|
if (existing.browser === browser && existing.state === "alive") {
|
|
225
323
|
if (opts.dialogs !== undefined && opts.dialogs !== existing.dialogPolicy) {
|
|
@@ -240,33 +338,50 @@ export async function acquireTab(
|
|
|
240
338
|
return { tab: tabs.get(name)!, created: false };
|
|
241
339
|
}
|
|
242
340
|
} else {
|
|
243
|
-
|
|
341
|
+
if (existing.browser === browser) {
|
|
342
|
+
holdBrowser(browser);
|
|
343
|
+
replacementHold = true;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
await releaseTab(name, { kill: false });
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (replacementHold) await releaseBrowser(browser, { kill: false });
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
244
351
|
}
|
|
245
352
|
}
|
|
246
353
|
|
|
247
|
-
|
|
248
|
-
|
|
354
|
+
let initPayload: WorkerInitPayload;
|
|
355
|
+
try {
|
|
356
|
+
initPayload = await buildInitPayload(browser, opts);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
if (replacementHold) await releaseBrowser(browser, { kill: false });
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
let worker = await spawnAcquireWorker();
|
|
249
362
|
let info: ReadyInfo;
|
|
250
363
|
try {
|
|
251
|
-
info = await
|
|
364
|
+
info = await initializeAcquireWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
|
|
252
365
|
} catch (error) {
|
|
253
366
|
// `BuildMessage`-class failures arrive asynchronously via the worker's `error` event,
|
|
254
367
|
// after `spawnTabWorker`'s synchronous try/catch has already returned. Fall back to
|
|
255
368
|
// the inline worker here so module-resolution failures don't poison every tab open.
|
|
256
369
|
await worker.terminate().catch(() => undefined);
|
|
257
370
|
if (worker.mode === "inline") {
|
|
258
|
-
if (
|
|
371
|
+
if (replacementHold) await releaseBrowser(browser, { kill: false });
|
|
372
|
+
else if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
|
|
259
373
|
throw error;
|
|
260
374
|
}
|
|
261
375
|
logger.warn("Tab worker init failed; retrying with inline tab worker (no sync-loop guard)", {
|
|
262
376
|
error: error instanceof Error ? error.message : String(error),
|
|
263
377
|
});
|
|
264
|
-
worker = await spawnInlineWorker();
|
|
378
|
+
worker = await (workerFactoryForTest?.() ?? spawnInlineWorker());
|
|
265
379
|
try {
|
|
266
|
-
info = await
|
|
380
|
+
info = await initializeAcquireWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
|
|
267
381
|
} catch (inlineError) {
|
|
268
382
|
await worker.terminate().catch(() => undefined);
|
|
269
|
-
if (
|
|
383
|
+
if (replacementHold) await releaseBrowser(browser, { kill: false });
|
|
384
|
+
else if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
|
|
270
385
|
const finalError = new ToolError(
|
|
271
386
|
`Failed to start browser tab worker (inline fallback also failed): ${inlineError instanceof Error ? inlineError.message : String(inlineError)}`,
|
|
272
387
|
);
|
|
@@ -275,7 +390,14 @@ export async function acquireTab(
|
|
|
275
390
|
}
|
|
276
391
|
}
|
|
277
392
|
|
|
393
|
+
afterWorkerInitializationForTest?.(name);
|
|
394
|
+
if (opts.requireVacantName && tabs.has(name)) {
|
|
395
|
+
await worker.terminate().catch(() => undefined);
|
|
396
|
+
if (replacementHold) await releaseBrowser(browser, { kill: false });
|
|
397
|
+
throw new ToolError(`Tab ${JSON.stringify(name)} was replaced during recovery.`);
|
|
398
|
+
}
|
|
278
399
|
holdBrowser(browser);
|
|
400
|
+
if (replacementHold) await releaseBrowser(browser, { kill: false });
|
|
279
401
|
const tab: TabSession = {
|
|
280
402
|
name,
|
|
281
403
|
browser,
|
|
@@ -288,8 +410,10 @@ export async function acquireTab(
|
|
|
288
410
|
kindTag: browser.kind.kind,
|
|
289
411
|
ownerId: opts.ownerId,
|
|
290
412
|
lastUsedAt: Date.now(),
|
|
413
|
+
recoveryOpts: freezeRecoveryOptions(opts),
|
|
291
414
|
};
|
|
292
415
|
worker.onMessage(msg => handleTabMessage(tab, msg));
|
|
416
|
+
worker.onError(error => markTabDead(tab, error));
|
|
293
417
|
tabs.set(name, tab);
|
|
294
418
|
return { tab, created: true };
|
|
295
419
|
}
|
|
@@ -307,8 +431,9 @@ async function runInTabWithSnapshot(
|
|
|
307
431
|
opts: { code: string; timeoutMs: number; signal?: AbortSignal; session?: ToolSession },
|
|
308
432
|
snapshot: SessionSnapshot,
|
|
309
433
|
): Promise<RunResultOk> {
|
|
310
|
-
|
|
311
|
-
if (
|
|
434
|
+
let tab = tabs.get(name);
|
|
435
|
+
if (tab?.state === "dead") tab = await recoverDeadTab(name, opts.session);
|
|
436
|
+
if (tab?.state !== "alive") throw new ToolError(`Tab ${JSON.stringify(name)} is not alive. Reopen it.`);
|
|
312
437
|
if (tab.pending.size > 0) throw new ToolError(`Tab ${JSON.stringify(name)} is busy`);
|
|
313
438
|
tab.lastUsedAt = Date.now();
|
|
314
439
|
const id = Snowflake.next();
|
|
@@ -340,6 +465,82 @@ async function runInTabWithSnapshot(
|
|
|
340
465
|
tab.pending.delete(id);
|
|
341
466
|
}
|
|
342
467
|
}
|
|
468
|
+
function markTabDead(tab: TabSession, error: Error): void {
|
|
469
|
+
if (tabs.get(tab.name) !== tab || tab.state !== "alive" || tab.releasing) return;
|
|
470
|
+
tab.state = "dead";
|
|
471
|
+
const closeError = new ToolError(`Browser tab worker stopped: ${error.message}`);
|
|
472
|
+
for (const pending of tab.pending.values()) pending.reject(closeError);
|
|
473
|
+
tab.pending.clear();
|
|
474
|
+
registerDeadTabRecovery(tab.name, tab.ownerId, Object.freeze({ tab, browser: tab.browser, opts: tab.recoveryOpts }));
|
|
475
|
+
}
|
|
476
|
+
function freezeRecoveryOptions(opts: AcquireTabOptions): AcquireTabOptions {
|
|
477
|
+
return Object.freeze({
|
|
478
|
+
...opts,
|
|
479
|
+
signal: undefined,
|
|
480
|
+
viewport: opts.viewport ? Object.freeze({ ...opts.viewport }) : undefined,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function recoverDeadTab(name: string, session: ToolSession | undefined): Promise<TabSession | undefined> {
|
|
485
|
+
const ownerId = session?.getSessionId?.() ?? undefined;
|
|
486
|
+
const existing = recoveryPromiseForOwner(name, ownerId);
|
|
487
|
+
if (existing) return await existing;
|
|
488
|
+
if (recoveringTabs.has(name)) return undefined;
|
|
489
|
+
const peeked = peekDeadTabRecovery<DeadTabDescriptor>(name, ownerId);
|
|
490
|
+
if (peeked.status !== "consumed" || !peeked.descriptor) return undefined;
|
|
491
|
+
const descriptor = peeked.descriptor;
|
|
492
|
+
const consumed = consumeDeadTabRecovery<DeadTabDescriptor>(name, ownerId);
|
|
493
|
+
if (consumed.status === "owner_mismatch") return undefined;
|
|
494
|
+
if (consumed.status === "expired_or_missing") {
|
|
495
|
+
await releaseDeadTabForRecovery(name, descriptor.tab, ownerId);
|
|
496
|
+
return undefined;
|
|
497
|
+
}
|
|
498
|
+
const recovery = withTemporaryBrowserHold(descriptor.browser, async (): Promise<TabSession> => {
|
|
499
|
+
if (!(await releaseDeadTabForRecovery(name, descriptor.tab, ownerId, true))) {
|
|
500
|
+
throw new ToolError(`Tab ${JSON.stringify(name)} is not alive. Reopen it.`);
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
return (
|
|
504
|
+
await acquireTab(name, descriptor.browser, {
|
|
505
|
+
...descriptor.opts,
|
|
506
|
+
recoveryTargetId: descriptor.tab.targetId,
|
|
507
|
+
requireVacantName: true,
|
|
508
|
+
})
|
|
509
|
+
).tab;
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if (descriptor.tab.kindTag === "headless") await closeOrphanTarget(descriptor.tab).catch(() => undefined);
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
recoveringTabs.set(name, { ownerId, promise: recovery });
|
|
516
|
+
try {
|
|
517
|
+
return await recovery;
|
|
518
|
+
} finally {
|
|
519
|
+
if (recoveringTabs.get(name)?.promise === recovery) recoveringTabs.delete(name);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Claim and release exactly the dead tab that produced a recovery descriptor. Every
|
|
525
|
+
* predicate and the release election run synchronously before teardown can await.
|
|
526
|
+
*/
|
|
527
|
+
export async function releaseDeadTabForRecovery(
|
|
528
|
+
name: string,
|
|
529
|
+
expected: TabSession,
|
|
530
|
+
ownerId: string | undefined,
|
|
531
|
+
preserveHeadlessTarget = false,
|
|
532
|
+
): Promise<boolean> {
|
|
533
|
+
if (
|
|
534
|
+
tabs.get(name) !== expected ||
|
|
535
|
+
expected.state !== "dead" ||
|
|
536
|
+
expected.ownerId !== ownerId ||
|
|
537
|
+
!beginRelease(expected)
|
|
538
|
+
) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
await releaseClaimedTab(expected, { kill: false }, false, preserveHeadlessTarget);
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
343
544
|
|
|
344
545
|
export async function releaseTab(name: string, opts: ReleaseTabOptions = {}): Promise<boolean> {
|
|
345
546
|
const tab = tabs.get(name);
|
|
@@ -351,9 +552,19 @@ export async function releaseTab(name: string, opts: ReleaseTabOptions = {}): Pr
|
|
|
351
552
|
logger.debug("releaseTab: already releasing", { name });
|
|
352
553
|
return false;
|
|
353
554
|
}
|
|
354
|
-
|
|
555
|
+
await releaseClaimedTab(tab, opts, tab.state === "alive");
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function releaseClaimedTab(
|
|
560
|
+
tab: TabSession,
|
|
561
|
+
opts: ReleaseTabOptions,
|
|
562
|
+
wasAlive: boolean,
|
|
563
|
+
preserveHeadlessTarget = false,
|
|
564
|
+
): Promise<void> {
|
|
355
565
|
tab.state = "dead";
|
|
356
|
-
|
|
566
|
+
discardDeadTabRecovery(tab.name);
|
|
567
|
+
const closeError = new ToolError(`Tab ${JSON.stringify(tab.name)} was closed`);
|
|
357
568
|
for (const [id, pending] of tab.pending) {
|
|
358
569
|
try {
|
|
359
570
|
tab.worker.send({ type: "abort", id });
|
|
@@ -371,19 +582,29 @@ export async function releaseTab(name: string, opts: ReleaseTabOptions = {}): Pr
|
|
|
371
582
|
forced = true;
|
|
372
583
|
}
|
|
373
584
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
585
|
+
try {
|
|
586
|
+
await awaitWithinBudget(
|
|
587
|
+
Promise.resolve().then(() => tab.worker.terminate()),
|
|
588
|
+
remainingBudget(deadlineAt),
|
|
589
|
+
"worker.terminate",
|
|
590
|
+
);
|
|
591
|
+
if (!preserveHeadlessTarget && (forced || !wasAlive) && tab.kindTag === "headless") {
|
|
592
|
+
await awaitWithinBudget(
|
|
593
|
+
Promise.resolve().then(() => closeOrphanTarget(tab)),
|
|
594
|
+
remainingBudget(deadlineAt),
|
|
595
|
+
"closeOrphanTarget",
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
} finally {
|
|
599
|
+
await awaitWithinBudget(
|
|
600
|
+
Promise.resolve().then(() => releaseBrowser(tab.browser, { kill: opts.kill ?? false })),
|
|
601
|
+
remainingBudget(deadlineAt),
|
|
602
|
+
"releaseBrowser",
|
|
603
|
+
);
|
|
604
|
+
// Only delete if the map still holds THIS tab: a same-name reacquire during our async
|
|
605
|
+
// teardown may have installed a fresh tab that we must not evict.
|
|
606
|
+
if (tabs.get(tab.name) === tab) tabs.delete(tab.name);
|
|
377
607
|
}
|
|
378
|
-
await awaitWithinBudget(
|
|
379
|
-
releaseBrowser(tab.browser, { kill: opts.kill ?? false }),
|
|
380
|
-
remainingBudget(deadlineAt),
|
|
381
|
-
"releaseBrowser",
|
|
382
|
-
);
|
|
383
|
-
// Only delete if the map still holds THIS tab: a same-name reacquire during our async
|
|
384
|
-
// teardown may have installed a fresh tab that we must not evict.
|
|
385
|
-
if (tabs.get(name) === tab) tabs.delete(name);
|
|
386
|
-
return true;
|
|
387
608
|
}
|
|
388
609
|
|
|
389
610
|
export async function releaseAllTabs(opts: ReleaseTabOptions = {}): Promise<number> {
|
|
@@ -404,14 +625,20 @@ export async function releaseTabsForOwner(
|
|
|
404
625
|
opts: ReleaseTabOptions = {},
|
|
405
626
|
): Promise<number> {
|
|
406
627
|
if (!ownerId) return 0;
|
|
407
|
-
const
|
|
628
|
+
const ownedTabs = [...tabs.values()].filter(tab => tab.ownerId === ownerId);
|
|
408
629
|
let count = 0;
|
|
409
|
-
for (const
|
|
410
|
-
if (await
|
|
630
|
+
for (const tab of ownedTabs) {
|
|
631
|
+
if (await releaseOwnedTab(tab, ownerId, opts)) count++;
|
|
411
632
|
}
|
|
412
633
|
return count;
|
|
413
634
|
}
|
|
414
635
|
|
|
636
|
+
async function releaseOwnedTab(expected: TabSession, ownerId: string, opts: ReleaseTabOptions): Promise<boolean> {
|
|
637
|
+
if (tabs.get(expected.name) !== expected || expected.ownerId !== ownerId || !beginRelease(expected)) return false;
|
|
638
|
+
await releaseClaimedTab(expected, opts, expected.state === "alive");
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
|
|
415
642
|
export async function dropHeadlessTabs(): Promise<void> {
|
|
416
643
|
const names = [...tabs.values()].filter(tab => tab.kindTag === "headless").map(tab => tab.name);
|
|
417
644
|
for (const name of names) await releaseTab(name);
|
|
@@ -421,6 +648,15 @@ async function buildInitPayload(browser: BrowserHandle, opts: AcquireTabOptions)
|
|
|
421
648
|
const safeDir = getPuppeteerDir();
|
|
422
649
|
const browserWSEndpoint = browser.browser.wsEndpoint();
|
|
423
650
|
if (!browserWSEndpoint) throw new ToolError("Browser websocket endpoint is unavailable");
|
|
651
|
+
if (opts.recoveryTargetId) {
|
|
652
|
+
return {
|
|
653
|
+
mode: "attach",
|
|
654
|
+
browserWSEndpoint,
|
|
655
|
+
safeDir,
|
|
656
|
+
targetId: opts.recoveryTargetId,
|
|
657
|
+
dialogs: opts.dialogs,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
424
660
|
if (browser.kind.kind === "headless") {
|
|
425
661
|
return {
|
|
426
662
|
mode: "headless",
|
|
@@ -444,6 +680,12 @@ async function buildInitPayload(browser: BrowserHandle, opts: AcquireTabOptions)
|
|
|
444
680
|
dialogs: opts.dialogs,
|
|
445
681
|
};
|
|
446
682
|
}
|
|
683
|
+
export async function buildInitPayloadForTest(
|
|
684
|
+
browser: BrowserHandle,
|
|
685
|
+
opts: AcquireTabOptions,
|
|
686
|
+
): Promise<WorkerInitPayload> {
|
|
687
|
+
return await buildInitPayload(browser, opts);
|
|
688
|
+
}
|
|
447
689
|
|
|
448
690
|
function handleTabMessage(tab: TabSession, msg: WorkerOutbound): void {
|
|
449
691
|
if (msg.type === "result") {
|
|
@@ -531,13 +773,17 @@ async function forceKillTab(name: string, reason: string): Promise<void> {
|
|
|
531
773
|
if (!tab) return;
|
|
532
774
|
if (!beginRelease(tab)) return;
|
|
533
775
|
tab.state = "dead";
|
|
776
|
+
discardDeadTabRecovery(name);
|
|
534
777
|
const error = new ToolError(reason);
|
|
535
778
|
for (const pending of tab.pending.values()) pending.reject(error);
|
|
536
779
|
tab.pending.clear();
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
780
|
+
try {
|
|
781
|
+
await tab.worker.terminate().catch(() => undefined);
|
|
782
|
+
if (tab.kindTag === "headless") await closeOrphanTarget(tab).catch(() => undefined);
|
|
783
|
+
} finally {
|
|
784
|
+
await releaseBrowser(tab.browser, { kill: false }).catch(() => undefined);
|
|
785
|
+
if (tabs.get(name) === tab) tabs.delete(name);
|
|
786
|
+
}
|
|
541
787
|
}
|
|
542
788
|
|
|
543
789
|
async function closeOrphanTarget(tab: TabSession): Promise<void> {
|
package/src/tools/computer.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { ImageContent } from "@gajae-code/ai";
|
|
|
6
6
|
import { prompt } from "@gajae-code/utils";
|
|
7
7
|
import * as z from "zod/v4";
|
|
8
8
|
import computerDescription from "../prompts/tools/computer.md" with { type: "text" };
|
|
9
|
-
import { resizeImage } from "../utils/image-resize";
|
|
9
|
+
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
10
10
|
import { markScreenshotFallbackDirCreatedForGc } from "./computer-gc";
|
|
11
11
|
import type { ToolSession } from "./index";
|
|
12
12
|
import type { OutputMeta } from "./output-meta";
|
|
@@ -701,11 +701,16 @@ async function inlineImageContentFromNativeResult(
|
|
|
701
701
|
details: ComputerToolDetails,
|
|
702
702
|
session: ToolSession,
|
|
703
703
|
): Promise<ImageContent | undefined> {
|
|
704
|
+
// Anthropic rejects requests carrying more than 20 images when any image
|
|
705
|
+
// exceeds 2000px per dimension ("many-image requests"), so gating on bytes
|
|
706
|
+
// alone is not enough: an in-budget full-resolution capture can still brick
|
|
707
|
+
// the session once enough screenshots accumulate in history. Always route
|
|
708
|
+
// through resizeImage (its fast path returns already-small images
|
|
709
|
+
// untouched) and tell the model how to map coordinates back to the native
|
|
710
|
+
// screenshot frame when the inline image was scaled.
|
|
704
711
|
const image = fullResolutionImageContentFromNativeResult(value);
|
|
705
712
|
if (!image) return undefined;
|
|
706
713
|
const maxBytes = getInlineScreenshotMaxBytes(session);
|
|
707
|
-
const originalBytes = Buffer.byteLength(image.data, "base64");
|
|
708
|
-
if (originalBytes <= maxBytes) return image;
|
|
709
714
|
|
|
710
715
|
try {
|
|
711
716
|
const resized = await resizeImage(image, {
|
|
@@ -715,6 +720,8 @@ async function inlineImageContentFromNativeResult(
|
|
|
715
720
|
jpegQuality: COMPUTER_INLINE_SCREENSHOT_JPEG_QUALITY,
|
|
716
721
|
});
|
|
717
722
|
if (resized.buffer.length <= maxBytes) {
|
|
723
|
+
const note = formatDimensionNote(resized);
|
|
724
|
+
if (note) details.message = `${details.message} ${note}`;
|
|
718
725
|
return { type: "image", data: resized.data, mimeType: resized.mimeType };
|
|
719
726
|
}
|
|
720
727
|
} catch {
|
package/src/tools/cron.ts
CHANGED
|
@@ -58,6 +58,8 @@ export interface CronJobSnapshot {
|
|
|
58
58
|
createdAt: number;
|
|
59
59
|
expiresAt?: number;
|
|
60
60
|
nextFireAt?: number;
|
|
61
|
+
/** True while this owner-scoped schedule's delivery is in progress. */
|
|
62
|
+
firing?: boolean;
|
|
61
63
|
humanSchedule: string;
|
|
62
64
|
ownerId?: string;
|
|
63
65
|
}
|
|
@@ -444,7 +446,7 @@ function formatCronFireContent(snapshot: CronJobSnapshot): string {
|
|
|
444
446
|
return `<task-notification>\nScheduled task ${snapshot.id} fired (${snapshot.humanSchedule}).\n\n${snapshot.prompt}\n</task-notification>`;
|
|
445
447
|
}
|
|
446
448
|
|
|
447
|
-
function deliverCronFire(record: CronScheduleRecord): void {
|
|
449
|
+
function deliverCronFire(record: CronScheduleRecord): Promise<void> | undefined {
|
|
448
450
|
const content = formatCronFireContent(record.snapshot);
|
|
449
451
|
const details = {
|
|
450
452
|
id: record.snapshot.id,
|
|
@@ -455,16 +457,9 @@ function deliverCronFire(record: CronScheduleRecord): void {
|
|
|
455
457
|
{ customType: "cron-fire", content, display: false, attribution: "agent", details },
|
|
456
458
|
{ triggerTurn: true, deliverAs: "followUp" },
|
|
457
459
|
);
|
|
458
|
-
if (sendPromise)
|
|
459
|
-
void sendPromise.catch(error => {
|
|
460
|
-
logger.warn("Cron fire delivery failed", {
|
|
461
|
-
id: record.snapshot.id,
|
|
462
|
-
error: error instanceof Error ? error.message : String(error),
|
|
463
|
-
});
|
|
464
|
-
});
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
460
|
+
if (sendPromise) return sendPromise;
|
|
467
461
|
record.session.steer?.({ customType: "cron-fire", content, details });
|
|
462
|
+
return undefined;
|
|
468
463
|
}
|
|
469
464
|
|
|
470
465
|
function scheduleRecord(ownerId: string | undefined, state: OwnerScheduleState, record: CronScheduleRecord): void {
|
|
@@ -509,16 +504,35 @@ function fireRecord(ownerId: string | undefined, state: OwnerScheduleState, id:
|
|
|
509
504
|
deleteRecord(ownerId, id);
|
|
510
505
|
return;
|
|
511
506
|
}
|
|
507
|
+
record.snapshot.firing = true;
|
|
508
|
+
notifyCronChange();
|
|
509
|
+
const settle = () => {
|
|
510
|
+
if (record.disposed) return;
|
|
511
|
+
record.snapshot.firing = false;
|
|
512
|
+
notifyCronChange();
|
|
513
|
+
if (!record.snapshot.recurring) {
|
|
514
|
+
deleteRecord(ownerId, id);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
scheduleRecord(ownerId, state, record);
|
|
518
|
+
};
|
|
512
519
|
try {
|
|
513
|
-
deliverCronFire(record);
|
|
520
|
+
const delivery = deliverCronFire(record);
|
|
521
|
+
if (delivery) {
|
|
522
|
+
void delivery.then(settle, error => {
|
|
523
|
+
logger.warn("Cron fire delivery failed", {
|
|
524
|
+
id,
|
|
525
|
+
error: error instanceof Error ? error.message : String(error),
|
|
526
|
+
});
|
|
527
|
+
settle();
|
|
528
|
+
});
|
|
529
|
+
} else {
|
|
530
|
+
settle();
|
|
531
|
+
}
|
|
514
532
|
} catch (error) {
|
|
515
533
|
logger.warn("Cron fire delivery failed", { id, error: error instanceof Error ? error.message : String(error) });
|
|
534
|
+
settle();
|
|
516
535
|
}
|
|
517
|
-
if (!record.snapshot.recurring) {
|
|
518
|
-
deleteRecord(ownerId, id);
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
scheduleRecord(ownerId, state, record);
|
|
522
536
|
}
|
|
523
537
|
|
|
524
538
|
const CRON_ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
package/src/tools/eval.ts
CHANGED
|
@@ -183,8 +183,7 @@ function timeoutSecondsFromMs(timeoutMs: number): number {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
async function resolveBackend(session: ToolSession, language: EvalLanguage): Promise<ResolvedBackend> {
|
|
186
|
-
const allowPy = (session
|
|
187
|
-
const allowJs = (session.settings.get("eval.js") as boolean | undefined) ?? true;
|
|
186
|
+
const { python: allowPy, js: allowJs } = resolveEvalBackends(session);
|
|
188
187
|
|
|
189
188
|
if (language === "python") {
|
|
190
189
|
if (!allowPy) throw new ToolError("Python backend is disabled (eval.py = false).");
|
package/src/tools/fetch.ts
CHANGED
|
@@ -1292,20 +1292,20 @@ async function materializeReadUrlCacheEntry(
|
|
|
1292
1292
|
session: ToolSession,
|
|
1293
1293
|
entry: ReadUrlCacheEntry,
|
|
1294
1294
|
): Promise<ReadUrlCacheEntry | null> {
|
|
1295
|
+
if (entry.output.length > 0) return entry;
|
|
1295
1296
|
if (entry.artifactId) {
|
|
1296
1297
|
const artifactOutput = await readArtifactOutput(session, entry.artifactId);
|
|
1297
1298
|
if (artifactOutput !== null) {
|
|
1298
1299
|
return { ...entry, output: artifactOutput };
|
|
1299
1300
|
}
|
|
1300
1301
|
}
|
|
1301
|
-
|
|
1302
|
-
return entry.output.length > 0 ? entry : null;
|
|
1302
|
+
return null;
|
|
1303
1303
|
}
|
|
1304
1304
|
|
|
1305
1305
|
async function persistReadUrlArtifact(session: ToolSession, output: string): Promise<string | undefined> {
|
|
1306
1306
|
const { path: artifactPath, id } = (await session.allocateOutputArtifact?.("read")) ?? {};
|
|
1307
1307
|
if (!artifactPath) return undefined;
|
|
1308
|
-
await Bun.write(artifactPath, output);
|
|
1308
|
+
await Bun.write(artifactPath, wrapUntrustedContent(output));
|
|
1309
1309
|
return id;
|
|
1310
1310
|
}
|
|
1311
1311
|
|
|
@@ -1379,6 +1379,13 @@ export async function loadReadUrlCacheEntry(
|
|
|
1379
1379
|
return fresh;
|
|
1380
1380
|
}
|
|
1381
1381
|
|
|
1382
|
+
const UNTRUSTED_CONTENT_OPEN = "<untrusted-content>";
|
|
1383
|
+
const UNTRUSTED_CONTENT_CLOSE = "</untrusted-content>";
|
|
1384
|
+
|
|
1385
|
+
export function wrapUntrustedContent(content: string): string {
|
|
1386
|
+
return `${UNTRUSTED_CONTENT_OPEN}\n${content.replace(/<\/untrusted-content>/gi, "</untrusted-content>")}\n${UNTRUSTED_CONTENT_CLOSE}`;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1382
1389
|
function buildUrlReadOutput(result: FetchRenderResult, content: string): string {
|
|
1383
1390
|
let output = "";
|
|
1384
1391
|
output += `URL: ${result.finalUrl}\n`;
|
|
@@ -1413,7 +1420,7 @@ export async function executeReadUrl(
|
|
|
1413
1420
|
truncated: Boolean(cacheEntry.details.truncated || needsArtifact),
|
|
1414
1421
|
};
|
|
1415
1422
|
|
|
1416
|
-
const contentBlocks: Array<TextContent | ImageContent> = [{ type: "text", text: output }];
|
|
1423
|
+
const contentBlocks: Array<TextContent | ImageContent> = [{ type: "text", text: wrapUntrustedContent(output) }];
|
|
1417
1424
|
if (cacheEntry.image) {
|
|
1418
1425
|
contentBlocks.push({ type: "image", data: cacheEntry.image.data, mimeType: cacheEntry.image.mimeType });
|
|
1419
1426
|
}
|