@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.
@@ -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
+ }