mixdog 0.9.55 → 0.9.59
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 +3 -1
- package/scripts/_smoke_wd.mjs +7 -0
- package/scripts/agent-terminal-reap-test.mjs +127 -2
- package/scripts/compact-file-reattach-test.mjs +88 -0
- package/scripts/compact-pressure-test.mjs +45 -21
- package/scripts/compact-recall-digest-test.mjs +57 -0
- package/scripts/compact-smoke.mjs +35 -39
- package/scripts/desktop-session-bridge-test.mjs +271 -9
- package/scripts/generate-oc-icons.mjs +11 -0
- package/scripts/live-share-test.mjs +148 -0
- package/scripts/live-worker-smoke.mjs +1 -1
- package/scripts/max-output-recovery-test.mjs +12 -1
- package/scripts/mouse-tracking-restore-smoke.mjs +107 -0
- package/scripts/provider-admission-scheduler-test.mjs +100 -0
- package/scripts/provider-contract-test.mjs +49 -0
- package/scripts/resource-admission-test.mjs +5 -2
- package/scripts/session-ingest-compaction-smoke.mjs +38 -0
- package/scripts/steering-drain-buckets-test.mjs +15 -0
- package/scripts/submit-commandbusy-race-test.mjs +54 -3
- package/scripts/tool-smoke.mjs +2 -3
- package/scripts/tui-ambiguous-width-test.mjs +113 -0
- package/scripts/tui-runtime-load-bench.mjs +5 -0
- package/scripts/tui-transcript-perf-test.mjs +167 -0
- package/src/app.mjs +23 -0
- package/src/cli.mjs +6 -0
- package/src/lib/keychain-cjs.cjs +70 -20
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +4 -2
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +18 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +111 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +61 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +13 -4
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +43 -2
- package/src/runtime/agent/orchestrator/session/compact/file-reattach.mjs +112 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +203 -49
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +110 -15
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +109 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -7
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +39 -39
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +108 -4
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +19 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -11
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +132 -24
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +39 -21
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +91 -1
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +14 -33
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +169 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +474 -42
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +17 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +36 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +249 -11
- package/src/runtime/channels/data/voice-runtime-manifest.json +21 -5
- package/src/runtime/channels/lib/scheduler.mjs +8 -6
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +71 -8
- package/src/runtime/channels/lib/voice-transcription.mjs +2 -2
- package/src/runtime/channels/lib/whisper-language.mjs +4 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +7 -0
- package/src/runtime/memory/lib/embedding-worker.mjs +20 -0
- package/src/runtime/memory/lib/query-handlers.mjs +10 -4
- package/src/runtime/memory/lib/recall-format.mjs +43 -2
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +90 -64
- package/src/runtime/shared/err-text.mjs +4 -1
- package/src/runtime/shared/resource-admission.mjs +11 -0
- package/src/runtime/shared/schedule-model-ref.mjs +28 -0
- package/src/runtime/shared/schedule-session-run.mjs +63 -0
- package/src/runtime/shared/schedules-db.mjs +8 -3
- package/src/runtime/shared/tool-result-summary.mjs +4 -1
- package/src/runtime/shared/tool-surface.mjs +7 -7
- package/src/runtime/shared/transcript-writer.mjs +17 -0
- package/src/session-runtime/channel-config-api.mjs +11 -0
- package/src/session-runtime/config-helpers.mjs +9 -6
- package/src/session-runtime/context-status.mjs +80 -3
- package/src/session-runtime/lifecycle-api.mjs +154 -40
- package/src/session-runtime/provider-auth-api.mjs +21 -2
- package/src/session-runtime/provider-usage.mjs +4 -1
- package/src/session-runtime/resource-api.mjs +12 -11
- package/src/session-runtime/runtime-core.mjs +45 -11
- package/src/session-runtime/session-text.mjs +56 -6
- package/src/session-runtime/session-turn-api.mjs +70 -2
- package/src/session-runtime/tool-catalog.mjs +2 -1
- package/src/session-runtime/workflow-agents-api.mjs +2 -2
- package/src/standalone/agent-tool/render.mjs +5 -1
- package/src/standalone/agent-tool.mjs +63 -3
- package/src/standalone/channel-admin.mjs +19 -3
- package/src/standalone/channel-daemon.mjs +40 -0
- package/src/tui/app/resume-picker.mjs +5 -1
- package/src/tui/app/settings-picker.mjs +19 -0
- package/src/tui/app/transcript-window.mjs +45 -8
- package/src/tui/app/use-mouse-input.mjs +101 -31
- package/src/tui/app/use-transcript-window.mjs +53 -9
- package/src/tui/components/ToolExecution.jsx +3 -4
- package/src/tui/components/TranscriptItem.jsx +3 -0
- package/src/tui/dist/index.mjs +17391 -14920
- package/src/tui/engine/context-state.mjs +10 -1
- package/src/tui/engine/live-share.mjs +390 -0
- package/src/tui/engine/queue-helpers.mjs +23 -0
- package/src/tui/engine/session-api-ext.mjs +439 -40
- package/src/tui/engine/session-api.mjs +7 -1
- package/src/tui/engine/session-flow.mjs +21 -7
- package/src/tui/engine/tool-card-results.mjs +3 -1
- package/src/tui/engine/turn.mjs +57 -4
- package/src/tui/engine.mjs +211 -7
- package/src/tui/index.jsx +2 -0
- package/src/tui/lib/voice-setup.mjs +10 -4
- package/src/tui/paste-attachments.mjs +4 -1
- package/src/vendor/statusline/src/gateway/session-routes.mjs +24 -1
- package/src/workflows/default/WORKFLOW.md +3 -0
- package/vendor/ink/build/display-width.js +14 -0
- package/vendor/ink/build/output.js +24 -3
- package/vendor/ink/build/wrap-text.d.ts +2 -0
- package/vendor/ink/build/wrap-text.js +45 -4
|
@@ -24,6 +24,13 @@ export function createContextState({ runtime, getState, updateState, getPendingS
|
|
|
24
24
|
};
|
|
25
25
|
const baseRouteState = () => ({
|
|
26
26
|
sessionId: runtime.id,
|
|
27
|
+
// Fork-on-resume marker: when the engine opened a live session as a fork
|
|
28
|
+
// (fresh id, copied transcript), hosts validating "resume returned the
|
|
29
|
+
// requested session" must accept the fork by its origin id.
|
|
30
|
+
sessionForkedFrom: runtime.session?.forkedFrom || null,
|
|
31
|
+
// Remote-attach marker: this surface is a live viewer on a session owned
|
|
32
|
+
// by another process; submits are injected, transcript follows disk.
|
|
33
|
+
sessionRemoteAttached: runtime.session?.remoteAttached === true,
|
|
27
34
|
clientHostPid: runtime.clientHostPid || null,
|
|
28
35
|
model: runtime.model,
|
|
29
36
|
provider: runtime.provider,
|
|
@@ -94,7 +101,9 @@ export function createContextState({ runtime, getState, updateState, getPendingS
|
|
|
94
101
|
const hasTurnActivity = state.busy === true
|
|
95
102
|
|| state.spinner != null
|
|
96
103
|
|| state.thinking != null;
|
|
97
|
-
const
|
|
104
|
+
const hasConversationMessages = Number(ctx?.messages?.count || 0) > 0;
|
|
105
|
+
const isFreshSession = !hasProviderUsage && !hasApiContextUsage && !hasTurnActivity
|
|
106
|
+
&& !hasConversationMessages;
|
|
98
107
|
if (isFreshSession) {
|
|
99
108
|
stats.currentEstimatedContextTokens = 0;
|
|
100
109
|
stats.currentContextTokens = 0;
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/tui/engine/live-share.mjs — real-time cross-surface session mirroring.
|
|
3
|
+
*
|
|
4
|
+
* OWNER leg: hosts a local named-pipe (win32) / unix-socket server for the
|
|
5
|
+
* session it owns and pushes frame-batched transcript deltas (items /
|
|
6
|
+
* streaming tail / spinner) to attached viewers. Push-only: zero polling,
|
|
7
|
+
* idle cost is one open listener.
|
|
8
|
+
* VIEWER leg: connects to the owner's pipe and mirrors those deltas into the
|
|
9
|
+
* local engine store, so streaming output appears live on every co-open
|
|
10
|
+
* surface (desktop <-> terminal). Viewer submits travel over the same pipe
|
|
11
|
+
* into the owner's normal submit queue (concurrent input from both sides is
|
|
12
|
+
* serialized there); the durable pending spool remains the fallback when the
|
|
13
|
+
* pipe is down. Ownership/promotion semantics are unchanged — the pipe is a
|
|
14
|
+
* transport, never a second writer.
|
|
15
|
+
*/
|
|
16
|
+
import { createServer, connect } from 'node:net';
|
|
17
|
+
import { unlinkSync } from 'node:fs';
|
|
18
|
+
|
|
19
|
+
// Above this many patched rows per frame the delta degenerates to a full
|
|
20
|
+
// items push (bulk rewrites: compaction, clear, history restore).
|
|
21
|
+
const MAX_FRAME_PATCHES = 48;
|
|
22
|
+
const MAX_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
23
|
+
// Desync-recovery `sync` requests are throttled so a corrupt stream cannot
|
|
24
|
+
// make the owner serialize full transcripts every frame.
|
|
25
|
+
const SYNC_REQUEST_MIN_INTERVAL_MS = 500;
|
|
26
|
+
|
|
27
|
+
export function liveSharePipePath(sessionId, sessionFilePath) {
|
|
28
|
+
return process.platform === 'win32'
|
|
29
|
+
? `\\\\.\\pipe\\mixdog-live-${sessionId}`
|
|
30
|
+
: `${sessionFilePath}.live.sock`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function frameLine(frame) {
|
|
34
|
+
return `${JSON.stringify(frame)}\n`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function attachLineReader(socket, onFrame, onOverflow) {
|
|
38
|
+
let buffer = '';
|
|
39
|
+
socket.on('data', (chunk) => {
|
|
40
|
+
buffer += chunk;
|
|
41
|
+
if (buffer.length > MAX_BUFFER_BYTES) {
|
|
42
|
+
buffer = '';
|
|
43
|
+
onOverflow?.();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let index;
|
|
47
|
+
while ((index = buffer.indexOf('\n')) >= 0) {
|
|
48
|
+
const line = buffer.slice(0, index);
|
|
49
|
+
buffer = buffer.slice(index + 1);
|
|
50
|
+
if (!line.trim()) continue;
|
|
51
|
+
let frame = null;
|
|
52
|
+
try { frame = JSON.parse(line); } catch { continue; }
|
|
53
|
+
if (frame && typeof frame === 'object') onFrame(frame);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createLiveShare({
|
|
59
|
+
ownerSessionId,
|
|
60
|
+
viewerSessionId,
|
|
61
|
+
socketPathFor,
|
|
62
|
+
getPublishedState,
|
|
63
|
+
listeners,
|
|
64
|
+
onRemoteSubmit,
|
|
65
|
+
onOwnerClosed,
|
|
66
|
+
viewerApply,
|
|
67
|
+
}) {
|
|
68
|
+
// ---- owner: pipe server + delta publisher ----
|
|
69
|
+
let server = null;
|
|
70
|
+
let serverId = '';
|
|
71
|
+
let serverPath = '';
|
|
72
|
+
const sockets = new Set();
|
|
73
|
+
let lastItems = null;
|
|
74
|
+
let lastTail = null;
|
|
75
|
+
let lastSpinner = null;
|
|
76
|
+
|
|
77
|
+
const broadcast = (frame) => {
|
|
78
|
+
if (sockets.size === 0) return;
|
|
79
|
+
const line = frameLine(frame);
|
|
80
|
+
for (const socket of sockets) {
|
|
81
|
+
try { socket.write(line); } catch { /* per-socket close handles it */ }
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const fullFrame = (st) => ({
|
|
86
|
+
t: 'full',
|
|
87
|
+
sessionId: serverId,
|
|
88
|
+
items: Array.isArray(st.items) ? st.items : [],
|
|
89
|
+
tail: st.streamingTail || null,
|
|
90
|
+
spinner: st.spinner || null,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Runs on every frame-batched publish. With no viewers it only re-baselines
|
|
94
|
+
// references (pure pointer assignments) — effectively free.
|
|
95
|
+
const onPublish = () => {
|
|
96
|
+
const st = getPublishedState();
|
|
97
|
+
if (!server || sockets.size === 0) {
|
|
98
|
+
lastItems = st.items;
|
|
99
|
+
lastTail = st.streamingTail;
|
|
100
|
+
lastSpinner = st.spinner;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const frame = { t: 'delta' };
|
|
104
|
+
let dirty = false;
|
|
105
|
+
if (st.items !== lastItems) {
|
|
106
|
+
const prev = Array.isArray(lastItems) ? lastItems : [];
|
|
107
|
+
const next = Array.isArray(st.items) ? st.items : [];
|
|
108
|
+
let structural = next.length < prev.length;
|
|
109
|
+
const changed = [];
|
|
110
|
+
if (!structural) {
|
|
111
|
+
for (let i = 0; i < prev.length; i++) {
|
|
112
|
+
if (next[i] === prev[i]) continue;
|
|
113
|
+
if (!next[i]?.id || next[i].id !== prev[i]?.id) { structural = true; break; }
|
|
114
|
+
changed.push(next[i]);
|
|
115
|
+
if (changed.length > MAX_FRAME_PATCHES) { structural = true; break; }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (structural) {
|
|
119
|
+
frame.items = next;
|
|
120
|
+
} else {
|
|
121
|
+
if (changed.length) frame.changed = changed;
|
|
122
|
+
if (next.length > prev.length) frame.appended = next.slice(prev.length);
|
|
123
|
+
}
|
|
124
|
+
lastItems = st.items;
|
|
125
|
+
dirty = true;
|
|
126
|
+
}
|
|
127
|
+
if (st.streamingTail !== lastTail) {
|
|
128
|
+
const nextTail = st.streamingTail || null;
|
|
129
|
+
const prevTail = lastTail || null;
|
|
130
|
+
// Streaming text grows append-only frame to frame: ship just the new
|
|
131
|
+
// suffix so long responses stay a few bytes per frame, not O(text).
|
|
132
|
+
if (nextTail && prevTail && nextTail.id === prevTail.id
|
|
133
|
+
&& typeof nextTail.text === 'string' && typeof prevTail.text === 'string'
|
|
134
|
+
&& nextTail.text.length >= prevTail.text.length
|
|
135
|
+
&& nextTail.text.startsWith(prevTail.text)) {
|
|
136
|
+
const meta = {};
|
|
137
|
+
for (const [key, value] of Object.entries(nextTail)) {
|
|
138
|
+
if (key === 'text') continue;
|
|
139
|
+
if (!Object.is(prevTail[key], value)) meta[key] = value;
|
|
140
|
+
}
|
|
141
|
+
frame.tailAppend = {
|
|
142
|
+
id: nextTail.id,
|
|
143
|
+
base: prevTail.text.length,
|
|
144
|
+
text: nextTail.text.slice(prevTail.text.length),
|
|
145
|
+
...(Object.keys(meta).length ? { meta } : {}),
|
|
146
|
+
};
|
|
147
|
+
} else {
|
|
148
|
+
frame.tail = nextTail;
|
|
149
|
+
}
|
|
150
|
+
lastTail = st.streamingTail;
|
|
151
|
+
dirty = true;
|
|
152
|
+
}
|
|
153
|
+
if (st.spinner !== lastSpinner) {
|
|
154
|
+
frame.spinner = st.spinner || null;
|
|
155
|
+
lastSpinner = st.spinner;
|
|
156
|
+
dirty = true;
|
|
157
|
+
}
|
|
158
|
+
if (dirty) broadcast(frame);
|
|
159
|
+
};
|
|
160
|
+
listeners.add(onPublish);
|
|
161
|
+
|
|
162
|
+
const stopServer = () => {
|
|
163
|
+
if (!server) return;
|
|
164
|
+
try { broadcast({ t: 'close' }); } catch { /* sockets closing anyway */ }
|
|
165
|
+
for (const socket of sockets) {
|
|
166
|
+
try { socket.destroy(); } catch { /* already gone */ }
|
|
167
|
+
}
|
|
168
|
+
sockets.clear();
|
|
169
|
+
const closing = server;
|
|
170
|
+
server = null;
|
|
171
|
+
try { closing.close(); } catch { /* already closed */ }
|
|
172
|
+
if (process.platform !== 'win32' && serverPath) {
|
|
173
|
+
try { unlinkSync(serverPath); } catch { /* never created / already gone */ }
|
|
174
|
+
}
|
|
175
|
+
serverId = '';
|
|
176
|
+
serverPath = '';
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const startServer = (id) => {
|
|
180
|
+
const path = socketPathFor(id);
|
|
181
|
+
const next = createServer((socket) => {
|
|
182
|
+
socket.setNoDelay?.(true);
|
|
183
|
+
sockets.add(socket);
|
|
184
|
+
const cleanup = () => sockets.delete(socket);
|
|
185
|
+
socket.on('close', cleanup);
|
|
186
|
+
socket.on('error', () => {
|
|
187
|
+
cleanup();
|
|
188
|
+
try { socket.destroy(); } catch { /* already gone */ }
|
|
189
|
+
});
|
|
190
|
+
attachLineReader(socket, (frame) => {
|
|
191
|
+
if (frame.t === 'submit' && typeof frame.text === 'string' && frame.text.trim()) {
|
|
192
|
+
onRemoteSubmit(frame.text);
|
|
193
|
+
} else if (frame.t === 'sync') {
|
|
194
|
+
try { socket.write(frameLine(fullFrame(getPublishedState()))); } catch { /* close handles */ }
|
|
195
|
+
}
|
|
196
|
+
}, () => { try { socket.destroy(); } catch { /* already gone */ } });
|
|
197
|
+
try {
|
|
198
|
+
const st = getPublishedState();
|
|
199
|
+
lastItems = st.items;
|
|
200
|
+
lastTail = st.streamingTail;
|
|
201
|
+
lastSpinner = st.spinner;
|
|
202
|
+
socket.write(frameLine(fullFrame(st)));
|
|
203
|
+
} catch {
|
|
204
|
+
try { socket.destroy(); } catch { /* already gone */ }
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
next.on('error', () => {
|
|
208
|
+
// EADDRINUSE (another live owner) or a transient listen failure: give
|
|
209
|
+
// up quietly; the next ensure() tick retries.
|
|
210
|
+
if (server === next) { server = null; serverId = ''; serverPath = ''; }
|
|
211
|
+
try { next.close(); } catch { /* already closed */ }
|
|
212
|
+
});
|
|
213
|
+
if (process.platform !== 'win32') {
|
|
214
|
+
try { unlinkSync(path); } catch { /* no stale socket */ }
|
|
215
|
+
}
|
|
216
|
+
server = next;
|
|
217
|
+
serverId = id;
|
|
218
|
+
serverPath = path;
|
|
219
|
+
try { next.listen(path); } catch {
|
|
220
|
+
server = null;
|
|
221
|
+
serverId = '';
|
|
222
|
+
serverPath = '';
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// ---- viewer: pipe client mirroring owner deltas ----
|
|
227
|
+
let client = null;
|
|
228
|
+
let clientId = '';
|
|
229
|
+
let clientUp = false;
|
|
230
|
+
let clientSyncedId = '';
|
|
231
|
+
let lastSyncRequestAt = 0;
|
|
232
|
+
const viewerSyncWaiters = new Set();
|
|
233
|
+
|
|
234
|
+
const settleViewerSync = (id, synced) => {
|
|
235
|
+
for (const waiter of [...viewerSyncWaiters]) {
|
|
236
|
+
if (waiter.id === id) waiter.finish(synced);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const waitForViewerSync = (id, timeoutMs = 750) => {
|
|
241
|
+
const target = String(id || '');
|
|
242
|
+
if (!target) return Promise.resolve(false);
|
|
243
|
+
if (clientUp && clientId === target && clientSyncedId === target) {
|
|
244
|
+
return Promise.resolve(true);
|
|
245
|
+
}
|
|
246
|
+
return new Promise((resolve) => {
|
|
247
|
+
const waiter = {
|
|
248
|
+
id: target,
|
|
249
|
+
timer: null,
|
|
250
|
+
finish: null,
|
|
251
|
+
};
|
|
252
|
+
waiter.finish = (synced) => {
|
|
253
|
+
if (!viewerSyncWaiters.delete(waiter)) return;
|
|
254
|
+
if (waiter.timer) clearTimeout(waiter.timer);
|
|
255
|
+
resolve(synced === true);
|
|
256
|
+
};
|
|
257
|
+
viewerSyncWaiters.add(waiter);
|
|
258
|
+
waiter.timer = setTimeout(() => waiter.finish(false), Math.max(1, Number(timeoutMs) || 750));
|
|
259
|
+
waiter.timer.unref?.();
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const requestSync = (socket) => {
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
if (now - lastSyncRequestAt < SYNC_REQUEST_MIN_INTERVAL_MS) return;
|
|
266
|
+
lastSyncRequestAt = now;
|
|
267
|
+
try { socket.write(frameLine({ t: 'sync' })); } catch { /* close handles */ }
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const upsertItem = (item) => {
|
|
271
|
+
if (!item || item.id == null) return;
|
|
272
|
+
const items = viewerApply.getState().items;
|
|
273
|
+
const exists = Array.isArray(items) && items.some((it) => it?.id === item.id);
|
|
274
|
+
if (exists) viewerApply.patchItem(item.id, item);
|
|
275
|
+
else viewerApply.appendItems([item]);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const applyViewerFrame = (frame, socket) => {
|
|
279
|
+
if (frame.t === 'full') {
|
|
280
|
+
viewerApply.replaceItems(Array.isArray(frame.items) ? frame.items : []);
|
|
281
|
+
if (frame.tail) viewerApply.updateStreamingTail(frame.tail.id, frame.tail);
|
|
282
|
+
viewerApply.set({ spinner: frame.spinner || null });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (frame.t !== 'delta') return;
|
|
286
|
+
if (Array.isArray(frame.items)) {
|
|
287
|
+
viewerApply.replaceItems(frame.items, { preserveStreamingTail: true });
|
|
288
|
+
} else {
|
|
289
|
+
for (const item of frame.changed || []) upsertItem(item);
|
|
290
|
+
for (const item of frame.appended || []) upsertItem(item);
|
|
291
|
+
}
|
|
292
|
+
if (frame.tailAppend) {
|
|
293
|
+
const current = viewerApply.getState().streamingTail;
|
|
294
|
+
const base = Number(frame.tailAppend.base) || 0;
|
|
295
|
+
if (current && current.id === frame.tailAppend.id
|
|
296
|
+
&& typeof current.text === 'string' && current.text.length === base) {
|
|
297
|
+
viewerApply.updateStreamingTail(frame.tailAppend.id, {
|
|
298
|
+
...(frame.tailAppend.meta || {}),
|
|
299
|
+
text: current.text + String(frame.tailAppend.text || ''),
|
|
300
|
+
});
|
|
301
|
+
} else {
|
|
302
|
+
// Suffix base mismatch — this mirror lost a frame; resync fully.
|
|
303
|
+
requestSync(socket);
|
|
304
|
+
}
|
|
305
|
+
} else if ('tail' in frame) {
|
|
306
|
+
if (frame.tail) viewerApply.updateStreamingTail(frame.tail.id, frame.tail);
|
|
307
|
+
else viewerApply.clearStreamingTail();
|
|
308
|
+
}
|
|
309
|
+
if ('spinner' in frame) viewerApply.set({ spinner: frame.spinner || null });
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const stopClient = () => {
|
|
313
|
+
const closing = client;
|
|
314
|
+
const closingId = clientId;
|
|
315
|
+
client = null;
|
|
316
|
+
clientId = '';
|
|
317
|
+
clientUp = false;
|
|
318
|
+
clientSyncedId = '';
|
|
319
|
+
if (closingId) settleViewerSync(closingId, false);
|
|
320
|
+
if (closing) {
|
|
321
|
+
try { closing.destroy(); } catch { /* already gone */ }
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const startClient = (id) => {
|
|
326
|
+
let socket;
|
|
327
|
+
try { socket = connect(socketPathFor(id)); } catch { return; }
|
|
328
|
+
client = socket;
|
|
329
|
+
clientId = id;
|
|
330
|
+
clientUp = false;
|
|
331
|
+
clientSyncedId = '';
|
|
332
|
+
socket.setNoDelay?.(true);
|
|
333
|
+
socket.on('connect', () => { if (client === socket) clientUp = true; });
|
|
334
|
+
const down = (ownerClosed) => {
|
|
335
|
+
const wasUp = clientUp && client === socket;
|
|
336
|
+
if (client === socket) { client = null; clientId = ''; clientUp = false; }
|
|
337
|
+
try { socket.destroy(); } catch { /* already gone */ }
|
|
338
|
+
// A live link that dropped means the owner ended or crashed: nudge the
|
|
339
|
+
// promotion path instead of waiting for the next store-mtime change.
|
|
340
|
+
if (wasUp) onOwnerClosed?.(id, ownerClosed);
|
|
341
|
+
};
|
|
342
|
+
socket.on('error', () => down(false));
|
|
343
|
+
socket.on('close', () => down(false));
|
|
344
|
+
attachLineReader(socket, (frame) => {
|
|
345
|
+
if (client !== socket) return;
|
|
346
|
+
if (frame.t === 'close') { down(true); return; }
|
|
347
|
+
if (viewerSessionId() !== id) return;
|
|
348
|
+
try {
|
|
349
|
+
applyViewerFrame(frame, socket);
|
|
350
|
+
// A connected socket is not an entry boundary: the viewer must wait
|
|
351
|
+
// until the owner's atomic full frame has replaced the stale disk
|
|
352
|
+
// restore. Desktop resume holds its renderer publication on this
|
|
353
|
+
// barrier, preventing "last user message first, whole turn later".
|
|
354
|
+
if (frame.t === 'full') {
|
|
355
|
+
clientSyncedId = id;
|
|
356
|
+
settleViewerSync(id, true);
|
|
357
|
+
}
|
|
358
|
+
} catch { requestSync(socket); }
|
|
359
|
+
}, () => down(false));
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
// Reconciles both legs against the current session role; called from the
|
|
364
|
+
// engine share tick (also serves as the reconnect/retry cadence).
|
|
365
|
+
ensure() {
|
|
366
|
+
const ownerId = String(ownerSessionId() || '');
|
|
367
|
+
const attachId = ownerId ? '' : String(viewerSessionId() || '');
|
|
368
|
+
if (serverId && serverId !== ownerId) stopServer();
|
|
369
|
+
if (ownerId && !server) startServer(ownerId);
|
|
370
|
+
if (clientId && clientId !== attachId) stopClient();
|
|
371
|
+
if (attachId && !client) startClient(attachId);
|
|
372
|
+
},
|
|
373
|
+
viewerConnected: () => clientUp,
|
|
374
|
+
waitForViewerSync,
|
|
375
|
+
sendSubmit(text) {
|
|
376
|
+
if (!clientUp || !client) return false;
|
|
377
|
+
try {
|
|
378
|
+
client.write(frameLine({ t: 'submit', text: String(text) }));
|
|
379
|
+
return true;
|
|
380
|
+
} catch {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
dispose() {
|
|
385
|
+
listeners.delete(onPublish);
|
|
386
|
+
stopServer();
|
|
387
|
+
stopClient();
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|
|
@@ -81,6 +81,7 @@ export function promptContentText(content) {
|
|
|
81
81
|
if (typeof part === 'string') return part;
|
|
82
82
|
if (part?.type === 'text') return part.text || '';
|
|
83
83
|
if (part?.type === 'image') return '[Image]';
|
|
84
|
+
if (part?.type === 'file') return '[File]';
|
|
84
85
|
return part?.text || '';
|
|
85
86
|
}).filter(Boolean).join('\n');
|
|
86
87
|
}
|
|
@@ -150,6 +151,28 @@ export function mergePastedImages(entries) {
|
|
|
150
151
|
return Object.keys(out).length > 0 ? out : null;
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
// Byte-free image metadata for the user transcript item. The prompt content
|
|
155
|
+
// keeps the full base64 payload for the provider send; transcript items (and
|
|
156
|
+
// therefore desktop snapshots published on every update) carry only
|
|
157
|
+
// name/mime/size so previews can render without bloating state.
|
|
158
|
+
export function promptContentImageMeta(content, pastedImages) {
|
|
159
|
+
const parts = Array.isArray(content) ? content.filter((part) => part?.type === 'image') : [];
|
|
160
|
+
if (parts.length === 0) return null;
|
|
161
|
+
const named = pastedImages && typeof pastedImages === 'object' ? Object.values(pastedImages) : [];
|
|
162
|
+
return parts.map((part, index) => {
|
|
163
|
+
const meta = named[index] && typeof named[index] === 'object' ? named[index] : null;
|
|
164
|
+
const data = typeof part.data === 'string'
|
|
165
|
+
? part.data
|
|
166
|
+
: (typeof part.content === 'string' ? part.content : '');
|
|
167
|
+
return {
|
|
168
|
+
id: meta?.id ?? null,
|
|
169
|
+
name: String(meta?.filename || `Image ${index + 1}`),
|
|
170
|
+
mimeType: String(part.mimeType || part.mediaType || meta?.mediaType || 'image/png'),
|
|
171
|
+
bytes: data.length,
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
153
176
|
export function mergePastedTexts(entries) {
|
|
154
177
|
const out = {};
|
|
155
178
|
for (const entry of entries || []) {
|