@awak-app/simy-cli 0.7.20 → 0.7.22
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 +73 -12
- package/package.json +2 -2
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +9 -1
- package/src/codex-work-pattern-sync.js +243 -43
- package/src/console/sessions.js +6 -6
- package/src/delivery-hooks/command.js +8 -6
- package/src/delivery-hooks/lifecycle-recovery.js +82 -0
- package/src/delivery-hooks/local-supervisor.js +25 -2
- package/src/delivery-hooks/supervisor-activation.js +25 -0
- package/src/delivery-hooks/supervisor-service.js +2 -2
- package/src/delivery-hooks/supervisor-watchdog.js +11 -6
- package/src/desktop-host.js +84 -11
- package/src/index.js +1 -1
- package/src/session-supervision/claude-sessions.js +7 -7
- package/src/session-supervision/codex-app-server.js +19 -9
- package/src/session-supervision/codex-peer-opinion.js +128 -0
- package/src/session-supervision/index.js +21 -12
- package/src/session-supervision/remote-control.js +27 -0
- package/src/session-supervision/remote-errors.js +22 -0
- package/src/session-supervision/remote-hosts.js +17 -13
- package/src/session-supervision/remote-session-list.js +56 -0
- package/src/session-supervision/session-list.js +83 -23
- package/src/sqm/agent-skill.js +4 -2
- package/src/sqm/provider-command.js +1 -1
- package/src/sqm/provider-plugin-repair.js +105 -0
- package/src/sqm/provider-plugin-status.js +20 -8
|
@@ -144,19 +144,20 @@ export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = p
|
|
|
144
144
|
output(JSON.stringify(result, null, 2)); return result.status === 'not_requested' ? 2 : 0;
|
|
145
145
|
}
|
|
146
146
|
if (['supervisor-enable', 'supervisor-disable', 'supervisor-watch', 'supervisor-status', 'supervisor-watchdog-install'].includes(args[0])) {
|
|
147
|
-
const options = parseDeliveryOptions(args.slice(1), new Set(['root']));
|
|
147
|
+
const options = parseDeliveryOptions(args.slice(1), new Set(['root', ...(args[0] === 'supervisor-enable' ? ['provider', 'session'] : [])]));
|
|
148
148
|
const supervisorRoot = options.root || dirname(root);
|
|
149
149
|
if (!isAbsolute(supervisorRoot)) throw new Error('Supervisor root must be absolute.');
|
|
150
150
|
if (args[0] === 'supervisor-watch') {
|
|
151
151
|
const controller = new AbortController(), interrupt = () => controller.abort('user_interrupt'), terminate = () => controller.abort('service_termination');
|
|
152
|
-
process.once('SIGINT', interrupt); process.once('SIGTERM', terminate);
|
|
152
|
+
process.once('SIGINT', interrupt); process.once('SIGTERM', terminate); process.once('SIGHUP', terminate);
|
|
153
153
|
try { await watchSupervisor({ root: supervisorRoot, signal: controller.signal }); }
|
|
154
|
-
finally { process.removeListener('SIGINT', interrupt); process.removeListener('SIGTERM', terminate); }
|
|
154
|
+
finally { process.removeListener('SIGINT', interrupt); process.removeListener('SIGTERM', terminate); process.removeListener('SIGHUP', terminate); }
|
|
155
155
|
return 0;
|
|
156
156
|
}
|
|
157
157
|
const result = args[0] === 'supervisor-status' ? await supervisorWatchdogStatus({ root: supervisorRoot })
|
|
158
158
|
: args[0] === 'supervisor-watchdog-install' ? await installSupervisorService({ root: supervisorRoot })
|
|
159
|
-
: await setSupervisorWatchdog({ root: supervisorRoot, enabled: args[0] === 'supervisor-enable', webOrigin
|
|
159
|
+
: await setSupervisorWatchdog({ root: supervisorRoot, enabled: args[0] === 'supervisor-enable', webOrigin,
|
|
160
|
+
...((options.provider || options.session) ? { target: { provider: options.provider, session: options.session } } : {}) });
|
|
160
161
|
output(JSON.stringify(result, null, 2)); return 0;
|
|
161
162
|
}
|
|
162
163
|
if (args[0] === 'policy-adopt') {
|
|
@@ -187,11 +188,12 @@ export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = p
|
|
|
187
188
|
return 0;
|
|
188
189
|
}
|
|
189
190
|
if (action === 'supervise') {
|
|
190
|
-
|
|
191
|
+
const once = args.includes('--once');
|
|
192
|
+
const options = parseDeliveryOptions(args.filter(arg => arg !== '--once'), new Set(['activation-epoch']));
|
|
191
193
|
const controller = new AbortController();
|
|
192
194
|
const stop = () => controller.abort();
|
|
193
195
|
process.once('SIGINT', stop); process.once('SIGTERM', stop);
|
|
194
|
-
try { await superviseLocalDelivery({ root, webOrigin, once:
|
|
196
|
+
try { await superviseLocalDelivery({ root, webOrigin, once, activationEpoch: options['activation-epoch'], signal: controller.signal }); }
|
|
195
197
|
finally { process.removeListener('SIGINT', stop); process.removeListener('SIGTERM', stop); }
|
|
196
198
|
return 0;
|
|
197
199
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { automaticContinuationAllowed, hasPendingUserStop, queuePurposeDiagnosis,
|
|
2
|
+
updateRecord, advanceDeliveryControlRevision } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
3
|
+
import { requireProviderLink } from '../provider-links.js';
|
|
4
|
+
import { openCodexAppServer, readCodexObservation } from '../session-supervision/codex-app-server.js';
|
|
5
|
+
|
|
6
|
+
function eligible(record) {
|
|
7
|
+
const run = record?.run, managed = run?.managed;
|
|
8
|
+
if (record?.provider !== 'codex' || record.autoContinue !== true || !automaticContinuationAllowed(record) ||
|
|
9
|
+
!managed?.purpose?.enabled || managed.resumeChannel !== 'codex_app_server' ||
|
|
10
|
+
!['active', 'recovery_required', 'blocked'].includes(run.state) || run.stopReason?.actor === 'user' ||
|
|
11
|
+
managed.resumeLease || managed.liveWorkspaceLease || managed.outcomeAudit?.lease ||
|
|
12
|
+
managed.nativeWait || managed.purpose.nativeBoundary || managed.providerWarning ||
|
|
13
|
+
managed.userQuestion || managed.understandingQuestion || managed.dependencyWait ||
|
|
14
|
+
managed.authenticationWait || managed.phase === 'understanding' || managed.usageBlocked) return false;
|
|
15
|
+
// A restart is never approval for an effect. Only a controller-generated
|
|
16
|
+
// local context-selection wait can yield to independent read diagnosis.
|
|
17
|
+
const pending = managed.pendingApproval;
|
|
18
|
+
if (pending && (pending.denied || pending.kind !== 'scope_change' || managed.decision?.assessment?.externalEffect !== 'local' ||
|
|
19
|
+
managed.decision.assessment.reversibility !== 'reversible' || managed.decision.assessment.inObjective !== true ||
|
|
20
|
+
managed.decision.assessment.userChoice !== false)) return false;
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Called once by the owning supervisor after a durable activation is loaded.
|
|
25
|
+
// It preserves the same run, purpose, budgets and effect history, and queues a
|
|
26
|
+
// read. An interrupted write is reconciled before any proposal to repeat it.
|
|
27
|
+
export async function reconcileLifecycleRecovery({ root, records, eventId, authorize = async () => {}, sessionEndedAt }) {
|
|
28
|
+
for (const record of records) {
|
|
29
|
+
if (!eligible(record) || hasPendingUserStop(root, record)) continue;
|
|
30
|
+
try {
|
|
31
|
+
const link = await requireProviderLink({ root, provider: record.provider });
|
|
32
|
+
await authorize(record);
|
|
33
|
+
await updateRecord(root, record.provider, record.session, async current => {
|
|
34
|
+
if (!eligible(current) || hasPendingUserStop(root, current) || current.run.id !== record.run.id ||
|
|
35
|
+
current.run.revision !== record.run.revision || current.run.controlRevision !== record.run.controlRevision ||
|
|
36
|
+
current.autoContinueEpoch !== record.autoContinueEpoch || current.run.managed.lifecycleRecovery?.eventId === eventId ||
|
|
37
|
+
sessionEndedAt !== undefined && !endedSession(current, sessionEndedAt)) return current;
|
|
38
|
+
await requireProviderLink({ root, provider: record.provider, expectedEpoch: link.epoch });
|
|
39
|
+
await authorize(current);
|
|
40
|
+
const run = current.run, managed = run.managed, previousState = run.state;
|
|
41
|
+
const pending = managed.pendingApproval ? structuredClone(managed.pendingApproval) : null;
|
|
42
|
+
const queued = queuePurposeDiagnosis(run,
|
|
43
|
+
'The provider session ended or the local execution host returned after an interruption. Read the saved task and actual current artifacts first. Keep the original goal, corrections, permission ceiling, budgets and explicit stops. Reconcile unfinished or uncertain effects before proposing more work; do not replay a prior write or treat recovery as approval. Inspect the current execution context and choose useful authorized work.',
|
|
44
|
+
`lifecycle:${eventId}`, Date.now(), 'interrupted_owner');
|
|
45
|
+
if (queued) {
|
|
46
|
+
managed.lifecycleRecovery = { eventId, at: Date.now(), previousState, mode: 'read_before_continue', retainedApproval: pending };
|
|
47
|
+
advanceDeliveryControlRevision(run);
|
|
48
|
+
run.updatedAt = Date.now();
|
|
49
|
+
}
|
|
50
|
+
return current;
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (['provider_link_disconnected', 'provider_link_changed'].includes(error.code) ||
|
|
54
|
+
/^Delivery state (?:is busy; retry the command\.|lock owner is unavailable; inspect the lock before retrying\.)$/.test(error.message)) continue;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function endedSession(record, at = record.hookEndedAt) {
|
|
61
|
+
return record.lastEvent === 'SessionEnd' && Number.isFinite(at) && at > 0 && record.hookEndedAt === at &&
|
|
62
|
+
record.run?.state === 'recovery_required' && record.run.recoveryCause === 'session_end';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function observeEndedSession(record) {
|
|
66
|
+
const client = await openCodexAppServer();
|
|
67
|
+
try { return await readCodexObservation(client, record.session); }
|
|
68
|
+
finally { await client.close(); }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A provider can end while the supervisor remains alive. Reconcile each such
|
|
72
|
+
// event once, after confirming the original conversation has no active turn.
|
|
73
|
+
export async function reconcileEndedSessions({ root, records, eventId, authorize, observe = observeEndedSession }) {
|
|
74
|
+
for (const record of records) {
|
|
75
|
+
if (!endedSession(record) || !eligible(record) || hasPendingUserStop(root, record)) continue;
|
|
76
|
+
let observation;
|
|
77
|
+
try { observation = await observe(record); } catch { continue; }
|
|
78
|
+
if (observation?.isRunning !== false) continue;
|
|
79
|
+
await reconcileLifecycleRecovery({ root, records: [record], authorize,
|
|
80
|
+
eventId: `${eventId}:session-end:${record.session}:${record.hookEndedAt}`, sessionEndedAt: record.hookEndedAt });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { dirname } from 'node:path';
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { listRecords } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
3
|
+
import { readSupervisorState } from './supervisor-owner.js';
|
|
4
|
+
import { supervisorTargetMatches } from './supervisor-activation.js';
|
|
5
|
+
import { reconcileLifecycleRecovery, reconcileEndedSessions } from './lifecycle-recovery.js';
|
|
2
6
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
7
|
import { assertNoDesktopHostForOrigin } from '../desktop-process-lock.js';
|
|
4
8
|
import { createManagedSupervisor } from './supervisor.js';
|
|
@@ -6,6 +10,8 @@ import { acquireSupervisorOwner } from './supervisor-owner.js';
|
|
|
6
10
|
import { pendingSupervisorHandover, acknowledgeSupervisorDrain } from './supervisor-handover.js';
|
|
7
11
|
|
|
8
12
|
export async function superviseLocalDelivery({ root, webOrigin, once = false, signal,
|
|
13
|
+
activationEpoch, readActivation = readSupervisorState, reconcileStartup = reconcileLifecycleRecovery,
|
|
14
|
+
reconcileSessionEnds = reconcileEndedSessions,
|
|
9
15
|
createSupervisor = createManagedSupervisor, checkOwner = assertNoDesktopHostForOrigin, wait = delay,
|
|
10
16
|
acquireOwner = acquireSupervisorOwner, pendingHandover = pendingSupervisorHandover,
|
|
11
17
|
acknowledgeDrain = acknowledgeSupervisorDrain } = {}) {
|
|
@@ -16,13 +22,28 @@ export async function superviseLocalDelivery({ root, webOrigin, once = false, si
|
|
|
16
22
|
const owner = await acquireOwner(dirname(root), { handoverVersion: 1 });
|
|
17
23
|
try {
|
|
18
24
|
await check();
|
|
25
|
+
const activationFile = join(dirname(root), 'supervisor-runtime/control.json');
|
|
26
|
+
const activation = activationEpoch ? await readActivation(activationFile) : null;
|
|
27
|
+
const activationCurrent = value => value?.version === 1 && value.enabled === true && value.epoch === activationEpoch;
|
|
28
|
+
if (activationEpoch && !activationCurrent(activation)) return;
|
|
29
|
+
const authorizeDispatch = async record => {
|
|
30
|
+
const current = await readActivation(activationFile);
|
|
31
|
+
if (!activationCurrent(current) || !supervisorTargetMatches(current, record)) throw new Error('Supervisor activation or target changed.');
|
|
32
|
+
return activationEpoch;
|
|
33
|
+
};
|
|
34
|
+
const readRecords = async () => (await listRecords(root)).filter(record => supervisorTargetMatches(activation, record));
|
|
35
|
+
if (activationEpoch) await reconcileStartup({ root, records: await readRecords(),
|
|
36
|
+
eventId: `${activationEpoch}:${owner.nonce}`, authorize: authorizeDispatch });
|
|
19
37
|
const supervisor = createSupervisor({ root, webOrigin,
|
|
20
|
-
|
|
38
|
+
...(activationEpoch ? { readRecords, authorizeDispatch } : {}),
|
|
39
|
+
shouldYield: async () => Boolean(activationEpoch && !activationCurrent(await readActivation(activationFile)) ||
|
|
40
|
+
await pendingHandover(dirname(root), owner.nonce)) });
|
|
21
41
|
const stop = () => { void supervisor.close(); };
|
|
22
42
|
signal?.addEventListener('abort', stop, { once: true });
|
|
23
43
|
try {
|
|
24
44
|
do {
|
|
25
45
|
if (signal?.aborted) break;
|
|
46
|
+
if (activationEpoch && !activationCurrent(await readActivation(activationFile))) break;
|
|
26
47
|
const handover = await pendingHandover(dirname(root), owner.nonce);
|
|
27
48
|
if (handover) {
|
|
28
49
|
await supervisor.drain();
|
|
@@ -30,6 +51,8 @@ export async function superviseLocalDelivery({ root, webOrigin, once = false, si
|
|
|
30
51
|
break;
|
|
31
52
|
}
|
|
32
53
|
await check();
|
|
54
|
+
if (activationEpoch) await reconcileSessionEnds({ root, records: await readRecords(),
|
|
55
|
+
eventId: activationEpoch, authorize: authorizeDispatch });
|
|
33
56
|
await supervisor.tick();
|
|
34
57
|
if (once) { await supervisor.idle?.(); break; }
|
|
35
58
|
try { await wait(3_000, undefined, { signal }); }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readRecord, automaticContinuationAllowed } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
3
|
+
|
|
4
|
+
// Activation is durable OS recovery consent, separate from task permissions.
|
|
5
|
+
// A scoped activation binds the existing run/consent, never a future task that
|
|
6
|
+
// happens to reuse the conversation id.
|
|
7
|
+
export async function supervisorTarget(root, target) {
|
|
8
|
+
if (!target || !['codex', 'claude'].includes(target.provider) || typeof target.session !== 'string' || !target.session) {
|
|
9
|
+
throw new Error('Select an exact provider and session for supervisor recovery.');
|
|
10
|
+
}
|
|
11
|
+
const record = await readRecord(join(root, 'delivery-hooks'), target.provider, target.session);
|
|
12
|
+
if (!record?.run?.managed || record.autoContinue !== true || !record.autoContinueEpoch ||
|
|
13
|
+
!automaticContinuationAllowed(record) || ['stopped', 'paused', 'completed', 'reported_done', 'budget_exhausted'].includes(record.run.state)) {
|
|
14
|
+
throw new Error('Supervisor activation requires an existing opted-in task; explicit task stops remain stopped.');
|
|
15
|
+
}
|
|
16
|
+
return { provider: record.provider, session: record.session, runId: record.run.id, autoContinueEpoch: record.autoContinueEpoch };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function supervisorTargetMatches(control, record) {
|
|
20
|
+
if (!Object.hasOwn(control || {}, 'targets')) return true;
|
|
21
|
+
return Array.isArray(control.targets) && control.targets.some(target =>
|
|
22
|
+
target.provider === record.provider && target.session === record.session &&
|
|
23
|
+
typeof target.runId === 'string' && target.runId === record.run?.id &&
|
|
24
|
+
typeof target.autoContinueEpoch === 'string' && target.autoContinueEpoch === record.autoContinueEpoch);
|
|
25
|
+
}
|
|
@@ -24,12 +24,12 @@ export function supervisorServiceDefinition({ root, entry, node = process.execPa
|
|
|
24
24
|
if (platform === 'linux') {
|
|
25
25
|
const name = 'simy-delivery-supervisor.service';
|
|
26
26
|
return { name, file: join(home, '.config/systemd/user', name),
|
|
27
|
-
content: `# ${marker}\n[Unit]\nDescription=SIMY Delivery supervisor recovery\n\n[Service]\nType=simple\nExecStart=${[node, ...args].map(systemdArgument).join(' ')}\nRestart=
|
|
27
|
+
content: `# ${marker}\n[Unit]\nDescription=SIMY Delivery supervisor recovery\n\n[Service]\nType=simple\nExecStart=${[node, ...args].map(systemdArgument).join(' ')}\nRestart=always\nRestartSec=15\nKillMode=process\n\n[Install]\nWantedBy=default.target\n` };
|
|
28
28
|
}
|
|
29
29
|
if (platform === 'darwin') {
|
|
30
30
|
const name = 'com.awak.simy.delivery-supervisor';
|
|
31
31
|
return { name, file: join(home, 'Library/LaunchAgents', `${name}.plist`), content:
|
|
32
|
-
`<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><!-- ${marker} --><dict><key>Label</key><string>${name}</string><key>ProgramArguments</key><array>${[node, ...args].map(arg => `<string>${xml(arg)}</string>`).join('')}</array><key>RunAtLoad</key><true/><key>KeepAlive</key><
|
|
32
|
+
`<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><!-- ${marker} --><dict><key>Label</key><string>${name}</string><key>ProgramArguments</key><array>${[node, ...args].map(arg => `<string>${xml(arg)}</string>`).join('')}</array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>ThrottleInterval</key><integer>15</integer></dict></plist>` };
|
|
33
33
|
}
|
|
34
34
|
if (platform === 'win32') {
|
|
35
35
|
const name = 'SIMYDeliverySupervisor';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { supervisorTarget } from './supervisor-activation.js';
|
|
1
2
|
import { spawn } from 'node:child_process';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { randomUUID } from 'node:crypto';
|
|
@@ -19,7 +20,9 @@ async function verifiedPin(root) {
|
|
|
19
20
|
return { digest: pin.digest, release: pin.release };
|
|
20
21
|
}
|
|
21
22
|
export async function setSupervisorWatchdog({ root, enabled, webOrigin,
|
|
22
|
-
inspectPin = verifiedPin, now = Date.now(), expectedEpoch } = {}) {
|
|
23
|
+
inspectPin = verifiedPin, now = Date.now(), expectedEpoch, target } = {}) {
|
|
24
|
+
if (target && !enabled) throw new Error('A recovery target requires explicit enable.');
|
|
25
|
+
const binding = target ? await supervisorTarget(root, target) : null;
|
|
23
26
|
const pin = enabled ? await inspectPin(root) : null;
|
|
24
27
|
const lock = await acquireDesktopProcessLock(join(folder(root), 'control-lock'), {
|
|
25
28
|
alive: supervisorProcessAlive, identity: supervisorProcessIdentity });
|
|
@@ -27,6 +30,8 @@ export async function setSupervisorWatchdog({ root, enabled, webOrigin,
|
|
|
27
30
|
const previous = await readSupervisorState(controlPath(root));
|
|
28
31
|
if (expectedEpoch !== undefined && previous?.epoch !== expectedEpoch) return previous;
|
|
29
32
|
const control = { version: 1, enabled: enabled === true, epoch: randomUUID(), updatedAt: now,
|
|
33
|
+
selection: { source: 'explicit_command', action: enabled ? 'enable' : 'disable', at: now },
|
|
34
|
+
...(binding ? { targets: [binding] } : {}),
|
|
30
35
|
...(pin ? { ...pin, webOrigin: resolveWebOrigin(webOrigin) } : {}) };
|
|
31
36
|
await saveSupervisorState(controlPath(root), control);
|
|
32
37
|
return control;
|
|
@@ -49,9 +54,11 @@ export function createSupervisorWatchdog({ root, inspectPin = verifiedPin, readC
|
|
|
49
54
|
if (closed) return;
|
|
50
55
|
const at = clock(), control = await readControl();
|
|
51
56
|
controlEpoch = control?.epoch;
|
|
57
|
+
const wake = lastWall !== null && (at < lastWall || at - lastWall > 60_000);
|
|
58
|
+
lastWall = at;
|
|
52
59
|
const previous = pendingPin || await readSupervisorState(statePath(root));
|
|
53
60
|
const state = { version: 1, handoverVersion: 1, watcherNonce, at, action: 'observe', supervisor: null, lastExit,
|
|
54
|
-
lastLaunchAt: launchTime ?? previous?.lastLaunchAt ?? null, epoch: control?.epoch || null };
|
|
61
|
+
lastLaunchAt: launchTime ?? previous?.lastLaunchAt ?? null, epoch: control?.epoch || null, lifecycle: wake ? 'wake' : 'observe' };
|
|
55
62
|
if (!control || control.version !== 1 || control.enabled !== true) { state.action = 'disabled'; await save(state); return state; }
|
|
56
63
|
state.supervisor = await observeOwner(root, { now: at });
|
|
57
64
|
if (state.supervisor.state === 'running') {
|
|
@@ -77,10 +84,8 @@ export function createSupervisorWatchdog({ root, inspectPin = verifiedPin, readC
|
|
|
77
84
|
}
|
|
78
85
|
// Resume/sleep or clock regression is an observation boundary, never proof
|
|
79
86
|
// that a live provider stopped. Re-observe on the next tick first.
|
|
80
|
-
if (
|
|
81
|
-
lastWall = at; state.action = 'wake_reobserve'; await save(state); return state;
|
|
87
|
+
if (wake) { state.action = 'wake_reobserve'; await save(state); return state;
|
|
82
88
|
}
|
|
83
|
-
lastWall = at;
|
|
84
89
|
try { await checkDesktop(root, null); }
|
|
85
90
|
catch (error) {
|
|
86
91
|
if (error.code !== 'process_conflict') throw error;
|
|
@@ -102,7 +107,7 @@ export function createSupervisorWatchdog({ root, inspectPin = verifiedPin, readC
|
|
|
102
107
|
!finalControl?.enabled || finalControl.epoch !== control.epoch) {
|
|
103
108
|
state.action = 'launch_cancelled'; await save(state); return state;
|
|
104
109
|
}
|
|
105
|
-
const args = [join(pin.release, 'src/index.js'), 'delivery', 'supervise', '--host', control.webOrigin, '--no-auto-update'];
|
|
110
|
+
const args = [join(pin.release, 'src/index.js'), 'delivery', 'supervise', '--activation-epoch', control.epoch, '--host', control.webOrigin, '--no-auto-update'];
|
|
106
111
|
child = launch(process.execPath, args, { env: { ...process.env, SIMY_HOME: root, SIMY_AUTO_UPDATE: '0' },
|
|
107
112
|
detached: process.platform !== 'win32', stdio: 'ignore', windowsHide: true });
|
|
108
113
|
const launched = child;
|
package/src/desktop-host.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { remoteOperationFailure } from './session-supervision/remote-errors.js';
|
|
1
2
|
import { createRemoteHostManager } from './session-supervision/remote-hosts.js';
|
|
2
3
|
import { readDesktopAutoUpdate, writeDesktopAutoUpdate } from './desktop-update-settings.js';
|
|
3
4
|
import { createUpdateRuntime } from './update-detachment/runtime.js';
|
|
@@ -14,6 +15,7 @@ import { randomUUID } from "node:crypto";
|
|
|
14
15
|
import { readCodexStatistics, unavailableStatistics } from './session-supervision/statistics.js';
|
|
15
16
|
import { readClaudeStatistics } from './session-supervision/claude-sessions.js';
|
|
16
17
|
import { inspectProviderPlugins, unavailablePluginStatus } from './sqm/provider-plugin-status.js';
|
|
18
|
+
import { repairProviderPlugin } from './sqm/provider-plugin-repair.js';
|
|
17
19
|
import { openCodexAppServer } from './session-supervision/codex-app-server.js';
|
|
18
20
|
import { conversationSqmChecks } from './session-supervision/sqm-presentation.js';
|
|
19
21
|
import { stopOwnedDesktopHost } from "./desktop-force-quit.js";
|
|
@@ -25,7 +27,7 @@ import { controlDelivery, verifyDelivery, createRecordReader, updateRecord, reco
|
|
|
25
27
|
import { createManagedSupervisor } from "./delivery-hooks/supervisor.js";
|
|
26
28
|
import { createActionAssociationHost } from "./session-supervision/action-association-host.js";
|
|
27
29
|
import { createStopNotifier } from "./session-supervision/stop-notifications.js";
|
|
28
|
-
import { AutoContinueUnavailableError, answerSession, continueSession, resumeSession, discoverSessions, enrollSession, monitorSession, trackSession, setSessionAutoContinue } from "./session-supervision/index.js";
|
|
30
|
+
import { AutoContinueUnavailableError, answerSession, continueSession, followupSession, resumeSession, discoverSessions, enrollSession, monitorSession, trackSession, setSessionAutoContinue } from "./session-supervision/index.js";
|
|
29
31
|
import { createContinuationAnalysisManager } from "./session-supervision/analysis-manager.js";
|
|
30
32
|
import { analyzeContinuation, readContinuationContext } from "./session-supervision/continuation-analysis.js";
|
|
31
33
|
import { continueAnalyzedSession, createAutomaticContinuation } from "./session-supervision/automatic-continuation.js";
|
|
@@ -177,8 +179,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
177
179
|
auto_update_busy: updateSettings.auto_update_busy === true } };
|
|
178
180
|
if (env.SIMY_REMOTE_CHILD === "1") status = { ...status, remoteSessionProtocol: 1 };
|
|
179
181
|
if (remoteHosts) {
|
|
180
|
-
void remoteHosts.update(providerLinks.
|
|
181
|
-
if (providerLinks.
|
|
182
|
+
void remoteHosts.update(PROVIDER_LINKS.some(provider => providerLinks[provider].connected) ? status : { ...status, access: { state: "unavailable" } }).catch(() => {});
|
|
183
|
+
if (PROVIDER_LINKS.some(provider => providerLinks[provider].connected)) status = remoteHosts.project(status, { providerLinks });
|
|
182
184
|
}
|
|
183
185
|
if (updateProgress) status = { ...status, update: { ...status.update, ...updateProgress } };
|
|
184
186
|
const { sampledAt, ...content } = status;
|
|
@@ -502,6 +504,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
502
504
|
const changeProviderLink = async (provider, connected) => {
|
|
503
505
|
if (!PROVIDER_LINKS.includes(provider)) throw new Error("Unsupported SIMY provider connection.");
|
|
504
506
|
if (providerLinkBusy.has(provider)) throw new Error("SIMY connection is already changing.");
|
|
507
|
+
providerRepairControllers.get(provider)?.abort();
|
|
505
508
|
providerLinkBusy.add(provider);
|
|
506
509
|
providerLinkFeedback[provider] = { state: connected ? "connecting" : "disconnecting" };
|
|
507
510
|
let phase = "read_settings";
|
|
@@ -574,6 +577,11 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
574
577
|
const providerAuthBusy = new Set();
|
|
575
578
|
let pluginInstallationActive = false, pluginSetupEpoch = 0;
|
|
576
579
|
let providerPlugins = { codex: { state: 'checking' }, claude: { state: 'checking' } };
|
|
580
|
+
const providerRepairs = {}, providerRepairControllers = new Map(), providerRepairJobs = new Set();
|
|
581
|
+
const providerPluginEpochs = { codex: 0, claude: 0 };
|
|
582
|
+
const pluginDiagnostic = ({ provider, reason, elapsedMs }) => {
|
|
583
|
+
diagnostics.write(`provider plugin inspection provider=${provider} reason=${reason} elapsedMs=${elapsedMs}`);
|
|
584
|
+
};
|
|
577
585
|
let pluginInspection = null, lastPluginInspectionAt = -Infinity;
|
|
578
586
|
let pluginInspectionAbort = new AbortController();
|
|
579
587
|
const refreshProviderPlugins = () => {
|
|
@@ -581,9 +589,17 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
581
589
|
if (pluginInspection) return pluginInspection;
|
|
582
590
|
if (pluginInspectionAbort.signal.aborted) pluginInspectionAbort = new AbortController();
|
|
583
591
|
lastPluginInspectionAt = Date.now();
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
592
|
+
const epochs = { ...providerPluginEpochs };
|
|
593
|
+
const apply = value => {
|
|
594
|
+
for (const provider of PROVIDER_LINKS) {
|
|
595
|
+
if (!providerRepairControllers.has(provider) && epochs[provider] === providerPluginEpochs[provider]) {
|
|
596
|
+
providerPlugins[provider] = value[provider] || unavailablePluginStatus();
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
pluginInspection = Promise.resolve().then(() => (dependencies.inspectProviderPlugins || inspectProviderPlugins)({ signal: pluginInspectionAbort.signal, details: true, onDiagnostic: pluginDiagnostic }))
|
|
601
|
+
.then(apply)
|
|
602
|
+
.catch(() => apply({ codex: unavailablePluginStatus(), claude: unavailablePluginStatus() }))
|
|
587
603
|
.finally(() => { pluginInspection = null; if (!closed && !relay) void publish().catch(() => {}); });
|
|
588
604
|
return pluginInspection;
|
|
589
605
|
};
|
|
@@ -607,6 +623,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
607
623
|
}]));
|
|
608
624
|
status.pluginSetup = { ...pluginSetup };
|
|
609
625
|
status.providerPlugins = structuredClone(providerPlugins);
|
|
626
|
+
status.providerRepairs = structuredClone(providerRepairs);
|
|
610
627
|
for (const provider of ["codex", "claude"]) {
|
|
611
628
|
if (providerAuth[provider]?.state === "awaiting_sign_in" && !providerAuthBusy.has(provider) && status.providers?.[provider]?.state === "ready") delete providerAuth[provider];
|
|
612
629
|
}
|
|
@@ -803,6 +820,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
803
820
|
clearInterval(timer);
|
|
804
821
|
teamThreads?.close();
|
|
805
822
|
pluginInspectionAbort.abort();
|
|
823
|
+
for (const controller of providerRepairControllers.values()) controller.abort();
|
|
806
824
|
activeStatisticsRead?.controller.abort();
|
|
807
825
|
discoveryStopped = true;
|
|
808
826
|
encodedStatus = null; lastStatus = null;
|
|
@@ -811,6 +829,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
811
829
|
// reader must never postpone cancellation of an owned Provider process.
|
|
812
830
|
const results = await Promise.allSettled([stopDiscovery({ drain: true }), publishing, connectionPublishing,
|
|
813
831
|
pluginInspection,
|
|
832
|
+
...providerRepairJobs,
|
|
814
833
|
remoteHosts?.close(), actionAssociations.close(), automaticContinuation?.close(), analysisManager?.close(),
|
|
815
834
|
managedSupervisor.close(), recordReader.close(), stopWorkflows(),
|
|
816
835
|
agent ? createAgentShutdown(agent)() : undefined]);
|
|
@@ -1162,12 +1181,12 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1162
1181
|
}
|
|
1163
1182
|
|
|
1164
1183
|
let commandProviderLink;
|
|
1165
|
-
if (command.provider && !["refresh", "provider_connect", "provider_disconnect", "delivery_stop", "delivery_pause", "delivery_reject", "session_untrack", "session_unmonitor"].includes(action) &&
|
|
1184
|
+
if (command.provider && !["provider_repair", "refresh", "provider_connect", "provider_disconnect", "delivery_stop", "delivery_pause", "delivery_reject", "session_untrack", "session_unmonitor"].includes(action) &&
|
|
1166
1185
|
!(action === "session_set_auto_continue" && command.enabled === false)) {
|
|
1167
1186
|
commandProviderLink = await requireProviderLink({ root: deliveryRoot, provider: command.provider });
|
|
1168
1187
|
}
|
|
1169
1188
|
if (command.hostId && command.hostId !== 'local') {
|
|
1170
|
-
if (!remoteHosts || command.provider
|
|
1189
|
+
if (!remoteHosts || !PROVIDER_LINKS.includes(command.provider)) throw Object.assign(new Error('Remote host is unavailable.'), { code: 'remote_host_unavailable' });
|
|
1171
1190
|
await authorizeProvider(command.provider, commandScope, commandProviderLink?.epoch);
|
|
1172
1191
|
const status = await agent.getDesktopStatus();
|
|
1173
1192
|
// Status collection can yield while provider consent is revoked.
|
|
@@ -1187,7 +1206,48 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1187
1206
|
await remoteHosts.save({ alias: command.alias, label: command.label }, action === 'remote_host_remove');
|
|
1188
1207
|
emit({ type: 'result', id: command.id, ok: true }); await publish(); return;
|
|
1189
1208
|
}
|
|
1190
|
-
if (action ===
|
|
1209
|
+
if (action === 'provider_repair') {
|
|
1210
|
+
const provider = command.provider;
|
|
1211
|
+
if (!PROVIDER_LINKS.includes(provider)) throw new Error('Unsupported provider.');
|
|
1212
|
+
if (providerRepairControllers.has(provider) || providerLinkBusy.has(provider)) throw new Error('Provider repair is already running.');
|
|
1213
|
+
const controller = new AbortController();
|
|
1214
|
+
providerRepairControllers.set(provider, controller);
|
|
1215
|
+
providerPluginEpochs[provider]++;
|
|
1216
|
+
providerRepairs[provider] = { state: 'repairing' };
|
|
1217
|
+
const beganAt = Date.now();
|
|
1218
|
+
let repairJob;
|
|
1219
|
+
try {
|
|
1220
|
+
await publish();
|
|
1221
|
+
repairJob = Promise.resolve().then(() => (dependencies.repairProviderPlugin || repairProviderPlugin)({
|
|
1222
|
+
provider, signal: controller.signal, onDiagnostic: pluginDiagnostic,
|
|
1223
|
+
authorize: async () => {
|
|
1224
|
+
if (closed || relay || controller.signal.aborted) throw Object.assign(new Error('Repair cancelled.'), { reason: 'cancelled' });
|
|
1225
|
+
await authorizeWork(commandScope);
|
|
1226
|
+
},
|
|
1227
|
+
}));
|
|
1228
|
+
providerRepairJobs.add(repairJob);
|
|
1229
|
+
const result = await repairJob;
|
|
1230
|
+
if (closed || controller.signal.aborted) throw Object.assign(new Error('Repair cancelled.'), { reason: 'cancelled' });
|
|
1231
|
+
await authorizeWork(commandScope);
|
|
1232
|
+
if (result?.plugin?.state !== 'enabled') throw Object.assign(new Error('Repair was not verified.'), { reason: 'verification_failed' });
|
|
1233
|
+
providerPlugins[provider] = result.plugin;
|
|
1234
|
+
providerRepairs[provider] = { state: 'repaired' };
|
|
1235
|
+
diagnostics.write(`provider plugin repair provider=${provider} result=repaired elapsedMs=${Date.now() - beganAt}`);
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
const reasons = ['timeout', 'cli_missing', 'invalid_inventory', 'command_failed', 'verification_failed',
|
|
1238
|
+
'scope_requires_attention', 'source_conflict', 'busy', 'permission_denied', 'cancelled'];
|
|
1239
|
+
const reason = reasons.includes(error?.reason) ? error.reason : 'command_failed';
|
|
1240
|
+
providerRepairs[provider] = { state: 'failed', reason };
|
|
1241
|
+
diagnostics.write(`provider plugin repair provider=${provider} result=failed reason=${reason} elapsedMs=${Date.now() - beganAt}`);
|
|
1242
|
+
throw error;
|
|
1243
|
+
} finally {
|
|
1244
|
+
providerRepairJobs.delete(repairJob);
|
|
1245
|
+
providerRepairControllers.delete(provider);
|
|
1246
|
+
await publish();
|
|
1247
|
+
}
|
|
1248
|
+
emit({ type: 'result', id: command.id, ok: true });
|
|
1249
|
+
return;
|
|
1250
|
+
} else if (action === "provider_connect" || action === "provider_disconnect") {
|
|
1191
1251
|
await changeProviderLink(command.provider, action === "provider_connect");
|
|
1192
1252
|
emit({ type: "result", id: command.id, ok: true });
|
|
1193
1253
|
return;
|
|
@@ -1529,6 +1589,19 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1529
1589
|
await (dependencies.monitorSession || monitorSession)({ root: join(root, "delivery-hooks"), provider: command.provider,
|
|
1530
1590
|
session: command.session, monitor: action === "session_monitor", expectedProviderEpoch: commandProviderLink?.epoch });
|
|
1531
1591
|
await refreshSessions(true);
|
|
1592
|
+
} else if (action === "session_followup") {
|
|
1593
|
+
const candidate = await selectedSession(command);
|
|
1594
|
+
if (!candidate.followup?.previousRunId || command.previousRunId !== candidate.followup.previousRunId) {
|
|
1595
|
+
throw new Error('The finished task changed. Refresh this conversation before starting its follow-up.');
|
|
1596
|
+
}
|
|
1597
|
+
if (typeof command.objective !== 'string' || !command.objective.trim() || command.objective.length > 4000) {
|
|
1598
|
+
throw new Error('Enter the current goal to continue.');
|
|
1599
|
+
}
|
|
1600
|
+
await authorizeProvider(candidate.provider, commandScope, selectedLinkEpochs.get(candidate));
|
|
1601
|
+
await (dependencies.followupSession || followupSession)({ root: join(root, 'delivery-hooks'),
|
|
1602
|
+
provider: command.provider, session: command.session, previousRunId: command.previousRunId,
|
|
1603
|
+
objective: command.objective, followupAutoContinue: true, expectedProviderEpoch: selectedLinkEpochs.get(candidate) });
|
|
1604
|
+
await refreshSessions(true);
|
|
1532
1605
|
} else if (action === "session_enroll") {
|
|
1533
1606
|
await (dependencies.enrollSession || enrollSession)({ root: join(root, "delivery-hooks"), provider: command.provider,
|
|
1534
1607
|
session: command.session, objective: command.objective, acceptanceCriteria: command.acceptanceCriteria,
|
|
@@ -1571,7 +1644,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1571
1644
|
providerLinkFeedback[provider] = { state: 'error', message: desktopActionError(command.action, error) };
|
|
1572
1645
|
}
|
|
1573
1646
|
}
|
|
1574
|
-
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) });
|
|
1647
|
+
emit({ type: "result", id: command.id, ok: false, ...(remoteOperationFailure(error?.code).code ? { code: error.code } : {}), ...(error?.code === 'cancelled' || statisticsRequest?.controller.signal.aborted ? { code: 'cancelled' } : {}), message: ["update", "prepare_update_restart"].includes(command.action) && updateProgress ? updateProgress.message : desktopActionError(command.action, error) });
|
|
1575
1648
|
} finally {
|
|
1576
1649
|
// Access rejection can happen before the shared reader is constructed.
|
|
1577
1650
|
if (statisticsRequest && !statisticsRequest.promise && activeStatisticsRead === statisticsRequest) {
|
|
@@ -1584,7 +1657,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1584
1657
|
void execute();
|
|
1585
1658
|
} else if (["login", "organization_select", "organization_cancel", "authorization_open"].includes(command.action)) {
|
|
1586
1659
|
authorizationQueue = authorizationQueue.then(execute).catch(() => {});
|
|
1587
|
-
} else if (/^(codex|claude)_login$/.test(command.action || "") || ["provider_connect", "provider_disconnect"].includes(command.action)) {
|
|
1660
|
+
} else if (/^(codex|claude)_login$/.test(command.action || "") || ["provider_repair", "provider_connect", "provider_disconnect"].includes(command.action)) {
|
|
1588
1661
|
// Opening an auth terminal must not wait for an unrelated provider turn.
|
|
1589
1662
|
// Per-provider busy guards prevent overlapping connection changes.
|
|
1590
1663
|
void execute();
|
package/src/index.js
CHANGED
|
@@ -149,7 +149,7 @@ async function runCommand(installMode) {
|
|
|
149
149
|
simy sqm lint Validate module reliability before publication
|
|
150
150
|
simy sqm replay Replay modules against labelled historical snapshots
|
|
151
151
|
simy update --check Check the installed and latest stable CLI versions
|
|
152
|
-
simy sessions Show local Codex conversations (--interactive for search/navigation)
|
|
152
|
+
simy sessions Show local and registered remote Codex/Claude conversations (--interactive for search/navigation)
|
|
153
153
|
simy update ${installMode === "macos_installer" ? "Show Apple Silicon update status and installer action" : "Install an eligible stable CLI update"}
|
|
154
154
|
|
|
155
155
|
Options:
|
|
@@ -160,17 +160,17 @@ async function inspectTranscript(root, projectName, fileName, observedAt, includ
|
|
|
160
160
|
finally { await handle?.close(); }
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
async function collect({ home = homedir(), limit = 100, session = null, prioritySessions = [], includeContext = false, cacheMetadata = false, includeStatistics = false, includeBoundary = false, signal } = {}) {
|
|
163
|
+
async function collect({ home = homedir(), all = false, limit = 100, session = null, prioritySessions = [], includeContext = false, cacheMetadata = false, includeStatistics = false, includeBoundary = false, signal } = {}) {
|
|
164
164
|
if (signal?.aborted) return [];
|
|
165
165
|
const root = await projectsRoot(home);
|
|
166
166
|
if (!root) return [];
|
|
167
|
-
const maximum = Number.isInteger(limit) ? Math.max(1, Math.min(limit, RECENT_SESSION_LIMIT)) : 100;
|
|
167
|
+
const maximum = all ? Infinity : Number.isInteger(limit) ? Math.max(1, Math.min(limit, RECENT_SESSION_LIMIT)) : 100;
|
|
168
168
|
const observedAt = Date.now();
|
|
169
169
|
const result = new Map();
|
|
170
170
|
const descriptors = [], priorities = new Set(prioritySessions);
|
|
171
171
|
let projects;
|
|
172
172
|
try { projects = await readdir(root, { withFileTypes: true }); } catch { return []; }
|
|
173
|
-
for (const entry of projects.filter(item => item.isDirectory() && !item.isSymbolicLink()).slice(0, MAX_PROJECTS)) {
|
|
173
|
+
for (const entry of projects.filter(item => item.isDirectory() && !item.isSymbolicLink()).slice(0, all || session ? undefined : MAX_PROJECTS)) {
|
|
174
174
|
if (signal?.aborted) return [];
|
|
175
175
|
const project = join(root, entry.name);
|
|
176
176
|
let files;
|
|
@@ -203,11 +203,11 @@ async function collect({ home = homedir(), limit = 100, session = null, priority
|
|
|
203
203
|
...(!candidate.parentSession && origin.parentSession ? { parentSession: origin.parentSession, parentSessionSource: origin.source } : {}) };
|
|
204
204
|
return candidate;
|
|
205
205
|
};
|
|
206
|
-
const selected = session ? descriptors : [
|
|
206
|
+
const selected = session || all ? descriptors : [
|
|
207
207
|
...recentSessionWindow(descriptors.filter(item => desktopLineage.get(item.session)?.providerArchived !== true), { limit: maximum, priority: item => priorities.has(item.session) }),
|
|
208
208
|
...recentSessionWindow(descriptors.filter(item => desktopLineage.get(item.session)?.providerArchived === true), { limit: maximum, priority: item => priorities.has(item.session) }),
|
|
209
209
|
];
|
|
210
|
-
const filesToInspect = selected.slice(0, MAX_TRANSCRIPTS);
|
|
210
|
+
const filesToInspect = all ? selected : selected.slice(0, MAX_TRANSCRIPTS);
|
|
211
211
|
let nextFile = 0;
|
|
212
212
|
// Catalog prefixes are bounded independently of full transcript size. Use a
|
|
213
213
|
// small I/O pool; exact-context/statistics reads stay serial and identity-safe.
|
|
@@ -259,8 +259,8 @@ async function collect({ home = homedir(), limit = 100, session = null, priority
|
|
|
259
259
|
];
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
export async function listClaudeSessions({ home, limit, prioritySessions, signal } = {}) {
|
|
263
|
-
return collect({ home, limit, prioritySessions, signal, cacheMetadata: true });
|
|
262
|
+
export async function listClaudeSessions({ home, all = false, limit, prioritySessions, signal } = {}) {
|
|
263
|
+
return collect({ home, all, limit, prioritySessions, signal, cacheMetadata: true });
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
export async function readClaudeSession(session, { home } = {}) {
|
|
@@ -16,7 +16,7 @@ import { workspaceIdentity, verificationInstruction, normalizeVerification, asse
|
|
|
16
16
|
import { effectiveAcceptanceCriteria } from '../codex-plugins/simy/hooks/acceptance-policy.mjs';
|
|
17
17
|
import { buildPurposeInstruction } from './purpose-instruction.js';
|
|
18
18
|
import { peerOpinionPrompt } from '../codex-plugins/simy/hooks/purpose-cycle.mjs';
|
|
19
|
-
import {
|
|
19
|
+
import { runCodexPeerOpinion } from './codex-peer-opinion.js';
|
|
20
20
|
import { associationContract } from '../codex-plugins/simy/hooks/action-association.mjs';
|
|
21
21
|
import { spawn } from 'node:child_process';
|
|
22
22
|
import { realpath, stat, open } from 'node:fs/promises';
|
|
@@ -235,11 +235,13 @@ export function codexThreadMetadata(thread) {
|
|
|
235
235
|
};
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
export async function listCodexSessions({ client, maxPages = 40, pageSize = 25, maxSessions = RECENT_SESSION_LIMIT, archived = false, inspectState = false, prioritySessions = [], observationBudgetMs = 3000, codexHome } = {}) {
|
|
238
|
+
export async function listCodexSessions({ client, all = false, signal, maxPages = 40, pageSize = 25, maxSessions = RECENT_SESSION_LIMIT, archived = false, inspectState = false, prioritySessions = [], observationBudgetMs = 3000, codexHome } = {}) {
|
|
239
239
|
const sessions = [];
|
|
240
|
-
const windowSize = Math.min(maxSessions, maxPages * pageSize);
|
|
240
|
+
const windowSize = all ? Infinity : Math.min(maxSessions, maxPages * pageSize);
|
|
241
|
+
const cursors = new Set(), ids = new Set();
|
|
241
242
|
let cursor = null, listed = 0, pages = 0, batchSize = pageSize;
|
|
242
243
|
while (pages < Math.max(maxPages, Math.ceil(windowSize / batchSize)) && listed < windowSize) {
|
|
244
|
+
signal?.throwIfAborted();
|
|
243
245
|
const limit = Math.min(batchSize, windowSize - listed);
|
|
244
246
|
let response;
|
|
245
247
|
try { response = await client.request('thread/list', { cursor, limit, sourceKinds: SOURCE_KINDS,
|
|
@@ -251,14 +253,21 @@ export async function listCodexSessions({ client, maxPages = 40, pageSize = 25,
|
|
|
251
253
|
batchSize = Math.max(1, Math.floor(limit / 2));
|
|
252
254
|
continue;
|
|
253
255
|
}
|
|
254
|
-
|
|
256
|
+
if (!Array.isArray(response?.data) || response.data.length > limit)
|
|
257
|
+
throw new CodexAppServerError('invalid_page', 'Codex returned an invalid conversation page.');
|
|
258
|
+
const threads = response.data;
|
|
255
259
|
listed += threads.length; pages++;
|
|
256
260
|
for (const thread of threads) {
|
|
257
261
|
const metadata = codexThreadMetadata(thread);
|
|
258
|
-
if (metadata && SOURCE_KINDS.includes(metadata.source)
|
|
262
|
+
if (metadata && SOURCE_KINDS.includes(metadata.source) && !ids.has(metadata.session)) {
|
|
263
|
+
ids.add(metadata.session); sessions.push(metadata);
|
|
264
|
+
}
|
|
259
265
|
}
|
|
260
266
|
cursor = response?.nextCursor || null;
|
|
261
267
|
if (!cursor) break;
|
|
268
|
+
if (typeof cursor !== 'string' || cursors.has(cursor))
|
|
269
|
+
throw new CodexAppServerError('invalid_page', 'Codex repeated a conversation page cursor.');
|
|
270
|
+
cursors.add(cursor);
|
|
262
271
|
}
|
|
263
272
|
const deadline = Date.now() + Math.max(1, Math.min(observationBudgetMs, 5000));
|
|
264
273
|
// A recent page can omit an explicitly supervised conversation, including
|
|
@@ -645,7 +654,7 @@ async function frozenNativeSandbox(record, root, signal) {
|
|
|
645
654
|
return policy;
|
|
646
655
|
}
|
|
647
656
|
|
|
648
|
-
export async function executeCodexAppServer(record, { webOrigin, openClient = openCodexAppServer, signal, authorizePreparation = async () => {}, beforeDispatch = async () => {}, onStarted = async () => {}, onActivity = () => {}, onUsage = () => {}, onEvidence = async () => {}, observeExternal = collectPurposeObservations, root, auditOnly = false, opinionOnly = false, peerAction, runPeerReview =
|
|
657
|
+
export async function executeCodexAppServer(record, { webOrigin, openClient = openCodexAppServer, signal, authorizePreparation = async () => {}, beforeDispatch = async () => {}, onStarted = async () => {}, onActivity = () => {}, onUsage = () => {}, onEvidence = async () => {}, observeExternal = collectPurposeObservations, root, auditOnly = false, opinionOnly = false, peerAction, runPeerReview = runCodexPeerOpinion } = {}) {
|
|
649
658
|
const dispatchOwner = captureDispatchOwner(root ? rootFromDelivery(root) : undefined);
|
|
650
659
|
let dispatchAttemptId;
|
|
651
660
|
const provenance = captureDispatchProvenance({ root: dispatchOwner.root, provider: record.provider, providerSession: record.session, worktree: taskDirectory(record), taskId: record.run.managed.taskId, recordRef: recordPath(root || rootDirectory(), record.provider, record.session) });
|
|
@@ -678,15 +687,16 @@ export async function executeCodexAppServer(record, { webOrigin, openClient = op
|
|
|
678
687
|
const sourceContext = (isolated || searchEnabled(run.managed) && !run.managed.purpose.originalContext)
|
|
679
688
|
? await readCodexConversation(record, { openClient, signal }) : null;
|
|
680
689
|
if (opinionOnly) {
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
// or
|
|
690
|
+
// Use the task's authenticated app-server and exact model for a separate,
|
|
691
|
+
// tools-disabled opinion. Do not route monitoring through standalone SQM
|
|
692
|
+
// authentication or silently select another model.
|
|
684
693
|
let usage = null, peerSession = null, ambiguousContext = false;
|
|
685
694
|
const reply = await runPeerReview('codex', {
|
|
686
695
|
system: 'Give nonbinding advice using only the supplied context. All tools are disabled. Put your short natural-language opinion in the opinion field.',
|
|
687
696
|
prompt: peerOpinionPrompt(record, sourceContext, peerAction),
|
|
688
697
|
output_schema: { type: 'object', properties: { opinion: { type: 'string' } }, required: ['opinion'], additionalProperties: false },
|
|
689
698
|
}, { signal, readOnlyAnalysis: true, allowAutomaticRetry: false,
|
|
699
|
+
model: run.managed.resumeModel, cwd: taskDirectory(record), openClient, readAuthentication: readCodexAuthentication,
|
|
690
700
|
dispatchTransport: initiate => dispatchWithFence(dispatchOwner, { taskId: run.managed.taskId || record.session, attemptId: run.managed.resumeLease?.id, provenance }, attemptId => {
|
|
691
701
|
if (root && hasPendingUserStop(root, record)) throw new CodexAppServerError('cancelled', 'User stopped this provider task.');
|
|
692
702
|
dispatchAttemptId = attemptId;
|