@awak-app/simy-cli 0.7.31 → 0.7.33
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/agent.js +7 -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 +27 -3
- 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 +176 -74
- 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 +39 -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 +101 -24
- package/src/session-supervision/owner-transport.js +23 -8
- package/src/session-supervision/progressive-discovery.js +66 -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/team-threads.js +3 -6
- package/src/update-connection.js +16 -0
- package/src/web-api.js +9 -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) } = {}) {
|
|
@@ -286,6 +261,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
286
261
|
const remoteMetadata = createRemoteMetadata({ root, webOrigin, onChange: () => { if (!closed && !relay) setImmediate(() => void publish().catch(() => {})); } });
|
|
287
262
|
let teamThreads;
|
|
288
263
|
let currentAccess = { state: 'checking', organizationId: null, organizationName: null };
|
|
264
|
+
let updateIdentityRevision = 0;
|
|
289
265
|
let preferenceScope = null, emailSync = null, emailSyncRetryAt = 0;
|
|
290
266
|
let nativeDevelopmentReports = { state: 'loading', enrollments: [] };
|
|
291
267
|
let nativeDevelopmentReportsScope = null;
|
|
@@ -304,6 +280,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
304
280
|
const previouslyReady = desktopAccessReady(currentAccess);
|
|
305
281
|
currentAccess = access || { state: 'unavailable', organizationId: null, organizationName: null };
|
|
306
282
|
if (previousScope !== accessScope(currentAccess) || previouslyReady && !desktopAccessReady(currentAccess)) {
|
|
283
|
+
updateIdentityRevision++;
|
|
307
284
|
// A privilege loss also invalidates consent reads, even when the
|
|
308
285
|
// credential/organization revision itself did not change.
|
|
309
286
|
nativeDevelopmentReports = { state: 'loading', enrollments: [] };
|
|
@@ -317,7 +294,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
317
294
|
statisticsCache.clear();
|
|
318
295
|
activeStatisticsRead?.controller.abort();
|
|
319
296
|
explicitAnalyses.clear();
|
|
320
|
-
discoveryController?.abort();
|
|
297
|
+
discoveryController?.abort(); enrichmentController?.abort();
|
|
321
298
|
sessionRefreshAgain = Boolean(sessionRefresh) && desktopAccessReady(currentAccess);
|
|
322
299
|
for (const item of sessionStatus.sessions) automaticContinuation?.revoke(item.provider, item.session);
|
|
323
300
|
sessionStatus = { sessions: [], providers: {}, refreshedAt: null, initialLoading: true }; sessionRefreshedAt = 0;
|
|
@@ -562,12 +539,50 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
562
539
|
let commands;
|
|
563
540
|
let sessionStatus = { sessions: [], providers: { codex: { discovery: "loading", reason: null }, claude: { discovery: "loading", reason: null } }, refreshedAt: null, initialLoading: true };
|
|
564
541
|
let sessionRefresh = null, sessionRefreshedAt = 0, sessionRefreshAgain = false, discoveryController = null, discoveryStopped = false;
|
|
542
|
+
let enrichmentController = null, sessionEnrichment = null;
|
|
543
|
+
let catalogAnalysisJob = null, catalogAnalysisAgain = false;
|
|
544
|
+
const withCurrentSettings = (sessions, records) => {
|
|
545
|
+
const byKey = new Map(records.map(record => [`${record.provider}:${record.session}`, record]));
|
|
546
|
+
return sessions.map(candidate => projectSessionAutoContinue(candidate, byKey.get(`${candidate.provider}:${candidate.session}`)));
|
|
547
|
+
};
|
|
548
|
+
const refreshCatalogAnalysis = () => {
|
|
549
|
+
catalogAnalysisAgain = true;
|
|
550
|
+
if (catalogAnalysisJob) return catalogAnalysisJob;
|
|
551
|
+
const scope = accessScope(currentAccess), manager = analysisManager;
|
|
552
|
+
catalogAnalysisJob = Promise.resolve().then(async () => {
|
|
553
|
+
while (catalogAnalysisAgain) {
|
|
554
|
+
catalogAnalysisAgain = false;
|
|
555
|
+
const records = await recordReader.list();
|
|
556
|
+
if (closed || relay || discoveryStopped || manager !== analysisManager ||
|
|
557
|
+
!desktopAccessReady(currentAccess) || accessScope(currentAccess) !== scope || !coordinator.acceptsNewWork()) return;
|
|
558
|
+
await manager.update(analysisSessions(withCurrentSettings(sessionStatus.sessions, records)));
|
|
559
|
+
}
|
|
560
|
+
}).catch(() => {}).finally(() => {
|
|
561
|
+
catalogAnalysisJob = null;
|
|
562
|
+
if (catalogAnalysisAgain && !closed && !relay && !discoveryStopped) void refreshCatalogAnalysis();
|
|
563
|
+
});
|
|
564
|
+
return catalogAnalysisJob;
|
|
565
|
+
};
|
|
565
566
|
const hasUnfinishedSessions = records => records.some(record => record.autoContinue === true || record.run && !TERMINAL.has(record.run.state)) ||
|
|
566
567
|
sessionStatus.sessions.some(candidate => candidate.autoContinue === true || ['queued', 'analyzing'].includes(analysisManager.project(candidate).analysis?.state));
|
|
568
|
+
const discoveryDrains = new Set();
|
|
569
|
+
const trackDiscoveryDrain = operation => {
|
|
570
|
+
const settled = Promise.resolve(operation).then(() => {}, () => {});
|
|
571
|
+
discoveryDrains.add(settled);
|
|
572
|
+
void settled.then(() => discoveryDrains.delete(settled));
|
|
573
|
+
};
|
|
567
574
|
const stopDiscovery = async ({ drain = false } = {}) => {
|
|
568
|
-
discoveryController?.abort();
|
|
575
|
+
discoveryController?.abort(); enrichmentController?.abort();
|
|
569
576
|
sessionRefreshAgain = false;
|
|
570
|
-
if (drain
|
|
577
|
+
if (drain) {
|
|
578
|
+
let timer;
|
|
579
|
+
try {
|
|
580
|
+
await Promise.race([
|
|
581
|
+
Promise.allSettled([sessionRefresh, sessionEnrichment, catalogAnalysisJob, ...discoveryDrains]),
|
|
582
|
+
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); }),
|
|
583
|
+
]);
|
|
584
|
+
} finally { clearTimeout(timer); }
|
|
585
|
+
}
|
|
571
586
|
};
|
|
572
587
|
let confirmedUpdateStop;
|
|
573
588
|
const stopWorkForUpdate = () => confirmedUpdateStop ||= (async () => {
|
|
@@ -604,27 +619,59 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
604
619
|
const controller = discoveryController = new AbortController();
|
|
605
620
|
const attempt = { id: randomUUID(), requestedAt: Date.now(), state: 'pending' };
|
|
606
621
|
sessionStatus = { ...sessionStatus, discoveryAttempt: attempt };
|
|
607
|
-
|
|
622
|
+
enrichmentController?.abort();
|
|
623
|
+
const applyCatalog = async (value, sourceSignal = controller.signal) => {
|
|
624
|
+
if (closed || relay || controller.signal.aborted || sourceSignal.aborted || sessionStatus.discoveryAttempt?.id !== attempt.id) return;
|
|
625
|
+
const latestAccess = await accessSnapshot();
|
|
626
|
+
if (closed || relay || controller.signal.aborted || sourceSignal.aborted || sessionStatus.discoveryAttempt?.id !== attempt.id) return;
|
|
627
|
+
if (!desktopAccessReady(latestAccess) || accessScope(latestAccess) !== requestedScope) {
|
|
628
|
+
sessionRefreshAgain = desktopAccessReady(latestAccess); return;
|
|
629
|
+
}
|
|
630
|
+
const complete = new Set(value.completeProviders || (value.historyLoading === undefined ? PROVIDER_LINKS : []));
|
|
631
|
+
const merge = (previous, incoming) => {
|
|
632
|
+
const rows = new Map(previous.filter(item => !complete.has(item.provider)).map(item => [`${item.provider}:${item.session}`, item]));
|
|
633
|
+
for (const item of incoming || []) rows.set(`${item.provider}:${item.session}`, item);
|
|
634
|
+
return [...rows.values()];
|
|
635
|
+
};
|
|
636
|
+
sessionStatus = { ...sessionStatus, ...value,
|
|
637
|
+
sessions: linkedSessions(merge(sessionStatus.sessions, value.sessions)).sort(compareSessionWork),
|
|
638
|
+
selectionCandidates: merge(sessionStatus.selectionCandidates || [], value.selectionCandidates),
|
|
639
|
+
initialLoading: false, completeProviders: [...complete], discoveryAttempt: attempt };
|
|
640
|
+
sessionRefreshedAt = Date.now();
|
|
641
|
+
void publish().catch(() => {});
|
|
642
|
+
void refreshCatalogAnalysis();
|
|
643
|
+
};
|
|
644
|
+
sessionRefresh = boundedDiscovery(signal => (dependencies.discoverSessions || discoverSessions)({
|
|
645
|
+
root: deliveryRoot, signal, onDrain: trackDiscoveryDrain, includeArchived: true, includeSelectionCandidates: true,
|
|
646
|
+
connectedProviders: PROVIDER_LINKS.filter(provider => providerLinks[provider].connected),
|
|
647
|
+
timeoutMs: dependencies.discoveryTimeoutMs ?? 30_000,
|
|
648
|
+
onProgress: value => applyCatalog(value, signal),
|
|
649
|
+
}), { signal: controller.signal, timeoutMs: (dependencies.discoveryTimeoutMs ?? 30_000) + 1000, onDrain: dependencies.discoverSessions ? undefined : trackDiscoveryDrain })
|
|
608
650
|
.then(async value => {
|
|
651
|
+
await applyCatalog(value);
|
|
609
652
|
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
|
-
|
|
653
|
+
const failed = Object.values(value.providers || {}).some(provider => provider.discovery === 'unavailable');
|
|
654
|
+
sessionStatus = { ...sessionStatus, historyLoading: false,
|
|
655
|
+
discoveryAttempt: { ...attempt, state: failed ? 'failed' : 'succeeded', completedAt: Date.now() } };
|
|
656
|
+
// Publish before PR/network observation and queue maintenance. Neither
|
|
657
|
+
// operation owns the first usable catalog or the discovery promise.
|
|
658
|
+
void publish().catch(() => {});
|
|
659
|
+
const enrichment = enrichmentController = new AbortController();
|
|
660
|
+
const catalog = sessionStatus;
|
|
661
|
+
sessionEnrichment = boundedDiscovery(async signal => {
|
|
662
|
+
// Queue maintenance and PR checks are independent background work.
|
|
663
|
+
const queue = signal.aborted ? Promise.resolve() : refreshCatalogAnalysis();
|
|
664
|
+
const sessions = await observePullRequests(catalog.sessions, { signal });
|
|
665
|
+
const latestAccess = await accessSnapshot();
|
|
666
|
+
if (closed || relay || signal.aborted || !desktopAccessReady(latestAccess) || accessScope(latestAccess) !== requestedScope || sessionStatus !== catalog) return;
|
|
667
|
+
sessionStatus = { ...sessionStatus, sessions: linkedSessions(sessions) };
|
|
668
|
+
await queue;
|
|
669
|
+
if (!signal.aborted) void publish().catch(() => {});
|
|
670
|
+
}, { signal: enrichment.signal, timeoutMs: dependencies.enrichmentTimeoutMs ?? 10_000, onDrain: trackDiscoveryDrain })
|
|
671
|
+
.catch(() => {}).finally(() => { if (enrichmentController === enrichment) { enrichmentController = null; sessionEnrichment = null; } });
|
|
672
|
+
})
|
|
626
673
|
.catch(() => { if (closed || relay || controller.signal.aborted) return; sessionStatus = { ...sessionStatus, initialLoading: false,
|
|
627
|
-
discoveryAttempt: { ...attempt, state: 'failed', completedAt: Date.now() },
|
|
674
|
+
historyLoading: false, discoveryAttempt: { ...attempt, state: 'failed', completedAt: Date.now() },
|
|
628
675
|
providers: { codex: { discovery: "unavailable", reason: "Conversation discovery unavailable." }, claude: { discovery: "unavailable", reason: "Conversation discovery unavailable." } } }; sessionRefreshedAt = Date.now(); })
|
|
629
676
|
.finally(() => { if (discoveryController === controller) discoveryController = null; sessionRefresh = null; if (sessionRefreshAgain && !closed && !relay) { sessionRefreshAgain = false; refreshSessions(true); }
|
|
630
677
|
if (!closed) setImmediate(() => { void publish().catch(() => {}); }); });
|
|
@@ -639,7 +686,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
639
686
|
// Preference changes may use the observation already displayed by the UI.
|
|
640
687
|
// Execution still takes the fresh-discovery path and rechecks opt-in epochs.
|
|
641
688
|
const matches = item => item.provider === command.provider && item.session === command.session;
|
|
642
|
-
let candidate = preferObserved ? sessionStatus.sessions.find(matches) : null;
|
|
689
|
+
let candidate = preferObserved || reuseDiscovery ? sessionStatus.sessions.find(matches) : null;
|
|
643
690
|
if (!candidate) {
|
|
644
691
|
// Analysis joins an existing discovery instead of scheduling a second
|
|
645
692
|
// full scan. Enrollment retains its fresh-discovery boundary.
|
|
@@ -650,6 +697,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
650
697
|
candidate = sessionStatus.sessions.find(matches);
|
|
651
698
|
}
|
|
652
699
|
if (!candidate) throw new Error('This conversation is unavailable; refresh its provider.');
|
|
700
|
+
candidate = projectSessionAutoContinue(candidate, await readRecord(deliveryRoot, command.provider, command.session));
|
|
653
701
|
await authorizeProvider(command.provider, scope, link.epoch);
|
|
654
702
|
const selected = { ...candidate };
|
|
655
703
|
selectedScopes.set(selected, scope);
|
|
@@ -668,7 +716,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
668
716
|
phase = "save_disconnect";
|
|
669
717
|
if (!connected) providerLinks = await writeProviderLink(root, provider, false);
|
|
670
718
|
if (activeStatisticsRead?.key.startsWith(`${provider}:`)) activeStatisticsRead.controller.abort();
|
|
671
|
-
discoveryController?.abort();
|
|
719
|
+
discoveryController?.abort(); enrichmentController?.abort();
|
|
672
720
|
sessionRefreshAgain = false;
|
|
673
721
|
for (const item of sessionStatus.sessions.filter(item => item.provider === provider)) automaticContinuation?.revoke(provider, item.session);
|
|
674
722
|
for (const key of explicitAnalyses) if (key.startsWith(`${provider}:`)) explicitAnalyses.delete(key);
|
|
@@ -697,7 +745,10 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
697
745
|
} finally { providerLinkBusy.delete(provider); await publish(); }
|
|
698
746
|
};
|
|
699
747
|
const makeAutomaticContinuation = () => createAutomaticContinuation({ root: join(root, 'delivery-hooks'),
|
|
700
|
-
getManager: () => analysisManager,
|
|
748
|
+
getManager: () => analysisManager,
|
|
749
|
+
// Fresh exact context and enrollment still validate the provider below;
|
|
750
|
+
// unrelated history pagination is not an execution-readiness boundary.
|
|
751
|
+
selectCandidate: candidate => selectedSession(candidate, { reuseDiscovery: true }),
|
|
701
752
|
readSetting: dependencies.readSessionAutoContinue,
|
|
702
753
|
readContext: dependencies.readContinuationContext || readContinuationContext,
|
|
703
754
|
enroll: guardedEnroll, beforeEnroll: candidate => authorizeProvider(candidate.provider, selectedScopes.get(candidate), selectedLinkEpochs.get(candidate)),
|
|
@@ -788,7 +839,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
788
839
|
const packageInfo = dependencies.packageInfo || await readCliPackageInfo();
|
|
789
840
|
diagnostics.write(`desktop starting version=${packageInfo.version} pid=${process.pid} role=${candidate ? "update-candidate" : "supervisor"}`);
|
|
790
841
|
const makeAnalysisManager = () => (dependencies.createAnalysisManager || createContinuationAnalysisManager)({
|
|
791
|
-
root: join(root, 'session-analysis'), analyze: async (...args) => {
|
|
842
|
+
root: join(root, 'session-analysis'), waitNoticeAfterMs: dependencies.analysisWaitNoticeAfterMs, analyze: async (...args) => {
|
|
792
843
|
const scope = await authorizeWork();
|
|
793
844
|
const [candidate, options] = args;
|
|
794
845
|
const link = await requireProviderLink({ root: deliveryRoot, provider: candidate.provider });
|
|
@@ -948,9 +999,12 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
948
999
|
...(readbackOrder ? { readbackId: acceptanceReadbackId, readbackOrder } : {}) } };
|
|
949
1000
|
});
|
|
950
1001
|
await actionAssociations.decorate(status.deliveryHooks, projection);
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
1002
|
+
const catalog = refreshSessions();
|
|
1003
|
+
const discovery = { ...catalog, sessions: withCurrentSettings(catalog.sessions, records) };
|
|
1004
|
+
if (coordinator.acceptsNewWork()) automaticContinuation?.tick(discovery.sessions.filter(candidate => Array.isArray(discovery.completeProviders) && discovery.completeProviders.includes(candidate.provider)));
|
|
1005
|
+
let sessions = discovery.sessions.map(candidate => automaticContinuation.project(analysisManager.project(candidate)));
|
|
1006
|
+
await analysisManager.observeContinuationWaits?.(sessions);
|
|
1007
|
+
sessions = sessions.map(candidate => analysisManager.projectContinuationWait?.(candidate) || candidate);
|
|
954
1008
|
await actionAssociations.decorateSessions?.(sessions, projection);
|
|
955
1009
|
const stateRecordsBySession = new Map((projection?.records || records).filter(record => record.run).map(record => [`${record.provider}:${record.session}`, record]));
|
|
956
1010
|
const stateRunsBySession = new Map([...stateRecordsBySession].map(([key, record]) => [key, record.run]));
|
|
@@ -980,7 +1034,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
980
1034
|
const run = runsBySession.get(`${session.provider}:${session.session}`);
|
|
981
1035
|
if (run) run.actionDeadline = session.actionDeadline;
|
|
982
1036
|
}
|
|
983
|
-
status.sessionSupervision = { ...discovery, refreshing: Boolean(sessionRefresh), sessions: reconcileFamilyCompletion(sessions.map(candidate => (
|
|
1037
|
+
status.sessionSupervision = { ...discovery, refreshing: Boolean(sessionRefresh), sessions: reconcileFamilyCompletion(sessions.map(candidate => projectSessionAutoContinue(candidate,
|
|
1038
|
+
stateRecordsBySession.get(`${candidate.provider}:${candidate.session}`))).map(candidate => analysisManager.projectContinuationWait?.(candidate) || candidate).map(candidate => ({ ...candidate,
|
|
984
1039
|
decisionRequest: sessionDecisionPresentation(candidate, runsBySession.get(`${candidate.provider}:${candidate.session}`)),
|
|
985
1040
|
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
1041
|
runsBySession.get(`${candidate.provider}:${candidate.session}`), { rawRecord: stateRecordsBySession.get(`${candidate.provider}:${candidate.session}`) }) })).sort(compareSessionWork) };
|
|
@@ -1020,6 +1075,15 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1020
1075
|
// running frame cannot suppress a newly stopped conversation.
|
|
1021
1076
|
stopNotifier.observe(records);
|
|
1022
1077
|
emitStatus(status);
|
|
1078
|
+
const notices = await analysisManager.takeContinuationWaitNotifications?.(sessions) || [];
|
|
1079
|
+
const noticeAccess = await accessSnapshot();
|
|
1080
|
+
if (!closed && !relay && desktopAccessReady(noticeAccess) && accessScope(noticeAccess) === requestedScope) {
|
|
1081
|
+
for (const event of notices) {
|
|
1082
|
+
const scoped = { ...event, scope: reportController.controllerScope };
|
|
1083
|
+
await notificationJournal.append(scoped).catch(() => {});
|
|
1084
|
+
emit(event);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1023
1087
|
}
|
|
1024
1088
|
}).finally(() => { publishing = null; if (publishAgain && !closed) { publishAgain = false; setImmediate(() => { void publish().catch(() => {}); }); } });
|
|
1025
1089
|
return publishing;
|
|
@@ -1108,6 +1172,9 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1108
1172
|
updater.start({ registry: agent.registry, coordinator, isDesktopIdle: () => !unfinishedDesktopWork,
|
|
1109
1173
|
restartDaemon: async (targetVersion, installation) => {
|
|
1110
1174
|
if (!installation?.entryPath || !installation?.executablePath) throw new Error("Signed update runtime is unavailable.");
|
|
1175
|
+
const updateIdentity = await agent.getDesktopStatus();
|
|
1176
|
+
if (!await acceptStatusAccess(updateIdentity.access)) throw new Error("Updated desktop runtime identity changed during handoff.");
|
|
1177
|
+
const expectedIdentityRevision = updateIdentityRevision;
|
|
1111
1178
|
discoveryStopped = true;
|
|
1112
1179
|
await stopDiscovery({ drain: true });
|
|
1113
1180
|
// The candidate shares this cache. Stop inference before it starts so a
|
|
@@ -1122,7 +1189,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1122
1189
|
try { child = (dependencies.spawn || spawn)(installation.executablePath,
|
|
1123
1190
|
[join(dirname(installation.entryPath), "desktop-host.js")], {
|
|
1124
1191
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1125
|
-
env: { ...env, SIMY_DESKTOP_SUPERVISOR_TOKEN: supervisorToken, ...(transferToken ? {SIMY_UPDATE_TRANSFER_TOKEN:transferToken} : {}) },
|
|
1192
|
+
env: { ...updateConnectionEnvironment({ env, root, webOrigin }), SIMY_DESKTOP_SUPERVISOR_TOKEN: supervisorToken, ...(transferToken ? {SIMY_UPDATE_TRANSFER_TOKEN:transferToken} : {}) },
|
|
1126
1193
|
}); } catch (error) {
|
|
1127
1194
|
if(transferToken)await updateRuntime.reclaimFailedHandoff();
|
|
1128
1195
|
actionAssociations = makeActionAssociations();
|
|
@@ -1149,6 +1216,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1149
1216
|
});
|
|
1150
1217
|
try {
|
|
1151
1218
|
const first = await new Promise((accept, reject) => {
|
|
1219
|
+
let validating = false;
|
|
1152
1220
|
const limit = setTimeout(() => reject(new Error("Updated desktop runtime did not start in time.")), 30_000);
|
|
1153
1221
|
const finish = (fn, value) => { clearTimeout(limit); fn(value); };
|
|
1154
1222
|
child.once("error", () => finish(reject, new Error("Updated desktop runtime could not start.")));
|
|
@@ -1161,13 +1229,27 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1161
1229
|
}
|
|
1162
1230
|
let event; try { event = JSON.parse(line); } catch { return; }
|
|
1163
1231
|
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)) {
|
|
1232
|
+
if (event.type === "status" && !validating) {
|
|
1233
|
+
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
1234
|
finish(reject, new Error("Updated desktop runtime identity mismatch."));
|
|
1167
|
-
} else {
|
|
1235
|
+
} else {
|
|
1236
|
+
validating = true;
|
|
1237
|
+
void (async () => {
|
|
1238
|
+
// Sign-in and organization changes use a separate command
|
|
1239
|
+
// queue; re-read after all candidate startup awaits.
|
|
1240
|
+
const currentIdentity = await agent.getDesktopStatus();
|
|
1241
|
+
if (!await acceptStatusAccess(currentIdentity.access)) {
|
|
1242
|
+
finish(reject, new Error("Updated desktop runtime identity changed during handoff.")); return;
|
|
1243
|
+
}
|
|
1244
|
+
if (closed || child.exitCode !== null || expectedIdentityRevision !== updateIdentityRevision || !sameUpdateIdentity(updateIdentity, currentIdentity)) {
|
|
1245
|
+
finish(reject, new Error("Updated desktop runtime identity changed during handoff."));
|
|
1246
|
+
} else { accepted = true; finish(accept, event); }
|
|
1247
|
+
})().catch(error => finish(reject, error));
|
|
1248
|
+
}
|
|
1168
1249
|
}
|
|
1169
1250
|
});
|
|
1170
1251
|
});
|
|
1252
|
+
if (expectedIdentityRevision !== updateIdentityRevision || agent.getDesktopAccess && !sameAccess(updateIdentity.access, agent.getDesktopAccess())) throw new Error("Updated desktop runtime identity changed during handoff.");
|
|
1171
1253
|
relay = child;
|
|
1172
1254
|
await stopAgent();
|
|
1173
1255
|
if (child.exitCode !== null || closed) throw new Error("Updated runtime stopped during handoff.");
|
|
@@ -1579,18 +1661,30 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1579
1661
|
return;
|
|
1580
1662
|
}
|
|
1581
1663
|
if (action === 'session_get_status') {
|
|
1582
|
-
await selectedSession(command);
|
|
1583
|
-
await publish();
|
|
1584
1664
|
await authorizeProvider(command.provider, commandScope, commandProviderLink?.epoch);
|
|
1585
|
-
const
|
|
1665
|
+
const cached = lastStatus?.sessionSupervision?.sessions?.find(item =>
|
|
1586
1666
|
item.provider === command.provider && item.session === command.session && (!item.hostId || item.hostId === 'local'));
|
|
1587
|
-
|
|
1667
|
+
const record = await readRecord(deliveryRoot, command.provider, command.session);
|
|
1668
|
+
const observed = sessionStatus.sessions.find(item => item.provider === command.provider && item.session === command.session);
|
|
1669
|
+
if (!record && !observed) throw new Error('This conversation status is unavailable; refresh its provider.');
|
|
1670
|
+
// Read one durable task without scheduling a complete provider scan
|
|
1671
|
+
// or waiting for optional account-wide enrichment. Observation times
|
|
1672
|
+
// remain original, so a cached running flag cannot become fresh.
|
|
1673
|
+
const candidate = { ...(cached || { provider: command.provider, session: command.session, cwd: record?.cwd,
|
|
1674
|
+
observation: { observedAt: record?.runtime?.observedAt }, isRunning: record?.runtime?.isRunning }), ...observed };
|
|
1675
|
+
const row = { ...projectSessionAutoContinue(automaticContinuation.project(analysisManager.project(candidate)), record),
|
|
1676
|
+
workState: sessionWorkState(candidate, record?.run, { provider: sessionStatus.providers?.[command.provider] }) };
|
|
1677
|
+
const controls = await (dependencies.deliveryHookStatus || deliveryHookStatus)({ root: deliveryRoot, records: record ? [record] : [] });
|
|
1678
|
+
const selectedRun = controls.runs?.find(run => run.provider === command.provider && run.session === command.session);
|
|
1679
|
+
row.decisionRequest = sessionDecisionPresentation(row, selectedRun);
|
|
1680
|
+
row.conversationPresentation = conversationPresentation(row, selectedRun, { rawRecord: record });
|
|
1681
|
+
await authorizeProvider(command.provider, commandScope, commandProviderLink?.epoch);
|
|
1588
1682
|
// The same app projection carries observation time and next action;
|
|
1589
1683
|
// saved consent and an active record cannot prove a provider send.
|
|
1590
1684
|
const { provider, session, title, autoContinue, autoContinueStatus, autoContinueReason,
|
|
1591
|
-
binding, workState, dispatchReadiness, conversationPresentation } = row;
|
|
1685
|
+
binding, workState, dispatchReadiness, conversationPresentation: presentation, decisionRequest } = row;
|
|
1592
1686
|
emit({ type: 'result', id: command.id, ok: true, data: { provider, session, title,
|
|
1593
|
-
autoContinue, autoContinueStatus, autoContinueReason, binding, workState, dispatchReadiness, conversationPresentation } });
|
|
1687
|
+
autoContinue, autoContinueStatus, autoContinueReason, binding, workState, dispatchReadiness, conversationPresentation: presentation, decisionRequest } });
|
|
1594
1688
|
return;
|
|
1595
1689
|
}
|
|
1596
1690
|
if (action === 'session_read_context') {
|
|
@@ -1666,7 +1760,11 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1666
1760
|
await agent.refreshDesktopAccess?.();
|
|
1667
1761
|
} else if (action === 'desktop_sqm_settings') {
|
|
1668
1762
|
const verified = await agent.refreshDesktopAccess?.();
|
|
1669
|
-
|
|
1763
|
+
// Navigation starts no work. Keep scope and entitlement checks while
|
|
1764
|
+
// allowing users to inspect SQM during an update admission hold.
|
|
1765
|
+
const currentAccess = await accessSnapshot();
|
|
1766
|
+
requireDesktopAccess(currentAccess);
|
|
1767
|
+
if (accessScope(currentAccess) !== commandScope) throw Object.assign(new Error('Desktop account or organization changed.'), { code: 'desktop_access_required' });
|
|
1670
1768
|
if (!desktopAccessReady(verified) || verified.organizationId !== access.organizationId ||
|
|
1671
1769
|
!validDesktopAccessPath(verified.settingsPath, verified.organizationSlug)) throw new Error('Organization settings are unavailable; refresh access first.');
|
|
1672
1770
|
const url = new URL(`/org/${encodeURIComponent(verified.organizationSlug)}/quality-monitor`, webOrigin);
|
|
@@ -1807,23 +1905,27 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1807
1905
|
await actionAssociations.correct(command, analysisManager.project(candidate));
|
|
1808
1906
|
} else if (action === "session_set_auto_continue") {
|
|
1809
1907
|
if (typeof command.enabled !== 'boolean') throw new Error('Choose whether automatic continuation is enabled.');
|
|
1810
|
-
let admission, providerEpoch;
|
|
1908
|
+
let admission, target, providerEpoch;
|
|
1811
1909
|
if (command.enabled) {
|
|
1812
1910
|
const candidate = await selectedSession(command, { preferObserved: true });
|
|
1813
1911
|
const canEnable = candidate.autoContinueAvailability?.canEnable ?? candidate.capabilities?.canAutoContinue;
|
|
1814
|
-
if (canEnable !== true) {
|
|
1912
|
+
if (candidate.controllerCanSetAutoContinue !== true && canEnable !== true) {
|
|
1815
1913
|
throw new AutoContinueUnavailableError({ reason: candidate.autoContinueAvailability?.reason || candidate.reason,
|
|
1816
1914
|
reasonCode: candidate.autoContinueAvailability?.reasonCode || candidate.continuationReasonCode });
|
|
1817
1915
|
}
|
|
1818
1916
|
admission = candidate.continuationAdmission;
|
|
1917
|
+
target = { provider: candidate.provider, session: candidate.session, cwd: candidate.cwd };
|
|
1819
1918
|
providerEpoch = selectedLinkEpochs.get(candidate);
|
|
1820
1919
|
}
|
|
1821
1920
|
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 });
|
|
1921
|
+
const saved = await (dependencies.setSessionAutoContinue || saveSessionAutoContinuePreference)({ root: join(root, 'delivery-hooks'),
|
|
1922
|
+
provider: command.provider, session: command.session, enabled: command.enabled, initialContinuationPolicy: command.continuationPolicy, admission, target, expectedProviderEpoch: providerEpoch });
|
|
1923
|
+
if (saved) sessionStatus = { ...sessionStatus,
|
|
1924
|
+
sessions: sessionStatus.sessions.map(candidate => projectSessionAutoContinue(candidate, saved)) };
|
|
1824
1925
|
if (!command.enabled) automaticContinuation.revoke(command.provider, command.session);
|
|
1825
1926
|
// Acknowledge the durable preference before discovery or analysis.
|
|
1826
1927
|
emit({ type: "result", id: command.id, ok: true });
|
|
1928
|
+
void refreshCatalogAnalysis();
|
|
1827
1929
|
refreshSessions(true);
|
|
1828
1930
|
// Discovery's tracking transition schedules analysis. A second
|
|
1829
1931
|
// forced request could invalidate a concurrent manual recommendation.
|
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;
|