@awak-app/simy-cli 0.5.71 → 0.5.73
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 +101 -3
- package/package.json +2 -2
- package/src/agent.js +205 -12
- package/src/backend-executable.js +45 -2
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/README.md +22 -0
- package/src/codex-plugins/simy/hooks/continuation-policy.mjs +27 -0
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +180 -31
- package/src/codex-plugins/simy/hooks/outcome-progress.mjs +129 -0
- package/src/coding-run-recovery.js +88 -0
- package/src/coding-run-store.js +2 -2
- package/src/continuation-policy.js +2 -0
- package/src/delivery-hooks/command.js +11 -5
- package/src/delivery-hooks/desktop.js +12 -3
- package/src/delivery-hooks/supervisor.js +17 -12
- package/src/desktop-host.js +19 -8
- package/src/desktop-sqm-settings.js +24 -7
- package/src/device-signing-identity.js +65 -0
- package/src/diagnostic-log.js +68 -0
- package/src/doctor.js +55 -0
- package/src/index.js +18 -1
- package/src/local-task.js +68 -3
- package/src/orchestrator/budget.js +55 -4
- package/src/orchestrator/charter-validation.js +33 -0
- package/src/orchestrator/completion-contract.js +1 -1
- package/src/orchestrator/contract.js +2 -0
- package/src/orchestrator/delivery/references/state-and-receipts.md +21 -3
- package/src/orchestrator/evidence.js +20 -19
- package/src/orchestrator/independent-audit.js +9 -0
- package/src/orchestrator/instruction.js +6 -1
- package/src/orchestrator/loop.js +66 -41
- package/src/orchestrator/outcome-monitor.js +95 -44
- package/src/orchestrator/outcome-progress.js +2 -0
- package/src/orchestrator/recovery.js +3 -0
- package/src/purpose-local-proof.js +380 -0
- package/src/runner.js +227 -19
- package/src/runtime-diagnostics.js +77 -0
- package/src/session-supervision/codex-app-server.js +3 -2
- package/src/session-supervision/index.js +16 -4
- package/src/sqm/bundle-store.js +14 -7
- package/src/sqm/claude-plugin.js +2 -0
- package/src/sqm/command.js +42 -4
- package/src/sqm/diagnostics.js +23 -0
- package/src/sqm/evidence-client.js +24 -7
- package/src/sqm/history-store.js +21 -5
- package/src/sqm/index.js +116 -17
- package/src/sqm/proof.js +4 -23
- package/src/sqm/report.js +9 -2
- package/src/sqm/request-recovery.js +82 -0
- package/src/sqm/review-worker.js +97 -15
- package/src/sqm/session.js +33 -3
- package/src/startup-preflight.js +44 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
2
|
import os from "node:os";
|
|
3
|
+
import { readdir } from "node:fs/promises";
|
|
4
|
+
import { readdirSync, statSync } from "node:fs";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
|
|
5
7
|
const MACOS_CODEX_BINARY = "/Applications/ChatGPT.app/Contents/Resources/codex";
|
|
@@ -25,11 +27,12 @@ export async function resolveBackendExecutable(backend, options = {}) {
|
|
|
25
27
|
|
|
26
28
|
export async function inspectBackendExecutable(
|
|
27
29
|
backend,
|
|
28
|
-
{ spawnImpl = spawn, candidates
|
|
30
|
+
{ spawnImpl = spawn, candidates, timeoutMs = 5_000, signal, discoveryOptions = {} } = {},
|
|
29
31
|
) {
|
|
30
32
|
const assertActive = () => { if (signal?.aborted) throw Object.assign(new Error('Backend inspection was cancelled.'), { code: 'cancelled' }); };
|
|
31
33
|
assertActive();
|
|
32
34
|
const policy = versionPolicy(backend);
|
|
35
|
+
candidates ??= [...backendCandidates(backend, discoveryOptions), ...await bundledClaudeCandidates(backend, discoveryOptions)];
|
|
33
36
|
for (const candidate of [...new Set(candidates.filter(Boolean))]) {
|
|
34
37
|
assertActive();
|
|
35
38
|
const probe = await probeExecutable(candidate, { spawnImpl, timeoutMs, signal });
|
|
@@ -85,7 +88,7 @@ export function backendCandidates(backend, {
|
|
|
85
88
|
"claude",
|
|
86
89
|
// Desktop launch environments may predate the installer's PATH update.
|
|
87
90
|
paths.join(userHome, ".local", "bin", platform === "win32" ? "claude.exe" : "claude"),
|
|
88
|
-
...(platform === "darwin" ? ["/opt/homebrew/bin/claude", "/usr/local/bin/claude"] : []),
|
|
91
|
+
...(platform === "darwin" ? ["/opt/homebrew/bin/claude", "/usr/local/bin/claude", ...claudeDesktopCandidates(userHome)] : []),
|
|
89
92
|
];
|
|
90
93
|
}
|
|
91
94
|
return [
|
|
@@ -95,6 +98,46 @@ export function backendCandidates(backend, {
|
|
|
95
98
|
];
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
// Claude Desktop can install below <version>/<content hash>/claude.app while
|
|
102
|
+
// an older PATH wrapper still points at <version>/claude.app. Discover only the
|
|
103
|
+
// known app layout; never recursively execute arbitrary files from user folders.
|
|
104
|
+
export async function bundledClaudeCandidates(backend, { platform = process.platform, home = os.homedir(), readDirectory = readdir } = {}) {
|
|
105
|
+
if (backend !== "claude" || platform !== "darwin") return [];
|
|
106
|
+
const root = path.posix.join(home, "Library", "Application Support", "Claude", "claude-code");
|
|
107
|
+
const entries = async target => { try { return await readDirectory(target, { withFileTypes: true }); } catch { return []; } };
|
|
108
|
+
const versions = (await entries(root)).filter(item => item.isDirectory() && /^\d+\.\d+\.\d+$/.test(item.name))
|
|
109
|
+
.sort((a, b) => compareVersions(b.name, a.name)).slice(0, 8);
|
|
110
|
+
const result = [];
|
|
111
|
+
for (const version of versions) {
|
|
112
|
+
const base = path.posix.join(root, version.name);
|
|
113
|
+
const hashes = (await entries(base)).filter(item => item.isDirectory() && /^[a-f0-9]{8,128}$/i.test(item.name)).slice(0, 8);
|
|
114
|
+
for (const hash of hashes) result.push(path.posix.join(base, hash.name, "claude.app", "Contents", "MacOS", "claude"));
|
|
115
|
+
result.push(path.posix.join(base, "claude.app", "Contents", "MacOS", "claude"));
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
// Desktop installs use either <version>/claude.app or <version>/<hash>/claude.app.
|
|
120
|
+
// Only inspect these two known levels; never recurse through arbitrary home paths
|
|
121
|
+
// or descend through version/hash directory symlinks. Existing explicit/PATH installs retain priority.
|
|
122
|
+
function claudeDesktopCandidates(home) {
|
|
123
|
+
const root = path.join(home, "Library", "Application Support", "Claude", "claude-code");
|
|
124
|
+
const directories = directory => {
|
|
125
|
+
try { return readdirSync(directory, { withFileTypes: true }).filter(entry => entry.isDirectory()).map(entry => entry.name); }
|
|
126
|
+
catch { return []; }
|
|
127
|
+
};
|
|
128
|
+
return directories(root).filter(name => /^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/.test(name))
|
|
129
|
+
.sort((left, right) => compareVersions(right, left)).slice(0, 32)
|
|
130
|
+
.flatMap(version => {
|
|
131
|
+
const versionRoot = path.join(root, version);
|
|
132
|
+
const bundle = directory => path.join(directory, "claude.app", "Contents", "MacOS", "claude");
|
|
133
|
+
return [bundle(versionRoot), ...directories(versionRoot)
|
|
134
|
+
.filter(name => /^[a-zA-Z0-9_-]{8,128}$/.test(name)).sort().slice(0, 8)
|
|
135
|
+
.map(hash => bundle(path.join(versionRoot, hash)))];
|
|
136
|
+
}).filter(candidate => {
|
|
137
|
+
try { return statSync(candidate).isFile(); } catch { return false; }
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
98
141
|
export function parseBackendVersion(output) {
|
|
99
142
|
const match = String(output || "").match(/(?:^|\s|v)(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?=\s|$|\))/);
|
|
100
143
|
return match?.[1] ?? null;
|
package/src/cli-contract.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./durable-local-task-steps-contract.js";
|
|
14
14
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
15
15
|
|
|
16
|
-
export const CLI_VERSION = "0.5.
|
|
16
|
+
export const CLI_VERSION = "0.5.73";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
|
@@ -31,6 +31,28 @@ keyed by provider and session ID and bound to the canonical working directory.
|
|
|
31
31
|
Historical hook observations do not prove hooks are still enabled. See the
|
|
32
32
|
bundled delivery-loop skill for checkpoint commands and explicit recovery.
|
|
33
33
|
|
|
34
|
+
Managed tasks may explicitly select `--continuation-policy progress`. Existing
|
|
35
|
+
tasks without that choice keep their bounded contract. Progress mode treats the
|
|
36
|
+
ordinary time, continuation and token thresholds as warnings, with independent
|
|
37
|
+
hard ceilings of 24 hours from the original start, 50 continuations and, on the
|
|
38
|
+
host-observed Codex channel, 15 million provider tokens. Claude live hooks expose
|
|
39
|
+
no trustworthy usage stream; their status explicitly reports that token limits
|
|
40
|
+
are unsupported. Required approvals, missing observed usage, manual stops and
|
|
41
|
+
unreconciled effects continue to block dispatch.
|
|
42
|
+
|
|
43
|
+
The host records passing, failed and unevaluated frozen acceptance checks.
|
|
44
|
+
Provider summaries, test counts, renamed evidence and restored old criteria
|
|
45
|
+
cannot manufacture progress. Three attempts without new verified criteria, or
|
|
46
|
+
30 minutes without verified progress at a dispatch/checkpoint boundary, pause
|
|
47
|
+
the original run. Active tools are allowed to reach their checkpoint within the
|
|
48
|
+
hard ceiling; activity alone does not prove progress.
|
|
49
|
+
|
|
50
|
+
`delivery extend` raises explicitly selected ceilings within those global caps.
|
|
51
|
+
It records the old and new bounds, preserves run identity, usage, checkpoints,
|
|
52
|
+
criteria and approvals, and does not start a new provider turn. A separate resume
|
|
53
|
+
is required after an exhausted run becomes paused. `delivery continuation_policy`
|
|
54
|
+
requires an explicit choice and never infers progress consent from an extension.
|
|
55
|
+
|
|
34
56
|
## Project 実装状況 (dev-work) hook
|
|
35
57
|
|
|
36
58
|
`dev-work.mjs` runs on Stop in both distributions. In a Git work tree with a
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Missing policy means the persisted legacy budget contract, never implicit consent.
|
|
2
|
+
export const CONTINUATION_POLICIES = Object.freeze(["bounded", "progress"]);
|
|
3
|
+
export const MAX_COMPLETION_TOKENS = 15_000_000;
|
|
4
|
+
export const DEFAULT_COMPLETION_ATTEMPT_CEILING = 50;
|
|
5
|
+
|
|
6
|
+
export function continuationMode(value) {
|
|
7
|
+
if (value === undefined || value === null) return "bounded";
|
|
8
|
+
if (!CONTINUATION_POLICIES.includes(value)) throw new Error("Invalid continuation policy.");
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function progressLimits(input = {}) {
|
|
13
|
+
return {
|
|
14
|
+
continuation_policy: continuationMode(input.continuation_policy),
|
|
15
|
+
provider_token_ceiling: finiteLimit(input.provider_token_ceiling, MAX_COMPLETION_TOKENS, MAX_COMPLETION_TOKENS),
|
|
16
|
+
attempt_ceiling: finiteLimit(input.attempt_ceiling, DEFAULT_COMPLETION_ATTEMPT_CEILING, 100),
|
|
17
|
+
max_no_progress_attempts: finiteLimit(input.max_no_progress_attempts, 3, 20),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function finiteLimit(value, fallback, maximum) {
|
|
22
|
+
if (value === undefined || value === null) return fallback;
|
|
23
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
|
|
24
|
+
throw new Error(`Continuation ceiling must be an integer between 1 and ${maximum}.`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
@@ -8,6 +8,8 @@ import { execFile } from 'node:child_process';
|
|
|
8
8
|
import { promisify } from 'node:util';
|
|
9
9
|
import { acquireWorkspaceLease, releaseWorkspaceLease } from './workspace-lease.mjs';
|
|
10
10
|
import { associationContract, validateAssociationResult } from './action-association.mjs';
|
|
11
|
+
import { continuationMode, MAX_COMPLETION_TOKENS } from './continuation-policy.mjs';
|
|
12
|
+
import { evaluateOutcomeProgress } from './outcome-progress.mjs';
|
|
11
13
|
|
|
12
14
|
export const TERMINAL = new Set(['completed', 'reported_done', 'stopped', 'failed', 'budget_exhausted']);
|
|
13
15
|
const EVENTS = new Set(['SessionStart', 'UserPromptSubmit', 'Stop', 'StopFailure', 'SessionEnd', 'Interrupt', 'PermissionRequest', 'Notification']);
|
|
@@ -386,7 +388,7 @@ function managedSpec(spec, session, provider, observedProcess, admission, now) {
|
|
|
386
388
|
const criteria = config.acceptanceCriteria.map((item, index) => {
|
|
387
389
|
if (typeof item !== 'object' || !item || !text(item.description, 500)) throw new Error('Each acceptance criterion needs a description.');
|
|
388
390
|
const id = text(item.id, 80) || `criterion-${index + 1}`;
|
|
389
|
-
if (!/^[A-Za-z0-9_-]+$/.test(id) || ['__proto__', 'constructor', 'prototype', 'toString', 'valueOf', 'hasOwnProperty'].includes(id)) throw new Error('Invalid acceptance criterion ID.');
|
|
391
|
+
if (!/^[A-Za-z0-9_-]+$/.test(id) || ['USER-GOAL', '__proto__', 'constructor', 'prototype', 'toString', 'valueOf', 'hasOwnProperty'].includes(id)) throw new Error('Invalid acceptance criterion ID.');
|
|
390
392
|
const check = item.check || { type: 'manual' };
|
|
391
393
|
if (!['manual', 'file_exists', 'file_contains', 'file_equals'].includes(check.type)) throw new Error('Unsupported acceptance check.');
|
|
392
394
|
if (check.type !== 'manual' && (!text(check.path, 500) || isAbsolute(check.path) || check.path.split(/[\\/]/).includes('..'))) throw new Error('Acceptance file must be relative to the task directory.');
|
|
@@ -414,6 +416,7 @@ function managedSpec(spec, session, provider, observedProcess, admission, now) {
|
|
|
414
416
|
const autoRecovery = config.autoRecovery === true && exactCodexSession && Boolean(resumeModel) &&
|
|
415
417
|
(appServerChannel || config.resumeChannel === 'codex_exec' && Boolean(observedProcess?.identity));
|
|
416
418
|
if (config.maxTokens !== undefined && !(provider === 'codex' && appServerChannel)) throw new Error('Provider token budgets require the host-observed Codex app-server channel.');
|
|
419
|
+
if (config.providerTokenCeiling !== undefined && !(provider === 'codex' && appServerChannel)) throw new Error('Provider token ceilings require the host-observed Codex app-server channel.');
|
|
417
420
|
const managed = { taskId: randomUUID(), thread: { provider, id: session },
|
|
418
421
|
actionId: text(config.actionId, 200) || null, actionDueAt, acceptanceCriteria: criteria, verified: {}, remainingWork: text(config.remainingWork, 4000) || null,
|
|
419
422
|
initialAction, proposedAction: initialAction, approvalPolicy, approvalScopes: [...new Set(scopes)].slice(0, 20),
|
|
@@ -421,6 +424,9 @@ function managedSpec(spec, session, provider, observedProcess, admission, now) {
|
|
|
421
424
|
maxNoProgressMinutes: positive(config.maxNoProgressMinutes, 30, 1440), lastVerifiedProgressAt: now,
|
|
422
425
|
maxTokens: config.maxTokens === undefined ? null : positive(config.maxTokens, null, Number.MAX_SAFE_INTEGER), tokensUsed: 0, usageAvailable: null,
|
|
423
426
|
noProgress: 0, verifiedRevision: 0, consumedVerifiedRevision: 0, notification: null, notificationKey: null,
|
|
427
|
+
continuationPolicy: continuationMode(config.continuationPolicy),
|
|
428
|
+
providerTokenCeiling: appServerChannel ? positive(config.providerTokenCeiling, MAX_COMPLETION_TOKENS, MAX_COMPLETION_TOKENS) : null,
|
|
429
|
+
budgetCapability: { time: true, continuations: true, providerTokens: appServerChannel },
|
|
424
430
|
autoRecovery, resumeModel, autoRecoveryUnavailableReason: config.autoRecovery === true && !autoRecovery ? 'Exact Codex process, session UUID, and resumeModel are required.' : null,
|
|
425
431
|
autoRecoverySandbox: config.autoRecoverySandbox || 'read-only', resumeChannel: config.resumeChannel || null,
|
|
426
432
|
capability: appServerChannel && autoRecovery ? 'codex_app_server' : autoRecovery ? 'codex_exec_resume' : 'live_hook',
|
|
@@ -430,7 +436,7 @@ function managedSpec(spec, session, provider, observedProcess, admission, now) {
|
|
|
430
436
|
if (initialPlan) recordDecision(managed, initialPlan, Date.now());
|
|
431
437
|
return managed;
|
|
432
438
|
}
|
|
433
|
-
export async function startDelivery({ root = rootDirectory(), provider, session, cwd = process.cwd(), objective, nextStep, maxContinuations, maxFailures, maxMinutes, managed, admission,
|
|
439
|
+
export async function startDelivery({ root = rootDirectory(), provider, session, cwd = process.cwd(), objective, nextStep, maxContinuations, maxFailures, maxMinutes, continuationPolicy, hardMaxMinutes, hardMaxContinuations, providerTokenCeiling, managed, admission,
|
|
434
440
|
liveHookAdmission, autoContinueEpoch, inspectProcess = processIdentity, now = Date.now() }) {
|
|
435
441
|
cwd = await realpath(cwd);
|
|
436
442
|
if (!text(objective) || !text(nextStep)) throw new Error('Objective and concrete next step are required.');
|
|
@@ -446,7 +452,11 @@ export async function startDelivery({ root = rootDirectory(), provider, session,
|
|
|
446
452
|
}
|
|
447
453
|
if ((!previous?.observedAt && !admitted) || previous.cwd !== cwd) throw new Error('No matching verified provider session observed here. Select an exact discovered session or enable SIMY hooks.');
|
|
448
454
|
if (previous.run) throw new Error('This session already has a Delivery Loop. Resume or stop it; budgets cannot be reset by starting again.');
|
|
449
|
-
const
|
|
455
|
+
const managedConfig = managed && { ...(managed === true ? {} : managed),
|
|
456
|
+
...(continuationPolicy === undefined ? {} : { continuationPolicy }),
|
|
457
|
+
...(providerTokenCeiling === undefined ? {} : { providerTokenCeiling }) };
|
|
458
|
+
const managedState = managedSpec(managedConfig, session, provider, previous.process, admitted ? admission : null, now);
|
|
459
|
+
if (!managedState && continuationMode(continuationPolicy) === 'progress') throw new Error('Progress continuation requires frozen managed acceptance criteria.');
|
|
450
460
|
if (autoContinueEpoch && managedState) managedState.autoContinueEpoch = autoContinueEpoch;
|
|
451
461
|
if (managedState && previous.associationTaskId) {
|
|
452
462
|
managedState.taskId = previous.associationTaskId;
|
|
@@ -458,6 +468,11 @@ export async function startDelivery({ root = rootDirectory(), provider, session,
|
|
|
458
468
|
const queued = managedState?.capability === 'codex_app_server';
|
|
459
469
|
const run = { id: randomUUID(), objective: text(objective), state: waiting ? 'blocked' : queued ? 'recovery_required' : 'active', nextStep: text(nextStep), startedAt: now, updatedAt: now,
|
|
460
470
|
deadline: now + positive(maxMinutes, 30, 1440) * 60_000, maxContinuations: positive(maxContinuations, 6, 50), maxFailures: positive(maxFailures, 3, 10),
|
|
471
|
+
...(managedState?.continuationPolicy === 'progress' ? {
|
|
472
|
+
hardDeadline: now + positive(hardMaxMinutes ?? managedConfig.hardMaxMinutes, 1440, 1440) * 60_000,
|
|
473
|
+
hardMaxContinuations: positive(hardMaxContinuations ?? managedConfig.hardMaxContinuations, 50, 50),
|
|
474
|
+
budgetWarnings: [], budgetExtensionHistory: [],
|
|
475
|
+
} : {}),
|
|
461
476
|
continuations: 0, failures: 0, revision: 0, consumedRevision: -1, checkpoint: null, reason: waiting ? managedState.decision.reason : queued ? 'Exact-session supervision is queued; SIMY will acquire the provider writer lock before dispatch.' : 'started',
|
|
462
477
|
...(queued ? { recoveryCause: 'session_supervision' } : {}), managed: managedState };
|
|
463
478
|
if (waiting) escalate(run, run.reason);
|
|
@@ -552,6 +567,7 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
552
567
|
}
|
|
553
568
|
run.checkpoint = { status: effectiveStatus, summary: text(checkpoint.summary), nextStep, blocker: checkpoint.blocker, evidence, acceptanceMet: checkpoint.acceptanceMet === true };
|
|
554
569
|
run.revision += 1; run.updatedAt = now;
|
|
570
|
+
recordManagedOutcomeProgress(run, now);
|
|
555
571
|
if (run.managed && effectiveStatus === 'completed') {
|
|
556
572
|
const pending = run.managed.acceptanceCriteria.filter(item => !Object.hasOwn(run.managed.verified, item.id));
|
|
557
573
|
if (pending.length) { run.state = 'verification_pending'; run.reason = `Acceptance verification pending: ${pending.map(item => item.id).join(', ')}`; }
|
|
@@ -582,7 +598,7 @@ export async function checkpointActionAssociation({ root, provider, session, cwd
|
|
|
582
598
|
let result;
|
|
583
599
|
try { result = validateAssociationResult(job, checkpoint); }
|
|
584
600
|
catch { result = { result: 'no_match', actionId: null, reason: 'Invalid association evidence; original work continues.', invalid: true }; }
|
|
585
|
-
if (now
|
|
601
|
+
if (managedBudgetReason(run, now)) result = { result: 'no_match', actionId: null, reason: 'Association budget expired.', invalid: true };
|
|
586
602
|
managed.phase = job.saved.phase; managed.plan = job.saved.plan; managed.decision = job.saved.decision;
|
|
587
603
|
run.nextStep = job.saved.nextStep;
|
|
588
604
|
if (association.invalidated || association.namespace !== job.namespace) result = { result: 'no_match', actionId: null, reason: 'Association account context changed.', invalid: true };
|
|
@@ -608,7 +624,7 @@ async function checkpointUnderstanding({ root, provider, session, cwd, checkpoin
|
|
|
608
624
|
if (!run || record.cwd !== cwd || run.managed?.phase !== 'understanding' ||
|
|
609
625
|
TERMINAL.has(run.state) || ['blocked', 'paused', 'recovery_required', 'verification_pending'].includes(run.state)) throw new Error('No active session understanding turn.');
|
|
610
626
|
if (expectedLease && (run.state !== 'auto_resuming' || run.managed.resumeLease?.id !== expectedLease || run.revision !== run.managed.resumeLease.checkpointRevision)) throw new Error('Session understanding lease changed; checkpoint discarded.');
|
|
611
|
-
if (now
|
|
627
|
+
if (managedBudgetReason(run, now)) throw new Error('Session understanding deadline expired.');
|
|
612
628
|
const decision = run.managed.decision;
|
|
613
629
|
const result = checkpoint.actionResult;
|
|
614
630
|
if (decision?.action.kind !== 'read' || result?.decisionId !== decision.id || !['observed', 'failed', 'not_run'].includes(result.status) || !text(result.summary, 300)) throw new Error('Understanding result must match the selected read decision.');
|
|
@@ -663,30 +679,49 @@ async function checkpointUnderstanding({ root, provider, session, cwd, checkpoin
|
|
|
663
679
|
return record;
|
|
664
680
|
});
|
|
665
681
|
}
|
|
682
|
+
function newVerifiedCriterion(run, id, now) {
|
|
683
|
+
const managed = run.managed;
|
|
684
|
+
if (managed.continuationPolicy === 'progress') {
|
|
685
|
+
const objectiveDigest = digest(JSON.stringify({ objective: run.objective, criteria: managed.acceptanceCriteria }));
|
|
686
|
+
if (managed.progressObjectiveDigest !== objectiveDigest) {
|
|
687
|
+
managed.progressObjectiveDigest = objectiveDigest;
|
|
688
|
+
managed.verifiedCriterionHistory = [];
|
|
689
|
+
}
|
|
690
|
+
if (managed.verifiedCriterionHistory.includes(id)) return;
|
|
691
|
+
managed.verifiedCriterionHistory.push(id);
|
|
692
|
+
} else if (Object.hasOwn(managed.verified, id)) return;
|
|
693
|
+
managed.verifiedRevision += 1;
|
|
694
|
+
managed.lastVerifiedProgressAt = Math.max(managed.lastVerifiedProgressAt ?? run.startedAt, now);
|
|
695
|
+
}
|
|
666
696
|
export async function verifyAutomaticCriteria(record, now) {
|
|
667
697
|
const managed = record.run.managed;
|
|
698
|
+
managed.criterionObservations = {};
|
|
699
|
+
const observe = (criterion, passed, detail, sha256 = null) => {
|
|
700
|
+
managed.criterionObservations[criterion.id] = { criterion_id: criterion.id, passed,
|
|
701
|
+
verification_method: `host_${criterion.check.type}`, observed_result: detail,
|
|
702
|
+
evidence_refs: [`host-check:${managed.taskId}:${criterion.id}:${sha256 || 'failed'}`] };
|
|
703
|
+
};
|
|
704
|
+
const failed = (criterion, detail) => { delete managed.verified[criterion.id]; observe(criterion, false, detail); };
|
|
668
705
|
for (const criterion of managed.acceptanceCriteria) {
|
|
669
706
|
if (criterion.check.type === 'manual') continue;
|
|
670
707
|
const path = criterion.check.type === 'file_equals' ? criterion.check.exactPath || criterion.check.path : criterion.check.path;
|
|
671
|
-
if (!path) {
|
|
708
|
+
if (!path) { failed(criterion, 'Frozen acceptance file path is absent'); continue; }
|
|
672
709
|
const file = resolve(record.cwd, path);
|
|
673
710
|
const rel = relative(record.cwd, file);
|
|
674
|
-
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
711
|
+
if (rel.startsWith('..') || isAbsolute(rel)) { failed(criterion, 'Acceptance path is outside the enrolled workspace'); continue; }
|
|
675
712
|
try {
|
|
676
713
|
const actual = await realpath(file);
|
|
677
714
|
const actualRel = relative(record.cwd, actual);
|
|
678
|
-
if (actualRel.startsWith('..') || isAbsolute(actualRel)) {
|
|
715
|
+
if (actualRel.startsWith('..') || isAbsolute(actualRel)) { failed(criterion, 'Resolved acceptance path is outside the enrolled workspace'); continue; }
|
|
679
716
|
const info = await stat(actual);
|
|
680
|
-
if (!info.isFile() || info.size > 1024 * 1024) {
|
|
717
|
+
if (!info.isFile() || info.size > 1024 * 1024) { failed(criterion, 'Acceptance file is not a bounded regular file'); continue; }
|
|
681
718
|
const content = await readFile(actual);
|
|
682
|
-
if (criterion.check.type === 'file_contains' && !content.toString('utf8').includes(criterion.check.text)) {
|
|
683
|
-
if (criterion.check.type === 'file_equals' && !content.equals(Buffer.from(criterion.check.text, 'utf8'))) {
|
|
684
|
-
|
|
685
|
-
managed.verifiedRevision += 1;
|
|
686
|
-
managed.lastVerifiedProgressAt = Math.max(managed.lastVerifiedProgressAt ?? record.run.startedAt, now);
|
|
687
|
-
}
|
|
719
|
+
if (criterion.check.type === 'file_contains' && !content.toString('utf8').includes(criterion.check.text)) { failed(criterion, 'Observed file lacks the required frozen content'); continue; }
|
|
720
|
+
if (criterion.check.type === 'file_equals' && !content.equals(Buffer.from(criterion.check.text, 'utf8'))) { failed(criterion, 'Observed file differs from the frozen exact contents'); continue; }
|
|
721
|
+
newVerifiedCriterion(record.run, criterion.id, now);
|
|
688
722
|
managed.verified[criterion.id] = { source: criterion.check.type, checkedAt: now, path, sha256: digest(content) };
|
|
689
|
-
|
|
723
|
+
observe(criterion, true, 'Frozen file acceptance check passed', digest(content));
|
|
724
|
+
} catch { failed(criterion, 'Acceptance file could not be independently read'); }
|
|
690
725
|
}
|
|
691
726
|
}
|
|
692
727
|
export async function verifyDelivery({ root = rootDirectory(), provider, session, taskId, criterionId, now = Date.now() }) {
|
|
@@ -696,10 +731,7 @@ export async function verifyDelivery({ root = rootDirectory(), provider, session
|
|
|
696
731
|
if (taskId !== run.managed.taskId) throw new Error('Managed task identity changed; refresh its status.');
|
|
697
732
|
const criterion = run.managed.acceptanceCriteria.find(item => item.id === criterionId);
|
|
698
733
|
if (!criterion || criterion.check.type !== 'manual') throw new Error('Only manual criteria can be verified by the user.');
|
|
699
|
-
|
|
700
|
-
run.managed.verifiedRevision += 1;
|
|
701
|
-
run.managed.lastVerifiedProgressAt = Math.max(run.managed.lastVerifiedProgressAt ?? run.startedAt, now);
|
|
702
|
-
}
|
|
734
|
+
newVerifiedCriterion(run, criterionId, now);
|
|
703
735
|
run.managed.verified[criterionId] = { source: 'user', checkedAt: now };
|
|
704
736
|
if (run.state === 'verification_pending' && run.checkpoint?.status === 'completed') {
|
|
705
737
|
await verifyAutomaticCriteria(record, now);
|
|
@@ -742,13 +774,70 @@ export function prepareRecoveryDiagnosis(run, now = Date.now()) {
|
|
|
742
774
|
run.nextStep = decision.action.summary;
|
|
743
775
|
return decision;
|
|
744
776
|
}
|
|
745
|
-
export function
|
|
746
|
-
if (now >= run.deadline || run.continuations >= run.maxContinuations || run.failures >= run.maxFailures) return 'Original delivery time, continuation, or failure budget exhausted.';
|
|
777
|
+
export function deliveryBudgetState(run, now = Date.now(), { checkProgress = true } = {}) {
|
|
747
778
|
const managed = run.managed;
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
779
|
+
const progress = managed?.continuationPolicy === 'progress';
|
|
780
|
+
const deadline = progress ? run.hardDeadline ?? run.startedAt + 1440 * 60_000 : run.deadline;
|
|
781
|
+
const maxContinuations = progress ? run.hardMaxContinuations ?? 50 : run.maxContinuations;
|
|
782
|
+
const tokenCeiling = progress ? managed.providerTokenCeiling : managed?.maxTokens;
|
|
783
|
+
const warnings = [];
|
|
784
|
+
const checkpointDetail = run.checkpoint ? '' : ' No progress checkpoint was recorded.';
|
|
785
|
+
if (progress) {
|
|
786
|
+
if (now >= run.deadline) warnings.push('soft_time_limit');
|
|
787
|
+
if (run.continuations >= run.maxContinuations) warnings.push('soft_continuation_limit');
|
|
788
|
+
if (managed.maxTokens && managed.tokensUsed >= managed.maxTokens) warnings.push('soft_token_limit');
|
|
789
|
+
}
|
|
790
|
+
let reason = null;
|
|
791
|
+
if (now >= deadline) {
|
|
792
|
+
reason = progress ? 'Delivery hard time ceiling reached.' : `Delivery time limit exhausted.${checkpointDetail}`;
|
|
793
|
+
} else if (run.continuations >= maxContinuations) {
|
|
794
|
+
reason = progress ? 'Delivery hard continuation ceiling reached.' : `Delivery continuation limit exhausted (${run.continuations}/${maxContinuations}).${checkpointDetail}`;
|
|
795
|
+
} else if (run.failures >= run.maxFailures) {
|
|
796
|
+
reason = `Delivery failure limit exhausted (${run.failures}/${run.maxFailures}).${checkpointDetail}`;
|
|
797
|
+
} else if (managed?.usageBlocked) {
|
|
798
|
+
reason = `Reliable provider token usage is unavailable; budgeted continuation is blocked.${checkpointDetail}`;
|
|
799
|
+
} else if (tokenCeiling && managed.tokensUsed >= tokenCeiling) {
|
|
800
|
+
reason = progress ? 'Provider hard token ceiling reached.' : `Provider token budget exhausted (${managed.tokensUsed}/${tokenCeiling}).${checkpointDetail}`;
|
|
801
|
+
} else if (checkProgress && managed && now - (managed.lastVerifiedProgressAt ?? run.startedAt) >= (managed.maxNoProgressMinutes ?? 30) * 60_000) {
|
|
802
|
+
reason = 'No verified acceptance progress within the configured time limit.';
|
|
803
|
+
} else if (checkProgress && managed && managed.noProgress >= managed.maxNoProgress) {
|
|
804
|
+
reason = 'No verified acceptance progress within the configured continuation limit.';
|
|
805
|
+
}
|
|
806
|
+
return { continuationPolicy: progress ? 'progress' : 'bounded', deadline, maxContinuations, tokenCeiling, warnings, reason };
|
|
807
|
+
}
|
|
808
|
+
export function managedBudgetReason(run, now = Date.now(), options) {
|
|
809
|
+
return deliveryBudgetState(run, now, options).reason;
|
|
810
|
+
}
|
|
811
|
+
export function managedDispatchDeadline(run) {
|
|
812
|
+
return deliveryBudgetState(run).deadline;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function recordManagedOutcomeProgress(run, now, assessmentAvailable = true) {
|
|
816
|
+
const managed = run.managed;
|
|
817
|
+
if (managed?.continuationPolicy !== 'progress') return;
|
|
818
|
+
const current = managed.acceptanceCriteria.map(criterion => ({ id: criterion.id, statement: criterion.description }));
|
|
819
|
+
const objectiveDigest = digest(JSON.stringify({ objective: run.objective, criteria: managed.acceptanceCriteria }));
|
|
820
|
+
const results = current.flatMap(criterion => {
|
|
821
|
+
const observation = managed.verified[criterion.id];
|
|
822
|
+
return observation ? [{ criterion_id: criterion.id, passed: true,
|
|
823
|
+
verification_method: observation.source === 'user' ? 'explicit_user_verification' : `host_${observation.source}`,
|
|
824
|
+
observed_result: `Frozen ${criterion.id} acceptance check verified`,
|
|
825
|
+
evidence_refs: [observation.source === 'user' ? `user:${managed.taskId}:${criterion.id}:${observation.checkedAt}`
|
|
826
|
+
: `host:${criterion.id}:${observation.sha256}`] }]
|
|
827
|
+
: managed.criterionObservations?.[criterion.id] ? [managed.criterionObservations[criterion.id]] : [];
|
|
828
|
+
});
|
|
829
|
+
managed.outcomeProgress = evaluateOutcomeProgress({
|
|
830
|
+
criteria: current, objectiveDigest, objectiveVersion: managed.understandingRevision || 1,
|
|
831
|
+
criterionResults: results, assessmentAvailable,
|
|
832
|
+
previous: managed.outcomeProgress, maxNoProgressAttempts: managed.maxNoProgress,
|
|
833
|
+
findings: run.checkpoint?.status === 'blocked' ? [{
|
|
834
|
+
code: run.checkpoint.blocker, blocker_kind: run.checkpoint.blocker === 'external_dependency' ? 'external' : 'human',
|
|
835
|
+
explanation: run.checkpoint.summary,
|
|
836
|
+
}] : [],
|
|
837
|
+
});
|
|
838
|
+
managed.noProgress = managed.outcomeProgress.no_progress_attempts;
|
|
839
|
+
if (managed.outcomeProgress.has_progress) managed.lastVerifiedProgressAt = Math.max(managed.lastVerifiedProgressAt ?? run.startedAt, now);
|
|
840
|
+
managed.progressCheckpointRevision = run.revision;
|
|
752
841
|
}
|
|
753
842
|
// Manual/legacy runs have no marker and retain their existing controls.
|
|
754
843
|
export function automaticContinuationAllowed(record) {
|
|
@@ -759,6 +848,7 @@ export function automaticContinuationAllowed(record) {
|
|
|
759
848
|
export function decideStop(run, now = Date.now()) {
|
|
760
849
|
if (TERMINAL.has(run.state) || ['blocked', 'recovery_required', 'paused', 'verification_pending'].includes(run.state)) return { run, output: {} };
|
|
761
850
|
const next = structuredClone(run);
|
|
851
|
+
next.budgetWarnings = deliveryBudgetState(next, now).warnings;
|
|
762
852
|
const finish = (state, reason) => { next.state = state; next.reason = reason; next.updatedAt = now; return { run: next, output: {} }; };
|
|
763
853
|
// A fresh, explicitly evidenced completion may finish at the budget boundary.
|
|
764
854
|
const fresh = next.checkpoint && next.revision > next.consumedRevision;
|
|
@@ -776,7 +866,7 @@ export function decideStop(run, now = Date.now()) {
|
|
|
776
866
|
} else { escalate(next, cp.summary); return finish('blocked', cp.summary); }
|
|
777
867
|
}
|
|
778
868
|
const budgetReason = managedBudgetReason(next, now);
|
|
779
|
-
if (budgetReason) return finish('budget_exhausted', budgetReason);
|
|
869
|
+
if (budgetReason) return finish(next.managed?.continuationPolicy === 'progress' && budgetReason.startsWith('No verified') ? 'paused' : 'budget_exhausted', budgetReason);
|
|
780
870
|
const initialHook = next.managed?.awaitingInitialHook && next.revision === 0 && !next.checkpoint;
|
|
781
871
|
const replanHook = next.managed?.capability === 'live_hook' && next.managed.awaitingReplanHook === true;
|
|
782
872
|
const understandingHook = next.managed?.capability === 'live_hook' && (next.managed.awaitingUnderstandingHook === true || next.managed.awaitingAssociationHook === true);
|
|
@@ -811,11 +901,13 @@ export function decideStop(run, now = Date.now()) {
|
|
|
811
901
|
const reportedStep = next.managed.associationCheckpointRevision !== next.revision && next.managed.contextBootstrap && next.managed.phase === 'active' && !next.managed.acceptanceCriteria.length &&
|
|
812
902
|
!['session_understanding', 'action_association'].includes(cp?.kind) && cp?.status === 'in_progress' &&
|
|
813
903
|
next.managed.workHistory?.at(-1)?.result?.status === 'observed' && next.managed.workHistory.at(-1).result.evidence?.length;
|
|
814
|
-
if (next.managed.
|
|
904
|
+
if (next.managed.continuationPolicy === 'progress') {
|
|
905
|
+
if (next.managed.associationCheckpointRevision !== next.revision && !initialHook && !replanHook && !understandingHook && !approvedHook && !legacyApprovedHook && !['session_understanding', 'action_association'].includes(cp?.kind) && (!fresh || next.managed.progressCheckpointRevision !== next.revision)) recordManagedOutcomeProgress(next, now, Boolean(fresh));
|
|
906
|
+
} else if (next.managed.verifiedRevision > next.managed.consumedVerifiedRevision) next.managed.noProgress = 0;
|
|
815
907
|
else if (next.managed.associationCheckpointRevision !== next.revision && !initialHook && !replanHook && !understandingHook && !approvedHook && !legacyApprovedHook && !['session_understanding', 'action_association'].includes(cp?.kind)) next.managed.noProgress += 1;
|
|
816
908
|
if (reportedStep) next.managed.reportedProgress = (next.managed.reportedProgress || 0) + 1;
|
|
817
909
|
next.managed.consumedVerifiedRevision = next.managed.verifiedRevision;
|
|
818
|
-
if (next.managed.noProgress >= next.managed.maxNoProgress) { escalate(next, 'No verified acceptance progress within the configured continuation limit.'); return finish('failed', 'No verified acceptance progress within the configured continuation limit.'); }
|
|
910
|
+
if (next.managed.noProgress >= next.managed.maxNoProgress) { escalate(next, 'No verified acceptance progress within the configured continuation limit.'); return finish(next.managed.continuationPolicy === 'progress' ? 'paused' : 'failed', 'No verified acceptance progress within the configured continuation limit.'); }
|
|
819
911
|
}
|
|
820
912
|
next.consumedRevision = next.revision;
|
|
821
913
|
if (initialHook) next.managed.awaitingInitialHook = false;
|
|
@@ -1005,7 +1097,11 @@ export function canReplanDelivery(run, provider, now = Date.now()) {
|
|
|
1005
1097
|
run.state === 'blocked' && managed.pendingApproval?.denied === true &&
|
|
1006
1098
|
!managedBudgetReason(run, now));
|
|
1007
1099
|
}
|
|
1008
|
-
export
|
|
1100
|
+
export function canChangeDeliveryContinuation(run) {
|
|
1101
|
+
return Boolean(run?.managed && ['paused', 'blocked', 'failed', 'budget_exhausted', 'verification_pending', 'recovery_required'].includes(run.state) &&
|
|
1102
|
+
!['pending', 'pending_received', 'transferred'].includes(run.managed.handoff?.status));
|
|
1103
|
+
}
|
|
1104
|
+
export async function controlDelivery({ root = rootDirectory(), provider, session, taskId, approvalId, autonomyPolicy, continuationPolicy, limits, questionId, waitId, answer, action, now = Date.now() }) {
|
|
1009
1105
|
for (let retry = 0; ; retry += 1) {
|
|
1010
1106
|
try { return await updateRecord(root, provider, session, record => {
|
|
1011
1107
|
const run = record?.run;
|
|
@@ -1013,6 +1109,52 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
|
|
|
1013
1109
|
if (run.managed && taskId !== run.managed.taskId) throw new Error('Managed task identity changed; refresh its status.');
|
|
1014
1110
|
if (['pending', 'pending_received'].includes(run.managed?.handoff?.status)) throw new Error('Task handoff is locked; use delivery handoff_cancel or retry the same target to complete the transfer.');
|
|
1015
1111
|
if (action !== 'stop' && run.managed?.handoff?.status === 'transferred') throw new Error('Task handoff is locked; continue in the received target session.');
|
|
1112
|
+
if (action === 'extend' || action === 'continuation_policy') {
|
|
1113
|
+
if (!canChangeDeliveryContinuation(run)) throw new Error('Pause a managed task before changing its continuation bounds.');
|
|
1114
|
+
if (action === 'continuation_policy') {
|
|
1115
|
+
if (!['bounded', 'progress'].includes(continuationPolicy)) throw new Error('Select an explicit continuation policy.');
|
|
1116
|
+
run.managed.continuationPolicy = continuationMode(continuationPolicy);
|
|
1117
|
+
if (continuationPolicy === 'progress') {
|
|
1118
|
+
run.hardDeadline ??= run.startedAt + 1440 * 60_000;
|
|
1119
|
+
run.hardMaxContinuations ??= 50;
|
|
1120
|
+
if (run.managed.resumeChannel === 'codex_app_server') run.managed.providerTokenCeiling ??= MAX_COMPLETION_TOKENS;
|
|
1121
|
+
}
|
|
1122
|
+
run.continuationPolicyHistory ||= [];
|
|
1123
|
+
run.continuationPolicyHistory.push({ policy: continuationPolicy, at: now, source: 'explicit_controller_selection' });
|
|
1124
|
+
} else {
|
|
1125
|
+
if (!limits || typeof limits !== 'object' || Array.isArray(limits) || !Object.keys(limits).length ||
|
|
1126
|
+
Object.keys(limits).some(key => !['hardMaxMinutes', 'hardMaxContinuations', 'providerTokenCeiling'].includes(key))) throw new Error('Supply explicit continuation ceiling increases.');
|
|
1127
|
+
const progress = run.managed.continuationPolicy === 'progress';
|
|
1128
|
+
const before = { deadline: progress ? run.hardDeadline : run.deadline,
|
|
1129
|
+
maxContinuations: progress ? run.hardMaxContinuations : run.maxContinuations,
|
|
1130
|
+
tokens: progress ? run.managed.providerTokenCeiling : run.managed.maxTokens };
|
|
1131
|
+
const after = { ...before };
|
|
1132
|
+
if (limits.hardMaxMinutes !== undefined) {
|
|
1133
|
+
after.deadline = run.startedAt + positive(limits.hardMaxMinutes, null, 1440) * 60_000;
|
|
1134
|
+
if (after.deadline <= before.deadline) throw new Error('Extension must increase the original deadline within 24 hours of its start.');
|
|
1135
|
+
}
|
|
1136
|
+
if (limits.hardMaxContinuations !== undefined) {
|
|
1137
|
+
after.maxContinuations = positive(limits.hardMaxContinuations, null, 50);
|
|
1138
|
+
if (after.maxContinuations <= before.maxContinuations) throw new Error('Extension must increase the continuation ceiling within 50 turns.');
|
|
1139
|
+
}
|
|
1140
|
+
if (limits.providerTokenCeiling !== undefined) {
|
|
1141
|
+
if (run.managed.resumeChannel !== 'codex_app_server') throw new Error('Provider token ceilings require host-observed Codex usage.');
|
|
1142
|
+
after.tokens = positive(limits.providerTokenCeiling, null, MAX_COMPLETION_TOKENS);
|
|
1143
|
+
if (after.tokens <= (before.tokens || 0)) throw new Error('Extension must increase the provider token ceiling.');
|
|
1144
|
+
}
|
|
1145
|
+
if (after.deadline <= now || after.maxContinuations <= run.continuations || after.tokens && after.tokens <= run.managed.tokensUsed) throw new Error('Extended ceilings must exceed already consumed time, turns, and tokens.');
|
|
1146
|
+
if (progress) { run.hardDeadline = after.deadline; run.hardMaxContinuations = after.maxContinuations; run.managed.providerTokenCeiling = after.tokens; }
|
|
1147
|
+
else { run.deadline = after.deadline; run.maxContinuations = after.maxContinuations; run.managed.maxTokens = after.tokens; }
|
|
1148
|
+
run.budgetExtensionHistory ||= [];
|
|
1149
|
+
run.budgetExtensionHistory.push({ before, after, at: now, source: 'explicit_controller_extension' });
|
|
1150
|
+
}
|
|
1151
|
+
run.budgetWarnings = deliveryBudgetState(run, now).warnings;
|
|
1152
|
+
if (run.state === 'budget_exhausted' && !managedBudgetReason(run, now)) {
|
|
1153
|
+
run.state = 'paused'; run.reason = 'Continuation bounds changed; explicitly resume this same task.';
|
|
1154
|
+
}
|
|
1155
|
+
run.updatedAt = now;
|
|
1156
|
+
return record;
|
|
1157
|
+
}
|
|
1016
1158
|
if (action === 'dependency_ready') {
|
|
1017
1159
|
const wait = run.managed?.dependencyWait;
|
|
1018
1160
|
if (run.state !== 'blocked' || run.blocker !== 'external_dependency' || !wait || wait.id !== waitId ||
|
|
@@ -1166,7 +1308,14 @@ export async function reconcileRecords({ root = rootDirectory(), now = Date.now(
|
|
|
1166
1308
|
run.state = 'completed'; run.reason = run.checkpoint.summary; run.updatedAt = now;
|
|
1167
1309
|
}
|
|
1168
1310
|
}
|
|
1169
|
-
|
|
1311
|
+
const activeTurn = run.managed?.continuationPolicy === 'progress' &&
|
|
1312
|
+
(run.state === 'auto_resuming' || current.runtime?.isRunning === true || run.managed.executionAttempt?.phase === 'dispatching');
|
|
1313
|
+
const budgetState = deliveryBudgetState(run, now, { checkProgress: !activeTurn });
|
|
1314
|
+
run.budgetWarnings = budgetState.warnings;
|
|
1315
|
+
if (budgetState.reason && !TERMINAL.has(run.state) && !['verification_pending', 'paused'].includes(run.state)) {
|
|
1316
|
+
run.state = run.managed?.continuationPolicy === 'progress' && budgetState.reason.startsWith('No verified') ? 'paused' : 'budget_exhausted';
|
|
1317
|
+
run.reason = budgetState.reason; run.updatedAt = now;
|
|
1318
|
+
}
|
|
1170
1319
|
else if (run.state === 'active' && observed !== null && observed !== current.process?.identity && current.process?.identity === record.process?.identity) {
|
|
1171
1320
|
run.state = 'recovery_required'; run.recoveryCause = 'process_exit'; run.reason = run.managed?.autoRecovery ? 'Observed provider process exited. Managed recovery is eligible for a bounded exact-session attempt.' : 'Observed provider process exited. Reopen the existing conversation and explicitly resume.'; run.updatedAt = now;
|
|
1172
1321
|
}
|