@harness-mix/cli 0.2.3 → 0.2.4

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 (34) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +469 -467
  3. package/output/native-build/desktop-controller.mjs +1 -1
  4. package/output/native-build/renderer-extension.js +23 -4
  5. package/package.json +11 -9
  6. package/scripts/antigravity-adapter-test.cjs +647 -626
  7. package/scripts/codex-adapter-test.cjs +162 -127
  8. package/scripts/collaboration-test.cjs +274 -262
  9. package/scripts/jsonl-stdin-test.cjs +40 -31
  10. package/scripts/kiro-cursor-adapters-test.cjs +124 -100
  11. package/scripts/native-acp-depth-test.cjs +30 -5
  12. package/scripts/native-update-apply-test.cjs +269 -215
  13. package/scripts/native-update.cjs +78 -0
  14. package/scripts/native-vendor-adapters-test.cjs +196 -154
  15. package/scripts/salvage-rollout-writes.cjs +72 -0
  16. package/scripts/zcode-adapter-test.cjs +329 -0
  17. package/scripts/zcode-live-probe.cjs +66 -0
  18. package/src/main/adapters/antigravity.js +1428 -1418
  19. package/src/main/adapters/codex.js +656 -649
  20. package/src/main/adapters/native-acp-command.js +51 -48
  21. package/src/main/adapters/native-acp.js +47 -12
  22. package/src/main/adapters/qoder.js +12 -8
  23. package/src/main/adapters/zcode.js +921 -10
  24. package/src/main/host/collaboration.js +723 -715
  25. package/src/main/host/jsonl.js +130 -120
  26. package/src/main/native/config.js +9 -9
  27. package/src/main/native/launcher.js +252 -237
  28. package/src/main/native/process-utils.js +157 -57
  29. package/src/main/native/protocol.js +1221 -1187
  30. package/src/main/native/update-state.js +123 -110
  31. package/src/main/native/updater.js +460 -394
  32. package/src/native-ui/desktop-control/src/renderer-cdp-control-session.ts +358 -358
  33. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3211 -3181
  34. package/src/native-ui/renderer-extension/src/settings/connections-page.ts +2 -2
@@ -1,127 +1,162 @@
1
- const assert = require('node:assert/strict');
2
- const { projectNotification, queueRequest, usageView, modelView } = require('../src/main/adapters/codex');
3
-
4
- function fakeSession() {
5
- return {
6
- nativeSessionId: 'thread-native',
7
- pendingApprovals: new Map(),
8
- state: {
9
- turn: null, compaction: null, nativeTurnId: null, usage: undefined,
10
- itemText: new Map(), reasoningItems: new Set(), toolOutput: new Map(),
11
- },
12
- };
13
- }
14
-
15
- (async () => {
16
- const session = fakeSession();
17
- const events = [];
18
- const emit = event => events.push(event);
19
- projectNotification({ method: 'turn/started', params: { threadId: 'thread-native', turn: { id: 'turn-1' } } }, session, emit);
20
- projectNotification({ method: 'item/agentMessage/delta', params: { threadId: 'thread-native', turnId: 'turn-1', itemId: 'answer-1', delta: '你好' } }, session, emit);
21
- projectNotification({ method: 'item/reasoning/summaryTextDelta', params: { threadId: 'thread-native', turnId: 'turn-1', itemId: 'reason-1', delta: '分析' } }, session, emit);
22
- projectNotification({ method: 'item/started', params: { threadId: 'thread-native', turnId: 'turn-1', item: { type: 'commandExecution', id: 'tool-1', command: 'git status', status: 'inProgress' } } }, session, emit);
23
- projectNotification({ method: 'item/commandExecution/outputDelta', params: { threadId: 'thread-native', turnId: 'turn-1', itemId: 'tool-1', delta: 'clean' } }, session, emit);
24
- projectNotification({ method: 'item/completed', params: { threadId: 'thread-native', turnId: 'turn-1', item: { type: 'commandExecution', id: 'tool-1', command: 'git status', status: 'completed', aggregatedOutput: 'clean' } } }, session, emit);
25
- projectNotification({ method: 'thread/tokenUsage/updated', params: { threadId: 'thread-native', turnId: 'turn-1', tokenUsage: { last: { totalTokens: 250 }, total: { inputTokens: 200, outputTokens: 50, cachedInputTokens: 25 }, modelContextWindow: 1000 } } }, session, emit);
26
- projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'turn-1', status: 'completed' } } }, session, emit);
27
-
28
- assert.ok(events.some(event => event.kind === 'text-delta' && event.text === '你好'));
29
- assert.ok(events.some(event => event.kind === 'thinking-delta' && event.text === '分析'));
30
- assert.ok(events.some(event => event.kind === 'tool' && event.toolCallId === 'tool-1' && event.state === 'done' && event.title === 'exec_command' && event.input === 'git status'));
31
- assert.ok(events.some(event => event.kind === 'usage' && event.usage.contextPercent === 25));
32
- assert.ok(events.some(event => event.kind === 'completed' && event.nativeRef.checkpointId === 'turn-1'));
33
-
34
- const questionSession = fakeSession();
35
- const questions = [];
36
- const answer = queueRequest({ id: 7, method: 'item/tool/requestUserInput', params: {
37
- threadId: 'thread-native', turnId: 'turn-1', itemId: 'question-1', isBlocking: true,
38
- questions: [
39
- { id: 'framework', header: '框架', question: '选择框架', options: [{ label: 'React' }, { label: 'Vue' }] },
40
- { id: 'name', header: '名称', question: '输入名称', options: null },
41
- ],
42
- } }, questionSession, event => questions.push(event));
43
- assert.equal(questions.length, 2);
44
- const adapter = require('../src/main/adapters/codex').create();
45
- await adapter.respond(questionSession, 'codex-7-framework', { optionId: 'React' });
46
- await adapter.respond(questionSession, 'codex-7-name', { value: 'Harness Mix' });
47
- assert.deepEqual(await answer, { answers: { framework: { answers: ['React'] }, name: { answers: ['Harness Mix'] } } });
48
-
49
- const approvalSession = fakeSession();
50
- const approval = queueRequest({ id: 8, method: 'item/commandExecution/requestApproval', params: {
51
- threadId: 'thread-native', turnId: 'turn-1', itemId: 'tool-2', command: 'npm test', availableDecisions: ['accept', 'decline'],
52
- } }, approvalSession, () => {});
53
- await adapter.respond(approvalSession, 'codex-8', { optionId: 'accept' });
54
- assert.deepEqual(await approval, { decision: 'accept' });
55
- const mcpSession = fakeSession();
56
- const mcpEvents = [];
57
- const mcpApproval = queueRequest({ id: 9, method: 'mcpServer/elicitation/request', params: {
58
- threadId: 'thread-native', serverName: 'harness-mix', mode: 'form', message: 'Allow tool?',
59
- requestedSchema: { type: 'object', properties: {}, additionalProperties: false },
60
- } }, mcpSession, event => mcpEvents.push(event));
61
- assert.equal(mcpEvents[0].kind, 'approval');
62
- assert.equal(mcpSession.pendingApprovals.size, 1, 'MCP requests await a native user response');
63
- await adapter.respond(mcpSession, 'codex-9', { optionId: 'decline' });
64
- assert.deepEqual(await mcpApproval, { action: 'decline', content: null });
65
-
66
- const compactSession = fakeSession();
67
- const compactEvents = [];
68
- compactSession.host = { async request(method) {
69
- assert.equal(method, 'thread/compact/start');
70
- projectNotification({ method: 'turn/started', params: { threadId: 'thread-native', turn: { id: 'compact-turn' } } }, compactSession, event => compactEvents.push(event));
71
- projectNotification({ method: 'item/completed', params: { threadId: 'thread-native', turnId: 'compact-turn', item: { type: 'contextCompaction', id: 'compact-item' } } }, compactSession, event => compactEvents.push(event));
72
- projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'compact-turn', status: 'completed' } } }, compactSession, event => compactEvents.push(event));
73
- return {};
74
- } };
75
- await adapter.executeCommand(compactSession, 'compact', { emit: event => compactEvents.push(event) });
76
- const compactDone = compactEvents.find(event => event.kind === 'completed');
77
- assert.equal(compactDone.nativeRef.checkpointId, 'compact-turn');
78
- assert.ok(compactEvents.some(event => event.kind === 'text-delta' && /Codex 压缩/.test(event.text)));
79
-
80
- assert.equal(usageView({ last: { totalTokens: 50 }, total: {}, modelContextWindow: 200 }).contextPercent, 25);
81
- assert.deepEqual(modelView({ model: 'gpt-x', displayName: 'GPT X', supportedReasoningEfforts: [], isDefault: true }).id, 'gpt-x');
82
-
83
- // 权限透传:Desktop 选择(回合级)优先,缺失时回退线程级设置,均转发到原生 turn/start
84
- const sendSession = fakeSession();
85
- sendSession.model = { id: 'gpt-x' };
86
- sendSession.turnPermissions = { approvalPolicy: 'on-request', sandboxPolicy: { type: 'readOnly' } };
87
- const sentRequests = [];
88
- sendSession.host = { async request(method, params) { sentRequests.push({ method, params }); return { turn: { id: `turn-${sentRequests.length}` } }; } };
89
- const first = adapter.send(sendSession, '第一轮', {}, { images: [], turnPermissions: { approvalPolicy: 'never', approvalsReviewer: 'guardian', sandboxPolicy: { type: 'dangerFullAccess' } } });
90
- const turnStart = sentRequests.find(r => r.method === 'turn/start');
91
- assert.equal(turnStart.params.approvalPolicy, 'never', '回合级权限覆盖优先转发');
92
- assert.equal(turnStart.params.approvalsReviewer, 'guardian_subagent', 'Desktop 的 guardian 归一化为原生枚举');
93
- assert.deepEqual(turnStart.params.sandboxPolicy, { type: 'dangerFullAccess' });
94
- projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'turn-1', status: 'completed' } } }, sendSession, () => {});
95
- await first;
96
- sentRequests.length = 0;
97
- const second = adapter.send(sendSession, '第二轮', {}, { images: [] });
98
- const fallbackStart = sentRequests.find(r => r.method === 'turn/start');
99
- assert.equal(fallbackStart.params.approvalPolicy, 'on-request', '无线索级覆盖时回退线程级设置');
100
- assert.deepEqual(fallbackStart.params.sandboxPolicy, { type: 'readOnly' });
101
- projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'turn-1', status: 'completed' } } }, sendSession, () => {});
102
- await second;
103
-
104
- // Queue resume can race app-server's active-turn cleanup. Retry only that transient
105
- // without surfacing a failed Core turn or duplicating unrelated failures.
106
- const retrySession = fakeSession();
107
- let startAttempts = 0;
108
- retrySession.host = { async request(method) {
109
- assert.equal(method, 'turn/start');
110
- startAttempts++;
111
- if (startAttempts < 3) throw new Error("Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.");
112
- return { turn: { id: 'turn-after-settlement' } };
113
- } };
114
- const retried = adapter.send(retrySession, '编辑后的排队消息', {}, { images: [] });
115
- while (startAttempts < 3) await new Promise(resolve => setTimeout(resolve, 10));
116
- assert.equal(retrySession.state.nativeTurnId, 'turn-after-settlement');
117
- projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'turn-after-settlement', status: 'completed' } } }, retrySession, () => {});
118
- await retried;
119
- assert.equal(startAttempts, 3, 'Only the transient app-server busy race is retried');
120
-
121
- const fatalSession = fakeSession();
122
- let fatalAttempts = 0;
123
- fatalSession.host = { async request() { fatalAttempts++; throw new Error('Authentication required'); } };
124
- await assert.rejects(adapter.send(fatalSession, '不可重试的错误', {}, { images: [] }), /Authentication required/);
125
- assert.equal(fatalAttempts, 1, 'Unrelated app-server errors are never retried or hidden');
126
- console.log('codex adapter: native notifications, usage, multi-question and approvals passed');
127
- })().catch(error => { console.error(error); process.exitCode = 1; });
1
+ const assert = require('node:assert/strict');
2
+ const { projectNotification, queueRequest, usageView, modelView } = require('../src/main/adapters/codex');
3
+
4
+ function fakeSession() {
5
+ return {
6
+ nativeSessionId: 'thread-native',
7
+ pendingApprovals: new Map(),
8
+ state: {
9
+ turn: null, compaction: null, nativeTurnId: null, usage: undefined,
10
+ itemText: new Map(), reasoningItems: new Set(), toolOutput: new Map(),
11
+ },
12
+ };
13
+ }
14
+
15
+ (async () => {
16
+ const session = fakeSession();
17
+ const events = [];
18
+ const emit = event => events.push(event);
19
+ projectNotification({ method: 'turn/started', params: { threadId: 'thread-native', turn: { id: 'turn-1' } } }, session, emit);
20
+ projectNotification({ method: 'item/agentMessage/delta', params: { threadId: 'thread-native', turnId: 'turn-1', itemId: 'answer-1', delta: '你好' } }, session, emit);
21
+ projectNotification({ method: 'item/reasoning/summaryTextDelta', params: { threadId: 'thread-native', turnId: 'turn-1', itemId: 'reason-1', delta: '分析' } }, session, emit);
22
+ projectNotification({ method: 'item/started', params: { threadId: 'thread-native', turnId: 'turn-1', item: { type: 'commandExecution', id: 'tool-1', command: 'git status', status: 'inProgress' } } }, session, emit);
23
+ projectNotification({ method: 'item/commandExecution/outputDelta', params: { threadId: 'thread-native', turnId: 'turn-1', itemId: 'tool-1', delta: 'clean' } }, session, emit);
24
+ projectNotification({ method: 'item/completed', params: { threadId: 'thread-native', turnId: 'turn-1', item: { type: 'commandExecution', id: 'tool-1', command: 'git status', status: 'completed', aggregatedOutput: 'clean' } } }, session, emit);
25
+ projectNotification({ method: 'thread/tokenUsage/updated', params: { threadId: 'thread-native', turnId: 'turn-1', tokenUsage: { last: { totalTokens: 250 }, total: { inputTokens: 200, outputTokens: 50, cachedInputTokens: 25 }, modelContextWindow: 1000 } } }, session, emit);
26
+ projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'turn-1', status: 'completed' } } }, session, emit);
27
+
28
+ assert.ok(events.some(event => event.kind === 'text-delta' && event.text === '你好'));
29
+ assert.ok(events.some(event => event.kind === 'thinking-delta' && event.text === '分析'));
30
+ assert.ok(events.some(event => event.kind === 'tool' && event.toolCallId === 'tool-1' && event.state === 'done' && event.title === 'exec_command' && event.input === 'git status'));
31
+ assert.ok(events.some(event => event.kind === 'usage' && event.usage.contextPercent === 25));
32
+ assert.ok(events.some(event => event.kind === 'completed' && event.nativeRef.checkpointId === 'turn-1'));
33
+
34
+ const questionSession = fakeSession();
35
+ const questions = [];
36
+ const answer = queueRequest({ id: 7, method: 'item/tool/requestUserInput', params: {
37
+ threadId: 'thread-native', turnId: 'turn-1', itemId: 'question-1', isBlocking: true,
38
+ questions: [
39
+ { id: 'framework', header: '框架', question: '选择框架', options: [{ label: 'React' }, { label: 'Vue' }] },
40
+ { id: 'name', header: '名称', question: '输入名称', options: null },
41
+ ],
42
+ } }, questionSession, event => questions.push(event));
43
+ assert.equal(questions.length, 2);
44
+ const adapter = require('../src/main/adapters/codex').create();
45
+ await adapter.respond(questionSession, 'codex-7-framework', { optionId: 'React' });
46
+ await adapter.respond(questionSession, 'codex-7-name', { value: 'Harness Mix' });
47
+ assert.deepEqual(await answer, { answers: { framework: { answers: ['React'] }, name: { answers: ['Harness Mix'] } } });
48
+
49
+ const approvalSession = fakeSession();
50
+ const approval = queueRequest({ id: 8, method: 'item/commandExecution/requestApproval', params: {
51
+ threadId: 'thread-native', turnId: 'turn-1', itemId: 'tool-2', command: 'npm test', availableDecisions: ['accept', 'decline'],
52
+ } }, approvalSession, () => {});
53
+ await adapter.respond(approvalSession, 'codex-8', { optionId: 'accept' });
54
+ assert.deepEqual(await approval, { decision: 'accept' });
55
+ const mcpSession = fakeSession();
56
+ const mcpEvents = [];
57
+ const mcpApproval = queueRequest({ id: 9, method: 'mcpServer/elicitation/request', params: {
58
+ threadId: 'thread-native', serverName: 'harness-mix', mode: 'form', message: 'Allow tool?',
59
+ requestedSchema: { type: 'object', properties: {}, additionalProperties: false },
60
+ } }, mcpSession, event => mcpEvents.push(event));
61
+ assert.equal(mcpEvents[0].kind, 'approval');
62
+ assert.equal(mcpSession.pendingApprovals.size, 1, 'MCP requests await a native user response');
63
+ await adapter.respond(mcpSession, 'codex-9', { optionId: 'decline' });
64
+ assert.deepEqual(await mcpApproval, { action: 'decline', content: null });
65
+
66
+ const compactSession = fakeSession();
67
+ const compactEvents = [];
68
+ compactSession.host = { async request(method) {
69
+ assert.equal(method, 'thread/compact/start');
70
+ projectNotification({ method: 'turn/started', params: { threadId: 'thread-native', turn: { id: 'compact-turn' } } }, compactSession, event => compactEvents.push(event));
71
+ projectNotification({ method: 'item/completed', params: { threadId: 'thread-native', turnId: 'compact-turn', item: { type: 'contextCompaction', id: 'compact-item' } } }, compactSession, event => compactEvents.push(event));
72
+ projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'compact-turn', status: 'completed' } } }, compactSession, event => compactEvents.push(event));
73
+ return {};
74
+ } };
75
+ await adapter.executeCommand(compactSession, 'compact', { emit: event => compactEvents.push(event) });
76
+ const compactDone = compactEvents.find(event => event.kind === 'completed');
77
+ assert.equal(compactDone.nativeRef.checkpointId, 'compact-turn');
78
+ assert.ok(compactEvents.some(event => event.kind === 'text-delta' && /Codex 压缩/.test(event.text)));
79
+
80
+ assert.equal(usageView({ last: { totalTokens: 50 }, total: {}, modelContextWindow: 200 }).contextPercent, 25);
81
+ assert.deepEqual(modelView({ model: 'gpt-x', displayName: 'GPT X', supportedReasoningEfforts: [], isDefault: true }).id, 'gpt-x');
82
+
83
+ // 权限透传:Desktop 选择(回合级)优先,缺失时回退线程级设置,均转发到原生 turn/start
84
+ const sendSession = fakeSession();
85
+ sendSession.model = { id: 'gpt-x' };
86
+ sendSession.turnPermissions = { approvalPolicy: 'on-request', sandboxPolicy: { type: 'readOnly' } };
87
+ const sentRequests = [];
88
+ sendSession.host = { async request(method, params) { sentRequests.push({ method, params }); return { turn: { id: `turn-${sentRequests.length}` } }; } };
89
+ const first = adapter.send(sendSession, '第一轮', {}, { images: [], turnPermissions: { approvalPolicy: 'never', approvalsReviewer: 'guardian', sandboxPolicy: { type: 'dangerFullAccess' } } });
90
+ const turnStart = sentRequests.find(r => r.method === 'turn/start');
91
+ assert.equal(turnStart.params.approvalPolicy, 'never', '回合级权限覆盖优先转发');
92
+ assert.equal(turnStart.params.approvalsReviewer, 'guardian_subagent', 'Desktop 的 guardian 归一化为原生枚举');
93
+ assert.deepEqual(turnStart.params.sandboxPolicy, { type: 'dangerFullAccess' });
94
+ projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'turn-1', status: 'completed' } } }, sendSession, () => {});
95
+ await first;
96
+ sentRequests.length = 0;
97
+ const second = adapter.send(sendSession, '第二轮', {}, { images: [] });
98
+ const fallbackStart = sentRequests.find(r => r.method === 'turn/start');
99
+ assert.equal(fallbackStart.params.approvalPolicy, 'on-request', '无线索级覆盖时回退线程级设置');
100
+ assert.deepEqual(fallbackStart.params.sandboxPolicy, { type: 'readOnly' });
101
+ projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'turn-1', status: 'completed' } } }, sendSession, () => {});
102
+ await second;
103
+
104
+ // Queue resume can race app-server's active-turn cleanup. Retry only that transient
105
+ // without surfacing a failed Core turn or duplicating unrelated failures.
106
+ const retrySession = fakeSession();
107
+ let startAttempts = 0;
108
+ retrySession.host = { async request(method) {
109
+ assert.equal(method, 'turn/start');
110
+ startAttempts++;
111
+ if (startAttempts < 3) throw new Error("Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.");
112
+ return { turn: { id: 'turn-after-settlement' } };
113
+ } };
114
+ const retried = adapter.send(retrySession, '编辑后的排队消息', {}, { images: [] });
115
+ while (startAttempts < 3) await new Promise(resolve => setTimeout(resolve, 10));
116
+ assert.equal(retrySession.state.nativeTurnId, 'turn-after-settlement');
117
+ projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'turn-after-settlement', status: 'completed' } } }, retrySession, () => {});
118
+ await retried;
119
+ assert.equal(startAttempts, 3, 'Only the transient app-server busy race is retried');
120
+
121
+ const fatalSession = fakeSession();
122
+ let fatalAttempts = 0;
123
+ fatalSession.host = { async request() { fatalAttempts++; throw new Error('Authentication required'); } };
124
+ await assert.rejects(adapter.send(fatalSession, '不可重试的错误', {}, { images: [] }), /Authentication required/);
125
+ assert.equal(fatalAttempts, 1, 'Unrelated app-server errors are never retried or hidden');
126
+
127
+ // cancel() 必须能结算每种挂起的原生请求形状:requestUserInput 的条目经由 group
128
+ // 结算(条目本身没有 resolve,直接调用会 TypeError 并吞掉后续 interrupt)。
129
+ const cancelSession = fakeSession();
130
+ const cancelledAnswer = queueRequest({ id: 21, method: 'item/tool/requestUserInput', params: {
131
+ threadId: 'thread-native', turnId: 'turn-c', itemId: 'q-c', isBlocking: true,
132
+ questions: [{ id: 'pick', header: '选择', question: '选择一项', options: [{ label: 'A' }, { label: 'B' }] }],
133
+ } }, cancelSession, () => {});
134
+ const cancelledApproval = queueRequest({ id: 22, method: 'item/commandExecution/requestApproval', params: {
135
+ threadId: 'thread-native', turnId: 'turn-c', itemId: 't-c', command: 'npm test', availableDecisions: ['accept', 'decline'],
136
+ } }, cancelSession, () => {});
137
+ const cancelledElicitation = queueRequest({ id: 23, method: 'mcpServer/elicitation/request', params: {
138
+ threadId: 'thread-native', serverName: 'harness-mix', mode: 'select', message: 'Allow?',
139
+ } }, cancelSession, () => {});
140
+ const cancelRequests = [];
141
+ cancelSession.host = { request: (method) => { cancelRequests.push(method); return Promise.resolve({}); } };
142
+ cancelSession.state.turn = { resolve() {}, reject() {} };
143
+ await adapter.cancel(cancelSession);
144
+ assert.equal(cancelSession.pendingApprovals.size, 0);
145
+ assert.deepEqual(await cancelledAnswer, { answers: {} }, 'group 条目以空答案结算');
146
+ assert.deepEqual(await cancelledApproval, { decision: 'decline' });
147
+ assert.deepEqual(await cancelledElicitation, { action: 'cancel', content: null });
148
+ assert.equal(cancelRequests.length, 0, 'turn/start 尚在途(无 nativeTurnId)时不发 interrupt');
149
+ assert.equal(cancelSession.state.turn, null, '本地回合被释放,线程不会卡在「当前回合尚未结束」');
150
+
151
+ // nativeTurnId 已知时:interrupt 发出后释放本地回合。
152
+ const interruptSession = fakeSession();
153
+ const interrupts = [];
154
+ interruptSession.host = { request: (method) => { interrupts.push(method); return Promise.resolve({}); } };
155
+ interruptSession.state.nativeTurnId = 'turn-live';
156
+ interruptSession.state.turn = { resolve() {}, reject() {} };
157
+ await adapter.cancel(interruptSession);
158
+ assert.deepEqual(interrupts, ['turn/interrupt']);
159
+ assert.equal(interruptSession.state.turn, null);
160
+
161
+ console.log('codex adapter: native notifications, usage, multi-question, approvals and cancel shapes passed');
162
+ })().catch(error => { console.error(error); process.exitCode = 1; });