@awak-app/simy-cli 0.7.15 → 0.7.17
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 +225 -98
- package/package.json +5 -3
- package/src/agent.js +129 -7
- package/src/backend-executable.js +7 -6
- package/src/bounded-local-task-subtask-pool.js +32 -6
- package/src/bounded-local-task-subtasks-contract.js +1 -1
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/.codex-plugin/plugin.json +2 -2
- package/src/codex-plugins/simy/hooks/README.md +8 -3
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +302 -78
- package/src/codex-plugins/simy/hooks/hooks.json +1 -1
- package/src/codex-plugins/simy/hooks/loop-kernel/conformance.json +136 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/index.mjs +42 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/outcome-review.mjs +31 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/pin.json +6 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/policy.json +141 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/task-pool.mjs +34 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/work-frontier.mjs +132 -0
- package/src/codex-plugins/simy/hooks/loop-kernel.mjs +6 -0
- package/src/codex-plugins/simy/hooks/outcome-progress.mjs +16 -5
- package/src/codex-plugins/simy/hooks/purpose-completion-contract.mjs +10 -0
- package/src/codex-plugins/simy/hooks/purpose-cycle.mjs +12 -11
- package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +28 -38
- 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 +166 -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/codex-plugins/simy/hooks/work-frontier.mjs +2 -0
- package/src/codex-plugins/simy/skills/diagnose/SKILL.md +10 -4
- package/src/coding-run-recovery.js +4 -2
- package/src/delivery-hooks/claude-workflow.md +10 -7
- package/src/delivery-hooks/command.js +76 -10
- package/src/delivery-hooks/consistency-monitor.js +20 -4
- package/src/delivery-hooks/consult.js +60 -0
- package/src/delivery-hooks/diagnosis.js +2 -2
- package/src/delivery-hooks/legacy-hook-routing.js +46 -8
- package/src/delivery-hooks/local-runtime.js +81 -1
- package/src/delivery-hooks/local-supervisor.js +13 -4
- package/src/delivery-hooks/repair-continuation.js +80 -0
- package/src/delivery-hooks/repair.js +164 -0
- 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 +38 -6
- package/src/delivery-hooks/supervisor-watchdog.js +15 -4
- package/src/delivery-hooks/supervisor.js +151 -18
- package/src/desktop-access.js +2 -1
- package/src/desktop-host.js +35 -2
- package/src/durable-local-task-worker.js +372 -58
- package/src/execution-capability-contract.js +15 -1
- package/src/index.js +3 -3
- package/src/local-photos-read.swift +33 -0
- package/src/local-photos.js +47 -0
- package/src/local-task-attachment-store.js +31 -0
- package/src/local-task-destination.js +174 -0
- package/src/local-task.js +119 -7
- package/src/native-delegation-contract.js +68 -0
- package/src/native-provider-identity.js +60 -0
- package/src/orchestrator/audit.js +24 -323
- package/src/orchestrator/budget.js +22 -11
- 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 +15 -164
- package/src/orchestrator/instruction.js +19 -254
- package/src/orchestrator/loop.js +35 -95
- package/src/orchestrator/outcome-monitor.js +21 -0
- package/src/orchestrator/pr-closure.js +2 -16
- package/src/orchestrator/recovery.js +32 -2
- package/src/orchestrator/ux-specification.js +3 -12
- package/src/provider-delegation.js +336 -0
- package/src/runner.js +5 -2
- package/src/session-supervision/acceptance-reads.js +34 -0
- package/src/session-supervision/artifact-observer.js +138 -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 +61 -22
- package/src/session-supervision/completion-artifacts.js +44 -0
- package/src/session-supervision/consistency-auditor.js +8 -11
- package/src/session-supervision/continuation-analysis.js +47 -28
- package/src/session-supervision/final-response-recovery.js +5 -0
- package/src/session-supervision/index.js +22 -5
- 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 +32 -12
- package/src/session-supervision/purpose-verification.js +50 -216
- package/src/session-supervision/recovery-frontier.js +55 -0
- package/src/session-supervision/stop-notifications.js +8 -8
- package/src/sqm/agent-skill.js +1 -1
- package/src/sqm/checkers.js +12 -6
- package/src/sqm/claude-plugin-update.js +22 -8
- package/src/sqm/claude-plugin.js +6 -0
- package/src/sqm/local-tests.js +2 -1
- package/src/sqm/plugin-cache.js +67 -0
- package/src/sqm/proof.js +25 -0
- package/src/sqm/report.js +1 -0
- package/src/sqm/workflow-onboarding.js +3 -39
- package/src/sqm/workflow-update.js +17 -6
- package/src/startup-preflight.js +2 -20
- package/src/team-status-fence-store.js +32 -0
- package/src/team-threads.js +457 -0
|
@@ -2,8 +2,11 @@ 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
|
+
import { installPinnedCliEntry } from './legacy-hook-routing.js';
|
|
6
7
|
import { pinnedWorkflowSource } from './local-runtime.js';
|
|
8
|
+
import { inspectBackendExecutable, backendCandidates, CODEX_WRITER_LOCK_MINIMUM_VERSION } from '../backend-executable.js';
|
|
9
|
+
import { runSceneReasoning } from '../codex-plugins/simy/hooks/scene-reasoning.mjs';
|
|
7
10
|
import { readSupervisorState } from './supervisor-owner.js';
|
|
8
11
|
|
|
9
12
|
const exec = promisify(execFile);
|
|
@@ -12,9 +15,12 @@ const xml = text => String(text).replaceAll('&', '&').replaceAll('<', '<'
|
|
|
12
15
|
const systemdArgument = text => `"${String(text).replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%').replaceAll('$', () => '$$')}"`;
|
|
13
16
|
const windowsArgument = text => `"${String(text).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, '$1$1')}"`;
|
|
14
17
|
|
|
15
|
-
export function supervisorServiceDefinition({ root, entry, node = process.execPath, platform = process.platform, home = homedir() }) {
|
|
18
|
+
export function supervisorServiceDefinition({ root, entry, node = process.execPath, platform = process.platform, home = homedir(), providerEnvironment = {} }) {
|
|
16
19
|
if ([root, entry, node].some(value => typeof value !== 'string' || /[\r\n\0]/.test(value))) throw new Error('Invalid supervisor service path.');
|
|
17
|
-
const
|
|
20
|
+
const environment = Object.fromEntries(Object.entries(providerEnvironment).filter(([key]) => ['PATH', 'SIMY_CODEX_BIN', 'SIMY_CODEX_EXPECTED_VERSION'].includes(key)));
|
|
21
|
+
if (Object.values(environment).some(value => typeof value !== 'string' || /[\r\n\0]/.test(value))) throw new Error('Invalid provider environment.');
|
|
22
|
+
const bootstrap = Object.keys(environment).length ? ['--import', `data:text/javascript,${encodeURIComponent(`Object.assign(process.env,${JSON.stringify(environment)})`)}`] : [];
|
|
23
|
+
const args = [...bootstrap, entry, 'delivery', 'runtime', 'supervisor-watch', '--root', root, '--no-auto-update'];
|
|
18
24
|
if (platform === 'linux') {
|
|
19
25
|
const name = 'simy-delivery-supervisor.service';
|
|
20
26
|
return { name, file: join(home, '.config/systemd/user', name),
|
|
@@ -37,17 +43,19 @@ export function supervisorServiceDefinition({ root, entry, node = process.execPa
|
|
|
37
43
|
// Explicit service installation only. Installing/updating a runtime alone
|
|
38
44
|
// never enables this service or enrolls a conversation.
|
|
39
45
|
export async function installSupervisorService({ root, home = homedir(), platform = process.platform,
|
|
40
|
-
run = exec, inspectPin = pinnedWorkflowSource } = {}) {
|
|
46
|
+
run = exec, inspectPin = pinnedWorkflowSource, inspectBackend = inspectBackendExecutable, environment = process.env } = {}) {
|
|
41
47
|
const control = await readSupervisorState(join(root, 'supervisor-runtime/control.json'));
|
|
42
48
|
if (control?.version !== 1 || control.enabled !== true) throw new Error('Enable supervisor recovery explicitly before installing its OS service.');
|
|
43
49
|
await inspectPin({ root });
|
|
44
50
|
const pin = await readSupervisorState(join(root, 'delivery-runtime/current.json'));
|
|
45
51
|
if (!pin || pin.digest !== control.digest || pin.release !== control.release) throw new Error('Supervisor activation no longer matches the fixed runtime.');
|
|
46
|
-
const
|
|
52
|
+
const provider = await supervisorProviderEnvironment({ home, platform, environment, inspectBackend });
|
|
53
|
+
const definition = supervisorServiceDefinition({ root, home, platform, entry: join(root, 'bin/simy-runtime.mjs'), providerEnvironment: provider.environment });
|
|
47
54
|
try {
|
|
48
55
|
const info = await lstat(definition.file);
|
|
49
56
|
if (!info.isFile() || info.isSymbolicLink() || !(await readFile(definition.file, 'utf8')).includes(marker)) throw new Error('Supervisor service path is not managed by SIMY.');
|
|
50
57
|
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
58
|
+
await installPinnedCliEntry(root);
|
|
51
59
|
await mkdir(dirname(definition.file), { recursive: true, mode: 0o700 });
|
|
52
60
|
await writeFile(definition.file, definition.content, { mode: 0o600 });
|
|
53
61
|
if (platform === 'linux') {
|
|
@@ -61,5 +69,29 @@ export async function installSupervisorService({ root, home = homedir(), platfor
|
|
|
61
69
|
await run(scheduler, ['/Create', '/TN', definition.name, '/XML', definition.file, '/F']);
|
|
62
70
|
await run(scheduler, ['/Run', '/TN', definition.name]);
|
|
63
71
|
}
|
|
64
|
-
return { ...definition, content: undefined, status: 'registered', scope: 'previously_enrolled_tasks_only' };
|
|
72
|
+
return { ...definition, content: undefined, status: 'registered', execution: 'awaiting_service_observation', provider: provider.inspection, scope: 'previously_enrolled_tasks_only' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
// Resolve and re-probe under exactly the environment passed to the service.
|
|
77
|
+
// This is a prerequisite observation, never proof of task execution/acceptance.
|
|
78
|
+
export async function supervisorProviderEnvironment({ home = homedir(), platform = process.platform, environment = process.env,
|
|
79
|
+
inspectBackend = inspectBackendExecutable, onStep } = {}) {
|
|
80
|
+
return runSceneReasoning({ home, platform, environment }, {
|
|
81
|
+
understand_scene: scene => ({ ...scene, candidates: backendCandidates('codex', { home, platform, environment }).filter(value => typeof value === 'string' && isAbsolute(value)) }),
|
|
82
|
+
derive_perspectives: async scene => ({ ...scene, inspection: await inspectBackend('codex', {
|
|
83
|
+
candidates: scene.candidates, minimumVersion: CODEX_WRITER_LOCK_MINIMUM_VERSION, discoveryOptions: { home, platform, environment },
|
|
84
|
+
}) }),
|
|
85
|
+
prioritize_options: scene => {
|
|
86
|
+
if (!scene.inspection.compatible || !isAbsolute(scene.inspection.executable || '')) throw new Error('No compatible absolute Codex executable for the supervisor service.');
|
|
87
|
+
const selected = scene.inspection.executable;
|
|
88
|
+
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 } };
|
|
89
|
+
},
|
|
90
|
+
connect_to_next_check: async scene => {
|
|
91
|
+
const verified = await inspectBackend('codex', { candidates: [scene.selected], minimumVersion: CODEX_WRITER_LOCK_MINIMUM_VERSION,
|
|
92
|
+
discoveryOptions: { home, platform, environment: scene.environment }, executionEnvironment: scene.environment });
|
|
93
|
+
if (!verified.compatible || verified.executable !== scene.selected || verified.installed_version !== scene.inspection.installed_version) throw new Error('Supervisor provider changed during service-environment verification.');
|
|
94
|
+
return { environment: scene.environment, inspection: verified };
|
|
95
|
+
},
|
|
96
|
+
}, { onStep });
|
|
65
97
|
}
|
|
@@ -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,10 @@
|
|
|
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
|
+
import { bindCompletionArtifacts } from '../session-supervision/completion-artifacts.js';
|
|
4
|
+
import { recoveryWorkProposal, admitRecoveryWork } from '../session-supervision/recovery-frontier.js';
|
|
3
5
|
import { safeCheckpointDiagnostic } from './checkpoint-diagnostic.js';
|
|
6
|
+
import { continueResultCycle, resultEvent, executeResultCycle, acceptResultEvent } from '../codex-plugins/simy/hooks/result-cycle.mjs';
|
|
7
|
+
import { finalResponseRecoveryInstruction } from '../session-supervision/final-response-recovery.js';
|
|
4
8
|
import { recordHandoffStage } from '../codex-plugins/simy/hooks/consistency-handoff.mjs';
|
|
5
9
|
import { retainControllerEvidence } from '../codex-plugins/simy/hooks/purpose-evidence.mjs';
|
|
6
10
|
import { workspaceIdentity, reviewWorkspaceIdentity } from '../session-supervision/purpose-verification.js';
|
|
@@ -15,10 +19,12 @@ import { requireProviderLink } from '../provider-links.js';
|
|
|
15
19
|
import { spawn } from 'node:child_process';
|
|
16
20
|
import { randomUUID } from 'node:crypto';
|
|
17
21
|
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';
|
|
22
|
+
import { hasPendingUserStop, recordPath, canExecuteAutonomousExternal, automaticContinuationAllowed, queuePurposeDiagnosis, settlePurposeVerification, informationWaitInstruction, assessedCheckpointContract, checkpointDelivery, decideStop, deliveryProgressContext, deliveryStop, listRecords, readRecord, managedBudgetReason, notifyState, processIdentity, sharesTaskWorkspace, taskDirectory, reconcileRecords, updateRecord, verifyAutomaticCriteria } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
19
23
|
import { resolveBackendExecutable } from '../backend-executable.js';
|
|
20
|
-
import {
|
|
24
|
+
import { safeProviderFailure, providerEnvironmentIdentity, checkProviderRetry } from '../session-supervision/provider-failure.js';
|
|
25
|
+
import { executeCodexAppServer, inspectCodexAppServer } from '../session-supervision/codex-app-server.js';
|
|
21
26
|
import { acquireWorkspaceLease, leaseOwnerDead } from '../session-supervision/workspace-lease.js';
|
|
27
|
+
import { createTaskPool } from '../codex-plugins/simy/hooks/loop-kernel.mjs';
|
|
22
28
|
|
|
23
29
|
const MAX_OUTPUT = 64 * 1024;
|
|
24
30
|
const SAFE_PROVIDER_CODES = new Set(['authentication_required', 'identity', 'permission_narrowed', 'permission_restore', 'turn_failed', 'checkpoint',
|
|
@@ -100,7 +106,7 @@ export async function executeCodexResume(record, { signal, root, leaseId, execut
|
|
|
100
106
|
remainingWork: run.managed.remainingWork, acceptanceCriteria: run.managed.acceptanceCriteria, proposedAction: run.managed.proposedAction,
|
|
101
107
|
currentDecision: run.managed.autonomyPolicy ? run.managed.decision : undefined, diagnosis: run.managed.plan?.diagnosis,
|
|
102
108
|
progress: deliveryProgressContext(run) };
|
|
103
|
-
const prompt = run.managed.autonomyPolicy ? `Continue the existing SIMY managed task in this exact session. Context: ${JSON.stringify(context)}\nExecute ONLY currentDecision.action. Its expectedVerification and rollback apply to this turn. Stay within provider permissions. End with ONLY one JSON checkpoint. ${assessedCheckpointContract(run.managed.decision, run.managed)} Do not write SIMY state directly.` : `Continue the existing SIMY managed task in this exact session. Context: ${JSON.stringify(context)}\n${minimalSufficientChangeInstruction()}\nStay within the original task and provider permissions. End with ONLY one JSON object: {"status":"in_progress|completed|blocked|error","summary":"...","nextStep":"...","remainingWork":"...","proposedAction":{"kind":"read|local_edit|local_test|external|destructive|scope_change|unknown","scope":"...","summary":"..."},"evidence":[],"acceptanceMet":false}. For blocked include blocker (authorization, missing_information, external_dependency), and for authorization include approval {kind,scope,summary}. Do not claim completion without checking frozen acceptance. SIMY independently verifies supported criteria. Do not write SIMY state directly. If approval or input is needed, return blocked and stop.`;
|
|
109
|
+
const prompt = run.managed.autonomyPolicy ? `Continue the existing SIMY managed task in this exact session. Context: ${JSON.stringify(context)}\nExecute ONLY currentDecision.action. Its expectedVerification and rollback apply to this turn. Stay within provider permissions. End with ONLY one JSON checkpoint. ${assessedCheckpointContract(run.managed.decision, run.managed)} Do not write SIMY state directly.` : `Continue the existing SIMY managed task in this exact session. Context: ${JSON.stringify(context)}\n${minimalSufficientChangeInstruction()}\nStay within the original task and provider permissions. End with ONLY one JSON object: {"status":"in_progress|completed|blocked|error","summary":"...","nextStep":"...","remainingWork":"...","proposedAction":{"kind":"read|local_edit|local_test|external|destructive|scope_change|unknown","scope":"...","summary":"..."},"evidence":[],"acceptanceMet":false}. ${informationWaitInstruction} For blocked include blocker (authorization, missing_information, external_dependency), and for authorization include approval {kind,scope,summary}. Do not claim completion without checking frozen acceptance. SIMY independently verifies supported criteria. Do not write SIMY state directly. If approval or input is needed, return blocked and stop.`;
|
|
104
110
|
const sandbox = run.managed.autonomyPolicy && run.managed.decision?.action.kind === 'read' ? 'read-only' : run.managed.autoRecoverySandbox;
|
|
105
111
|
const args = ['exec', '--json', '--skip-git-repo-check', '-s', sandbox, '-m', run.managed.resumeModel, 'resume', record.session, prompt];
|
|
106
112
|
if (signal?.aborted) throw new Error('Managed recovery cancelled before provider dispatch.');
|
|
@@ -150,7 +156,7 @@ export async function executeCodexResume(record, { signal, root, leaseId, execut
|
|
|
150
156
|
});
|
|
151
157
|
}
|
|
152
158
|
|
|
153
|
-
export async function tickManagedTasks({ root, webOrigin, now = Date.now(), execute, inspectProcess = processIdentity, signal, beforeClaim, authorizeDispatch, readRecords, observeTurn, observeAuthentication } = {}) {
|
|
159
|
+
export async function tickManagedTasks({ root, webOrigin, now = Date.now(), execute, inspectProcess = processIdentity, signal, beforeClaim, authorizeDispatch, readRecords, observeTurn, observeAuthentication, inspectProvider = inspectCodexAppServer, shouldYield } = {}) {
|
|
154
160
|
const tickWallStart = Date.now();
|
|
155
161
|
const currentTime = () => now + Math.max(0, Date.now() - tickWallStart);
|
|
156
162
|
if (signal?.aborted) return;
|
|
@@ -160,7 +166,7 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
160
166
|
if (signal?.aborted) return;
|
|
161
167
|
const records = await readCandidates();
|
|
162
168
|
for (let record of records) {
|
|
163
|
-
if (signal?.aborted) break;
|
|
169
|
+
if (signal?.aborted || await shouldYield?.()) break;
|
|
164
170
|
const claimNow = currentTime();
|
|
165
171
|
if (!record.run?.managed) continue;
|
|
166
172
|
record = await recoverOutcomeAuditLease(record, { root, inspectProcess });
|
|
@@ -181,9 +187,27 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
181
187
|
const owner = run.managed.resumeLease?.owner;
|
|
182
188
|
const ownerDead = await leaseOwnerDead(owner, inspectProcess);
|
|
183
189
|
// A live/unknown owner is never taken over merely because time passed.
|
|
184
|
-
|
|
185
|
-
|
|
190
|
+
const acceptedCheckpoint = run.managed.executionAttempt?.id === run.managed.resumeLease?.id &&
|
|
191
|
+
run.managed.executionAttempt?.phase === 'checkpointed' &&
|
|
192
|
+
run.managed.executionAttempt?.checkpointRevision === run.revision;
|
|
193
|
+
if (!ownerDead && (owner || !acceptedCheckpoint)) continue;
|
|
194
|
+
await updateRecord(root, record.provider, record.session, async current => {
|
|
186
195
|
if (hasPendingUserStop(root, current) || current?.run?.state !== 'auto_resuming' || current.run.managed.resumeLease?.id !== run.managed.resumeLease?.id) return current;
|
|
196
|
+
const accepted = current.run.managed.executionAttempt;
|
|
197
|
+
if (accepted?.id === current.run.managed.resumeLease?.id && accepted.phase === 'checkpointed' &&
|
|
198
|
+
accepted.checkpointRevision === current.run.revision) {
|
|
199
|
+
// The checkpoint transaction survived but its host did not. Settle
|
|
200
|
+
// that checkpoint; never dispatch the already acknowledged action.
|
|
201
|
+
delete current.run.managed.resumeLease;
|
|
202
|
+
await verifyAutomaticCriteria(current, claimNow);
|
|
203
|
+
const decision = decideStop(current.run, claimNow);
|
|
204
|
+
current.run = decision.run;
|
|
205
|
+
if (current.run.state === 'active') {
|
|
206
|
+
current.run.state = 'recovery_required';
|
|
207
|
+
current.run.recoveryCause = current.run.managed.resumeChannel === 'codex_app_server' ? 'session_supervision' : 'process_exit';
|
|
208
|
+
}
|
|
209
|
+
current.run.updatedAt = claimNow; notifyState(current.run, claimNow); return current;
|
|
210
|
+
}
|
|
187
211
|
current.run.state = 'recovery_required'; current.run.recoveryCause = 'interrupted_owner'; current.run.managed.autoRecovery = false;
|
|
188
212
|
recordRecoveryIncident(current.run, { code: 'interrupted_owner', subject: current.run.managed.executionAttempt?.id || '', now: claimNow });
|
|
189
213
|
recordRecoveryStage(current.run, 'interrupted', { now: claimNow, attemptId: current.run.managed.executionAttempt?.id });
|
|
@@ -192,12 +216,34 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
192
216
|
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; }
|
|
193
217
|
if (attempt?.phase === 'dispatching' && attempt.action?.kind !== 'read') current.run.managed.executionUncertain = {
|
|
194
218
|
attemptId: attempt.id, decisionId: attempt.decisionId, action: attempt.action, at: claimNow };
|
|
219
|
+
// Ownership is released only after the existing process-identity check
|
|
220
|
+
// proves the owner dead and uncertain effects have been retained.
|
|
221
|
+
delete current.run.managed.resumeLease;
|
|
195
222
|
current.run.updatedAt = claimNow; notifyState(current.run, claimNow); return current;
|
|
196
223
|
});
|
|
197
224
|
continue;
|
|
198
225
|
}
|
|
199
226
|
if (!automaticContinuationAllowed(record)) continue;
|
|
200
|
-
|
|
227
|
+
let providerLink;
|
|
228
|
+
try { providerLink = await requireProviderLink({ root, provider: record.provider }); } catch { continue; }
|
|
229
|
+
if (run.managed.providerCapabilityWait) {
|
|
230
|
+
if (run.state !== 'recovery_required' || run.managed.resumeLease || managedBudgetReason(run, claimNow)) continue;
|
|
231
|
+
const expected = run.managed.providerCapabilityWait;
|
|
232
|
+
let check;
|
|
233
|
+
try { check = await checkProviderRetry(expected, inspectProvider, { now: claimNow, signal }); } catch { continue; }
|
|
234
|
+
record = await updateRecord(root, record.provider, record.session, current => {
|
|
235
|
+
if (current?.run?.id !== run.id || !automaticContinuationAllowed(current) || current.run.state !== 'recovery_required' ||
|
|
236
|
+
current.run.managed.resumeLease || JSON.stringify(current.run.managed.providerCapabilityWait) !== JSON.stringify(expected)) return current;
|
|
237
|
+
if (check.retry) {
|
|
238
|
+
delete current.run.managed.providerCapabilityWait;
|
|
239
|
+
current.run.managed.autoRecovery = true; current.run.recoveryCause = 'session_supervision';
|
|
240
|
+
current.run.managed.nextAttemptAt = claimNow;
|
|
241
|
+
} else current.run.managed.providerCapabilityWait = check.wait;
|
|
242
|
+
return current;
|
|
243
|
+
});
|
|
244
|
+
run = record.run;
|
|
245
|
+
if (run.managed.providerCapabilityWait) continue;
|
|
246
|
+
}
|
|
201
247
|
const appServer = run.managed.resumeChannel === 'codex_app_server';
|
|
202
248
|
const causeAllowed = run.recoveryCause === 'process_exit' || appServer && ['session_supervision', 'approved_action'].includes(run.recoveryCause);
|
|
203
249
|
const approvedWait = run.managed.decision?.outcome === 'wait' && ['read', 'local_edit', 'local_test', 'external'].includes(run.managed.decision.action?.kind) &&
|
|
@@ -207,7 +253,7 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
207
253
|
if (run.state !== 'recovery_required' || !causeAllowed || !run.managed.autoRecovery || run.managed.pendingApproval || run.managed.autonomyPolicy && run.managed.decision?.outcome === 'wait' && !approvedWait || appServer && run.managed.nextAttemptAt > claimNow) continue;
|
|
208
254
|
let authorizationScope;
|
|
209
255
|
const authorizeClaim = async () => {
|
|
210
|
-
await requireProviderLink({ root, provider: record.provider });
|
|
256
|
+
await requireProviderLink({ root, provider: record.provider, expectedEpoch: providerLink.epoch });
|
|
211
257
|
if (!authorizeDispatch) return;
|
|
212
258
|
const latest = await authorizeDispatch(record);
|
|
213
259
|
if (authorizationScope !== undefined && latest !== authorizationScope) throw Object.assign(new Error("Desktop authorization scope changed."), { code: "desktop_access_required" });
|
|
@@ -228,8 +274,16 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
228
274
|
let claimed;
|
|
229
275
|
try { claimed = await updateRecord(root, record.provider, record.session, current => {
|
|
230
276
|
const live = current?.run;
|
|
277
|
+
// Re-read if the selected task or action changed while acquiring the state lock.
|
|
278
|
+
if (live?.id !== run.id || live.revision !== run.revision || live.controlRevision !== run.controlRevision ||
|
|
279
|
+
live.managed?.taskId !== run.managed?.taskId || live.managed?.decision?.id !== run.managed?.decision?.id ||
|
|
280
|
+
live.managed?.providerCapabilityWait) return current;
|
|
281
|
+
|
|
231
282
|
if (!automaticContinuationAllowed(current)) return current;
|
|
232
283
|
if (live?.managed?.outcomeAudit?.lease) return current;
|
|
284
|
+
if (current.autoContinueEpoch !== record.autoContinueEpoch ||
|
|
285
|
+
live.managed?.executionUncertain && live.managed.recoveryDiagnosisDecisionId !== live.managed.decision?.id ||
|
|
286
|
+
['pending', 'transferred', 'pending_received'].includes(live.managed?.handoff?.status)) return current;
|
|
233
287
|
const appChannel = live?.managed?.resumeChannel === 'codex_app_server';
|
|
234
288
|
const allowedCause = live?.recoveryCause === 'process_exit' || appChannel && ['session_supervision', 'approved_action'].includes(live?.recoveryCause);
|
|
235
289
|
const approved = live?.managed?.decision?.outcome === 'wait' && ['read', 'local_edit', 'local_test', 'external'].includes(live.managed.decision.action?.kind) &&
|
|
@@ -239,12 +293,20 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
239
293
|
if (stop) {
|
|
240
294
|
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;
|
|
241
295
|
}
|
|
296
|
+
const workProposal = recoveryWorkProposal(current);
|
|
297
|
+
if (workProposal?.blocked) return current;
|
|
242
298
|
live.state = 'auto_resuming'; live.managed.resumeLease = { id: leaseId, claimedAt: currentTime(), checkpointRevision: live.revision, decisionId: live.managed.decision?.id || null,
|
|
243
|
-
owner: { pid: process.pid, identity: ownerIdentity } };
|
|
299
|
+
owner: { pid: process.pid, identity: ownerIdentity }, workProposal: workProposal ? { inputDigest: workProposal.inputDigest } : null };
|
|
244
300
|
live.managed.executionAttempt = { id: leaseId, decisionId: live.managed.decision?.id || null,
|
|
245
301
|
action: live.managed.decision?.action || live.managed.proposedAction,
|
|
246
302
|
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
303
|
live.managed.executionAttempt.recoveryIncidentId = live.managed.recovery?.currentId || null;
|
|
304
|
+
const pendingResult = live.resultCycle?.events.find(row => row.id === live.resultCycle.pendingId);
|
|
305
|
+
// Carry the actual preceding result into its dispatch receipt. A synthetic
|
|
306
|
+
// initial/condition event is needed only when no unconsumed result exists.
|
|
307
|
+
if (!pendingResult || pendingResult.dispatchAttemptId) acceptResultEvent(current, resultEvent(current, {
|
|
308
|
+
kind: 'condition_changed', origin: 'supervisor_claim',
|
|
309
|
+
sourceId: `dispatch:${live.managed.decision?.id}:${live.revision}`, now: currentTime() }));
|
|
248
310
|
// Retain the repair action identity during independent verification.
|
|
249
311
|
// Older receipts can still be executing after a verifier; bind those to
|
|
250
312
|
// the next actual diagnosis/repair only after all lease/authority fences.
|
|
@@ -318,7 +380,8 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
318
380
|
if (!result) {
|
|
319
381
|
await authorizeClaim();
|
|
320
382
|
const runner = execute || (appServer ? executeCodexAppServer : executeCodexResume);
|
|
321
|
-
result = await
|
|
383
|
+
result = await executeResultCycle({ record: claimed, attemptId: leaseId, execute: runner, persist: persistReceipt,
|
|
384
|
+
options: { signal: controller.signal, root, webOrigin, leaseId, currentTime, leaseMonitored: true,
|
|
322
385
|
onStarted: async observation => {
|
|
323
386
|
await persistReceipt(current => {
|
|
324
387
|
const attempt = current?.run?.managed?.executionAttempt;
|
|
@@ -352,6 +415,16 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
352
415
|
const live = current?.run;
|
|
353
416
|
if (live?.state !== 'auto_resuming' || live.managed?.resumeLease?.id !== leaseId) throw new Error('Managed recovery lease changed before provider dispatch.');
|
|
354
417
|
if (!automaticContinuationAllowed(current)) throw new Error('Automatic continuation was disabled before provider dispatch.');
|
|
418
|
+
if (live.managed.executionAttempt?.id !== leaseId || live.managed.executionAttempt.phase !== 'claimed') {
|
|
419
|
+
throw new Error('Managed attempt was already dispatched or replaced.');
|
|
420
|
+
}
|
|
421
|
+
if (current.autoContinueEpoch !== claimed.autoContinueEpoch ||
|
|
422
|
+
live.revision !== live.managed.resumeLease.checkpointRevision ||
|
|
423
|
+
(live.managed.decision?.id || null) !== live.managed.resumeLease.decisionId ||
|
|
424
|
+
['pending', 'transferred', 'pending_received'].includes(live.managed.handoff?.status) ||
|
|
425
|
+
live.managed.executionUncertain && live.managed.recoveryDiagnosisDecisionId !== live.managed.decision?.id) {
|
|
426
|
+
throw new Error('Managed recovery authorization changed before provider dispatch.');
|
|
427
|
+
}
|
|
355
428
|
if (appServer) {
|
|
356
429
|
if (!providerSandbox || !['readOnly', 'workspaceWrite', 'dangerFullAccess', 'externalSandbox'].includes(providerSandbox.type)) throw new Error('Original Codex sandbox policy is unavailable.');
|
|
357
430
|
// Observation is not enrollment authority. In particular, a
|
|
@@ -362,6 +435,7 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
362
435
|
const ledger=live.managed.purpose?.evidence;
|
|
363
436
|
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.');
|
|
364
437
|
if (live.managed.decision?.autonomousExternal && !canExecuteAutonomousExternal(live.managed)) throw new Error('Autonomous external authority changed before dispatch.');
|
|
438
|
+
admitRecoveryWork(current, live.managed.resumeLease.workProposal, leaseId, currentTime());
|
|
365
439
|
const approved = live.managed.approvedAction;
|
|
366
440
|
live.managed.executionAttempt.phase = 'dispatching';
|
|
367
441
|
live.managed.executionAttempt.dispatchedAt = currentTime();
|
|
@@ -376,7 +450,7 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
376
450
|
return current;
|
|
377
451
|
});
|
|
378
452
|
await authorizeClaim();
|
|
379
|
-
} });
|
|
453
|
+
} } });
|
|
380
454
|
}
|
|
381
455
|
}
|
|
382
456
|
} catch (error) { result = error?.code === 'provider_link_disconnected'
|
|
@@ -384,15 +458,25 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
384
458
|
: error?.code === 'desktop_access_required'
|
|
385
459
|
? { code: -3, busy: true, accessDenied: true }
|
|
386
460
|
: { code: controller.signal.aborted ? -2 : -1, cancelled: controller.signal.aborted,
|
|
387
|
-
providerTurnId: error?.providerTurnId, providerSessionId:error?.providerSessionId, question: error?.question,
|
|
461
|
+
providerTurnId: error?.providerTurnId, providerSessionId:error?.providerSessionId, question: error?.question,
|
|
462
|
+
finalResponseWithoutCheckpoint: error?.finalResponseWithoutCheckpoint,
|
|
463
|
+
errorCode: error?.code || 'provider_error', diagnostic: safeProviderFailure(error?.diagnostic), restorationUnverified: error?.restorationUnverified === true }; }
|
|
388
464
|
try {
|
|
465
|
+
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)) {
|
|
466
|
+
try { result.providerEnvironment = await providerEnvironmentIdentity(inspectProvider); } catch { /* No guessed identity: normal bounded failure path remains. */ }
|
|
467
|
+
}
|
|
389
468
|
await usageQueue;
|
|
390
469
|
if (usageError) result = { ...result, code: -1, errorCode: 'usage_unavailable' };
|
|
391
470
|
usage ||= result?.usage || null;
|
|
392
471
|
if (controller.signal.aborted) result = { ...result, code: -2, cancelled: true };
|
|
393
472
|
if (result.code === 0) {
|
|
394
473
|
try {
|
|
395
|
-
if (result.verification) await settlePurposeVerification({ root, provider: record.provider, session: record.session, expectedLease: leaseId, review: result.verification, verifySnapshot: current =>
|
|
474
|
+
if (result.verification) await settlePurposeVerification({ root, provider: record.provider, session: record.session, expectedLease: leaseId, review: result.verification, verifySnapshot: async current => {
|
|
475
|
+
const refreshed = await bindCompletionArtifacts(current, result.verification, {
|
|
476
|
+
authorize: async () => { await assertActiveLease(root, record.provider, record.session, leaseId, controller.signal, currentTime); await authorizeClaim(); }, signal: controller.signal });
|
|
477
|
+
Object.assign(result.verification, refreshed);
|
|
478
|
+
return reviewWorkspaceIdentity(current, root, result.verification, { signal: controller.signal });
|
|
479
|
+
}, now: Date.now() });
|
|
396
480
|
else await checkpointDelivery({ root, provider: record.provider, session: record.session, cwd: record.cwd,
|
|
397
481
|
checkpoint: result.checkpoint || parseAgentCheckpoint(result.output, record.session), expectedLease: leaseId, toolEvidence: result.toolEvidence || [], sourceContext: result.sourceContext, controllerEvidence: result.controllerEvidence, now: Date.now() });
|
|
398
482
|
if (result.dispatchAttemptId) queueDispatchReceipt({ root: rootFromDelivery(root), attemptId: result.dispatchAttemptId, receiptId: `checkpoint:${leaseId}`, resultRef: `delivery-record:${record.provider}:${record.session}:${leaseId}` });
|
|
@@ -435,7 +519,15 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
435
519
|
}
|
|
436
520
|
if (result.errorCode === 'checkpoint_rejected') live.managed.lastCheckpointRejection = { at: Date.now(), decisionId: claimed.run.managed.decision?.id, diagnostic: result.diagnostic };
|
|
437
521
|
else if (result.code === 0) delete live.managed.lastCheckpointRejection;
|
|
522
|
+
if (result.errorCode === 'checkpoint' && result.finalResponseWithoutCheckpoint) {
|
|
523
|
+
live.managed.finalResponseWithoutCheckpoint = { ...result.finalResponseWithoutCheckpoint,
|
|
524
|
+
providerTurnId: result.providerTurnId, providerSessionId: result.providerSessionId || record.session };
|
|
525
|
+
} else if (result.code === 0) delete live.managed.finalResponseWithoutCheckpoint;
|
|
438
526
|
const attempt = live.managed.executionAttempt;
|
|
527
|
+
const attemptedUnit = live.managed.purpose?.evidence?.units?.find(unit => unit.id === attempt?.decisionId);
|
|
528
|
+
if (attemptedUnit?.status === 'dispatched' && result.code !== 0) {
|
|
529
|
+
attemptedUnit.status = attempt.action?.kind === 'read' ? 'failed' : 'unknown';
|
|
530
|
+
}
|
|
439
531
|
if (!result.busy && attempt?.phase === 'dispatching' && attempt.action?.kind !== 'read') live.managed.executionUncertain = {
|
|
440
532
|
attemptId: leaseId, decisionId: attempt.decisionId, action: attempt.action, at: Date.now() };
|
|
441
533
|
live.managed.attempts ||= [];
|
|
@@ -445,8 +537,9 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
445
537
|
checkpointRevision: live.revision, outcome: result.busy ? 'busy' : result.cancelled ? 'cancelled' : result.code === 0 ? 'checkpoint' : 'failed',
|
|
446
538
|
errorCode: result.code === 0 || result.busy || result.cancelled ? null : safeProviderCode(result.errorCode),
|
|
447
539
|
...(result.providerSessionId ? {providerSessionId:result.providerSessionId} : {}),
|
|
540
|
+
...(safeProviderFailure(result.diagnostic) ? { diagnostic: safeProviderFailure(result.diagnostic) } : {}),
|
|
448
541
|
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.',
|
|
542
|
+
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
543
|
nextStep: live.nextStep });
|
|
451
544
|
live.managed.attempts = live.managed.attempts.slice(-20);
|
|
452
545
|
if (result.code !== 0) recordRecoveryStage(live, result.busy ? 'waiting' : 'interrupted', {
|
|
@@ -486,6 +579,10 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
486
579
|
} else if (result.cancelled) {
|
|
487
580
|
live.managed.autoRecovery = false; live.state = 'recovery_required'; live.recoveryCause = 'shutdown_cancelled';
|
|
488
581
|
live.reason = 'Managed recovery was cancelled; inspect the original session before the next bounded recovery.';
|
|
582
|
+
} else if (result.providerEnvironment && !dispatched) {
|
|
583
|
+
live.managed.providerCapabilityWait = { diagnostic: safeProviderFailure(result.diagnostic), environmentIdentity: result.providerEnvironment.identity, nextCheckAt: currentTime() + 60000, checks: 0 };
|
|
584
|
+
live.managed.autoRecovery = true; live.state = 'recovery_required'; live.recoveryCause = 'session_supervision';
|
|
585
|
+
live.reason = 'Provider capability needs an environment change; bounded read-only checks continue without spending another provider turn.';
|
|
489
586
|
} else if (result.code !== 0) {
|
|
490
587
|
live.failures += 1; live.managed.autoRecovery = false;
|
|
491
588
|
live.state = failureLimitReached(live) ? 'failed' : 'recovery_required';
|
|
@@ -493,6 +590,16 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
493
590
|
live.reason = appServer ? providerFailureReason(safeProviderCode(result.errorCode)) +
|
|
494
591
|
(result.restorationUnverified ? ' Original provider permissions also require inspection.' : '') :
|
|
495
592
|
'Codex recovery process failed; inspect its checkpoint and original session before the next bounded recovery.';
|
|
593
|
+
// Recover the missing handoff with a read diagnosis, never by replaying
|
|
594
|
+
// the last action. The existing dispatcher rechecks leases and rights.
|
|
595
|
+
if (appServer && live.state !== 'failed' && automaticContinuationAllowed(current) &&
|
|
596
|
+
!live.managed.pendingApproval && !live.managed.nativeWait && !live.managed.purpose?.nativeBoundary &&
|
|
597
|
+
(result.errorCode === 'checkpoint' && result.finalResponseWithoutCheckpoint || result.errorCode === 'checkpoint_rejected')) {
|
|
598
|
+
await continueResultCycle(current, resultEvent(current, { kind: 'final_response', origin: 'executor_result',
|
|
599
|
+
sourceId: result.providerTurnId || leaseId, attemptId: leaseId, resultRef: `attempt:${leaseId}` }), {
|
|
600
|
+
selectNext: () => queuePurposeDiagnosis(live, finalResponseRecoveryInstruction,
|
|
601
|
+
`missing-handoff:${result.providerTurnId || leaseId}`, Date.now(), 'checkpoint') });
|
|
602
|
+
}
|
|
496
603
|
} else if (live.revision <= run.revision) {
|
|
497
604
|
live.failures += 1;
|
|
498
605
|
live.reason = result.code === 0 ? 'Codex recovery returned without a checkpoint.' : 'Codex recovery failed; inspect the original session.';
|
|
@@ -514,6 +621,9 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
514
621
|
current.run.reason = 'Verified checkpoint recorded; next bounded exact-session attempt is pending.';
|
|
515
622
|
}
|
|
516
623
|
}
|
|
624
|
+
if (result.code !== 0) await continueResultCycle(current, resultEvent(current, {
|
|
625
|
+
kind: 'failure', origin: 'executor_result', sourceId: result.providerTurnId || leaseId,
|
|
626
|
+
attemptId: leaseId, resultRef: `attempt:${leaseId}` }));
|
|
517
627
|
current.run.updatedAt = Date.now(); notifyState(current.run, current.run.updatedAt); return current;
|
|
518
628
|
});
|
|
519
629
|
// Settling uncertainty must precede unlocking: otherwise another session
|
|
@@ -526,19 +636,42 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
526
636
|
export function createManagedSupervisor(options) {
|
|
527
637
|
let running = null, auditing = null, closed = false;
|
|
528
638
|
const controller = new AbortController();
|
|
639
|
+
const workers = createTaskPool({ concurrency: options.concurrency ?? 4 });
|
|
640
|
+
const readCandidates = options.readRecords || (() => listRecords(options.root));
|
|
529
641
|
return {
|
|
530
642
|
tick() {
|
|
531
643
|
if (closed) return Promise.resolve();
|
|
532
644
|
if (!auditing) auditing = (async () => {
|
|
533
645
|
const runtime = await desktopRuntimeStatus({ root: dirname(options.root) }).catch(() => ({ state: 'runtime_observation_failed' }));
|
|
534
646
|
await runConsistencyMonitor({ root: options.root, runtime, readRecords: options.readRecords || (() => listRecords(options.root)),
|
|
535
|
-
onAudit: (record, findings, request) => auditConsistencyTask(record, findings, request, {
|
|
647
|
+
onAudit: async (record, findings, request) => closed || await options.shouldYield?.() ? undefined : auditConsistencyTask(record, findings, request, {
|
|
536
648
|
root: options.root, signal: controller.signal, authorize: options.authorizeDispatch || (async () => {}) }) });
|
|
537
649
|
})().catch(() => {}).finally(() => { auditing = null; });
|
|
538
|
-
if (!running) running =
|
|
650
|
+
if (!running) running = (async () => {
|
|
651
|
+
for (const record of await readCandidates()) {
|
|
652
|
+
if (closed || await options.shouldYield?.()) break;
|
|
653
|
+
if (!record.run?.managed) continue;
|
|
654
|
+
const key = `${record.provider}:${record.session}`;
|
|
655
|
+
workers.offer(key, async () => {
|
|
656
|
+
if (closed || await options.shouldYield?.()) return;
|
|
657
|
+
// A queue entry is a wakeup, not authority or a saved action. Re-read
|
|
658
|
+
// consent, revisions and task identity at admission and each claim.
|
|
659
|
+
const readRecords = async () => {
|
|
660
|
+
const rows = options.readRecords ? await options.readRecords()
|
|
661
|
+
: [await readRecord(options.root, record.provider, record.session)];
|
|
662
|
+
return rows.filter(row => row?.provider === record.provider && row.session === record.session &&
|
|
663
|
+
row.run?.id === record.run.id && row.run?.managed?.taskId === record.run.managed.taskId);
|
|
664
|
+
};
|
|
665
|
+
await tickManagedTasks({ ...options, readRecords, signal: controller.signal });
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
})().finally(() => { running = null; });
|
|
539
669
|
return running;
|
|
540
670
|
},
|
|
541
|
-
|
|
671
|
+
status() { return workers.snapshot(); },
|
|
672
|
+
async idle() { await running; await workers.idle(); },
|
|
673
|
+
async drain() { closed = true; await Promise.allSettled([running, auditing, workers.stop()]); },
|
|
674
|
+
async close() { closed = true; controller.abort(); await Promise.allSettled([running, auditing, workers.stop()]); },
|
|
542
675
|
};
|
|
543
676
|
}
|
|
544
677
|
|
package/src/desktop-access.js
CHANGED
|
@@ -13,7 +13,8 @@ const recoveryActions = new Set(['refresh', 'reconnect', 'login', 'authorization
|
|
|
13
13
|
export function desktopActionAllowed(access, command) {
|
|
14
14
|
return desktopAccessReady(access) || recoveryActions.has(command?.action) ||
|
|
15
15
|
command?.action === 'session_set_auto_continue' && command.enabled === false ||
|
|
16
|
-
command?.action === 'session_set_email_reports' && command.enabled === false
|
|
16
|
+
command?.action === 'session_set_email_reports' && command.enabled === false ||
|
|
17
|
+
command?.action === 'team_thread_share' && command.enabled === false;
|
|
17
18
|
}
|
|
18
19
|
export function requireDesktopAccess(access) {
|
|
19
20
|
if (!desktopAccessReady(access)) throw Object.assign(new Error('Sign in to an organization with SQM and Developer access before using SIMY.'), { code: 'desktop_access_required' });
|
package/src/desktop-host.js
CHANGED
|
@@ -5,6 +5,8 @@ import { createLedgerCoordinatorAdapters } from "./update-detachment/ledger-adap
|
|
|
5
5
|
import { createUpdateCoordinator } from "./update-detachment/coordinator.js";
|
|
6
6
|
import { sessionWorkState, compareSessionWork, reconcileFamilyCompletion, expireSessionWorkState } from './session-supervision/work-state.js';
|
|
7
7
|
import { createPullRequestObserver } from './session-supervision/pull-requests.js';
|
|
8
|
+
import { createTeamThreads, teamThreadKey } from './team-threads.js';
|
|
9
|
+
import { createTeamStatusFenceStore } from './team-status-fence-store.js';
|
|
8
10
|
import { sessionDecisionPresentation } from './session-supervision/decision-presentation.js';
|
|
9
11
|
import { PROVIDER_LINKS, readProviderLinks, writeProviderLink, requireProviderLink } from "./provider-links.js";
|
|
10
12
|
import { randomUUID } from "node:crypto";
|
|
@@ -221,6 +223,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
221
223
|
let statisticsSelection = { key: null, generation: 0 };
|
|
222
224
|
const stopNotifier = createStopNotifier({ emit: event => { if (!closed && !relay) emit(event); } });
|
|
223
225
|
if (env.SIMY_REMOTE_CHILD !== "1" && process.platform !== "win32") remoteHosts = (dependencies.createRemoteHostManager || createRemoteHostManager)({ root, onChange: () => { if (!closed && !relay) setImmediate(() => { void publish().catch(() => {}); }); } });
|
|
226
|
+
let teamThreads;
|
|
224
227
|
let currentAccess = { state: 'checking', organizationId: null, organizationName: null };
|
|
225
228
|
let nativeDevelopmentReports = { state: 'loading', enrollments: [] };
|
|
226
229
|
let nativeDevelopmentReportsScope = null;
|
|
@@ -713,6 +716,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
713
716
|
if (JSON.stringify(await readProviderLinks(root)) !== requestedLinks) { publishAgain = true; return; }
|
|
714
717
|
if (connectionRevision !== requestedConnectionRevision || !sameAccess(sampledAccess, agent?.getDesktopAccess ? agent.getDesktopAccess() : await readAccess())) { publishAgain = true; return; }
|
|
715
718
|
status.nativeDevelopmentReports = { state: 'loading', enrollments: [] };
|
|
719
|
+
status.teamThreads = teamThreads?.snapshot();
|
|
716
720
|
if (!closed && !relay) emitStatus(status);
|
|
717
721
|
return;
|
|
718
722
|
}
|
|
@@ -764,6 +768,13 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
764
768
|
status.sessionSupervision = { ...discovery, refreshing: Boolean(sessionRefresh), sessions: reconcileFamilyCompletion(sessions.map(candidate => ({ ...candidate,
|
|
765
769
|
decisionRequest: sessionDecisionPresentation(candidate, runsBySession.get(`${candidate.provider}:${candidate.session}`)),
|
|
766
770
|
workState: sessionWorkState(candidate, runsBySession.get(`${candidate.provider}:${candidate.session}`) || stateRunsBySession.get(`${candidate.provider}:${candidate.session}`), { provider: discovery.providers?.[candidate.provider] }) }))).sort(compareSessionWork) };
|
|
771
|
+
teamThreads?.tick(sessions);
|
|
772
|
+
status.teamThreads = teamThreads?.snapshot();
|
|
773
|
+
for (const session of status.sessionSupervision.sessions) {
|
|
774
|
+
const key = teamThreadKey(agent.getTeamThreadsScope?.() || {}, session);
|
|
775
|
+
const own = status.teamThreads?.own.find(item => item.threadKey === key);
|
|
776
|
+
session.teamSharing = { enabled: own?.enabled === true, pendingStop: status.teamThreads?.pendingStops.includes(key) === true };
|
|
777
|
+
}
|
|
767
778
|
const latestAccess = await accessSnapshot();
|
|
768
779
|
if (JSON.stringify(await readProviderLinks(root)) !== requestedLinks ||
|
|
769
780
|
!desktopAccessReady(latestAccess) || accessScope(latestAccess) !== requestedScope ||
|
|
@@ -779,12 +790,14 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
779
790
|
if (accessScope(latestConnectionStatus?.access) !== requestedScope || !desktopAccessReady(latestConnectionStatus?.access)) { publishAgain = true; return; }
|
|
780
791
|
status = { ...structuredClone(latestConnectionStatus), deliveryHooks: status.deliveryHooks, sessionSupervision: status.sessionSupervision };
|
|
781
792
|
}
|
|
793
|
+
status.teamThreads = teamThreads?.snapshot();
|
|
782
794
|
if (!closed && !relay) emitStatus(status);
|
|
783
795
|
}).finally(() => { publishing = null; if (publishAgain && !closed) { publishAgain = false; setImmediate(() => { void publish().catch(() => {}); }); } });
|
|
784
796
|
return publishing;
|
|
785
797
|
};
|
|
786
798
|
const stopAgent = async () => {
|
|
787
799
|
clearInterval(timer);
|
|
800
|
+
teamThreads?.close();
|
|
788
801
|
pluginInspectionAbort.abort();
|
|
789
802
|
activeStatisticsRead?.controller.abort();
|
|
790
803
|
discoveryStopped = true;
|
|
@@ -843,6 +856,13 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
843
856
|
},
|
|
844
857
|
});
|
|
845
858
|
if (env.SIMY_UPDATE_TRANSFER_TOKEN) await coordinator.reconnect();
|
|
859
|
+
teamThreads = createTeamThreads({
|
|
860
|
+
readScope: () => agent.getTeamThreadsScope?.() || {key: accessScope(currentAccess),allowed:false,authenticated:false},
|
|
861
|
+
request: (body,signal) => agent.requestTeamThreads(body,signal),
|
|
862
|
+
fenceStore: createTeamStatusFenceStore(root),
|
|
863
|
+
readContext: dependencies.readTeamThreadContext,
|
|
864
|
+
onChange: () => { if(!closed) setImmediate(() => {void publish().catch(() => {});}); },
|
|
865
|
+
});
|
|
846
866
|
stopWorkflows = (dependencies.startWorkflowUpdates || startWorkflowUpdates)({ enabled: env.SIMY_AUTO_UPDATE !== "0" });
|
|
847
867
|
updater.on?.("change", () => { void publish().catch(() => {}); });
|
|
848
868
|
updater.start({ registry: agent.registry, coordinator, isDesktopIdle: () => !unfinishedDesktopWork,
|
|
@@ -1105,15 +1125,28 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1105
1125
|
const commandScope = accessScope(access);
|
|
1106
1126
|
if (!desktopActionAllowed(access, command)) throw Object.assign(new Error('Desktop access is required.'), { code: 'desktop_access_required' });
|
|
1107
1127
|
const recoveryControl = ['provider_disconnect', 'session_unmonitor'].includes(action) ||
|
|
1108
|
-
['session_set_auto_continue', 'session_set_email_reports'].includes(action) && command.enabled === false;
|
|
1128
|
+
['session_set_auto_continue', 'session_set_email_reports', 'team_thread_share'].includes(action) && command.enabled === false;
|
|
1109
1129
|
if (['detach_update', 'update_reconnect'].includes(action)) requireDesktopAccess(access);
|
|
1110
1130
|
else if (!recoveryControl && desktopAccessReady(access) && !desktopActionAllowed({ state: 'unavailable' }, command)) await authorizeWork();
|
|
1111
1131
|
|
|
1112
1132
|
if (!coordinator.acceptsNewWork() && !recoveryControl && !["refresh", "reconnect", "task_detail", "check_update", "quit", "delivery_stop", "delivery_pause", "delivery_reject"].includes(action)) {
|
|
1113
1133
|
throw new Error("The update connection requires recovery.");
|
|
1114
1134
|
}
|
|
1135
|
+
if (['team_threads_refresh','team_thread_open','team_thread_share'].includes(action)) {
|
|
1136
|
+
let ok;
|
|
1137
|
+
if(action === 'team_threads_refresh') ok = await teamThreads.refresh({memberId:command.memberId});
|
|
1138
|
+
else if(action === 'team_thread_open') ok = await teamThreads.open({memberId:command.memberId,threadKey:command.threadKey});
|
|
1139
|
+
else {
|
|
1140
|
+
if (command.organizationId !== undefined && command.organizationId !== access.organizationId) throw new Error('Sharing organization changed. Confirm again.');
|
|
1141
|
+
const selected = sessionStatus.sessions.find(item => item.provider === command.provider && item.session === command.session);
|
|
1142
|
+
if(!selected) throw new Error('Refresh and select your conversation again.');
|
|
1143
|
+
ok = await teamThreads.setSharing(selected,command.enabled);
|
|
1144
|
+
}
|
|
1145
|
+
emit({type:'result',id:command.id,ok:ok===true}); void publish().catch(() => {}); return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1115
1148
|
let commandProviderLink;
|
|
1116
|
-
if (command.provider && !["provider_connect", "provider_disconnect", "delivery_stop", "delivery_pause", "delivery_reject", "session_untrack", "session_unmonitor"].includes(action) &&
|
|
1149
|
+
if (command.provider && !["refresh", "provider_connect", "provider_disconnect", "delivery_stop", "delivery_pause", "delivery_reject", "session_untrack", "session_unmonitor"].includes(action) &&
|
|
1117
1150
|
!(action === "session_set_auto_continue" && command.enabled === false)) {
|
|
1118
1151
|
commandProviderLink = await requireProviderLink({ root: deliveryRoot, provider: command.provider });
|
|
1119
1152
|
}
|