@sayknow-cli/coding-agent 0.4.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/cli.d.ts +9 -1
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +34 -4
- package/dist/types/config/model-profile-contract.d.ts +40 -0
- package/dist/types/config/model-profiles.d.ts +2 -3
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +156 -10
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/extensibility/extensions/types.d.ts +43 -2
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +33 -2
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +22 -9
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/tool-status-header.d.ts +4 -2
- package/dist/types/modes/components/welcome.d.ts +5 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +13 -2
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/runtime/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +1 -1
- package/dist/types/runtime-mcp/manager.d.ts +9 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/runtime-mcp/redaction.d.ts +2 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +13 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +9 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +52 -4
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +106 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +33 -9
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +286 -12
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/host/control/operations.d.ts +3 -2
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/query/handlers.d.ts +14 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/session.d.ts +7 -0
- package/dist/types/sdk/startup-capability.d.ts +9 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +83 -24
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +70 -10
- package/dist/types/session/internal/managed-session-storage.d.ts +34 -12
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +89 -18
- package/dist/types/session/session-storage.d.ts +31 -0
- package/dist/types/session/streaming-output.d.ts +60 -5
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/skc-runtime/deep-interview-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/skc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/skc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +112 -1
- package/dist/types/skc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/skc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/skc-runtime/state-writer.d.ts +5 -1
- package/dist/types/skc-runtime/team-launch.d.ts +1 -1
- package/dist/types/skc-runtime/team-runtime.d.ts +9 -2
- package/dist/types/skc-runtime/team-store.d.ts +12 -0
- package/dist/types/skc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +7 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +6 -5
- package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +28 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +43 -5
- package/dist/types/skill-state/workflow-hud.d.ts +3 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/provider-retry-status.d.ts +13 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +133 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +22 -9
- package/dist/types/tools/image-gen.d.ts +63 -1
- package/dist/types/tools/index.d.ts +7 -1
- package/dist/types/tools/output-meta.d.ts +21 -1
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -1
- package/dist/types/tools/resource-gc.d.ts +23 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -12
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/compile-args.ts +0 -1
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-docs-index.ts +4 -1
- package/scripts/generate-sdk-operation-inventory.ts +9 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/scripts/verify-skc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/capability/index.ts +58 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/args.ts +16 -100
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/mcp-cli.ts +4 -21
- package/src/cli/notify-cli.ts +6 -38
- package/src/cli/read-cli.ts +2 -1
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +133 -11
- package/src/commands/daemon.ts +22 -5
- package/src/commands/deep-interview.ts +26 -1
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/sdk.ts +134 -17
- package/src/commands/team.ts +1 -1
- package/src/commit/agentic/index.ts +3 -3
- package/src/commit/map-reduce/index.ts +2 -2
- package/src/config/keybindings.ts +158 -25
- package/src/config/model-profile-contract.ts +174 -0
- package/src/config/model-profiles.ts +51 -10
- package/src/config/model-registry.ts +24 -4
- package/src/config/model-resolver.ts +2 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +157 -10
- package/src/config/settings.ts +254 -108
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/skc/skills/deep-interview/SKILL.md +147 -24
- package/src/defaults/skc/skills/ralplan/SKILL.md +91 -34
- package/src/defaults/skc/skills/team/SKILL.md +1 -1
- package/src/defaults/skc/skills/ultragoal/SKILL.md +68 -17
- package/src/discovery/ssh.ts +12 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/js/tool-bridge.ts +1 -1
- package/src/eval/py/executor.ts +37 -13
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +42 -4
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/extensibility/shared-events.ts +5 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +2 -1
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/internal-urls/docs-index.generated.ts +17 -16
- package/src/internal-urls/local-protocol.ts +87 -25
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +120 -42
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +52 -0
- package/src/modes/acp/acp-agent.ts +647 -83
- package/src/modes/acp/acp-event-mapper.ts +106 -12
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +61 -4
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/sayknow-pet-widget.ts +27 -8
- package/src/modes/components/session-observer-overlay.ts +187 -32
- package/src/modes/components/settings-selector.ts +60 -18
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/components/tool-status-header.ts +26 -8
- package/src/modes/components/welcome.ts +50 -23
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/command-controller.ts +101 -54
- package/src/modes/controllers/event-controller.ts +119 -27
- package/src/modes/controllers/extension-ui-controller.ts +23 -12
- package/src/modes/controllers/input-controller.ts +100 -8
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +4 -1
- package/src/modes/interactive-mode.ts +201 -60
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/runtime-init.ts +20 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +3 -3
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +6 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +99 -4
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/architect.md +17 -3
- package/src/prompts/agents/critic.md +17 -2
- package/src/prompts/agents/executor.md +1 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +6 -3
- package/src/prompts/tools/cron.md +3 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/discoverable-tool-metadata.ts +1 -1
- package/src/runtime-mcp/json-rpc.ts +28 -11
- package/src/runtime-mcp/manager.ts +77 -59
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/redaction.ts +30 -0
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/runtime-mcp/transports/http.ts +63 -22
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/identity.ts +22 -12
- package/src/sdk/broker/lifecycle.ts +293 -43
- package/src/sdk/broker/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +22 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +10 -2
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/index.ts +793 -121
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/lifecycle-commands.ts +63 -10
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +87 -25
- package/src/sdk/bus/operator-runtime.ts +31 -5
- package/src/sdk/bus/prompt-reconciliation.ts +240 -0
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/bus/telegram-cli.ts +6 -13
- package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +34 -9
- package/src/sdk/bus/telegram-daemon-control.ts +161 -40
- package/src/sdk/bus/telegram-daemon.ts +3649 -593
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/host/control/dispatch.ts +8 -2
- package/src/sdk/host/control/operations.ts +3 -2
- package/src/sdk/host/host.ts +27 -9
- package/src/sdk/host/query/handlers.ts +125 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +145 -0
- package/src/sdk/protocol/operation-registry.ts +24 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +69 -8
- package/src/sdk/startup-capability.ts +15 -1
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +1112 -314
- package/src/session/blob-store.ts +25 -7
- package/src/session/btw-contract.ts +59 -0
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +1426 -259
- package/src/session/internal/managed-session-storage.ts +414 -238
- package/src/session/internal/native-publish-outcome.ts +271 -0
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +2079 -469
- package/src/session/session-storage.ts +151 -11
- package/src/session/streaming-output.ts +262 -43
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +49 -5
- package/src/setup/provider-presets.json +2 -1
- package/src/skc-runtime/deep-interview-runtime.ts +303 -36
- package/src/skc-runtime/deep-interview-stage.ts +1016 -0
- package/src/skc-runtime/deep-interview-state.ts +8 -5
- package/src/skc-runtime/gc-render.ts +1 -0
- package/src/skc-runtime/gc-runtime.ts +11 -1
- package/src/skc-runtime/linux-proc.ts +70 -36
- package/src/skc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/skc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/skc-runtime/ralplan-runtime.ts +1057 -75
- package/src/skc-runtime/repository-binding.ts +267 -0
- package/src/skc-runtime/session-state-sidecar.ts +25 -9
- package/src/skc-runtime/state-runtime.ts +50 -12
- package/src/skc-runtime/state-writer.ts +68 -56
- package/src/skc-runtime/team-launch.ts +34 -5
- package/src/skc-runtime/team-runtime.ts +41 -1
- package/src/skc-runtime/team-store.ts +32 -0
- package/src/skc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/skc-runtime/team-workers.ts +1 -0
- package/src/skc-runtime/tmux-common.ts +13 -2
- package/src/skc-runtime/tmux-owner-isolation.ts +20 -1
- package/src/skc-runtime/tmux-sessions.ts +45 -6
- package/src/skc-runtime/ultragoal-guard.ts +93 -15
- package/src/skc-runtime/ultragoal-receipt-freshness.ts +123 -0
- package/src/skc-runtime/ultragoal-runtime.ts +469 -38
- package/src/skc-runtime/workflow-manifest.generated.json +195 -5
- package/src/skc-runtime/workflow-manifest.ts +67 -3
- package/src/skill-state/workflow-hud.ts +25 -2
- package/src/skill-state/workflow-mutation-guard.ts +104 -12
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/task/executor.ts +165 -47
- package/src/task/index.ts +123 -15
- package/src/task/provider-retry-status.ts +88 -0
- package/src/task/receipt.ts +3 -0
- package/src/task/render.ts +45 -26
- package/src/task/skc-command.ts +2 -2
- package/src/task/types.ts +55 -10
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/ask.ts +1 -1
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-allowed-prefixes.ts +45 -1
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +127 -154
- package/src/tools/image-gen.ts +476 -23
- package/src/tools/index.ts +7 -1
- package/src/tools/output-meta.ts +176 -9
- package/src/tools/path-utils.ts +5 -1
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1544 -310
- package/src/tools/resource-gc.ts +665 -125
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/tools/subagent-render.ts +74 -16
- package/src/tools/subagent.ts +2 -0
- package/src/tools/tool-result.ts +14 -1
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +140 -27
- package/src/web/scrapers/docs-rs.ts +30 -5
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
- package/src/web/search/providers/codex.ts +1 -1
- package/src/workflow/workflow-intent-diff.ts +4 -1
package/src/sdk/bus/index.ts
CHANGED
|
@@ -30,9 +30,12 @@ import { promisify } from "node:util";
|
|
|
30
30
|
import { ThinkingLevel } from "@sayknow-cli/agent-core";
|
|
31
31
|
import type { ImageContent, TextContent, Tool } from "@sayknow-cli/ai";
|
|
32
32
|
import { NotificationServer, nativeBuildInfo } from "@sayknow-cli/natives";
|
|
33
|
-
import { logger, postmortem,
|
|
33
|
+
import { logger, postmortem, VERSION } from "@sayknow-cli/utils";
|
|
34
|
+
import { isModelProfileProviderAvailable, projectModelProfileCatalog } from "../../config/model-profile-contract";
|
|
35
|
+
import { isAuthenticated, kNoAuth } from "../../config/model-registry";
|
|
34
36
|
import { Settings } from "../../config/settings";
|
|
35
37
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
|
|
38
|
+
import { INTERACTIVE_SELECTOR_RESUME_ORIGIN } from "../../extensibility/shared-events";
|
|
36
39
|
import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
|
|
37
40
|
import {
|
|
38
41
|
NotificationGatePolicyChangedError,
|
|
@@ -40,7 +43,6 @@ import {
|
|
|
40
43
|
type WorkflowGateTerminalController,
|
|
41
44
|
type WorkflowGateTerminalProof,
|
|
42
45
|
} from "../../modes/shared/agent-wire/workflow-gate-broker";
|
|
43
|
-
import btwUserPrompt from "../../prompts/system/btw-user.md" with { type: "text" };
|
|
44
46
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
45
47
|
import { parseThinkingLevel } from "../../thinking";
|
|
46
48
|
import type {
|
|
@@ -54,19 +56,20 @@ import type {
|
|
|
54
56
|
AskSettlementResult,
|
|
55
57
|
} from "../../tools";
|
|
56
58
|
import { registerAskAnswerSource, registerWorkflowGateEmitterListener } from "../../tools/ask-answer-registry";
|
|
59
|
+
import { acpFinalTextFromMessage } from "../acp/final-text";
|
|
57
60
|
import { ensureBroker } from "../broker/ensure";
|
|
58
61
|
import { SessionIndex } from "../broker/session-index";
|
|
59
62
|
import { SessionSdkHost, shouldHostSdk } from "../host";
|
|
60
63
|
import { type ControlSurface, dispatchControl } from "../host/control";
|
|
61
64
|
import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "../host/query";
|
|
62
65
|
import { projectQ10Models } from "../models.js";
|
|
66
|
+
import { PROMPT_CLIENT_REF_MAX_LENGTH } from "../prompt-status";
|
|
63
67
|
import { OPERATIONS } from "../protocol/operation-registry";
|
|
64
68
|
import {
|
|
65
69
|
lifecycleStartupCapabilityForApi,
|
|
66
70
|
normalizeSdkStartupFailure,
|
|
67
71
|
type SdkStartupFailure,
|
|
68
72
|
} from "../startup-capability";
|
|
69
|
-
|
|
70
73
|
import { registerTelegramFileSink } from "./attachment-registry";
|
|
71
74
|
import { ensureDiscordDaemon, ensureSlackDaemon } from "./chat-daemon-control";
|
|
72
75
|
import {
|
|
@@ -80,16 +83,37 @@ import {
|
|
|
80
83
|
sessionTag,
|
|
81
84
|
} from "./config";
|
|
82
85
|
import { telegramControlCommandUsage } from "./config-commands";
|
|
86
|
+
import {
|
|
87
|
+
isNativeControlDrainAvailable,
|
|
88
|
+
runIdentityControlSuccessPath,
|
|
89
|
+
type TerminalSendOutcome,
|
|
90
|
+
} from "./control-drain-lease";
|
|
83
91
|
import { imageAttachmentsFromMessage, notificationActionPayload, summaryFromMessage, truncate } from "./helpers";
|
|
92
|
+
import { createKindAwareReconciliation } from "./kind-aware-reconciliation";
|
|
84
93
|
import { assertNativeRuntimeCompatibility } from "./native-runtime-compatibility";
|
|
94
|
+
import { createPromptReconciliation, sanitizePromptFailure } from "./prompt-reconciliation";
|
|
95
|
+
import { createReconciliationStore } from "./reconciliation-store";
|
|
85
96
|
import { NotificationSessionController, type NotificationSessionRuntime } from "./session-control";
|
|
86
97
|
import {
|
|
87
98
|
ASK_SELECTED_ACK_CAPABILITY,
|
|
88
99
|
type EnsureDaemonResult,
|
|
89
100
|
endpointAuthorityDigest,
|
|
90
101
|
ensureTelegramDaemonRunningDetailed,
|
|
102
|
+
type RegisterNotificationRootResult,
|
|
103
|
+
unregisterNotificationRoot,
|
|
91
104
|
} from "./telegram-daemon";
|
|
92
105
|
|
|
106
|
+
export type {
|
|
107
|
+
IdentityControlSuccessPathInput,
|
|
108
|
+
IdentityControlTerminalPathInput,
|
|
109
|
+
TerminalSendOutcome,
|
|
110
|
+
} from "./control-drain-lease";
|
|
111
|
+
export {
|
|
112
|
+
isNativeControlDrainAvailable,
|
|
113
|
+
runIdentityControlSuccessPath,
|
|
114
|
+
runIdentityControlTerminalPath,
|
|
115
|
+
} from "./control-drain-lease";
|
|
116
|
+
|
|
93
117
|
// ===========================================================================
|
|
94
118
|
// Session lifecycle control protocol (TypeScript mirror of the Rust wire
|
|
95
119
|
// contract in `crates/skc-sdk/src/lifecycle.rs`).
|
|
@@ -402,6 +426,7 @@ interface UnattendedGatePresentation {
|
|
|
402
426
|
question: string;
|
|
403
427
|
options: string[];
|
|
404
428
|
controls: readonly AskRemoteControl[];
|
|
429
|
+
recommendedIndex?: number;
|
|
405
430
|
multi: boolean;
|
|
406
431
|
allowEmpty: boolean;
|
|
407
432
|
navigationLabel?: "Next" | "Done";
|
|
@@ -410,6 +435,14 @@ interface UnattendedGatePresentation {
|
|
|
410
435
|
onActivated?: (actionId: string, lease: { actionId: string; registrationEpoch: number }) => void;
|
|
411
436
|
onClosed?: () => void;
|
|
412
437
|
}
|
|
438
|
+
function recommendedIndexFromGateOptions(options: readonly unknown[]): number | undefined {
|
|
439
|
+
const descriptions = options.map(option => (option as { description?: unknown }).description);
|
|
440
|
+
const recommended = descriptions.filter(description => description === "recommended");
|
|
441
|
+
return recommended.length === 1 &&
|
|
442
|
+
descriptions.every(description => description === undefined || description === "recommended")
|
|
443
|
+
? descriptions.indexOf("recommended")
|
|
444
|
+
: undefined;
|
|
445
|
+
}
|
|
413
446
|
|
|
414
447
|
type RetireStatus = "retired" | "already_terminal" | "claimed" | "stale";
|
|
415
448
|
type DirectControlOutcome = "accepted" | "rejected" | "unknown";
|
|
@@ -740,6 +773,9 @@ export class PresentationArbiter {
|
|
|
740
773
|
? `(${presentation.selectedOptions.length} selected) ${presentation.question}`
|
|
741
774
|
: presentation.question,
|
|
742
775
|
options: presentation.options,
|
|
776
|
+
...(presentation.recommendedIndex === undefined
|
|
777
|
+
? {}
|
|
778
|
+
: { recommendedIndex: presentation.recommendedIndex }),
|
|
743
779
|
controls: presentation.multi
|
|
744
780
|
? [
|
|
745
781
|
{
|
|
@@ -869,6 +905,8 @@ interface SessionRuntime {
|
|
|
869
905
|
disposeGateListener: () => void;
|
|
870
906
|
/** Whether notification-only delivery and answer resources are active. */
|
|
871
907
|
notificationsActive: boolean;
|
|
908
|
+
/** Rejects new SDK frames while a leased terminal response drains. */
|
|
909
|
+
inboundFenced: boolean;
|
|
872
910
|
/** Set as soon as terminal teardown is requested, before startup settles. */
|
|
873
911
|
stopping: boolean;
|
|
874
912
|
/** Recreates notification-only resources after `/notify on`. */
|
|
@@ -896,6 +934,8 @@ interface SessionRuntime {
|
|
|
896
934
|
hostStopped: boolean;
|
|
897
935
|
serverStopped: boolean;
|
|
898
936
|
brokerRegistrationReleased: boolean;
|
|
937
|
+
/** Managed Telegram root registration released during terminal teardown. */
|
|
938
|
+
notificationRootRegistration?: { settings: Settings; cwd: string; registrationToken?: string };
|
|
899
939
|
verbosity: "lean" | "verbose";
|
|
900
940
|
sessionTag: string;
|
|
901
941
|
/** Whether the agent loop is currently running (drives the typing indicator). */
|
|
@@ -907,11 +947,24 @@ interface SessionRuntime {
|
|
|
907
947
|
/** Records a correlated prompt terminal boundary after agent unwind. */
|
|
908
948
|
/** Atomically claims a correlated prompt terminal boundary after agent unwind. */
|
|
909
949
|
recordPromptTerminal: (correlation: { commandId: string; turnId: string } | undefined) => boolean;
|
|
950
|
+
/** Transitions the authoritative reconciliation record at lifecycle ingress; terminal outcomes settle once. */
|
|
951
|
+
notePromptReconciliation: (
|
|
952
|
+
correlation: { commandId: string; turnId: string } | undefined,
|
|
953
|
+
frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
|
|
954
|
+
) => void;
|
|
955
|
+
/** Settles and emits one sanitized correlated prompt failure. */
|
|
956
|
+
emitPromptFailure: (correlation: { commandId: string; turnId: string }, error: unknown) => void;
|
|
910
957
|
/** Records correlated lifecycle frames for replay and delivers them only to the accepted requester after acknowledgement. */
|
|
911
958
|
emitPromptLifecycle: (
|
|
912
959
|
correlation: { commandId: string; turnId: string } | undefined,
|
|
913
960
|
frame:
|
|
914
|
-
| {
|
|
961
|
+
| {
|
|
962
|
+
type: "agent_start" | "agent_end";
|
|
963
|
+
sessionId: string;
|
|
964
|
+
commandId?: string;
|
|
965
|
+
turnId?: string;
|
|
966
|
+
finalText?: string;
|
|
967
|
+
}
|
|
915
968
|
| {
|
|
916
969
|
type: "agent_failed";
|
|
917
970
|
sessionId: string;
|
|
@@ -940,12 +993,21 @@ interface SessionRuntime {
|
|
|
940
993
|
turnClosed?: boolean;
|
|
941
994
|
/** Finalized while provisional policy was held; flush exactly once on stable activation. */
|
|
942
995
|
pendingFinal?: { text?: string; messageRef?: string };
|
|
996
|
+
/**
|
|
997
|
+
* Lean-mode deferred final answer: latest assistant text observed at `turn_end`,
|
|
998
|
+
* emitted once at `agent_end` so intermediate tool-turn narration does not flood
|
|
999
|
+
* remote clients. Cleared after flush or when replaced by a newer turn.
|
|
1000
|
+
*/
|
|
1001
|
+
pendingSettled?: { text: string; messageRef?: string };
|
|
943
1002
|
/** Durable gates emitted while ownership is provisional; presented only after stable activation. */
|
|
944
1003
|
deferredGatePresentations: Array<() => void>;
|
|
945
1004
|
/** SDK control frames received during provisional ownership; replayed only after stable activation. */
|
|
946
1005
|
deferredInboundControls: Array<() => void>;
|
|
947
1006
|
/** Started tool calls awaiting a terminal activity frame, keyed by tool call id. */
|
|
948
|
-
inFlightTools: Map<
|
|
1007
|
+
inFlightTools: Map<
|
|
1008
|
+
string,
|
|
1009
|
+
{ toolName: string; args?: unknown; pendingPhase?: "completed" | "failed" | "cancelled" }
|
|
1010
|
+
>;
|
|
949
1011
|
/** Cancels the postmortem cleanup that emits `session_closed` on process teardown. */
|
|
950
1012
|
cancelPostmortemCleanup: () => void;
|
|
951
1013
|
/** Disposes side-turn resources when their owning logical session becomes unavailable. */
|
|
@@ -955,7 +1017,6 @@ interface SessionRuntime {
|
|
|
955
1017
|
const SENSITIVE_MODEL_LABEL =
|
|
956
1018
|
/(?:\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;
|
|
957
1019
|
const TOOL_SUMMARY_MAX = 280;
|
|
958
|
-
const EMPTY_CAPABILITIES: ReadonlySet<string> = new Set();
|
|
959
1020
|
|
|
960
1021
|
/** Stable projection of the tool-owned safe-display seam (never the full Tool surface). */
|
|
961
1022
|
type SafeSummaryTool = Pick<Tool, "safeSummary" | "safeSummaryFields">;
|
|
@@ -1020,6 +1081,14 @@ function pushSessionFrame(
|
|
|
1020
1081
|
runtime.server.pushFrame(JSON.stringify(frame));
|
|
1021
1082
|
}
|
|
1022
1083
|
|
|
1084
|
+
async function pushTerminalSessionFrame(
|
|
1085
|
+
runtime: Pick<SessionRuntime, "server" | "host">,
|
|
1086
|
+
frame: { type: "session_closed"; sessionId: string },
|
|
1087
|
+
): Promise<boolean> {
|
|
1088
|
+
runtime.host.emitEvent({ kind: frame.type, payload: frame });
|
|
1089
|
+
return await runtime.server.pushFrameAndWait(JSON.stringify(frame), 1_000);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1023
1092
|
function pushFileAttachment(
|
|
1024
1093
|
runtime: Pick<SessionRuntime, "server" | "host">,
|
|
1025
1094
|
frame: { type: "file_attachment"; sessionId: string; name: string; mime?: string; caption?: string },
|
|
@@ -1068,7 +1137,7 @@ const defaultConfig: NotificationConfig = {
|
|
|
1068
1137
|
idleTimeoutMs: 60_000,
|
|
1069
1138
|
rich: { enabled: true },
|
|
1070
1139
|
richDraft: { enabled: false },
|
|
1071
|
-
toolActivity: { enabled:
|
|
1140
|
+
toolActivity: { enabled: false },
|
|
1072
1141
|
streaming: { enabled: true },
|
|
1073
1142
|
topics: {},
|
|
1074
1143
|
btw: { enabled: true },
|
|
@@ -1594,6 +1663,7 @@ function registerInteractiveAnswerSource(
|
|
|
1594
1663
|
question: request.question,
|
|
1595
1664
|
options: request.options,
|
|
1596
1665
|
controls: request.controls,
|
|
1666
|
+
recommendedIndex: request.recommendedIndex,
|
|
1597
1667
|
multi: false,
|
|
1598
1668
|
allowEmpty: false,
|
|
1599
1669
|
selectedOptions: [],
|
|
@@ -1660,6 +1730,7 @@ const UNINSTALLED_CONTROL_OPERATIONS = new Set(["auth.login", "host_tools.regist
|
|
|
1660
1730
|
|
|
1661
1731
|
const CONTROL_BINDINGS: Readonly<Record<string, string | undefined>> = {
|
|
1662
1732
|
"model.cycle": "cycleModel",
|
|
1733
|
+
"model.profile.set": "setModelProfile",
|
|
1663
1734
|
"thinking.cycle": "cycleThinkingLevel",
|
|
1664
1735
|
"queue.steering_mode.set": "setQueueMode",
|
|
1665
1736
|
"queue.follow_up_mode.set": "setQueueMode",
|
|
@@ -1756,6 +1827,10 @@ function sdkQuerySurface(
|
|
|
1756
1827
|
followupQueueDepth: 0,
|
|
1757
1828
|
}),
|
|
1758
1829
|
configOverrides: ReadonlyMap<string, unknown> = new Map(),
|
|
1830
|
+
promptStatusLookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown,
|
|
1831
|
+
skillStatusLookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown = () => ({
|
|
1832
|
+
status: "unknown",
|
|
1833
|
+
}),
|
|
1759
1834
|
): SessionSurface {
|
|
1760
1835
|
const metadata = () => ({
|
|
1761
1836
|
sessionId: id,
|
|
@@ -1823,6 +1898,26 @@ function sdkQuerySurface(
|
|
|
1823
1898
|
const currentThinkingLevel = api.getThinkingLevel();
|
|
1824
1899
|
return projectQ10Models({ models, currentModel, currentThinkingLevel });
|
|
1825
1900
|
},
|
|
1901
|
+
getModelProfiles: async () => {
|
|
1902
|
+
const profiles = ctx.modelRegistry.getModelProfiles();
|
|
1903
|
+
const catalog = projectModelProfileCatalog(profiles, ctx.modelRegistry.getError());
|
|
1904
|
+
const providers = new Set([...profiles.values()].flatMap(profile => profile.requiredProviders));
|
|
1905
|
+
const authenticatedProviders = new Set<string>();
|
|
1906
|
+
await Promise.all(
|
|
1907
|
+
[...providers].map(async provider => {
|
|
1908
|
+
try {
|
|
1909
|
+
const credential = await ctx.modelRegistry.getApiKeyForProvider(provider, id);
|
|
1910
|
+
if (credential === kNoAuth || isAuthenticated(credential)) authenticatedProviders.add(provider);
|
|
1911
|
+
} catch {
|
|
1912
|
+
// A provider whose credential state cannot be read is not currently configurable.
|
|
1913
|
+
}
|
|
1914
|
+
}),
|
|
1915
|
+
);
|
|
1916
|
+
return catalog.map(item => ({
|
|
1917
|
+
...item,
|
|
1918
|
+
available: isModelProfileProviderAvailable(profiles.get(item.id)!, authenticatedProviders),
|
|
1919
|
+
}));
|
|
1920
|
+
},
|
|
1826
1921
|
getSkillState: () => ctx.getSkillState(),
|
|
1827
1922
|
getGates: () => {
|
|
1828
1923
|
const workflowGate = ctx.workflowGate;
|
|
@@ -1862,6 +1957,10 @@ function sdkQuerySurface(
|
|
|
1862
1957
|
getExtensions: () => ctx.getExtensions(),
|
|
1863
1958
|
getArtifactRange: (id, offset, length) => ctx.getArtifactRange?.(id, offset, length),
|
|
1864
1959
|
getJobs: () => ctx.getJobs(),
|
|
1960
|
+
getPromptStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
|
|
1961
|
+
promptStatusLookup(selector),
|
|
1962
|
+
getSkillInvokeStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
|
|
1963
|
+
skillStatusLookup(selector),
|
|
1865
1964
|
installedQueries: installedOperations(ctx, "query"),
|
|
1866
1965
|
};
|
|
1867
1966
|
}
|
|
@@ -1887,13 +1986,31 @@ function sdkControlSurface(
|
|
|
1887
1986
|
onPromptAccepted: (
|
|
1888
1987
|
correlation: { commandId: string; turnId: string },
|
|
1889
1988
|
requesterConnectionId?: string,
|
|
1890
|
-
|
|
1989
|
+
clientRef?: string,
|
|
1990
|
+
trackReconciliation?: boolean,
|
|
1991
|
+
) => void | Promise<void> = () => {},
|
|
1891
1992
|
onPromptFailed: (correlation: { commandId: string; turnId: string }, error: unknown) => void = () => {},
|
|
1892
1993
|
acceptGateResolution: () => boolean,
|
|
1893
1994
|
trackGateResolution: <T>(resolution: Promise<T>) => Promise<T>,
|
|
1995
|
+
admitPrompt: (clientRef?: string) => void,
|
|
1996
|
+
releasePromptAdmission: (clientRef?: string) => void,
|
|
1894
1997
|
settings?: Settings,
|
|
1895
1998
|
configOverrides: Map<string, unknown> = new Map(),
|
|
1896
1999
|
configRevision: { current: number } = { current: 0 },
|
|
2000
|
+
skillRecon?: {
|
|
2001
|
+
admit: (clientRef?: string) => void;
|
|
2002
|
+
release: (clientRef?: string) => void;
|
|
2003
|
+
noteAccepted: (
|
|
2004
|
+
correlation: { commandId: string; turnId: string },
|
|
2005
|
+
clientRef?: string,
|
|
2006
|
+
extra?: { skillName?: string },
|
|
2007
|
+
) => Promise<void>;
|
|
2008
|
+
noteTransition: (
|
|
2009
|
+
correlation: { commandId: string; turnId: string } | undefined,
|
|
2010
|
+
frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
|
|
2011
|
+
) => Promise<void>;
|
|
2012
|
+
lookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown;
|
|
2013
|
+
},
|
|
1897
2014
|
): ControlSurface {
|
|
1898
2015
|
const unavailable = (operation: string, reason: string) => () => {
|
|
1899
2016
|
throw Object.assign(new Error(`${operation} is unavailable: ${reason}`), { code: "unavailable" });
|
|
@@ -1967,19 +2084,35 @@ function sdkControlSurface(
|
|
|
1967
2084
|
deliverAs?: "steer" | "followUp",
|
|
1968
2085
|
rejectWhenBusy = false,
|
|
1969
2086
|
requesterConnectionId?: string,
|
|
2087
|
+
clientRef?: string,
|
|
2088
|
+
trackReconciliation = false,
|
|
1970
2089
|
) => {
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
2090
|
+
const trimmedClientRef = typeof clientRef === "string" ? clientRef.trim() : undefined;
|
|
2091
|
+
if (clientRef !== undefined && (!trimmedClientRef || trimmedClientRef.length > PROMPT_CLIENT_REF_MAX_LENGTH))
|
|
2092
|
+
throw Object.assign(new Error("clientRef must be a non-empty string of at most 128 characters."), {
|
|
2093
|
+
code: "invalid_input",
|
|
1974
2094
|
});
|
|
2095
|
+
if (trackReconciliation) admitPrompt(trimmedClientRef);
|
|
2096
|
+
try {
|
|
2097
|
+
if (forceFresh && isSessionBusy()) {
|
|
2098
|
+
throw Object.assign(
|
|
2099
|
+
new Error("Previous turn did not finish aborting before replacement prompt submission."),
|
|
2100
|
+
{
|
|
2101
|
+
code: "busy",
|
|
2102
|
+
},
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
if (rejectWhenBusy && isSessionBusy())
|
|
2106
|
+
throw Object.assign(
|
|
2107
|
+
new Error("turn.prompt is unavailable while the agent is busy; use turn.steer explicitly."),
|
|
2108
|
+
{
|
|
2109
|
+
code: "busy",
|
|
2110
|
+
},
|
|
2111
|
+
);
|
|
2112
|
+
} catch (error) {
|
|
2113
|
+
if (trackReconciliation) releasePromptAdmission(trimmedClientRef);
|
|
2114
|
+
throw error;
|
|
1975
2115
|
}
|
|
1976
|
-
if (rejectWhenBusy && isSessionBusy())
|
|
1977
|
-
throw Object.assign(
|
|
1978
|
-
new Error("turn.prompt is unavailable while the agent is busy; use turn.steer explicitly."),
|
|
1979
|
-
{
|
|
1980
|
-
code: "busy",
|
|
1981
|
-
},
|
|
1982
|
-
);
|
|
1983
2116
|
const promptImages = Array.isArray(images) ? (images as { data: string; mimeType?: string }[]) : [];
|
|
1984
2117
|
const content: string | (TextContent | ImageContent)[] =
|
|
1985
2118
|
promptImages.length > 0
|
|
@@ -2008,12 +2141,17 @@ function sdkControlSurface(
|
|
|
2008
2141
|
error: Object.assign(new Error("Prompt preflight was cancelled before execution."), { code: "busy" }),
|
|
2009
2142
|
});
|
|
2010
2143
|
pendingPreflightCancellations.add(cancelPreflight);
|
|
2011
|
-
const
|
|
2144
|
+
const settleAccepted = async () => {
|
|
2012
2145
|
if (preflightSettled) return;
|
|
2013
2146
|
accepted = true;
|
|
2014
|
-
onPromptAccepted(correlation, requesterConnectionId);
|
|
2147
|
+
await onPromptAccepted(correlation, requesterConnectionId, trimmedClientRef, trackReconciliation);
|
|
2015
2148
|
settlePreflight({ status: "accepted" });
|
|
2016
2149
|
};
|
|
2150
|
+
// Durable fence preferred; keep legacy onPreflightAccepted for hosts/tests that only fire the sync hook.
|
|
2151
|
+
const onPreflightAcceptCommit = settleAccepted;
|
|
2152
|
+
const onPreflightAccepted = () => {
|
|
2153
|
+
void settleAccepted();
|
|
2154
|
+
};
|
|
2017
2155
|
// Do not acknowledge the prompt until AgentSession's async preflight
|
|
2018
2156
|
// succeeds. The terminal result records correlation before agent_start can fire.
|
|
2019
2157
|
let submission: Promise<void> | undefined;
|
|
@@ -2021,6 +2159,7 @@ function sdkControlSurface(
|
|
|
2021
2159
|
submission = Promise.resolve(
|
|
2022
2160
|
api.sendUserMessage(content, {
|
|
2023
2161
|
...(deliverAs ? { deliverAs } : !forceFresh && isBusy() ? { deliverAs: "steer" as const } : {}),
|
|
2162
|
+
onPreflightAcceptCommit,
|
|
2024
2163
|
onPreflightAccepted,
|
|
2025
2164
|
}),
|
|
2026
2165
|
);
|
|
@@ -2048,13 +2187,17 @@ function sdkControlSurface(
|
|
|
2048
2187
|
try {
|
|
2049
2188
|
const result = await preflight.promise;
|
|
2050
2189
|
if (result.status === "rejected") throw result.error;
|
|
2051
|
-
return { commandId, turnId, accepted: true };
|
|
2190
|
+
return { commandId, turnId, accepted: true, ...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}) };
|
|
2191
|
+
} catch (error) {
|
|
2192
|
+
if (trackReconciliation) releasePromptAdmission(trimmedClientRef);
|
|
2193
|
+
throw error;
|
|
2052
2194
|
} finally {
|
|
2053
2195
|
pendingPreflightCancellations.delete(cancelPreflight);
|
|
2054
2196
|
}
|
|
2055
2197
|
};
|
|
2056
2198
|
const surface: ControlSurface & { cancelPendingPreflights(): void } = {
|
|
2057
|
-
prompt: (text, images) =>
|
|
2199
|
+
prompt: (text, images, clientRef) =>
|
|
2200
|
+
submitPrompt(text, images, false, undefined, true, controlRequesterContext.getStore(), clientRef, true),
|
|
2058
2201
|
steer: text => sendSteer(text),
|
|
2059
2202
|
followUp: text => submitPrompt(text, undefined, false, "followUp", false, controlRequesterContext.getStore()),
|
|
2060
2203
|
abort: () => {
|
|
@@ -2227,13 +2370,84 @@ function sdkControlSurface(
|
|
|
2227
2370
|
code: "terminal_uncertain",
|
|
2228
2371
|
});
|
|
2229
2372
|
},
|
|
2230
|
-
invokeSkill: (name, args) => {
|
|
2373
|
+
invokeSkill: async (name, args, clientRef) => {
|
|
2231
2374
|
if (!bindings.has("invokeSkill") || !ctx.invokeSkill)
|
|
2232
2375
|
return unavailable("skill.invoke", "no skill invocation seam is installed")();
|
|
2233
2376
|
|
|
2234
2377
|
if (typeof args !== "undefined" && typeof args !== "string")
|
|
2235
2378
|
throw Object.assign(new Error("skill.invoke args must be a string."), { code: "invalid_input" });
|
|
2236
|
-
|
|
2379
|
+
const trimmedClientRef =
|
|
2380
|
+
typeof clientRef === "string" ? clientRef.trim() : clientRef === undefined ? undefined : "";
|
|
2381
|
+
if (clientRef !== undefined && (!trimmedClientRef || trimmedClientRef.length > 128))
|
|
2382
|
+
throw Object.assign(new Error("clientRef must be a non-empty string of at most 128 characters."), {
|
|
2383
|
+
code: "invalid_input",
|
|
2384
|
+
});
|
|
2385
|
+
const commandId = crypto.randomUUID();
|
|
2386
|
+
const turnId = crypto.randomUUID();
|
|
2387
|
+
const correlation = { commandId, turnId };
|
|
2388
|
+
if (skillRecon) skillRecon.admit(trimmedClientRef);
|
|
2389
|
+
const { promise: acceptedP, resolve, reject } = Promise.withResolvers<Record<string, unknown>>();
|
|
2390
|
+
let phase: "pending" | "accepted" | "rejected" = "pending";
|
|
2391
|
+
const settleAccept = (value: Record<string, unknown>) => {
|
|
2392
|
+
if (phase !== "pending") return;
|
|
2393
|
+
phase = "accepted";
|
|
2394
|
+
resolve(value);
|
|
2395
|
+
};
|
|
2396
|
+
const settleReject = (error: unknown) => {
|
|
2397
|
+
if (phase !== "pending") return;
|
|
2398
|
+
phase = "rejected";
|
|
2399
|
+
reject(error);
|
|
2400
|
+
};
|
|
2401
|
+
let prepared: { name: string; path: string; lineCount?: number; cleanedArgs?: string } | undefined;
|
|
2402
|
+
const run = Promise.resolve(
|
|
2403
|
+
ctx.invokeSkill(name, args as string | undefined, {
|
|
2404
|
+
onSkillPrepared: meta => {
|
|
2405
|
+
prepared = meta;
|
|
2406
|
+
},
|
|
2407
|
+
onPreflightAcceptCommit: async () => {
|
|
2408
|
+
const meta = prepared ?? { name: String(name), path: "" };
|
|
2409
|
+
if (skillRecon)
|
|
2410
|
+
await skillRecon.noteAccepted(correlation, trimmedClientRef, { skillName: meta.name });
|
|
2411
|
+
settleAccept({
|
|
2412
|
+
accepted: true,
|
|
2413
|
+
commandId,
|
|
2414
|
+
turnId,
|
|
2415
|
+
name: meta.name,
|
|
2416
|
+
path: meta.path,
|
|
2417
|
+
...(meta.lineCount !== undefined ? { lineCount: meta.lineCount } : {}),
|
|
2418
|
+
...(meta.cleanedArgs !== undefined ? { args: meta.cleanedArgs } : {}),
|
|
2419
|
+
...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}),
|
|
2420
|
+
});
|
|
2421
|
+
},
|
|
2422
|
+
}),
|
|
2423
|
+
).then(
|
|
2424
|
+
result => {
|
|
2425
|
+
if (phase === "pending") {
|
|
2426
|
+
// Completed without fence (e.g. queue path) — still surface result.
|
|
2427
|
+
settleAccept({
|
|
2428
|
+
accepted: true,
|
|
2429
|
+
commandId,
|
|
2430
|
+
turnId,
|
|
2431
|
+
...(typeof result === "object" && result ? (result as object) : {}),
|
|
2432
|
+
...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}),
|
|
2433
|
+
});
|
|
2434
|
+
} else if (skillRecon) {
|
|
2435
|
+
void skillRecon.noteTransition(correlation, { type: "agent_end" });
|
|
2436
|
+
}
|
|
2437
|
+
return result;
|
|
2438
|
+
},
|
|
2439
|
+
error => {
|
|
2440
|
+
if (phase === "pending") {
|
|
2441
|
+
if (skillRecon) skillRecon.release(trimmedClientRef);
|
|
2442
|
+
settleReject(error);
|
|
2443
|
+
} else if (skillRecon) {
|
|
2444
|
+
void skillRecon.noteTransition(correlation, { type: "agent_failed", error });
|
|
2445
|
+
}
|
|
2446
|
+
throw error;
|
|
2447
|
+
},
|
|
2448
|
+
);
|
|
2449
|
+
void run.catch(() => {});
|
|
2450
|
+
return await acceptedP;
|
|
2237
2451
|
},
|
|
2238
2452
|
setPlanMode: async on => {
|
|
2239
2453
|
if (!bindings.has("setPlanMode") || !ctx.setPlanMode)
|
|
@@ -2266,6 +2480,14 @@ function sdkControlSurface(
|
|
|
2266
2480
|
);
|
|
2267
2481
|
return typed("model.set", { id: `${model.provider}/${model.id}`, thinkingLevel });
|
|
2268
2482
|
},
|
|
2483
|
+
setModelProfile: async id => {
|
|
2484
|
+
if (!bindings.has("setModelProfile") || !ctx.setModelProfile)
|
|
2485
|
+
return unavailable("model.profile.set", "no model-profile activation seam is installed")();
|
|
2486
|
+
if (!id) throw Object.assign(new Error("model.profile.set requires a profile id."), { code: "invalid_input" });
|
|
2487
|
+
if (!ctx.modelRegistry.getModelProfile(id))
|
|
2488
|
+
throw Object.assign(new Error(`Model profile ${id} was not found.`), { code: "invalid_input" });
|
|
2489
|
+
return { changed: await ctx.setModelProfile(id), id };
|
|
2490
|
+
},
|
|
2269
2491
|
cycleModel: async () => {
|
|
2270
2492
|
if (!bindings.has("cycleModel"))
|
|
2271
2493
|
return unavailable("model.cycle", "no session model-cycle seam is installed")();
|
|
@@ -2761,6 +2983,18 @@ export async function ensureConfiguredProviderDaemons(
|
|
|
2761
2983
|
if (isSlackConfigured(cfg)) await ensureProviderDaemon("slack", settings);
|
|
2762
2984
|
}
|
|
2763
2985
|
|
|
2986
|
+
/**
|
|
2987
|
+
* Classify whether a session identity event must await notification endpoint startup.
|
|
2988
|
+
* Only an interactive selector resume may defer this ancillary startup; all other
|
|
2989
|
+
* events, including branches and unknown origins, fail closed to awaiting it.
|
|
2990
|
+
*/
|
|
2991
|
+
export function shouldAwaitNotificationStartup(event: {
|
|
2992
|
+
type: "session_switch" | "session_branch";
|
|
2993
|
+
transition?: { origin: string };
|
|
2994
|
+
}): boolean {
|
|
2995
|
+
return event.type !== "session_switch" || event.transition?.origin !== INTERACTIVE_SELECTOR_RESUME_ORIGIN;
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2764
2998
|
export function createNotificationsExtension(
|
|
2765
2999
|
api: ExtensionAPI,
|
|
2766
3000
|
options: {
|
|
@@ -2769,6 +3003,7 @@ export function createNotificationsExtension(
|
|
|
2769
3003
|
settings: Settings;
|
|
2770
3004
|
cwd: string;
|
|
2771
3005
|
sessionId: string;
|
|
3006
|
+
onRegistered?: (registration: RegisterNotificationRootResult) => void;
|
|
2772
3007
|
}) => Promise<EnsureDaemonResult>;
|
|
2773
3008
|
ensureProviderDaemon?: (provider: "discord" | "slack", settings: Settings) => Promise<unknown>;
|
|
2774
3009
|
/** Suppress auto-delivery for a SKC-spawned child under `sessionScope=primary`. */
|
|
@@ -2778,7 +3013,7 @@ export function createNotificationsExtension(
|
|
|
2778
3013
|
sdkHostModeSupported?: boolean;
|
|
2779
3014
|
|
|
2780
3015
|
onSdkRequest?: (kind: "control" | "query", connectionId: string, frame: Record<string, unknown>) => void;
|
|
2781
|
-
|
|
3016
|
+
runBtwTurn?: (question: string, signal: AbortSignal) => Promise<{ replyText: string }>;
|
|
2782
3017
|
/** Observes settlement of optional session-branch startup after reconciliation completes. */
|
|
2783
3018
|
onBranchStartupSettled?: (receipt: { sessionId: string; status: SessionStartResult["status"] }) => void;
|
|
2784
3019
|
readNotificationFile?: (path: string) => Promise<Buffer>;
|
|
@@ -2799,10 +3034,15 @@ export function createNotificationsExtension(
|
|
|
2799
3034
|
const cleanupRetries = new Map<string, SessionRuntime>();
|
|
2800
3035
|
const sessionStartPromises = new Map<string, Promise<SessionStartResult>>();
|
|
2801
3036
|
const branchStartupTasks = new Set<Promise<void>>();
|
|
3037
|
+
const sessionLifecycleTasks = new Set<Promise<void>>();
|
|
2802
3038
|
let activeRuntimeId: string | undefined;
|
|
2803
3039
|
let identityControlInFlight = false;
|
|
2804
3040
|
let deferredIdentityRotation:
|
|
2805
|
-
| {
|
|
3041
|
+
| {
|
|
3042
|
+
event: { previousSessionFile?: string; transition?: { origin: string } };
|
|
3043
|
+
ctx: ExtensionContext;
|
|
3044
|
+
awaitStartup: boolean;
|
|
3045
|
+
}
|
|
2806
3046
|
| undefined;
|
|
2807
3047
|
let extensionShuttingDown = false;
|
|
2808
3048
|
|
|
@@ -2810,13 +3050,24 @@ export function createNotificationsExtension(
|
|
|
2810
3050
|
settings: Settings,
|
|
2811
3051
|
cwd: string,
|
|
2812
3052
|
id: string,
|
|
3053
|
+
onRegistered?: (registrationToken: string) => void,
|
|
2813
3054
|
): Promise<"ready" | "blocked_identity"> {
|
|
2814
3055
|
if (options.ensureTelegramDaemon) {
|
|
2815
|
-
return (await options.ensureTelegramDaemon({
|
|
3056
|
+
return (await options.ensureTelegramDaemon({
|
|
3057
|
+
settings,
|
|
3058
|
+
cwd,
|
|
3059
|
+
sessionId: id,
|
|
3060
|
+
onRegistered: registration => onRegistered?.(registration.token),
|
|
3061
|
+
})) === "blocked"
|
|
2816
3062
|
? "blocked_identity"
|
|
2817
3063
|
: "ready";
|
|
2818
3064
|
}
|
|
2819
|
-
return (await ensureTelegramDaemonRunningDetailed({
|
|
3065
|
+
return (await ensureTelegramDaemonRunningDetailed({
|
|
3066
|
+
settings,
|
|
3067
|
+
cwd,
|
|
3068
|
+
sessionId: id,
|
|
3069
|
+
onRegistered: registration => onRegistered?.(registration.token),
|
|
3070
|
+
})) === "blocked_identity"
|
|
2820
3071
|
? "blocked_identity"
|
|
2821
3072
|
: "ready";
|
|
2822
3073
|
}
|
|
@@ -2825,9 +3076,10 @@ export function createNotificationsExtension(
|
|
|
2825
3076
|
cfg: NotificationConfig,
|
|
2826
3077
|
cwd: string,
|
|
2827
3078
|
id: string,
|
|
3079
|
+
onRegistered?: (registrationToken: string) => void,
|
|
2828
3080
|
): Promise<boolean> {
|
|
2829
3081
|
if (isTelegramConfigured(cfg)) {
|
|
2830
|
-
if ((await ensureTelegramOwner(settings, cwd, id)) === "blocked_identity") return false;
|
|
3082
|
+
if ((await ensureTelegramOwner(settings, cwd, id, onRegistered)) === "blocked_identity") return false;
|
|
2831
3083
|
}
|
|
2832
3084
|
await ensureConfiguredProviderDaemons(settings, cfg, options.ensureProviderDaemon);
|
|
2833
3085
|
return true;
|
|
@@ -2840,7 +3092,6 @@ export function createNotificationsExtension(
|
|
|
2840
3092
|
"session.branch",
|
|
2841
3093
|
]);
|
|
2842
3094
|
const sessionId = (ctx: ExtensionContext): string => ctx.sessionManager.getSessionId();
|
|
2843
|
-
const sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
|
|
2844
3095
|
|
|
2845
3096
|
async function stopSession(
|
|
2846
3097
|
id: string,
|
|
@@ -2852,6 +3103,7 @@ export function createNotificationsExtension(
|
|
|
2852
3103
|
const requestedRuntime = retryRuntime ?? activeRuntime;
|
|
2853
3104
|
if (expectedRuntime && requestedRuntime !== expectedRuntime) return false;
|
|
2854
3105
|
if (reason === "session" && requestedRuntime) {
|
|
3106
|
+
requestedRuntime.inboundFenced = true;
|
|
2855
3107
|
requestedRuntime.stopping = true;
|
|
2856
3108
|
requestedRuntime.abortEphemeralTurns();
|
|
2857
3109
|
}
|
|
@@ -2915,6 +3167,12 @@ export function createNotificationsExtension(
|
|
|
2915
3167
|
try {
|
|
2916
3168
|
rt.workflowGate?.setRuntimeTurnProvider?.(null);
|
|
2917
3169
|
} catch {}
|
|
3170
|
+
try {
|
|
3171
|
+
const delivered = await pushTerminalSessionFrame(rt, { type: "session_closed", sessionId: id });
|
|
3172
|
+
if (!delivered) logger.warn("notifications: session_closed socket delivery was not acknowledged");
|
|
3173
|
+
} catch (e) {
|
|
3174
|
+
logger.warn(`notifications: session_closed failed: ${String(e)}`);
|
|
3175
|
+
}
|
|
2918
3176
|
await rt.waitForGateResolutionQuiescence();
|
|
2919
3177
|
try {
|
|
2920
3178
|
rt.disposeAckRecoveryParticipant();
|
|
@@ -2956,14 +3214,6 @@ export function createNotificationsExtension(
|
|
|
2956
3214
|
ownerReleaseFailures.push(e);
|
|
2957
3215
|
logger.warn(`sdk query snapshots: close failed: ${String(e)}`);
|
|
2958
3216
|
}
|
|
2959
|
-
let closeFrameSent = false;
|
|
2960
|
-
try {
|
|
2961
|
-
pushSessionFrame(rt, { type: "session_closed", sessionId: id });
|
|
2962
|
-
closeFrameSent = true;
|
|
2963
|
-
} catch (e) {
|
|
2964
|
-
logger.warn(`notifications: session_closed failed: ${String(e)}`);
|
|
2965
|
-
}
|
|
2966
|
-
if (closeFrameSent) await sleep(100);
|
|
2967
3217
|
let serverStopped = rt.serverStopped;
|
|
2968
3218
|
if (!serverStopped) {
|
|
2969
3219
|
try {
|
|
@@ -2982,6 +3232,20 @@ export function createNotificationsExtension(
|
|
|
2982
3232
|
hostStopped: rt.hostStopped && rt.serverStopped,
|
|
2983
3233
|
brokerRegistrationReleased: rt.brokerRegistrationReleased,
|
|
2984
3234
|
});
|
|
3235
|
+
if (rt.notificationRootRegistration) {
|
|
3236
|
+
try {
|
|
3237
|
+
await unregisterNotificationRoot({
|
|
3238
|
+
settings: rt.notificationRootRegistration.settings,
|
|
3239
|
+
cwd: rt.notificationRootRegistration.cwd,
|
|
3240
|
+
sessionId: id,
|
|
3241
|
+
registrationToken: rt.notificationRootRegistration.registrationToken,
|
|
3242
|
+
});
|
|
3243
|
+
rt.notificationRootRegistration = undefined;
|
|
3244
|
+
} catch (e) {
|
|
3245
|
+
ownerReleaseFailures.push(e);
|
|
3246
|
+
logger.warn(`notifications: Telegram root unregister failed: ${String(e)}`);
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
2985
3249
|
if (ownerReleaseFailures.length > 0) {
|
|
2986
3250
|
cleanupRetries.set(id, rt);
|
|
2987
3251
|
throw new AggregateError(ownerReleaseFailures, `SDK notification runtime ${id} owner release failed.`);
|
|
@@ -3130,7 +3394,13 @@ export function createNotificationsExtension(
|
|
|
3130
3394
|
const promptSubmissionKey = (correlation: { commandId: string; turnId: string }) =>
|
|
3131
3395
|
`${correlation.commandId}:${correlation.turnId}`;
|
|
3132
3396
|
type PromptLifecycleFrame =
|
|
3133
|
-
| {
|
|
3397
|
+
| {
|
|
3398
|
+
type: "agent_start" | "agent_end";
|
|
3399
|
+
sessionId: string;
|
|
3400
|
+
commandId?: string;
|
|
3401
|
+
turnId?: string;
|
|
3402
|
+
finalText?: string;
|
|
3403
|
+
}
|
|
3134
3404
|
| {
|
|
3135
3405
|
type: "agent_failed";
|
|
3136
3406
|
sessionId: string;
|
|
@@ -3145,11 +3415,39 @@ export function createNotificationsExtension(
|
|
|
3145
3415
|
failed: boolean;
|
|
3146
3416
|
error: unknown;
|
|
3147
3417
|
terminal: boolean;
|
|
3418
|
+
retainCorrelation: boolean;
|
|
3148
3419
|
createdAt: number;
|
|
3149
3420
|
bufferedFrames: Array<PromptLifecycleFrame | Record<string, unknown>>;
|
|
3150
3421
|
};
|
|
3151
3422
|
const promptSubmissions = new Map<string, PromptSubmission>();
|
|
3152
3423
|
const promptTerminalTombstones = new Map<string, number>();
|
|
3424
|
+
// Authoritative bounded reconciliation state for Q26 turn.prompt_status
|
|
3425
|
+
// (contract documented in ./prompt-reconciliation and ../prompt-status).
|
|
3426
|
+
// Active records never age into terminal; documented TTL/capacity
|
|
3427
|
+
// eviction is the only removal, after which lookups report `unknown`.
|
|
3428
|
+
const sessionFile =
|
|
3429
|
+
typeof ctx.sessionManager?.getSessionFile === "function" ? ctx.sessionManager.getSessionFile() : null;
|
|
3430
|
+
const reconciliationSessionId =
|
|
3431
|
+
typeof ctx.sessionManager?.getSessionId === "function" ? ctx.sessionManager.getSessionId() : "";
|
|
3432
|
+
const durableStore =
|
|
3433
|
+
sessionFile && reconciliationSessionId
|
|
3434
|
+
? createReconciliationStore({ sessionFile, sessionId: String(reconciliationSessionId) })
|
|
3435
|
+
: null;
|
|
3436
|
+
const kindReconciliation = createKindAwareReconciliation({ store: durableStore });
|
|
3437
|
+
if (durableStore) void kindReconciliation.hydrateFromStore();
|
|
3438
|
+
// Backward-compatible process-local prompt reconciler kept for unit-test isolation;
|
|
3439
|
+
// production path uses kindReconciliation for prompt+skill.
|
|
3440
|
+
const reconciliation = createPromptReconciliation();
|
|
3441
|
+
const admitPromptSubmission = (clientRef?: string) => kindReconciliation.admit("prompt", clientRef);
|
|
3442
|
+
const notePromptReconciliationAccepted = async (
|
|
3443
|
+
correlation: { commandId: string; turnId: string },
|
|
3444
|
+
clientRef?: string,
|
|
3445
|
+
) => {
|
|
3446
|
+
await kindReconciliation.noteAccepted("prompt", correlation, clientRef);
|
|
3447
|
+
};
|
|
3448
|
+
const lookupPromptStatus = (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
|
|
3449
|
+
kindReconciliation.lookup("prompt", selector);
|
|
3450
|
+
const releasePromptAdmission = (clientRef?: string) => kindReconciliation.releaseAdmission("prompt", clientRef);
|
|
3153
3451
|
const removePendingPromptCorrelation = (correlation: { commandId: string; turnId: string }) => {
|
|
3154
3452
|
const pendingIndex = pendingPromptCorrelations.findIndex(
|
|
3155
3453
|
candidate => candidate.commandId === correlation.commandId && candidate.turnId === correlation.turnId,
|
|
@@ -3167,14 +3465,21 @@ export function createNotificationsExtension(
|
|
|
3167
3465
|
removePendingPromptCorrelation(correlation);
|
|
3168
3466
|
addTerminalTombstone(key);
|
|
3169
3467
|
};
|
|
3468
|
+
const expirePromptDelivery = (key: string, submission: PromptSubmission) => {
|
|
3469
|
+
promptSubmissions.delete(key);
|
|
3470
|
+
if (!submission.terminal && submission.retainCorrelation) return;
|
|
3471
|
+
const [commandId, turnId] = key.split(":", 2);
|
|
3472
|
+
if (!commandId || !turnId) return;
|
|
3473
|
+
removePendingPromptCorrelation({ commandId, turnId });
|
|
3474
|
+
addTerminalTombstone(key);
|
|
3475
|
+
};
|
|
3170
3476
|
const cleanupPromptRecords = (now = Date.now()) => {
|
|
3477
|
+
kindReconciliation.cleanup();
|
|
3478
|
+
reconciliation.cleanup();
|
|
3171
3479
|
for (const [key, expiresAt] of promptTerminalTombstones)
|
|
3172
3480
|
if (expiresAt <= now) promptTerminalTombstones.delete(key);
|
|
3173
3481
|
for (const [key, submission] of promptSubmissions)
|
|
3174
|
-
if (submission.createdAt + PROMPT_SUBMISSION_TTL_MS <= now)
|
|
3175
|
-
const [commandId, turnId] = key.split(":", 2);
|
|
3176
|
-
if (commandId && turnId) finalizePrompt(key, { commandId, turnId });
|
|
3177
|
-
}
|
|
3482
|
+
if (submission.createdAt + PROMPT_SUBMISSION_TTL_MS <= now) expirePromptDelivery(key, submission);
|
|
3178
3483
|
};
|
|
3179
3484
|
const abandonPrompt = (submission: PromptSubmission) => {
|
|
3180
3485
|
submission.abandoned = true;
|
|
@@ -3185,6 +3490,7 @@ export function createNotificationsExtension(
|
|
|
3185
3490
|
frame: PromptLifecycleFrame,
|
|
3186
3491
|
) => {
|
|
3187
3492
|
cleanupPromptRecords();
|
|
3493
|
+
|
|
3188
3494
|
if (!correlation || !runtime) {
|
|
3189
3495
|
emitAgentLifecycle(runtime!, frame as Extract<PromptLifecycleFrame, { type: "agent_start" | "agent_end" }>);
|
|
3190
3496
|
return;
|
|
@@ -3247,18 +3553,25 @@ export function createNotificationsExtension(
|
|
|
3247
3553
|
if (commandId && turnId) finalizePrompt(key, { commandId, turnId });
|
|
3248
3554
|
}
|
|
3249
3555
|
};
|
|
3250
|
-
const recordPromptAccepted = (
|
|
3556
|
+
const recordPromptAccepted = async (
|
|
3251
3557
|
correlation: { commandId: string; turnId: string },
|
|
3252
3558
|
requesterConnectionId?: string,
|
|
3559
|
+
clientRef?: string,
|
|
3560
|
+
trackReconciliation = false,
|
|
3253
3561
|
) => {
|
|
3254
|
-
if (!requesterConnectionId)
|
|
3562
|
+
if (!requesterConnectionId) {
|
|
3563
|
+
// No delivery owner: tracked prompts cannot be reconciled. Release
|
|
3564
|
+
// their admission reservation instead of leaking the active slot.
|
|
3565
|
+
if (trackReconciliation) releasePromptAdmission(clientRef);
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
// Register delivery ownership synchronously before any await so post-accept
|
|
3569
|
+
// failures that race the durable write still emit correlated terminals.
|
|
3255
3570
|
cleanupPromptRecords();
|
|
3256
3571
|
while (promptSubmissions.size >= PROMPT_SUBMISSION_CAPACITY) {
|
|
3257
3572
|
const oldest = promptSubmissions.entries().next().value as [string, PromptSubmission] | undefined;
|
|
3258
3573
|
if (!oldest) break;
|
|
3259
|
-
|
|
3260
|
-
const [commandId, turnId] = key.split(":", 2);
|
|
3261
|
-
if (commandId && turnId) finalizePrompt(key, { commandId, turnId });
|
|
3574
|
+
expirePromptDelivery(oldest[0], oldest[1]);
|
|
3262
3575
|
}
|
|
3263
3576
|
pendingPromptCorrelations.push(correlation);
|
|
3264
3577
|
promptSubmissions.set(promptSubmissionKey(correlation), {
|
|
@@ -3268,9 +3581,11 @@ export function createNotificationsExtension(
|
|
|
3268
3581
|
failed: false,
|
|
3269
3582
|
error: undefined,
|
|
3270
3583
|
terminal: false,
|
|
3584
|
+
retainCorrelation: trackReconciliation,
|
|
3271
3585
|
createdAt: Date.now(),
|
|
3272
3586
|
bufferedFrames: [],
|
|
3273
3587
|
});
|
|
3588
|
+
if (trackReconciliation) await notePromptReconciliationAccepted(correlation, clientRef);
|
|
3274
3589
|
};
|
|
3275
3590
|
const recordPromptTerminal = (correlation: { commandId: string; turnId: string } | undefined) => {
|
|
3276
3591
|
if (!correlation) return false;
|
|
@@ -3283,17 +3598,15 @@ export function createNotificationsExtension(
|
|
|
3283
3598
|
return true;
|
|
3284
3599
|
};
|
|
3285
3600
|
const emitPromptFailure = (correlation: { commandId: string; turnId: string }, error: unknown) => {
|
|
3601
|
+
const sanitized = sanitizePromptFailure(error);
|
|
3602
|
+
void kindReconciliation.noteTransition("prompt", correlation, { type: "agent_failed", error: sanitized });
|
|
3286
3603
|
const submission = promptSubmissions.get(promptSubmissionKey(correlation));
|
|
3287
3604
|
if (!submission || !runtime || !recordPromptTerminal(correlation)) return;
|
|
3288
|
-
const candidate = error as { code?: unknown; message?: unknown };
|
|
3289
3605
|
emitPromptLifecycle(correlation, {
|
|
3290
3606
|
type: "agent_failed",
|
|
3291
3607
|
sessionId: runtime.id,
|
|
3292
3608
|
...correlation,
|
|
3293
|
-
error:
|
|
3294
|
-
code: typeof candidate.code === "string" ? candidate.code : "internal",
|
|
3295
|
-
message: typeof candidate.message === "string" ? candidate.message : "Prompt submission failed.",
|
|
3296
|
-
},
|
|
3609
|
+
error: sanitized,
|
|
3297
3610
|
});
|
|
3298
3611
|
};
|
|
3299
3612
|
const recordPromptFailure = (correlation: { commandId: string; turnId: string }, error: unknown) => {
|
|
@@ -3336,6 +3649,8 @@ export function createNotificationsExtension(
|
|
|
3336
3649
|
};
|
|
3337
3650
|
},
|
|
3338
3651
|
configOverrides,
|
|
3652
|
+
lookupPromptStatus,
|
|
3653
|
+
selector => kindReconciliation.lookup("skill", selector),
|
|
3339
3654
|
),
|
|
3340
3655
|
id,
|
|
3341
3656
|
revisions,
|
|
@@ -3351,9 +3666,19 @@ export function createNotificationsExtension(
|
|
|
3351
3666
|
recordPromptFailure,
|
|
3352
3667
|
() => runtime?.stopping !== true,
|
|
3353
3668
|
trackGateResolution,
|
|
3669
|
+
admitPromptSubmission,
|
|
3670
|
+
releasePromptAdmission,
|
|
3354
3671
|
settings,
|
|
3355
3672
|
configOverrides,
|
|
3356
3673
|
configRevision,
|
|
3674
|
+
{
|
|
3675
|
+
admit: (clientRef?: string) => kindReconciliation.admit("skill", clientRef),
|
|
3676
|
+
release: (clientRef?: string) => kindReconciliation.releaseAdmission("skill", clientRef),
|
|
3677
|
+
noteAccepted: (correlation, clientRef, extra) =>
|
|
3678
|
+
kindReconciliation.noteAccepted("skill", correlation, clientRef, extra),
|
|
3679
|
+
noteTransition: (correlation, frame) => kindReconciliation.noteTransition("skill", correlation, frame),
|
|
3680
|
+
lookup: selector => kindReconciliation.lookup("skill", selector),
|
|
3681
|
+
},
|
|
3357
3682
|
);
|
|
3358
3683
|
const abandonPromptResponse = (connectionId: string, frame: Record<string, unknown>) => {
|
|
3359
3684
|
if (
|
|
@@ -3409,7 +3734,7 @@ export function createNotificationsExtension(
|
|
|
3409
3734
|
stateRoot,
|
|
3410
3735
|
token,
|
|
3411
3736
|
sendFrame: (connectionId, frame) => sendSdkFrame(connectionId, frame),
|
|
3412
|
-
connectionCapabilities: connectionId => hostCapCache.get(connectionId)
|
|
3737
|
+
connectionCapabilities: connectionId => hostCapCache.get(connectionId),
|
|
3413
3738
|
installProviderDefinitions,
|
|
3414
3739
|
onProviderDefinitionsRemoved: removeProviderDefinitions,
|
|
3415
3740
|
onFrame: handler => {
|
|
@@ -3419,6 +3744,75 @@ export function createNotificationsExtension(
|
|
|
3419
3744
|
};
|
|
3420
3745
|
},
|
|
3421
3746
|
onRequest: options.onSdkRequest,
|
|
3747
|
+
beforeControlResponse: async (_connectionId, request, response, sendTerminal) => {
|
|
3748
|
+
if (typeof request.operation !== "string" || !identityControlOperations.has(request.operation)) return;
|
|
3749
|
+
const pending = deferredIdentityRotation;
|
|
3750
|
+
deferredIdentityRotation = undefined;
|
|
3751
|
+
identityControlInFlight = false;
|
|
3752
|
+
if (response.ok !== true || !pending) return;
|
|
3753
|
+
|
|
3754
|
+
const predecessorId = activeRuntimeId ?? sessionIdFromFile(pending.event.previousSessionFile);
|
|
3755
|
+
if (!predecessorId) throw new Error("notifications: identity control predecessor is unknown.");
|
|
3756
|
+
let terminalAttempted = false;
|
|
3757
|
+
let terminalOutcome: TerminalSendOutcome | undefined;
|
|
3758
|
+
try {
|
|
3759
|
+
terminalOutcome = await runIdentityControlSuccessPath({
|
|
3760
|
+
fence: () => {
|
|
3761
|
+
const predecessor = runtimes.get(predecessorId);
|
|
3762
|
+
if (!predecessor || predecessor.stopping || predecessor.serverStopped)
|
|
3763
|
+
throw new Error(`notifications: predecessor runtime ${predecessorId} cannot be fenced.`);
|
|
3764
|
+
predecessor.inboundFenced = true;
|
|
3765
|
+
},
|
|
3766
|
+
ensurePredecessorSendCapable: () => {
|
|
3767
|
+
const predecessor = runtimes.get(predecessorId);
|
|
3768
|
+
if (!predecessor || predecessor.stopping || predecessor.serverStopped)
|
|
3769
|
+
throw new Error(`notifications: predecessor runtime ${predecessorId} is not send-capable.`);
|
|
3770
|
+
},
|
|
3771
|
+
startSuccessor: async () => {
|
|
3772
|
+
const successorId = sessionId(pending.ctx);
|
|
3773
|
+
try {
|
|
3774
|
+
await rotateSessionAuthority(pending.event, pending.ctx, true, {
|
|
3775
|
+
deferPredecessorStop: true,
|
|
3776
|
+
});
|
|
3777
|
+
const successor = runtimes.get(successorId);
|
|
3778
|
+
if (!successor?.host.started || activeRuntimeId !== successorId)
|
|
3779
|
+
throw new Error(`notifications: successor runtime ${successorId} was not ready.`);
|
|
3780
|
+
} catch (error) {
|
|
3781
|
+
(response as Record<string, unknown>).ok = false;
|
|
3782
|
+
delete (response as Record<string, unknown>).result;
|
|
3783
|
+
(response as Record<string, unknown>).error = {
|
|
3784
|
+
code: "unavailable",
|
|
3785
|
+
message: error instanceof Error ? error.message : "Successor session was not ready.",
|
|
3786
|
+
};
|
|
3787
|
+
}
|
|
3788
|
+
},
|
|
3789
|
+
sendTerminal: async () => {
|
|
3790
|
+
terminalAttempted = true;
|
|
3791
|
+
try {
|
|
3792
|
+
await sendTerminal();
|
|
3793
|
+
return "written";
|
|
3794
|
+
} catch {
|
|
3795
|
+
return "write_failed";
|
|
3796
|
+
}
|
|
3797
|
+
},
|
|
3798
|
+
stopPredecessor: async () => {
|
|
3799
|
+
try {
|
|
3800
|
+
await stopSession(predecessorId);
|
|
3801
|
+
} catch (error) {
|
|
3802
|
+
if (!terminalAttempted) throw error;
|
|
3803
|
+
logger.error(`notifications: deferred predecessor cleanup failed: ${String(error)}`);
|
|
3804
|
+
}
|
|
3805
|
+
},
|
|
3806
|
+
requireNativeControlDrain:
|
|
3807
|
+
(request as { requireNativeControlDrain?: unknown }).requireNativeControlDrain === true,
|
|
3808
|
+
});
|
|
3809
|
+
} catch (error) {
|
|
3810
|
+
if (!terminalAttempted) throw error;
|
|
3811
|
+
logger.error(
|
|
3812
|
+
`notifications: identity terminal delivery failed (${terminalOutcome ?? "unknown"}): ${String(error)}`,
|
|
3813
|
+
);
|
|
3814
|
+
}
|
|
3815
|
+
},
|
|
3422
3816
|
afterControlResponse: async (connectionId, request, response) => {
|
|
3423
3817
|
if (
|
|
3424
3818
|
(request.operation === "turn.prompt" ||
|
|
@@ -3439,13 +3833,6 @@ export function createNotificationsExtension(
|
|
|
3439
3833
|
}
|
|
3440
3834
|
|
|
3441
3835
|
if (request.operation === "session.close" && response.ok === true) ctx.shutdown();
|
|
3442
|
-
if (typeof request.operation === "string" && identityControlOperations.has(request.operation)) {
|
|
3443
|
-
const pending = deferredIdentityRotation;
|
|
3444
|
-
deferredIdentityRotation = undefined;
|
|
3445
|
-
identityControlInFlight = false;
|
|
3446
|
-
if (response.ok === true && pending)
|
|
3447
|
-
await rotateSessionAuthority(pending.event, pending.ctx, pending.awaitStartup);
|
|
3448
|
-
}
|
|
3449
3836
|
},
|
|
3450
3837
|
control: async (connectionId, frame) => {
|
|
3451
3838
|
const request = frame as {
|
|
@@ -3465,6 +3852,21 @@ export function createNotificationsExtension(
|
|
|
3465
3852
|
ok: false,
|
|
3466
3853
|
error: { code: "conflict", message: "session identity mutation is already active" },
|
|
3467
3854
|
};
|
|
3855
|
+
const requireNativeControlDrain =
|
|
3856
|
+
(request as { requireNativeControlDrain?: unknown }).requireNativeControlDrain === true ||
|
|
3857
|
+
(!!request.input &&
|
|
3858
|
+
typeof request.input === "object" &&
|
|
3859
|
+
!Array.isArray(request.input) &&
|
|
3860
|
+
(request.input as { requireNativeControlDrain?: unknown }).requireNativeControlDrain === true);
|
|
3861
|
+
if (requireNativeControlDrain && !isNativeControlDrainAvailable())
|
|
3862
|
+
return {
|
|
3863
|
+
id: requestId,
|
|
3864
|
+
ok: false,
|
|
3865
|
+
error: {
|
|
3866
|
+
code: "unavailable",
|
|
3867
|
+
message: "SDK identity control requires the native control-drain lease.",
|
|
3868
|
+
},
|
|
3869
|
+
};
|
|
3468
3870
|
if (rotatesIdentity) identityControlInFlight = true;
|
|
3469
3871
|
const response = await controlRequesterContext.run(connectionId, () =>
|
|
3470
3872
|
dispatchControl(
|
|
@@ -3481,6 +3883,7 @@ export function createNotificationsExtension(
|
|
|
3481
3883
|
},
|
|
3482
3884
|
),
|
|
3483
3885
|
);
|
|
3886
|
+
|
|
3484
3887
|
if (rotatesIdentity && response.ok !== true) {
|
|
3485
3888
|
identityControlInFlight = false;
|
|
3486
3889
|
deferredIdentityRotation = undefined;
|
|
@@ -3535,6 +3938,7 @@ export function createNotificationsExtension(
|
|
|
3535
3938
|
workflowGate: undefined,
|
|
3536
3939
|
gatePresentations,
|
|
3537
3940
|
stopping: false,
|
|
3941
|
+
inboundFenced: false,
|
|
3538
3942
|
abortEphemeralTurns: () => {},
|
|
3539
3943
|
disableEphemeralTurns: () => {},
|
|
3540
3944
|
cancelPostmortemCleanup: () => {},
|
|
@@ -3550,12 +3954,20 @@ export function createNotificationsExtension(
|
|
|
3550
3954
|
pendingPromptCorrelations,
|
|
3551
3955
|
activePromptCorrelation: undefined,
|
|
3552
3956
|
recordPromptTerminal,
|
|
3957
|
+
notePromptReconciliation: (correlation, frame) => {
|
|
3958
|
+
void kindReconciliation.noteTransition("prompt", correlation, frame);
|
|
3959
|
+
},
|
|
3960
|
+
emitPromptFailure,
|
|
3553
3961
|
emitPromptLifecycle,
|
|
3554
3962
|
emitPromptEvent,
|
|
3555
3963
|
pendingInbound: new Set<number>(),
|
|
3556
|
-
inFlightTools: new Map<
|
|
3964
|
+
inFlightTools: new Map<
|
|
3965
|
+
string,
|
|
3966
|
+
{ toolName: string; args?: unknown; pendingPhase?: "completed" | "failed" | "cancelled" }
|
|
3967
|
+
>(),
|
|
3557
3968
|
deferredGatePresentations: [],
|
|
3558
3969
|
deferredInboundControls: [],
|
|
3970
|
+
notificationRootRegistration: undefined,
|
|
3559
3971
|
};
|
|
3560
3972
|
const initializedRuntime = runtime;
|
|
3561
3973
|
runtimes.set(id, initializedRuntime);
|
|
@@ -3589,10 +4001,10 @@ export function createNotificationsExtension(
|
|
|
3589
4001
|
};
|
|
3590
4002
|
|
|
3591
4003
|
const ephemeralTurns = new EphemeralTurnHost(sendSdkFrame, async (question, signal) => {
|
|
3592
|
-
if (!options.
|
|
4004
|
+
if (!options.runBtwTurn) throw new Error("Ephemeral turns are unavailable.");
|
|
3593
4005
|
const generation = initializedRuntime.policyGeneration;
|
|
3594
4006
|
if (initializedRuntime.policySuspended) throw new Error("Notification policy is provisional.");
|
|
3595
|
-
const result = await options.
|
|
4007
|
+
const result = await options.runBtwTurn(question, signal);
|
|
3596
4008
|
if (
|
|
3597
4009
|
initializedRuntime.policySuspended ||
|
|
3598
4010
|
initializedRuntime.policyGeneration !== generation ||
|
|
@@ -3610,7 +4022,30 @@ export function createNotificationsExtension(
|
|
|
3610
4022
|
const frame = JSON.parse(inbound.json) as unknown;
|
|
3611
4023
|
if (!frame || typeof frame !== "object") return;
|
|
3612
4024
|
const typedFrame = frame as Record<string, unknown>;
|
|
4025
|
+
if (initializedRuntime.inboundFenced) {
|
|
4026
|
+
if (typedFrame.type === "control_request" && typeof typedFrame.id === "string") {
|
|
4027
|
+
try {
|
|
4028
|
+
server.sendTo(
|
|
4029
|
+
inbound.connectionId,
|
|
4030
|
+
JSON.stringify({
|
|
4031
|
+
type: "control_response",
|
|
4032
|
+
id: typedFrame.id,
|
|
4033
|
+
ok: false,
|
|
4034
|
+
error: { code: "endpoint_stale", message: "session endpoint is draining." },
|
|
4035
|
+
}),
|
|
4036
|
+
);
|
|
4037
|
+
} catch {}
|
|
4038
|
+
}
|
|
4039
|
+
return;
|
|
4040
|
+
}
|
|
3613
4041
|
if (typedFrame.type === "ephemeral_turn" || typedFrame.type === "ephemeral_turn_cancel") return;
|
|
4042
|
+
if (typedFrame.type === "event_replay") {
|
|
4043
|
+
const capabilities = Array.isArray(typedFrame.capabilities) ? typedFrame.capabilities : [];
|
|
4044
|
+
hostCapCache.set(
|
|
4045
|
+
inbound.connectionId,
|
|
4046
|
+
new Set(capabilities.filter((capability): capability is string => typeof capability === "string")),
|
|
4047
|
+
);
|
|
4048
|
+
}
|
|
3614
4049
|
inboundSdkFrame?.(inbound.connectionId, typedFrame);
|
|
3615
4050
|
} catch {}
|
|
3616
4051
|
});
|
|
@@ -3637,7 +4072,12 @@ export function createNotificationsExtension(
|
|
|
3637
4072
|
|
|
3638
4073
|
server.onReply((err, reply) => {
|
|
3639
4074
|
if (err || !reply) return;
|
|
3640
|
-
if (
|
|
4075
|
+
if (
|
|
4076
|
+
runtime?.inboundFenced ||
|
|
4077
|
+
runtime?.stopping ||
|
|
4078
|
+
runtime?.policySuspended ||
|
|
4079
|
+
runtimes.get(id) !== runtime
|
|
4080
|
+
) {
|
|
3641
4081
|
try {
|
|
3642
4082
|
server.closeClaimInvalid(reply.replyReceiptId, "session_stopping");
|
|
3643
4083
|
} catch {}
|
|
@@ -3751,7 +4191,7 @@ export function createNotificationsExtension(
|
|
|
3751
4191
|
}
|
|
3752
4192
|
const gate = runtime?.workflowGate;
|
|
3753
4193
|
const workflowGateActive =
|
|
3754
|
-
gate?.
|
|
4194
|
+
gate?.supportsRemoteGateAnswers?.() === true &&
|
|
3755
4195
|
typeof gate.onGateEmitted === "function" &&
|
|
3756
4196
|
typeof gate.resolveGateFromNotification === "function";
|
|
3757
4197
|
const gateId = gatePresentations.routeFor(reply.id);
|
|
@@ -3879,6 +4319,7 @@ export function createNotificationsExtension(
|
|
|
3879
4319
|
// thread (forwarded by the daemon over the WS, fail-closed at the daemon).
|
|
3880
4320
|
server.onInbound((err, inbound) => {
|
|
3881
4321
|
if (err || !inbound) return;
|
|
4322
|
+
if (initializedRuntime.inboundFenced) return;
|
|
3882
4323
|
const authenticatedInbound = inbound as typeof inbound & {
|
|
3883
4324
|
connectionId: string;
|
|
3884
4325
|
messageId?: number;
|
|
@@ -3979,7 +4420,7 @@ export function createNotificationsExtension(
|
|
|
3979
4420
|
}
|
|
3980
4421
|
if (typeof inbound.redact === "boolean") {
|
|
3981
4422
|
if (inbound.redact && !runtime.committedRedact) {
|
|
3982
|
-
terminalizeInFlightTools(runtime, runtime.id, "
|
|
4423
|
+
terminalizeInFlightTools(runtime, runtime.id, "cancelled");
|
|
3983
4424
|
}
|
|
3984
4425
|
runtime.committedRedact = inbound.redact;
|
|
3985
4426
|
runtime.redact = inbound.redact;
|
|
@@ -4038,12 +4479,19 @@ export function createNotificationsExtension(
|
|
|
4038
4479
|
}
|
|
4039
4480
|
if (notificationsEnabledForSession && settingsAvailable && settings) {
|
|
4040
4481
|
try {
|
|
4041
|
-
|
|
4482
|
+
let registrationToken: string | undefined;
|
|
4483
|
+
if (
|
|
4484
|
+
!(await ensureConfiguredDaemonOwners(settings, cfg, ctx.cwd, id, token => {
|
|
4485
|
+
registrationToken = token;
|
|
4486
|
+
}))
|
|
4487
|
+
) {
|
|
4042
4488
|
const result = failLifecycleStartup("failed", "Telegram daemon ownership is blocked.");
|
|
4043
4489
|
finishStartup(result);
|
|
4044
4490
|
await cleanupAbandonedStartup();
|
|
4045
4491
|
return result;
|
|
4046
4492
|
}
|
|
4493
|
+
if (isTelegramConfigured(cfg))
|
|
4494
|
+
runtime.notificationRootRegistration = { settings, cwd: ctx.cwd, registrationToken };
|
|
4047
4495
|
} catch (error) {
|
|
4048
4496
|
const result = failLifecycleStartup("failed", error);
|
|
4049
4497
|
finishStartup(result);
|
|
@@ -4204,7 +4652,9 @@ export function createNotificationsExtension(
|
|
|
4204
4652
|
? Gate
|
|
4205
4653
|
: never,
|
|
4206
4654
|
): void => {
|
|
4207
|
-
const
|
|
4655
|
+
const rawGateOptions = g.options ?? [];
|
|
4656
|
+
const options = rawGateOptions.map(o => String((o as { label?: unknown }).label ?? ""));
|
|
4657
|
+
const recommendedIndex = recommendedIndexFromGateOptions(rawGateOptions);
|
|
4208
4658
|
gateOptions.set(g.gate_id, options);
|
|
4209
4659
|
const promptCtx = g.context as { prompt?: unknown; title?: unknown } | undefined;
|
|
4210
4660
|
const question =
|
|
@@ -4221,6 +4671,7 @@ export function createNotificationsExtension(
|
|
|
4221
4671
|
sessionId: id,
|
|
4222
4672
|
question,
|
|
4223
4673
|
options,
|
|
4674
|
+
...(recommendedIndex === undefined ? {} : { recommendedIndex }),
|
|
4224
4675
|
controls: [],
|
|
4225
4676
|
multi: stageState.multi === true,
|
|
4226
4677
|
allowEmpty: stageState.allow_empty === true,
|
|
@@ -4316,8 +4767,15 @@ export function createNotificationsExtension(
|
|
|
4316
4767
|
runtime.redact = true;
|
|
4317
4768
|
runtime.verbosity = "lean";
|
|
4318
4769
|
runtime.stream = false;
|
|
4770
|
+
for (const [toolCallId, tool] of runtime.inFlightTools) {
|
|
4771
|
+
runtime.inFlightTools.set(toolCallId, {
|
|
4772
|
+
toolName: tool.toolName,
|
|
4773
|
+
...(tool.pendingPhase ? { pendingPhase: tool.pendingPhase } : {}),
|
|
4774
|
+
});
|
|
4775
|
+
}
|
|
4319
4776
|
return;
|
|
4320
4777
|
}
|
|
4778
|
+
const wasPolicySuspended = runtime.policySuspended;
|
|
4321
4779
|
const redactionEnabled = policy.redact && !runtime.committedRedact;
|
|
4322
4780
|
runtime.policyGeneration++;
|
|
4323
4781
|
runtime.committedRedact = policy.redact;
|
|
@@ -4325,7 +4783,8 @@ export function createNotificationsExtension(
|
|
|
4325
4783
|
runtime.redact = policy.redact;
|
|
4326
4784
|
runtime.verbosity = policy.verbosity;
|
|
4327
4785
|
runtime.stream = policy.stream;
|
|
4328
|
-
if (redactionEnabled) terminalizeInFlightTools(runtime, runtime.id, "
|
|
4786
|
+
if (redactionEnabled) terminalizeInFlightTools(runtime, runtime.id, "cancelled", true);
|
|
4787
|
+
else if (wasPolicySuspended && !policy.redact) settleProvisionalToolTerminals(runtime, runtime.id);
|
|
4329
4788
|
},
|
|
4330
4789
|
activate: binding => {
|
|
4331
4790
|
const runtime = runtimes.get(binding.sessionId);
|
|
@@ -4339,7 +4798,23 @@ export function createNotificationsExtension(
|
|
|
4339
4798
|
const { settings, settingsAvailable } = resolveSettings(options.settings);
|
|
4340
4799
|
if (!settingsAvailable || !settings) return "blocked_identity";
|
|
4341
4800
|
try {
|
|
4342
|
-
|
|
4801
|
+
let registrationToken: string | undefined;
|
|
4802
|
+
const result = await ensureTelegramOwner(settings, binding.cwd, binding.sessionId, token => {
|
|
4803
|
+
registrationToken = token;
|
|
4804
|
+
});
|
|
4805
|
+
const runtime = runtimes.get(binding.sessionId);
|
|
4806
|
+
const configured = isTelegramConfigured(resolveSettings(options.settings).cfg);
|
|
4807
|
+
if (result === "ready" && runtime && !runtime.stopping && configured) {
|
|
4808
|
+
runtime.notificationRootRegistration = { settings, cwd: binding.cwd, registrationToken };
|
|
4809
|
+
} else if (registrationToken !== undefined) {
|
|
4810
|
+
await unregisterNotificationRoot({
|
|
4811
|
+
settings,
|
|
4812
|
+
cwd: binding.cwd,
|
|
4813
|
+
sessionId: binding.sessionId,
|
|
4814
|
+
registrationToken,
|
|
4815
|
+
});
|
|
4816
|
+
}
|
|
4817
|
+
return result;
|
|
4343
4818
|
} catch {
|
|
4344
4819
|
return "failed";
|
|
4345
4820
|
}
|
|
@@ -4437,7 +4912,16 @@ export function createNotificationsExtension(
|
|
|
4437
4912
|
};
|
|
4438
4913
|
|
|
4439
4914
|
api.on("session_start", async (_event, ctx) => {
|
|
4440
|
-
|
|
4915
|
+
const task = startAndReconcileSession(ctx);
|
|
4916
|
+
// Track full start+reconcile so settled startups join replacement-token
|
|
4917
|
+
// reconcile before owner release. Pending native startup (/notify on) stays
|
|
4918
|
+
// nonblocking: shutdown only awaits these tasks when sessionStartPromises is clear.
|
|
4919
|
+
sessionLifecycleTasks.add(task);
|
|
4920
|
+
try {
|
|
4921
|
+
await task;
|
|
4922
|
+
} finally {
|
|
4923
|
+
sessionLifecycleTasks.delete(task);
|
|
4924
|
+
}
|
|
4441
4925
|
});
|
|
4442
4926
|
|
|
4443
4927
|
// A session endpoint's token and generation are authority for exactly one
|
|
@@ -4483,10 +4967,27 @@ export function createNotificationsExtension(
|
|
|
4483
4967
|
});
|
|
4484
4968
|
};
|
|
4485
4969
|
|
|
4970
|
+
const preparePredecessorForTerminal = async (id: string): Promise<void> => {
|
|
4971
|
+
const predecessor = runtimes.get(id);
|
|
4972
|
+
if (!predecessor || cleanupRetries.has(id))
|
|
4973
|
+
throw new Error(`notifications: predecessor runtime ${id} is not safely send-capable.`);
|
|
4974
|
+
predecessor.inboundFenced = true;
|
|
4975
|
+
// Release broker/host authority while leaving the native server alive for
|
|
4976
|
+
// the one accepted terminal response. stopAndWait is deferred to stopSession.
|
|
4977
|
+
const stopped = await predecessor.host.stop();
|
|
4978
|
+
if (stopped !== "stopped" && predecessor.host.started)
|
|
4979
|
+
throw new Error(`notifications: predecessor runtime ${id} host release was not proven.`);
|
|
4980
|
+
predecessor.hostStopped = true;
|
|
4981
|
+
predecessor.brokerRegistrationReleased = !predecessor.brokerRegistrationActive || predecessor.hostStopped;
|
|
4982
|
+
if (predecessor.brokerRegistrationActive && predecessor.hostStopped) predecessor.brokerRegistrationActive = false;
|
|
4983
|
+
predecessor.host.reverse.dispose();
|
|
4984
|
+
};
|
|
4985
|
+
|
|
4486
4986
|
const rotateSessionAuthority = async (
|
|
4487
4987
|
event: { previousSessionFile?: string },
|
|
4488
4988
|
ctx: ExtensionContext,
|
|
4489
4989
|
awaitStartup: boolean,
|
|
4990
|
+
options: { deferPredecessorStop?: boolean } = {},
|
|
4490
4991
|
): Promise<void> => {
|
|
4491
4992
|
if (extensionShuttingDown) return;
|
|
4492
4993
|
const newId = sessionId(ctx);
|
|
@@ -4495,7 +4996,11 @@ export function createNotificationsExtension(
|
|
|
4495
4996
|
const pendingStartup = sessionStartPromises.get(newId);
|
|
4496
4997
|
if (pendingStartup) {
|
|
4497
4998
|
if (awaitStartup) {
|
|
4498
|
-
await pendingStartup;
|
|
4999
|
+
const result = await pendingStartup;
|
|
5000
|
+
if (!lifecycleStartupCapability && result.status === "failed")
|
|
5001
|
+
throw new Error(
|
|
5002
|
+
`notifications: SDK startup failed: ${result.failure?.message ?? "Unknown startup failure."}`,
|
|
5003
|
+
);
|
|
4499
5004
|
if (!extensionShuttingDown && runtimes.has(newId) && activeRuntimeId === newId)
|
|
4500
5005
|
await controller.reconcileCurrentSession(ctx);
|
|
4501
5006
|
} else {
|
|
@@ -4510,14 +5015,14 @@ export function createNotificationsExtension(
|
|
|
4510
5015
|
}
|
|
4511
5016
|
if (prevId && prevId !== newId) {
|
|
4512
5017
|
controller.rekeySession(prevId, newId);
|
|
4513
|
-
|
|
4514
|
-
await
|
|
4515
|
-
}
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
5018
|
+
if (options.deferPredecessorStop) {
|
|
5019
|
+
await preparePredecessorForTerminal(prevId);
|
|
5020
|
+
} else {
|
|
5021
|
+
const stopped = await stopSession(prevId);
|
|
5022
|
+
if (cleanupRetries.has(prevId) || runtimes.has(prevId) || sessionStartPromises.has(prevId))
|
|
5023
|
+
throw new Error(`notifications: predecessor runtime ${prevId} release is uncertain.`);
|
|
5024
|
+
if (!stopped && activeRuntimeId === prevId)
|
|
5025
|
+
throw new Error(`notifications: predecessor runtime ${prevId} release was not proven.`);
|
|
4521
5026
|
}
|
|
4522
5027
|
}
|
|
4523
5028
|
if (extensionShuttingDown) return;
|
|
@@ -4538,22 +5043,37 @@ export function createNotificationsExtension(
|
|
|
4538
5043
|
trackBranchStartup(newId, ctx, startup);
|
|
4539
5044
|
};
|
|
4540
5045
|
api.on("session_switch", async (event, ctx) => {
|
|
5046
|
+
const awaitStartup = shouldAwaitNotificationStartup(event);
|
|
4541
5047
|
if (identityControlInFlight) {
|
|
4542
|
-
deferredIdentityRotation = { event, ctx, awaitStartup
|
|
5048
|
+
deferredIdentityRotation = { event, ctx, awaitStartup };
|
|
4543
5049
|
return;
|
|
4544
5050
|
}
|
|
4545
|
-
await rotateSessionAuthority(event, ctx,
|
|
5051
|
+
await rotateSessionAuthority(event, ctx, awaitStartup);
|
|
4546
5052
|
});
|
|
4547
5053
|
api.on("session_branch", async (event, ctx) => {
|
|
4548
5054
|
if (identityControlInFlight) {
|
|
4549
|
-
deferredIdentityRotation = { event, ctx, awaitStartup:
|
|
5055
|
+
deferredIdentityRotation = { event, ctx, awaitStartup: true };
|
|
4550
5056
|
return;
|
|
4551
5057
|
}
|
|
4552
|
-
await rotateSessionAuthority(event, ctx,
|
|
5058
|
+
await rotateSessionAuthority(event, ctx, true);
|
|
4553
5059
|
});
|
|
4554
5060
|
|
|
4555
|
-
const terminalizeInFlightTools = (
|
|
4556
|
-
|
|
5061
|
+
const terminalizeInFlightTools = (
|
|
5062
|
+
rt: SessionRuntime,
|
|
5063
|
+
id: string,
|
|
5064
|
+
phase: "cancelled" | "failed",
|
|
5065
|
+
allowSafeRedactedFrame = false,
|
|
5066
|
+
): void => {
|
|
5067
|
+
if (rt.policySuspended && !allowSafeRedactedFrame) {
|
|
5068
|
+
for (const [toolCallId, tool] of rt.inFlightTools) {
|
|
5069
|
+
rt.inFlightTools.set(toolCallId, {
|
|
5070
|
+
toolName: tool.toolName,
|
|
5071
|
+
pendingPhase: tool.pendingPhase ?? phase,
|
|
5072
|
+
});
|
|
5073
|
+
}
|
|
5074
|
+
return;
|
|
5075
|
+
}
|
|
5076
|
+
if (rt.notificationsActive && (!rt.redact || allowSafeRedactedFrame)) {
|
|
4557
5077
|
for (const [toolCallId, { toolName }] of rt.inFlightTools) {
|
|
4558
5078
|
try {
|
|
4559
5079
|
pushSessionFrame(rt, { type: "tool_activity", sessionId: id, toolCallId, toolName, phase });
|
|
@@ -4565,6 +5085,27 @@ export function createNotificationsExtension(
|
|
|
4565
5085
|
rt.inFlightTools.clear();
|
|
4566
5086
|
};
|
|
4567
5087
|
|
|
5088
|
+
const settleProvisionalToolTerminals = (rt: SessionRuntime, id: string): void => {
|
|
5089
|
+
for (const [toolCallId, tool] of rt.inFlightTools) {
|
|
5090
|
+
if (!tool.pendingPhase) continue;
|
|
5091
|
+
try {
|
|
5092
|
+
if (rt.notificationsActive && !rt.redact) {
|
|
5093
|
+
pushSessionFrame(rt, {
|
|
5094
|
+
type: "tool_activity",
|
|
5095
|
+
sessionId: id,
|
|
5096
|
+
toolCallId,
|
|
5097
|
+
toolName: tool.toolName,
|
|
5098
|
+
phase: tool.pendingPhase,
|
|
5099
|
+
});
|
|
5100
|
+
}
|
|
5101
|
+
} catch (e) {
|
|
5102
|
+
logger.warn(`notifications: provisional tool_activity settlement failed: ${String(e)}`);
|
|
5103
|
+
} finally {
|
|
5104
|
+
rt.inFlightTools.delete(toolCallId);
|
|
5105
|
+
}
|
|
5106
|
+
}
|
|
5107
|
+
};
|
|
5108
|
+
|
|
4568
5109
|
const resetTurnStreamState = (rt: SessionRuntime): void => {
|
|
4569
5110
|
rt.currentTurnText = undefined;
|
|
4570
5111
|
rt.preAskFlushedText = undefined;
|
|
@@ -4579,22 +5120,45 @@ export function createNotificationsExtension(
|
|
|
4579
5120
|
if (!pending) return;
|
|
4580
5121
|
rt.pendingFinal = undefined;
|
|
4581
5122
|
if (pending.text && rt.notificationsActive && !rt.redact) {
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
phase: "finalized",
|
|
4587
|
-
finalAnswer: true,
|
|
5123
|
+
// Under lean, hold intermediate finals until agent_end when the agent is still
|
|
5124
|
+
// running so provisional-policy activation cannot reintroduce per-turn spam.
|
|
5125
|
+
if (rt.verbosity === "lean" && rt.busy) {
|
|
5126
|
+
rt.pendingSettled = {
|
|
4588
5127
|
text: pending.text,
|
|
4589
5128
|
...(pending.messageRef ? { messageRef: pending.messageRef } : {}),
|
|
4590
|
-
}
|
|
4591
|
-
}
|
|
4592
|
-
|
|
5129
|
+
};
|
|
5130
|
+
} else {
|
|
5131
|
+
try {
|
|
5132
|
+
pushSessionFrame(rt, {
|
|
5133
|
+
type: "turn_stream",
|
|
5134
|
+
sessionId: id,
|
|
5135
|
+
phase: "finalized",
|
|
5136
|
+
finalAnswer: true,
|
|
5137
|
+
text: pending.text,
|
|
5138
|
+
...(pending.messageRef ? { messageRef: pending.messageRef } : {}),
|
|
5139
|
+
});
|
|
5140
|
+
} catch (error) {
|
|
5141
|
+
logger.warn(`notifications: pushFrame (pending turn) failed: ${String(error)}`);
|
|
5142
|
+
}
|
|
4593
5143
|
}
|
|
4594
5144
|
}
|
|
4595
5145
|
resetTurnStreamState(rt);
|
|
4596
5146
|
};
|
|
4597
5147
|
|
|
5148
|
+
/** Emit the deferred lean settled answer exactly once (agent_end / idle). */
|
|
5149
|
+
const flushPendingSettled = (rt: SessionRuntime, id: string): void => {
|
|
5150
|
+
const settled = rt.pendingSettled;
|
|
5151
|
+
rt.pendingSettled = undefined;
|
|
5152
|
+
if (!settled?.text || !rt.notificationsActive || rt.redact || rt.policySuspended) return;
|
|
5153
|
+
const previousLiveRef = rt.liveRef;
|
|
5154
|
+
if (settled.messageRef) rt.liveRef = settled.messageRef;
|
|
5155
|
+
try {
|
|
5156
|
+
flushTurnText(rt, id, settled.text, true);
|
|
5157
|
+
} finally {
|
|
5158
|
+
if (!settled.messageRef) rt.liveRef = previousLiveRef;
|
|
5159
|
+
}
|
|
5160
|
+
};
|
|
5161
|
+
|
|
4598
5162
|
// Drive the live typing indicator: mark busy when the agent loop starts so
|
|
4599
5163
|
// the daemon shows "typing…" in the thread while the agent is thinking,
|
|
4600
5164
|
// before any turn output exists. Cleared on `agent_end` below.
|
|
@@ -4607,6 +5171,7 @@ export function createNotificationsExtension(
|
|
|
4607
5171
|
rt.busy = true;
|
|
4608
5172
|
const correlation = rt.pendingPromptCorrelations.shift();
|
|
4609
5173
|
rt.activePromptCorrelation = correlation;
|
|
5174
|
+
rt.notePromptReconciliation(correlation, { type: "agent_start" });
|
|
4610
5175
|
rt.emitPromptLifecycle(correlation, { type: "agent_start", sessionId: id, ...correlation });
|
|
4611
5176
|
try {
|
|
4612
5177
|
// `activity` is the native live-host lifecycle surface. The separately
|
|
@@ -4651,13 +5216,54 @@ export function createNotificationsExtension(
|
|
|
4651
5216
|
rt.busy = false;
|
|
4652
5217
|
const correlation = rt.activePromptCorrelation;
|
|
4653
5218
|
if (correlation) {
|
|
4654
|
-
|
|
4655
|
-
|
|
5219
|
+
const terminalAssistant = (Array.isArray(event.messages) ? [...event.messages].reverse() : []).find(
|
|
5220
|
+
message =>
|
|
5221
|
+
message &&
|
|
5222
|
+
typeof message === "object" &&
|
|
5223
|
+
(message as { role?: unknown }).role === "assistant" &&
|
|
5224
|
+
((message as { stopReason?: unknown }).stopReason === "error" ||
|
|
5225
|
+
(message as { stopReason?: unknown }).stopReason === "aborted"),
|
|
5226
|
+
) as { stopReason?: "error" | "aborted"; errorMessage?: unknown } | undefined;
|
|
5227
|
+
const failureCode =
|
|
5228
|
+
event.stopReason === "cancelled"
|
|
5229
|
+
? "cancelled"
|
|
5230
|
+
: terminalAssistant?.stopReason === "error"
|
|
5231
|
+
? "agent_error"
|
|
5232
|
+
: terminalAssistant?.stopReason === "aborted"
|
|
5233
|
+
? "aborted"
|
|
5234
|
+
: undefined;
|
|
5235
|
+
if (failureCode)
|
|
5236
|
+
rt.emitPromptFailure(correlation, {
|
|
5237
|
+
code: failureCode,
|
|
5238
|
+
message:
|
|
5239
|
+
typeof terminalAssistant?.errorMessage === "string"
|
|
5240
|
+
? terminalAssistant.errorMessage
|
|
5241
|
+
: "Prompt submission failed.",
|
|
5242
|
+
});
|
|
5243
|
+
else {
|
|
5244
|
+
rt.notePromptReconciliation(correlation, { type: "agent_end" });
|
|
5245
|
+
if (rt.recordPromptTerminal(correlation)) {
|
|
5246
|
+
const finalAssistant = (Array.isArray(event.messages) ? [...event.messages].reverse() : []).find(
|
|
5247
|
+
message =>
|
|
5248
|
+
message &&
|
|
5249
|
+
typeof message === "object" &&
|
|
5250
|
+
(message as { role?: unknown }).role === "assistant" &&
|
|
5251
|
+
(message as { stopReason?: unknown }).stopReason === "stop",
|
|
5252
|
+
);
|
|
5253
|
+
const finalText = finalAssistant ? acpFinalTextFromMessage(finalAssistant).text : "";
|
|
5254
|
+
rt.emitPromptLifecycle(correlation, {
|
|
5255
|
+
type: "agent_end",
|
|
5256
|
+
sessionId: id,
|
|
5257
|
+
...correlation,
|
|
5258
|
+
...(finalText ? { finalText } : {}),
|
|
5259
|
+
});
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
4656
5262
|
} else {
|
|
4657
5263
|
rt.emitPromptLifecycle(undefined, { type: "agent_end", sessionId: id });
|
|
4658
5264
|
}
|
|
4659
5265
|
rt.activePromptCorrelation = undefined;
|
|
4660
|
-
terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "
|
|
5266
|
+
terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "failed");
|
|
4661
5267
|
try {
|
|
4662
5268
|
pushSessionFrame(rt, { type: "activity", sessionId: id, state: "idle" });
|
|
4663
5269
|
} catch (e) {
|
|
@@ -4696,9 +5302,14 @@ export function createNotificationsExtension(
|
|
|
4696
5302
|
logger.warn(`notifications: noteIdle failed: ${String(e)}`);
|
|
4697
5303
|
}
|
|
4698
5304
|
|
|
5305
|
+
// Lean: emit the latest deferred assistant answer exactly once at idle.
|
|
5306
|
+
// Intermediate tool-turn narration was held on turn_end; ask lead-ins were
|
|
5307
|
+
// already flushed immediately and deduped via preAskFlushedText.
|
|
5308
|
+
flushPendingSettled(rt, id);
|
|
5309
|
+
|
|
4699
5310
|
// On idle, stream a context update with metadata (token/model usage +
|
|
4700
|
-
// working-tree diff) unless redaction is on.
|
|
4701
|
-
//
|
|
5311
|
+
// working-tree diff) unless redaction is on. Under verbose the agent's last
|
|
5312
|
+
// message is already streamed per turn_end; lean flushes it just above.
|
|
4702
5313
|
if (!rt.redact && rt.verbosity === "verbose") {
|
|
4703
5314
|
const usage = (
|
|
4704
5315
|
ctx as { getContextUsage?: () => { tokens: number | null; contextWindow: number } | undefined }
|
|
@@ -4727,17 +5338,20 @@ export function createNotificationsExtension(
|
|
|
4727
5338
|
}
|
|
4728
5339
|
});
|
|
4729
5340
|
|
|
4730
|
-
// Stream viable agent output
|
|
4731
|
-
//
|
|
4732
|
-
//
|
|
4733
|
-
//
|
|
4734
|
-
//
|
|
4735
|
-
// rate-limit pool
|
|
5341
|
+
// Stream viable agent output. Verbose mirrors each turn that produced assistant
|
|
5342
|
+
// text. Lean defers the latest answer until agent_end (idle) so tool-heavy runs
|
|
5343
|
+
// do not flood remote clients with intermediate narration; ask lead-ins still
|
|
5344
|
+
// flush immediately. Tool-only turns yield no text and are skipped. Redaction
|
|
5345
|
+
// suppresses streamed content (only the one-time identity header survives).
|
|
5346
|
+
// The daemon coalesces/throttles these via its shared rate-limit pool.
|
|
4736
5347
|
// Push the in-flight turn's assistant text as a finalized turn_stream, deduped
|
|
4737
5348
|
// against what was already flushed for this turn (the pre-ask lead-in).
|
|
4738
5349
|
const flushTurnText = (rt: SessionRuntime, id: string, text: string | undefined, finalAnswer: boolean): void => {
|
|
4739
5350
|
if (!text || text === rt.preAskFlushedText || !rt.notificationsActive || rt.policySuspended) return;
|
|
4740
5351
|
rt.preAskFlushedText = text;
|
|
5352
|
+
// Ask lead-ins supersede any deferred lean settled answer from earlier turns
|
|
5353
|
+
// so agent_end does not re-emit stale intermediate narration (#2863 review).
|
|
5354
|
+
if (!finalAnswer) rt.pendingSettled = undefined;
|
|
4741
5355
|
// Decision A: a stream-enabled turn must finalize as an in-place edit of ONE
|
|
4742
5356
|
// live message, never a fresh (rich-promotable) send. If live frames were
|
|
4743
5357
|
// async-queued and none landed before this flush, reuse the per-turn ref
|
|
@@ -4767,14 +5381,13 @@ export function createNotificationsExtension(
|
|
|
4767
5381
|
// path and ordered before it — unlike message_update, which is queued async),
|
|
4768
5382
|
// then flushed here before the ask tool's execute calls registerAsk.
|
|
4769
5383
|
api.on("tool_execution_start", (event, ctx) => {
|
|
5384
|
+
const id = sessionId(ctx);
|
|
5385
|
+
const rt = runtimes.get(id);
|
|
5386
|
+
rt?.emitPromptEvent(event);
|
|
4770
5387
|
if (event.toolName === "ask") {
|
|
4771
|
-
const id = sessionId(ctx);
|
|
4772
|
-
const rt = runtimes.get(id);
|
|
4773
5388
|
if (!rt?.notificationsActive || rt.redact) return;
|
|
4774
5389
|
flushTurnText(rt, id, rt.currentTurnText, false);
|
|
4775
5390
|
}
|
|
4776
|
-
const id = sessionId(ctx);
|
|
4777
|
-
const rt = runtimes.get(id);
|
|
4778
5391
|
if (!rt?.notificationsActive || rt.redact) return;
|
|
4779
5392
|
rt.inFlightTools.set(event.toolCallId, { toolName: event.toolName, args: event.args });
|
|
4780
5393
|
try {
|
|
@@ -4790,12 +5403,32 @@ export function createNotificationsExtension(
|
|
|
4790
5403
|
}
|
|
4791
5404
|
});
|
|
4792
5405
|
|
|
5406
|
+
api.on("tool_execution_update", (event, ctx) => {
|
|
5407
|
+
const rt = runtimes.get(sessionId(ctx));
|
|
5408
|
+
rt?.emitPromptEvent(event);
|
|
5409
|
+
});
|
|
5410
|
+
|
|
4793
5411
|
api.on("tool_execution_end", (event, ctx) => {
|
|
4794
5412
|
const id = sessionId(ctx);
|
|
4795
5413
|
const rt = runtimes.get(id);
|
|
4796
5414
|
if (!rt) return;
|
|
5415
|
+
rt.emitPromptEvent(event);
|
|
4797
5416
|
const inFlight = rt.inFlightTools.get(event.toolCallId);
|
|
4798
|
-
if (!
|
|
5417
|
+
if (!inFlight) return;
|
|
5418
|
+
if (!rt.notificationsActive) {
|
|
5419
|
+
rt.inFlightTools.delete(event.toolCallId);
|
|
5420
|
+
return;
|
|
5421
|
+
}
|
|
5422
|
+
if (rt.policySuspended) {
|
|
5423
|
+
if (!inFlight.pendingPhase) {
|
|
5424
|
+
rt.inFlightTools.set(event.toolCallId, {
|
|
5425
|
+
toolName: inFlight.toolName,
|
|
5426
|
+
pendingPhase: event.isError ? "failed" : "completed",
|
|
5427
|
+
});
|
|
5428
|
+
}
|
|
5429
|
+
return;
|
|
5430
|
+
}
|
|
5431
|
+
if (rt.redact) {
|
|
4799
5432
|
rt.inFlightTools.delete(event.toolCallId);
|
|
4800
5433
|
return;
|
|
4801
5434
|
}
|
|
@@ -4878,19 +5511,37 @@ export function createNotificationsExtension(
|
|
|
4878
5511
|
rt.turnClosed = true;
|
|
4879
5512
|
return;
|
|
4880
5513
|
}
|
|
4881
|
-
if (text)
|
|
5514
|
+
if (text) {
|
|
5515
|
+
if (rt.verbosity === "verbose") {
|
|
5516
|
+
// Verbose: one finalized turn_stream per turn with assistant text.
|
|
5517
|
+
flushTurnText(rt, id, text, true);
|
|
5518
|
+
} else if (text !== rt.preAskFlushedText) {
|
|
5519
|
+
// Lean: hold the latest answer until agent_end. Skip when this turn
|
|
5520
|
+
// already flushed the same text as an ask lead-in (no duplicate at idle).
|
|
5521
|
+
rt.pendingSettled = {
|
|
5522
|
+
text,
|
|
5523
|
+
...(rt.liveRef ? { messageRef: rt.liveRef } : {}),
|
|
5524
|
+
};
|
|
5525
|
+
} else {
|
|
5526
|
+
// Lead-in already flushed: drop any older deferred settled text so idle
|
|
5527
|
+
// does not re-emit intermediate narration after the ask prompt (#2863).
|
|
5528
|
+
rt.pendingSettled = undefined;
|
|
5529
|
+
}
|
|
5530
|
+
}
|
|
4882
5531
|
resetTurnStreamState(rt);
|
|
4883
5532
|
});
|
|
4884
5533
|
|
|
4885
|
-
// Live streaming (opt-in): push throttled in-progress assistant
|
|
4886
|
-
// non-finalized turn_stream frames so remote clients edit one message
|
|
4887
|
-
// turn streams.
|
|
4888
|
-
//
|
|
5534
|
+
// Live streaming (opt-in + verbose only): push throttled in-progress assistant
|
|
5535
|
+
// text as non-finalized turn_stream frames so remote clients edit one message
|
|
5536
|
+
// as the turn streams. Lean keeps settled-answer-only delivery; live frames
|
|
5537
|
+
// are suppressed even when the stream preference is on. The finalized frame
|
|
5538
|
+
// (turn_end / agent_end) carries the same messageRef and lands the authoritative
|
|
5539
|
+
// text. Suppressed under redaction.
|
|
4889
5540
|
api.on("message_update", (event, ctx) => {
|
|
4890
5541
|
const id = sessionId(ctx);
|
|
4891
5542
|
const rt = runtimes.get(id);
|
|
4892
5543
|
rt?.emitPromptEvent(event);
|
|
4893
|
-
if (!rt?.notificationsActive || !rt.stream || rt.redact || rt.turnClosed) return;
|
|
5544
|
+
if (!rt?.notificationsActive || !rt.stream || rt.redact || rt.turnClosed || rt.verbosity !== "verbose") return;
|
|
4894
5545
|
if ((event.message as { role?: unknown }).role !== "assistant") return;
|
|
4895
5546
|
if (rt.liveRef === undefined && rt.turnSeq !== undefined) {
|
|
4896
5547
|
rt.liveRef = String(rt.turnSeq);
|
|
@@ -4942,13 +5593,29 @@ export function createNotificationsExtension(
|
|
|
4942
5593
|
extensionShuttingDown = true;
|
|
4943
5594
|
identityControlInFlight = false;
|
|
4944
5595
|
deferredIdentityRotation = undefined;
|
|
4945
|
-
await Promise.allSettled([...branchStartupTasks]);
|
|
4946
5596
|
const id = sessionId(ctx);
|
|
5597
|
+
// Join settled start+reconcile before owner release. Keep pending native
|
|
5598
|
+
// startup (/notify on) nonblocking — do not await sessionLifecycleTasks while
|
|
5599
|
+
// sessionStartPromises still has this id.
|
|
5600
|
+
if (!sessionStartPromises.has(id)) await Promise.allSettled([...sessionLifecycleTasks]);
|
|
5601
|
+
await Promise.allSettled([...branchStartupTasks]);
|
|
4947
5602
|
const rt = runtimes.get(id);
|
|
4948
|
-
if (rt) terminalizeInFlightTools(rt, id, "
|
|
5603
|
+
if (rt) terminalizeInFlightTools(rt, id, "cancelled");
|
|
5604
|
+
// Startup is only genuinely in flight when a `sessionStartPromises` entry
|
|
5605
|
+
// exists. Once startup has settled, the host is broker-visible and its
|
|
5606
|
+
// post-start `reconcileCurrentSession` may already have minted a
|
|
5607
|
+
// replacement notification-root token whose unregister is still awaiting
|
|
5608
|
+
// its file lock and atomic registry write. Returning before that settles
|
|
5609
|
+
// leaves a stale `sessions[id]` row that the retained older token is
|
|
5610
|
+
// correctly fenced from removing, so shutdown must join it.
|
|
5611
|
+
const startupWasPending = sessionStartPromises.has(id);
|
|
4949
5612
|
const controllerStop =
|
|
4950
5613
|
typeof ctx.sessionManager.getCwd === "function" ? controller.stopCurrentSession(ctx) : Promise.resolve(false);
|
|
4951
|
-
|
|
5614
|
+
const settledControllerStop = controllerStop.catch(error => {
|
|
5615
|
+
logger.warn(`notifications: controller shutdown failed: ${String(error)}`);
|
|
5616
|
+
return false;
|
|
5617
|
+
});
|
|
5618
|
+
if (startupWasPending) void settledControllerStop;
|
|
4952
5619
|
try {
|
|
4953
5620
|
await stopSession(id);
|
|
4954
5621
|
} catch (error) {
|
|
@@ -4959,5 +5626,10 @@ export function createNotificationsExtension(
|
|
|
4959
5626
|
// error severity (matching the postmortem cleanup precedent).
|
|
4960
5627
|
logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
|
|
4961
5628
|
}
|
|
5629
|
+
// Keep shutdown nonblocking only while native startup is genuinely
|
|
5630
|
+
// pending (the `/notify on` path); otherwise await the controller queue so
|
|
5631
|
+
// completed-start reconciliation and its replacement-token cleanup are
|
|
5632
|
+
// joined before lifecycle shutdown returns.
|
|
5633
|
+
if (!startupWasPending) await settledControllerStop;
|
|
4962
5634
|
});
|
|
4963
5635
|
}
|