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.
- package/package.json +1 -1
- package/scripts/runtime-dependency-cache-key.mjs +94 -0
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +25 -14
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +38 -1
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +16 -14
- package/src/runtime/agent/orchestrator/session/store.mjs +1 -0
- package/src/runtime/shared/turn-snapshot.mjs +94 -157
- package/src/session-runtime/runtime-core.mjs +10 -7
- package/src/session-runtime/session-turn-api.mjs +18 -7
- package/src/standalone/agent-tool.mjs +32 -0
- package/src/tui/dist/index.mjs +109 -15
- package/src/tui/engine/live-share.mjs +127 -18
- package/src/tui/engine.mjs +12 -1
|
@@ -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
|
-
|
|
78
|
+
const delivered = enqueueRemoteAttachedPrompt(prompt);
|
|
69
79
|
return {
|
|
70
|
-
|
|
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
|
|
112
|
-
//
|
|
113
|
-
//
|
|
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,
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
261
|
-
//
|
|
262
|
-
if (server === next) {
|
|
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 =
|
|
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 {
|
|
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', () => {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|