aws-runtime-bridge 1.9.40 → 1.9.45
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 +31 -4
- package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/AcodeSdkAdapter.js +382 -57
- package/dist/adapter/ClaudeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/ClaudeSdkAdapter.js +1 -0
- 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/types.d.ts +21 -6
- package/dist/adapter/types.d.ts.map +1 -1
- package/dist/adapter/types.js +10 -1
- package/dist/index.js +11 -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/file-browser.d.ts.map +1 -1
- package/dist/routes/file-browser.js +47 -1
- package/dist/routes/processes.d.ts.map +1 -1
- package/dist/routes/processes.js +7 -0
- package/dist/routes/runtime-binding.d.ts.map +1 -1
- package/dist/routes/runtime-binding.js +6 -3
- package/dist/routes/sessions.js +93 -5
- package/dist/routes/sub-agents.js +15 -1
- package/dist/routes/terminal.d.ts +15 -0
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +162 -5
- 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/aws-client-agent-mcp.d.ts.map +1 -1
- package/dist/services/aws-client-agent-mcp.js +11 -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 +7 -1
- package/dist/services/session-output.d.ts.map +1 -1
- package/dist/services/session-output.js +16 -2
- package/dist/services/workspace-files.d.ts +48 -0
- package/dist/services/workspace-files.d.ts.map +1 -1
- package/dist/services/workspace-files.js +285 -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 +43 -7
- package/package/acode/dist/compaction.d.ts.map +1 -1
- package/package/acode/dist/compaction.js +286 -31
- package/package/acode/dist/runtime.d.ts +130 -0
- package/package/acode/dist/runtime.d.ts.map +1 -1
- package/package/acode/dist/runtime.js +972 -71
- package/package/acode/dist/sub-agent-manager.d.ts +96 -1
- package/package/acode/dist/sub-agent-manager.d.ts.map +1 -1
- package/package/acode/dist/sub-agent-manager.js +293 -3
- package/package/acode/dist/sub-agent-store.d.ts +8 -1
- package/package/acode/dist/sub-agent-store.d.ts.map +1 -1
- package/package/acode/dist/sub-agent-store.js +55 -0
- package/package/acode/dist/sub-agent-tools.d.ts.map +1 -1
- package/package/acode/dist/sub-agent-tools.js +5 -4
- package/package/acode/dist/sub-agent-types.d.ts +4 -0
- package/package/acode/dist/sub-agent-types.d.ts.map +1 -1
- package/package/acode/dist/types.d.ts +19 -0
- package/package/acode/dist/types.d.ts.map +1 -1
- 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 -0
- 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 +102 -34
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.js +637 -209
- package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +151 -51
- 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 +7 -0
- package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.js +18 -0
- package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-store.d.ts +32 -9
- package/package/aws-client-agent-mcp/dist/memory-store.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-store.js +58 -19
- package/package/aws-client-agent-mcp/dist/memory-store.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/memory-tools.js +3 -3
- package/package/aws-client-agent-mcp/dist/memory-tools.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.d.ts +21 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.js +47 -21
- 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 -0
- 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 +18 -0
- package/package/aws-client-agent-mcp/dist/server-memory-store.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts +9 -0
- 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 +1 -1
|
@@ -41,6 +41,12 @@ const ACODE_AUTO_POLL_PROMPT = [
|
|
|
41
41
|
"Do not answer in plain text before calling aws-mcp__poll_message.",
|
|
42
42
|
"After handling any received work, reply through the appropriate aws-mcp messaging tool, then continue listening again.",
|
|
43
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;
|
|
44
50
|
const ACODE_AUTO_POLL_DELAY_MS = 250;
|
|
45
51
|
// Backoff used when a polling-driven turn fails (e.g., 60s MCP tool timeout,
|
|
46
52
|
// model 5xx, provider unreachable). Larger than ACODE_AUTO_POLL_DELAY_MS to
|
|
@@ -116,6 +122,25 @@ function lookupModelContextLimit(modelName) {
|
|
|
116
122
|
return MODEL_CONTEXT_LIMITS[matches[0]].contextLimit;
|
|
117
123
|
return DEFAULT_CONTEXT_LIMIT;
|
|
118
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
|
+
}
|
|
119
144
|
function toUsage(value) {
|
|
120
145
|
if (!value || typeof value !== "object") {
|
|
121
146
|
return undefined;
|
|
@@ -224,12 +249,13 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
224
249
|
const resolvedModel = resolved.model;
|
|
225
250
|
// priority: acode.json top-level contextLimit > model lookup table > 128K default
|
|
226
251
|
const contextLimit = configContextLimit ?? lookupModelContextLimit(resolvedModel);
|
|
252
|
+
const maxOutput = lookupModelMaxOutput(resolvedModel);
|
|
227
253
|
const session = {
|
|
228
254
|
sessionId,
|
|
229
255
|
status: "starting",
|
|
230
256
|
messages: [],
|
|
231
257
|
createdAt: new Date().toISOString(),
|
|
232
|
-
totalUsage: { inputTokens: 0, outputTokens: 0, contextLimit },
|
|
258
|
+
totalUsage: { inputTokens: 0, outputTokens: 0, contextLimit, maxOutput },
|
|
233
259
|
};
|
|
234
260
|
this.sessions.set(sessionId, {
|
|
235
261
|
adapterSession: session,
|
|
@@ -361,6 +387,7 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
361
387
|
entry.autoPollActive = false;
|
|
362
388
|
entry.autoPollObservedPollingTool = false;
|
|
363
389
|
entry.autoPollObservedAssistantText = false;
|
|
390
|
+
entry.autoPollObservedAnyTool = false;
|
|
364
391
|
entry.stopAutoPollAfterCurrentTurn = false;
|
|
365
392
|
entry.idlePollLoop?.stop();
|
|
366
393
|
if (!entry.runtime.resetContext) {
|
|
@@ -368,7 +395,17 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
368
395
|
}
|
|
369
396
|
await entry.runtime.resetContext();
|
|
370
397
|
entry.adapterSession.messages = [];
|
|
371
|
-
|
|
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
|
+
};
|
|
372
409
|
entry.adapterSession.status = "waiting_input";
|
|
373
410
|
this.emit("status-change", sessionId, "waiting_input", {
|
|
374
411
|
usage: entry.adapterSession.totalUsage,
|
|
@@ -619,6 +656,9 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
619
656
|
: undefined,
|
|
620
657
|
env: config.envOverrides,
|
|
621
658
|
subAgentManager,
|
|
659
|
+
...(config.subAgentModel ? { subAgentModel: config.subAgentModel } : {}),
|
|
660
|
+
...(config.subAgentEffort ? { subAgentEffort: config.subAgentEffort } : {}),
|
|
661
|
+
...(config.instanceAiConfig?.effort ? { effort: config.instanceAiConfig.effort } : {}),
|
|
622
662
|
});
|
|
623
663
|
const debugLogger = getInstanceAiConfigDebugLogger();
|
|
624
664
|
void debugLogger.log({
|
|
@@ -715,11 +755,15 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
715
755
|
content: text,
|
|
716
756
|
timestamp: event.timestamp,
|
|
717
757
|
});
|
|
758
|
+
// 收到流式数据,重置看门狗
|
|
759
|
+
this.resetDataFlowWatchdog(event.sessionId, entry);
|
|
718
760
|
this.emitProviderEvent(event, "text_delta", { text });
|
|
719
761
|
return;
|
|
720
762
|
}
|
|
721
763
|
if (event.type === "thinking_delta") {
|
|
722
764
|
const text = typeof event.data.text === "string" ? event.data.text : "";
|
|
765
|
+
// 收到流式思考过程数据,重置看门狗
|
|
766
|
+
this.resetDataFlowWatchdog(event.sessionId, entry);
|
|
723
767
|
this.emitProviderEvent(event, "thinking", { text });
|
|
724
768
|
return;
|
|
725
769
|
}
|
|
@@ -742,26 +786,61 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
742
786
|
}
|
|
743
787
|
if (event.type === "ai_request") {
|
|
744
788
|
const estimatedTokens = toEstimatedContextTokens(event.data);
|
|
789
|
+
let displayUsage;
|
|
745
790
|
if (estimatedTokens > 0) {
|
|
746
|
-
// ★
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
//
|
|
752
|
-
|
|
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 {
|
|
753
804
|
entry.adapterSession.totalUsage.inputTokens = estimatedTokens;
|
|
754
805
|
}
|
|
755
|
-
// ★
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
};
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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";
|
|
764
833
|
}
|
|
834
|
+
const aiRequestActionInfo = {
|
|
835
|
+
actionType: "ai_request",
|
|
836
|
+
actionLabel: "AI请求",
|
|
837
|
+
};
|
|
838
|
+
if (displayUsage) {
|
|
839
|
+
aiRequestActionInfo.usage = displayUsage;
|
|
840
|
+
}
|
|
841
|
+
this.emit("status-change", event.sessionId, entry.adapterSession.status, aiRequestActionInfo);
|
|
842
|
+
// 启动数据流看门狗:ai_request 发出后开始监控流式数据返回
|
|
843
|
+
this.startDataFlowWatchdog(event.sessionId, entry);
|
|
765
844
|
this.emitProviderEvent(event, "ai_request", {
|
|
766
845
|
model: typeof event.data.model === "string" ? event.data.model : undefined,
|
|
767
846
|
endpoint: typeof event.data.endpoint === "string"
|
|
@@ -795,8 +874,25 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
795
874
|
if (event.type === "turn_complete") {
|
|
796
875
|
const usage = toUsage(event.data.usage);
|
|
797
876
|
if (usage) {
|
|
798
|
-
// ★
|
|
799
|
-
|
|
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
|
+
}
|
|
800
896
|
}
|
|
801
897
|
// 关键日志:记录 turn_complete 事件
|
|
802
898
|
const previousStatus = entry.adapterSession.status;
|
|
@@ -817,9 +913,24 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
817
913
|
// session got permanently stuck in 'error'. Now we always recover
|
|
818
914
|
// for non-terminal sessions so the idle loop can retry.
|
|
819
915
|
adapterDebugLog(`[ACode] Recovering from failed turn_complete (session: ${event.sessionId})`);
|
|
916
|
+
this.cancelDataFlowWatchdog(entry);
|
|
820
917
|
this.recoverTurnFailure(event.sessionId, entry);
|
|
821
918
|
return;
|
|
822
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);
|
|
929
|
+
this.recoverTurnFailure(event.sessionId, entry);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
// turn 正常完成,取消数据流看门狗
|
|
933
|
+
this.cancelDataFlowWatchdog(entry);
|
|
823
934
|
this.emitProviderEvent(event, "turn_complete", {
|
|
824
935
|
usage: entry.adapterSession.totalUsage,
|
|
825
936
|
responseBody: typeof event.data.responseBody === "string"
|
|
@@ -836,15 +947,17 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
836
947
|
adapterDebugLog(`[ACode] Status changed to waiting_input after turn_complete (session: ${event.sessionId})`);
|
|
837
948
|
if (entry.autoPollActive &&
|
|
838
949
|
entry.autoPollObservedAssistantText &&
|
|
839
|
-
!entry.autoPollObservedPollingTool
|
|
950
|
+
!entry.autoPollObservedPollingTool &&
|
|
951
|
+
!entry.autoPollObservedAnyTool) {
|
|
840
952
|
entry.stopAutoPollAfterCurrentTurn = true;
|
|
841
|
-
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}`);
|
|
842
954
|
return;
|
|
843
955
|
}
|
|
844
956
|
this.armIdleLoopOrAutoPoll(event.sessionId);
|
|
845
957
|
return;
|
|
846
958
|
}
|
|
847
959
|
if (event.type === "session_complete") {
|
|
960
|
+
this.cancelDataFlowWatchdog(entry);
|
|
848
961
|
this.cancelScheduledAutoPoll(entry);
|
|
849
962
|
entry.failedTurnRecoveryScheduled = false;
|
|
850
963
|
entry.idlePollLoop?.stop();
|
|
@@ -857,6 +970,9 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
857
970
|
}
|
|
858
971
|
if (event.type === "question_request") {
|
|
859
972
|
// ask_user_question 工具被调用:runtime 在 execute() 中阻塞等待答案。
|
|
973
|
+
// 取消数据流看门狗(用户思考/回答时间不计入 40s 超时),
|
|
974
|
+
// 等用户回答后 LLM 重新输出时 assistant_delta/thinking_delta 会重新启动看门狗。
|
|
975
|
+
this.cancelDataFlowWatchdog(entry);
|
|
860
976
|
// 取消 submit 超时检测(用户回答时间不计入 2 分钟超时),
|
|
861
977
|
// 记录 toolCallId + questions 供 sendQuestionAnswer 和 getPendingQuestion 使用,
|
|
862
978
|
// 转发事件让 terminal.ts 推到前端。
|
|
@@ -873,6 +989,7 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
873
989
|
}
|
|
874
990
|
this.emit("ask-user-question", {
|
|
875
991
|
sessionId: event.sessionId,
|
|
992
|
+
toolCallId: toolCallId || undefined,
|
|
876
993
|
questions,
|
|
877
994
|
});
|
|
878
995
|
return;
|
|
@@ -881,11 +998,16 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
881
998
|
// nonFatal=true 表示工具执行失败等非致命错误(Agent 会继续处理),
|
|
882
999
|
// 仅推送事件到时间线,不变更运行态为 "error",避免面板启动/停止按钮异常。
|
|
883
1000
|
if (event.data.nonFatal) {
|
|
1001
|
+
// ★ 修复:重试期间(每次 fetch failed 后退避)无流式数据到达,
|
|
1002
|
+
// 看门狗仍以"上次 delta + 40s"倒计时,可能在重试周期内误触发 40s 超时。
|
|
1003
|
+
// 重置数据流计时基准,让看门狗从重试时刻重新计时。
|
|
1004
|
+
this.resetDataFlowWatchdog(event.sessionId, entry);
|
|
884
1005
|
this.emitProviderEvent(event, "error", {
|
|
885
1006
|
text: String(event.data.message || "ACode runtime error"),
|
|
886
1007
|
});
|
|
887
1008
|
return;
|
|
888
1009
|
}
|
|
1010
|
+
this.cancelDataFlowWatchdog(entry);
|
|
889
1011
|
entry.adapterSession.status = "error";
|
|
890
1012
|
this.emitProviderEvent(event, "error", {
|
|
891
1013
|
text: String(event.data.message || "ACode runtime error"),
|
|
@@ -991,6 +1113,13 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
991
1113
|
}, CAPABILITIES_FALLBACK_MS);
|
|
992
1114
|
return;
|
|
993
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}`);
|
|
994
1123
|
const server = typeof event.data.server === "string" ? event.data.server : "mcp";
|
|
995
1124
|
const tool = typeof event.data.tool === "string" ? event.data.tool : "";
|
|
996
1125
|
const toolName = server && tool ? `${server}__${tool}` : tool || server;
|
|
@@ -1000,8 +1129,13 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1000
1129
|
? event.data.arguments
|
|
1001
1130
|
: undefined;
|
|
1002
1131
|
const actionInfo = getToolActionInfo(toolName, toolInput);
|
|
1003
|
-
if (entry.autoPollActive
|
|
1004
|
-
|
|
1132
|
+
if (entry.autoPollActive) {
|
|
1133
|
+
// ★ 修复永久卡死:任何工具调用(包括工作工具)都标记为已观察,
|
|
1134
|
+
// 避免 turn_complete 时误判为"只输出文本不调用工具"而停止 auto-poll。
|
|
1135
|
+
entry.autoPollObservedAnyTool = true;
|
|
1136
|
+
if (actionInfo.actionType === "idle") {
|
|
1137
|
+
entry.autoPollObservedPollingTool = true;
|
|
1138
|
+
}
|
|
1005
1139
|
}
|
|
1006
1140
|
const actionDetail = isAcodeSemanticToolAction(actionInfo.actionType)
|
|
1007
1141
|
? actionInfo.actionDetail ||
|
|
@@ -1035,6 +1169,14 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1035
1169
|
? "waiting_input"
|
|
1036
1170
|
: "thinking";
|
|
1037
1171
|
entry.adapterSession.status = status;
|
|
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;
|
|
1038
1180
|
// ★ 修复重复时间线条目:idle 类型(poll_message)的 tool_use_end status-change
|
|
1039
1181
|
// 始终传 undefined,避免 terminal.ts 的 restoredActionInfo 恢复出 actionType+actionLabel
|
|
1040
1182
|
// 导致 dashboard 创建第二个"调用MCP"条目。tool_use_start 的 status-change 已创建条目。
|
|
@@ -1045,6 +1187,16 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1045
1187
|
}
|
|
1046
1188
|
const status = actionInfo.actionType === "idle" ? "waiting_input" : "tool_using";
|
|
1047
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
|
+
}
|
|
1048
1200
|
this.emitProviderEvent(event, "tool_use_start", eventData);
|
|
1049
1201
|
this.emit("status-change", event.sessionId, status, eventData);
|
|
1050
1202
|
return;
|
|
@@ -1134,9 +1286,37 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1134
1286
|
}
|
|
1135
1287
|
if (event.type === "session_started") {
|
|
1136
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;
|
|
1137
1298
|
if (estimatedTokens > 0) {
|
|
1138
1299
|
entry.adapterSession.totalUsage.inputTokens = estimatedTokens;
|
|
1139
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) {
|
|
1140
1320
|
this.emit("status-change", event.sessionId, entry.adapterSession.status, {
|
|
1141
1321
|
usage: entry.adapterSession.totalUsage,
|
|
1142
1322
|
});
|
|
@@ -1176,12 +1356,19 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1176
1356
|
if (isStart) {
|
|
1177
1357
|
const sizeKB = String(data.sizeKB ?? data.sizeBeforeKB ?? "?");
|
|
1178
1358
|
const msgCount = String(data.messageCount ?? data.messageCountBefore ?? "?");
|
|
1359
|
+
const estimatedTokens = data.estimatedTokens;
|
|
1179
1360
|
const actualInput = data.actualInputTokens ? `input=${data.actualInputTokens}` : "";
|
|
1180
1361
|
const actualOutput = data.actualOutputTokens ? `output=${data.actualOutputTokens}` : "";
|
|
1181
1362
|
const trigger = data.trigger ? `, 触发: ${data.trigger}` : "";
|
|
1182
1363
|
actionLabel = "上下文压缩开始";
|
|
1183
|
-
|
|
1184
|
-
|
|
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
|
+
}
|
|
1185
1372
|
}
|
|
1186
1373
|
else if (isPhase) {
|
|
1187
1374
|
// 子事件(阶段事件):使用 runtime 传入的 actionLabel 和 actionDetail
|
|
@@ -1190,6 +1377,9 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1190
1377
|
}
|
|
1191
1378
|
else {
|
|
1192
1379
|
// action === "end" —— 作为压缩完成子事件,嵌套在父事件下
|
|
1380
|
+
const tokensBefore = data.tokensBefore;
|
|
1381
|
+
const tokensAfter = data.tokensAfter;
|
|
1382
|
+
const freedTokensNum = data.freedTokens;
|
|
1193
1383
|
const sizeBefore = String(data.sizeBeforeKB ?? "?");
|
|
1194
1384
|
const sizeAfter = String(data.sizeAfterKB ?? "?");
|
|
1195
1385
|
const freedKB = String(data.freedKB ?? "?");
|
|
@@ -1197,9 +1387,17 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1197
1387
|
const pruned = Number(data.prunedToolMessages ?? 0);
|
|
1198
1388
|
const summarized = Number(data.summarizedMessages ?? 0);
|
|
1199
1389
|
actionLabel = "压缩完成";
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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
|
+
}
|
|
1203
1401
|
}
|
|
1204
1402
|
// ★ 更新 adapter 内部状态为 compacting,使前端 agent 卡片显示压缩中
|
|
1205
1403
|
// 子事件(phase/end)不重复更新状态,仅推送到时间线
|
|
@@ -1232,12 +1430,17 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1232
1430
|
if (isPhase && parentId && !actionInfo.parentId) {
|
|
1233
1431
|
actionInfo.parentId = parentId;
|
|
1234
1432
|
}
|
|
1235
|
-
// ★ 压缩结束时重置 totalUsage
|
|
1433
|
+
// ★ 压缩结束时重置 totalUsage(input 和 output 均归零),使面板显示压缩后的真实上下文大小。
|
|
1434
|
+
// 与 runtime 压缩后重置 accumulatedInputTokens/accumulatedOutputTokens = 0(runtime.ts)对齐。
|
|
1435
|
+
// 若只重置 input 不重置 output,压缩后面板 total 仍含压缩前 output,持续虚高。
|
|
1236
1436
|
// 将 usage 合并到 end 事件的 actionInfo 中,避免额外的无 actionInfo status-change
|
|
1237
1437
|
// 触发前端 handleDeltaStatusEvent 创建无 actionId 的占位事件(会导致扁平化渲染)。
|
|
1238
1438
|
if (isEnd) {
|
|
1239
1439
|
entry.adapterSession.totalUsage.inputTokens = 0;
|
|
1240
|
-
|
|
1440
|
+
entry.adapterSession.totalUsage.outputTokens = 0;
|
|
1441
|
+
// ★ 使用 spread 副本,避免后续事件(如 ai_request)修改 totalUsage 时
|
|
1442
|
+
// 通过引用别名反向改变已发出的 compaction 事件 usage 快照。
|
|
1443
|
+
actionInfo.usage = { ...entry.adapterSession.totalUsage };
|
|
1241
1444
|
}
|
|
1242
1445
|
this.emitProviderEvent(event, "context_compaction", actionInfo);
|
|
1243
1446
|
// ★ 与其他事件(tool_use_start/turn_complete 等)一致,
|
|
@@ -1291,9 +1494,16 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1291
1494
|
const entry = this.sessions.get(sessionId);
|
|
1292
1495
|
if (!entry || this.isTerminalStatus(entry.adapterSession.status))
|
|
1293
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;
|
|
1294
1503
|
entry.autoPollActive = true;
|
|
1295
1504
|
entry.autoPollObservedPollingTool = false;
|
|
1296
1505
|
entry.autoPollObservedAssistantText = false;
|
|
1506
|
+
entry.autoPollObservedAnyTool = false;
|
|
1297
1507
|
entry.stopAutoPollAfterCurrentTurn = false;
|
|
1298
1508
|
if (!options?.keepWaitingStatus) {
|
|
1299
1509
|
entry.adapterSession.status = "thinking";
|
|
@@ -1310,6 +1520,16 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1310
1520
|
const latestEntry = this.sessions.get(sessionId);
|
|
1311
1521
|
if (!latestEntry)
|
|
1312
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
|
+
}
|
|
1313
1533
|
// 无论 submit 是正常返回还是超时恢复,都需要取消超时检测(超时回调里已设置 submitTimeoutTimer=undefined,再调 cancel 是安全的)
|
|
1314
1534
|
this.cancelSubmitTimeoutDetection(latestEntry);
|
|
1315
1535
|
latestEntry.autoPollActive = false;
|
|
@@ -1327,6 +1547,7 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1327
1547
|
latestEntry.stopAutoPollAfterCurrentTurn = false;
|
|
1328
1548
|
latestEntry.autoPollObservedPollingTool = false;
|
|
1329
1549
|
latestEntry.autoPollObservedAssistantText = false;
|
|
1550
|
+
latestEntry.autoPollObservedAnyTool = false;
|
|
1330
1551
|
return;
|
|
1331
1552
|
}
|
|
1332
1553
|
if (latestEntry.adapterSession.status === "error") {
|
|
@@ -1421,14 +1642,37 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1421
1642
|
if (this.isTerminalStatus(entry.adapterSession.status)) {
|
|
1422
1643
|
return;
|
|
1423
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
|
+
}
|
|
1424
1666
|
// ★ 修复:超时恢复前强制 emit turn_complete,计入已有的 output tokens,
|
|
1425
1667
|
// 否则面板上 output_tokens 永远是 0(turn_complete 从未发射)。
|
|
1426
|
-
// 使用消息历史中的 assistant 文本粗略估算 output tokens
|
|
1668
|
+
// 使用消息历史中的 assistant 文本粗略估算 output tokens(累计估算)。
|
|
1669
|
+
// 此为异常恢复路径(turn_complete 未到达,无真实 completion_tokens),
|
|
1670
|
+
// 直接覆盖式赋值(不再用 delta += 累加,避免与覆盖式语义冲突导致重复计算)。
|
|
1427
1671
|
const estimatedOutput = this.estimateOutputTokensFromMessages(entry.adapterSession.messages);
|
|
1428
|
-
if (estimatedOutput > 0
|
|
1429
|
-
const
|
|
1430
|
-
entry.adapterSession.totalUsage.outputTokens
|
|
1431
|
-
adapterDebugLog(`[ACode] recoverTurnFailure:
|
|
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} ` +
|
|
1432
1676
|
`(totalUsage now: input=${entry.adapterSession.totalUsage.inputTokens}, ` +
|
|
1433
1677
|
`output=${entry.adapterSession.totalUsage.outputTokens})`);
|
|
1434
1678
|
}
|
|
@@ -1501,7 +1745,10 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1501
1745
|
adapterDebugLog(`[ACode] Skipping recovery: status is ${entry.adapterSession.status} (not thinking/tool_using)`);
|
|
1502
1746
|
return;
|
|
1503
1747
|
}
|
|
1504
|
-
|
|
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.`);
|
|
1505
1752
|
adapterDebugLog(`[ACode] Recovering to waiting_input...`);
|
|
1506
1753
|
entry.adapterSession.status = "waiting_input";
|
|
1507
1754
|
// ★ 修复:检测 idle 命令是否包含 poll_message,在 waiting_input 状态变更中携带 idle 动作信息
|
|
@@ -1519,36 +1766,114 @@ export class AcodeSdkAdapter extends EventEmitter {
|
|
|
1519
1766
|
adapterDebugLog(`[ACode] ✅ Idle loop armed, recovery process complete`);
|
|
1520
1767
|
}
|
|
1521
1768
|
/**
|
|
1522
|
-
*
|
|
1769
|
+
* 启动异步看门狗:周期性轮询运行时状态,仅在真正卡死时恢复。
|
|
1523
1770
|
*
|
|
1524
|
-
*
|
|
1525
|
-
*
|
|
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 的竞态和重复恢复
|
|
1526
1782
|
*/
|
|
1527
1783
|
startSubmitTimeoutDetection(sessionId, entry, source) {
|
|
1528
|
-
// 清除之前的超时检测
|
|
1529
1784
|
this.cancelSubmitTimeoutDetection(entry);
|
|
1530
|
-
entry.
|
|
1531
|
-
entry.
|
|
1785
|
+
entry.watchdogSubmitStartTime = Date.now();
|
|
1786
|
+
entry.watchdogLastDataFlowTime = Date.now();
|
|
1787
|
+
entry.watchdogTimer = setInterval(() => {
|
|
1532
1788
|
const currentEntry = this.sessions.get(sessionId);
|
|
1533
|
-
if (!currentEntry)
|
|
1789
|
+
if (!currentEntry) {
|
|
1790
|
+
this.cancelSubmitTimeoutDetection(entry);
|
|
1534
1791
|
return;
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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);
|
|
1543
1825
|
}
|
|
1544
1826
|
/**
|
|
1545
|
-
*
|
|
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
|
+
* 取消看门狗
|
|
1546
1848
|
*/
|
|
1547
1849
|
cancelSubmitTimeoutDetection(entry) {
|
|
1548
|
-
if (entry.
|
|
1549
|
-
|
|
1550
|
-
entry.
|
|
1850
|
+
if (entry.watchdogTimer) {
|
|
1851
|
+
clearInterval(entry.watchdogTimer);
|
|
1852
|
+
entry.watchdogTimer = undefined;
|
|
1551
1853
|
}
|
|
1552
|
-
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();
|
|
1553
1878
|
}
|
|
1554
1879
|
}
|