@muyichengshayu/promptx 0.2.9 → 0.2.10

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.10
4
+
5
+ - 修复 Claude Code 结果完成时偶发卡死的问题:当 Claude CLI 在 result 事件后延迟退出时,runner 会进入优雅退出等待,避免强制 kill 导致消息丢失。
6
+ - 完善 Kimi Code 执行过程展示:思考过程(thinking)不再被隐藏,可直接在执行面板中查看;工具调用(ReadFile / WriteFile / Shell 等)正确映射到前端分类(读取/写入/命令),便于分组展示。
7
+ - 补齐 Kimi Code 事件流完整性:新增 thread.started 事件发送,修复会话创建元信息泄漏到过程日志的问题。
8
+ - 健壮化 Kimi Code 参数解析:支持 arguments 既为字符串也可为对象,避免空参数展示为 `{}`。
9
+ - 代码变更弹窗轮次选择器日期精确到秒:select option 中显示完整时分秒,方便在密集 run 中快速定位目标轮次。
10
+
3
11
  ## 0.2.9
4
12
 
5
13
  - 接入 `Kimi Code CLI` 执行引擎:工作台 Agent 选择器新增 Kimi Code 选项,支持本地会话发现、线程复用、TodoList 过程展示与停止控制,与现有 Codex / Claude Code / OpenCode 并列为第四类 Agent。
@@ -21,6 +21,14 @@ import { createManagedSpawnOptions, forceStopChildProcess } from '../processCont
21
21
  const CLAUDE_CODE_BIN = process.env.CLAUDE_CODE_BIN || 'claude'
22
22
  const CLAUDE_DEFAULT_ARGS = ['--dangerously-skip-permissions']
23
23
  const RESOLVED_CLAUDE_CODE_BIN = resolveClaudeCodeBinary()
24
+ const CLAUDE_RESULT_EXIT_GRACE_MS = Math.max(
25
+ 0,
26
+ Number(process.env.PROMPTX_CLAUDE_RESULT_EXIT_GRACE_MS) || 3000
27
+ )
28
+ const CLAUDE_RESULT_FORCE_STOP_GRACE_MS = Math.max(
29
+ 200,
30
+ Number(process.env.PROMPTX_CLAUDE_RESULT_FORCE_STOP_GRACE_MS) || 1000
31
+ )
24
32
 
25
33
  function resolveClaudeCodeBinary() {
26
34
  if (process.platform !== 'win32') {
@@ -798,8 +806,60 @@ export function streamPromptToClaudeCodeSession(sessionInput, prompt, callbacks
798
806
  let finalSessionId = String(session.engineSessionId || session.engineThreadId || session.codexThreadId || '').trim()
799
807
  let fatalClaudeErrorMessage = ''
800
808
  let fatalClaudeErrorTriggered = false
809
+ let resultExitGraceTimer = null
810
+ let settled = false
811
+ let resolveResult = null
812
+ let rejectResult = null
801
813
  const normalizationState = createClaudeNormalizationState()
802
814
 
815
+ const clearResultExitGraceTimer = () => {
816
+ if (resultExitGraceTimer) {
817
+ clearTimeout(resultExitGraceTimer)
818
+ resultExitGraceTimer = null
819
+ }
820
+ }
821
+
822
+ const settleCompleted = (options = {}) => {
823
+ if (settled) {
824
+ return
825
+ }
826
+
827
+ settled = true
828
+ clearResultExitGraceTimer()
829
+ onEvent(createCompletedEnvelopeEvent(finalMessage))
830
+ resolveResult?.({
831
+ sessionId: session.id,
832
+ threadId: finalSessionId,
833
+ message: finalMessage,
834
+ })
835
+
836
+ if (options.stopChild && child.exitCode === null && child.signalCode === null) {
837
+ forceStopChildProcess(child, { graceMs: CLAUDE_RESULT_FORCE_STOP_GRACE_MS })
838
+ }
839
+ }
840
+
841
+ const settleError = (error) => {
842
+ if (settled) {
843
+ return
844
+ }
845
+
846
+ settled = true
847
+ clearResultExitGraceTimer()
848
+ rejectResult?.(error)
849
+ }
850
+
851
+ const scheduleResultExitGrace = () => {
852
+ if (settled || resultExitGraceTimer || CLAUDE_RESULT_EXIT_GRACE_MS <= 0) {
853
+ return
854
+ }
855
+
856
+ resultExitGraceTimer = setTimeout(() => {
857
+ resultExitGraceTimer = null
858
+ settleCompleted({ stopChild: true })
859
+ }, CLAUDE_RESULT_EXIT_GRACE_MS)
860
+ resultExitGraceTimer.unref?.()
861
+ }
862
+
803
863
  const rememberSessionId = (sessionId) => {
804
864
  const value = String(sessionId || '').trim()
805
865
  if (!value || value === finalSessionId) {
@@ -836,6 +896,7 @@ export function streamPromptToClaudeCodeSession(sessionInput, prompt, callbacks
836
896
 
837
897
  if (String(event?.type || '').trim().toLowerCase() === 'result') {
838
898
  finalMessage = extractClaudeResultText(event) || finalMessage
899
+ scheduleResultExitGrace()
839
900
  }
840
901
  }
841
902
 
@@ -857,11 +918,18 @@ export function streamPromptToClaudeCodeSession(sessionInput, prompt, callbacks
857
918
  })
858
919
 
859
920
  const result = new Promise((resolve, reject) => {
921
+ resolveResult = resolve
922
+ rejectResult = reject
923
+
860
924
  child.on('error', (error) => {
861
- reject(normalizeSpawnError(error))
925
+ settleError(normalizeSpawnError(error))
862
926
  })
863
927
 
864
928
  child.on('close', (code) => {
929
+ if (settled) {
930
+ return
931
+ }
932
+
865
933
  flushBufferedText(stdoutBuffer).forEach(emitClaudeJsonLine)
866
934
  flushBufferedText(stderrBuffer).forEach((line) => {
867
935
  lastStderrLine = line
@@ -870,17 +938,11 @@ export function streamPromptToClaudeCodeSession(sessionInput, prompt, callbacks
870
938
 
871
939
  if (code !== 0) {
872
940
  const detail = fatalClaudeErrorMessage || lastStderrLine || 'Claude Code 执行失败。'
873
- reject(new Error(detail))
941
+ settleError(new Error(detail))
874
942
  return
875
943
  }
876
944
 
877
- onEvent(createCompletedEnvelopeEvent(finalMessage))
878
-
879
- resolve({
880
- sessionId: session.id,
881
- threadId: finalSessionId,
882
- message: finalMessage,
883
- })
945
+ settleCompleted()
884
946
  })
885
947
  })
886
948
 
@@ -1,7 +1,14 @@
1
1
  import test from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
3
 
4
- import { createClaudeNormalizationState, normalizeClaudeEvents } from './claudeCodeRunner.js'
4
+ import {
5
+ createClaudeNormalizationState,
6
+ extractClaudeAssistantText,
7
+ extractClaudeResultText,
8
+ extractClaudeSessionId,
9
+ normalizeClaudeEvent,
10
+ normalizeClaudeEvents,
11
+ } from './claudeCodeRunner.js'
5
12
 
6
13
  test('runner claudeCodeRunner maps fatal auth api_retry to error event', () => {
7
14
  assert.deepEqual(
@@ -277,3 +284,184 @@ test('runner claudeCodeRunner maps TodoWrite into todo_list events', () => {
277
284
  }]
278
285
  )
279
286
  })
287
+
288
+ test('extractClaudeAssistantText joins nested text parts', () => {
289
+ const text = extractClaudeAssistantText({
290
+ type: 'assistant',
291
+ message: {
292
+ content: [
293
+ { type: 'text', text: '第一段' },
294
+ { type: 'text', text: '第二段' },
295
+ ],
296
+ },
297
+ })
298
+
299
+ assert.equal(text, '第一段\n第二段')
300
+ })
301
+
302
+ test('extractClaudeSessionId reads common session id fields', () => {
303
+ assert.equal(extractClaudeSessionId({ session_id: 'claude-session-1' }), 'claude-session-1')
304
+ assert.equal(extractClaudeSessionId({ result: { session_id: 'claude-session-2' } }), 'claude-session-2')
305
+ })
306
+
307
+ test('normalizeClaudeEvent maps assistant output to agent message', () => {
308
+ assert.deepEqual(
309
+ normalizeClaudeEvent({
310
+ type: 'assistant',
311
+ message: {
312
+ content: [{ type: 'text', text: '已完成修改' }],
313
+ },
314
+ }),
315
+ {
316
+ type: 'item.completed',
317
+ item: {
318
+ type: 'agent_message',
319
+ text: '已完成修改',
320
+ },
321
+ }
322
+ )
323
+ })
324
+
325
+ test('normalizeClaudeEvent maps result output to turn completion', () => {
326
+ assert.deepEqual(
327
+ normalizeClaudeEvent({
328
+ type: 'result',
329
+ result: '最终回复',
330
+ }),
331
+ {
332
+ type: 'turn.completed',
333
+ result: '最终回复',
334
+ }
335
+ )
336
+
337
+ assert.equal(extractClaudeResultText({ result: '最终回复' }), '最终回复')
338
+ })
339
+
340
+ test('normalizeClaudeEvents maps system init to thread start', () => {
341
+ assert.deepEqual(
342
+ normalizeClaudeEvents({
343
+ type: 'system',
344
+ subtype: 'init',
345
+ session_id: 'claude-session-init',
346
+ }),
347
+ [{
348
+ type: 'thread.started',
349
+ thread_id: 'claude-session-init',
350
+ }]
351
+ )
352
+ })
353
+
354
+ test('normalizeClaudeEvents maps thinking, tool use and text blocks', () => {
355
+ const state = createClaudeNormalizationState()
356
+
357
+ assert.deepEqual(
358
+ normalizeClaudeEvents({
359
+ type: 'assistant',
360
+ message: {
361
+ content: [
362
+ { type: 'thinking', thinking: '先看看目录结构' },
363
+ { type: 'tool_use', id: 'tool-1', name: 'Bash', input: { command: 'ls -1' } },
364
+ { type: 'text', text: '已查看完成' },
365
+ ],
366
+ },
367
+ }, state),
368
+ [
369
+ {
370
+ type: 'item.started',
371
+ item: {
372
+ type: 'reasoning',
373
+ text: '先看看目录结构',
374
+ },
375
+ },
376
+ {
377
+ type: 'item.started',
378
+ item: {
379
+ type: 'command_execution',
380
+ command: 'Bash: ls -1',
381
+ status: 'in_progress',
382
+ },
383
+ },
384
+ {
385
+ type: 'item.completed',
386
+ item: {
387
+ type: 'agent_message',
388
+ text: '已查看完成',
389
+ },
390
+ },
391
+ ]
392
+ )
393
+ })
394
+
395
+ test('normalizeClaudeEvents maps tool results back to remembered tool call', () => {
396
+ const state = createClaudeNormalizationState()
397
+ normalizeClaudeEvents({
398
+ type: 'assistant',
399
+ message: {
400
+ content: [
401
+ { type: 'tool_use', id: 'tool-2', name: 'Bash', input: { command: 'pwd' } },
402
+ ],
403
+ },
404
+ }, state)
405
+
406
+ assert.deepEqual(
407
+ normalizeClaudeEvents({
408
+ type: 'user',
409
+ message: {
410
+ content: [
411
+ { type: 'tool_result', tool_use_id: 'tool-2', content: '/tmp/demo', is_error: false },
412
+ ],
413
+ },
414
+ }, state),
415
+ [{
416
+ type: 'item.completed',
417
+ item: {
418
+ type: 'command_execution',
419
+ command: 'Bash: pwd',
420
+ status: 'completed',
421
+ exit_code: 0,
422
+ aggregated_output: '/tmp/demo',
423
+ },
424
+ }]
425
+ )
426
+ })
427
+
428
+ test('normalizeClaudeEvents stringifies structured tool results', () => {
429
+ const state = createClaudeNormalizationState()
430
+ normalizeClaudeEvents({
431
+ type: 'assistant',
432
+ message: {
433
+ content: [
434
+ { type: 'tool_use', id: 'tool-3', name: 'Read', input: { file_path: '/tmp/demo.txt' } },
435
+ ],
436
+ },
437
+ }, state)
438
+
439
+ assert.deepEqual(
440
+ normalizeClaudeEvents({
441
+ type: 'user',
442
+ message: {
443
+ content: [
444
+ {
445
+ type: 'tool_result',
446
+ tool_use_id: 'tool-3',
447
+ content: [
448
+ { type: 'text', text: '<path>/tmp/demo.txt</path>' },
449
+ { type: 'text', text: '<type>file</type>' },
450
+ ],
451
+ },
452
+ ],
453
+ },
454
+ },
455
+ state),
456
+ [{
457
+ type: 'item.completed',
458
+ item: {
459
+ type: 'command_execution',
460
+ command: 'Read: /tmp/demo.txt',
461
+ status: 'completed',
462
+ exit_code: 0,
463
+ aggregated_output: '<path>/tmp/demo.txt</path>\n<type>file</type>',
464
+ },
465
+ }]
466
+ )
467
+ })
@@ -162,30 +162,46 @@ function stringifyKimiToolResultContent(value) {
162
162
  }
163
163
  }
164
164
 
165
+ function mapKimiToolNameForDisplay(name = '') {
166
+ const normalized = String(name || '').trim().toLowerCase()
167
+ const aliasMap = {
168
+ readfile: 'read',
169
+ writefile: 'write',
170
+ editfile: 'edit',
171
+ search: 'search',
172
+ shell: 'shell',
173
+ settodolist: 'todo',
174
+ }
175
+ return aliasMap[normalized] || normalized || 'kimi tool'
176
+ }
177
+
165
178
  function buildKimiToolCommand(name = '', input = {}) {
166
- const toolName = String(name || 'Kimi tool').trim() || 'Kimi tool'
179
+ const displayName = mapKimiToolNameForDisplay(name)
167
180
  if (!input || typeof input !== 'object') {
168
- return toolName
181
+ return displayName
169
182
  }
170
183
 
171
184
  const command = String(input.command || '').trim()
172
185
  if (command) {
173
- return `${toolName}: ${command}`
186
+ return `${displayName}: ${command}`
174
187
  }
175
188
 
176
189
  const singleValueKeys = ['file_path', 'path', 'pattern', 'query', 'url', 'description']
177
190
  for (const key of singleValueKeys) {
178
191
  const value = String(input[key] || '').trim()
179
192
  if (value) {
180
- return `${toolName}: ${value}`
193
+ return `${displayName}: ${value}`
181
194
  }
182
195
  }
183
196
 
184
197
  try {
185
198
  const compact = JSON.stringify(input)
186
- return compact.length <= 240 ? `${toolName}: ${compact}` : `${toolName}: ${compact.slice(0, 237)}...`
199
+ if (compact === '{}') {
200
+ return displayName
201
+ }
202
+ return compact.length <= 240 ? `${displayName}: ${compact}` : `${displayName}: ${compact.slice(0, 237)}...`
187
203
  } catch {
188
- return toolName
204
+ return displayName
189
205
  }
190
206
  }
191
207
 
@@ -296,12 +312,18 @@ export function normalizeKimiEvents(event = {}, state = createKimiNormalizationS
296
312
  toolCalls.forEach((toolCall) => {
297
313
  const toolUseId = String(toolCall?.id || '').trim()
298
314
  const name = String(toolCall?.function?.name || toolCall?.name || 'Kimi tool').trim() || 'Kimi tool'
299
- const argsText = toolCall?.function?.arguments || toolCall?.arguments || '{}'
300
315
  let parsedArgs = {}
301
- try {
302
- parsedArgs = JSON.parse(argsText)
303
- } catch {
304
- parsedArgs = {}
316
+ const rawArgs = toolCall?.function?.arguments ?? toolCall?.arguments
317
+ if (rawArgs != null) {
318
+ if (typeof rawArgs === 'string') {
319
+ try {
320
+ parsedArgs = JSON.parse(rawArgs)
321
+ } catch {
322
+ parsedArgs = {}
323
+ }
324
+ } else if (typeof rawArgs === 'object') {
325
+ parsedArgs = rawArgs
326
+ }
305
327
  }
306
328
  const command = buildKimiToolCommand(name, parsedArgs)
307
329
  const isTodoTool = isKimiTodoToolName(name)
@@ -446,6 +468,7 @@ export function streamPromptToKimiCodeSession(sessionInput, prompt, callbacks =
446
468
 
447
469
  finalSessionId = value
448
470
  onThreadStarted(value)
471
+ onEvent(createAgentEventEnvelopeEvent(createThreadStartedEvent(value)))
449
472
  }
450
473
 
451
474
  const emitKimiJsonLine = (line) => {
@@ -1,7 +1,15 @@
1
1
  import test from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
3
 
4
- import { normalizeOpenCodeEvents } from './openCodeRunner.js'
4
+ import {
5
+ createOpenCodeNormalizationState,
6
+ extractOpenCodeErrorMessage,
7
+ extractOpenCodeSessionId,
8
+ extractOpenCodeText,
9
+ extractOpenCodeUsage,
10
+ normalizeOpenCodeEvent,
11
+ normalizeOpenCodeEvents,
12
+ } from './openCodeRunner.js'
5
13
 
6
14
  test('runner openCodeRunner maps sub-agent task tool_use to collaboration events', () => {
7
15
  assert.deepEqual(
@@ -71,3 +79,158 @@ test('runner openCodeRunner maps sub-agent task tool_use to collaboration events
71
79
  ]
72
80
  )
73
81
  })
82
+
83
+ test('extractOpenCodeSessionId reads common session id fields', () => {
84
+ assert.equal(extractOpenCodeSessionId({ sessionID: 'opencode-session-1' }), 'opencode-session-1')
85
+ assert.equal(extractOpenCodeSessionId({ sessionId: 'opencode-session-2' }), 'opencode-session-2')
86
+ })
87
+
88
+ test('extractOpenCodeText trims text payload', () => {
89
+ assert.equal(
90
+ extractOpenCodeText({
91
+ type: 'text',
92
+ part: {
93
+ type: 'text',
94
+ text: '\n\n已完成修改\n',
95
+ },
96
+ }),
97
+ '已完成修改'
98
+ )
99
+ })
100
+
101
+ test('normalizeOpenCodeEvent maps text output to agent message', () => {
102
+ assert.deepEqual(
103
+ normalizeOpenCodeEvent({
104
+ type: 'text',
105
+ part: {
106
+ type: 'text',
107
+ text: '已完成修改',
108
+ },
109
+ }),
110
+ {
111
+ type: 'item.completed',
112
+ item: {
113
+ type: 'agent_message',
114
+ text: '已完成修改',
115
+ },
116
+ }
117
+ )
118
+ })
119
+
120
+ test('normalizeOpenCodeEvents maps first step_start to turn.started only once', () => {
121
+ const state = createOpenCodeNormalizationState()
122
+
123
+ assert.deepEqual(
124
+ normalizeOpenCodeEvents({
125
+ type: 'step_start',
126
+ sessionID: 'ses_1',
127
+ }, state),
128
+ [{ type: 'turn.started' }]
129
+ )
130
+
131
+ assert.deepEqual(
132
+ normalizeOpenCodeEvents({
133
+ type: 'step_start',
134
+ sessionID: 'ses_1',
135
+ }, state),
136
+ []
137
+ )
138
+ })
139
+
140
+ test('normalizeOpenCodeEvents maps completed tool_use to command events', () => {
141
+ assert.deepEqual(
142
+ normalizeOpenCodeEvents({
143
+ type: 'tool_use',
144
+ sessionID: 'ses_2',
145
+ part: {
146
+ type: 'tool',
147
+ tool: 'read',
148
+ state: {
149
+ status: 'completed',
150
+ input: {
151
+ filePath: '/tmp/demo.txt',
152
+ },
153
+ output: '<path>/tmp/demo.txt</path>',
154
+ },
155
+ },
156
+ }),
157
+ [
158
+ {
159
+ type: 'item.started',
160
+ item: {
161
+ type: 'command_execution',
162
+ command: 'read: /tmp/demo.txt',
163
+ status: 'in_progress',
164
+ },
165
+ },
166
+ {
167
+ type: 'item.completed',
168
+ item: {
169
+ type: 'command_execution',
170
+ command: 'read: /tmp/demo.txt',
171
+ status: 'completed',
172
+ exit_code: 0,
173
+ aggregated_output: '<path>/tmp/demo.txt</path>',
174
+ },
175
+ },
176
+ ]
177
+ )
178
+ })
179
+
180
+ test('normalizeOpenCodeEvent maps step_finish stop to turn completion with usage', () => {
181
+ const event = normalizeOpenCodeEvent({
182
+ type: 'step_finish',
183
+ part: {
184
+ type: 'step-finish',
185
+ reason: 'stop',
186
+ tokens: {
187
+ input: 321,
188
+ output: 12,
189
+ cache: {
190
+ read: 256,
191
+ },
192
+ },
193
+ },
194
+ })
195
+
196
+ assert.deepEqual(event, {
197
+ type: 'turn.completed',
198
+ usage: {
199
+ input_tokens: 321,
200
+ output_tokens: 12,
201
+ cached_input_tokens: 256,
202
+ },
203
+ })
204
+
205
+ assert.deepEqual(extractOpenCodeUsage({
206
+ part: {
207
+ tokens: {
208
+ input: 321,
209
+ output: 12,
210
+ cache: {
211
+ read: 256,
212
+ },
213
+ },
214
+ },
215
+ }), {
216
+ input_tokens: 321,
217
+ output_tokens: 12,
218
+ cached_input_tokens: 256,
219
+ })
220
+ })
221
+
222
+ test('extractOpenCodeErrorMessage reads nested API errors', () => {
223
+ assert.equal(
224
+ extractOpenCodeErrorMessage({
225
+ type: 'error',
226
+ error: {
227
+ name: 'APIError',
228
+ data: {
229
+ message: 'openai_error',
230
+ responseBody: '{"error":{"message":"bad gateway"}}',
231
+ },
232
+ },
233
+ }),
234
+ 'openai_error'
235
+ )
236
+ })
@@ -154,6 +154,43 @@ process.stdout.write(JSON.stringify({
154
154
  return cmdPath
155
155
  }
156
156
 
157
+ function createHangingFakeClaudeBinary(tempDir) {
158
+ const scriptPath = path.join(tempDir, process.platform === 'win32' ? 'fake-claude-hang.js' : 'fake-claude-hang')
159
+ const script = `#!/usr/bin/env node
160
+ process.stdout.write(JSON.stringify({
161
+ type: 'system',
162
+ subtype: 'init',
163
+ session_id: 'thread-contract-1',
164
+ }) + '\\n')
165
+
166
+ process.stdout.write(JSON.stringify({
167
+ type: 'assistant',
168
+ message: {
169
+ content: [
170
+ { type: 'text', text: '已完成修改' },
171
+ ],
172
+ },
173
+ }) + '\\n')
174
+
175
+ process.stdout.write(JSON.stringify({
176
+ type: 'result',
177
+ result: '最终回复',
178
+ }) + '\\n')
179
+
180
+ setInterval(() => {}, 1000)
181
+ `
182
+
183
+ fs.writeFileSync(scriptPath, script, { mode: 0o755 })
184
+
185
+ if (process.platform !== 'win32') {
186
+ return scriptPath
187
+ }
188
+
189
+ const cmdPath = path.join(tempDir, 'fake-claude-hang.cmd')
190
+ fs.writeFileSync(cmdPath, '@echo off\r\nnode "%~dp0fake-claude-hang.js" %*\r\n')
191
+ return cmdPath
192
+ }
193
+
157
194
  function createFakeOpenCodeBinary(tempDir) {
158
195
  const scriptPath = path.join(tempDir, process.platform === 'win32' ? 'fake-opencode.js' : 'fake-opencode')
159
196
  const script = `#!/usr/bin/env node
@@ -380,3 +417,33 @@ test('Codex / Claude Code / OpenCode runner 会产出兼容的核心事件结构
380
417
  }
381
418
  )
382
419
  })
420
+
421
+ test('Claude Code runner 在 result 后进程不退出时会按 grace timeout 完成', async () => {
422
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-runner-claude-hang-'))
423
+ const fakeClaudeBin = createHangingFakeClaudeBinary(tempDir)
424
+
425
+ await withEnv(
426
+ {
427
+ CLAUDE_CODE_BIN: fakeClaudeBin,
428
+ PROMPTX_CLAUDE_RESULT_EXIT_GRACE_MS: '30',
429
+ PROMPTX_CLAUDE_RESULT_FORCE_STOP_GRACE_MS: '30',
430
+ },
431
+ async () => {
432
+ const { streamPromptToClaudeCodeSession } = await importFreshRunnerModules()
433
+ const result = await collectRunnerContractEvents(streamPromptToClaudeCodeSession)
434
+
435
+ assert.deepEqual(result.result, {
436
+ sessionId: 'session-1',
437
+ threadId: 'thread-contract-1',
438
+ message: '最终回复',
439
+ })
440
+ assertOrderedSubsequence(projectRunnerContractPhases(result.events), [
441
+ 'status',
442
+ 'thread.started',
443
+ 'agent_message',
444
+ 'turn.completed',
445
+ 'completed',
446
+ ])
447
+ }
448
+ )
449
+ })
@@ -461,7 +461,7 @@ test('runManager 会为长时间静默的 running run 补发可见进度事件',
461
461
  .filter((payload) => payload.type === 'status')
462
462
 
463
463
  assert.equal(
464
- statusEvents.some((payload) => String(payload.message || '').includes('最近暂无新的过程输出')),
464
+ statusEvents.some((payload) => payload.messageKey === 'runner.status.thinking'),
465
465
  true
466
466
  )
467
467