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.
- package/package.json +3 -2
- package/scripts/abort-recovery-test.mjs +118 -0
- package/scripts/compact-smoke.mjs +7 -7
- package/scripts/dispatch-persist-recovery-test.mjs +141 -0
- package/scripts/explore-bench-tmp.mjs +19 -0
- package/scripts/explore-bench.mjs +101 -14
- package/scripts/explore-prompt-policy-test.mjs +31 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +11 -11
- package/scripts/notify-completion-mirror-test.mjs +73 -0
- package/scripts/openai-ws-early-settle-test.mjs +176 -0
- package/scripts/output-style-smoke.mjs +17 -17
- package/scripts/provider-toolcall-test.mjs +333 -14
- package/scripts/recall-bench-cases.json +3 -3
- package/scripts/recall-bench.mjs +1 -1
- package/scripts/recall-quality-cases.json +4 -4
- package/scripts/recall-usecase-cases.json +2 -2
- package/scripts/session-bench.mjs +13 -13
- package/scripts/session-sweep.mjs +266 -0
- package/scripts/steering-drain-buckets-test.mjs +72 -11
- package/scripts/submit-commandbusy-race-test.mjs +114 -0
- package/scripts/tool-smoke.mjs +83 -10
- package/src/app.mjs +2 -1
- package/src/defaults/cycle3-review-prompt.md +9 -0
- package/src/defaults/skills/setup/SKILL.md +93 -293
- package/src/lib/rules-builder.cjs +3 -2
- package/src/output-styles/default.md +2 -2
- package/src/output-styles/extreme-minimal.md +1 -1
- package/src/output-styles/minimal.md +1 -1
- package/src/output-styles/simple.md +2 -3
- package/src/rules/agent/30-explorer.md +40 -14
- package/src/rules/lead/01-general.md +4 -0
- package/src/rules/shared/01-tool.md +7 -3
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +15 -3
- package/src/runtime/agent/orchestrator/config.mjs +1 -1
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +31 -8
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +13 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +35 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +36 -37
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +72 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +26 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -28
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +18 -15
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +119 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +113 -2
- package/src/runtime/agent/orchestrator/providers/registry.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +78 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +57 -31
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +28 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +30 -55
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +24 -16
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +116 -21
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +35 -20
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +77 -31
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +25 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +73 -25
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +17 -7
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -12
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +56 -6
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -0
- package/src/runtime/channels/lib/worker-main.mjs +5 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +104 -0
- package/src/runtime/memory/lib/ko-morph.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle3.mjs +64 -4
- package/src/runtime/memory/lib/memory-text-utils.mjs +4 -4
- package/src/runtime/memory/lib/recall-format.mjs +3 -3
- package/src/runtime/shared/child-spawn-gate.mjs +15 -0
- package/src/runtime/shared/config.mjs +98 -12
- package/src/runtime/shared/process-listener-headroom.mjs +12 -0
- package/src/session-runtime/cwd-plugins.mjs +6 -3
- package/src/session-runtime/model-route-api.mjs +50 -2
- package/src/session-runtime/provider-auth-api.mjs +8 -1
- package/src/session-runtime/provider-models.mjs +3 -3
- package/src/session-runtime/resource-api.mjs +22 -0
- package/src/session-runtime/runtime-core.mjs +56 -10
- package/src/session-runtime/settings-api.mjs +11 -2
- package/src/session-runtime/warmup-schedulers.mjs +7 -1
- package/src/standalone/agent-tool.mjs +15 -9
- package/src/standalone/explore-tool.mjs +4 -1
- package/src/tui/App.jsx +6 -5
- package/src/tui/app/transcript-window.mjs +60 -10
- package/src/tui/app/use-transcript-window.mjs +2 -1
- package/src/tui/components/ContextPanel.jsx +4 -1
- package/src/tui/components/ToolExecution.jsx +15 -12
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +8 -8
- package/src/tui/dist/index.mjs +482 -151
- package/src/tui/engine/agent-job-feed.mjs +36 -6
- package/src/tui/engine/notification-plan.mjs +3 -3
- package/src/tui/engine/queue-helpers.mjs +8 -0
- package/src/tui/engine/render-timing.mjs +104 -8
- package/src/tui/engine/session-api.mjs +58 -3
- package/src/tui/engine/session-flow.mjs +93 -29
- package/src/tui/engine/tool-card-results.mjs +28 -13
- package/src/tui/engine/turn.mjs +238 -157
- package/src/tui/engine.mjs +17 -8
- package/src/tui/index.jsx +10 -1
- package/src/workflows/default/WORKFLOW.md +8 -4
- package/src/workflows/solo/WORKFLOW.md +8 -4
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// notifyFnForSession must not double-inject a background-task completion. The
|
|
2
|
+
// mirror skip is gated on an EXPLICIT model-visible-delivery ack
|
|
3
|
+
// (modelVisibleDelivered), set ONLY by the TUI execution-ui path that injects
|
|
4
|
+
// the completion body into the active loop — never on a generic truthy
|
|
5
|
+
// onNotification return. So: delivered ack → mirror suppressed; headless /
|
|
6
|
+
// display-only / API listener (no ack) → mirror kept as the sole delivery.
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
|
|
10
|
+
import { shouldMirrorCompletionToPendingQueue } from '../src/session-runtime/runtime-core.mjs';
|
|
11
|
+
|
|
12
|
+
// A terminal agent completion with a Result body — accepted by
|
|
13
|
+
// shouldPersistModelVisibleToolCompletion.
|
|
14
|
+
const completionText = 'Async agent task task_1 completed finished.\n\nResult:\n> ok';
|
|
15
|
+
const completionMeta = { type: 'agent_task_result', execution_id: 'task_1', status: 'completed' };
|
|
16
|
+
|
|
17
|
+
test('delivered-live completion: no pending mirror (explicit model-visible ack)', () => {
|
|
18
|
+
const mirror = shouldMirrorCompletionToPendingQueue({
|
|
19
|
+
callerSessionId: 'sess_live',
|
|
20
|
+
modelVisibleDelivered: true,
|
|
21
|
+
hasEnqueue: true,
|
|
22
|
+
text: completionText,
|
|
23
|
+
meta: completionMeta,
|
|
24
|
+
});
|
|
25
|
+
assert.equal(mirror, false, 'live twin already injected → mirror suppressed');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('headless completion: pending mirror kept (no ack)', () => {
|
|
29
|
+
const mirror = shouldMirrorCompletionToPendingQueue({
|
|
30
|
+
callerSessionId: 'sess_headless',
|
|
31
|
+
modelVisibleDelivered: false,
|
|
32
|
+
hasEnqueue: true,
|
|
33
|
+
text: completionText,
|
|
34
|
+
meta: completionMeta,
|
|
35
|
+
});
|
|
36
|
+
assert.equal(mirror, true, 'no listener delivered → queue copy is the sole delivery');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('display-only listener returning true (no ack): pending mirror kept', () => {
|
|
40
|
+
// A generic display/API listener consumes the event (would return true) but
|
|
41
|
+
// never injects the model-visible body, so it sets NO modelVisibleDelivered
|
|
42
|
+
// ack. The mirror must stay, or the model never sees the completion body.
|
|
43
|
+
const mirror = shouldMirrorCompletionToPendingQueue({
|
|
44
|
+
callerSessionId: 'sess_display_only',
|
|
45
|
+
modelVisibleDelivered: false,
|
|
46
|
+
hasEnqueue: true,
|
|
47
|
+
text: completionText,
|
|
48
|
+
meta: completionMeta,
|
|
49
|
+
});
|
|
50
|
+
assert.equal(mirror, true, 'generic handled===true is NOT an ack → mirror kept');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('unhandled but non-terminal notification is never mirrored', () => {
|
|
54
|
+
const mirror = shouldMirrorCompletionToPendingQueue({
|
|
55
|
+
callerSessionId: 'sess_headless',
|
|
56
|
+
modelVisibleDelivered: false,
|
|
57
|
+
hasEnqueue: true,
|
|
58
|
+
text: 'still running...',
|
|
59
|
+
meta: { status: 'running' },
|
|
60
|
+
});
|
|
61
|
+
assert.equal(mirror, false, 'only persistable terminal completions mirror');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('missing session id or enqueue capability never mirrors', () => {
|
|
65
|
+
assert.equal(shouldMirrorCompletionToPendingQueue({
|
|
66
|
+
callerSessionId: '', modelVisibleDelivered: false, hasEnqueue: true,
|
|
67
|
+
text: completionText, meta: completionMeta,
|
|
68
|
+
}), false, 'no caller session → nothing to mirror into');
|
|
69
|
+
assert.equal(shouldMirrorCompletionToPendingQueue({
|
|
70
|
+
callerSessionId: 'sess_headless', modelVisibleDelivered: false, hasEnqueue: false,
|
|
71
|
+
text: completionText, meta: completionMeta,
|
|
72
|
+
}), false, 'no enqueue capability → cannot mirror');
|
|
73
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Regression: OpenAI OAuth WS early tool-call settle.
|
|
2
|
+
//
|
|
3
|
+
// A tool-call response can be fully formed (function_call_arguments.done +
|
|
4
|
+
// output_item.done) yet the server never emits the terminal
|
|
5
|
+
// response.completed/response.done frame. Before the fix _streamResponse
|
|
6
|
+
// coasted on that silence until a stall error (gated out of the loop via
|
|
7
|
+
// pendingToolUse) or the 30-min agent watchdog, and -- because the socket was
|
|
8
|
+
// still pooled -- the missing terminal frame later leaked onto the NEXT request
|
|
9
|
+
// as orphan bytes (http_status=0 wedge). The fix resolves early with the
|
|
10
|
+
// captured tool call and flags `closeSocket` so the pool discards the socket.
|
|
11
|
+
import test from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { EventEmitter } from 'node:events';
|
|
14
|
+
import { _streamResponse } from '../src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs';
|
|
15
|
+
|
|
16
|
+
// Minimal fake WS: matches the .on/.off/.close/.ping surface _streamResponse
|
|
17
|
+
// uses. close() is a no-op here -- the early-settle path sets done=true before
|
|
18
|
+
// calling it, so the real closeHandler would short-circuit anyway.
|
|
19
|
+
class FakeSocket extends EventEmitter {
|
|
20
|
+
constructor() {
|
|
21
|
+
super();
|
|
22
|
+
this.readyState = 1;
|
|
23
|
+
this.closed = null;
|
|
24
|
+
}
|
|
25
|
+
close(code, reason) { this.closed = { code, reason }; }
|
|
26
|
+
ping() {}
|
|
27
|
+
feed(events) { for (const e of events) this.emit('message', JSON.stringify(e)); }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const FAST_TIMEOUTS = { interChunkMs: 40, preResponseCreatedMs: 5000, firstMeaningfulMs: 5000 };
|
|
31
|
+
|
|
32
|
+
const TOOL_CALL_EVENTS = [
|
|
33
|
+
{ type: 'response.created', response: { id: 'resp_1', model: 'gpt-5.5' } },
|
|
34
|
+
{ type: 'response.output_item.added', item: { type: 'function_call', id: 'fc_1', name: 'explore', call_id: 'call_1' } },
|
|
35
|
+
{ type: 'response.function_call_arguments.delta', item_id: 'fc_1', delta: '{"query"' },
|
|
36
|
+
{ type: 'response.function_call_arguments.done', item_id: 'fc_1', arguments: '{"query":"x"}', call_id: 'call_1', name: 'explore' },
|
|
37
|
+
{ type: 'response.output_item.done', item: { type: 'function_call', id: 'fc_1', name: 'explore', call_id: 'call_1', arguments: '{"query":"x"}' } },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
test('ws early settle: complete tool call resolves without response.completed and flags closeSocket', async () => {
|
|
41
|
+
const socket = new FakeSocket();
|
|
42
|
+
const state = {};
|
|
43
|
+
const emitted = [];
|
|
44
|
+
const p = _streamResponse({
|
|
45
|
+
entry: { socket },
|
|
46
|
+
state,
|
|
47
|
+
onToolCall: (c) => emitted.push(c),
|
|
48
|
+
_timeouts: FAST_TIMEOUTS,
|
|
49
|
+
});
|
|
50
|
+
socket.feed(TOOL_CALL_EVENTS);
|
|
51
|
+
const result = await p;
|
|
52
|
+
assert.equal(result.closeSocket, true, 'socket must be dropped, not pooled');
|
|
53
|
+
assert.ok(Array.isArray(result.toolCalls) && result.toolCalls.length === 1);
|
|
54
|
+
assert.equal(result.toolCalls[0].id, 'call_1');
|
|
55
|
+
assert.equal(result.toolCalls[0].name, 'explore');
|
|
56
|
+
assert.deepEqual(result.toolCalls[0].arguments, { query: 'x' });
|
|
57
|
+
assert.equal(emitted.length, 1, 'tool call dispatched exactly once');
|
|
58
|
+
assert.equal(state.wsEarlySettle, 'inter_chunk');
|
|
59
|
+
assert.ok(socket.closed, 'socket.close() invoked so it is never reused');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('ws early settle: normal response.completed path keeps the socket (no closeSocket)', async () => {
|
|
63
|
+
const socket = new FakeSocket();
|
|
64
|
+
const p = _streamResponse({ entry: { socket }, state: {}, _timeouts: FAST_TIMEOUTS });
|
|
65
|
+
socket.feed([
|
|
66
|
+
...TOOL_CALL_EVENTS,
|
|
67
|
+
{
|
|
68
|
+
type: 'response.completed',
|
|
69
|
+
response: {
|
|
70
|
+
id: 'resp_1',
|
|
71
|
+
model: 'gpt-5.5',
|
|
72
|
+
output: [{ type: 'function_call', id: 'fc_1', name: 'explore', call_id: 'call_1', arguments: '{"query":"x"}' }],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
const result = await p;
|
|
77
|
+
assert.equal(result.closeSocket, undefined, 'normal completion must not mark closeSocket');
|
|
78
|
+
assert.equal(result.toolCalls.length, 1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('ws early settle: deferred salvage still pending does NOT early-settle (fails as stall)', async () => {
|
|
82
|
+
const socket = new FakeSocket();
|
|
83
|
+
const p = _streamResponse({ entry: { socket }, state: {}, _timeouts: FAST_TIMEOUTS });
|
|
84
|
+
// No output_item.added -> pendingCalls empty; args.done carries no call_id/name
|
|
85
|
+
// -> a deferred placeholder that only response.completed could salvage.
|
|
86
|
+
socket.feed([
|
|
87
|
+
{ type: 'response.created', response: { id: 'resp_1', model: 'gpt-5.5' } },
|
|
88
|
+
{ type: 'response.function_call_arguments.delta', item_id: 'fc_1', delta: '{"query"' },
|
|
89
|
+
{ type: 'response.function_call_arguments.done', item_id: 'fc_1', arguments: '{"query":"x"}' },
|
|
90
|
+
]);
|
|
91
|
+
await assert.rejects(p, /inter-chunk inactivity/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('ws early settle: complete tool then partial second tool does NOT early-settle', async () => {
|
|
95
|
+
const socket = new FakeSocket();
|
|
96
|
+
const p = _streamResponse({ entry: { socket }, state: {}, _timeouts: FAST_TIMEOUTS });
|
|
97
|
+
socket.feed([
|
|
98
|
+
...TOOL_CALL_EVENTS,
|
|
99
|
+
{ type: 'response.output_item.added', item: { type: 'function_call', id: 'fc_2', name: 'grep', call_id: 'call_2' } },
|
|
100
|
+
{ type: 'response.function_call_arguments.delta', item_id: 'fc_2', delta: '{"pattern"' },
|
|
101
|
+
]);
|
|
102
|
+
await assert.rejects(p, /inter-chunk inactivity/);
|
|
103
|
+
assert.equal(socket.closed?.reason, 'inter_chunk_timeout', 'partial second tool must use stall path, not early settle');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('ws early settle: complete tool_search_call clears in-flight and can early-settle', async () => {
|
|
107
|
+
const socket = new FakeSocket();
|
|
108
|
+
const state = {};
|
|
109
|
+
const p = _streamResponse({ entry: { socket }, state, _timeouts: FAST_TIMEOUTS });
|
|
110
|
+
socket.feed([
|
|
111
|
+
{ type: 'response.created', response: { id: 'resp_1', model: 'gpt-5.5' } },
|
|
112
|
+
{ type: 'response.output_item.added', item: { type: 'tool_search_call', id: 'ts_1' } },
|
|
113
|
+
{ type: 'response.output_item.done', item: { type: 'tool_search_call', id: 'ts_1', arguments: '{"query":"fetch"}' } },
|
|
114
|
+
]);
|
|
115
|
+
const result = await p;
|
|
116
|
+
assert.equal(result.closeSocket, true);
|
|
117
|
+
assert.equal(result.toolCalls[0].id, 'ts_1');
|
|
118
|
+
assert.equal(result.toolCalls[0].name, 'load_tool');
|
|
119
|
+
assert.deepEqual(result.toolCalls[0].arguments, { query: 'fetch' });
|
|
120
|
+
assert.equal(state.wsEarlySettle, 'inter_chunk');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('ws early settle: complete tool_search_call plus partial custom tool does NOT early-settle', async () => {
|
|
124
|
+
const socket = new FakeSocket();
|
|
125
|
+
const p = _streamResponse({ entry: { socket }, state: {}, _timeouts: FAST_TIMEOUTS });
|
|
126
|
+
socket.feed([
|
|
127
|
+
{ type: 'response.created', response: { id: 'resp_1', model: 'gpt-5.5' } },
|
|
128
|
+
{ type: 'response.output_item.added', item: { type: 'tool_search_call', id: 'ts_1' } },
|
|
129
|
+
{ type: 'response.output_item.done', item: { type: 'tool_search_call', id: 'ts_1', arguments: '{"query":"fetch"}' } },
|
|
130
|
+
{ type: 'response.output_item.added', item: { type: 'custom_tool_call', id: 'ct_2', call_id: 'call_custom_2', name: 'apply_patch' } },
|
|
131
|
+
{ type: 'response.custom_tool_call_input.delta', item_id: 'ct_2', delta: '*** Begin Patch' },
|
|
132
|
+
]);
|
|
133
|
+
await assert.rejects(p, /inter-chunk inactivity/);
|
|
134
|
+
assert.equal(socket.closed?.reason, 'inter_chunk_timeout', 'partial custom tool must keep stall path');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('ws early settle: output_item.done salvages deferred function_call and can early-settle', async () => {
|
|
138
|
+
const socket = new FakeSocket();
|
|
139
|
+
const p = _streamResponse({ entry: { socket }, state: {}, _timeouts: FAST_TIMEOUTS });
|
|
140
|
+
socket.feed([
|
|
141
|
+
{ type: 'response.created', response: { id: 'resp_1', model: 'gpt-5.5' } },
|
|
142
|
+
{ type: 'response.function_call_arguments.delta', item_id: 'fc_salvage', delta: '{"query"' },
|
|
143
|
+
{ type: 'response.function_call_arguments.done', item_id: 'fc_salvage', arguments: '{"query":"x"}' },
|
|
144
|
+
{ type: 'response.output_item.done', item: { type: 'function_call', id: 'fc_salvage', name: 'explore', call_id: 'call_salvage', arguments: '{"query":"x"}' } },
|
|
145
|
+
]);
|
|
146
|
+
const result = await p;
|
|
147
|
+
assert.equal(result.closeSocket, true);
|
|
148
|
+
assert.equal(result.toolCalls[0].id, 'call_salvage');
|
|
149
|
+
assert.equal(result.toolCalls[0].name, 'explore');
|
|
150
|
+
assert.deepEqual(result.toolCalls[0].arguments, { query: 'x' });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('ws early settle: call_id-only tool_search done clears id-tracked active item', async () => {
|
|
154
|
+
const socket = new FakeSocket();
|
|
155
|
+
const p = _streamResponse({ entry: { socket }, state: {}, _timeouts: FAST_TIMEOUTS });
|
|
156
|
+
socket.feed([
|
|
157
|
+
{ type: 'response.created', response: { id: 'resp_1', model: 'gpt-5.5' } },
|
|
158
|
+
{ type: 'response.output_item.added', item: { type: 'tool_search_call', id: 'ts_added', call_id: 'ts_call' } },
|
|
159
|
+
{ type: 'response.output_item.done', item: { type: 'tool_search_call', call_id: 'ts_call', arguments: '{"query":"fetch"}' } },
|
|
160
|
+
]);
|
|
161
|
+
const result = await p;
|
|
162
|
+
assert.equal(result.closeSocket, true);
|
|
163
|
+
assert.equal(result.toolCalls[0].id, 'ts_call');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('ws early settle: function args.done without item.done does NOT early-settle', async () => {
|
|
167
|
+
const socket = new FakeSocket();
|
|
168
|
+
const p = _streamResponse({ entry: { socket }, state: {}, _timeouts: FAST_TIMEOUTS });
|
|
169
|
+
socket.feed([
|
|
170
|
+
{ type: 'response.created', response: { id: 'resp_1', model: 'gpt-5.5' } },
|
|
171
|
+
{ type: 'response.output_item.added', item: { type: 'function_call', id: 'fc_empty', name: 'explore', call_id: 'call_empty' } },
|
|
172
|
+
{ type: 'response.function_call_arguments.done', item_id: 'fc_empty', arguments: '{}', call_id: 'call_empty', name: 'explore' },
|
|
173
|
+
]);
|
|
174
|
+
await assert.rejects(p, /inter-chunk inactivity/);
|
|
175
|
+
assert.equal(socket.closed?.reason, 'inter_chunk_timeout', 'function item must stay active until output_item.done');
|
|
176
|
+
});
|
|
@@ -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(!/^(
|
|
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(/[.!?。!?]
|
|
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: '
|
|
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 "
|
|
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
|
|
119
|
-
implementation: '-
|
|
120
|
-
crowded: '-
|
|
121
|
-
blocker: '`scripts/output-style-smoke.mjs
|
|
122
|
-
semicolon: '
|
|
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: '
|
|
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: '
|
|
134
|
-
timing: 'Mapped for 2m 32s\n\ndefault
|
|
135
|
-
tooManySentences: '
|
|
136
|
-
mixed: '
|
|
137
|
-
tooManyBullets: '-
|
|
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;
|