aws-runtime-bridge 1.9.33 → 1.9.41
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/adapter/AcodeSdkAdapter.d.ts +66 -5
- package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/AcodeSdkAdapter.js +771 -69
- package/dist/adapter/ClaudeSdkAdapter.d.ts +6 -0
- package/dist/adapter/ClaudeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/ClaudeSdkAdapter.js +24 -1
- package/dist/adapter/OpencodeSdkAdapter.d.ts +19 -0
- package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/OpencodeSdkAdapter.js +62 -0
- package/dist/adapter/idle-poll-loop.d.ts +2 -1
- package/dist/adapter/idle-poll-loop.d.ts.map +1 -1
- package/dist/adapter/types.d.ts +86 -9
- package/dist/adapter/types.d.ts.map +1 -1
- package/dist/adapter/types.js +15 -5
- package/dist/index.js +15 -0
- package/dist/routes/acode-todos.d.ts +11 -0
- package/dist/routes/acode-todos.d.ts.map +1 -0
- package/dist/routes/acode-todos.js +75 -0
- package/dist/routes/ai-sources.js +4 -1
- package/dist/routes/dashboard.d.ts.map +1 -1
- package/dist/routes/dashboard.js +1 -1
- package/dist/routes/memory.d.ts +1 -1
- package/dist/routes/memory.d.ts.map +1 -1
- package/dist/routes/memory.js +3 -3
- package/dist/routes/pty.d.ts.map +1 -1
- package/dist/routes/pty.js +9 -0
- package/dist/routes/runtime-binding.d.ts.map +1 -1
- package/dist/routes/runtime-binding.js +6 -3
- package/dist/routes/sessions.js +89 -4
- package/dist/routes/sub-agents.d.ts +14 -0
- package/dist/routes/sub-agents.d.ts.map +1 -0
- package/dist/routes/sub-agents.js +267 -0
- package/dist/routes/terminal.d.ts +12 -1
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +326 -24
- package/dist/services/acode-package.d.ts +8 -1
- package/dist/services/acode-package.d.ts.map +1 -1
- package/dist/services/acode-package.js +33 -4
- package/dist/services/agent-event-store.d.ts +1 -0
- package/dist/services/agent-event-store.d.ts.map +1 -1
- package/dist/services/aws-client-agent-mcp.d.ts.map +1 -1
- package/dist/services/aws-client-agent-mcp.js +11 -4
- package/dist/services/event-delta-pusher.js +4 -4
- package/dist/services/fileSnapshotStore.js +1 -1
- package/dist/services/legacy-path-migration.d.ts +3 -0
- package/dist/services/legacy-path-migration.d.ts.map +1 -0
- package/dist/services/legacy-path-migration.js +66 -0
- package/dist/services/lifecycle-state.d.ts.map +1 -1
- package/dist/services/lifecycle-state.js +1 -1
- package/dist/services/runtime-binding.d.ts +5 -0
- package/dist/services/runtime-binding.d.ts.map +1 -1
- package/dist/services/runtime-binding.js +33 -1
- package/dist/services/session-output.d.ts +10 -1
- package/dist/services/session-output.d.ts.map +1 -1
- package/dist/services/session-output.js +54 -4
- package/dist/services/sub-agent-manager-instance.d.ts +4 -0
- package/dist/services/sub-agent-manager-instance.d.ts.map +1 -0
- package/dist/services/sub-agent-manager-instance.js +12 -0
- package/dist/services/workspace-files.d.ts +1 -0
- package/dist/services/workspace-files.d.ts.map +1 -1
- package/dist/services/workspace-files.js +57 -1
- package/package/acode/dist/ask-user-question-tool.d.ts +47 -0
- package/package/acode/dist/ask-user-question-tool.d.ts.map +1 -0
- package/package/acode/dist/ask-user-question-tool.js +233 -0
- package/package/acode/dist/built-in-file-tools.d.ts +2 -0
- package/package/acode/dist/built-in-file-tools.d.ts.map +1 -1
- package/package/acode/dist/built-in-file-tools.js +504 -3
- package/package/acode/dist/compaction.d.ts +242 -0
- package/package/acode/dist/compaction.d.ts.map +1 -0
- package/package/acode/dist/compaction.js +919 -0
- package/package/acode/dist/config.d.ts +6 -1
- package/package/acode/dist/config.d.ts.map +1 -1
- package/package/acode/dist/config.js +27 -2
- package/package/acode/dist/index.d.ts +4 -0
- package/package/acode/dist/index.d.ts.map +1 -1
- package/package/acode/dist/index.js +4 -0
- package/package/acode/dist/runtime.d.ts +257 -0
- package/package/acode/dist/runtime.d.ts.map +1 -1
- package/package/acode/dist/runtime.js +1843 -191
- package/package/acode/dist/skill-tool.d.ts +30 -0
- package/package/acode/dist/skill-tool.d.ts.map +1 -0
- package/package/acode/dist/skill-tool.js +174 -0
- package/package/acode/dist/sub-agent-manager.d.ts +264 -0
- package/package/acode/dist/sub-agent-manager.d.ts.map +1 -0
- package/package/acode/dist/sub-agent-manager.js +825 -0
- package/package/acode/dist/sub-agent-store.d.ts +40 -0
- package/package/acode/dist/sub-agent-store.d.ts.map +1 -0
- package/package/acode/dist/sub-agent-store.js +164 -0
- package/package/acode/dist/sub-agent-tools.d.ts +72 -0
- package/package/acode/dist/sub-agent-tools.d.ts.map +1 -0
- package/package/acode/dist/sub-agent-tools.js +429 -0
- package/package/acode/dist/sub-agent-types.d.ts +85 -0
- package/package/acode/dist/sub-agent-types.d.ts.map +1 -0
- package/package/acode/dist/sub-agent-types.js +8 -0
- package/package/acode/dist/todo-store.d.ts +42 -0
- package/package/acode/dist/todo-store.d.ts.map +1 -0
- package/package/acode/dist/todo-store.js +68 -0
- package/package/acode/dist/todo-tool.d.ts +6 -0
- package/package/acode/dist/todo-tool.d.ts.map +1 -0
- package/package/acode/dist/todo-tool.js +138 -0
- package/package/acode/dist/types.d.ts +41 -1
- package/package/acode/dist/types.d.ts.map +1 -1
- package/package/acode/package.json +13 -2
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts +9 -0
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/agent-client.js +69 -10
- package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/config.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/config.js +4 -0
- package/package/aws-client-agent-mcp/dist/config.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/constants.d.ts +2 -0
- package/package/aws-client-agent-mcp/dist/constants.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/constants.js +3 -1
- package/package/aws-client-agent-mcp/dist/constants.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/context-manager.d.ts +4 -1
- package/package/aws-client-agent-mcp/dist/context-manager.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/context-manager.js +6 -2
- package/package/aws-client-agent-mcp/dist/context-manager.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/context-manager.test.js +5 -3
- package/package/aws-client-agent-mcp/dist/context-manager.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.d.ts +5 -0
- package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.js +15 -1
- package/package/aws-client-agent-mcp/dist/http-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +162 -71
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.js +1007 -504
- package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +371 -253
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +6 -17
- package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.js +20 -75
- package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-store.d.ts +45 -45
- package/package/aws-client-agent-mcp/dist/memory-store.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-store.js +114 -162
- package/package/aws-client-agent-mcp/dist/memory-store.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-store.test.js +175 -164
- package/package/aws-client-agent-mcp/dist/memory-store.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-tools.d.ts +3 -9
- package/package/aws-client-agent-mcp/dist/memory-tools.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-tools.js +31 -58
- package/package/aws-client-agent-mcp/dist/memory-tools.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.d.ts +40 -9
- package/package/aws-client-agent-mcp/dist/message-buffer.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.js +123 -23
- package/package/aws-client-agent-mcp/dist/message-buffer.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts +13 -13
- package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/server-memory-store.js +23 -16
- package/package/aws-client-agent-mcp/dist/server-memory-store.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.js +8 -5
- package/package/aws-client-agent-mcp/dist/status-reporter.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.test.js +93 -0
- package/package/aws-client-agent-mcp/dist/status-reporter.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts +12 -25
- package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +34 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.js +146 -18
- package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
- package/package.json +17 -7
- package/dist/adapter/AcodeSdkAdapter.test.d.ts +0 -2
- package/dist/adapter/AcodeSdkAdapter.test.d.ts.map +0 -1
- package/dist/adapter/AcodeSdkAdapter.test.js +0 -2000
- package/dist/adapter/AdapterRegistry.test.d.ts +0 -5
- package/dist/adapter/AdapterRegistry.test.d.ts.map +0 -1
- package/dist/adapter/AdapterRegistry.test.js +0 -118
- package/dist/adapter/ClaudeSdkAdapter.test.d.ts +0 -2
- package/dist/adapter/ClaudeSdkAdapter.test.d.ts.map +0 -1
- package/dist/adapter/ClaudeSdkAdapter.test.js +0 -311
- package/dist/adapter/CodexSdkAdapter.test.d.ts +0 -2
- package/dist/adapter/CodexSdkAdapter.test.d.ts.map +0 -1
- package/dist/adapter/CodexSdkAdapter.test.js +0 -270
- package/dist/adapter/OpencodeSdkAdapter.test.d.ts +0 -2
- package/dist/adapter/OpencodeSdkAdapter.test.d.ts.map +0 -1
- package/dist/adapter/OpencodeSdkAdapter.test.js +0 -158
- package/dist/adapter/acode-tool-names.test.d.ts +0 -2
- package/dist/adapter/acode-tool-names.test.d.ts.map +0 -1
- package/dist/adapter/acode-tool-names.test.js +0 -28
- package/dist/adapter/adapter.test.d.ts +0 -5
- package/dist/adapter/adapter.test.d.ts.map +0 -1
- package/dist/adapter/adapter.test.js +0 -180
- package/dist/adapter/idle-poll-loop.test.d.ts +0 -2
- package/dist/adapter/idle-poll-loop.test.d.ts.map +0 -1
- package/dist/adapter/idle-poll-loop.test.js +0 -85
- package/dist/adapter/types.test.d.ts +0 -2
- package/dist/adapter/types.test.d.ts.map +0 -1
- package/dist/adapter/types.test.js +0 -50
- package/dist/adapters/cc-switch/mcp-acode.test.d.ts +0 -2
- package/dist/adapters/cc-switch/mcp-acode.test.d.ts.map +0 -1
- package/dist/adapters/cc-switch/mcp-acode.test.js +0 -80
- package/dist/adapters/cc-switch/mcp-claudecode.test.d.ts +0 -2
- package/dist/adapters/cc-switch/mcp-claudecode.test.d.ts.map +0 -1
- package/dist/adapters/cc-switch/mcp-claudecode.test.js +0 -75
- package/dist/adapters/cc-switch/mcp-codex.test.d.ts +0 -2
- package/dist/adapters/cc-switch/mcp-codex.test.d.ts.map +0 -1
- package/dist/adapters/cc-switch/mcp-codex.test.js +0 -126
- package/dist/adapters/cc-switch/mcp-opencode.test.d.ts +0 -2
- package/dist/adapters/cc-switch/mcp-opencode.test.d.ts.map +0 -1
- package/dist/adapters/cc-switch/mcp-opencode.test.js +0 -62
- package/dist/adapters/cc-switch/skill-directory.test.d.ts +0 -2
- package/dist/adapters/cc-switch/skill-directory.test.d.ts.map +0 -1
- package/dist/adapters/cc-switch/skill-directory.test.js +0 -74
- package/dist/config.test.d.ts +0 -2
- package/dist/config.test.d.ts.map +0 -1
- package/dist/config.test.js +0 -107
- package/dist/routes/ai-sources.test.d.ts +0 -2
- package/dist/routes/ai-sources.test.d.ts.map +0 -1
- package/dist/routes/ai-sources.test.js +0 -169
- package/dist/routes/file-browser.test.d.ts +0 -5
- package/dist/routes/file-browser.test.d.ts.map +0 -1
- package/dist/routes/file-browser.test.js +0 -155
- package/dist/routes/git.test.d.ts +0 -2
- package/dist/routes/git.test.d.ts.map +0 -1
- package/dist/routes/git.test.js +0 -330
- package/dist/routes/instance.test.d.ts +0 -5
- package/dist/routes/instance.test.d.ts.map +0 -1
- package/dist/routes/instance.test.js +0 -389
- package/dist/routes/mcp.test.d.ts +0 -5
- package/dist/routes/mcp.test.d.ts.map +0 -1
- package/dist/routes/mcp.test.js +0 -92
- package/dist/routes/memory-route-wiring.test.d.ts +0 -2
- package/dist/routes/memory-route-wiring.test.d.ts.map +0 -1
- package/dist/routes/memory-route-wiring.test.js +0 -15
- package/dist/routes/memory.test.d.ts +0 -2
- package/dist/routes/memory.test.d.ts.map +0 -1
- package/dist/routes/memory.test.js +0 -177
- package/dist/routes/properties.test.d.ts +0 -5
- package/dist/routes/properties.test.d.ts.map +0 -1
- package/dist/routes/properties.test.js +0 -72
- package/dist/routes/pty.test.d.ts +0 -2
- package/dist/routes/pty.test.d.ts.map +0 -1
- package/dist/routes/pty.test.js +0 -82
- package/dist/routes/runtime-binding.test.d.ts +0 -2
- package/dist/routes/runtime-binding.test.d.ts.map +0 -1
- package/dist/routes/runtime-binding.test.js +0 -32
- package/dist/routes/sessions.test.d.ts +0 -5
- package/dist/routes/sessions.test.d.ts.map +0 -1
- package/dist/routes/sessions.test.js +0 -86
- package/dist/routes/skills.test.d.ts +0 -5
- package/dist/routes/skills.test.d.ts.map +0 -1
- package/dist/routes/skills.test.js +0 -50
- package/dist/routes/system-metrics.test.d.ts +0 -2
- package/dist/routes/system-metrics.test.d.ts.map +0 -1
- package/dist/routes/system-metrics.test.js +0 -16
- package/dist/routes/terminal.stop-options.test.d.ts +0 -2
- package/dist/routes/terminal.stop-options.test.d.ts.map +0 -1
- package/dist/routes/terminal.stop-options.test.js +0 -39
- package/dist/routes/terminal.test.d.ts +0 -2
- package/dist/routes/terminal.test.d.ts.map +0 -1
- package/dist/routes/terminal.test.js +0 -746
- package/dist/routes/yml.test.d.ts +0 -5
- package/dist/routes/yml.test.d.ts.map +0 -1
- package/dist/routes/yml.test.js +0 -58
- package/dist/services/ai-source-apply.test.d.ts +0 -2
- package/dist/services/ai-source-apply.test.d.ts.map +0 -1
- package/dist/services/ai-source-apply.test.js +0 -138
- package/dist/services/auto-register.test.d.ts +0 -2
- package/dist/services/auto-register.test.d.ts.map +0 -1
- package/dist/services/auto-register.test.js +0 -184
- package/dist/services/aws-client-agent-mcp.test.d.ts +0 -2
- package/dist/services/aws-client-agent-mcp.test.d.ts.map +0 -1
- package/dist/services/aws-client-agent-mcp.test.js +0 -242
- package/dist/services/bridge-package-publish-hooks.test.d.ts +0 -2
- package/dist/services/bridge-package-publish-hooks.test.d.ts.map +0 -1
- package/dist/services/bridge-package-publish-hooks.test.js +0 -136
- package/dist/services/cc-switch-discovery.test.d.ts +0 -2
- package/dist/services/cc-switch-discovery.test.d.ts.map +0 -1
- package/dist/services/cc-switch-discovery.test.js +0 -56
- package/dist/services/cli-commands.test.d.ts +0 -2
- package/dist/services/cli-commands.test.d.ts.map +0 -1
- package/dist/services/cli-commands.test.js +0 -230
- package/dist/services/instance-init-service.test.d.ts +0 -2
- package/dist/services/instance-init-service.test.d.ts.map +0 -1
- package/dist/services/instance-init-service.test.js +0 -140
- package/dist/services/instance-state.test.d.ts +0 -2
- package/dist/services/instance-state.test.d.ts.map +0 -1
- package/dist/services/instance-state.test.js +0 -245
- package/dist/services/mcp-launch-binding-queue.test.d.ts +0 -2
- package/dist/services/mcp-launch-binding-queue.test.d.ts.map +0 -1
- package/dist/services/mcp-launch-binding-queue.test.js +0 -181
- package/dist/services/runtime-binding.test.d.ts +0 -2
- package/dist/services/runtime-binding.test.d.ts.map +0 -1
- package/dist/services/runtime-binding.test.js +0 -78
- package/dist/services/runtime-lifecycle-policy.test.d.ts +0 -2
- package/dist/services/runtime-lifecycle-policy.test.d.ts.map +0 -1
- package/dist/services/runtime-lifecycle-policy.test.js +0 -41
- package/dist/services/session-output.test.d.ts +0 -5
- package/dist/services/session-output.test.d.ts.map +0 -1
- package/dist/services/session-output.test.js +0 -177
- package/dist/services/startup-config-wizard.test.d.ts +0 -2
- package/dist/services/startup-config-wizard.test.d.ts.map +0 -1
- package/dist/services/startup-config-wizard.test.js +0 -184
- package/dist/services/terminal-persistence.test.d.ts +0 -5
- package/dist/services/terminal-persistence.test.d.ts.map +0 -1
- package/dist/services/terminal-persistence.test.js +0 -159
- package/dist/services/tool-installer.test.d.ts +0 -2
- package/dist/services/tool-installer.test.d.ts.map +0 -1
- package/dist/services/tool-installer.test.js +0 -225
- package/dist/services/workspace-files.test.d.ts +0 -2
- package/dist/services/workspace-files.test.d.ts.map +0 -1
- package/dist/services/workspace-files.test.js +0 -648
- package/dist/services/workspace-watch.test.d.ts +0 -2
- package/dist/services/workspace-watch.test.d.ts.map +0 -1
- package/dist/services/workspace-watch.test.js +0 -38
- package/dist/utils/file-utils.test.d.ts +0 -2
- package/dist/utils/file-utils.test.d.ts.map +0 -1
- package/dist/utils/file-utils.test.js +0 -244
- package/dist/utils/logger.test.d.ts +0 -2
- package/dist/utils/logger.test.d.ts.map +0 -1
- package/dist/utils/logger.test.js +0 -93
- package/dist/utils/sdk-package-loader.test.d.ts +0 -2
- package/dist/utils/sdk-package-loader.test.d.ts.map +0 -1
- package/dist/utils/sdk-package-loader.test.js +0 -84
- package/dist/utils/validation.test.d.ts +0 -2
- package/dist/utils/validation.test.d.ts.map +0 -1
- package/dist/utils/validation.test.js +0 -88
- package/dist/utils/yaml-utils.test.d.ts +0 -2
- package/dist/utils/yaml-utils.test.d.ts.map +0 -1
- package/dist/utils/yaml-utils.test.js +0 -363
|
@@ -19,16 +19,34 @@ import { getAcodeConfigFile, getAcodeSkillsDir, getRuntimeHomeDir, } from "../co
|
|
|
19
19
|
import { getReleasedAcodeEntryPath } from "../services/acode-package.js";
|
|
20
20
|
import { getInstanceAiConfigDebugLogger } from "../utils/instance-ai-config-debug-logger.js";
|
|
21
21
|
import { redactForLog } from "../services/instance-ai-config-service.js";
|
|
22
|
+
import { subAgentManager } from "../services/sub-agent-manager-instance.js";
|
|
22
23
|
import { resolveAcodeProviderConfig, } from "./instance-ai-config-resolver.js";
|
|
23
24
|
import { isMessagePollingIdleCommand, isPollMessageToolDescriptor, } from "./acode-tool-names.js";
|
|
24
25
|
import { IdlePollLoop } from "./idle-poll-loop.js";
|
|
25
26
|
import { getCanonicalMcpDetail, getToolActionInfo } from "./types.js";
|
|
27
|
+
/**
|
|
28
|
+
* Adapter 调试日志:受 ACODE_COMPACTION_DEBUG=1 开关控制(与 runtime.ts 共用同一开关)。
|
|
29
|
+
* 默认关闭以避免生产环境噪声与潜在的 sessionId/agentId/event 信息泄漏。
|
|
30
|
+
* 文件中所有原 `console.log` 调用均改为走此 helper。
|
|
31
|
+
*/
|
|
32
|
+
const ACODE_DEBUG_ENABLED = process.env.ACODE_COMPACTION_DEBUG === "1";
|
|
33
|
+
function adapterDebugLog(...args) {
|
|
34
|
+
if (!ACODE_DEBUG_ENABLED)
|
|
35
|
+
return;
|
|
36
|
+
console.log(...args);
|
|
37
|
+
}
|
|
26
38
|
const ACODE_AUTO_POLL_PROMPT = [
|
|
27
39
|
"Continue the AgentsWork message loop now.",
|
|
28
40
|
"You MUST call the exact MCP tool named aws-mcp__poll_message as your next action and wait for the next assigned message or task.",
|
|
29
41
|
"Do not answer in plain text before calling aws-mcp__poll_message.",
|
|
30
42
|
"After handling any received work, reply through the appropriate aws-mcp messaging tool, then continue listening again.",
|
|
31
43
|
].join(" ");
|
|
44
|
+
// 异步看门狗轮询间隔:每 5s 检查一次运行时状态
|
|
45
|
+
const ACODE_WATCHDOG_POLL_INTERVAL_MS = 5_000;
|
|
46
|
+
// 数据流看门狗超时:ai_request 发出后,若 40s 内没有收到任何流式数据(assistant_delta/thinking_delta),
|
|
47
|
+
// 触发 recoverTurnFailure 强制恢复。覆盖 provider requestTimeoutMs 配置不当或流挂死等场景。
|
|
48
|
+
// 仅在 thinking/tool_using 状态下累计计时,waiting_input 状态下不计时(agent 正在有意等待)。
|
|
49
|
+
const ACODE_DATA_FLOW_STREAM_WATCHDOG_MS = 40_000;
|
|
32
50
|
const ACODE_AUTO_POLL_DELAY_MS = 250;
|
|
33
51
|
// Backoff used when a polling-driven turn fails (e.g., 60s MCP tool timeout,
|
|
34
52
|
// model 5xx, provider unreachable). Larger than ACODE_AUTO_POLL_DELAY_MS to
|
|
@@ -37,6 +55,9 @@ const ACODE_AUTO_POLL_FAILURE_DELAY_MS = 2000;
|
|
|
37
55
|
// 新增:submit 超时检测配置(治本修复:防止状态永久卡住)
|
|
38
56
|
// 当 submit 超过此时间未返回,强制触发恢复机制
|
|
39
57
|
const ACODE_SUBMIT_TIMEOUT_MS = 120000; // 120秒(2分钟)
|
|
58
|
+
// 兜底超时:mcp_status 缓存能力后,等待 skill_status 的最长时间。
|
|
59
|
+
// 超时后单独发出 MCP 能力清单,避免 skill_status 事件缺失导致"暂无能力清单"。
|
|
60
|
+
const CAPABILITIES_FALLBACK_MS = 3000;
|
|
40
61
|
const ACODE_MESSAGE_POLLING_IDLE_PROMPT = "系统提醒:当前工作已完成或运行时已进入空闲状态。请立即调用 poll_message 工具阻塞等待新消息和流程线任务提醒;收到流程任务提醒后调用 my_task 查看已由后台静默抢占的任务详情并直接处理;完成任务后调用 submit_task_result 提交结果,然后继续 poll_message。";
|
|
41
62
|
function getAcodePackageEntryPath() {
|
|
42
63
|
return path.resolve(getReleasedAcodeEntryPath());
|
|
@@ -101,6 +122,25 @@ function lookupModelContextLimit(modelName) {
|
|
|
101
122
|
return MODEL_CONTEXT_LIMITS[matches[0]].contextLimit;
|
|
102
123
|
return DEFAULT_CONTEXT_LIMIT;
|
|
103
124
|
}
|
|
125
|
+
const DEFAULT_MAX_OUTPUT = 4_096;
|
|
126
|
+
/**
|
|
127
|
+
* Look up model max output token limit by model name.
|
|
128
|
+
* Mirrors lookupModelContextLimit: exact match first, then longest-prefix match.
|
|
129
|
+
* Used to compute the compaction point (contextLimit - maxOutput) on the frontend.
|
|
130
|
+
*/
|
|
131
|
+
function lookupModelMaxOutput(modelName) {
|
|
132
|
+
if (!modelName)
|
|
133
|
+
return DEFAULT_MAX_OUTPUT;
|
|
134
|
+
const key = modelName.toLowerCase().trim();
|
|
135
|
+
if (MODEL_CONTEXT_LIMITS[key])
|
|
136
|
+
return MODEL_CONTEXT_LIMITS[key].maxOutput;
|
|
137
|
+
const matches = Object.keys(MODEL_CONTEXT_LIMITS)
|
|
138
|
+
.filter((prefix) => key.startsWith(prefix))
|
|
139
|
+
.sort((a, b) => b.length - a.length);
|
|
140
|
+
if (matches.length > 0)
|
|
141
|
+
return MODEL_CONTEXT_LIMITS[matches[0]].maxOutput;
|
|
142
|
+
return DEFAULT_MAX_OUTPUT;
|
|
143
|
+
}
|
|
104
144
|
function toUsage(value) {
|
|
105
145
|
if (!value || typeof value !== "object") {
|
|
106
146
|
return undefined;
|
|
@@ -209,12 +249,13 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
209
249
|
const resolvedModel = resolved.model;
|
|
210
250
|
// priority: acode.json top-level contextLimit > model lookup table > 128K default
|
|
211
251
|
const contextLimit = configContextLimit ?? lookupModelContextLimit(resolvedModel);
|
|
252
|
+
const maxOutput = lookupModelMaxOutput(resolvedModel);
|
|
212
253
|
const session = {
|
|
213
254
|
sessionId,
|
|
214
255
|
status: "starting",
|
|
215
256
|
messages: [],
|
|
216
257
|
createdAt: new Date().toISOString(),
|
|
217
|
-
totalUsage: { inputTokens: 0, outputTokens: 0, contextLimit },
|
|
258
|
+
totalUsage: { inputTokens: 0, outputTokens: 0, contextLimit, maxOutput },
|
|
218
259
|
};
|
|
219
260
|
this.sessions.set(sessionId, {
|
|
220
261
|
adapterSession: session,
|
|
@@ -249,20 +290,24 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
249
290
|
session.status = "error";
|
|
250
291
|
this.emitRuntimeErrorEvent(sessionId, error);
|
|
251
292
|
this.emit("status-change", sessionId, "error");
|
|
293
|
+
const failedEntry = this.sessions.get(sessionId);
|
|
294
|
+
if (failedEntry?.capabilitiesFallbackTimer) {
|
|
295
|
+
clearTimeout(failedEntry.capabilitiesFallbackTimer);
|
|
296
|
+
}
|
|
252
297
|
this.sessions.delete(sessionId);
|
|
253
298
|
throw error;
|
|
254
299
|
}
|
|
255
300
|
}
|
|
256
301
|
async sendMessage(sessionId, message) {
|
|
257
302
|
const sendStartTime = Date.now();
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
303
|
+
adapterDebugLog(`[ACode] ═══════════════════════════════════════════════════`);
|
|
304
|
+
adapterDebugLog(`[ACode] ▶️ sendMessage STARTED`);
|
|
305
|
+
adapterDebugLog(`[ACode] Session: ${sessionId}`);
|
|
306
|
+
adapterDebugLog(`[ACode] Message: ${message.slice(0, 100)}${message.length > 100 ? '...' : ''}`);
|
|
307
|
+
adapterDebugLog(`[ACode] ════════════════════════════════════════════════════`);
|
|
263
308
|
const entry = this.requireSession(sessionId);
|
|
264
309
|
const previousStatus = entry.adapterSession.status;
|
|
265
|
-
|
|
310
|
+
adapterDebugLog(`[ACode] Previous status: ${previousStatus}`);
|
|
266
311
|
this.cancelScheduledAutoPoll(entry);
|
|
267
312
|
entry.failedTurnRecoveryScheduled = false;
|
|
268
313
|
entry.idlePollLoop?.stop();
|
|
@@ -275,39 +320,63 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
275
320
|
};
|
|
276
321
|
entry.adapterSession.messages.push(userMessage);
|
|
277
322
|
this.emit("conversation-message", sessionId, userMessage);
|
|
278
|
-
|
|
323
|
+
adapterDebugLog(`[ACode] Changing status: ${previousStatus} → thinking`);
|
|
279
324
|
entry.adapterSession.status = "thinking";
|
|
280
325
|
this.emit("status-change", sessionId, "thinking");
|
|
281
326
|
// 启动超时检测
|
|
282
327
|
this.startSubmitTimeoutDetection(sessionId, entry, "sendMessage");
|
|
283
328
|
try {
|
|
284
|
-
|
|
329
|
+
adapterDebugLog(`[ACode] ▶️ Calling runtime.submit()...`);
|
|
285
330
|
const submitStartTime = Date.now();
|
|
286
331
|
await entry.runtime.submit(message);
|
|
287
332
|
const submitDuration = Date.now() - submitStartTime;
|
|
288
333
|
// submit 成功返回,取消超时检测
|
|
289
334
|
this.cancelSubmitTimeoutDetection(entry);
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
335
|
+
adapterDebugLog(`[ACode] ✅ runtime.submit() completed in ${submitDuration}ms`);
|
|
336
|
+
adapterDebugLog(`[ACode] Current status after submit: ${entry.adapterSession.status}`);
|
|
337
|
+
adapterDebugLog(`[ACode] ▶️ Calling recoverResolvedTurnWithoutTerminalEvent()...`);
|
|
293
338
|
this.recoverResolvedTurnWithoutTerminalEvent(sessionId);
|
|
294
|
-
|
|
339
|
+
adapterDebugLog(`[ACode] Status after recovery: ${entry.adapterSession.status}`);
|
|
295
340
|
const totalDuration = Date.now() - sendStartTime;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
341
|
+
adapterDebugLog(`[ACode] ════════════════════════════════════════════════════`);
|
|
342
|
+
adapterDebugLog(`[ACode] ✅ sendMessage COMPLETED in ${totalDuration}ms`);
|
|
343
|
+
adapterDebugLog(`[ACode] ═══════════════════════════════════════════════════`);
|
|
299
344
|
}
|
|
300
345
|
catch (error) {
|
|
301
346
|
const errorDuration = Date.now() - sendStartTime;
|
|
302
347
|
console.error(`[ACode] ❌ sendMessage FAILED after ${errorDuration}ms:`, error instanceof Error ? error.message : String(error));
|
|
303
348
|
// submit 抛出异常,取消超时检测
|
|
304
349
|
this.cancelSubmitTimeoutDetection(entry);
|
|
305
|
-
|
|
350
|
+
adapterDebugLog(`[ACode] ▶️ Calling emitRuntimeError with recover=true...`);
|
|
306
351
|
this.emitRuntimeError(sessionId, error, { recover: true });
|
|
307
|
-
|
|
352
|
+
adapterDebugLog(`[ACode] Status after error recovery: ${entry.adapterSession.status}`);
|
|
308
353
|
}
|
|
309
354
|
}
|
|
310
355
|
async sendConfirmation(_sessionId, _accept) { }
|
|
356
|
+
/**
|
|
357
|
+
* 接收前端对 ask_user_question 的回答(来自 terminal.ts /sdk/answer 路由)。
|
|
358
|
+
* 通过 pendingQuestionToolCallId 找到 runtime.submitQuestionAnswer 的入参 toolCallId,
|
|
359
|
+
* resolve 阻塞的工具 execute(),让 ACode 继续下一轮 LLM 请求。
|
|
360
|
+
* 重新启动 submit 超时检测(继续计费后续工具执行时间)。
|
|
361
|
+
*/
|
|
362
|
+
async sendQuestionAnswer(sessionId, answers) {
|
|
363
|
+
const entry = this.sessions.get(sessionId);
|
|
364
|
+
if (!entry) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const toolCallId = entry.pendingQuestionToolCallId;
|
|
368
|
+
if (!toolCallId) {
|
|
369
|
+
adapterDebugLog(`[AcodeSdkAdapter] sendQuestionAnswer: no pending question for session ${sessionId}`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
entry.pendingQuestionToolCallId = undefined;
|
|
373
|
+
entry.pendingQuestions = undefined;
|
|
374
|
+
if (entry.runtime.submitQuestionAnswer) {
|
|
375
|
+
entry.runtime.submitQuestionAnswer(toolCallId, answers);
|
|
376
|
+
}
|
|
377
|
+
// 用户回答后,重新启动 submit 超时检测(剩余工具执行时间计入)
|
|
378
|
+
this.startSubmitTimeoutDetection(sessionId, entry, "sendQuestionAnswer");
|
|
379
|
+
}
|
|
311
380
|
async abortCurrentTurn(sessionId) {
|
|
312
381
|
await this.requireSession(sessionId).runtime.abort();
|
|
313
382
|
}
|
|
@@ -318,6 +387,7 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
318
387
|
entry.autoPollActive = false;
|
|
319
388
|
entry.autoPollObservedPollingTool = false;
|
|
320
389
|
entry.autoPollObservedAssistantText = false;
|
|
390
|
+
entry.autoPollObservedAnyTool = false;
|
|
321
391
|
entry.stopAutoPollAfterCurrentTurn = false;
|
|
322
392
|
entry.idlePollLoop?.stop();
|
|
323
393
|
if (!entry.runtime.resetContext) {
|
|
@@ -325,7 +395,17 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
325
395
|
}
|
|
326
396
|
await entry.runtime.resetContext();
|
|
327
397
|
entry.adapterSession.messages = [];
|
|
328
|
-
|
|
398
|
+
// ★ 重置 token 计数但保留压缩配置(contextLimit/maxOutput/inputLimit/compactionThreshold),
|
|
399
|
+
// 这些值在 session 期间不变,避免重置后前端压缩点竖线消失。
|
|
400
|
+
const preservedUsage = entry.adapterSession.totalUsage;
|
|
401
|
+
entry.adapterSession.totalUsage = {
|
|
402
|
+
inputTokens: 0,
|
|
403
|
+
outputTokens: 0,
|
|
404
|
+
contextLimit: preservedUsage.contextLimit,
|
|
405
|
+
maxOutput: preservedUsage.maxOutput,
|
|
406
|
+
inputLimit: preservedUsage.inputLimit,
|
|
407
|
+
compactionThreshold: preservedUsage.compactionThreshold,
|
|
408
|
+
};
|
|
329
409
|
entry.adapterSession.status = "waiting_input";
|
|
330
410
|
this.emit("status-change", sessionId, "waiting_input", {
|
|
331
411
|
usage: entry.adapterSession.totalUsage,
|
|
@@ -344,6 +424,10 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
344
424
|
this.cancelScheduledAutoPoll(entry);
|
|
345
425
|
entry.failedTurnRecoveryScheduled = false;
|
|
346
426
|
entry.idlePollLoop?.stop();
|
|
427
|
+
if (entry.capabilitiesFallbackTimer) {
|
|
428
|
+
clearTimeout(entry.capabilitiesFallbackTimer);
|
|
429
|
+
entry.capabilitiesFallbackTimer = undefined;
|
|
430
|
+
}
|
|
347
431
|
await entry.runtime.stop();
|
|
348
432
|
entry.adapterSession.status = "terminated";
|
|
349
433
|
this.sessions.delete(sessionId);
|
|
@@ -361,6 +445,29 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
361
445
|
getSessionStatus(sessionId) {
|
|
362
446
|
return this.sessions.get(sessionId)?.adapterSession.status;
|
|
363
447
|
}
|
|
448
|
+
/**
|
|
449
|
+
* 主动查询会话的运行时能力清单(MCP 工具 + skills)。
|
|
450
|
+
* 只读 adapter 内存缓存(lastCombinedCapabilities),不触发 runtime 重新探测。
|
|
451
|
+
* 由 mcp_status / skill_status 事件维护;若尚未收到任何 runtime_capabilities 事件则返回 undefined。
|
|
452
|
+
*/
|
|
453
|
+
getCapabilities(sessionId) {
|
|
454
|
+
return this.sessions.get(sessionId)?.lastCombinedCapabilities;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* 主动查询会话当前挂起的 ask_user_question。
|
|
458
|
+
* 只读 adapter 内存缓存(pendingQuestionToolCallId + pendingQuestions)。
|
|
459
|
+
* 由 bridge GET /runtime/sdk/pending-questions 端点调用,用于前端刷新后重建提问 UI。
|
|
460
|
+
* 返回 undefined 表示当前无挂起提问(会话不存在或 agent 未提问)。
|
|
461
|
+
*/
|
|
462
|
+
getPendingQuestion(sessionId) {
|
|
463
|
+
const entry = this.sessions.get(sessionId);
|
|
464
|
+
if (!entry?.pendingQuestionToolCallId)
|
|
465
|
+
return undefined;
|
|
466
|
+
return {
|
|
467
|
+
toolCallId: entry.pendingQuestionToolCallId,
|
|
468
|
+
questions: entry.pendingQuestions ?? [],
|
|
469
|
+
};
|
|
470
|
+
}
|
|
364
471
|
cleanup() {
|
|
365
472
|
for (const sessionId of this.sessions.keys()) {
|
|
366
473
|
void this.terminateSession(sessionId);
|
|
@@ -548,6 +655,10 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
548
655
|
? config.memory
|
|
549
656
|
: undefined,
|
|
550
657
|
env: config.envOverrides,
|
|
658
|
+
subAgentManager,
|
|
659
|
+
...(config.subAgentModel ? { subAgentModel: config.subAgentModel } : {}),
|
|
660
|
+
...(config.subAgentEffort ? { subAgentEffort: config.subAgentEffort } : {}),
|
|
661
|
+
...(config.instanceAiConfig?.effort ? { effort: config.instanceAiConfig.effort } : {}),
|
|
551
662
|
});
|
|
552
663
|
const debugLogger = getInstanceAiConfigDebugLogger();
|
|
553
664
|
void debugLogger.log({
|
|
@@ -644,11 +755,15 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
644
755
|
content: text,
|
|
645
756
|
timestamp: event.timestamp,
|
|
646
757
|
});
|
|
758
|
+
// 收到流式数据,重置看门狗
|
|
759
|
+
this.resetDataFlowWatchdog(event.sessionId, entry);
|
|
647
760
|
this.emitProviderEvent(event, "text_delta", { text });
|
|
648
761
|
return;
|
|
649
762
|
}
|
|
650
763
|
if (event.type === "thinking_delta") {
|
|
651
764
|
const text = typeof event.data.text === "string" ? event.data.text : "";
|
|
765
|
+
// 收到流式思考过程数据,重置看门狗
|
|
766
|
+
this.resetDataFlowWatchdog(event.sessionId, entry);
|
|
652
767
|
this.emitProviderEvent(event, "thinking", { text });
|
|
653
768
|
return;
|
|
654
769
|
}
|
|
@@ -671,13 +786,61 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
671
786
|
}
|
|
672
787
|
if (event.type === "ai_request") {
|
|
673
788
|
const estimatedTokens = toEstimatedContextTokens(event.data);
|
|
789
|
+
let displayUsage;
|
|
674
790
|
if (estimatedTokens > 0) {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
791
|
+
// ★ 限制估算值的增长幅度,防止 JSON 结构开销导致的虚高跳变。
|
|
792
|
+
// estimateTokensFromText(requestBody) 基于 pretty-printed JSON 字符串长度 / 3 估算,
|
|
793
|
+
// JSON 结构开销(键名、引号、缩进、转义)导致估算值系统性虚高 3-5x+,
|
|
794
|
+
// 在 turn_complete 到达前会造成面板显示从 xx k 突跳到几 M 再跳回。
|
|
795
|
+
// 正常单轮上下文增长 < 50%,cap 1.5x 足够覆盖增长同时防止跳变。
|
|
796
|
+
// turn_complete 携带 provider 实际 prompt_tokens 时始终覆盖此估算值。
|
|
797
|
+
// 压缩后 context_compaction end 将 inputTokens 重置为 0,
|
|
798
|
+
// 此处 0 值直接使用估算值(压缩后对话更小,估算值也相应更小)。
|
|
799
|
+
const currentInput = entry.adapterSession.totalUsage.inputTokens;
|
|
800
|
+
if (currentInput > 0) {
|
|
801
|
+
entry.adapterSession.totalUsage.inputTokens = Math.min(estimatedTokens, Math.floor(currentInput * 1.5));
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
entry.adapterSession.totalUsage.inputTokens = estimatedTokens;
|
|
805
|
+
}
|
|
806
|
+
// ★ outputTokens 保持上一轮 turn_complete 写入的单轮值(覆盖式语义),
|
|
807
|
+
// 不再用 estimateOutputTokensFromMessages(累计估算)通过 Math.max 撑高。
|
|
808
|
+
// 该累计估算会把历史所有 assistant 输出叠加,与 prompt_tokens(已含历史) 重复,
|
|
809
|
+
// 导致显示 total 系统性虚高。结构与 runtime 的 actualUsed 一致:
|
|
810
|
+
// input(请求体估算) + output(当前轮 completion)。
|
|
811
|
+
displayUsage = { ...entry.adapterSession.totalUsage };
|
|
812
|
+
}
|
|
813
|
+
// ★ 修复 BUG:状态切换与 status-change 发射必须移到 if (estimatedTokens > 0) 块外。
|
|
814
|
+
// 原实现仅在 estimatedTokens > 0 时才切换状态并发射 status-change,
|
|
815
|
+
// 导致 estimatedTokens === 0(如 ai_request 无 requestBody 或无法解析)时:
|
|
816
|
+
// 1. adapter 内部 status 保持 waiting_input(未切换为 thinking)
|
|
817
|
+
// 2. 不发射 status-change 事件,但 emitProviderEvent 照常发射
|
|
818
|
+
// 3. terminal.ts 通过 provider event 路径发送 thinking + ai_request 到前端
|
|
819
|
+
// 4. recoverResolvedTurnWithoutTerminalEvent 因 status 是 waiting_input 而跳过恢复
|
|
820
|
+
// 5. 残留 actionType:"ai_request" 永远无法清理,前端永久显示 "AI请求: 模型: xxx"
|
|
821
|
+
// ★ 压缩完成后 adapterSession.status 仍为 "compacting",此处恢复为 "thinking",
|
|
822
|
+
// 使 status-change 走普通路径(而非 compacting 串行队列),
|
|
823
|
+
// 并带上 actionType 避免 restoredActionInfo 覆盖 usage。
|
|
824
|
+
if (entry.adapterSession.status === "compacting") {
|
|
825
|
+
entry.adapterSession.status = "thinking";
|
|
826
|
+
}
|
|
827
|
+
// ★ 修复:ai_request 表示 AI 正在请求模型,无论 keepWaitingStatus 如何,
|
|
828
|
+
// 状态都应切换为 "thinking"。否则 keepWaitingStatus=true 时 status 保持
|
|
829
|
+
// "waiting_input",导致 submit 完成后跳过 recoverResolvedTurnWithoutTerminalEvent,
|
|
830
|
+
// 残留 actionType:"ai_request" 永远无法清理,前端永久显示 "AI请求: 模型: xxx"。
|
|
831
|
+
if (entry.adapterSession.status === "waiting_input") {
|
|
832
|
+
entry.adapterSession.status = "thinking";
|
|
833
|
+
}
|
|
834
|
+
const aiRequestActionInfo = {
|
|
835
|
+
actionType: "ai_request",
|
|
836
|
+
actionLabel: "AI请求",
|
|
837
|
+
};
|
|
838
|
+
if (displayUsage) {
|
|
839
|
+
aiRequestActionInfo.usage = displayUsage;
|
|
680
840
|
}
|
|
841
|
+
this.emit("status-change", event.sessionId, entry.adapterSession.status, aiRequestActionInfo);
|
|
842
|
+
// 启动数据流看门狗:ai_request 发出后开始监控流式数据返回
|
|
843
|
+
this.startDataFlowWatchdog(event.sessionId, entry);
|
|
681
844
|
this.emitProviderEvent(event, "ai_request", {
|
|
682
845
|
model: typeof event.data.model === "string" ? event.data.model : undefined,
|
|
683
846
|
endpoint: typeof event.data.endpoint === "string"
|
|
@@ -711,12 +874,29 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
711
874
|
if (event.type === "turn_complete") {
|
|
712
875
|
const usage = toUsage(event.data.usage);
|
|
713
876
|
if (usage) {
|
|
714
|
-
|
|
715
|
-
|
|
877
|
+
// ★ 同时更新 inputTokens 和 outputTokens(均覆盖式赋值)。
|
|
878
|
+
// provider 返回的 prompt_tokens 是当前上下文大小的最准确度量,
|
|
879
|
+
// 优于 ai_request 的 requestBody/3 估算。直接赋值(非累加),
|
|
880
|
+
// 因为 prompt_tokens 代表本次请求时 provider 看到的完整上下文大小。
|
|
881
|
+
// outputTokens 也采用覆盖式(= 当前轮 completion_tokens),与 runtime 的
|
|
882
|
+
// state.accumulatedOutputTokens(runtime.ts,覆盖式)同构。
|
|
883
|
+
// 之前 outputTokens 用 += 累加,导致显示的 total = prompt_tokens(已含历史output)
|
|
884
|
+
// + Σcompletion 把历史 output 重复计算,面板系统性虚高,先于 runtime 达到压缩阈值。
|
|
885
|
+
// ★ 过滤 fallback usage:当 provider 不返回 usage 时,runtime 会用
|
|
886
|
+
// { inputTokens: trimmed.length, outputTokens: text.length } 作为 fallback。
|
|
887
|
+
// trimmed.length 只是用户消息长度(通常 < 1000),远小于实际上下文,
|
|
888
|
+
// 会把面板从几十 k 砸到几百,造成显示突降。检测方式:fallback inputTokens
|
|
889
|
+
// 远小于当前值(< 20%),说明不是真实 provider 数据,跳过更新。
|
|
890
|
+
const currentInput = entry.adapterSession.totalUsage.inputTokens;
|
|
891
|
+
const isFallbackUsage = currentInput > 0 && usage.inputTokens < Math.floor(currentInput * 0.2);
|
|
892
|
+
if (!isFallbackUsage) {
|
|
893
|
+
entry.adapterSession.totalUsage.inputTokens = usage.inputTokens;
|
|
894
|
+
entry.adapterSession.totalUsage.outputTokens = usage.outputTokens;
|
|
895
|
+
}
|
|
716
896
|
}
|
|
717
897
|
// 关键日志:记录 turn_complete 事件
|
|
718
898
|
const previousStatus = entry.adapterSession.status;
|
|
719
|
-
|
|
899
|
+
adapterDebugLog(`[ACode] turn_complete event received (session: ${event.sessionId}), ` +
|
|
720
900
|
`previous status: ${previousStatus}, failed: ${event.data.failed === true}, ` +
|
|
721
901
|
`aborted: ${event.data.aborted === true}`);
|
|
722
902
|
if (isFailedAcodeTurnComplete(event.data)) {
|
|
@@ -732,10 +912,25 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
732
912
|
// initial prompt turn failed, autoPollActive was false and the
|
|
733
913
|
// session got permanently stuck in 'error'. Now we always recover
|
|
734
914
|
// for non-terminal sessions so the idle loop can retry.
|
|
735
|
-
|
|
915
|
+
adapterDebugLog(`[ACode] Recovering from failed turn_complete (session: ${event.sessionId})`);
|
|
916
|
+
this.cancelDataFlowWatchdog(entry);
|
|
917
|
+
this.recoverTurnFailure(event.sessionId, entry);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
// ★ 修复竞态:看门狗触发 abort() 后,runtime 在 catch 块发射
|
|
921
|
+
// turn_complete { aborted: true }(而非 failed: true)。
|
|
922
|
+
// 原逻辑走正常完成路径,立即 armIdleLoopOrAutoPoll(250ms 重臂),
|
|
923
|
+
// 导致新 submit 在前一轮 executeSubmit 仍占用 submitQueue 时就排队,
|
|
924
|
+
// 加剧竞态。改为走 R2 恢复路径(2s 退避),给前一轮更多时间完成。
|
|
925
|
+
if (event.data.aborted === true && entry.autoPollActive) {
|
|
926
|
+
adapterDebugLog(`[ACode] Recovering from aborted turn_complete (session: ${event.sessionId}), ` +
|
|
927
|
+
`using R2 2s backoff instead of immediate rearm`);
|
|
928
|
+
this.cancelDataFlowWatchdog(entry);
|
|
736
929
|
this.recoverTurnFailure(event.sessionId, entry);
|
|
737
930
|
return;
|
|
738
931
|
}
|
|
932
|
+
// turn 正常完成,取消数据流看门狗
|
|
933
|
+
this.cancelDataFlowWatchdog(entry);
|
|
739
934
|
this.emitProviderEvent(event, "turn_complete", {
|
|
740
935
|
usage: entry.adapterSession.totalUsage,
|
|
741
936
|
responseBody: typeof event.data.responseBody === "string"
|
|
@@ -749,18 +944,20 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
749
944
|
this.emit("status-change", event.sessionId, "waiting_input", {
|
|
750
945
|
usage: entry.adapterSession.totalUsage,
|
|
751
946
|
});
|
|
752
|
-
|
|
947
|
+
adapterDebugLog(`[ACode] Status changed to waiting_input after turn_complete (session: ${event.sessionId})`);
|
|
753
948
|
if (entry.autoPollActive &&
|
|
754
949
|
entry.autoPollObservedAssistantText &&
|
|
755
|
-
!entry.autoPollObservedPollingTool
|
|
950
|
+
!entry.autoPollObservedPollingTool &&
|
|
951
|
+
!entry.autoPollObservedAnyTool) {
|
|
756
952
|
entry.stopAutoPollAfterCurrentTurn = true;
|
|
757
|
-
console.warn(`[${this.displayName}] Auto-poll turn produced assistant text without calling
|
|
953
|
+
console.warn(`[${this.displayName}] Auto-poll turn produced assistant text without calling any tool; stopping auto-poll rearm for session ${event.sessionId}`);
|
|
758
954
|
return;
|
|
759
955
|
}
|
|
760
956
|
this.armIdleLoopOrAutoPoll(event.sessionId);
|
|
761
957
|
return;
|
|
762
958
|
}
|
|
763
959
|
if (event.type === "session_complete") {
|
|
960
|
+
this.cancelDataFlowWatchdog(entry);
|
|
764
961
|
this.cancelScheduledAutoPoll(entry);
|
|
765
962
|
entry.failedTurnRecoveryScheduled = false;
|
|
766
963
|
entry.idlePollLoop?.stop();
|
|
@@ -771,7 +968,46 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
771
968
|
this.emit("status-change", event.sessionId, "completed");
|
|
772
969
|
return;
|
|
773
970
|
}
|
|
971
|
+
if (event.type === "question_request") {
|
|
972
|
+
// ask_user_question 工具被调用:runtime 在 execute() 中阻塞等待答案。
|
|
973
|
+
// 取消数据流看门狗(用户思考/回答时间不计入 40s 超时),
|
|
974
|
+
// 等用户回答后 LLM 重新输出时 assistant_delta/thinking_delta 会重新启动看门狗。
|
|
975
|
+
this.cancelDataFlowWatchdog(entry);
|
|
976
|
+
// 取消 submit 超时检测(用户回答时间不计入 2 分钟超时),
|
|
977
|
+
// 记录 toolCallId + questions 供 sendQuestionAnswer 和 getPendingQuestion 使用,
|
|
978
|
+
// 转发事件让 terminal.ts 推到前端。
|
|
979
|
+
this.cancelSubmitTimeoutDetection(entry);
|
|
980
|
+
const toolCallId = typeof event.data.toolCallId === "string"
|
|
981
|
+
? event.data.toolCallId
|
|
982
|
+
: "";
|
|
983
|
+
const questions = Array.isArray(event.data.questions)
|
|
984
|
+
? event.data.questions
|
|
985
|
+
: [];
|
|
986
|
+
if (toolCallId) {
|
|
987
|
+
entry.pendingQuestionToolCallId = toolCallId;
|
|
988
|
+
entry.pendingQuestions = questions;
|
|
989
|
+
}
|
|
990
|
+
this.emit("ask-user-question", {
|
|
991
|
+
sessionId: event.sessionId,
|
|
992
|
+
toolCallId: toolCallId || undefined,
|
|
993
|
+
questions,
|
|
994
|
+
});
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
774
997
|
if (event.type === "error") {
|
|
998
|
+
// nonFatal=true 表示工具执行失败等非致命错误(Agent 会继续处理),
|
|
999
|
+
// 仅推送事件到时间线,不变更运行态为 "error",避免面板启动/停止按钮异常。
|
|
1000
|
+
if (event.data.nonFatal) {
|
|
1001
|
+
// ★ 修复:重试期间(每次 fetch failed 后退避)无流式数据到达,
|
|
1002
|
+
// 看门狗仍以"上次 delta + 40s"倒计时,可能在重试周期内误触发 40s 超时。
|
|
1003
|
+
// 重置数据流计时基准,让看门狗从重试时刻重新计时。
|
|
1004
|
+
this.resetDataFlowWatchdog(event.sessionId, entry);
|
|
1005
|
+
this.emitProviderEvent(event, "error", {
|
|
1006
|
+
text: String(event.data.message || "ACode runtime error"),
|
|
1007
|
+
});
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
this.cancelDataFlowWatchdog(entry);
|
|
775
1011
|
entry.adapterSession.status = "error";
|
|
776
1012
|
this.emitProviderEvent(event, "error", {
|
|
777
1013
|
text: String(event.data.message || "ACode runtime error"),
|
|
@@ -812,8 +1048,78 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
812
1048
|
"Please configure an MCP server that provides poll_message (e.g. aws-mcp) in the dashboard panel for this runtime.";
|
|
813
1049
|
console.warn(`[${this.displayName}] ${message}`);
|
|
814
1050
|
}
|
|
1051
|
+
// 将启动时加载的 MCP 工具清单缓存到 entry,等待 skill_status 事件到达后合并发出,
|
|
1052
|
+
// 避免两个 runtime_capabilities 事件并发 HTTP POST 导致其中一个丢失
|
|
1053
|
+
const mcpServers = Array.isArray(event.data.servers)
|
|
1054
|
+
? event.data.servers
|
|
1055
|
+
: [];
|
|
1056
|
+
const mcpTools = tools
|
|
1057
|
+
.map((tool) => ({
|
|
1058
|
+
name: String(tool.name ?? ""),
|
|
1059
|
+
source: String(tool.source ?? ""),
|
|
1060
|
+
description: String(tool.description ?? ""),
|
|
1061
|
+
}))
|
|
1062
|
+
.filter((tool) => tool.name);
|
|
1063
|
+
const mcpCapabilities = {
|
|
1064
|
+
kind: "mcp",
|
|
1065
|
+
mcpServers,
|
|
1066
|
+
mcpTools,
|
|
1067
|
+
};
|
|
1068
|
+
entry.cachedMcpCapabilities = mcpCapabilities;
|
|
1069
|
+
// 同步保存到 lastMcpCapabilities,运行中再次发 skill_status(如 skill 目录变更)
|
|
1070
|
+
// 时,会用它合并 MCP 信息,避免覆盖为空数组
|
|
1071
|
+
entry.lastMcpCapabilities = mcpCapabilities;
|
|
1072
|
+
// 更新 lastCombinedCapabilities 的 MCP 部分,保留已有的 skills(若 skill_status 已先到达)
|
|
1073
|
+
entry.lastCombinedCapabilities = {
|
|
1074
|
+
kind: "combined",
|
|
1075
|
+
mcpServers,
|
|
1076
|
+
mcpTools,
|
|
1077
|
+
skills: entry.lastCombinedCapabilities?.skills ?? [],
|
|
1078
|
+
skillsDir: entry.lastCombinedCapabilities?.skillsDir,
|
|
1079
|
+
skillGroups: entry.lastCombinedCapabilities?.skillGroups ?? [],
|
|
1080
|
+
};
|
|
1081
|
+
adapterDebugLog(`[cap-diag] MCP capabilities cached: ${mcpServers.length} servers, ${mcpTools.length} tools`, {
|
|
1082
|
+
sessionId: event.sessionId,
|
|
1083
|
+
servers: mcpServers,
|
|
1084
|
+
toolNames: mcpTools.map(t => t.name),
|
|
1085
|
+
});
|
|
1086
|
+
// 兜底:如果 skill_status 事件在 CAPABILITIES_FALLBACK_MS 内未到达,
|
|
1087
|
+
// 单独发出 MCP 能力清单,避免 skill_status 缺失导致"暂无能力清单"。
|
|
1088
|
+
if (entry.capabilitiesFallbackTimer) {
|
|
1089
|
+
clearTimeout(entry.capabilitiesFallbackTimer);
|
|
1090
|
+
}
|
|
1091
|
+
entry.capabilitiesFallbackTimer = setTimeout(() => {
|
|
1092
|
+
const cached = entry.cachedMcpCapabilities;
|
|
1093
|
+
if (!cached)
|
|
1094
|
+
return;
|
|
1095
|
+
const fallbackCapabilities = {
|
|
1096
|
+
...cached,
|
|
1097
|
+
skills: [],
|
|
1098
|
+
skillGroups: [],
|
|
1099
|
+
};
|
|
1100
|
+
this.emitProviderEvent(event, "runtime_capabilities", {
|
|
1101
|
+
capabilities: fallbackCapabilities,
|
|
1102
|
+
actionType: "runtime_capabilities",
|
|
1103
|
+
actionLabel: "运行时能力",
|
|
1104
|
+
actionDetail: JSON.stringify(fallbackCapabilities),
|
|
1105
|
+
});
|
|
1106
|
+
adapterDebugLog(`[cap-diag] MCP capabilities emitted via fallback (skill_status not received within ${CAPABILITIES_FALLBACK_MS}ms)`, {
|
|
1107
|
+
sessionId: event.sessionId,
|
|
1108
|
+
mcpServers: cached.mcpServers,
|
|
1109
|
+
mcpToolsCount: cached.mcpTools?.length ?? 0,
|
|
1110
|
+
});
|
|
1111
|
+
entry.cachedMcpCapabilities = undefined;
|
|
1112
|
+
entry.capabilitiesFallbackTimer = undefined;
|
|
1113
|
+
}, CAPABILITIES_FALLBACK_MS);
|
|
815
1114
|
return;
|
|
816
1115
|
}
|
|
1116
|
+
const eventAction = String(event.data.action ?? "");
|
|
1117
|
+
adapterDebugLog(`[ACode] 📡 mcp_status event: action=${eventAction}, ` +
|
|
1118
|
+
`server=${typeof event.data.server === "string" ? event.data.server : "?"}, ` +
|
|
1119
|
+
`tool=${typeof event.data.tool === "string" ? event.data.tool : "?"}, ` +
|
|
1120
|
+
`toolUseId=${typeof event.data.toolUseId === "string" ? event.data.toolUseId.slice(0, 16) + "…" : "none"}, ` +
|
|
1121
|
+
`failed=${event.data.failed === true}, ` +
|
|
1122
|
+
`session=${event.sessionId}, status=${entry.adapterSession.status}`);
|
|
817
1123
|
const server = typeof event.data.server === "string" ? event.data.server : "mcp";
|
|
818
1124
|
const tool = typeof event.data.tool === "string" ? event.data.tool : "";
|
|
819
1125
|
const toolName = server && tool ? `${server}__${tool}` : tool || server;
|
|
@@ -823,8 +1129,13 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
823
1129
|
? event.data.arguments
|
|
824
1130
|
: undefined;
|
|
825
1131
|
const actionInfo = getToolActionInfo(toolName, toolInput);
|
|
826
|
-
if (entry.autoPollActive
|
|
827
|
-
|
|
1132
|
+
if (entry.autoPollActive) {
|
|
1133
|
+
// ★ 修复永久卡死:任何工具调用(包括工作工具)都标记为已观察,
|
|
1134
|
+
// 避免 turn_complete 时误判为"只输出文本不调用工具"而停止 auto-poll。
|
|
1135
|
+
entry.autoPollObservedAnyTool = true;
|
|
1136
|
+
if (actionInfo.actionType === "idle") {
|
|
1137
|
+
entry.autoPollObservedPollingTool = true;
|
|
1138
|
+
}
|
|
828
1139
|
}
|
|
829
1140
|
const actionDetail = isAcodeSemanticToolAction(actionInfo.actionType)
|
|
830
1141
|
? actionInfo.actionDetail ||
|
|
@@ -843,6 +1154,8 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
843
1154
|
...(actionInput ? { actionInput } : {}),
|
|
844
1155
|
...(toolInput ? { toolInput } : {}),
|
|
845
1156
|
...(toolUseId ? { toolUseId, actionId: toolUseId } : {}),
|
|
1157
|
+
// 传递 MCP 服务器名,供前端时间线展示工具来源
|
|
1158
|
+
metadata: { mcpServer: server },
|
|
846
1159
|
};
|
|
847
1160
|
if (event.data.action === "tool_result") {
|
|
848
1161
|
this.emitProviderEvent(event, "tool_use_end", {
|
|
@@ -856,24 +1169,154 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
856
1169
|
? "waiting_input"
|
|
857
1170
|
: "thinking";
|
|
858
1171
|
entry.adapterSession.status = status;
|
|
859
|
-
//
|
|
860
|
-
//
|
|
861
|
-
//
|
|
862
|
-
//
|
|
863
|
-
|
|
1172
|
+
// ★ 修复 BUG(时间线永久卡在"编辑文件"):工具执行完成时取消数据流看门狗。
|
|
1173
|
+
// 原实现中 tool_use_start/tool_use_end 既不取消也不重置看门狗,
|
|
1174
|
+
// 导致工具执行期间看门狗仍以"上次 delta + 40s"倒计时,
|
|
1175
|
+
// 若工具执行时间接近或超过 40s(如大文件编辑、Windows 杀毒扫描),
|
|
1176
|
+
// 看门狗会在 tool_using 状态误触发,abort 无效(内置工具不可中断),
|
|
1177
|
+
// recoverTurnFailure 不补发 tool_use_end → 时间线永久卡在"编辑文件"。
|
|
1178
|
+
this.cancelDataFlowWatchdog(entry);
|
|
1179
|
+
entry.pendingToolEventData = undefined;
|
|
1180
|
+
// ★ 修复重复时间线条目:idle 类型(poll_message)的 tool_use_end status-change
|
|
1181
|
+
// 始终传 undefined,避免 terminal.ts 的 restoredActionInfo 恢复出 actionType+actionLabel
|
|
1182
|
+
// 导致 dashboard 创建第二个"调用MCP"条目。tool_use_start 的 status-change 已创建条目。
|
|
1183
|
+
// 非 idle 类型仍按原逻辑传 eventData(waiting_input 时)以保留输入参数展示。
|
|
1184
|
+
const isIdleTool = actionInfo.actionType === "idle";
|
|
1185
|
+
this.emit("status-change", event.sessionId, status, isIdleTool ? undefined : (status === "thinking" ? undefined : eventData));
|
|
864
1186
|
return;
|
|
865
1187
|
}
|
|
866
1188
|
const status = actionInfo.actionType === "idle" ? "waiting_input" : "tool_using";
|
|
867
1189
|
entry.adapterSession.status = status;
|
|
1190
|
+
// ★ 修复 BUG(时间线永久卡在"编辑文件"):工具开始执行时重置数据流看门狗。
|
|
1191
|
+
// 工具执行(如 edit_file)可能耗时较长(大文件、Windows I/O、杀毒扫描),
|
|
1192
|
+
// 期间无流式 delta 数据返回。若不重置看门狗,40s 超时后会在 tool_using 状态误触发,
|
|
1193
|
+
// 导致 recoverTurnFailure 不补发 tool_use_end,时间线永久卡住。
|
|
1194
|
+
// 重置看门狗让计时从工具开始时重新计算,给工具执行留出充足时间。
|
|
1195
|
+
if (status === "tool_using") {
|
|
1196
|
+
this.resetDataFlowWatchdog(event.sessionId, entry);
|
|
1197
|
+
// 缓存当前工具事件数据,供 recoverTurnFailure 补发 tool_use_end 使用
|
|
1198
|
+
entry.pendingToolEventData = { ...eventData };
|
|
1199
|
+
}
|
|
868
1200
|
this.emitProviderEvent(event, "tool_use_start", eventData);
|
|
869
1201
|
this.emit("status-change", event.sessionId, status, eventData);
|
|
870
1202
|
return;
|
|
871
1203
|
}
|
|
1204
|
+
if (event.type === "skill_status") {
|
|
1205
|
+
// skill_status 事件可能在两个时机到达:
|
|
1206
|
+
// 1. ACode runtime 启动时(包含已加载的 Skill 列表)
|
|
1207
|
+
// 2. 运行中 skills 目录变更触发重扫后(动态感知新 skill)
|
|
1208
|
+
// 合并 MCP 能力清单后,作为单个 runtime_capabilities 事件发出,
|
|
1209
|
+
// 避免与 mcp_status 的 runtime_capabilities 事件并发 HTTP POST 导致其中一个丢失。
|
|
1210
|
+
// 启动时用 cachedMcpCapabilities(等待合并);运行中 cachedMcp 已清理,
|
|
1211
|
+
// 回退到 lastMcpCapabilities(保留最近一次 MCP 信息,避免覆盖为空数组)。
|
|
1212
|
+
const skillsRaw = Array.isArray(event.data.skills)
|
|
1213
|
+
? event.data.skills
|
|
1214
|
+
: [];
|
|
1215
|
+
const skills = skillsRaw
|
|
1216
|
+
.map((skill) => ({
|
|
1217
|
+
name: String(skill.name ?? ""),
|
|
1218
|
+
description: typeof skill.description === "string"
|
|
1219
|
+
? skill.description
|
|
1220
|
+
: undefined,
|
|
1221
|
+
directory: typeof skill.directory === "string"
|
|
1222
|
+
? skill.directory
|
|
1223
|
+
: undefined,
|
|
1224
|
+
}))
|
|
1225
|
+
.filter((skill) => skill.name);
|
|
1226
|
+
const skillsDir = typeof event.data.skillsDir === "string"
|
|
1227
|
+
? event.data.skillsDir
|
|
1228
|
+
: undefined;
|
|
1229
|
+
// 按目录分组的 skills(含空目录节点),供前端按目录展示
|
|
1230
|
+
const skillGroupsRaw = Array.isArray(event.data.skillGroups)
|
|
1231
|
+
? event.data.skillGroups
|
|
1232
|
+
: [];
|
|
1233
|
+
const skillGroups = skillGroupsRaw
|
|
1234
|
+
.map((group) => {
|
|
1235
|
+
const groupSkills = Array.isArray(group.skills)
|
|
1236
|
+
? group.skills
|
|
1237
|
+
: [];
|
|
1238
|
+
return {
|
|
1239
|
+
dir: typeof group.dir === "string" ? group.dir : "",
|
|
1240
|
+
exists: group.exists === true,
|
|
1241
|
+
skills: groupSkills
|
|
1242
|
+
.map((skill) => ({
|
|
1243
|
+
name: String(skill.name ?? ""),
|
|
1244
|
+
description: typeof skill.description === "string"
|
|
1245
|
+
? skill.description
|
|
1246
|
+
: undefined,
|
|
1247
|
+
directory: typeof skill.directory === "string"
|
|
1248
|
+
? skill.directory
|
|
1249
|
+
: undefined,
|
|
1250
|
+
}))
|
|
1251
|
+
.filter((skill) => skill.name),
|
|
1252
|
+
};
|
|
1253
|
+
})
|
|
1254
|
+
.filter((group) => group.dir);
|
|
1255
|
+
// 优先用启动缓存(cachedMcp),运行中回退到 lastMcpCapabilities
|
|
1256
|
+
const mcpSource = entry.cachedMcpCapabilities ?? entry.lastMcpCapabilities;
|
|
1257
|
+
const isStartupMerge = entry.cachedMcpCapabilities !== undefined;
|
|
1258
|
+
const mergedCapabilities = {
|
|
1259
|
+
kind: "combined",
|
|
1260
|
+
mcpServers: mcpSource?.mcpServers ?? [],
|
|
1261
|
+
mcpTools: mcpSource?.mcpTools ?? [],
|
|
1262
|
+
skills,
|
|
1263
|
+
skillsDir,
|
|
1264
|
+
skillGroups,
|
|
1265
|
+
};
|
|
1266
|
+
this.emitProviderEvent(event, "runtime_capabilities", {
|
|
1267
|
+
capabilities: mergedCapabilities,
|
|
1268
|
+
actionType: "runtime_capabilities",
|
|
1269
|
+
actionLabel: "运行时能力",
|
|
1270
|
+
actionDetail: JSON.stringify(mergedCapabilities),
|
|
1271
|
+
});
|
|
1272
|
+
// 更新 lastCombinedCapabilities,作为主动查询的单一数据源
|
|
1273
|
+
entry.lastCombinedCapabilities = mergedCapabilities;
|
|
1274
|
+
adapterDebugLog(`[cap-diag] Combined capabilities emitted ${isStartupMerge ? "(startup merge)" : "(runtime refresh)"}: ${(mcpSource?.mcpTools ?? []).length} MCP tools, ${skills.length} skills`, {
|
|
1275
|
+
sessionId: event.sessionId,
|
|
1276
|
+
mcpServers: mcpSource?.mcpServers ?? [],
|
|
1277
|
+
skillsDir,
|
|
1278
|
+
});
|
|
1279
|
+
// 清理启动缓存与兜底定时器(仅启动合并路径需要;运行中刷新时 cachedMcp 已是 undefined)
|
|
1280
|
+
if (entry.capabilitiesFallbackTimer) {
|
|
1281
|
+
clearTimeout(entry.capabilitiesFallbackTimer);
|
|
1282
|
+
entry.capabilitiesFallbackTimer = undefined;
|
|
1283
|
+
}
|
|
1284
|
+
entry.cachedMcpCapabilities = undefined;
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
872
1287
|
if (event.type === "session_started") {
|
|
873
1288
|
const estimatedTokens = toEstimatedContextTokens(event.data);
|
|
1289
|
+
// ★ 用 runtime emit 的实际压缩配置覆盖 totalUsage。
|
|
1290
|
+
// runtime 的 contextLimit/maxOutput/inputLimit 来自 resolveProviderConfig(读 provider.default.* 三级),
|
|
1291
|
+
// 是后端实际触发压缩用的值;adapter 启动时从顶层+查表解析的值仅作初始占位,必须被覆盖。
|
|
1292
|
+
// compactionThreshold 是后端算好的实际阈值,前端直接显示此值,不再自算。
|
|
1293
|
+
const runtimeContextLimit = toFiniteTokenCount(event.data.contextLimit) || undefined;
|
|
1294
|
+
const runtimeMaxOutput = toFiniteTokenCount(event.data.maxOutput) || undefined;
|
|
1295
|
+
const runtimeInputLimit = toFiniteTokenCount(event.data.inputLimit) || undefined;
|
|
1296
|
+
const runtimeCompactionThreshold = toFiniteTokenCount(event.data.compactionThreshold) || undefined;
|
|
1297
|
+
let usageChanged = false;
|
|
874
1298
|
if (estimatedTokens > 0) {
|
|
875
1299
|
entry.adapterSession.totalUsage.inputTokens = estimatedTokens;
|
|
876
1300
|
entry.adapterSession.totalUsage.outputTokens = 0;
|
|
1301
|
+
usageChanged = true;
|
|
1302
|
+
}
|
|
1303
|
+
if (runtimeContextLimit !== undefined) {
|
|
1304
|
+
entry.adapterSession.totalUsage.contextLimit = runtimeContextLimit;
|
|
1305
|
+
usageChanged = true;
|
|
1306
|
+
}
|
|
1307
|
+
if (runtimeMaxOutput !== undefined) {
|
|
1308
|
+
entry.adapterSession.totalUsage.maxOutput = runtimeMaxOutput;
|
|
1309
|
+
usageChanged = true;
|
|
1310
|
+
}
|
|
1311
|
+
if (runtimeInputLimit !== undefined) {
|
|
1312
|
+
entry.adapterSession.totalUsage.inputLimit = runtimeInputLimit;
|
|
1313
|
+
usageChanged = true;
|
|
1314
|
+
}
|
|
1315
|
+
if (runtimeCompactionThreshold !== undefined) {
|
|
1316
|
+
entry.adapterSession.totalUsage.compactionThreshold = runtimeCompactionThreshold;
|
|
1317
|
+
usageChanged = true;
|
|
1318
|
+
}
|
|
1319
|
+
if (usageChanged) {
|
|
877
1320
|
this.emit("status-change", event.sessionId, entry.adapterSession.status, {
|
|
878
1321
|
usage: entry.adapterSession.totalUsage,
|
|
879
1322
|
});
|
|
@@ -898,6 +1341,113 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
898
1341
|
});
|
|
899
1342
|
return;
|
|
900
1343
|
}
|
|
1344
|
+
if (event.type === "context_compaction") {
|
|
1345
|
+
const data = event.data;
|
|
1346
|
+
const action = data.action;
|
|
1347
|
+
const isStart = action === "start";
|
|
1348
|
+
const isEnd = action === "end";
|
|
1349
|
+
const isPhase = action === "phase";
|
|
1350
|
+
// parentId 用于将子事件(phase)关联到父压缩事件
|
|
1351
|
+
const parentId = data.parentId;
|
|
1352
|
+
// compactionId 作为父事件的 actionId,子事件通过 parentId 引用它
|
|
1353
|
+
const compactionId = data.compactionId;
|
|
1354
|
+
let actionLabel;
|
|
1355
|
+
let actionDetail;
|
|
1356
|
+
if (isStart) {
|
|
1357
|
+
const sizeKB = String(data.sizeKB ?? data.sizeBeforeKB ?? "?");
|
|
1358
|
+
const msgCount = String(data.messageCount ?? data.messageCountBefore ?? "?");
|
|
1359
|
+
const estimatedTokens = data.estimatedTokens;
|
|
1360
|
+
const actualInput = data.actualInputTokens ? `input=${data.actualInputTokens}` : "";
|
|
1361
|
+
const actualOutput = data.actualOutputTokens ? `output=${data.actualOutputTokens}` : "";
|
|
1362
|
+
const trigger = data.trigger ? `, 触发: ${data.trigger}` : "";
|
|
1363
|
+
actionLabel = "上下文压缩开始";
|
|
1364
|
+
if (estimatedTokens !== undefined) {
|
|
1365
|
+
actionDetail = `压缩前: ~${estimatedTokens} tokens, 消息: ${msgCount}条` +
|
|
1366
|
+
(actualInput ? `, 实际 tokens: ${actualInput} ${actualOutput}` : "") + trigger;
|
|
1367
|
+
}
|
|
1368
|
+
else {
|
|
1369
|
+
actionDetail = `压缩前: ${sizeKB}KB, 消息: ${msgCount}条` +
|
|
1370
|
+
(actualInput ? `, 实际 tokens: ${actualInput} ${actualOutput}` : "") + trigger;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
else if (isPhase) {
|
|
1374
|
+
// 子事件(阶段事件):使用 runtime 传入的 actionLabel 和 actionDetail
|
|
1375
|
+
actionLabel = String(data.phaseLabel ?? "压缩阶段");
|
|
1376
|
+
actionDetail = String(data.phaseDetail ?? "");
|
|
1377
|
+
}
|
|
1378
|
+
else {
|
|
1379
|
+
// action === "end" —— 作为压缩完成子事件,嵌套在父事件下
|
|
1380
|
+
const tokensBefore = data.tokensBefore;
|
|
1381
|
+
const tokensAfter = data.tokensAfter;
|
|
1382
|
+
const freedTokensNum = data.freedTokens;
|
|
1383
|
+
const sizeBefore = String(data.sizeBeforeKB ?? "?");
|
|
1384
|
+
const sizeAfter = String(data.sizeAfterKB ?? "?");
|
|
1385
|
+
const freedKB = String(data.freedKB ?? "?");
|
|
1386
|
+
const strategy = String(data.strategy ?? "?");
|
|
1387
|
+
const pruned = Number(data.prunedToolMessages ?? 0);
|
|
1388
|
+
const summarized = Number(data.summarizedMessages ?? 0);
|
|
1389
|
+
actionLabel = "压缩完成";
|
|
1390
|
+
if (tokensBefore !== undefined && tokensAfter !== undefined) {
|
|
1391
|
+
const diff = freedTokensNum ?? (tokensBefore - tokensAfter);
|
|
1392
|
+
actionDetail = `释放: ${diff} tokens, ${tokensBefore} tokens → ${tokensAfter} tokens, 策略: ${strategy}` +
|
|
1393
|
+
(pruned ? `, 裁剪工具: ${pruned}条` : "") +
|
|
1394
|
+
(summarized ? `, 摘要化: ${summarized}条` : "");
|
|
1395
|
+
}
|
|
1396
|
+
else {
|
|
1397
|
+
actionDetail = `释放: ${freedKB}KB, ${sizeBefore}KB → ${sizeAfter}KB, 策略: ${strategy}` +
|
|
1398
|
+
(pruned ? `, 裁剪工具: ${pruned}条` : "") +
|
|
1399
|
+
(summarized ? `, 摘要化: ${summarized}条` : "");
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
// ★ 更新 adapter 内部状态为 compacting,使前端 agent 卡片显示压缩中
|
|
1403
|
+
// 子事件(phase/end)不重复更新状态,仅推送到时间线
|
|
1404
|
+
if (isStart) {
|
|
1405
|
+
entry.adapterSession.status = "compacting";
|
|
1406
|
+
}
|
|
1407
|
+
const actionInfo = {
|
|
1408
|
+
actionType: "context_compaction",
|
|
1409
|
+
actionLabel,
|
|
1410
|
+
actionDetail,
|
|
1411
|
+
};
|
|
1412
|
+
// ★ 事件层级:start 为父事件(root),phase 和 end 为子事件(nested)
|
|
1413
|
+
// - 父事件:actionId = compactionId
|
|
1414
|
+
// - 子事件:parentId = compactionId, actionId 唯一(避免前端 seenActionIds 去重冲突)
|
|
1415
|
+
if (isStart && compactionId) {
|
|
1416
|
+
actionInfo.actionId = compactionId;
|
|
1417
|
+
}
|
|
1418
|
+
else if ((isPhase || isEnd) && compactionId) {
|
|
1419
|
+
actionInfo.parentId = compactionId;
|
|
1420
|
+
if (isPhase) {
|
|
1421
|
+
const phase = String(data.phase ?? "unknown");
|
|
1422
|
+
actionInfo.actionId = `${compactionId}-phase-${phase}`;
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
// end 事件使用独立 actionId,避免与 start 的 compactionId 重复
|
|
1426
|
+
actionInfo.actionId = `${compactionId}-end`;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
// 兼容旧路径:runtime 可能通过 parentId 字段传入
|
|
1430
|
+
if (isPhase && parentId && !actionInfo.parentId) {
|
|
1431
|
+
actionInfo.parentId = parentId;
|
|
1432
|
+
}
|
|
1433
|
+
// ★ 压缩结束时重置 totalUsage(input 和 output 均归零),使面板显示压缩后的真实上下文大小。
|
|
1434
|
+
// 与 runtime 压缩后重置 accumulatedInputTokens/accumulatedOutputTokens = 0(runtime.ts)对齐。
|
|
1435
|
+
// 若只重置 input 不重置 output,压缩后面板 total 仍含压缩前 output,持续虚高。
|
|
1436
|
+
// 将 usage 合并到 end 事件的 actionInfo 中,避免额外的无 actionInfo status-change
|
|
1437
|
+
// 触发前端 handleDeltaStatusEvent 创建无 actionId 的占位事件(会导致扁平化渲染)。
|
|
1438
|
+
if (isEnd) {
|
|
1439
|
+
entry.adapterSession.totalUsage.inputTokens = 0;
|
|
1440
|
+
entry.adapterSession.totalUsage.outputTokens = 0;
|
|
1441
|
+
// ★ 使用 spread 副本,避免后续事件(如 ai_request)修改 totalUsage 时
|
|
1442
|
+
// 通过引用别名反向改变已发出的 compaction 事件 usage 快照。
|
|
1443
|
+
actionInfo.usage = { ...entry.adapterSession.totalUsage };
|
|
1444
|
+
}
|
|
1445
|
+
this.emitProviderEvent(event, "context_compaction", actionInfo);
|
|
1446
|
+
// ★ 与其他事件(tool_use_start/turn_complete 等)一致,
|
|
1447
|
+
// 通过 status-change 事件将压缩状态推送到面板时间线。
|
|
1448
|
+
this.emit("status-change", event.sessionId, "compacting", actionInfo);
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
901
1451
|
this.emitProviderEvent(event, "thinking", {
|
|
902
1452
|
text: JSON.stringify(event.data),
|
|
903
1453
|
});
|
|
@@ -944,9 +1494,16 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
944
1494
|
const entry = this.sessions.get(sessionId);
|
|
945
1495
|
if (!entry || this.isTerminalStatus(entry.adapterSession.status))
|
|
946
1496
|
return;
|
|
1497
|
+
// ★ 修复竞态:递增 token,.finally() 只在 token 匹配时执行清理。
|
|
1498
|
+
// 防止前一轮 submit() 的 .finally() 清除新一轮的看门狗和 autoPollActive。
|
|
1499
|
+
// 场景:看门狗 120s 触发 abort → R2 恢复 → 新一轮 submit 排队 →
|
|
1500
|
+
// 前一轮 executeSubmit 数百秒后才完成 → .finally() 错误清除新一轮状态。
|
|
1501
|
+
const myToken = (entry.autoPollToken ?? 0) + 1;
|
|
1502
|
+
entry.autoPollToken = myToken;
|
|
947
1503
|
entry.autoPollActive = true;
|
|
948
1504
|
entry.autoPollObservedPollingTool = false;
|
|
949
1505
|
entry.autoPollObservedAssistantText = false;
|
|
1506
|
+
entry.autoPollObservedAnyTool = false;
|
|
950
1507
|
entry.stopAutoPollAfterCurrentTurn = false;
|
|
951
1508
|
if (!options?.keepWaitingStatus) {
|
|
952
1509
|
entry.adapterSession.status = "thinking";
|
|
@@ -963,6 +1520,16 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
963
1520
|
const latestEntry = this.sessions.get(sessionId);
|
|
964
1521
|
if (!latestEntry)
|
|
965
1522
|
return;
|
|
1523
|
+
// ★ token 守卫:如果 token 不匹配,说明新一轮 submitAutoPollPrompt 已启动,
|
|
1524
|
+
// 当前轮的清理操作(取消看门狗、清除 autoPollActive)会错误覆盖新一轮状态。
|
|
1525
|
+
// 新一轮已设置了新的看门狗和 autoPollActive=true,直接跳过清理。
|
|
1526
|
+
// 关键:这防止了"前一轮 .finally() 清除新一轮看门狗 → 新一轮 fetch 挂死时无保护 → 永久卡死"的竞态。
|
|
1527
|
+
if (latestEntry.autoPollToken !== myToken) {
|
|
1528
|
+
adapterDebugLog(`[ACode] submitAutoPollPrompt.finally: token mismatch ` +
|
|
1529
|
+
`(my=${myToken}, current=${latestEntry.autoPollToken}), ` +
|
|
1530
|
+
`skipping cleanup — newer turn has taken over`);
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
966
1533
|
// 无论 submit 是正常返回还是超时恢复,都需要取消超时检测(超时回调里已设置 submitTimeoutTimer=undefined,再调 cancel 是安全的)
|
|
967
1534
|
this.cancelSubmitTimeoutDetection(latestEntry);
|
|
968
1535
|
latestEntry.autoPollActive = false;
|
|
@@ -980,6 +1547,7 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
980
1547
|
latestEntry.stopAutoPollAfterCurrentTurn = false;
|
|
981
1548
|
latestEntry.autoPollObservedPollingTool = false;
|
|
982
1549
|
latestEntry.autoPollObservedAssistantText = false;
|
|
1550
|
+
latestEntry.autoPollObservedAnyTool = false;
|
|
983
1551
|
return;
|
|
984
1552
|
}
|
|
985
1553
|
if (latestEntry.adapterSession.status === "error") {
|
|
@@ -1057,10 +1625,57 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1057
1625
|
data: { text: error instanceof Error ? error.message : String(error) },
|
|
1058
1626
|
});
|
|
1059
1627
|
}
|
|
1628
|
+
/**
|
|
1629
|
+
* 从会话消息中估算 AI 输出 token 数(字符数 / 4 粗略估算)。
|
|
1630
|
+
* 注意:该估算对 CJK 文本偏高(实际每字约 1-2 tokens),
|
|
1631
|
+
* 对纯 ASCII 文本偏低(约 3-5 字符/token),仅供面板展示实时趋势使用,
|
|
1632
|
+
* 不应用于精确计费或配额判定。精确值以 turn_complete 事件中的 usage 为准。
|
|
1633
|
+
*/
|
|
1634
|
+
estimateOutputTokensFromMessages(messages) {
|
|
1635
|
+
const assistantText = messages
|
|
1636
|
+
.filter((m) => m.role === "assistant")
|
|
1637
|
+
.map((m) => m.content)
|
|
1638
|
+
.join("");
|
|
1639
|
+
return Math.max(0, Math.round(assistantText.length / 4));
|
|
1640
|
+
}
|
|
1060
1641
|
recoverTurnFailure(sessionId, entry) {
|
|
1061
1642
|
if (this.isTerminalStatus(entry.adapterSession.status)) {
|
|
1062
1643
|
return;
|
|
1063
1644
|
}
|
|
1645
|
+
// ★ 修复 BUG(时间线永久卡在"编辑文件"):当恢复前状态为 tool_using 时,
|
|
1646
|
+
// 补发 tool_use_end 事件,关闭前端时间线中由 tool_use_start 创建的工具条目。
|
|
1647
|
+
// 根因:数据流看门狗在 tool_using 状态触发后调用本方法,但原实现只切换状态为
|
|
1648
|
+
// waiting_input,从不补发 tool_use_end。而 runtime 端因 abortRequested===true,
|
|
1649
|
+
// 在 catch 块中 rethrow 前跳过了 tool_result 发射(3241 行),
|
|
1650
|
+
// 导致 adapter 永远收不到 tool_result,tool_use_end 永不发射,时间线永久卡住。
|
|
1651
|
+
if (entry.adapterSession.status === "tool_using" && entry.pendingToolEventData) {
|
|
1652
|
+
const toolData = entry.pendingToolEventData;
|
|
1653
|
+
adapterDebugLog(`[ACode] recoverTurnFailure: 补发 tool_use_end (tool: ${toolData.toolName ?? "unknown"}) ` +
|
|
1654
|
+
`关闭时间线条目,避免永久卡在工具执行状态`);
|
|
1655
|
+
this.emit("event", {
|
|
1656
|
+
type: "tool_use_end",
|
|
1657
|
+
sessionId,
|
|
1658
|
+
timestamp: new Date().toISOString(),
|
|
1659
|
+
data: {
|
|
1660
|
+
...toolData,
|
|
1661
|
+
toolResult: "[aborted] 工具执行被数据流看门狗中断",
|
|
1662
|
+
},
|
|
1663
|
+
});
|
|
1664
|
+
entry.pendingToolEventData = undefined;
|
|
1665
|
+
}
|
|
1666
|
+
// ★ 修复:超时恢复前强制 emit turn_complete,计入已有的 output tokens,
|
|
1667
|
+
// 否则面板上 output_tokens 永远是 0(turn_complete 从未发射)。
|
|
1668
|
+
// 使用消息历史中的 assistant 文本粗略估算 output tokens(累计估算)。
|
|
1669
|
+
// 此为异常恢复路径(turn_complete 未到达,无真实 completion_tokens),
|
|
1670
|
+
// 直接覆盖式赋值(不再用 delta += 累加,避免与覆盖式语义冲突导致重复计算)。
|
|
1671
|
+
const estimatedOutput = this.estimateOutputTokensFromMessages(entry.adapterSession.messages);
|
|
1672
|
+
if (estimatedOutput > 0) {
|
|
1673
|
+
const prev = entry.adapterSession.totalUsage.outputTokens;
|
|
1674
|
+
entry.adapterSession.totalUsage.outputTokens = estimatedOutput;
|
|
1675
|
+
adapterDebugLog(`[ACode] recoverTurnFailure: 估算覆盖 output tokens ${prev} -> ${estimatedOutput} ` +
|
|
1676
|
+
`(totalUsage now: input=${entry.adapterSession.totalUsage.inputTokens}, ` +
|
|
1677
|
+
`output=${entry.adapterSession.totalUsage.outputTokens})`);
|
|
1678
|
+
}
|
|
1064
1679
|
// ★ 修复:检测 idle 命令是否包含 poll_message,在 waiting_input 状态变更中携带 idle 动作信息
|
|
1065
1680
|
const idleCommands = entry.idleCommands?.idleInputCommand;
|
|
1066
1681
|
const willResumePolling = Boolean(idleCommands && isMessagePollingIdleCommand(idleCommands));
|
|
@@ -1072,13 +1687,19 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1072
1687
|
if (entry.autoPollTimer || entry.failedTurnRecoveryScheduled) {
|
|
1073
1688
|
if (entry.adapterSession.status !== "waiting_input") {
|
|
1074
1689
|
entry.adapterSession.status = "waiting_input";
|
|
1075
|
-
this.emit("status-change", sessionId, "waiting_input",
|
|
1690
|
+
this.emit("status-change", sessionId, "waiting_input", {
|
|
1691
|
+
usage: entry.adapterSession.totalUsage,
|
|
1692
|
+
...idleActionInfo,
|
|
1693
|
+
});
|
|
1076
1694
|
}
|
|
1077
1695
|
return;
|
|
1078
1696
|
}
|
|
1079
1697
|
this.cancelScheduledAutoPoll(entry);
|
|
1080
1698
|
entry.adapterSession.status = "waiting_input";
|
|
1081
|
-
this.emit("status-change", sessionId, "waiting_input",
|
|
1699
|
+
this.emit("status-change", sessionId, "waiting_input", {
|
|
1700
|
+
usage: entry.adapterSession.totalUsage,
|
|
1701
|
+
...idleActionInfo,
|
|
1702
|
+
});
|
|
1082
1703
|
entry.failedTurnRecoveryScheduled = true;
|
|
1083
1704
|
entry.autoPollTimer = setTimeout(() => {
|
|
1084
1705
|
const currentEntry = this.sessions.get(sessionId);
|
|
@@ -1112,20 +1733,23 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1112
1733
|
* look permanently stuck. Recover to waiting_input and arm the listening loop.
|
|
1113
1734
|
*/
|
|
1114
1735
|
recoverResolvedTurnWithoutTerminalEvent(sessionId) {
|
|
1115
|
-
|
|
1736
|
+
adapterDebugLog(`[ACode] ▶️ recoverResolvedTurnWithoutTerminalEvent called for session ${sessionId}`);
|
|
1116
1737
|
const entry = this.sessions.get(sessionId);
|
|
1117
1738
|
if (!entry || this.isTerminalStatus(entry.adapterSession.status)) {
|
|
1118
|
-
|
|
1739
|
+
adapterDebugLog(`[ACode] Skipping recovery: entry=${!!entry}, isTerminal=${entry ? this.isTerminalStatus(entry.adapterSession.status) : 'N/A'}`);
|
|
1119
1740
|
return;
|
|
1120
1741
|
}
|
|
1121
|
-
|
|
1742
|
+
adapterDebugLog(`[ACode] Current status: ${entry.adapterSession.status}`);
|
|
1122
1743
|
if (entry.adapterSession.status !== "thinking" &&
|
|
1123
1744
|
entry.adapterSession.status !== "tool_using") {
|
|
1124
|
-
|
|
1745
|
+
adapterDebugLog(`[ACode] Skipping recovery: status is ${entry.adapterSession.status} (not thinking/tool_using)`);
|
|
1125
1746
|
return;
|
|
1126
1747
|
}
|
|
1127
|
-
console.
|
|
1128
|
-
|
|
1748
|
+
console.warn(`[ACode] ⚠️ Detected stuck state: ${entry.adapterSession.status} after submit resolved! ` +
|
|
1749
|
+
`Recovering to waiting_input. ` +
|
|
1750
|
+
`If runtime.submit() is still pending (e.g., built-in tool execution not interruptible by abort()), ` +
|
|
1751
|
+
`the submitQueue is deadlocked and subsequent auto-poll/user submits will never execute.`);
|
|
1752
|
+
adapterDebugLog(`[ACode] Recovering to waiting_input...`);
|
|
1129
1753
|
entry.adapterSession.status = "waiting_input";
|
|
1130
1754
|
// ★ 修复:检测 idle 命令是否包含 poll_message,在 waiting_input 状态变更中携带 idle 动作信息
|
|
1131
1755
|
const idleCommands = entry.idleCommands?.idleInputCommand;
|
|
@@ -1136,42 +1760,120 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1136
1760
|
actionDetail: 'aws-mcp/poll_message',
|
|
1137
1761
|
} : undefined;
|
|
1138
1762
|
this.emit("status-change", sessionId, "waiting_input", idleActionInfo);
|
|
1139
|
-
|
|
1140
|
-
|
|
1763
|
+
adapterDebugLog(`[ACode] ✅ Recovery complete, status now: ${entry.adapterSession.status}`);
|
|
1764
|
+
adapterDebugLog(`[ACode] ▶️ Arming idle loop or auto-poll...`);
|
|
1141
1765
|
this.armIdleLoopOrAutoPoll(sessionId);
|
|
1142
|
-
|
|
1766
|
+
adapterDebugLog(`[ACode] ✅ Idle loop armed, recovery process complete`);
|
|
1143
1767
|
}
|
|
1144
1768
|
/**
|
|
1145
|
-
*
|
|
1769
|
+
* 启动异步看门狗:周期性轮询运行时状态,仅在真正卡死时恢复。
|
|
1146
1770
|
*
|
|
1147
|
-
*
|
|
1148
|
-
*
|
|
1771
|
+
* 替代原来两个独立的 setTimeout(submitTimeoutDetection + dataFlowWatchdog),
|
|
1772
|
+
* 改为单一 setInterval 轮询。核心设计:
|
|
1773
|
+
* - 每 ACODE_WATCHDOG_POLL_INTERVAL_MS(5s)检查一次状态
|
|
1774
|
+
* - waiting_input 状态下不累计超时(agent 正在有意等待,如 poll_message/ask_user_question)
|
|
1775
|
+
* - 仅在 thinking/tool_using 状态下累计"活跃持续时间"和"无数据流时间"
|
|
1776
|
+
* - submit 超时 120s 或数据流超时 40s 时触发 recoverTurnFailure
|
|
1777
|
+
*
|
|
1778
|
+
* 相比 setTimeout 方案的优势:
|
|
1779
|
+
* 1. 状态感知:每次轮询都读取当前真实状态,不会因状态在定时周期内多次变化而误判
|
|
1780
|
+
* 2. waiting_input 自动暂停计时:无需递归重新计时,天然跳过
|
|
1781
|
+
* 3. 单一 timer:避免两个独立 timer 的竞态和重复恢复
|
|
1149
1782
|
*/
|
|
1150
1783
|
startSubmitTimeoutDetection(sessionId, entry, source) {
|
|
1151
|
-
// 清除之前的超时检测
|
|
1152
1784
|
this.cancelSubmitTimeoutDetection(entry);
|
|
1153
|
-
entry.
|
|
1154
|
-
entry.
|
|
1785
|
+
entry.watchdogSubmitStartTime = Date.now();
|
|
1786
|
+
entry.watchdogLastDataFlowTime = Date.now();
|
|
1787
|
+
entry.watchdogTimer = setInterval(() => {
|
|
1155
1788
|
const currentEntry = this.sessions.get(sessionId);
|
|
1156
|
-
if (!currentEntry)
|
|
1789
|
+
if (!currentEntry) {
|
|
1790
|
+
this.cancelSubmitTimeoutDetection(entry);
|
|
1157
1791
|
return;
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1792
|
+
}
|
|
1793
|
+
const status = currentEntry.adapterSession.status;
|
|
1794
|
+
const now = Date.now();
|
|
1795
|
+
// waiting_input:agent 正在有意等待(poll_message/ask_user_question),不计时
|
|
1796
|
+
if (status === "waiting_input") {
|
|
1797
|
+
// 重置计时基准,下次进入 thinking/tool_using 时从 0 开始计
|
|
1798
|
+
currentEntry.watchdogSubmitStartTime = now;
|
|
1799
|
+
currentEntry.watchdogLastDataFlowTime = now;
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
// 仅 thinking/tool_using 状态下检查超时
|
|
1803
|
+
if (status !== "thinking" && status !== "tool_using") {
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
const submitElapsed = now - (currentEntry.watchdogSubmitStartTime || now);
|
|
1807
|
+
const dataFlowElapsed = now - (currentEntry.watchdogLastDataFlowTime || now);
|
|
1808
|
+
// 数据流超时(40s 无流式数据)— 比 submit 超时(120s)更短,优先触发
|
|
1809
|
+
if (dataFlowElapsed >= ACODE_DATA_FLOW_STREAM_WATCHDOG_MS) {
|
|
1810
|
+
console.warn(`[ACode] CRITICAL: Data flow watchdog triggered after ${Math.round(dataFlowElapsed / 1000)}s ` +
|
|
1811
|
+
`(source: ${source}, session: ${sessionId}, status: ${status}). ` +
|
|
1812
|
+
`No stream data received. Forcing recovery via recoverTurnFailure.`);
|
|
1813
|
+
this.executeWatchdogRecovery(sessionId, currentEntry, source, "data_flow", dataFlowElapsed);
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
// submit 超时(120s 未返回)
|
|
1817
|
+
if (submitElapsed >= ACODE_SUBMIT_TIMEOUT_MS) {
|
|
1818
|
+
console.warn(`[ACode] CRITICAL: submit timeout detected after ${Math.round(submitElapsed / 1000)}s ` +
|
|
1819
|
+
`(source: ${source}, session: ${sessionId}, status: ${status}). ` +
|
|
1820
|
+
`Forcing recovery to prevent permanent state stuck.`);
|
|
1821
|
+
this.executeWatchdogRecovery(sessionId, currentEntry, source, "submit_timeout", submitElapsed);
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
}, ACODE_WATCHDOG_POLL_INTERVAL_MS);
|
|
1166
1825
|
}
|
|
1167
1826
|
/**
|
|
1168
|
-
*
|
|
1827
|
+
* 执行看门狗恢复:abort runtime + recoverTurnFailure
|
|
1828
|
+
*/
|
|
1829
|
+
executeWatchdogRecovery(sessionId, entry, source, reason, elapsedMs) {
|
|
1830
|
+
// ★ 关键修复:先 abort runtime 打断挂起的 fetch / stream reader,
|
|
1831
|
+
// 否则 submitQueue 永久阻塞,后续 submit 全部排队等待,agent 彻底死锁。
|
|
1832
|
+
adapterDebugLog(`[ACode] Watchdog (${reason}): calling runtime.abort() after ${Math.round(elapsedMs / 1000)}s...`);
|
|
1833
|
+
try {
|
|
1834
|
+
void entry.runtime.abort();
|
|
1835
|
+
adapterDebugLog(`[ACode] Watchdog (${reason}): runtime.abort() called`);
|
|
1836
|
+
}
|
|
1837
|
+
catch (abortError) {
|
|
1838
|
+
console.warn(`[ACode] ⚠️ Watchdog (${reason}): runtime.abort() threw: ${abortError instanceof Error ? abortError.message : String(abortError)}`);
|
|
1839
|
+
}
|
|
1840
|
+
// 取消看门狗(recoverTurnFailure 会重新 arm idle loop)
|
|
1841
|
+
this.cancelSubmitTimeoutDetection(entry);
|
|
1842
|
+
this.recoverTurnFailure(sessionId, entry);
|
|
1843
|
+
console.warn(`[ACode] ⚠️ Watchdog (${reason}) recovery complete. ` +
|
|
1844
|
+
`If abort() could not cancel the underlying operation, submitQueue may still be deadlocked.`);
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* 取消看门狗
|
|
1169
1848
|
*/
|
|
1170
1849
|
cancelSubmitTimeoutDetection(entry) {
|
|
1171
|
-
if (entry.
|
|
1172
|
-
|
|
1173
|
-
entry.
|
|
1850
|
+
if (entry.watchdogTimer) {
|
|
1851
|
+
clearInterval(entry.watchdogTimer);
|
|
1852
|
+
entry.watchdogTimer = undefined;
|
|
1174
1853
|
}
|
|
1175
|
-
entry.
|
|
1854
|
+
entry.watchdogSubmitStartTime = undefined;
|
|
1855
|
+
entry.watchdogLastDataFlowTime = undefined;
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* 重置数据流计时基准:收到流式数据(assistant_delta/thinking_delta)时调用。
|
|
1859
|
+
* 不再重启定时器,仅更新计时基准,下次轮询时从新基准计算。
|
|
1860
|
+
*/
|
|
1861
|
+
resetDataFlowWatchdog(sessionId, entry) {
|
|
1862
|
+
void sessionId;
|
|
1863
|
+
entry.watchdogLastDataFlowTime = Date.now();
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* 启动数据流看门狗:由 ai_request 调用,初始化数据流计时基准。
|
|
1867
|
+
* 实际的轮询由 startSubmitTimeoutDetection 的 interval 统一处理。
|
|
1868
|
+
*/
|
|
1869
|
+
startDataFlowWatchdog(sessionId, entry) {
|
|
1870
|
+
void sessionId;
|
|
1871
|
+
entry.watchdogLastDataFlowTime = Date.now();
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1874
|
+
* 取消数据流看门狗(已合并到 cancelSubmitTimeoutDetection,此方法保留兼容性)
|
|
1875
|
+
*/
|
|
1876
|
+
cancelDataFlowWatchdog(entry) {
|
|
1877
|
+
entry.watchdogLastDataFlowTime = Date.now();
|
|
1176
1878
|
}
|
|
1177
1879
|
}
|