aws-runtime-bridge 1.9.9 → 1.9.20

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.
Files changed (121) hide show
  1. package/dist/adapter/AcodeSdkAdapter.d.ts +40 -0
  2. package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -1
  3. package/dist/adapter/AcodeSdkAdapter.js +353 -7
  4. package/dist/adapter/AcodeSdkAdapter.test.js +155 -8
  5. package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
  6. package/dist/adapter/OpencodeSdkAdapter.js +36 -3
  7. package/dist/adapter/instance-ai-config-resolver.d.ts +49 -0
  8. package/dist/adapter/instance-ai-config-resolver.d.ts.map +1 -0
  9. package/dist/adapter/instance-ai-config-resolver.js +46 -0
  10. package/dist/adapter/types.d.ts +27 -2
  11. package/dist/adapter/types.d.ts.map +1 -1
  12. package/dist/index.js +7 -3
  13. package/dist/routes/dashboard.d.ts.map +1 -1
  14. package/dist/routes/dashboard.js +1092 -8
  15. package/dist/routes/events.d.ts +7 -0
  16. package/dist/routes/events.d.ts.map +1 -0
  17. package/dist/routes/events.js +22 -0
  18. package/dist/routes/file-browser.d.ts +1 -1
  19. package/dist/routes/file-browser.d.ts.map +1 -1
  20. package/dist/routes/file-browser.js +25 -9
  21. package/dist/routes/file-snapshot.d.ts +10 -0
  22. package/dist/routes/file-snapshot.d.ts.map +1 -0
  23. package/dist/routes/file-snapshot.js +156 -0
  24. package/dist/routes/git.d.ts +10 -0
  25. package/dist/routes/git.d.ts.map +1 -1
  26. package/dist/routes/git.js +305 -2
  27. package/dist/routes/git.test.js +39 -2
  28. package/dist/routes/instance.js +3 -1
  29. package/dist/routes/memory.d.ts +3 -0
  30. package/dist/routes/memory.d.ts.map +1 -1
  31. package/dist/routes/memory.js +3 -0
  32. package/dist/routes/terminal.d.ts +2 -1
  33. package/dist/routes/terminal.d.ts.map +1 -1
  34. package/dist/routes/terminal.js +378 -41
  35. package/dist/routes/terminal.test.js +33 -7
  36. package/dist/services/agent-event-store.d.ts +59 -0
  37. package/dist/services/agent-event-store.d.ts.map +1 -0
  38. package/dist/services/agent-event-store.js +68 -0
  39. package/dist/services/event-delta-pusher.d.ts +40 -0
  40. package/dist/services/event-delta-pusher.d.ts.map +1 -0
  41. package/dist/services/event-delta-pusher.js +95 -0
  42. package/dist/services/fileSnapshotStore.d.ts +96 -0
  43. package/dist/services/fileSnapshotStore.d.ts.map +1 -0
  44. package/dist/services/fileSnapshotStore.js +500 -0
  45. package/dist/services/instance-ai-config-service.d.ts +49 -0
  46. package/dist/services/instance-ai-config-service.d.ts.map +1 -0
  47. package/dist/services/instance-ai-config-service.js +174 -0
  48. package/dist/services/panel-auth.d.ts +23 -0
  49. package/dist/services/panel-auth.d.ts.map +1 -1
  50. package/dist/services/panel-auth.js +64 -1
  51. package/dist/services/ring-buffer.d.ts +67 -0
  52. package/dist/services/ring-buffer.d.ts.map +1 -0
  53. package/dist/services/ring-buffer.js +103 -0
  54. package/dist/services/session-output.d.ts +47 -3
  55. package/dist/services/session-output.d.ts.map +1 -1
  56. package/dist/services/session-output.js +112 -2
  57. package/dist/utils/instance-ai-config-debug-logger.d.ts +37 -0
  58. package/dist/utils/instance-ai-config-debug-logger.d.ts.map +1 -0
  59. package/dist/utils/instance-ai-config-debug-logger.js +63 -0
  60. package/dist/utils/logger.d.ts +18 -0
  61. package/dist/utils/logger.d.ts.map +1 -1
  62. package/dist/utils/logger.js +48 -4
  63. package/package/acode/dist/built-in-file-tools.d.ts +1 -1
  64. package/package/acode/dist/built-in-file-tools.d.ts.map +1 -1
  65. package/package/acode/dist/built-in-file-tools.js +5 -3
  66. package/package/acode/dist/builtins/commandArtifacts.d.ts +8 -1
  67. package/package/acode/dist/builtins/commandArtifacts.d.ts.map +1 -1
  68. package/package/acode/dist/builtins/commandArtifacts.js +24 -4
  69. package/package/acode/dist/builtins/shellCommandResult.d.ts +1 -0
  70. package/package/acode/dist/builtins/shellCommandResult.d.ts.map +1 -1
  71. package/package/acode/dist/builtins/shellCommandResult.js +3 -3
  72. package/package/acode/dist/builtins/shellCommandRunner.d.ts +1 -0
  73. package/package/acode/dist/builtins/shellCommandRunner.d.ts.map +1 -1
  74. package/package/acode/dist/builtins/shellCommandRunner.js +1 -0
  75. package/package/acode/dist/cli.js +2 -0
  76. package/package/acode/dist/runtime.d.ts.map +1 -1
  77. package/package/acode/dist/runtime.js +156 -5
  78. package/package/acode/dist/types.d.ts +1 -0
  79. package/package/acode/dist/types.d.ts.map +1 -1
  80. package/package/aws-client-agent-mcp/dist/agent-client.d.ts +5 -0
  81. package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
  82. package/package/aws-client-agent-mcp/dist/agent-client.js +16 -0
  83. package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
  84. package/package/aws-client-agent-mcp/dist/context-manager.d.ts +136 -0
  85. package/package/aws-client-agent-mcp/dist/context-manager.d.ts.map +1 -0
  86. package/package/aws-client-agent-mcp/dist/context-manager.js +473 -0
  87. package/package/aws-client-agent-mcp/dist/context-manager.js.map +1 -0
  88. package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts +6 -0
  89. package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts.map +1 -0
  90. package/package/aws-client-agent-mcp/dist/context-manager.test.js +307 -0
  91. package/package/aws-client-agent-mcp/dist/context-manager.test.js.map +1 -0
  92. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +170 -2
  93. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
  94. package/package/aws-client-agent-mcp/dist/mcp-server.js +947 -9
  95. package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
  96. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +103 -1
  97. package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
  98. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +15 -1
  99. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +1 -1
  100. package/package/aws-client-agent-mcp/dist/mcp-tools.js +62 -2
  101. package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +1 -1
  102. package/package/aws-client-agent-mcp/dist/memory-store.d.ts +46 -1
  103. package/package/aws-client-agent-mcp/dist/memory-store.d.ts.map +1 -1
  104. package/package/aws-client-agent-mcp/dist/memory-store.js +164 -12
  105. package/package/aws-client-agent-mcp/dist/memory-store.js.map +1 -1
  106. package/package/aws-client-agent-mcp/dist/memory-tools.d.ts +8 -0
  107. package/package/aws-client-agent-mcp/dist/memory-tools.d.ts.map +1 -1
  108. package/package/aws-client-agent-mcp/dist/memory-tools.js +88 -1
  109. package/package/aws-client-agent-mcp/dist/memory-tools.js.map +1 -1
  110. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts +7 -1
  111. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts.map +1 -1
  112. package/package/aws-client-agent-mcp/dist/server-memory-store.js +18 -0
  113. package/package/aws-client-agent-mcp/dist/server-memory-store.js.map +1 -1
  114. package/package/aws-client-agent-mcp/dist/types.d.ts +103 -2
  115. package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
  116. package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
  117. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +27 -2
  118. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
  119. package/package/aws-client-agent-mcp/dist/websocket-client.js +122 -5
  120. package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
  121. package/package.json +1 -1
@@ -16,16 +16,44 @@ import { adapterRegistry, getToolActionInfo, isTransientClipboardPasteFilePath,
16
16
  import { validateToken } from "../middleware/auth.js";
17
17
  import { getAgentProcessManager } from "../services/agent-process-manager.js";
18
18
  import { getAiConfigService } from "../services/cc-switch-sdk.js";
19
+ import { prepareAcodeInstanceConfig, redactForLog } from "../services/instance-ai-config-service.js";
19
20
  import { enqueueMcpLaunchBinding } from "../services/mcp-launch-binding-queue.js";
20
21
  import { findClaudeCodeProcesses, isProcessRunning, } from "../services/process-detector.js";
21
- import { sendOutput, sendQuestionRequest, sendStatus, } from "../services/session-output.js";
22
+ import { bridgeEventBuffer, sendOutput, sendQuestionRequest, sendStatus, } from "../services/session-output.js";
23
+ import { getAllPushStats, startHeartbeatScheduler } from "../services/event-delta-pusher.js";
22
24
  import { findPersistedSessionByAgentId, removePersistedSession, savePersistedSessions, updatePersistedSessionAutoCommands, updatePersistedSessionRuntimeState, upsertPersistedSession, } from "../services/terminal-persistence.js";
25
+ import { getInstanceAiConfigDebugLogger } from "../utils/instance-ai-config-debug-logger.js";
23
26
  import { logger as log } from "../utils/logger.js";
24
27
  export const terminalRouter = Router();
25
28
  // 导出 SDK 会话存储,供其他模块使用(如 sessions.ts)
26
29
  export const sdkSessions = new Map();
27
30
  const TOOL_RESULT_PREVIEW_MAX_LENGTH = 4000;
28
31
  const AI_REQUEST_BODY_MARKER = "\n\n请求体:\n";
32
+ /**
33
+ * 把请求体中的 instanceAiConfig 归一化为 AdapterInstanceAiConfig。
34
+ * 任意字段类型错误或非字符串都返回 undefined,调用方应继续走 env/file fallback。
35
+ */
36
+ function sanitizeInstanceAiConfig(value) {
37
+ if (value == null)
38
+ return undefined;
39
+ if (typeof value !== "object" || Array.isArray(value))
40
+ return undefined;
41
+ const raw = value;
42
+ const out = {};
43
+ if (typeof raw.model === "string" && raw.model.trim()) {
44
+ out.model = raw.model.trim();
45
+ }
46
+ if (typeof raw.baseURL === "string" && raw.baseURL.trim()) {
47
+ out.baseURL = raw.baseURL.trim();
48
+ }
49
+ if (typeof raw.apiKey === "string" && raw.apiKey.trim()) {
50
+ out.apiKey = raw.apiKey.trim();
51
+ }
52
+ if (typeof raw.effort === "string" && raw.effort.trim()) {
53
+ out.effort = raw.effort.trim();
54
+ }
55
+ return out;
56
+ }
29
57
  const AI_REQUEST_HEADERS_MARKER = "\n\n请求头:\n";
30
58
  const startingAgents = new Set();
31
59
  export function resolveRuntimeContextResetSupport(providerId, hasResetContext) {
@@ -83,9 +111,13 @@ export function resolveStatusChangeUsage(actionInfo) {
83
111
  const outputTokens = Number.isFinite(usage.outputTokens)
84
112
  ? usage.outputTokens
85
113
  : 0;
86
- if (inputTokens <= 0 && outputTokens <= 0)
114
+ if (inputTokens <= 0 && outputTokens <= 0 && !usage.contextLimit)
87
115
  return undefined;
88
- return { inputTokens, outputTokens };
116
+ return {
117
+ inputTokens,
118
+ outputTokens,
119
+ ...(usage.contextLimit ? { contextLimit: usage.contextLimit } : {}),
120
+ };
89
121
  }
90
122
  export function resolveStatusChangeRuntimeState(status, actionInfo) {
91
123
  const normalizedStatus = String(status || "")
@@ -109,14 +141,14 @@ const TERMINAL_COMMAND_INACTIVITY_TIMEOUT_MS = 60_000;
109
141
  function padDatePart(value) {
110
142
  return String(value).padStart(2, "0");
111
143
  }
112
- export function buildAcodeCommandOutputPath(workspacePath, now = new Date()) {
144
+ export function buildAcodeCommandOutputPath(agentId, now = new Date()) {
113
145
  const year = now.getFullYear();
114
146
  const month = padDatePart(now.getMonth() + 1);
115
147
  const day = padDatePart(now.getDate());
116
148
  const hour = padDatePart(now.getHours());
117
149
  const minute = padDatePart(now.getMinutes());
118
150
  const timestamp = `${year}${month}${day}-${hour}${minute}${padDatePart(now.getSeconds())}-${String(now.getMilliseconds()).padStart(3, "0")}`;
119
- return path.join(workspacePath, ".agentswork", "acode", "cmd-dist", `${year}-${month}-${day}`, hour, minute, `${timestamp}-cmd.out`);
151
+ return path.join(os.homedir(), ".acode", "cmd-dist", agentId, `${year}-${month}-${day}`, hour, minute, `${timestamp}-cmd.out`);
120
152
  }
121
153
  export function countTerminalOutputLines(output) {
122
154
  if (!output) {
@@ -138,7 +170,7 @@ export function formatCommandInactivityTimeoutNotice(timeoutMs = TERMINAL_COMMAN
138
170
  }
139
171
  async function persistTerminalCommandOutput(state) {
140
172
  const outputPath = state.outputPath ||
141
- buildAcodeCommandOutputPath(state.workspacePath, state.startedAt);
173
+ buildAcodeCommandOutputPath(state.agentId, state.startedAt);
142
174
  state.outputPath = outputPath;
143
175
  await fs.mkdir(path.dirname(outputPath), { recursive: true });
144
176
  await fs.writeFile(outputPath, state.chunks.join(""), "utf-8");
@@ -395,7 +427,7 @@ async function executeTerminalCommand(sessionId, commandValue) {
395
427
  const stderrDecoder = createTerminalOutputDecoder();
396
428
  const usesAcodeCommandOutputPolicy = entry.providerId === "acode";
397
429
  const outputSpilloverState = {
398
- workspacePath: entry.workspacePath,
430
+ agentId: entry.agentId,
399
431
  startedAt: new Date(),
400
432
  chunks: [],
401
433
  spilled: false,
@@ -908,19 +940,11 @@ export function resolveRuntimeStatusFromProviderEvent(event, currentRuntimeStatu
908
940
  };
909
941
  }
910
942
  if (event.type === "turn_complete") {
911
- const responseActionInfo = event.data
912
- .responseBody
913
- ? {
914
- actionType: "ai_request",
915
- actionLabel: "AI响应",
916
- actionResult: event.data.responseBody,
917
- usage: event.data.usage,
918
- }
919
- : undefined;
943
+ // 方案 A:不设置 "AI响应" action 信息,避免状态卡在 action 展示
944
+ // 只返回干净的 waiting_input 状态,usage 信息保留用于 token 统计
920
945
  return {
921
946
  status: "waiting_input",
922
- actionInfo: responseActionInfo ||
923
- (event.data.usage ? { usage: event.data.usage } : undefined),
947
+ actionInfo: event.data.usage ? { usage: event.data.usage } : undefined,
924
948
  };
925
949
  }
926
950
  if (event.type === "error") {
@@ -928,15 +952,111 @@ export function resolveRuntimeStatusFromProviderEvent(event, currentRuntimeStatu
928
952
  }
929
953
  return undefined;
930
954
  }
931
- function forwardSdkOutputEvent(event, entry) {
955
+ // Per-session 输出队列:确保 sendOutput HTTP POST 按事件顺序串行到达 Server,
956
+ // 避免并发 POST 因网络时序导致流式文本 chunk 乱序。
957
+ const sdkOutputQueueBySession = new Map();
958
+ /** 小 chunk 积攒缓存:sessionId:sourceEventType → { buffer, seq, timer, sourceEventType, entry },不足 20 字符时积攒最多 500ms 再推送 */
959
+ const sdkOutputSmallChunkAccumulators = new Map();
960
+ /** 小 chunk 积攒阈值:字符数少于此值的 output 事件积攒发送 */
961
+ const SMALL_CHUNK_CHAR_THRESHOLD = 20;
962
+ /** 小 chunk 最大积攒时间(毫秒) */
963
+ const SMALL_CHUNK_MAX_ACCUMULATE_MS = 500;
964
+ function flushSmallChunkAccumulator(sessionId, sourceEventType) {
965
+ const key = sourceEventType
966
+ ? `${sessionId}:${sourceEventType}`
967
+ : undefined;
968
+ // 如果指定了 key 则精确刷新;否则回退到按 sessionId 前缀查找并刷新所有(兼容大 chunk 触发的全量刷新)
969
+ let acc;
970
+ let accKey;
971
+ if (key) {
972
+ acc = sdkOutputSmallChunkAccumulators.get(key);
973
+ accKey = key;
974
+ }
975
+ else {
976
+ // 刷新该 sessionId 下的所有 accumulator(可能有多种 sourceEventType)
977
+ for (const [k, v] of sdkOutputSmallChunkAccumulators) {
978
+ if (k.startsWith(sessionId + ":") && v.buffer) {
979
+ flushSmallChunkAccumulator(sessionId, v.sourceEventType);
980
+ }
981
+ }
982
+ return;
983
+ }
984
+ if (!acc || !acc.buffer)
985
+ return;
986
+ acc.timer = null;
987
+ const output = acc.buffer;
988
+ const seq = acc.seq;
989
+ const entry = acc.entry;
990
+ const eventType = acc.sourceEventType;
991
+ acc.buffer = "";
992
+ if (accKey)
993
+ sdkOutputSmallChunkAccumulators.delete(accKey);
994
+ const metadata = {
995
+ sourceEventType: eventType,
996
+ logCategory: "acode",
997
+ };
998
+ const authOptions = {
999
+ runtimeAccessToken: entry.runtimeAccessToken,
1000
+ schedulerBaseUrl: entry.schedulerBaseUrl,
1001
+ };
1002
+ const prev = sdkOutputQueueBySession.get(sessionId) ?? Promise.resolve();
1003
+ const next = prev.then(() => sendOutput(entry.agentId, output, sessionId, seq, metadata, authOptions), () => sendOutput(entry.agentId, output, sessionId, seq, metadata, authOptions));
1004
+ sdkOutputQueueBySession.set(sessionId, next);
1005
+ void next.finally(() => {
1006
+ if (sdkOutputQueueBySession.get(sessionId) === next) {
1007
+ sdkOutputQueueBySession.delete(sessionId);
1008
+ }
1009
+ });
1010
+ }
1011
+ function enqueueSdkOutputEvent(event, entry) {
932
1012
  const output = formatSdkOutputEvent(event);
933
1013
  if (!output) {
934
1014
  return;
935
1015
  }
936
1016
  entry.seq += 1;
937
- void sendOutput(entry.agentId, output, event.sessionId, entry.seq, buildSdkOutputMetadata(event), {
1017
+ const seq = entry.seq;
1018
+ const metadata = buildSdkOutputMetadata(event);
1019
+ const authOptions = {
938
1020
  runtimeAccessToken: entry.runtimeAccessToken,
939
1021
  schedulerBaseUrl: entry.schedulerBaseUrl,
1022
+ };
1023
+ // 小 chunk 积攒优化:命令输出和 AI 文本增量等小片段不足阈值时积攒再推送
1024
+ if ((event.type === "command_output" ||
1025
+ event.type === "text_delta" ||
1026
+ event.type === "thinking") &&
1027
+ output.length < SMALL_CHUNK_CHAR_THRESHOLD) {
1028
+ const accKey = `${event.sessionId}:${event.type}`;
1029
+ let acc = sdkOutputSmallChunkAccumulators.get(accKey);
1030
+ if (!acc) {
1031
+ acc = {
1032
+ buffer: "",
1033
+ seq: 0,
1034
+ timer: null,
1035
+ entry,
1036
+ sourceEventType: event.type,
1037
+ };
1038
+ }
1039
+ acc.buffer += output;
1040
+ acc.seq = seq;
1041
+ acc.entry = entry;
1042
+ if (!acc.timer) {
1043
+ const capturedSessionId = event.sessionId;
1044
+ const capturedType = event.type;
1045
+ acc.timer = setTimeout(() => flushSmallChunkAccumulator(capturedSessionId, capturedType), SMALL_CHUNK_MAX_ACCUMULATE_MS);
1046
+ }
1047
+ sdkOutputSmallChunkAccumulators.set(accKey, acc);
1048
+ return;
1049
+ }
1050
+ // 大 chunk 直接发送(先刷新可能存在的积攒缓存)
1051
+ flushSmallChunkAccumulator(event.sessionId);
1052
+ const prev = sdkOutputQueueBySession.get(event.sessionId) ?? Promise.resolve();
1053
+ const next = prev.then(() => sendOutput(entry.agentId, output, event.sessionId, seq, metadata, authOptions), () => sendOutput(entry.agentId, output, event.sessionId, seq, metadata, authOptions));
1054
+ sdkOutputQueueBySession.set(event.sessionId, next);
1055
+ // 队列空闲时自动清理,避免内存泄漏
1056
+ void next.finally(() => {
1057
+ if (sdkOutputQueueBySession.get(event.sessionId) === next) {
1058
+ sdkOutputQueueBySession.delete(event.sessionId);
1059
+ }
940
1060
  });
941
1061
  }
942
1062
  export function resolveSdkProviderId(command) {
@@ -948,6 +1068,8 @@ function ensureAdapterInitialized() {
948
1068
  if (adapterInitialized)
949
1069
  return;
950
1070
  registerSdkProviders(adapterRegistry, wireSdkAdapterEvents);
1071
+ // 需求 4.2.1 #2:启动全局心跳调度器(每 30s 推送空包检测 Server 连接存活)
1072
+ startHeartbeatScheduler();
951
1073
  adapterInitialized = true;
952
1074
  }
953
1075
  function wireSdkAdapterEvents(adapter, definition) {
@@ -955,20 +1077,36 @@ function wireSdkAdapterEvents(adapter, definition) {
955
1077
  const entry = sdkSessions.get(event.sessionId);
956
1078
  if (entry) {
957
1079
  console.log(`[${definition.displayName} Adapter] Event: ${event.type} for session ${event.sessionId}`);
958
- const runtimeStatus = resolveRuntimeStatusFromProviderEvent(event, entry.currentRuntimeStatus);
959
- if (runtimeStatus) {
960
- void sendStatus(entry.agentId, event.sessionId, runtimeStatus.status, runtimeStatus.actionInfo, resolveStatusChangeUsage(runtimeStatus.actionInfo), {
961
- runtimeAccessToken: entry.runtimeAccessToken,
962
- schedulerBaseUrl: entry.schedulerBaseUrl,
963
- });
1080
+ // Skip sendStatus for thinking/text_delta events — the status-change listener
1081
+ // already emits the authoritative thinking status with restored actionInfo,
1082
+ // avoiding duplicate timeline entries in agentEventStore.
1083
+ const isThinkingDelta = event.type === "thinking" || event.type === "text_delta";
1084
+ if (!isThinkingDelta) {
1085
+ const runtimeStatus = resolveRuntimeStatusFromProviderEvent(event, entry.currentRuntimeStatus);
1086
+ if (runtimeStatus) {
1087
+ void sendStatus(entry.agentId, event.sessionId, runtimeStatus.status, runtimeStatus.actionInfo, resolveStatusChangeUsage(runtimeStatus.actionInfo), {
1088
+ runtimeAccessToken: entry.runtimeAccessToken,
1089
+ schedulerBaseUrl: entry.schedulerBaseUrl,
1090
+ },
1091
+ // Pass original timestamp from ACode runtime for correct timeline ordering
1092
+ // Convert ISO string timestamp to epoch milliseconds
1093
+ event.timestamp ? new Date(event.timestamp).getTime() : undefined,
1094
+ // Pass original event type for frontend gray-zone rendering (thinking vs text_delta)
1095
+ event.type);
1096
+ }
964
1097
  }
965
- forwardSdkOutputEvent(event, entry);
1098
+ enqueueSdkOutputEvent(event, entry);
966
1099
  const toolResultActionInfo = buildToolResultTimelineActionInfo(event);
967
1100
  if (toolResultActionInfo) {
968
1101
  void sendStatus(entry.agentId, event.sessionId, "tool_result", toolResultActionInfo, undefined, {
969
1102
  runtimeAccessToken: entry.runtimeAccessToken,
970
1103
  schedulerBaseUrl: entry.schedulerBaseUrl,
971
- });
1104
+ },
1105
+ // Pass original timestamp from ACode runtime for correct timeline ordering
1106
+ // Convert ISO string timestamp to epoch milliseconds
1107
+ event.timestamp ? new Date(event.timestamp).getTime() : undefined,
1108
+ // Pass original event type (tool_use_end) for timeline sourceEventType
1109
+ event.type);
972
1110
  }
973
1111
  }
974
1112
  });
@@ -977,18 +1115,45 @@ function wireSdkAdapterEvents(adapter, definition) {
977
1115
  if (entry) {
978
1116
  const runtimeState = resolveStatusChangeRuntimeState(status, actionInfo);
979
1117
  entry.currentRuntimeStatus = runtimeState.status;
980
- entry.currentRuntimeActionType = runtimeState.actionInfo?.actionType;
981
- entry.currentRuntimeActionLabel = runtimeState.actionInfo?.actionLabel;
982
- entry.currentRuntimeActionDetail =
983
- runtimeState.actionInfo?.actionDetail;
1118
+ // 当新状态未携带动作且进入 thinking 时,保留上一个 idle 类动作(如 poll_message),
1119
+ // 避免页面刷新后丢失"监听消息"状态而错误显示"思考中"。
1120
+ // 注意:MCP client 发送的 actionType 是 "mcp" 而非 "idle",需要通过 actionDetail 判断。
1121
+ // ★ 关键修复: turn_complete 后进入 waiting_input 时,如果携带的是 ai_request 动作,
1122
+ // 不应该保留之前的 tool_using 动作,避免永久卡在工具调用状态。
1123
+ const prevActionDetail = entry.currentRuntimeActionDetail ?? "";
1124
+ const isPrevPollMessage = prevActionDetail.includes("poll_message") || prevActionDetail.includes("get_message");
1125
+ const isNewAiRequestAction = runtimeState.actionInfo?.actionType === "ai_request";
1126
+ const shouldPreservePrevAction = !isNewAiRequestAction && // 新动作不是 AI 响应
1127
+ (runtimeState.status === "thinking" || runtimeState.status === "waiting_input") && // 状态是 thinking 或 waiting_input
1128
+ (entry.currentRuntimeActionType === "idle" || entry.currentRuntimeActionType === "mcp") && // 之前是 idle/mcp 动作
1129
+ isPrevPollMessage; // 且之前是 poll_message
1130
+ if (runtimeState.actionInfo?.actionType || // 新状态携带了动作
1131
+ !shouldPreservePrevAction // 不满足保留条件
1132
+ ) {
1133
+ entry.currentRuntimeActionType = runtimeState.actionInfo?.actionType;
1134
+ entry.currentRuntimeActionLabel = runtimeState.actionInfo?.actionLabel;
1135
+ entry.currentRuntimeActionDetail =
1136
+ runtimeState.actionInfo?.actionDetail;
1137
+ }
984
1138
  console.log(`[${definition.displayName} Adapter] Status changed: ${runtimeState.status} for agent ${entry.agentId}`, runtimeState.actionInfo || "");
985
1139
  void updatePersistedSessionRuntimeState(sessionId, {
986
1140
  runtimeStatus: runtimeState.status,
987
- runtimeActionType: runtimeState.actionInfo?.actionType,
988
- runtimeActionLabel: runtimeState.actionInfo?.actionLabel,
989
- runtimeActionDetail: runtimeState.actionInfo?.actionDetail,
1141
+ runtimeActionType: entry.currentRuntimeActionType,
1142
+ runtimeActionLabel: entry.currentRuntimeActionLabel,
1143
+ runtimeActionDetail: entry.currentRuntimeActionDetail,
990
1144
  });
991
- void sendStatus(entry.agentId, sessionId, runtimeState.status, runtimeState.actionInfo, resolveStatusChangeUsage(runtimeState.actionInfo), {
1145
+ // thinking + poll_message 动作保留时,派生恢复用 actionInfo 传给 sendStatus
1146
+ // 支持 actionType 为 "idle" 或 "mcp",通过 actionDetail 中的 poll_message/get_message 判断
1147
+ const restoredActionInfo = runtimeState.actionInfo?.actionType
1148
+ ? runtimeState.actionInfo
1149
+ : entry.currentRuntimeActionType
1150
+ ? {
1151
+ actionType: entry.currentRuntimeActionType,
1152
+ actionLabel: entry.currentRuntimeActionLabel ?? "",
1153
+ actionDetail: entry.currentRuntimeActionDetail,
1154
+ }
1155
+ : undefined;
1156
+ void sendStatus(entry.agentId, sessionId, runtimeState.status, restoredActionInfo, resolveStatusChangeUsage(runtimeState.actionInfo), {
992
1157
  runtimeAccessToken: entry.runtimeAccessToken,
993
1158
  schedulerBaseUrl: entry.schedulerBaseUrl,
994
1159
  });
@@ -1008,6 +1173,17 @@ function wireSdkAdapterEvents(adapter, definition) {
1008
1173
  const entry = sdkSessions.get(data.sessionId);
1009
1174
  if (entry) {
1010
1175
  console.log(`[${definition.displayName} Adapter] Permission request: ${data.toolName} for agent ${entry.agentId}`);
1176
+ const actionDetail = data.toolInput
1177
+ ? redactTransientClipboardPastePaths(JSON.stringify(data.toolInput).slice(0, 400))
1178
+ : undefined;
1179
+ void sendStatus(entry.agentId, data.sessionId, "waiting_input", {
1180
+ actionType: "other",
1181
+ actionLabel: "权限确认",
1182
+ actionDetail: actionDetail || data.toolName,
1183
+ }, undefined, {
1184
+ runtimeAccessToken: entry.runtimeAccessToken,
1185
+ schedulerBaseUrl: entry.schedulerBaseUrl,
1186
+ });
1011
1187
  }
1012
1188
  });
1013
1189
  }
@@ -1049,7 +1225,24 @@ async function startSdkSession(req, res) {
1049
1225
  // MCP 配置 - 加载 aws-client-agent-mcp
1050
1226
  mcpConfigPath, extraMcpServers, envOverrides,
1051
1227
  // 空闲命令配置
1052
- idleInputAutoCommand, nonInputAutoCommand, } = req.body || {};
1228
+ idleInputAutoCommand, nonInputAutoCommand,
1229
+ // Per-agent model override(优先于全局配置)
1230
+ model: perAgentModel,
1231
+ // Per-instance AI provider 覆盖(仅 ACode 消费;优先级:api > env > file > global)
1232
+ instanceAiConfig: perInstanceAiConfig, } = req.body || {};
1233
+ const _dbg = getInstanceAiConfigDebugLogger();
1234
+ void _dbg.log({
1235
+ level: "info",
1236
+ category: "launch",
1237
+ message: "startSdkSession: 收到启动请求",
1238
+ source: "bridge",
1239
+ agentId: String(agentId || "").trim() || undefined,
1240
+ details: {
1241
+ reqBodyKeys: Object.keys(req.body || {}),
1242
+ perInstanceAiConfig: perInstanceAiConfig ? redactForLog(perInstanceAiConfig) : null,
1243
+ perAgentModel: perAgentModel ?? null,
1244
+ },
1245
+ });
1053
1246
  let sessionId = null;
1054
1247
  const normalizedAgentId = String(agentId || "").trim();
1055
1248
  const runtimeAccessToken = String(req.body?.runtimeAccessToken || "").trim() || undefined;
@@ -1098,7 +1291,6 @@ async function startSdkSession(req, res) {
1098
1291
  // 根据命令选择对应的 adapter
1099
1292
  const normalizedCommand = (command || "acode").toLowerCase();
1100
1293
  const providerId = resolveSdkProviderId(command);
1101
- console.log(`[Runtime] ★★★ SDK启动 ★★★ command="${command}", normalizedCommand="${normalizedCommand}", providerId="${providerId}"`);
1102
1294
  const adapter = adapterRegistry.get(providerId);
1103
1295
  sessionId = uuidv4();
1104
1296
  const savedClaudeConfig = providerId === "claude-code"
@@ -1107,18 +1299,108 @@ async function startSdkSession(req, res) {
1107
1299
  const claudeLaunchConfig = providerId === "claude-code"
1108
1300
  ? buildClaudeCodeLaunchConfig(savedClaudeConfig, envOverrides)
1109
1301
  : buildClaudeCodeLaunchConfig(null, envOverrides);
1302
+ // 模型解析优先级(ACode 与非 ACode 不同):
1303
+ //
1304
+ // ACode:模型仲裁收敛到 AcodeSdkAdapter.resolveAcodeProvider → resolveAcodeProviderConfig 纯函数
1305
+ // 优先级:perSessionModel(request) > instanceAiConfig(instance) > acode.json(global) > default
1306
+ // 这里不预解析,将原始输入传递给 adapter
1307
+ //
1308
+ // 非 ACode (claude-code/codex/opencode):
1309
+ // 优先级:perAgentModel(request) > claudeLaunchConfig.model(global)
1310
+ // 通过 ANTHROPIC_MODEL/CODEX_MODEL 环境变量注入
1311
+ const resolvedModel = providerId === "acode"
1312
+ ? (perAgentModel ? String(perAgentModel).trim() : undefined)
1313
+ : (() => {
1314
+ const requestModel = perAgentModel ? String(perAgentModel).trim() : undefined;
1315
+ const globalModel = claudeLaunchConfig.model ? String(claudeLaunchConfig.model).trim() : undefined;
1316
+ return requestModel || globalModel;
1317
+ })();
1318
+ void _dbg.log({
1319
+ level: "info",
1320
+ category: "resolve",
1321
+ message: "模型解析结果",
1322
+ source: "bridge",
1323
+ agentId: normalizedAgentId,
1324
+ sessionId: sessionId ?? undefined,
1325
+ details: {
1326
+ perAgentModel: perAgentModel ?? null,
1327
+ globalModel: claudeLaunchConfig.model ?? null,
1328
+ resolvedModel: resolvedModel ?? null,
1329
+ providerId,
1330
+ note: providerId === "acode" ? "adapter handles full priority" : "request > global",
1331
+ },
1332
+ });
1333
+ // 构建 effectiveEnvOverrides:将 resolvedModel 注入各 provider 对应的环境变量
1334
+ const effectiveEnvOverrides = { ...claudeLaunchConfig.envOverrides };
1335
+ if (resolvedModel) {
1336
+ // Claude Code 使用 ANTHROPIC_MODEL
1337
+ effectiveEnvOverrides.ANTHROPIC_MODEL = resolvedModel;
1338
+ // Codex 使用 CODEX_MODEL
1339
+ effectiveEnvOverrides.CODEX_MODEL = resolvedModel;
1340
+ // ACode / OpenCode: 不通过 env var 传递模型
1341
+ }
1342
+ // ACode 实例自配置:解析 api > env > file,deep-merge 全局后写入 per-instance acode.json
1343
+ // 其它 provider 不消费 instanceAiConfig,保持向后兼容
1344
+ // 模型仲裁由 AcodeSdkAdapter.resolveAcodeProvider 统一处理
1345
+ let resolvedInstanceAiConfig;
1346
+ let resolvedMcpConfigPath = mcpConfigPath;
1347
+ if (providerId === "acode") {
1348
+ const mergedInstanceConfig = perInstanceAiConfig || {};
1349
+ const prepared = await prepareAcodeInstanceConfig({
1350
+ apiOverrides: sanitizeInstanceAiConfig(mergedInstanceConfig) ?? undefined,
1351
+ agentId: normalizedAgentId,
1352
+ sessionId: sessionId ?? undefined,
1353
+ runtimeBridgeBaseUrl: requestedSchedulerBaseUrl,
1354
+ });
1355
+ resolvedInstanceAiConfig = prepared.instanceConfig.config;
1356
+ resolvedMcpConfigPath = prepared.perInstanceConfigPath;
1357
+ void _dbg.log({
1358
+ level: "info",
1359
+ category: "apply",
1360
+ message: "prepareAcodeInstanceConfig 完成",
1361
+ source: "bridge",
1362
+ agentId: normalizedAgentId,
1363
+ sessionId: sessionId ?? undefined,
1364
+ details: {
1365
+ sanitizedApiOverrides: sanitizeInstanceAiConfig(mergedInstanceConfig) ? redactForLog(sanitizeInstanceAiConfig(mergedInstanceConfig)) : null,
1366
+ resolvedInstanceAiConfig: resolvedInstanceAiConfig ? redactForLog(resolvedInstanceAiConfig) : null,
1367
+ perInstanceConfigPath: resolvedMcpConfigPath,
1368
+ instanceConfigSources: prepared.instanceConfig.source ?? null,
1369
+ },
1370
+ });
1371
+ }
1110
1372
  // 构建SDK配置
1111
1373
  const config = {
1112
1374
  command: command || "acode",
1113
1375
  workingDirectory: workspacePath,
1376
+ agentId: normalizedAgentId,
1114
1377
  autoAccept: Boolean(autoAccept),
1115
1378
  initialPrompt,
1116
- model: claudeLaunchConfig.model,
1379
+ model: resolvedModel,
1117
1380
  // MCP 配置:不再默认注入 aws-mcp;由用户在面板中安装/配置
1118
- mcpConfigPath,
1381
+ // ACode 场景下,per-instance acode.json 已由 instance-ai-config-service 写入
1382
+ mcpConfigPath: resolvedMcpConfigPath,
1383
+ // ACode 实例自配置:来源与值会写入 d:/Instance-ai-configuration-debug.log
1384
+ ...(resolvedInstanceAiConfig
1385
+ ? { instanceAiConfig: resolvedInstanceAiConfig }
1386
+ : {}),
1119
1387
  extraMcpServers: extraMcpServers || {},
1120
- envOverrides: buildRuntimeEnv(String(agentId), String(workspacePath), process.env, claudeLaunchConfig.envOverrides),
1388
+ envOverrides: buildRuntimeEnv(String(agentId), String(workspacePath), process.env, effectiveEnvOverrides),
1121
1389
  };
1390
+ void _dbg.log({
1391
+ level: "info",
1392
+ category: "launch",
1393
+ message: "传递给 Adapter 的完整 AdapterSessionConfig",
1394
+ source: "bridge",
1395
+ agentId: normalizedAgentId,
1396
+ sessionId,
1397
+ details: {
1398
+ adapterModel: config.model ?? null,
1399
+ adapterInstanceAiConfig: config.instanceAiConfig ? redactForLog(config.instanceAiConfig) : null,
1400
+ adapterMcpConfigPath: config.mcpConfigPath ?? null,
1401
+ },
1402
+ });
1403
+ await _dbg.flush();
1122
1404
  // 存储会话信息
1123
1405
  sdkSessions.set(sessionId, {
1124
1406
  agentId: normalizedAgentId,
@@ -1784,3 +2066,58 @@ terminalRouter.post("/stop-all", validateToken, async (_req, res) => {
1784
2066
  errors: errors.length > 0 ? errors : undefined,
1785
2067
  });
1786
2068
  });
2069
+ // ============ 桥接侧事件查询 ============
2070
+ /**
2071
+ * 查询最近 N 条桥接侧本地事件。
2072
+ *
2073
+ * 用于前端面板或调试工具获取最近的状态/输出事件快照。
2074
+ * GET /runtime/events/last/:n
2075
+ */
2076
+ terminalRouter.get("/events/last/:n", validateToken, (req, res) => {
2077
+ const nParam = parseInt(req.params.n, 10);
2078
+ if (!Number.isFinite(nParam) || nParam < 1) {
2079
+ res.status(400).json({ error: ":n must be a positive integer" });
2080
+ return;
2081
+ }
2082
+ const entries = bridgeEventBuffer.getLastN(nParam);
2083
+ res.json({
2084
+ ok: true,
2085
+ count: entries.length,
2086
+ entries,
2087
+ });
2088
+ });
2089
+ /**
2090
+ * 查询自指定 index(不含)之后的桥接侧本地事件。
2091
+ *
2092
+ * 增量轮询端点:客户端保存上一次看到的 index,
2093
+ * 下次传入该 index 仅获取增量事件,适用于实时刷新场景。
2094
+ * GET /runtime/events/since/:index
2095
+ */
2096
+ terminalRouter.get("/events/since/:index", validateToken, (req, res) => {
2097
+ const indexParam = parseInt(req.params.index, 10);
2098
+ if (!Number.isFinite(indexParam) || indexParam < 0) {
2099
+ res.status(400).json({ error: ":index must be a non-negative integer" });
2100
+ return;
2101
+ }
2102
+ const slice = bridgeEventBuffer.getSinceIndex(indexParam);
2103
+ res.json({
2104
+ ok: true,
2105
+ entries: slice.entries,
2106
+ nextIndex: slice.nextIndex,
2107
+ oldestIndex: slice.oldestIndex,
2108
+ dataLoss: slice.oldestIndex > indexParam + 1,
2109
+ });
2110
+ });
2111
+ /**
2112
+ * 获取 Bridge 侧事件推送统计(需求 4.2.1 #3)。
2113
+ * 返回每个 Agent 的待推送数、累计成功/失败、连续失败次数、最近延迟等。
2114
+ * GET /runtime/events/push-stats
2115
+ */
2116
+ terminalRouter.get("/events/push-stats", validateToken, (_req, res) => {
2117
+ const stats = getAllPushStats();
2118
+ res.json({
2119
+ ok: true,
2120
+ count: stats.length,
2121
+ agents: stats,
2122
+ });
2123
+ });
@@ -293,6 +293,19 @@ describe("SDK provider event runtime status mapping", () => {
293
293
  actionId: "tool-idle-1",
294
294
  });
295
295
  });
296
+ it("turn_complete returns clean waiting_input without action info (A方案)", () => {
297
+ // 验证: turn_complete 后返回干净的 waiting_input 状态,不设置 "AI响应" action
298
+ // 这样可以避免状态卡在 "AI响应" 显示
299
+ const state = resolveRuntimeStatusFromProviderEvent(event("turn_complete", {
300
+ responseBody: '{"choices":[{"message":{"content":"done"}}]}',
301
+ usage: { inputTokens: 100, outputTokens: 50 },
302
+ }));
303
+ expect(state?.status).toBe("waiting_input");
304
+ // 方案 A:不再设置 ai_request action,只保留 usage
305
+ expect(state?.actionInfo?.actionType).toBeUndefined();
306
+ expect(state?.actionInfo?.actionLabel).toBeUndefined();
307
+ expect(state?.actionInfo?.usage).toEqual({ inputTokens: 100, outputTokens: 50 });
308
+ });
296
309
  });
297
310
  describe("terminal command helpers", () => {
298
311
  it("expands a whole mini-term clipboard paste file path into SDK input content", async () => {
@@ -339,9 +352,9 @@ describe("terminal command helpers", () => {
339
352
  expect(countTerminalOutputLines("one\ntwo\n")).toBe(2);
340
353
  expect(countTerminalOutputLines("one\r\ntwo\rthree")).toBe(3);
341
354
  });
342
- it("builds ACode command output spillover paths under the workspace cmd-dist directory", () => {
343
- const outputPath = buildAcodeCommandOutputPath(path.join("D:", "code", "repo"), new Date(2026, 1, 3, 4, 5, 6, 7));
344
- expect(outputPath).toBe(path.join("D:", "code", "repo", ".agentswork", "acode", "cmd-dist", "2026-02-03", "04", "05", "20260203-040506-007-cmd.out"));
355
+ it("builds ACode command output spillover paths under ~/.acode/cmd-dist/<agentId>/", () => {
356
+ const outputPath = buildAcodeCommandOutputPath("agent-abc-123", new Date(2026, 1, 3, 4, 5, 6, 7));
357
+ expect(outputPath).toBe(path.join(os.homedir(), ".acode", "cmd-dist", "agent-abc-123", "2026-02-03", "04", "05", "20260203-040506-007-cmd.out"));
345
358
  });
346
359
  it("formats ACode long-output and inactivity notices for terminal users", () => {
347
360
  expect(formatCommandOutputSpilloverNotice("/repo/out.txt", 300)).toContain("命令输出超过 300 行");
@@ -399,7 +412,7 @@ describe("SDK status usage forwarding", () => {
399
412
  actionLabel: "Calling MCP",
400
413
  })).toBeUndefined();
401
414
  });
402
- it("forwards structured AI response body through action result", () => {
415
+ it("returns waiting_input without action info on turn_complete (A方案:避免状态卡住)", () => {
403
416
  const runtimeStatus = resolveRuntimeStatusFromProviderEvent({
404
417
  type: "turn_complete",
405
418
  sessionId: "session-1",
@@ -410,16 +423,29 @@ describe("SDK status usage forwarding", () => {
410
423
  finishReason: "stop",
411
424
  },
412
425
  });
426
+ // 方案 A:不再设置 "AI响应" action 信息,只保留 usage 用于 token 统计
413
427
  expect(runtimeStatus).toEqual({
414
428
  status: "waiting_input",
415
429
  actionInfo: {
416
- actionType: "ai_request",
417
- actionLabel: "AI响应",
418
- actionResult: '{"choices":[{"message":{"content":"done"}}]}',
419
430
  usage: { inputTokens: 12, outputTokens: 4 },
420
431
  },
421
432
  });
422
433
  });
434
+ it("returns waiting_input without action info when turn_complete has no usage", () => {
435
+ const runtimeStatus = resolveRuntimeStatusFromProviderEvent({
436
+ type: "turn_complete",
437
+ sessionId: "session-1",
438
+ timestamp: new Date(0).toISOString(),
439
+ data: {
440
+ responseBody: '{"choices":[{"message":{"content":"done"}}]}',
441
+ finishReason: "stop",
442
+ },
443
+ });
444
+ expect(runtimeStatus).toEqual({
445
+ status: "waiting_input",
446
+ actionInfo: undefined,
447
+ });
448
+ });
423
449
  it("includes file paths and commands in tool status action details", () => {
424
450
  expect(resolveRuntimeStatusFromProviderEvent({
425
451
  type: "tool_use_start",