@awak-app/simy-cli 0.7.43 → 0.7.45
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/src/cli-contract.js +1 -1
- package/src/delivery-hooks/supervisor.js +6 -2
- package/src/desktop-catalog-frames.js +39 -5
- package/src/desktop-host.js +7 -4
- package/src/session-supervision/codex-app-server.js +19 -2
- package/src/session-supervision/index.js +25 -8
- package/src/session-supervision/owner-transport.js +4 -2
package/package.json
CHANGED
package/src/cli-contract.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "./durable-local-task-steps-contract.js";
|
|
15
15
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
16
16
|
|
|
17
|
-
export const CLI_VERSION = "0.7.
|
|
17
|
+
export const CLI_VERSION = "0.7.45";
|
|
18
18
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
19
19
|
|
|
20
20
|
export function narrativeImageCapabilityMetadata() {
|
|
@@ -160,7 +160,7 @@ export async function executeCodexResume(record, { signal, root, leaseId, execut
|
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
export async function tickManagedTasks({ root, webOrigin, now = Date.now(), execute, inspectProcess = processIdentity, signal, beforeClaim, authorizeDispatch, readRecords, observeTurn, observeAuthentication, inspectProvider = inspectCodexAppServer, shouldYield, nativeHandoff = reconcileNativeHandoff } = {}) {
|
|
163
|
+
export async function tickManagedTasks({ root, webOrigin, now = Date.now(), execute, inspectProcess = processIdentity, signal, beforeClaim, authorizeDispatch, readRecords, readCancellationRecords, observeTurn, observeAuthentication, inspectProvider = inspectCodexAppServer, shouldYield, nativeHandoff = reconcileNativeHandoff } = {}) {
|
|
164
164
|
const tickWallStart = Date.now();
|
|
165
165
|
const currentTime = () => now + Math.max(0, Date.now() - tickWallStart);
|
|
166
166
|
if (signal?.aborted) return;
|
|
@@ -170,7 +170,11 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
170
170
|
// OFF changes the consent epoch and removes a task from a scoped activation.
|
|
171
171
|
// Still reconcile its old native queue on reconnect, without admitting work
|
|
172
172
|
// outside this supervisor's selected scope.
|
|
173
|
-
|
|
173
|
+
// Desktop callers can share their fresh metadata-validated global index.
|
|
174
|
+
// Keep it separate from the selected scope: a revoked native queue must still
|
|
175
|
+
// be cancelled, and the handoff reconciler re-reads authority before acting.
|
|
176
|
+
const cancellationOnly = readRecords ? (await (readCancellationRecords ? readCancellationRecords() : listRecords(root)))
|
|
177
|
+
.filter(r => r.run?.managed?.nativeHandoff && !selectedIds.has(identity(r))) : [];
|
|
174
178
|
for (const record of [...selected, ...cancellationOnly]) {
|
|
175
179
|
if (signal?.aborted || await shouldYield?.()) return;
|
|
176
180
|
if ((!execute || record.run?.managed?.nativeHandoff) && await nativeHandoff({ root, record, signal, authorize: authorizeDispatch, cancelOnly: !selectedIds.has(identity(record)) })) nativeOwned.add(identity(record));
|
|
@@ -41,6 +41,38 @@ function updateRows(previous, incoming, full, { unchanged = false, immutableInpu
|
|
|
41
41
|
return { rows: immutableInput ? incoming : rows, payload: { count: incoming.length, changes } };
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// A display ordering change is not a row-content change. Version 2 references
|
|
45
|
+
// positions in the last delivered base; ambiguous identities are sent in full.
|
|
46
|
+
const rowIdentity = row => row && typeof row.session === 'string' && row.session
|
|
47
|
+
? JSON.stringify([row.hostId ?? 'local', row.provider ?? null, row.session]) : null;
|
|
48
|
+
function updateOrderedRows(previous, incoming, full, { unchanged = false, immutableInput = false } = {}) {
|
|
49
|
+
if (full || previous === null) return updateRows(previous, incoming, true, { immutableInput });
|
|
50
|
+
if (unchanged) return { rows: previous, payload: { count: previous.length, changes: [] } };
|
|
51
|
+
const positions = new Map(), incomingCounts = new Map();
|
|
52
|
+
for (let index = 0; index < previous.length; index++) {
|
|
53
|
+
const key = rowIdentity(previous[index]);
|
|
54
|
+
if (key !== null) positions.set(key, positions.has(key) ? -1 : index);
|
|
55
|
+
}
|
|
56
|
+
for (const row of incoming) { const key = rowIdentity(row); if (key !== null) incomingCounts.set(key, (incomingCounts.get(key) || 0) + 1); }
|
|
57
|
+
const order = [], changes = [], rows = [], reused = new Set();
|
|
58
|
+
let reordered = previous.length !== incoming.length;
|
|
59
|
+
for (let index = 0; index < incoming.length; index++) {
|
|
60
|
+
const row = incoming[index], key = rowIdentity(row), source = positions.get(key);
|
|
61
|
+
const unique = key !== null && incomingCounts.get(key) === 1 && source >= 0;
|
|
62
|
+
const reuse = unique && !reused.has(source) && isDeepStrictEqual(previous[source], row) ? source
|
|
63
|
+
: !unique && index < previous.length && !reused.has(index) && isDeepStrictEqual(previous[index], row) ? index : null;
|
|
64
|
+
if (reuse !== null) {
|
|
65
|
+
rows.push(previous[reuse]); order.push(reuse); reused.add(reuse);
|
|
66
|
+
if (reuse !== index) reordered = true;
|
|
67
|
+
} else {
|
|
68
|
+
const detached = immutableInput ? row : structuredClone(row);
|
|
69
|
+
rows.push(detached); order.push(null); changes.push([index, detached]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { rows: immutableInput ? incoming : !reordered && changes.length === 0 ? previous : rows,
|
|
73
|
+
payload: { count: incoming.length, changes, ...(reordered ? { order } : {}) } };
|
|
74
|
+
}
|
|
75
|
+
|
|
44
76
|
// Detach mutable producer data once. Unchanged catalog rows may be shared only
|
|
45
77
|
// between these owned read-only snapshots; changed rows get detached copies and
|
|
46
78
|
// a new array. Older queued frames and durable writes must remain immutable.
|
|
@@ -69,7 +101,9 @@ export function createDesktopStatusSnapshot() {
|
|
|
69
101
|
// One encoder per ordered output stream. Call only when a frame actually enters
|
|
70
102
|
// that stream, after replaceable/backpressured snapshots have been coalesced.
|
|
71
103
|
// This cache is transport state; it never authorizes work or provider dispatch.
|
|
72
|
-
export function createDesktopCatalogEncoder({ immutableInput = false } = {}) {
|
|
104
|
+
export function createDesktopCatalogEncoder({ immutableInput = false, version = 1 } = {}) {
|
|
105
|
+
if (![1, 2].includes(version)) throw new Error('Unsupported desktop catalog version.');
|
|
106
|
+
const projectRows = version === 2 ? updateOrderedRows : updateRows;
|
|
73
107
|
let rows = null, candidates = null, sourceRows = null, sourceCandidates = null, scope = null, revision = 0;
|
|
74
108
|
const reset = () => { rows = null; candidates = null; sourceRows = null; sourceCandidates = null; scope = null; };
|
|
75
109
|
return { reset, encode(frame) {
|
|
@@ -81,16 +115,16 @@ export function createDesktopCatalogEncoder({ immutableInput = false } = {}) {
|
|
|
81
115
|
const incoming = status.sessionSupervision.sessions;
|
|
82
116
|
const identity = catalogScope(status);
|
|
83
117
|
const previous = revision++;
|
|
84
|
-
const full = rows === null || scope !== identity || rows.length !== incoming.length;
|
|
85
|
-
const snapshot =
|
|
118
|
+
const full = rows === null || scope !== identity || version === 1 && rows.length !== incoming.length;
|
|
119
|
+
const snapshot = projectRows(rows, incoming, full, { immutableInput, unchanged: immutableInput && sourceRows === incoming });
|
|
86
120
|
rows = snapshot.rows; sourceRows = incoming; scope = identity;
|
|
87
|
-
const catalog = { version
|
|
121
|
+
const catalog = { version, revision, scope, ...(full ? {} : { baseRevision: previous }), ...snapshot.payload };
|
|
88
122
|
const { sessions, ...metadata } = status.sessionSupervision;
|
|
89
123
|
let supervision = metadata;
|
|
90
124
|
if (Array.isArray(metadata.selectionCandidates)) {
|
|
91
125
|
// This is another complete directory, used by the conversation picker.
|
|
92
126
|
// Do not silently retransmit it as supposedly small status metadata.
|
|
93
|
-
const snapshot =
|
|
127
|
+
const snapshot = projectRows(candidates, metadata.selectionCandidates, full,
|
|
94
128
|
{ immutableInput, unchanged: immutableInput && sourceCandidates === metadata.selectionCandidates });
|
|
95
129
|
candidates = snapshot.rows; sourceCandidates = metadata.selectionCandidates; catalog.selectionCandidates = snapshot.payload;
|
|
96
130
|
const { selectionCandidates, ...small } = metadata;
|
package/src/desktop-host.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createDesktopFrameWriter } from './desktop-frame-writer.js';
|
|
|
2
2
|
import { createDesktopCatalogEncoder, createDesktopStatusSnapshot, copyOwnedDesktopStatus } from './desktop-catalog-frames.js';
|
|
3
3
|
import { boundedDiscovery } from './session-supervision/progressive-discovery.js';
|
|
4
4
|
import { createCatalogRefreshPolicy } from './session-supervision/catalog-refresh.js';
|
|
5
|
-
import { supervisedSession } from './session-supervision/discovery-window.js';
|
|
5
|
+
import { liveSessionWindow, supervisedSession } from './session-supervision/discovery-window.js';
|
|
6
6
|
import { createClaudeDesktopArchiveController } from './session-supervision/claude-desktop-archive.js';
|
|
7
7
|
import { createClaudeDesktopNativeAdapter } from './session-supervision/claude-desktop-native.js';
|
|
8
8
|
import { applyControllerExecutionContext } from './session-supervision/execution-context.js';
|
|
@@ -160,8 +160,8 @@ export { createDesktopFrameWriter } from './desktop-frame-writer.js';
|
|
|
160
160
|
|
|
161
161
|
export async function startDesktopHost({ input = process.stdin, output = process.stdout, env = process.env,
|
|
162
162
|
updatePackageRoot, dependencies = {}, exit = (code) => process.exit(code) } = {}) {
|
|
163
|
-
let catalogEncoder = env.SIMY_REMOTE_CHILD !== '1' && env.SIMY_DESKTOP_CATALOG_DELTA
|
|
164
|
-
? createDesktopCatalogEncoder({ immutableInput: true }) : null;
|
|
163
|
+
let catalogEncoder = env.SIMY_REMOTE_CHILD !== '1' && ['1', '2'].includes(env.SIMY_DESKTOP_CATALOG_DELTA)
|
|
164
|
+
? createDesktopCatalogEncoder({ immutableInput: true, version: env.SIMY_DESKTOP_CATALOG_VERSION === '2' ? 2 : Number(env.SIMY_DESKTOP_CATALOG_DELTA) }) : null;
|
|
165
165
|
// Object frames below own detached snapshots. Queue their references rather
|
|
166
166
|
// than copying multi-MiB history again whenever a native reader is paused.
|
|
167
167
|
const frames = createDesktopFrameWriter(output, { capture: frame => frame,
|
|
@@ -605,6 +605,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
605
605
|
const link = await requireProviderLink({ root: deliveryRoot, provider: record.provider });
|
|
606
606
|
return JSON.stringify([scope, link.epoch]);
|
|
607
607
|
}, readRecords: () => recordReader.list(),
|
|
608
|
+
readCancellationRecords: () => recordReader.list(),
|
|
608
609
|
readCurrentRecord: (provider, session) => readRecord(deliveryRoot, provider, session),
|
|
609
610
|
beforeClaim: record => actionAssociations.prepareCached?.(record) || record });
|
|
610
611
|
let shuttingDown = null;
|
|
@@ -723,6 +724,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
723
724
|
const historyTimeoutMs = dependencies.historyDiscoveryTimeoutMs ?? dependencies.discoveryTimeoutMs ?? 300_000;
|
|
724
725
|
sessionRefresh = boundedDiscovery((signal, startHistory) => (dependencies.discoverSessions || discoverSessions)({
|
|
725
726
|
root: deliveryRoot, signal, onDrain: trackDiscoveryDrain, includeArchived: true, includeSelectionCandidates: true, recentOnly: refresh.recentOnly,
|
|
727
|
+
recentArchiveCandidates: liveSessionWindow(sessionStatus.sessions.filter(item => item.provider === 'codex' && item.archived !== true && item.delegated !== true)),
|
|
726
728
|
readRecords: () => recordReader.list(),
|
|
727
729
|
connectedProviders: PROVIDER_LINKS.filter(provider => providerLinks[provider].connected),
|
|
728
730
|
timeoutMs: firstPageTimeoutMs, historyTimeoutMs,
|
|
@@ -2562,7 +2564,8 @@ if (isDesktopHostEntry()) {
|
|
|
2562
2564
|
try {
|
|
2563
2565
|
const root = process.env.SIMY_HOME?.trim() || join(homedir(), '.simy');
|
|
2564
2566
|
if (process.env.SIMY_DESKTOP_ATTACH_OWNER === '1' && await relayToOwner({ root, webOrigin: resolveWebOrigin(null, process.env), desktopClient: true,
|
|
2565
|
-
catalogDeltas:
|
|
2567
|
+
catalogDeltas: ['1', '2'].includes(process.env.SIMY_DESKTOP_CATALOG_DELTA)
|
|
2568
|
+
? process.env.SIMY_DESKTOP_CATALOG_VERSION === '2' ? 2 : Number(process.env.SIMY_DESKTOP_CATALOG_DELTA) : false })) process.exit(0);
|
|
2566
2569
|
try { await adoptInstalledRuntime({ root }); }
|
|
2567
2570
|
catch (error) {
|
|
2568
2571
|
if (error.code === PROCESS_CONFLICT_CODE) throw error;
|
|
@@ -329,8 +329,13 @@ export async function listCodexSessions({ client, all = false, signal, onPage, m
|
|
|
329
329
|
// Absence from the recent active window does not mean archived. Only the
|
|
330
330
|
// provider's archived list or its exact archived rollout path proves it.
|
|
331
331
|
const archiveProbeOffsets = new Map();
|
|
332
|
+
// Navigation eligibility is distinct from positive archive fencing: excluded
|
|
333
|
+
// worker sources may still revoke consent without replacing delegated rows.
|
|
334
|
+
export function archivedCodexMetadata(metadata) {
|
|
335
|
+
return metadata && SOURCE_KINDS.includes(metadata.source) ? { ...metadata, archived: true } : null;
|
|
336
|
+
}
|
|
332
337
|
export async function readCodexArchiveStates(client, candidates, { budgetMs = 3000,
|
|
333
|
-
codexHome = process.env.CODEX_HOME || join(homedir(), '.codex') } = {}) {
|
|
338
|
+
codexHome = process.env.CODEX_HOME || join(homedir(), '.codex'), observedArchived } = {}) {
|
|
334
339
|
const states = new Map();
|
|
335
340
|
if (!candidates.length) return states;
|
|
336
341
|
const deadline = Date.now() + budgetMs;
|
|
@@ -343,7 +348,13 @@ export async function readCodexArchiveStates(client, candidates, { budgetMs = 30
|
|
|
343
348
|
if (method === 'thread/list' && params.archived === true && Array.isArray(response?.data)) {
|
|
344
349
|
for (const thread of response.data.slice(0, params.limit)) {
|
|
345
350
|
const metadata = codexThreadMetadata(thread);
|
|
346
|
-
if (metadata && eligible.has(metadata.session))
|
|
351
|
+
if (metadata && eligible.has(metadata.session)) {
|
|
352
|
+
states.set(metadata.session, true);
|
|
353
|
+
if (observedArchived) {
|
|
354
|
+
const archived = archivedCodexMetadata(metadata);
|
|
355
|
+
if (archived) observedArchived.set(metadata.session, archived);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
347
358
|
}
|
|
348
359
|
}
|
|
349
360
|
return response;
|
|
@@ -363,6 +374,12 @@ export async function readCodexArchiveStates(client, candidates, { budgetMs = 30
|
|
|
363
374
|
if (thread?.id !== candidate.session || typeof thread.path !== 'string' || !isAbsolute(thread.path)) continue;
|
|
364
375
|
const state = codexArchivePathState(thread.path, candidate.session, codexHome);
|
|
365
376
|
if (state !== undefined) states.set(candidate.session, state);
|
|
377
|
+
if (state === false) observedArchived?.delete(candidate.session);
|
|
378
|
+
if (state === true && observedArchived) {
|
|
379
|
+
const metadata = codexThreadMetadata(thread);
|
|
380
|
+
const archived = archivedCodexMetadata(metadata);
|
|
381
|
+
if (archived) observedArchived.set(metadata.session, archived);
|
|
382
|
+
}
|
|
366
383
|
} catch { /* Unknown or missing sessions remain visible. */ }
|
|
367
384
|
}
|
|
368
385
|
}));
|
|
@@ -10,7 +10,7 @@ import { realpath } from 'node:fs/promises';
|
|
|
10
10
|
import { randomUUID } from 'node:crypto';
|
|
11
11
|
import { isAbsolute } from 'node:path';
|
|
12
12
|
import { TERMINAL as TERMINAL_RUNS, advanceDeliveryControlRevision, deliveryDecisionRevision, associationBlocksFollowup, canUpgradeDelivery, canExplicitlyResumeStopped, enablePurposeDelivery, claudeHookChannel, controlDelivery, listRecords, readRecord, processIdentity, startDelivery, updateRecord, recordProviderArchive, ownerRenewalPending } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
13
|
-
import { listCodexSessions, codexThreadMetadata, openCodexAppServer, readCodexSession, readCodexObservation, readCodexArchiveStates, CodexAppServerError } from './codex-app-server.js';
|
|
13
|
+
import { listCodexSessions, codexThreadMetadata, openCodexAppServer, readCodexSession, readCodexObservation, readCodexArchiveStates, archivedCodexMetadata, CodexAppServerError } from './codex-app-server.js';
|
|
14
14
|
import { listClaudeSessions, readClaudeSession } from './claude-sessions.js';
|
|
15
15
|
import { loadPurposeRuleSet } from './purpose-instruction.js';
|
|
16
16
|
import { originalSessionTarget } from './original-session-target.js';
|
|
@@ -165,7 +165,7 @@ export async function discoverSessions(options = {}) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
async function discoverSessionCatalog({ root, openClient = openCodexAppServer, listClaude = listClaudeSessions,
|
|
168
|
-
inspectProcess = processIdentity, now: requestedNow, signal, listCodexTitles = readCodexTitles, listCodexChildren = readCodexChildSessions, codexHome, connectedProviders = ['codex', 'claude'], includeArchived = false, includeSelectionCandidates = false, recentOnly = false, catalogSnapshot, recordSnapshot, archiveReconciliations = new Map(), readRecords = listRecords, onProgress } = {}) {
|
|
168
|
+
inspectProcess = processIdentity, now: requestedNow, signal, listCodexTitles = readCodexTitles, listCodexChildren = readCodexChildSessions, codexHome, connectedProviders = ['codex', 'claude'], includeArchived = false, includeSelectionCandidates = false, recentOnly = false, recentArchiveCandidates = [], catalogSnapshot, recordSnapshot, archiveReconciliations = new Map(), readRecords = listRecords, onProgress } = {}) {
|
|
169
169
|
const now = requestedNow ?? Date.now();
|
|
170
170
|
const assertActive = () => { if (signal?.aborted) throw new CodexAppServerError('cancelled', 'Conversation discovery was cancelled.'); };
|
|
171
171
|
assertActive();
|
|
@@ -195,19 +195,22 @@ async function discoverSessionCatalog({ root, openClient = openCodexAppServer, l
|
|
|
195
195
|
const active = await listCodexSessions({ client, all: !recentOnly, signal, inspectState: true, codexHome,
|
|
196
196
|
onPage: async items => { discovered = [...items]; await progress({ discovered, archivedCodex }); },
|
|
197
197
|
prioritySessions: priorities.filter(record => record.provider === 'codex').map(record => record.session) });
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
const observedArchives = new Map();
|
|
199
|
+
// Archived navigation belongs to the full/history refresh. A recent
|
|
200
|
+
// poll still observes missing supervised sessions below, so archival
|
|
201
|
+
// revokes their consent immediately without rewriting cold fences.
|
|
202
|
+
if (includeArchived && !recentOnly) {
|
|
200
203
|
const archiveClient = { request: async (method, params) => {
|
|
201
204
|
const response = await client.request(method, params);
|
|
202
205
|
if (method === 'thread/list' && params.archived === true) {
|
|
203
206
|
for (const thread of (response?.data || []).slice(0, params.limit)) {
|
|
204
|
-
const item = codexThreadMetadata(thread);
|
|
205
|
-
if (item
|
|
207
|
+
const item = archivedCodexMetadata(codexThreadMetadata(thread));
|
|
208
|
+
if (item) observedArchives.set(item.session, item);
|
|
206
209
|
}
|
|
207
210
|
}
|
|
208
211
|
return response;
|
|
209
212
|
} };
|
|
210
|
-
try { await listCodexSessions({ client: archiveClient, all:
|
|
213
|
+
try { await listCodexSessions({ client: archiveClient, all: true, signal, archived: true,
|
|
211
214
|
onPage: async () => { archivedCodex = [...observedArchives.values()]; await progress({ discovered, archivedCodex }); } }); }
|
|
212
215
|
catch (error) {
|
|
213
216
|
codexError = 'Codex archive discovery incomplete. Refresh to retry.';
|
|
@@ -216,7 +219,21 @@ async function discoverSessionCatalog({ root, openClient = openCodexAppServer, l
|
|
|
216
219
|
archivedCodex = [...observedArchives.values()];
|
|
217
220
|
}
|
|
218
221
|
const ids = new Set(active.map(item => item.session));
|
|
219
|
-
|
|
222
|
+
const archiveCandidates = new Map(records.filter(record => record.provider === 'codex' && !ids.has(record.session) && (!recentOnly || supervisedSession(record)))
|
|
223
|
+
.map(record => [record.session, record]));
|
|
224
|
+
// A recent, untracked conversation can be archived in Codex itself.
|
|
225
|
+
// Observe omissions from the previous bounded live window too; pins
|
|
226
|
+
// and historical navigation never expand this into an unbounded set.
|
|
227
|
+
if (recentOnly) for (const item of recentArchiveCandidates.slice(0, RECENT_SESSION_LIMIT)) {
|
|
228
|
+
if (item.provider === 'codex' && item.archived !== true && item.delegated !== true && !ids.has(item.session) && !archiveCandidates.has(item.session)) archiveCandidates.set(item.session, item);
|
|
229
|
+
}
|
|
230
|
+
const exactArchiveObservations = new Map();
|
|
231
|
+
archiveStates = await readCodexArchiveStates(client, [...archiveCandidates.values()],
|
|
232
|
+
{ codexHome, ...(includeArchived ? { observedArchived: exactArchiveObservations } : {}) });
|
|
233
|
+
// Preserve current titles/ancestry from the already-required archive
|
|
234
|
+
// observations, including exact-read recovery during listing outages.
|
|
235
|
+
for (const [session, item] of exactArchiveObservations) if (!observedArchives.has(session)) observedArchives.set(session, item);
|
|
236
|
+
archivedCodex = [...observedArchives.values()];
|
|
220
237
|
for (const item of archivedCodex) archiveStates.set(item.session, true);
|
|
221
238
|
return active;
|
|
222
239
|
}, signal); }
|
|
@@ -45,7 +45,8 @@ export async function createOwnerTransport({ root, nonce, allowedActions, dispat
|
|
|
45
45
|
if (!authenticated) {
|
|
46
46
|
if (command.nonce !== nonce) { peer.destroy(); return; }
|
|
47
47
|
authenticated = true; desktopClient = allowDesktopClient && command.client === 'desktop'; controllerArchive = allowControllerArchive && command.client === 'archive-controller'; clearTimeout(timeout); peers.add(peer);
|
|
48
|
-
if (desktopClient && (command.desktopProtocol === 2 || command.catalogDeltas === 1))
|
|
48
|
+
if (desktopClient && (command.desktopProtocol === 2 || command.catalogDeltas === 1))
|
|
49
|
+
catalogEncoders.set(peer, createDesktopCatalogEncoder({ version: command.catalogVersion === 2 ? 2 : 1 }));
|
|
49
50
|
if ([1, 2].includes(command.desktopProtocol) && desktopClient) send(peer, { type: 'desktop_owner_ready', version: catalogEncoders.has(peer) ? 2 : 1 });
|
|
50
51
|
// One-shot controls need their result, not multi-megabyte broadcasts
|
|
51
52
|
// that can fill the write queue before the result is sent.
|
|
@@ -119,7 +120,8 @@ export async function relayToOwner({ root, webOrigin, input = process.stdin, out
|
|
|
119
120
|
} catch (error) { peer.destroy(); throw error; }
|
|
120
121
|
// A protocol-1 owner replies with complete frames. New native windows accept
|
|
121
122
|
// both versions, so attaching never requires replacing its execution owner.
|
|
122
|
-
peer.write(JSON.stringify({ nonce: owner.nonce, ...(desktopClient ? { client: 'desktop', desktopProtocol: 1,
|
|
123
|
+
peer.write(JSON.stringify({ nonce: owner.nonce, ...(desktopClient ? { client: 'desktop', desktopProtocol: 1,
|
|
124
|
+
...(catalogDeltas ? { catalogDeltas: 1, ...(catalogDeltas === 2 ? { catalogVersion: 2 } : {}) } : {}) } : {}) }) + '\n');
|
|
123
125
|
// Old owners ignore unknown hello fields and expose only the restricted SSH
|
|
124
126
|
// projection. Never attach a native window or forward its controls as though
|
|
125
127
|
// that were a complete desktop endpoint. Keep the original owner untouched.
|