@awak-app/simy-cli 0.7.33 → 0.7.34
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/cli-observability.js +1 -1
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +17 -9
- package/src/desktop-host.js +83 -10
- package/src/session-supervision/codex-app-server.js +7 -1
- package/src/session-supervision/index.js +27 -6
- package/src/session-supervision/progressive-discovery.js +21 -8
- package/src/sqm/local-tests.js +2 -1
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.34";
|
|
18
18
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
19
19
|
|
|
20
20
|
export function narrativeImageCapabilityMetadata() {
|
package/src/cli-observability.js
CHANGED
|
@@ -7,7 +7,7 @@ import { execFile } from 'node:child_process';
|
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
8
|
import { webApiHeaders, webApiUrl } from './web-api.js';
|
|
9
9
|
// Keep this leaf module package-safe: hooks load it before the full CLI contract.
|
|
10
|
-
const CLI_VERSION = '0.7.
|
|
10
|
+
const CLI_VERSION = '0.7.34';
|
|
11
11
|
|
|
12
12
|
const STORE = 'cli-observability-state.json';
|
|
13
13
|
const LEGACY_CONFIG = 'cli-observability.json';
|
|
@@ -340,7 +340,8 @@ export async function readRecord(root = rootDirectory(), provider, session) {
|
|
|
340
340
|
} catch { return null; } // Match listRecords' treatment of unreadable records.
|
|
341
341
|
}
|
|
342
342
|
const RECORD_NAME = /^[a-f0-9]{64}\.json$/;
|
|
343
|
-
const RECORD_CACHE_LIMIT =
|
|
343
|
+
const RECORD_CACHE_LIMIT = 10_000;
|
|
344
|
+
const RECORD_CACHE_BYTES = 32 * 1024 * 1024;
|
|
344
345
|
const READ_CONCURRENCY = 8;
|
|
345
346
|
async function recordNames(root) {
|
|
346
347
|
try { return (await readdir(root)).filter(name => RECORD_NAME.test(name)).sort(); }
|
|
@@ -367,13 +368,16 @@ export async function listRecords(root = rootDirectory()) {
|
|
|
367
368
|
// A shared candidate/display index, never mutation authority. Every list checks
|
|
368
369
|
// metadata so external hooks, removals and direct writes invalidate cached JSON.
|
|
369
370
|
// Fresh execution reads and updateRecord locks remain independent of this index.
|
|
370
|
-
export function createRecordReader(root = rootDirectory()) {
|
|
371
|
+
export function createRecordReader(root = rootDirectory(), { cacheEntries = RECORD_CACHE_LIMIT, cacheBytes = RECORD_CACHE_BYTES } = {}) {
|
|
372
|
+
if (!Number.isSafeInteger(cacheEntries) || cacheEntries < 1 || cacheEntries > RECORD_CACHE_LIMIT ||
|
|
373
|
+
!Number.isSafeInteger(cacheBytes) || cacheBytes < 1 || cacheBytes > RECORD_CACHE_BYTES) throw new Error('Invalid record cache limits.');
|
|
371
374
|
const cache = new Map();
|
|
372
|
-
let inFlight = null, closed = false;
|
|
375
|
+
let inFlight = null, closed = false, retainedBytes = 0;
|
|
376
|
+
const discard = name => { retainedBytes -= cache.get(name)?.bytes || 0; cache.delete(name); };
|
|
373
377
|
const fingerprint = info => [info.dev, info.ino, info.size, info.mtimeNs, info.ctimeNs].join(':');
|
|
374
378
|
const scan = async () => {
|
|
375
|
-
const names = await recordNames(root), live = new Set(names.slice(0,
|
|
376
|
-
for (const name of cache.keys()) if (!live.has(name))
|
|
379
|
+
const names = await recordNames(root), live = new Set(names.slice(0, cacheEntries));
|
|
380
|
+
for (const name of cache.keys()) if (!live.has(name)) discard(name);
|
|
377
381
|
return mapRecordNames(names, async name => {
|
|
378
382
|
const path = join(root, name);
|
|
379
383
|
try {
|
|
@@ -384,10 +388,14 @@ export function createRecordReader(root = rootDirectory()) {
|
|
|
384
388
|
// Do not retain a value when a write races the read; the next list must
|
|
385
389
|
// inspect it again. This also prevents malformed transient JSON sticking.
|
|
386
390
|
const after = await stat(path, { bigint: true });
|
|
387
|
-
|
|
388
|
-
|
|
391
|
+
discard(name);
|
|
392
|
+
const bytes = Number(after.size);
|
|
393
|
+
if (live.has(name) && bytes <= cacheBytes && fingerprint(after) === key) {
|
|
394
|
+
while (retainedBytes + bytes > cacheBytes && cache.size) discard(cache.keys().next().value);
|
|
395
|
+
cache.set(name, { key, record, bytes }); retainedBytes += bytes;
|
|
396
|
+
}
|
|
389
397
|
return record;
|
|
390
|
-
} catch {
|
|
398
|
+
} catch { discard(name); return null; }
|
|
391
399
|
});
|
|
392
400
|
};
|
|
393
401
|
return {
|
|
@@ -398,7 +406,7 @@ export function createRecordReader(root = rootDirectory()) {
|
|
|
398
406
|
// cached records or a simultaneous caller's view.
|
|
399
407
|
return currentProviderConsent(root, structuredClone(await inFlight));
|
|
400
408
|
},
|
|
401
|
-
async close() { closed = true; await inFlight?.catch(() => {}); cache.clear(); },
|
|
409
|
+
async close() { closed = true; await inFlight?.catch(() => {}); cache.clear(); retainedBytes = 0; },
|
|
402
410
|
};
|
|
403
411
|
}
|
|
404
412
|
function positive(value, fallback, max) {
|
package/src/desktop-host.js
CHANGED
|
@@ -171,11 +171,16 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
171
171
|
// volatile; reuse the encoded body after comparing freshly authorized data.
|
|
172
172
|
// Detach the comparison value because registries may mutate nested objects.
|
|
173
173
|
let encodedStatus = null, lastStatus = null, updateProgress = null;
|
|
174
|
+
let projectCatalogFrame = status => status;
|
|
175
|
+
let resolveFirstStatus;
|
|
176
|
+
const firstStatus = new Promise(resolve => { resolveFirstStatus = resolve; });
|
|
177
|
+
let startupPublication;
|
|
174
178
|
// Per verified source reader, never accepted from a projection or command.
|
|
175
179
|
const acceptanceReaderId = randomUUID();
|
|
176
180
|
let acceptanceReadbackSequence = 0;
|
|
177
181
|
let remoteHosts;
|
|
178
182
|
const emitStatus = status => {
|
|
183
|
+
status = projectCatalogFrame(status);
|
|
179
184
|
const updateSettings = updater?.snapshot?.() || {};
|
|
180
185
|
status = { ...status, update: { ...status.update,
|
|
181
186
|
auto_update_supported: updateSettings.auto_update_supported === true,
|
|
@@ -207,6 +212,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
207
212
|
}
|
|
208
213
|
const observation = sampledAt === undefined ? '' : `"sampledAt":${JSON.stringify(sampledAt)}${encodedStatus ? ',' : ''}`;
|
|
209
214
|
frames.write(`{"type":"status","status":{${observation}${encodedStatus}}}\n`, true);
|
|
215
|
+
resolveFirstStatus?.(); resolveFirstStatus = null;
|
|
210
216
|
};
|
|
211
217
|
const root = env.SIMY_HOME?.trim() || join(homedir(), ".simy");
|
|
212
218
|
const controllerPreferences = createControllerStore(root);
|
|
@@ -224,6 +230,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
224
230
|
const supervisorToken = lock?.nonce || env.SIMY_DESKTOP_SUPERVISOR_TOKEN;
|
|
225
231
|
const deliveryRoot = join(root, "delivery-hooks");
|
|
226
232
|
const recordReader = createRecordReader(deliveryRoot);
|
|
233
|
+
let frameRecordScope = null, frameRecords = new Map();
|
|
234
|
+
const pendingCatalogPreferences = new Map();
|
|
227
235
|
let providerLinks = await readProviderLinks(root);
|
|
228
236
|
const providerLinkBusy = new Set(), providerLinkFeedback = {}, explicitAnalyses = new Set();
|
|
229
237
|
const linkedSessions = sessions => sessions.filter(item => providerLinks[item.provider]?.connected === true);
|
|
@@ -291,6 +299,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
291
299
|
nativeCompletionInFlight.clear();
|
|
292
300
|
}
|
|
293
301
|
if (previousScope !== accessScope(currentAccess)) {
|
|
302
|
+
frameRecordScope = null; frameRecords.clear();
|
|
303
|
+
pendingCatalogPreferences.clear();
|
|
294
304
|
statisticsCache.clear();
|
|
295
305
|
activeStatisticsRead?.controller.abort();
|
|
296
306
|
explicitAnalyses.clear();
|
|
@@ -539,6 +549,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
539
549
|
let commands;
|
|
540
550
|
let sessionStatus = { sessions: [], providers: { codex: { discovery: "loading", reason: null }, claude: { discovery: "loading", reason: null } }, refreshedAt: null, initialLoading: true };
|
|
541
551
|
let sessionRefresh = null, sessionRefreshedAt = 0, sessionRefreshAgain = false, discoveryController = null, discoveryStopped = false;
|
|
552
|
+
let catalogRevision = 0;
|
|
542
553
|
let enrichmentController = null, sessionEnrichment = null;
|
|
543
554
|
let catalogAnalysisJob = null, catalogAnalysisAgain = false;
|
|
544
555
|
const withCurrentSettings = (sessions, records) => {
|
|
@@ -636,17 +647,21 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
636
647
|
sessionStatus = { ...sessionStatus, ...value,
|
|
637
648
|
sessions: linkedSessions(merge(sessionStatus.sessions, value.sessions)).sort(compareSessionWork),
|
|
638
649
|
selectionCandidates: merge(sessionStatus.selectionCandidates || [], value.selectionCandidates),
|
|
639
|
-
initialLoading: false, completeProviders: [...complete], discoveryAttempt: attempt };
|
|
650
|
+
initialLoading: false, completeProviders: [...complete], discoveryAttempt: attempt, catalogRevision: ++catalogRevision };
|
|
640
651
|
sessionRefreshedAt = Date.now();
|
|
641
652
|
void publish().catch(() => {});
|
|
642
653
|
void refreshCatalogAnalysis();
|
|
643
654
|
};
|
|
644
|
-
|
|
655
|
+
const firstPageTimeoutMs = dependencies.discoveryTimeoutMs ?? 30_000;
|
|
656
|
+
const historyTimeoutMs = dependencies.historyDiscoveryTimeoutMs ?? dependencies.discoveryTimeoutMs ?? 300_000;
|
|
657
|
+
sessionRefresh = boundedDiscovery((signal, startHistory) => (dependencies.discoverSessions || discoverSessions)({
|
|
645
658
|
root: deliveryRoot, signal, onDrain: trackDiscoveryDrain, includeArchived: true, includeSelectionCandidates: true,
|
|
659
|
+
readRecords: () => recordReader.list(),
|
|
646
660
|
connectedProviders: PROVIDER_LINKS.filter(provider => providerLinks[provider].connected),
|
|
647
|
-
timeoutMs:
|
|
648
|
-
onProgress: value => applyCatalog(value, signal),
|
|
649
|
-
}), { signal: controller.signal, timeoutMs:
|
|
661
|
+
timeoutMs: firstPageTimeoutMs, historyTimeoutMs,
|
|
662
|
+
onProgress: value => { if (value.sessions?.length) startHistory(); return applyCatalog(value, signal); },
|
|
663
|
+
}), { signal: controller.signal, timeoutMs: firstPageTimeoutMs + 1000, historyTimeoutMs: historyTimeoutMs + 1000,
|
|
664
|
+
onDrain: dependencies.discoverSessions ? undefined : trackDiscoveryDrain })
|
|
650
665
|
.then(async value => {
|
|
651
666
|
await applyCatalog(value);
|
|
652
667
|
if (closed || relay || controller.signal.aborted) return;
|
|
@@ -678,6 +693,33 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
678
693
|
}
|
|
679
694
|
return sessionStatus;
|
|
680
695
|
};
|
|
696
|
+
// Provider metadata is already scoped and cancelable. A connection heartbeat
|
|
697
|
+
// can publish it while optional Action/deadline enrichment owns a slow full
|
|
698
|
+
// projection. These rows do not authorize execution or mint record readbacks.
|
|
699
|
+
projectCatalogFrame = status => {
|
|
700
|
+
if (!desktopAccessReady(status.access) || accessScope(status.access) !== accessScope(currentAccess) ||
|
|
701
|
+
sessionStatus.initialLoading !== false || !sessionStatus.discoveryAttempt) return status;
|
|
702
|
+
const previous = status.sessionSupervision;
|
|
703
|
+
if (!pendingCatalogPreferences.size && previous?.discoveryAttempt?.id === sessionStatus.discoveryAttempt.id &&
|
|
704
|
+
previous.catalogRevision === sessionStatus.catalogRevision && previous.initialLoading === false &&
|
|
705
|
+
previous.historyLoading === sessionStatus.historyLoading) return status;
|
|
706
|
+
const prior = new Map((previous?.sessions || []).map(row => [`${row.provider}:${row.session}`, row]));
|
|
707
|
+
const records = frameRecordScope === accessScope(currentAccess) ? frameRecords : new Map();
|
|
708
|
+
const sessions = linkedSessions(sessionStatus.sessions).map(candidate => {
|
|
709
|
+
const key = `${candidate.provider}:${candidate.session}`, record = pendingCatalogPreferences.get(key) || records.get(key);
|
|
710
|
+
const observed = projectSessionAutoContinue({ ...prior.get(key), ...candidate }, record);
|
|
711
|
+
const analyzed = analysisManager?.project(observed) || observed;
|
|
712
|
+
const row = automaticContinuation?.project(analyzed) || analyzed;
|
|
713
|
+
const projected = { ...row, actionDeadline: row.actionDeadline || { status: 'unavailable', online: false, reminder: null },
|
|
714
|
+
workState: sessionWorkState(row, record?.run, { provider: sessionStatus.providers?.[candidate.provider] }) };
|
|
715
|
+
return { ...projected, conversationPresentation: conversationPresentation(projected, record?.run) };
|
|
716
|
+
}).sort(compareSessionWork);
|
|
717
|
+
const providers = { ...sessionStatus.providers };
|
|
718
|
+
for (const provider of PROVIDER_LINKS) {
|
|
719
|
+
if (!providerLinks[provider].connected) providers[provider] = { discovery: 'disconnected', reason: null };
|
|
720
|
+
}
|
|
721
|
+
return { ...status, sessionSupervision: { ...sessionStatus, sessions, providers } };
|
|
722
|
+
};
|
|
681
723
|
const claudeDesktopNative = dependencies.claudeDesktopNative || createClaudeDesktopNativeAdapter();
|
|
682
724
|
const archiveClaudeDesktop = createClaudeDesktopArchiveController({ native: claudeDesktopNative, ...(dependencies.readClaudeDesktopTarget ? { readTarget: dependencies.readClaudeDesktopTarget } : {}) });
|
|
683
725
|
const selectedSession = async (command, { reuseDiscovery = false, preferObserved = false } = {}) => {
|
|
@@ -883,10 +925,24 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
883
925
|
publishAgain = true;
|
|
884
926
|
return;
|
|
885
927
|
}
|
|
928
|
+
for (const [key, saved] of pendingCatalogPreferences) {
|
|
929
|
+
const current = await readRecord(deliveryRoot, saved.provider, saved.session);
|
|
930
|
+
if (pendingCatalogPreferences.get(key) === saved) pendingCatalogPreferences.set(key, current || {
|
|
931
|
+
provider: saved.provider, session: saved.session, cwd: saved.cwd,
|
|
932
|
+
autoContinue: false, autoContinueEpoch: null, tracked: false, monitor: false });
|
|
933
|
+
}
|
|
934
|
+
if (!await acceptStatusAccess(sampledAccess)) { publishAgain = true; return; }
|
|
935
|
+
if (JSON.stringify(await readProviderLinks(root)) !== requestedLinks) { publishAgain = true; return; }
|
|
936
|
+
const preferenceAccess = agent.getDesktopAccess ? agent.getDesktopAccess() : await accessSnapshot();
|
|
937
|
+
if (!sameAccess(preferenceAccess, status.access)) { publishAgain = true; return; }
|
|
886
938
|
// Select cached record evidence after all asynchronous authorization:
|
|
887
939
|
// a full projection may have advanced it while those reads were waiting.
|
|
888
940
|
const sameScope = desktopAccessReady(currentAccess) &&
|
|
889
941
|
accessScope(lastStatus?.access) === accessScope(currentAccess);
|
|
942
|
+
const cached = sameScope && lastStatus && controllerScope(lastStatus) === controllerScope(status) ? structuredClone(lastStatus) : null;
|
|
943
|
+
// A heartbeat is a complete frame: retain optional scoped fields until
|
|
944
|
+
// their replacement is ready, while fresh access/provider data wins.
|
|
945
|
+
if (cached) status = { ...cached, ...status };
|
|
890
946
|
status.deliveryHooks = sameScope && lastStatus?.deliveryHooks
|
|
891
947
|
? structuredClone(lastStatus.deliveryHooks) : { runs: [], providers: {} };
|
|
892
948
|
status.sessionSupervision = sameScope && lastStatus?.sessionSupervision
|
|
@@ -896,7 +952,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
896
952
|
status.nativeDevelopmentReports = sameScope && sameReportController(nativeDevelopmentReportsController, {
|
|
897
953
|
scope: accessScope(status.access), controllerScope: controllerScope(status), deviceSession: reportDeviceSession(),
|
|
898
954
|
}) && nativeDevelopmentReportsScope === accessScope(currentAccess)
|
|
899
|
-
? structuredClone(nativeDevelopmentReports) : { state: 'loading', enrollments: [] };
|
|
955
|
+
? { ...structuredClone(cached?.nativeDevelopmentReports || {}), ...structuredClone(nativeDevelopmentReports) } : { state: 'loading', enrollments: [] };
|
|
900
956
|
// Cached enrichment must follow the current connection preference even
|
|
901
957
|
// while a full projection is waiting on records or remote metadata.
|
|
902
958
|
status.deliveryHooks.runs = linkedSessions(status.deliveryHooks.runs || []);
|
|
@@ -936,6 +992,15 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
936
992
|
if (preferenceScope) await controllerPreferences.load(preferenceScope);
|
|
937
993
|
const requestedLinks = await decorateProviderSettings(status);
|
|
938
994
|
const allRecords = await recordReader.list();
|
|
995
|
+
if (accessScope(currentAccess) === requestedScope) {
|
|
996
|
+
// Keep explicit preference overrides until the scope changes. A full
|
|
997
|
+
// projection that started before the save can finish after this read.
|
|
998
|
+
// Heartbeats refresh these identities from disk, including external OFF.
|
|
999
|
+
frameRecordScope = requestedScope;
|
|
1000
|
+
frameRecords = new Map(allRecords.map(record => [`${record.provider}:${record.session}`, record]));
|
|
1001
|
+
}
|
|
1002
|
+
// Begin the usable catalog before optional record/Action enrichment.
|
|
1003
|
+
refreshSessions();
|
|
939
1004
|
const records = linkedSessions(allRecords);
|
|
940
1005
|
const update = status.update || updater.snapshot?.() || {};
|
|
941
1006
|
if (!stoppingForUpdate) unfinishedDesktopWork = hasUnfinishedSessions(allRecords);
|
|
@@ -966,7 +1031,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
966
1031
|
if (reportController.controllerScope) void syncEmailPreferences(reportController);
|
|
967
1032
|
const preferenceState = reportController.controllerScope ? await controllerPreferences.load(reportController.controllerScope) : null;
|
|
968
1033
|
status.notificationPreferences = preferenceState?.notificationPreferences || {};
|
|
969
|
-
status.nativeDevelopmentReports = { ...structuredClone(sameReportController(reportController, nativeDevelopmentReportsController) ? nativeDevelopmentReports : { state: 'loading', enrollments: [] }), deliveries: Object.values(preferenceState?.conversations || {}).flatMap(row => Object.values(row.emailDeliveries || {})), preferences: Object.values(preferenceState?.conversations || {}).flatMap(row => row.emailPreference ? [{ ...row.emailPreference.request, sync: row.emailPreference.state }] : []) };
|
|
1034
|
+
status.nativeDevelopmentReports = { ...structuredClone(sameReportController(reportController, nativeDevelopmentReportsController) ? nativeDevelopmentReports : { state: 'loading', enrollments: [] }), deliveries: Object.values(preferenceState?.conversations || {}).flatMap(row => Object.values(row.emailDeliveries || {})).map(({ receipt_id, ...delivery }) => delivery), preferences: Object.values(preferenceState?.conversations || {}).flatMap(row => row.emailPreference ? [{ ...row.emailPreference.request, sync: row.emailPreference.state }] : []) };
|
|
970
1035
|
// This is an asynchronous local attestation. It neither changes a
|
|
971
1036
|
// Delivery Loop nor places receipt or device data in status frames.
|
|
972
1037
|
let reportCompletions = [];
|
|
@@ -1266,7 +1331,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1266
1331
|
}
|
|
1267
1332
|
} });
|
|
1268
1333
|
await refreshPlugins();
|
|
1269
|
-
|
|
1334
|
+
startupPublication = publish();
|
|
1335
|
+
void startupPublication.catch(() => {});
|
|
1270
1336
|
const maintenance = new Map();
|
|
1271
1337
|
const tick = worker => {
|
|
1272
1338
|
if (closed || relay || !coordinator.acceptsNewWork() || maintenance.has(worker)) return;
|
|
@@ -1920,8 +1986,11 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1920
1986
|
if (command.enabled) await authorizeWork(commandScope);
|
|
1921
1987
|
const saved = await (dependencies.setSessionAutoContinue || saveSessionAutoContinuePreference)({ root: join(root, 'delivery-hooks'),
|
|
1922
1988
|
provider: command.provider, session: command.session, enabled: command.enabled, initialContinuationPolicy: command.continuationPolicy, admission, target, expectedProviderEpoch: providerEpoch });
|
|
1923
|
-
if (saved)
|
|
1924
|
-
|
|
1989
|
+
if (saved) {
|
|
1990
|
+
pendingCatalogPreferences.set(`${saved.provider}:${saved.session}`, saved);
|
|
1991
|
+
sessionStatus = { ...sessionStatus, catalogRevision: ++catalogRevision,
|
|
1992
|
+
sessions: sessionStatus.sessions.map(candidate => projectSessionAutoContinue(candidate, saved)) };
|
|
1993
|
+
}
|
|
1925
1994
|
if (!command.enabled) automaticContinuation.revoke(command.provider, command.session);
|
|
1926
1995
|
// Acknowledge the durable preference before discovery or analysis.
|
|
1927
1996
|
emit({ type: "result", id: command.id, ok: true });
|
|
@@ -2252,6 +2321,10 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
2252
2321
|
ownerTransport = await createOwnerTransport({ root, nonce: lock.nonce, allowedActions, allowControllerArchive: env.SIMY_REMOTE_CHILD !== '1', dispatch: command => commands.emit('line', JSON.stringify(command)) });
|
|
2253
2322
|
await publish();
|
|
2254
2323
|
}
|
|
2324
|
+
// Register controls before awaiting initial presentation. A fast, authorized
|
|
2325
|
+
// catalog frame is sufficient; unrelated optional enrichment may still wait.
|
|
2326
|
+
try { await Promise.race([startupPublication, firstStatus]); }
|
|
2327
|
+
catch (error) { await close(); throw error; }
|
|
2255
2328
|
return { close, publish, agent };
|
|
2256
2329
|
}
|
|
2257
2330
|
|
|
@@ -235,11 +235,12 @@ export function codexThreadMetadata(thread) {
|
|
|
235
235
|
};
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
export async function listCodexSessions({ client, all = false, signal, onPage, maxPages = 40, pageSize = 25, maxSessions = RECENT_SESSION_LIMIT, archived = false, inspectState = false, prioritySessions = [], observationBudgetMs = 3000, codexHome } = {}) {
|
|
238
|
+
export async function listCodexSessions({ client, all = false, signal, onPage, maxPages = 40, pageSize = 25, historyPageSize = 200, maxSessions = RECENT_SESSION_LIMIT, archived = false, inspectState = false, prioritySessions = [], observationBudgetMs = 3000, codexHome } = {}) {
|
|
239
239
|
const sessions = [];
|
|
240
240
|
const windowSize = all ? Infinity : Math.min(maxSessions, maxPages * pageSize);
|
|
241
241
|
const cursors = new Set(), ids = new Set();
|
|
242
242
|
let cursor = null, listed = 0, pages = 0, batchSize = pageSize, nextProgress = 0;
|
|
243
|
+
let reducedBatch = false;
|
|
243
244
|
try {
|
|
244
245
|
while (pages < Math.max(maxPages, Math.ceil(windowSize / batchSize)) && listed < windowSize) {
|
|
245
246
|
signal?.throwIfAborted();
|
|
@@ -251,6 +252,7 @@ export async function listCodexSessions({ client, all = false, signal, onPage, m
|
|
|
251
252
|
// Keep the transport's byte limit. Retry the same cursor in smaller
|
|
252
253
|
// batches rather than dropping discovery for large metadata envelopes.
|
|
253
254
|
if (error?.code !== 'response_too_large' || limit <= 1) throw error;
|
|
255
|
+
reducedBatch = true;
|
|
254
256
|
batchSize = Math.max(1, Math.floor(limit / 2));
|
|
255
257
|
continue;
|
|
256
258
|
}
|
|
@@ -267,6 +269,10 @@ export async function listCodexSessions({ client, all = false, signal, onPage, m
|
|
|
267
269
|
if (onPage && (pages === 1 || sessions.length >= nextProgress)) {
|
|
268
270
|
await onPage(sessions); nextProgress = Math.max(100, sessions.length * 2);
|
|
269
271
|
}
|
|
272
|
+
// Keep the first frame small, then amortize expensive metadata RPCs over
|
|
273
|
+
// bounded history pages. The transport byte ceiling still wins: once a
|
|
274
|
+
// response is oversized, retain its reduced batch for the rest of the scan.
|
|
275
|
+
if (all && pages === 1 && !reducedBatch) batchSize = Math.max(pageSize, Math.min(200, historyPageSize));
|
|
270
276
|
cursor = response?.nextCursor || null;
|
|
271
277
|
if (!cursor) break;
|
|
272
278
|
if (typeof cursor !== 'string' || cursors.has(cursor))
|
|
@@ -165,20 +165,25 @@ 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, catalogSnapshot, onProgress } = {}) {
|
|
168
|
+
inspectProcess = processIdentity, now: requestedNow, signal, listCodexTitles = readCodexTitles, listCodexChildren = readCodexChildSessions, codexHome, connectedProviders = ['codex', 'claude'], includeArchived = false, includeSelectionCandidates = false, 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();
|
|
172
172
|
const connected = new Set(connectedProviders);
|
|
173
|
-
const records = (await
|
|
173
|
+
const records = (recordSnapshot || await readRecords(root)).filter(record => connected.has(record.provider));
|
|
174
174
|
const priorities = records.filter(retainedSession);
|
|
175
175
|
let archivedCodex = [];
|
|
176
176
|
let discovered = [], codexError = null, codexReasonCode = null, claude = [], claudeError = null;
|
|
177
177
|
let archiveStates = new Map();
|
|
178
178
|
const progress = async snapshot => {
|
|
179
179
|
if (!onProgress || signal?.aborted) return;
|
|
180
|
+
const observed = new Set([...(snapshot.discovered || []), ...(snapshot.claude || []), ...(snapshot.archivedCodex || [])]
|
|
181
|
+
.map(item => `${item.provider}:${item.session}`));
|
|
180
182
|
const value = await discoverSessionCatalog({ root, inspectProcess, now: requestedNow, signal, connectedProviders, includeArchived, codexHome,
|
|
181
|
-
|
|
183
|
+
// Pages reuse this attempt's records. Do not rescan or project the entire
|
|
184
|
+
// retained corpus for every page; the complete catalog still includes it.
|
|
185
|
+
catalogSnapshot: snapshot, recordSnapshot: records.filter(record => observed.has(`${record.provider}:${record.session}`)), archiveReconciliations,
|
|
186
|
+
listCodexChildren: async () => [], listCodexTitles: async () => new Map() });
|
|
182
187
|
if (!signal?.aborted) await onProgress(value);
|
|
183
188
|
};
|
|
184
189
|
if (catalogSnapshot) {
|
|
@@ -234,11 +239,22 @@ async function discoverSessionCatalog({ root, openClient = openCodexAppServer, l
|
|
|
234
239
|
const claudeItem = record.provider === 'claude' ? claude.find(item => item.session === record.session && item.cwd === record.cwd) : null;
|
|
235
240
|
const archiveState = record.provider === 'codex' ? archiveStates.get(record.session) ?? (activeCodex.has(record.session) ? false : undefined) : claudeItem?.archived;
|
|
236
241
|
if (typeof archiveState !== 'boolean') continue;
|
|
242
|
+
const key = `${record.provider}:${record.session}`;
|
|
243
|
+
const reconciled = archiveReconciliations.get(key);
|
|
244
|
+
if (reconciled?.archived === archiveState) {
|
|
245
|
+
records[index] = reconciled.record;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
// Pages repeat an ever-growing prefix. Persist positive archive fencing
|
|
249
|
+
// before exposing it once, rather than repeatedly refreshing the same old
|
|
250
|
+
// observation and contending with hook writers on every page. This map is
|
|
251
|
+
// private to one scoped attempt; execution still reads fresh durable state.
|
|
252
|
+
const remember = () => archiveReconciliations.set(key, { archived: archiveState, record: records[index] });
|
|
237
253
|
const isArchived = archiveState;
|
|
238
254
|
if (isArchived) await recordProviderArchive(root, record.provider, record.session, true, now);
|
|
239
|
-
if (isArchived && record.providerArchived === true) continue;
|
|
240
|
-
if (!isArchived && !(record.providerArchived === true && archiveState === false)) continue;
|
|
241
|
-
try {
|
|
255
|
+
if (isArchived && record.providerArchived === true) { remember(); continue; }
|
|
256
|
+
if (!isArchived && !(record.providerArchived === true && archiveState === false)) { remember(); continue; }
|
|
257
|
+
try { const updated = await updateRecord(root, record.provider, record.session, current => {
|
|
242
258
|
if (!current) return current;
|
|
243
259
|
current.providerArchived = isArchived;
|
|
244
260
|
if (isArchived) {
|
|
@@ -251,11 +267,16 @@ async function discoverSessionCatalog({ root, openClient = openCodexAppServer, l
|
|
|
251
267
|
}
|
|
252
268
|
return current;
|
|
253
269
|
});
|
|
270
|
+
if (updated) records[index] = updated;
|
|
254
271
|
if (!isArchived) await recordProviderArchive(root, record.provider, record.session, false, now);
|
|
272
|
+
remember();
|
|
255
273
|
} catch {
|
|
256
274
|
// Reconciliation can contend with a hook writer or an unreclaimable
|
|
257
275
|
// legacy lock. Preserve stored state and retry on the next refresh;
|
|
258
276
|
// one record must not discard either provider's entire catalog.
|
|
277
|
+
// A successfully persisted positive fence already revokes execution.
|
|
278
|
+
// Retry its JSON reconciliation on the next attempt, not each old prefix.
|
|
279
|
+
if (isArchived) remember();
|
|
259
280
|
}
|
|
260
281
|
}
|
|
261
282
|
assertActive();
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// A deadline releases the caller even when an adapter ignores cancellation.
|
|
2
2
|
// The adapter still receives an abort signal; late values never reach the UI.
|
|
3
|
-
export async function boundedDiscovery(work, { signal, timeoutMs = 30_000, onDrain } = {}) {
|
|
3
|
+
export async function boundedDiscovery(work, { signal, timeoutMs = 30_000, historyTimeoutMs = timeoutMs, onDrain } = {}) {
|
|
4
4
|
const controller = new AbortController();
|
|
5
|
+
const startedAt = Date.now();
|
|
6
|
+
let historyStarted = false;
|
|
5
7
|
let timer, rejectStop;
|
|
6
8
|
const stopped = new Promise((_, reject) => { rejectStop = reject; });
|
|
7
9
|
const cancel = () => {
|
|
@@ -9,14 +11,23 @@ export async function boundedDiscovery(work, { signal, timeoutMs = 30_000, onDra
|
|
|
9
11
|
controller.abort(error); rejectStop(error);
|
|
10
12
|
};
|
|
11
13
|
signal?.addEventListener('abort', cancel, { once: true });
|
|
12
|
-
|
|
14
|
+
const expire = () => {
|
|
13
15
|
const error = Object.assign(new Error('Conversation discovery timed out. Refresh to retry.'), { code: 'discovery_timeout' });
|
|
14
16
|
controller.abort(error); rejectStop(error);
|
|
15
|
-
}
|
|
17
|
+
};
|
|
18
|
+
timer = setTimeout(expire, timeoutMs);
|
|
19
|
+
const startHistory = () => {
|
|
20
|
+
if (historyStarted || controller.signal.aborted) return;
|
|
21
|
+
historyStarted = true;
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
// The background budget is measured from the original request. Repeated
|
|
24
|
+
// pages cannot postpone the absolute deadline indefinitely.
|
|
25
|
+
timer = setTimeout(expire, Math.max(0, startedAt + Math.max(timeoutMs, historyTimeoutMs) - Date.now()));
|
|
26
|
+
};
|
|
16
27
|
try {
|
|
17
28
|
if (signal?.aborted) cancel();
|
|
18
29
|
const operation = Promise.resolve().then(() => {
|
|
19
|
-
controller.signal.throwIfAborted(); return work(controller.signal);
|
|
30
|
+
controller.signal.throwIfAborted(); return work(controller.signal, startHistory);
|
|
20
31
|
});
|
|
21
32
|
onDrain?.(operation.then(() => {}, () => {}));
|
|
22
33
|
return await Promise.race([stopped, operation]);
|
|
@@ -27,7 +38,7 @@ export async function boundedDiscovery(work, { signal, timeoutMs = 30_000, onDra
|
|
|
27
38
|
}
|
|
28
39
|
}
|
|
29
40
|
|
|
30
|
-
export async function discoverProgressively({ discover, onProgress, signal, timeoutMs = 30_000,
|
|
41
|
+
export async function discoverProgressively({ discover, onProgress, signal, timeoutMs = 30_000, historyTimeoutMs = timeoutMs,
|
|
31
42
|
connectedProviders = ['codex', 'claude'], onDrain, ...options }) {
|
|
32
43
|
const values = new Map(), complete = new Set(), settled = new Set();
|
|
33
44
|
let notifications = Promise.resolve();
|
|
@@ -46,10 +57,12 @@ export async function discoverProgressively({ discover, onProgress, signal, time
|
|
|
46
57
|
};
|
|
47
58
|
await Promise.all(connectedProviders.map(async provider => {
|
|
48
59
|
try {
|
|
49
|
-
const value = await boundedDiscovery(childSignal => discover({ ...options, signal: childSignal,
|
|
60
|
+
const value = await boundedDiscovery((childSignal, startHistory) => discover({ ...options, signal: childSignal,
|
|
50
61
|
connectedProviders: [provider], onProgress: async value => {
|
|
51
|
-
childSignal.throwIfAborted();
|
|
52
|
-
|
|
62
|
+
childSignal.throwIfAborted();
|
|
63
|
+
if (value.sessions?.length) startHistory();
|
|
64
|
+
values.set(provider, value); await notify();
|
|
65
|
+
} }), { signal, timeoutMs, historyTimeoutMs, onDrain });
|
|
53
66
|
signal?.throwIfAborted();
|
|
54
67
|
values.set(provider, value);
|
|
55
68
|
if (['ready', 'local_index'].includes(value.providers?.[provider]?.discovery)) complete.add(provider);
|
package/src/sqm/local-tests.js
CHANGED
|
@@ -16,7 +16,8 @@ const profiles = {
|
|
|
16
16
|
// finite catalog-owned deadline while allowing slow local process I/O.
|
|
17
17
|
{ id: 'repair-and-stop-boundaries', timeoutMs: 1_200_000, files: ['test/purpose-search.test.js', 'test/delivery-explicit-resume.test.js', 'test/purpose-continuation.test.js'] },
|
|
18
18
|
{ id: 'sqm-runner', files: ['test/sqm-local-tests.test.js'] },
|
|
19
|
-
{ id: 'provider-startup', files: ['test/codex-app-server-discovery.test.js', 'test/session-supervision.test.js'
|
|
19
|
+
{ id: 'provider-startup', files: ['test/codex-app-server-discovery.test.js', 'test/session-supervision.test.js',
|
|
20
|
+
'test/progressive-discovery.test.js', 'test/desktop-record-index.test.js', 'test/desktop-background-performance.test.js'] },
|
|
20
21
|
{ id: 'handoff-and-audit-recovery', files: ['test/consistency-handoff.test.js', 'test/consistency-monitor.test.js', 'test/consistency-auditor.test.js', 'test/desktop-host.test.js'] },
|
|
21
22
|
{ id: 'migration-and-current-outcomes', files: ['test/autonomous-upgrade.test.js', 'test/local-delivery-runtime.test.js', 'test/delivery-migration-crash.test.js', 'test/purpose-inspection.test.js', 'test/purpose-search-scenarios.test.js', 'test/sqm-large-diff.test.js'] },
|
|
22
23
|
],
|