mixdog 0.9.81 → 0.9.83
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 +5 -5
- 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/store/save-worker.mjs +16 -14
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.83",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone mixdog coding-agent CLI/TUI workspace.",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"test:tui-input-render": "node --test scripts/prompt-immediate-render-test.mjs",
|
|
84
84
|
"test:tui-streaming-window": "node --test scripts/streaming-tail-window-test.mjs",
|
|
85
85
|
"test:tui-ambiguous-width": "node --test scripts/tui-ambiguous-width-test.mjs",
|
|
86
|
-
"test:release-assets": "node --check scripts/verify-release-assets.mjs && node --check scripts/verify-release-assets-test.mjs && node --test scripts/verify-release-assets-test.mjs",
|
|
86
|
+
"test:release-assets": "node --check scripts/verify-release-assets.mjs && node --check scripts/verify-release-assets-test.mjs && node --check scripts/deploy-workflow-test.mjs && node --test scripts/verify-release-assets-test.mjs scripts/deploy-workflow-test.mjs",
|
|
87
87
|
"test:release-focused": "npm run test:release-assets && npm run test:tool-contracts && npm run test:placeholder && npm run smoke:patch && npm run test:patch-binary-cache && npm run test:providers && npm run test:deferred-tools && npm run smoke:compact && node --test scripts/code-graph-root-federation-test.mjs scripts/code-graph-aggregate-cwd-test.mjs && npm run test:code-graph-dispatch && node --test scripts/code-graph-disk-hit-test.mjs && npm run test:shellhardening && node --test scripts/windows-hide-spawn-options-test.mjs && npm run test:session && npm run test:workflow-editor && npm run test:embedding-runtime && node --test scripts/tui-transcript-perf-test.mjs",
|
|
88
88
|
"test:native-edit-wire": "node --test scripts/native-edit-wire-test.mjs",
|
|
89
89
|
"test:patch-binary-cache": "node --test scripts/patch-binary-cache-test.mjs",
|
|
@@ -106,9 +106,9 @@
|
|
|
106
106
|
"audit:models": "node scripts/model-catalog-audit.mjs",
|
|
107
107
|
"patch:replay": "node scripts/patch-replay.mjs",
|
|
108
108
|
"build:tui": "node scripts/build-tui.mjs",
|
|
109
|
-
"release:patch": "
|
|
110
|
-
"release:minor": "
|
|
111
|
-
"release:major": "
|
|
109
|
+
"release:patch": "gh workflow run deploy.yml --ref main -f bump=patch -f native=auto",
|
|
110
|
+
"release:minor": "gh workflow run deploy.yml --ref main -f bump=minor -f native=auto",
|
|
111
|
+
"release:major": "gh workflow run deploy.yml --ref main -f bump=major -f native=auto"
|
|
112
112
|
},
|
|
113
113
|
"engines": {
|
|
114
114
|
"node": ">=22.0.0"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { readFile } from 'node:fs/promises'
|
|
5
|
+
import { dirname, join, resolve } from 'node:path'
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
7
|
+
|
|
8
|
+
import { embeddingRuntimeTarget } from './prune-embedding-runtime.mjs'
|
|
9
|
+
|
|
10
|
+
export const RUNTIME_DEPENDENCY_CACHE_SCHEMA = 1
|
|
11
|
+
|
|
12
|
+
function sha256(value) {
|
|
13
|
+
return createHash('sha256').update(value).digest('hex')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function normalizeRuntimeLockfile(lockfile) {
|
|
17
|
+
const normalized = structuredClone(
|
|
18
|
+
typeof lockfile === 'string' || Buffer.isBuffer(lockfile)
|
|
19
|
+
? JSON.parse(String(lockfile))
|
|
20
|
+
: lockfile,
|
|
21
|
+
)
|
|
22
|
+
// Deploy updates the package identity before every release, but that does
|
|
23
|
+
// not change the production dependency tree cached by native packagers.
|
|
24
|
+
delete normalized.name
|
|
25
|
+
delete normalized.version
|
|
26
|
+
if (normalized.packages?.['']) {
|
|
27
|
+
delete normalized.packages[''].name
|
|
28
|
+
delete normalized.packages[''].version
|
|
29
|
+
}
|
|
30
|
+
return normalized
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function runtimeDependencyFingerprint({
|
|
34
|
+
lockfile,
|
|
35
|
+
prunerSource,
|
|
36
|
+
target,
|
|
37
|
+
host = `${process.platform}-${process.arch}`,
|
|
38
|
+
nodeAbi = process.versions.modules,
|
|
39
|
+
}) {
|
|
40
|
+
return sha256(JSON.stringify({
|
|
41
|
+
schemaVersion: RUNTIME_DEPENDENCY_CACHE_SCHEMA,
|
|
42
|
+
target,
|
|
43
|
+
host,
|
|
44
|
+
nodeAbi,
|
|
45
|
+
lockfile: normalizeRuntimeLockfile(lockfile),
|
|
46
|
+
prunerSha256: sha256(prunerSource),
|
|
47
|
+
}))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function runtimeDependencyCacheIdentity(rootDir, target) {
|
|
51
|
+
const [lockfile, prunerSource] = await Promise.all([
|
|
52
|
+
readFile(join(rootDir, 'package-lock.json')),
|
|
53
|
+
readFile(join(rootDir, 'scripts', 'prune-embedding-runtime.mjs')),
|
|
54
|
+
])
|
|
55
|
+
const host = `${process.platform}-${process.arch}`
|
|
56
|
+
const nodeAbi = process.versions.modules
|
|
57
|
+
return {
|
|
58
|
+
schemaVersion: RUNTIME_DEPENDENCY_CACHE_SCHEMA,
|
|
59
|
+
target: target.key,
|
|
60
|
+
host,
|
|
61
|
+
nodeAbi,
|
|
62
|
+
fingerprint: runtimeDependencyFingerprint({
|
|
63
|
+
lockfile,
|
|
64
|
+
prunerSource,
|
|
65
|
+
target: target.key,
|
|
66
|
+
host,
|
|
67
|
+
nodeAbi,
|
|
68
|
+
}),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function runtimeDependencyCacheKey(identity) {
|
|
73
|
+
return `runtime-deps-v${identity.schemaVersion}-${identity.target}-${identity.fingerprint}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : ''
|
|
77
|
+
if (invokedPath === import.meta.url) {
|
|
78
|
+
const optionValue = (name) => {
|
|
79
|
+
const prefix = `--${name}=`
|
|
80
|
+
const argument = process.argv.find((value) => value.startsWith(prefix))
|
|
81
|
+
return argument ? argument.slice(prefix.length) : ''
|
|
82
|
+
}
|
|
83
|
+
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
84
|
+
const target = embeddingRuntimeTarget({
|
|
85
|
+
platform: optionValue('platform') || undefined,
|
|
86
|
+
arch: optionValue('arch') || undefined,
|
|
87
|
+
})
|
|
88
|
+
runtimeDependencyCacheIdentity(rootDir, target)
|
|
89
|
+
.then((identity) => process.stdout.write(`${runtimeDependencyCacheKey(identity)}\n`))
|
|
90
|
+
.catch((error) => {
|
|
91
|
+
process.stderr.write(`Runtime dependency cache key failed: ${error?.message || error}\n`)
|
|
92
|
+
process.exitCode = 1
|
|
93
|
+
})
|
|
94
|
+
}
|
|
@@ -203,8 +203,11 @@ export function contentToText(content, fallback = '') {
|
|
|
203
203
|
|
|
204
204
|
function storedHistoryImagePlaceholder(part) {
|
|
205
205
|
const info = imageInfo(part) || geminiInlineInfo(part);
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
// Inline base64 already gives us its MIME type. Do not call
|
|
207
|
+
// imageUrlFromPart in that case: it would manufacture a second
|
|
208
|
+
// `data:...;base64,<entire payload>` string merely to discard it.
|
|
209
|
+
const url = info ? null : imageUrlFromPart(part);
|
|
210
|
+
const fileUri = info ? null : imageFileUriFromPart(part);
|
|
208
211
|
const mimeType = info?.mimeType || imageMimeFromDataUrl(url) || fileUri?.mimeType || (part?.type === 'image' ? DEFAULT_IMAGE_MIME : '');
|
|
209
212
|
return `[Image omitted from stored history${mimeType ? `: ${mimeType}` : ''}]`;
|
|
210
213
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Worker } from 'worker_threads';
|
|
2
2
|
import { guardedSaveOptions as _guardedSaveOptions } from './write-guards.mjs';
|
|
3
|
-
import { _ensureLifecycleFields } from './serialize.mjs';
|
|
3
|
+
import { _ensureLifecycleFields, _sessionForDisk } from './serialize.mjs';
|
|
4
4
|
import { setLiveSession, _droppedSaveIds, clearSessionSaveError } from './live-state.mjs';
|
|
5
5
|
import { _cacheSessionSummary, _rollbackCachedSessionSummary, _queueSessionSummaryUpsert } from './summary-cache.mjs';
|
|
6
6
|
|
|
@@ -23,6 +23,17 @@ let _saveWorkerRefCount = 0;
|
|
|
23
23
|
let _deferredSaveReqId = 0;
|
|
24
24
|
export const _deferredSessionSaves = new Map();
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Build the exact payload the worker will persist BEFORE Worker.postMessage
|
|
28
|
+
* structured-clones it on the caller's thread. Inline image/document bytes and
|
|
29
|
+
* transient live-turn aliases are disk-ineligible already; removing them here
|
|
30
|
+
* prevents a multi-megabyte duplicate allocation and long clone pause while
|
|
31
|
+
* preserving canonical text/tool history byte-for-byte.
|
|
32
|
+
*/
|
|
33
|
+
export function _sessionPayloadForSaveWorker(session) {
|
|
34
|
+
return _sessionForDisk(session);
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
function _getOrSpawnWorker() {
|
|
27
38
|
if (_saveWorker) return _saveWorker;
|
|
28
39
|
_saveWorker = new Worker(new URL('../save-session-worker.mjs', import.meta.url), {
|
|
@@ -179,19 +190,10 @@ export function saveSessionAsync(session, opts) {
|
|
|
179
190
|
const id = session.id;
|
|
180
191
|
const summaryVersion = _cacheSessionSummary(session);
|
|
181
192
|
const safeOpts = opts?._sessionWriteGuard ? opts : _guardedSaveOptions(id, opts);
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
// non-cloneable values (a function, and raw messages that can hold functions),
|
|
187
|
-
// which makes structuredClone throw "could not be cloned" for every mid-turn
|
|
188
|
-
// iteration save. The worker strips both via _sessionForDisk anyway, so drop
|
|
189
|
-
// them from the cloned payload here WITHOUT mutating the live session object.
|
|
190
|
-
const clonePayload = (session && typeof session === 'object'
|
|
191
|
-
&& (Object.prototype.hasOwnProperty.call(session, 'liveTurnMessages')
|
|
192
|
-
|| Object.prototype.hasOwnProperty.call(session, 'toolApprovalHook')))
|
|
193
|
-
? (() => { const { liveTurnMessages: _dropLTM, toolApprovalHook: _dropTAH, ...rest } = session; return rest; })()
|
|
194
|
-
: session;
|
|
193
|
+
// Worker.postMessage clones on THIS thread. Project to the canonical disk
|
|
194
|
+
// shape first so media bytes and duplicate live-turn aliases never enter
|
|
195
|
+
// that clone. setLiveSession above deliberately retains the rich original.
|
|
196
|
+
const clonePayload = _sessionPayloadForSaveWorker(session);
|
|
195
197
|
return new Promise((resolve, reject) => {
|
|
196
198
|
const waiter = { resolve, reject };
|
|
197
199
|
if (_saveAsyncInflight.has(id)) {
|
|
@@ -1,184 +1,121 @@
|
|
|
1
|
-
// Turn-scoped
|
|
1
|
+
// Turn-scoped review registry.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// lead edits, subagent edits, background shell jobs, even external editors —
|
|
9
|
-
// which transcript-parsed patches structurally cannot see.
|
|
3
|
+
// The public function names retain the original shadow-snapshot API so desktop
|
|
4
|
+
// capability callers stay compatible. The tracked data now follows Codex's
|
|
5
|
+
// attribution rule instead: only successful apply_patch UI diffs are recorded,
|
|
6
|
+
// each worker keeps its own review, and a Lead turn may read its child reviews
|
|
7
|
+
// without reassigning them to the Lead.
|
|
10
8
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// throw into the turn path. MIXDOG_TURN_SNAPSHOT=0 disables the feature.
|
|
15
|
-
import { execFile } from 'child_process';
|
|
16
|
-
import { createHash } from 'crypto';
|
|
17
|
-
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
18
|
-
import { join, resolve } from 'path';
|
|
19
|
-
import { resolvePluginData } from './plugin-paths.mjs';
|
|
9
|
+
// Lead patches already live in the Lead transcript. Worker patches are
|
|
10
|
+
// delivered through agentLoop.onToolResult and frozen here against the owning
|
|
11
|
+
// Lead turn generation. Shell/background/external-editor writes are excluded.
|
|
20
12
|
|
|
21
13
|
const DISABLED = /^(0|false|off)$/i.test(String(process.env.MIXDOG_TURN_SNAPSHOT || ''));
|
|
22
|
-
const
|
|
23
|
-
const BEGIN_WAIT_CAP_MS = 1_500;
|
|
14
|
+
const TURN_CACHE_MAX = 32;
|
|
24
15
|
const MAX_PATCH_BYTES = 2_000_000;
|
|
25
|
-
const BASE_CACHE_MAX = 32;
|
|
26
|
-
const FAILURE_BACKOFF_MS = 5 * 60_000;
|
|
27
16
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
const _failedUntil = new Map(); // gitdir → timestamp
|
|
17
|
+
const _turnsBySession = new Map();
|
|
18
|
+
let _agentTurnSeq = 0;
|
|
31
19
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
if (!raw) return '';
|
|
35
|
-
const full = resolve(raw);
|
|
36
|
-
return process.platform === 'win32' ? full.toLowerCase() : full;
|
|
20
|
+
function clean(value) {
|
|
21
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
37
22
|
}
|
|
38
23
|
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
24
|
+
function trimTurnCache() {
|
|
25
|
+
while (_turnsBySession.size > TURN_CACHE_MAX) {
|
|
26
|
+
const oldest = _turnsBySession.keys().next().value;
|
|
27
|
+
if (oldest === undefined) break;
|
|
28
|
+
_turnsBySession.delete(oldest);
|
|
29
|
+
}
|
|
42
30
|
}
|
|
43
31
|
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
});
|
|
32
|
+
function mergePatches(values) {
|
|
33
|
+
let merged = '';
|
|
34
|
+
for (const value of Array.isArray(values) ? values : [values]) {
|
|
35
|
+
const patch = typeof value === 'string' ? value.trim() : '';
|
|
36
|
+
if (!patch) continue;
|
|
37
|
+
const next = merged ? `${merged}\n${patch}` : patch;
|
|
38
|
+
if (next.length > MAX_PATCH_BYTES) break;
|
|
39
|
+
merged = next;
|
|
40
|
+
}
|
|
41
|
+
return merged;
|
|
55
42
|
}
|
|
56
43
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
_queues.set(gitdir, tail.catch(() => {}));
|
|
62
|
-
return tail;
|
|
44
|
+
function publicAgentReviews(sessionId) {
|
|
45
|
+
const turn = _turnsBySession.get(clean(sessionId));
|
|
46
|
+
if (!turn) return [];
|
|
47
|
+
return [...turn.agents.values()].map((review) => ({ ...review }));
|
|
63
48
|
}
|
|
64
49
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
mkdirSync(gitdir, { recursive: true });
|
|
68
|
-
const init = await _git(['init', '--bare', '--quiet', gitdir]);
|
|
69
|
-
if (init.code !== 0) return false;
|
|
70
|
-
// The shadow repo indexes the project via --work-tree; never let it descend
|
|
71
|
-
// into the project's real .git, and keep bytes stable/gc quiet.
|
|
72
|
-
await _git(['--git-dir', gitdir, 'config', 'core.bare', 'false']);
|
|
73
|
-
await _git(['--git-dir', gitdir, 'config', 'core.autocrlf', 'false']);
|
|
74
|
-
await _git(['--git-dir', gitdir, 'config', 'gc.auto', '0']);
|
|
75
|
-
try {
|
|
76
|
-
mkdirSync(join(gitdir, 'info'), { recursive: true });
|
|
77
|
-
writeFileSync(join(gitdir, 'info', 'exclude'), '.git/\n');
|
|
78
|
-
// First-snapshot cost: reuse the project's own git objects so unchanged
|
|
79
|
-
// blobs need no re-hash/store (the opencode chromium lesson).
|
|
80
|
-
const projectObjects = join(worktree, '.git', 'objects');
|
|
81
|
-
if (existsSync(projectObjects)) {
|
|
82
|
-
mkdirSync(join(gitdir, 'objects', 'info'), { recursive: true });
|
|
83
|
-
writeFileSync(join(gitdir, 'objects', 'info', 'alternates'), `${projectObjects}\n`);
|
|
84
|
-
}
|
|
85
|
-
} catch { /* exclusions/alternates are optimizations, not requirements */ }
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
50
|
+
/** Compatibility no-op: apply_patch tracking has no project warmup cost. */
|
|
51
|
+
export function prewarmTurnSnapshot() {}
|
|
88
52
|
|
|
89
|
-
|
|
90
|
-
async function
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
_failedUntil.set(gitdir, Date.now() + FAILURE_BACKOFF_MS);
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
const base = ['--git-dir', gitdir, '--work-tree', key];
|
|
102
|
-
const add = await _git([...base, 'add', '-A', '--', '.'], { cwd: key });
|
|
103
|
-
if (add.code !== 0) {
|
|
104
|
-
_failedUntil.set(gitdir, Date.now() + FAILURE_BACKOFF_MS);
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
const tree = await _git([...base, 'write-tree'], { cwd: key });
|
|
108
|
-
if (tree.code !== 0) return null;
|
|
109
|
-
const hash = tree.stdout.trim();
|
|
110
|
-
return /^[0-9a-f]{40,64}$/.test(hash) ? hash : null;
|
|
53
|
+
/** Start a new user turn and invalidate the prior turn's child review group. */
|
|
54
|
+
export async function beginTurnSnapshot(_worktree, sessionId) {
|
|
55
|
+
const ownerSessionId = clean(sessionId);
|
|
56
|
+
if (DISABLED || !ownerSessionId) return;
|
|
57
|
+
const generation = (_turnsBySession.get(ownerSessionId)?.generation || 0) + 1;
|
|
58
|
+
_turnsBySession.delete(ownerSessionId);
|
|
59
|
+
_turnsBySession.set(ownerSessionId, {
|
|
60
|
+
generation,
|
|
61
|
+
agents: new Map(),
|
|
111
62
|
});
|
|
63
|
+
trimTurnCache();
|
|
112
64
|
}
|
|
113
65
|
|
|
114
|
-
/**
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
66
|
+
/** Bind one worker execution to the Lead turn that launched it. */
|
|
67
|
+
export function beginAgentTurnReview(ownerSessionId, childSessionId, meta = {}) {
|
|
68
|
+
if (DISABLED) return null;
|
|
69
|
+
const owner = clean(ownerSessionId);
|
|
70
|
+
const child = clean(childSessionId);
|
|
71
|
+
const turn = _turnsBySession.get(owner);
|
|
72
|
+
if (!owner || !child || !turn) return null;
|
|
73
|
+
_agentTurnSeq += 1;
|
|
74
|
+
return {
|
|
75
|
+
id: `agent-review-${_agentTurnSeq}`,
|
|
76
|
+
ownerSessionId: owner,
|
|
77
|
+
generation: turn.generation,
|
|
78
|
+
sessionId: child,
|
|
79
|
+
agent: clean(meta.agent) || null,
|
|
80
|
+
tag: clean(meta.tag) || null,
|
|
81
|
+
completed: false,
|
|
82
|
+
};
|
|
119
83
|
}
|
|
120
84
|
|
|
121
|
-
/**
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
})
|
|
137
|
-
|
|
85
|
+
/** Freeze successful worker apply_patch diffs into their owning Lead turn. */
|
|
86
|
+
export function completeAgentTurnReview(handle, patches = []) {
|
|
87
|
+
if (!handle || handle.completed === true) return false;
|
|
88
|
+
handle.completed = true;
|
|
89
|
+
const turn = _turnsBySession.get(clean(handle.ownerSessionId));
|
|
90
|
+
if (!turn || turn.generation !== handle.generation) return false;
|
|
91
|
+
const patch = mergePatches(patches);
|
|
92
|
+
if (!patch) return false;
|
|
93
|
+
const key = clean(handle.sessionId) || handle.id;
|
|
94
|
+
const prior = turn.agents.get(key);
|
|
95
|
+
turn.agents.set(key, {
|
|
96
|
+
sessionId: key,
|
|
97
|
+
agent: handle.agent || prior?.agent || null,
|
|
98
|
+
tag: handle.tag || prior?.tag || null,
|
|
99
|
+
patch: mergePatches([prior?.patch, patch]),
|
|
100
|
+
});
|
|
101
|
+
return true;
|
|
138
102
|
}
|
|
139
103
|
|
|
140
|
-
/**
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (head === base.tree) return { supported: true, files: [], patch: '', baseTree: base.tree, headTree: head };
|
|
153
|
-
const gitdir = _gitDirFor(key);
|
|
154
|
-
const argsBase = ['--git-dir', gitdir];
|
|
155
|
-
const [numstat, patch] = await Promise.all([
|
|
156
|
-
_git([...argsBase, 'diff-tree', '-r', '--numstat', '-z', base.tree, head]),
|
|
157
|
-
_git([...argsBase, 'diff-tree', '-r', '-p', '--no-color', base.tree, head]),
|
|
158
|
-
]);
|
|
159
|
-
if (numstat.code !== 0) return { supported: false, files: [], patch: '' };
|
|
160
|
-
const files = [];
|
|
161
|
-
const fields = numstat.stdout.split('\0').filter(Boolean);
|
|
162
|
-
for (const row of fields) {
|
|
163
|
-
const match = row.match(/^(\d+|-)\t(\d+|-)\t([\s\S]+)$/);
|
|
164
|
-
if (!match) continue;
|
|
165
|
-
files.push({
|
|
166
|
-
name: match[3],
|
|
167
|
-
additions: match[1] === '-' ? 0 : Number(match[1]),
|
|
168
|
-
deletions: match[2] === '-' ? 0 : Number(match[2]),
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
let patchText = patch.code === 0 ? patch.stdout : '';
|
|
172
|
-
if (patchText.length > MAX_PATCH_BYTES) {
|
|
173
|
-
patchText = patchText.slice(0, MAX_PATCH_BYTES);
|
|
174
|
-
const cut = patchText.lastIndexOf('\ndiff --git ');
|
|
175
|
-
if (cut > 0) patchText = patchText.slice(0, cut + 1);
|
|
176
|
-
}
|
|
177
|
-
return { supported: true, files, patch: patchText, baseTree: base.tree, headTree: head };
|
|
104
|
+
/** Return only attributed child reviews; Lead review is transcript-derived. */
|
|
105
|
+
export async function getTurnReviewDiff(_worktree, sessionId) {
|
|
106
|
+
if (DISABLED) return { supported: false, files: [], patch: '', agents: [] };
|
|
107
|
+
const ownerSessionId = clean(sessionId);
|
|
108
|
+
const turn = _turnsBySession.get(ownerSessionId);
|
|
109
|
+
return {
|
|
110
|
+
supported: true,
|
|
111
|
+
files: [],
|
|
112
|
+
patch: '',
|
|
113
|
+
agents: publicAgentReviews(ownerSessionId),
|
|
114
|
+
...(turn ? { generation: turn.generation } : { reason: 'no-turn' }),
|
|
115
|
+
};
|
|
178
116
|
}
|
|
179
117
|
|
|
180
118
|
export function _resetTurnSnapshotForTest() {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
_failedUntil.clear();
|
|
119
|
+
_turnsBySession.clear();
|
|
120
|
+
_agentTurnSeq = 0;
|
|
184
121
|
}
|
|
@@ -1227,18 +1227,21 @@ export async function createMixdogSessionRuntime({
|
|
|
1227
1227
|
const startedAt = performance.now();
|
|
1228
1228
|
bootProfile('session:create:start', { mode, reason });
|
|
1229
1229
|
const promise = (async () => {
|
|
1230
|
+
// Demand-only: this starts only after the user submits (unless an
|
|
1231
|
+
// explicitly enabled prewarm caller asks for a session). Core-memory
|
|
1232
|
+
// startup does not depend on keychain/provider readiness, so overlap the
|
|
1233
|
+
// two cold paths instead of paying their bounded waits serially.
|
|
1234
|
+
const coreMemoryContextPromise = loadCoreMemoryContext();
|
|
1230
1235
|
await awaitKeychainPrewarm();
|
|
1231
1236
|
ensureConfigForRouteProvider();
|
|
1232
1237
|
await resolveMissingRouteModelForFirstTurn();
|
|
1233
1238
|
requireModelRoute();
|
|
1234
1239
|
bootProfile('session:create:route-ready', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
1235
|
-
//
|
|
1236
|
-
//
|
|
1237
|
-
// display fields, never provider/model that the memory load reads — so run
|
|
1238
|
-
// them concurrently instead of serially on the boot path.
|
|
1240
|
+
// Route effort waits on provider readiness while the already-started
|
|
1241
|
+
// memory load continues independently.
|
|
1239
1242
|
const [, coreMemoryContext] = await Promise.all([
|
|
1240
1243
|
refreshRouteEffort(),
|
|
1241
|
-
|
|
1244
|
+
coreMemoryContextPromise,
|
|
1242
1245
|
]);
|
|
1243
1246
|
bootProfile('session:create:effort-ready', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
1244
1247
|
const providerImpl = reg.getProvider(route.provider);
|
|
@@ -1846,8 +1849,8 @@ export async function createMixdogSessionRuntime({
|
|
|
1846
1849
|
...channelConfigApi,
|
|
1847
1850
|
...providerAuthApi,
|
|
1848
1851
|
...mediaApi,
|
|
1849
|
-
// Turn-scoped
|
|
1850
|
-
//
|
|
1852
|
+
// Turn-scoped attributed review: child apply_patch diffs for this Lead turn.
|
|
1853
|
+
// Lead patches are read from its transcript by the renderer.
|
|
1851
1854
|
getTurnReviewDiff: () => getTurnSnapshotReviewDiff(currentCwd, session?.id),
|
|
1852
1855
|
get id() {
|
|
1853
1856
|
return session?.id || null;
|
|
@@ -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
|
}
|
package/src/tui/engine.mjs
CHANGED
|
@@ -899,6 +899,7 @@ export async function createEngineSession({
|
|
|
899
899
|
const timer = setTimeout(() => {
|
|
900
900
|
if (flags.disposed || !state.sessionRemoteAttached) return;
|
|
901
901
|
if (String(state.sessionId || '') !== id) return;
|
|
902
|
+
if (liveShare.viewerConnected()) return;
|
|
902
903
|
void Promise.resolve(api.resume(id, { quiet: true })).catch(() => { /* tick retries */ });
|
|
903
904
|
}, 1500);
|
|
904
905
|
timer.unref?.();
|
|
@@ -928,9 +929,16 @@ export async function createEngineSession({
|
|
|
928
929
|
if (typeof api.submit === 'function') {
|
|
929
930
|
const baseSubmit = api.submit;
|
|
930
931
|
api.submit = (prompt, options = {}) => {
|
|
931
|
-
if (state.sessionRemoteAttached
|
|
932
|
+
if (state.sessionRemoteAttached) {
|
|
932
933
|
const text = String(promptDisplayText(prompt, options) || '').trim();
|
|
934
|
+
if (!text) return false;
|
|
935
|
+
// Reconcile first so a session that became attachable this event-loop
|
|
936
|
+
// turn uses the instant pipe path. If the pipe is still opening, write
|
|
937
|
+
// directly to the durable owner spool instead of starting a fake local
|
|
938
|
+
// turn that renders an error/synthetic assistant message.
|
|
939
|
+
try { liveShare.ensure(); } catch { /* durable fallback below */ }
|
|
933
940
|
if (text && liveShare.sendSubmit(text)) return true;
|
|
941
|
+
return runtime.enqueueRemoteAttachedPrompt?.(prompt) === true;
|
|
934
942
|
}
|
|
935
943
|
return baseSubmit(prompt, options);
|
|
936
944
|
};
|
|
@@ -973,6 +981,9 @@ export async function createEngineSession({
|
|
|
973
981
|
return result;
|
|
974
982
|
};
|
|
975
983
|
}
|
|
984
|
+
// Cover engines whose runtime already has a session at construction time;
|
|
985
|
+
// do not wait for a lifecycle method or the 3s safety pulse to open the pipe.
|
|
986
|
+
reconcileLiveShareNow();
|
|
976
987
|
// Instant input pickup: watch the shared pending spool so an attached
|
|
977
988
|
// surface's fallback submit reaches this owner immediately instead of on
|
|
978
989
|
// the 3s tick. Best-effort — the tick below remains the safety net.
|