@winmatrix/supervisor 1.0.5
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/package.json +24 -0
- package/src/blocked-args-filter.mjs +95 -0
- package/src/claude-session-lib.mjs +487 -0
- package/src/deliverable-tracker.mjs +119 -0
- package/src/engine-driver-config.mjs +131 -0
- package/src/env-sanitizer.mjs +112 -0
- package/src/resume-degradation.mjs +117 -0
- package/src/run-agent-engine.mjs +1580 -0
- package/src/run-engine.mjs +192 -0
- package/src/token-usage.mjs +147 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D11.4: Deliverable 提取
|
|
3
|
+
*
|
|
4
|
+
* EngineFinalResult.output(text 字段)不应是全量事件文本的累加,
|
|
5
|
+
* 而应是最后一次工具调用之后的文本输出。
|
|
6
|
+
*
|
|
7
|
+
* 参考 Multica 的 acpDeliverableTracker 设计:
|
|
8
|
+
* - 每次 tool_call 事件后重置 buffer
|
|
9
|
+
* - 最后的 text 累积作为 deliverable
|
|
10
|
+
* - 无 tool_call 时全部 text 作为 deliverable
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Deliverable tracker 状态
|
|
15
|
+
* @typedef {Object} DeliverableState
|
|
16
|
+
* @property {string} currentBuffer - 当前累积的文本
|
|
17
|
+
* @property {boolean} hasToolCall - 是否遇到过工具调用
|
|
18
|
+
* @property {string} lastDeliverable - 上一个完整的 deliverable(最后一次 tool_call 后的文本)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 创建新的 deliverable tracker 状态
|
|
23
|
+
* @returns {DeliverableState}
|
|
24
|
+
*/
|
|
25
|
+
export function createDeliverableState() {
|
|
26
|
+
return {
|
|
27
|
+
currentBuffer: '',
|
|
28
|
+
hasToolCall: false,
|
|
29
|
+
lastDeliverable: '',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 处理一个 Engine 进度事件,更新 deliverable 状态
|
|
35
|
+
*
|
|
36
|
+
* @param {DeliverableState} state - 当前状态
|
|
37
|
+
* @param {Object} event - EngineProgressEvent
|
|
38
|
+
* @returns {DeliverableState} 更新后的状态
|
|
39
|
+
*/
|
|
40
|
+
export function updateDeliverable(state, event) {
|
|
41
|
+
if (!event || !event.type) return state;
|
|
42
|
+
|
|
43
|
+
switch (event.type) {
|
|
44
|
+
case 'content_delta':
|
|
45
|
+
// 文本增量 → 追加到 buffer
|
|
46
|
+
return {
|
|
47
|
+
...state,
|
|
48
|
+
currentBuffer: state.currentBuffer + (event.text || ''),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
case 'tool_call':
|
|
52
|
+
// 工具调用 → 当前 buffer 成为 lastDeliverable,重置 buffer
|
|
53
|
+
return {
|
|
54
|
+
...state,
|
|
55
|
+
hasToolCall: true,
|
|
56
|
+
lastDeliverable: state.currentBuffer,
|
|
57
|
+
currentBuffer: '',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
case 'thinking_delta':
|
|
61
|
+
case 'stderr_delta':
|
|
62
|
+
case 'artifact':
|
|
63
|
+
case 'usage':
|
|
64
|
+
case 'status':
|
|
65
|
+
case 'interaction_requested':
|
|
66
|
+
case 'interaction_resolved':
|
|
67
|
+
// 这些事件不影响 deliverable
|
|
68
|
+
return state;
|
|
69
|
+
|
|
70
|
+
case 'tool_result_delta':
|
|
71
|
+
// 工具结果不影响 deliverable(只关注 Agent 的文本输出)
|
|
72
|
+
return state;
|
|
73
|
+
|
|
74
|
+
case 'result':
|
|
75
|
+
// 终态事件,不影响 deliverable buffer
|
|
76
|
+
return state;
|
|
77
|
+
|
|
78
|
+
case 'error':
|
|
79
|
+
// 错误不影响 deliverable
|
|
80
|
+
return state;
|
|
81
|
+
|
|
82
|
+
default:
|
|
83
|
+
return state;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 从 deliverable 状态提取最终 deliverable 文本
|
|
89
|
+
*
|
|
90
|
+
* 规则:
|
|
91
|
+
* 1. 如果有 tool_call,deliverable = currentBuffer(最后一次 tool_call 后的文本)
|
|
92
|
+
* 2. 如果没有 tool_call,deliverable = currentBuffer(全部文本)
|
|
93
|
+
* 3. 如果 currentBuffer 为空(如最后一次事件是 tool_call 后无文本),返回空字符串
|
|
94
|
+
*
|
|
95
|
+
* @param {DeliverableState} state
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
export function extractDeliverable(state) {
|
|
99
|
+
// 无论是否有 tool_call,deliverable 始终是 currentBuffer
|
|
100
|
+
// 有 tool_call 时,currentBuffer 是最后一次 tool_call 后的文本
|
|
101
|
+
// 无 tool_call 时,currentBuffer 是全部文本
|
|
102
|
+
return state.currentBuffer;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 从事件数组中直接计算 deliverable(不依赖 tracker 状态)
|
|
107
|
+
* 用于从持久化的 events.jsonl 回溯计算
|
|
108
|
+
*
|
|
109
|
+
* @param {Object[]} events - EngineProgressEvent 数组
|
|
110
|
+
* @returns {string}
|
|
111
|
+
*/
|
|
112
|
+
export function computeDeliverableFromEvents(events) {
|
|
113
|
+
let state = createDeliverableState();
|
|
114
|
+
for (const envelope of events) {
|
|
115
|
+
const event = envelope.event || envelope;
|
|
116
|
+
state = updateDeliverable(state, event);
|
|
117
|
+
}
|
|
118
|
+
return extractDeliverable(state);
|
|
119
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine Driver 配置(design D11.2 / D11.3 / D11.4)
|
|
3
|
+
*
|
|
4
|
+
* 每种 Engine 声明:
|
|
5
|
+
* - resumeRejectedPatterns: resume 被拒绝时 Engine 输出的特征短语
|
|
6
|
+
* - blockedArgs: 受协议保护的 CLI 参数,用户 customArgs 中的匹配项将被过滤
|
|
7
|
+
* - gracefulShutdownTimeoutMs: 两阶段关闭 Phase 1 超时(stdin.Close() 后等待 Engine 自行退出)
|
|
8
|
+
*
|
|
9
|
+
* 参考 Multica 开源项目的 18 种 agent CLI 生产适配经验。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} EngineDriverConfig
|
|
14
|
+
* @property {string[]} resumeRejectedPatterns
|
|
15
|
+
* @property {string[]} blockedArgs
|
|
16
|
+
* @property {number} gracefulShutdownTimeoutMs
|
|
17
|
+
* @property {string} [phase2TimeoutMs]
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** @type {Record<string, EngineDriverConfig>} */
|
|
21
|
+
export const ENGINE_DRIVER_CONFIGS = {
|
|
22
|
+
'claude-code': {
|
|
23
|
+
/**
|
|
24
|
+
* D11.2: Claude Code resume 拒绝检测模式
|
|
25
|
+
* 来源: Claude Code CLI 输出的中英文错误消息
|
|
26
|
+
*/
|
|
27
|
+
resumeRejectedPatterns: [
|
|
28
|
+
'no conversation found',
|
|
29
|
+
'no saved session found',
|
|
30
|
+
'已绑定另外',
|
|
31
|
+
'bound to another account',
|
|
32
|
+
'bound to a different account',
|
|
33
|
+
'session not found',
|
|
34
|
+
'cannot resume session',
|
|
35
|
+
],
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* D11.3: Claude Code 受协议保护的 CLI 参数
|
|
39
|
+
* Supervisor 自身设置这些参数,不允许用户 customArgs 覆盖
|
|
40
|
+
*/
|
|
41
|
+
blockedArgs: [
|
|
42
|
+
'-p', '--print',
|
|
43
|
+
'--output-format',
|
|
44
|
+
'--input-format',
|
|
45
|
+
'--permission-mode',
|
|
46
|
+
'--mcp-config',
|
|
47
|
+
'--settings',
|
|
48
|
+
'--strict-mcp-config',
|
|
49
|
+
'--effort',
|
|
50
|
+
'--verbose',
|
|
51
|
+
'--include-partial-messages',
|
|
52
|
+
],
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* D11.2: Claude Code 对 stdin.Close() 会写状态文件再退出
|
|
56
|
+
* 给 10s grace period
|
|
57
|
+
*/
|
|
58
|
+
gracefulShutdownTimeoutMs: 10000,
|
|
59
|
+
phase2TimeoutMs: 5000,
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
'codex': {
|
|
63
|
+
resumeRejectedPatterns: [
|
|
64
|
+
'thread not found',
|
|
65
|
+
'cannot resume thread',
|
|
66
|
+
'session expired',
|
|
67
|
+
'invalid thread id',
|
|
68
|
+
],
|
|
69
|
+
|
|
70
|
+
blockedArgs: [
|
|
71
|
+
'app-server',
|
|
72
|
+
'--listen',
|
|
73
|
+
],
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Codex app-server 需要较长 drain 时间
|
|
77
|
+
*/
|
|
78
|
+
gracefulShutdownTimeoutMs: 10000,
|
|
79
|
+
phase2TimeoutMs: 5000,
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
'hermes': {
|
|
83
|
+
resumeRejectedPatterns: [
|
|
84
|
+
'session not found',
|
|
85
|
+
'cannot resume session',
|
|
86
|
+
'invalid session',
|
|
87
|
+
'session expired',
|
|
88
|
+
],
|
|
89
|
+
|
|
90
|
+
blockedArgs: [
|
|
91
|
+
'acp',
|
|
92
|
+
],
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Hermes 通常退出较快
|
|
96
|
+
*/
|
|
97
|
+
gracefulShutdownTimeoutMs: 5000,
|
|
98
|
+
phase2TimeoutMs: 5000,
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
'openclaw': {
|
|
102
|
+
resumeRejectedPatterns: [
|
|
103
|
+
'session not found',
|
|
104
|
+
'cannot resume session',
|
|
105
|
+
'invalid session',
|
|
106
|
+
'session expired',
|
|
107
|
+
],
|
|
108
|
+
|
|
109
|
+
blockedArgs: [
|
|
110
|
+
'agent-server',
|
|
111
|
+
'--listen',
|
|
112
|
+
],
|
|
113
|
+
|
|
114
|
+
gracefulShutdownTimeoutMs: 5000,
|
|
115
|
+
phase2TimeoutMs: 5000,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 获取 Engine 的 driver 配置,未知 engine 使用默认配置
|
|
121
|
+
* @param {string} engineId
|
|
122
|
+
* @returns {EngineDriverConfig}
|
|
123
|
+
*/
|
|
124
|
+
export function getEngineDriverConfig(engineId) {
|
|
125
|
+
return ENGINE_DRIVER_CONFIGS[engineId] ?? {
|
|
126
|
+
resumeRejectedPatterns: [],
|
|
127
|
+
blockedArgs: [],
|
|
128
|
+
gracefulShutdownTimeoutMs: 5000,
|
|
129
|
+
phase2TimeoutMs: 5000,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D11.3: 环境变量隔离
|
|
3
|
+
*
|
|
4
|
+
* Supervisor 启动 Engine 子进程前,过滤可能干扰 Engine 行为的环境变量。
|
|
5
|
+
* 参考 Multica 的 isFilteredChildEnvKey() 设计。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 需要过滤的环境变量前缀/名称模式
|
|
10
|
+
* 这些是 Supervisor 自身的运行时标记,不应泄漏到 Engine 子进程
|
|
11
|
+
*/
|
|
12
|
+
const FILTERED_ENV_PATTERNS = [
|
|
13
|
+
// Supervisor 自身的 Claude 运行时标记
|
|
14
|
+
/^CLAUDECODE_/i,
|
|
15
|
+
/^CLAUDE_CODE_/i,
|
|
16
|
+
|
|
17
|
+
// Supervisor/WinMatrix 内部标记(但保留白名单中的)
|
|
18
|
+
/^WINMATRIX_SUPERVISOR_/i,
|
|
19
|
+
|
|
20
|
+
// Multica 兼容(如果 daemon 环境泄漏)
|
|
21
|
+
/^MULTICA_/i,
|
|
22
|
+
|
|
23
|
+
// Agent Engine Gateway 内部标记(Supervisor 自身使用的,不传递给 Engine)
|
|
24
|
+
/^WM_AGENT_ENGINE_INTERNAL_/i,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 必须保留的环境变量前缀(即使匹配了过滤模式)
|
|
29
|
+
*/
|
|
30
|
+
const ENV_PASSTHROUGH_PATTERNS = [
|
|
31
|
+
// Engine 需要的 API 密钥和端点
|
|
32
|
+
/_API_KEY$/i,
|
|
33
|
+
/_BASE_URL$/i,
|
|
34
|
+
/_API_BASE$/i,
|
|
35
|
+
|
|
36
|
+
// 系统基础环境变量
|
|
37
|
+
/^PATH$/i,
|
|
38
|
+
/^HOME$/i,
|
|
39
|
+
/^USER$/i,
|
|
40
|
+
/^SHELL$/i,
|
|
41
|
+
/^LANG$/i,
|
|
42
|
+
/^LC_/i,
|
|
43
|
+
/^TERM$/i,
|
|
44
|
+
/^TZ$/i,
|
|
45
|
+
|
|
46
|
+
// Node.js 运行时
|
|
47
|
+
/^NODE_/i,
|
|
48
|
+
/^NPM_/i,
|
|
49
|
+
|
|
50
|
+
// Engine 需要的配置(白名单)
|
|
51
|
+
/^WIN_AGENT_ENGINE_/i, // Gateway Run 标记等
|
|
52
|
+
/^CLAUDE_/i, // Claude 配置(但 CLAUDECODE_ 和 CLAUDE_CODE_ 被过滤)
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 过滤 Supervisor 内部使用的临时标记,不传递给 Engine
|
|
57
|
+
*/
|
|
58
|
+
const FILTERED_EXACT_KEYS = new Set([
|
|
59
|
+
'WIN_AGENT_ENGINE_GATEWAY_RUN', // Supervisor 设置,但 Engine 不需要
|
|
60
|
+
'WM_INTERNAL_SUPERVISOR',
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 判断一个环境变量 key 是否应该被过滤
|
|
65
|
+
* @param {string} key
|
|
66
|
+
* @returns {boolean}
|
|
67
|
+
*/
|
|
68
|
+
export function isFilteredEnvKey(key) {
|
|
69
|
+
// 精确匹配的过滤项(优先级最高)
|
|
70
|
+
if (FILTERED_EXACT_KEYS.has(key)) return true;
|
|
71
|
+
|
|
72
|
+
// 先检查是否匹配过滤模式(过滤优先于白名单)
|
|
73
|
+
// 这样 CLAUDE_CODE_ENTRYPOINT 被过滤,即使 CLAUDE_ 在白名单中
|
|
74
|
+
for (const pattern of FILTERED_ENV_PATTERNS) {
|
|
75
|
+
if (pattern.test(key)) return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 再检查白名单(未被过滤的保留)
|
|
79
|
+
for (const pattern of ENV_PASSTHROUGH_PATTERNS) {
|
|
80
|
+
if (pattern.test(key)) return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 从 process.env 构建传递给 Engine 子进程的清洁环境变量
|
|
88
|
+
* @param {Record<string, string>} [overrides] - 需要额外设置的环境变量
|
|
89
|
+
* @returns {Record<string, string>}
|
|
90
|
+
*/
|
|
91
|
+
export function sanitizeEnv(overrides = {}) {
|
|
92
|
+
const clean = {};
|
|
93
|
+
|
|
94
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
95
|
+
if (value !== undefined && !isFilteredEnvKey(key)) {
|
|
96
|
+
clean[key] = value;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 应用 overrides
|
|
101
|
+
Object.assign(clean, overrides);
|
|
102
|
+
|
|
103
|
+
return clean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 获取被过滤的环境变量 key 列表(用于审计日志)
|
|
108
|
+
* @returns {string[]}
|
|
109
|
+
*/
|
|
110
|
+
export function getFilteredEnvKeys() {
|
|
111
|
+
return Object.keys(process.env).filter(isFilteredEnvKey);
|
|
112
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D11.2: Resume 降级链
|
|
3
|
+
*
|
|
4
|
+
* 当 EngineSessionBinding.mode='resume' 时,Supervisor 必须处理
|
|
5
|
+
* resume 被 Engine 拒绝的情况。降级流程:
|
|
6
|
+
*
|
|
7
|
+
* 1. 检测到 resume rejected(短语匹配 / 结构化错误)
|
|
8
|
+
* 2. 标记 run 为 resume_degraded
|
|
9
|
+
* 3. 清除 resume sessionRef,改为 ephemeral mode
|
|
10
|
+
* 4. 在 prompt 尾部注入 continuity notice
|
|
11
|
+
* 5. 上报 resume_degraded 事件
|
|
12
|
+
*
|
|
13
|
+
* 降级只发生一次,不允许链式降级。
|
|
14
|
+
*
|
|
15
|
+
* 参考 Multica 的 resumeRejectedPhrases + fresh session 重试设计。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { getEngineDriverConfig } from './engine-driver-config.mjs';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Continuity notice 模板 — 注入到降级后 prompt 尾部
|
|
22
|
+
*/
|
|
23
|
+
const CONTINUITY_NOTICE = [
|
|
24
|
+
'',
|
|
25
|
+
'---',
|
|
26
|
+
'[System: 上一次会话上下文不可恢复,已从新会话开始。请基于当前信息继续工作。如果之前的上下文对当前任务重要,请明确说明。]',
|
|
27
|
+
].join('\n');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 检测 Engine 输出是否表明 resume 被拒绝
|
|
31
|
+
*
|
|
32
|
+
* @param {string} engineId
|
|
33
|
+
* @param {string} output - Engine 的 stdout/stderr 累积输出
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
export function isResumeRejected(engineId, output) {
|
|
37
|
+
const config = getEngineDriverConfig(engineId);
|
|
38
|
+
const patterns = config.resumeRejectedPatterns;
|
|
39
|
+
|
|
40
|
+
if (!patterns || patterns.length === 0) return false;
|
|
41
|
+
if (!output) return false;
|
|
42
|
+
|
|
43
|
+
const lowerOutput = output.toLowerCase();
|
|
44
|
+
return patterns.some(pattern => lowerOutput.includes(pattern.toLowerCase()));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 检测 Engine stderr 是否表明 resume 被拒绝
|
|
49
|
+
* (某些 Engine 把错误信息写到 stderr)
|
|
50
|
+
*
|
|
51
|
+
* @param {string} engineId
|
|
52
|
+
* @param {string} stderr - Engine 的 stderr 累积输出
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
export function isResumeRejectedInStderr(engineId, stderr) {
|
|
56
|
+
return isResumeRejected(engineId, stderr);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 生成降级后的 continuity notice
|
|
61
|
+
* 追加到 prompt 尾部
|
|
62
|
+
*
|
|
63
|
+
* @param {Object} [options]
|
|
64
|
+
* @param {string} [options.originalSessionId] - 被拒绝的原 session ID
|
|
65
|
+
* @param {string} [options.reason] - 拒绝原因
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
export function buildContinuityNotice(options = {}) {
|
|
69
|
+
let notice = CONTINUITY_NOTICE;
|
|
70
|
+
|
|
71
|
+
if (options.originalSessionId) {
|
|
72
|
+
notice += `\n[原会话 ID: ${options.originalSessionId},无法恢复]`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (options.reason) {
|
|
76
|
+
notice += `\n[原因: ${options.reason}]`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return notice;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 构建 resume 降级事件
|
|
84
|
+
* 用于上报到 events.jsonl
|
|
85
|
+
*
|
|
86
|
+
* @param {Object} options
|
|
87
|
+
* @param {string} options.originalSessionId
|
|
88
|
+
* @param {string} [options.reason]
|
|
89
|
+
* @param {string} [options.detectedPattern] - 匹配到的拒绝模式
|
|
90
|
+
* @returns {Object} EngineProgressEvent
|
|
91
|
+
*/
|
|
92
|
+
export function buildResumeDegradedEvent(options) {
|
|
93
|
+
return {
|
|
94
|
+
type: 'status',
|
|
95
|
+
text: `resume_degraded: session ${options.originalSessionId} rejected, falling back to new session`,
|
|
96
|
+
metadata: {
|
|
97
|
+
resumeDegraded: true,
|
|
98
|
+
originalSessionId: options.originalSessionId,
|
|
99
|
+
reason: options.reason || 'resume_rejected',
|
|
100
|
+
detectedPattern: options.detectedPattern || null,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 检测 Engine 退出是否因为 resume 失败
|
|
107
|
+
* (exit code 非零 + stderr 包含拒绝模式)
|
|
108
|
+
*
|
|
109
|
+
* @param {string} engineId
|
|
110
|
+
* @param {number|null} exitCode
|
|
111
|
+
* @param {string} stderr
|
|
112
|
+
* @returns {boolean}
|
|
113
|
+
*/
|
|
114
|
+
export function isResumeFailureOnExit(engineId, exitCode, stderr) {
|
|
115
|
+
if (exitCode === 0) return false;
|
|
116
|
+
return isResumeRejectedInStderr(engineId, stderr);
|
|
117
|
+
}
|