@awak-app/simy-cli 0.6.23 → 0.6.25
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 +53 -0
- package/package.json +3 -3
- package/src/cli-contract.js +9 -1
- package/src/codex-plugins/simy/hooks/acceptance-policy.mjs +5 -0
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +533 -74
- package/src/codex-plugins/simy/hooks/purpose-cycle.mjs +63 -0
- package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +154 -0
- package/src/codex-plugins/simy/hooks/purpose-rule-policy.mjs +34 -0
- package/src/codex-plugins/simy/hooks/purpose-rule-review.mjs +65 -0
- package/src/codex-plugins/simy/hooks/purpose-search.mjs +123 -0
- package/src/codex-plugins/simy/hooks/purpose-workspace.mjs +97 -0
- package/src/codex-plugins/simy/skills/delivery-loop/SKILL.md +19 -5
- package/src/codex-plugins/simy/skills/sqm-loop/SKILL.md +19 -4
- package/src/delivery-hooks/command.js +48 -0
- package/src/delivery-hooks/consistency-monitor.js +126 -0
- package/src/delivery-hooks/desktop-runtime.js +70 -0
- package/src/delivery-hooks/desktop.js +13 -3
- package/src/delivery-hooks/legacy-hook-routing.js +61 -0
- package/src/delivery-hooks/local-runtime.js +123 -0
- package/src/delivery-hooks/local-supervisor.js +25 -0
- package/src/delivery-hooks/runtime-watchdog.js +101 -0
- package/src/delivery-hooks/supervisor.js +90 -22
- package/src/desktop-host.js +24 -1
- package/src/durable-local-task-worker.js +57 -12
- package/src/fp-narrative-image.js +180 -0
- package/src/index.js +1 -1
- package/src/local-task.js +1 -0
- package/src/orchestrator/audit.js +1 -1
- package/src/orchestrator/charter-repair.js +93 -0
- package/src/orchestrator/charter-validation.js +14 -6
- package/src/orchestrator/checklist-continuation.js +1 -1
- package/src/orchestrator/contract.js +1 -3
- package/src/orchestrator/delivery/WORKFLOW.md +33 -60
- package/src/orchestrator/delivery/controls/harness-controls.json +1326 -0
- package/src/orchestrator/delivery/references/delivery-levels.md +17 -44
- package/src/orchestrator/delivery/references/pr-closure.md +2 -3
- package/src/orchestrator/delivery/references/state-and-receipts.md +14 -5
- package/src/orchestrator/delivery/references/ux-specification.md +1 -2
- package/src/orchestrator/delivery-level.js +4 -5
- package/src/orchestrator/independent-audit.js +9 -5
- package/src/orchestrator/instruction.js +1 -0
- package/src/orchestrator/loop.js +55 -18
- package/src/orchestrator/risk.js +1 -1
- package/src/orchestrator/ux-specification.js +2 -2
- package/src/runner.js +4 -0
- package/src/session-supervision/analysis-manager.js +2 -2
- package/src/session-supervision/automatic-continuation.js +6 -5
- package/src/session-supervision/autonomous-upgrade.js +55 -0
- package/src/session-supervision/codex-app-server.js +200 -24
- package/src/session-supervision/consistency-auditor.js +113 -0
- package/src/session-supervision/index.js +38 -11
- package/src/session-supervision/purpose-candidate.js +44 -0
- package/src/session-supervision/purpose-instruction.js +47 -0
- package/src/session-supervision/purpose-observer.js +172 -0
- package/src/session-supervision/purpose-recovery.js +264 -0
- package/src/session-supervision/purpose-verification.js +154 -0
- package/src/sqm/agent-skill.js +6 -3
- package/src/sqm/artifact-paths.js +39 -0
- package/src/sqm/claude-plugin.js +7 -0
- package/src/sqm/command.js +95 -27
- package/src/sqm/history-store.js +3 -3
- package/src/sqm/repository.js +19 -5
- package/src/sqm/workflow-update.js +14 -0
- package/src/workflow-reporting.js +2 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
|
+
import { assertNoDesktopHostForOrigin } from '../desktop-process-lock.js';
|
|
4
|
+
import { createManagedSupervisor } from './supervisor.js';
|
|
5
|
+
|
|
6
|
+
export async function superviseLocalDelivery({ root, once = false, signal,
|
|
7
|
+
createSupervisor = createManagedSupervisor, checkOwner = assertNoDesktopHostForOrigin, wait = delay } = {}) {
|
|
8
|
+
// Delivery records share this local root across origins. A staging desktop
|
|
9
|
+
// can own the same provider session, so fence every live owner in this root.
|
|
10
|
+
const check = () => checkOwner(dirname(root), null);
|
|
11
|
+
await check();
|
|
12
|
+
const supervisor = createSupervisor({ root });
|
|
13
|
+
const stop = () => { void supervisor.close(); };
|
|
14
|
+
signal?.addEventListener('abort', stop, { once: true });
|
|
15
|
+
try {
|
|
16
|
+
do {
|
|
17
|
+
if (signal?.aborted) break;
|
|
18
|
+
await check();
|
|
19
|
+
await supervisor.tick();
|
|
20
|
+
if (once) break;
|
|
21
|
+
try { await wait(3_000, undefined, { signal }); }
|
|
22
|
+
catch (error) { if (!signal?.aborted) throw error; }
|
|
23
|
+
} while (!signal?.aborted);
|
|
24
|
+
} finally { signal?.removeEventListener('abort', stop); await supervisor.close(); }
|
|
25
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
8
|
+
import { assertNoDesktopHostForOrigin } from '../desktop-process-lock.js';
|
|
9
|
+
import { pinnedWorkflowSource, localRuntimeStatus } from './local-runtime.js';
|
|
10
|
+
import { desktopRuntimeStatus } from './desktop-runtime.js';
|
|
11
|
+
import { consistencyMonitorStatus } from './consistency-monitor.js';
|
|
12
|
+
const exec = promisify(execFile);
|
|
13
|
+
async function read(file) { try { return JSON.parse(await readFile(file, 'utf8')); } catch (error) { if (error.code === 'ENOENT') return null; throw error; } }
|
|
14
|
+
async function save(file, value) {
|
|
15
|
+
const temp = `${file}.${randomUUID()}.tmp`;
|
|
16
|
+
try { await writeFile(temp, JSON.stringify(value), { mode: 0o600 }); await rename(temp, file); }
|
|
17
|
+
finally { const { rm } = await import('node:fs/promises'); await rm(temp, { force: true }); }
|
|
18
|
+
}
|
|
19
|
+
async function candidate(root) {
|
|
20
|
+
await pinnedWorkflowSource({ root });
|
|
21
|
+
const pin = await read(join(root, 'delivery-runtime/current.json'));
|
|
22
|
+
const value = await read(join(root, 'desktop-runtime/candidate.json'));
|
|
23
|
+
if (!pin || value?.digest !== pin.digest || value?.version !== pin.version || value.app !== join(root, 'desktop-runtime', `SIMY Local-${pin.digest}.app`)) throw Error('Local desktop candidate identity mismatch.');
|
|
24
|
+
const actual = await localRuntimeStatus({ root, source: join(value.app, 'Contents/Resources/cli') });
|
|
25
|
+
if (actual.sourceDigest !== pin.digest || actual.version !== pin.version) throw Error('Local desktop candidate integrity mismatch.');
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function suspendDesktopWatchdog(root) {
|
|
30
|
+
const file = join(root, 'desktop-runtime/active.json'), active = await read(file);
|
|
31
|
+
if (active) await save(file, { ...active, suspendedBy: 'user_quit', suspendedAt: Date.now() });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function activateLocalDesktop({ root, checkOwner = assertNoDesktopHostForOrigin, run = exec } = {}) {
|
|
35
|
+
const value = await candidate(root);
|
|
36
|
+
await checkOwner(root, null); // A live owner is never killed by activation.
|
|
37
|
+
await run('/usr/bin/codesign', ['--verify', '--deep', '--strict', value.app]);
|
|
38
|
+
await run('/usr/bin/open', ['-n', '-a', value.app]);
|
|
39
|
+
await save(join(root, 'desktop-runtime/active.json'), { ...value, activatedAt: Date.now(), scope: 'previously_opted_in_tasks' });
|
|
40
|
+
return { ...value, activation: 'launch_requested' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// External deterministic observer survives host exits and sleep/wake. Only an
|
|
44
|
+
// explicitly activated candidate may be relaunched, with the normal owner lock.
|
|
45
|
+
// A live mismatched/unhealthy host is reported, never forcibly replaced.
|
|
46
|
+
export async function tickRuntimeWatchdog({ root, now = Date.now(), run = exec,
|
|
47
|
+
checkOwner = assertNoDesktopHostForOrigin, observeHost = desktopRuntimeStatus,
|
|
48
|
+
observeMonitor = consistencyMonitorStatus } = {}) {
|
|
49
|
+
await mkdir(join(root, 'desktop-runtime'), { recursive: true, mode: 0o700 });
|
|
50
|
+
const file = join(root, 'desktop-runtime/watchdog.json');
|
|
51
|
+
const previous = await read(file) || {};
|
|
52
|
+
const state = { lastAttemptAt: now, lastSuccessAt: previous.lastSuccessAt || null, ownerPid: process.pid, launches: previous.launches || 0, lastLaunchAt: previous.lastLaunchAt || null,
|
|
53
|
+
host: null, monitor: null, error: null, action: 'observe' };
|
|
54
|
+
try {
|
|
55
|
+
state.host = await observeHost({ root });
|
|
56
|
+
state.monitor = await observeMonitor(join(root, 'delivery-hooks'), now);
|
|
57
|
+
const active = await read(join(root, 'desktop-runtime/active.json'));
|
|
58
|
+
if (active?.suspendedBy === 'user_quit') state.action = 'user_quit_preserved';
|
|
59
|
+
if (active && !active.suspendedBy && state.host.state !== 'matched' && now - (previous.lastLaunchAt || 0) >= 60_000) {
|
|
60
|
+
try { await checkOwner(root, null); }
|
|
61
|
+
catch (error) { if (error.code !== 'process_conflict') throw error; state.action = 'live_owner_preserved'; }
|
|
62
|
+
if (state.action !== 'live_owner_preserved') {
|
|
63
|
+
const value = await candidate(root);
|
|
64
|
+
// Installing another candidate does not authorize its automatic activation.
|
|
65
|
+
if (value.digest !== active.digest) throw Error('Active desktop pin changed; activate the prepared candidate explicitly.');
|
|
66
|
+
await run('/usr/bin/codesign', ['--verify', '--deep', '--strict', value.app]);
|
|
67
|
+
await run('/usr/bin/open', ['-n', '-a', value.app]);
|
|
68
|
+
state.action = 'relaunch_requested'; state.lastLaunchAt = now; state.launches++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
state.lastSuccessAt = now;
|
|
72
|
+
} catch { state.error = { code: 'runtime_watchdog_failed', at: now }; }
|
|
73
|
+
await save(file, state);
|
|
74
|
+
return state;
|
|
75
|
+
}
|
|
76
|
+
export async function watchLocalRuntime({ root, signal, once = false } = {}) {
|
|
77
|
+
do {
|
|
78
|
+
if (signal?.aborted) break;
|
|
79
|
+
await tickRuntimeWatchdog({ root });
|
|
80
|
+
if (once) break;
|
|
81
|
+
try { await delay(30_000, undefined, { signal }); } catch (error) { if (!signal?.aborted) throw error; }
|
|
82
|
+
} while (!signal?.aborted);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function installRuntimeWatchdog({ root, home = homedir(), run = exec, platform = process.platform } = {}) {
|
|
86
|
+
if (platform !== 'darwin') throw Error('Desktop watchdog installation currently requires macOS.');
|
|
87
|
+
const active = await read(join(root, 'desktop-runtime/active.json'));
|
|
88
|
+
const value = await candidate(root);
|
|
89
|
+
if (!active || active.digest !== value.digest) throw Error('Activate the verified desktop before installing its watchdog.');
|
|
90
|
+
const folder = join(home, 'Library/LaunchAgents'); await mkdir(folder, { recursive: true });
|
|
91
|
+
const label = 'com.awak.simy.local-runtime-watchdog';
|
|
92
|
+
const plist = join(folder, `${label}.plist`);
|
|
93
|
+
const xml = value => String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"');
|
|
94
|
+
const content = `<?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"><dict><key>Label</key><string>${label}</string><key>ProgramArguments</key><array><string>${xml(join(root, 'bin/simy-delivery'))}</string><string>runtime</string><string>watch</string></array><key>EnvironmentVariables</key><dict><key>SIMY_HOME</key><string>${xml(root)}</string></dict><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>ThrottleInterval</key><integer>30</integer></dict></plist>`;
|
|
95
|
+
await writeFile(plist, content, { mode: 0o600 });
|
|
96
|
+
await run('/usr/bin/plutil', ['-lint', plist]);
|
|
97
|
+
const domain = `gui/${process.getuid()}`;
|
|
98
|
+
try { await run('/bin/launchctl', ['bootout', `${domain}/${label}`]); } catch { /* First install has no job. */ }
|
|
99
|
+
await run('/bin/launchctl', ['bootstrap', domain, plist]);
|
|
100
|
+
return { label, plist, scope: 'activated_local_desktop_only' };
|
|
101
|
+
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
+
import { retainControllerEvidence } from '../codex-plugins/simy/hooks/purpose-evidence.mjs';
|
|
2
|
+
import { workspaceIdentity, reviewWorkspaceIdentity } from '../session-supervision/purpose-verification.js';
|
|
3
|
+
import { searchEnabled, failureLimitReached } from '../codex-plugins/simy/hooks/purpose-search.mjs';
|
|
4
|
+
import { reconcilePurposeTasks } from '../session-supervision/purpose-recovery.js';
|
|
5
|
+
import { runConsistencyMonitor } from './consistency-monitor.js';
|
|
6
|
+
import { desktopRuntimeStatus } from './desktop-runtime.js';
|
|
7
|
+
import { auditConsistencyTask } from '../session-supervision/consistency-auditor.js';
|
|
1
8
|
import { minimalSufficientChangeInstruction } from '../codex-plugins/simy/hooks/minimal-sufficient.mjs';
|
|
2
9
|
import { requireProviderLink } from '../provider-links.js';
|
|
3
10
|
import { spawn } from 'node:child_process';
|
|
4
11
|
import { randomUUID } from 'node:crypto';
|
|
5
12
|
import { dirname } from 'node:path';
|
|
6
|
-
import { automaticContinuationAllowed, assessedCheckpointContract, checkpointDelivery, decideStop, deliveryProgressContext, deliveryStop, listRecords, readRecord, managedBudgetReason, notifyState, processIdentity, sharesTaskWorkspace, taskDirectory, reconcileRecords, updateRecord, verifyAutomaticCriteria } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
13
|
+
import { 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';
|
|
7
14
|
import { resolveBackendExecutable } from '../backend-executable.js';
|
|
8
15
|
import { executeCodexAppServer } from '../session-supervision/codex-app-server.js';
|
|
9
16
|
import { acquireWorkspaceLease, leaseOwnerDead } from '../session-supervision/workspace-lease.js';
|
|
10
17
|
|
|
11
18
|
const MAX_OUTPUT = 64 * 1024;
|
|
12
|
-
const SAFE_PROVIDER_CODES = new Set(['identity', 'permission_narrowed', 'permission_restore', 'turn_failed', 'checkpoint',
|
|
13
|
-
'closed', 'timeout', 'unsupported', 'unavailable', 'manual_provider_action', 'checkpoint_rejected', 'provider_approval']);
|
|
19
|
+
const SAFE_PROVIDER_CODES = new Set(['authentication_required', 'identity', 'permission_narrowed', 'permission_restore', 'turn_failed', 'checkpoint',
|
|
20
|
+
'closed', 'timeout', 'unsupported', 'unavailable', 'manual_provider_action', 'checkpoint_rejected', 'provider_approval', 'provider_question', 'usage_unavailable']);
|
|
14
21
|
function safeProviderCode(code) { return SAFE_PROVIDER_CODES.has(code) ? code : 'provider_error'; }
|
|
15
22
|
function providerFailureReason(code) {
|
|
16
23
|
if (code === 'permission_restore') return 'Codex sandbox restoration could not be confirmed; inspect the original conversation before resuming.';
|
|
@@ -133,18 +140,21 @@ export async function executeCodexResume(record, { signal, root, leaseId, execut
|
|
|
133
140
|
});
|
|
134
141
|
}
|
|
135
142
|
|
|
136
|
-
export async function tickManagedTasks({ root, now = Date.now(), execute, inspectProcess = processIdentity, signal, beforeClaim, authorizeDispatch, readRecords } = {}) {
|
|
143
|
+
export async function tickManagedTasks({ root, now = Date.now(), execute, inspectProcess = processIdentity, signal, beforeClaim, authorizeDispatch, readRecords, observeTurn, observeAuthentication } = {}) {
|
|
137
144
|
const tickWallStart = Date.now();
|
|
138
145
|
const currentTime = () => now + Math.max(0, Date.now() - tickWallStart);
|
|
139
146
|
if (signal?.aborted) return;
|
|
140
|
-
const
|
|
141
|
-
|
|
147
|
+
const readCandidates = readRecords || (() => listRecords(root));
|
|
148
|
+
await reconcilePurposeTasks({ root, signal, now, observeTurn, observeAuthentication, readRecords: readCandidates });
|
|
149
|
+
await reconcileRecords({ root, now, ...(inspectProcess ? { inspectProcess } : {}), records: await readCandidates() });
|
|
142
150
|
if (signal?.aborted) return;
|
|
151
|
+
const records = await readCandidates();
|
|
143
152
|
for (let record of records) {
|
|
144
153
|
if (signal?.aborted) break;
|
|
145
154
|
const claimNow = currentTime();
|
|
146
155
|
let run = record.run;
|
|
147
156
|
if (!run?.managed) continue;
|
|
157
|
+
if (run.managed.maxTokens && run.managed.outcomeAudit?.lease) continue;
|
|
148
158
|
if (run.state === 'auto_resuming') {
|
|
149
159
|
const owner = run.managed.resumeLease?.owner;
|
|
150
160
|
const ownerDead = await leaseOwnerDead(owner, inspectProcess);
|
|
@@ -152,9 +162,10 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
152
162
|
if (!ownerDead && (owner || claimNow - run.managed.resumeLease?.claimedAt <= 5 * 60_000)) continue;
|
|
153
163
|
await updateRecord(root, record.provider, record.session, current => {
|
|
154
164
|
if (current?.run?.state !== 'auto_resuming' || current.run.managed.resumeLease?.id !== run.managed.resumeLease?.id) return current;
|
|
155
|
-
current.run.state = 'recovery_required'; current.run.managed.autoRecovery = false;
|
|
165
|
+
current.run.state = 'recovery_required'; current.run.recoveryCause = 'interrupted_owner'; current.run.managed.autoRecovery = false;
|
|
156
166
|
current.run.reason = 'Managed recovery was interrupted; inspect the original session before manual resume.';
|
|
157
167
|
const attempt = current.run.managed.executionAttempt;
|
|
168
|
+
if (attempt?.budgetReservation && attempt.phase === 'dispatching') { attempt.budgetReservation.state = 'unknown'; current.run.managed.unknownConsumption = true; if (current.run.managed.maxTokens) current.run.managed.usageBlocked = true; }
|
|
158
169
|
if (attempt?.phase === 'dispatching' && attempt.action?.kind !== 'read') current.run.managed.executionUncertain = {
|
|
159
170
|
attemptId: attempt.id, decisionId: attempt.decisionId, action: attempt.action, at: claimNow };
|
|
160
171
|
current.run.updatedAt = claimNow; notifyState(current.run, claimNow); return current;
|
|
@@ -165,7 +176,7 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
165
176
|
try { await requireProviderLink({ root, provider: record.provider }); } catch { continue; }
|
|
166
177
|
const appServer = run.managed.resumeChannel === 'codex_app_server';
|
|
167
178
|
const causeAllowed = run.recoveryCause === 'process_exit' || appServer && ['session_supervision', 'approved_action'].includes(run.recoveryCause);
|
|
168
|
-
const approvedWait = run.managed.decision?.outcome === 'wait' && ['read', 'local_edit', 'local_test'].includes(run.managed.decision.action?.kind) &&
|
|
179
|
+
const approvedWait = run.managed.decision?.outcome === 'wait' && ['read', 'local_edit', 'local_test', 'external'].includes(run.managed.decision.action?.kind) &&
|
|
169
180
|
run.managed.approvedAction?.id === run.managed.decision.id && !run.managed.approvedAction.dispatchedAt;
|
|
170
181
|
if (run.managed.executionUncertain && run.managed.recoveryDiagnosisDecisionId !== run.managed.decision?.id) continue;
|
|
171
182
|
if (['pending', 'transferred', 'pending_received'].includes(run.managed.handoff?.status)) continue;
|
|
@@ -196,17 +207,22 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
196
207
|
if (!automaticContinuationAllowed(current)) return current;
|
|
197
208
|
const appChannel = live?.managed?.resumeChannel === 'codex_app_server';
|
|
198
209
|
const allowedCause = live?.recoveryCause === 'process_exit' || appChannel && ['session_supervision', 'approved_action'].includes(live?.recoveryCause);
|
|
199
|
-
const approved = live?.managed?.decision?.outcome === 'wait' && ['read', 'local_edit', 'local_test'].includes(live.managed.decision.action?.kind) &&
|
|
210
|
+
const approved = live?.managed?.decision?.outcome === 'wait' && ['read', 'local_edit', 'local_test', 'external'].includes(live.managed.decision.action?.kind) &&
|
|
200
211
|
live.managed.approvedAction?.id === live.managed.decision.id && !live.managed.approvedAction.dispatchedAt;
|
|
201
212
|
if (!live?.managed?.autoRecovery || live.state !== 'recovery_required' || !allowedCause || live.managed.pendingApproval || live.managed.autonomyPolicy && (!live.managed.decision || live.managed.decision.outcome === 'wait' && !approved) || appChannel && live.managed.nextAttemptAt > currentTime()) return current;
|
|
202
213
|
const stop = deliveryStop(live, currentTime());
|
|
203
214
|
if (stop) {
|
|
204
|
-
live.state = stop.code === 'failure_limit' ? 'failed' : 'paused'; live.reason = stop.reason; live.updatedAt = currentTime(); notifyState(live, live.updatedAt); return current;
|
|
215
|
+
live.state = stop.code === 'purpose_budget' ? 'budget_exhausted' : stop.code === 'failure_limit' ? 'failed' : 'paused'; live.reason = stop.reason; live.updatedAt = currentTime(); notifyState(live, live.updatedAt); return current;
|
|
205
216
|
}
|
|
206
217
|
live.state = 'auto_resuming'; live.managed.resumeLease = { id: leaseId, claimedAt: currentTime(), checkpointRevision: live.revision, decisionId: live.managed.decision?.id || null,
|
|
207
218
|
owner: { pid: process.pid, identity: ownerIdentity } };
|
|
208
219
|
live.managed.executionAttempt = { id: leaseId, decisionId: live.managed.decision?.id || null,
|
|
209
|
-
action: live.managed.decision?.action || live.managed.proposedAction,
|
|
220
|
+
action: live.managed.decision?.action || live.managed.proposedAction,
|
|
221
|
+
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() };
|
|
222
|
+
for (const request of live.managed.consistencyOutbox || []) if (['pending', 'received', 'inspected', 'inspection_failed'].includes(request.stage) && request.targetTask === live.managed.taskId) {
|
|
223
|
+
request.stage = 'received'; request.receivedAt = currentTime(); request.decisionId = live.managed.decision?.id;
|
|
224
|
+
request.leaseId = leaseId;
|
|
225
|
+
}
|
|
210
226
|
live.managed.autoAttempts = (live.managed.autoAttempts || 0) + 1;
|
|
211
227
|
live.updatedAt = currentTime(); return current;
|
|
212
228
|
}); } catch (error) {
|
|
@@ -220,6 +236,15 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
220
236
|
let result;
|
|
221
237
|
let resource;
|
|
222
238
|
let lastActivityAt = currentTime();
|
|
239
|
+
let usage = null, usageQueue = Promise.resolve(), usageError = null;
|
|
240
|
+
// Tool and token notifications can arrive together. Share one in-process
|
|
241
|
+
// write queue so our own receipts do not contend for the record lock.
|
|
242
|
+
let receiptQueue = Promise.resolve();
|
|
243
|
+
const persistReceipt = mutate => {
|
|
244
|
+
const pending = receiptQueue.then(() => updateRecord(root, record.provider, record.session, mutate));
|
|
245
|
+
receiptQueue = pending.catch(() => {});
|
|
246
|
+
return pending;
|
|
247
|
+
};
|
|
223
248
|
let dispatched = false, waitingForTool = false;
|
|
224
249
|
const controller = new AbortController();
|
|
225
250
|
const abort = () => controller.abort();
|
|
@@ -263,6 +288,14 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
263
288
|
await authorizeClaim();
|
|
264
289
|
const runner = execute || (appServer ? executeCodexAppServer : executeCodexResume);
|
|
265
290
|
result = await runner(claimed, { signal: controller.signal, root, leaseId, currentTime, leaseMonitored: true,
|
|
291
|
+
onEvidence: async evidence => {
|
|
292
|
+
await persistReceipt(current => {
|
|
293
|
+
if (current?.run?.managed?.resumeLease?.id !== leaseId || current.run.managed.decision?.id !== evidence.actionId) throw new Error('Evidence capture lease changed.');
|
|
294
|
+
if (searchEnabled(current.run.managed)) retainControllerEvidence(current.run.managed, evidence, currentTime());
|
|
295
|
+
return current;
|
|
296
|
+
});
|
|
297
|
+
},
|
|
298
|
+
onUsage: observed => { usage = observed; usageQueue = persistReceipt(current => { if (current?.run?.managed?.resumeLease?.id === leaseId) current.run.managed.executionAttempt.observedUsage = observed; return current; }).catch(error=>{usageError=error;}); if (claimed.run.managed.maxTokens && (claimed.run.managed.tokensUsed || 0) + observed.tokens >= claimed.run.managed.maxTokens) controller.abort(); },
|
|
266
299
|
onActivity: observation => { lastActivityAt = currentTime(); waitingForTool = observation?.waitingForTool === true; },
|
|
267
300
|
beforeDispatch: async providerSandbox => {
|
|
268
301
|
await assertActiveLease(root, record.provider, record.session, leaseId, controller.signal, currentTime);
|
|
@@ -272,9 +305,13 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
272
305
|
if (!automaticContinuationAllowed(current)) throw new Error('Automatic continuation was disabled before provider dispatch.');
|
|
273
306
|
if (appServer && !live.managed.providerSandbox) {
|
|
274
307
|
if (!providerSandbox || !['readOnly', 'workspaceWrite', 'dangerFullAccess', 'externalSandbox'].includes(providerSandbox.type)) throw new Error('Original Codex sandbox policy is unavailable.');
|
|
275
|
-
live.managed.providerSandbox = { type: providerSandbox.type,
|
|
308
|
+
live.managed.providerSandbox = { type: providerSandbox.type, networkAccess: providerSandbox.networkAccess === true,
|
|
276
309
|
...(providerSandbox.type === 'workspaceWrite' ? { writableRoots: Array.isArray(providerSandbox.writableRoots) ? providerSandbox.writableRoots.filter(value => typeof value === 'string').slice(0, 20) : [] } : {}) };
|
|
277
310
|
}
|
|
311
|
+
const selected=live.managed.plan?.alternatives?.find(row=>row.id===live.managed.decision?.selectedOptionId);
|
|
312
|
+
const ledger=live.managed.purpose?.evidence;
|
|
313
|
+
if(selected?.dependsOn?.some(id=>!ledger?.units?.some(unit=>unit.id===id && unit.status==='verified' && unit.purposeRevision===ledger.purposeRevision))) throw new Error('Required dependency is not independently verified.');
|
|
314
|
+
if (live.managed.decision?.autonomousExternal && !canExecuteAutonomousExternal(live.managed)) throw new Error('Autonomous external authority changed before dispatch.');
|
|
278
315
|
const approved = live.managed.approvedAction;
|
|
279
316
|
live.managed.executionAttempt.phase = 'dispatching';
|
|
280
317
|
live.managed.executionAttempt.dispatchedAt = currentTime();
|
|
@@ -295,13 +332,17 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
295
332
|
: error?.code === 'desktop_access_required'
|
|
296
333
|
? { code: -3, busy: true, accessDenied: true }
|
|
297
334
|
: { code: controller.signal.aborted ? -2 : -1, cancelled: controller.signal.aborted,
|
|
298
|
-
errorCode: error?.code || 'provider_error', restorationUnverified: error?.restorationUnverified === true }; }
|
|
299
|
-
|
|
335
|
+
providerTurnId: error?.providerTurnId, question: error?.question, errorCode: error?.code || 'provider_error', restorationUnverified: error?.restorationUnverified === true }; }
|
|
336
|
+
try {
|
|
337
|
+
await usageQueue;
|
|
338
|
+
if (usageError) result = { ...result, code: -1, errorCode: 'usage_unavailable' };
|
|
339
|
+
usage ||= result?.usage || null;
|
|
300
340
|
if (controller.signal.aborted) result = { ...result, code: -2, cancelled: true };
|
|
301
341
|
if (result.code === 0) {
|
|
302
342
|
try {
|
|
303
|
-
await
|
|
304
|
-
|
|
343
|
+
if (result.verification) await settlePurposeVerification({ root, provider: record.provider, session: record.session, expectedLease: leaseId, review: result.verification, verifySnapshot: current => reviewWorkspaceIdentity(current, root, result.verification, { signal: controller.signal }), now: Date.now() });
|
|
344
|
+
else await checkpointDelivery({ root, provider: record.provider, session: record.session, cwd: record.cwd,
|
|
345
|
+
checkpoint: result.checkpoint || parseAgentCheckpoint(result.output, record.session), expectedLease: leaseId, toolEvidence: result.toolEvidence || [], sourceContext: result.sourceContext, controllerEvidence: result.controllerEvidence, now: Date.now() });
|
|
305
346
|
await updateRecord(root, record.provider, record.session, current => {
|
|
306
347
|
if (current?.run?.managed?.executionAttempt?.id !== leaseId) return current;
|
|
307
348
|
current.run.managed.executionAttempt.phase = 'checkpointed';
|
|
@@ -318,16 +359,27 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
318
359
|
}
|
|
319
360
|
return current;
|
|
320
361
|
});
|
|
321
|
-
} catch { result = { code: -1, errorCode: 'checkpoint_rejected' }; }
|
|
362
|
+
} catch (error) { result = { ...result, code: -1, errorCode: 'checkpoint_rejected' }; }
|
|
322
363
|
}
|
|
323
364
|
await updateRecord(root, record.provider, record.session, async current => {
|
|
324
365
|
const live = current?.run;
|
|
325
366
|
if (live?.managed?.resumeLease?.id !== leaseId) return current;
|
|
326
367
|
delete live.managed.resumeLease;
|
|
368
|
+
if (searchEnabled(live.managed) && !result.busy && (dispatched || result.code === 0)) live.continuations += 1;
|
|
369
|
+
if (live.managed.executionAttempt?.budgetReservation) live.managed.executionAttempt.budgetReservation.state = usage && result.code===0 ? 'settled' : dispatched ? 'unknown' : 'released';
|
|
370
|
+
if (dispatched && (!usage || result.code!==0)) {live.managed.unknownConsumption = true;if(live.managed.maxTokens) live.managed.usageBlocked=true;}
|
|
371
|
+
if (usage) {
|
|
372
|
+
live.managed.tokensUsed = (live.managed.tokensUsed || 0) + usage.tokens;
|
|
373
|
+
live.managed.usageAvailable = true;
|
|
374
|
+
live.managed.lastUsage = { ...usage, attemptId: leaseId };
|
|
375
|
+
} else if (!result.busy && live.managed.maxTokens && dispatched) {
|
|
376
|
+
live.managed.usageAvailable = false; live.managed.usageBlocked = true;
|
|
377
|
+
}
|
|
327
378
|
const attempt = live.managed.executionAttempt;
|
|
328
379
|
if (!result.busy && attempt?.phase === 'dispatching' && attempt.action?.kind !== 'read') live.managed.executionUncertain = {
|
|
329
380
|
attemptId: leaseId, decisionId: attempt.decisionId, action: attempt.action, at: Date.now() };
|
|
330
381
|
live.managed.attempts ||= [];
|
|
382
|
+
if (live.managed.purpose?.enabled && result.providerTurnId) live.managed.purpose.lastOwnedTurnId = result.providerTurnId;
|
|
331
383
|
live.managed.attempts.push({ id: leaseId, startedAt: claimed.run.managed.resumeLease.claimedAt, endedAt: Date.now(),
|
|
332
384
|
checkpointRevision: live.revision, outcome: result.busy ? 'busy' : result.cancelled ? 'cancelled' : result.code === 0 ? 'checkpoint' : 'failed',
|
|
333
385
|
errorCode: result.code === 0 || result.busy || result.cancelled ? null : safeProviderCode(result.errorCode),
|
|
@@ -354,15 +406,24 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
354
406
|
live.state = 'recovery_required'; live.recoveryCause = 'session_supervision';
|
|
355
407
|
live.reason = result.accessDenied ? 'Sign in to a SIMY organization with SQM and Developer access before continuing.' : 'Original Codex conversation is in use; SIMY will retry after the writer lock becomes available.';
|
|
356
408
|
live.managed.nextAttemptAt = Date.now() + 15_000;
|
|
409
|
+
} else if (result.errorCode === 'authentication_required') {
|
|
410
|
+
live.state = 'blocked'; live.blocker = 'external_dependency'; live.blockerSource = 'connection'; live.managed.autoRecovery = false;
|
|
411
|
+
live.managed.authenticationWait = { since: Date.now(), nextCheckAt: Date.now() + 60_000 };
|
|
412
|
+
live.reason = 'The configured provider is not logged in. SIMY will recheck account availability and resume diagnosis when restored within the existing budget.';
|
|
413
|
+
} else if (result.errorCode === 'provider_question') {
|
|
414
|
+
live.state = 'blocked'; live.blocker = 'missing_information'; live.blockerSource = 'question'; live.managed.autoRecovery = false;
|
|
415
|
+
live.managed.userQuestion = result.question || 'Inspect the original conversation for the unanswered provider question.';
|
|
416
|
+
live.reason = 'Investigate the provider question; use existing evidence for routine decisions and preserve genuine user choices.';
|
|
357
417
|
} else if (result.errorCode === 'provider_approval') {
|
|
358
|
-
live.state = 'blocked'; live.blocker = 'authorization'; live.managed.autoRecovery = false;
|
|
418
|
+
live.state = 'blocked'; live.blocker = 'authorization'; live.blockerSource = 'provider'; live.managed.autoRecovery = false;
|
|
419
|
+
live.managed.nativeWait = { turnId: result.providerTurnId || null, observedAt: Date.now() };
|
|
359
420
|
live.reason = 'Codex requires provider approval or user input in the original conversation.';
|
|
360
421
|
} else if (result.cancelled) {
|
|
361
422
|
live.managed.autoRecovery = false; live.state = 'recovery_required'; live.recoveryCause = 'shutdown_cancelled';
|
|
362
423
|
live.reason = 'Managed recovery was cancelled; inspect the original session before manual resume.';
|
|
363
424
|
} else if (result.code !== 0) {
|
|
364
425
|
live.failures += 1; live.managed.autoRecovery = false;
|
|
365
|
-
live.state = live
|
|
426
|
+
live.state = failureLimitReached(live) ? 'failed' : 'recovery_required';
|
|
366
427
|
live.recoveryCause = 'provider_error';
|
|
367
428
|
live.reason = appServer ? providerFailureReason(safeProviderCode(result.errorCode)) +
|
|
368
429
|
(result.restorationUnverified ? ' Original provider permissions also require inspection.' : '') :
|
|
@@ -370,7 +431,7 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
370
431
|
} else if (live.revision <= run.revision) {
|
|
371
432
|
live.failures += 1;
|
|
372
433
|
live.reason = result.code === 0 ? 'Codex recovery returned without a checkpoint.' : 'Codex recovery failed; inspect the original session.';
|
|
373
|
-
live.state = live
|
|
434
|
+
live.state = failureLimitReached(live) ? 'failed' : 'recovery_required';
|
|
374
435
|
live.recoveryCause = 'process_exit';
|
|
375
436
|
} else {
|
|
376
437
|
if (live.checkpoint?.status === 'completed') {
|
|
@@ -393,18 +454,25 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
393
454
|
// Settling uncertainty must precede unlocking: otherwise another session
|
|
394
455
|
// could start a write in the effect/checkpoint persistence gap.
|
|
395
456
|
await resource?.release({ unconfirmed: result.restorationUnverified === true });
|
|
457
|
+
} finally { stopWatch(); signal?.removeEventListener('abort', abort); }
|
|
396
458
|
}
|
|
397
459
|
}
|
|
398
460
|
|
|
399
461
|
export function createManagedSupervisor(options) {
|
|
400
|
-
let running = null, closed = false;
|
|
462
|
+
let running = null, auditing = null, closed = false;
|
|
401
463
|
const controller = new AbortController();
|
|
402
464
|
return {
|
|
403
465
|
tick() {
|
|
404
466
|
if (closed) return Promise.resolve();
|
|
467
|
+
if (!auditing) auditing = (async () => {
|
|
468
|
+
const runtime = await desktopRuntimeStatus({ root: dirname(options.root) }).catch(() => ({ state: 'runtime_observation_failed' }));
|
|
469
|
+
await runConsistencyMonitor({ root: options.root, runtime, readRecords: options.readRecords || (() => listRecords(options.root)),
|
|
470
|
+
onAudit: (record, findings, request) => auditConsistencyTask(record, findings, request, {
|
|
471
|
+
root: options.root, signal: controller.signal, authorize: options.authorizeDispatch || (async () => {}) }) });
|
|
472
|
+
})().catch(() => {}).finally(() => { auditing = null; });
|
|
405
473
|
if (!running) running = tickManagedTasks({ ...options, signal: controller.signal }).finally(() => { running = null; });
|
|
406
474
|
return running;
|
|
407
475
|
},
|
|
408
|
-
async close() { closed = true; controller.abort(); await running; },
|
|
476
|
+
async close() { closed = true; controller.abort(); await Promise.allSettled([running, auditing]); },
|
|
409
477
|
};
|
|
410
478
|
}
|
package/src/desktop-host.js
CHANGED
|
@@ -23,12 +23,16 @@ import { spawn, execFile } from "node:child_process";
|
|
|
23
23
|
import { promisify, isDeepStrictEqual } from "node:util";
|
|
24
24
|
import { createInterface } from "node:readline";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { pathToFileURL } from "node:url";
|
|
26
27
|
import { realpathSync } from "node:fs";
|
|
27
28
|
import { dirname, join } from "node:path";
|
|
28
29
|
import { homedir } from "node:os";
|
|
29
30
|
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
30
31
|
import { startAgent } from "./agent.js";
|
|
31
32
|
import { createRuntimeDiagnostics, captureRuntimeOutput } from "./runtime-diagnostics.js";
|
|
33
|
+
import { suspendDesktopWatchdog } from './delivery-hooks/runtime-watchdog.js';
|
|
34
|
+
import { attestDesktopRuntime } from './delivery-hooks/desktop-runtime.js';
|
|
35
|
+
import { pinnedWorkflowSource } from './delivery-hooks/local-runtime.js';
|
|
32
36
|
import { applyLoginShellPath } from "./login-shell-path.js";
|
|
33
37
|
import { createCliAutoUpdater, readCliPackageInfo } from "./auto-update.js";
|
|
34
38
|
import { createAgentShutdown } from "./shutdown.js";
|
|
@@ -139,6 +143,9 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
139
143
|
throw new Error("Desktop update supervisor is no longer running.");
|
|
140
144
|
}
|
|
141
145
|
const lock = candidate ? null : await acquireDesktopProcessLock(root, { webOrigin });
|
|
146
|
+
if (lock && !dependencies.startAgent) {
|
|
147
|
+
try { await attestDesktopRuntime({ root }); } catch (error) { await lock.release(); throw error; }
|
|
148
|
+
}
|
|
142
149
|
const supervisorToken = lock?.nonce || env.SIMY_DESKTOP_SUPERVISOR_TOKEN;
|
|
143
150
|
const deliveryRoot = join(root, "delivery-hooks");
|
|
144
151
|
const recordReader = createRecordReader(deliveryRoot);
|
|
@@ -970,6 +977,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
970
977
|
const status = await agent.getDesktopStatus();
|
|
971
978
|
if ((status.tasks?.activeCount || 0) + (status.sqmTasks?.activeCount || 0) > 0) throw new Error("Tasks are still running. Confirm before quitting.");
|
|
972
979
|
}
|
|
980
|
+
if (command.forUpdate !== true) await suspendDesktopWatchdog(root);
|
|
973
981
|
// Cancel first, then acknowledge. The native app waits for the host
|
|
974
982
|
// exit; this deadline also covers a stuck remote flush/discovery.
|
|
975
983
|
const deadline = setTimeout(async () => {
|
|
@@ -1455,7 +1463,7 @@ export function isDesktopHostEntry(entry = process.argv[1]) {
|
|
|
1455
1463
|
catch { return false; }
|
|
1456
1464
|
}
|
|
1457
1465
|
|
|
1458
|
-
|
|
1466
|
+
export function startDesktopEntry() {
|
|
1459
1467
|
let host;
|
|
1460
1468
|
const stop = () => { void host?.close().finally(() => process.exit(0)); };
|
|
1461
1469
|
process.once("SIGTERM", stop); process.once("SIGINT", stop);
|
|
@@ -1476,3 +1484,18 @@ if (isDesktopHostEntry()) {
|
|
|
1476
1484
|
process.exitCode = 1;
|
|
1477
1485
|
});
|
|
1478
1486
|
}
|
|
1487
|
+
|
|
1488
|
+
if (isDesktopHostEntry()) {
|
|
1489
|
+
try {
|
|
1490
|
+
const root = process.env.SIMY_HOME?.trim() || join(homedir(), '.simy');
|
|
1491
|
+
const selected = await pinnedWorkflowSource({ root });
|
|
1492
|
+
if (selected) {
|
|
1493
|
+
process.env.SIMY_AUTO_UPDATE = '0';
|
|
1494
|
+
const selectedHost = await import(pathToFileURL(join(selected, 'src/desktop-host.js')).href);
|
|
1495
|
+
selectedHost.startDesktopEntry();
|
|
1496
|
+
} else startDesktopEntry();
|
|
1497
|
+
} catch {
|
|
1498
|
+
process.stdout.write(JSON.stringify({ type: 'error', message: 'Local desktop runtime integrity could not be verified. Inspect delivery runtime status; the previous task state was retained.' }) + '\n');
|
|
1499
|
+
process.exitCode = 1;
|
|
1500
|
+
}
|
|
1501
|
+
}
|