@awak-app/simy-cli 0.7.32 → 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/README.md +22 -3
- package/package.json +3 -2
- package/src/auto-update.js +10 -1
- package/src/cli-contract.js +1 -1
- package/src/cli-observability.js +1 -1
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +44 -12
- package/src/codex-plugins/simy/hooks/purpose-workspace.mjs +13 -4
- package/src/delivery-hooks/desktop.js +1 -1
- package/src/desktop-access.js +1 -1
- package/src/desktop-frame-writer.js +29 -0
- package/src/desktop-host.js +252 -77
- package/src/orchestrator/loop.js +2 -0
- package/src/session-supervision/analysis-manager.js +83 -18
- package/src/session-supervision/claude-desktop-lineage.js +1 -1
- package/src/session-supervision/claude-sessions.js +16 -4
- package/src/session-supervision/codex-app-server.js +45 -30
- package/src/session-supervision/continuation-readiness.js +58 -0
- package/src/session-supervision/decision-presentation.js +11 -3
- package/src/session-supervision/index.js +126 -28
- package/src/session-supervision/owner-transport.js +23 -8
- package/src/session-supervision/progressive-discovery.js +79 -0
- package/src/session-supervision/remote-entry.js +7 -4
- package/src/session-supervision/remote-host-recovery.js +21 -0
- package/src/session-supervision/remote-hosts.js +28 -8
- package/src/session-supervision/remote-owner-entry.js +20 -0
- package/src/session-supervision/remote-service.js +36 -0
- package/src/session-supervision/statistics.js +19 -3
- package/src/session-supervision/work-state.js +7 -1
- package/src/sqm/local-tests.js +2 -1
- package/src/update-connection.js +16 -0
package/src/desktop-host.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createDesktopFrameWriter } from './desktop-frame-writer.js';
|
|
2
|
+
import { boundedDiscovery } from './session-supervision/progressive-discovery.js';
|
|
1
3
|
import { createClaudeDesktopArchiveController } from './session-supervision/claude-desktop-archive.js';
|
|
2
4
|
import { createClaudeDesktopNativeAdapter } from './session-supervision/claude-desktop-native.js';
|
|
3
5
|
import { applyControllerExecutionContext } from './session-supervision/execution-context.js';
|
|
@@ -8,6 +10,7 @@ import { createNotificationJournal } from './session-supervision/notification-jo
|
|
|
8
10
|
import { createRemoteMetadata } from './session-supervision/remote-metadata.js';
|
|
9
11
|
import { projectControllerCompletion } from './native-development-report-completion.js';
|
|
10
12
|
import { remoteOperationFailure } from './session-supervision/remote-errors.js';
|
|
13
|
+
import { updateConnectionEnvironment, sameUpdateIdentity } from './update-connection.js';
|
|
11
14
|
import { copyDesktopText } from './desktop-clipboard.js';
|
|
12
15
|
import { createRemoteHostManager, allowedActions, controllerPolicyActions } from './session-supervision/remote-hosts.js';
|
|
13
16
|
import { manualAcceptanceCommand, manualAcceptanceContinuation } from './session-supervision/manual-acceptance-command.js';
|
|
@@ -40,7 +43,7 @@ import { controlDelivery, verifyDelivery, decideManualAcceptance, createRecordRe
|
|
|
40
43
|
import { createManagedSupervisor } from "./delivery-hooks/supervisor.js";
|
|
41
44
|
import { createActionAssociationHost } from "./session-supervision/action-association-host.js";
|
|
42
45
|
import { createStopNotifier } from "./session-supervision/stop-notifications.js";
|
|
43
|
-
import { AutoContinueUnavailableError, answerSession, continueSession, followupSession, resumeSession, discoverSessions, enrollSession, monitorSession, trackSession, setSessionAutoContinue } from "./session-supervision/index.js";
|
|
46
|
+
import { AutoContinueUnavailableError, answerSession, continueSession, followupSession, resumeSession, discoverSessions, projectSessionAutoContinue, enrollSession, monitorSession, trackSession, setSessionAutoContinue, saveSessionAutoContinuePreference } from "./session-supervision/index.js";
|
|
44
47
|
import { renewClaudeSessionOwner } from './session-supervision/claude-owner-renewal.js';
|
|
45
48
|
import { createContinuationAnalysisManager } from "./session-supervision/analysis-manager.js";
|
|
46
49
|
import { analyzeContinuation, readContinuationContext } from "./session-supervision/continuation-analysis.js";
|
|
@@ -149,35 +152,7 @@ function desktopActionError(action, error) {
|
|
|
149
152
|
return "Action could not finish. Refresh the status and try again.";
|
|
150
153
|
}
|
|
151
154
|
|
|
152
|
-
|
|
153
|
-
// not. Never enqueue periodic snapshots behind a native reader that has paused.
|
|
154
|
-
// Control frames retain the stream's FIFO order and never wait for GUI drain.
|
|
155
|
-
export function createDesktopFrameWriter(output) {
|
|
156
|
-
let blocked = false, pendingStatus = null, statusClosed = false;
|
|
157
|
-
const drain = () => {
|
|
158
|
-
blocked = false;
|
|
159
|
-
const frame = pendingStatus;
|
|
160
|
-
pendingStatus = null;
|
|
161
|
-
if (!statusClosed && frame !== null) write(frame, true);
|
|
162
|
-
};
|
|
163
|
-
const write = (frame, isStatus = false) => {
|
|
164
|
-
if (isStatus && statusClosed) return;
|
|
165
|
-
if (isStatus && blocked) { pendingStatus = frame; return; }
|
|
166
|
-
// A control frame must not be followed by a snapshot captured before it.
|
|
167
|
-
// The next authorized projection will publish fresh state.
|
|
168
|
-
if (!isStatus) pendingStatus = null;
|
|
169
|
-
const accepted = output.write(frame);
|
|
170
|
-
if (accepted === false && !blocked && !statusClosed) {
|
|
171
|
-
blocked = true;
|
|
172
|
-
output.once('drain', drain);
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
return { write, close() {
|
|
176
|
-
statusClosed = true;
|
|
177
|
-
pendingStatus = null;
|
|
178
|
-
output.removeListener('drain', drain);
|
|
179
|
-
} };
|
|
180
|
-
}
|
|
155
|
+
export { createDesktopFrameWriter } from './desktop-frame-writer.js';
|
|
181
156
|
|
|
182
157
|
export async function startDesktopHost({ input = process.stdin, output = process.stdout, env = process.env,
|
|
183
158
|
updatePackageRoot, dependencies = {}, exit = (code) => process.exit(code) } = {}) {
|
|
@@ -196,11 +171,16 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
196
171
|
// volatile; reuse the encoded body after comparing freshly authorized data.
|
|
197
172
|
// Detach the comparison value because registries may mutate nested objects.
|
|
198
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;
|
|
199
178
|
// Per verified source reader, never accepted from a projection or command.
|
|
200
179
|
const acceptanceReaderId = randomUUID();
|
|
201
180
|
let acceptanceReadbackSequence = 0;
|
|
202
181
|
let remoteHosts;
|
|
203
182
|
const emitStatus = status => {
|
|
183
|
+
status = projectCatalogFrame(status);
|
|
204
184
|
const updateSettings = updater?.snapshot?.() || {};
|
|
205
185
|
status = { ...status, update: { ...status.update,
|
|
206
186
|
auto_update_supported: updateSettings.auto_update_supported === true,
|
|
@@ -232,6 +212,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
232
212
|
}
|
|
233
213
|
const observation = sampledAt === undefined ? '' : `"sampledAt":${JSON.stringify(sampledAt)}${encodedStatus ? ',' : ''}`;
|
|
234
214
|
frames.write(`{"type":"status","status":{${observation}${encodedStatus}}}\n`, true);
|
|
215
|
+
resolveFirstStatus?.(); resolveFirstStatus = null;
|
|
235
216
|
};
|
|
236
217
|
const root = env.SIMY_HOME?.trim() || join(homedir(), ".simy");
|
|
237
218
|
const controllerPreferences = createControllerStore(root);
|
|
@@ -249,6 +230,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
249
230
|
const supervisorToken = lock?.nonce || env.SIMY_DESKTOP_SUPERVISOR_TOKEN;
|
|
250
231
|
const deliveryRoot = join(root, "delivery-hooks");
|
|
251
232
|
const recordReader = createRecordReader(deliveryRoot);
|
|
233
|
+
let frameRecordScope = null, frameRecords = new Map();
|
|
234
|
+
const pendingCatalogPreferences = new Map();
|
|
252
235
|
let providerLinks = await readProviderLinks(root);
|
|
253
236
|
const providerLinkBusy = new Set(), providerLinkFeedback = {}, explicitAnalyses = new Set();
|
|
254
237
|
const linkedSessions = sessions => sessions.filter(item => providerLinks[item.provider]?.connected === true);
|
|
@@ -286,6 +269,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
286
269
|
const remoteMetadata = createRemoteMetadata({ root, webOrigin, onChange: () => { if (!closed && !relay) setImmediate(() => void publish().catch(() => {})); } });
|
|
287
270
|
let teamThreads;
|
|
288
271
|
let currentAccess = { state: 'checking', organizationId: null, organizationName: null };
|
|
272
|
+
let updateIdentityRevision = 0;
|
|
289
273
|
let preferenceScope = null, emailSync = null, emailSyncRetryAt = 0;
|
|
290
274
|
let nativeDevelopmentReports = { state: 'loading', enrollments: [] };
|
|
291
275
|
let nativeDevelopmentReportsScope = null;
|
|
@@ -304,6 +288,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
304
288
|
const previouslyReady = desktopAccessReady(currentAccess);
|
|
305
289
|
currentAccess = access || { state: 'unavailable', organizationId: null, organizationName: null };
|
|
306
290
|
if (previousScope !== accessScope(currentAccess) || previouslyReady && !desktopAccessReady(currentAccess)) {
|
|
291
|
+
updateIdentityRevision++;
|
|
307
292
|
// A privilege loss also invalidates consent reads, even when the
|
|
308
293
|
// credential/organization revision itself did not change.
|
|
309
294
|
nativeDevelopmentReports = { state: 'loading', enrollments: [] };
|
|
@@ -314,10 +299,12 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
314
299
|
nativeCompletionInFlight.clear();
|
|
315
300
|
}
|
|
316
301
|
if (previousScope !== accessScope(currentAccess)) {
|
|
302
|
+
frameRecordScope = null; frameRecords.clear();
|
|
303
|
+
pendingCatalogPreferences.clear();
|
|
317
304
|
statisticsCache.clear();
|
|
318
305
|
activeStatisticsRead?.controller.abort();
|
|
319
306
|
explicitAnalyses.clear();
|
|
320
|
-
discoveryController?.abort();
|
|
307
|
+
discoveryController?.abort(); enrichmentController?.abort();
|
|
321
308
|
sessionRefreshAgain = Boolean(sessionRefresh) && desktopAccessReady(currentAccess);
|
|
322
309
|
for (const item of sessionStatus.sessions) automaticContinuation?.revoke(item.provider, item.session);
|
|
323
310
|
sessionStatus = { sessions: [], providers: {}, refreshedAt: null, initialLoading: true }; sessionRefreshedAt = 0;
|
|
@@ -562,12 +549,51 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
562
549
|
let commands;
|
|
563
550
|
let sessionStatus = { sessions: [], providers: { codex: { discovery: "loading", reason: null }, claude: { discovery: "loading", reason: null } }, refreshedAt: null, initialLoading: true };
|
|
564
551
|
let sessionRefresh = null, sessionRefreshedAt = 0, sessionRefreshAgain = false, discoveryController = null, discoveryStopped = false;
|
|
552
|
+
let catalogRevision = 0;
|
|
553
|
+
let enrichmentController = null, sessionEnrichment = null;
|
|
554
|
+
let catalogAnalysisJob = null, catalogAnalysisAgain = false;
|
|
555
|
+
const withCurrentSettings = (sessions, records) => {
|
|
556
|
+
const byKey = new Map(records.map(record => [`${record.provider}:${record.session}`, record]));
|
|
557
|
+
return sessions.map(candidate => projectSessionAutoContinue(candidate, byKey.get(`${candidate.provider}:${candidate.session}`)));
|
|
558
|
+
};
|
|
559
|
+
const refreshCatalogAnalysis = () => {
|
|
560
|
+
catalogAnalysisAgain = true;
|
|
561
|
+
if (catalogAnalysisJob) return catalogAnalysisJob;
|
|
562
|
+
const scope = accessScope(currentAccess), manager = analysisManager;
|
|
563
|
+
catalogAnalysisJob = Promise.resolve().then(async () => {
|
|
564
|
+
while (catalogAnalysisAgain) {
|
|
565
|
+
catalogAnalysisAgain = false;
|
|
566
|
+
const records = await recordReader.list();
|
|
567
|
+
if (closed || relay || discoveryStopped || manager !== analysisManager ||
|
|
568
|
+
!desktopAccessReady(currentAccess) || accessScope(currentAccess) !== scope || !coordinator.acceptsNewWork()) return;
|
|
569
|
+
await manager.update(analysisSessions(withCurrentSettings(sessionStatus.sessions, records)));
|
|
570
|
+
}
|
|
571
|
+
}).catch(() => {}).finally(() => {
|
|
572
|
+
catalogAnalysisJob = null;
|
|
573
|
+
if (catalogAnalysisAgain && !closed && !relay && !discoveryStopped) void refreshCatalogAnalysis();
|
|
574
|
+
});
|
|
575
|
+
return catalogAnalysisJob;
|
|
576
|
+
};
|
|
565
577
|
const hasUnfinishedSessions = records => records.some(record => record.autoContinue === true || record.run && !TERMINAL.has(record.run.state)) ||
|
|
566
578
|
sessionStatus.sessions.some(candidate => candidate.autoContinue === true || ['queued', 'analyzing'].includes(analysisManager.project(candidate).analysis?.state));
|
|
579
|
+
const discoveryDrains = new Set();
|
|
580
|
+
const trackDiscoveryDrain = operation => {
|
|
581
|
+
const settled = Promise.resolve(operation).then(() => {}, () => {});
|
|
582
|
+
discoveryDrains.add(settled);
|
|
583
|
+
void settled.then(() => discoveryDrains.delete(settled));
|
|
584
|
+
};
|
|
567
585
|
const stopDiscovery = async ({ drain = false } = {}) => {
|
|
568
|
-
discoveryController?.abort();
|
|
586
|
+
discoveryController?.abort(); enrichmentController?.abort();
|
|
569
587
|
sessionRefreshAgain = false;
|
|
570
|
-
if (drain
|
|
588
|
+
if (drain) {
|
|
589
|
+
let timer;
|
|
590
|
+
try {
|
|
591
|
+
await Promise.race([
|
|
592
|
+
Promise.allSettled([sessionRefresh, sessionEnrichment, catalogAnalysisJob, ...discoveryDrains]),
|
|
593
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(Object.assign(new Error('Conversation discovery has not finished closing; runtime handoff was refused.'), { code: 'discovery_drain_incomplete' })), dependencies.discoveryDrainTimeoutMs ?? 4000); }),
|
|
594
|
+
]);
|
|
595
|
+
} finally { clearTimeout(timer); }
|
|
596
|
+
}
|
|
571
597
|
};
|
|
572
598
|
let confirmedUpdateStop;
|
|
573
599
|
const stopWorkForUpdate = () => confirmedUpdateStop ||= (async () => {
|
|
@@ -604,33 +630,96 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
604
630
|
const controller = discoveryController = new AbortController();
|
|
605
631
|
const attempt = { id: randomUUID(), requestedAt: Date.now(), state: 'pending' };
|
|
606
632
|
sessionStatus = { ...sessionStatus, discoveryAttempt: attempt };
|
|
607
|
-
|
|
633
|
+
enrichmentController?.abort();
|
|
634
|
+
const applyCatalog = async (value, sourceSignal = controller.signal) => {
|
|
635
|
+
if (closed || relay || controller.signal.aborted || sourceSignal.aborted || sessionStatus.discoveryAttempt?.id !== attempt.id) return;
|
|
636
|
+
const latestAccess = await accessSnapshot();
|
|
637
|
+
if (closed || relay || controller.signal.aborted || sourceSignal.aborted || sessionStatus.discoveryAttempt?.id !== attempt.id) return;
|
|
638
|
+
if (!desktopAccessReady(latestAccess) || accessScope(latestAccess) !== requestedScope) {
|
|
639
|
+
sessionRefreshAgain = desktopAccessReady(latestAccess); return;
|
|
640
|
+
}
|
|
641
|
+
const complete = new Set(value.completeProviders || (value.historyLoading === undefined ? PROVIDER_LINKS : []));
|
|
642
|
+
const merge = (previous, incoming) => {
|
|
643
|
+
const rows = new Map(previous.filter(item => !complete.has(item.provider)).map(item => [`${item.provider}:${item.session}`, item]));
|
|
644
|
+
for (const item of incoming || []) rows.set(`${item.provider}:${item.session}`, item);
|
|
645
|
+
return [...rows.values()];
|
|
646
|
+
};
|
|
647
|
+
sessionStatus = { ...sessionStatus, ...value,
|
|
648
|
+
sessions: linkedSessions(merge(sessionStatus.sessions, value.sessions)).sort(compareSessionWork),
|
|
649
|
+
selectionCandidates: merge(sessionStatus.selectionCandidates || [], value.selectionCandidates),
|
|
650
|
+
initialLoading: false, completeProviders: [...complete], discoveryAttempt: attempt, catalogRevision: ++catalogRevision };
|
|
651
|
+
sessionRefreshedAt = Date.now();
|
|
652
|
+
void publish().catch(() => {});
|
|
653
|
+
void refreshCatalogAnalysis();
|
|
654
|
+
};
|
|
655
|
+
const firstPageTimeoutMs = dependencies.discoveryTimeoutMs ?? 30_000;
|
|
656
|
+
const historyTimeoutMs = dependencies.historyDiscoveryTimeoutMs ?? dependencies.discoveryTimeoutMs ?? 300_000;
|
|
657
|
+
sessionRefresh = boundedDiscovery((signal, startHistory) => (dependencies.discoverSessions || discoverSessions)({
|
|
658
|
+
root: deliveryRoot, signal, onDrain: trackDiscoveryDrain, includeArchived: true, includeSelectionCandidates: true,
|
|
659
|
+
readRecords: () => recordReader.list(),
|
|
660
|
+
connectedProviders: PROVIDER_LINKS.filter(provider => providerLinks[provider].connected),
|
|
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 })
|
|
608
665
|
.then(async value => {
|
|
666
|
+
await applyCatalog(value);
|
|
609
667
|
if (closed || relay || controller.signal.aborted) return;
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
668
|
+
const failed = Object.values(value.providers || {}).some(provider => provider.discovery === 'unavailable');
|
|
669
|
+
sessionStatus = { ...sessionStatus, historyLoading: false,
|
|
670
|
+
discoveryAttempt: { ...attempt, state: failed ? 'failed' : 'succeeded', completedAt: Date.now() } };
|
|
671
|
+
// Publish before PR/network observation and queue maintenance. Neither
|
|
672
|
+
// operation owns the first usable catalog or the discovery promise.
|
|
673
|
+
void publish().catch(() => {});
|
|
674
|
+
const enrichment = enrichmentController = new AbortController();
|
|
675
|
+
const catalog = sessionStatus;
|
|
676
|
+
sessionEnrichment = boundedDiscovery(async signal => {
|
|
677
|
+
// Queue maintenance and PR checks are independent background work.
|
|
678
|
+
const queue = signal.aborted ? Promise.resolve() : refreshCatalogAnalysis();
|
|
679
|
+
const sessions = await observePullRequests(catalog.sessions, { signal });
|
|
680
|
+
const latestAccess = await accessSnapshot();
|
|
681
|
+
if (closed || relay || signal.aborted || !desktopAccessReady(latestAccess) || accessScope(latestAccess) !== requestedScope || sessionStatus !== catalog) return;
|
|
682
|
+
sessionStatus = { ...sessionStatus, sessions: linkedSessions(sessions) };
|
|
683
|
+
await queue;
|
|
684
|
+
if (!signal.aborted) void publish().catch(() => {});
|
|
685
|
+
}, { signal: enrichment.signal, timeoutMs: dependencies.enrichmentTimeoutMs ?? 10_000, onDrain: trackDiscoveryDrain })
|
|
686
|
+
.catch(() => {}).finally(() => { if (enrichmentController === enrichment) { enrichmentController = null; sessionEnrichment = null; } });
|
|
687
|
+
})
|
|
626
688
|
.catch(() => { if (closed || relay || controller.signal.aborted) return; sessionStatus = { ...sessionStatus, initialLoading: false,
|
|
627
|
-
discoveryAttempt: { ...attempt, state: 'failed', completedAt: Date.now() },
|
|
689
|
+
historyLoading: false, discoveryAttempt: { ...attempt, state: 'failed', completedAt: Date.now() },
|
|
628
690
|
providers: { codex: { discovery: "unavailable", reason: "Conversation discovery unavailable." }, claude: { discovery: "unavailable", reason: "Conversation discovery unavailable." } } }; sessionRefreshedAt = Date.now(); })
|
|
629
691
|
.finally(() => { if (discoveryController === controller) discoveryController = null; sessionRefresh = null; if (sessionRefreshAgain && !closed && !relay) { sessionRefreshAgain = false; refreshSessions(true); }
|
|
630
692
|
if (!closed) setImmediate(() => { void publish().catch(() => {}); }); });
|
|
631
693
|
}
|
|
632
694
|
return sessionStatus;
|
|
633
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
|
+
};
|
|
634
723
|
const claudeDesktopNative = dependencies.claudeDesktopNative || createClaudeDesktopNativeAdapter();
|
|
635
724
|
const archiveClaudeDesktop = createClaudeDesktopArchiveController({ native: claudeDesktopNative, ...(dependencies.readClaudeDesktopTarget ? { readTarget: dependencies.readClaudeDesktopTarget } : {}) });
|
|
636
725
|
const selectedSession = async (command, { reuseDiscovery = false, preferObserved = false } = {}) => {
|
|
@@ -639,7 +728,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
639
728
|
// Preference changes may use the observation already displayed by the UI.
|
|
640
729
|
// Execution still takes the fresh-discovery path and rechecks opt-in epochs.
|
|
641
730
|
const matches = item => item.provider === command.provider && item.session === command.session;
|
|
642
|
-
let candidate = preferObserved ? sessionStatus.sessions.find(matches) : null;
|
|
731
|
+
let candidate = preferObserved || reuseDiscovery ? sessionStatus.sessions.find(matches) : null;
|
|
643
732
|
if (!candidate) {
|
|
644
733
|
// Analysis joins an existing discovery instead of scheduling a second
|
|
645
734
|
// full scan. Enrollment retains its fresh-discovery boundary.
|
|
@@ -650,6 +739,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
650
739
|
candidate = sessionStatus.sessions.find(matches);
|
|
651
740
|
}
|
|
652
741
|
if (!candidate) throw new Error('This conversation is unavailable; refresh its provider.');
|
|
742
|
+
candidate = projectSessionAutoContinue(candidate, await readRecord(deliveryRoot, command.provider, command.session));
|
|
653
743
|
await authorizeProvider(command.provider, scope, link.epoch);
|
|
654
744
|
const selected = { ...candidate };
|
|
655
745
|
selectedScopes.set(selected, scope);
|
|
@@ -668,7 +758,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
668
758
|
phase = "save_disconnect";
|
|
669
759
|
if (!connected) providerLinks = await writeProviderLink(root, provider, false);
|
|
670
760
|
if (activeStatisticsRead?.key.startsWith(`${provider}:`)) activeStatisticsRead.controller.abort();
|
|
671
|
-
discoveryController?.abort();
|
|
761
|
+
discoveryController?.abort(); enrichmentController?.abort();
|
|
672
762
|
sessionRefreshAgain = false;
|
|
673
763
|
for (const item of sessionStatus.sessions.filter(item => item.provider === provider)) automaticContinuation?.revoke(provider, item.session);
|
|
674
764
|
for (const key of explicitAnalyses) if (key.startsWith(`${provider}:`)) explicitAnalyses.delete(key);
|
|
@@ -697,7 +787,10 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
697
787
|
} finally { providerLinkBusy.delete(provider); await publish(); }
|
|
698
788
|
};
|
|
699
789
|
const makeAutomaticContinuation = () => createAutomaticContinuation({ root: join(root, 'delivery-hooks'),
|
|
700
|
-
getManager: () => analysisManager,
|
|
790
|
+
getManager: () => analysisManager,
|
|
791
|
+
// Fresh exact context and enrollment still validate the provider below;
|
|
792
|
+
// unrelated history pagination is not an execution-readiness boundary.
|
|
793
|
+
selectCandidate: candidate => selectedSession(candidate, { reuseDiscovery: true }),
|
|
701
794
|
readSetting: dependencies.readSessionAutoContinue,
|
|
702
795
|
readContext: dependencies.readContinuationContext || readContinuationContext,
|
|
703
796
|
enroll: guardedEnroll, beforeEnroll: candidate => authorizeProvider(candidate.provider, selectedScopes.get(candidate), selectedLinkEpochs.get(candidate)),
|
|
@@ -788,7 +881,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
788
881
|
const packageInfo = dependencies.packageInfo || await readCliPackageInfo();
|
|
789
882
|
diagnostics.write(`desktop starting version=${packageInfo.version} pid=${process.pid} role=${candidate ? "update-candidate" : "supervisor"}`);
|
|
790
883
|
const makeAnalysisManager = () => (dependencies.createAnalysisManager || createContinuationAnalysisManager)({
|
|
791
|
-
root: join(root, 'session-analysis'), analyze: async (...args) => {
|
|
884
|
+
root: join(root, 'session-analysis'), waitNoticeAfterMs: dependencies.analysisWaitNoticeAfterMs, analyze: async (...args) => {
|
|
792
885
|
const scope = await authorizeWork();
|
|
793
886
|
const [candidate, options] = args;
|
|
794
887
|
const link = await requireProviderLink({ root: deliveryRoot, provider: candidate.provider });
|
|
@@ -832,10 +925,24 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
832
925
|
publishAgain = true;
|
|
833
926
|
return;
|
|
834
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; }
|
|
835
938
|
// Select cached record evidence after all asynchronous authorization:
|
|
836
939
|
// a full projection may have advanced it while those reads were waiting.
|
|
837
940
|
const sameScope = desktopAccessReady(currentAccess) &&
|
|
838
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 };
|
|
839
946
|
status.deliveryHooks = sameScope && lastStatus?.deliveryHooks
|
|
840
947
|
? structuredClone(lastStatus.deliveryHooks) : { runs: [], providers: {} };
|
|
841
948
|
status.sessionSupervision = sameScope && lastStatus?.sessionSupervision
|
|
@@ -845,7 +952,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
845
952
|
status.nativeDevelopmentReports = sameScope && sameReportController(nativeDevelopmentReportsController, {
|
|
846
953
|
scope: accessScope(status.access), controllerScope: controllerScope(status), deviceSession: reportDeviceSession(),
|
|
847
954
|
}) && nativeDevelopmentReportsScope === accessScope(currentAccess)
|
|
848
|
-
? structuredClone(nativeDevelopmentReports) : { state: 'loading', enrollments: [] };
|
|
955
|
+
? { ...structuredClone(cached?.nativeDevelopmentReports || {}), ...structuredClone(nativeDevelopmentReports) } : { state: 'loading', enrollments: [] };
|
|
849
956
|
// Cached enrichment must follow the current connection preference even
|
|
850
957
|
// while a full projection is waiting on records or remote metadata.
|
|
851
958
|
status.deliveryHooks.runs = linkedSessions(status.deliveryHooks.runs || []);
|
|
@@ -885,6 +992,15 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
885
992
|
if (preferenceScope) await controllerPreferences.load(preferenceScope);
|
|
886
993
|
const requestedLinks = await decorateProviderSettings(status);
|
|
887
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();
|
|
888
1004
|
const records = linkedSessions(allRecords);
|
|
889
1005
|
const update = status.update || updater.snapshot?.() || {};
|
|
890
1006
|
if (!stoppingForUpdate) unfinishedDesktopWork = hasUnfinishedSessions(allRecords);
|
|
@@ -915,7 +1031,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
915
1031
|
if (reportController.controllerScope) void syncEmailPreferences(reportController);
|
|
916
1032
|
const preferenceState = reportController.controllerScope ? await controllerPreferences.load(reportController.controllerScope) : null;
|
|
917
1033
|
status.notificationPreferences = preferenceState?.notificationPreferences || {};
|
|
918
|
-
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 }] : []) };
|
|
919
1035
|
// This is an asynchronous local attestation. It neither changes a
|
|
920
1036
|
// Delivery Loop nor places receipt or device data in status frames.
|
|
921
1037
|
let reportCompletions = [];
|
|
@@ -948,9 +1064,12 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
948
1064
|
...(readbackOrder ? { readbackId: acceptanceReadbackId, readbackOrder } : {}) } };
|
|
949
1065
|
});
|
|
950
1066
|
await actionAssociations.decorate(status.deliveryHooks, projection);
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
1067
|
+
const catalog = refreshSessions();
|
|
1068
|
+
const discovery = { ...catalog, sessions: withCurrentSettings(catalog.sessions, records) };
|
|
1069
|
+
if (coordinator.acceptsNewWork()) automaticContinuation?.tick(discovery.sessions.filter(candidate => Array.isArray(discovery.completeProviders) && discovery.completeProviders.includes(candidate.provider)));
|
|
1070
|
+
let sessions = discovery.sessions.map(candidate => automaticContinuation.project(analysisManager.project(candidate)));
|
|
1071
|
+
await analysisManager.observeContinuationWaits?.(sessions);
|
|
1072
|
+
sessions = sessions.map(candidate => analysisManager.projectContinuationWait?.(candidate) || candidate);
|
|
954
1073
|
await actionAssociations.decorateSessions?.(sessions, projection);
|
|
955
1074
|
const stateRecordsBySession = new Map((projection?.records || records).filter(record => record.run).map(record => [`${record.provider}:${record.session}`, record]));
|
|
956
1075
|
const stateRunsBySession = new Map([...stateRecordsBySession].map(([key, record]) => [key, record.run]));
|
|
@@ -980,7 +1099,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
980
1099
|
const run = runsBySession.get(`${session.provider}:${session.session}`);
|
|
981
1100
|
if (run) run.actionDeadline = session.actionDeadline;
|
|
982
1101
|
}
|
|
983
|
-
status.sessionSupervision = { ...discovery, refreshing: Boolean(sessionRefresh), sessions: reconcileFamilyCompletion(sessions.map(candidate => (
|
|
1102
|
+
status.sessionSupervision = { ...discovery, refreshing: Boolean(sessionRefresh), sessions: reconcileFamilyCompletion(sessions.map(candidate => projectSessionAutoContinue(candidate,
|
|
1103
|
+
stateRecordsBySession.get(`${candidate.provider}:${candidate.session}`))).map(candidate => analysisManager.projectContinuationWait?.(candidate) || candidate).map(candidate => ({ ...candidate,
|
|
984
1104
|
decisionRequest: sessionDecisionPresentation(candidate, runsBySession.get(`${candidate.provider}:${candidate.session}`)),
|
|
985
1105
|
workState: sessionWorkState(candidate, runsBySession.get(`${candidate.provider}:${candidate.session}`) || stateRunsBySession.get(`${candidate.provider}:${candidate.session}`), { provider: discovery.providers?.[candidate.provider] }) }))).map(candidate => ({ ...candidate, conversationPresentation: conversationPresentation(candidate,
|
|
986
1106
|
runsBySession.get(`${candidate.provider}:${candidate.session}`), { rawRecord: stateRecordsBySession.get(`${candidate.provider}:${candidate.session}`) }) })).sort(compareSessionWork) };
|
|
@@ -1020,6 +1140,15 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1020
1140
|
// running frame cannot suppress a newly stopped conversation.
|
|
1021
1141
|
stopNotifier.observe(records);
|
|
1022
1142
|
emitStatus(status);
|
|
1143
|
+
const notices = await analysisManager.takeContinuationWaitNotifications?.(sessions) || [];
|
|
1144
|
+
const noticeAccess = await accessSnapshot();
|
|
1145
|
+
if (!closed && !relay && desktopAccessReady(noticeAccess) && accessScope(noticeAccess) === requestedScope) {
|
|
1146
|
+
for (const event of notices) {
|
|
1147
|
+
const scoped = { ...event, scope: reportController.controllerScope };
|
|
1148
|
+
await notificationJournal.append(scoped).catch(() => {});
|
|
1149
|
+
emit(event);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1023
1152
|
}
|
|
1024
1153
|
}).finally(() => { publishing = null; if (publishAgain && !closed) { publishAgain = false; setImmediate(() => { void publish().catch(() => {}); }); } });
|
|
1025
1154
|
return publishing;
|
|
@@ -1108,6 +1237,9 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1108
1237
|
updater.start({ registry: agent.registry, coordinator, isDesktopIdle: () => !unfinishedDesktopWork,
|
|
1109
1238
|
restartDaemon: async (targetVersion, installation) => {
|
|
1110
1239
|
if (!installation?.entryPath || !installation?.executablePath) throw new Error("Signed update runtime is unavailable.");
|
|
1240
|
+
const updateIdentity = await agent.getDesktopStatus();
|
|
1241
|
+
if (!await acceptStatusAccess(updateIdentity.access)) throw new Error("Updated desktop runtime identity changed during handoff.");
|
|
1242
|
+
const expectedIdentityRevision = updateIdentityRevision;
|
|
1111
1243
|
discoveryStopped = true;
|
|
1112
1244
|
await stopDiscovery({ drain: true });
|
|
1113
1245
|
// The candidate shares this cache. Stop inference before it starts so a
|
|
@@ -1122,7 +1254,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1122
1254
|
try { child = (dependencies.spawn || spawn)(installation.executablePath,
|
|
1123
1255
|
[join(dirname(installation.entryPath), "desktop-host.js")], {
|
|
1124
1256
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1125
|
-
env: { ...env, SIMY_DESKTOP_SUPERVISOR_TOKEN: supervisorToken, ...(transferToken ? {SIMY_UPDATE_TRANSFER_TOKEN:transferToken} : {}) },
|
|
1257
|
+
env: { ...updateConnectionEnvironment({ env, root, webOrigin }), SIMY_DESKTOP_SUPERVISOR_TOKEN: supervisorToken, ...(transferToken ? {SIMY_UPDATE_TRANSFER_TOKEN:transferToken} : {}) },
|
|
1126
1258
|
}); } catch (error) {
|
|
1127
1259
|
if(transferToken)await updateRuntime.reclaimFailedHandoff();
|
|
1128
1260
|
actionAssociations = makeActionAssociations();
|
|
@@ -1149,6 +1281,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1149
1281
|
});
|
|
1150
1282
|
try {
|
|
1151
1283
|
const first = await new Promise((accept, reject) => {
|
|
1284
|
+
let validating = false;
|
|
1152
1285
|
const limit = setTimeout(() => reject(new Error("Updated desktop runtime did not start in time.")), 30_000);
|
|
1153
1286
|
const finish = (fn, value) => { clearTimeout(limit); fn(value); };
|
|
1154
1287
|
child.once("error", () => finish(reject, new Error("Updated desktop runtime could not start.")));
|
|
@@ -1161,13 +1294,27 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1161
1294
|
}
|
|
1162
1295
|
let event; try { event = JSON.parse(line); } catch { return; }
|
|
1163
1296
|
if (accepted) { emit(event); return; }
|
|
1164
|
-
if (event.type === "status") {
|
|
1165
|
-
if (!validCandidateStatus(event.status, targetVersion, webOrigin) || transferToken && (event.status.updateDetachment?.connectionState !== 'connected' || event.status.updateDetachment.operationId !== updateRuntime.snapshot(coordinator.snapshot()).operationId)) {
|
|
1297
|
+
if (event.type === "status" && !validating) {
|
|
1298
|
+
if (!validCandidateStatus(event.status, targetVersion, webOrigin) || !sameUpdateIdentity(updateIdentity, event.status) || transferToken && (event.status.updateDetachment?.connectionState !== 'connected' || event.status.updateDetachment.operationId !== updateRuntime.snapshot(coordinator.snapshot()).operationId)) {
|
|
1166
1299
|
finish(reject, new Error("Updated desktop runtime identity mismatch."));
|
|
1167
|
-
} else {
|
|
1300
|
+
} else {
|
|
1301
|
+
validating = true;
|
|
1302
|
+
void (async () => {
|
|
1303
|
+
// Sign-in and organization changes use a separate command
|
|
1304
|
+
// queue; re-read after all candidate startup awaits.
|
|
1305
|
+
const currentIdentity = await agent.getDesktopStatus();
|
|
1306
|
+
if (!await acceptStatusAccess(currentIdentity.access)) {
|
|
1307
|
+
finish(reject, new Error("Updated desktop runtime identity changed during handoff.")); return;
|
|
1308
|
+
}
|
|
1309
|
+
if (closed || child.exitCode !== null || expectedIdentityRevision !== updateIdentityRevision || !sameUpdateIdentity(updateIdentity, currentIdentity)) {
|
|
1310
|
+
finish(reject, new Error("Updated desktop runtime identity changed during handoff."));
|
|
1311
|
+
} else { accepted = true; finish(accept, event); }
|
|
1312
|
+
})().catch(error => finish(reject, error));
|
|
1313
|
+
}
|
|
1168
1314
|
}
|
|
1169
1315
|
});
|
|
1170
1316
|
});
|
|
1317
|
+
if (expectedIdentityRevision !== updateIdentityRevision || agent.getDesktopAccess && !sameAccess(updateIdentity.access, agent.getDesktopAccess())) throw new Error("Updated desktop runtime identity changed during handoff.");
|
|
1171
1318
|
relay = child;
|
|
1172
1319
|
await stopAgent();
|
|
1173
1320
|
if (child.exitCode !== null || closed) throw new Error("Updated runtime stopped during handoff.");
|
|
@@ -1184,7 +1331,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1184
1331
|
}
|
|
1185
1332
|
} });
|
|
1186
1333
|
await refreshPlugins();
|
|
1187
|
-
|
|
1334
|
+
startupPublication = publish();
|
|
1335
|
+
void startupPublication.catch(() => {});
|
|
1188
1336
|
const maintenance = new Map();
|
|
1189
1337
|
const tick = worker => {
|
|
1190
1338
|
if (closed || relay || !coordinator.acceptsNewWork() || maintenance.has(worker)) return;
|
|
@@ -1579,18 +1727,30 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1579
1727
|
return;
|
|
1580
1728
|
}
|
|
1581
1729
|
if (action === 'session_get_status') {
|
|
1582
|
-
await selectedSession(command);
|
|
1583
|
-
await publish();
|
|
1584
1730
|
await authorizeProvider(command.provider, commandScope, commandProviderLink?.epoch);
|
|
1585
|
-
const
|
|
1731
|
+
const cached = lastStatus?.sessionSupervision?.sessions?.find(item =>
|
|
1586
1732
|
item.provider === command.provider && item.session === command.session && (!item.hostId || item.hostId === 'local'));
|
|
1587
|
-
|
|
1733
|
+
const record = await readRecord(deliveryRoot, command.provider, command.session);
|
|
1734
|
+
const observed = sessionStatus.sessions.find(item => item.provider === command.provider && item.session === command.session);
|
|
1735
|
+
if (!record && !observed) throw new Error('This conversation status is unavailable; refresh its provider.');
|
|
1736
|
+
// Read one durable task without scheduling a complete provider scan
|
|
1737
|
+
// or waiting for optional account-wide enrichment. Observation times
|
|
1738
|
+
// remain original, so a cached running flag cannot become fresh.
|
|
1739
|
+
const candidate = { ...(cached || { provider: command.provider, session: command.session, cwd: record?.cwd,
|
|
1740
|
+
observation: { observedAt: record?.runtime?.observedAt }, isRunning: record?.runtime?.isRunning }), ...observed };
|
|
1741
|
+
const row = { ...projectSessionAutoContinue(automaticContinuation.project(analysisManager.project(candidate)), record),
|
|
1742
|
+
workState: sessionWorkState(candidate, record?.run, { provider: sessionStatus.providers?.[command.provider] }) };
|
|
1743
|
+
const controls = await (dependencies.deliveryHookStatus || deliveryHookStatus)({ root: deliveryRoot, records: record ? [record] : [] });
|
|
1744
|
+
const selectedRun = controls.runs?.find(run => run.provider === command.provider && run.session === command.session);
|
|
1745
|
+
row.decisionRequest = sessionDecisionPresentation(row, selectedRun);
|
|
1746
|
+
row.conversationPresentation = conversationPresentation(row, selectedRun, { rawRecord: record });
|
|
1747
|
+
await authorizeProvider(command.provider, commandScope, commandProviderLink?.epoch);
|
|
1588
1748
|
// The same app projection carries observation time and next action;
|
|
1589
1749
|
// saved consent and an active record cannot prove a provider send.
|
|
1590
1750
|
const { provider, session, title, autoContinue, autoContinueStatus, autoContinueReason,
|
|
1591
|
-
binding, workState, dispatchReadiness, conversationPresentation } = row;
|
|
1751
|
+
binding, workState, dispatchReadiness, conversationPresentation: presentation, decisionRequest } = row;
|
|
1592
1752
|
emit({ type: 'result', id: command.id, ok: true, data: { provider, session, title,
|
|
1593
|
-
autoContinue, autoContinueStatus, autoContinueReason, binding, workState, dispatchReadiness, conversationPresentation } });
|
|
1753
|
+
autoContinue, autoContinueStatus, autoContinueReason, binding, workState, dispatchReadiness, conversationPresentation: presentation, decisionRequest } });
|
|
1594
1754
|
return;
|
|
1595
1755
|
}
|
|
1596
1756
|
if (action === 'session_read_context') {
|
|
@@ -1666,7 +1826,11 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1666
1826
|
await agent.refreshDesktopAccess?.();
|
|
1667
1827
|
} else if (action === 'desktop_sqm_settings') {
|
|
1668
1828
|
const verified = await agent.refreshDesktopAccess?.();
|
|
1669
|
-
|
|
1829
|
+
// Navigation starts no work. Keep scope and entitlement checks while
|
|
1830
|
+
// allowing users to inspect SQM during an update admission hold.
|
|
1831
|
+
const currentAccess = await accessSnapshot();
|
|
1832
|
+
requireDesktopAccess(currentAccess);
|
|
1833
|
+
if (accessScope(currentAccess) !== commandScope) throw Object.assign(new Error('Desktop account or organization changed.'), { code: 'desktop_access_required' });
|
|
1670
1834
|
if (!desktopAccessReady(verified) || verified.organizationId !== access.organizationId ||
|
|
1671
1835
|
!validDesktopAccessPath(verified.settingsPath, verified.organizationSlug)) throw new Error('Organization settings are unavailable; refresh access first.');
|
|
1672
1836
|
const url = new URL(`/org/${encodeURIComponent(verified.organizationSlug)}/quality-monitor`, webOrigin);
|
|
@@ -1807,23 +1971,30 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1807
1971
|
await actionAssociations.correct(command, analysisManager.project(candidate));
|
|
1808
1972
|
} else if (action === "session_set_auto_continue") {
|
|
1809
1973
|
if (typeof command.enabled !== 'boolean') throw new Error('Choose whether automatic continuation is enabled.');
|
|
1810
|
-
let admission, providerEpoch;
|
|
1974
|
+
let admission, target, providerEpoch;
|
|
1811
1975
|
if (command.enabled) {
|
|
1812
1976
|
const candidate = await selectedSession(command, { preferObserved: true });
|
|
1813
1977
|
const canEnable = candidate.autoContinueAvailability?.canEnable ?? candidate.capabilities?.canAutoContinue;
|
|
1814
|
-
if (canEnable !== true) {
|
|
1978
|
+
if (candidate.controllerCanSetAutoContinue !== true && canEnable !== true) {
|
|
1815
1979
|
throw new AutoContinueUnavailableError({ reason: candidate.autoContinueAvailability?.reason || candidate.reason,
|
|
1816
1980
|
reasonCode: candidate.autoContinueAvailability?.reasonCode || candidate.continuationReasonCode });
|
|
1817
1981
|
}
|
|
1818
1982
|
admission = candidate.continuationAdmission;
|
|
1983
|
+
target = { provider: candidate.provider, session: candidate.session, cwd: candidate.cwd };
|
|
1819
1984
|
providerEpoch = selectedLinkEpochs.get(candidate);
|
|
1820
1985
|
}
|
|
1821
1986
|
if (command.enabled) await authorizeWork(commandScope);
|
|
1822
|
-
await (dependencies.setSessionAutoContinue ||
|
|
1823
|
-
provider: command.provider, session: command.session, enabled: command.enabled, initialContinuationPolicy: command.continuationPolicy, admission, expectedProviderEpoch: providerEpoch });
|
|
1987
|
+
const saved = await (dependencies.setSessionAutoContinue || saveSessionAutoContinuePreference)({ root: join(root, 'delivery-hooks'),
|
|
1988
|
+
provider: command.provider, session: command.session, enabled: command.enabled, initialContinuationPolicy: command.continuationPolicy, admission, target, expectedProviderEpoch: providerEpoch });
|
|
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
|
+
}
|
|
1824
1994
|
if (!command.enabled) automaticContinuation.revoke(command.provider, command.session);
|
|
1825
1995
|
// Acknowledge the durable preference before discovery or analysis.
|
|
1826
1996
|
emit({ type: "result", id: command.id, ok: true });
|
|
1997
|
+
void refreshCatalogAnalysis();
|
|
1827
1998
|
refreshSessions(true);
|
|
1828
1999
|
// Discovery's tracking transition schedules analysis. A second
|
|
1829
2000
|
// forced request could invalidate a concurrent manual recommendation.
|
|
@@ -2150,6 +2321,10 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
2150
2321
|
ownerTransport = await createOwnerTransport({ root, nonce: lock.nonce, allowedActions, allowControllerArchive: env.SIMY_REMOTE_CHILD !== '1', dispatch: command => commands.emit('line', JSON.stringify(command)) });
|
|
2151
2322
|
await publish();
|
|
2152
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; }
|
|
2153
2328
|
return { close, publish, agent };
|
|
2154
2329
|
}
|
|
2155
2330
|
|
package/src/orchestrator/loop.js
CHANGED
|
@@ -121,10 +121,12 @@ async function runCodingLoopOwned({
|
|
|
121
121
|
backend: snapshot.charter.backend, attempt_number: attemptNumber, instruction,
|
|
122
122
|
});
|
|
123
123
|
await publish(snapshot, "dispatching", onUpdate);
|
|
124
|
+
if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
|
|
124
125
|
appendEvent(snapshot, "coding", `${snapshot.charter.backend} is executing locally.`, {
|
|
125
126
|
backend: snapshot.charter.backend, attempt_number: attemptNumber, instruction,
|
|
126
127
|
});
|
|
127
128
|
await publish(snapshot, "coding", onUpdate);
|
|
129
|
+
if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
|
|
128
130
|
const recordCompletion = async execution => {
|
|
129
131
|
attempt = buildAttempt({ attemptNumber, charter: snapshot.charter, instruction, promptInterventions, execution });
|
|
130
132
|
if (attempt.status !== "completed" || Array.isArray(attempt.raw_output) || !Object.keys(attempt.raw_output).length) return;
|