@gajae-code/coding-agent 0.11.0 → 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 +63 -1
- package/dist/types/async/job-manager.d.ts +46 -7
- 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-receipt-freshness.d.ts +11 -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/command-controller.d.ts +2 -2
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -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 +21 -19
- 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 +21 -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 +22 -15
- 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/ensure.d.ts +9 -0
- 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/discord-daemon.d.ts +5 -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 +69 -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/tools/subagent-render.d.ts +1 -0
- package/dist/types/tools/subagent.d.ts +6 -1
- 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 +39 -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 +455 -69
- 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 +83 -12
- package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +102 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +156 -1319
- 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/command-controller.ts +9 -15
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +72 -25
- 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 +302 -1151
- 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 +78 -19
- 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 +47 -20
- 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 +21 -2
- package/src/sdk/broker/ensure.ts +85 -25
- 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 +14 -4
- 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/discord-daemon.ts +25 -8
- package/src/sdk/bus/html-format.ts +20 -3
- package/src/sdk/bus/index.ts +834 -113
- package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
- package/src/sdk/bus/native-runtime-compatibility.ts +1 -1
- 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 +1491 -247
- 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 +1901 -637
- 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 +62 -37
- 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/tools/subagent-render.ts +19 -8
- package/src/tools/subagent.ts +84 -32
- 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,12 +873,19 @@ 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;
|
|
875
883
|
/** True only after the exact host generation was registered with the broker index. */
|
|
876
884
|
brokerRegistrationActive: boolean;
|
|
885
|
+
/** Terminal cleanup proof retained across retries; each owner is released at most once after proof. */
|
|
886
|
+
hostStopped: boolean;
|
|
887
|
+
serverStopped: boolean;
|
|
888
|
+
brokerRegistrationReleased: boolean;
|
|
877
889
|
verbosity: "lean" | "verbose";
|
|
878
890
|
sessionTag: string;
|
|
879
891
|
/** Whether the agent loop is currently running (drives the typing indicator). */
|
|
@@ -914,10 +926,52 @@ interface SessionRuntime {
|
|
|
914
926
|
/** True between turn_end and the next turn_start: drops late async message_update
|
|
915
927
|
* frames so a stale live edit can never be emitted after the finalized turn. */
|
|
916
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 }>;
|
|
917
931
|
/** Cancels the postmortem cleanup that emits `session_closed` on process teardown. */
|
|
918
932
|
cancelPostmortemCleanup: () => void;
|
|
919
|
-
/**
|
|
920
|
-
|
|
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);
|
|
921
975
|
}
|
|
922
976
|
|
|
923
977
|
/** Request-local requester authority for stable ControlSurface dispatches. */
|
|
@@ -934,9 +988,28 @@ function pushSessionFrame(
|
|
|
934
988
|
frame: { type: string; [key: string]: unknown },
|
|
935
989
|
): void {
|
|
936
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
|
+
}
|
|
937
1001
|
runtime.server.pushFrame(JSON.stringify(frame));
|
|
938
1002
|
}
|
|
939
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
|
+
|
|
940
1013
|
/** Agent lifecycle is SDK session truth, independent of optional chat delivery. */
|
|
941
1014
|
function emitAgentLifecycle(
|
|
942
1015
|
runtime: Pick<SessionRuntime, "server" | "host">,
|
|
@@ -977,6 +1050,7 @@ const defaultConfig: NotificationConfig = {
|
|
|
977
1050
|
rich: { enabled: true },
|
|
978
1051
|
richDraft: { enabled: false },
|
|
979
1052
|
topics: {},
|
|
1053
|
+
btw: { enabled: true },
|
|
980
1054
|
};
|
|
981
1055
|
|
|
982
1056
|
export function notificationsEnabled(): boolean {
|
|
@@ -1624,11 +1698,18 @@ function hasTerminalArbitrationCapability(
|
|
|
1624
1698
|
Required<
|
|
1625
1699
|
Pick<
|
|
1626
1700
|
WorkflowGateEmitter,
|
|
1627
|
-
|
|
1701
|
+
| "resolveGate"
|
|
1702
|
+
| "recoverAcceptedGates"
|
|
1703
|
+
| "lookupCompletedResolution"
|
|
1704
|
+
| "prepareTerminalization"
|
|
1705
|
+
| "clearPreparedTerminalization"
|
|
1706
|
+
| "registerGateTerminalController"
|
|
1628
1707
|
>
|
|
1629
1708
|
> {
|
|
1630
1709
|
return (
|
|
1631
1710
|
typeof workflowGate?.resolveGate === "function" &&
|
|
1711
|
+
typeof workflowGate.recoverAcceptedGates === "function" &&
|
|
1712
|
+
typeof workflowGate.lookupCompletedResolution === "function" &&
|
|
1632
1713
|
typeof workflowGate.prepareTerminalization === "function" &&
|
|
1633
1714
|
typeof workflowGate.clearPreparedTerminalization === "function" &&
|
|
1634
1715
|
typeof workflowGate.registerGateTerminalController === "function"
|
|
@@ -1982,7 +2063,7 @@ function sdkControlSurface(
|
|
|
1982
2063
|
pending.completeDirect();
|
|
1983
2064
|
return { resolved: true };
|
|
1984
2065
|
},
|
|
1985
|
-
answerGate: async (id, response, expectedSessionId) => {
|
|
2066
|
+
answerGate: async (id, response, expectedSessionId, idempotencyKey) => {
|
|
1986
2067
|
if (!acceptGateResolution())
|
|
1987
2068
|
throw Object.assign(new Error("Workflow gate is no longer answerable."), { code: "resource_gone" });
|
|
1988
2069
|
if (expectedSessionId === undefined) auditMissingExpectedSessionId("workflow.gate_answer");
|
|
@@ -1995,6 +2076,26 @@ function sdkControlSurface(
|
|
|
1995
2076
|
throw Object.assign(new Error("Workflow gates are unavailable for this session."), {
|
|
1996
2077
|
code: "resource_gone",
|
|
1997
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
|
+
}
|
|
1998
2099
|
const prepared = presentations.prepareDirectControl(id);
|
|
1999
2100
|
if (!prepared || prepared.status === "stale")
|
|
2000
2101
|
throw Object.assign(new Error("Workflow gate is no longer answerable."), { code: "resource_gone" });
|
|
@@ -2003,31 +2104,22 @@ function sdkControlSurface(
|
|
|
2003
2104
|
if (prepared.status !== "queued" && prepared.status !== "retired")
|
|
2004
2105
|
throw new Error(`Unexpected direct control preparation: ${prepared.status}`);
|
|
2005
2106
|
if (
|
|
2006
|
-
|
|
2007
|
-
id,
|
|
2008
|
-
prepared.status === "queued" ? "not_published" : "retired",
|
|
2009
|
-
) !== true
|
|
2107
|
+
workflowGate.prepareTerminalization(id, prepared.status === "queued" ? "not_published" : "retired") !== true
|
|
2010
2108
|
) {
|
|
2011
2109
|
presentations.finishDirectControl(id, prepared, "rejected");
|
|
2012
2110
|
throw Object.assign(new Error("Workflow gate lacks a terminalization proof."), { code: "resource_gone" });
|
|
2013
2111
|
}
|
|
2014
2112
|
try {
|
|
2015
|
-
const resolution = await trackGateResolution(
|
|
2016
|
-
ctx.workflowGate?.resolveGate?.({
|
|
2017
|
-
gate_id: id,
|
|
2018
|
-
answer: response,
|
|
2019
|
-
idempotency_key: id,
|
|
2020
|
-
}) ?? unavailable("workflow.gate_answer", "workflow gates are unavailable for this session")(),
|
|
2021
|
-
);
|
|
2113
|
+
const resolution = await trackGateResolution(workflowGate.resolveGate(gateResponse));
|
|
2022
2114
|
const status = (resolution as { status?: unknown }).status;
|
|
2023
2115
|
if (status === "accepted" || status === "rejected") {
|
|
2024
|
-
if (status === "rejected")
|
|
2116
|
+
if (status === "rejected") workflowGate.clearPreparedTerminalization(id);
|
|
2025
2117
|
presentations.finishDirectControl(id, prepared, status);
|
|
2026
2118
|
return resolution;
|
|
2027
2119
|
}
|
|
2028
2120
|
} catch (error) {
|
|
2029
2121
|
const outcome = reconcileDirectControlFailure(id);
|
|
2030
|
-
if (outcome === "rejected")
|
|
2122
|
+
if (outcome === "rejected") workflowGate.clearPreparedTerminalization(id);
|
|
2031
2123
|
presentations.finishDirectControl(id, prepared, outcome);
|
|
2032
2124
|
if (outcome === "unknown")
|
|
2033
2125
|
throw Object.assign(new Error("Workflow gate resolution outcome is uncertain."), {
|
|
@@ -2036,7 +2128,7 @@ function sdkControlSurface(
|
|
|
2036
2128
|
throw error;
|
|
2037
2129
|
}
|
|
2038
2130
|
const outcome = reconcileDirectControlFailure(id);
|
|
2039
|
-
if (outcome === "rejected")
|
|
2131
|
+
if (outcome === "rejected") workflowGate.clearPreparedTerminalization(id);
|
|
2040
2132
|
presentations.finishDirectControl(id, prepared, outcome);
|
|
2041
2133
|
logger.warn("workflow_gate_direct_control_uncertain_outcome", {
|
|
2042
2134
|
operation: "workflow.gate_answer",
|
|
@@ -2060,6 +2152,22 @@ function sdkControlSurface(
|
|
|
2060
2152
|
throw Object.assign(new Error("Workflow gates are unavailable for this session."), {
|
|
2061
2153
|
code: "resource_gone",
|
|
2062
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
|
+
}
|
|
2063
2171
|
const prepared = presentations.prepareDirectControl(id);
|
|
2064
2172
|
if (!prepared || prepared.status === "stale")
|
|
2065
2173
|
throw Object.assign(new Error("Workflow plan is no longer answerable."), { code: "resource_gone" });
|
|
@@ -2068,31 +2176,22 @@ function sdkControlSurface(
|
|
|
2068
2176
|
if (prepared.status !== "queued" && prepared.status !== "retired")
|
|
2069
2177
|
throw new Error(`Unexpected direct control preparation: ${prepared.status}`);
|
|
2070
2178
|
if (
|
|
2071
|
-
|
|
2072
|
-
id,
|
|
2073
|
-
prepared.status === "queued" ? "not_published" : "retired",
|
|
2074
|
-
) !== true
|
|
2179
|
+
workflowGate.prepareTerminalization(id, prepared.status === "queued" ? "not_published" : "retired") !== true
|
|
2075
2180
|
) {
|
|
2076
2181
|
presentations.finishDirectControl(id, prepared, "rejected");
|
|
2077
2182
|
throw Object.assign(new Error("Workflow plan lacks a terminalization proof."), { code: "resource_gone" });
|
|
2078
2183
|
}
|
|
2079
2184
|
try {
|
|
2080
|
-
const resolution = await trackGateResolution(
|
|
2081
|
-
ctx.workflowGate?.resolveGate?.({
|
|
2082
|
-
gate_id: id,
|
|
2083
|
-
answer: choice,
|
|
2084
|
-
idempotency_key: id,
|
|
2085
|
-
}) ?? unavailable("workflow.plan_approve", "workflow gates are unavailable for this session")(),
|
|
2086
|
-
);
|
|
2185
|
+
const resolution = await trackGateResolution(workflowGate.resolveGate(gateResponse));
|
|
2087
2186
|
const status = (resolution as { status?: unknown }).status;
|
|
2088
2187
|
if (status === "accepted" || status === "rejected") {
|
|
2089
|
-
if (status === "rejected")
|
|
2188
|
+
if (status === "rejected") workflowGate.clearPreparedTerminalization(id);
|
|
2090
2189
|
presentations.finishDirectControl(id, prepared, status);
|
|
2091
2190
|
return resolution;
|
|
2092
2191
|
}
|
|
2093
2192
|
} catch (error) {
|
|
2094
2193
|
const outcome = reconcileDirectControlFailure(id);
|
|
2095
|
-
if (outcome === "rejected")
|
|
2194
|
+
if (outcome === "rejected") workflowGate.clearPreparedTerminalization(id);
|
|
2096
2195
|
presentations.finishDirectControl(id, prepared, outcome);
|
|
2097
2196
|
if (outcome === "unknown")
|
|
2098
2197
|
throw Object.assign(new Error("Workflow plan resolution outcome is uncertain."), {
|
|
@@ -2101,7 +2200,7 @@ function sdkControlSurface(
|
|
|
2101
2200
|
throw error;
|
|
2102
2201
|
}
|
|
2103
2202
|
const outcome = reconcileDirectControlFailure(id);
|
|
2104
|
-
if (outcome === "rejected")
|
|
2203
|
+
if (outcome === "rejected") workflowGate.clearPreparedTerminalization(id);
|
|
2105
2204
|
presentations.finishDirectControl(id, prepared, outcome);
|
|
2106
2205
|
logger.warn("workflow_gate_direct_control_uncertain_outcome", {
|
|
2107
2206
|
operation: "workflow.plan_approve",
|
|
@@ -2120,14 +2219,14 @@ function sdkControlSurface(
|
|
|
2120
2219
|
throw Object.assign(new Error("skill.invoke args must be a string."), { code: "invalid_input" });
|
|
2121
2220
|
return ctx.invokeSkill(name, args);
|
|
2122
2221
|
},
|
|
2123
|
-
setPlanMode: on => {
|
|
2222
|
+
setPlanMode: async on => {
|
|
2124
2223
|
if (!bindings.has("setPlanMode") || !ctx.setPlanMode)
|
|
2125
2224
|
return unavailable("mode.plan.set", "no plan-mode seam is installed")();
|
|
2126
2225
|
|
|
2127
2226
|
if (typeof on !== "boolean")
|
|
2128
2227
|
throw Object.assign(new Error("mode.plan.set requires a boolean on value."), { code: "invalid_input" });
|
|
2129
2228
|
|
|
2130
|
-
return { state: ctx.setPlanMode(on) };
|
|
2229
|
+
return { state: await ctx.setPlanMode(on) };
|
|
2131
2230
|
},
|
|
2132
2231
|
operateGoal: (op, objective) => {
|
|
2133
2232
|
if (!bindings.has("operateGoal") || !ctx.operateGoal)
|
|
@@ -2248,6 +2347,367 @@ function sdkControlSurface(
|
|
|
2248
2347
|
return surface;
|
|
2249
2348
|
}
|
|
2250
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
|
+
}
|
|
2251
2711
|
/** Parse only v3 frames carried through the existing control-command seam. */
|
|
2252
2712
|
function sdkInboundFrame(commandJson: string | undefined): Record<string, unknown> | undefined {
|
|
2253
2713
|
if (!commandJson) return undefined;
|
|
@@ -2283,6 +2743,7 @@ export function createNotificationsExtension(
|
|
|
2283
2743
|
controller?: NotificationSessionController;
|
|
2284
2744
|
|
|
2285
2745
|
onSdkRequest?: (kind: "control" | "query", connectionId: string, frame: Record<string, unknown>) => void;
|
|
2746
|
+
runEphemeralTurn?: (promptText: string, signal: AbortSignal) => Promise<{ replyText: string }>;
|
|
2286
2747
|
} = {},
|
|
2287
2748
|
): void {
|
|
2288
2749
|
const lifecycleStartupCapability = lifecycleStartupCapabilityForApi(api);
|
|
@@ -2294,10 +2755,14 @@ export function createNotificationsExtension(
|
|
|
2294
2755
|
getConfig: () => resolveSettings(options.settings).cfg,
|
|
2295
2756
|
});
|
|
2296
2757
|
|
|
2758
|
+
// Failed terminal teardown remains fenced from normal runtime lookup while the
|
|
2759
|
+
// exact runtime object retains authority for an explicit idempotent retry.
|
|
2760
|
+
const cleanupRetries = new Map<string, SessionRuntime>();
|
|
2297
2761
|
const sessionStartPromises = new Map<string, Promise<SessionStartResult>>();
|
|
2298
2762
|
let activeRuntimeId: string | undefined;
|
|
2299
2763
|
let identityControlInFlight = false;
|
|
2300
2764
|
let deferredIdentityRotation: { event: { previousSessionFile?: string }; ctx: ExtensionContext } | undefined;
|
|
2765
|
+
let extensionShuttingDown = false;
|
|
2301
2766
|
|
|
2302
2767
|
async function ensureTelegramOwner(
|
|
2303
2768
|
settings: Settings,
|
|
@@ -2341,13 +2806,18 @@ export function createNotificationsExtension(
|
|
|
2341
2806
|
reason: "session" | "notifications" = "session",
|
|
2342
2807
|
expectedRuntime?: SessionRuntime,
|
|
2343
2808
|
): Promise<boolean> {
|
|
2344
|
-
const
|
|
2809
|
+
const retryRuntime = cleanupRetries.get(id);
|
|
2810
|
+
const activeRuntime = runtimes.get(id);
|
|
2811
|
+
const requestedRuntime = retryRuntime ?? activeRuntime;
|
|
2345
2812
|
if (expectedRuntime && requestedRuntime !== expectedRuntime) return false;
|
|
2346
|
-
if (reason === "session" && requestedRuntime)
|
|
2813
|
+
if (reason === "session" && requestedRuntime) {
|
|
2814
|
+
requestedRuntime.stopping = true;
|
|
2815
|
+
requestedRuntime.abortEphemeralTurns();
|
|
2816
|
+
}
|
|
2347
2817
|
if (reason === "session" && requestedRuntime) {
|
|
2348
2818
|
// Fence the exact runtime before awaiting its startup promise: a late start
|
|
2349
2819
|
// must observe removal and clean itself up rather than becoming reachable.
|
|
2350
|
-
runtimes.delete(id);
|
|
2820
|
+
if (runtimes.get(id) === requestedRuntime) runtimes.delete(id);
|
|
2351
2821
|
if (activeRuntimeId === id) activeRuntimeId = undefined;
|
|
2352
2822
|
}
|
|
2353
2823
|
const pendingStart = sessionStartPromises.get(id);
|
|
@@ -2355,7 +2825,13 @@ export function createNotificationsExtension(
|
|
|
2355
2825
|
void pendingStart
|
|
2356
2826
|
.catch(() => {})
|
|
2357
2827
|
.then(() => {
|
|
2358
|
-
if (runtimes.get(id) === requestedRuntime
|
|
2828
|
+
if (runtimes.get(id) === requestedRuntime || cleanupRetries.get(id) === 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
|
+
);
|
|
2359
2835
|
});
|
|
2360
2836
|
const rt = requestedRuntime;
|
|
2361
2837
|
|
|
@@ -2367,6 +2843,7 @@ export function createNotificationsExtension(
|
|
|
2367
2843
|
}
|
|
2368
2844
|
if (reason === "notifications" && rt.host.started) {
|
|
2369
2845
|
rt.notificationsActive = false;
|
|
2846
|
+
rt.disableEphemeralTurns();
|
|
2370
2847
|
try {
|
|
2371
2848
|
rt.disposeAnswerSource();
|
|
2372
2849
|
} catch {}
|
|
@@ -2378,13 +2855,13 @@ export function createNotificationsExtension(
|
|
|
2378
2855
|
rt.pendingInteractive.clear();
|
|
2379
2856
|
return true;
|
|
2380
2857
|
}
|
|
2858
|
+
// Keep this exact object authoritative for the full terminal release, including
|
|
2859
|
+
// the interval before a failed owner can be recorded for a later retry.
|
|
2860
|
+
cleanupRetries.set(id, rt);
|
|
2381
2861
|
|
|
2382
2862
|
try {
|
|
2383
2863
|
rt.cancelPostmortemCleanup();
|
|
2384
2864
|
} catch {}
|
|
2385
|
-
try {
|
|
2386
|
-
rt.stopBrokerHeartbeat();
|
|
2387
|
-
} catch {}
|
|
2388
2865
|
try {
|
|
2389
2866
|
rt.disposeAnswerSource();
|
|
2390
2867
|
} catch {}
|
|
@@ -2394,6 +2871,9 @@ export function createNotificationsExtension(
|
|
|
2394
2871
|
try {
|
|
2395
2872
|
rt.disposeGateListener();
|
|
2396
2873
|
} catch {}
|
|
2874
|
+
try {
|
|
2875
|
+
rt.workflowGate?.setRuntimeTurnProvider?.(null);
|
|
2876
|
+
} catch {}
|
|
2397
2877
|
await rt.waitForGateResolutionQuiescence();
|
|
2398
2878
|
try {
|
|
2399
2879
|
rt.disposeAckRecoveryParticipant();
|
|
@@ -2405,16 +2885,24 @@ export function createNotificationsExtension(
|
|
|
2405
2885
|
try {
|
|
2406
2886
|
rt.disposeGateTerminalController();
|
|
2407
2887
|
} catch {}
|
|
2408
|
-
let hostStopped =
|
|
2409
|
-
let brokerRegistrationReleased =
|
|
2888
|
+
let hostStopped = rt.hostStopped;
|
|
2889
|
+
let brokerRegistrationReleased = rt.brokerRegistrationReleased;
|
|
2890
|
+
const ownerReleaseFailures: unknown[] = [];
|
|
2410
2891
|
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2892
|
+
if (!hostStopped) {
|
|
2893
|
+
try {
|
|
2894
|
+
const stopped = await rt.host.stop();
|
|
2895
|
+
hostStopped = stopped === "stopped";
|
|
2896
|
+
brokerRegistrationReleased = !rt.brokerRegistrationActive || hostStopped;
|
|
2897
|
+
if (rt.brokerRegistrationActive && hostStopped) rt.brokerRegistrationActive = false;
|
|
2898
|
+
if (hostStopped) {
|
|
2899
|
+
rt.hostStopped = true;
|
|
2900
|
+
rt.brokerRegistrationReleased = brokerRegistrationReleased;
|
|
2901
|
+
}
|
|
2902
|
+
} catch (e) {
|
|
2903
|
+
ownerReleaseFailures.push(e);
|
|
2904
|
+
logger.warn(`sdk host: stop failed: ${String(e)}`);
|
|
2905
|
+
}
|
|
2418
2906
|
}
|
|
2419
2907
|
rt.host.reverse.dispose();
|
|
2420
2908
|
// Resolve any still-pending interactive asks so the ask tool is not left hanging.
|
|
@@ -2424,6 +2912,7 @@ export function createNotificationsExtension(
|
|
|
2424
2912
|
rt.cursors.close();
|
|
2425
2913
|
await rt.revisions.close();
|
|
2426
2914
|
} catch (e) {
|
|
2915
|
+
ownerReleaseFailures.push(e);
|
|
2427
2916
|
logger.warn(`sdk query snapshots: close failed: ${String(e)}`);
|
|
2428
2917
|
}
|
|
2429
2918
|
let closeFrameSent = false;
|
|
@@ -2434,18 +2923,27 @@ export function createNotificationsExtension(
|
|
|
2434
2923
|
logger.warn(`notifications: session_closed failed: ${String(e)}`);
|
|
2435
2924
|
}
|
|
2436
2925
|
if (closeFrameSent) await sleep(100);
|
|
2437
|
-
let serverStopped =
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2926
|
+
let serverStopped = rt.serverStopped;
|
|
2927
|
+
if (!serverStopped) {
|
|
2928
|
+
try {
|
|
2929
|
+
await rt.server.stopAndWait();
|
|
2930
|
+
serverStopped = true;
|
|
2931
|
+
rt.serverStopped = true;
|
|
2932
|
+
} catch (e) {
|
|
2933
|
+
ownerReleaseFailures.push(e);
|
|
2934
|
+
logger.warn(`notifications: stop failed: ${String(e)}`);
|
|
2935
|
+
}
|
|
2443
2936
|
}
|
|
2444
2937
|
lifecycleStartupCapability?.rollback?.recordStop(rt.host.generation, {
|
|
2445
2938
|
runtimeRemoved: true,
|
|
2446
|
-
hostStopped: hostStopped && serverStopped,
|
|
2447
|
-
brokerRegistrationReleased,
|
|
2939
|
+
hostStopped: rt.hostStopped && rt.serverStopped,
|
|
2940
|
+
brokerRegistrationReleased: rt.brokerRegistrationReleased,
|
|
2448
2941
|
});
|
|
2942
|
+
if (ownerReleaseFailures.length > 0) {
|
|
2943
|
+
cleanupRetries.set(id, rt);
|
|
2944
|
+
throw new AggregateError(ownerReleaseFailures, `SDK notification runtime ${id} owner release failed.`);
|
|
2945
|
+
}
|
|
2946
|
+
if (cleanupRetries.get(id) === rt) cleanupRetries.delete(id);
|
|
2449
2947
|
return true;
|
|
2450
2948
|
}
|
|
2451
2949
|
|
|
@@ -2484,6 +2982,8 @@ export function createNotificationsExtension(
|
|
|
2484
2982
|
if (lifecycleRequired && !isNotificationEligibleContext(ctx)) return failLifecycleStartup("ineligible");
|
|
2485
2983
|
const pendingStart = sessionStartPromises.get(id);
|
|
2486
2984
|
if (pendingStart) return pendingStart;
|
|
2985
|
+
const retainedCleanup = cleanupRetries.get(id);
|
|
2986
|
+
if (retainedCleanup) return failLifecycleStartup("failed", "SDK notification runtime cleanup is still pending.");
|
|
2487
2987
|
const existingRuntime = runtimes.get(id);
|
|
2488
2988
|
if (existingRuntime) {
|
|
2489
2989
|
activeRuntimeId = id;
|
|
@@ -2526,7 +3026,6 @@ export function createNotificationsExtension(
|
|
|
2526
3026
|
}
|
|
2527
3027
|
const gatePresentations = new PresentationArbiter(server, () => runtime?.redact ?? redact, tag);
|
|
2528
3028
|
let inboundSdkFrame: ((connectionId: string, frame: Record<string, unknown>) => void) | undefined;
|
|
2529
|
-
let stopBrokerHeartbeat = () => {};
|
|
2530
3029
|
const inFlightGateResolutions = new Set<Promise<void>>();
|
|
2531
3030
|
const trackGateResolution = <T>(resolution: Promise<T>): Promise<T> => {
|
|
2532
3031
|
const quiesced = resolution.then(
|
|
@@ -2568,6 +3067,8 @@ export function createNotificationsExtension(
|
|
|
2568
3067
|
if (capability === "permission") ctx.setSdkPermissionProvider?.(undefined);
|
|
2569
3068
|
};
|
|
2570
3069
|
|
|
3070
|
+
const hostCapCache = new Map<string, ReadonlySet<string>>();
|
|
3071
|
+
|
|
2571
3072
|
const configOverrides = new Map<string, unknown>();
|
|
2572
3073
|
const configRevision = { current: 0 };
|
|
2573
3074
|
const PROMPT_SUBMISSION_CAPACITY = 128;
|
|
@@ -2726,6 +3227,11 @@ export function createNotificationsExtension(
|
|
|
2726
3227
|
submission.failed = true;
|
|
2727
3228
|
submission.error = error;
|
|
2728
3229
|
removePendingPromptCorrelation(correlation);
|
|
3230
|
+
if (
|
|
3231
|
+
runtime?.activePromptCorrelation?.commandId === correlation.commandId &&
|
|
3232
|
+
runtime.activePromptCorrelation.turnId === correlation.turnId
|
|
3233
|
+
)
|
|
3234
|
+
runtime.activePromptCorrelation = undefined;
|
|
2729
3235
|
emitPromptFailure(correlation, error);
|
|
2730
3236
|
};
|
|
2731
3237
|
const acknowledgePrompt = (connectionId: string, correlation: { commandId: string; turnId: string }) => {
|
|
@@ -2793,6 +3299,10 @@ export function createNotificationsExtension(
|
|
|
2793
3299
|
};
|
|
2794
3300
|
|
|
2795
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
|
+
}
|
|
2796
3306
|
const json = JSON.stringify(frame);
|
|
2797
3307
|
if (connectionId.startsWith("seam:")) {
|
|
2798
3308
|
try {
|
|
@@ -2824,6 +3334,7 @@ export function createNotificationsExtension(
|
|
|
2824
3334
|
stateRoot,
|
|
2825
3335
|
token,
|
|
2826
3336
|
sendFrame: (connectionId, frame) => sendSdkFrame(connectionId, frame),
|
|
3337
|
+
connectionCapabilities: connectionId => hostCapCache.get(connectionId) ?? EMPTY_CAPABILITIES,
|
|
2827
3338
|
installProviderDefinitions,
|
|
2828
3339
|
onProviderDefinitionsRemoved: removeProviderDefinitions,
|
|
2829
3340
|
onFrame: handler => {
|
|
@@ -2930,6 +3441,9 @@ export function createNotificationsExtension(
|
|
|
2930
3441
|
idleSeq: 0,
|
|
2931
3442
|
pendingInteractive,
|
|
2932
3443
|
brokerRegistrationActive: false,
|
|
3444
|
+
hostStopped: false,
|
|
3445
|
+
serverStopped: false,
|
|
3446
|
+
brokerRegistrationReleased: false,
|
|
2933
3447
|
disposeAnswerSource: () => {},
|
|
2934
3448
|
disposeFileSink: () => {},
|
|
2935
3449
|
disposeGateListener: () => {},
|
|
@@ -2938,14 +3452,16 @@ export function createNotificationsExtension(
|
|
|
2938
3452
|
disposeGateTerminalController: () => {},
|
|
2939
3453
|
disposeAckRecoveryParticipant: () => {},
|
|
2940
3454
|
disposeGateEmitterListener: () => {},
|
|
3455
|
+
trackGateResolution,
|
|
2941
3456
|
waitForGateResolutionQuiescence: async () => {
|
|
2942
3457
|
await Promise.allSettled(inFlightGateResolutions);
|
|
2943
3458
|
},
|
|
2944
3459
|
workflowGate: undefined,
|
|
2945
3460
|
gatePresentations,
|
|
2946
3461
|
stopping: false,
|
|
3462
|
+
abortEphemeralTurns: () => {},
|
|
3463
|
+
disableEphemeralTurns: () => {},
|
|
2947
3464
|
cancelPostmortemCleanup: () => {},
|
|
2948
|
-
stopBrokerHeartbeat,
|
|
2949
3465
|
|
|
2950
3466
|
redact,
|
|
2951
3467
|
verbosity,
|
|
@@ -2957,8 +3473,10 @@ export function createNotificationsExtension(
|
|
|
2957
3473
|
recordPromptTerminal,
|
|
2958
3474
|
emitPromptLifecycle,
|
|
2959
3475
|
pendingInbound: new Set<number>(),
|
|
3476
|
+
inFlightTools: new Map<string, { toolName: string; args: unknown }>(),
|
|
2960
3477
|
};
|
|
2961
|
-
|
|
3478
|
+
const initializedRuntime = runtime;
|
|
3479
|
+
runtimes.set(id, initializedRuntime);
|
|
2962
3480
|
activeRuntimeId = id;
|
|
2963
3481
|
const startSettled = Promise.withResolvers<SessionStartResult>();
|
|
2964
3482
|
sessionStartPromises.set(id, startSettled.promise);
|
|
@@ -2980,7 +3498,7 @@ export function createNotificationsExtension(
|
|
|
2980
3498
|
};
|
|
2981
3499
|
const cleanupAbandonedStartup = async (): Promise<void> => {
|
|
2982
3500
|
try {
|
|
2983
|
-
server.
|
|
3501
|
+
await server.stopAndWait();
|
|
2984
3502
|
} catch {}
|
|
2985
3503
|
try {
|
|
2986
3504
|
await host.stop();
|
|
@@ -2992,16 +3510,40 @@ export function createNotificationsExtension(
|
|
|
2992
3510
|
} catch {}
|
|
2993
3511
|
};
|
|
2994
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();
|
|
2995
3519
|
try {
|
|
2996
3520
|
server.onSdkFrame((err, inbound) => {
|
|
2997
3521
|
if (err || !inbound) return;
|
|
2998
3522
|
try {
|
|
2999
|
-
|
|
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);
|
|
3000
3528
|
} catch {}
|
|
3001
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
|
+
});
|
|
3002
3543
|
server.onConnectionClose((_err, connectionId) => {
|
|
3003
3544
|
if (!connectionId) return;
|
|
3004
3545
|
host.handleDisconnect(connectionId);
|
|
3546
|
+
hostCapCache.delete(connectionId);
|
|
3005
3547
|
for (const submission of promptSubmissions.values())
|
|
3006
3548
|
if (submission.connectionId === connectionId) abandonPrompt(submission);
|
|
3007
3549
|
});
|
|
@@ -3223,6 +3765,11 @@ export function createNotificationsExtension(
|
|
|
3223
3765
|
// thread (forwarded by the daemon over the WS, fail-closed at the daemon).
|
|
3224
3766
|
server.onInbound((err, inbound) => {
|
|
3225
3767
|
if (err || !inbound) return;
|
|
3768
|
+
const authenticatedInbound = inbound as typeof inbound & {
|
|
3769
|
+
connectionId: string;
|
|
3770
|
+
messageId?: number;
|
|
3771
|
+
reason?: string;
|
|
3772
|
+
};
|
|
3226
3773
|
if (inbound.kind === "control_command") {
|
|
3227
3774
|
const frame = sdkInboundFrame(inbound.commandJson);
|
|
3228
3775
|
if (frame) {
|
|
@@ -3230,6 +3777,25 @@ export function createNotificationsExtension(
|
|
|
3230
3777
|
return;
|
|
3231
3778
|
}
|
|
3232
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
|
+
}
|
|
3233
3799
|
|
|
3234
3800
|
if (inbound.kind === "user_message") {
|
|
3235
3801
|
// Inject as a user turn (steers/continues the agent; the resulting
|
|
@@ -3277,6 +3843,12 @@ export function createNotificationsExtension(
|
|
|
3277
3843
|
update.verbosity = inbound.verbosity;
|
|
3278
3844
|
}
|
|
3279
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
|
+
}
|
|
3280
3852
|
runtime.redact = inbound.redact;
|
|
3281
3853
|
update.redact = inbound.redact;
|
|
3282
3854
|
}
|
|
@@ -3358,6 +3930,11 @@ export function createNotificationsExtension(
|
|
|
3358
3930
|
};
|
|
3359
3931
|
host.emitEvent({ kind: identityHeader.type, payload: identityHeader });
|
|
3360
3932
|
const endpoint = await server.start();
|
|
3933
|
+
ephemeralTurns.configureAuthority({
|
|
3934
|
+
sessionId: id,
|
|
3935
|
+
endpointDigest: endpointAuthorityDigest(endpoint.url, token),
|
|
3936
|
+
eventGeneration: host.generation,
|
|
3937
|
+
});
|
|
3361
3938
|
throwIfLifecycleStopped();
|
|
3362
3939
|
if (runtimes.get(id) !== runtime) {
|
|
3363
3940
|
finishStartup({ status: "failed" });
|
|
@@ -3401,30 +3978,20 @@ export function createNotificationsExtension(
|
|
|
3401
3978
|
},
|
|
3402
3979
|
});
|
|
3403
3980
|
throwIfLifecycleStopped();
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
.append({
|
|
3408
|
-
type: "host_heartbeat",
|
|
3409
|
-
sessionId: id,
|
|
3410
|
-
locator,
|
|
3411
|
-
endpointGeneration: host.generation,
|
|
3412
|
-
pid: process.pid,
|
|
3413
|
-
})
|
|
3414
|
-
.catch(error => logger.warn(`sdk broker heartbeat failed: ${String(error)}`));
|
|
3415
|
-
}, 5_000);
|
|
3416
|
-
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.
|
|
3417
3984
|
} catch (brokerError) {
|
|
3418
3985
|
if (lifecycleRequired) throw brokerError;
|
|
3419
3986
|
logger.warn(`sdk broker registration skipped: ${String(brokerError)}`);
|
|
3420
3987
|
}
|
|
3421
3988
|
}
|
|
3422
3989
|
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
runtime.enableNotifications = () => {
|
|
3990
|
+
const startedRuntime = initializedRuntime;
|
|
3991
|
+
initializedRuntime.enableNotifications = () => {
|
|
3426
3992
|
const runtime = startedRuntime;
|
|
3427
3993
|
if (runtime.notificationsActive) return;
|
|
3994
|
+
ephemeralTurns.enable();
|
|
3428
3995
|
runtime.notificationsActive = true;
|
|
3429
3996
|
runtime.disposeAnswerSource = registerInteractiveAnswerSource(
|
|
3430
3997
|
runtime.id,
|
|
@@ -3435,35 +4002,42 @@ export function createNotificationsExtension(
|
|
|
3435
4002
|
if (runtime.redact) return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
|
|
3436
4003
|
try {
|
|
3437
4004
|
const data = await fs.promises.readFile(file.path);
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
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
|
+
);
|
|
3445
4015
|
return { ok: true };
|
|
3446
4016
|
} catch (e) {
|
|
3447
4017
|
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
3448
4018
|
}
|
|
3449
4019
|
});
|
|
3450
4020
|
};
|
|
3451
|
-
const activeRuntime =
|
|
4021
|
+
const activeRuntime = initializedRuntime;
|
|
3452
4022
|
// A native terminal close (SIGHUP), SIGTERM, Ctrl+C exit, or fatal error
|
|
3453
4023
|
// skips AgentSession.dispose(), so the `session_shutdown` extension event
|
|
3454
4024
|
// never fires and the daemon-side topic would be orphaned. postmortem
|
|
3455
4025
|
// awaits registered cleanups on those paths, so send the graceful
|
|
3456
4026
|
// `session_closed` frame from there too. stopSession() cancels this
|
|
3457
4027
|
// registration on every other teardown path, so it never double-fires.
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
4028
|
+
initializedRuntime.cancelPostmortemCleanup = postmortem.register(
|
|
4029
|
+
`notifications-session-closed:${id}`,
|
|
4030
|
+
async () => {
|
|
4031
|
+
await stopSession(initializedRuntime.id);
|
|
4032
|
+
},
|
|
4033
|
+
);
|
|
3461
4034
|
logger.info(`notifications: serving session ${id} at ${endpoint.url}`);
|
|
3462
4035
|
// A workflow-gate emitter can be installed after session startup.
|
|
3463
4036
|
// Attach dynamically so the SDK bus presents every durable gate.
|
|
3464
4037
|
const attachWorkflowGate = (gate: WorkflowGateEmitter | undefined): void => {
|
|
3465
4038
|
if (activeRuntime.workflowGate === gate) return;
|
|
3466
4039
|
activeRuntime.disposeGateListener();
|
|
4040
|
+
activeRuntime.workflowGate?.setRuntimeTurnProvider?.(null);
|
|
3467
4041
|
activeRuntime.disposeAckRecoveryParticipant();
|
|
3468
4042
|
gatePresentations.dispose();
|
|
3469
4043
|
activeRuntime.disposeGateTerminalController();
|
|
@@ -3476,6 +4050,7 @@ export function createNotificationsExtension(
|
|
|
3476
4050
|
return;
|
|
3477
4051
|
}
|
|
3478
4052
|
activeRuntime.workflowGate = gate;
|
|
4053
|
+
gate.setRuntimeTurnProvider?.(() => activeRuntime.activePromptCorrelation?.turnId);
|
|
3479
4054
|
if (hasTerminalArbitrationCapability(gate)) {
|
|
3480
4055
|
const controller: WorkflowGateTerminalController = {
|
|
3481
4056
|
completeGateInteractions: gateId => gatePresentations.complete(gateId),
|
|
@@ -3529,18 +4104,30 @@ export function createNotificationsExtension(
|
|
|
3529
4104
|
});
|
|
3530
4105
|
activeRuntime.disposeAckRecoveryParticipant = () => gate.setAckRecoveryParticipant?.(null);
|
|
3531
4106
|
}
|
|
3532
|
-
void gate.recoverAcceptedGates
|
|
4107
|
+
void (typeof gate.recoverAcceptedGates === "function"
|
|
4108
|
+
? trackGateResolution(gate.recoverAcceptedGates()).catch(() => {})
|
|
4109
|
+
: Promise.resolve());
|
|
3533
4110
|
};
|
|
3534
4111
|
activeRuntime.disposeGateEmitterListener = registerWorkflowGateEmitterListener(id, attachWorkflowGate);
|
|
3535
4112
|
if (ctx.workflowGate) attachWorkflowGate(ctx.workflowGate);
|
|
3536
|
-
if (notificationsEnabledForSession)
|
|
3537
|
-
finishStartup({ status: "started", runtime });
|
|
3538
|
-
return { status: "started", runtime };
|
|
4113
|
+
if (notificationsEnabledForSession) initializedRuntime.enableNotifications();
|
|
4114
|
+
finishStartup({ status: "started", runtime: initializedRuntime });
|
|
4115
|
+
return { status: "started", runtime: initializedRuntime };
|
|
3539
4116
|
} catch (e) {
|
|
3540
4117
|
logger.warn(`notifications: failed to start server: ${String(e)}`);
|
|
3541
4118
|
const result = failLifecycleStartup("failed", e);
|
|
3542
4119
|
finishStartup(result);
|
|
3543
|
-
|
|
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();
|
|
3544
4131
|
return { ...result, runtime };
|
|
3545
4132
|
}
|
|
3546
4133
|
}
|
|
@@ -3665,13 +4252,27 @@ export function createNotificationsExtension(
|
|
|
3665
4252
|
event: { previousSessionFile?: string },
|
|
3666
4253
|
ctx: ExtensionContext,
|
|
3667
4254
|
): Promise<void> => {
|
|
4255
|
+
if (extensionShuttingDown) return;
|
|
3668
4256
|
const newId = sessionId(ctx);
|
|
3669
4257
|
const prevId = activeRuntimeId ?? sessionIdFromFile(event.previousSessionFile);
|
|
3670
4258
|
if (prevId && prevId !== newId) {
|
|
3671
4259
|
controller.rekeySession(prevId, newId);
|
|
3672
|
-
|
|
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
|
+
}
|
|
3673
4269
|
}
|
|
4270
|
+
if (extensionShuttingDown) return;
|
|
3674
4271
|
await startSession(ctx);
|
|
4272
|
+
if (extensionShuttingDown) {
|
|
4273
|
+
await stopSession(newId);
|
|
4274
|
+
return;
|
|
4275
|
+
}
|
|
3675
4276
|
await controller.reconcileCurrentSession(ctx);
|
|
3676
4277
|
};
|
|
3677
4278
|
api.on("session_switch", async (event, ctx) => {
|
|
@@ -3689,6 +4290,19 @@ export function createNotificationsExtension(
|
|
|
3689
4290
|
await rotateSessionAuthority(event, ctx);
|
|
3690
4291
|
});
|
|
3691
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
|
+
|
|
3692
4306
|
// Drive the live typing indicator: mark busy when the agent loop starts so
|
|
3693
4307
|
// the daemon shows "typing…" in the thread while the agent is thinking,
|
|
3694
4308
|
// before any turn output exists. Cleared on `agent_end` below.
|
|
@@ -3717,7 +4331,9 @@ export function createNotificationsExtension(
|
|
|
3717
4331
|
api.on("turn_start", (_event, ctx) => {
|
|
3718
4332
|
const id = sessionId(ctx);
|
|
3719
4333
|
const rt = runtimes.get(id);
|
|
3720
|
-
if (!rt
|
|
4334
|
+
if (!rt) return;
|
|
4335
|
+
rt.turnSeq = (rt.turnSeq ?? 0) + 1;
|
|
4336
|
+
if (!rt.notificationsActive) return;
|
|
3721
4337
|
// A new turn is live: re-open the live-stream window (see turnClosed).
|
|
3722
4338
|
rt.turnClosed = false;
|
|
3723
4339
|
if (rt.pendingInbound.size === 0) return;
|
|
@@ -3735,7 +4351,7 @@ export function createNotificationsExtension(
|
|
|
3735
4351
|
// per `turn_end`. turn_end fires once per turn iteration, so a single
|
|
3736
4352
|
// user-visible idle previously produced many idle pings (the flood); agent_end
|
|
3737
4353
|
// fires exactly once per settle, yielding exactly one idle notification.
|
|
3738
|
-
api.on("agent_end", (
|
|
4354
|
+
api.on("agent_end", (event, ctx) => {
|
|
3739
4355
|
const id = sessionId(ctx);
|
|
3740
4356
|
const rt = runtimes.get(id);
|
|
3741
4357
|
if (!rt) return;
|
|
@@ -3748,13 +4364,17 @@ export function createNotificationsExtension(
|
|
|
3748
4364
|
} else {
|
|
3749
4365
|
rt.emitPromptLifecycle(undefined, { type: "agent_end", sessionId: id });
|
|
3750
4366
|
}
|
|
4367
|
+
rt.activePromptCorrelation = undefined;
|
|
4368
|
+
terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "unknown");
|
|
3751
4369
|
try {
|
|
3752
4370
|
pushSessionFrame(rt, { type: "activity", sessionId: id, state: "idle" });
|
|
3753
4371
|
} catch (e) {
|
|
3754
4372
|
logger.warn(`notifications: activity (idle) failed: ${String(e)}`);
|
|
3755
4373
|
}
|
|
3756
4374
|
if (!rt.notificationsActive) return;
|
|
3757
|
-
void rt.workflowGate?.recoverAcceptedGates
|
|
4375
|
+
void (typeof rt.workflowGate?.recoverAcceptedGates === "function"
|
|
4376
|
+
? rt.trackGateResolution(rt.workflowGate.recoverAcceptedGates()).catch(() => {})
|
|
4377
|
+
: Promise.resolve());
|
|
3758
4378
|
const seq = rt.idleSeq++;
|
|
3759
4379
|
// Re-assert the identity header so the daemon renames the topic once the
|
|
3760
4380
|
// session title has been auto-generated ("{repo}/{branch} - {title}"). The
|
|
@@ -3826,11 +4446,10 @@ export function createNotificationsExtension(
|
|
|
3826
4446
|
rt.preAskFlushedText = text;
|
|
3827
4447
|
// Decision A: a stream-enabled turn must finalize as an in-place edit of ONE
|
|
3828
4448
|
// live message, never a fresh (rich-promotable) send. If live frames were
|
|
3829
|
-
// async-queued and none landed before this flush,
|
|
3830
|
-
//
|
|
3831
|
-
//
|
|
3832
|
-
if (finalAnswer && rt.stream && rt.liveRef === undefined) {
|
|
3833
|
-
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) {
|
|
3834
4453
|
rt.liveRef = String(rt.turnSeq);
|
|
3835
4454
|
}
|
|
3836
4455
|
try {
|
|
@@ -3854,11 +4473,99 @@ export function createNotificationsExtension(
|
|
|
3854
4473
|
// path and ordered before it — unlike message_update, which is queued async),
|
|
3855
4474
|
// then flushed here before the ask tool's execute calls registerAsk.
|
|
3856
4475
|
api.on("tool_execution_start", (event, ctx) => {
|
|
3857
|
-
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
|
+
}
|
|
3858
4482
|
const id = sessionId(ctx);
|
|
3859
4483
|
const rt = runtimes.get(id);
|
|
3860
4484
|
if (!rt?.notificationsActive || rt.redact) return;
|
|
3861
|
-
|
|
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
|
+
}
|
|
3862
4569
|
});
|
|
3863
4570
|
|
|
3864
4571
|
api.on("turn_end", (event, ctx) => {
|
|
@@ -3888,8 +4595,7 @@ export function createNotificationsExtension(
|
|
|
3888
4595
|
const rt = runtimes.get(id);
|
|
3889
4596
|
if (!rt?.notificationsActive || !rt.stream || rt.redact || rt.turnClosed) return;
|
|
3890
4597
|
if ((event.message as { role?: unknown }).role !== "assistant") return;
|
|
3891
|
-
if (rt.liveRef === undefined) {
|
|
3892
|
-
rt.turnSeq = (rt.turnSeq ?? 0) + 1;
|
|
4598
|
+
if (rt.liveRef === undefined && rt.turnSeq !== undefined) {
|
|
3893
4599
|
rt.liveRef = String(rt.turnSeq);
|
|
3894
4600
|
}
|
|
3895
4601
|
const now = Date.now();
|
|
@@ -3935,9 +4641,24 @@ export function createNotificationsExtension(
|
|
|
3935
4641
|
});
|
|
3936
4642
|
|
|
3937
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");
|
|
3938
4650
|
const controllerStop =
|
|
3939
4651
|
typeof ctx.sessionManager.getCwd === "function" ? controller.stopCurrentSession(ctx) : Promise.resolve(false);
|
|
3940
4652
|
void controllerStop.catch(error => logger.warn(`notifications: controller shutdown failed: ${String(error)}`));
|
|
3941
|
-
|
|
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
|
+
}
|
|
3942
4663
|
});
|
|
3943
4664
|
}
|