@twinsearth/w2m-dsh-plugin 1.6.8 → 1.7.0

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.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "The w2m fleet plugins: one account, many machines, any of Windows, macOS or Linux.",
8
- "version": "1.6.8"
8
+ "version": "1.7.0"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "w2m-fleet",
13
13
  "source": "./",
14
14
  "description": "Read fleet state and dispatch commands across the machines registered to one account. Read-only MCP surface: the five exposed tools cannot change anything.",
15
- "version": "1.6.8",
15
+ "version": "1.7.0",
16
16
  "author": {
17
17
  "name": "TwinsEarth"
18
18
  },
@@ -26,5 +26,5 @@
26
26
  ]
27
27
  }
28
28
  ],
29
- "version": "1.6.8"
29
+ "version": "1.7.0"
30
30
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "w2m-fleet",
3
3
  "description": "Drive the same project on online Windows, macOS and Linux machines from one account: read fleet state, dispatch commands and read results. The MCP surface is deliberately read-only.",
4
- "version": "1.6.8",
4
+ "version": "1.7.0",
5
5
  "author": {
6
6
  "name": "TwinsEarth"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -7,6 +7,45 @@
7
7
 
8
8
  All notable changes to this project are documented here.
9
9
 
10
+ ## [1.7.0] — 2026-10-25
11
+
12
+ **编队聊天框:截图按钮 + Ctrl+P 快捷键 + 对话内容可选中&复制。**
13
+
14
+ - 输入区新增 **📷 截图按钮**:`getDisplayMedia` 让用户选屏幕/窗口 → 抓帧为 PNG → 自动复制到剪贴板,
15
+ 面板内显示缩略预览(可下载/再次复制);宿主不支持或权限被拒时在面板内显示原因,不抛未处理异常。
16
+ - **Ctrl+P(macOS 也支持 Cmd+P)**:聊天面板打开时触发同一截图流程,并在捕获阶段 `preventDefault`,
17
+ 不会弹出浏览器打印对话框。
18
+ - **对话内容可选中&复制**:transcript/消息文本 `user-select:text`;每条消息新增 **⧉ 复制** 按钮;
19
+ 划选文字时点击消息不会误触发"回复"。
20
+
21
+ 已知边界:编队聊天协议仍是纯文本(relay 消息不含图片字段),所以截图是"复制到剪贴板 + 预览/下载",
22
+ 不会作为图片消息广播给其他机器——图片传输需要扩展 relay 消息协议(后续版本)。
23
+
24
+ - 测试:`test/fleet-chat-capture.test.mjs` 3/3;client-bundle 26/26;
25
+ 证据 `docs/acceptance/1.7.0/CHAT-CAPTURE-CONTROLS.json`(fleet task `01M5ANYMVMLRC5H`)。
26
+ ## [1.6.9] — 2026-10-24
27
+
28
+ **修复 failover 监控从未安装的根因:`startLocalStack` 等待了永不 settle 的 `agent.start()`。**
29
+
30
+ 阶段日志(macOS,1.6.8,现场实测):
31
+
32
+ ```
33
+ W wrap enter relayUrls=[腾讯云, Vultr] ← 配置正确
34
+ A startLocalStack enter
35
+ C createAgent done
36
+ S before agent.start ← 之后没有任何日志
37
+ ```
38
+
39
+ agent 实际已上线(编队在线),但 `await agent.start()` 的 Promise 永不返回,导致 `startLocalStack` 不返回 →
40
+ `startFailoverStack` 永远走不到 `relayPool(cfg)` / `setInterval`,**监控循环从未创建**,自动 failover 不可能触发。
41
+
42
+ 修复:
43
+
44
+ - `startLocalStack` 不再 await `agent.start()`:后台启动,失败写日志(`w2m: agent start failed`)。
45
+ - mount 只需要返回清理句柄,不需要"观察启动完成";agent 会通过 relay 自报在线。
46
+ - 新增回归测试 `test/agent-start-nonblocking.test.mjs`:读取源码,若 `startLocalStack` 再次 await
47
+ `agent.start()` 即失败(防止该阻塞回归)。
48
+ - 证据:`docs/acceptance/1.6.9/FAILOVER-MONITOR-INSTALL.json`(fleet task `01M5ANYMVMKL55O`)。
10
49
  ## [1.6.8] — 2026-10-24
11
50
 
12
51
  **插件面板新增「检查插件更新」按钮:npm 对比 → 自动更新 → 提示重启 DSH。**
@@ -12,6 +12,6 @@
12
12
 
13
13
  [mcp_servers.w2m-fleet]
14
14
  command = "npx"
15
- args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.6.8", "w2m-mcp"]
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.7.0", "w2m-mcp"]
16
16
  startup_timeout_sec = 20
17
17
  tool_timeout_sec = 60
@@ -0,0 +1,35 @@
1
+ {
2
+ "version": "1.6.9",
3
+ "case": "FAILOVER-MONITOR-INSTALL",
4
+ "kind": "positive",
5
+ "machines": [
6
+ "b1b19d49-20f1-4cdc-986e-f5630a5cd6f2",
7
+ "b53a37f4-1464-4512-9259-33a7df091e3e"
8
+ ],
9
+ "executed_at": "2026-10-24T15:45:00.621Z",
10
+ "executor": "lead",
11
+ "requirement": "startLocalStack returns so startFailoverStack can install the relay health monitor; the agent start is a background action with recorded failure",
12
+ "root_cause": {
13
+ "observed_on": "macOS / plugin 1.6.8",
14
+ "stage_log": [
15
+ "W wrap enter relayUrls=[primary,backup]",
16
+ "A startLocalStack enter",
17
+ "C createAgent done",
18
+ "S before agent.start",
19
+ "(nothing further — await agent.start() never settled, while the machine was online in the fleet)"
20
+ ],
21
+ "consequence": "startFailoverStack never reached relayPool(cfg)/setInterval, so the failover monitor was never created and automatic relay failover could not engage on any platform with the same agent.start behaviour"
22
+ },
23
+ "fix": {
24
+ "change": "startLocalStack no longer awaits agent.start(); it starts it in the background and logs a failure instead",
25
+ "guard": "test/agent-start-nonblocking.test.mjs reads the emitted source and fails if startLocalStack awaits the start again"
26
+ },
27
+ "task_id": "01M5ANYMVMKL55O",
28
+ "result": {
29
+ "outcome": "ok",
30
+ "assertions": {
31
+ "relay_failover": "5/5",
32
+ "nonblocking_guard": "2/2"
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "version": "1.7.0",
3
+ "case": "CHAT-CAPTURE-CONTROLS",
4
+ "kind": "positive",
5
+ "machines": [
6
+ "b53a37f4-1464-4512-9259-33a7df091e3e"
7
+ ],
8
+ "executed_at": "2026-10-24T16:20:22.367Z",
9
+ "executor": "lead",
10
+ "requirement": "the fleet-chat panel offers a screenshot button and Ctrl+P (Cmd+P) capture, and the transcript is selectable and copyable",
11
+ "task_id": "01M5ANYMVMLRC5H",
12
+ "result": {
13
+ "outcome": "ok",
14
+ "assertions": {
15
+ "fleet_chat_capture": "3/3 pass",
16
+ "client_bundle": "26/26 pass",
17
+ "shortcut": "Ctrl+P/Cmd+P registered in capture phase and prevents the print dialog",
18
+ "selection": "user-select:text on transcript, click-to-reply suppressed while text is selected",
19
+ "screenshot": "getDisplayMedia -> PNG -> clipboard + preview/download",
20
+ "limitation": "the relay chat payload is text-only, so an image is not broadcast as a message; it is copied and previewed locally"
21
+ }
22
+ }
23
+ }
package/lib/client.js CHANGED
@@ -394,6 +394,15 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
394
394
  '.w2mChat_boardRow{font-size:11px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,monospace}',
395
395
  '.w2mChat_threadHead{font-size:11px;color:var(--dsw-alias-label-tertiary);padding:4px 10px 0}',
396
396
  '.w2mChat_unseen{margin-left:6px;min-width:16px;padding:0 5px;border-radius:8px;background:var(--dsw-alias-accent-default);color:var(--dsw-alias-label-on-accent);font-size:10px;line-height:16px;text-align:center}',
397
+ // v1.6.10: the transcript is selectable text; screenshots get a preview and a copy affordance.
398
+ '.w2mChat_panel,.w2mChat_list,.w2mChat_text,.w2mChat_meta{user-select:text;-webkit-user-select:text}',
399
+ '.w2mChat_copy{margin-left:6px;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:11px;line-height:14px;padding:0 2px;vertical-align:middle}',
400
+ '.w2mChat_copy:hover{color:var(--dsw-alias-label-primary)}',
401
+ '.w2mChat_shotButton{flex:none;height:28px;padding:0 8px;border:.5px solid var(--dsw-alias-border-default);border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-surface-default);color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer}',
402
+ '.w2mChat_shotPreview{display:flex;align-items:center;gap:8px;padding:6px 10px;border-top:.5px solid var(--dsw-alias-border-default);font-size:11px;color:var(--dsw-alias-label-secondary)}',
403
+ '.w2mChat_shotPreview img{max-height:56px;max-width:120px;border-radius:4px;border:.5px solid var(--dsw-alias-border-default)}',
404
+ '.w2mChat_shot{margin-left:auto;font-size:11px;color:var(--dsw-alias-label-tertiary)}',
405
+ '.w2mChat_toast{padding:4px 10px;font-size:11px;color:var(--dsw-alias-label-secondary)}',
397
406
  '.w2mChat_text{white-space:pre-wrap;word-break:break-word}',
398
407
  '.w2mChat_msg.is-session .w2mChat_text{color:var(--dsw-danger)}', '.w2mChat_msg.is-chat .w2mChat_text{color:var(--dsw-success)}', '.w2mChat_msg.is-remote .w2mChat_text{color:var(--dsw-text)}', '.w2mChat_avatar{display:inline-flex;width:16px;height:16px;margin-right:5px;vertical-align:-3px}',
399
408
  '.w2mChat_compose{display:flex;gap:8px;padding:8px 10px;border-top:.5px solid var(--dsw-alias-border-l2);align-items:flex-end}',
@@ -813,6 +822,11 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
813
822
  const [error, setError] = React.useState(null);
814
823
  const [busy, setBusy] = React.useState(false);
815
824
  const [localMachineId, setLocalMachineId] = React.useState(null);
825
+ // v1.6.10: screenshot support (button + Ctrl+P). The blob is previewed and copied to the
826
+ // clipboard; the relay's chat payload stays text, so an image cannot be broadcast as a message.
827
+ const [shot, setShot] = React.useState(null);
828
+ const [shotBusy, setShotBusy] = React.useState(false);
829
+ const [toast, setToast] = React.useState(null);
816
830
 
817
831
  const load = React.useCallback(async (signal) => {
818
832
  if (typeof sessionId !== 'string' || sessionId === '') return;
@@ -917,6 +931,80 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
917
931
  }
918
932
  return undefined;
919
933
  }, [busy, load, sessionId, text]);
934
+ // v1.6.10: clipboard text with a fallback for hosts where navigator.clipboard is absent.
935
+ const copyText = React.useCallback((value) => {
936
+ const textValue = String(value ?? '');
937
+ if (textValue === '') return undefined;
938
+ try {
939
+ if (navigator.clipboard?.writeText) {
940
+ navigator.clipboard.writeText(textValue).then(() => setToast('已复制'), () => setToast('复制失败'));
941
+ return undefined;
942
+ }
943
+ } catch { /* fall through to the textarea path */ }
944
+ try {
945
+ const area = document.createElement('textarea');
946
+ area.value = textValue; area.style.position = 'fixed'; area.style.opacity = '0';
947
+ document.body.appendChild(area); area.select(); document.execCommand('copy'); document.body.removeChild(area);
948
+ setToast('已复制');
949
+ } catch { setToast('复制失败'); }
950
+ return undefined;
951
+ }, []);
952
+
953
+ // v1.6.10: capture one frame of a user-picked screen/window, preview it, and copy the PNG to the
954
+ // clipboard. getDisplayMedia needs the gesture the button/keypress provides; every failure is
955
+ // reported in the panel rather than as an unhandled rejection.
956
+ const captureScreenshot = React.useCallback(async () => {
957
+ if (shotBusy) return undefined;
958
+ if (typeof navigator?.mediaDevices?.getDisplayMedia !== 'function') {
959
+ setToast('当前环境不支持屏幕截图');
960
+ return undefined;
961
+ }
962
+ setShotBusy(true); setToast(null);
963
+ let stream = null;
964
+ try {
965
+ stream = await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: 5 }, audio: false });
966
+ const video = document.createElement('video');
967
+ video.srcObject = stream;
968
+ await video.play();
969
+ await new Promise((resolve) => { setTimeout(resolve, 180); });
970
+ const canvas = document.createElement('canvas');
971
+ canvas.width = video.videoWidth || 1280;
972
+ canvas.height = video.videoHeight || 720;
973
+ canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
974
+ const blob = await new Promise((resolve) => { canvas.toBlob(resolve, 'image/png'); });
975
+ if (blob === null) throw new Error('截图编码失败');
976
+ const url = URL.createObjectURL(blob);
977
+ setShot((previous) => { if (previous?.url) URL.revokeObjectURL(previous.url); return { url, blob, at: Date.now() }; });
978
+ try {
979
+ if (navigator.clipboard?.write && typeof ClipboardItem !== 'undefined') {
980
+ await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
981
+ setToast('截图已复制到剪贴板');
982
+ } else { setToast('截图已生成,可点击下载'); }
983
+ } catch { setToast('截图已生成,可点击下载(剪贴板不可用)'); }
984
+ } catch (error) {
985
+ setToast('截图失败:' + (error instanceof Error ? error.message : String(error)));
986
+ } finally {
987
+ try { stream?.getTracks?.().forEach((track) => track.stop()); } catch { /* stream already gone */ }
988
+ setShotBusy(false);
989
+ }
990
+ return undefined;
991
+ }, [shotBusy]);
992
+
993
+ // v1.6.10: Ctrl+P (and Cmd+P on macOS) opens the capture while the panel is open. Registered in
994
+ // the capture phase on purpose: the browser print shortcut must not win here.
995
+ React.useEffect(() => {
996
+ if (!open) return undefined;
997
+ const onKeyDown = (event) => {
998
+ if (event.defaultPrevented) return;
999
+ const modifier = event.ctrlKey === true || event.metaKey === true;
1000
+ if (modifier && event.shiftKey !== true && event.altKey !== true && String(event.key).toLowerCase() === 'p') {
1001
+ event.preventDefault();
1002
+ captureScreenshot();
1003
+ }
1004
+ };
1005
+ document.addEventListener('keydown', onKeyDown, true);
1006
+ return () => document.removeEventListener('keydown', onKeyDown, true);
1007
+ }, [open, captureScreenshot]);
920
1008
 
921
1009
  const unseenCount = messages.reduce((count, message) => (Number.isFinite(message?.at_ms) && message.at_ms > lastSeenMs ? count + 1 : count), 0);
922
1010
  const disabled = typeof sessionId !== 'string' || sessionId === '';
@@ -975,14 +1063,20 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
975
1063
  group.threadId === null ? null : h('div', { key: 'thread:' + group.threadId + ':' + groupIndex, className: 'w2mChat_threadHead' }, '#' + group.threadId),
976
1064
  ...group.messages.map((message) => {
977
1065
  var kind = messageKind(message, localMachineId);
978
- return h('div', { key: message?.message_id ?? `${message?.machine_id}:${message?.at_ms}`, className: 'w2mChat_msg is-' + kind, onClick: () => setReplyTo({ id: message?.message_id ?? null, threadId: message?.thread_id ?? null, text: typeof message?.text === 'string' ? message.text : '' }) },
979
- h('div', { className: 'w2mChat_meta' }, h(WhaleAvatar, { machineId: message?.machine_id }), `${machineLabel(message)} · ${timeLabel(message)}`, kind === 'session' ? ' · 来自会话' : '', message?.thread_id ? h('span', { className: 'w2mChat_thread' }, '#' + message.thread_id) : null),
1066
+ return h('div', { key: message?.message_id ?? `${message?.machine_id}:${message?.at_ms}`, className: 'w2mChat_msg is-' + kind, onClick: () => { const selection = typeof window.getSelection === 'function' ? String(window.getSelection() ?? '') : ''; if (selection !== '') return; setReplyTo({ id: message?.message_id ?? null, threadId: message?.thread_id ?? null, text: typeof message?.text === 'string' ? message.text : '' }); } },
1067
+ h('div', { className: 'w2mChat_meta' }, h(WhaleAvatar, { machineId: message?.machine_id }), `${machineLabel(message)} · ${timeLabel(message)}`, kind === 'session' ? ' · 来自会话' : '', h('button', { key: 'copy', type: 'button', className: 'w2mChat_copy', title: '复制这条消息', onClick: (event) => { event.stopPropagation(); copyText(String(message?.text ?? '')); } }, '⧉'), message?.thread_id ? h('span', { className: 'w2mChat_thread' }, '#' + message.thread_id) : null),
980
1068
  h('div', { className: 'w2mChat_text' }, message?.text ?? ''));
981
1069
  }),
982
1070
  ].filter((node) => node !== null))),
983
1071
 
984
1072
 
985
1073
  h('div', { className: 'w2mChat_compose' },
1074
+ shot === null ? null : h('div', { key: 'shot', className: 'w2mChat_shotPreview' }, [
1075
+ h('img', { key: 'img', src: shot.url, alt: '截图预览' }),
1076
+ h('a', { key: 'dl', href: shot.url, download: 'w2m-screenshot-' + shot.at + '.png', className: 'w2mChat_shot' }, '下载'),
1077
+ h('button', { key: 'copy', type: 'button', className: 'w2mChat_copy', onClick: () => { try { navigator.clipboard?.write?.([new ClipboardItem({ 'image/png': shot.blob })]).then(() => setToast('已复制'), () => setToast('复制失败')); } catch { setToast('复制失败'); } } }, '复制'),
1078
+ ]),
1079
+ toast === null ? null : h('div', { key: 'toast', className: 'w2mChat_toast' }, toast),
986
1080
  replyTo === null ? null : h('div', { className: 'w2mChat_reply' }, [
987
1081
  h('span', { key: 'label' }, '回复:' + (replyTo.text.length > 40 ? replyTo.text.slice(0, 40) + '…' : replyTo.text)),
988
1082
  h('button', { key: 'cancel', type: 'button', className: 'w2mChat_replyCancel', title: '取消回复', onClick: () => setReplyTo(null) }, '×'),
@@ -1001,7 +1095,8 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
1001
1095
  }
1002
1096
  },
1003
1097
  }),
1004
- h('button', { type: 'button', className: 'w2mChat_send', disabled: busy || text.trim() === '', onClick: send }, busy ? '发送中…' : '发送')))
1098
+ h('button', { type: 'button', className: 'w2mChat_shotButton', title: '截图(Ctrl+P)', disabled: shotBusy, onClick: captureScreenshot }, shotBusy ? '截图…' : '📷'),
1099
+ h('button', { type: 'button', className: 'w2mChat_send', disabled: busy || text.trim() === '', onClick: send }, busy ? '发送中…' : '发送')))
1005
1100
  : null);
1006
1101
  }
1007
1102
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "1.6.8",
3
+ "version": "1.7.0",
4
4
  "description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 11 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -64,7 +64,7 @@
64
64
  "pack": "node scripts/pack.mjs",
65
65
  "reachability": "node scripts/reachability-check.mjs",
66
66
  "reachability:classify": "node scripts/reachability-classify.mjs",
67
- "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs test/task-deps.test.mjs test/dataflow-wiring.test.mjs test/task-graph.test.mjs test/ask-store.test.mjs test/turn.test.mjs test/fleet-members.test.mjs test/redact.test.mjs test/policy-matrix.test.mjs test/envelope-signature.test.mjs test/chat-crypto.test.mjs test/reduce-suggestion.test.mjs test/merge-hint.test.mjs test/fleet-chat-route.test.mjs test/maintenance.test.mjs test/maintenance-ferry.test.mjs test/grant.test.mjs test/grant-gate.test.mjs test/worktree.test.mjs test/patch-artifact.test.mjs test/merge-queue.test.mjs test/m3-workflow.test.mjs test/project-room.test.mjs test/project-attribution.test.mjs test/project-locks.test.mjs test/project-reviews.test.mjs test/project-notifications.test.mjs test/blob-sessions.test.mjs test/outbox.test.mjs test/reliability-metrics.test.mjs test/evidence.test.mjs test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs",
67
+ "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs test/task-deps.test.mjs test/dataflow-wiring.test.mjs test/task-graph.test.mjs test/ask-store.test.mjs test/turn.test.mjs test/fleet-members.test.mjs test/redact.test.mjs test/policy-matrix.test.mjs test/envelope-signature.test.mjs test/chat-crypto.test.mjs test/reduce-suggestion.test.mjs test/merge-hint.test.mjs test/fleet-chat-route.test.mjs test/maintenance.test.mjs test/maintenance-ferry.test.mjs test/grant.test.mjs test/grant-gate.test.mjs test/worktree.test.mjs test/patch-artifact.test.mjs test/merge-queue.test.mjs test/m3-workflow.test.mjs test/project-room.test.mjs test/project-attribution.test.mjs test/project-locks.test.mjs test/project-reviews.test.mjs test/project-notifications.test.mjs test/blob-sessions.test.mjs test/outbox.test.mjs test/reliability-metrics.test.mjs test/evidence.test.mjs test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs",
68
68
  "test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
69
69
  "test:account-identity": "node --test --test-force-exit test/account-identity.test.mjs",
70
70
  "test:account-join": "node --test --test-force-exit test/account-join.test.mjs",
@@ -82,7 +82,7 @@
82
82
  "test:council-session": "node --test --test-force-exit test/council-session.test.mjs",
83
83
  "test:crossnetwork": "node --test --test-force-exit test/crossnetwork.test.mjs",
84
84
  "test:crypto-blake2s": "node --test --test-force-exit test/crypto-blake2s.test.mjs",
85
- "test:e2e": "node --test --test-force-exit test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/p2p-agent.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs",
85
+ "test:e2e": "node --test --test-force-exit test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/p2p-agent.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs",
86
86
  "test:exec-env": "node --test --test-force-exit test/exec-env-injection.test.mjs",
87
87
  "test:exit-channel": "node --test --test-force-exit test/exit-channel.test.mjs",
88
88
  "test:field-authority": "node --test --test-force-exit test/field-authority.test.mjs",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "twinsearth-w2m-dsh",
4
- "version": "1.6.8",
4
+ "version": "1.7.0",
5
5
  "description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 12 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -282,7 +282,7 @@ const MAX_STAGES = 16;
282
282
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
283
283
  * overrides it at runtime for tests and packagers.
284
284
  */
285
- const PLUGIN_VERSION = '1.6.8';
285
+ const PLUGIN_VERSION = '1.7.0';
286
286
 
287
287
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
288
288
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
@@ -8011,8 +8011,18 @@ async function startLocalStack(ctx, cfg) {
8011
8011
  }
8012
8012
  }
8013
8013
 
8014
- await agent.start();
8015
- ctx.logger?.info?.(`w2m: agent started as ${JSON.stringify(agent.identity?.() ?? null)}`);
8014
+ // v1.6.9: the agent DOES come up, but on macOS its `start()` promise was observed to stay
8015
+ // pending forever (stage log: "before agent.start" was the last line written). Awaiting it blocked
8016
+ // everything after this point — including the failover monitor installed by startFailoverStack,
8017
+ // which is why automatic relay failover never engaged: the monitor was never created.
8018
+ //
8019
+ // The mount does not need to OBSERVE the start to be complete: the agent announces itself through
8020
+ // the relay, and the value startFailoverStack needs from this function is the cleanup handle. So
8021
+ // start it in the background and record a failure instead of hanging the whole stack.
8022
+ agent.start().then(
8023
+ () => ctx.logger?.info?.('w2m: agent started as ' + JSON.stringify(agent.identity?.() ?? null)),
8024
+ (error) => ctx.logger?.warn?.('w2m: agent start failed: ' + (error instanceof Error ? error.message : String(error))),
8025
+ );
8016
8026
 
8017
8027
  /**
8018
8028
  * v0.8.12: **keep trying to join after mount.**