aws-runtime-bridge 1.7.27 → 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.
- package/dist/adapter/ClaudeSdkAdapter.d.ts +5 -0
- package/dist/adapter/ClaudeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/ClaudeSdkAdapter.js +20 -6
- package/dist/adapter/ClaudeSdkAdapter.test.js +43 -0
- package/dist/adapter/CodexSdkAdapter.js +1 -1
- package/dist/adapter/OpencodeSdkAdapter.js +2 -2
- package/dist/adapter/types.d.ts +13 -4
- package/dist/adapter/types.d.ts.map +1 -1
- package/dist/index.js +101 -77
- package/dist/routes/sessions.js +16 -0
- package/dist/routes/terminal.d.ts +19 -1
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +62 -16
- package/dist/routes/terminal.test.js +25 -1
- package/dist/services/aws-client-agent-mcp.d.ts.map +1 -1
- package/dist/services/aws-client-agent-mcp.js +22 -3
- package/dist/services/mcp-launch-binding-queue.d.ts.map +1 -1
- package/dist/services/mcp-launch-binding-queue.js +8 -1
- package/dist/services/mcp-launch-binding-queue.test.js +11 -0
- package/dist/services/orphan-monitor.d.ts.map +1 -1
- package/dist/services/orphan-monitor.js +16 -5
- package/dist/services/runtime-binding.d.ts.map +1 -1
- package/dist/services/runtime-binding.js +8 -1
- package/dist/services/runtime-binding.test.js +15 -0
- package/dist/services/runtime-lifecycle-policy.d.ts +26 -0
- package/dist/services/runtime-lifecycle-policy.d.ts.map +1 -0
- package/dist/services/runtime-lifecycle-policy.js +34 -0
- package/dist/services/runtime-lifecycle-policy.test.d.ts +2 -0
- package/dist/services/runtime-lifecycle-policy.test.d.ts.map +1 -0
- package/dist/services/runtime-lifecycle-policy.test.js +41 -0
- package/dist/services/terminal-persistence.d.ts +12 -0
- package/dist/services/terminal-persistence.d.ts.map +1 -1
- package/dist/services/terminal-persistence.js +23 -0
- package/dist/services/terminal-persistence.test.js +18 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts +5 -0
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/agent-client.js +30 -3
- package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/agent-client.test.js +118 -0
- package/package/aws-client-agent-mcp/dist/agent-client.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/config.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/config.js +10 -1
- package/package/aws-client-agent-mcp/dist/config.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/config.test.js +10 -0
- package/package/aws-client-agent-mcp/dist/config.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.d.ts +5 -0
- package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.js +47 -3
- package/package/aws-client-agent-mcp/dist/http-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.test.js +344 -9
- package/package/aws-client-agent-mcp/dist/http-client.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +4 -3
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.js +28 -15
- package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +107 -11
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.js +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts +10 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +5 -0
- package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.js +26 -0
- package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
- package/package.json +1 -1
package/dist/routes/terminal.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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":"
|
|
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
|
|
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",
|
|
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",
|
|
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;
|
|
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
|
|
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;
|
|
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
|
-
|
|
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'
|
|
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;
|
|
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
|
|
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 @@
|
|
|
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[];
|