aws-runtime-bridge 1.7.28 → 1.7.29

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 (67) hide show
  1. package/dist/adapter/ClaudeSdkAdapter.d.ts +5 -0
  2. package/dist/adapter/ClaudeSdkAdapter.d.ts.map +1 -1
  3. package/dist/adapter/ClaudeSdkAdapter.js +20 -6
  4. package/dist/adapter/ClaudeSdkAdapter.test.js +43 -0
  5. package/dist/adapter/CodexSdkAdapter.js +1 -1
  6. package/dist/adapter/OpencodeSdkAdapter.js +2 -2
  7. package/dist/adapter/types.d.ts +13 -4
  8. package/dist/adapter/types.d.ts.map +1 -1
  9. package/dist/index.js +101 -77
  10. package/dist/routes/sessions.js +16 -0
  11. package/dist/routes/terminal.d.ts +19 -1
  12. package/dist/routes/terminal.d.ts.map +1 -1
  13. package/dist/routes/terminal.js +62 -16
  14. package/dist/routes/terminal.test.js +25 -1
  15. package/dist/services/aws-client-agent-mcp.d.ts.map +1 -1
  16. package/dist/services/aws-client-agent-mcp.js +22 -3
  17. package/dist/services/mcp-launch-binding-queue.d.ts.map +1 -1
  18. package/dist/services/mcp-launch-binding-queue.js +8 -1
  19. package/dist/services/mcp-launch-binding-queue.test.js +11 -0
  20. package/dist/services/orphan-monitor.d.ts.map +1 -1
  21. package/dist/services/orphan-monitor.js +16 -5
  22. package/dist/services/runtime-binding.d.ts.map +1 -1
  23. package/dist/services/runtime-binding.js +8 -1
  24. package/dist/services/runtime-binding.test.js +15 -0
  25. package/dist/services/runtime-lifecycle-policy.d.ts +26 -0
  26. package/dist/services/runtime-lifecycle-policy.d.ts.map +1 -0
  27. package/dist/services/runtime-lifecycle-policy.js +34 -0
  28. package/dist/services/runtime-lifecycle-policy.test.d.ts +2 -0
  29. package/dist/services/runtime-lifecycle-policy.test.d.ts.map +1 -0
  30. package/dist/services/runtime-lifecycle-policy.test.js +41 -0
  31. package/dist/services/terminal-persistence.d.ts +12 -0
  32. package/dist/services/terminal-persistence.d.ts.map +1 -1
  33. package/dist/services/terminal-persistence.js +23 -0
  34. package/dist/services/terminal-persistence.test.js +18 -0
  35. package/dist/types.d.ts +8 -0
  36. package/dist/types.d.ts.map +1 -1
  37. package/package/aws-client-agent-mcp/dist/agent-client.d.ts +5 -0
  38. package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
  39. package/package/aws-client-agent-mcp/dist/agent-client.js +30 -3
  40. package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
  41. package/package/aws-client-agent-mcp/dist/agent-client.test.js +118 -0
  42. package/package/aws-client-agent-mcp/dist/agent-client.test.js.map +1 -1
  43. package/package/aws-client-agent-mcp/dist/config.d.ts.map +1 -1
  44. package/package/aws-client-agent-mcp/dist/config.js +10 -1
  45. package/package/aws-client-agent-mcp/dist/config.js.map +1 -1
  46. package/package/aws-client-agent-mcp/dist/config.test.js +10 -0
  47. package/package/aws-client-agent-mcp/dist/config.test.js.map +1 -1
  48. package/package/aws-client-agent-mcp/dist/http-client.d.ts +5 -0
  49. package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +1 -1
  50. package/package/aws-client-agent-mcp/dist/http-client.js +47 -3
  51. package/package/aws-client-agent-mcp/dist/http-client.js.map +1 -1
  52. package/package/aws-client-agent-mcp/dist/http-client.test.js +344 -9
  53. package/package/aws-client-agent-mcp/dist/http-client.test.js.map +1 -1
  54. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +4 -3
  55. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
  56. package/package/aws-client-agent-mcp/dist/mcp-server.js +28 -15
  57. package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
  58. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +107 -11
  59. package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
  60. package/package/aws-client-agent-mcp/dist/types.d.ts +9 -0
  61. package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
  62. package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
  63. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +5 -0
  64. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
  65. package/package/aws-client-agent-mcp/dist/websocket-client.js +26 -0
  66. package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
  67. package/package.json +1 -1
@@ -15,14 +15,24 @@ import { adapterRegistry, registerSdkProviders, resolveSdkProviderIdByCommand, }
15
15
  import { validateToken } from '../middleware/auth.js';
16
16
  import { getAgentProcessManager } from '../services/agent-process-manager.js';
17
17
  import { enqueueMcpLaunchBinding } from '../services/mcp-launch-binding-queue.js';
18
- import { findClaudeCodeProcesses } from '../services/process-detector.js';
18
+ import { findClaudeCodeProcesses, isProcessRunning } from '../services/process-detector.js';
19
19
  import { sendOutput, sendQuestionRequest, sendStatus } from '../services/session-output.js';
20
- import { findPersistedSessionByAgentId, removePersistedSession, savePersistedSessions, updatePersistedSessionAutoCommands, upsertPersistedSession, } from '../services/terminal-persistence.js';
20
+ import { findPersistedSessionByAgentId, removePersistedSession, savePersistedSessions, updatePersistedSessionAutoCommands, updatePersistedSessionRuntimeState, upsertPersistedSession, } from '../services/terminal-persistence.js';
21
21
  export const terminalRouter = Router();
22
22
  // 导出 SDK 会话存储,供其他模块使用(如 sessions.ts)
23
23
  export const sdkSessions = new Map();
24
24
  const TOOL_RESULT_PREVIEW_MAX_LENGTH = 4000;
25
25
  const startingAgents = new Set();
26
+ export function resolveStatusChangeUsage(actionInfo) {
27
+ const usage = actionInfo?.usage;
28
+ if (!usage)
29
+ return undefined;
30
+ const inputTokens = Number.isFinite(usage.inputTokens) ? usage.inputTokens : 0;
31
+ const outputTokens = Number.isFinite(usage.outputTokens) ? usage.outputTokens : 0;
32
+ if (inputTokens <= 0 && outputTokens <= 0)
33
+ return undefined;
34
+ return { inputTokens, outputTokens };
35
+ }
26
36
  const terminalCommandStates = new Map();
27
37
  const DEFAULT_WINDOWS_TERMINAL_OUTPUT_ENCODING = 'gb18030';
28
38
  const DEFAULT_TERMINAL_OUTPUT_ENCODING = 'utf-8';
@@ -110,6 +120,10 @@ async function buildExistingSessionResponse(entry) {
110
120
  return {
111
121
  sessionId: entry.sessionId,
112
122
  status: adapter.getSessionStatus(entry.sessionId) || 'running',
123
+ runtimeStatus: entry.currentRuntimeStatus,
124
+ runtimeActionType: entry.currentRuntimeActionType,
125
+ runtimeActionLabel: entry.currentRuntimeActionLabel,
126
+ runtimeActionDetail: entry.currentRuntimeActionDetail,
113
127
  agentId: entry.agentId,
114
128
  workspacePath: entry.workspacePath,
115
129
  command: entry.config.command,
@@ -118,6 +132,19 @@ async function buildExistingSessionResponse(entry) {
118
132
  reused: true,
119
133
  };
120
134
  }
135
+ /**
136
+ * 判断持久化 SDK 会话能否在显式启动请求中复用。
137
+ * 主流程:必须存在 PID 且 OS 仍能确认该进程存活;否则视为重启后遗留的陈旧会话。
138
+ */
139
+ export function evaluatePersistedSessionReuse(session, isPidRunning = isProcessRunning) {
140
+ if (!session.pid) {
141
+ return { reusable: false, reason: 'missing-pid' };
142
+ }
143
+ if (!isPidRunning(session.pid)) {
144
+ return { reusable: false, reason: 'dead-pid' };
145
+ }
146
+ return { reusable: true, reason: 'active-pid' };
147
+ }
121
148
  function stopTerminalCommandProcess(sessionId) {
122
149
  const state = terminalCommandStates.get(sessionId);
123
150
  if (!state?.runningProcess) {
@@ -337,15 +364,25 @@ function wireSdkAdapterEvents(adapter, definition) {
337
364
  }
338
365
  }
339
366
  if (event.type === 'turn_complete' && event.data.usage) {
340
- sendStatus(entry.agentId, event.sessionId, 'waiting_input', undefined, event.data.usage);
367
+ sendStatus(entry.agentId, event.sessionId, 'waiting_input', undefined, resolveStatusChangeUsage({ usage: event.data.usage }));
341
368
  }
342
369
  }
343
370
  });
344
371
  adapter.on('status-change', (sessionId, status, actionInfo) => {
345
372
  const entry = sdkSessions.get(sessionId);
346
373
  if (entry) {
374
+ entry.currentRuntimeStatus = status;
375
+ entry.currentRuntimeActionType = actionInfo?.actionType;
376
+ entry.currentRuntimeActionLabel = actionInfo?.actionLabel;
377
+ entry.currentRuntimeActionDetail = actionInfo?.actionDetail;
347
378
  console.log(`[${definition.displayName} Adapter] Status changed: ${status} for agent ${entry.agentId}`, actionInfo || '');
348
- void sendStatus(entry.agentId, sessionId, status, actionInfo);
379
+ void updatePersistedSessionRuntimeState(sessionId, {
380
+ runtimeStatus: status,
381
+ runtimeActionType: actionInfo?.actionType,
382
+ runtimeActionLabel: actionInfo?.actionLabel,
383
+ runtimeActionDetail: actionInfo?.actionDetail,
384
+ });
385
+ void sendStatus(entry.agentId, sessionId, status, actionInfo, resolveStatusChangeUsage(actionInfo));
349
386
  }
350
387
  });
351
388
  adapter.on('ask-user-question', (data) => {
@@ -410,18 +447,26 @@ async function startSdkSession(req, res) {
410
447
  }
411
448
  const persistedSession = await findPersistedSessionByAgentId(normalizedAgentId);
412
449
  if (persistedSession) {
413
- res.json({
414
- sessionId: persistedSession.sessionId,
415
- status: persistedSession.status,
416
- agentId: persistedSession.agentId,
417
- workspacePath: persistedSession.workspacePath,
418
- command: persistedSession.command,
419
- mode: persistedSession.mode || 'sdk',
420
- pid: persistedSession.pid,
421
- reused: true,
422
- persistedOnly: true,
423
- });
424
- return;
450
+ const reuseDecision = evaluatePersistedSessionReuse(persistedSession);
451
+ if (!reuseDecision.reusable) {
452
+ console.warn(`[Runtime] Ignoring stale persisted SDK session for agentId=${normalizedAgentId}, `
453
+ + `sessionId=${persistedSession.sessionId}, reason=${reuseDecision.reason}`);
454
+ await removePersistedSession(persistedSession.sessionId);
455
+ }
456
+ else {
457
+ res.json({
458
+ sessionId: persistedSession.sessionId,
459
+ status: persistedSession.status,
460
+ agentId: persistedSession.agentId,
461
+ workspacePath: persistedSession.workspacePath,
462
+ command: persistedSession.command,
463
+ mode: persistedSession.mode || 'sdk',
464
+ pid: persistedSession.pid,
465
+ reused: true,
466
+ persistedOnly: true,
467
+ });
468
+ return;
469
+ }
425
470
  }
426
471
  if (startingAgents.has(normalizedAgentId)) {
427
472
  res.status(409).json({ error: 'SDK session is already starting for this agent' });
@@ -489,6 +534,7 @@ async function startSdkSession(req, res) {
489
534
  command: config.command,
490
535
  startedAt: new Date().toISOString(),
491
536
  status: 'running',
537
+ runtimeStatus: 'running',
492
538
  mode: 'sdk',
493
539
  pid: sdkPid,
494
540
  providerSessionId: adapter.getProviderSessionId(sessionId),
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { SDK_PROVIDER_DEFINITIONS } from '../adapter/SdkProviderSpi.js';
3
- import { buildRuntimeEnv, createTerminalOutputDecoder, decodeTerminalOutputChunk, formatTerminalPrompt, formatSdkOutputEvent, normalizeTerminalCommandInput, parseTerminalDirectoryChangeTarget, resolveTerminalOutputEncoding, resolveSdkProviderId, } from './terminal.js';
3
+ import { buildRuntimeEnv, createTerminalOutputDecoder, decodeTerminalOutputChunk, evaluatePersistedSessionReuse, formatTerminalPrompt, formatSdkOutputEvent, normalizeTerminalCommandInput, parseTerminalDirectoryChangeTarget, resolveTerminalOutputEncoding, resolveSdkProviderId, resolveStatusChangeUsage, } from './terminal.js';
4
4
  describe('terminal route validation', () => {
5
5
  it('requires agentId and workspacePath for start', () => {
6
6
  const validateStartRequest = (body) => {
@@ -83,6 +83,20 @@ describe('terminal configuration', () => {
83
83
  expect(env.AWS_WORKSPACE_PATH).toBe('/workspace');
84
84
  });
85
85
  });
86
+ describe('persisted SDK session reuse', () => {
87
+ it('does not reuse a persisted session without a PID', () => {
88
+ const decision = evaluatePersistedSessionReuse({}, () => true);
89
+ expect(decision).toEqual({ reusable: false, reason: 'missing-pid' });
90
+ });
91
+ it('does not reuse a persisted session when its PID is no longer running', () => {
92
+ const decision = evaluatePersistedSessionReuse({ pid: 1234 }, () => false);
93
+ expect(decision).toEqual({ reusable: false, reason: 'dead-pid' });
94
+ });
95
+ it('reuses a persisted session only when its PID is still running', () => {
96
+ const decision = evaluatePersistedSessionReuse({ pid: 1234 }, () => true);
97
+ expect(decision).toEqual({ reusable: true, reason: 'active-pid' });
98
+ });
99
+ });
86
100
  describe('terminal command helpers', () => {
87
101
  it('normalizes carriage-return terminal input into executable command text', () => {
88
102
  expect(normalizeTerminalCommandInput('npm test\r')).toBe('npm test');
@@ -130,6 +144,16 @@ describe('resolveSdkProviderId', () => {
130
144
  expect(resolveSdkProviderId(undefined)).toBe('claude-code');
131
145
  });
132
146
  });
147
+ describe('SDK status usage forwarding', () => {
148
+ it('keeps accumulated usage from status-change action info', () => {
149
+ expect(resolveStatusChangeUsage({ usage: { inputTokens: 12000, outputTokens: 345 } })).toEqual({
150
+ inputTokens: 12000,
151
+ outputTokens: 345,
152
+ });
153
+ expect(resolveStatusChangeUsage({ usage: { inputTokens: 0, outputTokens: 0 } })).toBeUndefined();
154
+ expect(resolveStatusChangeUsage({ actionType: 'mcp', actionLabel: 'Calling MCP' })).toBeUndefined();
155
+ });
156
+ });
133
157
  describe('SDK output forwarding', () => {
134
158
  it('formats text and thinking provider events for the terminal output callback path', () => {
135
159
  const textEvent = {
@@ -1 +1 @@
1
- {"version":3,"file":"aws-client-agent-mcp.d.ts","sourceRoot":"","sources":["../../src/services/aws-client-agent-mcp.ts"],"names":[],"mappings":"AAwBA,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAqB7C,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,sCAAsC;IACrD,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IACvC,OAAO,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,sCAAsC;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAaD,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;CAC3C;AAuBD,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAgLD,wBAAgB,+BAA+B,CAC7C,OAAO,GAAE,sCAA2C,GACnD,MAAM,GAAG,IAAI,CAoDf;AA2ED,wBAAgB,+BAA+B,CAC7C,OAAO,GAAE,sCAA2C,GACnD,kBAAkB,CAEpB;AAED,wBAAgB,gCAAgC,CAC9C,OAAO,GAAE,sCAA2C,GACnD,kBAAkB,CAsBpB;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,uBAAuB,GAC7B,kBAAkB,CAsBpB;AAED,wBAAgB,+BAA+B,IAAI,IAAI,CAgBtD"}
1
+ {"version":3,"file":"aws-client-agent-mcp.d.ts","sourceRoot":"","sources":["../../src/services/aws-client-agent-mcp.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAqB7C,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,sCAAsC;IACrD,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IACvC,OAAO,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,sCAAsC;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAaD,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;CAC3C;AAuBD,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAgLD,wBAAgB,+BAA+B,CAC7C,OAAO,GAAE,sCAA2C,GACnD,MAAM,GAAG,IAAI,CAoDf;AAgGD,wBAAgB,+BAA+B,CAC7C,OAAO,GAAE,sCAA2C,GACnD,kBAAkB,CAEpB;AAED,wBAAgB,gCAAgC,CAC9C,OAAO,GAAE,sCAA2C,GACnD,kBAAkB,CAsBpB;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,uBAAuB,GAC7B,kBAAkB,CAsBpB;AAED,wBAAgB,+BAA+B,IAAI,IAAI,CAgBtD"}
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { getRuntimeHomeDir, port, schedulerBaseUrl } from "../config.js";
6
6
  import { logger } from "../utils/logger.js";
7
- import { loadRuntimeBinding, normalizeSchedulerBaseUrl, } from "./runtime-binding.js";
7
+ import { loadRuntimeBinding } from "./runtime-binding.js";
8
8
  export const AWS_MCP_SERVER_NAME = "aws-mcp";
9
9
  const AWS_MCP_ALLOWED_ENV_KEYS = [
10
10
  "AWS_PROJECT_NAME",
@@ -233,8 +233,27 @@ function parseAwsClientAgentMcpArgs(raw) {
233
233
  logger.warn("[runtime-bridge] AWS_CLIENT_AGENT_MCP_ARGS 必须是 string[] JSON,已忽略");
234
234
  return [];
235
235
  }
236
+ function normalizeMcpSchedulerBaseUrl(value) {
237
+ const repaired = value.replace(/^((?:https?|wss?):\/\/(?:\[[^\]]+\]|[^/:?#]+):\d+):\d+(?=\/|$)/i, "$1");
238
+ try {
239
+ const url = new URL(repaired);
240
+ if (url.protocol === "ws:") {
241
+ url.protocol = "http:";
242
+ }
243
+ else if (url.protocol === "wss:") {
244
+ url.protocol = "https:";
245
+ }
246
+ else {
247
+ url.protocol = url.protocol.toLowerCase();
248
+ }
249
+ return url.origin;
250
+ }
251
+ catch {
252
+ return repaired.replace(/\/+$/, "");
253
+ }
254
+ }
236
255
  function toWebSocketUrl(baseUrl) {
237
- const url = new URL("/ws/agent", normalizeSchedulerBaseUrl(baseUrl) || baseUrl);
256
+ const url = new URL("/ws/agent", normalizeMcpSchedulerBaseUrl(baseUrl));
238
257
  if (url.protocol === "https:") {
239
258
  url.protocol = "wss:";
240
259
  }
@@ -244,7 +263,7 @@ function toWebSocketUrl(baseUrl) {
244
263
  return url.toString();
245
264
  }
246
265
  function toMcpHttpUrl(baseUrl) {
247
- return new URL("/mcp/call", normalizeSchedulerBaseUrl(baseUrl) || baseUrl).toString();
266
+ return new URL("/mcp/call", normalizeMcpSchedulerBaseUrl(baseUrl)).toString();
248
267
  }
249
268
  function resolveSchedulerBaseUrlForMcp() {
250
269
  const envSchedulerBaseUrl = String(process.env.AWS_RUNTIME_SCHEDULER_BASE_URL || "").trim();
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-launch-binding-queue.d.ts","sourceRoot":"","sources":["../../src/services/mcp-launch-binding-queue.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,uBAAwB,SAAQ,gBAAgB;IAC/D,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,4BAA4B;IAC3C,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAmDD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,qBAAqB,GAAG,gBAAgB,GAAG,IAAI,CA+B7F;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,uBAAuB,GAAG,IAAI,CA0C3G;AAED,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,OAAO,GAAG,MAAM,CAOpE;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAElD"}
1
+ {"version":3,"file":"mcp-launch-binding-queue.d.ts","sourceRoot":"","sources":["../../src/services/mcp-launch-binding-queue.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,uBAAwB,SAAQ,gBAAgB;IAC/D,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,4BAA4B;IAC3C,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AA2DD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,qBAAqB,GAAG,gBAAgB,GAAG,IAAI,CA+B7F;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,uBAAuB,GAAG,IAAI,CA0C3G;AAED,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,OAAO,GAAG,MAAM,CAOpE;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAElD"}
@@ -13,6 +13,13 @@ function normalizeWorkspacePath(workspacePath) {
13
13
  function normalizeOptionalUrl(value) {
14
14
  return String(value || "").trim();
15
15
  }
16
+ function canonicalizeSchedulerHostname(hostname) {
17
+ const normalized = hostname.toLowerCase();
18
+ if (normalized === "localhost" || normalized === "::1" || normalized === "[::1]") {
19
+ return "127.0.0.1";
20
+ }
21
+ return normalized;
22
+ }
16
23
  function normalizeSchedulerOrigin(serverUrl) {
17
24
  const raw = normalizeSchedulerBaseUrl(serverUrl) || String(serverUrl || "").trim();
18
25
  if (!raw) {
@@ -29,7 +36,7 @@ function normalizeSchedulerOrigin(serverUrl) {
29
36
  else {
30
37
  url.protocol = url.protocol.toLowerCase();
31
38
  }
32
- url.hostname = url.hostname.toLowerCase();
39
+ url.hostname = canonicalizeSchedulerHostname(url.hostname);
33
40
  return url.origin;
34
41
  }
35
42
  catch {
@@ -86,6 +86,17 @@ describe('mcp launch binding queue', () => {
86
86
  serverUrl: 'ws://127.0.0.1:8080/ws/agent',
87
87
  })?.schedulerBaseUrl).toBe('http://127.0.0.1:8080');
88
88
  });
89
+ it('matches localhost and 127.0.0.1 launch binding scheduler aliases', () => {
90
+ enqueueMcpLaunchBinding({
91
+ agentId: 'agent-a',
92
+ workspacePath: 'C:/repo/demo',
93
+ serverUrl: 'ws://localhost:8080/ws/agent',
94
+ });
95
+ expect(claimMcpLaunchBinding({
96
+ workspacePath: 'C:/repo/demo',
97
+ serverUrl: 'ws://127.0.0.1:8080/ws/agent',
98
+ })?.agentId).toBe('agent-a');
99
+ });
89
100
  it('falls back to the only same-workspace binding when scheduler URLs differ', () => {
90
101
  enqueueMcpLaunchBinding({
91
102
  agentId: 'agent-a',
@@ -1 +1 @@
1
- {"version":3,"file":"orphan-monitor.d.ts","sourceRoot":"","sources":["../../src/services/orphan-monitor.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAyBH;;GAEG;AACH,MAAM,MAAM,eAAe,GACvB,iBAAiB,GACjB,gBAAgB,GAChB,sBAAsB,GACtB,kBAAkB,GAClB,qBAAqB,GACrB,cAAc,CAAC;AAEnB;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,eAAe,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,cAAc,CAA2C;IACjE,OAAO,CAAC,OAAO,CAAkB;IAEjC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB5B;;OAEG;IACH,IAAI,IAAI,IAAI;IASZ;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA0E3B;;OAEG;YACW,oBAAoB;IA8BlC;;OAEG;YACW,iBAAiB;IAgC/B;;OAEG;YACW,eAAe;IA0B7B;;OAEG;YACW,mBAAmB;IAsBjC;;OAEG;YACW,iBAAiB;IA8C/B;;OAEG;IACH,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI;IAIrD;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI;IAOtD;;OAEG;IACH,OAAO,CAAC,IAAI;IAUZ;;OAEG;IACH,SAAS,IAAI;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,YAAY,EAAE,IAAI,GAAG,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE;CAO3E;AAKD;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,CAKhD;AAED;;GAEG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAGxD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAGxC"}
1
+ {"version":3,"file":"orphan-monitor.d.ts","sourceRoot":"","sources":["../../src/services/orphan-monitor.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA0BH;;GAEG;AACH,MAAM,MAAM,eAAe,GACvB,iBAAiB,GACjB,gBAAgB,GAChB,sBAAsB,GACtB,kBAAkB,GAClB,qBAAqB,GACrB,cAAc,CAAC;AAEnB;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,eAAe,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,cAAc,CAA2C;IACjE,OAAO,CAAC,OAAO,CAAkB;IAEjC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB5B;;OAEG;IACH,IAAI,IAAI,IAAI;IASZ;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA0E3B;;OAEG;YACW,oBAAoB;IAoClC;;OAEG;YACW,iBAAiB;IAwC/B;;OAEG;YACW,eAAe;IA0B7B;;OAEG;YACW,mBAAmB;IAsBjC;;OAEG;YACW,iBAAiB;IA8C/B;;OAEG;IACH,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI;IAIrD;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI;IAOtD;;OAEG;IACH,OAAO,CAAC,IAAI;IAUZ;;OAEG;IACH,SAAS,IAAI;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,YAAY,EAAE,IAAI,GAAG,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE;CAO3E;AAKD;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,CAKhD;AAED;;GAEG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAGxD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAGxC"}
@@ -8,6 +8,7 @@ import { loadPersistedSessions, savePersistedSessions } from './terminal-persist
8
8
  import { detectOrphanProcesses, checkAllSessionsHealth, terminateProcessTree, waitForProcessExit } from './process-detector.js';
9
9
  import { ORPHAN_SCAN_INTERVAL, ORPHAN_AUTO_CLEAN_MODE, ORPHAN_SCAN_ENABLED, schedulerBaseUrl } from '../config.js';
10
10
  import { getRuntimeAccessToken } from './runtime-binding.js';
11
+ import { shouldAutoCleanOrphanProcesses } from './runtime-lifecycle-policy.js';
11
12
  const logger = createLogger('orphan-monitor');
12
13
  /**
13
14
  * 孤儿进程监控器
@@ -126,11 +127,16 @@ export class OrphanMonitor {
126
127
  };
127
128
  this.emit(event);
128
129
  logger.warn(`检测到孤儿进程: agentId=${agentId}, pid=${pid}`);
129
- // 根据模式处理
130
+ // 根据模式处理。auto 模式也必须经过显式清理开关,避免重启恢复时误杀保留会话。
130
131
  switch (ORPHAN_AUTO_CLEAN_MODE) {
131
132
  case 'auto':
132
- // 自动终止
133
- await this.autoCleanOrphan(agentId, pid);
133
+ if (shouldAutoCleanOrphanProcesses()) {
134
+ await this.autoCleanOrphan(agentId, pid);
135
+ }
136
+ else {
137
+ logger.warn('auto 模式未启用破坏性清理开关,改为告警并保留孤儿进程');
138
+ await this.reportToScheduler(agentId, 'orphan_detected', { pid });
139
+ }
134
140
  break;
135
141
  case 'alert':
136
142
  // 告警并等待处理
@@ -167,11 +173,16 @@ export class OrphanMonitor {
167
173
  cpu: health.cpu,
168
174
  memoryMB: health.memoryMB,
169
175
  });
170
- // auto 模式下可以考虑终止异常进程
171
- if (ORPHAN_AUTO_CLEAN_MODE === 'auto' && health.status !== 'healthy') {
176
+ // auto 模式下也只有显式启用破坏性清理开关时才终止异常进程。
177
+ if (ORPHAN_AUTO_CLEAN_MODE === 'auto'
178
+ && health.status !== 'healthy'
179
+ && shouldAutoCleanOrphanProcesses()) {
172
180
  logger.warn(`auto 模式: 终止异常进程 ${health.pid} (${health.status})`);
173
181
  await this.autoCleanOrphan(agentId, health.pid);
174
182
  }
183
+ else if (ORPHAN_AUTO_CLEAN_MODE === 'auto' && health.status !== 'healthy') {
184
+ logger.warn(`auto 模式未启用破坏性清理开关,保留异常进程 ${health.pid} (${health.status})`);
185
+ }
175
186
  }
176
187
  /**
177
188
  * 自动清理孤儿进程
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-binding.d.ts","sourceRoot":"","sources":["../../src/services/runtime-binding.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AA2DF,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAqB5E;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED,wBAAgB,4BAA4B,IAAI,MAAM,CAErD;AAiCD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,OAAO,EACf,aAAa,EAAE,OAAO,GACrB,MAAM,CAYR;AAsCD,wBAAgB,4BAA4B,CAAC,KAAK,EAAE;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,MAAM,CAWT;AAED,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,OAAO,EACf,aAAa,EAAE,OAAO,GACrB,MAAM,GAAG,SAAS,CAUpB;AAED,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,OAAO,EACf,aAAa,EAAE,OAAO,GACrB,OAAO,CAeT;AAcD,wBAAgB,kBAAkB,IAAI,mBAAmB,CAgBxD;AAED,wBAAgB,4BAA4B,IAAI,IAAI,CAClD,mBAAmB,EACnB,WAAW,GAAG,aAAa,CAC5B,GAAG;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,CAWtB;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAG3C;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAcnE;AAED,wBAAgB,qBAAqB,CACnC,MAAM,CAAC,EAAE,OAAO,EAChB,aAAa,CAAC,EAAE,OAAO,GACtB,MAAM,GAAG,SAAS,CAyBpB;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAEjE;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,GAAG,mBAAmB,CAwCtB;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAkB1C"}
1
+ {"version":3,"file":"runtime-binding.d.ts","sourceRoot":"","sources":["../../src/services/runtime-binding.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAmEF,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAqB5E;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED,wBAAgB,4BAA4B,IAAI,MAAM,CAErD;AAiCD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,OAAO,EACf,aAAa,EAAE,OAAO,GACrB,MAAM,CAYR;AAsCD,wBAAgB,4BAA4B,CAAC,KAAK,EAAE;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,MAAM,CAWT;AAED,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,OAAO,EACf,aAAa,EAAE,OAAO,GACrB,MAAM,GAAG,SAAS,CAUpB;AAED,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,OAAO,EACf,aAAa,EAAE,OAAO,GACrB,OAAO,CAeT;AAcD,wBAAgB,kBAAkB,IAAI,mBAAmB,CAgBxD;AAED,wBAAgB,4BAA4B,IAAI,IAAI,CAClD,mBAAmB,EACnB,WAAW,GAAG,aAAa,CAC5B,GAAG;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,CAWtB;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAG3C;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAcnE;AAED,wBAAgB,qBAAqB,CACnC,MAAM,CAAC,EAAE,OAAO,EAChB,aAAa,CAAC,EAAE,OAAO,GACtB,MAAM,GAAG,SAAS,CAyBpB;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAEjE;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,GAAG,mBAAmB,CAwCtB;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAkB1C"}
@@ -49,6 +49,13 @@ function normalizeToken(token) {
49
49
  function repairDuplicatePort(raw) {
50
50
  return raw.replace(/^((?:https?|wss?):\/\/(?:\[[^\]]+\]|[^/:?#]+):\d+):\d+(?=\/|$)/i, "$1");
51
51
  }
52
+ function canonicalizeSchedulerHostname(hostname) {
53
+ const normalized = hostname.toLowerCase();
54
+ if (normalized === "localhost" || normalized === "::1" || normalized === "[::1]") {
55
+ return "127.0.0.1";
56
+ }
57
+ return normalized;
58
+ }
52
59
  export function normalizeSchedulerBaseUrl(value) {
53
60
  const raw = normalizeToken(value);
54
61
  if (!raw) {
@@ -66,7 +73,7 @@ export function normalizeSchedulerBaseUrl(value) {
66
73
  else {
67
74
  url.protocol = url.protocol.toLowerCase();
68
75
  }
69
- url.hostname = url.hostname.toLowerCase();
76
+ url.hostname = canonicalizeSchedulerHostname(url.hostname);
70
77
  return url.origin;
71
78
  }
72
79
  catch {
@@ -28,6 +28,10 @@ describe('runtime binding scheduler URL normalization', () => {
28
28
  expect(normalizeSchedulerBaseUrl('ws://127.0.0.1:8080/ws/agent')).toBe('http://127.0.0.1:8080');
29
29
  expect(normalizeSchedulerBaseUrl('wss://example.com/ws/agent')).toBe('https://example.com');
30
30
  });
31
+ it('canonicalizes loopback scheduler aliases to one origin', () => {
32
+ expect(normalizeSchedulerBaseUrl('ws://localhost:8080/ws/agent')).toBe('http://127.0.0.1:8080');
33
+ expect(normalizeSchedulerBaseUrl('http://localhost:8080')).toBe('http://127.0.0.1:8080');
34
+ });
31
35
  it("returns paired binding token only when requested scheduler URL matches", async () => {
32
36
  useRuntimeHome();
33
37
  const { saveRuntimeBinding } = await import("./runtime-binding.js");
@@ -60,4 +64,15 @@ describe('runtime binding scheduler URL normalization', () => {
60
64
  expect(getScopedRuntimeAccessToken("user-a", "http://scheduler.local:7380")).toBeUndefined();
61
65
  expect(getScopedRuntimeAccessToken("user-a", "http://scheduler.local:8080")).toBe("runtime-token-8080-123456");
62
66
  });
67
+ it("uses the same scoped runtime token for localhost and 127.0.0.1", () => {
68
+ useRuntimeHome();
69
+ const key = saveScopedRuntimeAccessToken({
70
+ userId: "user-a",
71
+ serverBaseUrl: "http://localhost:8080",
72
+ accessToken: "runtime-token-loopback-123456",
73
+ });
74
+ expect(buildRuntimeTokenKey("user-a", "ws://127.0.0.1:8080/ws/agent")).toBe(key);
75
+ expect(getScopedRuntimeAccessToken("user-a", "http://127.0.0.1:8080")).toBe("runtime-token-loopback-123456");
76
+ expect(getRuntimeAccessToken("user-a", "ws://localhost:8080/ws/agent")).toBe("runtime-token-loopback-123456");
77
+ });
63
78
  });
@@ -0,0 +1,26 @@
1
+ export interface GracefulShutdownPlan {
2
+ terminateSdkSessions: boolean;
3
+ closePtySessions: boolean;
4
+ terminateResidualProcesses: boolean;
5
+ unregisterInstance: boolean;
6
+ clearPersistedSessions: boolean;
7
+ }
8
+ /**
9
+ * Build shutdown side-effect policy from the requested restart mode.
10
+ * Main flow: explicit bridge shutdown cleans resources; scheduler restart keeps
11
+ * SDK/PTY processes and persisted metadata so the scheduler can restore state.
12
+ */
13
+ export declare function buildGracefulShutdownPlan(preserveSessions: boolean): GracefulShutdownPlan;
14
+ /**
15
+ * Decide whether startup may destructively clean persisted runtime sessions.
16
+ * Main flow: default bridge restart is non-destructive; operators can opt into
17
+ * legacy cleanup with AWS_BRIDGE_CLEAN_ORPHANS_ON_STARTUP=true.
18
+ */
19
+ export declare function shouldCleanupPersistedSessionsOnStartup(preserveSessions: boolean, env?: NodeJS.ProcessEnv): boolean;
20
+ /**
21
+ * Decide whether background orphan monitoring may terminate persisted sessions.
22
+ * Main flow: process termination is an explicit cleanup action, not a default
23
+ * monitoring behavior, so restart recovery cannot be undone by auto-clean mode.
24
+ */
25
+ export declare function shouldAutoCleanOrphanProcesses(env?: NodeJS.ProcessEnv): boolean;
26
+ //# sourceMappingURL=runtime-lifecycle-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-lifecycle-policy.d.ts","sourceRoot":"","sources":["../../src/services/runtime-lifecycle-policy.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,oBAAoB,EAAE,OAAO,CAAC;IAC9B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,0BAA0B,EAAE,OAAO,CAAC;IACpC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,sBAAsB,EAAE,OAAO,CAAC;CACjC;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,gBAAgB,EAAE,OAAO,GACxB,oBAAoB,CAUtB;AAED;;;;GAIG;AACH,wBAAgB,uCAAuC,CACrD,gBAAgB,EAAE,OAAO,EACzB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAMT;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAET"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Build shutdown side-effect policy from the requested restart mode.
3
+ * Main flow: explicit bridge shutdown cleans resources; scheduler restart keeps
4
+ * SDK/PTY processes and persisted metadata so the scheduler can restore state.
5
+ */
6
+ export function buildGracefulShutdownPlan(preserveSessions) {
7
+ const shouldCleanRuntimeState = !preserveSessions;
8
+ return {
9
+ terminateSdkSessions: shouldCleanRuntimeState,
10
+ closePtySessions: shouldCleanRuntimeState,
11
+ terminateResidualProcesses: shouldCleanRuntimeState,
12
+ unregisterInstance: shouldCleanRuntimeState,
13
+ clearPersistedSessions: shouldCleanRuntimeState,
14
+ };
15
+ }
16
+ /**
17
+ * Decide whether startup may destructively clean persisted runtime sessions.
18
+ * Main flow: default bridge restart is non-destructive; operators can opt into
19
+ * legacy cleanup with AWS_BRIDGE_CLEAN_ORPHANS_ON_STARTUP=true.
20
+ */
21
+ export function shouldCleanupPersistedSessionsOnStartup(preserveSessions, env = process.env) {
22
+ if (preserveSessions) {
23
+ return false;
24
+ }
25
+ return env.AWS_BRIDGE_CLEAN_ORPHANS_ON_STARTUP === "true";
26
+ }
27
+ /**
28
+ * Decide whether background orphan monitoring may terminate persisted sessions.
29
+ * Main flow: process termination is an explicit cleanup action, not a default
30
+ * monitoring behavior, so restart recovery cannot be undone by auto-clean mode.
31
+ */
32
+ export function shouldAutoCleanOrphanProcesses(env = process.env) {
33
+ return env.AWS_BRIDGE_CLEAN_ORPHANS_ON_STARTUP === "true";
34
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=runtime-lifecycle-policy.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-lifecycle-policy.test.d.ts","sourceRoot":"","sources":["../../src/services/runtime-lifecycle-policy.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildGracefulShutdownPlan, shouldAutoCleanOrphanProcesses, shouldCleanupPersistedSessionsOnStartup, } from "./runtime-lifecycle-policy.js";
3
+ describe("runtime lifecycle policy", () => {
4
+ it("keeps agent processes and persisted state during scheduler restart preservation", () => {
5
+ expect(buildGracefulShutdownPlan(true)).toEqual({
6
+ terminateSdkSessions: false,
7
+ closePtySessions: false,
8
+ terminateResidualProcesses: false,
9
+ unregisterInstance: false,
10
+ clearPersistedSessions: false,
11
+ });
12
+ });
13
+ it("cleans runtime state for explicit bridge shutdown", () => {
14
+ expect(buildGracefulShutdownPlan(false)).toEqual({
15
+ terminateSdkSessions: true,
16
+ closePtySessions: true,
17
+ terminateResidualProcesses: true,
18
+ unregisterInstance: true,
19
+ clearPersistedSessions: true,
20
+ });
21
+ });
22
+ it("does not destructively clean persisted sessions on startup unless explicitly enabled", () => {
23
+ expect(shouldCleanupPersistedSessionsOnStartup(false, {})).toBe(false);
24
+ expect(shouldCleanupPersistedSessionsOnStartup(false, {
25
+ AWS_BRIDGE_CLEAN_ORPHANS_ON_STARTUP: "true",
26
+ })).toBe(true);
27
+ expect(shouldCleanupPersistedSessionsOnStartup(true, {
28
+ AWS_BRIDGE_CLEAN_ORPHANS_ON_STARTUP: "true",
29
+ })).toBe(false);
30
+ });
31
+ it("does not let orphan monitor terminate preserved sessions unless cleanup is explicit", () => {
32
+ expect(shouldAutoCleanOrphanProcesses({})).toBe(false);
33
+ expect(shouldAutoCleanOrphanProcesses({
34
+ AWS_ORPHAN_AUTO_CLEAN_MODE: "auto",
35
+ })).toBe(false);
36
+ expect(shouldAutoCleanOrphanProcesses({
37
+ AWS_ORPHAN_AUTO_CLEAN_MODE: "auto",
38
+ AWS_BRIDGE_CLEAN_ORPHANS_ON_STARTUP: "true",
39
+ })).toBe(true);
40
+ });
41
+ });
@@ -37,6 +37,18 @@ export declare function updatePersistedSessionAutoCommands(agentId: string, comm
37
37
  idleInputAutoCommand: string;
38
38
  nonInputAutoCommand: string;
39
39
  }): Promise<boolean>;
40
+ /**
41
+ * 更新指定 SDK 会话的细粒度运行状态。
42
+ *
43
+ * 主流程:命中 sessionId 后仅替换 runtimeStatus/runtimeAction* 字段,保留会话 PID、命令和工作目录,
44
+ * 让 scheduler/server 重启后可以从 /runtime/sessions 恢复 Agent 卡片上的具体状态标签。
45
+ */
46
+ export declare function updatePersistedSessionRuntimeState(sessionId: string, runtimeState: {
47
+ runtimeStatus: string;
48
+ runtimeActionType?: string;
49
+ runtimeActionLabel?: string;
50
+ runtimeActionDetail?: string;
51
+ }): Promise<boolean>;
40
52
  /**
41
53
  * 移除持久化会话(从完整文件中删除,包括 stopped 状态的)
42
54
  *
@@ -1 +1 @@
1
- {"version":3,"file":"terminal-persistence.d.ts","sourceRoot":"","sources":["../../src/services/terminal-persistence.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA8BpD;;;;GAIG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAOzE;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAKvF;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAQrF;AAED;;;;GAIG;AACH,wBAAsB,kCAAkC,CACtD,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE;IAAE,oBAAoB,EAAE,MAAM,CAAC;IAAC,mBAAmB,EAAE,MAAM,CAAA;CAAE,GACtE,OAAO,CAAC,OAAO,CAAC,CAclB;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB7E;AAED;;;;;GAKG;AACH,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAKvC;AAED;;;;;GAKG;AACH,wBAAsB,+BAA+B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBpF"}
1
+ {"version":3,"file":"terminal-persistence.d.ts","sourceRoot":"","sources":["../../src/services/terminal-persistence.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA8BpD;;;;GAIG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAOzE;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAKvF;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAQrF;AAED;;;;GAIG;AACH,wBAAsB,kCAAkC,CACtD,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE;IAAE,oBAAoB,EAAE,MAAM,CAAC;IAAC,mBAAmB,EAAE,MAAM,CAAA;CAAE,GACtE,OAAO,CAAC,OAAO,CAAC,CAclB;AAED;;;;;GAKG;AACH,wBAAsB,kCAAkC,CACtD,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE;IACZ,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B,GACA,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB7E;AAED;;;;;GAKG;AACH,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAKvC;AAED;;;;;GAKG;AACH,wBAAsB,+BAA+B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBpF"}
@@ -96,6 +96,29 @@ export async function updatePersistedSessionAutoCommands(agentId, commands) {
96
96
  }));
97
97
  return updated;
98
98
  }
99
+ /**
100
+ * 更新指定 SDK 会话的细粒度运行状态。
101
+ *
102
+ * 主流程:命中 sessionId 后仅替换 runtimeStatus/runtimeAction* 字段,保留会话 PID、命令和工作目录,
103
+ * 让 scheduler/server 重启后可以从 /runtime/sessions 恢复 Agent 卡片上的具体状态标签。
104
+ */
105
+ export async function updatePersistedSessionRuntimeState(sessionId, runtimeState) {
106
+ let updated = false;
107
+ await updatePersistedSessions((sessions) => sessions.map((session) => {
108
+ if (session.sessionId !== sessionId) {
109
+ return session;
110
+ }
111
+ updated = true;
112
+ return {
113
+ ...session,
114
+ runtimeStatus: runtimeState.runtimeStatus,
115
+ runtimeActionType: runtimeState.runtimeActionType,
116
+ runtimeActionLabel: runtimeState.runtimeActionLabel,
117
+ runtimeActionDetail: runtimeState.runtimeActionDetail,
118
+ };
119
+ }));
120
+ return updated;
121
+ }
99
122
  /**
100
123
  * 移除持久化会话(从完整文件中删除,包括 stopped 状态的)
101
124
  *
@@ -117,6 +117,24 @@ describe('terminal-persistence service', () => {
117
117
  expect(sessions).toHaveLength(1);
118
118
  expect(sessions[0].sessionId).toBe('session-new');
119
119
  });
120
+ it('persists latest runtime action state for restart recovery', async () => {
121
+ await persistence.upsertPersistedSession(makeSession('session-1', 'agent-1'));
122
+ const updated = await persistence.updatePersistedSessionRuntimeState('session-1', {
123
+ runtimeStatus: 'tool_using',
124
+ runtimeActionType: 'mcp',
125
+ runtimeActionLabel: 'poll_message',
126
+ runtimeActionDetail: '等待群聊消息',
127
+ });
128
+ const sessions = await persistence.loadPersistedSessions();
129
+ expect(updated).toBe(true);
130
+ expect(sessions[0]).toMatchObject({
131
+ sessionId: 'session-1',
132
+ runtimeStatus: 'tool_using',
133
+ runtimeActionType: 'mcp',
134
+ runtimeActionLabel: 'poll_message',
135
+ runtimeActionDetail: '等待群聊消息',
136
+ });
137
+ });
120
138
  it('serializes concurrent upserts without losing sessions', async () => {
121
139
  await Promise.all([
122
140
  persistence.upsertPersistedSession(makeSession('session-1', 'agent-1')),
package/dist/types.d.ts CHANGED
@@ -230,6 +230,10 @@ export interface PersistedSession {
230
230
  command: string;
231
231
  startedAt: string;
232
232
  status: 'running' | 'stopped';
233
+ runtimeStatus?: string;
234
+ runtimeActionType?: string;
235
+ runtimeActionLabel?: string;
236
+ runtimeActionDetail?: string;
233
237
  pid?: number;
234
238
  providerSessionId?: string;
235
239
  mode?: 'sdk';
@@ -243,6 +247,10 @@ export interface SessionsListResponse {
243
247
  agentId: string;
244
248
  workspacePath: string;
245
249
  status: string;
250
+ runtimeStatus?: string;
251
+ runtimeActionType?: string;
252
+ runtimeActionLabel?: string;
253
+ runtimeActionDetail?: string;
246
254
  startedAt: string | null;
247
255
  }>;
248
256
  persisted: PersistedSession[];