@qduc/term2 0.22.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +1 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +30 -18
- package/dist/agent.js.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +20 -1
- package/dist/app.js.map +1 -1
- package/dist/cli.js +27 -1
- package/dist/cli.js.map +1 -1
- package/dist/components/input/ApplicationInputSurface.js +1 -1
- package/dist/components/input/ApplicationInputSurface.js.map +1 -1
- package/dist/components/input/SettingsMenuSession.d.ts.map +1 -1
- package/dist/components/input/SettingsMenuSession.js +8 -5
- package/dist/components/input/SettingsMenuSession.js.map +1 -1
- package/dist/components/input/SubagentPoolMenuSession.d.ts +9 -0
- package/dist/components/input/SubagentPoolMenuSession.d.ts.map +1 -0
- package/dist/components/input/{MentorPoolMenuSession.js → SubagentPoolMenuSession.js} +17 -10
- package/dist/components/input/SubagentPoolMenuSession.js.map +1 -0
- package/dist/components/input/menu-controller.d.ts.map +1 -1
- package/dist/components/input/menu-controller.js +4 -3
- package/dist/components/input/menu-controller.js.map +1 -1
- package/dist/components/input/menu-registry.js +2 -2
- package/dist/components/input/menu-registry.js.map +1 -1
- package/dist/components/input/menu-types.d.ts +5 -1
- package/dist/components/input/menu-types.d.ts.map +1 -1
- package/dist/components/input/triggers.d.ts.map +1 -1
- package/dist/components/input/triggers.js +10 -7
- package/dist/components/input/triggers.js.map +1 -1
- package/dist/components/layout/Banner.d.ts.map +1 -1
- package/dist/components/layout/Banner.js +4 -1
- package/dist/components/layout/Banner.js.map +1 -1
- package/dist/components/layout/StatusBar.d.ts.map +1 -1
- package/dist/components/layout/StatusBar.js +10 -17
- package/dist/components/layout/StatusBar.js.map +1 -1
- package/dist/components/menu/SettingsSelectionMenu.d.ts.map +1 -1
- package/dist/components/menu/SettingsSelectionMenu.js +3 -1
- package/dist/components/menu/SettingsSelectionMenu.js.map +1 -1
- package/dist/components/menu/SubagentPoolSelectionMenu.d.ts +19 -0
- package/dist/components/menu/SubagentPoolSelectionMenu.d.ts.map +1 -0
- package/dist/components/menu/{MentorPoolSelectionMenu.js → SubagentPoolSelectionMenu.js} +29 -15
- package/dist/components/menu/SubagentPoolSelectionMenu.js.map +1 -0
- package/dist/components/message/ChatMessage.js +5 -4
- package/dist/components/message/ChatMessage.js.map +1 -1
- package/dist/components/message/MessageList.d.ts.map +1 -1
- package/dist/components/message/MessageList.js +19 -0
- package/dist/components/message/MessageList.js.map +1 -1
- package/dist/components/theme.d.ts +2 -0
- package/dist/components/theme.d.ts.map +1 -1
- package/dist/components/theme.js +2 -0
- package/dist/components/theme.js.map +1 -1
- package/dist/context/InputContext.d.ts +1 -1
- package/dist/context/InputContext.d.ts.map +1 -1
- package/dist/context/InputContext.js +2 -2
- package/dist/context/InputContext.js.map +1 -1
- package/dist/contracts/conversation-items.d.ts +4 -0
- package/dist/contracts/conversation-items.d.ts.map +1 -1
- package/dist/contracts/provider-input.d.ts +2 -0
- package/dist/contracts/provider-input.d.ts.map +1 -1
- package/dist/contracts/provider-input.js.map +1 -1
- package/dist/gateway/assertion.d.ts +1 -1
- package/dist/gateway/assertion.d.ts.map +1 -1
- package/dist/gateway/contracts.d.ts +13 -3
- package/dist/gateway/contracts.d.ts.map +1 -1
- package/dist/gateway/contracts.js +56 -0
- package/dist/gateway/contracts.js.map +1 -1
- package/dist/gateway/gateway.d.ts +40 -1
- package/dist/gateway/gateway.d.ts.map +1 -1
- package/dist/gateway/gateway.js +1319 -76
- package/dist/gateway/gateway.js.map +1 -1
- package/dist/gateway/persistence/admission-persistence.d.ts +3 -3
- package/dist/gateway/persistence/admission-persistence.d.ts.map +1 -1
- package/dist/gateway/persistence/admission-persistence.js +25 -11
- package/dist/gateway/persistence/admission-persistence.js.map +1 -1
- package/dist/gateway/persistence/contracts.d.ts +5 -3
- package/dist/gateway/persistence/contracts.d.ts.map +1 -1
- package/dist/gateway/persistence/contracts.js +13 -0
- package/dist/gateway/persistence/contracts.js.map +1 -1
- package/dist/gateway/persistence/interaction-checkpoint.d.ts +5 -0
- package/dist/gateway/persistence/interaction-checkpoint.d.ts.map +1 -1
- package/dist/gateway/persistence/interaction-checkpoint.js +22 -0
- package/dist/gateway/persistence/interaction-checkpoint.js.map +1 -1
- package/dist/gateway/persistence/projection.d.ts.map +1 -1
- package/dist/gateway/persistence/projection.js +4 -1
- package/dist/gateway/persistence/projection.js.map +1 -1
- package/dist/gateway/persistence/session-index.d.ts +1 -1
- package/dist/gateway/persistence/session-index.d.ts.map +1 -1
- package/dist/gateway/persistence/session-index.js +2 -2
- package/dist/gateway/persistence/session-index.js.map +1 -1
- package/dist/gateway/persistence/session-snapshot.d.ts +32 -0
- package/dist/gateway/persistence/session-snapshot.d.ts.map +1 -0
- package/dist/gateway/persistence/session-snapshot.js +50 -0
- package/dist/gateway/persistence/session-snapshot.js.map +1 -0
- package/dist/gateway/runtime-factory.d.ts +37 -1
- package/dist/gateway/runtime-factory.d.ts.map +1 -1
- package/dist/gateway/runtime-factory.js +155 -24
- package/dist/gateway/runtime-factory.js.map +1 -1
- package/dist/gateway/safe-log.d.ts +1 -1
- package/dist/gateway/safe-log.d.ts.map +1 -1
- package/dist/gateway/safe-log.js +5 -46
- package/dist/gateway/safe-log.js.map +1 -1
- package/dist/gateway/serve-args.d.ts +53 -0
- package/dist/gateway/serve-args.d.ts.map +1 -0
- package/dist/gateway/serve-args.js +172 -0
- package/dist/gateway/serve-args.js.map +1 -0
- package/dist/gateway/serve.d.ts +79 -0
- package/dist/gateway/serve.d.ts.map +1 -0
- package/dist/gateway/serve.js +375 -0
- package/dist/gateway/serve.js.map +1 -0
- package/dist/gateway/server-session.d.ts +9 -0
- package/dist/gateway/server-session.d.ts.map +1 -1
- package/dist/gateway/server-session.js +142 -26
- package/dist/gateway/server-session.js.map +1 -1
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/gateway/server.js +13 -2
- package/dist/gateway/server.js.map +1 -1
- package/dist/hooks/use-subagent-pool-selection.d.ts +115 -0
- package/dist/hooks/use-subagent-pool-selection.d.ts.map +1 -0
- package/dist/hooks/{use-mentor-pool-selection.js → use-subagent-pool-selection.js} +110 -54
- package/dist/hooks/use-subagent-pool-selection.js.map +1 -0
- package/dist/lib/agent-client.d.ts +3 -0
- package/dist/lib/agent-client.d.ts.map +1 -1
- package/dist/lib/agent-client.js +7 -0
- package/dist/lib/agent-client.js.map +1 -1
- package/dist/lib/agent-configuration.d.ts +1 -0
- package/dist/lib/agent-configuration.d.ts.map +1 -1
- package/dist/lib/agent-configuration.js +3 -0
- package/dist/lib/agent-configuration.js.map +1 -1
- package/dist/lib/agent-factory.d.ts +2 -0
- package/dist/lib/agent-factory.d.ts.map +1 -1
- package/dist/lib/agent-factory.js +5 -0
- package/dist/lib/agent-factory.js.map +1 -1
- package/dist/lib/subagent-bridge.d.ts +5 -0
- package/dist/lib/subagent-bridge.d.ts.map +1 -1
- package/dist/lib/subagent-bridge.js +5 -0
- package/dist/lib/subagent-bridge.js.map +1 -1
- package/dist/lib/tool-selection-policy.d.ts +2 -0
- package/dist/lib/tool-selection-policy.d.ts.map +1 -1
- package/dist/lib/tool-selection-policy.js +6 -1
- package/dist/lib/tool-selection-policy.js.map +1 -1
- package/dist/prompts/explorer-delegation-contract.test.ts +1 -1
- package/dist/prompts/prompt-constructor.d.ts.map +1 -1
- package/dist/prompts/prompt-constructor.js +1 -0
- package/dist/prompts/prompt-constructor.js.map +1 -1
- package/dist/prompts/prompt-constructor.test.ts +3 -0
- package/dist/prompts/prompt-constructor.ts +1 -0
- package/dist/prompts/prompt-profiles.d.ts.map +1 -1
- package/dist/prompts/prompt-profiles.js +3 -2
- package/dist/prompts/prompt-profiles.js.map +1 -1
- package/dist/prompts/prompt-profiles.ts +4 -2
- package/dist/prompts/subagent-delegation.d.ts +2 -1
- package/dist/prompts/subagent-delegation.d.ts.map +1 -1
- package/dist/prompts/subagent-delegation.js +10 -5
- package/dist/prompts/subagent-delegation.js.map +1 -1
- package/dist/prompts/subagent-delegation.test.ts +10 -3
- package/dist/prompts/subagent-delegation.ts +16 -4
- package/dist/prompts/subagents/librarian.md +26 -21
- package/dist/prompts/subagents/reviewer.md +33 -0
- package/dist/providers/grok-auth.d.ts +7 -3
- package/dist/providers/grok-auth.d.ts.map +1 -1
- package/dist/providers/grok-auth.js +11 -6
- package/dist/providers/grok-auth.js.map +1 -1
- package/dist/providers/oauth-pkce.d.ts +7 -6
- package/dist/providers/oauth-pkce.d.ts.map +1 -1
- package/dist/providers/oauth-pkce.js +33 -6
- package/dist/providers/oauth-pkce.js.map +1 -1
- package/dist/services/agent-runtime/application-run-loop.d.ts.map +1 -1
- package/dist/services/agent-runtime/application-run-loop.js +10 -5
- package/dist/services/agent-runtime/application-run-loop.js.map +1 -1
- package/dist/services/agent-runtime/context-compaction/local-context-compactor.d.ts +9 -0
- package/dist/services/agent-runtime/context-compaction/local-context-compactor.d.ts.map +1 -1
- package/dist/services/agent-runtime/context-compaction/local-context-compactor.js +6 -0
- package/dist/services/agent-runtime/context-compaction/local-context-compactor.js.map +1 -1
- package/dist/services/agent-runtime/model-resolver.d.ts +6 -0
- package/dist/services/agent-runtime/model-resolver.d.ts.map +1 -1
- package/dist/services/agent-runtime/model-resolver.js +18 -4
- package/dist/services/agent-runtime/model-resolver.js.map +1 -1
- package/dist/services/approval/non-interactive-approval-policy.d.ts.map +1 -1
- package/dist/services/approval/non-interactive-approval-policy.js +2 -1
- package/dist/services/approval/non-interactive-approval-policy.js.map +1 -1
- package/dist/services/approval/shell-auto-approval-evaluator.js +2 -2
- package/dist/services/approval/shell-auto-approval-evaluator.js.map +1 -1
- package/dist/services/conversation/conversation-service.d.ts +28 -0
- package/dist/services/conversation/conversation-service.d.ts.map +1 -1
- package/dist/services/conversation/conversation-service.js +95 -27
- package/dist/services/conversation/conversation-service.js.map +1 -1
- package/dist/services/conversation/run-item-normalizer.d.ts.map +1 -1
- package/dist/services/conversation/run-item-normalizer.js +3 -0
- package/dist/services/conversation/run-item-normalizer.js.map +1 -1
- package/dist/services/conversation/session-browser.d.ts +5 -0
- package/dist/services/conversation/session-browser.d.ts.map +1 -1
- package/dist/services/conversation/session-browser.js +94 -31
- package/dist/services/conversation/session-browser.js.map +1 -1
- package/dist/services/conversation/session-index/session-index-database.d.ts +1 -0
- package/dist/services/conversation/session-index/session-index-database.d.ts.map +1 -1
- package/dist/services/conversation/session-index/session-index-database.js +6 -4
- package/dist/services/conversation/session-index/session-index-database.js.map +1 -1
- package/dist/services/conversation/session-index/session-index-schema.d.ts +1 -1
- package/dist/services/conversation/session-index/session-index-schema.d.ts.map +1 -1
- package/dist/services/conversation/session-index/session-index-schema.js +2 -1
- package/dist/services/conversation/session-index/session-index-schema.js.map +1 -1
- package/dist/services/conversation-agent-client.d.ts +3 -0
- package/dist/services/conversation-agent-client.d.ts.map +1 -1
- package/dist/services/memory/memory-capabilities.d.ts.map +1 -1
- package/dist/services/memory/memory-capabilities.js +2 -5
- package/dist/services/memory/memory-capabilities.js.map +1 -1
- package/dist/services/plan-mode-interceptor.js +2 -2
- package/dist/services/plan-mode-interceptor.js.map +1 -1
- package/dist/services/sandboxed-code-host/host-types.d.ts +12 -1
- package/dist/services/sandboxed-code-host/host-types.d.ts.map +1 -1
- package/dist/services/sandboxed-code-host/host-types.js.map +1 -1
- package/dist/services/sandboxed-code-host/host-worker.d.ts.map +1 -1
- package/dist/services/sandboxed-code-host/host-worker.js +146 -31
- package/dist/services/sandboxed-code-host/host-worker.js.map +1 -1
- package/dist/services/sandboxed-code-host/sandbox.d.ts +6 -1
- package/dist/services/sandboxed-code-host/sandbox.d.ts.map +1 -1
- package/dist/services/sandboxed-code-host/sandbox.js +5 -0
- package/dist/services/sandboxed-code-host/sandbox.js.map +1 -1
- package/dist/services/sandboxed-code-host/sandboxed-code-host.d.ts +3 -3
- package/dist/services/sandboxed-code-host/sandboxed-code-host.d.ts.map +1 -1
- package/dist/services/sandboxed-code-host/sandboxed-code-host.js +173 -22
- package/dist/services/sandboxed-code-host/sandboxed-code-host.js.map +1 -1
- package/dist/services/session/session-access-state.d.ts +5 -1
- package/dist/services/session/session-access-state.d.ts.map +1 -1
- package/dist/services/session/session-access-state.js +8 -1
- package/dist/services/session/session-access-state.js.map +1 -1
- package/dist/services/session/session-client-factory.d.ts +1 -0
- package/dist/services/session/session-client-factory.d.ts.map +1 -1
- package/dist/services/session/session-client-factory.js +1 -1
- package/dist/services/session/session-client-factory.js.map +1 -1
- package/dist/services/session/session-composition.d.ts +22 -4
- package/dist/services/session/session-composition.d.ts.map +1 -1
- package/dist/services/session/session-composition.js +9 -5
- package/dist/services/session/session-composition.js.map +1 -1
- package/dist/services/settings/ancillary-settings-migration.d.ts.map +1 -1
- package/dist/services/settings/ancillary-settings-migration.js +8 -1
- package/dist/services/settings/ancillary-settings-migration.js.map +1 -1
- package/dist/services/settings/settings-schema.d.ts +12 -40
- package/dist/services/settings/settings-schema.d.ts.map +1 -1
- package/dist/services/settings/settings-schema.js +27 -53
- package/dist/services/settings/settings-schema.js.map +1 -1
- package/dist/services/settings/settings-sources.d.ts +0 -3
- package/dist/services/settings/settings-sources.d.ts.map +1 -1
- package/dist/services/settings/settings-sources.js +0 -3
- package/dist/services/settings/settings-sources.js.map +1 -1
- package/dist/services/settings/settings-ui-metadata.d.ts.map +1 -1
- package/dist/services/settings/settings-ui-metadata.js +3 -3
- package/dist/services/settings/settings-ui-metadata.js.map +1 -1
- package/dist/services/settings/test-helpers/settings-consumer-inventory.d.ts +1 -1
- package/dist/services/settings/test-helpers/settings-consumer-inventory.d.ts.map +1 -1
- package/dist/services/settings/test-helpers/settings-consumer-inventory.js +5 -8
- package/dist/services/settings/test-helpers/settings-consumer-inventory.js.map +1 -1
- package/dist/services/subagents/execution-runner.js +1 -1
- package/dist/services/subagents/execution-runner.js.map +1 -1
- package/dist/services/subagents/nested-runner.d.ts +3 -0
- package/dist/services/subagents/nested-runner.d.ts.map +1 -1
- package/dist/services/subagents/nested-runner.js +32 -5
- package/dist/services/subagents/nested-runner.js.map +1 -1
- package/dist/services/subagents/role-loader.d.ts +1 -1
- package/dist/services/subagents/role-loader.d.ts.map +1 -1
- package/dist/services/subagents/role-loader.js +11 -19
- package/dist/services/subagents/role-loader.js.map +1 -1
- package/dist/services/subagents/runtime.d.ts +3 -0
- package/dist/services/subagents/runtime.d.ts.map +1 -1
- package/dist/services/subagents/runtime.js +35 -3
- package/dist/services/subagents/runtime.js.map +1 -1
- package/dist/services/subagents/subagent-async-registry.d.ts +10 -1
- package/dist/services/subagents/subagent-async-registry.d.ts.map +1 -1
- package/dist/services/subagents/subagent-async-registry.js +21 -4
- package/dist/services/subagents/subagent-async-registry.js.map +1 -1
- package/dist/services/subagents/subagent-manager.d.ts +3 -0
- package/dist/services/subagents/subagent-manager.d.ts.map +1 -1
- package/dist/services/subagents/subagent-manager.js.map +1 -1
- package/dist/services/subagents/subagent-pool-config.d.ts +39 -0
- package/dist/services/subagents/subagent-pool-config.d.ts.map +1 -0
- package/dist/services/subagents/subagent-pool-config.js +84 -0
- package/dist/services/subagents/subagent-pool-config.js.map +1 -0
- package/dist/services/subagents/subagent-role-pool-selector.d.ts +29 -0
- package/dist/services/subagents/subagent-role-pool-selector.d.ts.map +1 -0
- package/dist/services/subagents/subagent-role-pool-selector.js +56 -0
- package/dist/services/subagents/subagent-role-pool-selector.js.map +1 -0
- package/dist/services/subagents/tool-policy.d.ts +13 -0
- package/dist/services/subagents/tool-policy.d.ts.map +1 -1
- package/dist/services/subagents/tool-policy.js +36 -4
- package/dist/services/subagents/tool-policy.js.map +1 -1
- package/dist/services/subagents/types.d.ts +1 -1
- package/dist/services/subagents/types.d.ts.map +1 -1
- package/dist/services/subagents/types.js +1 -1
- package/dist/services/subagents/types.js.map +1 -1
- package/dist/test-helpers/outside-sandbox-temp-dir.d.ts +2 -0
- package/dist/test-helpers/outside-sandbox-temp-dir.d.ts.map +1 -0
- package/dist/test-helpers/outside-sandbox-temp-dir.js +38 -0
- package/dist/test-helpers/outside-sandbox-temp-dir.js.map +1 -0
- package/dist/tools/agent/configure-task-check-in.d.ts +2 -0
- package/dist/tools/agent/configure-task-check-in.d.ts.map +1 -1
- package/dist/tools/agent/configure-task-check-in.js +3 -0
- package/dist/tools/agent/configure-task-check-in.js.map +1 -1
- package/dist/tools/agent/run-explorer.d.ts +6 -0
- package/dist/tools/agent/run-explorer.d.ts.map +1 -0
- package/dist/tools/agent/run-explorer.js +58 -0
- package/dist/tools/agent/run-explorer.js.map +1 -0
- package/dist/tools/agent/run-subagent-async.d.ts +2 -0
- package/dist/tools/agent/run-subagent-async.d.ts.map +1 -1
- package/dist/tools/agent/run-subagent-async.js +3 -3
- package/dist/tools/agent/run-subagent-async.js.map +1 -1
- package/dist/tools/agent/run-subagent.d.ts +2 -2
- package/dist/tools/agent/run-subagent.d.ts.map +1 -1
- package/dist/tools/agent/run-subagent.js +8 -6
- package/dist/tools/agent/run-subagent.js.map +1 -1
- package/dist/tools/file/apply-patch.d.ts.map +1 -1
- package/dist/tools/file/apply-patch.js +7 -2
- package/dist/tools/file/apply-patch.js.map +1 -1
- package/dist/tools/file/create-file.d.ts.map +1 -1
- package/dist/tools/file/create-file.js +4 -0
- package/dist/tools/file/create-file.js.map +1 -1
- package/dist/tools/file/glob.d.ts.map +1 -1
- package/dist/tools/file/glob.js +6 -0
- package/dist/tools/file/glob.js.map +1 -1
- package/dist/tools/file/search-replace.d.ts.map +1 -1
- package/dist/tools/file/search-replace.js +6 -2
- package/dist/tools/file/search-replace.js.map +1 -1
- package/dist/tools/format-helpers.d.ts +5 -0
- package/dist/tools/format-helpers.d.ts.map +1 -1
- package/dist/tools/format-helpers.js.map +1 -1
- package/dist/tools/memory/memory-tools.d.ts +0 -1
- package/dist/tools/memory/memory-tools.d.ts.map +1 -1
- package/dist/tools/memory/memory-tools.js +1 -116
- package/dist/tools/memory/memory-tools.js.map +1 -1
- package/dist/tools/scripted-return-contract.d.ts +24 -0
- package/dist/tools/scripted-return-contract.d.ts.map +1 -0
- package/dist/tools/scripted-return-contract.js +36 -0
- package/dist/tools/scripted-return-contract.js.map +1 -0
- package/dist/tools/session-browser/session-browser-tools.d.ts.map +1 -1
- package/dist/tools/session-browser/session-browser-tools.js +21 -8
- package/dist/tools/session-browser/session-browser-tools.js.map +1 -1
- package/dist/tools/system/run-code/index.d.ts +3 -0
- package/dist/tools/system/run-code/index.d.ts.map +1 -1
- package/dist/tools/system/run-code/index.js +3 -0
- package/dist/tools/system/run-code/index.js.map +1 -1
- package/dist/tools/system/run-code/run-code-execution.d.ts +62 -0
- package/dist/tools/system/run-code/run-code-execution.d.ts.map +1 -0
- package/dist/tools/system/run-code/run-code-execution.js +74 -0
- package/dist/tools/system/run-code/run-code-execution.js.map +1 -0
- package/dist/tools/system/run-code/run-code-runtime-contract.d.ts +20 -0
- package/dist/tools/system/run-code/run-code-runtime-contract.d.ts.map +1 -0
- package/dist/tools/system/run-code/run-code-runtime-contract.js +2 -0
- package/dist/tools/system/run-code/run-code-runtime-contract.js.map +1 -0
- package/dist/tools/system/run-code/run-code-runtime.d.ts +99 -0
- package/dist/tools/system/run-code/run-code-runtime.d.ts.map +1 -0
- package/dist/tools/system/run-code/run-code-runtime.js +725 -0
- package/dist/tools/system/run-code/run-code-runtime.js.map +1 -0
- package/dist/tools/system/run-code/run-code-telemetry.d.ts +17 -10
- package/dist/tools/system/run-code/run-code-telemetry.d.ts.map +1 -1
- package/dist/tools/system/run-code/run-code-telemetry.js +53 -73
- package/dist/tools/system/run-code/run-code-telemetry.js.map +1 -1
- package/dist/tools/system/run-code/run-code.d.ts +22 -59
- package/dist/tools/system/run-code/run-code.d.ts.map +1 -1
- package/dist/tools/system/run-code/run-code.js +122 -786
- package/dist/tools/system/run-code/run-code.js.map +1 -1
- package/dist/tools/system/run-code/tools-header.d.ts +16 -4
- package/dist/tools/system/run-code/tools-header.d.ts.map +1 -1
- package/dist/tools/system/run-code/tools-header.js +33 -16
- package/dist/tools/system/run-code/tools-header.js.map +1 -1
- package/dist/tools/system/shell.d.ts +3 -0
- package/dist/tools/system/shell.d.ts.map +1 -1
- package/dist/tools/system/shell.js +18 -12
- package/dist/tools/system/shell.js.map +1 -1
- package/dist/tools/types.d.ts +12 -4
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js.map +1 -1
- package/dist/utils/ai/model-settings.d.ts +6 -0
- package/dist/utils/ai/model-settings.d.ts.map +1 -1
- package/dist/utils/ai/model-settings.js +7 -1
- package/dist/utils/ai/model-settings.js.map +1 -1
- package/dist/utils/ai/token-usage.d.ts +4 -5
- package/dist/utils/ai/token-usage.d.ts.map +1 -1
- package/dist/utils/ai/token-usage.js +3 -3
- package/dist/utils/ai/token-usage.js.map +1 -1
- package/dist/utils/conversation/conversation-utils.d.ts.map +1 -1
- package/dist/utils/conversation/conversation-utils.js +3 -0
- package/dist/utils/conversation/conversation-utils.js.map +1 -1
- package/dist/utils/settings-command.d.ts.map +1 -1
- package/dist/utils/settings-command.js +2 -1
- package/dist/utils/settings-command.js.map +1 -1
- package/dist/utils/shell/sandbox/sandbox-policy.d.ts +2 -0
- package/dist/utils/shell/sandbox/sandbox-policy.d.ts.map +1 -1
- package/dist/utils/shell/sandbox/sandbox-policy.js +1 -1
- package/dist/utils/shell/sandbox/sandbox-policy.js.map +1 -1
- package/dist/utils/streaming/streaming-session-factory.d.ts.map +1 -1
- package/dist/utils/streaming/streaming-session-factory.js +22 -22
- package/dist/utils/streaming/streaming-session-factory.js.map +1 -1
- package/dist/utils/value-suggestions.js +1 -1
- package/dist/utils/value-suggestions.js.map +1 -1
- package/package.json +1 -1
- package/dist/components/input/MentorPoolMenuSession.d.ts +0 -9
- package/dist/components/input/MentorPoolMenuSession.d.ts.map +0 -1
- package/dist/components/input/MentorPoolMenuSession.js.map +0 -1
- package/dist/components/menu/MentorPoolSelectionMenu.d.ts +0 -13
- package/dist/components/menu/MentorPoolSelectionMenu.d.ts.map +0 -1
- package/dist/components/menu/MentorPoolSelectionMenu.js.map +0 -1
- package/dist/hooks/use-mentor-pool-selection.d.ts +0 -105
- package/dist/hooks/use-mentor-pool-selection.d.ts.map +0 -1
- package/dist/hooks/use-mentor-pool-selection.js.map +0 -1
package/dist/gateway/gateway.js
CHANGED
|
@@ -7,12 +7,17 @@ import { createSafeLogMetadata, GatewayAuditLog } from './safe-log.js';
|
|
|
7
7
|
import { AssertionVerifier } from './assertion.js';
|
|
8
8
|
import { SqliteReplayLedger } from './replay-ledger.js';
|
|
9
9
|
import { GatewayServer } from './server.js';
|
|
10
|
+
import { RuntimeFactoryError } from './runtime-factory.js';
|
|
11
|
+
import { projectConversationMessage } from '../services/conversation/conversation-message-projection.js';
|
|
12
|
+
import { normalizeRunItem } from '../services/conversation/run-item-normalizer.js';
|
|
10
13
|
import { FROZEN_AGENT_EVENT_TYPES, GatewayPersistenceError, TERMINAL_AGENT_EVENT_TYPES, } from './persistence/contracts.js';
|
|
11
|
-
import { createSessionProjectionSource, } from './persistence/projection.js';
|
|
14
|
+
import { createSessionProjectionSource, hydrateConversationService, } from './persistence/projection.js';
|
|
12
15
|
import { decideInteraction, InteractionProtocolError, projectPendingInteraction, validatePendingInteractionDto, } from './interaction-protocol.js';
|
|
13
|
-
import { GatewayAdmissionPersistence } from './persistence/admission-persistence.js';
|
|
16
|
+
import { GatewayAdmissionPersistence, normalizedBodyHash } from './persistence/admission-persistence.js';
|
|
14
17
|
import { GATEWAY_EVENT_HEARTBEAT_INTERVAL_MS, GATEWAY_EVENT_STREAM_CONTENT_TYPE, GATEWAY_EVENT_STREAM_HEARTBEAT, } from './server.js';
|
|
15
18
|
import { ServerSession } from './server-session.js';
|
|
19
|
+
import { createSessionSettingsSnapshot } from './launcher-seam.js';
|
|
20
|
+
import { readSessionSnapshot, writeSessionSnapshot } from './persistence/session-snapshot.js';
|
|
16
21
|
import { ServerSessionError } from './server-session.js';
|
|
17
22
|
import { DynamicWorkspaceRegistry, DynamicWorkspaceRegistryError } from './dynamic-workspace-registry.js';
|
|
18
23
|
import { accessSync, constants as fsConstants, realpathSync, statSync } from 'node:fs';
|
|
@@ -28,7 +33,30 @@ import { loginToCodex } from '../providers/codex-auth.js';
|
|
|
28
33
|
import { loginToGrok } from '../providers/grok-auth.js';
|
|
29
34
|
const MAX_BUFFERED_SSE_EVENTS = 256;
|
|
30
35
|
const NONCRITICAL_RUNTIME_EVENT_TYPES = new Set(['text_delta', 'reasoning_delta']);
|
|
36
|
+
let malformedCommandMessageDrops = 0;
|
|
31
37
|
const isCriticalRuntimeEvent = (event) => !NONCRITICAL_RUNTIME_EVENT_TYPES.has(event.type);
|
|
38
|
+
/**
|
|
39
|
+
* `retry-turn` replays the canonical transcript verbatim, so a retry is only
|
|
40
|
+
* meaningful while that transcript still ends on the user message of a turn
|
|
41
|
+
* that never produced output. Assistant output or a tool result after the last
|
|
42
|
+
* genuine user message means the turn committed something; synthetic user
|
|
43
|
+
* items (shell context, mode notices, local summaries) are not turns.
|
|
44
|
+
*/
|
|
45
|
+
const hasFailedTurnToRetry = (history) => {
|
|
46
|
+
for (let index = history.length - 1; index >= 0; index -= 1) {
|
|
47
|
+
const item = history[index];
|
|
48
|
+
if (normalizeRunItem(item).some((normalized) => normalized.type === 'tool_result'))
|
|
49
|
+
return false;
|
|
50
|
+
const message = projectConversationMessage(item);
|
|
51
|
+
if (!message || message.isSynthetic)
|
|
52
|
+
continue;
|
|
53
|
+
if (message.role === 'user')
|
|
54
|
+
return true;
|
|
55
|
+
if (message.role === 'assistant')
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
};
|
|
32
60
|
const INTERACTION_METRICS = [
|
|
33
61
|
'interaction_presented',
|
|
34
62
|
'interaction_updated',
|
|
@@ -91,6 +119,37 @@ const PUBLIC_EVENT_PAYLOAD_KEYS = new Set([
|
|
|
91
119
|
'riskLevel',
|
|
92
120
|
'deniedRead',
|
|
93
121
|
'runBudgetEvidence',
|
|
122
|
+
'agentId',
|
|
123
|
+
'name',
|
|
124
|
+
'task',
|
|
125
|
+
'async',
|
|
126
|
+
'toolCallId',
|
|
127
|
+
'commandMessages',
|
|
128
|
+
'status',
|
|
129
|
+
'output',
|
|
130
|
+
'error',
|
|
131
|
+
'finalText',
|
|
132
|
+
'finalTextTruncated',
|
|
133
|
+
'toolsUsed',
|
|
134
|
+
'count',
|
|
135
|
+
'terminalCause',
|
|
136
|
+
'provider',
|
|
137
|
+
'attempt',
|
|
138
|
+
'maxRetries',
|
|
139
|
+
'errorMessage',
|
|
140
|
+
'retryType',
|
|
141
|
+
'errorKind',
|
|
142
|
+
'delayMs',
|
|
143
|
+
'retryAfterMs',
|
|
144
|
+
'attempts',
|
|
145
|
+
'maxAttempts',
|
|
146
|
+
'canRetry',
|
|
147
|
+
'sessionId',
|
|
148
|
+
'inputTokensBefore',
|
|
149
|
+
'inputTokensAfter',
|
|
150
|
+
'durationMs',
|
|
151
|
+
'strategy',
|
|
152
|
+
'errorCategory',
|
|
94
153
|
]);
|
|
95
154
|
const LOCAL_OWNER_PURPOSES = new Set([
|
|
96
155
|
'settings_read',
|
|
@@ -188,8 +247,15 @@ export class Term2Gateway {
|
|
|
188
247
|
#persisted = new Map();
|
|
189
248
|
#admissions;
|
|
190
249
|
#interactionBindings = new Map();
|
|
250
|
+
#childTurnIds = new Map();
|
|
251
|
+
#childRoles = new Map();
|
|
252
|
+
#lastTurnIds = new Map();
|
|
191
253
|
#interactionCounters = new Map();
|
|
192
254
|
#eventPersistenceTails = new Map();
|
|
255
|
+
#inflightCommands = new Map();
|
|
256
|
+
// Keyed by owner and session: an in-flight revival belongs to the owner who
|
|
257
|
+
// started it, and no other caller may inherit its result.
|
|
258
|
+
#sessionRestores = new Map();
|
|
193
259
|
#shutdownInProgress = false;
|
|
194
260
|
#shutdownPromise;
|
|
195
261
|
#server;
|
|
@@ -234,6 +300,7 @@ export class Term2Gateway {
|
|
|
234
300
|
otpTtlMs: config.pairing.otpTtlMs,
|
|
235
301
|
maxAttempts: config.pairing.maxAttempts,
|
|
236
302
|
trustStore: trustedStore,
|
|
303
|
+
...(config.pairing.printOtp ? { printOtp: config.pairing.printOtp } : {}),
|
|
237
304
|
})
|
|
238
305
|
: undefined;
|
|
239
306
|
this.#audit = new GatewayAuditLog(config.auditWriter);
|
|
@@ -373,6 +440,8 @@ export class Term2Gateway {
|
|
|
373
440
|
return await this.#submitMessage(claims, input.body);
|
|
374
441
|
if (claims.purpose === 'abort')
|
|
375
442
|
return await this.#abortSession(claims, input.body);
|
|
443
|
+
if (claims.purpose === 'command_invoke')
|
|
444
|
+
return await this.#invokeCommand(claims, input.body, input.correlationId);
|
|
376
445
|
if (claims.purpose === 'interaction_resolve')
|
|
377
446
|
return await this.#resolveInteraction(claims, route.interactionId, input.body);
|
|
378
447
|
if (claims.purpose === 'events_connect')
|
|
@@ -672,6 +741,7 @@ export class Term2Gateway {
|
|
|
672
741
|
turnId,
|
|
673
742
|
variant: dto.variant,
|
|
674
743
|
dto,
|
|
744
|
+
owner: 'root',
|
|
675
745
|
};
|
|
676
746
|
this.#interactionBindings.set(sessionId, binding);
|
|
677
747
|
this.#countInteraction('interaction_presented');
|
|
@@ -680,6 +750,56 @@ export class Term2Gateway {
|
|
|
680
750
|
#countInteraction(metric) {
|
|
681
751
|
this.#interactionCounters.set(metric, (this.#interactionCounters.get(metric) ?? 0) + 1);
|
|
682
752
|
}
|
|
753
|
+
#backgroundInteractionBinding(sessionId, turnId, event, session) {
|
|
754
|
+
const publicInteractionId = crypto.randomUUID();
|
|
755
|
+
if (event.type === 'subagent_approval_required') {
|
|
756
|
+
const snapshot = session.service.backgroundSubagentApprovals.getSnapshot();
|
|
757
|
+
const entry = snapshot.current;
|
|
758
|
+
if (!entry)
|
|
759
|
+
return undefined;
|
|
760
|
+
const dto = projectPendingInteraction({
|
|
761
|
+
agentName: event.role,
|
|
762
|
+
toolName: entry.toolName,
|
|
763
|
+
callId: entry.toolCallId,
|
|
764
|
+
argumentsText: entry.argumentsText,
|
|
765
|
+
}, publicInteractionId, 1);
|
|
766
|
+
const binding = {
|
|
767
|
+
publicInteractionId,
|
|
768
|
+
expectedInteractionId: 0,
|
|
769
|
+
continuationGeneration: crypto.randomUUID(),
|
|
770
|
+
revision: 1,
|
|
771
|
+
turnId,
|
|
772
|
+
variant: dto.variant,
|
|
773
|
+
dto,
|
|
774
|
+
owner: 'background_approval',
|
|
775
|
+
childRunId: event.agentId,
|
|
776
|
+
childRole: event.role,
|
|
777
|
+
backgroundEntry: entry,
|
|
778
|
+
backgroundRevision: snapshot.revision,
|
|
779
|
+
};
|
|
780
|
+
this.#interactionBindings.set(sessionId, binding);
|
|
781
|
+
return binding;
|
|
782
|
+
}
|
|
783
|
+
const argumentsText = JSON.stringify({
|
|
784
|
+
questions: [{ question: event.question, options: [], is_multi_select: false }],
|
|
785
|
+
});
|
|
786
|
+
const dto = projectPendingInteraction({ agentName: event.role, toolName: 'ask_user', argumentsText }, publicInteractionId, 1);
|
|
787
|
+
const binding = {
|
|
788
|
+
publicInteractionId,
|
|
789
|
+
expectedInteractionId: 0,
|
|
790
|
+
continuationGeneration: crypto.randomUUID(),
|
|
791
|
+
revision: 1,
|
|
792
|
+
turnId,
|
|
793
|
+
variant: dto.variant,
|
|
794
|
+
dto,
|
|
795
|
+
owner: 'background_question',
|
|
796
|
+
childRunId: event.runId,
|
|
797
|
+
childMessageId: event.messageId,
|
|
798
|
+
childRole: event.role,
|
|
799
|
+
};
|
|
800
|
+
this.#interactionBindings.set(sessionId, binding);
|
|
801
|
+
return binding;
|
|
802
|
+
}
|
|
683
803
|
async #ensurePersistedSession(ownerUserId, sessionId) {
|
|
684
804
|
const existing = this.#persisted.get(sessionId);
|
|
685
805
|
if (existing) {
|
|
@@ -754,10 +874,25 @@ export class Term2Gateway {
|
|
|
754
874
|
return publicError(503, 'persistence_unavailable', 'gateway persistence unavailable', true);
|
|
755
875
|
if (!isMessageBody(body))
|
|
756
876
|
return publicError(400, 'validation_error', 'message request is invalid');
|
|
757
|
-
|
|
758
|
-
|
|
877
|
+
let session = this.#sessions.get(claims.sessionId);
|
|
878
|
+
let persisted = this.#persisted.get(claims.sessionId);
|
|
879
|
+
if (!(session instanceof ServerSession) || !persisted) {
|
|
880
|
+
// A session from a previous gateway process has no live runtime; revive
|
|
881
|
+
// it through the same factory path a fresh session uses.
|
|
882
|
+
const restored = await this.#ensureLiveRuntime(claims.sub, claims.sessionId);
|
|
883
|
+
if (restored && 'body' in restored)
|
|
884
|
+
return restored;
|
|
885
|
+
if (restored) {
|
|
886
|
+
session = restored.session;
|
|
887
|
+
persisted = restored.persisted;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
759
890
|
if (!(session instanceof ServerSession) || !persisted)
|
|
760
891
|
return publicError(409, 'session_not_admitting', 'session is not admitting messages');
|
|
892
|
+
// Owner scoping holds on every path that reaches admission, live or
|
|
893
|
+
// restored: a session owned by someone else does not exist for this caller.
|
|
894
|
+
if (session.binding.ownerUserId !== claims.sub)
|
|
895
|
+
return publicError(404, 'not_found', 'session not found');
|
|
761
896
|
const turnId = crypto.randomUUID();
|
|
762
897
|
try {
|
|
763
898
|
const result = await this.#admissions.admit({
|
|
@@ -850,6 +985,401 @@ export class Term2Gateway {
|
|
|
850
985
|
};
|
|
851
986
|
return publicError(409, 'session_not_admitting', 'session is interrupted');
|
|
852
987
|
}
|
|
988
|
+
async #invokeCommand(claims, body, correlationId) {
|
|
989
|
+
if (!isCommandBody(body))
|
|
990
|
+
return publicError(400, 'validation_error', 'command request is invalid');
|
|
991
|
+
let session = this.#sessions.get(claims.sessionId);
|
|
992
|
+
if (!session || session.binding.ownerUserId !== claims.sub) {
|
|
993
|
+
// A restored session has no live runtime; commands revive it the same
|
|
994
|
+
// way message_submit does. Sessions unknown to this owner keep the 404.
|
|
995
|
+
const restored = await this.#ensureLiveRuntime(claims.sub, claims.sessionId);
|
|
996
|
+
if (restored && 'body' in restored)
|
|
997
|
+
return restored;
|
|
998
|
+
if (restored)
|
|
999
|
+
session = restored.session;
|
|
1000
|
+
}
|
|
1001
|
+
if (!session || session.binding.ownerUserId !== claims.sub) {
|
|
1002
|
+
return publicError(404, 'not_found', 'session not found');
|
|
1003
|
+
}
|
|
1004
|
+
if (!(session instanceof ServerSession)) {
|
|
1005
|
+
return publicError(409, 'session_not_admitting', 'session is not admitting commands');
|
|
1006
|
+
}
|
|
1007
|
+
if (session.status === 'running' || session.status === 'awaiting_interaction') {
|
|
1008
|
+
return publicError(409, 'session_busy', 'session is busy');
|
|
1009
|
+
}
|
|
1010
|
+
if (!COMMAND_ALLOWLIST.has(body.commandId)) {
|
|
1011
|
+
return publicError(422, 'command_not_allowed', 'command is not allowed');
|
|
1012
|
+
}
|
|
1013
|
+
await this.#audit.write(createSafeLogMetadata({
|
|
1014
|
+
operation: 'command_invoke',
|
|
1015
|
+
outcome: 'allowed',
|
|
1016
|
+
reasonCode: 'accepted',
|
|
1017
|
+
sessionId: session.sessionId,
|
|
1018
|
+
workspaceId: session.binding.workspaceId,
|
|
1019
|
+
grantVersion: session.binding.grantVersion,
|
|
1020
|
+
access: session.binding.access,
|
|
1021
|
+
...(correlationId ? { correlationId } : {}),
|
|
1022
|
+
}));
|
|
1023
|
+
const inflightKey = `${claims.sub}:${session.sessionId}:${body.clientRequestId}`;
|
|
1024
|
+
const bodyHash = normalizedBodyHash(body);
|
|
1025
|
+
const inflight = this.#inflightCommands.get(inflightKey);
|
|
1026
|
+
if (inflight) {
|
|
1027
|
+
if (inflight.bodyHash !== bodyHash) {
|
|
1028
|
+
throw new GatewayPersistenceError('conflict', 'idempotency key has a different body');
|
|
1029
|
+
}
|
|
1030
|
+
const existing = await inflight.promise;
|
|
1031
|
+
return {
|
|
1032
|
+
...existing,
|
|
1033
|
+
body: {
|
|
1034
|
+
...existing.body,
|
|
1035
|
+
replayed: true,
|
|
1036
|
+
},
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
if (this.#admissions) {
|
|
1040
|
+
const lookup = this.#admissions.lookup(claims.sub, session.sessionId, body.clientRequestId, body);
|
|
1041
|
+
if (lookup.kind === 'conflict') {
|
|
1042
|
+
throw new GatewayPersistenceError('conflict', 'idempotency key has a different body');
|
|
1043
|
+
}
|
|
1044
|
+
if (lookup.kind === 'replayed') {
|
|
1045
|
+
if (body.commandId === 'compact') {
|
|
1046
|
+
const parts = lookup.record.turnId.split(':');
|
|
1047
|
+
if (lookup.record.result === 'failed' || parts[1] === 'failed') {
|
|
1048
|
+
return publicError(409, 'compact_failed', 'compaction failed', false);
|
|
1049
|
+
}
|
|
1050
|
+
if (parts[1] === 'in_progress') {
|
|
1051
|
+
return publicError(409, 'compact_interrupted', 'compaction was interrupted', false);
|
|
1052
|
+
}
|
|
1053
|
+
if (parts[1] !== 'completed' && parts[1] !== 'not_reduced' && parts[1] !== 'nothing_to_retry') {
|
|
1054
|
+
return publicError(500, 'compact_invalid_state', 'invalid compaction record state', false);
|
|
1055
|
+
}
|
|
1056
|
+
const outcome = parts[1];
|
|
1057
|
+
const tokensBefore = parts[2] ? Number(parts[2]) : undefined;
|
|
1058
|
+
const tokensAfter = parts[3] ? Number(parts[3]) : undefined;
|
|
1059
|
+
return {
|
|
1060
|
+
status: 200,
|
|
1061
|
+
body: {
|
|
1062
|
+
commandId: body.commandId,
|
|
1063
|
+
outcome,
|
|
1064
|
+
...(tokensBefore !== undefined && !Number.isNaN(tokensBefore) ? { tokensBefore } : {}),
|
|
1065
|
+
...(tokensAfter !== undefined && !Number.isNaN(tokensAfter) ? { tokensAfter } : {}),
|
|
1066
|
+
replayed: true,
|
|
1067
|
+
},
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
if (lookup.record.turnId === 'nothing_to_retry') {
|
|
1071
|
+
return {
|
|
1072
|
+
status: 200,
|
|
1073
|
+
body: {
|
|
1074
|
+
commandId: body.commandId,
|
|
1075
|
+
outcome: 'nothing_to_retry',
|
|
1076
|
+
replayed: true,
|
|
1077
|
+
},
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
if (lookup.record.result === 'failed') {
|
|
1081
|
+
return publicError(409, 'retry_failed', 'retry failed', false);
|
|
1082
|
+
}
|
|
1083
|
+
return {
|
|
1084
|
+
status: 200,
|
|
1085
|
+
body: {
|
|
1086
|
+
commandId: body.commandId,
|
|
1087
|
+
outcome: 'accepted',
|
|
1088
|
+
turnId: lookup.record.turnId,
|
|
1089
|
+
replayed: true,
|
|
1090
|
+
},
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
const persisted = this.#persisted.get(claims.sessionId);
|
|
1095
|
+
if (this.#config.persistence && !persisted) {
|
|
1096
|
+
return publicError(503, 'persistence_unavailable', 'gateway persistence unavailable', true);
|
|
1097
|
+
}
|
|
1098
|
+
const inflightPromise = (async () => {
|
|
1099
|
+
try {
|
|
1100
|
+
if (body.commandId === 'compact') {
|
|
1101
|
+
if (this.#admissions) {
|
|
1102
|
+
this.#admissions.prepare({
|
|
1103
|
+
ownerUserId: claims.sub,
|
|
1104
|
+
sessionId: session.sessionId,
|
|
1105
|
+
clientRequestId: body.clientRequestId,
|
|
1106
|
+
body,
|
|
1107
|
+
turnId: 'compact:in_progress',
|
|
1108
|
+
});
|
|
1109
|
+
this.#config.persistence?.index.updateAdmission(claims.sub, session.sessionId, body.clientRequestId, {
|
|
1110
|
+
state: 'accepted',
|
|
1111
|
+
result: 'accepted',
|
|
1112
|
+
phase: 'committed',
|
|
1113
|
+
turnId: 'compact:in_progress',
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
// The outcome is mapped from the typed compaction result, never from
|
|
1117
|
+
// result text, so the response agrees with the journaled compaction
|
|
1118
|
+
// events: failed compactions return { outcome: 'failed', reason } with
|
|
1119
|
+
// a bounded reason code, and 'nothing_to_retry' is reserved for
|
|
1120
|
+
// genuinely nothing to compact (contract 13 §8).
|
|
1121
|
+
let outcome;
|
|
1122
|
+
let tokensBefore;
|
|
1123
|
+
let tokensAfter;
|
|
1124
|
+
let failureReason;
|
|
1125
|
+
const compactTurnId = crypto.randomUUID();
|
|
1126
|
+
try {
|
|
1127
|
+
const result = await session.withCommandTurn(compactTurnId, () => session.service.compactContextDetailed());
|
|
1128
|
+
switch (result.kind) {
|
|
1129
|
+
case 'compacted': {
|
|
1130
|
+
tokensBefore = result.tokensBefore;
|
|
1131
|
+
tokensAfter = result.tokensAfter;
|
|
1132
|
+
// A compaction that grew the context is not a completed one; the
|
|
1133
|
+
// numbers come from the same estimator on both sides.
|
|
1134
|
+
outcome =
|
|
1135
|
+
tokensBefore !== undefined && tokensAfter !== undefined && tokensAfter >= tokensBefore
|
|
1136
|
+
? 'not_reduced'
|
|
1137
|
+
: 'completed';
|
|
1138
|
+
break;
|
|
1139
|
+
}
|
|
1140
|
+
case 'nothing_to_compact':
|
|
1141
|
+
outcome = 'nothing_to_retry';
|
|
1142
|
+
break;
|
|
1143
|
+
case 'failed':
|
|
1144
|
+
outcome = 'failed';
|
|
1145
|
+
failureReason = result.reason;
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
if (this.#admissions) {
|
|
1149
|
+
const turnId = outcome === 'failed'
|
|
1150
|
+
? `compact:failed:${failureReason}`
|
|
1151
|
+
: `compact:${outcome}:${tokensBefore ?? ''}:${tokensAfter ?? ''}`;
|
|
1152
|
+
this.#config.persistence?.index.updateAdmission(claims.sub, session.sessionId, body.clientRequestId, {
|
|
1153
|
+
state: 'terminal',
|
|
1154
|
+
result: outcome === 'failed' ? 'failed' : 'accepted',
|
|
1155
|
+
phase: 'committed',
|
|
1156
|
+
turnId,
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
catch (error) {
|
|
1161
|
+
if (this.#admissions) {
|
|
1162
|
+
try {
|
|
1163
|
+
this.#config.persistence?.index.updateAdmission(claims.sub, session.sessionId, body.clientRequestId, {
|
|
1164
|
+
state: 'terminal',
|
|
1165
|
+
result: 'failed',
|
|
1166
|
+
phase: 'committed',
|
|
1167
|
+
turnId: 'compact:failed',
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
catch {
|
|
1171
|
+
// ignore
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
throw error;
|
|
1175
|
+
}
|
|
1176
|
+
return {
|
|
1177
|
+
status: 200,
|
|
1178
|
+
body: {
|
|
1179
|
+
commandId: 'compact',
|
|
1180
|
+
outcome,
|
|
1181
|
+
...(outcome === 'failed' ? { reason: failureReason } : {}),
|
|
1182
|
+
...(tokensBefore !== undefined ? { tokensBefore } : {}),
|
|
1183
|
+
...(tokensAfter !== undefined ? { tokensAfter } : {}),
|
|
1184
|
+
},
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
if (body.commandId === 'retry-tool') {
|
|
1188
|
+
const lastTool = session.service.peekLastToolOutput();
|
|
1189
|
+
if (!lastTool) {
|
|
1190
|
+
if (this.#admissions) {
|
|
1191
|
+
this.#admissions.prepare({
|
|
1192
|
+
ownerUserId: claims.sub,
|
|
1193
|
+
sessionId: session.sessionId,
|
|
1194
|
+
clientRequestId: body.clientRequestId,
|
|
1195
|
+
body,
|
|
1196
|
+
turnId: 'nothing_to_retry',
|
|
1197
|
+
});
|
|
1198
|
+
this.#config.persistence?.index.updateAdmission(claims.sub, session.sessionId, body.clientRequestId, {
|
|
1199
|
+
state: 'terminal',
|
|
1200
|
+
result: 'accepted',
|
|
1201
|
+
phase: 'committed',
|
|
1202
|
+
turnId: 'nothing_to_retry',
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
return {
|
|
1206
|
+
status: 200,
|
|
1207
|
+
body: {
|
|
1208
|
+
commandId: 'retry-tool',
|
|
1209
|
+
outcome: 'nothing_to_retry',
|
|
1210
|
+
},
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
const turnId = crypto.randomUUID();
|
|
1214
|
+
if (this.#admissions && persisted) {
|
|
1215
|
+
const runtime = session.createRetryRuntime('retry-tool');
|
|
1216
|
+
try {
|
|
1217
|
+
const result = await this.#admissions.admit({
|
|
1218
|
+
ownerUserId: claims.sub,
|
|
1219
|
+
sessionId: session.sessionId,
|
|
1220
|
+
clientRequestId: body.clientRequestId,
|
|
1221
|
+
body,
|
|
1222
|
+
turnId,
|
|
1223
|
+
runtime,
|
|
1224
|
+
persistence: persisted.persistence.critical,
|
|
1225
|
+
acceptedEvent: {
|
|
1226
|
+
sessionId: session.sessionId,
|
|
1227
|
+
type: 'assistant_started',
|
|
1228
|
+
payload: { turnId, clientRequestId: body.clientRequestId },
|
|
1229
|
+
},
|
|
1230
|
+
});
|
|
1231
|
+
if (result.kind === 'rejected') {
|
|
1232
|
+
return result.reason === 'queue_full'
|
|
1233
|
+
? publicError(429, 'queue_full', 'message queue is full', true)
|
|
1234
|
+
: publicError(409, 'session_not_admitting', 'session is not admitting commands');
|
|
1235
|
+
}
|
|
1236
|
+
return {
|
|
1237
|
+
status: 200,
|
|
1238
|
+
body: {
|
|
1239
|
+
commandId: 'retry-tool',
|
|
1240
|
+
outcome: 'accepted',
|
|
1241
|
+
turnId: result.record.turnId,
|
|
1242
|
+
...(result.replayed ? { replayed: true } : {}),
|
|
1243
|
+
},
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
catch (error) {
|
|
1247
|
+
try {
|
|
1248
|
+
await this.#enqueueEventPersistence(session.sessionId, async () => {
|
|
1249
|
+
const events = persisted.persistence.journal.events();
|
|
1250
|
+
const hasStarted = events.some((e) => e.type === 'assistant_started' && e.payload?.turnId === turnId);
|
|
1251
|
+
const hasTerminal = events.some((e) => (e.type === 'turn_completed' || e.type === 'turn_failed' || e.type === 'turn_aborted') &&
|
|
1252
|
+
e.payload?.turnId === turnId);
|
|
1253
|
+
if (hasStarted && !hasTerminal) {
|
|
1254
|
+
await persisted.persistence.critical.appendJournalCritical({
|
|
1255
|
+
sessionId: session.sessionId,
|
|
1256
|
+
type: 'turn_failed',
|
|
1257
|
+
payload: { turnId, outcome: 'failed', reason: 'runtime_error' },
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
catch {
|
|
1263
|
+
// ignore secondary journal error
|
|
1264
|
+
}
|
|
1265
|
+
return this.#mapError(error);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
void session.service.retryLastToolOutput({ preferredMessageId: turnId }).catch(() => undefined);
|
|
1269
|
+
return {
|
|
1270
|
+
status: 200,
|
|
1271
|
+
body: {
|
|
1272
|
+
commandId: 'retry-tool',
|
|
1273
|
+
outcome: 'accepted',
|
|
1274
|
+
turnId,
|
|
1275
|
+
},
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
if (body.commandId === 'retry-turn') {
|
|
1279
|
+
const state = session.service.exportState();
|
|
1280
|
+
if (!hasFailedTurnToRetry(state.history)) {
|
|
1281
|
+
if (this.#admissions) {
|
|
1282
|
+
this.#admissions.prepare({
|
|
1283
|
+
ownerUserId: claims.sub,
|
|
1284
|
+
sessionId: session.sessionId,
|
|
1285
|
+
clientRequestId: body.clientRequestId,
|
|
1286
|
+
body,
|
|
1287
|
+
turnId: 'nothing_to_retry',
|
|
1288
|
+
});
|
|
1289
|
+
this.#config.persistence?.index.updateAdmission(claims.sub, session.sessionId, body.clientRequestId, {
|
|
1290
|
+
state: 'terminal',
|
|
1291
|
+
result: 'accepted',
|
|
1292
|
+
phase: 'committed',
|
|
1293
|
+
turnId: 'nothing_to_retry',
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
return {
|
|
1297
|
+
status: 200,
|
|
1298
|
+
body: {
|
|
1299
|
+
commandId: 'retry-turn',
|
|
1300
|
+
outcome: 'nothing_to_retry',
|
|
1301
|
+
},
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
const turnId = crypto.randomUUID();
|
|
1305
|
+
if (this.#admissions && persisted) {
|
|
1306
|
+
const runtime = session.createRetryRuntime('retry-turn');
|
|
1307
|
+
try {
|
|
1308
|
+
const result = await this.#admissions.admit({
|
|
1309
|
+
ownerUserId: claims.sub,
|
|
1310
|
+
sessionId: session.sessionId,
|
|
1311
|
+
clientRequestId: body.clientRequestId,
|
|
1312
|
+
body,
|
|
1313
|
+
turnId,
|
|
1314
|
+
runtime,
|
|
1315
|
+
persistence: persisted.persistence.critical,
|
|
1316
|
+
acceptedEvent: {
|
|
1317
|
+
sessionId: session.sessionId,
|
|
1318
|
+
type: 'assistant_started',
|
|
1319
|
+
payload: { turnId, clientRequestId: body.clientRequestId },
|
|
1320
|
+
},
|
|
1321
|
+
});
|
|
1322
|
+
if (result.kind === 'rejected') {
|
|
1323
|
+
return result.reason === 'queue_full'
|
|
1324
|
+
? publicError(429, 'queue_full', 'message queue is full', true)
|
|
1325
|
+
: publicError(409, 'session_not_admitting', 'session is not admitting commands');
|
|
1326
|
+
}
|
|
1327
|
+
return {
|
|
1328
|
+
status: 200,
|
|
1329
|
+
body: {
|
|
1330
|
+
commandId: 'retry-turn',
|
|
1331
|
+
outcome: 'accepted',
|
|
1332
|
+
turnId: result.record.turnId,
|
|
1333
|
+
...(result.replayed ? { replayed: true } : {}),
|
|
1334
|
+
},
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
catch (error) {
|
|
1338
|
+
try {
|
|
1339
|
+
await this.#enqueueEventPersistence(session.sessionId, async () => {
|
|
1340
|
+
const events = persisted.persistence.journal.events();
|
|
1341
|
+
const hasStarted = events.some((e) => e.type === 'assistant_started' && e.payload?.turnId === turnId);
|
|
1342
|
+
const hasTerminal = events.some((e) => (e.type === 'turn_completed' || e.type === 'turn_failed' || e.type === 'turn_aborted') &&
|
|
1343
|
+
e.payload?.turnId === turnId);
|
|
1344
|
+
if (hasStarted && !hasTerminal) {
|
|
1345
|
+
await persisted.persistence.critical.appendJournalCritical({
|
|
1346
|
+
sessionId: session.sessionId,
|
|
1347
|
+
type: 'turn_failed',
|
|
1348
|
+
payload: { turnId, outcome: 'failed', reason: 'runtime_error' },
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
catch {
|
|
1354
|
+
// ignore secondary journal error
|
|
1355
|
+
}
|
|
1356
|
+
return this.#mapError(error);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
void session.service.retryLastFailedTurn({ preferredMessageId: turnId }).catch(() => undefined);
|
|
1360
|
+
return {
|
|
1361
|
+
status: 200,
|
|
1362
|
+
body: {
|
|
1363
|
+
commandId: 'retry-turn',
|
|
1364
|
+
outcome: 'accepted',
|
|
1365
|
+
turnId,
|
|
1366
|
+
},
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
return publicError(422, 'command_not_allowed', 'command is not allowed');
|
|
1370
|
+
}
|
|
1371
|
+
finally {
|
|
1372
|
+
this.#inflightCommands.delete(inflightKey);
|
|
1373
|
+
}
|
|
1374
|
+
})();
|
|
1375
|
+
this.#inflightCommands.set(inflightKey, { promise: inflightPromise, bodyHash });
|
|
1376
|
+
try {
|
|
1377
|
+
return await inflightPromise;
|
|
1378
|
+
}
|
|
1379
|
+
catch (error) {
|
|
1380
|
+
return this.#mapError(error);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
853
1383
|
async #resolveInteraction(claims, interactionId, body) {
|
|
854
1384
|
if (!isInteractionResolveRequest(body))
|
|
855
1385
|
return publicError(400, 'validation_error', 'interaction request is invalid');
|
|
@@ -857,6 +1387,62 @@ export class Term2Gateway {
|
|
|
857
1387
|
const session = this.#sessions.get(claims.sessionId);
|
|
858
1388
|
const binding = this.#interactionBindings.get(claims.sessionId);
|
|
859
1389
|
const snapshot = session instanceof ServerSession ? session.service.getPendingInteractionSnapshot() : null;
|
|
1390
|
+
if (session instanceof ServerSession &&
|
|
1391
|
+
binding?.publicInteractionId === interactionId &&
|
|
1392
|
+
binding.owner !== 'root') {
|
|
1393
|
+
if (body.revision !== binding.revision)
|
|
1394
|
+
return publicError(409, 'stale_interaction', 'interaction is stale');
|
|
1395
|
+
let decision;
|
|
1396
|
+
try {
|
|
1397
|
+
decision = decideInteraction(binding.dto, body);
|
|
1398
|
+
}
|
|
1399
|
+
catch (error) {
|
|
1400
|
+
if (error instanceof InteractionProtocolError)
|
|
1401
|
+
return publicError(400, 'validation_error', 'interaction decision is invalid');
|
|
1402
|
+
throw error;
|
|
1403
|
+
}
|
|
1404
|
+
let accepted = false;
|
|
1405
|
+
if (binding.owner === 'background_approval' &&
|
|
1406
|
+
binding.backgroundEntry &&
|
|
1407
|
+
binding.backgroundRevision !== undefined) {
|
|
1408
|
+
const current = session.service.backgroundSubagentApprovals.getSnapshot();
|
|
1409
|
+
if (current.revision !== binding.backgroundRevision || current.current?.runId !== binding.backgroundEntry.runId)
|
|
1410
|
+
return publicError(409, 'stale_interaction', 'interaction is stale');
|
|
1411
|
+
const result = session.service.backgroundSubagentApprovals.resolve({
|
|
1412
|
+
revision: binding.backgroundRevision,
|
|
1413
|
+
entry: binding.backgroundEntry,
|
|
1414
|
+
decision: { answer: decision.answer, rejectionReason: decision.rejectionReason },
|
|
1415
|
+
});
|
|
1416
|
+
accepted = result.kind === 'resolved';
|
|
1417
|
+
}
|
|
1418
|
+
else if (binding.owner === 'background_question' && binding.childRunId && binding.childMessageId) {
|
|
1419
|
+
accepted = session.service.answerBackgroundSubagentQuestion(binding.childRunId, binding.childMessageId, decision.approvalAnswer ?? decision.answer);
|
|
1420
|
+
}
|
|
1421
|
+
if (!accepted)
|
|
1422
|
+
return publicError(409, 'stale_interaction', 'interaction is stale');
|
|
1423
|
+
this.#countInteraction('interaction_resolved');
|
|
1424
|
+
await this.#enqueueEventPersistence(claims.sessionId, async () => {
|
|
1425
|
+
const persisted = this.#persisted.get(claims.sessionId);
|
|
1426
|
+
if (!persisted)
|
|
1427
|
+
throw new GatewayPersistenceError('not_found', 'session persistence is unavailable');
|
|
1428
|
+
persisted.persistence.interactionCheckpoint.clear();
|
|
1429
|
+
await persisted.persistence.journal.append({
|
|
1430
|
+
sessionId: claims.sessionId,
|
|
1431
|
+
type: 'interaction_resolved',
|
|
1432
|
+
payload: {
|
|
1433
|
+
turnId: binding.turnId,
|
|
1434
|
+
interactionId,
|
|
1435
|
+
outcome: decision.outcome,
|
|
1436
|
+
variant: binding.variant,
|
|
1437
|
+
},
|
|
1438
|
+
}, { durability: 'critical' });
|
|
1439
|
+
});
|
|
1440
|
+
this.#interactionBindings.delete(claims.sessionId);
|
|
1441
|
+
return {
|
|
1442
|
+
status: 202,
|
|
1443
|
+
body: { sessionId: claims.sessionId, turnId: binding.turnId, interactionId, accepted: true },
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
860
1446
|
// A live runtime owns the current interaction. A public ID from an older
|
|
861
1447
|
// presentation is stale even when the durable journal still says the
|
|
862
1448
|
// interaction was recovered during startup.
|
|
@@ -1153,13 +1739,32 @@ export class Term2Gateway {
|
|
|
1153
1739
|
return;
|
|
1154
1740
|
const session = this.#sessions.get(sessionId);
|
|
1155
1741
|
const snapshot = session instanceof ServerSession ? session.service.getPendingInteractionSnapshot() : null;
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1742
|
+
const childBinding = session instanceof ServerSession &&
|
|
1743
|
+
(event.type === 'subagent_approval_required' || event.type === 'subagent_question')
|
|
1744
|
+
? this.#backgroundInteractionBinding(sessionId, context.turnId, event, session)
|
|
1158
1745
|
: undefined;
|
|
1746
|
+
const binding = childBinding ??
|
|
1747
|
+
((event.type === 'approval_required' || event.type === 'subagent_approval_required') && snapshot
|
|
1748
|
+
? this.#interactionBindingFor(sessionId, snapshot, context.turnId)
|
|
1749
|
+
: undefined);
|
|
1750
|
+
if (childBinding) {
|
|
1751
|
+
persisted.persistence.interactionCheckpoint.save({
|
|
1752
|
+
turnId: childBinding.turnId,
|
|
1753
|
+
interaction: childBinding.dto,
|
|
1754
|
+
revision: childBinding.revision,
|
|
1755
|
+
generation: childBinding.continuationGeneration,
|
|
1756
|
+
settleOnRecovery: 'child_interrupted',
|
|
1757
|
+
...(childBinding.childRunId
|
|
1758
|
+
? { child: { agentId: childBinding.childRunId, role: childBinding.childRole ?? 'subagent' } }
|
|
1759
|
+
: {}),
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1159
1762
|
// An ask_user answer can advance the live binding before the original
|
|
1160
1763
|
// awaitable approval sink reaches this queue. Do not republish that
|
|
1161
1764
|
// old presentation as a second approval after interaction_updated.
|
|
1162
|
-
if (event.type === 'approval_required'
|
|
1765
|
+
if ((event.type === 'approval_required' || event.type === 'subagent_approval_required') &&
|
|
1766
|
+
binding?.revision &&
|
|
1767
|
+
binding.revision > 1)
|
|
1163
1768
|
return;
|
|
1164
1769
|
const mapped = mapConversationEvent(event, context.turnId, sessionId, binding, snapshot ?? undefined);
|
|
1165
1770
|
if (!mapped)
|
|
@@ -1231,6 +1836,8 @@ export class Term2Gateway {
|
|
|
1231
1836
|
return publicError(403, 'workspace_forbidden', 'workspace access denied');
|
|
1232
1837
|
}
|
|
1233
1838
|
if (error instanceof GatewayPersistenceError) {
|
|
1839
|
+
if (error.code === 'snapshot_unwritable')
|
|
1840
|
+
return publicError(503, 'snapshot_unwritable', 'session snapshot could not be persisted', true);
|
|
1234
1841
|
if (error.code === 'not_found' || error.code === 'owner_mismatch')
|
|
1235
1842
|
return publicError(404, 'not_found', 'session not found');
|
|
1236
1843
|
if (error.code === 'cursor_invalid')
|
|
@@ -1244,6 +1851,15 @@ export class Term2Gateway {
|
|
|
1244
1851
|
return publicError(409, 'stale_interaction', 'interaction is stale');
|
|
1245
1852
|
return publicError(409, 'session_not_admitting', 'session is not admitting');
|
|
1246
1853
|
}
|
|
1854
|
+
if (error instanceof RuntimeFactoryError) {
|
|
1855
|
+
if (error.code === 'resource_limit')
|
|
1856
|
+
return publicError(429, 'resource_exhausted', 'session resource limit reached', true);
|
|
1857
|
+
if (error.code === 'provider_unavailable')
|
|
1858
|
+
return publicError(409, 'provider_unavailable', 'session provider is unavailable');
|
|
1859
|
+
if (error.code === 'invalid_binding')
|
|
1860
|
+
return publicError(409, 'session_not_admitting', 'session is not admitting');
|
|
1861
|
+
return publicError(503, 'gateway_unavailable', 'gateway unavailable', true);
|
|
1862
|
+
}
|
|
1247
1863
|
return publicError(503, 'gateway_unavailable', 'gateway unavailable', true);
|
|
1248
1864
|
}
|
|
1249
1865
|
static create(config) {
|
|
@@ -1286,64 +1902,348 @@ export class Term2Gateway {
|
|
|
1286
1902
|
throw new GatewayStartupError();
|
|
1287
1903
|
const binding = this.#admission.admit(claims);
|
|
1288
1904
|
if (this.#config.runtimeFactory) {
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1905
|
+
const { session } = await this.#composeRuntimeBackedSession(binding, {
|
|
1906
|
+
auditOperation: 'session_create',
|
|
1907
|
+
ownedBinding: true,
|
|
1908
|
+
correlationId,
|
|
1909
|
+
});
|
|
1910
|
+
return session;
|
|
1911
|
+
}
|
|
1912
|
+
return this.#composeLegacyFixtureSession(claims, options, binding);
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Compose a live runtime-backed session through the RuntimeFactory: open (or
|
|
1916
|
+
* reuse) the persistence handle, create the runtime, then attach the shared
|
|
1917
|
+
* gateway wiring (dispose hooks, lifecycle registration, event and
|
|
1918
|
+
* background-subagent sinks). Fresh sessions and restart-restored sessions
|
|
1919
|
+
* take the same path; a restored session additionally hydrates its history
|
|
1920
|
+
* from the persisted transcript before the runtime becomes reachable.
|
|
1921
|
+
*/
|
|
1922
|
+
async #composeRuntimeBackedSession(binding, options) {
|
|
1923
|
+
const runtimeFactory = this.#config.runtimeFactory;
|
|
1924
|
+
if (!runtimeFactory)
|
|
1925
|
+
throw new GatewayStartupError();
|
|
1926
|
+
const authority = runtimeFactory.settingsAuthority;
|
|
1927
|
+
// A restored session runs on its persisted snapshot; a fresh session
|
|
1928
|
+
// captures the launcher's current one. Either way the factory receives an
|
|
1929
|
+
// explicit snapshot so it can never silently re-read a changed default.
|
|
1930
|
+
const runtimeSnapshot = options.snapshot ??
|
|
1931
|
+
(authority
|
|
1932
|
+
? createSessionSettingsSnapshot({
|
|
1933
|
+
settings: authority,
|
|
1934
|
+
effectiveToolPolicy: {
|
|
1935
|
+
allowWrite: this.#config.runtimeFactory?.allowWriteEnabled === true && binding.access === 'read_write',
|
|
1303
1936
|
},
|
|
1304
|
-
})
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1937
|
+
})
|
|
1938
|
+
: undefined);
|
|
1939
|
+
let persisted = this.#persisted.get(binding.sessionId);
|
|
1940
|
+
let createdSession;
|
|
1941
|
+
try {
|
|
1942
|
+
if (!persisted) {
|
|
1943
|
+
persisted = await this.#config.persistence?.open(binding);
|
|
1944
|
+
}
|
|
1945
|
+
const session = await runtimeFactory.create(binding, {
|
|
1946
|
+
settingsSnapshot: runtimeSnapshot,
|
|
1947
|
+
eventSink: (event, context) => {
|
|
1948
|
+
if (context.turnId)
|
|
1949
|
+
this.#lastTurnIds.set(binding.sessionId, context.turnId);
|
|
1950
|
+
if (event.type === 'subagent_started' && event.agentId && context.turnId)
|
|
1951
|
+
this.#childTurnIds.set(event.agentId, context.turnId);
|
|
1952
|
+
if (event.type === 'subagent_approval_required')
|
|
1953
|
+
this.#childRoles.set(event.agentId, event.role);
|
|
1954
|
+
const persistence = this.#persistConversationEvent(binding.sessionId, event, context);
|
|
1955
|
+
if (isCriticalRuntimeEvent(event))
|
|
1956
|
+
return persistence;
|
|
1957
|
+
// Streaming deltas remain observationally non-blocking, but their
|
|
1958
|
+
// persistence failure must be consumed so it cannot become an
|
|
1959
|
+
// unhandled rejection. A later critical event observes the
|
|
1960
|
+
// journal's failure latch and is awaited by the runtime.
|
|
1961
|
+
void persistence.catch(() => undefined);
|
|
1962
|
+
return undefined;
|
|
1963
|
+
},
|
|
1964
|
+
});
|
|
1965
|
+
createdSession = session;
|
|
1966
|
+
options.hydrate?.(session);
|
|
1967
|
+
if (runtimeSnapshot && !options.snapshot && persisted) {
|
|
1968
|
+
// Session-creation durability: a session must not become reachable
|
|
1969
|
+
// unless its provider/model identity is durably recorded, or a restart
|
|
1970
|
+
// could silently substitute the launcher's current default. Failure
|
|
1971
|
+
// rolls the whole composition back in the catch below.
|
|
1972
|
+
try {
|
|
1973
|
+
await writeSessionSnapshot(persisted.persistence.directory, {
|
|
1974
|
+
schemaVersion: 1,
|
|
1975
|
+
providerId: runtimeSnapshot.providerId,
|
|
1976
|
+
modelId: runtimeSnapshot.modelId,
|
|
1977
|
+
reasoningEffort: runtimeSnapshot.reasoningEffort,
|
|
1312
1978
|
});
|
|
1313
1979
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1980
|
+
catch {
|
|
1981
|
+
throw new GatewayPersistenceError('snapshot_unwritable', 'session provider/model snapshot could not be persisted');
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
if (persisted) {
|
|
1985
|
+
this.#persisted.set(binding.sessionId, persisted);
|
|
1986
|
+
session.addDisposeHook(async () => {
|
|
1987
|
+
await persisted.persistence.close();
|
|
1319
1988
|
this.#persisted.delete(binding.sessionId);
|
|
1320
1989
|
this.#interactionBindings.delete(binding.sessionId);
|
|
1990
|
+
this.#lastTurnIds.delete(binding.sessionId);
|
|
1991
|
+
await this.#config.persistence?.close(binding.sessionId, this.#shutdownInProgress ? 'interrupted' : 'closed');
|
|
1321
1992
|
});
|
|
1322
|
-
this.#sessions.set(binding.sessionId, session);
|
|
1323
|
-
await this.#audit.write(createSafeLogMetadata({
|
|
1324
|
-
operation: 'session_create',
|
|
1325
|
-
outcome: 'allowed',
|
|
1326
|
-
reasonCode: 'accepted',
|
|
1327
|
-
sessionId: binding.sessionId,
|
|
1328
|
-
workspaceId: binding.workspaceId,
|
|
1329
|
-
grantVersion: binding.grantVersion,
|
|
1330
|
-
access: binding.access,
|
|
1331
|
-
...(correlationId ? { correlationId } : {}),
|
|
1332
|
-
}));
|
|
1333
|
-
return session;
|
|
1334
1993
|
}
|
|
1335
|
-
|
|
1994
|
+
const release = this.#lifecycle.registerWorker({ close: () => session.dispose() });
|
|
1995
|
+
session.addDisposeHook(() => {
|
|
1996
|
+
release();
|
|
1997
|
+
this.#admission.remove(binding.sessionId);
|
|
1998
|
+
this.#sessions.delete(binding.sessionId);
|
|
1999
|
+
this.#persisted.delete(binding.sessionId);
|
|
2000
|
+
this.#interactionBindings.delete(binding.sessionId);
|
|
2001
|
+
});
|
|
2002
|
+
this.#sessions.set(binding.sessionId, session);
|
|
2003
|
+
session.service.setBackgroundSubagentEventSink((event) => {
|
|
2004
|
+
const agentId = event.type === 'subagent_question'
|
|
2005
|
+
? event.runId
|
|
2006
|
+
: event.type === 'subagent_completed'
|
|
2007
|
+
? event.result.agentId
|
|
2008
|
+
: event.agentId;
|
|
2009
|
+
if (event.type === 'subagent_started' && event.agentId && session.activeTurnId) {
|
|
2010
|
+
this.#childTurnIds.set(event.agentId, session.activeTurnId);
|
|
2011
|
+
this.#childRoles.set(event.agentId, event.role);
|
|
2012
|
+
}
|
|
2013
|
+
const turnId = agentId
|
|
2014
|
+
? this.#childTurnIds.get(agentId) ?? session.activeTurnId ?? this.#lastTurnIds.get(binding.sessionId)
|
|
2015
|
+
: session.activeTurnId ?? this.#lastTurnIds.get(binding.sessionId);
|
|
2016
|
+
if (turnId)
|
|
2017
|
+
void this.#persistConversationEvent(binding.sessionId, event, { turnId, discardedTurnIds: [] });
|
|
2018
|
+
});
|
|
2019
|
+
session.service.backgroundSubagentApprovals.subscribe(() => {
|
|
2020
|
+
const current = session.service.backgroundSubagentApprovals.getSnapshot().current;
|
|
2021
|
+
if (!current)
|
|
2022
|
+
return;
|
|
2023
|
+
const turnId = this.#childTurnIds.get(current.runId);
|
|
2024
|
+
if (!turnId)
|
|
2025
|
+
return;
|
|
2026
|
+
void this.#persistConversationEvent(binding.sessionId, {
|
|
2027
|
+
type: 'subagent_approval_required',
|
|
2028
|
+
agentId: current.runId,
|
|
2029
|
+
role: this.#childRoles.get(current.runId) ?? 'subagent',
|
|
2030
|
+
}, { turnId, discardedTurnIds: [] });
|
|
2031
|
+
});
|
|
2032
|
+
await this.#audit.write(createSafeLogMetadata({
|
|
2033
|
+
operation: options.auditOperation,
|
|
2034
|
+
outcome: 'allowed',
|
|
2035
|
+
reasonCode: 'accepted',
|
|
2036
|
+
sessionId: binding.sessionId,
|
|
2037
|
+
workspaceId: binding.workspaceId,
|
|
2038
|
+
grantVersion: binding.grantVersion,
|
|
2039
|
+
access: binding.access,
|
|
2040
|
+
...(options.correlationId ? { correlationId: options.correlationId } : {}),
|
|
2041
|
+
}));
|
|
2042
|
+
return { session, persisted };
|
|
2043
|
+
}
|
|
2044
|
+
catch (error) {
|
|
2045
|
+
// The runtime was created but the composition failed (snapshot
|
|
2046
|
+
// durability, hydration, ...): dispose it before the persistence it
|
|
2047
|
+
// may still write to.
|
|
2048
|
+
if (createdSession) {
|
|
2049
|
+
try {
|
|
2050
|
+
await createdSession.dispose();
|
|
2051
|
+
}
|
|
2052
|
+
catch {
|
|
2053
|
+
// Preserve the original composition failure.
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
// Roll the persistence handle back whoever opened it — composition
|
|
2057
|
+
// opened it, or #ensurePersistedSession installed it before the failed
|
|
2058
|
+
// restore. A failed restore must not leave an open handle in #persisted;
|
|
2059
|
+
// the read path and the next revival reopen on demand.
|
|
2060
|
+
if (persisted) {
|
|
1336
2061
|
try {
|
|
1337
|
-
await persisted
|
|
2062
|
+
await persisted.persistence.close();
|
|
1338
2063
|
await this.#config.persistence?.close(binding.sessionId, 'interrupted');
|
|
1339
2064
|
}
|
|
1340
2065
|
catch {
|
|
1341
|
-
// Preserve the
|
|
2066
|
+
// Preserve the original composition failure; persistence keeps its evidence.
|
|
1342
2067
|
}
|
|
2068
|
+
this.#persisted.delete(binding.sessionId);
|
|
2069
|
+
}
|
|
2070
|
+
if (options.ownedBinding)
|
|
1343
2071
|
this.#admission.remove(binding.sessionId);
|
|
2072
|
+
throw error;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
/**
|
|
2076
|
+
* Lazily revive a persisted session whose runtime died with a previous
|
|
2077
|
+
* gateway process. Returns the live runtime, a specific gateway error for a
|
|
2078
|
+
* refused revival (revoked grant, unavailable provider or model, resource
|
|
2079
|
+
* limits), or null when there is nothing this gateway can restore — callers
|
|
2080
|
+
* keep their legacy not-found/not-admitting behavior for null. Concurrent
|
|
2081
|
+
* first revivals of one session share a single creation promise.
|
|
2082
|
+
*/
|
|
2083
|
+
async #ensureLiveRuntime(ownerUserId, sessionId, correlationId) {
|
|
2084
|
+
const existing = this.#sessions.get(sessionId);
|
|
2085
|
+
const existingPersisted = this.#persisted.get(sessionId);
|
|
2086
|
+
if (existing instanceof ServerSession) {
|
|
2087
|
+
// Owner scoping holds on every return path, live or restored: a session
|
|
2088
|
+
// owned by someone else does not exist for this caller.
|
|
2089
|
+
if (existing.binding.ownerUserId !== ownerUserId)
|
|
2090
|
+
return publicError(404, 'not_found', 'session not found');
|
|
2091
|
+
if (existingPersisted)
|
|
2092
|
+
return { session: existing, persisted: existingPersisted };
|
|
2093
|
+
}
|
|
2094
|
+
if (!this.#config.runtimeFactory || !this.#config.persistence)
|
|
2095
|
+
return null;
|
|
2096
|
+
// A persisted session owned by another caller is a 404 for this caller,
|
|
2097
|
+
// not the legacy fallback: the index is owner-scoped for a reason. This
|
|
2098
|
+
// also stops a foreign caller from latching onto an in-flight revival.
|
|
2099
|
+
let foreign;
|
|
2100
|
+
try {
|
|
2101
|
+
foreign = this.#config.persistence.index.get(sessionId);
|
|
2102
|
+
}
|
|
2103
|
+
catch {
|
|
2104
|
+
foreign = null;
|
|
2105
|
+
}
|
|
2106
|
+
if (foreign && foreign.ownerUserId !== ownerUserId)
|
|
2107
|
+
return publicError(404, 'not_found', 'session not found');
|
|
2108
|
+
const restoreKey = `${ownerUserId}\u0000${sessionId}`;
|
|
2109
|
+
const guardOwner = (result) => {
|
|
2110
|
+
if (result && 'session' in result && result.session.binding.ownerUserId !== ownerUserId)
|
|
2111
|
+
return publicError(404, 'not_found', 'session not found');
|
|
2112
|
+
return result;
|
|
2113
|
+
};
|
|
2114
|
+
const inflight = this.#sessionRestores.get(restoreKey);
|
|
2115
|
+
if (inflight)
|
|
2116
|
+
return inflight.then(guardOwner);
|
|
2117
|
+
const restore = this.#restoreRuntime(ownerUserId, sessionId, correlationId);
|
|
2118
|
+
this.#sessionRestores.set(restoreKey, restore);
|
|
2119
|
+
restore.then(() => {
|
|
2120
|
+
if (this.#sessionRestores.get(restoreKey) === restore)
|
|
2121
|
+
this.#sessionRestores.delete(restoreKey);
|
|
2122
|
+
}, () => {
|
|
2123
|
+
// A rejected revival must clear the single-flight entry too, or the
|
|
2124
|
+
// session would stay wedged forever; everyone awaiting the shared
|
|
2125
|
+
// promise already holds the rejection, so swallow it here.
|
|
2126
|
+
if (this.#sessionRestores.get(restoreKey) === restore)
|
|
2127
|
+
this.#sessionRestores.delete(restoreKey);
|
|
2128
|
+
});
|
|
2129
|
+
return restore.then(guardOwner);
|
|
2130
|
+
}
|
|
2131
|
+
async #restoreRuntime(ownerUserId, sessionId, correlationId) {
|
|
2132
|
+
try {
|
|
2133
|
+
const index = this.#config.persistence.index;
|
|
2134
|
+
let record;
|
|
2135
|
+
try {
|
|
2136
|
+
record = index.getForOwner(ownerUserId, sessionId);
|
|
2137
|
+
}
|
|
2138
|
+
catch (error) {
|
|
2139
|
+
if (error instanceof GatewayPersistenceError && error.code === 'not_found')
|
|
2140
|
+
return null;
|
|
1344
2141
|
throw error;
|
|
1345
2142
|
}
|
|
2143
|
+
if (record.status === 'closed')
|
|
2144
|
+
return null;
|
|
2145
|
+
const grantVersion = Number(record.grantVersion);
|
|
2146
|
+
if (!Number.isSafeInteger(grantVersion))
|
|
2147
|
+
throw new GatewayPersistenceError('readonly', 'session grant version is invalid');
|
|
2148
|
+
// The admission layer is the grant authority: manifest and restored
|
|
2149
|
+
// dynamic grants, enabled state, and the workspace boundary probe all
|
|
2150
|
+
// revalidate before any runtime exists. A revoked grant fails here with
|
|
2151
|
+
// a typed admission error instead of a generic not-found.
|
|
2152
|
+
const binding = this.#admission.restore(sessionId, ownerUserId, record.workspaceId, grantVersion);
|
|
2153
|
+
const persisted = await this.#ensurePersistedSession(ownerUserId, sessionId);
|
|
2154
|
+
const snapshot = await this.#restoredSessionSnapshot(persisted, binding);
|
|
2155
|
+
if (snapshot && 'body' in snapshot)
|
|
2156
|
+
return snapshot;
|
|
2157
|
+
const unavailable = await this.#sessionSnapshotUnavailable(snapshot);
|
|
2158
|
+
if (unavailable)
|
|
2159
|
+
return unavailable;
|
|
2160
|
+
if (this.#lifecycle.state !== 'running')
|
|
2161
|
+
return publicError(503, 'gateway_unavailable', 'gateway is not accepting sessions', true);
|
|
2162
|
+
const directory = this.#config.persistence?.layout.existingSessionPath(binding.ownerUserId, binding.workspaceId, binding.sessionId);
|
|
2163
|
+
const composed = await this.#composeRuntimeBackedSession(binding, {
|
|
2164
|
+
auditOperation: 'session_resume',
|
|
2165
|
+
ownedBinding: false,
|
|
2166
|
+
correlationId,
|
|
2167
|
+
snapshot,
|
|
2168
|
+
hydrate: (session) => {
|
|
2169
|
+
if (directory)
|
|
2170
|
+
hydrateConversationService(session.service, directory, binding.sessionId);
|
|
2171
|
+
// Revival is complete: the coarse index lifecycle returns to the
|
|
2172
|
+
// resting state a fresh session holds; live turn state stays in the
|
|
2173
|
+
// ServerSession. The interrupted turn stays interrupted in the
|
|
2174
|
+
// journal; the next turn is a normal turn.
|
|
2175
|
+
this.#config.persistence?.index.update(binding.sessionId, {
|
|
2176
|
+
status: 'idle',
|
|
2177
|
+
interruptedAt: null,
|
|
2178
|
+
recoveryWarning: null,
|
|
2179
|
+
});
|
|
2180
|
+
},
|
|
2181
|
+
});
|
|
2182
|
+
return { session: composed.session, persisted };
|
|
2183
|
+
}
|
|
2184
|
+
catch (error) {
|
|
2185
|
+
return this.#mapError(error);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* The snapshot a restored session must come back on: the per-session
|
|
2190
|
+
* provider/model recorded when the session was created. Sessions created
|
|
2191
|
+
* before that record existed (or whose record is unreadable) fall back to
|
|
2192
|
+
* the launcher's current snapshot; the fallback is documented and is still
|
|
2193
|
+
* validated below, so it is never a silent substitution of an unavailable
|
|
2194
|
+
* provider or model. Only the production stack (settings authority) has a
|
|
2195
|
+
* snapshot to restore against; fixture gateways compose without one.
|
|
2196
|
+
*/
|
|
2197
|
+
async #restoredSessionSnapshot(persisted, binding) {
|
|
2198
|
+
const settings = this.#config.runtimeFactory?.settingsAuthority;
|
|
2199
|
+
if (!settings)
|
|
2200
|
+
return undefined;
|
|
2201
|
+
const stored = await readSessionSnapshot(persisted.persistence.directory);
|
|
2202
|
+
// Absent is the documented pre-change compatibility state; a record that
|
|
2203
|
+
// exists but cannot be trusted is corruption and is refused — falling
|
|
2204
|
+
// back would silently change the session's provider/model identity.
|
|
2205
|
+
if (stored.state === 'corrupt')
|
|
2206
|
+
return publicError(500, 'session_snapshot_invalid', 'persisted session snapshot is unreadable or malformed');
|
|
2207
|
+
const allowWrite = this.#config.runtimeFactory?.allowWriteEnabled === true;
|
|
2208
|
+
const effectiveAllowWrite = allowWrite && binding.access === 'read_write';
|
|
2209
|
+
if (stored.state === 'absent')
|
|
2210
|
+
return createSessionSettingsSnapshot({ settings, effectiveToolPolicy: { allowWrite: effectiveAllowWrite } });
|
|
2211
|
+
return createSessionSettingsSnapshot({
|
|
2212
|
+
settings,
|
|
2213
|
+
providerId: stored.snapshot.providerId,
|
|
2214
|
+
modelId: stored.snapshot.modelId,
|
|
2215
|
+
reasoningEffort: stored.snapshot.reasoningEffort,
|
|
2216
|
+
effectiveToolPolicy: { allowWrite: effectiveAllowWrite },
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
/**
|
|
2220
|
+
* A restored session replays into the runtime built from its snapshot.
|
|
2221
|
+
* Refuse instead of silently substituting when that snapshot's provider or
|
|
2222
|
+
* model is no longer available. Only the production stack (settings
|
|
2223
|
+
* authority + model catalog) can answer this; fixture gateways skip the
|
|
2224
|
+
* check.
|
|
2225
|
+
*/
|
|
2226
|
+
async #sessionSnapshotUnavailable(snapshot) {
|
|
2227
|
+
const settings = this.#config.runtimeFactory?.settingsAuthority;
|
|
2228
|
+
const catalog = this.#modelCatalog;
|
|
2229
|
+
if (!settings || !catalog || !snapshot)
|
|
2230
|
+
return null;
|
|
2231
|
+
const providerId = String(snapshot.providerId);
|
|
2232
|
+
if (!getAvailableProviderIds(settings, getProviderIds()).includes(providerId))
|
|
2233
|
+
return publicError(409, 'provider_unavailable', 'session provider is no longer available');
|
|
2234
|
+
let models;
|
|
2235
|
+
try {
|
|
2236
|
+
({ models } = await catalog.load(providerId));
|
|
2237
|
+
}
|
|
2238
|
+
catch {
|
|
2239
|
+
return publicError(503, 'model_catalog_unavailable', 'model catalog unavailable', true);
|
|
1346
2240
|
}
|
|
2241
|
+
if (!models.some((model) => model.id === snapshot.modelId))
|
|
2242
|
+
return publicError(409, 'model_unavailable', 'session model is no longer available');
|
|
2243
|
+
return null;
|
|
2244
|
+
}
|
|
2245
|
+
/** Legacy fixture composition without the production runtime factory. */
|
|
2246
|
+
async #composeLegacyFixtureSession(_claims, options, binding, correlationId) {
|
|
1347
2247
|
const providerProbe = this.#config.providerProbe;
|
|
1348
2248
|
if (!providerProbe)
|
|
1349
2249
|
throw new GatewayStartupError();
|
|
@@ -1378,6 +2278,17 @@ export class Term2Gateway {
|
|
|
1378
2278
|
}));
|
|
1379
2279
|
return session;
|
|
1380
2280
|
}
|
|
2281
|
+
/**
|
|
2282
|
+
* Shutdown must not fail because its audit record could not be built or written, but
|
|
2283
|
+
* losing an audit record is not silent: it goes to the launcher-supplied diagnostics
|
|
2284
|
+
* sink when the launcher configured one.
|
|
2285
|
+
*/
|
|
2286
|
+
#reportShutdownAuditFailure(error) {
|
|
2287
|
+
this.#config.logger?.warn('gateway shutdown audit failed', {
|
|
2288
|
+
errorCode: 'shutdown_audit_failed',
|
|
2289
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
1381
2292
|
async shutdown(graceMs = 5_000) {
|
|
1382
2293
|
if (this.#shutdownPromise)
|
|
1383
2294
|
return this.#shutdownPromise;
|
|
@@ -1413,14 +2324,36 @@ export class Term2Gateway {
|
|
|
1413
2324
|
if (this.#config.runtimeFactory && !(await waitBounded(this.#config.runtimeFactory.shutdown(remaining()))))
|
|
1414
2325
|
markForced();
|
|
1415
2326
|
this.#sessions.clear();
|
|
2327
|
+
// Close persistence handles that outlived their session: read-path opens
|
|
2328
|
+
// and compositions that failed before installing a dispose hook would
|
|
2329
|
+
// otherwise keep file descriptors and locks past shutdown.
|
|
2330
|
+
for (const [sessionId, persisted] of this.#persisted) {
|
|
2331
|
+
try {
|
|
2332
|
+
await waitBounded(persisted.persistence.close());
|
|
2333
|
+
await waitBounded(this.#config.persistence?.close(sessionId, 'interrupted') ?? Promise.resolve());
|
|
2334
|
+
}
|
|
2335
|
+
catch {
|
|
2336
|
+
// Shutdown proceeds; the interrupted status is recorded in the index.
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
this.#persisted.clear();
|
|
1416
2340
|
this.#replay.close();
|
|
1417
|
-
// The audit record is bounded by the same deadline. A broken audit sink
|
|
1418
|
-
//
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
2341
|
+
// The audit record is bounded by the same deadline. A broken audit sink must not
|
|
2342
|
+
// keep the local owner process alive indefinitely, and neither a synchronous
|
|
2343
|
+
// build failure nor a rejected write may turn a finished shutdown into a
|
|
2344
|
+
// rejection for the caller: the audit is best-effort, but never silent.
|
|
2345
|
+
try {
|
|
2346
|
+
await waitBounded(this.#audit
|
|
2347
|
+
.write(createSafeLogMetadata({
|
|
2348
|
+
operation: 'shutdown',
|
|
2349
|
+
outcome: forced ? 'interrupted' : 'allowed',
|
|
2350
|
+
reasonCode: forced ? 'forced_shutdown' : 'shutdown',
|
|
2351
|
+
}))
|
|
2352
|
+
.catch((error) => this.#reportShutdownAuditFailure(error)));
|
|
2353
|
+
}
|
|
2354
|
+
catch (error) {
|
|
2355
|
+
this.#reportShutdownAuditFailure(error);
|
|
2356
|
+
}
|
|
1424
2357
|
})();
|
|
1425
2358
|
return this.#shutdownPromise;
|
|
1426
2359
|
}
|
|
@@ -1461,7 +2394,7 @@ function matchPrivateRoute(method, pathname) {
|
|
|
1461
2394
|
return { purpose: 'session_create' };
|
|
1462
2395
|
if (method === 'GET' && pathname === '/private/agent/v1/sessions')
|
|
1463
2396
|
return { purpose: 'session_list' };
|
|
1464
|
-
const match = pathname.match(/^\/private\/agent\/v1\/sessions\/([A-Za-z0-9_-]{1,256})(?:\/(messages|abort|events|config|interactions\/([A-Za-z0-9_-]{1,256})))?$/);
|
|
2397
|
+
const match = pathname.match(/^\/private\/agent\/v1\/sessions\/([A-Za-z0-9_-]{1,256})(?:\/(messages|abort|events|config|commands|interactions\/([A-Za-z0-9_-]{1,256})))?$/);
|
|
1465
2398
|
if (!match)
|
|
1466
2399
|
return null;
|
|
1467
2400
|
const sessionId = match[1];
|
|
@@ -1475,6 +2408,8 @@ function matchPrivateRoute(method, pathname) {
|
|
|
1475
2408
|
return { purpose: 'events_connect', sessionId };
|
|
1476
2409
|
if (method === 'POST' && match[2] === 'config')
|
|
1477
2410
|
return { purpose: 'session_update', sessionId };
|
|
2411
|
+
if (method === 'POST' && match[2] === 'commands')
|
|
2412
|
+
return { purpose: 'command_invoke', sessionId };
|
|
1478
2413
|
if (method === 'POST' && match[2]?.startsWith('interactions/'))
|
|
1479
2414
|
return { purpose: 'interaction_resolve', sessionId, interactionId: match[3] };
|
|
1480
2415
|
return null;
|
|
@@ -1672,6 +2607,17 @@ function isTurnBody(body) {
|
|
|
1672
2607
|
Object.keys(body).length === 1 &&
|
|
1673
2608
|
isOpaqueId(body.turnId));
|
|
1674
2609
|
}
|
|
2610
|
+
const COMMAND_ALLOWLIST = new Set(['compact', 'retry-tool', 'retry-turn']);
|
|
2611
|
+
function isCommandBody(body) {
|
|
2612
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
2613
|
+
return false;
|
|
2614
|
+
const value = body;
|
|
2615
|
+
return (Object.keys(value).length === 2 &&
|
|
2616
|
+
typeof value.commandId === 'string' &&
|
|
2617
|
+
value.commandId.length > 0 &&
|
|
2618
|
+
value.commandId.length <= 256 &&
|
|
2619
|
+
isOpaqueId(value.clientRequestId));
|
|
2620
|
+
}
|
|
1675
2621
|
export function isInteractionResolveRequest(body) {
|
|
1676
2622
|
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
1677
2623
|
return false;
|
|
@@ -1721,7 +2667,9 @@ function isPublicEventEnvelope(event) {
|
|
|
1721
2667
|
try {
|
|
1722
2668
|
if (event.type === 'approval_required' ||
|
|
1723
2669
|
event.type === 'interaction_updated' ||
|
|
1724
|
-
event.type === 'interaction_recovered'
|
|
2670
|
+
event.type === 'interaction_recovered' ||
|
|
2671
|
+
event.type === 'subagent_approval_required' ||
|
|
2672
|
+
event.type === 'subagent_question') {
|
|
1725
2673
|
if (!event.payload.interaction ||
|
|
1726
2674
|
typeof event.payload.interaction !== 'object' ||
|
|
1727
2675
|
!validatePendingInteractionDto(event.payload.interaction))
|
|
@@ -1758,6 +2706,98 @@ function isPublicEventEnvelope(event) {
|
|
|
1758
2706
|
function boundedText(value, max = 8_192) {
|
|
1759
2707
|
return typeof value === 'string' ? value.slice(0, max) : '';
|
|
1760
2708
|
}
|
|
2709
|
+
function safeCounter(value) {
|
|
2710
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
|
2711
|
+
? Math.min(Math.floor(value), Number.MAX_SAFE_INTEGER)
|
|
2712
|
+
: 0;
|
|
2713
|
+
}
|
|
2714
|
+
function boundedInteger(value, max) {
|
|
2715
|
+
const number = safeCounter(value);
|
|
2716
|
+
return Math.min(number, max);
|
|
2717
|
+
}
|
|
2718
|
+
function normalizedUsagePayload(usage) {
|
|
2719
|
+
const inputTokens = safeCounter(usage.prompt_tokens);
|
|
2720
|
+
const outputTokens = safeCounter(usage.completion_tokens);
|
|
2721
|
+
return { inputTokens, outputTokens, totalTokens: safeCounter(usage.total_tokens) || inputTokens + outputTokens };
|
|
2722
|
+
}
|
|
2723
|
+
function commandStatus(value) {
|
|
2724
|
+
switch (value) {
|
|
2725
|
+
case 'pending':
|
|
2726
|
+
case 'running':
|
|
2727
|
+
return value;
|
|
2728
|
+
case 'completed':
|
|
2729
|
+
return 'completed';
|
|
2730
|
+
case 'failed':
|
|
2731
|
+
return 'failed';
|
|
2732
|
+
case 'cancelled':
|
|
2733
|
+
case 'interrupted':
|
|
2734
|
+
case 'aborted':
|
|
2735
|
+
return 'aborted';
|
|
2736
|
+
case 'backgrounded':
|
|
2737
|
+
return 'running';
|
|
2738
|
+
default:
|
|
2739
|
+
return null;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
function commandMessagePayload(input) {
|
|
2743
|
+
const message = input;
|
|
2744
|
+
const callId = typeof message.callId === 'string' ? message.callId : '';
|
|
2745
|
+
const toolName = typeof message.toolName === 'string' ? message.toolName : '';
|
|
2746
|
+
const status = commandStatus(message.status);
|
|
2747
|
+
if (!callId || !toolName || !status) {
|
|
2748
|
+
malformedCommandMessageDrops = Math.min(malformedCommandMessageDrops + 1, 1_000_000);
|
|
2749
|
+
return null;
|
|
2750
|
+
}
|
|
2751
|
+
return {
|
|
2752
|
+
callId: boundedText(callId, 256),
|
|
2753
|
+
toolName: boundedText(toolName, 256),
|
|
2754
|
+
status,
|
|
2755
|
+
...(typeof message.output === 'string' ? { output: boundedText(message.output, 16_384) } : {}),
|
|
2756
|
+
...(typeof message.failureReason === 'string' ? { error: boundedText(message.failureReason, 10_000) } : {}),
|
|
2757
|
+
message: {
|
|
2758
|
+
id: boundedText(message.id, 256),
|
|
2759
|
+
role: 'command',
|
|
2760
|
+
text: boundedText(message.command, 16_384),
|
|
2761
|
+
},
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
export function publicEventProjectionCounters() {
|
|
2765
|
+
return Object.freeze({ malformedCommandMessageDrops });
|
|
2766
|
+
}
|
|
2767
|
+
function commandMessageCandidate(base, message, type) {
|
|
2768
|
+
const projected = commandMessagePayload(message);
|
|
2769
|
+
if (!projected)
|
|
2770
|
+
return null;
|
|
2771
|
+
return {
|
|
2772
|
+
...base,
|
|
2773
|
+
type,
|
|
2774
|
+
payload: { ...base.payload, ...projected },
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
function errorReason(kind) {
|
|
2778
|
+
switch (kind) {
|
|
2779
|
+
case 'provider':
|
|
2780
|
+
return 'provider_error';
|
|
2781
|
+
case 'network':
|
|
2782
|
+
return 'network_error';
|
|
2783
|
+
case 'rate_limit':
|
|
2784
|
+
return 'rate_limit';
|
|
2785
|
+
case 'authentication':
|
|
2786
|
+
return 'authentication_error';
|
|
2787
|
+
case 'cancelled':
|
|
2788
|
+
return 'cancelled';
|
|
2789
|
+
case 'validation':
|
|
2790
|
+
return 'validation_error';
|
|
2791
|
+
case 'compaction':
|
|
2792
|
+
return 'compaction_failed';
|
|
2793
|
+
case 'retry_exhausted':
|
|
2794
|
+
return 'retry_exhausted';
|
|
2795
|
+
case 'interaction_continuation_failed':
|
|
2796
|
+
return 'interaction_continuation_failed';
|
|
2797
|
+
default:
|
|
2798
|
+
return 'runtime_error';
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
1761
2801
|
function interactionDtoFromApproval(approval, interactionId, revision) {
|
|
1762
2802
|
return projectPendingInteraction(approval, interactionId, revision);
|
|
1763
2803
|
}
|
|
@@ -1873,9 +2913,19 @@ function hasTerminalTurnEvent(events, turnId) {
|
|
|
1873
2913
|
}
|
|
1874
2914
|
function terminalTranscriptFact(event, turnId) {
|
|
1875
2915
|
if (event.type === 'final') {
|
|
2916
|
+
const finalText = boundedText(event.finalText, 16_384);
|
|
1876
2917
|
const items = event.turnItems ? [...event.turnItems] : [];
|
|
1877
|
-
|
|
1878
|
-
|
|
2918
|
+
const existingText = items.findIndex((item) => item.type === 'assistant_text');
|
|
2919
|
+
if (existingText >= 0) {
|
|
2920
|
+
// Derived turn items may carry an empty assistant_text entry when the
|
|
2921
|
+
// stream produced no text parts; the terminal finalText is the
|
|
2922
|
+
// authoritative text for the persisted turn.
|
|
2923
|
+
const existing = items[existingText];
|
|
2924
|
+
if (!existing.text && finalText)
|
|
2925
|
+
items[existingText] = { ...existing, text: finalText };
|
|
2926
|
+
}
|
|
2927
|
+
else if (finalText) {
|
|
2928
|
+
items.push({ type: 'assistant_text', text: finalText });
|
|
1879
2929
|
}
|
|
1880
2930
|
return { type: 'assistant_turn', turnId, turn: { items } };
|
|
1881
2931
|
}
|
|
@@ -1888,7 +2938,7 @@ function terminalTranscriptFact(event, turnId) {
|
|
|
1888
2938
|
}
|
|
1889
2939
|
return null;
|
|
1890
2940
|
}
|
|
1891
|
-
function mapConversationEvent(event, turnId, sessionId, interactionBinding, interactionSnapshot) {
|
|
2941
|
+
export function mapConversationEvent(event, turnId, sessionId, interactionBinding, interactionSnapshot) {
|
|
1892
2942
|
const base = { sessionId, payload: { turnId } };
|
|
1893
2943
|
switch (event.type) {
|
|
1894
2944
|
// The gateway-owned assistant_started admission marker is appended before
|
|
@@ -1905,18 +2955,7 @@ function mapConversationEvent(event, turnId, sessionId, interactionBinding, inte
|
|
|
1905
2955
|
payload: { turnId, callId: event.toolCallId, toolName: boundedText(event.toolName, 256) },
|
|
1906
2956
|
};
|
|
1907
2957
|
case 'command_message':
|
|
1908
|
-
return
|
|
1909
|
-
...base,
|
|
1910
|
-
type: 'command_message',
|
|
1911
|
-
payload: {
|
|
1912
|
-
turnId,
|
|
1913
|
-
message: {
|
|
1914
|
-
id: boundedText(event.message.id, 256),
|
|
1915
|
-
role: boundedText(event.message.sender ?? event.message.role, 32),
|
|
1916
|
-
text: boundedText(event.message.text, 16_384),
|
|
1917
|
-
},
|
|
1918
|
-
},
|
|
1919
|
-
};
|
|
2958
|
+
return commandMessageCandidate(base, event.message, 'command_message');
|
|
1920
2959
|
case 'approval_required': {
|
|
1921
2960
|
if (!interactionBinding)
|
|
1922
2961
|
return null;
|
|
@@ -1931,13 +2970,208 @@ function mapConversationEvent(event, turnId, sessionId, interactionBinding, inte
|
|
|
1931
2970
|
},
|
|
1932
2971
|
};
|
|
1933
2972
|
}
|
|
1934
|
-
case 'usage_update':
|
|
2973
|
+
case 'usage_update': {
|
|
2974
|
+
const inputTokens = safeCounter(event.usage.prompt_tokens);
|
|
2975
|
+
const outputTokens = safeCounter(event.usage.completion_tokens);
|
|
2976
|
+
const totalTokens = safeCounter(event.usage.total_tokens) || inputTokens + outputTokens;
|
|
1935
2977
|
return {
|
|
1936
2978
|
...base,
|
|
1937
2979
|
type: 'usage_update',
|
|
1938
2980
|
payload: {
|
|
1939
2981
|
turnId,
|
|
1940
|
-
|
|
2982
|
+
inputTokens,
|
|
2983
|
+
outputTokens,
|
|
2984
|
+
totalTokens,
|
|
2985
|
+
usage: { inputTokens, outputTokens, totalTokens },
|
|
2986
|
+
},
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
case 'retry':
|
|
2990
|
+
return {
|
|
2991
|
+
...base,
|
|
2992
|
+
type: 'retry',
|
|
2993
|
+
payload: {
|
|
2994
|
+
turnId,
|
|
2995
|
+
...(event.agentId ? { agentId: boundedText(event.agentId, 256) } : {}),
|
|
2996
|
+
toolName: boundedText(event.toolName, 256),
|
|
2997
|
+
attempt: boundedInteger(event.attempt, 512),
|
|
2998
|
+
maxRetries: boundedInteger(event.maxRetries, 512),
|
|
2999
|
+
errorMessage: boundedText(event.errorMessage),
|
|
3000
|
+
...(event.retryType ? { retryType: event.retryType } : {}),
|
|
3001
|
+
...(event.errorKind ? { errorKind: event.errorKind } : {}),
|
|
3002
|
+
...(event.delayMs !== undefined ? { delayMs: boundedInteger(event.delayMs, 86_400_000) } : {}),
|
|
3003
|
+
...(event.retryAfterMs !== undefined ? { retryAfterMs: boundedInteger(event.retryAfterMs, 86_400_000) } : {}),
|
|
3004
|
+
},
|
|
3005
|
+
};
|
|
3006
|
+
case 'retry_exhausted':
|
|
3007
|
+
return {
|
|
3008
|
+
...base,
|
|
3009
|
+
type: 'retry_exhausted',
|
|
3010
|
+
payload: {
|
|
3011
|
+
turnId,
|
|
3012
|
+
...(event.provider ? { provider: boundedText(event.provider, 256) } : {}),
|
|
3013
|
+
errorKind: event.errorKind,
|
|
3014
|
+
attempts: boundedInteger(event.attempts, 512),
|
|
3015
|
+
maxAttempts: boundedInteger(event.maxAttempts, 512),
|
|
3016
|
+
message: boundedText(event.message),
|
|
3017
|
+
canRetry: event.canRetry,
|
|
3018
|
+
},
|
|
3019
|
+
};
|
|
3020
|
+
case 'subagent_started':
|
|
3021
|
+
return {
|
|
3022
|
+
...base,
|
|
3023
|
+
type: 'subagent_started',
|
|
3024
|
+
payload: {
|
|
3025
|
+
turnId,
|
|
3026
|
+
agentId: boundedText(event.agentId, 256),
|
|
3027
|
+
...(event.name ? { name: boundedText(event.name, 256) } : {}),
|
|
3028
|
+
role: boundedText(event.role, 256),
|
|
3029
|
+
task: boundedText(event.task, 16_384),
|
|
3030
|
+
...(event.async !== undefined ? { async: event.async } : {}),
|
|
3031
|
+
},
|
|
3032
|
+
};
|
|
3033
|
+
case 'subagent_tool_started':
|
|
3034
|
+
return {
|
|
3035
|
+
...base,
|
|
3036
|
+
type: 'subagent_tool_started',
|
|
3037
|
+
payload: {
|
|
3038
|
+
turnId,
|
|
3039
|
+
agentId: boundedText(event.agentId, 256),
|
|
3040
|
+
role: boundedText(event.role, 256),
|
|
3041
|
+
toolCallId: boundedText(event.toolCallId, 256),
|
|
3042
|
+
toolName: boundedText(event.toolName, 256),
|
|
3043
|
+
...(event.commandMessages
|
|
3044
|
+
? {
|
|
3045
|
+
commandMessages: event.commandMessages
|
|
3046
|
+
.slice(0, 64)
|
|
3047
|
+
.map((message) => commandMessagePayload(message))
|
|
3048
|
+
.filter(Boolean),
|
|
3049
|
+
}
|
|
3050
|
+
: {}),
|
|
3051
|
+
},
|
|
3052
|
+
};
|
|
3053
|
+
case 'subagent_text_turn':
|
|
3054
|
+
return {
|
|
3055
|
+
...base,
|
|
3056
|
+
type: 'subagent_text_turn',
|
|
3057
|
+
payload: {
|
|
3058
|
+
turnId,
|
|
3059
|
+
agentId: boundedText(event.agentId, 256),
|
|
3060
|
+
role: boundedText(event.role, 256),
|
|
3061
|
+
text: boundedText(event.text, 16_384),
|
|
3062
|
+
},
|
|
3063
|
+
};
|
|
3064
|
+
case 'subagent_command_message':
|
|
3065
|
+
return commandMessageCandidate({
|
|
3066
|
+
...base,
|
|
3067
|
+
type: 'subagent_command_message',
|
|
3068
|
+
payload: {
|
|
3069
|
+
turnId,
|
|
3070
|
+
agentId: boundedText(event.agentId, 256),
|
|
3071
|
+
role: boundedText(event.role, 256),
|
|
3072
|
+
},
|
|
3073
|
+
}, event.message, 'subagent_command_message');
|
|
3074
|
+
case 'subagent_approval_required':
|
|
3075
|
+
if (!interactionBinding)
|
|
3076
|
+
return null;
|
|
3077
|
+
return {
|
|
3078
|
+
...base,
|
|
3079
|
+
type: 'subagent_approval_required',
|
|
3080
|
+
payload: {
|
|
3081
|
+
turnId,
|
|
3082
|
+
agentId: boundedText(event.agentId, 256),
|
|
3083
|
+
role: boundedText(event.role, 256),
|
|
3084
|
+
interaction: interactionBinding.dto,
|
|
3085
|
+
},
|
|
3086
|
+
};
|
|
3087
|
+
case 'subagent_completed': {
|
|
3088
|
+
const result = event.result;
|
|
3089
|
+
return {
|
|
3090
|
+
...base,
|
|
3091
|
+
type: 'subagent_completed',
|
|
3092
|
+
payload: {
|
|
3093
|
+
turnId,
|
|
3094
|
+
agentId: boundedText(result.agentId, 256),
|
|
3095
|
+
...(result.name ? { name: boundedText(result.name, 256) } : {}),
|
|
3096
|
+
role: boundedText(result.role, 256),
|
|
3097
|
+
status: result.status,
|
|
3098
|
+
finalText: boundedText(result.finalText, 16_384),
|
|
3099
|
+
...(result.finalTextTruncated !== undefined ? { finalTextTruncated: result.finalTextTruncated } : {}),
|
|
3100
|
+
toolsUsed: result.toolsUsed.slice(0, 64).map((tool) => ({
|
|
3101
|
+
toolName: boundedText(tool.toolName, 256),
|
|
3102
|
+
count: boundedInteger(tool.count, 1_000_000),
|
|
3103
|
+
})),
|
|
3104
|
+
...(result.usage ? { usage: normalizedUsagePayload(result.usage) } : {}),
|
|
3105
|
+
...(result.error ? { error: boundedText(result.error) } : {}),
|
|
3106
|
+
...(result.terminalCause ? { terminalCause: boundedText(String(result.terminalCause), 128) } : {}),
|
|
3107
|
+
...(event.async !== undefined ? { async: event.async } : {}),
|
|
3108
|
+
},
|
|
3109
|
+
};
|
|
3110
|
+
}
|
|
3111
|
+
case 'subagent_interrupted':
|
|
3112
|
+
return {
|
|
3113
|
+
...base,
|
|
3114
|
+
type: 'subagent_interrupted',
|
|
3115
|
+
payload: {
|
|
3116
|
+
turnId,
|
|
3117
|
+
agentId: boundedText(event.agentId, 256),
|
|
3118
|
+
role: boundedText(event.role, 256),
|
|
3119
|
+
finalText: boundedText(event.finalText, 16_384),
|
|
3120
|
+
},
|
|
3121
|
+
};
|
|
3122
|
+
case 'subagent_question':
|
|
3123
|
+
return {
|
|
3124
|
+
...base,
|
|
3125
|
+
type: 'subagent_question',
|
|
3126
|
+
payload: {
|
|
3127
|
+
turnId,
|
|
3128
|
+
agentId: boundedText(event.runId, 256),
|
|
3129
|
+
runId: boundedText(event.runId, 256),
|
|
3130
|
+
messageId: boundedText(event.messageId, 256),
|
|
3131
|
+
role: boundedText(event.role, 256),
|
|
3132
|
+
...(event.name ? { name: boundedText(event.name, 256) } : {}),
|
|
3133
|
+
question: boundedText(event.question),
|
|
3134
|
+
async: true,
|
|
3135
|
+
...(interactionBinding ? { interaction: interactionBinding.dto } : {}),
|
|
3136
|
+
},
|
|
3137
|
+
};
|
|
3138
|
+
case 'context_compaction_started':
|
|
3139
|
+
return {
|
|
3140
|
+
...base,
|
|
3141
|
+
type: 'context_compaction_started',
|
|
3142
|
+
payload: {
|
|
3143
|
+
turnId,
|
|
3144
|
+
provider: boundedText(event.provider, 256),
|
|
3145
|
+
sessionId: boundedText(event.sessionId, 256),
|
|
3146
|
+
...(event.inputTokensBefore !== undefined ? { inputTokensBefore: safeCounter(event.inputTokensBefore) } : {}),
|
|
3147
|
+
...(event.strategy ? { strategy: event.strategy } : {}),
|
|
3148
|
+
},
|
|
3149
|
+
};
|
|
3150
|
+
case 'context_compaction_completed':
|
|
3151
|
+
return {
|
|
3152
|
+
...base,
|
|
3153
|
+
type: 'context_compaction_completed',
|
|
3154
|
+
payload: {
|
|
3155
|
+
turnId,
|
|
3156
|
+
provider: boundedText(event.provider, 256),
|
|
3157
|
+
sessionId: boundedText(event.sessionId, 256),
|
|
3158
|
+
...(event.inputTokensBefore !== undefined ? { inputTokensBefore: safeCounter(event.inputTokensBefore) } : {}),
|
|
3159
|
+
...(event.inputTokensAfter !== undefined ? { inputTokensAfter: safeCounter(event.inputTokensAfter) } : {}),
|
|
3160
|
+
durationMs: boundedInteger(event.durationMs, 86_400_000),
|
|
3161
|
+
...(event.strategy ? { strategy: event.strategy } : {}),
|
|
3162
|
+
},
|
|
3163
|
+
};
|
|
3164
|
+
case 'context_compaction_failed':
|
|
3165
|
+
return {
|
|
3166
|
+
...base,
|
|
3167
|
+
type: 'context_compaction_failed',
|
|
3168
|
+
payload: {
|
|
3169
|
+
turnId,
|
|
3170
|
+
provider: boundedText(event.provider, 256),
|
|
3171
|
+
sessionId: boundedText(event.sessionId, 256),
|
|
3172
|
+
errorCategory: event.errorCategory,
|
|
3173
|
+
durationMs: boundedInteger(event.durationMs, 86_400_000),
|
|
3174
|
+
...(event.strategy ? { strategy: event.strategy } : {}),
|
|
1941
3175
|
},
|
|
1942
3176
|
};
|
|
1943
3177
|
case 'final':
|
|
@@ -1947,7 +3181,16 @@ function mapConversationEvent(event, turnId, sessionId, interactionBinding, inte
|
|
|
1947
3181
|
payload: { turnId, outcome: 'completed', text: boundedText(event.finalText, 16_384) },
|
|
1948
3182
|
};
|
|
1949
3183
|
case 'error':
|
|
1950
|
-
return {
|
|
3184
|
+
return {
|
|
3185
|
+
...base,
|
|
3186
|
+
type: 'turn_failed',
|
|
3187
|
+
payload: {
|
|
3188
|
+
turnId,
|
|
3189
|
+
outcome: 'failed',
|
|
3190
|
+
reason: errorReason(event.kind),
|
|
3191
|
+
...(event.finalText ? { finalText: boundedText(event.finalText, 16_384) } : {}),
|
|
3192
|
+
},
|
|
3193
|
+
};
|
|
1951
3194
|
default:
|
|
1952
3195
|
return null;
|
|
1953
3196
|
}
|