aws-runtime-bridge 1.9.10 → 1.9.21

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 (132) 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 +376 -10
  4. package/dist/adapter/AcodeSdkAdapter.test.js +155 -8
  5. package/dist/adapter/ClaudeSdkAdapter.d.ts.map +1 -1
  6. package/dist/adapter/ClaudeSdkAdapter.js +17 -1
  7. package/dist/adapter/CodexSdkAdapter.d.ts.map +1 -1
  8. package/dist/adapter/CodexSdkAdapter.js +15 -1
  9. package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
  10. package/dist/adapter/OpencodeSdkAdapter.js +56 -4
  11. package/dist/adapter/instance-ai-config-resolver.d.ts +49 -0
  12. package/dist/adapter/instance-ai-config-resolver.d.ts.map +1 -0
  13. package/dist/adapter/instance-ai-config-resolver.js +46 -0
  14. package/dist/adapter/types.d.ts +29 -2
  15. package/dist/adapter/types.d.ts.map +1 -1
  16. package/dist/index.js +5 -1
  17. package/dist/routes/dashboard.d.ts.map +1 -1
  18. package/dist/routes/dashboard.js +749 -15
  19. package/dist/routes/events.d.ts +7 -0
  20. package/dist/routes/events.d.ts.map +1 -0
  21. package/dist/routes/events.js +22 -0
  22. package/dist/routes/file-browser.d.ts +1 -1
  23. package/dist/routes/file-browser.d.ts.map +1 -1
  24. package/dist/routes/file-browser.js +25 -9
  25. package/dist/routes/file-snapshot.d.ts +10 -0
  26. package/dist/routes/file-snapshot.d.ts.map +1 -0
  27. package/dist/routes/file-snapshot.js +156 -0
  28. package/dist/routes/git.d.ts +10 -0
  29. package/dist/routes/git.d.ts.map +1 -1
  30. package/dist/routes/git.js +405 -2
  31. package/dist/routes/git.test.js +39 -2
  32. package/dist/routes/instance.js +3 -1
  33. package/dist/routes/memory.d.ts +3 -0
  34. package/dist/routes/memory.d.ts.map +1 -1
  35. package/dist/routes/memory.js +3 -0
  36. package/dist/routes/terminal.d.ts +17 -3
  37. package/dist/routes/terminal.d.ts.map +1 -1
  38. package/dist/routes/terminal.js +464 -47
  39. package/dist/routes/terminal.test.js +37 -9
  40. package/dist/services/agent-event-store.d.ts +60 -0
  41. package/dist/services/agent-event-store.d.ts.map +1 -0
  42. package/dist/services/agent-event-store.js +68 -0
  43. package/dist/services/event-delta-pusher.d.ts +40 -0
  44. package/dist/services/event-delta-pusher.d.ts.map +1 -0
  45. package/dist/services/event-delta-pusher.js +95 -0
  46. package/dist/services/fileSnapshotStore.d.ts +96 -0
  47. package/dist/services/fileSnapshotStore.d.ts.map +1 -0
  48. package/dist/services/fileSnapshotStore.js +500 -0
  49. package/dist/services/instance-ai-config-service.d.ts +49 -0
  50. package/dist/services/instance-ai-config-service.d.ts.map +1 -0
  51. package/dist/services/instance-ai-config-service.js +174 -0
  52. package/dist/services/panel-auth.d.ts +8 -0
  53. package/dist/services/panel-auth.d.ts.map +1 -1
  54. package/dist/services/panel-auth.js +8 -0
  55. package/dist/services/ring-buffer.d.ts +67 -0
  56. package/dist/services/ring-buffer.d.ts.map +1 -0
  57. package/dist/services/ring-buffer.js +103 -0
  58. package/dist/services/session-output.d.ts +49 -3
  59. package/dist/services/session-output.d.ts.map +1 -1
  60. package/dist/services/session-output.js +115 -2
  61. package/dist/utils/instance-ai-config-debug-logger.d.ts +37 -0
  62. package/dist/utils/instance-ai-config-debug-logger.d.ts.map +1 -0
  63. package/dist/utils/instance-ai-config-debug-logger.js +63 -0
  64. package/dist/utils/logger.d.ts +18 -0
  65. package/dist/utils/logger.d.ts.map +1 -1
  66. package/dist/utils/logger.js +48 -4
  67. package/package/acode/dist/built-in-file-tools.d.ts +1 -1
  68. package/package/acode/dist/built-in-file-tools.d.ts.map +1 -1
  69. package/package/acode/dist/built-in-file-tools.js +5 -3
  70. package/package/acode/dist/builtins/commandArtifacts.d.ts +8 -1
  71. package/package/acode/dist/builtins/commandArtifacts.d.ts.map +1 -1
  72. package/package/acode/dist/builtins/commandArtifacts.js +24 -4
  73. package/package/acode/dist/builtins/shellCommandResult.d.ts +1 -0
  74. package/package/acode/dist/builtins/shellCommandResult.d.ts.map +1 -1
  75. package/package/acode/dist/builtins/shellCommandResult.js +3 -3
  76. package/package/acode/dist/builtins/shellCommandRunner.d.ts +1 -0
  77. package/package/acode/dist/builtins/shellCommandRunner.d.ts.map +1 -1
  78. package/package/acode/dist/builtins/shellCommandRunner.js +1 -0
  79. package/package/acode/dist/builtins/shellOutputDecoder.d.ts +25 -2
  80. package/package/acode/dist/builtins/shellOutputDecoder.d.ts.map +1 -1
  81. package/package/acode/dist/builtins/shellOutputDecoder.js +69 -5
  82. package/package/acode/dist/cli.js +2 -0
  83. package/package/acode/dist/runtime.d.ts.map +1 -1
  84. package/package/acode/dist/runtime.js +156 -5
  85. package/package/acode/dist/types.d.ts +1 -0
  86. package/package/acode/dist/types.d.ts.map +1 -1
  87. package/package/aws-client-agent-mcp/dist/agent-client.d.ts +10 -3
  88. package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
  89. package/package/aws-client-agent-mcp/dist/agent-client.js +33 -11
  90. package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
  91. package/package/aws-client-agent-mcp/dist/context-manager.d.ts +136 -0
  92. package/package/aws-client-agent-mcp/dist/context-manager.d.ts.map +1 -0
  93. package/package/aws-client-agent-mcp/dist/context-manager.js +473 -0
  94. package/package/aws-client-agent-mcp/dist/context-manager.js.map +1 -0
  95. package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts +6 -0
  96. package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts.map +1 -0
  97. package/package/aws-client-agent-mcp/dist/context-manager.test.js +307 -0
  98. package/package/aws-client-agent-mcp/dist/context-manager.test.js.map +1 -0
  99. package/package/aws-client-agent-mcp/dist/http-client.d.ts +11 -0
  100. package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +1 -1
  101. package/package/aws-client-agent-mcp/dist/http-client.js +67 -0
  102. package/package/aws-client-agent-mcp/dist/http-client.js.map +1 -1
  103. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +166 -10
  104. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
  105. package/package/aws-client-agent-mcp/dist/mcp-server.js +933 -41
  106. package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
  107. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +150 -130
  108. package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
  109. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +15 -1
  110. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +1 -1
  111. package/package/aws-client-agent-mcp/dist/mcp-tools.js +62 -2
  112. package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +1 -1
  113. package/package/aws-client-agent-mcp/dist/memory-store.d.ts +58 -1
  114. package/package/aws-client-agent-mcp/dist/memory-store.d.ts.map +1 -1
  115. package/package/aws-client-agent-mcp/dist/memory-store.js +246 -15
  116. package/package/aws-client-agent-mcp/dist/memory-store.js.map +1 -1
  117. package/package/aws-client-agent-mcp/dist/memory-tools.d.ts +8 -0
  118. package/package/aws-client-agent-mcp/dist/memory-tools.d.ts.map +1 -1
  119. package/package/aws-client-agent-mcp/dist/memory-tools.js +106 -2
  120. package/package/aws-client-agent-mcp/dist/memory-tools.js.map +1 -1
  121. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts +7 -1
  122. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts.map +1 -1
  123. package/package/aws-client-agent-mcp/dist/server-memory-store.js +18 -0
  124. package/package/aws-client-agent-mcp/dist/server-memory-store.js.map +1 -1
  125. package/package/aws-client-agent-mcp/dist/types.d.ts +103 -2
  126. package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
  127. package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
  128. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +27 -2
  129. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
  130. package/package/aws-client-agent-mcp/dist/websocket-client.js +123 -5
  131. package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
  132. 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 || "")
@@ -102,21 +134,21 @@ export function resolveStatusChangeRuntimeState(status, actionInfo) {
102
134
  return actionInfo ? { status, actionInfo } : { status };
103
135
  }
104
136
  const terminalCommandStates = new Map();
105
- const DEFAULT_WINDOWS_TERMINAL_OUTPUT_ENCODING = "gb18030";
137
+ const DEFAULT_WINDOWS_TERMINAL_OUTPUT_ENCODING = "utf-8";
106
138
  const DEFAULT_TERMINAL_OUTPUT_ENCODING = "utf-8";
107
139
  const TERMINAL_COMMAND_OUTPUT_SPILLOVER_LINE_LIMIT = 300;
108
140
  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");
@@ -167,14 +199,65 @@ export function resolveTerminalOutputEncoding(platform = process.platform, env =
167
199
  }
168
200
  /**
169
201
  * 创建终端输出流式解码器,避免多字节中文被 chunk 边界截断。
202
+ *
203
+ * 主流程:环境变量显式指定非 utf-8 时固定编码 → 否则自动检测
204
+ * (先 UTF-8,遇替换字符回退 GB18030,覆盖不同程序的不同编码)。
170
205
  */
171
206
  export function createTerminalOutputDecoder(encoding = resolveTerminalOutputEncoding()) {
172
- try {
173
- return new TextDecoder(encoding, { fatal: false });
207
+ // 环境变量显式指定了非 utf-8 编码 → 固定编码,不做自动检测
208
+ if (encoding !== "utf-8") {
209
+ try {
210
+ return new TextDecoder(encoding, { fatal: false });
211
+ }
212
+ catch {
213
+ return new TextDecoder(DEFAULT_TERMINAL_OUTPUT_ENCODING, { fatal: false });
214
+ }
174
215
  }
175
- catch {
176
- return new TextDecoder(DEFAULT_TERMINAL_OUTPUT_ENCODING, { fatal: false });
216
+ // 默认 utf-8 → 启用自动检测
217
+ return createAutoDetectTerminalDecoder();
218
+ }
219
+ function createAutoDetectTerminalDecoder() {
220
+ let utf8Decoder = null;
221
+ let gbDecoder = null;
222
+ let detectedEncoding = null;
223
+ const rawChunks = [];
224
+ function getDecoder(enc) {
225
+ try {
226
+ return new TextDecoder(enc, { fatal: false });
227
+ }
228
+ catch {
229
+ return new TextDecoder("utf-8", { fatal: false });
230
+ }
177
231
  }
232
+ return {
233
+ decode(chunk, options) {
234
+ if (chunk !== undefined && chunk !== null) {
235
+ rawChunks.push(chunk instanceof ArrayBuffer
236
+ ? Buffer.from(chunk)
237
+ : Buffer.isBuffer(chunk)
238
+ ? chunk
239
+ : Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength));
240
+ }
241
+ if (detectedEncoding === "gb18030") {
242
+ if (!gbDecoder)
243
+ gbDecoder = getDecoder("gb18030");
244
+ return gbDecoder.decode(chunk, options);
245
+ }
246
+ if (!utf8Decoder)
247
+ utf8Decoder = getDecoder("utf-8");
248
+ const result = utf8Decoder.decode(chunk, options);
249
+ if (detectedEncoding === null && result.includes("\uFFFD")) {
250
+ detectedEncoding = "gb18030";
251
+ gbDecoder = getDecoder("gb18030");
252
+ const fullBuffer = Buffer.concat(rawChunks);
253
+ return gbDecoder.decode(fullBuffer);
254
+ }
255
+ if (detectedEncoding === null && !options?.stream) {
256
+ detectedEncoding = "utf-8";
257
+ }
258
+ return result;
259
+ },
260
+ };
178
261
  }
179
262
  /**
180
263
  * 解码终端输出 chunk;end=true 时刷新解码器内部剩余字节。
@@ -395,7 +478,7 @@ async function executeTerminalCommand(sessionId, commandValue) {
395
478
  const stderrDecoder = createTerminalOutputDecoder();
396
479
  const usesAcodeCommandOutputPolicy = entry.providerId === "acode";
397
480
  const outputSpilloverState = {
398
- workspacePath: entry.workspacePath,
481
+ agentId: entry.agentId,
399
482
  startedAt: new Date(),
400
483
  chunks: [],
401
484
  spilled: false,
@@ -807,6 +890,24 @@ function displayStringList(value) {
807
890
  .filter(Boolean)
808
891
  .map((item) => redactTransientClipboardPastePaths(item));
809
892
  }
893
+ /**
894
+ * 格式化工具调用输入参数为 JSON 字符串,用于时间线展示。
895
+ * 对敏感内容(如剪贴板临时路径)进行脱敏处理。
896
+ */
897
+ function formatToolInput(toolInput) {
898
+ if (!toolInput || Object.keys(toolInput).length === 0)
899
+ return undefined;
900
+ try {
901
+ const sanitized = redactTransientClipboardPastePaths(JSON.stringify(toolInput, null, 2));
902
+ // 限制输入参数长度,避免超大参数撑爆时间线
903
+ return sanitized.length > 8000
904
+ ? `${sanitized.slice(0, 8000)}\n...(输入参数过长,已截断)`
905
+ : sanitized;
906
+ }
907
+ catch {
908
+ return undefined;
909
+ }
910
+ }
810
911
  function formatAiRequestDetail(data) {
811
912
  const lines = [
812
913
  data.model ? `模型: ${data.model}` : undefined,
@@ -879,6 +980,7 @@ export function resolveRuntimeStatusFromProviderEvent(event, currentRuntimeStatu
879
980
  if (event.type === "tool_use_start") {
880
981
  const inferredAction = getToolActionInfo(event.data.toolName, event.data.toolInput);
881
982
  const actionDetail = resolveSdkToolLogDetail(event, inferredAction);
983
+ const actionInput = formatToolInput(event.data.toolInput);
882
984
  if (event.data.actionType === "idle" ||
883
985
  inferredAction.actionType === "idle") {
884
986
  return {
@@ -890,6 +992,7 @@ export function resolveRuntimeStatusFromProviderEvent(event, currentRuntimeStatu
890
992
  event.data.actionDetail ||
891
993
  inferredAction.actionDetail ||
892
994
  event.data.toolName,
995
+ actionInput,
893
996
  actionId: event.data.actionId || event.data.toolUseId,
894
997
  },
895
998
  };
@@ -903,24 +1006,17 @@ export function resolveRuntimeStatusFromProviderEvent(event, currentRuntimeStatu
903
1006
  event.data.actionDetail ||
904
1007
  inferredAction.actionDetail ||
905
1008
  event.data.toolName,
1009
+ actionInput,
906
1010
  actionId: event.data.actionId || event.data.toolUseId,
907
1011
  },
908
1012
  };
909
1013
  }
910
1014
  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;
1015
+ // 方案 A:不设置 "AI响应" action 信息,避免状态卡在 action 展示
1016
+ // 只返回干净的 waiting_input 状态,usage 信息保留用于 token 统计
920
1017
  return {
921
1018
  status: "waiting_input",
922
- actionInfo: responseActionInfo ||
923
- (event.data.usage ? { usage: event.data.usage } : undefined),
1019
+ actionInfo: event.data.usage ? { usage: event.data.usage } : undefined,
924
1020
  };
925
1021
  }
926
1022
  if (event.type === "error") {
@@ -928,15 +1024,111 @@ export function resolveRuntimeStatusFromProviderEvent(event, currentRuntimeStatu
928
1024
  }
929
1025
  return undefined;
930
1026
  }
931
- function forwardSdkOutputEvent(event, entry) {
1027
+ // Per-session 输出队列:确保 sendOutput HTTP POST 按事件顺序串行到达 Server,
1028
+ // 避免并发 POST 因网络时序导致流式文本 chunk 乱序。
1029
+ const sdkOutputQueueBySession = new Map();
1030
+ /** 小 chunk 积攒缓存:sessionId:sourceEventType → { buffer, seq, timer, sourceEventType, entry },不足 20 字符时积攒最多 500ms 再推送 */
1031
+ const sdkOutputSmallChunkAccumulators = new Map();
1032
+ /** 小 chunk 积攒阈值:字符数少于此值的 output 事件积攒发送 */
1033
+ const SMALL_CHUNK_CHAR_THRESHOLD = 20;
1034
+ /** 小 chunk 最大积攒时间(毫秒) */
1035
+ const SMALL_CHUNK_MAX_ACCUMULATE_MS = 500;
1036
+ function flushSmallChunkAccumulator(sessionId, sourceEventType) {
1037
+ const key = sourceEventType
1038
+ ? `${sessionId}:${sourceEventType}`
1039
+ : undefined;
1040
+ // 如果指定了 key 则精确刷新;否则回退到按 sessionId 前缀查找并刷新所有(兼容大 chunk 触发的全量刷新)
1041
+ let acc;
1042
+ let accKey;
1043
+ if (key) {
1044
+ acc = sdkOutputSmallChunkAccumulators.get(key);
1045
+ accKey = key;
1046
+ }
1047
+ else {
1048
+ // 刷新该 sessionId 下的所有 accumulator(可能有多种 sourceEventType)
1049
+ for (const [k, v] of sdkOutputSmallChunkAccumulators) {
1050
+ if (k.startsWith(sessionId + ":") && v.buffer) {
1051
+ flushSmallChunkAccumulator(sessionId, v.sourceEventType);
1052
+ }
1053
+ }
1054
+ return;
1055
+ }
1056
+ if (!acc || !acc.buffer)
1057
+ return;
1058
+ acc.timer = null;
1059
+ const output = acc.buffer;
1060
+ const seq = acc.seq;
1061
+ const entry = acc.entry;
1062
+ const eventType = acc.sourceEventType;
1063
+ acc.buffer = "";
1064
+ if (accKey)
1065
+ sdkOutputSmallChunkAccumulators.delete(accKey);
1066
+ const metadata = {
1067
+ sourceEventType: eventType,
1068
+ logCategory: "acode",
1069
+ };
1070
+ const authOptions = {
1071
+ runtimeAccessToken: entry.runtimeAccessToken,
1072
+ schedulerBaseUrl: entry.schedulerBaseUrl,
1073
+ };
1074
+ const prev = sdkOutputQueueBySession.get(sessionId) ?? Promise.resolve();
1075
+ const next = prev.then(() => sendOutput(entry.agentId, output, sessionId, seq, metadata, authOptions), () => sendOutput(entry.agentId, output, sessionId, seq, metadata, authOptions));
1076
+ sdkOutputQueueBySession.set(sessionId, next);
1077
+ void next.finally(() => {
1078
+ if (sdkOutputQueueBySession.get(sessionId) === next) {
1079
+ sdkOutputQueueBySession.delete(sessionId);
1080
+ }
1081
+ });
1082
+ }
1083
+ function enqueueSdkOutputEvent(event, entry) {
932
1084
  const output = formatSdkOutputEvent(event);
933
1085
  if (!output) {
934
1086
  return;
935
1087
  }
936
1088
  entry.seq += 1;
937
- void sendOutput(entry.agentId, output, event.sessionId, entry.seq, buildSdkOutputMetadata(event), {
1089
+ const seq = entry.seq;
1090
+ const metadata = buildSdkOutputMetadata(event);
1091
+ const authOptions = {
938
1092
  runtimeAccessToken: entry.runtimeAccessToken,
939
1093
  schedulerBaseUrl: entry.schedulerBaseUrl,
1094
+ };
1095
+ // 小 chunk 积攒优化:命令输出和 AI 文本增量等小片段不足阈值时积攒再推送
1096
+ if ((event.type === "command_output" ||
1097
+ event.type === "text_delta" ||
1098
+ event.type === "thinking") &&
1099
+ output.length < SMALL_CHUNK_CHAR_THRESHOLD) {
1100
+ const accKey = `${event.sessionId}:${event.type}`;
1101
+ let acc = sdkOutputSmallChunkAccumulators.get(accKey);
1102
+ if (!acc) {
1103
+ acc = {
1104
+ buffer: "",
1105
+ seq: 0,
1106
+ timer: null,
1107
+ entry,
1108
+ sourceEventType: event.type,
1109
+ };
1110
+ }
1111
+ acc.buffer += output;
1112
+ acc.seq = seq;
1113
+ acc.entry = entry;
1114
+ if (!acc.timer) {
1115
+ const capturedSessionId = event.sessionId;
1116
+ const capturedType = event.type;
1117
+ acc.timer = setTimeout(() => flushSmallChunkAccumulator(capturedSessionId, capturedType), SMALL_CHUNK_MAX_ACCUMULATE_MS);
1118
+ }
1119
+ sdkOutputSmallChunkAccumulators.set(accKey, acc);
1120
+ return;
1121
+ }
1122
+ // 大 chunk 直接发送(先刷新可能存在的积攒缓存)
1123
+ flushSmallChunkAccumulator(event.sessionId);
1124
+ const prev = sdkOutputQueueBySession.get(event.sessionId) ?? Promise.resolve();
1125
+ const next = prev.then(() => sendOutput(entry.agentId, output, event.sessionId, seq, metadata, authOptions), () => sendOutput(entry.agentId, output, event.sessionId, seq, metadata, authOptions));
1126
+ sdkOutputQueueBySession.set(event.sessionId, next);
1127
+ // 队列空闲时自动清理,避免内存泄漏
1128
+ void next.finally(() => {
1129
+ if (sdkOutputQueueBySession.get(event.sessionId) === next) {
1130
+ sdkOutputQueueBySession.delete(event.sessionId);
1131
+ }
940
1132
  });
941
1133
  }
942
1134
  export function resolveSdkProviderId(command) {
@@ -948,6 +1140,8 @@ function ensureAdapterInitialized() {
948
1140
  if (adapterInitialized)
949
1141
  return;
950
1142
  registerSdkProviders(adapterRegistry, wireSdkAdapterEvents);
1143
+ // 需求 4.2.1 #2:启动全局心跳调度器(每 30s 推送空包检测 Server 连接存活)
1144
+ startHeartbeatScheduler();
951
1145
  adapterInitialized = true;
952
1146
  }
953
1147
  function wireSdkAdapterEvents(adapter, definition) {
@@ -955,20 +1149,36 @@ function wireSdkAdapterEvents(adapter, definition) {
955
1149
  const entry = sdkSessions.get(event.sessionId);
956
1150
  if (entry) {
957
1151
  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
- });
1152
+ // Skip sendStatus for thinking/text_delta/tool_use_start events — the status-change
1153
+ // listener already emits the authoritative status with restored actionInfo,
1154
+ // avoiding duplicate timeline entries in agentEventStore.
1155
+ const isDeferredStatusEvent = event.type === "thinking" || event.type === "text_delta" || event.type === "tool_use_start";
1156
+ if (!isDeferredStatusEvent) {
1157
+ const runtimeStatus = resolveRuntimeStatusFromProviderEvent(event, entry.currentRuntimeStatus);
1158
+ if (runtimeStatus) {
1159
+ void sendStatus(entry.agentId, event.sessionId, runtimeStatus.status, runtimeStatus.actionInfo, resolveStatusChangeUsage(runtimeStatus.actionInfo), {
1160
+ runtimeAccessToken: entry.runtimeAccessToken,
1161
+ schedulerBaseUrl: entry.schedulerBaseUrl,
1162
+ },
1163
+ // Pass original timestamp from ACode runtime for correct timeline ordering
1164
+ // Convert ISO string timestamp to epoch milliseconds
1165
+ event.timestamp ? new Date(event.timestamp).getTime() : undefined,
1166
+ // Pass original event type for frontend gray-zone rendering (thinking vs text_delta)
1167
+ event.type);
1168
+ }
964
1169
  }
965
- forwardSdkOutputEvent(event, entry);
1170
+ enqueueSdkOutputEvent(event, entry);
966
1171
  const toolResultActionInfo = buildToolResultTimelineActionInfo(event);
967
1172
  if (toolResultActionInfo) {
968
1173
  void sendStatus(entry.agentId, event.sessionId, "tool_result", toolResultActionInfo, undefined, {
969
1174
  runtimeAccessToken: entry.runtimeAccessToken,
970
1175
  schedulerBaseUrl: entry.schedulerBaseUrl,
971
- });
1176
+ },
1177
+ // Pass original timestamp from ACode runtime for correct timeline ordering
1178
+ // Convert ISO string timestamp to epoch milliseconds
1179
+ event.timestamp ? new Date(event.timestamp).getTime() : undefined,
1180
+ // Pass original event type (tool_use_end) for timeline sourceEventType
1181
+ event.type);
972
1182
  }
973
1183
  }
974
1184
  });
@@ -977,18 +1187,48 @@ function wireSdkAdapterEvents(adapter, definition) {
977
1187
  if (entry) {
978
1188
  const runtimeState = resolveStatusChangeRuntimeState(status, actionInfo);
979
1189
  entry.currentRuntimeStatus = runtimeState.status;
980
- entry.currentRuntimeActionType = runtimeState.actionInfo?.actionType;
981
- entry.currentRuntimeActionLabel = runtimeState.actionInfo?.actionLabel;
982
- entry.currentRuntimeActionDetail =
983
- runtimeState.actionInfo?.actionDetail;
1190
+ // 当新状态未携带动作且进入 thinking 时,保留上一个 idle 类动作(如 poll_message),
1191
+ // 避免页面刷新后丢失"监听消息"状态而错误显示"思考中"。
1192
+ // 注意:MCP client 发送的 actionType 是 "mcp" 而非 "idle",需要通过 actionDetail 判断。
1193
+ // ★ 关键修复: turn_complete 后进入 waiting_input 时,如果携带的是 ai_request 动作,
1194
+ // 不应该保留之前的 tool_using 动作,避免永久卡在工具调用状态。
1195
+ const prevActionDetail = entry.currentRuntimeActionDetail ?? "";
1196
+ const isPrevPollMessage = prevActionDetail.includes("poll_message") || prevActionDetail.includes("get_message");
1197
+ const isNewAiRequestAction = runtimeState.actionInfo?.actionType === "ai_request";
1198
+ const shouldPreservePrevAction = !isNewAiRequestAction && // 新动作不是 AI 响应
1199
+ (runtimeState.status === "thinking" || runtimeState.status === "waiting_input") && // 状态是 thinking 或 waiting_input
1200
+ (entry.currentRuntimeActionType === "idle" || entry.currentRuntimeActionType === "mcp") && // 之前是 idle/mcp 动作
1201
+ isPrevPollMessage; // 且之前是 poll_message
1202
+ if (runtimeState.actionInfo?.actionType || // 新状态携带了动作
1203
+ !shouldPreservePrevAction // 不满足保留条件
1204
+ ) {
1205
+ entry.currentRuntimeActionType = runtimeState.actionInfo?.actionType;
1206
+ entry.currentRuntimeActionLabel = runtimeState.actionInfo?.actionLabel;
1207
+ entry.currentRuntimeActionDetail =
1208
+ runtimeState.actionInfo?.actionDetail;
1209
+ entry.currentRuntimeActionInput =
1210
+ runtimeState.actionInfo?.actionInput;
1211
+ }
984
1212
  console.log(`[${definition.displayName} Adapter] Status changed: ${runtimeState.status} for agent ${entry.agentId}`, runtimeState.actionInfo || "");
985
1213
  void updatePersistedSessionRuntimeState(sessionId, {
986
1214
  runtimeStatus: runtimeState.status,
987
- runtimeActionType: runtimeState.actionInfo?.actionType,
988
- runtimeActionLabel: runtimeState.actionInfo?.actionLabel,
989
- runtimeActionDetail: runtimeState.actionInfo?.actionDetail,
1215
+ runtimeActionType: entry.currentRuntimeActionType,
1216
+ runtimeActionLabel: entry.currentRuntimeActionLabel,
1217
+ runtimeActionDetail: entry.currentRuntimeActionDetail,
990
1218
  });
991
- void sendStatus(entry.agentId, sessionId, runtimeState.status, runtimeState.actionInfo, resolveStatusChangeUsage(runtimeState.actionInfo), {
1219
+ // thinking + poll_message 动作保留时,派生恢复用 actionInfo 传给 sendStatus
1220
+ // 支持 actionType 为 "idle" 或 "mcp",通过 actionDetail 中的 poll_message/get_message 判断
1221
+ const restoredActionInfo = runtimeState.actionInfo?.actionType
1222
+ ? runtimeState.actionInfo
1223
+ : entry.currentRuntimeActionType
1224
+ ? {
1225
+ actionType: entry.currentRuntimeActionType,
1226
+ actionLabel: entry.currentRuntimeActionLabel ?? "",
1227
+ actionDetail: entry.currentRuntimeActionDetail,
1228
+ actionInput: entry.currentRuntimeActionInput,
1229
+ }
1230
+ : undefined;
1231
+ void sendStatus(entry.agentId, sessionId, runtimeState.status, restoredActionInfo, resolveStatusChangeUsage(runtimeState.actionInfo), {
992
1232
  runtimeAccessToken: entry.runtimeAccessToken,
993
1233
  schedulerBaseUrl: entry.schedulerBaseUrl,
994
1234
  });
@@ -1008,6 +1248,17 @@ function wireSdkAdapterEvents(adapter, definition) {
1008
1248
  const entry = sdkSessions.get(data.sessionId);
1009
1249
  if (entry) {
1010
1250
  console.log(`[${definition.displayName} Adapter] Permission request: ${data.toolName} for agent ${entry.agentId}`);
1251
+ const actionDetail = data.toolInput
1252
+ ? redactTransientClipboardPastePaths(JSON.stringify(data.toolInput).slice(0, 400))
1253
+ : undefined;
1254
+ void sendStatus(entry.agentId, data.sessionId, "waiting_input", {
1255
+ actionType: "other",
1256
+ actionLabel: "权限确认",
1257
+ actionDetail: actionDetail || data.toolName,
1258
+ }, undefined, {
1259
+ runtimeAccessToken: entry.runtimeAccessToken,
1260
+ schedulerBaseUrl: entry.schedulerBaseUrl,
1261
+ });
1011
1262
  }
1012
1263
  });
1013
1264
  }
@@ -1049,7 +1300,24 @@ async function startSdkSession(req, res) {
1049
1300
  // MCP 配置 - 加载 aws-client-agent-mcp
1050
1301
  mcpConfigPath, extraMcpServers, envOverrides,
1051
1302
  // 空闲命令配置
1052
- idleInputAutoCommand, nonInputAutoCommand, } = req.body || {};
1303
+ idleInputAutoCommand, nonInputAutoCommand,
1304
+ // Per-agent model override(优先于全局配置)
1305
+ model: perAgentModel,
1306
+ // Per-instance AI provider 覆盖(仅 ACode 消费;优先级:api > env > file > global)
1307
+ instanceAiConfig: perInstanceAiConfig, } = req.body || {};
1308
+ const _dbg = getInstanceAiConfigDebugLogger();
1309
+ void _dbg.log({
1310
+ level: "info",
1311
+ category: "launch",
1312
+ message: "startSdkSession: 收到启动请求",
1313
+ source: "bridge",
1314
+ agentId: String(agentId || "").trim() || undefined,
1315
+ details: {
1316
+ reqBodyKeys: Object.keys(req.body || {}),
1317
+ perInstanceAiConfig: perInstanceAiConfig ? redactForLog(perInstanceAiConfig) : null,
1318
+ perAgentModel: perAgentModel ?? null,
1319
+ },
1320
+ });
1053
1321
  let sessionId = null;
1054
1322
  const normalizedAgentId = String(agentId || "").trim();
1055
1323
  const runtimeAccessToken = String(req.body?.runtimeAccessToken || "").trim() || undefined;
@@ -1074,9 +1342,14 @@ async function startSdkSession(req, res) {
1074
1342
  await removePersistedSession(persistedSession.sessionId);
1075
1343
  }
1076
1344
  else {
1345
+ const displayStatus = resolveSdkSessionDisplayStatus(persistedSession.runtimeStatus, persistedSession.runtimeStatus, persistedSession.runtimeActionType);
1077
1346
  res.json({
1078
1347
  sessionId: persistedSession.sessionId,
1079
- status: persistedSession.status,
1348
+ status: displayStatus,
1349
+ runtimeStatus: displayStatus,
1350
+ runtimeActionType: persistedSession.runtimeActionType,
1351
+ runtimeActionLabel: persistedSession.runtimeActionLabel,
1352
+ runtimeActionDetail: persistedSession.runtimeActionDetail,
1080
1353
  agentId: persistedSession.agentId,
1081
1354
  workspacePath: persistedSession.workspacePath,
1082
1355
  command: persistedSession.command,
@@ -1098,7 +1371,6 @@ async function startSdkSession(req, res) {
1098
1371
  // 根据命令选择对应的 adapter
1099
1372
  const normalizedCommand = (command || "acode").toLowerCase();
1100
1373
  const providerId = resolveSdkProviderId(command);
1101
- console.log(`[Runtime] ★★★ SDK启动 ★★★ command="${command}", normalizedCommand="${normalizedCommand}", providerId="${providerId}"`);
1102
1374
  const adapter = adapterRegistry.get(providerId);
1103
1375
  sessionId = uuidv4();
1104
1376
  const savedClaudeConfig = providerId === "claude-code"
@@ -1107,18 +1379,108 @@ async function startSdkSession(req, res) {
1107
1379
  const claudeLaunchConfig = providerId === "claude-code"
1108
1380
  ? buildClaudeCodeLaunchConfig(savedClaudeConfig, envOverrides)
1109
1381
  : buildClaudeCodeLaunchConfig(null, envOverrides);
1382
+ // 模型解析优先级(ACode 与非 ACode 不同):
1383
+ //
1384
+ // ACode:模型仲裁收敛到 AcodeSdkAdapter.resolveAcodeProvider → resolveAcodeProviderConfig 纯函数
1385
+ // 优先级:perSessionModel(request) > instanceAiConfig(instance) > acode.json(global) > default
1386
+ // 这里不预解析,将原始输入传递给 adapter
1387
+ //
1388
+ // 非 ACode (claude-code/codex/opencode):
1389
+ // 优先级:perAgentModel(request) > claudeLaunchConfig.model(global)
1390
+ // 通过 ANTHROPIC_MODEL/CODEX_MODEL 环境变量注入
1391
+ const resolvedModel = providerId === "acode"
1392
+ ? (perAgentModel ? String(perAgentModel).trim() : undefined)
1393
+ : (() => {
1394
+ const requestModel = perAgentModel ? String(perAgentModel).trim() : undefined;
1395
+ const globalModel = claudeLaunchConfig.model ? String(claudeLaunchConfig.model).trim() : undefined;
1396
+ return requestModel || globalModel;
1397
+ })();
1398
+ void _dbg.log({
1399
+ level: "info",
1400
+ category: "resolve",
1401
+ message: "模型解析结果",
1402
+ source: "bridge",
1403
+ agentId: normalizedAgentId,
1404
+ sessionId: sessionId ?? undefined,
1405
+ details: {
1406
+ perAgentModel: perAgentModel ?? null,
1407
+ globalModel: claudeLaunchConfig.model ?? null,
1408
+ resolvedModel: resolvedModel ?? null,
1409
+ providerId,
1410
+ note: providerId === "acode" ? "adapter handles full priority" : "request > global",
1411
+ },
1412
+ });
1413
+ // 构建 effectiveEnvOverrides:将 resolvedModel 注入各 provider 对应的环境变量
1414
+ const effectiveEnvOverrides = { ...claudeLaunchConfig.envOverrides };
1415
+ if (resolvedModel) {
1416
+ // Claude Code 使用 ANTHROPIC_MODEL
1417
+ effectiveEnvOverrides.ANTHROPIC_MODEL = resolvedModel;
1418
+ // Codex 使用 CODEX_MODEL
1419
+ effectiveEnvOverrides.CODEX_MODEL = resolvedModel;
1420
+ // ACode / OpenCode: 不通过 env var 传递模型
1421
+ }
1422
+ // ACode 实例自配置:解析 api > env > file,deep-merge 全局后写入 per-instance acode.json
1423
+ // 其它 provider 不消费 instanceAiConfig,保持向后兼容
1424
+ // 模型仲裁由 AcodeSdkAdapter.resolveAcodeProvider 统一处理
1425
+ let resolvedInstanceAiConfig;
1426
+ let resolvedMcpConfigPath = mcpConfigPath;
1427
+ if (providerId === "acode") {
1428
+ const mergedInstanceConfig = perInstanceAiConfig || {};
1429
+ const prepared = await prepareAcodeInstanceConfig({
1430
+ apiOverrides: sanitizeInstanceAiConfig(mergedInstanceConfig) ?? undefined,
1431
+ agentId: normalizedAgentId,
1432
+ sessionId: sessionId ?? undefined,
1433
+ runtimeBridgeBaseUrl: requestedSchedulerBaseUrl,
1434
+ });
1435
+ resolvedInstanceAiConfig = prepared.instanceConfig.config;
1436
+ resolvedMcpConfigPath = prepared.perInstanceConfigPath;
1437
+ void _dbg.log({
1438
+ level: "info",
1439
+ category: "apply",
1440
+ message: "prepareAcodeInstanceConfig 完成",
1441
+ source: "bridge",
1442
+ agentId: normalizedAgentId,
1443
+ sessionId: sessionId ?? undefined,
1444
+ details: {
1445
+ sanitizedApiOverrides: sanitizeInstanceAiConfig(mergedInstanceConfig) ? redactForLog(sanitizeInstanceAiConfig(mergedInstanceConfig)) : null,
1446
+ resolvedInstanceAiConfig: resolvedInstanceAiConfig ? redactForLog(resolvedInstanceAiConfig) : null,
1447
+ perInstanceConfigPath: resolvedMcpConfigPath,
1448
+ instanceConfigSources: prepared.instanceConfig.source ?? null,
1449
+ },
1450
+ });
1451
+ }
1110
1452
  // 构建SDK配置
1111
1453
  const config = {
1112
1454
  command: command || "acode",
1113
1455
  workingDirectory: workspacePath,
1456
+ agentId: normalizedAgentId,
1114
1457
  autoAccept: Boolean(autoAccept),
1115
1458
  initialPrompt,
1116
- model: claudeLaunchConfig.model,
1459
+ model: resolvedModel,
1117
1460
  // MCP 配置:不再默认注入 aws-mcp;由用户在面板中安装/配置
1118
- mcpConfigPath,
1461
+ // ACode 场景下,per-instance acode.json 已由 instance-ai-config-service 写入
1462
+ mcpConfigPath: resolvedMcpConfigPath,
1463
+ // ACode 实例自配置:来源与值会写入 d:/Instance-ai-configuration-debug.log
1464
+ ...(resolvedInstanceAiConfig
1465
+ ? { instanceAiConfig: resolvedInstanceAiConfig }
1466
+ : {}),
1119
1467
  extraMcpServers: extraMcpServers || {},
1120
- envOverrides: buildRuntimeEnv(String(agentId), String(workspacePath), process.env, claudeLaunchConfig.envOverrides),
1468
+ envOverrides: buildRuntimeEnv(String(agentId), String(workspacePath), process.env, effectiveEnvOverrides),
1121
1469
  };
1470
+ void _dbg.log({
1471
+ level: "info",
1472
+ category: "launch",
1473
+ message: "传递给 Adapter 的完整 AdapterSessionConfig",
1474
+ source: "bridge",
1475
+ agentId: normalizedAgentId,
1476
+ sessionId,
1477
+ details: {
1478
+ adapterModel: config.model ?? null,
1479
+ adapterInstanceAiConfig: config.instanceAiConfig ? redactForLog(config.instanceAiConfig) : null,
1480
+ adapterMcpConfigPath: config.mcpConfigPath ?? null,
1481
+ },
1482
+ });
1483
+ await _dbg.flush();
1122
1484
  // 存储会话信息
1123
1485
  sdkSessions.set(sessionId, {
1124
1486
  agentId: normalizedAgentId,
@@ -1784,3 +2146,58 @@ terminalRouter.post("/stop-all", validateToken, async (_req, res) => {
1784
2146
  errors: errors.length > 0 ? errors : undefined,
1785
2147
  });
1786
2148
  });
2149
+ // ============ 桥接侧事件查询 ============
2150
+ /**
2151
+ * 查询最近 N 条桥接侧本地事件。
2152
+ *
2153
+ * 用于前端面板或调试工具获取最近的状态/输出事件快照。
2154
+ * GET /runtime/events/last/:n
2155
+ */
2156
+ terminalRouter.get("/events/last/:n", validateToken, (req, res) => {
2157
+ const nParam = parseInt(req.params.n, 10);
2158
+ if (!Number.isFinite(nParam) || nParam < 1) {
2159
+ res.status(400).json({ error: ":n must be a positive integer" });
2160
+ return;
2161
+ }
2162
+ const entries = bridgeEventBuffer.getLastN(nParam);
2163
+ res.json({
2164
+ ok: true,
2165
+ count: entries.length,
2166
+ entries,
2167
+ });
2168
+ });
2169
+ /**
2170
+ * 查询自指定 index(不含)之后的桥接侧本地事件。
2171
+ *
2172
+ * 增量轮询端点:客户端保存上一次看到的 index,
2173
+ * 下次传入该 index 仅获取增量事件,适用于实时刷新场景。
2174
+ * GET /runtime/events/since/:index
2175
+ */
2176
+ terminalRouter.get("/events/since/:index", validateToken, (req, res) => {
2177
+ const indexParam = parseInt(req.params.index, 10);
2178
+ if (!Number.isFinite(indexParam) || indexParam < 0) {
2179
+ res.status(400).json({ error: ":index must be a non-negative integer" });
2180
+ return;
2181
+ }
2182
+ const slice = bridgeEventBuffer.getSinceIndex(indexParam);
2183
+ res.json({
2184
+ ok: true,
2185
+ entries: slice.entries,
2186
+ nextIndex: slice.nextIndex,
2187
+ oldestIndex: slice.oldestIndex,
2188
+ dataLoss: slice.oldestIndex > indexParam + 1,
2189
+ });
2190
+ });
2191
+ /**
2192
+ * 获取 Bridge 侧事件推送统计(需求 4.2.1 #3)。
2193
+ * 返回每个 Agent 的待推送数、累计成功/失败、连续失败次数、最近延迟等。
2194
+ * GET /runtime/events/push-stats
2195
+ */
2196
+ terminalRouter.get("/events/push-stats", validateToken, (_req, res) => {
2197
+ const stats = getAllPushStats();
2198
+ res.json({
2199
+ ok: true,
2200
+ count: stats.length,
2201
+ agents: stats,
2202
+ });
2203
+ });