@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
package/src/sdk/bus/index.ts
CHANGED
|
@@ -28,9 +28,9 @@ import * as os from "node:os";
|
|
|
28
28
|
import * as path from "node:path";
|
|
29
29
|
import { promisify } from "node:util";
|
|
30
30
|
import { ThinkingLevel } from "@gajae-code/agent-core";
|
|
31
|
-
import type { ImageContent, TextContent } from "@gajae-code/ai";
|
|
31
|
+
import type { ImageContent, TextContent, Tool } from "@gajae-code/ai";
|
|
32
32
|
import { NotificationServer, nativeBuildInfo } from "@gajae-code/natives";
|
|
33
|
-
import { logger, postmortem, VERSION } from "@gajae-code/utils";
|
|
33
|
+
import { logger, postmortem, prompt, VERSION } from "@gajae-code/utils";
|
|
34
34
|
import { Settings } from "../../config/settings";
|
|
35
35
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
|
|
36
36
|
import type {
|
|
@@ -38,6 +38,7 @@ import type {
|
|
|
38
38
|
WorkflowGateTerminalController,
|
|
39
39
|
WorkflowGateTerminalProof,
|
|
40
40
|
} from "../../modes/shared/agent-wire/workflow-gate-broker";
|
|
41
|
+
import btwUserPrompt from "../../prompts/system/btw-user.md" with { type: "text" };
|
|
41
42
|
import { parseThinkingLevel } from "../../thinking";
|
|
42
43
|
import type {
|
|
43
44
|
AskAnswerRequest,
|
|
@@ -76,10 +77,14 @@ import {
|
|
|
76
77
|
sessionTag,
|
|
77
78
|
} from "./config";
|
|
78
79
|
import { telegramControlCommandUsage } from "./config-commands";
|
|
79
|
-
import { imageAttachmentsFromMessage, notificationActionPayload, summaryFromMessage } from "./helpers";
|
|
80
|
+
import { imageAttachmentsFromMessage, notificationActionPayload, summaryFromMessage, truncate } from "./helpers";
|
|
80
81
|
import { assertNativeRuntimeCompatibility } from "./native-runtime-compatibility";
|
|
81
82
|
import { NotificationSessionController, type NotificationSessionRuntime } from "./session-control";
|
|
82
|
-
import {
|
|
83
|
+
import {
|
|
84
|
+
type EnsureDaemonResult,
|
|
85
|
+
endpointAuthorityDigest,
|
|
86
|
+
ensureTelegramDaemonRunningDetailed,
|
|
87
|
+
} from "./telegram-daemon";
|
|
83
88
|
|
|
84
89
|
// ===========================================================================
|
|
85
90
|
// Session lifecycle control protocol (TypeScript mirror of the Rust wire
|
|
@@ -868,7 +873,10 @@ interface SessionRuntime {
|
|
|
868
873
|
disposeGateTerminalController: () => void;
|
|
869
874
|
disposeAckRecoveryParticipant: () => void;
|
|
870
875
|
disposeGateEmitterListener: () => void;
|
|
876
|
+
/** Aborts and fences side turns while notification delivery is disabled. */
|
|
877
|
+
disableEphemeralTurns: () => void;
|
|
871
878
|
waitForGateResolutionQuiescence: () => Promise<void>;
|
|
879
|
+
trackGateResolution: <T>(resolution: Promise<T>) => Promise<T>;
|
|
872
880
|
workflowGate?: WorkflowGateEmitter;
|
|
873
881
|
gatePresentations?: PresentationArbiter;
|
|
874
882
|
redact: boolean;
|
|
@@ -918,10 +926,52 @@ interface SessionRuntime {
|
|
|
918
926
|
/** True between turn_end and the next turn_start: drops late async message_update
|
|
919
927
|
* frames so a stale live edit can never be emitted after the finalized turn. */
|
|
920
928
|
turnClosed?: boolean;
|
|
929
|
+
/** Started tool calls awaiting a terminal activity frame, keyed by tool call id. */
|
|
930
|
+
inFlightTools: Map<string, { toolName: string; args: unknown }>;
|
|
921
931
|
/** Cancels the postmortem cleanup that emits `session_closed` on process teardown. */
|
|
922
932
|
cancelPostmortemCleanup: () => void;
|
|
923
|
-
/**
|
|
924
|
-
|
|
933
|
+
/** Disposes side-turn resources when their owning logical session becomes unavailable. */
|
|
934
|
+
abortEphemeralTurns: () => void;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const SENSITIVE_MODEL_LABEL =
|
|
938
|
+
/(?:\b(?:https?|wss?):\/\/|\b[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}\b|\b(?:api[-_ ]?key|access[-_ ]?token|bearer|secret|password|account(?:\s*id)?|email|exception|stack trace)\b|\b(?:sk|pk|rk)-[A-Za-z0-9_-]{12,}\b)/i;
|
|
939
|
+
const TOOL_SUMMARY_MAX = 280;
|
|
940
|
+
const EMPTY_CAPABILITIES: ReadonlySet<string> = new Set();
|
|
941
|
+
|
|
942
|
+
/** Stable projection of the tool-owned safe-display seam (never the full Tool surface). */
|
|
943
|
+
type SafeSummaryTool = Pick<Tool, "safeSummary" | "safeSummaryFields">;
|
|
944
|
+
|
|
945
|
+
export function projectToolSummary(
|
|
946
|
+
tool: SafeSummaryTool | undefined,
|
|
947
|
+
kind: "args" | "result",
|
|
948
|
+
value: unknown,
|
|
949
|
+
): string | undefined {
|
|
950
|
+
let summary: string | undefined;
|
|
951
|
+
try {
|
|
952
|
+
if (tool?.safeSummary) {
|
|
953
|
+
summary = tool.safeSummary(kind, value);
|
|
954
|
+
} else {
|
|
955
|
+
const fields = tool?.safeSummaryFields?.[kind];
|
|
956
|
+
if (fields) {
|
|
957
|
+
const source =
|
|
958
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
959
|
+
? (value as Record<string, unknown>)
|
|
960
|
+
: undefined;
|
|
961
|
+
if (source) {
|
|
962
|
+
const projected: Record<string, unknown> = {};
|
|
963
|
+
for (const field of fields) if (Object.hasOwn(source, field)) projected[field] = source[field];
|
|
964
|
+
summary = JSON.stringify(projected);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
} catch {
|
|
969
|
+
return undefined;
|
|
970
|
+
}
|
|
971
|
+
if (typeof summary !== "string") return undefined;
|
|
972
|
+
const normalized = summary.replace(/[\u0000-\u001F\u007F-\u009F\u202A-\u202E\u2066-\u2069]/g, " ").trim();
|
|
973
|
+
if (!normalized || SENSITIVE_MODEL_LABEL.test(normalized)) return undefined;
|
|
974
|
+
return truncate(normalized, TOOL_SUMMARY_MAX);
|
|
925
975
|
}
|
|
926
976
|
|
|
927
977
|
/** Request-local requester authority for stable ControlSurface dispatches. */
|
|
@@ -938,9 +988,28 @@ function pushSessionFrame(
|
|
|
938
988
|
frame: { type: string; [key: string]: unknown },
|
|
939
989
|
): void {
|
|
940
990
|
runtime.host.emitEvent({ kind: frame.type, payload: frame });
|
|
991
|
+
if (frame.type === "turn_stream") {
|
|
992
|
+
runtime.server.pushTurnStreamUnchecked(
|
|
993
|
+
String(frame.sessionId),
|
|
994
|
+
frame.phase === "live" ? "live" : "finalized",
|
|
995
|
+
String(frame.text),
|
|
996
|
+
typeof frame.finalAnswer === "boolean" ? frame.finalAnswer : undefined,
|
|
997
|
+
typeof frame.messageRef === "string" ? frame.messageRef : undefined,
|
|
998
|
+
);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
941
1001
|
runtime.server.pushFrame(JSON.stringify(frame));
|
|
942
1002
|
}
|
|
943
1003
|
|
|
1004
|
+
function pushFileAttachment(
|
|
1005
|
+
runtime: Pick<SessionRuntime, "server" | "host">,
|
|
1006
|
+
frame: { type: "file_attachment"; sessionId: string; name: string; mime?: string; caption?: string },
|
|
1007
|
+
data: Buffer,
|
|
1008
|
+
): void {
|
|
1009
|
+
runtime.host.emitEvent({ kind: frame.type, payload: { ...frame, data: data.toString("base64") } });
|
|
1010
|
+
runtime.server.pushFileAttachmentUnchecked(frame.sessionId, frame.name, frame.mime, data, frame.caption);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
944
1013
|
/** Agent lifecycle is SDK session truth, independent of optional chat delivery. */
|
|
945
1014
|
function emitAgentLifecycle(
|
|
946
1015
|
runtime: Pick<SessionRuntime, "server" | "host">,
|
|
@@ -981,6 +1050,7 @@ const defaultConfig: NotificationConfig = {
|
|
|
981
1050
|
rich: { enabled: true },
|
|
982
1051
|
richDraft: { enabled: false },
|
|
983
1052
|
topics: {},
|
|
1053
|
+
btw: { enabled: true },
|
|
984
1054
|
};
|
|
985
1055
|
|
|
986
1056
|
export function notificationsEnabled(): boolean {
|
|
@@ -1628,11 +1698,18 @@ function hasTerminalArbitrationCapability(
|
|
|
1628
1698
|
Required<
|
|
1629
1699
|
Pick<
|
|
1630
1700
|
WorkflowGateEmitter,
|
|
1631
|
-
|
|
1701
|
+
| "resolveGate"
|
|
1702
|
+
| "recoverAcceptedGates"
|
|
1703
|
+
| "lookupCompletedResolution"
|
|
1704
|
+
| "prepareTerminalization"
|
|
1705
|
+
| "clearPreparedTerminalization"
|
|
1706
|
+
| "registerGateTerminalController"
|
|
1632
1707
|
>
|
|
1633
1708
|
> {
|
|
1634
1709
|
return (
|
|
1635
1710
|
typeof workflowGate?.resolveGate === "function" &&
|
|
1711
|
+
typeof workflowGate.recoverAcceptedGates === "function" &&
|
|
1712
|
+
typeof workflowGate.lookupCompletedResolution === "function" &&
|
|
1636
1713
|
typeof workflowGate.prepareTerminalization === "function" &&
|
|
1637
1714
|
typeof workflowGate.clearPreparedTerminalization === "function" &&
|
|
1638
1715
|
typeof workflowGate.registerGateTerminalController === "function"
|
|
@@ -1986,7 +2063,7 @@ function sdkControlSurface(
|
|
|
1986
2063
|
pending.completeDirect();
|
|
1987
2064
|
return { resolved: true };
|
|
1988
2065
|
},
|
|
1989
|
-
answerGate: async (id, response, expectedSessionId) => {
|
|
2066
|
+
answerGate: async (id, response, expectedSessionId, idempotencyKey) => {
|
|
1990
2067
|
if (!acceptGateResolution())
|
|
1991
2068
|
throw Object.assign(new Error("Workflow gate is no longer answerable."), { code: "resource_gone" });
|
|
1992
2069
|
if (expectedSessionId === undefined) auditMissingExpectedSessionId("workflow.gate_answer");
|
|
@@ -1999,6 +2076,26 @@ function sdkControlSurface(
|
|
|
1999
2076
|
throw Object.assign(new Error("Workflow gates are unavailable for this session."), {
|
|
2000
2077
|
code: "resource_gone",
|
|
2001
2078
|
});
|
|
2079
|
+
const workflowGate = ctx.workflowGate;
|
|
2080
|
+
if (!hasTerminalArbitrationCapability(workflowGate))
|
|
2081
|
+
throw Object.assign(new Error("Workflow gates are unavailable for this session."), {
|
|
2082
|
+
code: "resource_gone",
|
|
2083
|
+
});
|
|
2084
|
+
const gateResponse = {
|
|
2085
|
+
gate_id: id,
|
|
2086
|
+
answer: response,
|
|
2087
|
+
idempotency_key: idempotencyKey ?? id,
|
|
2088
|
+
};
|
|
2089
|
+
const completed = workflowGate.lookupCompletedResolution(gateResponse);
|
|
2090
|
+
if (completed.kind === "completed") return completed.resolution;
|
|
2091
|
+
if (completed.kind === "accepted_incomplete") {
|
|
2092
|
+
await trackGateResolution(workflowGate.recoverAcceptedGates());
|
|
2093
|
+
const recovered = workflowGate.lookupCompletedResolution(gateResponse);
|
|
2094
|
+
if (recovered.kind === "completed") return recovered.resolution;
|
|
2095
|
+
throw Object.assign(new Error("Workflow gate resolution outcome is uncertain."), {
|
|
2096
|
+
code: "terminal_uncertain",
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2002
2099
|
const prepared = presentations.prepareDirectControl(id);
|
|
2003
2100
|
if (!prepared || prepared.status === "stale")
|
|
2004
2101
|
throw Object.assign(new Error("Workflow gate is no longer answerable."), { code: "resource_gone" });
|
|
@@ -2007,31 +2104,22 @@ function sdkControlSurface(
|
|
|
2007
2104
|
if (prepared.status !== "queued" && prepared.status !== "retired")
|
|
2008
2105
|
throw new Error(`Unexpected direct control preparation: ${prepared.status}`);
|
|
2009
2106
|
if (
|
|
2010
|
-
|
|
2011
|
-
id,
|
|
2012
|
-
prepared.status === "queued" ? "not_published" : "retired",
|
|
2013
|
-
) !== true
|
|
2107
|
+
workflowGate.prepareTerminalization(id, prepared.status === "queued" ? "not_published" : "retired") !== true
|
|
2014
2108
|
) {
|
|
2015
2109
|
presentations.finishDirectControl(id, prepared, "rejected");
|
|
2016
2110
|
throw Object.assign(new Error("Workflow gate lacks a terminalization proof."), { code: "resource_gone" });
|
|
2017
2111
|
}
|
|
2018
2112
|
try {
|
|
2019
|
-
const resolution = await trackGateResolution(
|
|
2020
|
-
ctx.workflowGate?.resolveGate?.({
|
|
2021
|
-
gate_id: id,
|
|
2022
|
-
answer: response,
|
|
2023
|
-
idempotency_key: id,
|
|
2024
|
-
}) ?? unavailable("workflow.gate_answer", "workflow gates are unavailable for this session")(),
|
|
2025
|
-
);
|
|
2113
|
+
const resolution = await trackGateResolution(workflowGate.resolveGate(gateResponse));
|
|
2026
2114
|
const status = (resolution as { status?: unknown }).status;
|
|
2027
2115
|
if (status === "accepted" || status === "rejected") {
|
|
2028
|
-
if (status === "rejected")
|
|
2116
|
+
if (status === "rejected") workflowGate.clearPreparedTerminalization(id);
|
|
2029
2117
|
presentations.finishDirectControl(id, prepared, status);
|
|
2030
2118
|
return resolution;
|
|
2031
2119
|
}
|
|
2032
2120
|
} catch (error) {
|
|
2033
2121
|
const outcome = reconcileDirectControlFailure(id);
|
|
2034
|
-
if (outcome === "rejected")
|
|
2122
|
+
if (outcome === "rejected") workflowGate.clearPreparedTerminalization(id);
|
|
2035
2123
|
presentations.finishDirectControl(id, prepared, outcome);
|
|
2036
2124
|
if (outcome === "unknown")
|
|
2037
2125
|
throw Object.assign(new Error("Workflow gate resolution outcome is uncertain."), {
|
|
@@ -2040,7 +2128,7 @@ function sdkControlSurface(
|
|
|
2040
2128
|
throw error;
|
|
2041
2129
|
}
|
|
2042
2130
|
const outcome = reconcileDirectControlFailure(id);
|
|
2043
|
-
if (outcome === "rejected")
|
|
2131
|
+
if (outcome === "rejected") workflowGate.clearPreparedTerminalization(id);
|
|
2044
2132
|
presentations.finishDirectControl(id, prepared, outcome);
|
|
2045
2133
|
logger.warn("workflow_gate_direct_control_uncertain_outcome", {
|
|
2046
2134
|
operation: "workflow.gate_answer",
|
|
@@ -2064,6 +2152,22 @@ function sdkControlSurface(
|
|
|
2064
2152
|
throw Object.assign(new Error("Workflow gates are unavailable for this session."), {
|
|
2065
2153
|
code: "resource_gone",
|
|
2066
2154
|
});
|
|
2155
|
+
const workflowGate = ctx.workflowGate;
|
|
2156
|
+
if (!hasTerminalArbitrationCapability(workflowGate))
|
|
2157
|
+
throw Object.assign(new Error("Workflow gates are unavailable for this session."), {
|
|
2158
|
+
code: "resource_gone",
|
|
2159
|
+
});
|
|
2160
|
+
const gateResponse = { gate_id: id, answer: choice, idempotency_key: id };
|
|
2161
|
+
const completed = workflowGate.lookupCompletedResolution(gateResponse);
|
|
2162
|
+
if (completed.kind === "completed") return completed.resolution;
|
|
2163
|
+
if (completed.kind === "accepted_incomplete") {
|
|
2164
|
+
await trackGateResolution(workflowGate.recoverAcceptedGates());
|
|
2165
|
+
const recovered = workflowGate.lookupCompletedResolution(gateResponse);
|
|
2166
|
+
if (recovered.kind === "completed") return recovered.resolution;
|
|
2167
|
+
throw Object.assign(new Error("Workflow plan resolution outcome is uncertain."), {
|
|
2168
|
+
code: "terminal_uncertain",
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2067
2171
|
const prepared = presentations.prepareDirectControl(id);
|
|
2068
2172
|
if (!prepared || prepared.status === "stale")
|
|
2069
2173
|
throw Object.assign(new Error("Workflow plan is no longer answerable."), { code: "resource_gone" });
|
|
@@ -2072,31 +2176,22 @@ function sdkControlSurface(
|
|
|
2072
2176
|
if (prepared.status !== "queued" && prepared.status !== "retired")
|
|
2073
2177
|
throw new Error(`Unexpected direct control preparation: ${prepared.status}`);
|
|
2074
2178
|
if (
|
|
2075
|
-
|
|
2076
|
-
id,
|
|
2077
|
-
prepared.status === "queued" ? "not_published" : "retired",
|
|
2078
|
-
) !== true
|
|
2179
|
+
workflowGate.prepareTerminalization(id, prepared.status === "queued" ? "not_published" : "retired") !== true
|
|
2079
2180
|
) {
|
|
2080
2181
|
presentations.finishDirectControl(id, prepared, "rejected");
|
|
2081
2182
|
throw Object.assign(new Error("Workflow plan lacks a terminalization proof."), { code: "resource_gone" });
|
|
2082
2183
|
}
|
|
2083
2184
|
try {
|
|
2084
|
-
const resolution = await trackGateResolution(
|
|
2085
|
-
ctx.workflowGate?.resolveGate?.({
|
|
2086
|
-
gate_id: id,
|
|
2087
|
-
answer: choice,
|
|
2088
|
-
idempotency_key: id,
|
|
2089
|
-
}) ?? unavailable("workflow.plan_approve", "workflow gates are unavailable for this session")(),
|
|
2090
|
-
);
|
|
2185
|
+
const resolution = await trackGateResolution(workflowGate.resolveGate(gateResponse));
|
|
2091
2186
|
const status = (resolution as { status?: unknown }).status;
|
|
2092
2187
|
if (status === "accepted" || status === "rejected") {
|
|
2093
|
-
if (status === "rejected")
|
|
2188
|
+
if (status === "rejected") workflowGate.clearPreparedTerminalization(id);
|
|
2094
2189
|
presentations.finishDirectControl(id, prepared, status);
|
|
2095
2190
|
return resolution;
|
|
2096
2191
|
}
|
|
2097
2192
|
} catch (error) {
|
|
2098
2193
|
const outcome = reconcileDirectControlFailure(id);
|
|
2099
|
-
if (outcome === "rejected")
|
|
2194
|
+
if (outcome === "rejected") workflowGate.clearPreparedTerminalization(id);
|
|
2100
2195
|
presentations.finishDirectControl(id, prepared, outcome);
|
|
2101
2196
|
if (outcome === "unknown")
|
|
2102
2197
|
throw Object.assign(new Error("Workflow plan resolution outcome is uncertain."), {
|
|
@@ -2105,7 +2200,7 @@ function sdkControlSurface(
|
|
|
2105
2200
|
throw error;
|
|
2106
2201
|
}
|
|
2107
2202
|
const outcome = reconcileDirectControlFailure(id);
|
|
2108
|
-
if (outcome === "rejected")
|
|
2203
|
+
if (outcome === "rejected") workflowGate.clearPreparedTerminalization(id);
|
|
2109
2204
|
presentations.finishDirectControl(id, prepared, outcome);
|
|
2110
2205
|
logger.warn("workflow_gate_direct_control_uncertain_outcome", {
|
|
2111
2206
|
operation: "workflow.plan_approve",
|
|
@@ -2124,14 +2219,14 @@ function sdkControlSurface(
|
|
|
2124
2219
|
throw Object.assign(new Error("skill.invoke args must be a string."), { code: "invalid_input" });
|
|
2125
2220
|
return ctx.invokeSkill(name, args);
|
|
2126
2221
|
},
|
|
2127
|
-
setPlanMode: on => {
|
|
2222
|
+
setPlanMode: async on => {
|
|
2128
2223
|
if (!bindings.has("setPlanMode") || !ctx.setPlanMode)
|
|
2129
2224
|
return unavailable("mode.plan.set", "no plan-mode seam is installed")();
|
|
2130
2225
|
|
|
2131
2226
|
if (typeof on !== "boolean")
|
|
2132
2227
|
throw Object.assign(new Error("mode.plan.set requires a boolean on value."), { code: "invalid_input" });
|
|
2133
2228
|
|
|
2134
|
-
return { state: ctx.setPlanMode(on) };
|
|
2229
|
+
return { state: await ctx.setPlanMode(on) };
|
|
2135
2230
|
},
|
|
2136
2231
|
operateGoal: (op, objective) => {
|
|
2137
2232
|
if (!bindings.has("operateGoal") || !ctx.operateGoal)
|
|
@@ -2252,6 +2347,367 @@ function sdkControlSurface(
|
|
|
2252
2347
|
return surface;
|
|
2253
2348
|
}
|
|
2254
2349
|
|
|
2350
|
+
const EPHEMERAL_TURN_DEADLINE_MS = 120_000;
|
|
2351
|
+
const EPHEMERAL_TURN_TTL_MS = 300_000;
|
|
2352
|
+
const EPHEMERAL_TURN_MAX_RECORDS = 256;
|
|
2353
|
+
const EPHEMERAL_TURN_MAX_ACTIVE_PER_SESSION = 2;
|
|
2354
|
+
const EPHEMERAL_TURN_MAX_RESULT_BYTES = 262_144;
|
|
2355
|
+
|
|
2356
|
+
interface EphemeralTurnTuple {
|
|
2357
|
+
sessionId: string;
|
|
2358
|
+
requestId: string;
|
|
2359
|
+
updateId: number;
|
|
2360
|
+
messageId: number;
|
|
2361
|
+
threadId: string;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
type EphemeralTurnStatus = "ok" | "busy" | "timeout" | "cancelled" | "session_unavailable" | "failed";
|
|
2365
|
+
|
|
2366
|
+
interface EphemeralTurnAuthority {
|
|
2367
|
+
sessionId: string;
|
|
2368
|
+
endpointDigest: string;
|
|
2369
|
+
eventGeneration: number;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
interface EphemeralTurnEvent {
|
|
2373
|
+
tuple: EphemeralTurnTuple;
|
|
2374
|
+
authority: EphemeralTurnAuthority;
|
|
2375
|
+
status: EphemeralTurnStatus;
|
|
2376
|
+
text?: string;
|
|
2377
|
+
completedAt: number;
|
|
2378
|
+
expiresAt: number;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
interface EphemeralTurnTombstone {
|
|
2382
|
+
tuple: EphemeralTurnTuple;
|
|
2383
|
+
authority: EphemeralTurnAuthority;
|
|
2384
|
+
status: EphemeralTurnStatus;
|
|
2385
|
+
completedAt: number;
|
|
2386
|
+
expiresAt: number;
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
interface ActiveEphemeralTurn {
|
|
2390
|
+
tuple: EphemeralTurnTuple;
|
|
2391
|
+
authority: EphemeralTurnAuthority;
|
|
2392
|
+
connectionId: string;
|
|
2393
|
+
staleConnectionIds: Set<string>;
|
|
2394
|
+
controller: AbortController;
|
|
2395
|
+
subscribers: Set<string>;
|
|
2396
|
+
deadline: NodeJS.Timeout;
|
|
2397
|
+
abortListener: () => void;
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
function ephemeralTuple(frame: Record<string, unknown>): EphemeralTurnTuple | undefined {
|
|
2401
|
+
const { sessionId, requestId, updateId, messageId, threadId } = frame;
|
|
2402
|
+
return typeof sessionId === "string" &&
|
|
2403
|
+
typeof requestId === "string" &&
|
|
2404
|
+
typeof updateId === "number" &&
|
|
2405
|
+
Number.isSafeInteger(updateId) &&
|
|
2406
|
+
typeof messageId === "number" &&
|
|
2407
|
+
Number.isSafeInteger(messageId) &&
|
|
2408
|
+
messageId > 0 &&
|
|
2409
|
+
typeof threadId === "string"
|
|
2410
|
+
? { sessionId, requestId, updateId, messageId, threadId }
|
|
2411
|
+
: undefined;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
function sameEphemeralTuple(left: EphemeralTurnTuple, right: EphemeralTurnTuple): boolean {
|
|
2415
|
+
return (
|
|
2416
|
+
left.sessionId === right.sessionId &&
|
|
2417
|
+
left.requestId === right.requestId &&
|
|
2418
|
+
left.updateId === right.updateId &&
|
|
2419
|
+
left.messageId === right.messageId &&
|
|
2420
|
+
left.threadId === right.threadId
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
function ephemeralTupleKey(tuple: EphemeralTurnTuple): string {
|
|
2425
|
+
return JSON.stringify([tuple.sessionId, tuple.requestId, tuple.updateId, tuple.messageId, tuple.threadId]);
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
/** Host-owned, bounded idempotency and cancellation lifecycle for v3 side turns. */
|
|
2429
|
+
export class EphemeralTurnHost {
|
|
2430
|
+
#active = new Map<string, ActiveEphemeralTurn>();
|
|
2431
|
+
#terminalEvents = new Map<string, EphemeralTurnEvent>();
|
|
2432
|
+
#tombstones = new Map<string, EphemeralTurnTombstone>();
|
|
2433
|
+
#expiryTimer: NodeJS.Timeout | undefined;
|
|
2434
|
+
#disposed = false;
|
|
2435
|
+
#enabled = true;
|
|
2436
|
+
#now: () => number;
|
|
2437
|
+
#sendTo: (connectionId: string, frame: Record<string, unknown>) => void;
|
|
2438
|
+
#execute: (question: string, signal: AbortSignal) => Promise<{ replyText: string }>;
|
|
2439
|
+
#authority: EphemeralTurnAuthority | undefined;
|
|
2440
|
+
|
|
2441
|
+
constructor(
|
|
2442
|
+
sendTo: (connectionId: string, frame: Record<string, unknown>) => void,
|
|
2443
|
+
execute: (question: string, signal: AbortSignal) => Promise<{ replyText: string }>,
|
|
2444
|
+
now: () => number = Date.now,
|
|
2445
|
+
) {
|
|
2446
|
+
this.#sendTo = sendTo;
|
|
2447
|
+
this.#execute = execute;
|
|
2448
|
+
this.#now = now;
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
configureAuthority(authority: EphemeralTurnAuthority): void {
|
|
2452
|
+
if (this.#authority && !this.#sameAuthority(this.#authority, authority))
|
|
2453
|
+
for (const active of [...this.#active.values()]) active.controller.abort("session_unavailable");
|
|
2454
|
+
this.#authority = { ...authority };
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
disable(): void {
|
|
2458
|
+
if (this.#disposed) return;
|
|
2459
|
+
this.#enabled = false;
|
|
2460
|
+
for (const active of this.#active.values()) active.controller.abort("session_unavailable");
|
|
2461
|
+
this.#terminalEvents.clear();
|
|
2462
|
+
this.#tombstones.clear();
|
|
2463
|
+
if (this.#expiryTimer) clearTimeout(this.#expiryTimer);
|
|
2464
|
+
this.#expiryTimer = undefined;
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
enable(): void {
|
|
2468
|
+
if (!this.#disposed) this.#enabled = true;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
dispose(): void {
|
|
2472
|
+
this.#disposed = true;
|
|
2473
|
+
for (const active of this.#active.values()) active.controller.abort("session_unavailable");
|
|
2474
|
+
if (this.#expiryTimer) clearTimeout(this.#expiryTimer);
|
|
2475
|
+
this.#expiryTimer = undefined;
|
|
2476
|
+
this.#terminalEvents.clear();
|
|
2477
|
+
this.#tombstones.clear();
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
handle(connectionId: string, frame: Record<string, unknown>): boolean {
|
|
2481
|
+
if (!this.#enabled) return frame.type === "ephemeral_turn" || frame.type === "ephemeral_turn_cancel";
|
|
2482
|
+
if (frame.type === "ephemeral_turn") return this.#start(connectionId, frame);
|
|
2483
|
+
if (frame.type === "ephemeral_turn_cancel") return this.#cancel(connectionId, frame);
|
|
2484
|
+
return false;
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
sessionUnavailable(sessionId: string): void {
|
|
2488
|
+
for (const active of [...this.#active.values()])
|
|
2489
|
+
if (active.tuple.sessionId === sessionId) active.controller.abort("session_unavailable");
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
/** Testable event-ring eviction boundary; tombstones remain idempotency authority. */
|
|
2493
|
+
evictTerminalEvents(): void {
|
|
2494
|
+
this.#terminalEvents.clear();
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
#start(connectionId: string, frame: Record<string, unknown>): boolean {
|
|
2498
|
+
const tuple = ephemeralTuple(frame);
|
|
2499
|
+
const question = typeof frame.question === "string" ? frame.question.trim() : "";
|
|
2500
|
+
const authority = this.#authority;
|
|
2501
|
+
if (!tuple || !question || !authority || tuple.sessionId !== authority.sessionId) return true;
|
|
2502
|
+
this.#purge();
|
|
2503
|
+
const key = ephemeralTupleKey(tuple);
|
|
2504
|
+
const active = this.#active.get(key);
|
|
2505
|
+
if (active) {
|
|
2506
|
+
if (!this.#sameAuthority(active.authority, authority)) {
|
|
2507
|
+
active.controller.abort("session_unavailable");
|
|
2508
|
+
return true;
|
|
2509
|
+
}
|
|
2510
|
+
if (active.connectionId === connectionId || active.staleConnectionIds.has(connectionId)) return true;
|
|
2511
|
+
active.staleConnectionIds.add(active.connectionId);
|
|
2512
|
+
active.connectionId = connectionId;
|
|
2513
|
+
active.subscribers = new Set([connectionId]);
|
|
2514
|
+
return true;
|
|
2515
|
+
}
|
|
2516
|
+
const event = this.#terminalEvents.get(key);
|
|
2517
|
+
if (event) {
|
|
2518
|
+
if (this.#sameAuthority(event.authority, authority))
|
|
2519
|
+
this.#send(connectionId, event.tuple, event.status, event.text);
|
|
2520
|
+
return true;
|
|
2521
|
+
}
|
|
2522
|
+
const tombstone = this.#tombstones.get(key);
|
|
2523
|
+
if (tombstone) {
|
|
2524
|
+
if (this.#sameAuthority(tombstone.authority, authority)) this.#send(connectionId, tombstone.tuple, "failed");
|
|
2525
|
+
return true;
|
|
2526
|
+
}
|
|
2527
|
+
for (const candidate of this.#tombstones.values()) {
|
|
2528
|
+
if (candidate.tuple.sessionId === tuple.sessionId && candidate.tuple.requestId === tuple.requestId) {
|
|
2529
|
+
logger.warn("notifications: ephemeral request id conflict", {
|
|
2530
|
+
sessionId: tuple.sessionId,
|
|
2531
|
+
requestId: tuple.requestId,
|
|
2532
|
+
});
|
|
2533
|
+
return true;
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
for (const candidate of this.#active.values()) {
|
|
2537
|
+
if (candidate.tuple.sessionId === tuple.sessionId && candidate.tuple.requestId === tuple.requestId) {
|
|
2538
|
+
logger.warn("notifications: ephemeral request id conflict", {
|
|
2539
|
+
sessionId: tuple.sessionId,
|
|
2540
|
+
requestId: tuple.requestId,
|
|
2541
|
+
});
|
|
2542
|
+
return true;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
const activeForSession = [...this.#active.values()].filter(
|
|
2546
|
+
candidate => candidate.tuple.sessionId === tuple.sessionId,
|
|
2547
|
+
).length;
|
|
2548
|
+
if (activeForSession >= EPHEMERAL_TURN_MAX_ACTIVE_PER_SESSION) {
|
|
2549
|
+
const completedAt = this.#now();
|
|
2550
|
+
this.#finish(key, {
|
|
2551
|
+
tuple,
|
|
2552
|
+
authority: { ...authority },
|
|
2553
|
+
status: "busy",
|
|
2554
|
+
completedAt,
|
|
2555
|
+
expiresAt: completedAt + EPHEMERAL_TURN_TTL_MS,
|
|
2556
|
+
});
|
|
2557
|
+
this.#send(connectionId, tuple, "busy");
|
|
2558
|
+
return true;
|
|
2559
|
+
}
|
|
2560
|
+
const controller = new AbortController();
|
|
2561
|
+
const abortListener = () => this.#complete(key, this.#abortStatus(controller.signal));
|
|
2562
|
+
const record: ActiveEphemeralTurn = {
|
|
2563
|
+
tuple,
|
|
2564
|
+
authority: { ...authority },
|
|
2565
|
+
connectionId,
|
|
2566
|
+
controller,
|
|
2567
|
+
subscribers: new Set([connectionId]),
|
|
2568
|
+
staleConnectionIds: new Set(),
|
|
2569
|
+
deadline: setTimeout(() => controller.abort("timeout"), EPHEMERAL_TURN_DEADLINE_MS),
|
|
2570
|
+
abortListener,
|
|
2571
|
+
};
|
|
2572
|
+
this.#active.set(key, record);
|
|
2573
|
+
controller.signal.addEventListener("abort", abortListener, { once: true });
|
|
2574
|
+
void this.#execute(question, controller.signal).then(
|
|
2575
|
+
result =>
|
|
2576
|
+
this.#complete(
|
|
2577
|
+
key,
|
|
2578
|
+
controller.signal.aborted ? this.#abortStatus(controller.signal) : "ok",
|
|
2579
|
+
result.replyText,
|
|
2580
|
+
),
|
|
2581
|
+
() => this.#complete(key, controller.signal.aborted ? this.#abortStatus(controller.signal) : "failed"),
|
|
2582
|
+
);
|
|
2583
|
+
return true;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
#cancel(connectionId: string, frame: Record<string, unknown>): boolean {
|
|
2587
|
+
const tuple = ephemeralTuple(frame);
|
|
2588
|
+
const authority = this.#authority;
|
|
2589
|
+
if (!tuple || frame.reason !== "daemon_shutdown" || !authority || tuple.sessionId !== authority.sessionId)
|
|
2590
|
+
return true;
|
|
2591
|
+
const active = this.#active.get(ephemeralTupleKey(tuple));
|
|
2592
|
+
if (
|
|
2593
|
+
!active ||
|
|
2594
|
+
!sameEphemeralTuple(active.tuple, tuple) ||
|
|
2595
|
+
active.connectionId !== connectionId ||
|
|
2596
|
+
!this.#sameAuthority(active.authority, authority)
|
|
2597
|
+
)
|
|
2598
|
+
return true;
|
|
2599
|
+
active.controller.abort("cancelled");
|
|
2600
|
+
return true;
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
#abortStatus(signal: AbortSignal): EphemeralTurnStatus {
|
|
2604
|
+
return signal.reason === "timeout"
|
|
2605
|
+
? "timeout"
|
|
2606
|
+
: signal.reason === "session_unavailable"
|
|
2607
|
+
? "session_unavailable"
|
|
2608
|
+
: "cancelled";
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
#sameAuthority(left: EphemeralTurnAuthority, right: EphemeralTurnAuthority): boolean {
|
|
2612
|
+
return (
|
|
2613
|
+
left.sessionId === right.sessionId &&
|
|
2614
|
+
left.endpointDigest === right.endpointDigest &&
|
|
2615
|
+
left.eventGeneration === right.eventGeneration
|
|
2616
|
+
);
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
#complete(key: string, status: EphemeralTurnStatus, text?: string): void {
|
|
2620
|
+
const active = this.#active.get(key);
|
|
2621
|
+
if (!active) return;
|
|
2622
|
+
clearTimeout(active.deadline);
|
|
2623
|
+
active.controller.signal.removeEventListener("abort", active.abortListener);
|
|
2624
|
+
this.#active.delete(key);
|
|
2625
|
+
if (this.#disposed || !this.#enabled) return;
|
|
2626
|
+
const terminalTextIsValid =
|
|
2627
|
+
typeof text === "string" &&
|
|
2628
|
+
text.trim().length > 0 &&
|
|
2629
|
+
Buffer.byteLength(text, "utf8") <= EPHEMERAL_TURN_MAX_RESULT_BYTES;
|
|
2630
|
+
const terminalStatus = status === "ok" && !terminalTextIsValid ? "failed" : status;
|
|
2631
|
+
const completedAt = this.#now();
|
|
2632
|
+
const terminal: EphemeralTurnEvent = {
|
|
2633
|
+
tuple: active.tuple,
|
|
2634
|
+
authority: active.authority,
|
|
2635
|
+
status: terminalStatus,
|
|
2636
|
+
...(terminalStatus === "ok" ? { text: text ?? "" } : {}),
|
|
2637
|
+
completedAt,
|
|
2638
|
+
expiresAt: completedAt + EPHEMERAL_TURN_TTL_MS,
|
|
2639
|
+
};
|
|
2640
|
+
this.#finish(key, terminal);
|
|
2641
|
+
for (const connectionId of active.subscribers) {
|
|
2642
|
+
try {
|
|
2643
|
+
this.#send(connectionId, terminal.tuple, terminal.status, terminal.text);
|
|
2644
|
+
} catch {
|
|
2645
|
+
// Directed SDK delivery has already logged the disconnected route.
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
#finish(key: string, terminal: EphemeralTurnEvent): void {
|
|
2651
|
+
this.#terminalEvents.set(key, terminal);
|
|
2652
|
+
this.#tombstones.set(key, {
|
|
2653
|
+
tuple: terminal.tuple,
|
|
2654
|
+
authority: terminal.authority,
|
|
2655
|
+
status: terminal.status,
|
|
2656
|
+
completedAt: terminal.completedAt,
|
|
2657
|
+
expiresAt: terminal.expiresAt,
|
|
2658
|
+
});
|
|
2659
|
+
this.#purge();
|
|
2660
|
+
while (this.#terminalEvents.size > EPHEMERAL_TURN_MAX_RECORDS)
|
|
2661
|
+
this.#terminalEvents.delete(this.#terminalEvents.keys().next().value!);
|
|
2662
|
+
while (this.#tombstones.size > EPHEMERAL_TURN_MAX_RECORDS) {
|
|
2663
|
+
const oldestKey = this.#tombstones.keys().next().value!;
|
|
2664
|
+
this.#tombstones.delete(oldestKey);
|
|
2665
|
+
this.#terminalEvents.delete(oldestKey);
|
|
2666
|
+
}
|
|
2667
|
+
this.#scheduleExpiry();
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
#send(connectionId: string, tuple: EphemeralTurnTuple, status: EphemeralTurnStatus, text?: string): void {
|
|
2671
|
+
this.#sendTo(connectionId, {
|
|
2672
|
+
type: "ephemeral_turn_result",
|
|
2673
|
+
...tuple,
|
|
2674
|
+
status,
|
|
2675
|
+
...(status === "ok" ? { text: text ?? "" } : {}),
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
#purge(): void {
|
|
2680
|
+
const now = this.#now();
|
|
2681
|
+
for (const [key, tombstone] of this.#tombstones) {
|
|
2682
|
+
if (tombstone.expiresAt > now) continue;
|
|
2683
|
+
this.#tombstones.delete(key);
|
|
2684
|
+
this.#terminalEvents.delete(key);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
#scheduleExpiry(): void {
|
|
2689
|
+
if (this.#disposed) return;
|
|
2690
|
+
if (this.#expiryTimer) clearTimeout(this.#expiryTimer);
|
|
2691
|
+
const nextExpiry = [...this.#tombstones.values()].reduce(
|
|
2692
|
+
(earliest, tombstone) => Math.min(earliest, tombstone.expiresAt),
|
|
2693
|
+
Number.POSITIVE_INFINITY,
|
|
2694
|
+
);
|
|
2695
|
+
if (!Number.isFinite(nextExpiry)) {
|
|
2696
|
+
this.#expiryTimer = undefined;
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
this.#expiryTimer = setTimeout(
|
|
2700
|
+
() => {
|
|
2701
|
+
this.#expiryTimer = undefined;
|
|
2702
|
+
if (this.#disposed) return;
|
|
2703
|
+
this.#purge();
|
|
2704
|
+
this.#scheduleExpiry();
|
|
2705
|
+
},
|
|
2706
|
+
Math.max(0, nextExpiry - this.#now()),
|
|
2707
|
+
);
|
|
2708
|
+
this.#expiryTimer.unref();
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2255
2711
|
/** Parse only v3 frames carried through the existing control-command seam. */
|
|
2256
2712
|
function sdkInboundFrame(commandJson: string | undefined): Record<string, unknown> | undefined {
|
|
2257
2713
|
if (!commandJson) return undefined;
|
|
@@ -2285,8 +2741,11 @@ export function createNotificationsExtension(
|
|
|
2285
2741
|
/** Suppress auto-delivery for a GJC-spawned child under `sessionScope=primary`. */
|
|
2286
2742
|
spawnedByGjc?: boolean;
|
|
2287
2743
|
controller?: NotificationSessionController;
|
|
2744
|
+
/** Whether this host mode can own the root SDK endpoint. Default: true. */
|
|
2745
|
+
sdkHostModeSupported?: boolean;
|
|
2288
2746
|
|
|
2289
2747
|
onSdkRequest?: (kind: "control" | "query", connectionId: string, frame: Record<string, unknown>) => void;
|
|
2748
|
+
runEphemeralTurn?: (promptText: string, signal: AbortSignal) => Promise<{ replyText: string }>;
|
|
2290
2749
|
} = {},
|
|
2291
2750
|
): void {
|
|
2292
2751
|
const lifecycleStartupCapability = lifecycleStartupCapabilityForApi(api);
|
|
@@ -2305,6 +2764,7 @@ export function createNotificationsExtension(
|
|
|
2305
2764
|
let activeRuntimeId: string | undefined;
|
|
2306
2765
|
let identityControlInFlight = false;
|
|
2307
2766
|
let deferredIdentityRotation: { event: { previousSessionFile?: string }; ctx: ExtensionContext } | undefined;
|
|
2767
|
+
let extensionShuttingDown = false;
|
|
2308
2768
|
|
|
2309
2769
|
async function ensureTelegramOwner(
|
|
2310
2770
|
settings: Settings,
|
|
@@ -2352,7 +2812,10 @@ export function createNotificationsExtension(
|
|
|
2352
2812
|
const activeRuntime = runtimes.get(id);
|
|
2353
2813
|
const requestedRuntime = retryRuntime ?? activeRuntime;
|
|
2354
2814
|
if (expectedRuntime && requestedRuntime !== expectedRuntime) return false;
|
|
2355
|
-
if (reason === "session" && requestedRuntime)
|
|
2815
|
+
if (reason === "session" && requestedRuntime) {
|
|
2816
|
+
requestedRuntime.stopping = true;
|
|
2817
|
+
requestedRuntime.abortEphemeralTurns();
|
|
2818
|
+
}
|
|
2356
2819
|
if (reason === "session" && requestedRuntime) {
|
|
2357
2820
|
// Fence the exact runtime before awaiting its startup promise: a late start
|
|
2358
2821
|
// must observe removal and clean itself up rather than becoming reachable.
|
|
@@ -2365,7 +2828,12 @@ export function createNotificationsExtension(
|
|
|
2365
2828
|
.catch(() => {})
|
|
2366
2829
|
.then(() => {
|
|
2367
2830
|
if (runtimes.get(id) === requestedRuntime || cleanupRetries.get(id) === requestedRuntime)
|
|
2368
|
-
void stopSession(id, reason, requestedRuntime)
|
|
2831
|
+
void stopSession(id, reason, requestedRuntime).catch(error =>
|
|
2832
|
+
// A retained owner-release failure keeps the exact runtime in
|
|
2833
|
+
// cleanupRetries for a later retry; log it rather than letting a
|
|
2834
|
+
// fire-and-forget rejection become a fatal unhandled rejection.
|
|
2835
|
+
logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`),
|
|
2836
|
+
);
|
|
2369
2837
|
});
|
|
2370
2838
|
const rt = requestedRuntime;
|
|
2371
2839
|
|
|
@@ -2377,6 +2845,7 @@ export function createNotificationsExtension(
|
|
|
2377
2845
|
}
|
|
2378
2846
|
if (reason === "notifications" && rt.host.started) {
|
|
2379
2847
|
rt.notificationsActive = false;
|
|
2848
|
+
rt.disableEphemeralTurns();
|
|
2380
2849
|
try {
|
|
2381
2850
|
rt.disposeAnswerSource();
|
|
2382
2851
|
} catch {}
|
|
@@ -2395,9 +2864,6 @@ export function createNotificationsExtension(
|
|
|
2395
2864
|
try {
|
|
2396
2865
|
rt.cancelPostmortemCleanup();
|
|
2397
2866
|
} catch {}
|
|
2398
|
-
try {
|
|
2399
|
-
rt.stopBrokerHeartbeat();
|
|
2400
|
-
} catch {}
|
|
2401
2867
|
try {
|
|
2402
2868
|
rt.disposeAnswerSource();
|
|
2403
2869
|
} catch {}
|
|
@@ -2407,6 +2873,9 @@ export function createNotificationsExtension(
|
|
|
2407
2873
|
try {
|
|
2408
2874
|
rt.disposeGateListener();
|
|
2409
2875
|
} catch {}
|
|
2876
|
+
try {
|
|
2877
|
+
rt.workflowGate?.setRuntimeTurnProvider?.(null);
|
|
2878
|
+
} catch {}
|
|
2410
2879
|
await rt.waitForGateResolutionQuiescence();
|
|
2411
2880
|
try {
|
|
2412
2881
|
rt.disposeAckRecoveryParticipant();
|
|
@@ -2489,7 +2958,8 @@ export function createNotificationsExtension(
|
|
|
2489
2958
|
const lifecycleRequestId = safeLifecycleRequestId(process.env.GJC_LIFECYCLE_REQUEST_ID);
|
|
2490
2959
|
const { settings, cfg, settingsAvailable } = resolveSettings(options.settings);
|
|
2491
2960
|
const notificationsEnabledForSession = controller.query(ctx).effectiveEnabled;
|
|
2492
|
-
const sdkEnabledForSession =
|
|
2961
|
+
const sdkEnabledForSession =
|
|
2962
|
+
(options.sdkHostModeSupported ?? true) && shouldHostSdk(settings, isNotificationEligibleContext(ctx));
|
|
2493
2963
|
const lifecycleRequired = lifecycleStartupCapability !== undefined;
|
|
2494
2964
|
const failLifecycleStartup = (
|
|
2495
2965
|
reason: "disabled" | "ineligible" | "failed",
|
|
@@ -2559,7 +3029,6 @@ export function createNotificationsExtension(
|
|
|
2559
3029
|
}
|
|
2560
3030
|
const gatePresentations = new PresentationArbiter(server, () => runtime?.redact ?? redact, tag);
|
|
2561
3031
|
let inboundSdkFrame: ((connectionId: string, frame: Record<string, unknown>) => void) | undefined;
|
|
2562
|
-
let stopBrokerHeartbeat = () => {};
|
|
2563
3032
|
const inFlightGateResolutions = new Set<Promise<void>>();
|
|
2564
3033
|
const trackGateResolution = <T>(resolution: Promise<T>): Promise<T> => {
|
|
2565
3034
|
const quiesced = resolution.then(
|
|
@@ -2601,6 +3070,8 @@ export function createNotificationsExtension(
|
|
|
2601
3070
|
if (capability === "permission") ctx.setSdkPermissionProvider?.(undefined);
|
|
2602
3071
|
};
|
|
2603
3072
|
|
|
3073
|
+
const hostCapCache = new Map<string, ReadonlySet<string>>();
|
|
3074
|
+
|
|
2604
3075
|
const configOverrides = new Map<string, unknown>();
|
|
2605
3076
|
const configRevision = { current: 0 };
|
|
2606
3077
|
const PROMPT_SUBMISSION_CAPACITY = 128;
|
|
@@ -2759,6 +3230,11 @@ export function createNotificationsExtension(
|
|
|
2759
3230
|
submission.failed = true;
|
|
2760
3231
|
submission.error = error;
|
|
2761
3232
|
removePendingPromptCorrelation(correlation);
|
|
3233
|
+
if (
|
|
3234
|
+
runtime?.activePromptCorrelation?.commandId === correlation.commandId &&
|
|
3235
|
+
runtime.activePromptCorrelation.turnId === correlation.turnId
|
|
3236
|
+
)
|
|
3237
|
+
runtime.activePromptCorrelation = undefined;
|
|
2762
3238
|
emitPromptFailure(correlation, error);
|
|
2763
3239
|
};
|
|
2764
3240
|
const acknowledgePrompt = (connectionId: string, correlation: { commandId: string; turnId: string }) => {
|
|
@@ -2826,6 +3302,10 @@ export function createNotificationsExtension(
|
|
|
2826
3302
|
};
|
|
2827
3303
|
|
|
2828
3304
|
const sendSdkFrame = (connectionId: string, frame: Record<string, unknown>) => {
|
|
3305
|
+
if (extensionShuttingDown || runtime?.stopping || runtimes.get(id) !== runtime) {
|
|
3306
|
+
abandonPromptResponse(connectionId, frame);
|
|
3307
|
+
return;
|
|
3308
|
+
}
|
|
2829
3309
|
const json = JSON.stringify(frame);
|
|
2830
3310
|
if (connectionId.startsWith("seam:")) {
|
|
2831
3311
|
try {
|
|
@@ -2857,6 +3337,7 @@ export function createNotificationsExtension(
|
|
|
2857
3337
|
stateRoot,
|
|
2858
3338
|
token,
|
|
2859
3339
|
sendFrame: (connectionId, frame) => sendSdkFrame(connectionId, frame),
|
|
3340
|
+
connectionCapabilities: connectionId => hostCapCache.get(connectionId) ?? EMPTY_CAPABILITIES,
|
|
2860
3341
|
installProviderDefinitions,
|
|
2861
3342
|
onProviderDefinitionsRemoved: removeProviderDefinitions,
|
|
2862
3343
|
onFrame: handler => {
|
|
@@ -2974,14 +3455,16 @@ export function createNotificationsExtension(
|
|
|
2974
3455
|
disposeGateTerminalController: () => {},
|
|
2975
3456
|
disposeAckRecoveryParticipant: () => {},
|
|
2976
3457
|
disposeGateEmitterListener: () => {},
|
|
3458
|
+
trackGateResolution,
|
|
2977
3459
|
waitForGateResolutionQuiescence: async () => {
|
|
2978
3460
|
await Promise.allSettled(inFlightGateResolutions);
|
|
2979
3461
|
},
|
|
2980
3462
|
workflowGate: undefined,
|
|
2981
3463
|
gatePresentations,
|
|
2982
3464
|
stopping: false,
|
|
3465
|
+
abortEphemeralTurns: () => {},
|
|
3466
|
+
disableEphemeralTurns: () => {},
|
|
2983
3467
|
cancelPostmortemCleanup: () => {},
|
|
2984
|
-
stopBrokerHeartbeat,
|
|
2985
3468
|
|
|
2986
3469
|
redact,
|
|
2987
3470
|
verbosity,
|
|
@@ -2993,8 +3476,10 @@ export function createNotificationsExtension(
|
|
|
2993
3476
|
recordPromptTerminal,
|
|
2994
3477
|
emitPromptLifecycle,
|
|
2995
3478
|
pendingInbound: new Set<number>(),
|
|
3479
|
+
inFlightTools: new Map<string, { toolName: string; args: unknown }>(),
|
|
2996
3480
|
};
|
|
2997
|
-
|
|
3481
|
+
const initializedRuntime = runtime;
|
|
3482
|
+
runtimes.set(id, initializedRuntime);
|
|
2998
3483
|
activeRuntimeId = id;
|
|
2999
3484
|
const startSettled = Promise.withResolvers<SessionStartResult>();
|
|
3000
3485
|
sessionStartPromises.set(id, startSettled.promise);
|
|
@@ -3028,16 +3513,40 @@ export function createNotificationsExtension(
|
|
|
3028
3513
|
} catch {}
|
|
3029
3514
|
};
|
|
3030
3515
|
|
|
3516
|
+
const ephemeralTurns = new EphemeralTurnHost(sendSdkFrame, async (question, signal) => {
|
|
3517
|
+
if (!options.runEphemeralTurn) throw new Error("Ephemeral turns are unavailable.");
|
|
3518
|
+
return await options.runEphemeralTurn(prompt.render(btwUserPrompt, { question }), signal);
|
|
3519
|
+
});
|
|
3520
|
+
initializedRuntime.abortEphemeralTurns = () => ephemeralTurns.dispose();
|
|
3521
|
+
initializedRuntime.disableEphemeralTurns = () => ephemeralTurns.disable();
|
|
3031
3522
|
try {
|
|
3032
3523
|
server.onSdkFrame((err, inbound) => {
|
|
3033
3524
|
if (err || !inbound) return;
|
|
3034
3525
|
try {
|
|
3035
|
-
|
|
3526
|
+
const frame = JSON.parse(inbound.json) as unknown;
|
|
3527
|
+
if (!frame || typeof frame !== "object") return;
|
|
3528
|
+
const typedFrame = frame as Record<string, unknown>;
|
|
3529
|
+
if (typedFrame.type === "ephemeral_turn" || typedFrame.type === "ephemeral_turn_cancel") return;
|
|
3530
|
+
inboundSdkFrame?.(inbound.connectionId, typedFrame);
|
|
3036
3531
|
} catch {}
|
|
3037
3532
|
});
|
|
3533
|
+
// Required: the negotiated-capability callback is how the TS host learns
|
|
3534
|
+
// each connection's caps for replay-frame gating. If the linked
|
|
3535
|
+
// @gajae-code/natives binary predates it (linked/deduped installs where the
|
|
3536
|
+
// version did not change), fail loudly with an actionable message instead of
|
|
3537
|
+
// silently shipping a half-wired capability bridge.
|
|
3538
|
+
if (typeof server.onNegotiatedCapabilities !== "function") {
|
|
3539
|
+
throw new Error(
|
|
3540
|
+
"@gajae-code/natives is out of date: missing onNegotiatedCapabilities. Rebuild the native addon (bun --cwd=packages/natives run build).",
|
|
3541
|
+
);
|
|
3542
|
+
}
|
|
3543
|
+
server.onNegotiatedCapabilities((_err, connectionId, capabilities) => {
|
|
3544
|
+
if (connectionId) hostCapCache.set(connectionId, new Set(capabilities));
|
|
3545
|
+
});
|
|
3038
3546
|
server.onConnectionClose((_err, connectionId) => {
|
|
3039
3547
|
if (!connectionId) return;
|
|
3040
3548
|
host.handleDisconnect(connectionId);
|
|
3549
|
+
hostCapCache.delete(connectionId);
|
|
3041
3550
|
for (const submission of promptSubmissions.values())
|
|
3042
3551
|
if (submission.connectionId === connectionId) abandonPrompt(submission);
|
|
3043
3552
|
});
|
|
@@ -3259,6 +3768,11 @@ export function createNotificationsExtension(
|
|
|
3259
3768
|
// thread (forwarded by the daemon over the WS, fail-closed at the daemon).
|
|
3260
3769
|
server.onInbound((err, inbound) => {
|
|
3261
3770
|
if (err || !inbound) return;
|
|
3771
|
+
const authenticatedInbound = inbound as typeof inbound & {
|
|
3772
|
+
connectionId: string;
|
|
3773
|
+
messageId?: number;
|
|
3774
|
+
reason?: string;
|
|
3775
|
+
};
|
|
3262
3776
|
if (inbound.kind === "control_command") {
|
|
3263
3777
|
const frame = sdkInboundFrame(inbound.commandJson);
|
|
3264
3778
|
if (frame) {
|
|
@@ -3266,6 +3780,25 @@ export function createNotificationsExtension(
|
|
|
3266
3780
|
return;
|
|
3267
3781
|
}
|
|
3268
3782
|
}
|
|
3783
|
+
if (
|
|
3784
|
+
(inbound.kind === "ephemeral_turn" || inbound.kind === "ephemeral_turn_cancel") &&
|
|
3785
|
+
!runtime?.notificationsActive
|
|
3786
|
+
)
|
|
3787
|
+
return;
|
|
3788
|
+
if (inbound.kind === "ephemeral_turn" || inbound.kind === "ephemeral_turn_cancel") {
|
|
3789
|
+
ephemeralTurns.handle(authenticatedInbound.connectionId, {
|
|
3790
|
+
type: authenticatedInbound.kind,
|
|
3791
|
+
sessionId: authenticatedInbound.sessionId,
|
|
3792
|
+
requestId: authenticatedInbound.requestId,
|
|
3793
|
+
updateId: authenticatedInbound.updateId,
|
|
3794
|
+
messageId: authenticatedInbound.messageId,
|
|
3795
|
+
threadId: authenticatedInbound.threadId,
|
|
3796
|
+
...(authenticatedInbound.kind === "ephemeral_turn"
|
|
3797
|
+
? { question: authenticatedInbound.text }
|
|
3798
|
+
: { reason: authenticatedInbound.reason }),
|
|
3799
|
+
});
|
|
3800
|
+
return;
|
|
3801
|
+
}
|
|
3269
3802
|
|
|
3270
3803
|
if (inbound.kind === "user_message") {
|
|
3271
3804
|
// Inject as a user turn (steers/continues the agent; the resulting
|
|
@@ -3313,6 +3846,12 @@ export function createNotificationsExtension(
|
|
|
3313
3846
|
update.verbosity = inbound.verbosity;
|
|
3314
3847
|
}
|
|
3315
3848
|
if (typeof inbound.redact === "boolean") {
|
|
3849
|
+
// Redact turning ON: terminalize any already-visible in-flight tool
|
|
3850
|
+
// bubbles (while redact is still off so the helper emits) so they never
|
|
3851
|
+
// strand permanently in "started"; subsequent detail is then suppressed.
|
|
3852
|
+
if (inbound.redact && !runtime.redact) {
|
|
3853
|
+
terminalizeInFlightTools(runtime, runtime.id, "unknown");
|
|
3854
|
+
}
|
|
3316
3855
|
runtime.redact = inbound.redact;
|
|
3317
3856
|
update.redact = inbound.redact;
|
|
3318
3857
|
}
|
|
@@ -3394,6 +3933,11 @@ export function createNotificationsExtension(
|
|
|
3394
3933
|
};
|
|
3395
3934
|
host.emitEvent({ kind: identityHeader.type, payload: identityHeader });
|
|
3396
3935
|
const endpoint = await server.start();
|
|
3936
|
+
ephemeralTurns.configureAuthority({
|
|
3937
|
+
sessionId: id,
|
|
3938
|
+
endpointDigest: endpointAuthorityDigest(endpoint.url, token),
|
|
3939
|
+
eventGeneration: host.generation,
|
|
3940
|
+
});
|
|
3397
3941
|
throwIfLifecycleStopped();
|
|
3398
3942
|
if (runtimes.get(id) !== runtime) {
|
|
3399
3943
|
finishStartup({ status: "failed" });
|
|
@@ -3437,30 +3981,20 @@ export function createNotificationsExtension(
|
|
|
3437
3981
|
},
|
|
3438
3982
|
});
|
|
3439
3983
|
throwIfLifecycleStopped();
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
.append({
|
|
3444
|
-
type: "host_heartbeat",
|
|
3445
|
-
sessionId: id,
|
|
3446
|
-
locator,
|
|
3447
|
-
endpointGeneration: host.generation,
|
|
3448
|
-
pid: process.pid,
|
|
3449
|
-
})
|
|
3450
|
-
.catch(error => logger.warn(`sdk broker heartbeat failed: ${String(error)}`));
|
|
3451
|
-
}, 5_000);
|
|
3452
|
-
stopBrokerHeartbeat = () => clearInterval(timer);
|
|
3984
|
+
initializedRuntime.brokerRegistrationActive = true;
|
|
3985
|
+
// Host liveness is derived from alive(pid) when the index is read; heartbeats
|
|
3986
|
+
// are deliberately not appended to the durable session index.
|
|
3453
3987
|
} catch (brokerError) {
|
|
3454
3988
|
if (lifecycleRequired) throw brokerError;
|
|
3455
3989
|
logger.warn(`sdk broker registration skipped: ${String(brokerError)}`);
|
|
3456
3990
|
}
|
|
3457
3991
|
}
|
|
3458
3992
|
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
runtime.enableNotifications = () => {
|
|
3993
|
+
const startedRuntime = initializedRuntime;
|
|
3994
|
+
initializedRuntime.enableNotifications = () => {
|
|
3462
3995
|
const runtime = startedRuntime;
|
|
3463
3996
|
if (runtime.notificationsActive) return;
|
|
3997
|
+
ephemeralTurns.enable();
|
|
3464
3998
|
runtime.notificationsActive = true;
|
|
3465
3999
|
runtime.disposeAnswerSource = registerInteractiveAnswerSource(
|
|
3466
4000
|
runtime.id,
|
|
@@ -3471,35 +4005,42 @@ export function createNotificationsExtension(
|
|
|
3471
4005
|
if (runtime.redact) return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
|
|
3472
4006
|
try {
|
|
3473
4007
|
const data = await fs.promises.readFile(file.path);
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
4008
|
+
pushFileAttachment(
|
|
4009
|
+
runtime,
|
|
4010
|
+
{
|
|
4011
|
+
type: "file_attachment",
|
|
4012
|
+
sessionId: runtime.id,
|
|
4013
|
+
name: path.basename(file.path),
|
|
4014
|
+
caption: file.caption,
|
|
4015
|
+
},
|
|
4016
|
+
data,
|
|
4017
|
+
);
|
|
3481
4018
|
return { ok: true };
|
|
3482
4019
|
} catch (e) {
|
|
3483
4020
|
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
3484
4021
|
}
|
|
3485
4022
|
});
|
|
3486
4023
|
};
|
|
3487
|
-
const activeRuntime =
|
|
4024
|
+
const activeRuntime = initializedRuntime;
|
|
3488
4025
|
// A native terminal close (SIGHUP), SIGTERM, Ctrl+C exit, or fatal error
|
|
3489
4026
|
// skips AgentSession.dispose(), so the `session_shutdown` extension event
|
|
3490
4027
|
// never fires and the daemon-side topic would be orphaned. postmortem
|
|
3491
4028
|
// awaits registered cleanups on those paths, so send the graceful
|
|
3492
4029
|
// `session_closed` frame from there too. stopSession() cancels this
|
|
3493
4030
|
// registration on every other teardown path, so it never double-fires.
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
4031
|
+
initializedRuntime.cancelPostmortemCleanup = postmortem.register(
|
|
4032
|
+
`notifications-session-closed:${id}`,
|
|
4033
|
+
async () => {
|
|
4034
|
+
await stopSession(initializedRuntime.id);
|
|
4035
|
+
},
|
|
4036
|
+
);
|
|
3497
4037
|
logger.info(`notifications: serving session ${id} at ${endpoint.url}`);
|
|
3498
4038
|
// A workflow-gate emitter can be installed after session startup.
|
|
3499
4039
|
// Attach dynamically so the SDK bus presents every durable gate.
|
|
3500
4040
|
const attachWorkflowGate = (gate: WorkflowGateEmitter | undefined): void => {
|
|
3501
4041
|
if (activeRuntime.workflowGate === gate) return;
|
|
3502
4042
|
activeRuntime.disposeGateListener();
|
|
4043
|
+
activeRuntime.workflowGate?.setRuntimeTurnProvider?.(null);
|
|
3503
4044
|
activeRuntime.disposeAckRecoveryParticipant();
|
|
3504
4045
|
gatePresentations.dispose();
|
|
3505
4046
|
activeRuntime.disposeGateTerminalController();
|
|
@@ -3512,6 +4053,7 @@ export function createNotificationsExtension(
|
|
|
3512
4053
|
return;
|
|
3513
4054
|
}
|
|
3514
4055
|
activeRuntime.workflowGate = gate;
|
|
4056
|
+
gate.setRuntimeTurnProvider?.(() => activeRuntime.activePromptCorrelation?.turnId);
|
|
3515
4057
|
if (hasTerminalArbitrationCapability(gate)) {
|
|
3516
4058
|
const controller: WorkflowGateTerminalController = {
|
|
3517
4059
|
completeGateInteractions: gateId => gatePresentations.complete(gateId),
|
|
@@ -3565,18 +4107,30 @@ export function createNotificationsExtension(
|
|
|
3565
4107
|
});
|
|
3566
4108
|
activeRuntime.disposeAckRecoveryParticipant = () => gate.setAckRecoveryParticipant?.(null);
|
|
3567
4109
|
}
|
|
3568
|
-
void gate.recoverAcceptedGates
|
|
4110
|
+
void (typeof gate.recoverAcceptedGates === "function"
|
|
4111
|
+
? trackGateResolution(gate.recoverAcceptedGates()).catch(() => {})
|
|
4112
|
+
: Promise.resolve());
|
|
3569
4113
|
};
|
|
3570
4114
|
activeRuntime.disposeGateEmitterListener = registerWorkflowGateEmitterListener(id, attachWorkflowGate);
|
|
3571
4115
|
if (ctx.workflowGate) attachWorkflowGate(ctx.workflowGate);
|
|
3572
|
-
if (notificationsEnabledForSession)
|
|
3573
|
-
finishStartup({ status: "started", runtime });
|
|
3574
|
-
return { status: "started", runtime };
|
|
4116
|
+
if (notificationsEnabledForSession) initializedRuntime.enableNotifications();
|
|
4117
|
+
finishStartup({ status: "started", runtime: initializedRuntime });
|
|
4118
|
+
return { status: "started", runtime: initializedRuntime };
|
|
3575
4119
|
} catch (e) {
|
|
3576
4120
|
logger.warn(`notifications: failed to start server: ${String(e)}`);
|
|
3577
4121
|
const result = failLifecycleStartup("failed", e);
|
|
3578
4122
|
finishStartup(result);
|
|
3579
|
-
|
|
4123
|
+
let stopped = false;
|
|
4124
|
+
try {
|
|
4125
|
+
stopped = await stopSession(id, "session", runtime);
|
|
4126
|
+
} catch (error) {
|
|
4127
|
+
// A secondary owner-release failure during abandoned-startup cleanup is
|
|
4128
|
+
// retained for an explicit later retry via cleanupRetries; log it rather
|
|
4129
|
+
// than letting it escape startSession and surface a red extension error
|
|
4130
|
+
// through session_start / session_switch / session_branch.
|
|
4131
|
+
logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
|
|
4132
|
+
}
|
|
4133
|
+
if (!stopped) await cleanupAbandonedStartup();
|
|
3580
4134
|
return { ...result, runtime };
|
|
3581
4135
|
}
|
|
3582
4136
|
}
|
|
@@ -3701,13 +4255,27 @@ export function createNotificationsExtension(
|
|
|
3701
4255
|
event: { previousSessionFile?: string },
|
|
3702
4256
|
ctx: ExtensionContext,
|
|
3703
4257
|
): Promise<void> => {
|
|
4258
|
+
if (extensionShuttingDown) return;
|
|
3704
4259
|
const newId = sessionId(ctx);
|
|
3705
4260
|
const prevId = activeRuntimeId ?? sessionIdFromFile(event.previousSessionFile);
|
|
3706
4261
|
if (prevId && prevId !== newId) {
|
|
3707
4262
|
controller.rekeySession(prevId, newId);
|
|
3708
|
-
|
|
4263
|
+
try {
|
|
4264
|
+
await stopSession(prevId);
|
|
4265
|
+
} catch (error) {
|
|
4266
|
+
// A retained owner-release failure keeps the exact runtime in
|
|
4267
|
+
// cleanupRetries for an explicit later retry; log it rather than
|
|
4268
|
+
// surfacing a red extension error
|
|
4269
|
+
// while rotating session authority (/new, fork, resume, branch).
|
|
4270
|
+
logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
|
|
4271
|
+
}
|
|
3709
4272
|
}
|
|
4273
|
+
if (extensionShuttingDown) return;
|
|
3710
4274
|
await startSession(ctx);
|
|
4275
|
+
if (extensionShuttingDown) {
|
|
4276
|
+
await stopSession(newId);
|
|
4277
|
+
return;
|
|
4278
|
+
}
|
|
3711
4279
|
await controller.reconcileCurrentSession(ctx);
|
|
3712
4280
|
};
|
|
3713
4281
|
api.on("session_switch", async (event, ctx) => {
|
|
@@ -3725,6 +4293,19 @@ export function createNotificationsExtension(
|
|
|
3725
4293
|
await rotateSessionAuthority(event, ctx);
|
|
3726
4294
|
});
|
|
3727
4295
|
|
|
4296
|
+
const terminalizeInFlightTools = (rt: SessionRuntime, id: string, phase: "cancelled" | "unknown"): void => {
|
|
4297
|
+
if (rt.notificationsActive && !rt.redact) {
|
|
4298
|
+
for (const [toolCallId, { toolName }] of rt.inFlightTools) {
|
|
4299
|
+
try {
|
|
4300
|
+
pushSessionFrame(rt, { type: "tool_activity", sessionId: id, toolCallId, toolName, phase });
|
|
4301
|
+
} catch (e) {
|
|
4302
|
+
logger.warn(`notifications: synthetic tool_activity failed: ${String(e)}`);
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
}
|
|
4306
|
+
rt.inFlightTools.clear();
|
|
4307
|
+
};
|
|
4308
|
+
|
|
3728
4309
|
// Drive the live typing indicator: mark busy when the agent loop starts so
|
|
3729
4310
|
// the daemon shows "typing…" in the thread while the agent is thinking,
|
|
3730
4311
|
// before any turn output exists. Cleared on `agent_end` below.
|
|
@@ -3753,7 +4334,9 @@ export function createNotificationsExtension(
|
|
|
3753
4334
|
api.on("turn_start", (_event, ctx) => {
|
|
3754
4335
|
const id = sessionId(ctx);
|
|
3755
4336
|
const rt = runtimes.get(id);
|
|
3756
|
-
if (!rt
|
|
4337
|
+
if (!rt) return;
|
|
4338
|
+
rt.turnSeq = (rt.turnSeq ?? 0) + 1;
|
|
4339
|
+
if (!rt.notificationsActive) return;
|
|
3757
4340
|
// A new turn is live: re-open the live-stream window (see turnClosed).
|
|
3758
4341
|
rt.turnClosed = false;
|
|
3759
4342
|
if (rt.pendingInbound.size === 0) return;
|
|
@@ -3771,7 +4354,7 @@ export function createNotificationsExtension(
|
|
|
3771
4354
|
// per `turn_end`. turn_end fires once per turn iteration, so a single
|
|
3772
4355
|
// user-visible idle previously produced many idle pings (the flood); agent_end
|
|
3773
4356
|
// fires exactly once per settle, yielding exactly one idle notification.
|
|
3774
|
-
api.on("agent_end", (
|
|
4357
|
+
api.on("agent_end", (event, ctx) => {
|
|
3775
4358
|
const id = sessionId(ctx);
|
|
3776
4359
|
const rt = runtimes.get(id);
|
|
3777
4360
|
if (!rt) return;
|
|
@@ -3784,13 +4367,17 @@ export function createNotificationsExtension(
|
|
|
3784
4367
|
} else {
|
|
3785
4368
|
rt.emitPromptLifecycle(undefined, { type: "agent_end", sessionId: id });
|
|
3786
4369
|
}
|
|
4370
|
+
rt.activePromptCorrelation = undefined;
|
|
4371
|
+
terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "unknown");
|
|
3787
4372
|
try {
|
|
3788
4373
|
pushSessionFrame(rt, { type: "activity", sessionId: id, state: "idle" });
|
|
3789
4374
|
} catch (e) {
|
|
3790
4375
|
logger.warn(`notifications: activity (idle) failed: ${String(e)}`);
|
|
3791
4376
|
}
|
|
3792
4377
|
if (!rt.notificationsActive) return;
|
|
3793
|
-
void rt.workflowGate?.recoverAcceptedGates
|
|
4378
|
+
void (typeof rt.workflowGate?.recoverAcceptedGates === "function"
|
|
4379
|
+
? rt.trackGateResolution(rt.workflowGate.recoverAcceptedGates()).catch(() => {})
|
|
4380
|
+
: Promise.resolve());
|
|
3794
4381
|
const seq = rt.idleSeq++;
|
|
3795
4382
|
// Re-assert the identity header so the daemon renames the topic once the
|
|
3796
4383
|
// session title has been auto-generated ("{repo}/{branch} - {title}"). The
|
|
@@ -3862,11 +4449,10 @@ export function createNotificationsExtension(
|
|
|
3862
4449
|
rt.preAskFlushedText = text;
|
|
3863
4450
|
// Decision A: a stream-enabled turn must finalize as an in-place edit of ONE
|
|
3864
4451
|
// live message, never a fresh (rich-promotable) send. If live frames were
|
|
3865
|
-
// async-queued and none landed before this flush,
|
|
3866
|
-
//
|
|
3867
|
-
//
|
|
3868
|
-
if (finalAnswer && rt.stream && rt.liveRef === undefined) {
|
|
3869
|
-
rt.turnSeq = (rt.turnSeq ?? 0) + 1;
|
|
4452
|
+
// async-queued and none landed before this flush, reuse the per-turn ref
|
|
4453
|
+
// assigned at turn_start so the finalized frame remains editable (HTML edit)
|
|
4454
|
+
// and never rich-promotes a streamed final.
|
|
4455
|
+
if (finalAnswer && rt.stream && rt.liveRef === undefined && rt.turnSeq !== undefined) {
|
|
3870
4456
|
rt.liveRef = String(rt.turnSeq);
|
|
3871
4457
|
}
|
|
3872
4458
|
try {
|
|
@@ -3890,11 +4476,99 @@ export function createNotificationsExtension(
|
|
|
3890
4476
|
// path and ordered before it — unlike message_update, which is queued async),
|
|
3891
4477
|
// then flushed here before the ask tool's execute calls registerAsk.
|
|
3892
4478
|
api.on("tool_execution_start", (event, ctx) => {
|
|
3893
|
-
if (event.toolName
|
|
4479
|
+
if (event.toolName === "ask") {
|
|
4480
|
+
const id = sessionId(ctx);
|
|
4481
|
+
const rt = runtimes.get(id);
|
|
4482
|
+
if (!rt?.notificationsActive || rt.redact) return;
|
|
4483
|
+
flushTurnText(rt, id, rt.currentTurnText, false);
|
|
4484
|
+
}
|
|
3894
4485
|
const id = sessionId(ctx);
|
|
3895
4486
|
const rt = runtimes.get(id);
|
|
3896
4487
|
if (!rt?.notificationsActive || rt.redact) return;
|
|
3897
|
-
|
|
4488
|
+
rt.inFlightTools.set(event.toolCallId, { toolName: event.toolName, args: event.args });
|
|
4489
|
+
try {
|
|
4490
|
+
pushSessionFrame(rt, {
|
|
4491
|
+
type: "tool_activity",
|
|
4492
|
+
sessionId: id,
|
|
4493
|
+
toolCallId: event.toolCallId,
|
|
4494
|
+
toolName: event.toolName,
|
|
4495
|
+
phase: "started",
|
|
4496
|
+
});
|
|
4497
|
+
} catch (e) {
|
|
4498
|
+
logger.warn(`notifications: tool_activity start failed: ${String(e)}`);
|
|
4499
|
+
}
|
|
4500
|
+
});
|
|
4501
|
+
|
|
4502
|
+
api.on("tool_execution_end", (event, ctx) => {
|
|
4503
|
+
const id = sessionId(ctx);
|
|
4504
|
+
const rt = runtimes.get(id);
|
|
4505
|
+
if (!rt) return;
|
|
4506
|
+
const inFlight = rt.inFlightTools.get(event.toolCallId);
|
|
4507
|
+
if (!rt.notificationsActive || rt.redact) {
|
|
4508
|
+
rt.inFlightTools.delete(event.toolCallId);
|
|
4509
|
+
return;
|
|
4510
|
+
}
|
|
4511
|
+
try {
|
|
4512
|
+
const frame: {
|
|
4513
|
+
type: "tool_activity";
|
|
4514
|
+
sessionId: string;
|
|
4515
|
+
toolCallId: string;
|
|
4516
|
+
toolName: string;
|
|
4517
|
+
phase: "completed" | "failed";
|
|
4518
|
+
isError: boolean;
|
|
4519
|
+
argsSummary?: string;
|
|
4520
|
+
resultSummary?: string;
|
|
4521
|
+
} = {
|
|
4522
|
+
type: "tool_activity",
|
|
4523
|
+
sessionId: id,
|
|
4524
|
+
toolCallId: event.toolCallId,
|
|
4525
|
+
toolName: event.toolName,
|
|
4526
|
+
phase: event.isError ? "failed" : "completed",
|
|
4527
|
+
isError: event.isError,
|
|
4528
|
+
};
|
|
4529
|
+
if (rt.verbosity === "verbose") {
|
|
4530
|
+
const tool = ctx.resolveTool(event.toolName);
|
|
4531
|
+
const argsSummary = projectToolSummary(tool, "args", inFlight?.args);
|
|
4532
|
+
const resultSummary = projectToolSummary(tool, "result", event.result);
|
|
4533
|
+
if (argsSummary !== undefined) frame.argsSummary = argsSummary;
|
|
4534
|
+
if (resultSummary !== undefined) frame.resultSummary = resultSummary;
|
|
4535
|
+
}
|
|
4536
|
+
pushSessionFrame(rt, frame);
|
|
4537
|
+
} catch (e) {
|
|
4538
|
+
logger.warn(`notifications: tool_activity end failed: ${String(e)}`);
|
|
4539
|
+
} finally {
|
|
4540
|
+
rt.inFlightTools.delete(event.toolCallId);
|
|
4541
|
+
}
|
|
4542
|
+
});
|
|
4543
|
+
|
|
4544
|
+
api.on("reasoning_summary_end", (event, ctx) => {
|
|
4545
|
+
const id = sessionId(ctx);
|
|
4546
|
+
const rt = runtimes.get(id);
|
|
4547
|
+
if (!rt?.notificationsActive || rt.redact || rt.verbosity !== "verbose") return;
|
|
4548
|
+
if (!event.message || typeof event.message !== "object" || !("content" in event.message)) return;
|
|
4549
|
+
const content = event.message.content;
|
|
4550
|
+
if (!Array.isArray(content)) return;
|
|
4551
|
+
const block = content[event.contentIndex];
|
|
4552
|
+
if (block?.type !== "thinking" || (block.provenance !== "summary" && block.provenance !== "mixed")) return;
|
|
4553
|
+
// CoT boundary: emit ONLY the canonical provider-marked summaryText. Never
|
|
4554
|
+
// fall back to the event payload, which could carry inconsistent/mutated text.
|
|
4555
|
+
const text = block.summaryText;
|
|
4556
|
+
if (typeof text !== "string" || text === "") return;
|
|
4557
|
+
try {
|
|
4558
|
+
pushSessionFrame(rt, {
|
|
4559
|
+
type: "reasoning_summary",
|
|
4560
|
+
sessionId: id,
|
|
4561
|
+
text,
|
|
4562
|
+
// Coalesce on the reasoning block's stable itemId carried on the event, NOT
|
|
4563
|
+
// the mutable rt.turnSeq: a streamed reasoning_summary_end is queued async and
|
|
4564
|
+
// turn_start for the next iteration advances turnSeq synchronously first, so
|
|
4565
|
+
// reading turnSeq here could bind turn N's summary to turn N+1. Absent an
|
|
4566
|
+
// itemId, omit turnRef (threaded-render sends a fresh non-editable message).
|
|
4567
|
+
...((block as { itemId?: string }).itemId ? { turnRef: (block as { itemId?: string }).itemId } : {}),
|
|
4568
|
+
});
|
|
4569
|
+
} catch (e) {
|
|
4570
|
+
logger.warn(`notifications: reasoning_summary failed: ${String(e)}`);
|
|
4571
|
+
}
|
|
3898
4572
|
});
|
|
3899
4573
|
|
|
3900
4574
|
api.on("turn_end", (event, ctx) => {
|
|
@@ -3924,8 +4598,7 @@ export function createNotificationsExtension(
|
|
|
3924
4598
|
const rt = runtimes.get(id);
|
|
3925
4599
|
if (!rt?.notificationsActive || !rt.stream || rt.redact || rt.turnClosed) return;
|
|
3926
4600
|
if ((event.message as { role?: unknown }).role !== "assistant") return;
|
|
3927
|
-
if (rt.liveRef === undefined) {
|
|
3928
|
-
rt.turnSeq = (rt.turnSeq ?? 0) + 1;
|
|
4601
|
+
if (rt.liveRef === undefined && rt.turnSeq !== undefined) {
|
|
3929
4602
|
rt.liveRef = String(rt.turnSeq);
|
|
3930
4603
|
}
|
|
3931
4604
|
const now = Date.now();
|
|
@@ -3971,9 +4644,24 @@ export function createNotificationsExtension(
|
|
|
3971
4644
|
});
|
|
3972
4645
|
|
|
3973
4646
|
api.on("session_shutdown", async (_event, ctx) => {
|
|
4647
|
+
extensionShuttingDown = true;
|
|
4648
|
+
identityControlInFlight = false;
|
|
4649
|
+
deferredIdentityRotation = undefined;
|
|
4650
|
+
const id = sessionId(ctx);
|
|
4651
|
+
const rt = runtimes.get(id);
|
|
4652
|
+
if (rt) terminalizeInFlightTools(rt, id, "unknown");
|
|
3974
4653
|
const controllerStop =
|
|
3975
4654
|
typeof ctx.sessionManager.getCwd === "function" ? controller.stopCurrentSession(ctx) : Promise.resolve(false);
|
|
3976
4655
|
void controllerStop.catch(error => logger.warn(`notifications: controller shutdown failed: ${String(error)}`));
|
|
3977
|
-
|
|
4656
|
+
try {
|
|
4657
|
+
await stopSession(id);
|
|
4658
|
+
} catch (error) {
|
|
4659
|
+
// A retained owner-release failure keeps the exact runtime in
|
|
4660
|
+
// cleanupRetries for an explicit later retry; log it rather than
|
|
4661
|
+
// surfacing a red extension error at
|
|
4662
|
+
// shutdown. On terminal quit there is no later retry cycle, so log at
|
|
4663
|
+
// error severity (matching the postmortem cleanup precedent).
|
|
4664
|
+
logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
|
|
4665
|
+
}
|
|
3978
4666
|
});
|
|
3979
4667
|
}
|