@awak-app/simy-cli 0.7.14 → 0.7.16
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 +45 -72
- package/package.json +3 -2
- package/src/agent.js +1 -0
- package/src/backend-executable.js +7 -6
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +43 -13
- package/src/codex-plugins/simy/hooks/outcome-progress.mjs +1 -2
- package/src/codex-plugins/simy/hooks/purpose-cycle.mjs +12 -11
- package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +21 -33
- package/src/codex-plugins/simy/hooks/purpose-rule-policy.mjs +1 -2
- package/src/codex-plugins/simy/hooks/purpose-rule-review.mjs +5 -63
- package/src/codex-plugins/simy/hooks/purpose-search.mjs +21 -4
- package/src/codex-plugins/simy/hooks/result-cycle.mjs +140 -0
- package/src/codex-plugins/simy/hooks/scene-reasoning.mjs +98 -12
- package/src/codex-plugins/simy/hooks/user-experience.mjs +26 -0
- package/src/delivery-hooks/claude-workflow.md +1 -1
- package/src/delivery-hooks/command.js +31 -0
- package/src/delivery-hooks/consistency-monitor.js +20 -4
- package/src/delivery-hooks/local-runtime.js +32 -1
- package/src/delivery-hooks/local-supervisor.js +12 -3
- package/src/delivery-hooks/supervisor-handover.js +84 -0
- package/src/delivery-hooks/supervisor-owner.js +3 -2
- package/src/delivery-hooks/supervisor-service.js +36 -6
- package/src/delivery-hooks/supervisor-watchdog.js +15 -4
- package/src/delivery-hooks/supervisor.js +66 -9
- package/src/orchestrator/audit.js +24 -323
- package/src/orchestrator/charter-validation.js +3 -41
- package/src/orchestrator/checklist-continuation.js +2 -14
- package/src/orchestrator/completion-contract.js +4 -49
- package/src/orchestrator/contract.js +5 -9
- package/src/orchestrator/delivery/WORKFLOW.md +30 -710
- package/src/orchestrator/delivery/references/acceptance-evidence.md +4 -216
- package/src/orchestrator/delivery/references/change-boundary-matrix.md +4 -103
- package/src/orchestrator/delivery/references/chrome-user-verification.md +5 -133
- package/src/orchestrator/delivery/references/cross-repository-control.md +4 -74
- package/src/orchestrator/delivery/references/delivery-levels.md +3 -117
- package/src/orchestrator/delivery/references/local-sqm-evidence.md +1 -1
- package/src/orchestrator/delivery/references/minimum-completion.md +4 -144
- package/src/orchestrator/delivery/references/pr-closure.md +4 -200
- package/src/orchestrator/delivery/references/shared-system-specifications.md +4 -161
- package/src/orchestrator/delivery/references/state-and-receipts.md +4 -245
- package/src/orchestrator/delivery/references/state-model.md +4 -185
- package/src/orchestrator/delivery/references/ux-specification.md +4 -95
- package/src/orchestrator/delivery-level.js +2 -10
- package/src/orchestrator/evidence.js +2 -3
- package/src/orchestrator/independent-audit.js +7 -147
- package/src/orchestrator/instruction.js +19 -254
- package/src/orchestrator/loop.js +8 -36
- package/src/orchestrator/outcome-monitor.js +21 -0
- package/src/orchestrator/pr-closure.js +2 -16
- package/src/orchestrator/ux-specification.js +3 -12
- package/src/runner.js +3 -1
- package/src/session-supervision/acceptance-reads.js +34 -0
- package/src/session-supervision/browser-cdp.js +59 -0
- package/src/session-supervision/browser-recovery.js +97 -0
- package/src/session-supervision/codex-app-server.js +42 -17
- package/src/session-supervision/continuation-analysis.js +2 -1
- package/src/session-supervision/final-response-recovery.js +3 -0
- package/src/session-supervision/interaction-proxy.js +250 -0
- package/src/session-supervision/interaction-task.js +189 -0
- package/src/session-supervision/native-interaction.js +102 -0
- package/src/session-supervision/provider-failure.js +40 -0
- package/src/session-supervision/purpose-instruction.js +14 -14
- package/src/session-supervision/purpose-recovery.js +20 -9
- package/src/session-supervision/purpose-verification.js +43 -161
- package/src/sqm/claude-plugin.js +2 -0
- package/src/sqm/local-tests.js +2 -1
- package/src/startup-preflight.js +2 -20
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { acquireDesktopProcessLock } from '../desktop-process-lock.js';
|
|
8
8
|
import { compareVersions } from '../backend-executable.js';
|
|
9
9
|
import { stableHookRuntime } from '../hook-runtime.js';
|
|
10
|
+
import { acquireSupervisorOwner, readSupervisorState, supervisorProcessAlive, supervisorProcessIdentity } from './supervisor-owner.js';
|
|
10
11
|
|
|
11
12
|
const exec = promisify(execFile);
|
|
12
13
|
const packageRoot = fileURLToPath(new URL('../../', import.meta.url));
|
|
@@ -96,6 +97,7 @@ export async function adoptInstalledRuntime({ root, source = packageRoot, codexH
|
|
|
96
97
|
await cp(join(source, 'node_modules'), join(target, 'node_modules'), { recursive: true, dereference: true });
|
|
97
98
|
},
|
|
98
99
|
});
|
|
100
|
+
if (installation.activation === 'deferred') return { adopted: false, installation };
|
|
99
101
|
// A locally activated candidate is a separate explicit activation contract.
|
|
100
102
|
// Retain its receipt, but prevent watchdog replay of the superseded source.
|
|
101
103
|
const activeFile = join(root, 'desktop-runtime/active.json');
|
|
@@ -110,7 +112,7 @@ export async function adoptInstalledRuntime({ root, source = packageRoot, codexH
|
|
|
110
112
|
// Install a frozen candidate alongside the released app, without replacing a
|
|
111
113
|
// signed application, changing provider trust, or starting any registered task.
|
|
112
114
|
export async function installLocalRuntime({ root, source = packageRoot, allowMissingLock = false, includeDependencies = false,
|
|
113
|
-
expectedInstallationDigest, verifyBeforeActivate = async () => {},
|
|
115
|
+
expectedInstallationDigest, verifyBeforeActivate = async () => {}, handoverId,
|
|
114
116
|
installDependencies = async target => exec(process.platform === 'win32' ? 'npm.cmd' : 'npm',
|
|
115
117
|
['ci', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund'], { cwd: target, timeout: 120_000, maxBuffer: 1024 * 1024 }),
|
|
116
118
|
} = {}) {
|
|
@@ -148,6 +150,30 @@ export async function installLocalRuntime({ root, source = packageRoot, allowMis
|
|
|
148
150
|
const saved = JSON.parse(await readFile(join(release, 'runtime-build.json'), 'utf8'));
|
|
149
151
|
if (saved.schema !== 1 || saved.digest !== digest || await fingerprint(release, { allowMissingLock, includeDependencies }) !== digest) throw new Error('Existing candidate failed integrity verification.');
|
|
150
152
|
}
|
|
153
|
+
const activationLock = await acquireDesktopProcessLock(join(root, 'supervisor-runtime', 'control-lock'), {
|
|
154
|
+
alive: supervisorProcessAlive, identity: supervisorProcessIdentity });
|
|
155
|
+
let handoverOwner, handover;
|
|
156
|
+
try {
|
|
157
|
+
let control;
|
|
158
|
+
try { control = JSON.parse(await readFile(join(root, 'supervisor-runtime/control.json'), 'utf8')); }
|
|
159
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
160
|
+
// Preparing a release must not disable the existing watchdog's recovery
|
|
161
|
+
// contract. Keep its pin and launchers until a coordinated handover.
|
|
162
|
+
if (handoverId) {
|
|
163
|
+
handover = await readSupervisorState(join(root, 'supervisor-runtime/handover.json'));
|
|
164
|
+
if (!control?.enabled || handover?.version !== 1 || handover.id !== handoverId || handover.status !== 'drained' ||
|
|
165
|
+
handover.epoch !== control.epoch || handover.target?.digest !== digest || handover.target.release !== release ||
|
|
166
|
+
![handover.fromDigest, digest].includes(control.digest) ||
|
|
167
|
+
![handover.fromDigest, digest].includes(before.installation?.digest)) throw new Error('Supervisor handover changed or was stopped.');
|
|
168
|
+
// Fence every replacement until the previous owner has actually released
|
|
169
|
+
// its singleton, including the acknowledgement/release crash gap.
|
|
170
|
+
handoverOwner = await acquireSupervisorOwner(root);
|
|
171
|
+
}
|
|
172
|
+
if (!handover && control?.enabled === true && control.digest !== digest) {
|
|
173
|
+
return { schema: 1, version: before.version, digest, release,
|
|
174
|
+
activation: 'deferred', reason: 'supervisor_handover_required',
|
|
175
|
+
activeInstallation: before.installation };
|
|
176
|
+
}
|
|
151
177
|
await verifyBeforeActivate();
|
|
152
178
|
const bin = join(root, 'bin');
|
|
153
179
|
await mkdir(bin, { recursive: true, mode: 0o700 });
|
|
@@ -169,6 +195,11 @@ export async function installLocalRuntime({ root, source = packageRoot, allowMis
|
|
|
169
195
|
previous: before.installation?.digest === digest ? before.installation.previous
|
|
170
196
|
: before.installation ? { version: before.installation.version, digest: before.installation.digest, release: before.installation.release } : null };
|
|
171
197
|
await atomicJson(join(storage, 'current.json'), current);
|
|
198
|
+
if (handover) {
|
|
199
|
+
await atomicJson(join(root, 'supervisor-runtime/control.json'), { ...control, digest, release, updatedAt: Date.now() });
|
|
200
|
+
await atomicJson(join(root, 'supervisor-runtime/handover.json'), { ...handover, status: 'activated', activatedAt: Date.now() });
|
|
201
|
+
}
|
|
172
202
|
return current;
|
|
203
|
+
} finally { await handoverOwner?.release(); await activationLock.release(); }
|
|
173
204
|
} finally { await rm(staged, { recursive: true, force: true }); await rm(lock, { recursive: true, force: true }); }
|
|
174
205
|
}
|
|
@@ -3,23 +3,32 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|
|
3
3
|
import { assertNoDesktopHostForOrigin } from '../desktop-process-lock.js';
|
|
4
4
|
import { createManagedSupervisor } from './supervisor.js';
|
|
5
5
|
import { acquireSupervisorOwner } from './supervisor-owner.js';
|
|
6
|
+
import { pendingSupervisorHandover, acknowledgeSupervisorDrain } from './supervisor-handover.js';
|
|
6
7
|
|
|
7
8
|
export async function superviseLocalDelivery({ root, webOrigin, once = false, signal,
|
|
8
9
|
createSupervisor = createManagedSupervisor, checkOwner = assertNoDesktopHostForOrigin, wait = delay,
|
|
9
|
-
acquireOwner = acquireSupervisorOwner
|
|
10
|
+
acquireOwner = acquireSupervisorOwner, pendingHandover = pendingSupervisorHandover,
|
|
11
|
+
acknowledgeDrain = acknowledgeSupervisorDrain } = {}) {
|
|
10
12
|
// Delivery records share this local root across origins. A staging desktop
|
|
11
13
|
// can own the same provider session, so fence every live owner in this root.
|
|
12
14
|
const check = () => checkOwner(dirname(root), null);
|
|
13
15
|
await check();
|
|
14
|
-
const owner = await acquireOwner(dirname(root));
|
|
16
|
+
const owner = await acquireOwner(dirname(root), { handoverVersion: 1 });
|
|
15
17
|
try {
|
|
16
18
|
await check();
|
|
17
|
-
const supervisor = createSupervisor({ root, webOrigin
|
|
19
|
+
const supervisor = createSupervisor({ root, webOrigin,
|
|
20
|
+
shouldYield: async () => Boolean(await pendingHandover(dirname(root), owner.nonce)) });
|
|
18
21
|
const stop = () => { void supervisor.close(); };
|
|
19
22
|
signal?.addEventListener('abort', stop, { once: true });
|
|
20
23
|
try {
|
|
21
24
|
do {
|
|
22
25
|
if (signal?.aborted) break;
|
|
26
|
+
const handover = await pendingHandover(dirname(root), owner.nonce);
|
|
27
|
+
if (handover) {
|
|
28
|
+
await supervisor.drain();
|
|
29
|
+
await acknowledgeDrain(dirname(root), handover, signal);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
23
32
|
await check();
|
|
24
33
|
await supervisor.tick();
|
|
25
34
|
if (once) break;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { join, resolve } from 'node:path';
|
|
2
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { acquireDesktopProcessLock } from '../desktop-process-lock.js';
|
|
5
|
+
import { localRuntimeStatus, installLocalRuntime } from './local-runtime.js';
|
|
6
|
+
import { observeSupervisorOwner, readSupervisorState, saveSupervisorState, supervisorProcessIdentity, supervisorProcessAlive } from './supervisor-owner.js';
|
|
7
|
+
|
|
8
|
+
export const handoverPath = root => join(root, 'supervisor-runtime/handover.json');
|
|
9
|
+
const controlPath = root => join(root, 'supervisor-runtime/control.json');
|
|
10
|
+
const lockControl = root => acquireDesktopProcessLock(join(root, 'supervisor-runtime/control-lock'), {
|
|
11
|
+
alive: supervisorProcessAlive, identity: supervisorProcessIdentity });
|
|
12
|
+
|
|
13
|
+
export async function requestSupervisorHandover({ root, release, observeOwner = observeSupervisorOwner } = {}) {
|
|
14
|
+
const target = resolve(release);
|
|
15
|
+
if ((await lstat(target)).isSymbolicLink() || await realpath(target) !== target) throw new Error('Use a canonical immutable release.');
|
|
16
|
+
const candidate = await localRuntimeStatus({ root, source: target });
|
|
17
|
+
if (target !== join(resolve(root), 'delivery-runtime/releases', candidate.sourceDigest)) throw new Error('Handover target is not a verified installed release.');
|
|
18
|
+
const lock = await lockControl(root);
|
|
19
|
+
try {
|
|
20
|
+
const control = await readSupervisorState(controlPath(root));
|
|
21
|
+
if (!control?.enabled) return { status: 'not_requested', reason: 'supervisor_disabled' };
|
|
22
|
+
const owner = await observeOwner(root);
|
|
23
|
+
const watcher = await readSupervisorState(join(root, 'supervisor-runtime/watchdog.json'));
|
|
24
|
+
// Never signal old/unknown owners, or mistake process liveness for support.
|
|
25
|
+
if (owner.state !== 'running' || owner.handoverVersion !== 1 || !Number.isFinite(owner.lastHeartbeatAt) || owner.heartbeatStale) return {
|
|
26
|
+
status: 'not_requested', reason: 'owner_handover_unavailable', owner };
|
|
27
|
+
if (watcher?.handoverVersion !== 1 || watcher.epoch !== control.epoch || !Number.isFinite(watcher.at) ||
|
|
28
|
+
Date.now() - watcher.at > 30_000) return { status: 'not_requested', reason: 'watcher_handover_unavailable' };
|
|
29
|
+
const watcherOwner = await readSupervisorState(join(root, 'supervisor-runtime/watcher/desktop-app.lock/owner.json'));
|
|
30
|
+
if (!watcherOwner?.nonce || watcherOwner.nonce !== watcher.watcherNonce ||
|
|
31
|
+
await supervisorProcessIdentity(watcherOwner.pid).catch(() => null) !== watcherOwner.startedAt) return {
|
|
32
|
+
status: 'not_requested', reason: 'watcher_identity_unconfirmed' };
|
|
33
|
+
if (control.digest === candidate.sourceDigest) return { status: 'already_selected' };
|
|
34
|
+
const previous = await readSupervisorState(handoverPath(root));
|
|
35
|
+
if (previous && ['requested', 'drained'].includes(previous.status) && previous.epoch === control.epoch) {
|
|
36
|
+
if (previous.target.digest === candidate.sourceDigest) {
|
|
37
|
+
if (previous.status === 'drained' || previous.ownerNonce === owner.nonce) return previous;
|
|
38
|
+
// The original owner died before acknowledging. Rebind only after a
|
|
39
|
+
// fresh explicit retry, retaining the original identity and request.
|
|
40
|
+
const rebound = { ...previous, ownerNonce: owner.nonce, previousOwnerNonce: previous.ownerNonce, reboundAt: Date.now() };
|
|
41
|
+
await saveSupervisorState(handoverPath(root), rebound); return rebound;
|
|
42
|
+
}
|
|
43
|
+
throw new Error('Another supervisor handover is pending.');
|
|
44
|
+
}
|
|
45
|
+
const request = { version: 1, id: randomUUID(), status: 'requested', epoch: control.epoch,
|
|
46
|
+
ownerNonce: owner.nonce, fromDigest: control.digest, requestedAt: Date.now(),
|
|
47
|
+
target: { digest: candidate.sourceDigest, release: target, version: candidate.version } };
|
|
48
|
+
await saveSupervisorState(handoverPath(root), request);
|
|
49
|
+
return request;
|
|
50
|
+
} finally { await lock.release(); }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function pendingSupervisorHandover(root, nonce) {
|
|
54
|
+
const request = await readSupervisorState(handoverPath(root));
|
|
55
|
+
if (request?.version !== 1 || request.status !== 'requested' || request.ownerNonce !== nonce) return null;
|
|
56
|
+
const control = await readSupervisorState(controlPath(root));
|
|
57
|
+
return control?.enabled && control.epoch === request.epoch && control.digest === request.fromDigest ? request : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function acknowledgeSupervisorDrain(root, request, signal) {
|
|
61
|
+
const lock = await lockControl(root);
|
|
62
|
+
try {
|
|
63
|
+
const current = await pendingSupervisorHandover(root, request.ownerNonce);
|
|
64
|
+
if (signal?.aborted || current?.id !== request.id) return false;
|
|
65
|
+
await saveSupervisorState(handoverPath(root), { ...current, status: 'drained', drainedAt: Date.now() });
|
|
66
|
+
return true;
|
|
67
|
+
} finally { await lock.release(); }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A watcher can finish an interrupted activation from the same durable request.
|
|
71
|
+
// installLocalRuntime revalidates epoch, owner absence and release under locks.
|
|
72
|
+
export async function completeSupervisorHandover(root) {
|
|
73
|
+
const request = await readSupervisorState(handoverPath(root));
|
|
74
|
+
if (request?.version !== 1 || request.status !== 'drained') return null;
|
|
75
|
+
const control = await readSupervisorState(controlPath(root));
|
|
76
|
+
if (!control?.enabled || control.epoch !== request.epoch) return null;
|
|
77
|
+
// Installed bundles may omit the lockfile and include dependencies in their
|
|
78
|
+
// fingerprint. Keep that format when activating the already-frozen release.
|
|
79
|
+
const build = await readSupervisorState(join(request.target.release, 'runtime-build.json'));
|
|
80
|
+
const includeDependencies = build?.fingerprintVersion === 2;
|
|
81
|
+
const candidate = await localRuntimeStatus({ root, source: request.target.release, allowMissingLock: true, includeDependencies });
|
|
82
|
+
if (candidate.sourceDigest !== request.target.digest) throw new Error('Supervisor handover runtime changed before activation.');
|
|
83
|
+
return installLocalRuntime({ root, source: request.target.release, handoverId: request.id, allowMissingLock: true, includeDependencies });
|
|
84
|
+
}
|
|
@@ -47,6 +47,7 @@ export async function observeSupervisorOwner(root, { now = Date.now(), alive = s
|
|
|
47
47
|
try { heartbeat = await readSupervisorState(join(supervisorOwnerRoot(root), 'heartbeat.json')); } catch {}
|
|
48
48
|
const lastHeartbeatAt = heartbeat?.nonce === owner.nonce && Number.isFinite(heartbeat.at) ? heartbeat.at : null;
|
|
49
49
|
return { state: 'running', pid: owner.pid, nonce: owner.nonce, lastHeartbeatAt,
|
|
50
|
+
...(heartbeat?.nonce === owner.nonce && heartbeat.handoverVersion === 1 ? { handoverVersion: 1 } : {}),
|
|
50
51
|
heartbeatStale: lastHeartbeatAt !== null && now - lastHeartbeatAt > 60_000 };
|
|
51
52
|
}
|
|
52
53
|
|
|
@@ -54,14 +55,14 @@ export async function observeSupervisorOwner(root, { now = Date.now(), alive = s
|
|
|
54
55
|
// than letting a missing identity probe reclaim a live owner. Task leases and
|
|
55
56
|
// desktop ownership remain separate, and keep their existing authority checks.
|
|
56
57
|
export async function acquireSupervisorOwner(root, { alive = supervisorProcessAlive,
|
|
57
|
-
identity = supervisorProcessIdentity, heartbeatMs = 15_000 } = {}) {
|
|
58
|
+
identity = supervisorProcessIdentity, heartbeatMs = 15_000, handoverVersion } = {}) {
|
|
58
59
|
const directory = supervisorOwnerRoot(root);
|
|
59
60
|
const lock = await acquireDesktopProcessLock(directory, { alive, identity });
|
|
60
61
|
const file = join(directory, 'heartbeat.json');
|
|
61
62
|
let pending = Promise.resolve(), closed = false;
|
|
62
63
|
const beat = () => {
|
|
63
64
|
pending = pending.catch(() => {}).then(async () => {
|
|
64
|
-
if (!closed) await saveSupervisorState(file, { version: 1, pid: process.pid, nonce: lock.nonce, at: Date.now() });
|
|
65
|
+
if (!closed) await saveSupervisorState(file, { version: 1, pid: process.pid, nonce: lock.nonce, at: Date.now(), ...(handoverVersion === 1 ? { handoverVersion } : {}) });
|
|
65
66
|
});
|
|
66
67
|
return pending;
|
|
67
68
|
};
|
|
@@ -2,8 +2,10 @@ import { execFile } from 'node:child_process';
|
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
import { mkdir, lstat, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
5
|
+
import { dirname, join, isAbsolute, delimiter } from 'node:path';
|
|
6
6
|
import { pinnedWorkflowSource } from './local-runtime.js';
|
|
7
|
+
import { inspectBackendExecutable, backendCandidates, CODEX_WRITER_LOCK_MINIMUM_VERSION } from '../backend-executable.js';
|
|
8
|
+
import { runSceneReasoning } from '../codex-plugins/simy/hooks/scene-reasoning.mjs';
|
|
7
9
|
import { readSupervisorState } from './supervisor-owner.js';
|
|
8
10
|
|
|
9
11
|
const exec = promisify(execFile);
|
|
@@ -12,9 +14,12 @@ const xml = text => String(text).replaceAll('&', '&').replaceAll('<', '<'
|
|
|
12
14
|
const systemdArgument = text => `"${String(text).replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%').replaceAll('$', () => '$$')}"`;
|
|
13
15
|
const windowsArgument = text => `"${String(text).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, '$1$1')}"`;
|
|
14
16
|
|
|
15
|
-
export function supervisorServiceDefinition({ root, entry, node = process.execPath, platform = process.platform, home = homedir() }) {
|
|
17
|
+
export function supervisorServiceDefinition({ root, entry, node = process.execPath, platform = process.platform, home = homedir(), providerEnvironment = {} }) {
|
|
16
18
|
if ([root, entry, node].some(value => typeof value !== 'string' || /[\r\n\0]/.test(value))) throw new Error('Invalid supervisor service path.');
|
|
17
|
-
const
|
|
19
|
+
const environment = Object.fromEntries(Object.entries(providerEnvironment).filter(([key]) => ['PATH', 'SIMY_CODEX_BIN', 'SIMY_CODEX_EXPECTED_VERSION'].includes(key)));
|
|
20
|
+
if (Object.values(environment).some(value => typeof value !== 'string' || /[\r\n\0]/.test(value))) throw new Error('Invalid provider environment.');
|
|
21
|
+
const bootstrap = Object.keys(environment).length ? ['--import', `data:text/javascript,${encodeURIComponent(`Object.assign(process.env,${JSON.stringify(environment)})`)}`] : [];
|
|
22
|
+
const args = [...bootstrap, entry, 'delivery', 'runtime', 'supervisor-watch', '--root', root, '--no-auto-update'];
|
|
18
23
|
if (platform === 'linux') {
|
|
19
24
|
const name = 'simy-delivery-supervisor.service';
|
|
20
25
|
return { name, file: join(home, '.config/systemd/user', name),
|
|
@@ -37,13 +42,14 @@ export function supervisorServiceDefinition({ root, entry, node = process.execPa
|
|
|
37
42
|
// Explicit service installation only. Installing/updating a runtime alone
|
|
38
43
|
// never enables this service or enrolls a conversation.
|
|
39
44
|
export async function installSupervisorService({ root, home = homedir(), platform = process.platform,
|
|
40
|
-
run = exec, inspectPin = pinnedWorkflowSource } = {}) {
|
|
45
|
+
run = exec, inspectPin = pinnedWorkflowSource, inspectBackend = inspectBackendExecutable, environment = process.env } = {}) {
|
|
41
46
|
const control = await readSupervisorState(join(root, 'supervisor-runtime/control.json'));
|
|
42
47
|
if (control?.version !== 1 || control.enabled !== true) throw new Error('Enable supervisor recovery explicitly before installing its OS service.');
|
|
43
48
|
await inspectPin({ root });
|
|
44
49
|
const pin = await readSupervisorState(join(root, 'delivery-runtime/current.json'));
|
|
45
50
|
if (!pin || pin.digest !== control.digest || pin.release !== control.release) throw new Error('Supervisor activation no longer matches the fixed runtime.');
|
|
46
|
-
const
|
|
51
|
+
const provider = await supervisorProviderEnvironment({ home, platform, environment, inspectBackend });
|
|
52
|
+
const definition = supervisorServiceDefinition({ root, home, platform, entry: join(pin.release, 'src/index.js'), providerEnvironment: provider.environment });
|
|
47
53
|
try {
|
|
48
54
|
const info = await lstat(definition.file);
|
|
49
55
|
if (!info.isFile() || info.isSymbolicLink() || !(await readFile(definition.file, 'utf8')).includes(marker)) throw new Error('Supervisor service path is not managed by SIMY.');
|
|
@@ -61,5 +67,29 @@ export async function installSupervisorService({ root, home = homedir(), platfor
|
|
|
61
67
|
await run(scheduler, ['/Create', '/TN', definition.name, '/XML', definition.file, '/F']);
|
|
62
68
|
await run(scheduler, ['/Run', '/TN', definition.name]);
|
|
63
69
|
}
|
|
64
|
-
return { ...definition, content: undefined, status: 'registered', scope: 'previously_enrolled_tasks_only' };
|
|
70
|
+
return { ...definition, content: undefined, status: 'registered', execution: 'awaiting_service_observation', provider: provider.inspection, scope: 'previously_enrolled_tasks_only' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
// Resolve and re-probe under exactly the environment passed to the service.
|
|
75
|
+
// This is a prerequisite observation, never proof of task execution/acceptance.
|
|
76
|
+
export async function supervisorProviderEnvironment({ home = homedir(), platform = process.platform, environment = process.env,
|
|
77
|
+
inspectBackend = inspectBackendExecutable, onStep } = {}) {
|
|
78
|
+
return runSceneReasoning({ home, platform, environment }, {
|
|
79
|
+
understand_scene: scene => ({ ...scene, candidates: backendCandidates('codex', { home, platform, environment }).filter(value => typeof value === 'string' && isAbsolute(value)) }),
|
|
80
|
+
derive_perspectives: async scene => ({ ...scene, inspection: await inspectBackend('codex', {
|
|
81
|
+
candidates: scene.candidates, minimumVersion: CODEX_WRITER_LOCK_MINIMUM_VERSION, discoveryOptions: { home, platform, environment },
|
|
82
|
+
}) }),
|
|
83
|
+
prioritize_options: scene => {
|
|
84
|
+
if (!scene.inspection.compatible || !isAbsolute(scene.inspection.executable || '')) throw new Error('No compatible absolute Codex executable for the supervisor service.');
|
|
85
|
+
const selected = scene.inspection.executable;
|
|
86
|
+
return { ...scene, selected, environment: { PATH: [dirname(process.execPath), dirname(selected), environment.PATH || environment.Path || ''].join(delimiter), SIMY_CODEX_BIN: selected, SIMY_CODEX_EXPECTED_VERSION: scene.inspection.installed_version } };
|
|
87
|
+
},
|
|
88
|
+
connect_to_next_check: async scene => {
|
|
89
|
+
const verified = await inspectBackend('codex', { candidates: [scene.selected], minimumVersion: CODEX_WRITER_LOCK_MINIMUM_VERSION,
|
|
90
|
+
discoveryOptions: { home, platform, environment: scene.environment }, executionEnvironment: scene.environment });
|
|
91
|
+
if (!verified.compatible || verified.executable !== scene.selected || verified.installed_version !== scene.inspection.installed_version) throw new Error('Supervisor provider changed during service-environment verification.');
|
|
92
|
+
return { environment: scene.environment, inspection: verified };
|
|
93
|
+
},
|
|
94
|
+
}, { onStep });
|
|
65
95
|
}
|
|
@@ -5,6 +5,7 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|
|
5
5
|
import { acquireDesktopProcessLock, assertNoDesktopHostForOrigin } from '../desktop-process-lock.js';
|
|
6
6
|
import { pinnedWorkflowSource } from './local-runtime.js';
|
|
7
7
|
import { resolveWebOrigin } from '../web-origin.js';
|
|
8
|
+
import { completeSupervisorHandover } from './supervisor-handover.js';
|
|
8
9
|
import { observeSupervisorOwner, supervisorProcessAlive, supervisorProcessIdentity,
|
|
9
10
|
readSupervisorState, saveSupervisorState } from './supervisor-owner.js';
|
|
10
11
|
|
|
@@ -39,17 +40,18 @@ export async function supervisorWatchdogStatus({ root, now = Date.now(), observe
|
|
|
39
40
|
export function createSupervisorWatchdog({ root, inspectPin = verifiedPin, readControl = () => readSupervisorState(controlPath(root)),
|
|
40
41
|
observeOwner = observeSupervisorOwner, checkDesktop = assertNoDesktopHostForOrigin,
|
|
41
42
|
launch = (bin, args, options) => spawn(bin, args, options), now = Date.now,
|
|
43
|
+
completeHandover = completeSupervisorHandover, watcherNonce,
|
|
42
44
|
identity = supervisorProcessIdentity, save = state => saveSupervisorState(statePath(root), state) } = {}) {
|
|
43
45
|
let child = null, childIdentity = null, launchTime = null, closed = false, running = null, lastExit = null;
|
|
44
|
-
let pendingPin = null, lastWall = null, controlEpoch;
|
|
46
|
+
let pendingPin = null, lastWall = null, controlEpoch, handoverRetryAt = 0;
|
|
45
47
|
const clock = () => Math.max(0, now());
|
|
46
48
|
const tick = async () => {
|
|
47
49
|
if (closed) return;
|
|
48
50
|
const at = clock(), control = await readControl();
|
|
49
51
|
controlEpoch = control?.epoch;
|
|
50
52
|
const previous = pendingPin || await readSupervisorState(statePath(root));
|
|
51
|
-
const state = { version: 1, at, action: 'observe', supervisor: null, lastExit,
|
|
52
|
-
lastLaunchAt: launchTime ?? previous?.lastLaunchAt ?? null, epoch:
|
|
53
|
+
const state = { version: 1, handoverVersion: 1, watcherNonce, at, action: 'observe', supervisor: null, lastExit,
|
|
54
|
+
lastLaunchAt: launchTime ?? previous?.lastLaunchAt ?? null, epoch: control?.epoch || null };
|
|
53
55
|
if (!control || control.version !== 1 || control.enabled !== true) { state.action = 'disabled'; await save(state); return state; }
|
|
54
56
|
state.supervisor = await observeOwner(root, { now: at });
|
|
55
57
|
if (state.supervisor.state === 'running') {
|
|
@@ -57,6 +59,15 @@ export function createSupervisorWatchdog({ root, inspectPin = verifiedPin, readC
|
|
|
57
59
|
await save(state); return state;
|
|
58
60
|
}
|
|
59
61
|
if (!['missing', 'dead'].includes(state.supervisor.state)) { state.action = 'owner_unknown'; await save(state); return state; }
|
|
62
|
+
const retryAt = Math.max(handoverRetryAt, previous?.handoverRetryAt || 0);
|
|
63
|
+
if (retryAt > at) { state.action = 'handover_backoff'; state.handoverRetryAt = retryAt; await save(state); return state; }
|
|
64
|
+
try {
|
|
65
|
+
if (await completeHandover(root)) { state.action = 'handover_activated'; await save(state); return state; }
|
|
66
|
+
} catch (error) {
|
|
67
|
+
state.action = 'handover_wait'; state.reason = error.code === 'process_conflict' ? 'owner_not_released' : 'activation_failed';
|
|
68
|
+
handoverRetryAt = state.handoverRetryAt = at + 30_000;
|
|
69
|
+
await save(state); return state;
|
|
70
|
+
}
|
|
60
71
|
if (child && child.exitCode === null && child.signalCode === null) { state.action = 'child_starting'; await save(state); return state; }
|
|
61
72
|
// Persist the launch intent before spawn. On watchdog crash/restart this
|
|
62
73
|
// gives the previous child time to publish its singleton owner receipt.
|
|
@@ -125,7 +136,7 @@ export async function watchSupervisor({ root, signal, once = false, wait = delay
|
|
|
125
136
|
let watchdog, initialEpoch;
|
|
126
137
|
try {
|
|
127
138
|
initialEpoch = (await readSupervisorState(controlPath(root)))?.epoch;
|
|
128
|
-
watchdog = createWatchdog({ root });
|
|
139
|
+
watchdog = createWatchdog({ root, watcherNonce: lock.nonce });
|
|
129
140
|
do {
|
|
130
141
|
if (signal?.aborted) break;
|
|
131
142
|
await watchdog.tick();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { captureDispatchProvenance } from '../update-detachment/provenance.js';
|
|
2
2
|
import { captureDispatchOwner, assertTaskAdmission, dispatchWithFence, queueDispatchReceipt, rootFromDelivery, observeDispatchEvent } from '../update-detachment/dispatch-gate.js';
|
|
3
3
|
import { safeCheckpointDiagnostic } from './checkpoint-diagnostic.js';
|
|
4
|
+
import { continueResultCycle, resultEvent, executeResultCycle, acceptResultEvent } from '../codex-plugins/simy/hooks/result-cycle.mjs';
|
|
5
|
+
import { finalResponseRecoveryInstruction } from '../session-supervision/final-response-recovery.js';
|
|
4
6
|
import { recordHandoffStage } from '../codex-plugins/simy/hooks/consistency-handoff.mjs';
|
|
5
7
|
import { retainControllerEvidence } from '../codex-plugins/simy/hooks/purpose-evidence.mjs';
|
|
6
8
|
import { workspaceIdentity, reviewWorkspaceIdentity } from '../session-supervision/purpose-verification.js';
|
|
@@ -15,9 +17,10 @@ import { requireProviderLink } from '../provider-links.js';
|
|
|
15
17
|
import { spawn } from 'node:child_process';
|
|
16
18
|
import { randomUUID } from 'node:crypto';
|
|
17
19
|
import { dirname } from 'node:path';
|
|
18
|
-
import { hasPendingUserStop, recordPath, canExecuteAutonomousExternal, automaticContinuationAllowed, settlePurposeVerification, assessedCheckpointContract, checkpointDelivery, decideStop, deliveryProgressContext, deliveryStop, listRecords, readRecord, managedBudgetReason, notifyState, processIdentity, sharesTaskWorkspace, taskDirectory, reconcileRecords, updateRecord, verifyAutomaticCriteria } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
20
|
+
import { hasPendingUserStop, recordPath, canExecuteAutonomousExternal, automaticContinuationAllowed, queuePurposeDiagnosis, settlePurposeVerification, assessedCheckpointContract, checkpointDelivery, decideStop, deliveryProgressContext, deliveryStop, listRecords, readRecord, managedBudgetReason, notifyState, processIdentity, sharesTaskWorkspace, taskDirectory, reconcileRecords, updateRecord, verifyAutomaticCriteria } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
19
21
|
import { resolveBackendExecutable } from '../backend-executable.js';
|
|
20
|
-
import {
|
|
22
|
+
import { safeProviderFailure, providerEnvironmentIdentity, checkProviderRetry } from '../session-supervision/provider-failure.js';
|
|
23
|
+
import { executeCodexAppServer, inspectCodexAppServer } from '../session-supervision/codex-app-server.js';
|
|
21
24
|
import { acquireWorkspaceLease, leaseOwnerDead } from '../session-supervision/workspace-lease.js';
|
|
22
25
|
|
|
23
26
|
const MAX_OUTPUT = 64 * 1024;
|
|
@@ -150,7 +153,7 @@ export async function executeCodexResume(record, { signal, root, leaseId, execut
|
|
|
150
153
|
});
|
|
151
154
|
}
|
|
152
155
|
|
|
153
|
-
export async function tickManagedTasks({ root, webOrigin, now = Date.now(), execute, inspectProcess = processIdentity, signal, beforeClaim, authorizeDispatch, readRecords, observeTurn, observeAuthentication } = {}) {
|
|
156
|
+
export async function tickManagedTasks({ root, webOrigin, now = Date.now(), execute, inspectProcess = processIdentity, signal, beforeClaim, authorizeDispatch, readRecords, observeTurn, observeAuthentication, inspectProvider = inspectCodexAppServer, shouldYield } = {}) {
|
|
154
157
|
const tickWallStart = Date.now();
|
|
155
158
|
const currentTime = () => now + Math.max(0, Date.now() - tickWallStart);
|
|
156
159
|
if (signal?.aborted) return;
|
|
@@ -160,7 +163,7 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
160
163
|
if (signal?.aborted) return;
|
|
161
164
|
const records = await readCandidates();
|
|
162
165
|
for (let record of records) {
|
|
163
|
-
if (signal?.aborted) break;
|
|
166
|
+
if (signal?.aborted || await shouldYield?.()) break;
|
|
164
167
|
const claimNow = currentTime();
|
|
165
168
|
if (!record.run?.managed) continue;
|
|
166
169
|
record = await recoverOutcomeAuditLease(record, { root, inspectProcess });
|
|
@@ -198,6 +201,24 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
198
201
|
}
|
|
199
202
|
if (!automaticContinuationAllowed(record)) continue;
|
|
200
203
|
try { await requireProviderLink({ root, provider: record.provider }); } catch { continue; }
|
|
204
|
+
if (run.managed.providerCapabilityWait) {
|
|
205
|
+
if (run.state !== 'recovery_required' || run.managed.resumeLease || managedBudgetReason(run, claimNow)) continue;
|
|
206
|
+
const expected = run.managed.providerCapabilityWait;
|
|
207
|
+
let check;
|
|
208
|
+
try { check = await checkProviderRetry(expected, inspectProvider, { now: claimNow, signal }); } catch { continue; }
|
|
209
|
+
record = await updateRecord(root, record.provider, record.session, current => {
|
|
210
|
+
if (current?.run?.id !== run.id || !automaticContinuationAllowed(current) || current.run.state !== 'recovery_required' ||
|
|
211
|
+
current.run.managed.resumeLease || JSON.stringify(current.run.managed.providerCapabilityWait) !== JSON.stringify(expected)) return current;
|
|
212
|
+
if (check.retry) {
|
|
213
|
+
delete current.run.managed.providerCapabilityWait;
|
|
214
|
+
current.run.managed.autoRecovery = true; current.run.recoveryCause = 'session_supervision';
|
|
215
|
+
current.run.managed.nextAttemptAt = claimNow;
|
|
216
|
+
} else current.run.managed.providerCapabilityWait = check.wait;
|
|
217
|
+
return current;
|
|
218
|
+
});
|
|
219
|
+
run = record.run;
|
|
220
|
+
if (run.managed.providerCapabilityWait) continue;
|
|
221
|
+
}
|
|
201
222
|
const appServer = run.managed.resumeChannel === 'codex_app_server';
|
|
202
223
|
const causeAllowed = run.recoveryCause === 'process_exit' || appServer && ['session_supervision', 'approved_action'].includes(run.recoveryCause);
|
|
203
224
|
const approvedWait = run.managed.decision?.outcome === 'wait' && ['read', 'local_edit', 'local_test', 'external'].includes(run.managed.decision.action?.kind) &&
|
|
@@ -228,6 +249,7 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
228
249
|
let claimed;
|
|
229
250
|
try { claimed = await updateRecord(root, record.provider, record.session, current => {
|
|
230
251
|
const live = current?.run;
|
|
252
|
+
if (live?.id !== run.id || live.revision !== run.revision || live.managed?.decision?.id !== run.managed?.decision?.id || live.managed?.providerCapabilityWait) return current;
|
|
231
253
|
if (!automaticContinuationAllowed(current)) return current;
|
|
232
254
|
if (live?.managed?.outcomeAudit?.lease) return current;
|
|
233
255
|
const appChannel = live?.managed?.resumeChannel === 'codex_app_server';
|
|
@@ -245,6 +267,12 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
245
267
|
action: live.managed.decision?.action || live.managed.proposedAction,
|
|
246
268
|
budgetReservation: { rootTaskId: live.managed.taskId, tokens: live.managed.maxTokens ? Math.max(0, live.managed.maxTokens - (live.managed.tokensUsed || 0)) : null, state: 'reserved', epoch: leaseId }, phase: 'claimed', claimedAt: currentTime(), lastActivityAt: currentTime() };
|
|
247
269
|
live.managed.executionAttempt.recoveryIncidentId = live.managed.recovery?.currentId || null;
|
|
270
|
+
const pendingResult = live.resultCycle?.events.find(row => row.id === live.resultCycle.pendingId);
|
|
271
|
+
// Carry the actual preceding result into its dispatch receipt. A synthetic
|
|
272
|
+
// initial/condition event is needed only when no unconsumed result exists.
|
|
273
|
+
if (!pendingResult || pendingResult.dispatchAttemptId) acceptResultEvent(current, resultEvent(current, {
|
|
274
|
+
kind: 'condition_changed', origin: 'supervisor_claim',
|
|
275
|
+
sourceId: `dispatch:${live.managed.decision?.id}:${live.revision}`, now: currentTime() }));
|
|
248
276
|
// Retain the repair action identity during independent verification.
|
|
249
277
|
// Older receipts can still be executing after a verifier; bind those to
|
|
250
278
|
// the next actual diagnosis/repair only after all lease/authority fences.
|
|
@@ -318,7 +346,8 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
318
346
|
if (!result) {
|
|
319
347
|
await authorizeClaim();
|
|
320
348
|
const runner = execute || (appServer ? executeCodexAppServer : executeCodexResume);
|
|
321
|
-
result = await
|
|
349
|
+
result = await executeResultCycle({ record: claimed, attemptId: leaseId, execute: runner, persist: persistReceipt,
|
|
350
|
+
options: { signal: controller.signal, root, webOrigin, leaseId, currentTime, leaseMonitored: true,
|
|
322
351
|
onStarted: async observation => {
|
|
323
352
|
await persistReceipt(current => {
|
|
324
353
|
const attempt = current?.run?.managed?.executionAttempt;
|
|
@@ -376,7 +405,7 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
376
405
|
return current;
|
|
377
406
|
});
|
|
378
407
|
await authorizeClaim();
|
|
379
|
-
} });
|
|
408
|
+
} } });
|
|
380
409
|
}
|
|
381
410
|
}
|
|
382
411
|
} catch (error) { result = error?.code === 'provider_link_disconnected'
|
|
@@ -384,8 +413,13 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
384
413
|
: error?.code === 'desktop_access_required'
|
|
385
414
|
? { code: -3, busy: true, accessDenied: true }
|
|
386
415
|
: { code: controller.signal.aborted ? -2 : -1, cancelled: controller.signal.aborted,
|
|
387
|
-
providerTurnId: error?.providerTurnId, providerSessionId:error?.providerSessionId, question: error?.question,
|
|
416
|
+
providerTurnId: error?.providerTurnId, providerSessionId:error?.providerSessionId, question: error?.question,
|
|
417
|
+
finalResponseWithoutCheckpoint: error?.finalResponseWithoutCheckpoint,
|
|
418
|
+
errorCode: error?.code || 'provider_error', diagnostic: safeProviderFailure(error?.diagnostic), restorationUnverified: error?.restorationUnverified === true }; }
|
|
388
419
|
try {
|
|
420
|
+
if (!dispatched && ['unsupported', 'unavailable'].includes(result.errorCode) && ['writer_lock', 'provider_available', 'service_identity', 'bounded_history', 'connector_inventory', 'connector_pagination', 'review_configuration', 'review_isolation'].includes(result.diagnostic?.capability)) {
|
|
421
|
+
try { result.providerEnvironment = await providerEnvironmentIdentity(inspectProvider); } catch { /* No guessed identity: normal bounded failure path remains. */ }
|
|
422
|
+
}
|
|
389
423
|
await usageQueue;
|
|
390
424
|
if (usageError) result = { ...result, code: -1, errorCode: 'usage_unavailable' };
|
|
391
425
|
usage ||= result?.usage || null;
|
|
@@ -435,6 +469,10 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
435
469
|
}
|
|
436
470
|
if (result.errorCode === 'checkpoint_rejected') live.managed.lastCheckpointRejection = { at: Date.now(), decisionId: claimed.run.managed.decision?.id, diagnostic: result.diagnostic };
|
|
437
471
|
else if (result.code === 0) delete live.managed.lastCheckpointRejection;
|
|
472
|
+
if (result.errorCode === 'checkpoint' && result.finalResponseWithoutCheckpoint) {
|
|
473
|
+
live.managed.finalResponseWithoutCheckpoint = { ...result.finalResponseWithoutCheckpoint,
|
|
474
|
+
providerTurnId: result.providerTurnId, providerSessionId: result.providerSessionId || record.session };
|
|
475
|
+
} else if (result.code === 0) delete live.managed.finalResponseWithoutCheckpoint;
|
|
438
476
|
const attempt = live.managed.executionAttempt;
|
|
439
477
|
if (!result.busy && attempt?.phase === 'dispatching' && attempt.action?.kind !== 'read') live.managed.executionUncertain = {
|
|
440
478
|
attemptId: leaseId, decisionId: attempt.decisionId, action: attempt.action, at: Date.now() };
|
|
@@ -445,8 +483,9 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
445
483
|
checkpointRevision: live.revision, outcome: result.busy ? 'busy' : result.cancelled ? 'cancelled' : result.code === 0 ? 'checkpoint' : 'failed',
|
|
446
484
|
errorCode: result.code === 0 || result.busy || result.cancelled ? null : safeProviderCode(result.errorCode),
|
|
447
485
|
...(result.providerSessionId ? {providerSessionId:result.providerSessionId} : {}),
|
|
486
|
+
...(safeProviderFailure(result.diagnostic) ? { diagnostic: safeProviderFailure(result.diagnostic) } : {}),
|
|
448
487
|
restorationUnverified: result.restorationUnverified === true,
|
|
449
|
-
reason: result.accessDenied ? 'Desktop organization access is required before another provider turn.' : result.busy ? 'Exact provider session currently has another writer.' : result.cancelled ? 'Managed recovery cancelled before completion.' : result.code === 0 ? 'Structured checkpoint recorded.' : 'Provider failed or returned an invalid checkpoint.',
|
|
488
|
+
reason: result.accessDenied ? 'Desktop organization access is required before another provider turn.' : result.busy ? 'Exact provider session currently has another writer.' : result.cancelled ? 'Managed recovery cancelled before completion.' : result.code === 0 ? 'Structured checkpoint recorded.' : result.diagnostic ? `Provider capability unavailable: ${safeProviderFailure(result.diagnostic)?.capability || 'unknown'}. Retry after its environment changes.` : 'Provider failed or returned an invalid checkpoint.',
|
|
450
489
|
nextStep: live.nextStep });
|
|
451
490
|
live.managed.attempts = live.managed.attempts.slice(-20);
|
|
452
491
|
if (result.code !== 0) recordRecoveryStage(live, result.busy ? 'waiting' : 'interrupted', {
|
|
@@ -486,6 +525,10 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
486
525
|
} else if (result.cancelled) {
|
|
487
526
|
live.managed.autoRecovery = false; live.state = 'recovery_required'; live.recoveryCause = 'shutdown_cancelled';
|
|
488
527
|
live.reason = 'Managed recovery was cancelled; inspect the original session before the next bounded recovery.';
|
|
528
|
+
} else if (result.providerEnvironment && !dispatched) {
|
|
529
|
+
live.managed.providerCapabilityWait = { diagnostic: safeProviderFailure(result.diagnostic), environmentIdentity: result.providerEnvironment.identity, nextCheckAt: currentTime() + 60000, checks: 0 };
|
|
530
|
+
live.managed.autoRecovery = true; live.state = 'recovery_required'; live.recoveryCause = 'session_supervision';
|
|
531
|
+
live.reason = 'Provider capability needs an environment change; bounded read-only checks continue without spending another provider turn.';
|
|
489
532
|
} else if (result.code !== 0) {
|
|
490
533
|
live.failures += 1; live.managed.autoRecovery = false;
|
|
491
534
|
live.state = failureLimitReached(live) ? 'failed' : 'recovery_required';
|
|
@@ -493,6 +536,16 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
493
536
|
live.reason = appServer ? providerFailureReason(safeProviderCode(result.errorCode)) +
|
|
494
537
|
(result.restorationUnverified ? ' Original provider permissions also require inspection.' : '') :
|
|
495
538
|
'Codex recovery process failed; inspect its checkpoint and original session before the next bounded recovery.';
|
|
539
|
+
// Recover the missing handoff with a read diagnosis, never by replaying
|
|
540
|
+
// the last action. The existing dispatcher rechecks leases and rights.
|
|
541
|
+
if (appServer && live.state !== 'failed' && automaticContinuationAllowed(current) &&
|
|
542
|
+
!live.managed.pendingApproval && !live.managed.nativeWait && !live.managed.purpose?.nativeBoundary &&
|
|
543
|
+
(result.errorCode === 'checkpoint' && result.finalResponseWithoutCheckpoint || result.errorCode === 'checkpoint_rejected')) {
|
|
544
|
+
await continueResultCycle(current, resultEvent(current, { kind: 'final_response', origin: 'executor_result',
|
|
545
|
+
sourceId: result.providerTurnId || leaseId, attemptId: leaseId, resultRef: `attempt:${leaseId}` }), {
|
|
546
|
+
selectNext: () => queuePurposeDiagnosis(live, finalResponseRecoveryInstruction,
|
|
547
|
+
`missing-handoff:${result.providerTurnId || leaseId}`, Date.now(), 'checkpoint') });
|
|
548
|
+
}
|
|
496
549
|
} else if (live.revision <= run.revision) {
|
|
497
550
|
live.failures += 1;
|
|
498
551
|
live.reason = result.code === 0 ? 'Codex recovery returned without a checkpoint.' : 'Codex recovery failed; inspect the original session.';
|
|
@@ -514,6 +567,9 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
514
567
|
current.run.reason = 'Verified checkpoint recorded; next bounded exact-session attempt is pending.';
|
|
515
568
|
}
|
|
516
569
|
}
|
|
570
|
+
if (result.code !== 0) await continueResultCycle(current, resultEvent(current, {
|
|
571
|
+
kind: 'failure', origin: 'executor_result', sourceId: result.providerTurnId || leaseId,
|
|
572
|
+
attemptId: leaseId, resultRef: `attempt:${leaseId}` }));
|
|
517
573
|
current.run.updatedAt = Date.now(); notifyState(current.run, current.run.updatedAt); return current;
|
|
518
574
|
});
|
|
519
575
|
// Settling uncertainty must precede unlocking: otherwise another session
|
|
@@ -532,12 +588,13 @@ export function createManagedSupervisor(options) {
|
|
|
532
588
|
if (!auditing) auditing = (async () => {
|
|
533
589
|
const runtime = await desktopRuntimeStatus({ root: dirname(options.root) }).catch(() => ({ state: 'runtime_observation_failed' }));
|
|
534
590
|
await runConsistencyMonitor({ root: options.root, runtime, readRecords: options.readRecords || (() => listRecords(options.root)),
|
|
535
|
-
onAudit: (record, findings, request) => auditConsistencyTask(record, findings, request, {
|
|
591
|
+
onAudit: async (record, findings, request) => closed || await options.shouldYield?.() ? undefined : auditConsistencyTask(record, findings, request, {
|
|
536
592
|
root: options.root, signal: controller.signal, authorize: options.authorizeDispatch || (async () => {}) }) });
|
|
537
593
|
})().catch(() => {}).finally(() => { auditing = null; });
|
|
538
594
|
if (!running) running = tickManagedTasks({ ...options, signal: controller.signal }).finally(() => { running = null; });
|
|
539
595
|
return running;
|
|
540
596
|
},
|
|
597
|
+
async drain() { closed = true; await Promise.allSettled([running, auditing]); },
|
|
541
598
|
async close() { closed = true; controller.abort(); await Promise.allSettled([running, auditing]); },
|
|
542
599
|
};
|
|
543
600
|
}
|