mixdog 0.9.82 → 0.9.84

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.
@@ -45,7 +45,17 @@ export function createSessionTurnApi(deps) {
45
45
  activeToolSurface, applyResolvedCwd, resolveCwdPath, agentStatusState, notificationListeners,
46
46
  awaitInitialMcpConnect,
47
47
  } = deps;
48
+ const enqueueRemoteAttachedPrompt = (prompt) => {
49
+ const attachedSession = getSession();
50
+ if (!attachedSession?.remoteAttached || !attachedSession.id) return false;
51
+ try {
52
+ return Number(mgr.enqueueRemotePendingMessage?.(attachedSession.id, prompt)) > 0;
53
+ } catch {
54
+ return false;
55
+ }
56
+ };
48
57
  return {
58
+ enqueueRemoteAttachedPrompt,
49
59
  getTurnLiveness() {
50
60
  const sessionId = getSession()?.id;
51
61
  if (!sessionId || typeof mgr.getSessionProgressSnapshot !== 'function') return null;
@@ -65,9 +75,12 @@ export function createSessionTurnApi(deps) {
65
75
  // a normal user turn and this surface refreshes from disk.
66
76
  const attachedSession = getSession();
67
77
  if (attachedSession?.remoteAttached) {
68
- try { mgr.enqueueRemotePendingMessage?.(attachedSession.id, prompt); } catch { /* best-effort */ }
78
+ const delivered = enqueueRemoteAttachedPrompt(prompt);
69
79
  return {
70
- result: { content: 'Delivered to the live owner of this session — the reply will appear here shortly.' },
80
+ // This branch is only a race-safe fallback for callers that reached
81
+ // ask() before the live pipe was ready. Never manufacture an
82
+ // assistant response: the owner's mirrored transcript is authoritative.
83
+ result: { content: '', remoteAttached: true, delivered },
71
84
  session: attachedSession,
72
85
  };
73
86
  }
@@ -108,11 +121,9 @@ export function createSessionTurnApi(deps) {
108
121
  }
109
122
  }
110
123
  const session0 = getSession();
111
- // Turn-review base: capture the pre-turn worktree tree in the shadow
112
- // snapshot repo so the desktop review bar can diff EVERYTHING this
113
- // turn changes — subagent and background-job edits included. The wait
114
- // is capped so a cold first snapshot never stalls the turn; a late
115
- // base still lands through the shared promise.
124
+ // Turn-review boundary: start a fresh session+turn generation. Lead
125
+ // patches remain transcript-derived; worker apply_patch diffs bind to
126
+ // this generation and cannot leak into the next turn/session.
116
127
  try { await beginTurnSnapshot(getCurrentCwd(), session0?.id); } catch { /* never blocks a turn */ }
117
128
  if (session0.deferredInitialRefreshPending) {
118
129
  // FIRST TURN of a FRESH session (session-local gate, NOT the
@@ -59,6 +59,10 @@ import {
59
59
  } from './agent-tool/worker-rows.mjs';
60
60
  import { resolveAgentTerminalReapMs } from '../session-runtime/config-helpers.mjs';
61
61
  import { createWorkerIndex } from './agent-tool/worker-index.mjs';
62
+ import {
63
+ beginAgentTurnReview,
64
+ completeAgentTurnReview,
65
+ } from '../runtime/shared/turn-snapshot.mjs';
62
66
  // Re-export the static tool descriptor so importers of this facade keep the
63
67
  // identical public surface (`import { AGENT_TOOL } from './agent-tool.mjs'`).
64
68
  export { AGENT_TOOL };
@@ -91,6 +95,26 @@ export function createStandaloneAgent({
91
95
  if (typeof onSubagentEvent !== 'function') return;
92
96
  try { onSubagentEvent(phase, { agent_type: agent || null, ...extra }); } catch { /* best-effort */ }
93
97
  }
98
+ function createTurnReviewCollector(session, tag, agent, notifyContext = {}) {
99
+ const ownerSessionId = clean(
100
+ notifyContext?.callerSessionId
101
+ || notifyContext?.sessionId
102
+ || notifyContext?.routingSessionId
103
+ || session?.ownerSessionId,
104
+ );
105
+ const handle = beginAgentTurnReview(ownerSessionId, session?.id, { tag, agent });
106
+ const patches = [];
107
+ return {
108
+ onToolResult(message) {
109
+ if (typeof message?.uiDiff === 'string' && message.uiDiff.trim()) {
110
+ patches.push(message.uiDiff);
111
+ }
112
+ },
113
+ complete() {
114
+ completeAgentTurnReview(handle, patches);
115
+ },
116
+ };
117
+ }
94
118
  const tags = new Map();
95
119
  const tagAgents = new Map();
96
120
  const tagCwds = new Map();
@@ -869,6 +893,7 @@ export function createStandaloneAgent({
869
893
  async function runSpawn(prepared, notifyContext = null, job = null) {
870
894
  const { args, tag, session, agent, preset, presetName, workerCwd, prompt, watchdogPolicy } = prepared;
871
895
  const watchdog = startProgressIdleWatchdog(session.id, watchdogPolicy, agent);
896
+ const turnReview = createTurnReviewCollector(session, tag, agent, notifyContext || {});
872
897
  let finalStatus = 'idle';
873
898
  // SubagentStart: a worker session is about to run its first turn.
874
899
  emitSubagentEvent('start', agent, { session_id: session.id, tag });
@@ -907,8 +932,10 @@ export function createStandaloneAgent({
907
932
  handoffMsgStart = resolveHandoffMessageStartIndex(mgr.getSession(session.id));
908
933
  const result = await mgr.askSession(session.id, prompt, args.context || null, null, workerCwd, null, {
909
934
  notifyFn: workerNotifyFn(session.id, notifyContext || {}),
935
+ onToolResult: (message) => turnReview.onToolResult(message),
910
936
  ...(job ? {
911
937
  onTerminalResult: (terminalResult) => {
938
+ turnReview.complete();
912
939
  const value = completionValue(terminalResult);
913
940
  if (job) job._terminalResultValue = value;
914
941
  notifyOwnerAgentCompletionEarly(job, value, notifyContext || {});
@@ -999,6 +1026,7 @@ export function createStandaloneAgent({
999
1026
  }
1000
1027
  throw error;
1001
1028
  } finally {
1029
+ turnReview.complete();
1002
1030
  watchdog?.stop?.();
1003
1031
  upsertWorkerSessionDeferred(session, tag, {
1004
1032
  agent,
@@ -1056,6 +1084,7 @@ export function createStandaloneAgent({
1056
1084
  const sendAgent = session.agent || normalizeAgentName(args.agent);
1057
1085
  const watchdog = startProgressIdleWatchdog(sessionId, resolveAgentWatchdogPolicy(sendAgent), sendAgent);
1058
1086
  const tag = tagForSession(sessionId);
1087
+ const turnReview = createTurnReviewCollector(session, tag, sendAgent, notifyContext || {});
1059
1088
  let finalStatus = 'idle';
1060
1089
  upsertWorkerSessionDeferred(session, tag, { status: 'running', stage: 'running' });
1061
1090
  let handoffMsgStart = 0;
@@ -1078,8 +1107,10 @@ export function createStandaloneAgent({
1078
1107
  handoffMsgStart = resolveHandoffMessageStartIndex(mgr.getSession(sessionId));
1079
1108
  const result = await mgr.askSession(sessionId, prompt, args.context || null, null, session.cwd || defaultCwd, null, {
1080
1109
  notifyFn: workerNotifyFn(sessionId, notifyContext || {}),
1110
+ onToolResult: (message) => turnReview.onToolResult(message),
1081
1111
  ...(job ? {
1082
1112
  onTerminalResult: (terminalResult) => {
1113
+ turnReview.complete();
1083
1114
  const value = completionValue(terminalResult);
1084
1115
  if (job) job._terminalResultValue = value;
1085
1116
  notifyOwnerAgentCompletionEarly(job, value, notifyContext || {});
@@ -1153,6 +1184,7 @@ export function createStandaloneAgent({
1153
1184
  }
1154
1185
  throw error;
1155
1186
  } finally {
1187
+ turnReview.complete();
1156
1188
  watchdog?.stop?.();
1157
1189
  upsertWorkerSessionDeferred(session, tag, {
1158
1190
  status: finalStatus,
@@ -29555,6 +29555,8 @@ import { unlinkSync as unlinkSync4 } from "node:fs";
29555
29555
  var MAX_FRAME_PATCHES = 48;
29556
29556
  var MAX_BUFFER_BYTES = 8 * 1024 * 1024;
29557
29557
  var SYNC_REQUEST_MIN_INTERVAL_MS = 500;
29558
+ var LIVE_CONNECT_RETRY_MIN_MS = 10;
29559
+ var LIVE_CONNECT_RETRY_MAX_MS = 160;
29558
29560
  function liveSharePipePath(sessionId, sessionFilePath) {
29559
29561
  return process.platform === "win32" ? `\\\\.\\pipe\\mixdog-live-${sessionId}` : `${sessionFilePath}.live.sock`;
29560
29562
  }
@@ -29597,9 +29599,13 @@ function createLiveShare({
29597
29599
  onOwnerClosed,
29598
29600
  viewerApply
29599
29601
  }) {
29602
+ let disposed = false;
29600
29603
  let server = null;
29601
29604
  let serverId = "";
29602
29605
  let serverPath = "";
29606
+ let serverRetryTimer = null;
29607
+ let serverRetryId = "";
29608
+ let serverRetryDelayMs = LIVE_CONNECT_RETRY_MIN_MS;
29603
29609
  const sockets = /* @__PURE__ */ new Set();
29604
29610
  let lastItems = null;
29605
29611
  let lastTail = null;
@@ -29735,8 +29741,36 @@ function createLiveShare({
29735
29741
  if (dirty) broadcast(frame);
29736
29742
  };
29737
29743
  listeners.add(onPublish);
29744
+ const clearServerRetry = () => {
29745
+ if (serverRetryTimer) clearTimeout(serverRetryTimer);
29746
+ serverRetryTimer = null;
29747
+ serverRetryId = "";
29748
+ };
29749
+ const scheduleServerRetry = (id) => {
29750
+ const target = String(id || "");
29751
+ if (disposed || !target || server || serverRetryTimer || String(ownerSessionId() || "") !== target) return;
29752
+ const delay2 = serverRetryDelayMs;
29753
+ serverRetryDelayMs = Math.min(LIVE_CONNECT_RETRY_MAX_MS, Math.max(
29754
+ LIVE_CONNECT_RETRY_MIN_MS,
29755
+ serverRetryDelayMs * 2
29756
+ ));
29757
+ serverRetryId = target;
29758
+ serverRetryTimer = setTimeout(() => {
29759
+ serverRetryTimer = null;
29760
+ serverRetryId = "";
29761
+ if (disposed || server || String(ownerSessionId() || "") !== target) return;
29762
+ startServer2(target);
29763
+ }, delay2);
29764
+ serverRetryTimer.unref?.();
29765
+ };
29738
29766
  const stopServer = () => {
29739
- if (!server) return;
29767
+ clearServerRetry();
29768
+ serverRetryDelayMs = LIVE_CONNECT_RETRY_MIN_MS;
29769
+ if (!server) {
29770
+ serverId = "";
29771
+ serverPath = "";
29772
+ return;
29773
+ }
29740
29774
  try {
29741
29775
  broadcast({ t: "close" });
29742
29776
  } catch {
@@ -29764,6 +29798,8 @@ function createLiveShare({
29764
29798
  serverPath = "";
29765
29799
  };
29766
29800
  const startServer2 = (id) => {
29801
+ if (disposed || server || !id) return;
29802
+ clearServerRetry();
29767
29803
  const path4 = socketPathFor(id);
29768
29804
  const next = createServer((socket) => {
29769
29805
  socket.setNoDelay?.(true);
@@ -29813,12 +29849,18 @@ function createLiveShare({
29813
29849
  server = null;
29814
29850
  serverId = "";
29815
29851
  serverPath = "";
29852
+ scheduleServerRetry(id);
29816
29853
  }
29817
29854
  try {
29818
29855
  next.close();
29819
29856
  } catch {
29820
29857
  }
29821
29858
  });
29859
+ next.on("listening", () => {
29860
+ if (server !== next) return;
29861
+ clearServerRetry();
29862
+ serverRetryDelayMs = LIVE_CONNECT_RETRY_MIN_MS;
29863
+ });
29822
29864
  if (process.platform !== "win32") {
29823
29865
  try {
29824
29866
  unlinkSync4(path4);
@@ -29834,6 +29876,7 @@ function createLiveShare({
29834
29876
  server = null;
29835
29877
  serverId = "";
29836
29878
  serverPath = "";
29879
+ scheduleServerRetry(id);
29837
29880
  }
29838
29881
  };
29839
29882
  let client = null;
@@ -29841,15 +29884,19 @@ function createLiveShare({
29841
29884
  let clientUp = false;
29842
29885
  let clientSyncedId = "";
29843
29886
  let lastSyncRequestAt = 0;
29887
+ let clientRetryTimer = null;
29888
+ let clientRetryId = "";
29889
+ let clientRetryDelayMs = LIVE_CONNECT_RETRY_MIN_MS;
29844
29890
  const viewerSyncWaiters = /* @__PURE__ */ new Set();
29845
29891
  const settleViewerSync = (id, synced) => {
29846
29892
  for (const waiter of [...viewerSyncWaiters]) {
29847
29893
  if (waiter.id === id) waiter.finish(synced);
29848
29894
  }
29849
29895
  };
29850
- const waitForViewerSync = (id, timeoutMs = 750) => {
29896
+ const waitForViewerSync = (id, timeoutMs = 1500) => {
29851
29897
  const target = String(id || "");
29852
29898
  if (!target) return Promise.resolve(false);
29899
+ ensureShare();
29853
29900
  if (clientUp && clientId === target && clientSyncedId === target) {
29854
29901
  return Promise.resolve(true);
29855
29902
  }
@@ -29920,6 +29967,28 @@ function createLiveShare({
29920
29967
  } catch {
29921
29968
  }
29922
29969
  };
29970
+ const clearClientRetry = () => {
29971
+ if (clientRetryTimer) clearTimeout(clientRetryTimer);
29972
+ clientRetryTimer = null;
29973
+ clientRetryId = "";
29974
+ };
29975
+ const scheduleClientRetry = (id) => {
29976
+ const target = String(id || "");
29977
+ if (disposed || !target || client || clientRetryTimer || String(viewerSessionId() || "") !== target) return;
29978
+ const delay2 = clientRetryDelayMs;
29979
+ clientRetryDelayMs = Math.min(LIVE_CONNECT_RETRY_MAX_MS, Math.max(
29980
+ LIVE_CONNECT_RETRY_MIN_MS,
29981
+ clientRetryDelayMs * 2
29982
+ ));
29983
+ clientRetryId = target;
29984
+ clientRetryTimer = setTimeout(() => {
29985
+ clientRetryTimer = null;
29986
+ clientRetryId = "";
29987
+ if (disposed || client || String(viewerSessionId() || "") !== target) return;
29988
+ startClient(target);
29989
+ }, delay2);
29990
+ clientRetryTimer.unref?.();
29991
+ };
29923
29992
  const applyViewerFrame = (frame, socket) => {
29924
29993
  if (frame.t === "full") {
29925
29994
  viewerApply.replaceItems(Array.isArray(frame.items) ? frame.items : []);
@@ -29954,6 +30023,8 @@ function createLiveShare({
29954
30023
  if ("live" in frame) applyLiveState(frame.live);
29955
30024
  };
29956
30025
  const stopClient = () => {
30026
+ clearClientRetry();
30027
+ clientRetryDelayMs = LIVE_CONNECT_RETRY_MIN_MS;
29957
30028
  const closing = client;
29958
30029
  const closingId = clientId;
29959
30030
  const wasUp = clientUp;
@@ -29971,10 +30042,13 @@ function createLiveShare({
29971
30042
  if (wasUp) clearMirroredLiveState();
29972
30043
  };
29973
30044
  const startClient = (id) => {
30045
+ if (disposed || client || !id) return;
30046
+ clearClientRetry();
29974
30047
  let socket;
29975
30048
  try {
29976
30049
  socket = connect(socketPathFor(id));
29977
30050
  } catch {
30051
+ scheduleClientRetry(id);
29978
30052
  return;
29979
30053
  }
29980
30054
  client = socket;
@@ -29983,14 +30057,19 @@ function createLiveShare({
29983
30057
  clientSyncedId = "";
29984
30058
  socket.setNoDelay?.(true);
29985
30059
  socket.on("connect", () => {
29986
- if (client === socket) clientUp = true;
30060
+ if (client !== socket) return;
30061
+ clientUp = true;
30062
+ clearClientRetry();
30063
+ clientRetryDelayMs = LIVE_CONNECT_RETRY_MIN_MS;
29987
30064
  });
29988
30065
  const down = (ownerClosed) => {
29989
30066
  const wasUp = clientUp && client === socket;
29990
- if (client === socket) {
30067
+ const wasCurrent = client === socket;
30068
+ if (wasCurrent) {
29991
30069
  client = null;
29992
30070
  clientId = "";
29993
30071
  clientUp = false;
30072
+ clientSyncedId = "";
29994
30073
  }
29995
30074
  try {
29996
30075
  socket.destroy();
@@ -29998,6 +30077,7 @@ function createLiveShare({
29998
30077
  }
29999
30078
  if (wasUp) clearMirroredLiveState();
30000
30079
  if (wasUp) onOwnerClosed?.(id, ownerClosed);
30080
+ if (wasCurrent) scheduleClientRetry(id);
30001
30081
  };
30002
30082
  socket.on("error", () => down(false));
30003
30083
  socket.on("close", () => down(false));
@@ -30019,17 +30099,21 @@ function createLiveShare({
30019
30099
  }
30020
30100
  }, () => down(false));
30021
30101
  };
30102
+ const ensureShare = () => {
30103
+ if (disposed) return;
30104
+ const ownerId = String(ownerSessionId() || "");
30105
+ const attachId = ownerId ? "" : String(viewerSessionId() || "");
30106
+ if (serverRetryId && serverRetryId !== ownerId) clearServerRetry();
30107
+ if (!ownerId && serverRetryTimer) clearServerRetry();
30108
+ if (serverId && serverId !== ownerId) stopServer();
30109
+ if (ownerId && !server && !serverRetryTimer) startServer2(ownerId);
30110
+ if (clientRetryId && clientRetryId !== attachId) clearClientRetry();
30111
+ if (!attachId && clientRetryTimer) clearClientRetry();
30112
+ if (clientId && clientId !== attachId) stopClient();
30113
+ if (attachId && !client && !clientRetryTimer) startClient(attachId);
30114
+ };
30022
30115
  return {
30023
- // Reconciles both legs against the current session role; called from the
30024
- // engine share tick (also serves as the reconnect/retry cadence).
30025
- ensure() {
30026
- const ownerId = String(ownerSessionId() || "");
30027
- const attachId = ownerId ? "" : String(viewerSessionId() || "");
30028
- if (serverId && serverId !== ownerId) stopServer();
30029
- if (ownerId && !server) startServer2(ownerId);
30030
- if (clientId && clientId !== attachId) stopClient();
30031
- if (attachId && !client) startClient(attachId);
30032
- },
30116
+ ensure: ensureShare,
30033
30117
  viewerConnected: () => clientUp,
30034
30118
  waitForViewerSync,
30035
30119
  sendSubmit(text) {
@@ -30051,9 +30135,11 @@ function createLiveShare({
30051
30135
  }
30052
30136
  },
30053
30137
  dispose() {
30138
+ disposed = true;
30054
30139
  listeners.delete(onPublish);
30055
30140
  stopServer();
30056
30141
  stopClient();
30142
+ for (const waiter of [...viewerSyncWaiters]) waiter.finish(false);
30057
30143
  }
30058
30144
  };
30059
30145
  }
@@ -31198,6 +31284,7 @@ async function createEngineSession({
31198
31284
  const timer2 = setTimeout(() => {
31199
31285
  if (flags.disposed || !state.sessionRemoteAttached) return;
31200
31286
  if (String(state.sessionId || "") !== id) return;
31287
+ if (liveShare.viewerConnected()) return;
31201
31288
  void Promise.resolve(api.resume(id, { quiet: true })).catch(() => {
31202
31289
  });
31203
31290
  }, 1500);
@@ -31223,9 +31310,15 @@ async function createEngineSession({
31223
31310
  if (typeof api.submit === "function") {
31224
31311
  const baseSubmit = api.submit;
31225
31312
  api.submit = (prompt, options = {}) => {
31226
- if (state.sessionRemoteAttached && liveShare.viewerConnected()) {
31313
+ if (state.sessionRemoteAttached) {
31227
31314
  const text = String(promptDisplayText(prompt, options) || "").trim();
31315
+ if (!text) return false;
31316
+ try {
31317
+ liveShare.ensure();
31318
+ } catch {
31319
+ }
31228
31320
  if (text && liveShare.sendSubmit(text)) return true;
31321
+ return runtime.enqueueRemoteAttachedPrompt?.(prompt) === true;
31229
31322
  }
31230
31323
  return baseSubmit(prompt, options);
31231
31324
  };
@@ -31258,6 +31351,7 @@ async function createEngineSession({
31258
31351
  return result;
31259
31352
  };
31260
31353
  }
31354
+ reconcileLiveShareNow();
31261
31355
  let spoolWatcher = null;
31262
31356
  let spoolDebounce = null;
31263
31357
  try {
@@ -23,6 +23,11 @@ const MAX_BUFFER_BYTES = 8 * 1024 * 1024;
23
23
  // Desync-recovery `sync` requests are throttled so a corrupt stream cannot
24
24
  // make the owner serialize full transcripts every frame.
25
25
  const SYNC_REQUEST_MIN_INTERVAL_MS = 500;
26
+ // Session entry is latency-sensitive: the owner pipe can be a few event-loop
27
+ // turns behind the viewer resume. Retry locally instead of waiting for the
28
+ // coarse 3s engine safety tick.
29
+ const LIVE_CONNECT_RETRY_MIN_MS = 10;
30
+ const LIVE_CONNECT_RETRY_MAX_MS = 160;
26
31
 
27
32
  export function liveSharePipePath(sessionId, sessionFilePath) {
28
33
  return process.platform === 'win32'
@@ -66,10 +71,14 @@ export function createLiveShare({
66
71
  onOwnerClosed,
67
72
  viewerApply,
68
73
  }) {
74
+ let disposed = false;
69
75
  // ---- owner: pipe server + delta publisher ----
70
76
  let server = null;
71
77
  let serverId = '';
72
78
  let serverPath = '';
79
+ let serverRetryTimer = null;
80
+ let serverRetryId = '';
81
+ let serverRetryDelayMs = LIVE_CONNECT_RETRY_MIN_MS;
73
82
  const sockets = new Set();
74
83
  let lastItems = null;
75
84
  let lastTail = null;
@@ -206,8 +215,38 @@ export function createLiveShare({
206
215
  };
207
216
  listeners.add(onPublish);
208
217
 
218
+ const clearServerRetry = () => {
219
+ if (serverRetryTimer) clearTimeout(serverRetryTimer);
220
+ serverRetryTimer = null;
221
+ serverRetryId = '';
222
+ };
223
+
224
+ const scheduleServerRetry = (id) => {
225
+ const target = String(id || '');
226
+ if (disposed || !target || server || serverRetryTimer || String(ownerSessionId() || '') !== target) return;
227
+ const delay = serverRetryDelayMs;
228
+ serverRetryDelayMs = Math.min(LIVE_CONNECT_RETRY_MAX_MS, Math.max(
229
+ LIVE_CONNECT_RETRY_MIN_MS,
230
+ serverRetryDelayMs * 2,
231
+ ));
232
+ serverRetryId = target;
233
+ serverRetryTimer = setTimeout(() => {
234
+ serverRetryTimer = null;
235
+ serverRetryId = '';
236
+ if (disposed || server || String(ownerSessionId() || '') !== target) return;
237
+ startServer(target);
238
+ }, delay);
239
+ serverRetryTimer.unref?.();
240
+ };
241
+
209
242
  const stopServer = () => {
210
- if (!server) return;
243
+ clearServerRetry();
244
+ serverRetryDelayMs = LIVE_CONNECT_RETRY_MIN_MS;
245
+ if (!server) {
246
+ serverId = '';
247
+ serverPath = '';
248
+ return;
249
+ }
211
250
  try { broadcast({ t: 'close' }); } catch { /* sockets closing anyway */ }
212
251
  for (const socket of sockets) {
213
252
  try { socket.destroy(); } catch { /* already gone */ }
@@ -224,6 +263,8 @@ export function createLiveShare({
224
263
  };
225
264
 
226
265
  const startServer = (id) => {
266
+ if (disposed || server || !id) return;
267
+ clearServerRetry();
227
268
  const path = socketPathFor(id);
228
269
  const next = createServer((socket) => {
229
270
  socket.setNoDelay?.(true);
@@ -257,11 +298,21 @@ export function createLiveShare({
257
298
  }
258
299
  });
259
300
  next.on('error', () => {
260
- // EADDRINUSE (another live owner) or a transient listen failure: give
261
- // up quietly; the next ensure() tick retries.
262
- if (server === next) { server = null; serverId = ''; serverPath = ''; }
301
+ // EADDRINUSE (another live owner) or a transient listen failure: retry
302
+ // with a short bounded backoff while this surface still owns the id.
303
+ if (server === next) {
304
+ server = null;
305
+ serverId = '';
306
+ serverPath = '';
307
+ scheduleServerRetry(id);
308
+ }
263
309
  try { next.close(); } catch { /* already closed */ }
264
310
  });
311
+ next.on('listening', () => {
312
+ if (server !== next) return;
313
+ clearServerRetry();
314
+ serverRetryDelayMs = LIVE_CONNECT_RETRY_MIN_MS;
315
+ });
265
316
  if (process.platform !== 'win32') {
266
317
  try { unlinkSync(path); } catch { /* no stale socket */ }
267
318
  }
@@ -272,6 +323,7 @@ export function createLiveShare({
272
323
  server = null;
273
324
  serverId = '';
274
325
  serverPath = '';
326
+ scheduleServerRetry(id);
275
327
  }
276
328
  };
277
329
 
@@ -281,6 +333,9 @@ export function createLiveShare({
281
333
  let clientUp = false;
282
334
  let clientSyncedId = '';
283
335
  let lastSyncRequestAt = 0;
336
+ let clientRetryTimer = null;
337
+ let clientRetryId = '';
338
+ let clientRetryDelayMs = LIVE_CONNECT_RETRY_MIN_MS;
284
339
  const viewerSyncWaiters = new Set();
285
340
 
286
341
  const settleViewerSync = (id, synced) => {
@@ -289,9 +344,10 @@ export function createLiveShare({
289
344
  }
290
345
  };
291
346
 
292
- const waitForViewerSync = (id, timeoutMs = 750) => {
347
+ const waitForViewerSync = (id, timeoutMs = 1500) => {
293
348
  const target = String(id || '');
294
349
  if (!target) return Promise.resolve(false);
350
+ ensureShare();
295
351
  if (clientUp && clientId === target && clientSyncedId === target) {
296
352
  return Promise.resolve(true);
297
353
  }
@@ -363,6 +419,30 @@ export function createLiveShare({
363
419
  } catch { /* viewer store already disposed */ }
364
420
  };
365
421
 
422
+ const clearClientRetry = () => {
423
+ if (clientRetryTimer) clearTimeout(clientRetryTimer);
424
+ clientRetryTimer = null;
425
+ clientRetryId = '';
426
+ };
427
+
428
+ const scheduleClientRetry = (id) => {
429
+ const target = String(id || '');
430
+ if (disposed || !target || client || clientRetryTimer || String(viewerSessionId() || '') !== target) return;
431
+ const delay = clientRetryDelayMs;
432
+ clientRetryDelayMs = Math.min(LIVE_CONNECT_RETRY_MAX_MS, Math.max(
433
+ LIVE_CONNECT_RETRY_MIN_MS,
434
+ clientRetryDelayMs * 2,
435
+ ));
436
+ clientRetryId = target;
437
+ clientRetryTimer = setTimeout(() => {
438
+ clientRetryTimer = null;
439
+ clientRetryId = '';
440
+ if (disposed || client || String(viewerSessionId() || '') !== target) return;
441
+ startClient(target);
442
+ }, delay);
443
+ clientRetryTimer.unref?.();
444
+ };
445
+
366
446
  const applyViewerFrame = (frame, socket) => {
367
447
  if (frame.t === 'full') {
368
448
  viewerApply.replaceItems(Array.isArray(frame.items) ? frame.items : []);
@@ -400,6 +480,8 @@ export function createLiveShare({
400
480
  };
401
481
 
402
482
  const stopClient = () => {
483
+ clearClientRetry();
484
+ clientRetryDelayMs = LIVE_CONNECT_RETRY_MIN_MS;
403
485
  const closing = client;
404
486
  const closingId = clientId;
405
487
  const wasUp = clientUp;
@@ -422,22 +504,39 @@ export function createLiveShare({
422
504
  };
423
505
 
424
506
  const startClient = (id) => {
507
+ if (disposed || client || !id) return;
508
+ clearClientRetry();
425
509
  let socket;
426
- try { socket = connect(socketPathFor(id)); } catch { return; }
510
+ try { socket = connect(socketPathFor(id)); } catch {
511
+ scheduleClientRetry(id);
512
+ return;
513
+ }
427
514
  client = socket;
428
515
  clientId = id;
429
516
  clientUp = false;
430
517
  clientSyncedId = '';
431
518
  socket.setNoDelay?.(true);
432
- socket.on('connect', () => { if (client === socket) clientUp = true; });
519
+ socket.on('connect', () => {
520
+ if (client !== socket) return;
521
+ clientUp = true;
522
+ clearClientRetry();
523
+ clientRetryDelayMs = LIVE_CONNECT_RETRY_MIN_MS;
524
+ });
433
525
  const down = (ownerClosed) => {
434
526
  const wasUp = clientUp && client === socket;
435
- if (client === socket) { client = null; clientId = ''; clientUp = false; }
527
+ const wasCurrent = client === socket;
528
+ if (wasCurrent) {
529
+ client = null;
530
+ clientId = '';
531
+ clientUp = false;
532
+ clientSyncedId = '';
533
+ }
436
534
  try { socket.destroy(); } catch { /* already gone */ }
437
535
  if (wasUp) clearMirroredLiveState();
438
536
  // A live link that dropped means the owner ended or crashed: nudge the
439
537
  // promotion path instead of waiting for the next store-mtime change.
440
538
  if (wasUp) onOwnerClosed?.(id, ownerClosed);
539
+ if (wasCurrent) scheduleClientRetry(id);
441
540
  };
442
541
  socket.on('error', () => down(false));
443
542
  socket.on('close', () => down(false));
@@ -459,17 +558,25 @@ export function createLiveShare({
459
558
  }, () => down(false));
460
559
  };
461
560
 
561
+ // Reconciles both legs against the current session role. Failed pipe opens
562
+ // continue through the local retry timers; the engine's 3s call is only a
563
+ // safety net, never the normal connection cadence.
564
+ const ensureShare = () => {
565
+ if (disposed) return;
566
+ const ownerId = String(ownerSessionId() || '');
567
+ const attachId = ownerId ? '' : String(viewerSessionId() || '');
568
+ if (serverRetryId && serverRetryId !== ownerId) clearServerRetry();
569
+ if (!ownerId && serverRetryTimer) clearServerRetry();
570
+ if (serverId && serverId !== ownerId) stopServer();
571
+ if (ownerId && !server && !serverRetryTimer) startServer(ownerId);
572
+ if (clientRetryId && clientRetryId !== attachId) clearClientRetry();
573
+ if (!attachId && clientRetryTimer) clearClientRetry();
574
+ if (clientId && clientId !== attachId) stopClient();
575
+ if (attachId && !client && !clientRetryTimer) startClient(attachId);
576
+ };
577
+
462
578
  return {
463
- // Reconciles both legs against the current session role; called from the
464
- // engine share tick (also serves as the reconnect/retry cadence).
465
- ensure() {
466
- const ownerId = String(ownerSessionId() || '');
467
- const attachId = ownerId ? '' : String(viewerSessionId() || '');
468
- if (serverId && serverId !== ownerId) stopServer();
469
- if (ownerId && !server) startServer(ownerId);
470
- if (clientId && clientId !== attachId) stopClient();
471
- if (attachId && !client) startClient(attachId);
472
- },
579
+ ensure: ensureShare,
473
580
  viewerConnected: () => clientUp,
474
581
  waitForViewerSync,
475
582
  sendSubmit(text) {
@@ -491,9 +598,11 @@ export function createLiveShare({
491
598
  }
492
599
  },
493
600
  dispose() {
601
+ disposed = true;
494
602
  listeners.delete(onPublish);
495
603
  stopServer();
496
604
  stopClient();
605
+ for (const waiter of [...viewerSyncWaiters]) waiter.finish(false);
497
606
  },
498
607
  };
499
608
  }