@awak-app/simy-cli 0.6.19 → 0.6.21
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 +21 -0
- package/package.json +1 -1
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +26 -7
- package/src/desktop-host.js +24 -22
- package/src/provider-links.js +10 -4
- package/src/session-supervision/analysis-manager.js +1 -1
- package/src/session-supervision/claude-sessions.js +3 -2
- package/src/session-supervision/index.js +5 -3
package/README.md
CHANGED
|
@@ -107,6 +107,27 @@ run retains the legacy metadata endpoint
|
|
|
107
107
|
no longer impose local limits and changing them never resets usage or resumes work. Both routes require the original authorized
|
|
108
108
|
account, organization and device throughout the request.
|
|
109
109
|
|
|
110
|
+
## Provider reconnection and saved consent
|
|
111
|
+
|
|
112
|
+
Disconnecting an AI provider revokes its conversation monitoring and automatic
|
|
113
|
+
continuation with a durable connection epoch. Reconnecting restores discovery;
|
|
114
|
+
previous work remains paused until explicitly selected again. Old or damaged
|
|
115
|
+
conversation locks do not block this operation and are never deleted by it.
|
|
116
|
+
Individual locked conversations may still require support before they can be
|
|
117
|
+
edited or continued. Provider login is separate: for a signed-out Codex CLI,
|
|
118
|
+
run `codex login` in Terminal and refresh connections.
|
|
119
|
+
|
|
120
|
+
After the first connection change, the saved settings deliberately appear
|
|
121
|
+
**disconnected to older CLI/plugin versions** that cannot enforce the consent
|
|
122
|
+
epoch. Update the runtime and installed SIMY plugins together and start a fresh
|
|
123
|
+
provider session before using their hooks. Do not edit the saved `connected`
|
|
124
|
+
flag to bypass this fence. A rollback to older code leaves those integrations
|
|
125
|
+
disconnected; pause work and use a compatible version to restore the connection.
|
|
126
|
+
Connection failures write only provider, operation, and a bounded error code to
|
|
127
|
+
the private `logs/desktop-host.log`; they do not include credentials or conversation
|
|
128
|
+
content. Verify reconnect, fresh explicit consent, and that previous work remains
|
|
129
|
+
paused when validating a rollout. This does not prove provider model access.
|
|
130
|
+
|
|
110
131
|
## Apple Silicon Mac app
|
|
111
132
|
|
|
112
133
|
The macOS installer includes a native status window and menu bar app. Open
|
package/package.json
CHANGED
package/src/cli-contract.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./durable-local-task-steps-contract.js";
|
|
14
14
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
15
15
|
|
|
16
|
-
export const CLI_VERSION = "0.6.
|
|
16
|
+
export const CLI_VERSION = "0.6.21";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
|
@@ -37,11 +37,29 @@ export async function providerLinkState(root, provider) {
|
|
|
37
37
|
const valid = settings?.schemaVersion === 1 && ['codex', 'claude'].every(key => {
|
|
38
38
|
const value = settings.providers?.[key];
|
|
39
39
|
return value && typeof value.connected === 'boolean' && typeof value.requiresConsent === 'boolean' &&
|
|
40
|
-
(value.epoch === null || typeof value.epoch === 'string' && value.epoch.length > 0 && value.epoch.length <= 128)
|
|
40
|
+
(value.epoch === null || typeof value.epoch === 'string' && value.epoch.length > 0 && value.epoch.length <= 128) &&
|
|
41
|
+
(value.consentEpoch === undefined || value.consentEpoch === value.epoch && Boolean(value.consentEpoch) && value.connected === false && typeof value.enabled === 'boolean');
|
|
41
42
|
});
|
|
42
|
-
|
|
43
|
+
const value = valid ? settings.providers[provider] : null;
|
|
44
|
+
return value ? { ...value, connected: value.consentEpoch ? value.enabled : value.connected } : { connected: false, epoch: null };
|
|
43
45
|
} catch (error) { return { connected: error.code === 'ENOENT', epoch: null }; }
|
|
44
46
|
}
|
|
47
|
+
// Revocation is durable before touching any conversation lock. Project it on every
|
|
48
|
+
// read (including cached records) and under the write lock, so stale consent never
|
|
49
|
+
// returns after reconnect, restart, or a late writer from the previous epoch.
|
|
50
|
+
function applyProviderConsent(record, link) {
|
|
51
|
+
if (!record || !link.consentEpoch || record.providerConsentEpoch === link.consentEpoch) return record;
|
|
52
|
+
return { ...record, providerConsentEpoch: link.consentEpoch,
|
|
53
|
+
autoContinue: false, autoContinueEpoch: null, tracked: false, monitor: false,
|
|
54
|
+
...(record.run && !TERMINAL.has(record.run.state) ? { run: { ...record.run,
|
|
55
|
+
state: 'paused', reason: 'SIMY connection changed. Select this conversation again before continuing.',
|
|
56
|
+
revision: (record.run.revision || 0) + 1,
|
|
57
|
+
...(record.run.managed ? { managed: { ...record.run.managed, autoRecovery: false } } : {}) } } : {}) };
|
|
58
|
+
}
|
|
59
|
+
async function currentProviderConsent(root, records) {
|
|
60
|
+
const links = Object.fromEntries(await Promise.all(['codex', 'claude'].map(async provider => [provider, await providerLinkState(root, provider)])));
|
|
61
|
+
return records.map(record => applyProviderConsent(record, links[record.provider] || {}));
|
|
62
|
+
}
|
|
45
63
|
export async function linkedProvider(root, provider) {
|
|
46
64
|
return (await providerLinkState(root, provider)).connected === true;
|
|
47
65
|
}
|
|
@@ -158,8 +176,9 @@ export async function updateRecord(root, provider, session, mutate, { inspectPro
|
|
|
158
176
|
const lockToken = await acquireStateLock(lock, inspectProcess);
|
|
159
177
|
let temporary;
|
|
160
178
|
try {
|
|
161
|
-
const
|
|
162
|
-
const
|
|
179
|
+
const stored = await read(file);
|
|
180
|
+
const previous = applyProviderConsent(stored, await providerLinkState(root, provider));
|
|
181
|
+
const previousBytes = stored === null ? null : JSON.stringify(stored);
|
|
163
182
|
const next = await mutate(previous);
|
|
164
183
|
if (next) {
|
|
165
184
|
// Older file_equals drafts used `path`, which an older verifier could
|
|
@@ -184,7 +203,7 @@ export async function readRecord(root = rootDirectory(), provider, session) {
|
|
|
184
203
|
const path = recordPath(root, provider, session);
|
|
185
204
|
try {
|
|
186
205
|
const record = await read(path);
|
|
187
|
-
return record?.schemaVersion === 1 && record.provider === provider && record.session === session ? record : null;
|
|
206
|
+
return record?.schemaVersion === 1 && record.provider === provider && record.session === session ? applyProviderConsent(record, await providerLinkState(root, provider)) : null;
|
|
188
207
|
} catch { return null; } // Match listRecords' treatment of unreadable records.
|
|
189
208
|
}
|
|
190
209
|
const RECORD_NAME = /^[a-f0-9]{64}\.json$/;
|
|
@@ -209,7 +228,7 @@ async function readNamedRecord(root, name) {
|
|
|
209
228
|
} catch { return null; }
|
|
210
229
|
}
|
|
211
230
|
export async function listRecords(root = rootDirectory()) {
|
|
212
|
-
return mapRecordNames(await recordNames(root), name => readNamedRecord(root, name));
|
|
231
|
+
return currentProviderConsent(root, await mapRecordNames(await recordNames(root), name => readNamedRecord(root, name)));
|
|
213
232
|
}
|
|
214
233
|
|
|
215
234
|
// A shared candidate/display index, never mutation authority. Every list checks
|
|
@@ -244,7 +263,7 @@ export function createRecordReader(root = rootDirectory()) {
|
|
|
244
263
|
if (!inFlight) inFlight = scan().finally(() => { inFlight = null; });
|
|
245
264
|
// Consumers can decorate/mutate their own snapshots without corrupting the
|
|
246
265
|
// cached records or a simultaneous caller's view.
|
|
247
|
-
return structuredClone(await inFlight);
|
|
266
|
+
return currentProviderConsent(root, structuredClone(await inFlight));
|
|
248
267
|
},
|
|
249
268
|
async close() { closed = true; await inFlight?.catch(() => {}); cache.clear(); },
|
|
250
269
|
};
|
package/src/desktop-host.js
CHANGED
|
@@ -66,7 +66,12 @@ async function withReadCancellation(signal, work) {
|
|
|
66
66
|
function desktopActionError(action, error) {
|
|
67
67
|
if (error?.code === "provider_link_changed") return "SIMY connection changed. Refresh and select the conversation again.";
|
|
68
68
|
if (error?.code === "provider_link_disconnected") return "Reconnect this provider to SIMY before using it.";
|
|
69
|
-
if (["provider_connect", "provider_disconnect"].includes(action))
|
|
69
|
+
if (["provider_connect", "provider_disconnect"].includes(action)) {
|
|
70
|
+
if (error?.code === 'desktop_access_required') return "Sign in to your SIMY workspace, then try connecting again.";
|
|
71
|
+
if (['EACCES', 'EPERM', 'EROFS'].includes(error?.code)) return "SIMY could not save the connection settings. Check access to its local settings folder, then retry.";
|
|
72
|
+
if (error?.code === 'ENOSPC') return "SIMY could not save the connection settings. Free disk space, then retry.";
|
|
73
|
+
return "SIMY could not change the connection. Reopen SIMY and retry. If this continues, share its diagnostic log with support.";
|
|
74
|
+
}
|
|
70
75
|
if (action === "session_set_auto_continue") {
|
|
71
76
|
const messages = {
|
|
72
77
|
session_workspace_changed: "The conversation workspace changed. Refresh and select it again.",
|
|
@@ -297,10 +302,10 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
297
302
|
let organizationAnalysisDrain = Promise.resolve();
|
|
298
303
|
let organizationAnalysisEpoch = 0;
|
|
299
304
|
const authorizeWork = async expectedScope => {
|
|
300
|
-
if (closed || stoppingForUpdate || updater?.acceptsNewWork?.() === false || organizationAnalysesSuspended) throw new Error("SIMY is stopping. Reopen SIMY before starting new work.");
|
|
305
|
+
if (closed || stoppingForUpdate || updater?.acceptsNewWork?.() === false || organizationAnalysesSuspended) throw Object.assign(new Error("SIMY is stopping. Reopen SIMY before starting new work."), { code: "desktop_stopping" });
|
|
301
306
|
if (agent?.requireDesktopAccess) await agent.requireDesktopAccess();
|
|
302
307
|
const access = await accessSnapshot();
|
|
303
|
-
if (closed || stoppingForUpdate || updater?.acceptsNewWork?.() === false || organizationAnalysesSuspended) throw new Error("SIMY is stopping. Reopen SIMY before starting new work.");
|
|
308
|
+
if (closed || stoppingForUpdate || updater?.acceptsNewWork?.() === false || organizationAnalysesSuspended) throw Object.assign(new Error("SIMY is stopping. Reopen SIMY before starting new work."), { code: "desktop_stopping" });
|
|
304
309
|
requireDesktopAccess(access);
|
|
305
310
|
const scope = accessScope(access);
|
|
306
311
|
if (expectedScope !== undefined && expectedScope !== scope) throw Object.assign(new Error("Desktop account or organization changed."), { code: "desktop_access_required" });
|
|
@@ -406,29 +411,15 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
406
411
|
selectedLinkEpochs.set(selected, link.epoch);
|
|
407
412
|
return selected;
|
|
408
413
|
};
|
|
409
|
-
const revokeProviderConsent = async provider => {
|
|
410
|
-
const records = (await recordReader.list()).filter(record => record.provider === provider);
|
|
411
|
-
for (const record of records) {
|
|
412
|
-
automaticContinuation?.revoke(provider, record.session);
|
|
413
|
-
await updateRecord(deliveryRoot, provider, record.session, previous => {
|
|
414
|
-
if (!previous) return previous;
|
|
415
|
-
const next = { ...previous, autoContinue: false, autoContinueEpoch: randomUUID(), tracked: false, monitor: false };
|
|
416
|
-
if (previous.run && !TERMINAL.has(previous.run.state)) {
|
|
417
|
-
next.run = { ...previous.run, state: "paused", reason: "Provider disconnected from SIMY.",
|
|
418
|
-
revision: (previous.run.revision || 0) + 1, updatedAt: Date.now(),
|
|
419
|
-
...(previous.run.managed ? { managed: { ...previous.run.managed, autoRecovery: false } } : {}) };
|
|
420
|
-
}
|
|
421
|
-
return next;
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
414
|
const changeProviderLink = async (provider, connected) => {
|
|
426
415
|
if (!PROVIDER_LINKS.includes(provider)) throw new Error("Unsupported SIMY provider connection.");
|
|
427
416
|
if (providerLinkBusy.has(provider)) throw new Error("SIMY connection is already changing.");
|
|
428
417
|
providerLinkBusy.add(provider);
|
|
429
418
|
providerLinkFeedback[provider] = { state: connected ? "connecting" : "disconnecting" };
|
|
419
|
+
let phase = "read_settings";
|
|
430
420
|
try {
|
|
431
421
|
providerLinks = await readProviderLinks(root);
|
|
422
|
+
phase = "save_disconnect";
|
|
432
423
|
if (!connected) providerLinks = await writeProviderLink(root, provider, false);
|
|
433
424
|
if (activeStatisticsRead?.key.startsWith(`${provider}:`)) activeStatisticsRead.controller.abort();
|
|
434
425
|
discoveryController?.abort();
|
|
@@ -437,13 +428,16 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
437
428
|
for (const key of explicitAnalyses) if (key.startsWith(`${provider}:`)) explicitAnalyses.delete(key);
|
|
438
429
|
statisticsCache.clear();
|
|
439
430
|
if (!connected) sessionStatus = { ...sessionStatus, sessions: linkedSessions(sessionStatus.sessions) };
|
|
431
|
+
phase = "cancel_analysis";
|
|
440
432
|
await analysisManager.update(analysisSessions(sessionStatus.sessions));
|
|
433
|
+
phase = "publish_status";
|
|
441
434
|
await publish();
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
if (!connected || !providerLinks[provider].connected) await revokeProviderConsent(provider);
|
|
435
|
+
// The provider epoch revokes old consent atomically, independent of stale
|
|
436
|
+
// conversation locks. Record readers and writers enforce that boundary.
|
|
445
437
|
if (connected && !providerLinks[provider].connected) {
|
|
438
|
+
phase = "authorize_connect";
|
|
446
439
|
await authorizeWork();
|
|
440
|
+
phase = "save_connect";
|
|
447
441
|
providerLinks = await writeProviderLink(root, provider, true);
|
|
448
442
|
}
|
|
449
443
|
delete providerLinkFeedback[provider];
|
|
@@ -451,6 +445,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
451
445
|
catch { providerLinkFeedback[provider] = { message: "SIMY connection saved. Provider status could not be verified; refresh connections to check it." }; }
|
|
452
446
|
refreshSessions(true);
|
|
453
447
|
} catch (error) {
|
|
448
|
+
diagnostics.write(`provider connection phase provider=${provider} phase=${phase}`);
|
|
454
449
|
providerLinkFeedback[provider] = { state: "error", message: desktopActionError(connected ? "provider_connect" : "provider_disconnect", error) };
|
|
455
450
|
throw error;
|
|
456
451
|
} finally { providerLinkBusy.delete(provider); await publish(); }
|
|
@@ -1327,6 +1322,13 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1327
1322
|
emit({ type: "result", id: command.id, ok: true });
|
|
1328
1323
|
await publish();
|
|
1329
1324
|
} catch (error) {
|
|
1325
|
+
if (["provider_connect", "provider_disconnect"].includes(command.action)) {
|
|
1326
|
+
const code = ['desktop_access_required', 'desktop_stopping', 'EACCES', 'EPERM', 'ENOSPC', 'EROFS', 'EBUSY', 'ENOENT', 'ERR_INVALID_ARG_TYPE'].includes(error?.code) ? error.code : 'provider_connection_failed';
|
|
1327
|
+
const provider = PROVIDER_LINKS.includes(command.provider) ? command.provider : 'unsupported';
|
|
1328
|
+
const kind = ['Error', 'TypeError', 'RangeError', 'SyntaxError'].includes(error?.name) ? error.name : 'Error';
|
|
1329
|
+
const locations = [...String(error?.stack || '').matchAll(/(?:desktop-host|agent|provider-links|delivery-loop|analysis-manager)\.m?js:\d{1,6}:\d{1,6}/g)].slice(0, 3).map(match => match[0]).join(',') || 'unknown';
|
|
1330
|
+
diagnostics.write(`provider connection failed provider=${provider} operation=${command.action} code=${code} kind=${kind} locations=${locations}`);
|
|
1331
|
+
}
|
|
1330
1332
|
emit({ type: "result", id: command.id, ok: false, ...(error?.code === 'cancelled' || statisticsRequest?.controller.signal.aborted ? { code: 'cancelled' } : {}), message: ["update", "prepare_update_restart"].includes(command.action) && updateProgress ? updateProgress.message : desktopActionError(command.action, error) });
|
|
1331
1333
|
} finally {
|
|
1332
1334
|
// Access rejection can happen before the shared reader is constructed.
|
package/src/provider-links.js
CHANGED
|
@@ -17,10 +17,12 @@ async function readSettings(root) {
|
|
|
17
17
|
if (payload?.schemaVersion !== 1 || !payload.providers || PROVIDER_LINKS.some(provider => {
|
|
18
18
|
const value = payload.providers[provider];
|
|
19
19
|
return !value || typeof value.connected !== 'boolean' || typeof value.requiresConsent !== 'boolean' ||
|
|
20
|
-
value.epoch !== null && (typeof value.epoch !== 'string' || !value.epoch || value.epoch.length > 128)
|
|
20
|
+
value.epoch !== null && (typeof value.epoch !== 'string' || !value.epoch || value.epoch.length > 128) ||
|
|
21
|
+
value.consentEpoch !== undefined && (value.consentEpoch !== value.epoch || !value.consentEpoch || value.connected !== false || typeof value.enabled !== 'boolean');
|
|
21
22
|
})) throw new Error('Invalid SIMY provider connection settings.');
|
|
22
23
|
return Object.fromEntries(PROVIDER_LINKS.map(provider => [provider, {
|
|
23
|
-
connected: payload.providers[provider].connected,
|
|
24
|
+
connected: payload.providers[provider].consentEpoch ? payload.providers[provider].enabled : payload.providers[provider].connected,
|
|
25
|
+
...(payload.providers[provider].consentEpoch ? { consentEpoch: payload.providers[provider].consentEpoch } : {}),
|
|
24
26
|
epoch: payload.providers[provider].epoch,
|
|
25
27
|
requiresConsent: payload.providers[provider].requiresConsent,
|
|
26
28
|
}]));
|
|
@@ -59,11 +61,15 @@ export function writeProviderLink(root = defaultRoot(), provider, connected) {
|
|
|
59
61
|
const providers = await readSettings(root);
|
|
60
62
|
// Reconnection restores discovery only. Session execution consent is cleared
|
|
61
63
|
// separately and must be selected again, including after a host restart.
|
|
62
|
-
|
|
64
|
+
const epoch = randomUUID();
|
|
65
|
+
providers[provider] = { connected, epoch, consentEpoch: epoch, requiresConsent: true };
|
|
66
|
+
// Old plugins must remain disconnected: they cannot enforce epoch revocation.
|
|
67
|
+
const saved = Object.fromEntries(Object.entries(providers).map(([key, value]) => [key,
|
|
68
|
+
value.consentEpoch ? { ...value, connected: false, enabled: value.connected } : value]));
|
|
63
69
|
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
64
70
|
const target = join(root, 'provider-links.json'), temporary = `${target}.${randomUUID()}.tmp`;
|
|
65
71
|
try {
|
|
66
|
-
await writeFile(temporary, `${JSON.stringify({ schemaVersion: 1, providers })}\n`, { mode: 0o600 });
|
|
72
|
+
await writeFile(temporary, `${JSON.stringify({ schemaVersion: 1, providers: saved })}\n`, { mode: 0o600 });
|
|
67
73
|
await rename(temporary, target);
|
|
68
74
|
} finally { await rm(temporary, { force: true }); }
|
|
69
75
|
return providers;
|
|
@@ -307,7 +307,7 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
|
|
|
307
307
|
candidate = observedCandidate(candidate);
|
|
308
308
|
const revision = continuationAnalysisRevision(candidate), entry = entries.get(keyOf(candidate));
|
|
309
309
|
let analysis = entry?.revision === revision ? { ...entry.result, revision, requestId: entry.requestId } : { state: 'unanalysed', revision };
|
|
310
|
-
const previous = entry
|
|
310
|
+
const previous = entry && entry.cwd === candidate.cwd ? entry.settledResult : null;
|
|
311
311
|
if (candidate.isRunning === true || analysis.awaitingTurn === true) {
|
|
312
312
|
analysis = { state: 'waiting', awaitingTurn: true, revision,
|
|
313
313
|
reason: 'This turn is still running. SIMY analyzes only finished turns and will update automatically when it ends.',
|
|
@@ -72,7 +72,8 @@ function metadataFromPrefix(bytes, id) {
|
|
|
72
72
|
if (item.isSidechain === true) return null;
|
|
73
73
|
if (typeof item.cwd !== 'string' || !isAbsolute(item.cwd)) continue;
|
|
74
74
|
if (!cwd) cwd = resolve(item.cwd);
|
|
75
|
-
|
|
75
|
+
// A conversation can cd into a child directory without changing identity.
|
|
76
|
+
if (!within(cwd, resolve(item.cwd))) return null;
|
|
76
77
|
if (!title) title = sessionTitle(item.slug);
|
|
77
78
|
found = true;
|
|
78
79
|
}
|
|
@@ -233,7 +234,7 @@ async function conversationFromHandle(handle, opened, session, cwd) {
|
|
|
233
234
|
if (unscopedFileHistory(item)) continue;
|
|
234
235
|
if (item.sessionId !== session || item.isSidechain === true) { incomplete = true; continue; }
|
|
235
236
|
if (!['user', 'assistant'].includes(item.type) || item.isMeta === true) continue;
|
|
236
|
-
if (typeof item.cwd !== 'string' ||
|
|
237
|
+
if (typeof item.cwd !== 'string' || !isAbsolute(item.cwd) || !within(cwd, resolve(item.cwd))) { incomplete = true; continue; }
|
|
237
238
|
const text = textContent(item.message?.content);
|
|
238
239
|
// User tool_result envelopes are not a new human request. A missing
|
|
239
240
|
// human message/attachment-only request cannot silently reuse an old goal.
|
|
@@ -316,7 +316,7 @@ export async function setSessionAutoContinue({ root, provider, session, enabled,
|
|
|
316
316
|
...(continuationPolicy ?? savedPolicy) === undefined ? {} : { continuationPolicy: continuationPolicy ?? savedPolicy } };
|
|
317
317
|
if (settings.continuationPolicy !== undefined && !['bounded', 'progress'].includes(settings.continuationPolicy)) throw new Error('Choose a continuation policy.');
|
|
318
318
|
if (previous.run && continuationPolicy !== undefined) throw new Error('Change an existing task through its explicit continuation controls.');
|
|
319
|
-
const next = { ...previous, autoContinue: enabled, autoContinueEpoch: epoch,
|
|
319
|
+
const next = { ...previous, ...(enabled && link.consentEpoch ? { providerConsentEpoch: link.consentEpoch } : {}), autoContinue: enabled, autoContinueEpoch: epoch,
|
|
320
320
|
...(enabled ? { tracked: true, monitor: true, autoContinueSettings: settings } : { tracked: false, monitor: false }) };
|
|
321
321
|
// Adopt existing supervision without restarting it or renewing its budgets.
|
|
322
322
|
if (next.run?.managed) next.run.managed.autoContinueEpoch = epoch;
|
|
@@ -357,7 +357,8 @@ export async function trackSession({ root, provider, session, tracked = true, op
|
|
|
357
357
|
await requireProviderLink({ root, provider, expectedEpoch: link.epoch });
|
|
358
358
|
if (previous?.providerArchived === true) throw Object.assign(new Error('Restore the original provider conversation before enabling automatic continuation or tracking.'), { code: 'session_archived' });
|
|
359
359
|
if (previous && previous.cwd !== cwd) throw new Error('Session workspace changed; refresh its metadata.');
|
|
360
|
-
return { ...(previous || { schemaVersion: 1, provider, session, cwd, run: null }),
|
|
360
|
+
return { ...(previous || { schemaVersion: 1, provider, session, cwd, run: null }),
|
|
361
|
+
...(link.consentEpoch ? { providerConsentEpoch: link.consentEpoch } : {}), tracked: true, monitor: true,
|
|
361
362
|
admission: { ...(previous?.admission || {}), source: metadata.observation.source, provider, session, cwd, model: metadata.model, validatedAt: now } };
|
|
362
363
|
});
|
|
363
364
|
}
|
|
@@ -380,7 +381,8 @@ export async function monitorSession({ root, provider, session, monitor = true,
|
|
|
380
381
|
await requireProviderLink({ root, provider, expectedEpoch: link.epoch });
|
|
381
382
|
if (previous?.providerArchived === true) throw new Error('Restore the original provider conversation before enabling supervision.');
|
|
382
383
|
if (previous && previous.cwd !== cwd) throw new Error('Session workspace changed; refresh its metadata.');
|
|
383
|
-
return { ...(previous || { schemaVersion: 1, provider, session, cwd, run: null }),
|
|
384
|
+
return { ...(previous || { schemaVersion: 1, provider, session, cwd, run: null }),
|
|
385
|
+
...(link.consentEpoch ? { providerConsentEpoch: link.consentEpoch } : {}), monitor,
|
|
384
386
|
admission: { source: metadata.observation.source, provider, session, cwd, model: metadata.model, validatedAt: now } };
|
|
385
387
|
});
|
|
386
388
|
return metadata;
|