@awak-app/simy-cli 0.7.46 → 0.7.48
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 +2 -2
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/.mcp.json +11 -0
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +5 -3
- package/src/codex-plugins/simy/native/app-tools.mjs +45 -0
- package/src/codex-plugins/simy/native/bridge.mjs +156 -0
- package/src/codex-plugins/simy/native/guidance-intent.mjs +30 -0
- package/src/codex-plugins/simy/native/launch +36 -0
- package/src/codex-plugins/simy/native/server.mjs +26 -0
- package/src/codex-plugins/simy/native/submission-receipt.mjs +9 -0
- package/src/delivery-hooks/supervisor.js +28 -3
- package/src/desktop-host.js +185 -13
- package/src/session-supervision/claude-sessions.js +25 -14
- package/src/session-supervision/claude-stream-owner.js +266 -0
- package/src/session-supervision/claude-stream-presentation.js +27 -0
- package/src/session-supervision/claude-stream-transport.js +188 -0
- package/src/session-supervision/codex-app-server.js +30 -8
- package/src/session-supervision/codex-app-tools-connection.js +30 -0
- package/src/session-supervision/conversation-presentation.js +2 -0
- package/src/session-supervision/index.js +5 -1
- package/src/session-supervision/native-handoff.js +69 -13
- package/src/session-supervision/native-turn-observer.js +196 -0
- package/src/session-supervision/purpose-verification.js +18 -7
- package/src/session-supervision/session-guidance.js +160 -0
- package/src/sqm/claude-plugin.js +2 -0
- package/src/sqm/review-worker.js +5 -1
package/src/desktop-host.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {sendSessionGuidance, reconcileSessionGuidance, observeSessionGuidanceConnection} from './session-supervision/session-guidance.js';
|
|
2
|
+
import {guidanceAllowed, guidanceTarget, guidancePending} from './codex-plugins/simy/native/guidance-intent.mjs';
|
|
3
|
+
import { createClaudeStreamOwner } from './session-supervision/claude-stream-owner.js';
|
|
4
|
+
import { claudeStreamPresentation } from './session-supervision/claude-stream-presentation.js';
|
|
5
|
+
import { readOfficialNativeAccount, pairedNativeIdentity } from './native-provider-identity.js';
|
|
1
6
|
import { createDesktopFrameWriter } from './desktop-frame-writer.js';
|
|
2
7
|
import { createDesktopCatalogEncoder, createDesktopStatusSnapshot, copyOwnedDesktopStatus } from './desktop-catalog-frames.js';
|
|
3
8
|
import { boundedDiscovery } from './session-supervision/progressive-discovery.js';
|
|
@@ -43,7 +48,7 @@ import { stopDesktopWorkForUpdate } from './desktop-update-stop.js';
|
|
|
43
48
|
import { desktopAccessReady, desktopActionAllowed, requireDesktopAccess, validDesktopAccessPath } from './desktop-access.js';
|
|
44
49
|
import { createActionDeadlineService } from './session-supervision/action-deadline.js';
|
|
45
50
|
import { deliveryHookStatus, openHookSetup } from "./delivery-hooks/desktop.js";
|
|
46
|
-
import { controlDelivery, verifyDelivery, decideManualAcceptance, createRecordReader, readRecord, updateRecord, recordProviderArchive, TERMINAL } from "./codex-plugins/simy/hooks/delivery-loop.mjs";
|
|
51
|
+
import { controlDelivery, verifyDelivery, decideManualAcceptance, createRecordReader, readRecord, updateRecord, recordProviderArchive, processIdentity, TERMINAL } from "./codex-plugins/simy/hooks/delivery-loop.mjs";
|
|
47
52
|
import { createManagedSupervisor } from "./delivery-hooks/supervisor.js";
|
|
48
53
|
import { createActionAssociationHost } from "./session-supervision/action-association-host.js";
|
|
49
54
|
import { createStopNotifier } from "./session-supervision/stop-notifications.js";
|
|
@@ -167,7 +172,10 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
167
172
|
const frames = createDesktopFrameWriter(output, { capture: frame => frame,
|
|
168
173
|
encode: frame => typeof frame === 'string' ? frame : catalogEncoder.encode(frame) });
|
|
169
174
|
const pendingRemoteReceipts = new Map();
|
|
170
|
-
|
|
175
|
+
const guidanceReads = new Map(), guidanceReadAt = new Map(), guidanceProbes = new Map();
|
|
176
|
+
let guidanceAbort = new AbortController();
|
|
177
|
+
let operationJournal, ownerTransport, claudeOwner;
|
|
178
|
+
const claudeOwnerStates = new Map();
|
|
171
179
|
// An explicitly requested window update releases only its viewer. The same
|
|
172
180
|
// authenticated owner, tasks and pending permissions remain alive on IPC.
|
|
173
181
|
let windowUpdatePrepared = false, viewerDetached = false, relayStatus = null;
|
|
@@ -209,6 +217,16 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
209
217
|
const emitStatus = status => {
|
|
210
218
|
let catalogObservation;
|
|
211
219
|
status = projectCatalogFrame(status);
|
|
220
|
+
const claudeScope = agent?.getTeamThreadsScope?.();
|
|
221
|
+
if (desktopAccessReady(status.access) && providerLinks.claude?.connected && claudeScope?.authenticated && status.sessionSupervision?.sessions) {
|
|
222
|
+
const known = new Set(status.sessionSupervision.sessions.filter(row => row.provider === 'claude').map(row => row.session));
|
|
223
|
+
const owners = [...claudeOwnerStates.values()].filter(owner => owner.scope === claudeScope.fenceKey && owner.epoch === (providerLinks.claude.epoch ?? 'legacy-connected'));
|
|
224
|
+
const added = owners.filter(owner => !known.has(owner.session)).map(owner => ({ provider: 'claude', session: owner.session, hostId: 'local',
|
|
225
|
+
cwd: owner.cwd, title: 'Connected Claude conversation', model: owner.model, source: 'simy_claude_stream', isRunning: owner.state !== 'idle' && owner.state !== 'closed',
|
|
226
|
+
autoContinue: false, capabilities: { canMonitor: false, canAutoContinue: false }, conversationPresentation: {} }));
|
|
227
|
+
status = { ...status, sessionSupervision: { ...status.sessionSupervision, sessions: [...status.sessionSupervision.sessions, ...added]
|
|
228
|
+
.map(row => claudeStreamPresentation(row, owners.find(owner => owner.session === row.session))) } };
|
|
229
|
+
}
|
|
212
230
|
status = { ...status, windowUpdate: { version: 1, preservesWork: Boolean(ownerTransport), prepared: windowUpdatePrepared } };
|
|
213
231
|
const updateSettings = updater?.snapshot?.() || {};
|
|
214
232
|
status = { ...status, update: { ...status.update,
|
|
@@ -295,7 +313,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
295
313
|
...(dependencies.observeUpdateProvider ? {observe:dependencies.observeUpdateProvider} : {}) });
|
|
296
314
|
const coordinator = createUpdateCoordinator({ ...(dependencies.updateDetachment || {
|
|
297
315
|
store:updateRuntime.store,gate:updateRuntime.gate,
|
|
298
|
-
boundary:dependencies.updateSafeBoundary || (async()=>{const holds=updater?.updateDrainState?.();if(holds && (holds.activeWork || holds.applyHolds))return {safe:false,liveExecutions:holds.activeWork,pendingWrites:holds.applyHolds,unsettledPermissions:0};return updateRuntime.boundary();}),
|
|
316
|
+
boundary:dependencies.updateSafeBoundary || (async()=>{if(claudeOwner?.busy())return {safe:false,liveExecutions:1,pendingWrites:0,unsettledPermissions:1};const holds=updater?.updateDrainState?.();if(holds && (holds.activeWork || holds.applyHolds))return {safe:false,liveExecutions:holds.activeWork,pendingWrites:holds.applyHolds,unsettledPermissions:0};return updateRuntime.boundary();}),
|
|
299
317
|
reconcile:dependencies.reconcileUpdateConnection || createCoordinatorReconciler(dependencies.updateReconciliation) || updateRuntime.reconcile,
|
|
300
318
|
}), preserveConnectedOnStartup:true, initialState:env.SIMY_UPDATE_TRANSFER_TOKEN?'detached':'connected',
|
|
301
319
|
onChange:()=>{if(agent && !closed && !relay)setImmediate(()=>void publish().catch(()=>{}));} });
|
|
@@ -335,6 +353,11 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
335
353
|
const previouslyReady = desktopAccessReady(currentAccess);
|
|
336
354
|
currentAccess = access || { state: 'unavailable', organizationId: null, organizationName: null };
|
|
337
355
|
if (previousScope !== accessScope(currentAccess) || previouslyReady && !desktopAccessReady(currentAccess)) {
|
|
356
|
+
// revoke fences new writes synchronously. Cleanup must not be awaited
|
|
357
|
+
// from authorization: that refresh may belong to the owner operation
|
|
358
|
+
// currently holding the cleanup queue.
|
|
359
|
+
void claudeOwner?.revoke?.().catch(error => diagnostics.write(`Claude owner cleanup retained: ${error?.code || 'unknown'}`));
|
|
360
|
+
guidanceAbort.abort(); guidanceAbort = new AbortController();
|
|
338
361
|
updateIdentityRevision++;
|
|
339
362
|
// A privilege loss also invalidates consent reads, even when the
|
|
340
363
|
// credential/organization revision itself did not change.
|
|
@@ -592,6 +615,22 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
592
615
|
if (expectedScope !== undefined && expectedScope !== scope) throw Object.assign(new Error("Desktop account or organization changed."), { code: "desktop_access_required" });
|
|
593
616
|
return scope;
|
|
594
617
|
};
|
|
618
|
+
claudeOwner = (dependencies.createClaudeStreamOwner || createClaudeStreamOwner)({ root,
|
|
619
|
+
inspectProcess: dependencies.processIdentity || processIdentity,
|
|
620
|
+
executable: async () => { const native = await (dependencies.inspectBackendExecutable || inspectBackendExecutable)('claude');
|
|
621
|
+
if (!native?.compatible || !native.executable) throw Object.assign(new Error('Claude Code is unavailable.'), { code: 'claude_stream_unavailable' });
|
|
622
|
+
return native.executable; },
|
|
623
|
+
authorize: () => authorizeProvider('claude'),
|
|
624
|
+
context: async () => {
|
|
625
|
+
const scope = agent?.getTeamThreadsScope?.();
|
|
626
|
+
if (!scope?.authenticated || !scope.allowed || !scope.fenceKey) throw Object.assign(new Error('Sign in to SIMY first.'), { code: 'desktop_access_required' });
|
|
627
|
+
const link = await requireProviderLink({ root: deliveryRoot, provider: 'claude' });
|
|
628
|
+
const email = await (dependencies.readOfficialNativeAccount || readOfficialNativeAccount)('claude');
|
|
629
|
+
if (!email) throw Object.assign(new Error('Sign in to Claude Code first.'), { code: 'native_identity_unavailable' });
|
|
630
|
+
return { scope: scope.fenceKey, epoch: link.epoch ?? 'legacy-connected',
|
|
631
|
+
account: pairedNativeIdentity({ deviceId: scope.fenceKey, provider: 'claude', email, connectionEpoch: link.epoch }).account_id };
|
|
632
|
+
},
|
|
633
|
+
onChange: owner => { claudeOwnerStates.set(owner.session, owner); if (!closed && agent) setImmediate(() => { void publish().catch(() => {}); }); } });
|
|
595
634
|
const selectedScopes = new WeakMap(), selectedLinkEpochs = new WeakMap();
|
|
596
635
|
const guardedEnroll = async options => {
|
|
597
636
|
await authorizeProvider(options.provider);
|
|
@@ -618,7 +657,54 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
618
657
|
let catalogAnalysisJob = null, catalogAnalysisAgain = false;
|
|
619
658
|
const withCurrentSettings = (sessions, records) => {
|
|
620
659
|
const byKey = new Map(records.map(record => [`${record.provider}:${record.session}`, record]));
|
|
621
|
-
return sessions.map(candidate =>
|
|
660
|
+
return sessions.map(candidate => {
|
|
661
|
+
const record = byKey.get(`${candidate.provider}:${candidate.session}`);
|
|
662
|
+
const projected = projectSessionAutoContinue(candidate, record);
|
|
663
|
+
// An authenticated native Stop can be newer than the catalog's running
|
|
664
|
+
// observation while history is loading. It wakes read-only analysis;
|
|
665
|
+
// fresh context and the native writer lock still gate actual dispatch.
|
|
666
|
+
if (candidate.provider === 'codex' && projected.autoContinue === true && typeof candidate.cwd === 'string' && candidate.cwd && record?.cwd === candidate.cwd &&
|
|
667
|
+
record.lastEvent === 'Stop' && record.runtime?.isRunning === false && record.runtime.process?.identity &&
|
|
668
|
+
Number.isFinite(record.runtime.observedAt) && Number.isFinite(candidate.observation?.observedAt) &&
|
|
669
|
+
record.runtime.observedAt > candidate.observation.observedAt) return { ...projected, isRunning: false,
|
|
670
|
+
observation: { ...candidate.observation, observedAt: record.runtime.observedAt } };
|
|
671
|
+
return projected;
|
|
672
|
+
});
|
|
673
|
+
};
|
|
674
|
+
const automaticCandidates = discovery => discovery.sessions.filter(candidate => {
|
|
675
|
+
if (discovery.completeProviders?.includes(candidate.provider)) return true;
|
|
676
|
+
// A newly read exact Codex conversation need not wait for unrelated
|
|
677
|
+
// history pages. Context, consent, native permissions and the writer
|
|
678
|
+
// lock are still checked by the existing enrollment/dispatch path.
|
|
679
|
+
const admission = candidate.continuationAdmission;
|
|
680
|
+
return candidate.provider === 'codex' && discovery.providers?.codex?.discovery === 'ready' &&
|
|
681
|
+
candidate.observation?.source === 'codex_app_server' && admission?.source === 'codex_app_server' &&
|
|
682
|
+
admission.provider === candidate.provider && admission.session === candidate.session &&
|
|
683
|
+
admission.cwd === candidate.cwd && typeof admission.model === 'string' && admission.model.length > 0 &&
|
|
684
|
+
Number.isFinite(admission.validatedAt) && Number.isFinite(discovery.discoveryAttempt?.requestedAt) &&
|
|
685
|
+
admission.validatedAt >= discovery.discoveryAttempt.requestedAt;
|
|
686
|
+
});
|
|
687
|
+
let automaticRefresh = null, automaticRefreshAgain = false;
|
|
688
|
+
// Execution scheduling must not wait for optional Action/deadline projection.
|
|
689
|
+
// The coordinator still owns fresh context, opt-in, authority and writer checks.
|
|
690
|
+
const refreshAutomaticContinuation = () => {
|
|
691
|
+
automaticRefreshAgain = true;
|
|
692
|
+
if (automaticRefresh) return automaticRefresh;
|
|
693
|
+
const scope = accessScope(currentAccess), controller = automaticContinuation;
|
|
694
|
+
automaticRefresh = Promise.resolve().then(async () => {
|
|
695
|
+
while (automaticRefreshAgain) {
|
|
696
|
+
automaticRefreshAgain = false;
|
|
697
|
+
const records = await recordReader.list();
|
|
698
|
+
if (closed || relay || discoveryStopped || controller !== automaticContinuation ||
|
|
699
|
+
!desktopAccessReady(currentAccess) || accessScope(currentAccess) !== scope || !coordinator.acceptsNewWork()) return;
|
|
700
|
+
controller?.tick(automaticCandidates({ ...sessionStatus,
|
|
701
|
+
sessions: withCurrentSettings(sessionStatus.sessions, records) }));
|
|
702
|
+
}
|
|
703
|
+
}).catch(() => {}).finally(() => {
|
|
704
|
+
automaticRefresh = null;
|
|
705
|
+
if (automaticRefreshAgain && !closed && !relay && !discoveryStopped) void refreshAutomaticContinuation();
|
|
706
|
+
});
|
|
707
|
+
return automaticRefresh;
|
|
622
708
|
};
|
|
623
709
|
const refreshCatalogAnalysis = () => {
|
|
624
710
|
catalogAnalysisAgain = true;
|
|
@@ -633,6 +719,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
633
719
|
await manager.update(analysisSessions(withCurrentSettings(sessionStatus.sessions, records)), {
|
|
634
720
|
priorityKeys: new Set(records.filter(supervisedSession).map(record => `${record.provider}:${record.session}`)),
|
|
635
721
|
});
|
|
722
|
+
void refreshAutomaticContinuation();
|
|
636
723
|
}
|
|
637
724
|
}).catch(() => {}).finally(() => {
|
|
638
725
|
catalogAnalysisJob = null;
|
|
@@ -809,7 +896,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
809
896
|
candidate = sessionStatus.sessions.find(matches);
|
|
810
897
|
}
|
|
811
898
|
if (!candidate) throw new Error('This conversation is unavailable; refresh its provider.');
|
|
812
|
-
|
|
899
|
+
const selectedRecord = await readRecord(deliveryRoot, command.provider, command.session);
|
|
900
|
+
candidate = withCurrentSettings([candidate], selectedRecord ? [selectedRecord] : [])[0];
|
|
813
901
|
await authorizeProvider(command.provider, scope, link.epoch);
|
|
814
902
|
const selected = { ...candidate };
|
|
815
903
|
selectedScopes.set(selected, scope);
|
|
@@ -820,6 +908,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
820
908
|
if (!PROVIDER_LINKS.includes(provider)) throw new Error("Unsupported SIMY provider connection.");
|
|
821
909
|
if (providerLinkBusy.has(provider)) throw new Error("SIMY connection is already changing.");
|
|
822
910
|
providerRepairControllers.get(provider)?.abort();
|
|
911
|
+
guidanceAbort.abort(); guidanceAbort = new AbortController();
|
|
823
912
|
providerLinkBusy.add(provider);
|
|
824
913
|
providerLinkFeedback[provider] = { state: connected ? "connecting" : "disconnecting" };
|
|
825
914
|
let phase = "read_settings";
|
|
@@ -827,6 +916,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
827
916
|
providerLinks = await readProviderLinks(root);
|
|
828
917
|
phase = "save_disconnect";
|
|
829
918
|
if (!connected) providerLinks = await writeProviderLink(root, provider, false);
|
|
919
|
+
if (provider === 'claude' && !connected) await claudeOwner.revoke();
|
|
830
920
|
if (activeStatisticsRead?.key.startsWith(`${provider}:`)) activeStatisticsRead.controller.abort();
|
|
831
921
|
discoveryController?.abort(); enrichmentController?.abort();
|
|
832
922
|
sessionRefreshAgain = false;
|
|
@@ -958,9 +1048,12 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
958
1048
|
return (dependencies.analyzeContinuation || analyzeContinuation)(candidate, { ...options,
|
|
959
1049
|
beforeProviderDispatch: () => authorizeProvider(candidate.provider, scope, link.epoch) });
|
|
960
1050
|
},
|
|
961
|
-
onChange: () => { if (!closed && !relay)
|
|
1051
|
+
onChange: () => { if (!closed && !relay) {
|
|
1052
|
+
void refreshAutomaticContinuation();
|
|
1053
|
+
setImmediate(() => { void publish().catch(() => {}); });
|
|
1054
|
+
} },
|
|
962
1055
|
});
|
|
963
|
-
// Batch burst notifications and
|
|
1056
|
+
// Batch burst notifications and heartbeat/status requests into one frame. The
|
|
964
1057
|
// short window leaves commands responsive and the regular heartbeat intact.
|
|
965
1058
|
const publish = () => {
|
|
966
1059
|
if (closed || relay || !agent) return Promise.resolve();
|
|
@@ -985,6 +1078,14 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
985
1078
|
const sampledAccess = structuredClone(status.access || { state: 'unavailable', organizationId: null, organizationName: null });
|
|
986
1079
|
if (!await acceptStatusAccess(sampledAccess)) { publishAgain = true; return; }
|
|
987
1080
|
status.access = sampledAccess;
|
|
1081
|
+
const nativeScope = agent.getTeamThreadsScope?.();
|
|
1082
|
+
if (desktopAccessReady(sampledAccess) && providerLinks.claude?.connected && nativeScope?.authenticated && nativeScope.allowed &&
|
|
1083
|
+
nativeScope.fenceKey && claudeLoadedScope !== nativeScope.fenceKey) {
|
|
1084
|
+
try {
|
|
1085
|
+
for (const owner of await claudeOwner.list()) claudeOwnerStates.set(owner.session, owner);
|
|
1086
|
+
claudeLoadedScope = nativeScope.fenceKey;
|
|
1087
|
+
} catch { /* The connection stays unavailable until native identity is verified. */ }
|
|
1088
|
+
}
|
|
988
1089
|
status.updateDetachment = updateRuntime.snapshot(coordinator.snapshot(),updater?.snapshot?.(),{accessReady:desktopAccessReady(sampledAccess)});
|
|
989
1090
|
preferenceScope = controllerScope(status);
|
|
990
1091
|
if (preferenceScope) await controllerPreferences.load(preferenceScope);
|
|
@@ -1062,6 +1163,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1062
1163
|
}).finally(() => { connectionPublishing = null; });
|
|
1063
1164
|
return connectionPublishing;
|
|
1064
1165
|
};
|
|
1166
|
+
let claudeLoadedScope = null;
|
|
1065
1167
|
const projectStatus = () => {
|
|
1066
1168
|
if (closed || relay || !agent) return Promise.resolve();
|
|
1067
1169
|
if (publishing) { publishAgain = true; return publishConnectionStatus(); }
|
|
@@ -1079,6 +1181,22 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1079
1181
|
if (preferenceScope) await controllerPreferences.load(preferenceScope);
|
|
1080
1182
|
const requestedLinks = await decorateProviderSettings(status);
|
|
1081
1183
|
const allRecords = await recordReader.list();
|
|
1184
|
+
if (desktopAccessReady(currentAccess) && coordinator.acceptsNewWork()) {
|
|
1185
|
+
for (const record of allRecords) {
|
|
1186
|
+
if (guidanceReads.size >= 2) break;
|
|
1187
|
+
if (!guidancePending(record) || !record.run.managed.userGuidance.bridgeAttemptedAt || guidanceReads.has(record.session) ||
|
|
1188
|
+
Date.now() - (guidanceReadAt.get(record.session) || 0) < 15_000) continue;
|
|
1189
|
+
guidanceReadAt.set(record.session, Date.now());
|
|
1190
|
+
const scope = accessScope(currentAccess);
|
|
1191
|
+
const authorize = async () => {
|
|
1192
|
+
if (closed || !coordinator.acceptsNewWork() || accessScope(currentAccess) !== scope) throw new Error('Guidance read scope changed.');
|
|
1193
|
+
await authorizeProvider(record.provider, scope);
|
|
1194
|
+
};
|
|
1195
|
+
const work = reconcileSessionGuidance({root: deliveryRoot, record, authorize, signal: guidanceAbort.signal}).then(changed => {if (changed) void publish();})
|
|
1196
|
+
.catch(() => {}).finally(() => guidanceReads.delete(record.session));
|
|
1197
|
+
guidanceReads.set(record.session, work);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1082
1200
|
if (accessScope(currentAccess) === requestedScope) {
|
|
1083
1201
|
// Keep explicit preference overrides until the scope changes. A full
|
|
1084
1202
|
// projection that started before the save can finish after this read.
|
|
@@ -1153,7 +1271,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1153
1271
|
await actionAssociations.decorate(status.deliveryHooks, projection);
|
|
1154
1272
|
const catalog = refreshSessions();
|
|
1155
1273
|
const discovery = { ...catalog, sessions: withCurrentSettings(catalog.sessions, records) };
|
|
1156
|
-
|
|
1274
|
+
|
|
1157
1275
|
let sessions = discovery.sessions.map(candidate => automaticContinuation.project(analysisManager.project(candidate)));
|
|
1158
1276
|
await analysisManager.observeContinuationWaits?.(sessions);
|
|
1159
1277
|
sessions = sessions.map(candidate => analysisManager.projectContinuationWait?.(candidate) || candidate);
|
|
@@ -1254,7 +1372,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1254
1372
|
const results = await Promise.allSettled([stopDiscovery({ drain: true }), publishing, connectionPublishing,
|
|
1255
1373
|
pluginInspection,
|
|
1256
1374
|
...providerRepairJobs,
|
|
1257
|
-
controllerPreferences.close(), operationJournal.close(), remoteHosts?.close(), remoteMetadata.close(), actionAssociations.close(), automaticContinuation?.close(), analysisManager?.close(),
|
|
1375
|
+
claudeOwner.close(), controllerPreferences.close(), operationJournal.close(), remoteHosts?.close(), remoteMetadata.close(), actionAssociations.close(), automaticContinuation?.close(), analysisManager?.close(),
|
|
1258
1376
|
managedSupervisor.close(), recordReader.close(), stopWorkflows(),
|
|
1259
1377
|
agent ? createAgentShutdown(agent)() : undefined]);
|
|
1260
1378
|
const failure = results.find(result => result.status === "rejected");
|
|
@@ -1262,6 +1380,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1262
1380
|
};
|
|
1263
1381
|
const close = () => shuttingDown ||= (async () => {
|
|
1264
1382
|
closed = true;
|
|
1383
|
+
guidanceAbort.abort();
|
|
1265
1384
|
frames.close();
|
|
1266
1385
|
commands?.close();
|
|
1267
1386
|
input.pause?.();
|
|
@@ -1425,7 +1544,11 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1425
1544
|
const maintenance = new Map();
|
|
1426
1545
|
const tick = worker => {
|
|
1427
1546
|
if (closed || relay || !coordinator.acceptsNewWork() || maintenance.has(worker)) return;
|
|
1428
|
-
|
|
1547
|
+
// The existing heartbeat publishes completed background maintenance.
|
|
1548
|
+
// Completion itself does not prove a visible change; after the short
|
|
1549
|
+
// batching window it otherwise reprojects/repaints the entire history.
|
|
1550
|
+
// Explicit commands and actual change notifications keep their live path.
|
|
1551
|
+
const pending = Promise.resolve().then(() => worker.tick()).catch(() => {}).finally(() => maintenance.delete(worker));
|
|
1429
1552
|
maintenance.set(worker, pending);
|
|
1430
1553
|
};
|
|
1431
1554
|
tick(actionAssociations);
|
|
@@ -1435,6 +1558,9 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1435
1558
|
if (desktopAccessReady(currentAccess)) {
|
|
1436
1559
|
tick(actionAssociations);
|
|
1437
1560
|
tick(managedSupervisor);
|
|
1561
|
+
// Observe native Stop records even when the full status projection or
|
|
1562
|
+
// unrelated provider history is still pending.
|
|
1563
|
+
void refreshCatalogAnalysis();
|
|
1438
1564
|
}
|
|
1439
1565
|
void publish().catch(() => emit({ type: "error", message: "SIMY status could not be refreshed." }));
|
|
1440
1566
|
}, 3_000);
|
|
@@ -1499,6 +1625,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1499
1625
|
}
|
|
1500
1626
|
};
|
|
1501
1627
|
const executeUpdate = (prepareRestart = false) => {
|
|
1628
|
+
if (claudeOwner.busy()) throw new Error('Stop the connected Claude turn or answer its permission request before updating.');
|
|
1502
1629
|
const kind = prepareRestart ? 'restart' : 'install';
|
|
1503
1630
|
if (updateOperation && updateOperationKind !== kind) throw new Error('Another update operation is in progress. Retry after it finishes.');
|
|
1504
1631
|
if (updateOperation) return updateOperation;
|
|
@@ -1596,6 +1723,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1596
1723
|
await publish(); return;
|
|
1597
1724
|
}
|
|
1598
1725
|
if (action === "quit") {
|
|
1726
|
+
if (claudeOwner.busy()) throw new Error('Stop the connected Claude turn or answer its permission request before quitting.');
|
|
1599
1727
|
if (command.forUpdate === true) {
|
|
1600
1728
|
if (command.force !== true) throw new Error('Confirm before stopping unfinished tasks.');
|
|
1601
1729
|
await stopWorkForUpdate();
|
|
@@ -1850,7 +1978,23 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1850
1978
|
await authorizeProvider(command.provider, commandScope, commandProviderLink?.epoch);
|
|
1851
1979
|
const cached = lastStatus?.sessionSupervision?.sessions?.find(item =>
|
|
1852
1980
|
item.provider === command.provider && item.session === command.session && (!item.hostId || item.hostId === 'local'));
|
|
1853
|
-
|
|
1981
|
+
let record = await readRecord(deliveryRoot, command.provider, command.session);
|
|
1982
|
+
if (guidanceAllowed(record)) {
|
|
1983
|
+
const observation = record.run.managed.nativeConnectionObservation;
|
|
1984
|
+
const age = Date.now() - observation?.at;
|
|
1985
|
+
const key = JSON.stringify([commandScope, guidanceTarget(record)]);
|
|
1986
|
+
let probe = guidanceProbes.get(key);
|
|
1987
|
+
if (!probe && !(age >= 0 && age < 15_000) && guidanceProbes.size < 2) {
|
|
1988
|
+
const authorize = () => authorizeProvider(command.provider, commandScope, commandProviderLink?.epoch);
|
|
1989
|
+
const signal = AbortSignal.any([guidanceAbort.signal, AbortSignal.timeout(20_000)]);
|
|
1990
|
+
probe = (dependencies.observeSessionGuidanceConnection || observeSessionGuidanceConnection)({root: deliveryRoot,
|
|
1991
|
+
record, signal, authorize}).finally(() => guidanceProbes.delete(key));
|
|
1992
|
+
guidanceProbes.set(key, probe);
|
|
1993
|
+
}
|
|
1994
|
+
if (probe) await probe;
|
|
1995
|
+
record = await readRecord(deliveryRoot, command.provider, command.session);
|
|
1996
|
+
if (frameRecordScope === commandScope) frameRecords.set(`${command.provider}:${command.session}`, record);
|
|
1997
|
+
}
|
|
1854
1998
|
const observed = sessionStatus.sessions.find(item => item.provider === command.provider && item.session === command.session);
|
|
1855
1999
|
if (!record && !observed) throw new Error('This conversation status is unavailable; refresh its provider.');
|
|
1856
2000
|
// Read one durable task without scheduling a complete provider scan
|
|
@@ -1864,13 +2008,16 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1864
2008
|
const selectedRun = controls.runs?.find(run => run.provider === command.provider && run.session === command.session);
|
|
1865
2009
|
row.decisionRequest = sessionDecisionPresentation(row, selectedRun);
|
|
1866
2010
|
row.conversationPresentation = conversationPresentation(row, selectedRun, { rawRecord: record });
|
|
2011
|
+
const owner = claudeOwnerStates.get(command.session);
|
|
2012
|
+
if (owner?.scope === agent.getTeamThreadsScope?.()?.fenceKey) Object.assign(row, claudeStreamPresentation(row, owner));
|
|
1867
2013
|
await authorizeProvider(command.provider, commandScope, commandProviderLink?.epoch);
|
|
1868
2014
|
// The same app projection carries observation time and next action;
|
|
1869
2015
|
// saved consent and an active record cannot prove a provider send.
|
|
1870
2016
|
const { provider, session, title, autoContinue, autoContinueStatus, autoContinueReason,
|
|
1871
|
-
binding, workState, dispatchReadiness, conversationPresentation: presentation, decisionRequest } = row;
|
|
2017
|
+
binding, workState, dispatchReadiness, conversationPresentation: presentation, decisionRequest, claudeConnection, originalSessionTarget } = row;
|
|
1872
2018
|
emit({ type: 'result', id: command.id, ok: true, data: { provider, session, title,
|
|
1873
|
-
autoContinue, autoContinueStatus, autoContinueReason, binding, workState, dispatchReadiness, conversationPresentation: presentation, decisionRequest } });
|
|
2019
|
+
autoContinue, autoContinueStatus, autoContinueReason, binding, workState, dispatchReadiness, conversationPresentation: presentation, decisionRequest, claudeConnection, originalSessionTarget } });
|
|
2020
|
+
void publish().catch(() => {});
|
|
1874
2021
|
return;
|
|
1875
2022
|
}
|
|
1876
2023
|
if (action === 'session_read_context') {
|
|
@@ -2347,6 +2494,30 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
2347
2494
|
await (dependencies.monitorSession || monitorSession)({ root: join(root, "delivery-hooks"), provider: command.provider,
|
|
2348
2495
|
session: command.session, monitor: action === "session_monitor", expectedProviderEpoch: commandProviderLink?.epoch });
|
|
2349
2496
|
await refreshSessions(true);
|
|
2497
|
+
} else if (['claude_connection_create', 'claude_connection_connect', 'claude_connection_disconnect', 'claude_connection_stop', 'claude_permission_decide'].includes(action)) {
|
|
2498
|
+
if (command.hostId && command.hostId !== 'local' || command.provider !== 'claude') throw new Error('Connected Claude requires this local host.');
|
|
2499
|
+
await authorizeProvider('claude', commandScope, commandProviderLink?.epoch);
|
|
2500
|
+
const data = action === 'claude_connection_create' ? await claudeOwner.create({ cwd: command.cwd, model: command.model,
|
|
2501
|
+
permissionMode: command.permissionMode, confirmedNew: command.confirmedNew }) : action === 'claude_connection_connect' ? await claudeOwner.connect(command)
|
|
2502
|
+
: action === 'claude_connection_disconnect' ? await claudeOwner.disconnect(command) : action === 'claude_connection_stop' ? await claudeOwner.stop(command, { authorize: () => authorizeWork(commandScope) }) : await claudeOwner.decide(command);
|
|
2503
|
+
claudeOwnerStates.set(data.session, data);
|
|
2504
|
+
emit({ type: 'result', id: command.id, ok: true, data }); await publish(); return;
|
|
2505
|
+
} else if (action === 'session_guidance' && command.provider === 'claude') {
|
|
2506
|
+
if (command.hostId && command.hostId !== 'local') throw new Error('Direct guidance requires the original local host.');
|
|
2507
|
+
await authorizeProvider('claude', commandScope, commandProviderLink?.epoch);
|
|
2508
|
+
const data = await claudeOwner.send(command);
|
|
2509
|
+
emit({ type: 'result', id: command.id, ok: true, data }); await publish(); return;
|
|
2510
|
+
} else if (action === 'session_guidance') {
|
|
2511
|
+
if (command.hostId && command.hostId !== 'local') throw new Error('Direct guidance requires the original local host.');
|
|
2512
|
+
const candidate = await selectedSession(command);
|
|
2513
|
+
if (candidate.provider !== 'codex' || candidate.isRunning !== false) throw new Error('The original conversation must be idle before sending guidance.');
|
|
2514
|
+
const data = await (dependencies.sendSessionGuidance || sendSessionGuidance)({root: deliveryRoot,
|
|
2515
|
+
provider: command.provider, session: command.session, requestId: command.requestId,
|
|
2516
|
+
expectedRunId: command.expectedRunId, taskId: command.taskId, expectedRevision: command.expectedRevision,
|
|
2517
|
+
expectedEpoch: command.expectedEpoch, text: command.text, signal: guidanceAbort.signal,
|
|
2518
|
+
authorize: () => authorizeProvider(candidate.provider, commandScope, selectedLinkEpochs.get(candidate))});
|
|
2519
|
+
emit({type: 'result', id: command.id, ok: true, data});
|
|
2520
|
+
await publish(); return;
|
|
2350
2521
|
} else if (action === "session_followup") {
|
|
2351
2522
|
const candidate = await selectedSession(command);
|
|
2352
2523
|
if (!candidate.followup?.previousRunId || command.previousRunId !== candidate.followup.previousRunId) {
|
|
@@ -2465,6 +2636,7 @@ function sessionActionError(error, action) {
|
|
|
2465
2636
|
if (/workspace changed|metadata.*unavailable|session.*changed/i.test(message)) return "The conversation identity or workspace changed. Refresh and select it again.";
|
|
2466
2637
|
if (/objective|acceptance|autonomy policy|budget/i.test(message)) return message.replace(/[\r\n\x00-\x1f]/g, " ").slice(0, 240);
|
|
2467
2638
|
if (/question changed|answer is empty|session understanding|context/i.test(message)) return message.replace(/[\r\n\x00-\x1f]/g, " ").slice(0, 240);
|
|
2639
|
+
if (action === 'session_guidance') return 'Guidance acceptance is unconfirmed. Your draft is kept. Check the original conversation before sending again.';
|
|
2468
2640
|
if (action === "session_answer") return "Answer acceptance is unconfirmed. Check the original conversation before sending again.";
|
|
2469
2641
|
return "This conversation could not be enrolled. Refresh its provider status and try again.";
|
|
2470
2642
|
}
|
|
@@ -30,7 +30,7 @@ function candidateMetadata(id, metadata, lastActivityAt, observedAt) {
|
|
|
30
30
|
provider: 'claude', session: id, ...(metadata.parentSessionId ? { parentSession: { provider: 'claude', session: metadata.parentSessionId } } : {}), cwd: metadata.cwd, title: metadata.title,
|
|
31
31
|
source: 'claude_cli', lastActivityAt, createdAt: metadata.createdAt ?? null,
|
|
32
32
|
observation: { source: 'claude_local_index', observedAt },
|
|
33
|
-
availability: 'unsupported', model: null,
|
|
33
|
+
availability: 'unsupported', model: null, observedModel: metadata.observedModel || null,
|
|
34
34
|
capabilities: { canMonitor: true, canEnroll: false, canAutoContinue: false },
|
|
35
35
|
reason: 'Claude session ownership and safe exact-session takeover are unverified; monitoring only.',
|
|
36
36
|
};
|
|
@@ -66,7 +66,7 @@ function metadataFromPrefix(bytes, id) {
|
|
|
66
66
|
const text = bytes.toString('utf8');
|
|
67
67
|
const complete = text.slice(0, text.lastIndexOf('\n') + 1);
|
|
68
68
|
if (!complete) return null;
|
|
69
|
-
let cwd = '', title = '', customTitle = '', parentSessionId = null, found = false, createdAt = null;
|
|
69
|
+
let cwd = '', title = '', customTitle = '', parentSessionId = null, found = false, createdAt = null, observedModel = null;
|
|
70
70
|
for (const line of complete.split('\n').slice(0, MAX_LINES)) {
|
|
71
71
|
if (!line) continue;
|
|
72
72
|
let item;
|
|
@@ -83,10 +83,11 @@ function metadataFromPrefix(bytes, id) {
|
|
|
83
83
|
if (!cwd) cwd = resolve(item.cwd);
|
|
84
84
|
// A conversation can cd into a child directory without changing identity.
|
|
85
85
|
if (!within(cwd, resolve(item.cwd))) return null;
|
|
86
|
+
if (item.type === 'assistant' && /^claude-[a-zA-Z0-9._:-]{1,140}$/.test(item.message?.model || '')) observedModel = item.message.model;
|
|
86
87
|
if (!title) title = sessionTitle(item.slug);
|
|
87
88
|
found = true;
|
|
88
89
|
}
|
|
89
|
-
return found ? { cwd, title: customTitle || title, parentSessionId, createdAt } : null;
|
|
90
|
+
return found ? { cwd, title: customTitle || title, parentSessionId, createdAt, observedModel } : null;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
// /rename appends a custom-title envelope, often beyond the bounded prefix.
|
|
@@ -363,7 +364,7 @@ async function humanBoundaryFromHandle(handle, opened, path, session, cwd, reque
|
|
|
363
364
|
if (cursor && (cursor.path !== path || cursor.session !== session || cursor.cwd !== cwd ||
|
|
364
365
|
!matchesHumanCursorIdentity(cursor, initial) || !Number.isSafeInteger(cursor.size) ||
|
|
365
366
|
cursor.size <= 0 || opened.size < cursor.size || !/^[a-f0-9]{64}$/.test(cursor.prefixSHA256 || ''))) return null;
|
|
366
|
-
const prefixHash = createHash('sha256'), userIds = new Set();
|
|
367
|
+
const prefixHash = createHash('sha256'), userIds = new Set(), appendedUserIds = new Set();
|
|
367
368
|
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
368
369
|
let latest = null, scanned;
|
|
369
370
|
try {
|
|
@@ -383,7 +384,7 @@ async function humanBoundaryFromHandle(handle, opened, path, session, cwd, reque
|
|
|
383
384
|
if (item.message?.content?.every?.(part => part?.type === 'tool_result')) return;
|
|
384
385
|
if (!UUID.test(item.uuid || '') || userIds.has(item.uuid)) return false;
|
|
385
386
|
userIds.add(item.uuid);
|
|
386
|
-
if (!cursor || start >= cursor.size) latest = item;
|
|
387
|
+
if (!cursor || start >= cursor.size) { latest = item; appendedUserIds.add(item.uuid); }
|
|
387
388
|
} });
|
|
388
389
|
} catch (error) { if (signal?.aborted) throw error; return null; }
|
|
389
390
|
if (!scanned || cursor && prefixHash.digest('hex') !== cursor.prefixSHA256) return null;
|
|
@@ -393,7 +394,7 @@ async function humanBoundaryFromHandle(handle, opened, path, session, cwd, reque
|
|
|
393
394
|
humanFileFingerprint(initial) !== humanFileFingerprint(pathAfter)) return null;
|
|
394
395
|
if (request.snapshot) return { version: 2, session, cwd, path, dev: String(initial.dev), ino: String(initial.ino),
|
|
395
396
|
birthtimeNs: String(initial.birthtimeNs), size: opened.size, prefixSHA256: scanned.digest };
|
|
396
|
-
if (request.validateCursor) return cursor ? { valid: true } : null;
|
|
397
|
+
if (request.validateCursor) return cursor && (!request.streamOwner || [...appendedUserIds].every(id => id === request.allowedNewUserRef)) ? { valid: true } : null;
|
|
397
398
|
if (!cursor || !latest || latest.origin?.kind !== 'human' || latest.cwd !== cwd || latest.message?.role !== 'user') return null;
|
|
398
399
|
const at = Date.parse(latest.timestamp), text = textContent(latest.message.content);
|
|
399
400
|
if (!Number.isFinite(at) || at < request.notBefore || at > request.now ||
|
|
@@ -413,6 +414,12 @@ export async function snapshotClaudeHumanCursor({ session, cwd }, options) {
|
|
|
413
414
|
export async function validateClaudeHumanCursor(cursor, options) {
|
|
414
415
|
return !!cursor && !!await exactHumanTranscript(cursor.session, cursor.cwd, { cursor, validateCursor: true }, options);
|
|
415
416
|
}
|
|
417
|
+
// An owned stream may append only its currently acknowledged native user UUID.
|
|
418
|
+
// Assistant/progress metadata is allowed; another human writer invalidates it.
|
|
419
|
+
export async function validateClaudeStreamCursor(cursor, allowedNewUserRef = null, options) {
|
|
420
|
+
return !!cursor && !!await exactHumanTranscript(cursor.session, cursor.cwd,
|
|
421
|
+
{ cursor, validateCursor: true, streamOwner: true, allowedNewUserRef }, options);
|
|
422
|
+
}
|
|
416
423
|
export async function readClaudeHumanBoundary({ session, cwd, promptSHA256, promptBytes, notBefore, cursor, now = Date.now() }, options) {
|
|
417
424
|
if (!/^[a-f0-9]{64}$/.test(promptSHA256 || '') || !Number.isSafeInteger(promptBytes) || promptBytes <= 0 ||
|
|
418
425
|
!Number.isFinite(notBefore) || !Number.isFinite(now) || notBefore > now || !cursor) return null;
|
|
@@ -526,7 +533,7 @@ async function scanNativeRows(path, visit, { signal, expectedDigest } = {}) {
|
|
|
526
533
|
if (!before.isFile()) throw new Error('Native transcript unavailable');
|
|
527
534
|
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
528
535
|
const scanned = await scanTranscriptLines(handle, before, { maxBytes: MAX_COMPLETE_TRANSCRIPT_BYTES, signal,
|
|
529
|
-
onLine: line => visit(JSON.parse(decoder.decode(line))) });
|
|
536
|
+
onLine: (line, _offset, index) => visit(JSON.parse(decoder.decode(line)), index) });
|
|
530
537
|
const after = await handle.stat();
|
|
531
538
|
const pathAfter = await lstat(path);
|
|
532
539
|
if (await realpath(path) !== path || pathAfter.isSymbolicLink() || pathAfter.ino !== before.ino || pathAfter.dev !== before.dev ||
|
|
@@ -542,10 +549,10 @@ async function nativeParentTranscript(candidate, { home, signal }, visit) {
|
|
|
542
549
|
.filter(row => row.cwd === candidate.cwd && row.boundary?.complete);
|
|
543
550
|
if (matches.length !== 1) return null;
|
|
544
551
|
const parentPath = matches[0].transcriptPath;
|
|
545
|
-
const scanned = await scanNativeRows(parentPath, row => {
|
|
552
|
+
const scanned = await scanNativeRows(parentPath, (row, index) => {
|
|
546
553
|
if (row.sessionId !== undefined && row.sessionId !== candidate.session ||
|
|
547
554
|
['user', 'assistant'].includes(row.type) && (row.isSidechain === true || row.cwd !== candidate.cwd)) return false;
|
|
548
|
-
return visit(row);
|
|
555
|
+
return visit(row, index);
|
|
549
556
|
}, { signal, expectedDigest: matches[0].boundary.digest });
|
|
550
557
|
return { parentPath, ...scanned };
|
|
551
558
|
}
|
|
@@ -561,23 +568,27 @@ export async function readClaudeNativeAgentContract(candidate, { home, signal }
|
|
|
561
568
|
export async function readClaudeNativeReview(candidate, request, { home, signal } = {}) {
|
|
562
569
|
try {
|
|
563
570
|
const calls = [], contracts = createForegroundAgentContractCollector(candidate);
|
|
564
|
-
const parent = await nativeParentTranscript(candidate, { home, signal }, row => {
|
|
571
|
+
const parent = await nativeParentTranscript(candidate, { home, signal }, (row, index) => {
|
|
565
572
|
if (row.sessionId === candidate.session && !row.isSidechain && row.type === 'assistant' && Date.parse(row.timestamp) >= request.requestedAt) {
|
|
566
573
|
for (const part of row.message?.content || []) {
|
|
567
574
|
if (part.type === 'tool_use' && ['Agent', 'Task'].includes(part.name) && part.input?.prompt === request.prompt)
|
|
568
|
-
calls.push({ call: part, contract: contracts.finish() });
|
|
575
|
+
calls.push({ call: part, index, contract: contracts.finish() });
|
|
569
576
|
if (calls.length > 1) return false;
|
|
570
577
|
}
|
|
571
578
|
}
|
|
572
579
|
contracts.add(row);
|
|
573
580
|
});
|
|
574
581
|
if (!parent || calls.length !== 1) return null;
|
|
575
|
-
const { parentPath } = parent, { call, contract } = calls[0];
|
|
582
|
+
const { parentPath } = parent, { call, index, contract } = calls[0];
|
|
576
583
|
if (call.input.subagent_type !== request.agentType || !isForegroundNativeReviewInput(call.name, call.input, contract?.schema)) return null;
|
|
577
584
|
const receipts = [];
|
|
578
|
-
const receiptScan = await scanNativeRows(parentPath, row => {
|
|
585
|
+
const receiptScan = await scanNativeRows(parentPath, (row, receiptIndex) => {
|
|
579
586
|
if (row.sessionId === candidate.session && !row.isSidechain && row.type === 'user' &&
|
|
580
|
-
row.message?.content?.some?.(part => part.type === 'tool_result' && part.tool_use_id === call.id) && row.toolUseResult)
|
|
587
|
+
row.message?.content?.some?.(part => part.type === 'tool_result' && part.tool_use_id === call.id) && row.toolUseResult) {
|
|
588
|
+
// A result preceding its exact invocation cannot prove it returned.
|
|
589
|
+
if (receiptIndex <= index) return false;
|
|
590
|
+
receipts.push(row);
|
|
591
|
+
}
|
|
581
592
|
if (receipts.length > 1) return false;
|
|
582
593
|
}, { signal, expectedDigest: parent.digest });
|
|
583
594
|
if (receiptScan.snapshot !== parent.snapshot) return null;
|