mixdog 0.9.36 → 0.9.38

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 (115) hide show
  1. package/package.json +3 -2
  2. package/scripts/abort-recovery-test.mjs +118 -0
  3. package/scripts/compact-smoke.mjs +7 -7
  4. package/scripts/dispatch-persist-recovery-test.mjs +141 -0
  5. package/scripts/explore-bench-tmp.mjs +19 -0
  6. package/scripts/explore-bench.mjs +101 -14
  7. package/scripts/explore-prompt-policy-test.mjs +31 -0
  8. package/scripts/ingest-pure-conversation-smoke.mjs +11 -11
  9. package/scripts/notify-completion-mirror-test.mjs +73 -0
  10. package/scripts/openai-ws-early-settle-test.mjs +176 -0
  11. package/scripts/output-style-smoke.mjs +17 -17
  12. package/scripts/provider-toolcall-test.mjs +333 -14
  13. package/scripts/recall-bench-cases.json +3 -3
  14. package/scripts/recall-bench.mjs +1 -1
  15. package/scripts/recall-quality-cases.json +4 -4
  16. package/scripts/recall-usecase-cases.json +2 -2
  17. package/scripts/session-bench.mjs +13 -13
  18. package/scripts/session-sweep.mjs +266 -0
  19. package/scripts/steering-drain-buckets-test.mjs +72 -11
  20. package/scripts/submit-commandbusy-race-test.mjs +114 -0
  21. package/scripts/tool-smoke.mjs +83 -10
  22. package/src/app.mjs +2 -1
  23. package/src/defaults/cycle3-review-prompt.md +9 -0
  24. package/src/defaults/skills/setup/SKILL.md +93 -293
  25. package/src/lib/rules-builder.cjs +3 -2
  26. package/src/output-styles/default.md +2 -2
  27. package/src/output-styles/extreme-minimal.md +1 -1
  28. package/src/output-styles/minimal.md +1 -1
  29. package/src/output-styles/simple.md +2 -3
  30. package/src/rules/agent/30-explorer.md +40 -14
  31. package/src/rules/lead/01-general.md +4 -0
  32. package/src/rules/shared/01-tool.md +7 -3
  33. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +15 -3
  34. package/src/runtime/agent/orchestrator/config.mjs +1 -1
  35. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +31 -8
  36. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +13 -7
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +6 -6
  38. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
  39. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +35 -5
  40. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +27 -0
  41. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +36 -37
  42. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +2 -2
  43. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +72 -1
  44. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +26 -3
  45. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -28
  46. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +18 -15
  47. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +5 -2
  48. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +119 -3
  49. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +113 -2
  50. package/src/runtime/agent/orchestrator/providers/registry.mjs +44 -5
  51. package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +78 -0
  52. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +57 -31
  53. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +8 -6
  54. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +28 -2
  55. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +1 -3
  56. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +15 -2
  57. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +30 -55
  58. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -2
  59. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +24 -16
  60. package/src/runtime/agent/orchestrator/session/result-classification.mjs +2 -0
  61. package/src/runtime/agent/orchestrator/session/store.mjs +116 -21
  62. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -2
  63. package/src/runtime/agent/orchestrator/stall-policy.mjs +55 -0
  64. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +35 -20
  65. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -9
  66. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +77 -31
  67. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +25 -3
  68. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +9 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +73 -25
  70. package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -1
  71. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +17 -7
  72. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -12
  73. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +5 -0
  74. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +56 -6
  75. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  76. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -0
  77. package/src/runtime/channels/lib/worker-main.mjs +5 -0
  78. package/src/runtime/memory/lib/core-memory-store.mjs +104 -0
  79. package/src/runtime/memory/lib/ko-morph.mjs +1 -1
  80. package/src/runtime/memory/lib/memory-cycle3.mjs +64 -4
  81. package/src/runtime/memory/lib/memory-text-utils.mjs +4 -4
  82. package/src/runtime/memory/lib/recall-format.mjs +3 -3
  83. package/src/runtime/shared/child-spawn-gate.mjs +15 -0
  84. package/src/runtime/shared/config.mjs +98 -12
  85. package/src/runtime/shared/process-listener-headroom.mjs +12 -0
  86. package/src/session-runtime/cwd-plugins.mjs +6 -3
  87. package/src/session-runtime/model-route-api.mjs +50 -2
  88. package/src/session-runtime/provider-auth-api.mjs +8 -1
  89. package/src/session-runtime/provider-models.mjs +3 -3
  90. package/src/session-runtime/resource-api.mjs +22 -0
  91. package/src/session-runtime/runtime-core.mjs +56 -10
  92. package/src/session-runtime/settings-api.mjs +11 -2
  93. package/src/session-runtime/warmup-schedulers.mjs +7 -1
  94. package/src/standalone/agent-tool.mjs +15 -9
  95. package/src/standalone/explore-tool.mjs +4 -1
  96. package/src/tui/App.jsx +6 -5
  97. package/src/tui/app/transcript-window.mjs +60 -10
  98. package/src/tui/app/use-transcript-window.mjs +2 -1
  99. package/src/tui/components/ContextPanel.jsx +4 -1
  100. package/src/tui/components/ToolExecution.jsx +15 -12
  101. package/src/tui/components/TranscriptItem.jsx +1 -1
  102. package/src/tui/components/tool-execution/surface-detail.mjs +8 -8
  103. package/src/tui/dist/index.mjs +482 -151
  104. package/src/tui/engine/agent-job-feed.mjs +36 -6
  105. package/src/tui/engine/notification-plan.mjs +3 -3
  106. package/src/tui/engine/queue-helpers.mjs +8 -0
  107. package/src/tui/engine/render-timing.mjs +104 -8
  108. package/src/tui/engine/session-api.mjs +58 -3
  109. package/src/tui/engine/session-flow.mjs +93 -29
  110. package/src/tui/engine/tool-card-results.mjs +28 -13
  111. package/src/tui/engine/turn.mjs +238 -157
  112. package/src/tui/engine.mjs +17 -8
  113. package/src/tui/index.jsx +10 -1
  114. package/src/workflows/default/WORKFLOW.md +8 -4
  115. package/src/workflows/solo/WORKFLOW.md +8 -4
@@ -62,6 +62,8 @@ import {
62
62
  } from '../src/runtime/agent/orchestrator/providers/anthropic-effort.mjs';
63
63
  import { buildAnthropicBetaHeaders } from '../src/runtime/agent/orchestrator/providers/anthropic-betas.mjs';
64
64
  import { PATCH_TOOL_DEFS } from '../src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs';
65
+ import { sendViaHttpSse } from '../src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs';
66
+ import { buildRequestBody as buildOpenAIOAuthRequestBody } from '../src/runtime/agent/orchestrator/providers/openai-oauth.mjs';
65
67
 
66
68
  // --- Helpers ---------------------------------------------------------------
67
69
 
@@ -100,6 +102,33 @@ function compatResponsesEventStream(events) {
100
102
  };
101
103
  }
102
104
 
105
+ // Minimal 200-OK Response-like shape for the HTTP/SSE Responses path: frames
106
+ // each event as `event:<type>\ndata:<json>\n\n`, delivered synchronously so the
107
+ // semantic-idle watchdog never arms during the test.
108
+ function httpSseResponse(events) {
109
+ const encoder = new TextEncoder();
110
+ const chunks = events.map((e) => encoder.encode(`event: ${e.type}\ndata: ${JSON.stringify(e)}\n\n`));
111
+ let i = 0;
112
+ return {
113
+ status: 200,
114
+ ok: true,
115
+ headers: new Map(),
116
+ body: {
117
+ getReader() {
118
+ return {
119
+ read() {
120
+ return i < chunks.length
121
+ ? Promise.resolve({ done: false, value: chunks[i++] })
122
+ : Promise.resolve({ done: true, value: undefined });
123
+ },
124
+ cancel() { return Promise.resolve(); },
125
+ releaseLock() {},
126
+ };
127
+ },
128
+ },
129
+ };
130
+ }
131
+
103
132
  // === 1. openai-compat ======================================================
104
133
  // Chat path: parseToolCalls(choice, label) openai-compat.mjs:957
105
134
  // Responses path: parseResponsesToolCalls(response,...) openai-compat.mjs:972
@@ -220,6 +249,126 @@ test('openai-compat/xai Responses stream: response.completed salvages deferred f
220
249
  assert.deepEqual(captured, [{ id: 'fc_1', name: 'read', arguments: { path: 'a' } }]);
221
250
  });
222
251
 
252
+ // Missing-terminal + partial-tool gate (shared tool-stream-state.mjs tracker):
253
+ // a Responses stream that streams a CUSTOM tool call's input then truncates
254
+ // before response.output_item.done / response.completed. The call never lands
255
+ // in pendingCalls or toolCalls, so the truncation error must be gated as
256
+ // pendingToolUse via the shared active tool-item tracker (activeToolItems),
257
+ // NOT accepted as a text-only partial-final that would drop the in-flight tool.
258
+ test('openai-compat/xai Responses stream: mid custom-tool-input truncation is gated pendingToolUse via shared tracker', async () => {
259
+ const rejected = await consumeCompatResponsesStream(compatResponsesEventStream([
260
+ { type: 'response.created', response: { id: 'resp_ct', model: 'grok' } },
261
+ { type: 'response.output_text.delta', delta: 'partial ' },
262
+ { type: 'response.output_item.added', item: { type: 'custom_tool_call', id: 'ct_item_1' } },
263
+ { type: 'response.custom_tool_call_input.delta', item_id: 'ct_item_1', delta: '{"x":' },
264
+ // stream truncates here: no output_item.done, no response.completed.
265
+ ]), {
266
+ label: 'test',
267
+ parseResponsesToolCalls: compatParseResponsesToolCalls,
268
+ responseOutputText: () => '',
269
+ onTextDelta: () => {},
270
+ }).then(() => null, (e) => e);
271
+ assert.ok(rejected, 'expected the truncated stream to reject');
272
+ assert.equal(rejected.streamStalled, true);
273
+ // The in-flight custom tool must gate partial-final success even though it
274
+ // never reached pendingCalls/toolCalls — the active tracker carries it.
275
+ assert.equal(rejected.pendingToolUse, true);
276
+ assert.equal(rejected.partialContent, 'partial ');
277
+ });
278
+
279
+ // Precision half of the same tracker: once the tool call fully COMPLETES
280
+ // (output_item.done clears the active item) and text keeps streaming, a later
281
+ // truncation is a plain text-only partial — the tracker having been cleared is
282
+ // what lets pendingToolUse fall back to the real emit/pending state.
283
+ test('openai-compat/xai Responses stream: completed tool then trailing-text truncation clears active tracker', async () => {
284
+ let emitted = 0;
285
+ const rejected = await consumeCompatResponsesStream(compatResponsesEventStream([
286
+ { type: 'response.created', response: { id: 'resp_done', model: 'grok' } },
287
+ { type: 'response.output_item.added', item: { type: 'custom_tool_call', id: 'ct_done_1' } },
288
+ { type: 'response.custom_tool_call_input.delta', item_id: 'ct_done_1', delta: '{"x":1}' },
289
+ { type: 'response.output_item.done', item: { type: 'custom_tool_call', id: 'ct_done_1', name: 'load_tool', input: '{"x":1}' } },
290
+ { type: 'response.output_text.delta', delta: 'after tool ' },
291
+ // truncates before response.completed.
292
+ ]), {
293
+ label: 'test',
294
+ parseResponsesToolCalls: compatParseResponsesToolCalls,
295
+ responseOutputText: () => '',
296
+ onTextDelta: () => {},
297
+ onToolCall: () => { emitted += 1; },
298
+ }).then(() => null, (e) => e);
299
+ assert.ok(rejected, 'expected the truncated stream to reject');
300
+ assert.equal(rejected.streamStalled, true);
301
+ // A tool WAS emitted this turn, so the turn is still unsafe/tool-bearing —
302
+ // pendingToolUse stays true off the emit state, not a stale active latch.
303
+ assert.ok(emitted >= 1);
304
+ assert.equal(rejected.pendingToolUse, true);
305
+ });
306
+
307
+ // Reviewer fix: function output_item.done must delete the pendingCalls itemId
308
+ // before recomputing toolInFlight — otherwise a fully-completed function call
309
+ // keeps pendingCalls.size > 0 forever, and a later max-output cutoff on trailing
310
+ // text is misclassified as a truncated tool-in-flight stall instead of a clean
311
+ // length completion.
312
+ test('openai-compat/xai Responses stream: completed function call clears pendingCalls so max-output cutoff is clean length', async () => {
313
+ const out = await consumeCompatResponsesStream(compatResponsesEventStream([
314
+ { type: 'response.created', response: { id: 'resp_len', model: 'grok' } },
315
+ { type: 'response.output_item.added', item: { type: 'function_call', id: 'fi_len', name: 'read', call_id: 'fc_len' } },
316
+ { type: 'response.function_call_arguments.done', item_id: 'fi_len', arguments: '{"path":"a"}' },
317
+ { type: 'response.output_item.done', item: { type: 'function_call', id: 'fi_len', call_id: 'fc_len', name: 'read', arguments: '{"path":"a"}' } },
318
+ { type: 'response.incomplete', response: { incomplete_details: { reason: 'max_output_tokens' } } },
319
+ ]), {
320
+ label: 'test',
321
+ parseResponsesToolCalls: compatParseResponsesToolCalls,
322
+ responseOutputText: () => '',
323
+ });
324
+ // The completed call drained pendingCalls + the active tracker, so the
325
+ // max-output cutoff is a clean length truncation (no in-flight-tool stall).
326
+ assert.equal(out.stopReason, 'length');
327
+ assert.deepEqual(out.toolCalls, [{ id: 'fc_len', name: 'read', arguments: { path: 'a' } }]);
328
+ });
329
+
330
+ // Reviewer fix (HTTP/SSE): a max_output_tokens cutoff while a function call is
331
+ // still in flight (added + partial args, no output_item.done) must NOT mark a
332
+ // clean length completion — the tool arguments were truncated. Mirror compat:
333
+ // throw a stream-stalled pendingToolUse error so the loop gates/retries.
334
+ test('openai-oauth HTTP/SSE Responses: max-output cutoff with function call in flight → stream-stalled pendingToolUse', async () => {
335
+ const rejected = await sendViaHttpSse({
336
+ auth: { type: 'openai-direct', apiKey: 'k' },
337
+ body: { model: 'gpt', tools: [] },
338
+ useModel: 'gpt',
339
+ fetchFn: async () => httpSseResponse([
340
+ { type: 'response.created', response: { id: 'r', model: 'gpt' } },
341
+ { type: 'response.output_item.added', item: { type: 'function_call', id: 'fi_1', name: 'read', call_id: 'fc_1' } },
342
+ { type: 'response.function_call_arguments.delta', item_id: 'fi_1', delta: '{"path":' },
343
+ { type: 'response.incomplete', response: { incomplete_details: { reason: 'max_output_tokens' } } },
344
+ ]),
345
+ }).then(() => null, (e) => e);
346
+ assert.ok(rejected, 'expected the truncated-tool cutoff to reject');
347
+ assert.equal(rejected.streamStalled, true);
348
+ assert.equal(rejected.pendingToolUse, true);
349
+ });
350
+
351
+ // Reviewer fix (HTTP/SSE): function output_item.done must delete pendingCalls[id]
352
+ // before recomputing _toolInFlight — otherwise the completed call keeps
353
+ // pendingCalls.size > 0 and a later max-output cutoff is misread as a truncated
354
+ // tool. A fully-completed call before the cutoff must be a clean length result.
355
+ test('openai-oauth HTTP/SSE Responses: completed function call clears pendingCalls so max-output cutoff is clean length', async () => {
356
+ const out = await sendViaHttpSse({
357
+ auth: { type: 'openai-direct', apiKey: 'k' },
358
+ body: { model: 'gpt', tools: [] },
359
+ useModel: 'gpt',
360
+ fetchFn: async () => httpSseResponse([
361
+ { type: 'response.created', response: { id: 'r', model: 'gpt' } },
362
+ { type: 'response.output_item.added', item: { type: 'function_call', id: 'fi_1', name: 'read', call_id: 'fc_1' } },
363
+ { type: 'response.function_call_arguments.done', item_id: 'fi_1', arguments: '{"path":"a"}' },
364
+ { type: 'response.output_item.done', item: { type: 'function_call', id: 'fi_1', call_id: 'fc_1', name: 'read', arguments: '{"path":"a"}' } },
365
+ { type: 'response.incomplete', response: { incomplete_details: { reason: 'max_output_tokens' } } },
366
+ ]),
367
+ });
368
+ assert.equal(out.stopReason, 'length');
369
+ assert.deepEqual(out.toolCalls, [{ id: 'fc_1', name: 'read', arguments: { path: 'a' } }]);
370
+ });
371
+
223
372
  test('openai-compat/xai Responses: freeform apply_patch downgrades to function schema', () => {
224
373
  const tools = _toResponsesToolsForTest(PATCH_TOOL_DEFS);
225
374
  const patch = tools.find((tool) => tool.name === 'apply_patch');
@@ -229,6 +378,135 @@ test('openai-compat/xai Responses: freeform apply_patch downgrades to function s
229
378
  assert.deepEqual(patch.parameters?.required, ['patch']);
230
379
  });
231
380
 
381
+ test('openai-compat/xai Responses: load_tool downgrades from tool_search to function schema', () => {
382
+ const loadTool = {
383
+ name: 'load_tool',
384
+ description: 'load tools',
385
+ inputSchema: {
386
+ type: 'object',
387
+ properties: { names: { type: 'array', items: { type: 'string' } } },
388
+ },
389
+ };
390
+ const [xaiTool] = _toResponsesToolsForTest([loadTool], { provider: 'xai' });
391
+ assert.equal(xaiTool.type, 'function');
392
+ assert.equal(xaiTool.name, 'load_tool');
393
+ assert.equal(xaiTool.execution, undefined);
394
+ assert.equal(xaiTool.parameters, loadTool.inputSchema);
395
+
396
+ const [openaiTool] = _toResponsesToolsForTest([loadTool], { provider: 'openai' });
397
+ assert.equal(openaiTool.type, 'tool_search');
398
+ assert.equal(openaiTool.execution, 'client');
399
+ assert.equal(openaiTool.name, undefined);
400
+ });
401
+
402
+ test('openai-compat/xai Responses: load_tool history replays as function_call', () => {
403
+ const { input } = _toXaiResponsesInputForTest([
404
+ { role: 'user', content: 'load a tool' },
405
+ {
406
+ role: 'assistant',
407
+ content: '',
408
+ toolCalls: [{ id: 'call_load_1', name: 'load_tool', arguments: { names: ['read'] }, nativeType: 'tool_search_call' }],
409
+ },
410
+ {
411
+ role: 'tool',
412
+ toolCallId: 'call_load_1',
413
+ content: '{}',
414
+ nativeToolSearch: { openaiTools: [{ name: 'read' }] },
415
+ },
416
+ ], {
417
+ xaiResponses: {
418
+ previousResponseId: 'resp_same',
419
+ seenMessageCount: 0,
420
+ model: 'grok-4.5',
421
+ },
422
+ }, { model: 'grok-4.5' });
423
+ assert.equal(input.some((item) => item.type === 'tool_search_call'), false);
424
+ assert.equal(input.some((item) => item.type === 'tool_search_output'), false);
425
+ const call = input.find((item) => item.type === 'function_call' && item.name === 'load_tool');
426
+ assert.equal(call.call_id, 'call_load_1');
427
+ assert.deepEqual(JSON.parse(call.arguments), { names: ['read'] });
428
+ assert.equal(input.some((item) => item.type === 'function_call_output' && item.call_id === 'call_load_1'), true);
429
+ });
430
+
431
+ test('openai-compat/xai Responses: model switch drops prior tool transcript history', () => {
432
+ const { input, previousResponseId, continuationResetReason } = _toXaiResponsesInputForTest([
433
+ { role: 'system', content: 'sys' },
434
+ { role: 'user', content: 'before switch' },
435
+ {
436
+ role: 'assistant',
437
+ content: '',
438
+ toolCalls: [{ id: 'call_load_1', name: 'load_tool', arguments: { names: ['read'] }, nativeType: 'tool_search_call' }],
439
+ },
440
+ { role: 'tool', toolCallId: 'call_load_1', content: '{"loaded":["read"]}' },
441
+ { role: 'user', content: 'after switch' },
442
+ ], {
443
+ xaiResponses: {
444
+ previousResponseId: 'resp_old',
445
+ seenMessageCount: 4,
446
+ model: 'grok-4.20',
447
+ },
448
+ }, { model: 'grok-4.5' });
449
+ assert.equal(previousResponseId, null);
450
+ assert.equal(continuationResetReason, 'model_changed');
451
+ const serialized = JSON.stringify(input);
452
+ assert.equal(serialized.includes('tool_search'), false);
453
+ assert.equal(serialized.includes('function_call'), false);
454
+ assert.equal(serialized.includes('function_call_output'), false);
455
+ assert.deepEqual(input.map((item) => item.role), ['system', 'user', 'user']);
456
+ });
457
+
458
+ test('openai-compat/xai Responses: first Grok request after provider switch drops prior tool transcript history', () => {
459
+ const { input, previousResponseId, continuationResetReason } = _toXaiResponsesInputForTest([
460
+ { role: 'system', content: 'sys' },
461
+ { role: 'user', content: 'before switch' },
462
+ {
463
+ role: 'assistant',
464
+ content: '',
465
+ toolCalls: [{ id: 'call_patch_1', name: 'apply_patch', arguments: { patch: '*** Begin Patch\n*** End Patch\n' } }],
466
+ },
467
+ { role: 'tool', toolCallId: 'call_patch_1', content: 'OK' },
468
+ { role: 'user', content: 'after switch' },
469
+ ], {}, { model: 'grok-4.5' });
470
+ assert.equal(previousResponseId, null);
471
+ assert.equal(continuationResetReason, null);
472
+ const serialized = JSON.stringify(input);
473
+ assert.equal(serialized.includes('function_call'), false);
474
+ assert.equal(serialized.includes('function_call_output'), false);
475
+ assert.deepEqual(input.map((item) => item.role), ['system', 'user', 'user']);
476
+ });
477
+
478
+ test('openai-oauth Responses: load_tool schema and history stay function_call', () => {
479
+ const loadTool = {
480
+ name: 'load_tool',
481
+ description: 'load tools',
482
+ inputSchema: {
483
+ type: 'object',
484
+ properties: { names: { type: 'array', items: { type: 'string' } } },
485
+ },
486
+ };
487
+ const body = buildOpenAIOAuthRequestBody([
488
+ { role: 'system', content: 'sys' },
489
+ { role: 'user', content: 'load a tool' },
490
+ {
491
+ role: 'assistant',
492
+ content: '',
493
+ toolCalls: [{ id: 'call_load_1', name: 'tool_search', arguments: { names: ['read'] }, nativeType: 'tool_search_call' }],
494
+ },
495
+ { role: 'tool', toolCallId: 'call_load_1', content: '{}' },
496
+ ], 'gpt-5.5', [loadTool], {});
497
+ assert.equal(JSON.stringify(body).includes('tool_search'), false);
498
+ assert.deepEqual(body.tools?.[0], {
499
+ type: 'function',
500
+ name: 'load_tool',
501
+ description: loadTool.description,
502
+ parameters: loadTool.inputSchema,
503
+ });
504
+ const call = body.input.find((item) => item.type === 'function_call' && item.name === 'load_tool');
505
+ assert.equal(call.call_id, 'call_load_1');
506
+ assert.deepEqual(JSON.parse(call.arguments), { names: ['read'] });
507
+ assert.equal(body.input.some((item) => item.type === 'function_call_output' && item.call_id === 'call_load_1'), true);
508
+ });
509
+
232
510
  test('openai-compat/xai Responses: custom_tool_call history replays as function_call', () => {
233
511
  const rawPatch = '*** Begin Patch\n*** Add File: xai-history.txt\n+ok\n*** End Patch\n';
234
512
  const { input } = _toXaiResponsesInputForTest([
@@ -239,7 +517,13 @@ test('openai-compat/xai Responses: custom_tool_call history replays as function_
239
517
  toolCalls: [{ id: 'call_patch_1', name: 'apply_patch', arguments: { patch: rawPatch }, nativeType: 'custom_tool_call' }],
240
518
  },
241
519
  { role: 'tool', toolCallId: 'call_patch_1', content: 'OK' },
242
- ], null, { model: 'grok-composer-2.5-fast' });
520
+ ], {
521
+ xaiResponses: {
522
+ previousResponseId: 'resp_same',
523
+ seenMessageCount: 0,
524
+ model: 'grok-composer-2.5-fast',
525
+ },
526
+ }, { model: 'grok-composer-2.5-fast' });
243
527
  assert.equal(input.some((item) => item.type === 'custom_tool_call'), false);
244
528
  assert.equal(input.some((item) => item.type === 'custom_tool_call_output'), false);
245
529
  const call = input.find((item) => item.type === 'function_call' && item.name === 'apply_patch');
@@ -1092,6 +1376,35 @@ test('anthropic effort: sonnet-4-6 uses output_config + effort beta, not thinkin
1092
1376
  assert.ok(beta.includes(EFFORT_BETA_HEADER));
1093
1377
  });
1094
1378
 
1379
+ test('anthropic-oauth: load_tool native references replay as ordinary tool_result after model switches', () => {
1380
+ const body = _buildRequestBodyForCacheSmoke(
1381
+ [
1382
+ { role: 'user', content: 'load a tool' },
1383
+ {
1384
+ role: 'assistant',
1385
+ content: '',
1386
+ toolCalls: [{ id: 'toolu_load_1', name: 'load_tool', arguments: { names: ['read'] } }],
1387
+ },
1388
+ {
1389
+ role: 'tool',
1390
+ toolCallId: 'toolu_load_1',
1391
+ content: '{"loaded":["read"]}',
1392
+ nativeToolSearch: { toolReferences: ['read'] },
1393
+ },
1394
+ ],
1395
+ 'claude-opus-4-8',
1396
+ [],
1397
+ { effort: 'high' },
1398
+ );
1399
+ const serialized = JSON.stringify(body.messages);
1400
+ assert.equal(serialized.includes('tool_reference'), false);
1401
+ const toolResult = body.messages
1402
+ .flatMap((message) => Array.isArray(message.content) ? message.content : [])
1403
+ .find((block) => block?.type === 'tool_result' && block.tool_use_id === 'toolu_load_1');
1404
+ assert.ok(toolResult);
1405
+ assert.equal(toolResult.content, '{"loaded":["read"]}');
1406
+ });
1407
+
1095
1408
  test('anthropic effort: legacy sonnet-4-5 maps effort to thinking budget', () => {
1096
1409
  const model = 'claude-sonnet-4-5-20250514';
1097
1410
  const body = _buildRequestBodyForCacheSmoke(
@@ -1270,7 +1583,7 @@ test('canonical frame: full-frame builder leads with type and preserves codex bo
1270
1583
  assert.equal(frame.previous_response_id, undefined);
1271
1584
  });
1272
1585
 
1273
- test('canonical frame: delta insert keeps key order, drops empty instructions, overrides input', () => {
1586
+ test('canonical frame: delta insert keeps key order, drops chained instructions, overrides input', () => {
1274
1587
  const body = {
1275
1588
  model: 'gpt-5.5',
1276
1589
  instructions: 'sys',
@@ -1279,10 +1592,11 @@ test('canonical frame: delta insert keeps key order, drops empty instructions, o
1279
1592
  text: { verbosity: 'low' },
1280
1593
  };
1281
1594
  const delta = _buildResponseCreateFrame(body, { previousResponseId: 'resp_prev', inputOverride: [{ b: 2 }] });
1282
- assert.deepEqual(Object.keys(delta), ['type', 'model', 'instructions', 'previous_response_id', 'input', 'tool_choice', 'text']);
1595
+ assert.deepEqual(Object.keys(delta), ['type', 'model', 'previous_response_id', 'input', 'tool_choice', 'text']);
1596
+ assert.equal(delta.instructions, undefined);
1283
1597
  assert.equal(delta.previous_response_id, 'resp_prev');
1284
1598
  assert.deepEqual(delta.input, [{ b: 2 }]);
1285
- // Empty instructions is dropped in delta mode (server resolves via prev id).
1599
+ // Empty instructions is also dropped in delta mode (server resolves via prev id).
1286
1600
  const noInstr = _buildResponseCreateFrame({ ...body, instructions: '' }, { previousResponseId: 'resp_prev', inputOverride: [] });
1287
1601
  assert.deepEqual(Object.keys(noInstr), ['type', 'model', 'previous_response_id', 'input', 'tool_choice', 'text']);
1288
1602
  });
@@ -1328,11 +1642,11 @@ import {
1328
1642
  FULL_RESPONSES_TRANSPORT_CAPS,
1329
1643
  } from '../src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs';
1330
1644
 
1331
- test('transport policy: default (no env) is ws-delta / refs continuation ON', () => {
1645
+ test('transport policy: default (no env) is auto WS-first / refs continuation ON / HTTP fallback ON', () => {
1332
1646
  const p = resolveOpenAiTransportPolicy({});
1333
- assert.equal(p.mode, 'ws-delta');
1647
+ assert.equal(p.mode, 'auto');
1334
1648
  assert.equal(p.transport, 'ws');
1335
- assert.equal(p.allowHttpFallback, false);
1649
+ assert.equal(p.allowHttpFallback, true);
1336
1650
  assert.deepEqual(p.delta, { force: false, refs: true, optIn: true });
1337
1651
  });
1338
1652
 
@@ -1364,10 +1678,11 @@ test('transport policy: http-sse forces the HTTP transport with delta OFF', () =
1364
1678
  assert.equal(p.delta.optIn, false);
1365
1679
  });
1366
1680
 
1367
- test('transport policy: unknown MIXDOG_OAI_TRANSPORT falls back to default ws-delta', () => {
1681
+ test('transport policy: unknown MIXDOG_OAI_TRANSPORT falls back to default auto', () => {
1368
1682
  const p = resolveOpenAiTransportPolicy({ MIXDOG_OAI_TRANSPORT: 'quantum' });
1369
- assert.equal(p.mode, 'ws-delta');
1683
+ assert.equal(p.mode, 'auto');
1370
1684
  assert.equal(p.transport, 'ws');
1685
+ assert.equal(p.allowHttpFallback, true);
1371
1686
  });
1372
1687
 
1373
1688
  test('response frame builder can omit transport-only stream/background for codex warmup parity', () => {
@@ -1383,7 +1698,7 @@ test('transport policy: mode token aliases normalize', () => {
1383
1698
  assert.equal(_normalizeTransportMode(' ws delta '), 'ws-delta');
1384
1699
  assert.equal(_normalizeTransportMode('http/sse'), 'http-sse');
1385
1700
  assert.equal(_normalizeTransportMode('sse'), 'http-sse');
1386
- assert.equal(_normalizeTransportMode('auto'), 'ws-delta');
1701
+ assert.equal(_normalizeTransportMode('auto'), 'auto');
1387
1702
  assert.equal(_normalizeTransportMode('official'), null);
1388
1703
  assert.equal(_normalizeTransportMode(''), null);
1389
1704
  assert.equal(_normalizeTransportMode('bogus'), null);
@@ -1472,17 +1787,19 @@ test('responses transport policy: openai direct caps match oauth (both full)', (
1472
1787
  assert.deepEqual(direct.delta, { force: false, refs: true, optIn: true });
1473
1788
  });
1474
1789
 
1475
- test('responses transport policy: xai auto defers (transport=auto, delta OFF, no http fallback)', () => {
1790
+ test('responses transport policy: xai auto is WS-first with HTTP fallback', () => {
1476
1791
  const p = resolveResponsesTransportPolicy({}, RESPONSES_TRANSPORT_CAPABILITIES.xai);
1477
- assert.equal(p.mode, 'ws-delta');
1792
+ assert.equal(p.mode, 'auto');
1478
1793
  assert.equal(p.transport, 'ws');
1479
- assert.equal(p.allowHttpFallback, false);
1794
+ assert.equal(p.allowHttpFallback, true);
1480
1795
  assert.deepEqual(p.delta, { force: false, refs: true, optIn: true });
1481
1796
  });
1482
1797
 
1483
- test('responses transport policy: explicit auto is a ws-delta compatibility spelling', () => {
1798
+ test('responses transport policy: explicit auto is WS-first with HTTP fallback', () => {
1484
1799
  const p = resolveResponsesTransportPolicy({ MIXDOG_OAI_TRANSPORT: 'auto' }, RESPONSES_TRANSPORT_CAPABILITIES.xai);
1800
+ assert.equal(p.mode, 'auto');
1485
1801
  assert.equal(p.transport, 'ws');
1802
+ assert.equal(p.allowHttpFallback, true);
1486
1803
  assert.deepEqual(p.delta, { force: false, refs: true, optIn: true });
1487
1804
  });
1488
1805
 
@@ -1570,11 +1887,13 @@ test('grok-oauth: all Grok models inherit global transport; explicit setting is
1570
1887
  test('responses transport policy: _gateTransportMode down-shifts per capability', () => {
1571
1888
  // delta unsupported → ws-delta collapses to ws-full; others pass through.
1572
1889
  const noDelta = { ws: true, http: true, delta: false };
1890
+ assert.equal(_gateTransportMode('auto', noDelta), 'ws-full');
1573
1891
  assert.equal(_gateTransportMode('ws-delta', noDelta), 'ws-full');
1574
1892
  assert.equal(_gateTransportMode('ws-full', noDelta), 'ws-full');
1575
1893
  assert.equal(_gateTransportMode('http-sse', noDelta), 'http-sse');
1576
1894
  // WS unsupported → WS modes prefer HTTP.
1577
1895
  const httpOnly = { ws: false, http: true, delta: false };
1896
+ assert.equal(_gateTransportMode('auto', httpOnly), 'http-sse');
1578
1897
  assert.equal(_gateTransportMode('ws-full', httpOnly), 'http-sse');
1579
1898
  assert.equal(_gateTransportMode('ws-delta', httpOnly), 'http-sse');
1580
1899
  // HTTP unsupported → http-sse prefers full-frame WS.
@@ -4,8 +4,8 @@
4
4
  { "id": "period-24h", "label": "period window 24h", "args": { "period": "24h", "limit": 10 }, "expect": "browse" },
5
5
  { "id": "period-7d", "label": "period window 7d", "args": { "period": "7d", "limit": 10 }, "expect": "browse" },
6
6
  { "id": "scope-all", "label": "all-scope query", "args": { "query": "recall", "projectScope": "all", "limit": 10 }, "expect": "results" },
7
- { "id": "recent-dryrun-delta", "label": "recent-work topical: dryrun delta sum", "args": { "query": "드라이런 델타 합산", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["드라이런"], "topN": 5 } },
8
- { "id": "recent-remote-singleton", "label": "recent-work topical: remote singleton seat", "args": { "query": "remote 싱글톤 좌석", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["싱글톤"], "topN": 5 } },
9
- { "id": "recency-today", "label": "query+period recency: today's work (24h)", "args": { "query": "오늘 진행한 작업", "period": "24h", "limit": 10 }, "expect": { "kind": "results", "recencyOrdered": true } },
7
+ { "id": "recent-dryrun-delta", "label": "recent-work topical: dryrun delta sum", "args": { "query": "\uB4DC\uB77C\uC774\uB7F0 \uB378\uD0C0 \uD569\uC0B0", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["\uB4DC\uB77C\uC774\uB7F0"], "topN": 5 } },
8
+ { "id": "recent-remote-singleton", "label": "recent-work topical: remote singleton seat", "args": { "query": "remote \uC2F1\uAE00\uD1A4 \uC88C\uC11D", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["\uC2F1\uAE00\uD1A4"], "topN": 5 } },
9
+ { "id": "recency-today", "label": "query+period recency: today's work (24h)", "args": { "query": "\uC624\uB298 \uC9C4\uD589\uD55C \uC791\uC5C5", "period": "24h", "limit": 10 }, "expect": { "kind": "results", "recencyOrdered": true } },
10
10
  { "id": "xlang-embedding-crash", "label": "cross-language: embedding worker crash (strict topN)", "args": { "query": "embedding worker crash", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["embedding"], "topN": 3 } }
11
11
  ]
@@ -31,7 +31,7 @@ function hasFlag(name) { return process.argv.includes(`--${name}`); }
31
31
  const WARN_LATENCY_MS = 3000;
32
32
 
33
33
  const DEFAULT_CASES = [
34
- { id: 'kw-ko', label: 'keyword query (ko)', args: { query: '메모리 재현' }, expect: 'results' },
34
+ { id: 'kw-ko', label: 'keyword query (ko)', args: { query: '\uBA54\uBAA8\uB9AC \uC7AC\uD604' }, expect: 'results' },
35
35
  { id: 'kw-en', label: 'keyword query (en)', args: { query: 'memory recall pipeline' }, expect: 'results' },
36
36
  { id: 'short-1tok', label: 'short 1-token query', args: { query: 'recall' }, expect: 'results' },
37
37
  { id: 'short-2tok', label: 'short 2-token query', args: { query: 'cycle1 drain' }, expect: 'results' },
@@ -1,11 +1,11 @@
1
1
  [
2
- { "id": "q-kr-recall-phrase", "label": "Korean distinctive phrase: persistent memory", "args": { "query": "영속 메모리" }, "expect": { "kind": "results", "topNContains": ["영속 메모리"], "topN": 5 } },
2
+ { "id": "q-kr-recall-phrase", "label": "Korean distinctive phrase: persistent memory", "args": { "query": "\uC601\uC18D \uBA54\uBAA8\uB9AC" }, "expect": { "kind": "results", "topNContains": ["\uC601\uC18D \uBA54\uBAA8\uB9AC"], "topN": 5 } },
3
3
  { "id": "q-en-recall-pipeline", "label": "English distinctive phrase: recall fast-track builder", "args": { "query": "buildRecallFastTrackQuery" }, "expect": { "kind": "results", "topNContains": ["buildRecallFastTrackQuery"], "topN": 5 } },
4
4
  { "id": "q-cycle1-drain", "label": "cycle1 ECONNRESET topic", "args": { "query": "cycle1 ECONNRESET" }, "expect": { "kind": "results", "topNContains": ["cycle1", "ECONNRESET"], "topN": 5 } },
5
5
  { "id": "q-recall-general", "label": "bare recall keyword", "args": { "query": "recall" }, "expect": { "kind": "results", "topNContains": ["recall"], "topN": 5 } },
6
- { "id": "q-uc4-topic-kr", "label": "UC4 topic query with Korean qualifier", "args": { "query": "recall period last 개선", "limit": 8 }, "expect": { "kind": "results", "topNContains": ["개선"], "topN": 5 } },
7
- { "id": "q-recall-improve-kr", "label": "cache improvement topic", "args": { "query": "캐시 개선" }, "expect": { "kind": "results", "topNContains": ["개선"], "topN": 5 } },
8
- { "id": "q-fanout-recall-cache", "label": "fan-out array query cache+recall", "args": { "query": ["캐시 개선", "recall"], "limit": 5 }, "expect": { "kind": "results", "topNContains": ["개선", "recall"], "topN": 10 } },
6
+ { "id": "q-uc4-topic-kr", "label": "UC4 topic query with Korean qualifier", "args": { "query": "recall period last \uAC1C\uC120", "limit": 8 }, "expect": { "kind": "results", "topNContains": ["\uAC1C\uC120"], "topN": 5 } },
7
+ { "id": "q-recall-improve-kr", "label": "cache improvement topic", "args": { "query": "\uCE90\uC2DC \uAC1C\uC120" }, "expect": { "kind": "results", "topNContains": ["\uAC1C\uC120"], "topN": 5 } },
8
+ { "id": "q-fanout-recall-cache", "label": "fan-out array query cache+recall", "args": { "query": ["\uCE90\uC2DC \uAC1C\uC120", "recall"], "limit": 5 }, "expect": { "kind": "results", "topNContains": ["\uAC1C\uC120", "recall"], "topN": 10 } },
9
9
  { "id": "q-short-2tok-cycle", "label": "short 2-token cycle1 query", "args": { "query": "cycle1 drain", "limit": 8 }, "expect": { "kind": "results", "topNContains": ["cycle1"], "topN": 3 } },
10
10
  { "id": "q-recall-scope-project", "label": "project-scoped recall query", "args": { "query": "recall", "cwd": "C:\\Project\\mixdog" , "limit": 10 }, "expect": { "kind": "results", "topNContains": ["recall"], "topN": 5 } }
11
11
  ]
@@ -8,10 +8,10 @@
8
8
  { "id": "uc3-tod-dated", "label": "UC3 dated time window 2026-07-03 09:00~12:00", "args": { "period": "2026-07-03 09:00~12:00", "limit": 10 }, "expect": "browse" },
9
9
  { "id": "uc3-date", "label": "UC3 specific date 2026-07-02", "args": { "period": "2026-07-02", "limit": 10 }, "expect": "browse" },
10
10
  { "id": "uc3-range", "label": "UC3 date range 2026-07-01~2026-07-03", "args": { "period": "2026-07-01~2026-07-03", "limit": 10 }, "expect": "browse" },
11
- { "id": "uc4-topic", "label": "UC4 topic query", "args": { "query": "recall period last 개선", "limit": 8 }, "expect": "results" },
11
+ { "id": "uc4-topic", "label": "UC4 topic query", "args": { "query": "recall period last \uAC1C\uC120", "limit": 8 }, "expect": "results" },
12
12
  { "id": "uc4-topic-time", "label": "UC4 topic + time window", "args": { "query": "recall", "period": "24h", "limit": 8 }, "expect": "results" },
13
13
  { "id": "uc4-topic-tod", "label": "UC4 topic + time-of-day", "args": { "query": "recall", "period": "12:00~15:00", "limit": 8 }, "expect": "results" },
14
- { "id": "uc4-fanout", "label": "UC4 fan-out array query", "args": { "query": ["recall 개선", "battle balance"], "limit": 5 }, "expect": "results" },
14
+ { "id": "uc4-fanout", "label": "UC4 fan-out array query", "args": { "query": ["recall \uAC1C\uC120", "battle balance"], "limit": 5 }, "expect": "results" },
15
15
  { "id": "uc5-yesterday", "label": "UC5 calendar yesterday", "args": { "period": "yesterday", "limit": 10 }, "expect": "browse" },
16
16
  { "id": "uc5-thisweek", "label": "UC5 calendar this_week", "args": { "period": "this_week", "limit": 10 }, "expect": "browse" },
17
17
  { "id": "uc6-category", "label": "UC6 category decision 7d", "args": { "period": "7d", "category": "decision", "limit": 10 }, "expect": "browse" }
@@ -226,11 +226,11 @@ function cacheRatioFromUsage(rows) {
226
226
 
227
227
  function cacheBreakExplanation(reason) {
228
228
  const r = String(reason || '');
229
- if (r === 'no_anchor') return 'delta 기준점/previous response 없음';
230
- if (r === 'input_prefix_mismatch') return '요청 prefix 이전 turn 달라짐';
231
- if (r.startsWith('response_output_mismatch')) return '이전 응답 output 체인이 기대값과 다름';
232
- if (r === 'cache_key_changed') return 'cache key 변경';
233
- return r ? '원인 미분류' : '원인 기록 없음';
229
+ if (r === 'no_anchor') return 'delta \uAE30\uC900\uC810/previous response \uC5C6\uC74C';
230
+ if (r === 'input_prefix_mismatch') return '\uC694\uCCAD prefix\uAC00 \uC774\uC804 turn\uACFC \uB2EC\uB77C\uC9D0';
231
+ if (r.startsWith('response_output_mismatch')) return '\uC774\uC804 \uC751\uB2F5 output \uCCB4\uC778\uC774 \uAE30\uB300\uAC12\uACFC \uB2E4\uB984';
232
+ if (r === 'cache_key_changed') return 'cache key \uBCC0\uACBD';
233
+ return r ? '\uC6D0\uC778 \uBBF8\uBD84\uB958' : '\uC6D0\uC778 \uAE30\uB85D \uC5C6\uC74C';
234
234
  }
235
235
 
236
236
  function classifyCacheBreakPhase(row, usageRows, transportRows) {
@@ -1342,27 +1342,27 @@ function buildExecutiveSummary(report) {
1342
1342
  const tools = report.summary.total_tool_ms || 0;
1343
1343
  const total = headers + stream + tools;
1344
1344
  const dominant = [['stream', stream], ['tools', tools], ['headers', headers]].sort((a, b) => b[1] - a[1])[0];
1345
- lines.push(`주 병목=${dominant[0]} ${fmtMs(dominant[1])}/${fmtMs(total)}; cache=${fmtPct((report.summary.cache_ratio ?? 0) * 100)}; turns=${report.summary.turns}; tools=${report.summary.tool_calls}`);
1345
+ lines.push(`\uC8FC \uBCD1\uBAA9=${dominant[0]} ${fmtMs(dominant[1])}/${fmtMs(total)}; cache=${fmtPct((report.summary.cache_ratio ?? 0) * 100)}; turns=${report.summary.turns}; tools=${report.summary.tool_calls}`);
1346
1346
  if (report.cache.cache_breaks.length) {
1347
1347
  const summary = countBy(report.cache.cache_breaks, (b) => `${b.reason || 'unknown'}/${b.phase || 'unknown'}`).slice(0, 3).map(([k, v]) => `${k}×${v}`).join(', ');
1348
- lines.push(`캐시 깨짐=${report.cache.cache_breaks.length} (${summary})`);
1348
+ lines.push(`\uCE90\uC2DC \uAE68\uC9D0=${report.cache.cache_breaks.length} (${summary})`);
1349
1349
  }
1350
1350
  if (report.cache.actual_cache_misses?.length) {
1351
1351
  const top = [...report.cache.actual_cache_misses].sort((a, b) => (b.uncached_tokens || 0) - (a.uncached_tokens || 0))[0];
1352
- lines.push(`실제 캐시 미스=${report.cache.actual_cache_misses.length} (top uncached=${fmtTok(top?.uncached_tokens || 0)})`);
1352
+ lines.push(`\uC2E4\uC81C \uCE90\uC2DC \uBBF8\uC2A4=${report.cache.actual_cache_misses.length} (top uncached=${fmtTok(top?.uncached_tokens || 0)})`);
1353
1353
  }
1354
1354
  if (report.compact?.cache_breaks?.length) {
1355
1355
  const summary = countBy(report.compact.cache_breaks, (b) => `${b.reason || 'unknown'}/${b.phase || 'unknown'}`).slice(0, 3).map(([k, v]) => `${k}×${v}`).join(', ');
1356
- lines.push(`컴팩트 호출=${report.compact.sessions.length} session, cache reset=${report.compact.cache_breaks.length} (${summary})`);
1356
+ lines.push(`\uCEF4\uD329\uD2B8 \uD638\uCD9C=${report.compact.sessions.length} session, cache reset=${report.compact.cache_breaks.length} (${summary})`);
1357
1357
  }
1358
1358
  if (report.tools.failures.length) {
1359
1359
  const summary = countBy(report.tools.failures, (f) => f.category || f.tool || 'unknown').slice(0, 3).map(([k, v]) => `${k}×${v}`).join(', ');
1360
- lines.push(`툴 실패=${report.tools.failures.length} (${summary})`);
1360
+ lines.push(`\uD234 \uC2E4\uD328=${report.tools.failures.length} (${summary})`);
1361
1361
  }
1362
1362
  const topGrowth = report.tokens.growth_turns[0];
1363
- if (topGrowth) lines.push(`토큰 증폭=${topGrowth.agent || '-'} it=${topGrowth.turn_label} promptΔ=${fmtTok(topGrowth.prompt_delta)} out=${fmtTok(topGrowth.output_tokens + topGrowth.thinking_tokens)}`);
1363
+ if (topGrowth) lines.push(`\uD1A0\uD070 \uC99D\uD3ED=${topGrowth.agent || '-'} it=${topGrowth.turn_label} promptΔ=${fmtTok(topGrowth.prompt_delta)} out=${fmtTok(topGrowth.output_tokens + topGrowth.thinking_tokens)}`);
1364
1364
  const topChurn = report.tools.duplicates[0];
1365
- if (topChurn) lines.push(`툴 헛돎=${topChurn.tool}×${topChurn.count} ${compactText(topChurn.target, 90)}`);
1365
+ if (topChurn) lines.push(`\uD234 \uD5DB\uB3CE=${topChurn.tool}×${topChurn.count} ${compactText(topChurn.target, 90)}`);
1366
1366
  return lines;
1367
1367
  }
1368
1368
 
@@ -1626,7 +1626,7 @@ function renderText(report) {
1626
1626
  if (report.tools.failures.length) {
1627
1627
  lines.push('tool failures:');
1628
1628
  for (const f of report.tools.failures.slice(0, 10)) {
1629
- lines.push(`- ${f.agent || '-'} it=${f.iteration ?? '-'} ${f.tool || '-'} ${fmtMs(f.tool_ms)} category=${f.category || '-'} reason=${f.reason || 'trace 상세 stderr/예외 미저장'} result=${Math.round(f.bytes / 1024)}KB/${f.lines}l: ${f.target}`);
1629
+ lines.push(`- ${f.agent || '-'} it=${f.iteration ?? '-'} ${f.tool || '-'} ${fmtMs(f.tool_ms)} category=${f.category || '-'} reason=${f.reason || 'trace\uC5D0 \uC0C1\uC138 stderr/\uC608\uC678 \uBBF8\uC800\uC7A5'} result=${Math.round(f.bytes / 1024)}KB/${f.lines}l: ${f.target}`);
1630
1630
  if (opts.failuresOnly && f.preview) {
1631
1631
  const preview = compactText(f.preview, 700);
1632
1632
  lines.push(` preview: ${preview}`);