@gajae-code/coding-agent 0.11.1 → 0.11.2
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 +54 -0
- package/dist/types/cli/config-cli.d.ts +11 -1
- 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 +56 -1
- 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 +3 -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/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 +27 -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/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 +3 -0
- package/dist/types/tools/search-tool-bm25.d.ts +3 -4
- package/dist/types/utils/event-bus.d.ts +1 -1
- 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/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 +159 -2
- 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 +320 -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 +28 -8
- 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/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 +778 -93
- 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 +31 -1
- 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 +458 -479
- 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 +13 -5
- package/src/tools/read.ts +37 -1
- package/src/tools/resource-gc.ts +2 -2
- package/src/tools/search-tool-bm25.ts +11 -74
- package/src/utils/event-bus.ts +9 -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;
|
|
@@ -2287,6 +2743,7 @@ export function createNotificationsExtension(
|
|
|
2287
2743
|
controller?: NotificationSessionController;
|
|
2288
2744
|
|
|
2289
2745
|
onSdkRequest?: (kind: "control" | "query", connectionId: string, frame: Record<string, unknown>) => void;
|
|
2746
|
+
runEphemeralTurn?: (promptText: string, signal: AbortSignal) => Promise<{ replyText: string }>;
|
|
2290
2747
|
} = {},
|
|
2291
2748
|
): void {
|
|
2292
2749
|
const lifecycleStartupCapability = lifecycleStartupCapabilityForApi(api);
|
|
@@ -2305,6 +2762,7 @@ export function createNotificationsExtension(
|
|
|
2305
2762
|
let activeRuntimeId: string | undefined;
|
|
2306
2763
|
let identityControlInFlight = false;
|
|
2307
2764
|
let deferredIdentityRotation: { event: { previousSessionFile?: string }; ctx: ExtensionContext } | undefined;
|
|
2765
|
+
let extensionShuttingDown = false;
|
|
2308
2766
|
|
|
2309
2767
|
async function ensureTelegramOwner(
|
|
2310
2768
|
settings: Settings,
|
|
@@ -2352,7 +2810,10 @@ export function createNotificationsExtension(
|
|
|
2352
2810
|
const activeRuntime = runtimes.get(id);
|
|
2353
2811
|
const requestedRuntime = retryRuntime ?? activeRuntime;
|
|
2354
2812
|
if (expectedRuntime && requestedRuntime !== expectedRuntime) return false;
|
|
2355
|
-
if (reason === "session" && requestedRuntime)
|
|
2813
|
+
if (reason === "session" && requestedRuntime) {
|
|
2814
|
+
requestedRuntime.stopping = true;
|
|
2815
|
+
requestedRuntime.abortEphemeralTurns();
|
|
2816
|
+
}
|
|
2356
2817
|
if (reason === "session" && requestedRuntime) {
|
|
2357
2818
|
// Fence the exact runtime before awaiting its startup promise: a late start
|
|
2358
2819
|
// must observe removal and clean itself up rather than becoming reachable.
|
|
@@ -2365,7 +2826,12 @@ export function createNotificationsExtension(
|
|
|
2365
2826
|
.catch(() => {})
|
|
2366
2827
|
.then(() => {
|
|
2367
2828
|
if (runtimes.get(id) === requestedRuntime || cleanupRetries.get(id) === requestedRuntime)
|
|
2368
|
-
void stopSession(id, reason, requestedRuntime)
|
|
2829
|
+
void stopSession(id, reason, requestedRuntime).catch(error =>
|
|
2830
|
+
// A retained owner-release failure keeps the exact runtime in
|
|
2831
|
+
// cleanupRetries for a later retry; log it rather than letting a
|
|
2832
|
+
// fire-and-forget rejection become a fatal unhandled rejection.
|
|
2833
|
+
logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`),
|
|
2834
|
+
);
|
|
2369
2835
|
});
|
|
2370
2836
|
const rt = requestedRuntime;
|
|
2371
2837
|
|
|
@@ -2377,6 +2843,7 @@ export function createNotificationsExtension(
|
|
|
2377
2843
|
}
|
|
2378
2844
|
if (reason === "notifications" && rt.host.started) {
|
|
2379
2845
|
rt.notificationsActive = false;
|
|
2846
|
+
rt.disableEphemeralTurns();
|
|
2380
2847
|
try {
|
|
2381
2848
|
rt.disposeAnswerSource();
|
|
2382
2849
|
} catch {}
|
|
@@ -2395,9 +2862,6 @@ export function createNotificationsExtension(
|
|
|
2395
2862
|
try {
|
|
2396
2863
|
rt.cancelPostmortemCleanup();
|
|
2397
2864
|
} catch {}
|
|
2398
|
-
try {
|
|
2399
|
-
rt.stopBrokerHeartbeat();
|
|
2400
|
-
} catch {}
|
|
2401
2865
|
try {
|
|
2402
2866
|
rt.disposeAnswerSource();
|
|
2403
2867
|
} catch {}
|
|
@@ -2407,6 +2871,9 @@ export function createNotificationsExtension(
|
|
|
2407
2871
|
try {
|
|
2408
2872
|
rt.disposeGateListener();
|
|
2409
2873
|
} catch {}
|
|
2874
|
+
try {
|
|
2875
|
+
rt.workflowGate?.setRuntimeTurnProvider?.(null);
|
|
2876
|
+
} catch {}
|
|
2410
2877
|
await rt.waitForGateResolutionQuiescence();
|
|
2411
2878
|
try {
|
|
2412
2879
|
rt.disposeAckRecoveryParticipant();
|
|
@@ -2559,7 +3026,6 @@ export function createNotificationsExtension(
|
|
|
2559
3026
|
}
|
|
2560
3027
|
const gatePresentations = new PresentationArbiter(server, () => runtime?.redact ?? redact, tag);
|
|
2561
3028
|
let inboundSdkFrame: ((connectionId: string, frame: Record<string, unknown>) => void) | undefined;
|
|
2562
|
-
let stopBrokerHeartbeat = () => {};
|
|
2563
3029
|
const inFlightGateResolutions = new Set<Promise<void>>();
|
|
2564
3030
|
const trackGateResolution = <T>(resolution: Promise<T>): Promise<T> => {
|
|
2565
3031
|
const quiesced = resolution.then(
|
|
@@ -2601,6 +3067,8 @@ export function createNotificationsExtension(
|
|
|
2601
3067
|
if (capability === "permission") ctx.setSdkPermissionProvider?.(undefined);
|
|
2602
3068
|
};
|
|
2603
3069
|
|
|
3070
|
+
const hostCapCache = new Map<string, ReadonlySet<string>>();
|
|
3071
|
+
|
|
2604
3072
|
const configOverrides = new Map<string, unknown>();
|
|
2605
3073
|
const configRevision = { current: 0 };
|
|
2606
3074
|
const PROMPT_SUBMISSION_CAPACITY = 128;
|
|
@@ -2759,6 +3227,11 @@ export function createNotificationsExtension(
|
|
|
2759
3227
|
submission.failed = true;
|
|
2760
3228
|
submission.error = error;
|
|
2761
3229
|
removePendingPromptCorrelation(correlation);
|
|
3230
|
+
if (
|
|
3231
|
+
runtime?.activePromptCorrelation?.commandId === correlation.commandId &&
|
|
3232
|
+
runtime.activePromptCorrelation.turnId === correlation.turnId
|
|
3233
|
+
)
|
|
3234
|
+
runtime.activePromptCorrelation = undefined;
|
|
2762
3235
|
emitPromptFailure(correlation, error);
|
|
2763
3236
|
};
|
|
2764
3237
|
const acknowledgePrompt = (connectionId: string, correlation: { commandId: string; turnId: string }) => {
|
|
@@ -2826,6 +3299,10 @@ export function createNotificationsExtension(
|
|
|
2826
3299
|
};
|
|
2827
3300
|
|
|
2828
3301
|
const sendSdkFrame = (connectionId: string, frame: Record<string, unknown>) => {
|
|
3302
|
+
if (extensionShuttingDown || runtime?.stopping || runtimes.get(id) !== runtime) {
|
|
3303
|
+
abandonPromptResponse(connectionId, frame);
|
|
3304
|
+
return;
|
|
3305
|
+
}
|
|
2829
3306
|
const json = JSON.stringify(frame);
|
|
2830
3307
|
if (connectionId.startsWith("seam:")) {
|
|
2831
3308
|
try {
|
|
@@ -2857,6 +3334,7 @@ export function createNotificationsExtension(
|
|
|
2857
3334
|
stateRoot,
|
|
2858
3335
|
token,
|
|
2859
3336
|
sendFrame: (connectionId, frame) => sendSdkFrame(connectionId, frame),
|
|
3337
|
+
connectionCapabilities: connectionId => hostCapCache.get(connectionId) ?? EMPTY_CAPABILITIES,
|
|
2860
3338
|
installProviderDefinitions,
|
|
2861
3339
|
onProviderDefinitionsRemoved: removeProviderDefinitions,
|
|
2862
3340
|
onFrame: handler => {
|
|
@@ -2974,14 +3452,16 @@ export function createNotificationsExtension(
|
|
|
2974
3452
|
disposeGateTerminalController: () => {},
|
|
2975
3453
|
disposeAckRecoveryParticipant: () => {},
|
|
2976
3454
|
disposeGateEmitterListener: () => {},
|
|
3455
|
+
trackGateResolution,
|
|
2977
3456
|
waitForGateResolutionQuiescence: async () => {
|
|
2978
3457
|
await Promise.allSettled(inFlightGateResolutions);
|
|
2979
3458
|
},
|
|
2980
3459
|
workflowGate: undefined,
|
|
2981
3460
|
gatePresentations,
|
|
2982
3461
|
stopping: false,
|
|
3462
|
+
abortEphemeralTurns: () => {},
|
|
3463
|
+
disableEphemeralTurns: () => {},
|
|
2983
3464
|
cancelPostmortemCleanup: () => {},
|
|
2984
|
-
stopBrokerHeartbeat,
|
|
2985
3465
|
|
|
2986
3466
|
redact,
|
|
2987
3467
|
verbosity,
|
|
@@ -2993,8 +3473,10 @@ export function createNotificationsExtension(
|
|
|
2993
3473
|
recordPromptTerminal,
|
|
2994
3474
|
emitPromptLifecycle,
|
|
2995
3475
|
pendingInbound: new Set<number>(),
|
|
3476
|
+
inFlightTools: new Map<string, { toolName: string; args: unknown }>(),
|
|
2996
3477
|
};
|
|
2997
|
-
|
|
3478
|
+
const initializedRuntime = runtime;
|
|
3479
|
+
runtimes.set(id, initializedRuntime);
|
|
2998
3480
|
activeRuntimeId = id;
|
|
2999
3481
|
const startSettled = Promise.withResolvers<SessionStartResult>();
|
|
3000
3482
|
sessionStartPromises.set(id, startSettled.promise);
|
|
@@ -3028,16 +3510,40 @@ export function createNotificationsExtension(
|
|
|
3028
3510
|
} catch {}
|
|
3029
3511
|
};
|
|
3030
3512
|
|
|
3513
|
+
const ephemeralTurns = new EphemeralTurnHost(sendSdkFrame, async (question, signal) => {
|
|
3514
|
+
if (!options.runEphemeralTurn) throw new Error("Ephemeral turns are unavailable.");
|
|
3515
|
+
return await options.runEphemeralTurn(prompt.render(btwUserPrompt, { question }), signal);
|
|
3516
|
+
});
|
|
3517
|
+
initializedRuntime.abortEphemeralTurns = () => ephemeralTurns.dispose();
|
|
3518
|
+
initializedRuntime.disableEphemeralTurns = () => ephemeralTurns.disable();
|
|
3031
3519
|
try {
|
|
3032
3520
|
server.onSdkFrame((err, inbound) => {
|
|
3033
3521
|
if (err || !inbound) return;
|
|
3034
3522
|
try {
|
|
3035
|
-
|
|
3523
|
+
const frame = JSON.parse(inbound.json) as unknown;
|
|
3524
|
+
if (!frame || typeof frame !== "object") return;
|
|
3525
|
+
const typedFrame = frame as Record<string, unknown>;
|
|
3526
|
+
if (typedFrame.type === "ephemeral_turn" || typedFrame.type === "ephemeral_turn_cancel") return;
|
|
3527
|
+
inboundSdkFrame?.(inbound.connectionId, typedFrame);
|
|
3036
3528
|
} catch {}
|
|
3037
3529
|
});
|
|
3530
|
+
// Required: the negotiated-capability callback is how the TS host learns
|
|
3531
|
+
// each connection's caps for replay-frame gating. If the linked
|
|
3532
|
+
// @gajae-code/natives binary predates it (linked/deduped installs where the
|
|
3533
|
+
// version did not change), fail loudly with an actionable message instead of
|
|
3534
|
+
// silently shipping a half-wired capability bridge.
|
|
3535
|
+
if (typeof server.onNegotiatedCapabilities !== "function") {
|
|
3536
|
+
throw new Error(
|
|
3537
|
+
"@gajae-code/natives is out of date: missing onNegotiatedCapabilities. Rebuild the native addon (bun --cwd=packages/natives run build).",
|
|
3538
|
+
);
|
|
3539
|
+
}
|
|
3540
|
+
server.onNegotiatedCapabilities((_err, connectionId, capabilities) => {
|
|
3541
|
+
if (connectionId) hostCapCache.set(connectionId, new Set(capabilities));
|
|
3542
|
+
});
|
|
3038
3543
|
server.onConnectionClose((_err, connectionId) => {
|
|
3039
3544
|
if (!connectionId) return;
|
|
3040
3545
|
host.handleDisconnect(connectionId);
|
|
3546
|
+
hostCapCache.delete(connectionId);
|
|
3041
3547
|
for (const submission of promptSubmissions.values())
|
|
3042
3548
|
if (submission.connectionId === connectionId) abandonPrompt(submission);
|
|
3043
3549
|
});
|
|
@@ -3259,6 +3765,11 @@ export function createNotificationsExtension(
|
|
|
3259
3765
|
// thread (forwarded by the daemon over the WS, fail-closed at the daemon).
|
|
3260
3766
|
server.onInbound((err, inbound) => {
|
|
3261
3767
|
if (err || !inbound) return;
|
|
3768
|
+
const authenticatedInbound = inbound as typeof inbound & {
|
|
3769
|
+
connectionId: string;
|
|
3770
|
+
messageId?: number;
|
|
3771
|
+
reason?: string;
|
|
3772
|
+
};
|
|
3262
3773
|
if (inbound.kind === "control_command") {
|
|
3263
3774
|
const frame = sdkInboundFrame(inbound.commandJson);
|
|
3264
3775
|
if (frame) {
|
|
@@ -3266,6 +3777,25 @@ export function createNotificationsExtension(
|
|
|
3266
3777
|
return;
|
|
3267
3778
|
}
|
|
3268
3779
|
}
|
|
3780
|
+
if (
|
|
3781
|
+
(inbound.kind === "ephemeral_turn" || inbound.kind === "ephemeral_turn_cancel") &&
|
|
3782
|
+
!runtime?.notificationsActive
|
|
3783
|
+
)
|
|
3784
|
+
return;
|
|
3785
|
+
if (inbound.kind === "ephemeral_turn" || inbound.kind === "ephemeral_turn_cancel") {
|
|
3786
|
+
ephemeralTurns.handle(authenticatedInbound.connectionId, {
|
|
3787
|
+
type: authenticatedInbound.kind,
|
|
3788
|
+
sessionId: authenticatedInbound.sessionId,
|
|
3789
|
+
requestId: authenticatedInbound.requestId,
|
|
3790
|
+
updateId: authenticatedInbound.updateId,
|
|
3791
|
+
messageId: authenticatedInbound.messageId,
|
|
3792
|
+
threadId: authenticatedInbound.threadId,
|
|
3793
|
+
...(authenticatedInbound.kind === "ephemeral_turn"
|
|
3794
|
+
? { question: authenticatedInbound.text }
|
|
3795
|
+
: { reason: authenticatedInbound.reason }),
|
|
3796
|
+
});
|
|
3797
|
+
return;
|
|
3798
|
+
}
|
|
3269
3799
|
|
|
3270
3800
|
if (inbound.kind === "user_message") {
|
|
3271
3801
|
// Inject as a user turn (steers/continues the agent; the resulting
|
|
@@ -3313,6 +3843,12 @@ export function createNotificationsExtension(
|
|
|
3313
3843
|
update.verbosity = inbound.verbosity;
|
|
3314
3844
|
}
|
|
3315
3845
|
if (typeof inbound.redact === "boolean") {
|
|
3846
|
+
// Redact turning ON: terminalize any already-visible in-flight tool
|
|
3847
|
+
// bubbles (while redact is still off so the helper emits) so they never
|
|
3848
|
+
// strand permanently in "started"; subsequent detail is then suppressed.
|
|
3849
|
+
if (inbound.redact && !runtime.redact) {
|
|
3850
|
+
terminalizeInFlightTools(runtime, runtime.id, "unknown");
|
|
3851
|
+
}
|
|
3316
3852
|
runtime.redact = inbound.redact;
|
|
3317
3853
|
update.redact = inbound.redact;
|
|
3318
3854
|
}
|
|
@@ -3394,6 +3930,11 @@ export function createNotificationsExtension(
|
|
|
3394
3930
|
};
|
|
3395
3931
|
host.emitEvent({ kind: identityHeader.type, payload: identityHeader });
|
|
3396
3932
|
const endpoint = await server.start();
|
|
3933
|
+
ephemeralTurns.configureAuthority({
|
|
3934
|
+
sessionId: id,
|
|
3935
|
+
endpointDigest: endpointAuthorityDigest(endpoint.url, token),
|
|
3936
|
+
eventGeneration: host.generation,
|
|
3937
|
+
});
|
|
3397
3938
|
throwIfLifecycleStopped();
|
|
3398
3939
|
if (runtimes.get(id) !== runtime) {
|
|
3399
3940
|
finishStartup({ status: "failed" });
|
|
@@ -3437,30 +3978,20 @@ export function createNotificationsExtension(
|
|
|
3437
3978
|
},
|
|
3438
3979
|
});
|
|
3439
3980
|
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);
|
|
3981
|
+
initializedRuntime.brokerRegistrationActive = true;
|
|
3982
|
+
// Host liveness is derived from alive(pid) when the index is read; heartbeats
|
|
3983
|
+
// are deliberately not appended to the durable session index.
|
|
3453
3984
|
} catch (brokerError) {
|
|
3454
3985
|
if (lifecycleRequired) throw brokerError;
|
|
3455
3986
|
logger.warn(`sdk broker registration skipped: ${String(brokerError)}`);
|
|
3456
3987
|
}
|
|
3457
3988
|
}
|
|
3458
3989
|
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
runtime.enableNotifications = () => {
|
|
3990
|
+
const startedRuntime = initializedRuntime;
|
|
3991
|
+
initializedRuntime.enableNotifications = () => {
|
|
3462
3992
|
const runtime = startedRuntime;
|
|
3463
3993
|
if (runtime.notificationsActive) return;
|
|
3994
|
+
ephemeralTurns.enable();
|
|
3464
3995
|
runtime.notificationsActive = true;
|
|
3465
3996
|
runtime.disposeAnswerSource = registerInteractiveAnswerSource(
|
|
3466
3997
|
runtime.id,
|
|
@@ -3471,35 +4002,42 @@ export function createNotificationsExtension(
|
|
|
3471
4002
|
if (runtime.redact) return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
|
|
3472
4003
|
try {
|
|
3473
4004
|
const data = await fs.promises.readFile(file.path);
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
4005
|
+
pushFileAttachment(
|
|
4006
|
+
runtime,
|
|
4007
|
+
{
|
|
4008
|
+
type: "file_attachment",
|
|
4009
|
+
sessionId: runtime.id,
|
|
4010
|
+
name: path.basename(file.path),
|
|
4011
|
+
caption: file.caption,
|
|
4012
|
+
},
|
|
4013
|
+
data,
|
|
4014
|
+
);
|
|
3481
4015
|
return { ok: true };
|
|
3482
4016
|
} catch (e) {
|
|
3483
4017
|
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
3484
4018
|
}
|
|
3485
4019
|
});
|
|
3486
4020
|
};
|
|
3487
|
-
const activeRuntime =
|
|
4021
|
+
const activeRuntime = initializedRuntime;
|
|
3488
4022
|
// A native terminal close (SIGHUP), SIGTERM, Ctrl+C exit, or fatal error
|
|
3489
4023
|
// skips AgentSession.dispose(), so the `session_shutdown` extension event
|
|
3490
4024
|
// never fires and the daemon-side topic would be orphaned. postmortem
|
|
3491
4025
|
// awaits registered cleanups on those paths, so send the graceful
|
|
3492
4026
|
// `session_closed` frame from there too. stopSession() cancels this
|
|
3493
4027
|
// registration on every other teardown path, so it never double-fires.
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
4028
|
+
initializedRuntime.cancelPostmortemCleanup = postmortem.register(
|
|
4029
|
+
`notifications-session-closed:${id}`,
|
|
4030
|
+
async () => {
|
|
4031
|
+
await stopSession(initializedRuntime.id);
|
|
4032
|
+
},
|
|
4033
|
+
);
|
|
3497
4034
|
logger.info(`notifications: serving session ${id} at ${endpoint.url}`);
|
|
3498
4035
|
// A workflow-gate emitter can be installed after session startup.
|
|
3499
4036
|
// Attach dynamically so the SDK bus presents every durable gate.
|
|
3500
4037
|
const attachWorkflowGate = (gate: WorkflowGateEmitter | undefined): void => {
|
|
3501
4038
|
if (activeRuntime.workflowGate === gate) return;
|
|
3502
4039
|
activeRuntime.disposeGateListener();
|
|
4040
|
+
activeRuntime.workflowGate?.setRuntimeTurnProvider?.(null);
|
|
3503
4041
|
activeRuntime.disposeAckRecoveryParticipant();
|
|
3504
4042
|
gatePresentations.dispose();
|
|
3505
4043
|
activeRuntime.disposeGateTerminalController();
|
|
@@ -3512,6 +4050,7 @@ export function createNotificationsExtension(
|
|
|
3512
4050
|
return;
|
|
3513
4051
|
}
|
|
3514
4052
|
activeRuntime.workflowGate = gate;
|
|
4053
|
+
gate.setRuntimeTurnProvider?.(() => activeRuntime.activePromptCorrelation?.turnId);
|
|
3515
4054
|
if (hasTerminalArbitrationCapability(gate)) {
|
|
3516
4055
|
const controller: WorkflowGateTerminalController = {
|
|
3517
4056
|
completeGateInteractions: gateId => gatePresentations.complete(gateId),
|
|
@@ -3565,18 +4104,30 @@ export function createNotificationsExtension(
|
|
|
3565
4104
|
});
|
|
3566
4105
|
activeRuntime.disposeAckRecoveryParticipant = () => gate.setAckRecoveryParticipant?.(null);
|
|
3567
4106
|
}
|
|
3568
|
-
void gate.recoverAcceptedGates
|
|
4107
|
+
void (typeof gate.recoverAcceptedGates === "function"
|
|
4108
|
+
? trackGateResolution(gate.recoverAcceptedGates()).catch(() => {})
|
|
4109
|
+
: Promise.resolve());
|
|
3569
4110
|
};
|
|
3570
4111
|
activeRuntime.disposeGateEmitterListener = registerWorkflowGateEmitterListener(id, attachWorkflowGate);
|
|
3571
4112
|
if (ctx.workflowGate) attachWorkflowGate(ctx.workflowGate);
|
|
3572
|
-
if (notificationsEnabledForSession)
|
|
3573
|
-
finishStartup({ status: "started", runtime });
|
|
3574
|
-
return { status: "started", runtime };
|
|
4113
|
+
if (notificationsEnabledForSession) initializedRuntime.enableNotifications();
|
|
4114
|
+
finishStartup({ status: "started", runtime: initializedRuntime });
|
|
4115
|
+
return { status: "started", runtime: initializedRuntime };
|
|
3575
4116
|
} catch (e) {
|
|
3576
4117
|
logger.warn(`notifications: failed to start server: ${String(e)}`);
|
|
3577
4118
|
const result = failLifecycleStartup("failed", e);
|
|
3578
4119
|
finishStartup(result);
|
|
3579
|
-
|
|
4120
|
+
let stopped = false;
|
|
4121
|
+
try {
|
|
4122
|
+
stopped = await stopSession(id, "session", runtime);
|
|
4123
|
+
} catch (error) {
|
|
4124
|
+
// A secondary owner-release failure during abandoned-startup cleanup is
|
|
4125
|
+
// retained for an explicit later retry via cleanupRetries; log it rather
|
|
4126
|
+
// than letting it escape startSession and surface a red extension error
|
|
4127
|
+
// through session_start / session_switch / session_branch.
|
|
4128
|
+
logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
|
|
4129
|
+
}
|
|
4130
|
+
if (!stopped) await cleanupAbandonedStartup();
|
|
3580
4131
|
return { ...result, runtime };
|
|
3581
4132
|
}
|
|
3582
4133
|
}
|
|
@@ -3701,13 +4252,27 @@ export function createNotificationsExtension(
|
|
|
3701
4252
|
event: { previousSessionFile?: string },
|
|
3702
4253
|
ctx: ExtensionContext,
|
|
3703
4254
|
): Promise<void> => {
|
|
4255
|
+
if (extensionShuttingDown) return;
|
|
3704
4256
|
const newId = sessionId(ctx);
|
|
3705
4257
|
const prevId = activeRuntimeId ?? sessionIdFromFile(event.previousSessionFile);
|
|
3706
4258
|
if (prevId && prevId !== newId) {
|
|
3707
4259
|
controller.rekeySession(prevId, newId);
|
|
3708
|
-
|
|
4260
|
+
try {
|
|
4261
|
+
await stopSession(prevId);
|
|
4262
|
+
} catch (error) {
|
|
4263
|
+
// A retained owner-release failure keeps the exact runtime in
|
|
4264
|
+
// cleanupRetries for an explicit later retry; log it rather than
|
|
4265
|
+
// surfacing a red extension error
|
|
4266
|
+
// while rotating session authority (/new, fork, resume, branch).
|
|
4267
|
+
logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
|
|
4268
|
+
}
|
|
3709
4269
|
}
|
|
4270
|
+
if (extensionShuttingDown) return;
|
|
3710
4271
|
await startSession(ctx);
|
|
4272
|
+
if (extensionShuttingDown) {
|
|
4273
|
+
await stopSession(newId);
|
|
4274
|
+
return;
|
|
4275
|
+
}
|
|
3711
4276
|
await controller.reconcileCurrentSession(ctx);
|
|
3712
4277
|
};
|
|
3713
4278
|
api.on("session_switch", async (event, ctx) => {
|
|
@@ -3725,6 +4290,19 @@ export function createNotificationsExtension(
|
|
|
3725
4290
|
await rotateSessionAuthority(event, ctx);
|
|
3726
4291
|
});
|
|
3727
4292
|
|
|
4293
|
+
const terminalizeInFlightTools = (rt: SessionRuntime, id: string, phase: "cancelled" | "unknown"): void => {
|
|
4294
|
+
if (rt.notificationsActive && !rt.redact) {
|
|
4295
|
+
for (const [toolCallId, { toolName }] of rt.inFlightTools) {
|
|
4296
|
+
try {
|
|
4297
|
+
pushSessionFrame(rt, { type: "tool_activity", sessionId: id, toolCallId, toolName, phase });
|
|
4298
|
+
} catch (e) {
|
|
4299
|
+
logger.warn(`notifications: synthetic tool_activity failed: ${String(e)}`);
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
rt.inFlightTools.clear();
|
|
4304
|
+
};
|
|
4305
|
+
|
|
3728
4306
|
// Drive the live typing indicator: mark busy when the agent loop starts so
|
|
3729
4307
|
// the daemon shows "typing…" in the thread while the agent is thinking,
|
|
3730
4308
|
// before any turn output exists. Cleared on `agent_end` below.
|
|
@@ -3753,7 +4331,9 @@ export function createNotificationsExtension(
|
|
|
3753
4331
|
api.on("turn_start", (_event, ctx) => {
|
|
3754
4332
|
const id = sessionId(ctx);
|
|
3755
4333
|
const rt = runtimes.get(id);
|
|
3756
|
-
if (!rt
|
|
4334
|
+
if (!rt) return;
|
|
4335
|
+
rt.turnSeq = (rt.turnSeq ?? 0) + 1;
|
|
4336
|
+
if (!rt.notificationsActive) return;
|
|
3757
4337
|
// A new turn is live: re-open the live-stream window (see turnClosed).
|
|
3758
4338
|
rt.turnClosed = false;
|
|
3759
4339
|
if (rt.pendingInbound.size === 0) return;
|
|
@@ -3771,7 +4351,7 @@ export function createNotificationsExtension(
|
|
|
3771
4351
|
// per `turn_end`. turn_end fires once per turn iteration, so a single
|
|
3772
4352
|
// user-visible idle previously produced many idle pings (the flood); agent_end
|
|
3773
4353
|
// fires exactly once per settle, yielding exactly one idle notification.
|
|
3774
|
-
api.on("agent_end", (
|
|
4354
|
+
api.on("agent_end", (event, ctx) => {
|
|
3775
4355
|
const id = sessionId(ctx);
|
|
3776
4356
|
const rt = runtimes.get(id);
|
|
3777
4357
|
if (!rt) return;
|
|
@@ -3784,13 +4364,17 @@ export function createNotificationsExtension(
|
|
|
3784
4364
|
} else {
|
|
3785
4365
|
rt.emitPromptLifecycle(undefined, { type: "agent_end", sessionId: id });
|
|
3786
4366
|
}
|
|
4367
|
+
rt.activePromptCorrelation = undefined;
|
|
4368
|
+
terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "unknown");
|
|
3787
4369
|
try {
|
|
3788
4370
|
pushSessionFrame(rt, { type: "activity", sessionId: id, state: "idle" });
|
|
3789
4371
|
} catch (e) {
|
|
3790
4372
|
logger.warn(`notifications: activity (idle) failed: ${String(e)}`);
|
|
3791
4373
|
}
|
|
3792
4374
|
if (!rt.notificationsActive) return;
|
|
3793
|
-
void rt.workflowGate?.recoverAcceptedGates
|
|
4375
|
+
void (typeof rt.workflowGate?.recoverAcceptedGates === "function"
|
|
4376
|
+
? rt.trackGateResolution(rt.workflowGate.recoverAcceptedGates()).catch(() => {})
|
|
4377
|
+
: Promise.resolve());
|
|
3794
4378
|
const seq = rt.idleSeq++;
|
|
3795
4379
|
// Re-assert the identity header so the daemon renames the topic once the
|
|
3796
4380
|
// session title has been auto-generated ("{repo}/{branch} - {title}"). The
|
|
@@ -3862,11 +4446,10 @@ export function createNotificationsExtension(
|
|
|
3862
4446
|
rt.preAskFlushedText = text;
|
|
3863
4447
|
// Decision A: a stream-enabled turn must finalize as an in-place edit of ONE
|
|
3864
4448
|
// 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;
|
|
4449
|
+
// async-queued and none landed before this flush, reuse the per-turn ref
|
|
4450
|
+
// assigned at turn_start so the finalized frame remains editable (HTML edit)
|
|
4451
|
+
// and never rich-promotes a streamed final.
|
|
4452
|
+
if (finalAnswer && rt.stream && rt.liveRef === undefined && rt.turnSeq !== undefined) {
|
|
3870
4453
|
rt.liveRef = String(rt.turnSeq);
|
|
3871
4454
|
}
|
|
3872
4455
|
try {
|
|
@@ -3890,11 +4473,99 @@ export function createNotificationsExtension(
|
|
|
3890
4473
|
// path and ordered before it — unlike message_update, which is queued async),
|
|
3891
4474
|
// then flushed here before the ask tool's execute calls registerAsk.
|
|
3892
4475
|
api.on("tool_execution_start", (event, ctx) => {
|
|
3893
|
-
if (event.toolName
|
|
4476
|
+
if (event.toolName === "ask") {
|
|
4477
|
+
const id = sessionId(ctx);
|
|
4478
|
+
const rt = runtimes.get(id);
|
|
4479
|
+
if (!rt?.notificationsActive || rt.redact) return;
|
|
4480
|
+
flushTurnText(rt, id, rt.currentTurnText, false);
|
|
4481
|
+
}
|
|
3894
4482
|
const id = sessionId(ctx);
|
|
3895
4483
|
const rt = runtimes.get(id);
|
|
3896
4484
|
if (!rt?.notificationsActive || rt.redact) return;
|
|
3897
|
-
|
|
4485
|
+
rt.inFlightTools.set(event.toolCallId, { toolName: event.toolName, args: event.args });
|
|
4486
|
+
try {
|
|
4487
|
+
pushSessionFrame(rt, {
|
|
4488
|
+
type: "tool_activity",
|
|
4489
|
+
sessionId: id,
|
|
4490
|
+
toolCallId: event.toolCallId,
|
|
4491
|
+
toolName: event.toolName,
|
|
4492
|
+
phase: "started",
|
|
4493
|
+
});
|
|
4494
|
+
} catch (e) {
|
|
4495
|
+
logger.warn(`notifications: tool_activity start failed: ${String(e)}`);
|
|
4496
|
+
}
|
|
4497
|
+
});
|
|
4498
|
+
|
|
4499
|
+
api.on("tool_execution_end", (event, ctx) => {
|
|
4500
|
+
const id = sessionId(ctx);
|
|
4501
|
+
const rt = runtimes.get(id);
|
|
4502
|
+
if (!rt) return;
|
|
4503
|
+
const inFlight = rt.inFlightTools.get(event.toolCallId);
|
|
4504
|
+
if (!rt.notificationsActive || rt.redact) {
|
|
4505
|
+
rt.inFlightTools.delete(event.toolCallId);
|
|
4506
|
+
return;
|
|
4507
|
+
}
|
|
4508
|
+
try {
|
|
4509
|
+
const frame: {
|
|
4510
|
+
type: "tool_activity";
|
|
4511
|
+
sessionId: string;
|
|
4512
|
+
toolCallId: string;
|
|
4513
|
+
toolName: string;
|
|
4514
|
+
phase: "completed" | "failed";
|
|
4515
|
+
isError: boolean;
|
|
4516
|
+
argsSummary?: string;
|
|
4517
|
+
resultSummary?: string;
|
|
4518
|
+
} = {
|
|
4519
|
+
type: "tool_activity",
|
|
4520
|
+
sessionId: id,
|
|
4521
|
+
toolCallId: event.toolCallId,
|
|
4522
|
+
toolName: event.toolName,
|
|
4523
|
+
phase: event.isError ? "failed" : "completed",
|
|
4524
|
+
isError: event.isError,
|
|
4525
|
+
};
|
|
4526
|
+
if (rt.verbosity === "verbose") {
|
|
4527
|
+
const tool = ctx.resolveTool(event.toolName);
|
|
4528
|
+
const argsSummary = projectToolSummary(tool, "args", inFlight?.args);
|
|
4529
|
+
const resultSummary = projectToolSummary(tool, "result", event.result);
|
|
4530
|
+
if (argsSummary !== undefined) frame.argsSummary = argsSummary;
|
|
4531
|
+
if (resultSummary !== undefined) frame.resultSummary = resultSummary;
|
|
4532
|
+
}
|
|
4533
|
+
pushSessionFrame(rt, frame);
|
|
4534
|
+
} catch (e) {
|
|
4535
|
+
logger.warn(`notifications: tool_activity end failed: ${String(e)}`);
|
|
4536
|
+
} finally {
|
|
4537
|
+
rt.inFlightTools.delete(event.toolCallId);
|
|
4538
|
+
}
|
|
4539
|
+
});
|
|
4540
|
+
|
|
4541
|
+
api.on("reasoning_summary_end", (event, ctx) => {
|
|
4542
|
+
const id = sessionId(ctx);
|
|
4543
|
+
const rt = runtimes.get(id);
|
|
4544
|
+
if (!rt?.notificationsActive || rt.redact || rt.verbosity !== "verbose") return;
|
|
4545
|
+
if (!event.message || typeof event.message !== "object" || !("content" in event.message)) return;
|
|
4546
|
+
const content = event.message.content;
|
|
4547
|
+
if (!Array.isArray(content)) return;
|
|
4548
|
+
const block = content[event.contentIndex];
|
|
4549
|
+
if (block?.type !== "thinking" || (block.provenance !== "summary" && block.provenance !== "mixed")) return;
|
|
4550
|
+
// CoT boundary: emit ONLY the canonical provider-marked summaryText. Never
|
|
4551
|
+
// fall back to the event payload, which could carry inconsistent/mutated text.
|
|
4552
|
+
const text = block.summaryText;
|
|
4553
|
+
if (typeof text !== "string" || text === "") return;
|
|
4554
|
+
try {
|
|
4555
|
+
pushSessionFrame(rt, {
|
|
4556
|
+
type: "reasoning_summary",
|
|
4557
|
+
sessionId: id,
|
|
4558
|
+
text,
|
|
4559
|
+
// Coalesce on the reasoning block's stable itemId carried on the event, NOT
|
|
4560
|
+
// the mutable rt.turnSeq: a streamed reasoning_summary_end is queued async and
|
|
4561
|
+
// turn_start for the next iteration advances turnSeq synchronously first, so
|
|
4562
|
+
// reading turnSeq here could bind turn N's summary to turn N+1. Absent an
|
|
4563
|
+
// itemId, omit turnRef (threaded-render sends a fresh non-editable message).
|
|
4564
|
+
...((block as { itemId?: string }).itemId ? { turnRef: (block as { itemId?: string }).itemId } : {}),
|
|
4565
|
+
});
|
|
4566
|
+
} catch (e) {
|
|
4567
|
+
logger.warn(`notifications: reasoning_summary failed: ${String(e)}`);
|
|
4568
|
+
}
|
|
3898
4569
|
});
|
|
3899
4570
|
|
|
3900
4571
|
api.on("turn_end", (event, ctx) => {
|
|
@@ -3924,8 +4595,7 @@ export function createNotificationsExtension(
|
|
|
3924
4595
|
const rt = runtimes.get(id);
|
|
3925
4596
|
if (!rt?.notificationsActive || !rt.stream || rt.redact || rt.turnClosed) return;
|
|
3926
4597
|
if ((event.message as { role?: unknown }).role !== "assistant") return;
|
|
3927
|
-
if (rt.liveRef === undefined) {
|
|
3928
|
-
rt.turnSeq = (rt.turnSeq ?? 0) + 1;
|
|
4598
|
+
if (rt.liveRef === undefined && rt.turnSeq !== undefined) {
|
|
3929
4599
|
rt.liveRef = String(rt.turnSeq);
|
|
3930
4600
|
}
|
|
3931
4601
|
const now = Date.now();
|
|
@@ -3971,9 +4641,24 @@ export function createNotificationsExtension(
|
|
|
3971
4641
|
});
|
|
3972
4642
|
|
|
3973
4643
|
api.on("session_shutdown", async (_event, ctx) => {
|
|
4644
|
+
extensionShuttingDown = true;
|
|
4645
|
+
identityControlInFlight = false;
|
|
4646
|
+
deferredIdentityRotation = undefined;
|
|
4647
|
+
const id = sessionId(ctx);
|
|
4648
|
+
const rt = runtimes.get(id);
|
|
4649
|
+
if (rt) terminalizeInFlightTools(rt, id, "unknown");
|
|
3974
4650
|
const controllerStop =
|
|
3975
4651
|
typeof ctx.sessionManager.getCwd === "function" ? controller.stopCurrentSession(ctx) : Promise.resolve(false);
|
|
3976
4652
|
void controllerStop.catch(error => logger.warn(`notifications: controller shutdown failed: ${String(error)}`));
|
|
3977
|
-
|
|
4653
|
+
try {
|
|
4654
|
+
await stopSession(id);
|
|
4655
|
+
} catch (error) {
|
|
4656
|
+
// A retained owner-release failure keeps the exact runtime in
|
|
4657
|
+
// cleanupRetries for an explicit later retry; log it rather than
|
|
4658
|
+
// surfacing a red extension error at
|
|
4659
|
+
// shutdown. On terminal quit there is no later retry cycle, so log at
|
|
4660
|
+
// error severity (matching the postmortem cleanup precedent).
|
|
4661
|
+
logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
|
|
4662
|
+
}
|
|
3978
4663
|
});
|
|
3979
4664
|
}
|