@harness-mix/cli 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2 — 2026-09-18
4
+
5
+ - Aligns all six `@harness-mix/native-*` runtime packages at 0.2.2 with the CLI and pins `optionalDependencies` to the same version. 0.2.1 shipped with pins still at 0.1.11, so a fresh install's postinstall could overwrite the bundled fresh Shim binaries with the old-contract 0.1.11 ones; supersedes 0.2.1. Native binaries are rebuilt from unchanged Rust sources.
6
+
3
7
  ## 0.2.1 — 2026-09-18
4
8
 
5
9
  - Turn transcripts now match Codex's native three-stage experience: opening remarks and inter-tool commentary stream live as progress-phase agent messages alongside command rows while the turn runs, and on completion Codex Desktop folds every progress segment and tool item under the elapsed-time bar, leaving only the final-phase conclusion visible. The previous buffer-and-reclassify-as-reasoning approach (which hid narration while running) is removed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-mix/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "license": "(Apache-2.0 OR MIT)",
5
5
  "description": "Harness Mix native Codex UI bridge for Codex, Pi, Claude Code, DeepSeek Harness, Antigravity, CodeBuddy, Kiro CLI, Cursor CLI and other native coding harnesses.",
6
6
  "keywords": [
@@ -182,11 +182,11 @@
182
182
  "zod": "4.4.3"
183
183
  },
184
184
  "optionalDependencies": {
185
- "@harness-mix/native-win32-x64": "0.1.11",
186
- "@harness-mix/native-win32-arm64": "0.1.11",
187
- "@harness-mix/native-darwin-x64": "0.1.11",
188
- "@harness-mix/native-darwin-arm64": "0.1.11",
189
- "@harness-mix/native-linux-x64": "0.1.11",
190
- "@harness-mix/native-linux-arm64": "0.1.11"
185
+ "@harness-mix/native-win32-x64": "0.2.2",
186
+ "@harness-mix/native-win32-arm64": "0.2.2",
187
+ "@harness-mix/native-darwin-x64": "0.2.2",
188
+ "@harness-mix/native-darwin-arm64": "0.2.2",
189
+ "@harness-mix/native-linux-x64": "0.2.2",
190
+ "@harness-mix/native-linux-arm64": "0.2.2"
191
191
  }
192
192
  }
@@ -26,7 +26,7 @@ const { createWorkspace, inspectWorkspace, reviewWorkspace, applyWorkspace, remo
26
26
  * 仍由其原生程序维护,Adapter 只负责原生协议接入与事件转换。
27
27
  */
28
28
  class HostRuntime {
29
- constructor({ dataDirectory, observer = null, stuckTurnMs = 15 * 60 * 1000, stuckSweepMs = 60 * 1000 }) {
29
+ constructor({ dataDirectory, observer = null, stuckTurnMs = 15 * 60 * 1000, stuckSweepMs = 60 * 1000, delegationTimeoutMs = 30 * 60 * 1000 }) {
30
30
  this.store = new ThreadStore(dataDirectory);
31
31
  this.threads = [];
32
32
  this.sessions = new Map(); // threadId -> { adapter, ...session }
@@ -41,9 +41,14 @@ class HostRuntime {
41
41
  this.reviewTasks = new Set();
42
42
  this.openings = new Map();
43
43
  this.sending = new Set();
44
- // send 进行中被 cancel 的线程:Turn 尚未开始或 prompt 尚未投递时 abort 无从生效,
45
- // #send Turn 启动后/投递前据此结算取消,避免用户看不见的僵尸运行
46
- this.cancelRequests = new Set();
44
+ // threadId -> 在途 send 的票据:cancel 会提前放锁让新发送进入,
45
+ // send 退出时凭票据比对,避免其 finally 误删新发送的锁
46
+ this.sendTickets = new Map();
47
+ this.sendTicketSeq = 0;
48
+ // send 进行中被 cancel 的线程(threadId -> 被取消 send 的票据):Turn 尚未开始或
49
+ // prompt 尚未投递时 abort 无从生效,由 #send 在 Turn 启动后/投递前据此结算取消,
50
+ // 避免用户看不见的僵尸运行;票据不匹配的陈旧登记属上一代发送,由 #send 入口清理
51
+ this.cancelRequests = new Map();
47
52
  this.switching = new Set();
48
53
  // 同目录并发会话警告的去重记录:threadId -> 上次提醒时间
49
54
  this.concurrentCwdNotified = new Map();
@@ -52,6 +57,8 @@ class HostRuntime {
52
57
  // 即按超时结算,让 UI 停转、审查快照收尾。等待用户审批的回合属合法静默。
53
58
  this.turnActivity = new Map();
54
59
  this.stuckTurnMs = stuckTurnMs;
60
+ // /delegate 委派等待子任务结算的上限(对齐协作编排的 30 分钟),注入以便测试
61
+ this.delegationTimeoutMs = delegationTimeoutMs;
55
62
  this.watchdogTimer = setInterval(() => this.#sweepStuckTurns(), stuckSweepMs);
56
63
  this.watchdogTimer.unref?.();
57
64
  // 跨 Harness 协作:parentThreadId -> { childId, toolCallId, cancelled }
@@ -320,14 +327,26 @@ class HostRuntime {
320
327
  // Reserve before opening a native session: two submissions can otherwise both
321
328
  // pass isRunning() while awaiting the same opening promise.
322
329
  if (this.sending.has(threadId)) throw new Error('任务正在执行,请先停止或等待完成');
330
+ const ticket = ++this.sendTicketSeq;
323
331
  this.sending.add(threadId);
324
- try { return await this.#send(threadId, text, { commandId, attachments, delegateOf, collaborationOf, isolated, turnPermissions }); }
325
- finally { this.sending.delete(threadId); this.cancelRequests.delete(threadId); }
332
+ this.sendTickets.set(threadId, ticket);
333
+ try { return await this.#send(threadId, text, { commandId, attachments, delegateOf, collaborationOf, isolated, turnPermissions, ticket }); }
334
+ // cancel() 会提前放锁并让新发送进入:仅当票据仍归本次发送时才回收,否则
335
+ // 这里的 delete 会误删新发送的锁,使第三个发送与在途发送并发撞车
336
+ finally {
337
+ if (this.sendTickets.get(threadId) === ticket) {
338
+ this.sending.delete(threadId);
339
+ this.sendTickets.delete(threadId);
340
+ this.cancelRequests.delete(threadId);
341
+ }
342
+ }
326
343
  }
327
344
 
328
- async #send(threadId, text, { commandId, attachments, delegateOf, collaborationOf, isolated, turnPermissions }) {
345
+ async #send(threadId, text, { commandId, attachments, delegateOf, collaborationOf, isolated, turnPermissions, ticket }) {
329
346
  const thread = this.#requireThread(threadId);
330
347
  if (this.execution.isRunning(thread.id)) throw new Error("任务正在执行,请先停止或等待完成");
348
+ // 清理上一代发送遗留的取消登记(票据不匹配说明属于已结算的旧 send)
349
+ if (this.cancelRequests.get(threadId) !== ticket) this.cancelRequests.delete(threadId);
331
350
  const prepared = this.#prepareAttachments(thread, attachments);
332
351
  const typed = typeof text === "string" ? text.trim() : "";
333
352
  if (!typed && !prepared.images.length && !prepared.texts.length) throw new Error("请输入消息");
@@ -430,7 +449,10 @@ class HostRuntime {
430
449
  delete thread.errorKind;
431
450
  this.execution.turnStarted(thread, displayPrompt);
432
451
  // 会话打开/prompt 组装期间用户已按停止(cancel 时 Turn 尚未开始,原生侧无从 abort):直接结算取消,不再投递
433
- if (this.cancelRequests.delete(thread.id)) this.#applyEvent({ threadId, event: { kind: 'completed', stopReason: 'cancelled' } });
452
+ if (this.cancelRequests.get(thread.id) === ticket) {
453
+ this.cancelRequests.delete(thread.id);
454
+ this.#applyEvent({ threadId, event: { kind: 'completed', stopReason: 'cancelled' } });
455
+ }
434
456
  this.turnActivity.set(thread.id, Date.now());
435
457
  const message = thread.messages.at(-1);
436
458
  if (hasConcurrentTurn) message.concurrent = true;
@@ -548,12 +570,23 @@ class HostRuntime {
548
570
  let answer = '';
549
571
  try {
550
572
  await this.send(child.id, task, { delegateOf: parent.id });
551
- // Adapter 返回≠原生 Turn 完全结算,兜底等待至子任务真正空闲
552
- const grace = Date.now() + 30_000;
553
- while (this.execution.isRunning(child.id) && Date.now() < grace) await new Promise(resolve => setTimeout(resolve, 100));
554
- const lastTurn = this.execution.lastTurn(child.id);
555
- if (lastTurn?.status === 'error') failure = new Error(lastTurn.error || '子任务执行失败');
556
- else answer = this.#turnFinalText(child.id);
573
+ // Adapter 返回≠原生 Turn 完全结算(Pi 等非阻塞适配器收到 prompt ack 即返回,实际
574
+ // 执行由异步流驱动),有界等待至子任务真正空闲——对齐协作编排的 30 分钟上限,
575
+ // 超时主动取消子任务;已 wedge 的子任务由看门狗(stuckTurnMs)提前按 error 结算。
576
+ const until = Date.now() + this.delegationTimeoutMs;
577
+ while (this.execution.isRunning(child.id) && !delegation?.cancelled) {
578
+ if (Date.now() > until) {
579
+ await this.cancel(child.id);
580
+ failure = new Error(`子任务超过 ${Math.round(this.delegationTimeoutMs / 60000)} 分钟未结算,已自动取消`);
581
+ break;
582
+ }
583
+ await new Promise(resolve => setTimeout(resolve, 100));
584
+ }
585
+ if (!failure) {
586
+ const lastTurn = this.execution.lastTurn(child.id);
587
+ if (lastTurn?.status === 'error') failure = new Error(lastTurn.error || '子任务执行失败');
588
+ else answer = this.#turnFinalText(child.id);
589
+ }
557
590
  } catch (error) { failure = error; }
558
591
  const cancelled = delegation?.cancelled;
559
592
  this.delegations.delete(parent.id);
@@ -615,8 +648,8 @@ class HostRuntime {
615
648
 
616
649
  async cancel(threadId) {
617
650
  const thread = this.threads.find(t => t.id === threadId);
618
- // send 进行中(会话恢复 / prompt 尚未投递)时 abort 可能落空:登记取消请求,由 #send 在投递前结算
619
- if (this.sending.has(threadId)) this.cancelRequests.add(threadId);
651
+ // send 进行中(会话恢复 / prompt 尚未投递)时 abort 可能落空:按票据登记取消请求,由 #send 在投递前结算
652
+ if (this.sending.has(threadId)) this.cancelRequests.set(threadId, this.sendTickets.get(threadId));
620
653
  // If this thread is a collaboration child task, mark the collaboration job as cancelled immediately
621
654
  const childJob = [...this.collaboration.jobs.values()].find(j => j.childId === threadId && j.status === 'running');
622
655
  if (childJob) {
@@ -625,7 +658,9 @@ class HostRuntime {
625
658
  }
626
659
  // Record the user's cancellation immediately so UI and Core become idle without waiting on subtasks
627
660
  if (thread && this.execution.isRunning(thread.id)) this.#applyEvent({ threadId, event: { kind: 'completed', stopReason: 'cancelled' } });
661
+ // 立即放锁让用户可以重发;同时作废旧 send 的票据,其 finally 不得误删新发送的锁
628
662
  this.sending.delete(threadId);
663
+ this.sendTickets.delete(threadId);
629
664
  // Cancel child collaboration tasks with a hard timeout to prevent child hangs
630
665
  try {
631
666
  await Promise.race([
@@ -765,6 +800,8 @@ class HostRuntime {
765
800
  } else this.sessions.set(thread.id, attachSession(adapter, session, thread.id));
766
801
  await this.#save();
767
802
  this.#broadcast();
803
+ // 与 createThread 同一契约:通知 Desktop 侧边栏实时挂载分支会话(protocol.js 据此发 thread/started)
804
+ for (const listener of this.listeners) listener({ type: 'thread-created', thread });
768
805
  return thread;
769
806
  }
770
807
 
@@ -1179,6 +1216,11 @@ class HostRuntime {
1179
1216
  const last = this.turnActivity.get(thread.id) ?? this.execution.lastTurn(thread.id)?.createdAt ?? now;
1180
1217
  if (now - last < this.stuckTurnMs) continue;
1181
1218
  this.turnActivity.delete(thread.id);
1219
+ // 级联取消原生会话:仅结算 Turn 而不 abort,原生进程(死循环脚本/挂起的长连接)会
1220
+ // 常驻后台,下一次发送直接撞上原生侧的会话占用报错(如 Pi 的 already processing),
1221
+ // 该 Thread 永久无法恢复。fire-and-forget 不阻塞扫描;迟到事件由 execution.apply 忽略。
1222
+ const session = this.sessions.get(thread.id);
1223
+ if (session) void Promise.race([session.adapter.cancel(session), new Promise(r => setTimeout(r, 2000))]).catch(() => {});
1182
1224
  this.#applyEvent({ threadId: thread.id, event: { kind: 'error', timestamp: now,
1183
1225
  message: `原生会话超过 ${Math.round(this.stuckTurnMs / 60000)} 分钟未产生任何事件,判定为卡死并已自动结算。如原生进程仍在运行,可手动取消或开启新回合。` } });
1184
1226
  }
@@ -1154,7 +1154,11 @@ class NativeProtocol {
1154
1154
  const pending = this.approvals.get(message.id);
1155
1155
  if (!pending) return false;
1156
1156
  if (message.error) throw new Error(message.error.message || 'Approval UI error');
1157
- const answer = message.result?.answers?.[pending.requestId]?.answers?.[0];
1157
+ // Desktop answers 为 string[]:多选提问(如 ACP/OpenCode multiple)勾选多项时
1158
+ // 只取 [0] 会无声吞掉其余选项,且 opencode 对 multiple 题执行 JSON.parse(response.value),
1159
+ // 单值字符串会直接抛 SyntaxError——多答案 JSON 编码全量保留,单答案维持原字符串。
1160
+ const rawAnswers = message.result?.answers?.[pending.requestId]?.answers;
1161
+ const answer = Array.isArray(rawAnswers) && rawAnswers.length > 1 ? JSON.stringify(rawAnswers) : (rawAnswers?.[0] ?? '');
1158
1162
  const decision = message.result?.decision;
1159
1163
  const response = pending.item.type === 'question' ? { value: answer || '' } : pending.item.method === 'select' ? { value: answer || '' } : { confirmed: decision === 'accept' || decision === 'acceptForSession' };
1160
1164
  await this.runtime.respondApproval(pending.threadId, pending.requestId, response);