@gajae-code/coding-agent 0.11.11 → 0.12.1
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 +131 -4
- package/dist/types/async/job-manager.d.ts +17 -3
- package/dist/types/cli/read-cli.d.ts +1 -0
- 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/config/model-registry.d.ts +4 -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 +83 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/exa/mcp-client.d.ts +10 -1
- package/dist/types/exec/bash-executor.d.ts +45 -12
- package/dist/types/extensibility/extensions/types.d.ts +38 -4
- package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
- package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
- package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
- package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
- package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +70 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +27 -1
- package/dist/types/gjc-runtime/team-store.d.ts +12 -0
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +17 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +10 -19
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/session-observer-overlay.d.ts +22 -0
- package/dist/types/modes/components/settings-selector.d.ts +11 -0
- package/dist/types/modes/components/status-line/gh.d.ts +5 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/reminders/star-reminder.d.ts +3 -13
- package/dist/types/runtime-mcp/manager.d.ts +1 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
- package/dist/types/sdk/bus/config.d.ts +6 -2
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +44 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +44 -0
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +69 -0
- package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
- package/dist/types/sdk/bus/rich-render.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
- package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
- package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/prompt-status.d.ts +23 -3
- package/dist/types/sdk/session.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +82 -12
- package/dist/types/session/artifacts.d.ts +8 -0
- package/dist/types/session/blob-store.d.ts +57 -4
- package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +48 -19
- package/dist/types/session/session-storage.d.ts +4 -0
- package/dist/types/session/streaming-output.d.ts +96 -12
- package/dist/types/setup/credential-import.d.ts +12 -0
- package/dist/types/skill-state/active-state.d.ts +12 -4
- package/dist/types/skill-state/workflow-hud.d.ts +9 -0
- package/dist/types/task/executor.d.ts +3 -2
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/provider-retry-status.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +7 -1
- package/dist/types/task/types.d.ts +23 -0
- package/dist/types/tools/ask-answer-registry.d.ts +2 -2
- package/dist/types/tools/bash-interactive.d.ts +4 -1
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +17 -1
- package/dist/types/tools/image-gen.d.ts +4 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/output-meta.d.ts +44 -3
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -7
- package/dist/types/tools/resource-gc.d.ts +4 -0
- package/dist/types/tools/review.d.ts +5 -7
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/dist/types/utils/gh.d.ts +13 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/searxng.d.ts +7 -0
- package/dist/types/web/search/providers/xai.d.ts +2 -0
- package/package.json +11 -13
- package/scripts/acp-conformance-agent.ts +218 -0
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-gjc-bundle-settings.ts +306 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/generate-sdk-operation-inventory.ts +5 -0
- package/scripts/resident-memory-bench.ts +1341 -0
- package/scripts/run-acp-conformance.ts +285 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/async/job-manager.ts +78 -20
- package/src/capability/index.ts +8 -0
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/plugin-cli.ts +176 -18
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +29 -9
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/team.ts +20 -2
- package/src/config/file-lock.ts +11 -2
- package/src/config/model-registry.ts +21 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +74 -1
- package/src/config/settings.ts +246 -108
- package/src/coordinator-mcp/question-gate-codec.ts +1 -1
- package/src/coordinator-mcp/server.ts +5 -12
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +230 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +77 -41
- package/src/defaults/gjc/skills/team/SKILL.md +3 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +62 -39
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
- package/src/defaults/gjc-defaults.ts +4 -4
- package/src/eval/py/executor.ts +7 -2
- package/src/exa/mcp-client.ts +12 -3
- package/src/exec/bash-executor.ts +144 -27
- package/src/extensibility/extensions/runner.ts +15 -0
- package/src/extensibility/extensions/types.ts +46 -5
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
- package/src/extensibility/gjc-plugins/index.ts +19 -2
- package/src/extensibility/gjc-plugins/installer.ts +189 -50
- package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
- package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
- package/src/extensibility/gjc-plugins/observability.ts +3 -3
- package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
- package/src/extensibility/gjc-plugins/schema.ts +84 -12
- package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
- package/src/extensibility/gjc-plugins/types.ts +141 -0
- package/src/extensibility/gjc-plugins/validation.ts +9 -0
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
- package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
- package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
- package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
- package/src/gjc-runtime/deep-interview-state.ts +12 -943
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/launch-tmux.ts +25 -6
- package/src/gjc-runtime/managed-owner-admission.ts +1 -1
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +1149 -98
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +70 -222
- package/src/gjc-runtime/team-launch.ts +42 -5
- package/src/gjc-runtime/team-runtime.ts +1085 -40
- package/src/gjc-runtime/team-store.ts +32 -0
- package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
- package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/gjc-runtime/team-workers.ts +1 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +28 -0
- package/src/gjc-runtime/tmux-sessions.ts +58 -13
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +414 -618
- package/src/gjc-runtime/workflow-manifest.generated.json +105 -449
- package/src/gjc-runtime/workflow-manifest.ts +38 -247
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/hooks/skill-state.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +21 -22
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/main.ts +9 -5
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +98 -0
- package/src/modes/acp/acp-agent.ts +339 -191
- package/src/modes/acp/acp-event-mapper.ts +14 -7
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/gjc-bundle-settings.ts +508 -0
- 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 +12 -1
- package/src/modes/components/oauth-selector.ts +96 -28
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/session-observer-overlay.ts +457 -83
- package/src/modes/components/settings-selector.ts +116 -23
- package/src/modes/components/status-line/gh.ts +16 -0
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/components/tool-status-header.ts +3 -14
- package/src/modes/components/transcript-viewer-overlay.ts +99 -24
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +26 -2
- package/src/modes/controllers/input-controller.ts +18 -3
- package/src/modes/controllers/selector-controller.ts +127 -36
- package/src/modes/interactive-mode.ts +62 -3
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +22 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +94 -1
- package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
- package/src/prompts/agents/architect.md +9 -1
- package/src/prompts/agents/critic.md +9 -0
- package/src/prompts/system/eager-todo.md +8 -4
- package/src/prompts/system/system-prompt.md +11 -5
- package/src/prompts/tools/bash.md +5 -5
- package/src/prompts/tools/read.md +4 -2
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/reminders/star-reminder.ts +4 -42
- package/src/runtime-mcp/manager.ts +19 -4
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/acp/adapter.ts +22 -6
- package/src/sdk/broker/lifecycle.ts +25 -6
- package/src/sdk/broker/transport.ts +35 -3
- package/src/sdk/bus/attachment-registry.ts +5 -1
- package/src/sdk/bus/chat-daemon-cli.ts +4 -39
- package/src/sdk/bus/chat-daemon-control.ts +6 -3
- package/src/sdk/bus/config.ts +18 -2
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +984 -146
- package/src/sdk/bus/kind-aware-reconciliation.ts +317 -0
- package/src/sdk/bus/lifecycle-commands.ts +4 -1
- package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/reconciliation-store.ts +303 -0
- package/src/sdk/bus/rich-draft.ts +1 -1
- package/src/sdk/bus/rich-render.ts +2 -2
- package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
- package/src/sdk/bus/telegram-cli.ts +33 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
- package/src/sdk/bus/telegram-daemon.ts +916 -23
- package/src/sdk/bus/telegram-reference.ts +40 -3
- package/src/sdk/bus/threaded-render.ts +48 -3
- package/src/sdk/bus/topic-registry.ts +20 -0
- package/src/sdk/host/control/dispatch.ts +5 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +23 -1
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/host/query/revision-store.ts +36 -2
- package/src/sdk/host/reverse-leases.ts +44 -7
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/prompt-status.ts +16 -3
- package/src/sdk/protocol/operation-inventory.generated.json +51 -0
- package/src/sdk/protocol/operation-registry.ts +7 -2
- package/src/sdk/session.ts +101 -10
- package/src/session/agent-session.ts +1286 -220
- package/src/session/artifacts.ts +22 -0
- package/src/session/auth-broker-config.ts +10 -3
- package/src/session/blob-store.ts +915 -24
- package/src/session/internal/managed-session-scope.ts +44 -6
- package/src/session/internal/managed-session-storage.ts +48 -6
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +2364 -438
- package/src/session/session-storage.ts +9 -1
- package/src/session/streaming-output.ts +500 -111
- package/src/setup/credential-import.ts +75 -13
- package/src/setup/provider-onboarding.ts +19 -4
- package/src/skill-state/active-state.ts +32 -10
- package/src/skill-state/workflow-hud.ts +42 -2
- package/src/skill-state/workflow-mutation-guard.ts +560 -8
- package/src/task/executor.ts +122 -39
- package/src/task/index.ts +23 -14
- package/src/task/provider-retry-status.ts +16 -0
- package/src/task/receipt.ts +23 -7
- package/src/task/render.ts +6 -2
- package/src/task/types.ts +108 -0
- package/src/tools/ask-answer-registry.ts +12 -6
- package/src/tools/ask.ts +169 -118
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash-interactive.ts +8 -3
- package/src/tools/bash.ts +503 -60
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/fetch.ts +114 -23
- package/src/tools/find.ts +1 -0
- package/src/tools/image-gen.ts +29 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/output-meta.ts +281 -20
- package/src/tools/path-utils.ts +8 -3
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1560 -310
- package/src/tools/resource-gc.ts +182 -18
- package/src/tools/review.ts +5 -7
- package/src/tools/search.ts +1 -0
- package/src/tools/subagent-render.ts +6 -2
- package/src/tools/subagent.ts +6 -0
- package/src/tools/telegram-send.ts +6 -3
- package/src/tools/tool-result.ts +14 -1
- package/src/utils/gh.ts +39 -0
- package/src/utils/image-resize.ts +3 -0
- package/src/web/search/providers/anthropic.ts +4 -4
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/providers/kimi.ts +15 -5
- package/src/web/search/providers/searxng.ts +47 -4
- package/src/web/search/providers/xai.ts +7 -2
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
- package/src/gjc-runtime/deep-interview-draft.ts +0 -843
- package/src/gjc-runtime/deep-interview-payload.ts +0 -262
- package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
- package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
package/src/sdk/bus/index.ts
CHANGED
|
@@ -30,11 +30,12 @@ import { promisify } from "node:util";
|
|
|
30
30
|
import { ThinkingLevel } from "@gajae-code/agent-core";
|
|
31
31
|
import type { ImageContent, TextContent, Tool } from "@gajae-code/ai";
|
|
32
32
|
import { NotificationServer, nativeBuildInfo } from "@gajae-code/natives";
|
|
33
|
-
import { logger, postmortem, VERSION } from "@gajae-code/utils";
|
|
33
|
+
import { $credentialEnv, logger, postmortem, VERSION } from "@gajae-code/utils";
|
|
34
34
|
import { isModelProfileProviderAvailable, projectModelProfileCatalog } from "../../config/model-profile-contract";
|
|
35
35
|
import { isAuthenticated, kNoAuth } from "../../config/model-registry";
|
|
36
36
|
import { Settings } from "../../config/settings";
|
|
37
37
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
|
|
38
|
+
import { INTERACTIVE_SELECTOR_RESUME_ORIGIN } from "../../extensibility/shared-events";
|
|
38
39
|
import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
|
|
39
40
|
import {
|
|
40
41
|
NotificationGatePolicyChangedError,
|
|
@@ -43,9 +44,11 @@ import {
|
|
|
43
44
|
type WorkflowGateTerminalProof,
|
|
44
45
|
} from "../../modes/shared/agent-wire/workflow-gate-broker";
|
|
45
46
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
47
|
+
import type { ClientBridge } from "../../session/client-bridge";
|
|
46
48
|
import { parseThinkingLevel } from "../../thinking";
|
|
47
49
|
import type {
|
|
48
50
|
AskAnswerRequest,
|
|
51
|
+
AskAnswerSource,
|
|
49
52
|
AskAnswerSourceResult,
|
|
50
53
|
AskRemoteControl,
|
|
51
54
|
AskRemoteInteraction,
|
|
@@ -62,7 +65,7 @@ import { SessionSdkHost, shouldHostSdk } from "../host";
|
|
|
62
65
|
import { type ControlSurface, dispatchControl } from "../host/control";
|
|
63
66
|
import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "../host/query";
|
|
64
67
|
import { projectQ10Models } from "../models.js";
|
|
65
|
-
import { PROMPT_CLIENT_REF_MAX_LENGTH } from "../prompt-status";
|
|
68
|
+
import { PROMPT_CLIENT_REF_MAX_LENGTH, type SdkPromptTerminalOutcome } from "../prompt-status";
|
|
66
69
|
import { OPERATIONS } from "../protocol/operation-registry";
|
|
67
70
|
import {
|
|
68
71
|
lifecycleStartupCapabilityForApi,
|
|
@@ -82,9 +85,16 @@ import {
|
|
|
82
85
|
sessionTag,
|
|
83
86
|
} from "./config";
|
|
84
87
|
import { telegramControlCommandUsage } from "./config-commands";
|
|
88
|
+
import {
|
|
89
|
+
isNativeControlDrainAvailable,
|
|
90
|
+
runIdentityControlSuccessPath,
|
|
91
|
+
type TerminalSendOutcome,
|
|
92
|
+
} from "./control-drain-lease";
|
|
85
93
|
import { imageAttachmentsFromMessage, notificationActionPayload, summaryFromMessage, truncate } from "./helpers";
|
|
94
|
+
import { createKindAwareReconciliation } from "./kind-aware-reconciliation";
|
|
86
95
|
import { assertNativeRuntimeCompatibility } from "./native-runtime-compatibility";
|
|
87
96
|
import { createPromptReconciliation, sanitizePromptFailure } from "./prompt-reconciliation";
|
|
97
|
+
import { createReconciliationStore } from "./reconciliation-store";
|
|
88
98
|
import { NotificationSessionController, type NotificationSessionRuntime } from "./session-control";
|
|
89
99
|
import {
|
|
90
100
|
ASK_SELECTED_ACK_CAPABILITY,
|
|
@@ -95,6 +105,17 @@ import {
|
|
|
95
105
|
unregisterNotificationRoot,
|
|
96
106
|
} from "./telegram-daemon";
|
|
97
107
|
|
|
108
|
+
export type {
|
|
109
|
+
IdentityControlSuccessPathInput,
|
|
110
|
+
IdentityControlTerminalPathInput,
|
|
111
|
+
TerminalSendOutcome,
|
|
112
|
+
} from "./control-drain-lease";
|
|
113
|
+
export {
|
|
114
|
+
isNativeControlDrainAvailable,
|
|
115
|
+
runIdentityControlSuccessPath,
|
|
116
|
+
runIdentityControlTerminalPath,
|
|
117
|
+
} from "./control-drain-lease";
|
|
118
|
+
|
|
98
119
|
// ===========================================================================
|
|
99
120
|
// Session lifecycle control protocol (TypeScript mirror of the Rust wire
|
|
100
121
|
// contract in `crates/gjc-sdk/src/lifecycle.rs`).
|
|
@@ -625,7 +646,31 @@ export class PresentationArbiter {
|
|
|
625
646
|
) {}
|
|
626
647
|
|
|
627
648
|
retain(presentation: UnattendedGatePresentation): void {
|
|
628
|
-
const
|
|
649
|
+
const existing = this.presentations.get(presentation.gateId);
|
|
650
|
+
if (
|
|
651
|
+
existing &&
|
|
652
|
+
this.active?.gateId === presentation.gateId &&
|
|
653
|
+
(existing.options.length !== presentation.options.length ||
|
|
654
|
+
existing.options.some((option, index) => presentation.options[index] !== option))
|
|
655
|
+
) {
|
|
656
|
+
const active = this.active;
|
|
657
|
+
let status: RetireStatus;
|
|
658
|
+
try {
|
|
659
|
+
status = parseRetireStatus(this.server.retireIfUnclaimed(active).status);
|
|
660
|
+
} catch (error) {
|
|
661
|
+
logger.warn(`interactive presentation replay retirement failed: ${String(error)}`);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (!isTerminalProof(status)) return;
|
|
665
|
+
this.routes.delete(active.actionId);
|
|
666
|
+
this.active = undefined;
|
|
667
|
+
}
|
|
668
|
+
const alreadyPresent = existing !== undefined;
|
|
669
|
+
if (existing?.multi && presentation.multi) {
|
|
670
|
+
presentation.selectedOptions = existing.selectedOptions.filter(option =>
|
|
671
|
+
presentation.options.includes(option),
|
|
672
|
+
);
|
|
673
|
+
}
|
|
629
674
|
if (!alreadyPresent) this.queue.push(presentation.gateId);
|
|
630
675
|
this.presentations.set(presentation.gateId, presentation);
|
|
631
676
|
// A fresh durable replay is explicit production recovery after transient N-API exhaustion.
|
|
@@ -754,6 +799,13 @@ export class PresentationArbiter {
|
|
|
754
799
|
? `(${presentation.selectedOptions.length} selected) ${presentation.question}`
|
|
755
800
|
: presentation.question,
|
|
756
801
|
options: presentation.options,
|
|
802
|
+
...(presentation.multi
|
|
803
|
+
? {
|
|
804
|
+
selectedOptionIndices: presentation.options.flatMap((option, index) =>
|
|
805
|
+
presentation.selectedOptions.includes(option) ? [index] : [],
|
|
806
|
+
),
|
|
807
|
+
}
|
|
808
|
+
: {}),
|
|
757
809
|
...(presentation.recommendedIndex === undefined
|
|
758
810
|
? {}
|
|
759
811
|
: { recommendedIndex: presentation.recommendedIndex }),
|
|
@@ -886,6 +938,8 @@ interface SessionRuntime {
|
|
|
886
938
|
disposeGateListener: () => void;
|
|
887
939
|
/** Whether notification-only delivery and answer resources are active. */
|
|
888
940
|
notificationsActive: boolean;
|
|
941
|
+
/** Rejects new SDK frames while a leased terminal response drains. */
|
|
942
|
+
inboundFenced: boolean;
|
|
889
943
|
/** Set as soon as terminal teardown is requested, before startup settles. */
|
|
890
944
|
stopping: boolean;
|
|
891
945
|
/** Recreates notification-only resources after `/notify on`. */
|
|
@@ -923,6 +977,19 @@ interface SessionRuntime {
|
|
|
923
977
|
pendingPromptCorrelations: Array<{ commandId: string; turnId: string }>;
|
|
924
978
|
/** Identity bound to the agent lifecycle currently in flight. */
|
|
925
979
|
activePromptCorrelation?: { commandId: string; turnId: string };
|
|
980
|
+
/** Binds the executing Agent run to a correlated prompt so cleanup targets only it. */
|
|
981
|
+
bindPromptExecutionHandle: (correlation: { commandId: string; turnId: string }, handle: string | undefined) => void;
|
|
982
|
+
/** Reads the durable non-terminal claim for a correlated prompt, if any. */
|
|
983
|
+
peekPromptPendingOutcome: (correlation: {
|
|
984
|
+
commandId: string;
|
|
985
|
+
turnId: string;
|
|
986
|
+
}) => SdkPromptTerminalOutcome | undefined;
|
|
987
|
+
/** Claims, fences, finalizes, and publishes exactly one normalized prompt terminal. */
|
|
988
|
+
terminalizePrompt: (
|
|
989
|
+
correlation: { commandId: string; turnId: string },
|
|
990
|
+
outcome: SdkPromptTerminalOutcome,
|
|
991
|
+
extra?: { finalText?: string },
|
|
992
|
+
) => Promise<void>;
|
|
926
993
|
/** Records a correlated prompt terminal boundary after agent unwind. */
|
|
927
994
|
/** Atomically claims a correlated prompt terminal boundary after agent unwind. */
|
|
928
995
|
recordPromptTerminal: (correlation: { commandId: string; turnId: string } | undefined) => boolean;
|
|
@@ -943,6 +1010,7 @@ interface SessionRuntime {
|
|
|
943
1010
|
commandId?: string;
|
|
944
1011
|
turnId?: string;
|
|
945
1012
|
finalText?: string;
|
|
1013
|
+
outcome?: SdkPromptTerminalOutcome;
|
|
946
1014
|
}
|
|
947
1015
|
| {
|
|
948
1016
|
type: "agent_failed";
|
|
@@ -950,6 +1018,7 @@ interface SessionRuntime {
|
|
|
950
1018
|
commandId: string;
|
|
951
1019
|
turnId: string;
|
|
952
1020
|
error: { code: string; message: string };
|
|
1021
|
+
outcome?: SdkPromptTerminalOutcome;
|
|
953
1022
|
},
|
|
954
1023
|
) => void;
|
|
955
1024
|
/** Publishes one canonical agent-wire event to the client that owns the active prompt. */
|
|
@@ -1113,6 +1182,7 @@ const defaultConfig: NotificationConfig = {
|
|
|
1113
1182
|
redact: false,
|
|
1114
1183
|
verbosity: "lean",
|
|
1115
1184
|
sessionScope: "all",
|
|
1185
|
+
sound: "all",
|
|
1116
1186
|
idleTimeoutMs: 60_000,
|
|
1117
1187
|
rich: { enabled: true },
|
|
1118
1188
|
richDraft: { enabled: false },
|
|
@@ -1122,8 +1192,17 @@ const defaultConfig: NotificationConfig = {
|
|
|
1122
1192
|
btw: { enabled: true },
|
|
1123
1193
|
};
|
|
1124
1194
|
|
|
1195
|
+
/**
|
|
1196
|
+
* Whether the notifications control channel is enabled.
|
|
1197
|
+
*
|
|
1198
|
+
* Trusted sources only: enabling it opens the session control/answer channel, so
|
|
1199
|
+
* a repository must not be able to turn it on. `$env` merges the caller's
|
|
1200
|
+
* `cwd/.env` into `process.env`; the sibling resolvers in `config.ts` and
|
|
1201
|
+
* `session-control.ts` already read an injected env record rather than the merged
|
|
1202
|
+
* view, and this direct read was the outlier.
|
|
1203
|
+
*/
|
|
1125
1204
|
export function notificationsEnabled(): boolean {
|
|
1126
|
-
return
|
|
1205
|
+
return $credentialEnv("GJC_NOTIFICATIONS") === "1" || Boolean($credentialEnv("GJC_NOTIFICATIONS_TOKEN"));
|
|
1127
1206
|
}
|
|
1128
1207
|
|
|
1129
1208
|
function streamIntervalMs(): number {
|
|
@@ -1596,6 +1675,80 @@ async function requestRecoveredSelectedAck(
|
|
|
1596
1675
|
}
|
|
1597
1676
|
}
|
|
1598
1677
|
|
|
1678
|
+
function createSdkUiAskAnswerSource(
|
|
1679
|
+
requestElicitation: (params: Record<string, unknown>, signal?: AbortSignal) => Promise<unknown>,
|
|
1680
|
+
): AskAnswerSource {
|
|
1681
|
+
const awaitAnswerRequest = async (
|
|
1682
|
+
request: AskAnswerRequest,
|
|
1683
|
+
signal?: AbortSignal,
|
|
1684
|
+
): Promise<AskAnswerSourceResult> => {
|
|
1685
|
+
if (signal?.aborted) return undefined;
|
|
1686
|
+
const choices = new Map<string, AskRemoteInteraction>();
|
|
1687
|
+
const oneOf =
|
|
1688
|
+
request.interaction === "selector"
|
|
1689
|
+
? [
|
|
1690
|
+
...request.options.map((label, index) => {
|
|
1691
|
+
const value = `option:${index}`;
|
|
1692
|
+
choices.set(value, { kind: "value", value: label });
|
|
1693
|
+
return { const: value, title: label };
|
|
1694
|
+
}),
|
|
1695
|
+
...request.controls
|
|
1696
|
+
.filter(control => control.enabled)
|
|
1697
|
+
.map(control => {
|
|
1698
|
+
const value = `control:${control.id}`;
|
|
1699
|
+
choices.set(value, { kind: "control", controlId: control.id });
|
|
1700
|
+
return { const: value, title: control.label };
|
|
1701
|
+
}),
|
|
1702
|
+
]
|
|
1703
|
+
: [];
|
|
1704
|
+
const response = await requestElicitation(
|
|
1705
|
+
{
|
|
1706
|
+
mode: "form",
|
|
1707
|
+
message: request.question,
|
|
1708
|
+
requestedSchema: {
|
|
1709
|
+
type: "object",
|
|
1710
|
+
properties: {
|
|
1711
|
+
value: { type: "string", ...(oneOf.length > 0 ? { oneOf } : {}) },
|
|
1712
|
+
},
|
|
1713
|
+
required: ["value"],
|
|
1714
|
+
},
|
|
1715
|
+
},
|
|
1716
|
+
signal,
|
|
1717
|
+
);
|
|
1718
|
+
if (signal?.aborted || !isRecord(response) || response.action !== "accept") return undefined;
|
|
1719
|
+
const value = isRecord(response.content) ? response.content.value : undefined;
|
|
1720
|
+
if (typeof value !== "string") return undefined;
|
|
1721
|
+
if (request.interaction !== "selector") return value;
|
|
1722
|
+
const interaction = choices.get(value);
|
|
1723
|
+
if (!interaction) return undefined;
|
|
1724
|
+
if (interaction.kind === "value") return interaction.value;
|
|
1725
|
+
let settled: Promise<AskSettlementResult> | undefined;
|
|
1726
|
+
return {
|
|
1727
|
+
source: "remote",
|
|
1728
|
+
interaction,
|
|
1729
|
+
settle(settlement) {
|
|
1730
|
+
if (!settled) {
|
|
1731
|
+
settled = Promise.resolve(
|
|
1732
|
+
settlement.kind === "commit"
|
|
1733
|
+
? { kind: "committed", ack: { status: "failed", reason: "unsupported" } }
|
|
1734
|
+
: settlement.kind === "invalid"
|
|
1735
|
+
? { kind: "invalid_closed" }
|
|
1736
|
+
: { kind: "resolved_without_commit" },
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
return settled;
|
|
1740
|
+
},
|
|
1741
|
+
};
|
|
1742
|
+
};
|
|
1743
|
+
return {
|
|
1744
|
+
async awaitAnswer(question, options, signal) {
|
|
1745
|
+
const answer = await awaitAnswerRequest({ question, options, interaction: "selector", controls: [] }, signal);
|
|
1746
|
+
if (!answer || typeof answer === "string") return answer;
|
|
1747
|
+
return answer.interaction.kind === "value" ? answer.interaction.value : undefined;
|
|
1748
|
+
},
|
|
1749
|
+
awaitAnswerRequest,
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1599
1752
|
/** Register the interactive `ask` answer source for a session (the ask tool
|
|
1600
1753
|
* races the local UI against a remote reply). Returns the deregister disposer. */
|
|
1601
1754
|
function registerInteractiveAnswerSource(
|
|
@@ -1807,6 +1960,9 @@ function sdkQuerySurface(
|
|
|
1807
1960
|
}),
|
|
1808
1961
|
configOverrides: ReadonlyMap<string, unknown> = new Map(),
|
|
1809
1962
|
promptStatusLookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown,
|
|
1963
|
+
skillStatusLookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown = () => ({
|
|
1964
|
+
status: "unknown",
|
|
1965
|
+
}),
|
|
1810
1966
|
): SessionSurface {
|
|
1811
1967
|
const metadata = () => ({
|
|
1812
1968
|
sessionId: id,
|
|
@@ -1923,6 +2079,7 @@ function sdkQuerySurface(
|
|
|
1923
2079
|
getCapabilities: () => ({
|
|
1924
2080
|
operations: [...installedOperations(ctx, "control"), ...installedOperations(ctx, "query")],
|
|
1925
2081
|
hostTools: getInstalledDefinitions("host_tools") !== undefined,
|
|
2082
|
+
promptTerminalOutcomeVersion: 1,
|
|
1926
2083
|
}),
|
|
1927
2084
|
getAuthProviders: () => [...new Set(ctx.modelRegistry.getAll().map(model => model.provider))],
|
|
1928
2085
|
getTools: () => {
|
|
@@ -1935,6 +2092,8 @@ function sdkQuerySurface(
|
|
|
1935
2092
|
getJobs: () => ctx.getJobs(),
|
|
1936
2093
|
getPromptStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
|
|
1937
2094
|
promptStatusLookup(selector),
|
|
2095
|
+
getSkillInvokeStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
|
|
2096
|
+
skillStatusLookup(selector),
|
|
1938
2097
|
installedQueries: installedOperations(ctx, "query"),
|
|
1939
2098
|
};
|
|
1940
2099
|
}
|
|
@@ -1962,16 +2121,41 @@ function sdkControlSurface(
|
|
|
1962
2121
|
requesterConnectionId?: string,
|
|
1963
2122
|
clientRef?: string,
|
|
1964
2123
|
trackReconciliation?: boolean,
|
|
1965
|
-
) => void = () => {},
|
|
2124
|
+
) => void | Promise<void> = () => {},
|
|
1966
2125
|
onPromptFailed: (correlation: { commandId: string; turnId: string }, error: unknown) => void = () => {},
|
|
2126
|
+
onPromptAcceptFailed: (correlation: { commandId: string; turnId: string }) => void = () => {},
|
|
1967
2127
|
acceptGateResolution: () => boolean,
|
|
1968
2128
|
trackGateResolution: <T>(resolution: Promise<T>) => Promise<T>,
|
|
1969
2129
|
admitPrompt: (clientRef?: string) => void,
|
|
1970
2130
|
releasePromptAdmission: (clientRef?: string) => void,
|
|
2131
|
+
awaitReconciliationReady: () => Promise<void> = async () => {},
|
|
1971
2132
|
settings?: Settings,
|
|
1972
2133
|
configOverrides: Map<string, unknown> = new Map(),
|
|
1973
2134
|
configRevision: { current: number } = { current: 0 },
|
|
1974
|
-
|
|
2135
|
+
abortOwnedPrompt: (
|
|
2136
|
+
connectionId: string | undefined,
|
|
2137
|
+
) => Promise<{ aborted: true; disposition: "cancelled" | "already_terminal" | "idle" }> = async () => ({
|
|
2138
|
+
aborted: true,
|
|
2139
|
+
disposition: "idle",
|
|
2140
|
+
}),
|
|
2141
|
+
skillRecon?: {
|
|
2142
|
+
admit: (clientRef?: string) => void;
|
|
2143
|
+
release: (clientRef?: string) => void;
|
|
2144
|
+
noteAccepted: (
|
|
2145
|
+
correlation: { commandId: string; turnId: string },
|
|
2146
|
+
clientRef?: string,
|
|
2147
|
+
extra?: { skillName?: string },
|
|
2148
|
+
) => Promise<void>;
|
|
2149
|
+
noteTransition: (
|
|
2150
|
+
correlation: { commandId: string; turnId: string } | undefined,
|
|
2151
|
+
frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
|
|
2152
|
+
) => Promise<void>;
|
|
2153
|
+
lookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown;
|
|
2154
|
+
},
|
|
2155
|
+
): ControlSurface & {
|
|
2156
|
+
cancelPendingPreflights(): void;
|
|
2157
|
+
cancelPendingPreflightsForConnection(connectionId: string): void;
|
|
2158
|
+
} {
|
|
1975
2159
|
const unavailable = (operation: string, reason: string) => () => {
|
|
1976
2160
|
throw Object.assign(new Error(`${operation} is unavailable: ${reason}`), { code: "unavailable" });
|
|
1977
2161
|
};
|
|
@@ -2025,9 +2209,15 @@ function sdkControlSurface(
|
|
|
2025
2209
|
return unavailable(operation, "no typed session seam is installed")();
|
|
2026
2210
|
return ctx.sdkControl(operation, input);
|
|
2027
2211
|
};
|
|
2028
|
-
const pendingPreflightCancellations = new
|
|
2212
|
+
const pendingPreflightCancellations = new Map<string, { connectionId?: string; cancel: () => void }>();
|
|
2213
|
+
const preflightKey = (connectionId: string | undefined, correlation: { commandId: string; turnId: string }) =>
|
|
2214
|
+
`${connectionId ?? ""}\0${correlation.commandId}\0${correlation.turnId}`;
|
|
2029
2215
|
const cancelPendingPreflights = () => {
|
|
2030
|
-
for (const cancel of pendingPreflightCancellations) cancel();
|
|
2216
|
+
for (const { cancel } of pendingPreflightCancellations.values()) cancel();
|
|
2217
|
+
};
|
|
2218
|
+
const cancelPendingPreflightsForConnection = (connectionId: string) => {
|
|
2219
|
+
for (const entry of pendingPreflightCancellations.values())
|
|
2220
|
+
if (entry.connectionId === connectionId) entry.cancel();
|
|
2031
2221
|
};
|
|
2032
2222
|
const isSessionBusy = () => isBusy() || ctx.isIdle?.() === false;
|
|
2033
2223
|
const awaitAbortReady = async () => {
|
|
@@ -2052,7 +2242,18 @@ function sdkControlSurface(
|
|
|
2052
2242
|
throw Object.assign(new Error("clientRef must be a non-empty string of at most 128 characters."), {
|
|
2053
2243
|
code: "invalid_input",
|
|
2054
2244
|
});
|
|
2055
|
-
if (trackReconciliation)
|
|
2245
|
+
if (trackReconciliation) {
|
|
2246
|
+
// Restart recovery must be committed before a tracked prompt reserves capacity,
|
|
2247
|
+
// otherwise it can admit against pre-hydration state or a lost clientRef.
|
|
2248
|
+
try {
|
|
2249
|
+
await awaitReconciliationReady();
|
|
2250
|
+
} catch {
|
|
2251
|
+
throw Object.assign(new Error("Prompt reconciliation state is unavailable; retry after restart."), {
|
|
2252
|
+
code: "unavailable",
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
admitPrompt(trimmedClientRef);
|
|
2256
|
+
}
|
|
2056
2257
|
try {
|
|
2057
2258
|
if (forceFresh && isSessionBusy()) {
|
|
2058
2259
|
throw Object.assign(
|
|
@@ -2100,13 +2301,32 @@ function sdkControlSurface(
|
|
|
2100
2301
|
status: "rejected",
|
|
2101
2302
|
error: Object.assign(new Error("Prompt preflight was cancelled before execution."), { code: "busy" }),
|
|
2102
2303
|
});
|
|
2103
|
-
pendingPreflightCancellations.
|
|
2104
|
-
|
|
2304
|
+
pendingPreflightCancellations.set(preflightKey(requesterConnectionId, correlation), {
|
|
2305
|
+
connectionId: requesterConnectionId,
|
|
2306
|
+
cancel: cancelPreflight,
|
|
2307
|
+
});
|
|
2308
|
+
const settleAccepted = async () => {
|
|
2105
2309
|
if (preflightSettled) return;
|
|
2106
2310
|
accepted = true;
|
|
2107
|
-
|
|
2311
|
+
try {
|
|
2312
|
+
await onPromptAccepted(correlation, requesterConnectionId, trimmedClientRef, trackReconciliation);
|
|
2313
|
+
} catch (error) {
|
|
2314
|
+
// Durable acceptance failed, so the prompt was never accepted: reject the
|
|
2315
|
+
// control preflight and rethrow so the awaiting session does not execute it.
|
|
2316
|
+
accepted = false;
|
|
2317
|
+
onPromptAcceptFailed(correlation);
|
|
2318
|
+
settlePreflight({ status: "rejected", error });
|
|
2319
|
+
throw error;
|
|
2320
|
+
}
|
|
2108
2321
|
settlePreflight({ status: "accepted" });
|
|
2109
2322
|
};
|
|
2323
|
+
// Durable fence preferred; keep legacy onPreflightAccepted for hosts/tests that only fire the sync hook.
|
|
2324
|
+
const onPreflightAcceptCommit = settleAccepted;
|
|
2325
|
+
const onPreflightAccepted = () => {
|
|
2326
|
+
// Legacy fire-and-forget hook: the rejection is already reported through the
|
|
2327
|
+
// preflight promise, so swallow it here instead of leaking an unhandled rejection.
|
|
2328
|
+
void settleAccepted().catch(() => {});
|
|
2329
|
+
};
|
|
2110
2330
|
// Do not acknowledge the prompt until AgentSession's async preflight
|
|
2111
2331
|
// succeeds. The terminal result records correlation before agent_start can fire.
|
|
2112
2332
|
let submission: Promise<void> | undefined;
|
|
@@ -2114,6 +2334,7 @@ function sdkControlSurface(
|
|
|
2114
2334
|
submission = Promise.resolve(
|
|
2115
2335
|
api.sendUserMessage(content, {
|
|
2116
2336
|
...(deliverAs ? { deliverAs } : !forceFresh && isBusy() ? { deliverAs: "steer" as const } : {}),
|
|
2337
|
+
onPreflightAcceptCommit,
|
|
2117
2338
|
onPreflightAccepted,
|
|
2118
2339
|
}),
|
|
2119
2340
|
);
|
|
@@ -2146,24 +2367,35 @@ function sdkControlSurface(
|
|
|
2146
2367
|
if (trackReconciliation) releasePromptAdmission(trimmedClientRef);
|
|
2147
2368
|
throw error;
|
|
2148
2369
|
} finally {
|
|
2149
|
-
pendingPreflightCancellations.delete(
|
|
2370
|
+
pendingPreflightCancellations.delete(preflightKey(requesterConnectionId, correlation));
|
|
2150
2371
|
}
|
|
2151
2372
|
};
|
|
2152
|
-
const surface: ControlSurface & {
|
|
2373
|
+
const surface: ControlSurface & {
|
|
2374
|
+
cancelPendingPreflights(): void;
|
|
2375
|
+
cancelPendingPreflightsForConnection(connectionId: string): void;
|
|
2376
|
+
} = {
|
|
2153
2377
|
prompt: (text, images, clientRef) =>
|
|
2154
2378
|
submitPrompt(text, images, false, undefined, true, controlRequesterContext.getStore(), clientRef, true),
|
|
2155
2379
|
steer: text => sendSteer(text),
|
|
2156
2380
|
followUp: text => submitPrompt(text, undefined, false, "followUp", false, controlRequesterContext.getStore()),
|
|
2157
|
-
abort: () => {
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2381
|
+
abort: async () => {
|
|
2382
|
+
const requesterConnectionId = controlRequesterContext.getStore();
|
|
2383
|
+
const pendingPreflight = [...pendingPreflightCancellations.values()].some(
|
|
2384
|
+
entry => entry.connectionId === requesterConnectionId,
|
|
2385
|
+
);
|
|
2386
|
+
if (pendingPreflight) {
|
|
2387
|
+
if (requesterConnectionId) cancelPendingPreflightsForConnection(requesterConnectionId);
|
|
2388
|
+
else cancelPendingPreflights();
|
|
2389
|
+
return { aborted: true, disposition: "preflight_cancelled" };
|
|
2390
|
+
}
|
|
2391
|
+
return await abortOwnedPrompt(requesterConnectionId);
|
|
2161
2392
|
},
|
|
2162
2393
|
abortAndPrompt: async text => {
|
|
2163
2394
|
await awaitAbortReady();
|
|
2164
2395
|
return await submitPrompt(text, undefined, true, undefined, false, controlRequesterContext.getStore());
|
|
2165
2396
|
},
|
|
2166
2397
|
cancelPendingPreflights,
|
|
2398
|
+
cancelPendingPreflightsForConnection,
|
|
2167
2399
|
answerAsk: (id, answer) => {
|
|
2168
2400
|
const pending = pendingInteractive.get(id);
|
|
2169
2401
|
if (!pending) throw Object.assign(new Error(`Ask ${id} was not found.`), { code: "resource_gone" });
|
|
@@ -2324,13 +2556,93 @@ function sdkControlSurface(
|
|
|
2324
2556
|
code: "terminal_uncertain",
|
|
2325
2557
|
});
|
|
2326
2558
|
},
|
|
2327
|
-
invokeSkill: (name, args) => {
|
|
2559
|
+
invokeSkill: async (name, args, clientRef) => {
|
|
2328
2560
|
if (!bindings.has("invokeSkill") || !ctx.invokeSkill)
|
|
2329
2561
|
return unavailable("skill.invoke", "no skill invocation seam is installed")();
|
|
2330
2562
|
|
|
2331
2563
|
if (typeof args !== "undefined" && typeof args !== "string")
|
|
2332
2564
|
throw Object.assign(new Error("skill.invoke args must be a string."), { code: "invalid_input" });
|
|
2333
|
-
|
|
2565
|
+
const trimmedClientRef =
|
|
2566
|
+
typeof clientRef === "string" ? clientRef.trim() : clientRef === undefined ? undefined : "";
|
|
2567
|
+
if (clientRef !== undefined && (!trimmedClientRef || trimmedClientRef.length > 128))
|
|
2568
|
+
throw Object.assign(new Error("clientRef must be a non-empty string of at most 128 characters."), {
|
|
2569
|
+
code: "invalid_input",
|
|
2570
|
+
});
|
|
2571
|
+
const commandId = crypto.randomUUID();
|
|
2572
|
+
const turnId = crypto.randomUUID();
|
|
2573
|
+
const correlation = { commandId, turnId };
|
|
2574
|
+
if (skillRecon) {
|
|
2575
|
+
try {
|
|
2576
|
+
await awaitReconciliationReady();
|
|
2577
|
+
} catch {
|
|
2578
|
+
throw Object.assign(new Error("Skill reconciliation state is unavailable; retry after restart."), {
|
|
2579
|
+
code: "unavailable",
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
skillRecon.admit(trimmedClientRef);
|
|
2583
|
+
}
|
|
2584
|
+
const { promise: acceptedP, resolve, reject } = Promise.withResolvers<Record<string, unknown>>();
|
|
2585
|
+
let phase: "pending" | "accepted" | "rejected" = "pending";
|
|
2586
|
+
const settleAccept = (value: Record<string, unknown>) => {
|
|
2587
|
+
if (phase !== "pending") return;
|
|
2588
|
+
phase = "accepted";
|
|
2589
|
+
resolve(value);
|
|
2590
|
+
};
|
|
2591
|
+
const settleReject = (error: unknown) => {
|
|
2592
|
+
if (phase !== "pending") return;
|
|
2593
|
+
phase = "rejected";
|
|
2594
|
+
reject(error);
|
|
2595
|
+
};
|
|
2596
|
+
let prepared: { name: string; path: string; lineCount?: number; cleanedArgs?: string } | undefined;
|
|
2597
|
+
const run = Promise.resolve(
|
|
2598
|
+
ctx.invokeSkill(name, args as string | undefined, {
|
|
2599
|
+
onSkillPrepared: meta => {
|
|
2600
|
+
prepared = meta;
|
|
2601
|
+
},
|
|
2602
|
+
onPreflightAcceptCommit: async () => {
|
|
2603
|
+
const meta = prepared ?? { name: String(name), path: "" };
|
|
2604
|
+
if (skillRecon)
|
|
2605
|
+
await skillRecon.noteAccepted(correlation, trimmedClientRef, { skillName: meta.name });
|
|
2606
|
+
settleAccept({
|
|
2607
|
+
accepted: true,
|
|
2608
|
+
commandId,
|
|
2609
|
+
turnId,
|
|
2610
|
+
name: meta.name,
|
|
2611
|
+
path: meta.path,
|
|
2612
|
+
...(meta.lineCount !== undefined ? { lineCount: meta.lineCount } : {}),
|
|
2613
|
+
...(meta.cleanedArgs !== undefined ? { args: meta.cleanedArgs } : {}),
|
|
2614
|
+
...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}),
|
|
2615
|
+
});
|
|
2616
|
+
},
|
|
2617
|
+
}),
|
|
2618
|
+
).then(
|
|
2619
|
+
result => {
|
|
2620
|
+
if (phase === "pending") {
|
|
2621
|
+
// Completed without fence (e.g. queue path) — still surface result.
|
|
2622
|
+
settleAccept({
|
|
2623
|
+
accepted: true,
|
|
2624
|
+
commandId,
|
|
2625
|
+
turnId,
|
|
2626
|
+
...(typeof result === "object" && result ? (result as object) : {}),
|
|
2627
|
+
...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}),
|
|
2628
|
+
});
|
|
2629
|
+
} else if (skillRecon) {
|
|
2630
|
+
void skillRecon.noteTransition(correlation, { type: "agent_end" });
|
|
2631
|
+
}
|
|
2632
|
+
return result;
|
|
2633
|
+
},
|
|
2634
|
+
error => {
|
|
2635
|
+
if (phase === "pending") {
|
|
2636
|
+
if (skillRecon) skillRecon.release(trimmedClientRef);
|
|
2637
|
+
settleReject(error);
|
|
2638
|
+
} else if (skillRecon) {
|
|
2639
|
+
void skillRecon.noteTransition(correlation, { type: "agent_failed", error });
|
|
2640
|
+
}
|
|
2641
|
+
throw error;
|
|
2642
|
+
},
|
|
2643
|
+
);
|
|
2644
|
+
void run.catch(() => {});
|
|
2645
|
+
return await acceptedP;
|
|
2334
2646
|
},
|
|
2335
2647
|
setPlanMode: async on => {
|
|
2336
2648
|
if (!bindings.has("setPlanMode") || !ctx.setPlanMode)
|
|
@@ -2866,6 +3178,18 @@ export async function ensureConfiguredProviderDaemons(
|
|
|
2866
3178
|
if (isSlackConfigured(cfg)) await ensureProviderDaemon("slack", settings);
|
|
2867
3179
|
}
|
|
2868
3180
|
|
|
3181
|
+
/**
|
|
3182
|
+
* Classify whether a session identity event must await notification endpoint startup.
|
|
3183
|
+
* Only an interactive selector resume may defer this ancillary startup; all other
|
|
3184
|
+
* events, including branches and unknown origins, fail closed to awaiting it.
|
|
3185
|
+
*/
|
|
3186
|
+
export function shouldAwaitNotificationStartup(event: {
|
|
3187
|
+
type: "session_switch" | "session_branch";
|
|
3188
|
+
transition?: { origin: string };
|
|
3189
|
+
}): boolean {
|
|
3190
|
+
return event.type !== "session_switch" || event.transition?.origin !== INTERACTIVE_SELECTOR_RESUME_ORIGIN;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
2869
3193
|
export function createNotificationsExtension(
|
|
2870
3194
|
api: ExtensionAPI,
|
|
2871
3195
|
options: {
|
|
@@ -2905,10 +3229,15 @@ export function createNotificationsExtension(
|
|
|
2905
3229
|
const cleanupRetries = new Map<string, SessionRuntime>();
|
|
2906
3230
|
const sessionStartPromises = new Map<string, Promise<SessionStartResult>>();
|
|
2907
3231
|
const branchStartupTasks = new Set<Promise<void>>();
|
|
3232
|
+
const sessionLifecycleTasks = new Set<Promise<void>>();
|
|
2908
3233
|
let activeRuntimeId: string | undefined;
|
|
2909
3234
|
let identityControlInFlight = false;
|
|
2910
3235
|
let deferredIdentityRotation:
|
|
2911
|
-
| {
|
|
3236
|
+
| {
|
|
3237
|
+
event: { previousSessionFile?: string; transition?: { origin: string } };
|
|
3238
|
+
ctx: ExtensionContext;
|
|
3239
|
+
awaitStartup: boolean;
|
|
3240
|
+
}
|
|
2912
3241
|
| undefined;
|
|
2913
3242
|
let extensionShuttingDown = false;
|
|
2914
3243
|
|
|
@@ -2969,6 +3298,7 @@ export function createNotificationsExtension(
|
|
|
2969
3298
|
const requestedRuntime = retryRuntime ?? activeRuntime;
|
|
2970
3299
|
if (expectedRuntime && requestedRuntime !== expectedRuntime) return false;
|
|
2971
3300
|
if (reason === "session" && requestedRuntime) {
|
|
3301
|
+
requestedRuntime.inboundFenced = true;
|
|
2972
3302
|
requestedRuntime.stopping = true;
|
|
2973
3303
|
requestedRuntime.abortEphemeralTurns();
|
|
2974
3304
|
}
|
|
@@ -3180,7 +3510,6 @@ export function createNotificationsExtension(
|
|
|
3180
3510
|
if (lifecycleRequired && !lifecycleAgentDir)
|
|
3181
3511
|
return failLifecycleStartup("failed", "Lifecycle SDK startup requires an agent directory.");
|
|
3182
3512
|
|
|
3183
|
-
const gateOptions = new Map<string, string[]>();
|
|
3184
3513
|
const pendingInteractive = new Map<string, PendingInteractiveAsk>();
|
|
3185
3514
|
const pendingPromptCorrelations: Array<{ commandId: string; turnId: string }> = [];
|
|
3186
3515
|
const tag = sessionTag(id);
|
|
@@ -3218,32 +3547,86 @@ export function createNotificationsExtension(
|
|
|
3218
3547
|
|
|
3219
3548
|
const revisions = new RevisionStore(id, Date.now, { storageDir: stateRoot });
|
|
3220
3549
|
let host: SessionSdkHost | undefined;
|
|
3550
|
+
let disposeUiAnswerSource: (() => void) | undefined;
|
|
3221
3551
|
const installProviderDefinitions = (capability: string, definitions: unknown) => {
|
|
3222
3552
|
validateProviderDefinitions(capability, definitions);
|
|
3223
|
-
if (capability
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3553
|
+
if (capability === "permission") {
|
|
3554
|
+
ctx.setSdkPermissionProvider?.(async (toolCall, permissionOptions, signal) => {
|
|
3555
|
+
const result = await host!.reverse.request(
|
|
3556
|
+
"permission",
|
|
3557
|
+
"request",
|
|
3558
|
+
{
|
|
3559
|
+
toolCall,
|
|
3560
|
+
options: permissionOptions,
|
|
3561
|
+
},
|
|
3562
|
+
signal,
|
|
3563
|
+
);
|
|
3564
|
+
if (!result || typeof result !== "object")
|
|
3565
|
+
throw new Error("permission provider returned an invalid response");
|
|
3566
|
+
const response = result as { outcome?: unknown; optionId?: unknown; kind?: unknown };
|
|
3567
|
+
if (response.outcome === "cancelled") return { outcome: "cancelled" };
|
|
3568
|
+
if (response.outcome === "selected" && typeof response.optionId === "string")
|
|
3569
|
+
return {
|
|
3570
|
+
outcome: "selected",
|
|
3571
|
+
optionId: response.optionId,
|
|
3572
|
+
...(typeof response.kind === "string"
|
|
3573
|
+
? { kind: response.kind as "allow_once" | "allow_always" | "reject_once" | "reject_always" }
|
|
3574
|
+
: {}),
|
|
3575
|
+
};
|
|
3231
3576
|
throw new Error("permission provider returned an invalid response");
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3577
|
+
});
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3580
|
+
if (capability === "ui") {
|
|
3581
|
+
disposeUiAnswerSource?.();
|
|
3582
|
+
disposeUiAnswerSource = registerAskAnswerSource(
|
|
3583
|
+
id,
|
|
3584
|
+
createSdkUiAskAnswerSource(
|
|
3585
|
+
async (params, signal) => await host!.reverse.request("ui", "ui.elicit", params, signal),
|
|
3586
|
+
),
|
|
3587
|
+
);
|
|
3588
|
+
return;
|
|
3589
|
+
}
|
|
3590
|
+
if (capability !== "fs") return;
|
|
3591
|
+
// Only advertise the methods the client actually declared; a read-only client
|
|
3592
|
+
// must keep using the local write path instead of failing against the bridge.
|
|
3593
|
+
const names = new Set(
|
|
3594
|
+
(Array.isArray(definitions) ? definitions : [])
|
|
3595
|
+
.map(definition =>
|
|
3596
|
+
definition && typeof definition === "object" ? (definition as { name?: unknown }).name : undefined,
|
|
3597
|
+
)
|
|
3598
|
+
.filter((name): name is string => typeof name === "string"),
|
|
3599
|
+
);
|
|
3600
|
+
const canRead = names.size === 0 || names.has("fs.readTextFile");
|
|
3601
|
+
const canWrite = names.size === 0 || names.has("fs.writeTextFile");
|
|
3602
|
+
const bridge: ClientBridge = {
|
|
3603
|
+
capabilities: { readTextFile: canRead, writeTextFile: canWrite },
|
|
3604
|
+
deferAgentInitiatedTurns: true,
|
|
3605
|
+
async readTextFile(params) {
|
|
3606
|
+
const result = await host!.reverse.request("fs", "fs.readTextFile", params);
|
|
3607
|
+
if (
|
|
3608
|
+
!result ||
|
|
3609
|
+
typeof result !== "object" ||
|
|
3610
|
+
typeof (result as { content?: unknown }).content !== "string"
|
|
3611
|
+
)
|
|
3612
|
+
throw new Error("fs provider returned an invalid read response");
|
|
3613
|
+
return (result as { content: string }).content;
|
|
3614
|
+
},
|
|
3615
|
+
async writeTextFile(params) {
|
|
3616
|
+
await host!.reverse.request("fs", "fs.writeTextFile", params);
|
|
3617
|
+
},
|
|
3618
|
+
};
|
|
3619
|
+
if (!canRead) delete bridge.readTextFile;
|
|
3620
|
+
if (!canWrite) delete bridge.writeTextFile;
|
|
3621
|
+
ctx.setSdkClientBridge?.(bridge);
|
|
3244
3622
|
};
|
|
3245
3623
|
const removeProviderDefinitions = (capability: string) => {
|
|
3246
3624
|
if (capability === "permission") ctx.setSdkPermissionProvider?.(undefined);
|
|
3625
|
+
if (capability === "fs") ctx.setSdkClientBridge?.(undefined);
|
|
3626
|
+
if (capability === "ui") {
|
|
3627
|
+
disposeUiAnswerSource?.();
|
|
3628
|
+
disposeUiAnswerSource = undefined;
|
|
3629
|
+
}
|
|
3247
3630
|
};
|
|
3248
3631
|
|
|
3249
3632
|
const hostCapCache = new Map<string, ReadonlySet<string>>();
|
|
@@ -3254,6 +3637,8 @@ export function createNotificationsExtension(
|
|
|
3254
3637
|
const PROMPT_SUBMISSION_TTL_MS = 5 * 60_000;
|
|
3255
3638
|
const PROMPT_TERMINAL_TOMBSTONE_CAPACITY = 256;
|
|
3256
3639
|
const PROMPT_TERMINAL_TOMBSTONE_TTL_MS = 15 * 60_000;
|
|
3640
|
+
// SDK-owned terminalization grace; injectable in tests, never a user setting.
|
|
3641
|
+
const PROMPT_TERMINALIZATION_GRACE_MS = 10_000;
|
|
3257
3642
|
const promptSubmissionKey = (correlation: { commandId: string; turnId: string }) =>
|
|
3258
3643
|
`${correlation.commandId}:${correlation.turnId}`;
|
|
3259
3644
|
type PromptLifecycleFrame =
|
|
@@ -3263,6 +3648,7 @@ export function createNotificationsExtension(
|
|
|
3263
3648
|
commandId?: string;
|
|
3264
3649
|
turnId?: string;
|
|
3265
3650
|
finalText?: string;
|
|
3651
|
+
outcome?: SdkPromptTerminalOutcome;
|
|
3266
3652
|
}
|
|
3267
3653
|
| {
|
|
3268
3654
|
type: "agent_failed";
|
|
@@ -3270,6 +3656,7 @@ export function createNotificationsExtension(
|
|
|
3270
3656
|
commandId: string;
|
|
3271
3657
|
turnId: string;
|
|
3272
3658
|
error: { code: string; message: string };
|
|
3659
|
+
outcome?: SdkPromptTerminalOutcome;
|
|
3273
3660
|
};
|
|
3274
3661
|
type PromptSubmission = {
|
|
3275
3662
|
acknowledged: boolean;
|
|
@@ -3279,50 +3666,95 @@ export function createNotificationsExtension(
|
|
|
3279
3666
|
error: unknown;
|
|
3280
3667
|
terminal: boolean;
|
|
3281
3668
|
retainCorrelation: boolean;
|
|
3669
|
+
/** Fatal/uncertain closure: transport-level only, never a semantic terminal. */
|
|
3670
|
+
fatal?: boolean;
|
|
3282
3671
|
createdAt: number;
|
|
3672
|
+
deadlineMs: number;
|
|
3673
|
+
deadlineTimer?: Parameters<typeof clearTimeout>[0];
|
|
3674
|
+
phase: "active" | "outcome_claimed" | "terminalizing" | "publication_closed" | "delivered";
|
|
3675
|
+
outcome?: SdkPromptTerminalOutcome;
|
|
3676
|
+
/** Agent-owned resource run captured at acceptance; cleanup targets only this handle. */
|
|
3677
|
+
executionHandle?: string;
|
|
3283
3678
|
bufferedFrames: Array<PromptLifecycleFrame | Record<string, unknown>>;
|
|
3284
3679
|
};
|
|
3285
3680
|
const promptSubmissions = new Map<string, PromptSubmission>();
|
|
3286
|
-
|
|
3681
|
+
/** Connections fenced by a fatal prompt closure; their later frames are refused. */
|
|
3682
|
+
const fencedConnections = new Set<string>();
|
|
3683
|
+
let cancelPreflightsForConnection: ((connectionId: string) => void) | undefined;
|
|
3684
|
+
const promptTerminalTombstones = new Map<string, { connectionId: string; expiresAt: number }>();
|
|
3287
3685
|
// Authoritative bounded reconciliation state for Q26 turn.prompt_status
|
|
3288
3686
|
// (contract documented in ./prompt-reconciliation and ../prompt-status).
|
|
3289
3687
|
// Active records never age into terminal; documented TTL/capacity
|
|
3290
3688
|
// eviction is the only removal, after which lookups report `unknown`.
|
|
3689
|
+
const sessionFile =
|
|
3690
|
+
typeof ctx.sessionManager?.getSessionFile === "function" ? ctx.sessionManager.getSessionFile() : null;
|
|
3691
|
+
const reconciliationSessionId =
|
|
3692
|
+
typeof ctx.sessionManager?.getSessionId === "function" ? ctx.sessionManager.getSessionId() : "";
|
|
3693
|
+
const durableStore =
|
|
3694
|
+
sessionFile && reconciliationSessionId
|
|
3695
|
+
? createReconciliationStore({ sessionFile, sessionId: String(reconciliationSessionId) })
|
|
3696
|
+
: null;
|
|
3697
|
+
const kindReconciliation = createKindAwareReconciliation({ store: durableStore });
|
|
3698
|
+
// Restart recovery must commit before any prompt is admitted; otherwise a new
|
|
3699
|
+
// admission can race the hydrated full-state replacement.
|
|
3700
|
+
const reconciliationReady: Promise<void> = durableStore
|
|
3701
|
+
? kindReconciliation.hydrateFromStore()
|
|
3702
|
+
: Promise.resolve();
|
|
3703
|
+
// Never let a hydration rejection become an unhandled rejection; tracked prompts
|
|
3704
|
+
// re-await the same promise and fail closed below.
|
|
3705
|
+
void reconciliationReady.catch(() => {});
|
|
3706
|
+
// Backward-compatible process-local prompt reconciler kept for unit-test isolation;
|
|
3707
|
+
// production path uses kindReconciliation for prompt+skill.
|
|
3291
3708
|
const reconciliation = createPromptReconciliation();
|
|
3292
|
-
const admitPromptSubmission =
|
|
3293
|
-
const notePromptReconciliationAccepted =
|
|
3294
|
-
|
|
3709
|
+
const admitPromptSubmission = (clientRef?: string) => kindReconciliation.admit("prompt", clientRef);
|
|
3710
|
+
const notePromptReconciliationAccepted = async (
|
|
3711
|
+
correlation: { commandId: string; turnId: string },
|
|
3712
|
+
clientRef?: string,
|
|
3713
|
+
) => {
|
|
3714
|
+
await kindReconciliation.noteAccepted("prompt", correlation, clientRef);
|
|
3715
|
+
};
|
|
3716
|
+
const lookupPromptStatus = (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
|
|
3717
|
+
kindReconciliation.lookup("prompt", selector);
|
|
3718
|
+
const releasePromptAdmission = (clientRef?: string) => kindReconciliation.releaseAdmission("prompt", clientRef);
|
|
3295
3719
|
const removePendingPromptCorrelation = (correlation: { commandId: string; turnId: string }) => {
|
|
3296
3720
|
const pendingIndex = pendingPromptCorrelations.findIndex(
|
|
3297
3721
|
candidate => candidate.commandId === correlation.commandId && candidate.turnId === correlation.turnId,
|
|
3298
3722
|
);
|
|
3299
3723
|
if (pendingIndex !== -1) pendingPromptCorrelations.splice(pendingIndex, 1);
|
|
3300
3724
|
};
|
|
3301
|
-
const addTerminalTombstone = (key: string, now = Date.now()) => {
|
|
3725
|
+
const addTerminalTombstone = (key: string, connectionId: string, now = Date.now()) => {
|
|
3302
3726
|
promptTerminalTombstones.delete(key);
|
|
3303
|
-
promptTerminalTombstones.set(key, now + PROMPT_TERMINAL_TOMBSTONE_TTL_MS);
|
|
3727
|
+
promptTerminalTombstones.set(key, { connectionId, expiresAt: now + PROMPT_TERMINAL_TOMBSTONE_TTL_MS });
|
|
3304
3728
|
while (promptTerminalTombstones.size > PROMPT_TERMINAL_TOMBSTONE_CAPACITY)
|
|
3305
3729
|
promptTerminalTombstones.delete(promptTerminalTombstones.keys().next().value!);
|
|
3306
3730
|
};
|
|
3307
3731
|
const finalizePrompt = (key: string, correlation: { commandId: string; turnId: string }) => {
|
|
3732
|
+
const submission = promptSubmissions.get(key);
|
|
3733
|
+
if (submission?.deadlineTimer) clearTimeout(submission.deadlineTimer);
|
|
3308
3734
|
promptSubmissions.delete(key);
|
|
3309
3735
|
removePendingPromptCorrelation(correlation);
|
|
3310
|
-
addTerminalTombstone(key);
|
|
3736
|
+
if (submission) addTerminalTombstone(key, submission.connectionId);
|
|
3311
3737
|
};
|
|
3312
3738
|
const expirePromptDelivery = (key: string, submission: PromptSubmission) => {
|
|
3739
|
+
if (!submission.terminal) return;
|
|
3740
|
+
if (submission.deadlineTimer) clearTimeout(submission.deadlineTimer);
|
|
3313
3741
|
promptSubmissions.delete(key);
|
|
3314
|
-
|
|
3742
|
+
// A fatal closure is transport-level, not a committed semantic terminal: the
|
|
3743
|
+
// durable record stays authoritative, so it must never leave a tombstone.
|
|
3744
|
+
if (submission.fatal) return;
|
|
3315
3745
|
const [commandId, turnId] = key.split(":", 2);
|
|
3316
3746
|
if (!commandId || !turnId) return;
|
|
3317
3747
|
removePendingPromptCorrelation({ commandId, turnId });
|
|
3318
|
-
addTerminalTombstone(key);
|
|
3748
|
+
addTerminalTombstone(key, submission.connectionId);
|
|
3319
3749
|
};
|
|
3320
3750
|
const cleanupPromptRecords = (now = Date.now()) => {
|
|
3751
|
+
kindReconciliation.cleanup();
|
|
3321
3752
|
reconciliation.cleanup();
|
|
3322
|
-
for (const [key,
|
|
3323
|
-
if (expiresAt <= now) promptTerminalTombstones.delete(key);
|
|
3753
|
+
for (const [key, tombstone] of promptTerminalTombstones)
|
|
3754
|
+
if (tombstone.expiresAt <= now) promptTerminalTombstones.delete(key);
|
|
3324
3755
|
for (const [key, submission] of promptSubmissions)
|
|
3325
|
-
if (submission.createdAt + PROMPT_SUBMISSION_TTL_MS <= now)
|
|
3756
|
+
if (submission.terminal && submission.createdAt + PROMPT_SUBMISSION_TTL_MS <= now)
|
|
3757
|
+
expirePromptDelivery(key, submission);
|
|
3326
3758
|
};
|
|
3327
3759
|
const abandonPrompt = (submission: PromptSubmission) => {
|
|
3328
3760
|
submission.abandoned = true;
|
|
@@ -3356,7 +3788,10 @@ export function createNotificationsExtension(
|
|
|
3356
3788
|
logger.warn(`sdk: correlated lifecycle delivery failed: ${String(error)}`);
|
|
3357
3789
|
abandonPrompt(submission);
|
|
3358
3790
|
}
|
|
3359
|
-
if (submission.terminal)
|
|
3791
|
+
if (submission.terminal) {
|
|
3792
|
+
submission.phase = "delivered";
|
|
3793
|
+
finalizePrompt(key, correlation);
|
|
3794
|
+
}
|
|
3360
3795
|
};
|
|
3361
3796
|
const emitPromptEvent = (event: AgentSessionEvent) => {
|
|
3362
3797
|
if (!runtime?.activePromptCorrelation) return;
|
|
@@ -3392,11 +3827,12 @@ export function createNotificationsExtension(
|
|
|
3392
3827
|
}
|
|
3393
3828
|
}
|
|
3394
3829
|
if (submission.terminal) {
|
|
3830
|
+
submission.phase = "delivered";
|
|
3395
3831
|
const [commandId, turnId] = key.split(":", 2);
|
|
3396
3832
|
if (commandId && turnId) finalizePrompt(key, { commandId, turnId });
|
|
3397
3833
|
}
|
|
3398
3834
|
};
|
|
3399
|
-
const recordPromptAccepted = (
|
|
3835
|
+
const recordPromptAccepted = async (
|
|
3400
3836
|
correlation: { commandId: string; turnId: string },
|
|
3401
3837
|
requesterConnectionId?: string,
|
|
3402
3838
|
clientRef?: string,
|
|
@@ -3405,18 +3841,23 @@ export function createNotificationsExtension(
|
|
|
3405
3841
|
if (!requesterConnectionId) {
|
|
3406
3842
|
// No delivery owner: tracked prompts cannot be reconciled. Release
|
|
3407
3843
|
// their admission reservation instead of leaking the active slot.
|
|
3408
|
-
if (trackReconciliation)
|
|
3844
|
+
if (trackReconciliation) releasePromptAdmission(clientRef);
|
|
3409
3845
|
return;
|
|
3410
3846
|
}
|
|
3411
|
-
|
|
3847
|
+
// Register delivery ownership synchronously before any await so post-accept
|
|
3848
|
+
// failures that race the durable write still emit correlated terminals.
|
|
3412
3849
|
cleanupPromptRecords();
|
|
3413
3850
|
while (promptSubmissions.size >= PROMPT_SUBMISSION_CAPACITY) {
|
|
3414
|
-
const
|
|
3415
|
-
if (!
|
|
3416
|
-
|
|
3851
|
+
const oldestTerminal = [...promptSubmissions.entries()].find(([, submission]) => submission.terminal);
|
|
3852
|
+
if (!oldestTerminal)
|
|
3853
|
+
throw Object.assign(
|
|
3854
|
+
new Error("Too many active prompt submissions; reconcile or await terminal state."),
|
|
3855
|
+
{ code: "reconciliation_capacity" },
|
|
3856
|
+
);
|
|
3857
|
+
expirePromptDelivery(oldestTerminal[0], oldestTerminal[1]);
|
|
3417
3858
|
}
|
|
3418
3859
|
pendingPromptCorrelations.push(correlation);
|
|
3419
|
-
|
|
3860
|
+
const submission: PromptSubmission = {
|
|
3420
3861
|
acknowledged: false,
|
|
3421
3862
|
connectionId: requesterConnectionId,
|
|
3422
3863
|
abandoned: false,
|
|
@@ -3425,30 +3866,217 @@ export function createNotificationsExtension(
|
|
|
3425
3866
|
terminal: false,
|
|
3426
3867
|
retainCorrelation: trackReconciliation,
|
|
3427
3868
|
createdAt: Date.now(),
|
|
3869
|
+
deadlineMs: settings?.get("sdk.promptDeadlineMs") ?? 1_800_000,
|
|
3870
|
+
phase: "active",
|
|
3871
|
+
// Bound to the Agent run at `agent_start`; acceptance precedes execution.
|
|
3872
|
+
executionHandle: undefined,
|
|
3428
3873
|
bufferedFrames: [],
|
|
3429
|
-
}
|
|
3874
|
+
};
|
|
3875
|
+
promptSubmissions.set(promptSubmissionKey(correlation), submission);
|
|
3876
|
+
if (trackReconciliation) await notePromptReconciliationAccepted(correlation, clientRef);
|
|
3877
|
+
submission.deadlineTimer = setTimeout(() => {
|
|
3878
|
+
void terminalizePrompt(
|
|
3879
|
+
correlation,
|
|
3880
|
+
{
|
|
3881
|
+
kind: "failed",
|
|
3882
|
+
code: "prompt_deadline_exceeded",
|
|
3883
|
+
message: "Prompt deadline exceeded.",
|
|
3884
|
+
provenance: "deadline",
|
|
3885
|
+
},
|
|
3886
|
+
{ fence: true },
|
|
3887
|
+
);
|
|
3888
|
+
}, submission.deadlineMs);
|
|
3889
|
+
};
|
|
3890
|
+
/** Roll back process-local registration when durable acceptance failed. */
|
|
3891
|
+
const discardPromptAcceptance = (correlation: { commandId: string; turnId: string }) => {
|
|
3892
|
+
promptSubmissions.delete(promptSubmissionKey(correlation));
|
|
3893
|
+
removePendingPromptCorrelation(correlation);
|
|
3430
3894
|
};
|
|
3431
3895
|
const recordPromptTerminal = (correlation: { commandId: string; turnId: string } | undefined) => {
|
|
3432
3896
|
if (!correlation) return false;
|
|
3433
|
-
|
|
3434
|
-
const key = promptSubmissionKey(correlation);
|
|
3435
|
-
if (promptTerminalTombstones.has(key)) return false;
|
|
3436
|
-
const submission = promptSubmissions.get(key);
|
|
3897
|
+
const submission = promptSubmissions.get(promptSubmissionKey(correlation));
|
|
3437
3898
|
if (!submission || submission.terminal) return false;
|
|
3438
3899
|
submission.terminal = true;
|
|
3900
|
+
submission.phase = "publication_closed";
|
|
3439
3901
|
return true;
|
|
3440
3902
|
};
|
|
3903
|
+
/**
|
|
3904
|
+
* Fail-closed teardown: the durable claim survives for restart recovery, but the
|
|
3905
|
+
* waiting client must still reject exactly once, so an infrastructure failure frame
|
|
3906
|
+
* is delivered before the connection is released.
|
|
3907
|
+
*/
|
|
3908
|
+
const failPromptClosed = (
|
|
3909
|
+
correlation: { commandId: string; turnId: string },
|
|
3910
|
+
submission: PromptSubmission,
|
|
3911
|
+
code: string,
|
|
3912
|
+
message: string,
|
|
3913
|
+
) => {
|
|
3914
|
+
if (submission.deadlineTimer) clearTimeout(submission.deadlineTimer);
|
|
3915
|
+
if (!submission.terminal) {
|
|
3916
|
+
submission.terminal = true;
|
|
3917
|
+
submission.fatal = true;
|
|
3918
|
+
submission.phase = "publication_closed";
|
|
3919
|
+
// Deliver the transport failure directly to the accepted requester instead of
|
|
3920
|
+
// `emitPromptLifecycle`: this must never enter the resumable semantic event
|
|
3921
|
+
// ring, finalize reconciliation, or tombstone the correlation, because the
|
|
3922
|
+
// durable claim stays authoritative for Q26 and restart recovery.
|
|
3923
|
+
// Delivered regardless of acknowledgement: a pre-ack fatal failure must still
|
|
3924
|
+
// settle the waiting request instead of leaving it pending forever.
|
|
3925
|
+
if (runtime)
|
|
3926
|
+
try {
|
|
3927
|
+
runtime.server.sendTo(
|
|
3928
|
+
submission.connectionId,
|
|
3929
|
+
JSON.stringify({
|
|
3930
|
+
type: "agent_failed",
|
|
3931
|
+
sessionId: runtime.id,
|
|
3932
|
+
...correlation,
|
|
3933
|
+
error: { code, message },
|
|
3934
|
+
}),
|
|
3935
|
+
);
|
|
3936
|
+
} catch (error) {
|
|
3937
|
+
logger.warn(`sdk: fatal prompt closure delivery failed: ${String(error)}`);
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
// Fence the endpoint. The native transport exposes no close primitive, so the
|
|
3941
|
+
// connection is marked failed (every later inbound frame from it is refused),
|
|
3942
|
+
// its reverse leases are released, and its deliveries are abandoned. The durable
|
|
3943
|
+
// record stays active for Q26 and restart recovery.
|
|
3944
|
+
cancelPreflightsForConnection?.(submission.connectionId);
|
|
3945
|
+
fencedConnections.add(submission.connectionId);
|
|
3946
|
+
host?.handleDisconnect(submission.connectionId);
|
|
3947
|
+
for (const [key, owned] of promptSubmissions)
|
|
3948
|
+
if (owned.connectionId === submission.connectionId) {
|
|
3949
|
+
abandonPrompt(owned);
|
|
3950
|
+
const [commandId, turnId] = key.split(":", 2);
|
|
3951
|
+
if (commandId && turnId) removePendingPromptCorrelation({ commandId, turnId });
|
|
3952
|
+
}
|
|
3953
|
+
if (
|
|
3954
|
+
runtime?.activePromptCorrelation?.commandId === correlation.commandId &&
|
|
3955
|
+
runtime.activePromptCorrelation.turnId === correlation.turnId
|
|
3956
|
+
)
|
|
3957
|
+
runtime.activePromptCorrelation = undefined;
|
|
3958
|
+
};
|
|
3959
|
+
const bindPromptExecutionHandle = (
|
|
3960
|
+
correlation: { commandId: string; turnId: string },
|
|
3961
|
+
handle: string | undefined,
|
|
3962
|
+
) => {
|
|
3963
|
+
const submission = promptSubmissions.get(promptSubmissionKey(correlation));
|
|
3964
|
+
if (submission) submission.executionHandle = handle;
|
|
3965
|
+
};
|
|
3966
|
+
const terminalizePrompt = async (
|
|
3967
|
+
correlation: { commandId: string; turnId: string },
|
|
3968
|
+
requestedOutcome: SdkPromptTerminalOutcome,
|
|
3969
|
+
// Cleanup-initiated claims (cancel, deadline, owner disconnect) must abort the
|
|
3970
|
+
// run and prove settlement. A natural `agent_end`/`agent_failed` already unwound,
|
|
3971
|
+
// so aborting there would cancel the next turn instead of fencing this one.
|
|
3972
|
+
options: { fence?: boolean } = {},
|
|
3973
|
+
extra?: { finalText?: string; error?: { code: string; message: string } },
|
|
3974
|
+
) => {
|
|
3975
|
+
const submission = promptSubmissions.get(promptSubmissionKey(correlation));
|
|
3976
|
+
if (!submission || submission.terminal || submission.phase !== "active") return;
|
|
3977
|
+
submission.phase = "outcome_claimed";
|
|
3978
|
+
let winner: SdkPromptTerminalOutcome;
|
|
3979
|
+
try {
|
|
3980
|
+
winner = await kindReconciliation.claimPendingOutcome(correlation, requestedOutcome);
|
|
3981
|
+
} catch (error) {
|
|
3982
|
+
// The claim is the durability boundary: without it nothing may be published
|
|
3983
|
+
// and the endpoint must fail closed so the client rejects exactly once. The
|
|
3984
|
+
// last durable record stays active for restart recovery.
|
|
3985
|
+
logger.warn(`sdk: prompt claim persistence failed: ${String(error)}`);
|
|
3986
|
+
failPromptClosed(correlation, submission, "terminal_uncertain", "Prompt reconciliation is unavailable.");
|
|
3987
|
+
return;
|
|
3988
|
+
}
|
|
3989
|
+
submission.outcome = winner;
|
|
3990
|
+
submission.phase = "terminalizing";
|
|
3991
|
+
if (options.fence) {
|
|
3992
|
+
const seam = ctx as typeof ctx & {
|
|
3993
|
+
abortPromptAndWait?: (
|
|
3994
|
+
handle: string,
|
|
3995
|
+
options: { graceMs: number },
|
|
3996
|
+
) => Promise<{ status: "settled" } | { status: "unfenced" }>;
|
|
3997
|
+
};
|
|
3998
|
+
// Only the handle captured for this correlation may be fenced; a later run
|
|
3999
|
+
// must never be aborted by an older prompt's cleanup.
|
|
4000
|
+
if (typeof seam.abortPromptAndWait !== "function" || !submission.executionHandle) {
|
|
4001
|
+
failPromptClosed(
|
|
4002
|
+
correlation,
|
|
4003
|
+
submission,
|
|
4004
|
+
"terminal_uncertain",
|
|
4005
|
+
"Prompt resources could not be fenced with an exact run handle.",
|
|
4006
|
+
);
|
|
4007
|
+
return;
|
|
4008
|
+
}
|
|
4009
|
+
let proof: { status: "settled" } | { status: "unfenced" };
|
|
4010
|
+
try {
|
|
4011
|
+
proof = await seam.abortPromptAndWait(submission.executionHandle, {
|
|
4012
|
+
graceMs: PROMPT_TERMINALIZATION_GRACE_MS,
|
|
4013
|
+
});
|
|
4014
|
+
} catch (error) {
|
|
4015
|
+
logger.warn(`sdk: prompt resource fencing failed: ${String(error)}`);
|
|
4016
|
+
failPromptClosed(
|
|
4017
|
+
correlation,
|
|
4018
|
+
submission,
|
|
4019
|
+
"terminal_uncertain",
|
|
4020
|
+
"Prompt resources could not be settled before terminalization.",
|
|
4021
|
+
);
|
|
4022
|
+
return;
|
|
4023
|
+
}
|
|
4024
|
+
if (proof?.status !== "settled") {
|
|
4025
|
+
// No settlement proof: never publish a normal terminal. The durable
|
|
4026
|
+
// pending claim stays active so restart recovery finalizes it.
|
|
4027
|
+
failPromptClosed(
|
|
4028
|
+
correlation,
|
|
4029
|
+
submission,
|
|
4030
|
+
"terminal_uncertain",
|
|
4031
|
+
"Prompt resources did not settle before the terminalization grace expired.",
|
|
4032
|
+
);
|
|
4033
|
+
return;
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
try {
|
|
4037
|
+
await kindReconciliation.finalizePromptOutcome(correlation, winner, extra?.error);
|
|
4038
|
+
} catch (error) {
|
|
4039
|
+
// The durable pending claim survives; publishing an unpersisted terminal
|
|
4040
|
+
// would contradict it, so fail the endpoint closed instead.
|
|
4041
|
+
logger.warn(`sdk: prompt terminal persistence failed: ${String(error)}`);
|
|
4042
|
+
failPromptClosed(correlation, submission, "terminal_uncertain", "Prompt reconciliation is unavailable.");
|
|
4043
|
+
return;
|
|
4044
|
+
}
|
|
4045
|
+
if (submission.deadlineTimer) clearTimeout(submission.deadlineTimer);
|
|
4046
|
+
if (!recordPromptTerminal(correlation) || !runtime) return;
|
|
4047
|
+
if (winner.kind === "failed") {
|
|
4048
|
+
emitPromptLifecycle(correlation, {
|
|
4049
|
+
type: "agent_failed",
|
|
4050
|
+
sessionId: runtime.id,
|
|
4051
|
+
...correlation,
|
|
4052
|
+
error: extra?.error ?? { code: winner.code, message: winner.message },
|
|
4053
|
+
outcome: winner,
|
|
4054
|
+
});
|
|
4055
|
+
} else {
|
|
4056
|
+
emitPromptLifecycle(correlation, {
|
|
4057
|
+
type: "agent_end",
|
|
4058
|
+
sessionId: runtime.id,
|
|
4059
|
+
...correlation,
|
|
4060
|
+
...(extra?.finalText ? { finalText: extra.finalText } : {}),
|
|
4061
|
+
outcome: winner,
|
|
4062
|
+
});
|
|
4063
|
+
}
|
|
4064
|
+
};
|
|
3441
4065
|
const emitPromptFailure = (correlation: { commandId: string; turnId: string }, error: unknown) => {
|
|
3442
4066
|
const sanitized = sanitizePromptFailure(error);
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
4067
|
+
void terminalizePrompt(
|
|
4068
|
+
correlation,
|
|
4069
|
+
{
|
|
4070
|
+
kind: "failed",
|
|
4071
|
+
code: "prompt_failed",
|
|
4072
|
+
message: sanitized.message,
|
|
4073
|
+
provenance: "agent_failed",
|
|
4074
|
+
},
|
|
4075
|
+
{},
|
|
4076
|
+
// The normalized outcome is the ACP contract; the wire error keeps the
|
|
4077
|
+
// existing sanitized discriminator so SDK clients keep branching on it.
|
|
4078
|
+
{ error: sanitized },
|
|
4079
|
+
);
|
|
3452
4080
|
};
|
|
3453
4081
|
const recordPromptFailure = (correlation: { commandId: string; turnId: string }, error: unknown) => {
|
|
3454
4082
|
const submission = promptSubmissions.get(promptSubmissionKey(correlation));
|
|
@@ -3491,6 +4119,7 @@ export function createNotificationsExtension(
|
|
|
3491
4119
|
},
|
|
3492
4120
|
configOverrides,
|
|
3493
4121
|
lookupPromptStatus,
|
|
4122
|
+
selector => kindReconciliation.lookup("skill", selector),
|
|
3494
4123
|
),
|
|
3495
4124
|
id,
|
|
3496
4125
|
revisions,
|
|
@@ -3504,14 +4133,39 @@ export function createNotificationsExtension(
|
|
|
3504
4133
|
() => runtime?.busy === true || pendingPromptCorrelations.length > 0,
|
|
3505
4134
|
recordPromptAccepted,
|
|
3506
4135
|
recordPromptFailure,
|
|
4136
|
+
discardPromptAcceptance,
|
|
3507
4137
|
() => runtime?.stopping !== true,
|
|
3508
4138
|
trackGateResolution,
|
|
3509
4139
|
admitPromptSubmission,
|
|
3510
|
-
|
|
4140
|
+
releasePromptAdmission,
|
|
4141
|
+
() => reconciliationReady,
|
|
3511
4142
|
settings,
|
|
3512
4143
|
configOverrides,
|
|
3513
4144
|
configRevision,
|
|
4145
|
+
async connectionId => {
|
|
4146
|
+
const active = [...promptSubmissions.entries()].find(
|
|
4147
|
+
([, submission]) => submission.connectionId === connectionId && !submission.terminal,
|
|
4148
|
+
);
|
|
4149
|
+
if (!active) return { aborted: true, disposition: "idle" as const };
|
|
4150
|
+
const [commandId, turnId] = active[0].split(":", 2);
|
|
4151
|
+
if (commandId && turnId)
|
|
4152
|
+
await terminalizePrompt(
|
|
4153
|
+
{ commandId, turnId },
|
|
4154
|
+
{ kind: "stopped", reason: "cancelled", provenance: "client_cancel" },
|
|
4155
|
+
{ fence: true },
|
|
4156
|
+
);
|
|
4157
|
+
return { aborted: true, disposition: "cancelled" as const };
|
|
4158
|
+
},
|
|
4159
|
+
{
|
|
4160
|
+
admit: (clientRef?: string) => kindReconciliation.admit("skill", clientRef),
|
|
4161
|
+
release: (clientRef?: string) => kindReconciliation.releaseAdmission("skill", clientRef),
|
|
4162
|
+
noteAccepted: (correlation, clientRef, extra) =>
|
|
4163
|
+
kindReconciliation.noteAccepted("skill", correlation, clientRef, extra),
|
|
4164
|
+
noteTransition: (correlation, frame) => kindReconciliation.noteTransition("skill", correlation, frame),
|
|
4165
|
+
lookup: selector => kindReconciliation.lookup("skill", selector),
|
|
4166
|
+
},
|
|
3514
4167
|
);
|
|
4168
|
+
cancelPreflightsForConnection = controlSurface.cancelPendingPreflightsForConnection;
|
|
3515
4169
|
const abandonPromptResponse = (connectionId: string, frame: Record<string, unknown>) => {
|
|
3516
4170
|
if (
|
|
3517
4171
|
frame.type !== "control_response" ||
|
|
@@ -3576,6 +4230,75 @@ export function createNotificationsExtension(
|
|
|
3576
4230
|
};
|
|
3577
4231
|
},
|
|
3578
4232
|
onRequest: options.onSdkRequest,
|
|
4233
|
+
beforeControlResponse: async (_connectionId, request, response, sendTerminal) => {
|
|
4234
|
+
if (typeof request.operation !== "string" || !identityControlOperations.has(request.operation)) return;
|
|
4235
|
+
const pending = deferredIdentityRotation;
|
|
4236
|
+
deferredIdentityRotation = undefined;
|
|
4237
|
+
identityControlInFlight = false;
|
|
4238
|
+
if (response.ok !== true || !pending) return;
|
|
4239
|
+
|
|
4240
|
+
const predecessorId = activeRuntimeId ?? sessionIdFromFile(pending.event.previousSessionFile);
|
|
4241
|
+
if (!predecessorId) throw new Error("notifications: identity control predecessor is unknown.");
|
|
4242
|
+
let terminalAttempted = false;
|
|
4243
|
+
let terminalOutcome: TerminalSendOutcome | undefined;
|
|
4244
|
+
try {
|
|
4245
|
+
terminalOutcome = await runIdentityControlSuccessPath({
|
|
4246
|
+
fence: () => {
|
|
4247
|
+
const predecessor = runtimes.get(predecessorId);
|
|
4248
|
+
if (!predecessor || predecessor.stopping || predecessor.serverStopped)
|
|
4249
|
+
throw new Error(`notifications: predecessor runtime ${predecessorId} cannot be fenced.`);
|
|
4250
|
+
predecessor.inboundFenced = true;
|
|
4251
|
+
},
|
|
4252
|
+
ensurePredecessorSendCapable: () => {
|
|
4253
|
+
const predecessor = runtimes.get(predecessorId);
|
|
4254
|
+
if (!predecessor || predecessor.stopping || predecessor.serverStopped)
|
|
4255
|
+
throw new Error(`notifications: predecessor runtime ${predecessorId} is not send-capable.`);
|
|
4256
|
+
},
|
|
4257
|
+
startSuccessor: async () => {
|
|
4258
|
+
const successorId = sessionId(pending.ctx);
|
|
4259
|
+
try {
|
|
4260
|
+
await rotateSessionAuthority(pending.event, pending.ctx, true, {
|
|
4261
|
+
deferPredecessorStop: true,
|
|
4262
|
+
});
|
|
4263
|
+
const successor = runtimes.get(successorId);
|
|
4264
|
+
if (!successor?.host.started || activeRuntimeId !== successorId)
|
|
4265
|
+
throw new Error(`notifications: successor runtime ${successorId} was not ready.`);
|
|
4266
|
+
} catch (error) {
|
|
4267
|
+
(response as Record<string, unknown>).ok = false;
|
|
4268
|
+
delete (response as Record<string, unknown>).result;
|
|
4269
|
+
(response as Record<string, unknown>).error = {
|
|
4270
|
+
code: "unavailable",
|
|
4271
|
+
message: error instanceof Error ? error.message : "Successor session was not ready.",
|
|
4272
|
+
};
|
|
4273
|
+
}
|
|
4274
|
+
},
|
|
4275
|
+
sendTerminal: async () => {
|
|
4276
|
+
terminalAttempted = true;
|
|
4277
|
+
try {
|
|
4278
|
+
await sendTerminal();
|
|
4279
|
+
return "written";
|
|
4280
|
+
} catch {
|
|
4281
|
+
return "write_failed";
|
|
4282
|
+
}
|
|
4283
|
+
},
|
|
4284
|
+
stopPredecessor: async () => {
|
|
4285
|
+
try {
|
|
4286
|
+
await stopSession(predecessorId);
|
|
4287
|
+
} catch (error) {
|
|
4288
|
+
if (!terminalAttempted) throw error;
|
|
4289
|
+
logger.error(`notifications: deferred predecessor cleanup failed: ${String(error)}`);
|
|
4290
|
+
}
|
|
4291
|
+
},
|
|
4292
|
+
requireNativeControlDrain:
|
|
4293
|
+
(request as { requireNativeControlDrain?: unknown }).requireNativeControlDrain === true,
|
|
4294
|
+
});
|
|
4295
|
+
} catch (error) {
|
|
4296
|
+
if (!terminalAttempted) throw error;
|
|
4297
|
+
logger.error(
|
|
4298
|
+
`notifications: identity terminal delivery failed (${terminalOutcome ?? "unknown"}): ${String(error)}`,
|
|
4299
|
+
);
|
|
4300
|
+
}
|
|
4301
|
+
},
|
|
3579
4302
|
afterControlResponse: async (connectionId, request, response) => {
|
|
3580
4303
|
if (
|
|
3581
4304
|
(request.operation === "turn.prompt" ||
|
|
@@ -3596,13 +4319,6 @@ export function createNotificationsExtension(
|
|
|
3596
4319
|
}
|
|
3597
4320
|
|
|
3598
4321
|
if (request.operation === "session.close" && response.ok === true) ctx.shutdown();
|
|
3599
|
-
if (typeof request.operation === "string" && identityControlOperations.has(request.operation)) {
|
|
3600
|
-
const pending = deferredIdentityRotation;
|
|
3601
|
-
deferredIdentityRotation = undefined;
|
|
3602
|
-
identityControlInFlight = false;
|
|
3603
|
-
if (response.ok === true && pending)
|
|
3604
|
-
await rotateSessionAuthority(pending.event, pending.ctx, pending.awaitStartup);
|
|
3605
|
-
}
|
|
3606
4322
|
},
|
|
3607
4323
|
control: async (connectionId, frame) => {
|
|
3608
4324
|
const request = frame as {
|
|
@@ -3622,6 +4338,21 @@ export function createNotificationsExtension(
|
|
|
3622
4338
|
ok: false,
|
|
3623
4339
|
error: { code: "conflict", message: "session identity mutation is already active" },
|
|
3624
4340
|
};
|
|
4341
|
+
const requireNativeControlDrain =
|
|
4342
|
+
(request as { requireNativeControlDrain?: unknown }).requireNativeControlDrain === true ||
|
|
4343
|
+
(!!request.input &&
|
|
4344
|
+
typeof request.input === "object" &&
|
|
4345
|
+
!Array.isArray(request.input) &&
|
|
4346
|
+
(request.input as { requireNativeControlDrain?: unknown }).requireNativeControlDrain === true);
|
|
4347
|
+
if (requireNativeControlDrain && !isNativeControlDrainAvailable())
|
|
4348
|
+
return {
|
|
4349
|
+
id: requestId,
|
|
4350
|
+
ok: false,
|
|
4351
|
+
error: {
|
|
4352
|
+
code: "unavailable",
|
|
4353
|
+
message: "SDK identity control requires the native control-drain lease.",
|
|
4354
|
+
},
|
|
4355
|
+
};
|
|
3625
4356
|
if (rotatesIdentity) identityControlInFlight = true;
|
|
3626
4357
|
const response = await controlRequesterContext.run(connectionId, () =>
|
|
3627
4358
|
dispatchControl(
|
|
@@ -3693,6 +4424,7 @@ export function createNotificationsExtension(
|
|
|
3693
4424
|
workflowGate: undefined,
|
|
3694
4425
|
gatePresentations,
|
|
3695
4426
|
stopping: false,
|
|
4427
|
+
inboundFenced: false,
|
|
3696
4428
|
abortEphemeralTurns: () => {},
|
|
3697
4429
|
disableEphemeralTurns: () => {},
|
|
3698
4430
|
cancelPostmortemCleanup: () => {},
|
|
@@ -3707,8 +4439,13 @@ export function createNotificationsExtension(
|
|
|
3707
4439
|
busy: false,
|
|
3708
4440
|
pendingPromptCorrelations,
|
|
3709
4441
|
activePromptCorrelation: undefined,
|
|
4442
|
+
bindPromptExecutionHandle,
|
|
4443
|
+
peekPromptPendingOutcome: correlation => kindReconciliation.peekPendingOutcome(correlation),
|
|
4444
|
+
terminalizePrompt: (correlation, outcome, extra) => terminalizePrompt(correlation, outcome, {}, extra),
|
|
3710
4445
|
recordPromptTerminal,
|
|
3711
|
-
notePromptReconciliation:
|
|
4446
|
+
notePromptReconciliation: (correlation, frame) => {
|
|
4447
|
+
void kindReconciliation.noteTransition("prompt", correlation, frame);
|
|
4448
|
+
},
|
|
3712
4449
|
emitPromptFailure,
|
|
3713
4450
|
emitPromptLifecycle,
|
|
3714
4451
|
emitPromptEvent,
|
|
@@ -3767,6 +4504,23 @@ export function createNotificationsExtension(
|
|
|
3767
4504
|
});
|
|
3768
4505
|
initializedRuntime.abortEphemeralTurns = () => ephemeralTurns.dispose();
|
|
3769
4506
|
initializedRuntime.disableEphemeralTurns = () => ephemeralTurns.disable();
|
|
4507
|
+
const sendEndpointStale = (connectionId: string, frame: Record<string, unknown>) => {
|
|
4508
|
+
const id = typeof frame.id === "string" ? frame.id : undefined;
|
|
4509
|
+
if (!id) return;
|
|
4510
|
+
const error = { code: "endpoint_stale", message: "session endpoint is stale." };
|
|
4511
|
+
const responseType =
|
|
4512
|
+
frame.type === "control_request"
|
|
4513
|
+
? "control_response"
|
|
4514
|
+
: frame.type === "query_request"
|
|
4515
|
+
? "query_response"
|
|
4516
|
+
: frame.type === "global_request"
|
|
4517
|
+
? "global_response"
|
|
4518
|
+
: undefined;
|
|
4519
|
+
if (!responseType) return;
|
|
4520
|
+
try {
|
|
4521
|
+
server.sendTo(connectionId, JSON.stringify({ type: responseType, id, ok: false, error }));
|
|
4522
|
+
} catch {}
|
|
4523
|
+
};
|
|
3770
4524
|
try {
|
|
3771
4525
|
server.onSdkFrame((err, inbound) => {
|
|
3772
4526
|
if (err || !inbound) return;
|
|
@@ -3774,6 +4528,14 @@ export function createNotificationsExtension(
|
|
|
3774
4528
|
const frame = JSON.parse(inbound.json) as unknown;
|
|
3775
4529
|
if (!frame || typeof frame !== "object") return;
|
|
3776
4530
|
const typedFrame = frame as Record<string, unknown>;
|
|
4531
|
+
if (inbound.connectionId && fencedConnections.has(inbound.connectionId)) {
|
|
4532
|
+
sendEndpointStale(inbound.connectionId, typedFrame);
|
|
4533
|
+
return;
|
|
4534
|
+
}
|
|
4535
|
+
if (initializedRuntime.inboundFenced) {
|
|
4536
|
+
sendEndpointStale(inbound.connectionId, typedFrame);
|
|
4537
|
+
return;
|
|
4538
|
+
}
|
|
3777
4539
|
if (typedFrame.type === "ephemeral_turn" || typedFrame.type === "ephemeral_turn_cancel") return;
|
|
3778
4540
|
if (typedFrame.type === "event_replay") {
|
|
3779
4541
|
const capabilities = Array.isArray(typedFrame.capabilities) ? typedFrame.capabilities : [];
|
|
@@ -3800,15 +4562,31 @@ export function createNotificationsExtension(
|
|
|
3800
4562
|
});
|
|
3801
4563
|
server.onConnectionClose((_err, connectionId) => {
|
|
3802
4564
|
if (!connectionId) return;
|
|
4565
|
+
controlSurface.cancelPendingPreflightsForConnection(connectionId);
|
|
3803
4566
|
host.handleDisconnect(connectionId);
|
|
3804
4567
|
hostCapCache.delete(connectionId);
|
|
4568
|
+
// The socket is gone, so its fence has nothing left to refuse. Dropping the
|
|
4569
|
+
// entry keeps the set bounded by live connections instead of growing forever.
|
|
4570
|
+
fencedConnections.delete(connectionId);
|
|
4571
|
+
// Deliberate deviation from the plan's "claim prompt_failed on old-owner
|
|
4572
|
+
// disconnect": that would break the shipped Q26 reconnect contract, where a
|
|
4573
|
+
// client may drop its socket and reconcile the still-running prompt without
|
|
4574
|
+
// duplicate execution (see test/sdk-host-wiring.test.ts "turn.prompt_status
|
|
4575
|
+
// reconciles an accepted prompt across client reconnect"). A closed socket
|
|
4576
|
+
// therefore only abandons delivery; ACP rejects its own local waiter once and
|
|
4577
|
+
// terminal authority stays with the eventual normalized SDK outcome.
|
|
3805
4578
|
for (const submission of promptSubmissions.values())
|
|
3806
4579
|
if (submission.connectionId === connectionId) abandonPrompt(submission);
|
|
3807
4580
|
});
|
|
3808
4581
|
|
|
3809
4582
|
server.onReply((err, reply) => {
|
|
3810
4583
|
if (err || !reply) return;
|
|
3811
|
-
if (
|
|
4584
|
+
if (
|
|
4585
|
+
runtime?.inboundFenced ||
|
|
4586
|
+
runtime?.stopping ||
|
|
4587
|
+
runtime?.policySuspended ||
|
|
4588
|
+
runtimes.get(id) !== runtime
|
|
4589
|
+
) {
|
|
3812
4590
|
try {
|
|
3813
4591
|
server.closeClaimInvalid(reply.replyReceiptId, "session_stopping");
|
|
3814
4592
|
} catch {}
|
|
@@ -3965,7 +4743,7 @@ export function createNotificationsExtension(
|
|
|
3965
4743
|
} else if (presentation?.multi && typeof rawAnswer === "string") {
|
|
3966
4744
|
answer = { selected: presentation.selectedOptions, other: true, custom: rawAnswer };
|
|
3967
4745
|
} else {
|
|
3968
|
-
const mapped = mapAnswerToGate(reply.answerJson,
|
|
4746
|
+
const mapped = mapAnswerToGate(reply.answerJson, presentation?.options ?? []);
|
|
3969
4747
|
if (!mapped.ok) {
|
|
3970
4748
|
// A numeric selector outside options is invalid (issue #2030): close the
|
|
3971
4749
|
// exact claim/receipt and reissue the interaction — never a success ack.
|
|
@@ -4050,6 +4828,7 @@ export function createNotificationsExtension(
|
|
|
4050
4828
|
// thread (forwarded by the daemon over the WS, fail-closed at the daemon).
|
|
4051
4829
|
server.onInbound((err, inbound) => {
|
|
4052
4830
|
if (err || !inbound) return;
|
|
4831
|
+
if (initializedRuntime.inboundFenced) return;
|
|
4053
4832
|
const authenticatedInbound = inbound as typeof inbound & {
|
|
4054
4833
|
connectionId: string;
|
|
4055
4834
|
messageId?: number;
|
|
@@ -4317,16 +5096,28 @@ export function createNotificationsExtension(
|
|
|
4317
5096
|
if (!canDeliverAsync(runtime, generation)) {
|
|
4318
5097
|
return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
|
|
4319
5098
|
}
|
|
4320
|
-
|
|
4321
|
-
runtime,
|
|
4322
|
-
|
|
4323
|
-
type: "file_attachment",
|
|
5099
|
+
if (file.mime?.startsWith("image/")) {
|
|
5100
|
+
pushSessionFrame(runtime, {
|
|
5101
|
+
type: "image_attachment",
|
|
4324
5102
|
sessionId: runtime.id,
|
|
4325
|
-
|
|
5103
|
+
source: "telegram_send",
|
|
5104
|
+
mime: file.mime,
|
|
4326
5105
|
caption: file.caption,
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
5106
|
+
data: data.toString("base64"),
|
|
5107
|
+
});
|
|
5108
|
+
} else {
|
|
5109
|
+
pushFileAttachment(
|
|
5110
|
+
runtime,
|
|
5111
|
+
{
|
|
5112
|
+
type: "file_attachment",
|
|
5113
|
+
sessionId: runtime.id,
|
|
5114
|
+
name: path.basename(file.path),
|
|
5115
|
+
mime: file.mime,
|
|
5116
|
+
caption: file.caption,
|
|
5117
|
+
},
|
|
5118
|
+
data,
|
|
5119
|
+
);
|
|
5120
|
+
}
|
|
4330
5121
|
return { ok: true };
|
|
4331
5122
|
} catch (e) {
|
|
4332
5123
|
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
@@ -4360,7 +5151,6 @@ export function createNotificationsExtension(
|
|
|
4360
5151
|
activeRuntime.disposeGateTerminalController = () => {};
|
|
4361
5152
|
activeRuntime.disposeAckRecoveryParticipant = () => {};
|
|
4362
5153
|
activeRuntime.workflowGate = undefined;
|
|
4363
|
-
gateOptions.clear();
|
|
4364
5154
|
if (typeof gate?.onGateEmitted !== "function" || typeof gate.resolveGateFromNotification !== "function") {
|
|
4365
5155
|
return;
|
|
4366
5156
|
}
|
|
@@ -4385,7 +5175,6 @@ export function createNotificationsExtension(
|
|
|
4385
5175
|
const rawGateOptions = g.options ?? [];
|
|
4386
5176
|
const options = rawGateOptions.map(o => String((o as { label?: unknown }).label ?? ""));
|
|
4387
5177
|
const recommendedIndex = recommendedIndexFromGateOptions(rawGateOptions);
|
|
4388
|
-
gateOptions.set(g.gate_id, options);
|
|
4389
5178
|
const promptCtx = g.context as { prompt?: unknown; title?: unknown } | undefined;
|
|
4390
5179
|
const question =
|
|
4391
5180
|
(typeof promptCtx?.prompt === "string" && promptCtx.prompt) ||
|
|
@@ -4642,7 +5431,16 @@ export function createNotificationsExtension(
|
|
|
4642
5431
|
};
|
|
4643
5432
|
|
|
4644
5433
|
api.on("session_start", async (_event, ctx) => {
|
|
4645
|
-
|
|
5434
|
+
const task = startAndReconcileSession(ctx);
|
|
5435
|
+
// Track full start+reconcile so settled startups join replacement-token
|
|
5436
|
+
// reconcile before owner release. Pending native startup (/notify on) stays
|
|
5437
|
+
// nonblocking: shutdown only awaits these tasks when sessionStartPromises is clear.
|
|
5438
|
+
sessionLifecycleTasks.add(task);
|
|
5439
|
+
try {
|
|
5440
|
+
await task;
|
|
5441
|
+
} finally {
|
|
5442
|
+
sessionLifecycleTasks.delete(task);
|
|
5443
|
+
}
|
|
4646
5444
|
});
|
|
4647
5445
|
|
|
4648
5446
|
// A session endpoint's token and generation are authority for exactly one
|
|
@@ -4688,10 +5486,27 @@ export function createNotificationsExtension(
|
|
|
4688
5486
|
});
|
|
4689
5487
|
};
|
|
4690
5488
|
|
|
5489
|
+
const preparePredecessorForTerminal = async (id: string): Promise<void> => {
|
|
5490
|
+
const predecessor = runtimes.get(id);
|
|
5491
|
+
if (!predecessor || cleanupRetries.has(id))
|
|
5492
|
+
throw new Error(`notifications: predecessor runtime ${id} is not safely send-capable.`);
|
|
5493
|
+
predecessor.inboundFenced = true;
|
|
5494
|
+
// Release broker/host authority while leaving the native server alive for
|
|
5495
|
+
// the one accepted terminal response. stopAndWait is deferred to stopSession.
|
|
5496
|
+
const stopped = await predecessor.host.stop();
|
|
5497
|
+
if (stopped !== "stopped" && predecessor.host.started)
|
|
5498
|
+
throw new Error(`notifications: predecessor runtime ${id} host release was not proven.`);
|
|
5499
|
+
predecessor.hostStopped = true;
|
|
5500
|
+
predecessor.brokerRegistrationReleased = !predecessor.brokerRegistrationActive || predecessor.hostStopped;
|
|
5501
|
+
if (predecessor.brokerRegistrationActive && predecessor.hostStopped) predecessor.brokerRegistrationActive = false;
|
|
5502
|
+
predecessor.host.reverse.dispose();
|
|
5503
|
+
};
|
|
5504
|
+
|
|
4691
5505
|
const rotateSessionAuthority = async (
|
|
4692
5506
|
event: { previousSessionFile?: string },
|
|
4693
5507
|
ctx: ExtensionContext,
|
|
4694
5508
|
awaitStartup: boolean,
|
|
5509
|
+
options: { deferPredecessorStop?: boolean } = {},
|
|
4695
5510
|
): Promise<void> => {
|
|
4696
5511
|
if (extensionShuttingDown) return;
|
|
4697
5512
|
const newId = sessionId(ctx);
|
|
@@ -4700,7 +5515,11 @@ export function createNotificationsExtension(
|
|
|
4700
5515
|
const pendingStartup = sessionStartPromises.get(newId);
|
|
4701
5516
|
if (pendingStartup) {
|
|
4702
5517
|
if (awaitStartup) {
|
|
4703
|
-
await pendingStartup;
|
|
5518
|
+
const result = await pendingStartup;
|
|
5519
|
+
if (!lifecycleStartupCapability && result.status === "failed")
|
|
5520
|
+
throw new Error(
|
|
5521
|
+
`notifications: SDK startup failed: ${result.failure?.message ?? "Unknown startup failure."}`,
|
|
5522
|
+
);
|
|
4704
5523
|
if (!extensionShuttingDown && runtimes.has(newId) && activeRuntimeId === newId)
|
|
4705
5524
|
await controller.reconcileCurrentSession(ctx);
|
|
4706
5525
|
} else {
|
|
@@ -4715,14 +5534,14 @@ export function createNotificationsExtension(
|
|
|
4715
5534
|
}
|
|
4716
5535
|
if (prevId && prevId !== newId) {
|
|
4717
5536
|
controller.rekeySession(prevId, newId);
|
|
4718
|
-
|
|
4719
|
-
await
|
|
4720
|
-
}
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
5537
|
+
if (options.deferPredecessorStop) {
|
|
5538
|
+
await preparePredecessorForTerminal(prevId);
|
|
5539
|
+
} else {
|
|
5540
|
+
const stopped = await stopSession(prevId);
|
|
5541
|
+
if (cleanupRetries.has(prevId) || runtimes.has(prevId) || sessionStartPromises.has(prevId))
|
|
5542
|
+
throw new Error(`notifications: predecessor runtime ${prevId} release is uncertain.`);
|
|
5543
|
+
if (!stopped && activeRuntimeId === prevId)
|
|
5544
|
+
throw new Error(`notifications: predecessor runtime ${prevId} release was not proven.`);
|
|
4726
5545
|
}
|
|
4727
5546
|
}
|
|
4728
5547
|
if (extensionShuttingDown) return;
|
|
@@ -4743,18 +5562,19 @@ export function createNotificationsExtension(
|
|
|
4743
5562
|
trackBranchStartup(newId, ctx, startup);
|
|
4744
5563
|
};
|
|
4745
5564
|
api.on("session_switch", async (event, ctx) => {
|
|
5565
|
+
const awaitStartup = shouldAwaitNotificationStartup(event);
|
|
4746
5566
|
if (identityControlInFlight) {
|
|
4747
|
-
deferredIdentityRotation = { event, ctx, awaitStartup
|
|
5567
|
+
deferredIdentityRotation = { event, ctx, awaitStartup };
|
|
4748
5568
|
return;
|
|
4749
5569
|
}
|
|
4750
|
-
await rotateSessionAuthority(event, ctx,
|
|
5570
|
+
await rotateSessionAuthority(event, ctx, awaitStartup);
|
|
4751
5571
|
});
|
|
4752
5572
|
api.on("session_branch", async (event, ctx) => {
|
|
4753
5573
|
if (identityControlInFlight) {
|
|
4754
|
-
deferredIdentityRotation = { event, ctx, awaitStartup:
|
|
5574
|
+
deferredIdentityRotation = { event, ctx, awaitStartup: true };
|
|
4755
5575
|
return;
|
|
4756
5576
|
}
|
|
4757
|
-
await rotateSessionAuthority(event, ctx,
|
|
5577
|
+
await rotateSessionAuthority(event, ctx, true);
|
|
4758
5578
|
});
|
|
4759
5579
|
|
|
4760
5580
|
const terminalizeInFlightTools = (
|
|
@@ -4870,6 +5690,7 @@ export function createNotificationsExtension(
|
|
|
4870
5690
|
rt.busy = true;
|
|
4871
5691
|
const correlation = rt.pendingPromptCorrelations.shift();
|
|
4872
5692
|
rt.activePromptCorrelation = correlation;
|
|
5693
|
+
if (correlation) rt.bindPromptExecutionHandle(correlation, ctx.getActivePromptHandle());
|
|
4873
5694
|
rt.notePromptReconciliation(correlation, { type: "agent_start" });
|
|
4874
5695
|
rt.emitPromptLifecycle(correlation, { type: "agent_start", sessionId: id, ...correlation });
|
|
4875
5696
|
try {
|
|
@@ -4915,15 +5736,45 @@ export function createNotificationsExtension(
|
|
|
4915
5736
|
rt.busy = false;
|
|
4916
5737
|
const correlation = rt.activePromptCorrelation;
|
|
4917
5738
|
if (correlation) {
|
|
4918
|
-
const
|
|
5739
|
+
const assistants = (Array.isArray(event.messages) ? [...event.messages].reverse() : []).filter(
|
|
5740
|
+
message => message && typeof message === "object" && (message as { role?: unknown }).role === "assistant",
|
|
5741
|
+
) as Array<{ stopReason?: unknown; errorKind?: unknown }>;
|
|
5742
|
+
const finalAssistant = assistants[0];
|
|
5743
|
+
const pendingOutcome = rt.peekPromptPendingOutcome(correlation);
|
|
5744
|
+
let outcome: SdkPromptTerminalOutcome;
|
|
5745
|
+
if (pendingOutcome) outcome = pendingOutcome;
|
|
5746
|
+
else if (finalAssistant?.stopReason === "length")
|
|
5747
|
+
outcome = { kind: "stopped", reason: "max_tokens", provenance: "agent" };
|
|
5748
|
+
else if (finalAssistant?.errorKind === "provider_safety_stop")
|
|
5749
|
+
outcome = { kind: "stopped", reason: "refusal", provenance: "agent" };
|
|
5750
|
+
// A missing/normal final assistant with a non-failing loop stop reason is a
|
|
5751
|
+
// normal turn end; only explicit error/aborted assistants or non-completed
|
|
5752
|
+
// loop stop reasons are failures. Text is never parsed.
|
|
5753
|
+
else if (
|
|
5754
|
+
(event.stopReason === undefined || event.stopReason === "completed") &&
|
|
5755
|
+
finalAssistant?.stopReason !== "error" &&
|
|
5756
|
+
finalAssistant?.stopReason !== "aborted"
|
|
5757
|
+
)
|
|
5758
|
+
outcome = { kind: "stopped", reason: "end_turn", provenance: "agent" };
|
|
5759
|
+
else
|
|
5760
|
+
outcome = {
|
|
5761
|
+
kind: "failed",
|
|
5762
|
+
code: "prompt_failed",
|
|
5763
|
+
message: "Prompt submission failed.",
|
|
5764
|
+
provenance: "agent_failed",
|
|
5765
|
+
};
|
|
5766
|
+
const successAssistant = assistants.find(
|
|
5767
|
+
message => (message as { stopReason?: unknown }).stopReason === "stop",
|
|
5768
|
+
);
|
|
5769
|
+
const finalText = successAssistant ? acpFinalTextFromMessage(successAssistant).text : "";
|
|
5770
|
+
// The normalized outcome is the ACP contract; existing SDK clients keep the
|
|
5771
|
+
// legacy failure discriminator on the wire and in the reconciliation record.
|
|
5772
|
+
const terminalAssistant = assistants.find(
|
|
4919
5773
|
message =>
|
|
4920
|
-
message
|
|
4921
|
-
|
|
4922
|
-
(message as { role?: unknown }).role === "assistant" &&
|
|
4923
|
-
((message as { stopReason?: unknown }).stopReason === "error" ||
|
|
4924
|
-
(message as { stopReason?: unknown }).stopReason === "aborted"),
|
|
5774
|
+
(message as { stopReason?: unknown }).stopReason === "error" ||
|
|
5775
|
+
(message as { stopReason?: unknown }).stopReason === "aborted",
|
|
4925
5776
|
) as { stopReason?: "error" | "aborted"; errorMessage?: unknown } | undefined;
|
|
4926
|
-
const
|
|
5777
|
+
const legacyCode =
|
|
4927
5778
|
event.stopReason === "cancelled"
|
|
4928
5779
|
? "cancelled"
|
|
4929
5780
|
: terminalAssistant?.stopReason === "error"
|
|
@@ -4931,33 +5782,16 @@ export function createNotificationsExtension(
|
|
|
4931
5782
|
: terminalAssistant?.stopReason === "aborted"
|
|
4932
5783
|
? "aborted"
|
|
4933
5784
|
: undefined;
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
: "Prompt submission failed.",
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
if (rt.recordPromptTerminal(correlation)) {
|
|
4945
|
-
const finalAssistant = (Array.isArray(event.messages) ? [...event.messages].reverse() : []).find(
|
|
4946
|
-
message =>
|
|
4947
|
-
message &&
|
|
4948
|
-
typeof message === "object" &&
|
|
4949
|
-
(message as { role?: unknown }).role === "assistant" &&
|
|
4950
|
-
(message as { stopReason?: unknown }).stopReason === "stop",
|
|
4951
|
-
);
|
|
4952
|
-
const finalText = finalAssistant ? acpFinalTextFromMessage(finalAssistant).text : "";
|
|
4953
|
-
rt.emitPromptLifecycle(correlation, {
|
|
4954
|
-
type: "agent_end",
|
|
4955
|
-
sessionId: id,
|
|
4956
|
-
...correlation,
|
|
4957
|
-
...(finalText ? { finalText } : {}),
|
|
4958
|
-
});
|
|
4959
|
-
}
|
|
4960
|
-
}
|
|
5785
|
+
void rt.terminalizePrompt(correlation, outcome, {
|
|
5786
|
+
...(finalText ? { finalText } : {}),
|
|
5787
|
+
...(outcome.kind === "failed" && legacyCode
|
|
5788
|
+
? {
|
|
5789
|
+
// Only the legacy discriminator is preserved; provider text is never
|
|
5790
|
+
// retained, matching `sanitizePromptFailure`.
|
|
5791
|
+
error: { code: legacyCode, message: "Prompt submission failed." },
|
|
5792
|
+
}
|
|
5793
|
+
: {}),
|
|
5794
|
+
});
|
|
4961
5795
|
} else {
|
|
4962
5796
|
rt.emitPromptLifecycle(undefined, { type: "agent_end", sessionId: id });
|
|
4963
5797
|
}
|
|
@@ -5292,8 +6126,12 @@ export function createNotificationsExtension(
|
|
|
5292
6126
|
extensionShuttingDown = true;
|
|
5293
6127
|
identityControlInFlight = false;
|
|
5294
6128
|
deferredIdentityRotation = undefined;
|
|
5295
|
-
await Promise.allSettled([...branchStartupTasks]);
|
|
5296
6129
|
const id = sessionId(ctx);
|
|
6130
|
+
// Join settled start+reconcile before owner release. Keep pending native
|
|
6131
|
+
// startup (/notify on) nonblocking — do not await sessionLifecycleTasks while
|
|
6132
|
+
// sessionStartPromises still has this id.
|
|
6133
|
+
if (!sessionStartPromises.has(id)) await Promise.allSettled([...sessionLifecycleTasks]);
|
|
6134
|
+
await Promise.allSettled([...branchStartupTasks]);
|
|
5297
6135
|
const rt = runtimes.get(id);
|
|
5298
6136
|
if (rt) terminalizeInFlightTools(rt, id, "cancelled");
|
|
5299
6137
|
// Startup is only genuinely in flight when a `sessionStartPromises` entry
|