mixdog 0.9.36 → 0.9.37

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 (103) 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/explore-bench-tmp.mjs +19 -0
  5. package/scripts/explore-bench.mjs +36 -6
  6. package/scripts/explore-prompt-policy-test.mjs +27 -0
  7. package/scripts/ingest-pure-conversation-smoke.mjs +11 -11
  8. package/scripts/openai-ws-early-settle-test.mjs +176 -0
  9. package/scripts/output-style-smoke.mjs +17 -17
  10. package/scripts/provider-toolcall-test.mjs +333 -14
  11. package/scripts/recall-bench-cases.json +3 -3
  12. package/scripts/recall-bench.mjs +1 -1
  13. package/scripts/recall-quality-cases.json +4 -4
  14. package/scripts/recall-usecase-cases.json +2 -2
  15. package/scripts/session-bench.mjs +13 -13
  16. package/scripts/steering-drain-buckets-test.mjs +72 -11
  17. package/scripts/submit-commandbusy-race-test.mjs +114 -0
  18. package/scripts/tool-smoke.mjs +83 -10
  19. package/src/app.mjs +2 -1
  20. package/src/defaults/cycle3-review-prompt.md +9 -0
  21. package/src/defaults/skills/setup/SKILL.md +93 -293
  22. package/src/lib/rules-builder.cjs +3 -2
  23. package/src/output-styles/default.md +2 -2
  24. package/src/output-styles/extreme-minimal.md +1 -1
  25. package/src/output-styles/minimal.md +1 -1
  26. package/src/output-styles/simple.md +2 -3
  27. package/src/rules/agent/30-explorer.md +15 -7
  28. package/src/rules/lead/01-general.md +4 -0
  29. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +15 -3
  30. package/src/runtime/agent/orchestrator/config.mjs +1 -1
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +6 -6
  32. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
  33. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +35 -5
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +27 -0
  35. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +36 -37
  36. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +2 -2
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +72 -1
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +26 -3
  39. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -28
  40. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +18 -15
  41. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +4 -2
  42. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +113 -2
  43. package/src/runtime/agent/orchestrator/providers/registry.mjs +44 -5
  44. package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +78 -0
  45. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +57 -31
  46. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +8 -6
  47. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +28 -2
  48. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +1 -3
  49. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +15 -2
  50. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +26 -17
  51. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -2
  52. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +24 -16
  53. package/src/runtime/agent/orchestrator/session/result-classification.mjs +2 -0
  54. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -2
  55. package/src/runtime/agent/orchestrator/stall-policy.mjs +18 -0
  56. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +35 -20
  57. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
  58. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +77 -31
  59. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +25 -3
  60. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +9 -2
  61. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +73 -25
  62. package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -1
  63. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +17 -7
  64. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -12
  65. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +5 -0
  66. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +56 -6
  67. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -0
  68. package/src/runtime/channels/lib/worker-main.mjs +5 -0
  69. package/src/runtime/memory/lib/core-memory-store.mjs +104 -0
  70. package/src/runtime/memory/lib/ko-morph.mjs +1 -1
  71. package/src/runtime/memory/lib/memory-cycle3.mjs +64 -4
  72. package/src/runtime/memory/lib/memory-text-utils.mjs +4 -4
  73. package/src/runtime/memory/lib/recall-format.mjs +3 -3
  74. package/src/runtime/shared/child-spawn-gate.mjs +15 -0
  75. package/src/runtime/shared/config.mjs +98 -12
  76. package/src/runtime/shared/process-listener-headroom.mjs +12 -0
  77. package/src/session-runtime/cwd-plugins.mjs +6 -3
  78. package/src/session-runtime/model-route-api.mjs +50 -2
  79. package/src/session-runtime/provider-auth-api.mjs +8 -1
  80. package/src/session-runtime/resource-api.mjs +22 -0
  81. package/src/session-runtime/runtime-core.mjs +13 -2
  82. package/src/session-runtime/settings-api.mjs +11 -2
  83. package/src/standalone/agent-tool.mjs +15 -9
  84. package/src/standalone/explore-tool.mjs +4 -1
  85. package/src/tui/App.jsx +6 -5
  86. package/src/tui/app/transcript-window.mjs +60 -10
  87. package/src/tui/app/use-transcript-window.mjs +2 -1
  88. package/src/tui/components/ContextPanel.jsx +4 -1
  89. package/src/tui/components/ToolExecution.jsx +15 -12
  90. package/src/tui/components/TranscriptItem.jsx +1 -1
  91. package/src/tui/components/tool-execution/surface-detail.mjs +8 -8
  92. package/src/tui/dist/index.mjs +467 -151
  93. package/src/tui/engine/agent-job-feed.mjs +26 -6
  94. package/src/tui/engine/notification-plan.mjs +3 -3
  95. package/src/tui/engine/render-timing.mjs +104 -8
  96. package/src/tui/engine/session-api.mjs +58 -3
  97. package/src/tui/engine/session-flow.mjs +78 -28
  98. package/src/tui/engine/tool-card-results.mjs +28 -13
  99. package/src/tui/engine/turn.mjs +232 -156
  100. package/src/tui/engine.mjs +17 -8
  101. package/src/tui/index.jsx +10 -1
  102. package/src/workflows/default/WORKFLOW.md +8 -4
  103. package/src/workflows/solo/WORKFLOW.md +8 -4
@@ -13,7 +13,7 @@ function assert(condition, message) {
13
13
  if (!condition) throw new Error(message);
14
14
  }
15
15
 
16
- const DEFAULT_REPORT_LABELS = new Set(['바뀐 ', '확인한 ', '남은 리스크/다음 단계', '다음 단계']);
16
+ const DEFAULT_REPORT_LABELS = new Set(['\uBC14\uB010 \uC810', '\uD655\uC778\uD55C \uAC83', '\uB0A8\uC740 \uB9AC\uC2A4\uD06C/\uB2E4\uC74C \uB2E8\uACC4', '\uB2E4\uC74C \uB2E8\uACC4']);
17
17
 
18
18
  function assertCleanOutput(name, value, { maxLines = 3, maxBullets = 3, allowedLabels = new Set() } = {}) {
19
19
  const text = String(value || '').trim();
@@ -28,7 +28,7 @@ function assertCleanOutput(name, value, { maxLines = 3, maxBullets = 3, allowedL
28
28
  }
29
29
  assert(!/\b(?:tool trace|session metadata|model metadata|searched-path list)\b/i.test(text), `${name}: tool/meta trace found`);
30
30
  assert(!/\b(?:Mapped|Searched|Read|Called) for \d/i.test(text), `${name}: timing/tool status found`);
31
- assert(!/^(?:네|예|맞아요|좋아요|알겠습니다|Sure|Okay|Understood)[,.\s]/i.test(text), `${name}: acknowledgment preface found`);
31
+ assert(!/^(?:\uB124|\uC608|\uB9DE\uC544\uC694|\uC88B\uC544\uC694|\uC54C\uACA0\uC2B5\uB2C8\uB2E4|Sure|Okay|Understood)[,.\s]/i.test(text), `${name}: acknowledgment preface found`);
32
32
  assert(!/(.+)(?:\n|\s{2,})\1/.test(text), `${name}: repeated conclusion found`);
33
33
 
34
34
  const lines = text.split(/\r?\n/).filter((line) => line.trim());
@@ -41,7 +41,7 @@ function assertCleanOutput(name, value, { maxLines = 3, maxBullets = 3, allowedL
41
41
  assert(lines.length <= maxLines, `${name}: too many lines`);
42
42
 
43
43
  const withoutCode = text.replace(/`[^`]*`/g, '');
44
- const sentenceMarks = withoutCode.match(/[.!?。!?]|다\.|요\./g) || [];
44
+ const sentenceMarks = withoutCode.match(/[.!?。!?]|\uB2E4\.|\uC694\./g) || [];
45
45
  assert(sentenceMarks.length <= 2, `${name}: too many sentences`);
46
46
  }
47
47
 
@@ -52,7 +52,7 @@ for (const required of [
52
52
  'Mixdog default — the most detailed style',
53
53
  'State conclusions, not reasoning',
54
54
  'Use labels such as',
55
- '`바뀐 점`, `확인한 것`,',
55
+ '`\uBC14\uB010 \uC810`, `\uD655\uC778\uD55C \uAC83`,',
56
56
  'Synthesize agent or retrieval results',
57
57
  'Do not hide blockers',
58
58
  ]) {
@@ -97,7 +97,7 @@ try {
97
97
 
98
98
  mkdirSync(join(dataDir, 'output-styles'), { recursive: true });
99
99
  writeFileSync(join(dataDir, 'mixdog-config.json'), JSON.stringify({
100
- agent: { profile: { title: '홍길동님', language: 'system' } },
100
+ agent: { profile: { title: '\uD64D\uAE38\uB3D9\uB2D8', language: 'system' } },
101
101
  outputStyle: 'custom-smoke',
102
102
  }));
103
103
  writeFileSync(join(dataDir, 'output-styles', 'custom-smoke.md'), '---\nname: custom-smoke\n---\n\n# Custom Output Style\n\ncustom smoke style\n');
@@ -105,7 +105,7 @@ try {
105
105
  assert(customRules.includes('# Custom Output Style'), 'configured outputStyle must select custom style');
106
106
  assert(!customRules.includes('Mixdog default — the most detailed of the three styles'), 'custom outputStyle should not append default style');
107
107
  const profileMeta = rulesBuilder.buildLeadMetaContent({ PLUGIN_ROOT: join(root, 'src'), DATA_DIR: dataDir });
108
- assert(profileMeta.includes('Use "홍길동님" when directly addressing the user'), 'profile title must inject into Lead BP3 meta');
108
+ assert(profileMeta.includes('Use "\uD64D\uAE38\uB3D9\uB2D8" when directly addressing the user'), 'profile title must inject into Lead BP3 meta');
109
109
  assert(profileMeta.includes('do not repeat it in routine progress updates or pre-tool preambles'), 'profile title must not encourage title in preambles');
110
110
  assert(/Default user-facing response language from system locale/.test(profileMeta), 'system profile language must resolve from system locale');
111
111
  assert(profileMeta.includes('pre-tool preambles (even single-line)'), 'profile language must cover pre-tool preambles');
@@ -115,26 +115,26 @@ try {
115
115
 
116
116
  // assertCleanOutput encodes the Simple style compact contract (not Default, which may be longer).
117
117
  const goodOutputs = {
118
- explanation: 'Simple 스타일은 결과 줄과 근거 가지로 끝내고, 최종 handoff 짧은 bullet 라벨을 씁니다.',
119
- implementation: '- 바뀐 점: `scripts/output-style-smoke.mjs`의 default/simple 문자열 검증을 현재 프리셋에 맞췄습니다.\n- 확인한 것: `node scripts/output-style-smoke.mjs`를 실행했습니다.',
120
- crowded: '- 바뀐 점: compact guardrail Simple handoff controlled detail 검증합니다.\n- 확인한 것: bad 샘플 거부 규칙은 그대로입니다.',
121
- blocker: '`scripts/output-style-smoke.mjs`를 찾을 없어 변경이 막혔습니다.',
122
- semicolon: '스모크 스크립트를 갱신했고; Simple 계약에 맞는 예시 응답만 통과합니다.',
118
+ explanation: 'Simple \uC2A4\uD0C0\uC77C\uC740 \uACB0\uACFC \uD55C \uC904\uACFC \uADFC\uAC70 \uD55C \uAC00\uC9C0\uB85C \uB05D\uB0B4\uACE0, \uCD5C\uC885 handoff\uB9CC \uC9E7\uC740 bullet \uB77C\uBCA8\uC744 \uC501\uB2C8\uB2E4.',
119
+ implementation: '- \uBC14\uB010 \uC810: `scripts/output-style-smoke.mjs`\uC758 default/simple \uBB38\uC790\uC5F4 \uAC80\uC99D\uC744 \uD604\uC7AC \uD504\uB9AC\uC14B\uC5D0 \uB9DE\uCDC4\uC2B5\uB2C8\uB2E4.\n- \uD655\uC778\uD55C \uAC83: `node scripts/output-style-smoke.mjs`\uB97C \uC2E4\uD589\uD588\uC2B5\uB2C8\uB2E4.',
120
+ crowded: '- \uBC14\uB010 \uC810: compact guardrail\uC740 Simple handoff\uC6A9 controlled detail\uC744 \uAC80\uC99D\uD569\uB2C8\uB2E4.\n- \uD655\uC778\uD55C \uAC83: bad \uC0D8\uD50C \uAC70\uBD80 \uADDC\uCE59\uC740 \uADF8\uB300\uB85C\uC785\uB2C8\uB2E4.',
121
+ blocker: '`scripts/output-style-smoke.mjs`\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC5B4 \uBCC0\uACBD\uC774 \uB9C9\uD614\uC2B5\uB2C8\uB2E4.',
122
+ semicolon: '\uC2A4\uBAA8\uD06C \uC2A4\uD06C\uB9BD\uD2B8\uB97C \uAC31\uC2E0\uD588\uACE0; Simple \uACC4\uC57D\uC5D0 \uB9DE\uB294 \uC608\uC2DC \uC751\uB2F5\uB9CC \uD1B5\uACFC\uD569\uB2C8\uB2E4.',
123
123
  };
124
124
  for (const [name, output] of Object.entries(goodOutputs)) assertCleanOutput(name, output, { allowedLabels: DEFAULT_REPORT_LABELS });
125
125
 
126
126
  const badOutputs = {
127
127
  heading: '## Summary\nDone.',
128
128
  label: 'Changed: updated default style.',
129
- koreanLabel: '변경: default 스타일을 수정했습니다.\n검증: 스모크를 통과했습니다.',
129
+ koreanLabel: '\uBCC0\uACBD: default \uC2A4\uD0C0\uC77C\uC744 \uC218\uC815\uD588\uC2B5\uB2C8\uB2E4.\n\uAC80\uC99D: \uC2A4\uBAA8\uD06C\uB97C \uD1B5\uACFC\uD588\uC2B5\uB2C8\uB2E4.',
130
130
  numbered: '1. Updated style\n2. Ran smoke',
131
131
  paragraphs: 'Done.\n\nDone again.',
132
132
  nested: '- Updated\n - Nested detail',
133
- ack: '네, default 스타일을 수정했습니다.',
134
- timing: 'Mapped for 2m 32s\n\ndefault 스타일을 수정했습니다.',
135
- tooManySentences: '수정했습니다. 검증했습니다. 보고했습니다.',
136
- mixed: '수정했습니다.\n- 검증했습니다.',
137
- tooManyBullets: '- 바뀐 점: A\n- 확인한 것: B\n- 다음 단계: C\n- 남은 리스크/다음 단계: D',
133
+ ack: '\uB124, default \uC2A4\uD0C0\uC77C\uC744 \uC218\uC815\uD588\uC2B5\uB2C8\uB2E4.',
134
+ timing: 'Mapped for 2m 32s\n\ndefault \uC2A4\uD0C0\uC77C\uC744 \uC218\uC815\uD588\uC2B5\uB2C8\uB2E4.',
135
+ tooManySentences: '\uC218\uC815\uD588\uC2B5\uB2C8\uB2E4. \uAC80\uC99D\uD588\uC2B5\uB2C8\uB2E4. \uBCF4\uACE0\uD588\uC2B5\uB2C8\uB2E4.',
136
+ mixed: '\uC218\uC815\uD588\uC2B5\uB2C8\uB2E4.\n- \uAC80\uC99D\uD588\uC2B5\uB2C8\uB2E4.',
137
+ tooManyBullets: '- \uBC14\uB010 \uC810: A\n- \uD655\uC778\uD55C \uAC83: B\n- \uB2E4\uC74C \uB2E8\uACC4: C\n- \uB0A8\uC740 \uB9AC\uC2A4\uD06C/\uB2E4\uC74C \uB2E8\uACC4: D',
138
138
  };
139
139
  for (const [name, output] of Object.entries(badOutputs)) {
140
140
  let failed = false;
@@ -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" }