@awak-app/simy-cli 0.5.75 → 0.5.76
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 +21 -35
- package/package.json +1 -1
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +28 -22
- package/src/codex-plugins/simy/skills/delivery-loop/SKILL.md +3 -3
- package/src/console/app.js +1 -2
- package/src/delivery-hooks/command.js +1 -1
- package/src/delivery-hooks/desktop.js +5 -4
- package/src/delivery-hooks/supervisor.js +10 -19
- package/src/local-task.js +8 -40
- package/src/orchestrator/budget.js +15 -20
- package/src/orchestrator/checklist-continuation.js +3 -3
- package/src/orchestrator/delivery/WORKFLOW.md +15 -14
- package/src/orchestrator/delivery/references/acceptance-evidence.md +4 -4
- package/src/orchestrator/delivery/references/chrome-user-verification.md +1 -1
- package/src/orchestrator/delivery/references/desktop-monitor-triggers.md +1 -1
- package/src/orchestrator/delivery/references/minimum-completion.md +1 -1
- package/src/orchestrator/instruction.js +4 -8
- package/src/orchestrator/loop.js +1 -2
- package/src/orchestrator/outcome-monitor.js +3 -3
- package/src/orchestrator/pr-closure.js +1 -1
- package/src/orchestrator/presentation.js +5 -6
- package/src/runner.js +4 -21
- package/src/session-supervision/codex-app-server.js +1 -1
- package/src/session-supervision/index.js +4 -5
- package/src/sqm/bundle-store.js +1 -0
- package/src/sqm/diagnostics.js +19 -5
- package/src/sqm/evidence-client.js +1 -0
- package/src/sqm/review-worker.js +8 -4
package/README.md
CHANGED
|
@@ -29,38 +29,25 @@ actions together. Environment-only calls remain supported. Task checks reuse the
|
|
|
29
29
|
execution charter policy; model entitlement, external test resources and assurance
|
|
30
30
|
selection evidence are not guessed from a successful login or recommendation.
|
|
31
31
|
|
|
32
|
-
###
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
explicit hard ceiling (at most 10 million reported provider tokens). The Agentic
|
|
44
|
-
controller limits a whole progress run to its approved allocation, at most
|
|
45
|
-
15 million tokens, and persists the allocation with canonical receipts. Missing
|
|
46
|
-
reliable usage is charged conservatively against the reserved allocation. The
|
|
47
|
-
Web controller supports this mode for the connected local Codex/Claude engines;
|
|
48
|
-
unsupported cloud execution is refused before dispatch.
|
|
49
|
-
|
|
50
|
-
In the App, pause a managed Delivery task before changing its continuation
|
|
51
|
-
policy or extending its bounds. Extension keeps usage, task identity, acceptance
|
|
52
|
-
and approvals, and requires a separate resume action. Delivery hard ceilings are
|
|
53
|
-
24 hours and 50 continuations; host-observed Codex usage can also impose a token
|
|
54
|
-
ceiling. A Claude live-hook session without reliable token telemetry explicitly
|
|
55
|
-
uses time/continuation bounds instead of inventing usage.
|
|
32
|
+
### Local continuation
|
|
33
|
+
|
|
34
|
+
The recorded **Bounded** and **Progress** policy fields remain readable for
|
|
35
|
+
compatibility. Independently verified acceptance criteria permit further work. Local token usage, elapsed time
|
|
36
|
+
and attempt counters do not stop execution, including old tasks restored from
|
|
37
|
+
bounded policies. Repeated work without new proof pauses the same task. Approval
|
|
38
|
+
requests, explicit stops and authorization failures still pause execution.
|
|
39
|
+
Provider text claiming success does not count as verification. Token records
|
|
40
|
+
remain available for history and usage warnings. The Web controller supports
|
|
41
|
+
connected local Codex/Claude engines; unsupported cloud execution is refused
|
|
42
|
+
before dispatch.
|
|
56
43
|
|
|
57
44
|
For interrupted Agentic work, `GET /v1/agentic-loop/:id/recovery` provides a
|
|
58
45
|
read-only objective and checkout fingerprint. Reconcile an uncertain side effect
|
|
59
46
|
with `POST` to that endpoint (`action: reconcile`, authoritative
|
|
60
47
|
`operation_status`, `observation`, `evidence_refs`) before resuming. A paused
|
|
61
|
-
|
|
62
|
-
`POST /v1/agentic-loop/:id/continuation-ceilings
|
|
63
|
-
|
|
48
|
+
run retains the legacy metadata endpoint
|
|
49
|
+
`POST /v1/agentic-loop/:id/continuation-ceilings` for compatibility. These fields
|
|
50
|
+
no longer impose local limits and changing them never resets usage or resumes work. Both routes require the original authorized
|
|
64
51
|
account, organization and device throughout the request.
|
|
65
52
|
|
|
66
53
|
## Apple Silicon Mac app
|
|
@@ -186,7 +173,7 @@ tasks are not restarted. Missing or disabled plugins are not enabled automatical
|
|
|
186
173
|
Failed refreshes retry on the next check without blocking daemon startup.
|
|
187
174
|
The desktop Sessions page discovers existing provider conversations and lets users
|
|
188
175
|
choose monitoring or supported session supervision with a goal, acceptance
|
|
189
|
-
criteria
|
|
176
|
+
criteria and an autonomy policy. Local time, token and continuation counters do not stop work. Codex enrollment uses an exact
|
|
190
177
|
app-server session and checks writer ownership before each continuation. Claude
|
|
191
178
|
can continue through a verified live SIMY hook in its original process; sessions
|
|
192
179
|
without that hook support monitoring only, and cold takeover remains unavailable. See [session supervision](docs/session-supervision.md).
|
|
@@ -259,8 +246,7 @@ chat message creates the remote audit ledger and starts the local Agentic Loop
|
|
|
259
246
|
state machine; SIMY Web mirrors the lifecycle but is not required as the task
|
|
260
247
|
entry point. The CLI also accepts backend-issued launch challenges for Web-led
|
|
261
248
|
runs. It builds the requirement charter, runs Codex or Claude Code, audits
|
|
262
|
-
structured completion evidence, and re-instructs the executor
|
|
263
|
-
configured attempt budget.
|
|
249
|
+
structured completion evidence, and re-instructs the executor while verification shows useful progress.
|
|
264
250
|
|
|
265
251
|
Before Web creates an Agentic Loop, it classifies whether the request actually
|
|
266
252
|
needs a managed PR, verification, evidence, and merge lifecycle. A bounded
|
|
@@ -321,8 +307,8 @@ Local Tasks provide:
|
|
|
321
307
|
- `GET /v1/local-tasks/<id>/stream` for live Server-Sent Events;
|
|
322
308
|
- `POST /v1/local-tasks/<id>/control` with `{"action":"stop"}`;
|
|
323
309
|
- `GET /v1/local-tasks/<id>/artifacts/<artifact-id>` for a verified output;
|
|
324
|
-
- one Provider invocation
|
|
325
|
-
|
|
310
|
+
- one Provider invocation with recorded token usage; local token, elapsed-time
|
|
311
|
+
and attempt counters do not interrupt execution, including restored tasks;
|
|
326
312
|
- restart-safe terminal persistence. An interrupted active task fails closed
|
|
327
313
|
after CLI restart and is never replayed automatically.
|
|
328
314
|
|
|
@@ -706,9 +692,9 @@ evaluated action and marks evidence pending when the code or runtime context
|
|
|
706
692
|
changes. A non-green table is a checkpoint, not completion: the executor must
|
|
707
693
|
perform feasible authorized repairs, missing verification and bounded waits,
|
|
708
694
|
then re-observe and update the same criteria until the requested endpoint passes.
|
|
709
|
-
A genuine safety
|
|
710
|
-
with affected IDs and a concrete resumption condition
|
|
711
|
-
|
|
695
|
+
A genuine safety, authority, dependency or no-progress stop remains explicitly
|
|
696
|
+
unfinished with affected IDs and a concrete resumption condition. No background
|
|
697
|
+
monitor is implied. Both executor and independent auditor receive
|
|
712
698
|
this continuation contract. The [acceptance evidence guide](src/orchestrator/delivery/references/acceptance-evidence.md)
|
|
713
699
|
documents the offline validator shipped inside the CLI runtime. Its result establishes
|
|
714
700
|
record completeness and artifact integrity, not overall delivery or deployment.
|
package/package.json
CHANGED
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.76";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
|
@@ -415,8 +415,6 @@ function managedSpec(spec, session, provider, observedProcess, admission, now) {
|
|
|
415
415
|
const appServerChannel = config.resumeChannel === 'codex_app_server' && admission?.source === 'codex_app_server' && admission.session === session;
|
|
416
416
|
const autoRecovery = config.autoRecovery === true && exactCodexSession && Boolean(resumeModel) &&
|
|
417
417
|
(appServerChannel || config.resumeChannel === 'codex_exec' && Boolean(observedProcess?.identity));
|
|
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.');
|
|
420
418
|
const managed = { taskId: randomUUID(), thread: { provider, id: session },
|
|
421
419
|
actionId: text(config.actionId, 200) || null, actionDueAt, acceptanceCriteria: criteria, verified: {}, remainingWork: text(config.remainingWork, 4000) || null,
|
|
422
420
|
initialAction, proposedAction: initialAction, approvalPolicy, approvalScopes: [...new Set(scopes)].slice(0, 20),
|
|
@@ -426,7 +424,7 @@ function managedSpec(spec, session, provider, observedProcess, admission, now) {
|
|
|
426
424
|
noProgress: 0, verifiedRevision: 0, consumedVerifiedRevision: 0, notification: null, notificationKey: null,
|
|
427
425
|
continuationPolicy: continuationMode(config.continuationPolicy),
|
|
428
426
|
providerTokenCeiling: appServerChannel ? positive(config.providerTokenCeiling, MAX_COMPLETION_TOKENS, MAX_COMPLETION_TOKENS) : null,
|
|
429
|
-
budgetCapability: { time:
|
|
427
|
+
budgetCapability: { time: false, continuations: false, providerTokens: appServerChannel, localExecutionLimits: false },
|
|
430
428
|
autoRecovery, resumeModel, autoRecoveryUnavailableReason: config.autoRecovery === true && !autoRecovery ? 'Exact Codex process, session UUID, and resumeModel are required.' : null,
|
|
431
429
|
autoRecoverySandbox: config.autoRecoverySandbox || 'read-only', resumeChannel: config.resumeChannel || null,
|
|
432
430
|
capability: appServerChannel && autoRecovery ? 'codex_app_server' : autoRecovery ? 'codex_exec_resume' : 'live_hook',
|
|
@@ -777,9 +775,11 @@ export function prepareRecoveryDiagnosis(run, now = Date.now()) {
|
|
|
777
775
|
export function deliveryBudgetState(run, now = Date.now(), { checkProgress = true } = {}) {
|
|
778
776
|
const managed = run.managed;
|
|
779
777
|
const progress = managed?.continuationPolicy === 'progress';
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const
|
|
778
|
+
// Historical policies/counters remain immutable, but local elapsed time,
|
|
779
|
+
// turn count and token usage no longer limit either new or restored tasks.
|
|
780
|
+
const deadline = Infinity;
|
|
781
|
+
const maxContinuations = Infinity;
|
|
782
|
+
const tokenCeiling = null;
|
|
783
783
|
const warnings = [];
|
|
784
784
|
const checkpointDetail = run.checkpoint ? '' : ' No progress checkpoint was recorded.';
|
|
785
785
|
if (progress) {
|
|
@@ -788,22 +788,14 @@ export function deliveryBudgetState(run, now = Date.now(), { checkProgress = tru
|
|
|
788
788
|
if (managed.maxTokens && managed.tokensUsed >= managed.maxTokens) warnings.push('soft_token_limit');
|
|
789
789
|
}
|
|
790
790
|
let reason = null;
|
|
791
|
-
if (
|
|
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) {
|
|
791
|
+
if (run.failures >= run.maxFailures) {
|
|
796
792
|
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
793
|
} else if (checkProgress && managed && now - (managed.lastVerifiedProgressAt ?? run.startedAt) >= (managed.maxNoProgressMinutes ?? 30) * 60_000) {
|
|
802
794
|
reason = 'No verified acceptance progress within the configured time limit.';
|
|
803
795
|
} else if (checkProgress && managed && managed.noProgress >= managed.maxNoProgress) {
|
|
804
796
|
reason = 'No verified acceptance progress within the configured continuation limit.';
|
|
805
797
|
}
|
|
806
|
-
return { continuationPolicy: progress ? 'progress' : 'bounded', deadline, maxContinuations, tokenCeiling, warnings, reason };
|
|
798
|
+
return { continuationPolicy: progress ? 'progress' : 'bounded', localLimitsRemoved: true, deadline, maxContinuations, tokenCeiling, warnings, reason };
|
|
807
799
|
}
|
|
808
800
|
export function managedBudgetReason(run, now = Date.now(), options) {
|
|
809
801
|
return deliveryBudgetState(run, now, options).reason;
|
|
@@ -866,7 +858,7 @@ export function decideStop(run, now = Date.now()) {
|
|
|
866
858
|
} else { escalate(next, cp.summary); return finish('blocked', cp.summary); }
|
|
867
859
|
}
|
|
868
860
|
const budgetReason = managedBudgetReason(next, now);
|
|
869
|
-
if (budgetReason) return finish(
|
|
861
|
+
if (budgetReason) return finish(budgetReason.startsWith('No verified') ? 'paused' : 'budget_exhausted', budgetReason);
|
|
870
862
|
const initialHook = next.managed?.awaitingInitialHook && next.revision === 0 && !next.checkpoint;
|
|
871
863
|
const replanHook = next.managed?.capability === 'live_hook' && next.managed.awaitingReplanHook === true;
|
|
872
864
|
const understandingHook = next.managed?.capability === 'live_hook' && (next.managed.awaitingUnderstandingHook === true || next.managed.awaitingAssociationHook === true);
|
|
@@ -927,7 +919,7 @@ export function decideStop(run, now = Date.now()) {
|
|
|
927
919
|
next.reason = fresh ? 'continue_pending_work' : 'checkpoint_missing';
|
|
928
920
|
const selected = next.managed?.autonomyPolicy ? next.managed.decision : null;
|
|
929
921
|
const instruction = selected && next.managed.phase === 'action_association' ? `${associationContract(next.managed.actionAssociation.job)} Submit the JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping.` : selected && next.managed.phase === 'understanding' ? `Understand the current user request in this ORIGINAL conversation. ${next.managed.understandingAnswer ? `The user answered question ${next.managed.understandingAnswer.questionId}: ${next.managed.understandingAnswer.text}. ` : ''}${understandingCheckpointContract(selected)} Submit the JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping. A shell heredoc is suitable. This updates SIMY metadata only; do not create a checkpoint file in the task workspace.` : selected ? `${initialHook ? `Managed objective: ${next.objective}. Frozen acceptance criteria: ${JSON.stringify(next.managed.acceptanceCriteria)}. ` : ''}${replanHook ? `Replan context: ${next.managed.plan.diagnosis}. ` : ''}Execute only selected ${selected.action.kind} action in scope ${selected.action.scope}: ${selected.action.summary}. Expected verification: ${selected.expectedVerification}. Rollback: ${selected.rollback || 'none needed'}. ${assessedCheckpointContract(selected)} Submit that JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping. A shell heredoc is suitable; do not create a task-workspace checkpoint file.` : fresh ? next.nextStep : `Check the actual delivery status first. Record an honest checkpoint with simy delivery checkpoint before stopping; do not claim completion without verification. Pending step: ${next.nextStep}`;
|
|
930
|
-
return { run: next, output: { decision: 'block', reason: `SIMY Delivery Loop (${next.continuations}
|
|
922
|
+
return { run: next, output: { decision: 'block', reason: `SIMY Delivery Loop (${next.continuations} continuations). ${instruction}\nStay within the original request and permissions. If authorization or information is missing, record a blocked checkpoint and stop. Resume this same task; do not start a replacement loop.` } };
|
|
931
923
|
}
|
|
932
924
|
// A process signature protects against PID reuse. Never treat a stale timer alone
|
|
933
925
|
// as a crash; an idle provider may be waiting for its user.
|
|
@@ -1101,6 +1093,18 @@ export function canChangeDeliveryContinuation(run) {
|
|
|
1101
1093
|
return Boolean(run?.managed && ['paused', 'blocked', 'failed', 'budget_exhausted', 'verification_pending', 'recovery_required'].includes(run.state) &&
|
|
1102
1094
|
!['pending', 'pending_received', 'transferred'].includes(run.managed.handoff?.status));
|
|
1103
1095
|
}
|
|
1096
|
+
function reopenLegacyLimitStop(run, now) {
|
|
1097
|
+
if (run?.state !== 'budget_exhausted' ||
|
|
1098
|
+
managedBudgetReason(run, now, { checkProgress: false }) ||
|
|
1099
|
+
/No verified|failure limit/i.test(run.reason || '') ||
|
|
1100
|
+
run.managed?.noProgress >= run.managed?.maxNoProgress) return;
|
|
1101
|
+
if (run.managed) {
|
|
1102
|
+
run.managed.localLimitMigration ??= { at: now, previousLastVerifiedProgressAt: run.managed.lastVerifiedProgressAt };
|
|
1103
|
+
// Time spent stopped at an obsolete cap is not failed execution progress.
|
|
1104
|
+
run.managed.lastVerifiedProgressAt = now;
|
|
1105
|
+
}
|
|
1106
|
+
run.state = 'paused'; run.reason = 'Local limits removed; resume this same task.';
|
|
1107
|
+
}
|
|
1104
1108
|
export async function controlDelivery({ root = rootDirectory(), provider, session, taskId, approvalId, autonomyPolicy, continuationPolicy, limits, questionId, waitId, answer, action, now = Date.now() }) {
|
|
1105
1109
|
for (let retry = 0; ; retry += 1) {
|
|
1106
1110
|
try { return await updateRecord(root, provider, session, record => {
|
|
@@ -1109,6 +1113,7 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
|
|
|
1109
1113
|
if (run.managed && taskId !== run.managed.taskId) throw new Error('Managed task identity changed; refresh its status.');
|
|
1110
1114
|
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.');
|
|
1111
1115
|
if (action !== 'stop' && run.managed?.handoff?.status === 'transferred') throw new Error('Task handoff is locked; continue in the received target session.');
|
|
1116
|
+
reopenLegacyLimitStop(run, now);
|
|
1112
1117
|
if (action === 'extend' || action === 'continuation_policy') {
|
|
1113
1118
|
if (!canChangeDeliveryContinuation(run)) throw new Error('Pause a managed task before changing its continuation bounds.');
|
|
1114
1119
|
if (action === 'continuation_policy') {
|
|
@@ -1297,10 +1302,11 @@ export async function reconcileRecords({ root = rootDirectory(), now = Date.now(
|
|
|
1297
1302
|
// Paused runs have no reconciliation transition: budget enforcement excludes
|
|
1298
1303
|
// them, process-exit checks only apply to active runs, and no notification
|
|
1299
1304
|
// is generated. Resume/control still reread and validate the exact record.
|
|
1300
|
-
if (!record.run || TERMINAL.has(record.run.state) || record.run.state === 'paused') { reconciled.push(record); continue; }
|
|
1305
|
+
if (!record.run || TERMINAL.has(record.run.state) && record.run.state !== 'budget_exhausted' || record.run.state === 'paused') { reconciled.push(record); continue; }
|
|
1301
1306
|
const observed = record.process ? await inspectProcess(record.process.pid) : null;
|
|
1302
1307
|
const next = await updateRecord(root, record.provider, record.session, async current => {
|
|
1303
|
-
|
|
1308
|
+
reopenLegacyLimitStop(current?.run, now);
|
|
1309
|
+
if (!current?.run || TERMINAL.has(current.run.state) || current.run.state === 'paused') return current;
|
|
1304
1310
|
const run = current.run;
|
|
1305
1311
|
if (run.state === 'verification_pending' && run.managed) {
|
|
1306
1312
|
await verifyAutomaticCriteria(current, now);
|
|
@@ -1308,12 +1314,12 @@ export async function reconcileRecords({ root = rootDirectory(), now = Date.now(
|
|
|
1308
1314
|
run.state = 'completed'; run.reason = run.checkpoint.summary; run.updatedAt = now;
|
|
1309
1315
|
}
|
|
1310
1316
|
}
|
|
1311
|
-
const activeTurn = run.managed
|
|
1317
|
+
const activeTurn = run.managed &&
|
|
1312
1318
|
(run.state === 'auto_resuming' || current.runtime?.isRunning === true || run.managed.executionAttempt?.phase === 'dispatching');
|
|
1313
1319
|
const budgetState = deliveryBudgetState(run, now, { checkProgress: !activeTurn });
|
|
1314
1320
|
run.budgetWarnings = budgetState.warnings;
|
|
1315
1321
|
if (budgetState.reason && !TERMINAL.has(run.state) && !['verification_pending', 'paused'].includes(run.state)) {
|
|
1316
|
-
run.state =
|
|
1322
|
+
run.state = budgetState.reason.startsWith('No verified') ? 'paused' : 'budget_exhausted';
|
|
1317
1323
|
run.reason = budgetState.reason; run.updatedAt = now;
|
|
1318
1324
|
}
|
|
1319
1325
|
else if (run.state === 'active' && observed !== null && observed !== current.process?.identity && current.process?.identity === record.process?.identity) {
|
|
@@ -24,8 +24,8 @@ For missing information use `blocker: "missing_information"`. For a failed attem
|
|
|
24
24
|
|
|
25
25
|
For completed work use `{"status":"completed","summary":"Actual result","acceptanceMet":true,"evidence":["Exact checks and observed results"]}` only after checking the user's acceptance criteria. Evidence is a report, not independent certification: preserve failed checks and deployment gaps. The hook must not infer success from optimistic closing text.
|
|
26
26
|
|
|
27
|
-
4. The Stop hook evaluates checkpoints and supplies the next instruction within the original
|
|
28
|
-
5. After the user explicitly resolves the blocker and requests continuation, `simy delivery resume --provider <provider> --session <id>` retains
|
|
27
|
+
4. The Stop hook evaluates checkpoints and supplies the next instruction within the original scope while progress is verified. Missing checkpoints count toward the repeated-failure circuit. A human interrupt stops the loop. Approval/input events block it. Provider errors/exits may require reopening the existing conversation; these hooks cannot restart a dead provider process.
|
|
28
|
+
5. After the user explicitly resolves the blocker and requests continuation, `simy delivery resume --provider <provider> --session <id>` retains usage, failure and progress history; local time, turn and token counters do not stop work. `simy delivery stop` permanently ends this loop. `simy delivery status` shows observed hook activity and loop states; old observations do not prove hooks remain enabled today.
|
|
29
29
|
|
|
30
30
|
If the user selected **Continue this conversation** in SIMY desktop, the exact session already has a managed task. Do not run `delivery start` or ask the user to restate its goal. Follow the Stop hook's distinct `session_understanding` contract first: inspect only the original conversation context, report the original user goal and grounded progress, and submit the JSON checkpoint via `--file -`. Do not execute the next assessed work action during understanding. If context is materially unclear, report one focused question and wait for a matching `session_answer`; generic Resume is insufficient. A model-reported finished goal without an independent check is shown as `reported_done`, not verified `completed`.
|
|
31
31
|
|
|
@@ -41,6 +41,6 @@ File checks prove only the stated file predicate: `file_equals` compares complet
|
|
|
41
41
|
|
|
42
42
|
Cold recovery is **off by default**. Only when the user explicitly authorizes exact-session Codex CLI recovery, add `"autoRecovery":true,"resumeChannel":"codex_exec","resumeModel":"<exact model>","autoRecoverySandbox":"read-only"` (or an explicitly authorized `workspace-write`). An exact Codex UUID and observed process identity are also required; otherwise recovery stays manual. The recovered process returns a structured final checkpoint that SIMY validates and persists. It must not edit SIMY state directly. Claude and ordinary SessionEnd remain manual paths. Token usage is not measured by these hooks.
|
|
43
43
|
|
|
44
|
-
For an explicitly requested autonomous managed task, also enroll `"autonomyPolicy":"strict"|"balanced"|"autonomous"` and an `initialPlan` before work begins. Example initialPlan: `{"diagnosis":"Inspect input before repair","alternatives":[{"id":"inspect","action":{"kind":"read","scope":"repository","summary":"Inspect input.json"},"assessment":{"risk":"low","reversibility":"reversible","externalEffect":"none","blastRadius":"single_file","uncertainty":"low","inObjective":true,"userChoice":false},"expectedVerification":"Observe ready field","rollback":"No changes"}],"selectedOptionId":"inspect","recommendation":{"optionId":"inspect","reason":"Need evidence before editing"}}`. `initialAction` must exactly match the selected option and its scope must be enrolled in `approvalScopes`. For each subsequent action, report `actionResult:{decisionId,status:"observed"|"failed"|"not_run",summary,evidence}` for the current decision and a `plan` with diagnosis, one to five assessed alternatives, selectedOptionId, and recommendation for the **next** action. Never execute the next alternative in the current turn. Use only action kinds `read`, `local_edit`, `local_test`, `external`, `destructive`, `scope_change`, `unknown`; use `local_edit` for writing local files. Missing assessment facts, external/destructive effects, user choice, or unapproved scope wait. A recommendation is advisory, not an authorization. Worker reports are not independent acceptance verification. If an automatic file criterion is missing after claimed completion, SIMY schedules bounded read-only diagnosis; manual criteria still require the user. Task policy can be changed with `simy delivery set_policy --provider ... --session ... --task-id ... --autonomy-policy strict|balanced|autonomous`, which preserves
|
|
44
|
+
For an explicitly requested autonomous managed task, also enroll `"autonomyPolicy":"strict"|"balanced"|"autonomous"` and an `initialPlan` before work begins. Example initialPlan: `{"diagnosis":"Inspect input before repair","alternatives":[{"id":"inspect","action":{"kind":"read","scope":"repository","summary":"Inspect input.json"},"assessment":{"risk":"low","reversibility":"reversible","externalEffect":"none","blastRadius":"single_file","uncertainty":"low","inObjective":true,"userChoice":false},"expectedVerification":"Observe ready field","rollback":"No changes"}],"selectedOptionId":"inspect","recommendation":{"optionId":"inspect","reason":"Need evidence before editing"}}`. `initialAction` must exactly match the selected option and its scope must be enrolled in `approvalScopes`. For each subsequent action, report `actionResult:{decisionId,status:"observed"|"failed"|"not_run",summary,evidence}` for the current decision and a `plan` with diagnosis, one to five assessed alternatives, selectedOptionId, and recommendation for the **next** action. Never execute the next alternative in the current turn. Use only action kinds `read`, `local_edit`, `local_test`, `external`, `destructive`, `scope_change`, `unknown`; use `local_edit` for writing local files. Missing assessment facts, external/destructive effects, user choice, or unapproved scope wait. A recommendation is advisory, not an authorization. Worker reports are not independent acceptance verification. If an automatic file criterion is missing after claimed completion, SIMY schedules bounded read-only diagnosis; manual criteria still require the user. Task policy can be changed with `simy delivery set_policy --provider ... --session ... --task-id ... --autonomy-policy strict|balanced|autonomous`, which preserves usage history, approvals and scopes. Provider permissions remain authoritative.
|
|
45
45
|
|
|
46
46
|
In assessed mode, a blocked authorization checkpoint must also include an assessed `plan` whose selected action exactly matches `approval:{kind,scope,summary}` and a concrete `nextStep`. A missing-information blocker must include a specific `question` for the user. Do not turn a provider approval prompt into an automatic scheduling grant, even for a normally low-risk action. Resume only after the exact user decision; low-risk enrolled actions already allowed by the policy do not need an additional SIMY approval.
|
package/src/console/app.js
CHANGED
|
@@ -991,9 +991,8 @@ function buildRunDetailRows(run, width) {
|
|
|
991
991
|
};
|
|
992
992
|
|
|
993
993
|
const attemptNumber = run.snapshot.attempts.length + (run.child ? 1 : 0);
|
|
994
|
-
const maxAttempts = run.snapshot.charter?.max_attempts;
|
|
995
994
|
add(
|
|
996
|
-
`${executorLabel(run.request.backend)} · Attempt ${attemptNumber}
|
|
995
|
+
`${executorLabel(run.request.backend)} · Attempt ${attemptNumber} · PID ${run.child?.pid ?? "—"}`,
|
|
997
996
|
{ dimColor: true },
|
|
998
997
|
);
|
|
999
998
|
add(`${run.request.repository} · ${run.request.base_branch || run.snapshot.charter?.base_branch || "dev"}`, {
|
|
@@ -22,7 +22,7 @@ export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = p
|
|
|
22
22
|
output('simy delivery continuation_policy --provider PROVIDER --session ID --task-id ID --continuation-policy bounded|progress');
|
|
23
23
|
output('simy delivery extend --provider PROVIDER --session ID --task-id ID --hard-max-minutes N|--hard-max-continuations N|--provider-token-ceiling N');
|
|
24
24
|
output('simy delivery continue --provider codex|claude --session ID [--max-no-progress-minutes 30] [--max-tokens N (Codex only)]\nsimy delivery dependency_ready --provider PROVIDER --session ID --task-id ID --wait-id ID --answer TEXT\nsimy delivery handoff|handoff_cancel --provider codex --session ID --task-id ID --target-session ID');
|
|
25
|
-
output('simy delivery start --provider codex|claude --session ID --objective TEXT --next-step TEXT [--managed-file task.json] [--max-continuations 6] [--max-failures 3] [--max-minutes 30]\nsimy delivery checkpoint --provider codex|claude --session ID --file checkpoint.json|-\nsimy delivery status\nsimy delivery stop|pause|resume --provider codex|claude --session ID [--task-id ID]\nsimy delivery replan --provider PROVIDER --session ID --task-id ID\nsimy delivery approve|reject --provider PROVIDER --session ID --task-id ID --approval-id ID\nsimy delivery verify --provider PROVIDER --session ID --task-id ID --criterion-id ID\nsimy delivery set_policy --provider PROVIDER --session ID --task-id ID --autonomy-policy strict|balanced|autonomous\nEnable and review SIMY hooks in your provider first. Manual managed start requires an acceptance specification. Desktop Continue infers the original conversation goal first. Resume preserves original
|
|
25
|
+
output('simy delivery start --provider codex|claude --session ID --objective TEXT --next-step TEXT [--managed-file task.json] [--max-continuations 6] [--max-failures 3] [--max-minutes 30]\nsimy delivery checkpoint --provider codex|claude --session ID --file checkpoint.json|-\nsimy delivery status\nsimy delivery stop|pause|resume --provider codex|claude --session ID [--task-id ID]\nsimy delivery replan --provider PROVIDER --session ID --task-id ID\nsimy delivery approve|reject --provider PROVIDER --session ID --task-id ID --approval-id ID\nsimy delivery verify --provider PROVIDER --session ID --task-id ID --criterion-id ID\nsimy delivery set_policy --provider PROVIDER --session ID --task-id ID --autonomy-policy strict|balanced|autonomous\nEnable and review SIMY hooks in your provider first. Manual managed start requires an acceptance specification. Desktop Continue infers the original conversation goal first. Resume preserves original task identity, usage history and approvals; local time, turn and token counters do not stop work.');
|
|
26
26
|
return 0;
|
|
27
27
|
}
|
|
28
28
|
const options = {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import {
|
|
3
|
+
import { canReplanDelivery, deliveryBudgetState, managedBudgetReason, reconcileRecords, rootDirectory } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
4
4
|
import { originalSessionTarget } from '../session-supervision/original-session-target.js';
|
|
5
5
|
|
|
6
6
|
export const HOOK_SETUP = Object.freeze({
|
|
@@ -31,12 +31,13 @@ export async function deliveryHookStatus({ root = rootDirectory(), now = Date.no
|
|
|
31
31
|
runs: records.filter(r => r.run).sort((a, b) => b.run.updatedAt - a.run.updatedAt).slice(0, 30).map(r => ({
|
|
32
32
|
provider: r.provider, session: r.session, originalSessionTarget: originalSessionTarget(r.provider, r.session),
|
|
33
33
|
objective: r.run.objective, state: r.run.state, reason: r.run.reason,
|
|
34
|
-
continuations: r.run.continuations, maxContinuations:
|
|
34
|
+
continuations: r.run.continuations, maxContinuations: null, deadline: null,
|
|
35
|
+
localExecutionLimits: false,
|
|
35
36
|
continuationPolicy: r.run.managed?.continuationPolicy || 'bounded',
|
|
36
37
|
startedAt: r.run.startedAt,
|
|
37
38
|
continuationLimits: deliveryBudgetState(r.run, now),
|
|
38
|
-
canChangeContinuation:
|
|
39
|
-
hardDeadline:
|
|
39
|
+
canChangeContinuation: false,
|
|
40
|
+
hardDeadline: null, hardMaxContinuations: null,
|
|
40
41
|
budgetWarnings: deliveryBudgetState(r.run, now).warnings,
|
|
41
42
|
budgetCapability: r.run.managed?.budgetCapability || null,
|
|
42
43
|
outcomeProgress: r.run.managed?.outcomeProgress || null,
|
|
@@ -29,7 +29,9 @@ function watchLease(root, provider, session, inspect, onFailure) {
|
|
|
29
29
|
if (!stopped) inspect(record);
|
|
30
30
|
}).catch(() => { if (!stopped) onFailure(); }).finally(() => { reading = false; });
|
|
31
31
|
}, 500);
|
|
32
|
-
|
|
32
|
+
// An owned turn must stay alive until it settles or the lease cancels it.
|
|
33
|
+
// This watch is cleared in the execution's finally block; unlike the removed
|
|
34
|
+
// budget deadline, it does not impose a time limit on active work.
|
|
33
35
|
return () => { stopped = true; clearInterval(timer); };
|
|
34
36
|
}
|
|
35
37
|
async function assertActiveLease(root, provider, session, leaseId, signal, currentTime = Date.now) {
|
|
@@ -79,7 +81,7 @@ export async function executeCodexResume(record, { signal, root, leaseId, execut
|
|
|
79
81
|
remainingWork: run.managed.remainingWork, acceptanceCriteria: run.managed.acceptanceCriteria, proposedAction: run.managed.proposedAction,
|
|
80
82
|
currentDecision: run.managed.autonomyPolicy ? run.managed.decision : undefined, diagnosis: run.managed.plan?.diagnosis,
|
|
81
83
|
budgets: { ...deliveryBudgetState(run), continuations: run.continuations, failures: run.failures, maxFailures: run.maxFailures,
|
|
82
|
-
|
|
84
|
+
localExecutionLimits: false } };
|
|
83
85
|
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)} Do not write SIMY state directly.` : `Continue the existing SIMY managed task in this exact session. Context: ${JSON.stringify(context)}\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.`;
|
|
84
86
|
const sandbox = run.managed.autonomyPolicy && run.managed.decision?.action.kind === 'read' ? 'read-only' : run.managed.autoRecoverySandbox;
|
|
85
87
|
const args = ['exec', '--json', '--skip-git-repo-check', '-s', sandbox, '-m', run.managed.resumeModel, 'resume', record.session, prompt];
|
|
@@ -87,7 +89,7 @@ export async function executeCodexResume(record, { signal, root, leaseId, execut
|
|
|
87
89
|
if (currentTime() >= managedDispatchDeadline(run)) throw new Error('Managed recovery deadline expired before provider dispatch.');
|
|
88
90
|
await beforeDispatch();
|
|
89
91
|
if (signal?.aborted) throw new Error('Managed recovery cancelled before provider dispatch.');
|
|
90
|
-
|
|
92
|
+
|
|
91
93
|
return new Promise((resolve, reject) => {
|
|
92
94
|
const child = spawn(binary, args, { cwd: record.cwd, stdio: ['ignore', 'pipe', 'pipe'], signal,
|
|
93
95
|
env: { ...process.env, SIMY_HOME: dirname(root) } });
|
|
@@ -196,7 +198,7 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
196
198
|
if (!live?.managed?.autoRecovery || live.state !== 'recovery_required' || !allowedCause || live.managed.pendingApproval || live.managed.autonomyPolicy && (!live.managed.decision || live.managed.decision.outcome === 'wait' && !approved) || appChannel && live.managed.nextAttemptAt > currentTime()) return current;
|
|
197
199
|
const budgetReason = managedBudgetReason(live, currentTime());
|
|
198
200
|
if (budgetReason) {
|
|
199
|
-
live.state = '
|
|
201
|
+
live.state = budgetReason.startsWith('No verified') ? 'paused' : 'failed'; live.reason = budgetReason; live.updatedAt = currentTime(); notifyState(live, live.updatedAt); return current;
|
|
200
202
|
}
|
|
201
203
|
live.state = 'auto_resuming'; live.managed.resumeLease = { id: leaseId, claimedAt: currentTime(), checkpointRevision: live.revision, decisionId: live.managed.decision?.id || null,
|
|
202
204
|
owner: { pid: process.pid, identity: ownerIdentity } };
|
|
@@ -216,13 +218,11 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
216
218
|
let resource;
|
|
217
219
|
let lastActivityAt = currentTime();
|
|
218
220
|
let usage = null;
|
|
219
|
-
let tokenBudgetExceeded = false;
|
|
220
221
|
let dispatched = false, waitingForTool = false;
|
|
221
222
|
const controller = new AbortController();
|
|
222
223
|
const abort = () => controller.abort();
|
|
223
224
|
if (signal?.aborted) controller.abort();
|
|
224
225
|
else signal?.addEventListener('abort', abort, { once: true });
|
|
225
|
-
const timeout = setTimeout(abort, Math.max(1, managedDispatchDeadline(claimed.run) - currentTime()));
|
|
226
226
|
const stopWatch = watchLease(root, record.provider, record.session, current => {
|
|
227
227
|
if (current?.run?.state !== 'auto_resuming' || current.run.managed?.resumeLease?.id !== leaseId ||
|
|
228
228
|
managedBudgetReason(current.run, currentTime(), { checkProgress: false }) || !waitingForTool && currentTime() - lastActivityAt >= 5 * 60_000) controller.abort();
|
|
@@ -264,10 +264,7 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
264
264
|
onActivity: observation => { lastActivityAt = currentTime(); waitingForTool = observation?.waitingForTool === true; },
|
|
265
265
|
onUsage: observed => {
|
|
266
266
|
usage = observed;
|
|
267
|
-
|
|
268
|
-
if (tokenCeiling && (claimed.run.managed.tokensUsed || 0) + observed.tokens >= tokenCeiling) {
|
|
269
|
-
tokenBudgetExceeded = true; controller.abort();
|
|
270
|
-
}
|
|
267
|
+
|
|
271
268
|
},
|
|
272
269
|
beforeDispatch: async providerSandbox => {
|
|
273
270
|
await assertActiveLease(root, record.provider, record.session, leaseId, controller.signal, currentTime);
|
|
@@ -299,7 +296,7 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
299
296
|
? { code: -3, busy: true, accessDenied: true }
|
|
300
297
|
: { code: controller.signal.aborted ? -2 : -1, cancelled: controller.signal.aborted,
|
|
301
298
|
errorCode: error?.code || 'provider_error', restorationUnverified: error?.restorationUnverified === true }; }
|
|
302
|
-
finally {
|
|
299
|
+
finally { stopWatch(); signal?.removeEventListener('abort', abort); }
|
|
303
300
|
if (controller.signal.aborted) result = { ...result, code: -2, cancelled: true };
|
|
304
301
|
if (deliveryBudgetState(claimed.run).tokenCeiling && !usage && !result.busy && dispatched) {
|
|
305
302
|
result = { ...result, usageUnavailable: true };
|
|
@@ -330,6 +327,7 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
330
327
|
const live = current?.run;
|
|
331
328
|
if (live?.managed?.resumeLease?.id !== leaseId) return current;
|
|
332
329
|
delete live.managed.resumeLease;
|
|
330
|
+
delete live.managed.usageBlocked;
|
|
333
331
|
if (usage) {
|
|
334
332
|
live.managed.tokensUsed = (live.managed.tokensUsed || 0) + usage.tokens;
|
|
335
333
|
live.managed.usageAvailable = true;
|
|
@@ -360,14 +358,7 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
360
358
|
live.updatedAt = Date.now(); notifyState(live, live.updatedAt); return current;
|
|
361
359
|
}
|
|
362
360
|
live.budgetWarnings = deliveryBudgetState(live).warnings;
|
|
363
|
-
|
|
364
|
-
if (tokenBudgetExceeded || tokenCeiling && live.managed.tokensUsed >= tokenCeiling) {
|
|
365
|
-
live.state = 'budget_exhausted'; live.managed.autoRecovery = false;
|
|
366
|
-
live.reason = 'Observed provider token budget exhausted; inspect any interrupted action in the original conversation.';
|
|
367
|
-
} else if (live.managed.usageBlocked) {
|
|
368
|
-
live.state = 'recovery_required'; live.managed.autoRecovery = false;
|
|
369
|
-
live.reason = 'Provider usage was unavailable; token-budgeted continuation cannot safely start another turn.';
|
|
370
|
-
} else if (result.busy) {
|
|
361
|
+
if (result.busy) {
|
|
371
362
|
if (result.accessDenied && dispatched) {
|
|
372
363
|
// The final guard rejected before the provider call. Keep its one-shot approval unused.
|
|
373
364
|
if (live.managed.approvedAction?.id === live.managed.decision?.id) delete live.managed.approvedAction.dispatchedAt;
|
package/src/local-task.js
CHANGED
|
@@ -568,7 +568,6 @@ export async function startLocalTask(
|
|
|
568
568
|
|
|
569
569
|
return new Promise((resolve, reject) => {
|
|
570
570
|
let settled = false;
|
|
571
|
-
let timeout = null;
|
|
572
571
|
const rawStdout = [];
|
|
573
572
|
const tokenRecords = [];
|
|
574
573
|
let child;
|
|
@@ -617,21 +616,12 @@ export async function startLocalTask(
|
|
|
617
616
|
continuation_policy: task.continuation_policy || "bounded",
|
|
618
617
|
provider_token_ceiling: localTaskTokenCeiling(task),
|
|
619
618
|
});
|
|
620
|
-
if (task.
|
|
619
|
+
if (task.provider_tokens_used >= task.provider_token_budget && !task.token_warning_emitted) {
|
|
621
620
|
task.token_warning_emitted = true;
|
|
622
|
-
task.warnings.push("Provider token notification threshold reached. Work continues within the approved task
|
|
621
|
+
task.warnings.push("Provider token notification threshold reached. Work continues within the approved task; local usage does not stop execution.");
|
|
623
622
|
emitTaskEvent(task, "budget_warning", { provider_tokens_used: task.provider_tokens_used, provider_token_budget: task.provider_token_budget, provider_token_ceiling: localTaskTokenCeiling(task) });
|
|
624
623
|
}
|
|
625
|
-
|
|
626
|
-
localTaskTokensExhausted(task) &&
|
|
627
|
-
!task.stop_requested
|
|
628
|
-
) {
|
|
629
|
-
task.reason_code = "provider_token_budget_exhausted";
|
|
630
|
-
task.error = `Provider token ${task.continuation_policy === "progress" ? "hard ceiling" : "budget"} exceeded (${task.provider_tokens_used} / ${localTaskTokenCeiling(task)}).`;
|
|
631
|
-
task.stop_requested = true;
|
|
632
|
-
task.control_state = "stopping";
|
|
633
|
-
signalExecutorProcess(child, "SIGINT");
|
|
634
|
-
}
|
|
624
|
+
|
|
635
625
|
};
|
|
636
626
|
const stdoutDecoder = createProviderStreamDecoder({
|
|
637
627
|
backend: task.backend,
|
|
@@ -656,28 +646,12 @@ export async function startLocalTask(
|
|
|
656
646
|
});
|
|
657
647
|
child.stderr?.on("data", (chunk) => stderrDecoder.push(chunk.toString("utf8")));
|
|
658
648
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
task.reason_code = "provider_timeout";
|
|
662
|
-
task.error = `Local Task exceeded its ${task.timeout_ms}ms timeout.`;
|
|
663
|
-
task.stop_requested = true;
|
|
664
|
-
task.control_state = "stopping";
|
|
665
|
-
emitTaskEvent(task, "status", {
|
|
666
|
-
state: "stopping",
|
|
667
|
-
reason_code: task.reason_code,
|
|
668
|
-
message: "The Local Task timed out and is stopping.",
|
|
669
|
-
});
|
|
670
|
-
signalExecutorProcess(child, "SIGINT");
|
|
671
|
-
setTimeout(() => {
|
|
672
|
-
if (!settled && task.child === child) signalExecutorProcess(child, "SIGTERM");
|
|
673
|
-
}, 1_500).unref?.();
|
|
674
|
-
}, task.timeout_ms);
|
|
675
|
-
timeout.unref?.();
|
|
649
|
+
// timeout_ms is retained as legacy request metadata. Elapsed local execution
|
|
650
|
+
// no longer cancels a task; explicit user cancellation remains authoritative.
|
|
676
651
|
|
|
677
652
|
const finish = async ({ exitCode = null, error = null } = {}) => {
|
|
678
653
|
if (settled) return;
|
|
679
654
|
settled = true;
|
|
680
|
-
if (timeout) clearTimeout(timeout);
|
|
681
655
|
stdoutDecoder.flush();
|
|
682
656
|
stderrDecoder.flush();
|
|
683
657
|
task.child = null;
|
|
@@ -740,8 +714,7 @@ export async function startLocalTask(
|
|
|
740
714
|
const failed =
|
|
741
715
|
Boolean(error) ||
|
|
742
716
|
exitCode !== 0 ||
|
|
743
|
-
Boolean(task.error)
|
|
744
|
-
localTaskTokensExhausted(task);
|
|
717
|
+
Boolean(task.error);
|
|
745
718
|
task.status = manuallyStopped ? "stopped" : failed ? "failed" : "succeeded";
|
|
746
719
|
task.control_state = task.status === "stopped" ? "stopped" : "complete";
|
|
747
720
|
task.error ||= error || (exitCode !== 0 ? `Provider exited with code ${exitCode}.` : null);
|
|
@@ -1299,13 +1272,8 @@ function normalizeExpectedOutputs(value) {
|
|
|
1299
1272
|
}));
|
|
1300
1273
|
}
|
|
1301
1274
|
|
|
1302
|
-
function localTaskTokenCeiling(
|
|
1303
|
-
return
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
function localTaskTokensExhausted(task) {
|
|
1307
|
-
const ceiling = localTaskTokenCeiling(task);
|
|
1308
|
-
return task.continuation_policy === "progress" ? task.provider_tokens_used >= ceiling : task.provider_tokens_used > ceiling;
|
|
1275
|
+
function localTaskTokenCeiling() {
|
|
1276
|
+
return null;
|
|
1309
1277
|
}
|
|
1310
1278
|
|
|
1311
1279
|
function normalizeTokenBudget(value) {
|
|
@@ -67,7 +67,7 @@ export function extendProgressCeilings(snapshot, changes = {}) {
|
|
|
67
67
|
const provided = Object.entries(changes).filter(([, value]) => value !== undefined);
|
|
68
68
|
if (!provided.length) throw new Error("Explicitly increase at least one hard ceiling.");
|
|
69
69
|
for (const [field, value] of provided) {
|
|
70
|
-
const previous = field === "provider_token_ceiling" ?
|
|
70
|
+
const previous = field === "provider_token_ceiling" ? snapshot.charter.provider_token_ceiling : snapshot.charter.attempt_ceiling;
|
|
71
71
|
const used = field === "provider_token_ceiling" ? current.tokens_used : (snapshot.attempts || []).length;
|
|
72
72
|
if (!Number.isSafeInteger(value) || value <= previous || value <= used) {
|
|
73
73
|
throw new Error("Each requested hard ceiling must be an integer higher than its current limit and recorded usage.");
|
|
@@ -97,11 +97,12 @@ export function budgetState(snapshot, pendingAttempt = null) {
|
|
|
97
97
|
const attemptsUsed = attempts.length;
|
|
98
98
|
const retriesUsed = Math.max(attemptsUsed - 1, 0);
|
|
99
99
|
const policy = progressLimits(snapshot?.charter);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const
|
|
100
|
+
// Persisted thresholds remain readable for old contracts and usage reports.
|
|
101
|
+
// Local work stops on failed progress/approval, never on consumption counters.
|
|
102
|
+
const hardTokenCeiling = null;
|
|
103
|
+
const hardAttemptCeiling = null;
|
|
104
|
+
const tokenExhausted = false;
|
|
105
|
+
const retryExhausted = false;
|
|
105
106
|
return {
|
|
106
107
|
schema_version: 1,
|
|
107
108
|
token_budget: tokenBudget,
|
|
@@ -119,11 +120,12 @@ export function budgetState(snapshot, pendingAttempt = null) {
|
|
|
119
120
|
? "retry_budget_exhausted"
|
|
120
121
|
: null,
|
|
121
122
|
continuation_policy: policy.continuation_policy,
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
local_limits_removed: true,
|
|
124
|
+
token_warning: tokensUsed >= tokenBudget,
|
|
125
|
+
retry_warning: attemptsUsed >= retryBudget + 1,
|
|
124
126
|
hard_token_ceiling: hardTokenCeiling,
|
|
125
127
|
hard_attempt_ceiling: hardAttemptCeiling,
|
|
126
|
-
hard_tokens_remaining:
|
|
128
|
+
hard_tokens_remaining: null,
|
|
127
129
|
};
|
|
128
130
|
}
|
|
129
131
|
|
|
@@ -144,19 +146,12 @@ export function refreshBudgetState(snapshot, pendingAttempt = null) {
|
|
|
144
146
|
return state;
|
|
145
147
|
}
|
|
146
148
|
|
|
147
|
-
export function canStartProvider(snapshot
|
|
148
|
-
|
|
149
|
-
if (state.token_exhausted) {
|
|
150
|
-
return { allowed: false, reason: "token_budget_exhausted", budget: state };
|
|
151
|
-
}
|
|
152
|
-
if (phase === "executor" && attemptNumber > state.hard_attempt_ceiling) {
|
|
153
|
-
return { allowed: false, reason: "retry_budget_exhausted", budget: state };
|
|
154
|
-
}
|
|
155
|
-
return { allowed: true, reason: null, budget: state };
|
|
149
|
+
export function canStartProvider(snapshot) {
|
|
150
|
+
return { allowed: true, reason: null, budget: refreshBudgetState(snapshot) };
|
|
156
151
|
}
|
|
157
152
|
|
|
158
|
-
export function codingAttemptCeiling(
|
|
159
|
-
return
|
|
153
|
+
export function codingAttemptCeiling() {
|
|
154
|
+
return Infinity;
|
|
160
155
|
}
|
|
161
156
|
|
|
162
157
|
export function tokenUsageTotal(value) {
|
|
@@ -10,10 +10,10 @@ export function buildChecklistContinuationInstruction({ auditor = false } = {})
|
|
|
10
10
|
`The table is a progress checkpoint, not the deliverable or a stopping condition. Read: ${ACCEPTANCE_EVIDENCE_PATH}`,
|
|
11
11
|
"Maintain the complete 依頼チェックリスト with columns #, ご依頼, 現状, 判定 in the evidence record after chartering, each evaluated attempt, and in the final receipt. User-facing reporting follows workflow_reporting_contract; show the full table only when requested or when it is the most useful requested answer, never as mandatory commentary. Preserve stable criterion IDs, all rows and both applicable evidence layers. ✅ requires current valid passing evidence for every applicable layer; ❌ means an observed failure; 🟡 means pending, missing, stale, blocked or unverifiable evidence. Retain counts and highest proven state in that record.",
|
|
12
12
|
"After observing any non-green row, classify its cause and immediately take the next safe authorized action: implement missing behavior, repair a failure, exercise an untested path, collect missing evidence, or rerun stale verification. Re-observe the same criterion against the current candidate, run the validator, update the table, and repeat until every applicable requested outcome and gate for the authorized endpoint is verified. A next-action list without performing feasible actions is not completion. Never require another continue request merely because a table was shown.",
|
|
13
|
-
"For CI or other in-progress dependencies, use the available bounded wait/recheck or approved monitoring mechanism and maintain progress communication. Unchanged external state is not itself a blocker. Do not start duplicate jobs or busy-poll. Recheck current revision/results, repair authorized failures, and keep waiting only within the
|
|
14
|
-
"If a row genuinely requires new authority, credentials, a user/product decision, an unavailable dependency or
|
|
13
|
+
"For CI or other in-progress dependencies, use the available bounded wait/recheck or approved monitoring mechanism and maintain progress communication. Unchanged external state is not itself a blocker. Do not start duplicate jobs or busy-poll. Recheck current revision/results, repair authorized failures, and keep waiting only within the authorized scope and external service availability. If waiting cannot continue, report the concrete limit and explicit resumption condition; do not silently abandon it or promise an unscheduled follow-up.",
|
|
14
|
+
"If a row genuinely requires new authority, credentials, a user/product decision, an unavailable dependency or a repeated-failure or no-progress safety circuit, retain it as non-green and record affected IDs, evidence of the blocker, safe alternatives attempted, exact missing input, owner and resumption trigger. Continue independent in-scope work. Stop with blocked, best_available, failed or cancelled as appropriate, explicitly unfinished, not verified_complete. Never bypass a manual stop, approval, or expand authority to force green.",
|
|
15
15
|
"Do not end successfully while a safe authorized repair, verification or bounded wait remains. All-green applies to the frozen authorized endpoint: PR-only work does not authorize merge/deploy, but a PR URL, an improvement PR, an issue, a narrower test or a future plan does not satisfy an unmet outcome. Never drop/merge away rows, weaken criteria, relabel applicable checks as not_applicable, recolor unknowns, or copy new digests onto old evidence to force green. Do not change the requested endpoint to match the work already completed.",
|
|
16
|
-
"Before the final response, independently reconcile the original request with all criterion IDs and current evidence, check remaining endpoint gates, and preserve the full table and explicit disposition in the evidence record. Distinguish a partial artifact from verified completion and disclose material remaining outcomes in the requested response format; keep yellow/red counts in the record. A pr_ready_for_review or blocked controller state is not proof of overall completion. These instructions strengthen continuation and reporting; they do not add a background scheduler or remove
|
|
16
|
+
"Before the final response, independently reconcile the original request with all criterion IDs and current evidence, check remaining endpoint gates, and preserve the full table and explicit disposition in the evidence record. Distinguish a partial artifact from verified completion and disclose material remaining outcomes in the requested response format; keep yellow/red counts in the record. A pr_ready_for_review or blocked controller state is not proof of overall completion. These instructions strengthen continuation and reporting; they do not add a background scheduler or remove approval, manual-stop and progress protections.",
|
|
17
17
|
auditor
|
|
18
18
|
? "Remain read-only. Check that the executor performed feasible follow-up actions after non-green tables. Report premature stopping, omitted rows, unsupported green claims, or a blocker without evidence as findings, alongside exact criterion IDs and the necessary next action. Do not approve from a table's colors or the executor's completion wording."
|
|
19
19
|
: "Execute the continuation within this attempt instead of merely returning a yellow table or delegating feasible remaining work to the requester. Return control honestly at a genuine boundary; never fabricate completion evidence.",
|
|
@@ -119,8 +119,9 @@ Before material action, record:
|
|
|
119
119
|
- exclusions and authority boundaries, especially commit, push, Draft PR
|
|
120
120
|
creation or update, Ready transitions, merge, deployment, production writes,
|
|
121
121
|
paid APIs, external messages, and fixture cleanup;
|
|
122
|
-
-
|
|
123
|
-
|
|
122
|
+
- recorded usage and any external service limits. Local execution has no token,
|
|
123
|
+
elapsed-time or attempt ceiling, including restored tasks. Continue while
|
|
124
|
+
making verified progress; respect approvals, manual stops and failure circuits.
|
|
124
125
|
|
|
125
126
|
Do not silently weaken explicit conditions. Add discoveries as a charter patch.
|
|
126
127
|
A scope or success-condition change needs user direction when it materially
|
|
@@ -194,7 +195,7 @@ JSON authority or evidence. Finish the visible table with counts for green,
|
|
|
194
195
|
yellow, and red rows plus the highest proven state. In a terminal
|
|
195
196
|
receipt, add the controller's disposition immediately below it.
|
|
196
197
|
Treat making every applicable row green as the repair target within the frozen
|
|
197
|
-
scope and
|
|
198
|
+
scope and permissions, but never recolor, omit, merge away, or relax a row to
|
|
198
199
|
make the presentation green.
|
|
199
200
|
|
|
200
201
|
### Continue after the table; do not stop at yellow
|
|
@@ -212,19 +213,19 @@ not completion; do not wait for the requester to say “continue” after each t
|
|
|
212
213
|
|
|
213
214
|
For in-progress CI or dependencies, use available bounded wait/recheck or an
|
|
214
215
|
approved monitor, with progress updates and no duplicate jobs or busy-polling.
|
|
215
|
-
Unchanged external state alone is not a blocker.
|
|
216
|
-
|
|
216
|
+
Unchanged external state alone is not a blocker. Local token, elapsed-time and attempt counters do not stop work. Respect
|
|
217
|
+
external service availability and record any actual blocker and resumption condition instead of promising
|
|
217
218
|
an unscheduled follow-up. This workflow does not itself install a scheduler.
|
|
218
219
|
|
|
219
|
-
When genuinely blocked by authority, access, a decision, dependency or
|
|
220
|
-
|
|
220
|
+
When genuinely blocked by authority, access, a decision, dependency or triggered no-progress/repeated-failure
|
|
221
|
+
circuit, preserve each affected ID, observed blocker, attempted safe alternatives,
|
|
221
222
|
exact missing input, owner and resumption trigger. Continue independent authorized
|
|
222
223
|
work. Any terminal non-green table is explicitly unfinished with a justified
|
|
223
224
|
blocked/best_available/failed/cancelled disposition, never verified_complete.
|
|
224
225
|
Do not use best_available merely because a table, PR or plan now exists. A new
|
|
225
226
|
remediation PR transfers ownership, not acceptance; the original unmet row stays
|
|
226
227
|
non-green. All-green targets only the authorized endpoint, not unrequested merge
|
|
227
|
-
or deployment. Never weaken criteria
|
|
228
|
+
or deployment. Never weaken acceptance criteria to force green. Local token, time and attempt counters do not stop work; continue while verified progress improves.
|
|
228
229
|
|
|
229
230
|
Before the final response, reconcile original requirements, all IDs, current
|
|
230
231
|
evidence and endpoint gates; retain the full table, counts, highest proven state
|
|
@@ -454,7 +455,7 @@ For a Codex Desktop task whose requested outcome must be checked after the
|
|
|
454
455
|
conversation ends, register a Codex app heartbeat against that task before
|
|
455
456
|
claiming it is monitored. Preserve the original outcome, acceptance evidence,
|
|
456
457
|
the exact task or operation identity, the requested completion boundary, and
|
|
457
|
-
the response preferences, cadence,
|
|
458
|
+
the response preferences, cadence, progress protections and external service limits, and stop or
|
|
458
459
|
ask-user conditions in the heartbeat prompt. Verify that the automation
|
|
459
460
|
is active and bound to the intended task. On each run, use the Codex app's
|
|
460
461
|
supported task status and wait/read operations to distinguish a queued creation
|
|
@@ -464,7 +465,7 @@ operation before any retry; do not create a replacement while its fate is
|
|
|
464
465
|
unknown. Report an actionable blocker if identity, access, or evidence cannot
|
|
465
466
|
be resolved. Stay quiet on unchanged non-actionable state, and stop the
|
|
466
467
|
heartbeat when the accepted outcome is proved or monitoring is explicitly
|
|
467
|
-
cancelled. On authoritative failure,
|
|
468
|
+
cancelled. On authoritative failure, external service limits or repeated failures, or an unresolved
|
|
468
469
|
terminal identity/access blocker, report the evidence and pause or stop the
|
|
469
470
|
heartbeat pending a human decision. This is a separately registered,
|
|
470
471
|
task-scoped Codex app monitor;
|
|
@@ -495,7 +496,7 @@ For each attempt:
|
|
|
495
496
|
6. **Reduce:** append a typed receipt and atomically advance the snapshot.
|
|
496
497
|
|
|
497
498
|
An LLM may propose hypotheses, repairs, or verdicts, but it must not directly
|
|
498
|
-
override state transitions,
|
|
499
|
+
override state transitions, progress protections, authorization, or evidence gates.
|
|
499
500
|
Only the controller promotes a candidate or terminalizes the run.
|
|
500
501
|
|
|
501
502
|
Read [state and receipts](references/state-and-receipts.md) when the task has
|
|
@@ -712,13 +713,13 @@ action planning belongs to a separate workflow.
|
|
|
712
713
|
Use exactly one disposition:
|
|
713
714
|
|
|
714
715
|
- `verified_complete`: every applicable gate passed with evidence;
|
|
715
|
-
- `best_available`: the
|
|
716
|
+
- `best_available`: the loop produced a safe useful result but named
|
|
716
717
|
criteria remain unmet;
|
|
717
718
|
- `blocked`: missing authority, input, dependency, or safety precondition
|
|
718
719
|
prevents progress;
|
|
719
|
-
- `failed`: no safe useful candidate
|
|
720
|
+
- `failed`: no safe useful candidate can make further verified progress;
|
|
720
721
|
- `cancelled`: the user stopped or replaced the task.
|
|
721
722
|
|
|
722
|
-
Stop after
|
|
723
|
+
Stop after two consecutive non-improving candidates or an
|
|
723
724
|
A-B-A failure pattern without a materially different safe repair. Deliver the
|
|
724
725
|
best verified candidate, not merely the latest attempt.
|
|
@@ -80,16 +80,16 @@ After every evaluated action (test, repair, review or changed inputs):
|
|
|
80
80
|
|
|
81
81
|
Yellow and red tables are progress, not successful delivery. For in-progress
|
|
82
82
|
dependencies use available bounded waits/rechecks or approved monitoring without
|
|
83
|
-
busy-polling or duplicate jobs. Preserve time/attempt/
|
|
83
|
+
busy-polling or duplicate jobs. Preserve authorization and progress checks; local time/attempt/token counters are informational and keep the
|
|
84
84
|
requester informed. If progress genuinely requires new authority, access, a
|
|
85
|
-
decision, dependency availability or
|
|
85
|
+
decision, dependency availability or a triggered no-progress/failure circuit, retain affected rows and record
|
|
86
86
|
observed blocker, safe alternatives tried, exact missing input, owner and resume
|
|
87
87
|
trigger. Continue independent work; end only with an explicitly unfinished
|
|
88
88
|
disposition when no further safe in-scope action remains or a safety/user stop
|
|
89
89
|
applies. Do not require another “continue” for already authorized feasible work.
|
|
90
90
|
An issue or remediation PR does not satisfy the row it tracks. Do not weaken
|
|
91
|
-
criteria, hide rows, invent evidence
|
|
92
|
-
force green. Reconcile the final full table against the original request and all
|
|
91
|
+
criteria, hide rows, invent evidence or change the endpoint to
|
|
92
|
+
force green. Local execution counters do not impose a completion limit. Reconcile the final full table against the original request and all
|
|
93
93
|
current endpoint gates before reporting verified_complete.
|
|
94
94
|
|
|
95
95
|
All rows, including pending/failed ones, stay visible. A green matrix cannot advance
|
|
@@ -101,7 +101,7 @@ Codex may orchestrate the full sequence in one task:
|
|
|
101
101
|
queue/worker activity, and artifact hashes.
|
|
102
102
|
7. Evaluate the evidence against the frozen charter.
|
|
103
103
|
8. On failure, classify the boundary, add a pre-fix regression, patch the
|
|
104
|
-
smallest surface, rebuild changed images, and repeat within
|
|
104
|
+
smallest surface, rebuild changed images, and repeat within the authorized scope while verification improves.
|
|
105
105
|
|
|
106
106
|
The same Codex task may perform actor and verifier phases, but the verifier must
|
|
107
107
|
use recorded observable evidence rather than accepting the actor's completion
|
|
@@ -14,7 +14,7 @@ registration. A scheduler firing is an observation, not delivery progress.
|
|
|
14
14
|
| Pending creation remains unresolved after the provider's own timeout, or two scheduled observations when no timeout is available | The original operation still has no authoritative resource mapping. | Report unresolved identity and the owner; keep the original operation for recovery. Do not create another task on an absent list entry. |
|
|
15
15
|
| A previously documented external wait changes or reaches its deadline | Recheck the exact dependency and its owner/resumption condition. | Resume only when the dependency is cleared and the original authority permits it; otherwise report a material blocker once. Unchanged waiting is quiet. |
|
|
16
16
|
| Task reaches a terminal turn or a claimed result | Check the result and original acceptance evidence independently. | Continue authorized unmet work, report a concrete blocker, or stop monitoring only after the accepted outcome is proven. |
|
|
17
|
-
| Monitor or authority limit reached | Check scheduler status, task identity,
|
|
17
|
+
| Monitor or authority limit reached | Check scheduler status, task identity, no-progress/failure state, observation allowance, and any stop request. Local task usage counters do not impose execution ceilings. | Pause or stop and report what remains. The monitor cannot detect its own missed scheduler runs without another external observer. |
|
|
18
18
|
|
|
19
19
|
For an hourly heartbeat, the first confirmed idle observation is actionable;
|
|
20
20
|
the active/no-progress comparison needs two consecutive hourly snapshots.
|
|
@@ -125,7 +125,7 @@ execution remains subject to existing runner/host ownership controls.
|
|
|
125
125
|
PR recheck clears prior completion flags and reruns the floor. Missing evidence
|
|
126
126
|
never promotes an old successful audit. The old history is retained; do not rewrite
|
|
127
127
|
it as verified under the new floor. Fix the named criterion, collect fresh evidence
|
|
128
|
-
and resume
|
|
128
|
+
and resume with the same task identity and progress checks. Reconcile ambiguous external side effects
|
|
129
129
|
before retrying. The minimum pass is a prerequisite for existing CI/review/authority
|
|
130
130
|
gates, not merge, deployment or a complete production delivery.
|
|
131
131
|
|
|
@@ -39,14 +39,10 @@ export function buildCodingInstruction(
|
|
|
39
39
|
`Risk level: ${charter.risk.level}`,
|
|
40
40
|
`Design summary: ${charter.design_review.summary || "not required"}`,
|
|
41
41
|
`Design approved by: ${charter.design_review.approved_by || "not required"}`,
|
|
42
|
-
`Attempt: ${attemptNumber}
|
|
43
|
-
`
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"Continuation policy: verified acceptance progress. Token/retry thresholds notify; they do not relax acceptance or authorize new work.",
|
|
47
|
-
`Hard Provider token ceiling: ${budget?.hard_token_ceiling}; remaining: ${budget?.hard_tokens_remaining}. Hard attempt ceiling: ${budget?.hard_attempt_ceiling}.`,
|
|
48
|
-
"Report human/external blockers as soon as observed; continue independent work within the approved objective. Changed metadata alone is not progress.",
|
|
49
|
-
] : []),
|
|
42
|
+
`Attempt: ${attemptNumber}`,
|
|
43
|
+
`Provider tokens recorded: ${budget?.tokens_used ?? 0}`,
|
|
44
|
+
"Local execution has no token, elapsed-time or attempt cap, including restored tasks. Continue while verified acceptance improves; stop for unresolved approval, missing input, repeated identical failure or absent progress.",
|
|
45
|
+
"Report human/external blockers as soon as observed; continue independent work within the approved objective. Changed metadata alone is not progress.",
|
|
50
46
|
`Local evidence root: ${charter.ui_evidence_root || "not required"}`,
|
|
51
47
|
].join("\n"),
|
|
52
48
|
),
|
package/src/orchestrator/loop.js
CHANGED
|
@@ -77,8 +77,7 @@ async function runCodingLoopOwned({
|
|
|
77
77
|
|
|
78
78
|
let previousFindings = resume ? snapshot.final_audit?.findings || [] : [];
|
|
79
79
|
const firstAttempt = resume ? snapshot.attempts.length + 1 : 1;
|
|
80
|
-
const
|
|
81
|
-
const lastAttempt = progressPolicy ? codingAttemptCeiling(snapshot) : resume ? firstAttempt : snapshot.charter.max_attempts;
|
|
80
|
+
const lastAttempt = codingAttemptCeiling(snapshot);
|
|
82
81
|
for (
|
|
83
82
|
let attemptNumber = firstAttempt;
|
|
84
83
|
attemptNumber <= lastAttempt;
|
|
@@ -133,8 +133,8 @@ export function assessOutcomeProgress(charter, attempt, previousAttempt = null,
|
|
|
133
133
|
previous: previousAttempt?.outcome_progress || null,
|
|
134
134
|
maxNoProgressAttempts: options.maxNoProgressAttempts ?? 3,
|
|
135
135
|
});
|
|
136
|
-
//
|
|
137
|
-
//
|
|
136
|
+
// Legacy runs retain their stricter independently assessed two-candidate
|
|
137
|
+
// circuit, plus the shared no-progress circuit when verification is missing.
|
|
138
138
|
if (options.policy !== "progress") {
|
|
139
139
|
const criteria = acceptanceCriterionRows(charter);
|
|
140
140
|
const rows = Array.isArray(assessment?.criterion_results) ? assessment.criterion_results : [];
|
|
@@ -143,7 +143,7 @@ export function assessOutcomeProgress(charter, attempt, previousAttempt = null,
|
|
|
143
143
|
rows.filter((row) => row.criterion_id === criterion.id).length === 1);
|
|
144
144
|
const previous = previousAttempt?.outcome_progress;
|
|
145
145
|
result.status = completeAssessment ? "assessed" : "insufficient_evidence";
|
|
146
|
-
result.stalled = Boolean(completeAssessment && previous?.status === "assessed" &&
|
|
146
|
+
result.stalled = (!completeAssessment && result.stalled) || Boolean(completeAssessment && previous?.status === "assessed" &&
|
|
147
147
|
previous.objective_digest === result.objective_digest && result.unproven_ids.length > 0 &&
|
|
148
148
|
result.assessed_ids.every((id) => previous.assessed_ids.includes(id)) &&
|
|
149
149
|
result.unproven_ids.every((id) => previous.unproven_ids.includes(id)));
|
|
@@ -13,7 +13,7 @@ export function buildPrClosureInstruction({ auditor = false } = {}) {
|
|
|
13
13
|
`Before a closure decision, read the packaged Web/CLI procedure: ${PR_CLOSURE_GUIDE_PATH}`,
|
|
14
14
|
"Freeze the original outcome criteria and exact old head/base, current target base, successor heads, integrated candidate and deployed identity. Preserve intended, observed and unknown behavior separately.",
|
|
15
15
|
"Persist a criterion-ID before/after table: old PR, current base alone, integrated successors and deployed target. Account for all changed files and unintended added behavior; file counts, ancestry and green CI are not UX equivalence.",
|
|
16
|
-
"For every unresolved yellow/red row, create a new remediation PR within explicit implementation/commit/push/PR authority, grouping only coherent changes. Reuse an exact existing open remediation PR rather than duplicating it. Map every criterion ID to a real PR URL, owner and acceptance tests; a broad successor link or uncreated PR plan is insufficient for transfer_ready. Missing authority, unresolved product decisions or
|
|
16
|
+
"For every unresolved yellow/red row, create a new remediation PR within explicit implementation/commit/push/PR authority, grouping only coherent changes. Reuse an exact existing open remediation PR rather than duplicating it. Map every criterion ID to a real PR URL, owner and acceptance tests; a broad successor link or uncreated PR plan is insufficient for transfer_ready. Missing authority, unresolved product decisions or repeated-failure/no-progress blocks remain explicit blockers. PR creation alone never makes a row green.",
|
|
17
17
|
"Verify normal production reachability and the complete trigger-to-usable-result path, with independent technical postconditions and recovery. Code surviving only in fixtures is not inherited UX. Distinguish reuse, composition, conversational design, immediate ad-hoc work and background learning, including the no-workflow/no-match fallback.",
|
|
18
18
|
"Unknown, failed or stale applicable evidence blocks close_ready. Require independent review of original requirements and raw evidence before an authorized close. Preserve initial failures; tests changed to pass do not validate the unchanged original candidate.",
|
|
19
19
|
"Unfinished successors require transfer_ready with explicit informed administrative-close authorization, accepted owner, open tracking and remaining gates; never claim full inheritance. Intentional removal requires an explicit retirement decision and safe data/migration handling.",
|
|
@@ -12,7 +12,6 @@ export function summarizeCodingLoopEvent(snapshot, event) {
|
|
|
12
12
|
|
|
13
13
|
const charter = objectValue(snapshot?.charter);
|
|
14
14
|
const attemptNumber = numberValue(detail.attempt_number) || latestAttemptNumber(snapshot);
|
|
15
|
-
const maxAttempts = numberValue(charter.max_attempts) || 1;
|
|
16
15
|
const executor = executorLabel(detail.backend || charter.backend);
|
|
17
16
|
const repository = cleanString(charter.repository) || "the selected repository";
|
|
18
17
|
const branch = cleanString(detail.branch_name || charter.base_branch) || "the selected branch";
|
|
@@ -42,17 +41,17 @@ export function summarizeCodingLoopEvent(snapshot, event) {
|
|
|
42
41
|
return {
|
|
43
42
|
summary: `Execution scope set for: ${requirement}`,
|
|
44
43
|
result: `${countLabel(charter.acceptance_criteria, "acceptance criterion")}, ${countLabel(charter.expected_tests, "expected test")}, and ${countLabel(charter.expected_evidence, "evidence requirement")}.`,
|
|
45
|
-
next: `Attempt 1
|
|
44
|
+
next: `Attempt 1 will be handed to ${executor}.`,
|
|
46
45
|
};
|
|
47
46
|
case "dispatching":
|
|
48
47
|
return {
|
|
49
|
-
summary: `Preparing ${executor} attempt ${attemptNumber}
|
|
48
|
+
summary: `Preparing ${executor} attempt ${attemptNumber} for ${branch}.`,
|
|
50
49
|
result: "The executor instruction now includes the task, constraints, checks, and prior findings.",
|
|
51
50
|
next: `${executor} will start the local coding process.`,
|
|
52
51
|
};
|
|
53
52
|
case "coding":
|
|
54
53
|
return {
|
|
55
|
-
summary: `${executor} is implementing attempt ${attemptNumber}
|
|
54
|
+
summary: `${executor} is implementing attempt ${attemptNumber} on ${branch}.`,
|
|
56
55
|
result: "Live provider output is shown below as the local process works.",
|
|
57
56
|
next: "When execution finishes, SIMY will collect code, test, PR, and UI evidence.",
|
|
58
57
|
};
|
|
@@ -84,7 +83,7 @@ export function summarizeCodingLoopEvent(snapshot, event) {
|
|
|
84
83
|
return {
|
|
85
84
|
summary: `Attempt ${attemptNumber} needs another coding pass before review.`,
|
|
86
85
|
result: findingResult(findings),
|
|
87
|
-
next: `SIMY will give the findings to ${executor} for attempt ${
|
|
86
|
+
next: `SIMY will give the findings to ${executor} for attempt ${attemptNumber + 1}.`,
|
|
88
87
|
};
|
|
89
88
|
case "pr_ready_for_review":
|
|
90
89
|
return {
|
|
@@ -106,7 +105,7 @@ export function summarizeCodingLoopEvent(snapshot, event) {
|
|
|
106
105
|
};
|
|
107
106
|
case "blocked":
|
|
108
107
|
return {
|
|
109
|
-
summary: "The run cannot continue automatically with its
|
|
108
|
+
summary: "The run cannot continue automatically with its remaining evidence or approval requirements.",
|
|
110
109
|
result: findings.length ? findingResult(findings) : cleanString(event?.message),
|
|
111
110
|
next: "Review the findings, adjust the request or evidence, and resume the run.",
|
|
112
111
|
};
|
package/src/runner.js
CHANGED
|
@@ -239,7 +239,7 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null, loc
|
|
|
239
239
|
restoredSnapshot.charter.retry_budget = run.snapshot.charter.retry_budget;
|
|
240
240
|
restoredSnapshot.charter.token_budget = run.snapshot.charter.token_budget;
|
|
241
241
|
restoredSnapshot.charter.max_attempts = run.snapshot.charter.max_attempts;
|
|
242
|
-
refreshBudgetState(restoredSnapshot);
|
|
242
|
+
refreshRecoveryBudgetAudit(restoredSnapshot, refreshBudgetState(restoredSnapshot));
|
|
243
243
|
const lifecycleState = restoredSnapshot.metadata?.pr_lifecycle_state;
|
|
244
244
|
const persistedState =
|
|
245
245
|
typeof lifecycleState === "string" && RESTORABLE_STATES.has(lifecycleState)
|
|
@@ -418,12 +418,8 @@ function assertRunCanContinue(run) {
|
|
|
418
418
|
}
|
|
419
419
|
|
|
420
420
|
const budget = refreshBudgetState(run.snapshot);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
if (budget.retry_exhausted) {
|
|
425
|
-
throw new Error("The Agentic Loop run used its retry budget. Start a new run to continue.");
|
|
426
|
-
}
|
|
421
|
+
refreshRecoveryBudgetAudit(run.snapshot, budget);
|
|
422
|
+
|
|
427
423
|
}
|
|
428
424
|
|
|
429
425
|
export async function increaseLocalCodingRunProviderTokenBudget(run, value) {
|
|
@@ -1051,20 +1047,7 @@ export async function executeProcessAttempt({
|
|
|
1051
1047
|
if (run.localStateRoot) checkpointWrites.push(saveCodingRunSnapshot(run.localStateRoot, run.snapshot).catch(() => {
|
|
1052
1048
|
emitOutput(run, "The Provider usage checkpoint could not be persisted; recovery usage remains unverified.", { backend });
|
|
1053
1049
|
}));
|
|
1054
|
-
|
|
1055
|
-
terminalErrorCode = "TOKEN_BUDGET_EXHAUSTED";
|
|
1056
|
-
terminalError = `Provider token hard ceiling reached (${currentUsed} / ${initialBudget.hard_token_ceiling}). Extend the same run's ceiling explicitly before continuing.`;
|
|
1057
|
-
appendEvent(run.snapshot, run.status, terminalError, {
|
|
1058
|
-
code: "provider_token_ceiling_reached", phase, attempt_number: attemptNumber,
|
|
1059
|
-
provider_tokens_used: currentUsed, provider_token_ceiling: initialBudget.hard_token_ceiling,
|
|
1060
|
-
});
|
|
1061
|
-
if (!settled) {
|
|
1062
|
-
run.controlState = "stopping";
|
|
1063
|
-
void signalAndWaitForClose(child, "SIGINT", 1_500).then(async result => {
|
|
1064
|
-
if (result === "timeout" && !settled && run.child === child) result = await signalAndWaitForClose(child, "SIGTERM", 1_500);
|
|
1065
|
-
if (result === "timeout" && !settled && run.child === child) await signalAndWaitForClose(child, "SIGKILL", 750);
|
|
1066
|
-
}).catch(() => {});
|
|
1067
|
-
}
|
|
1050
|
+
|
|
1068
1051
|
},
|
|
1069
1052
|
});
|
|
1070
1053
|
const stderrDecoder = createProviderStreamDecoder({
|
|
@@ -423,7 +423,7 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
|
|
|
423
423
|
lastDependencyResolution: run.managed.lastDependencyResolution,
|
|
424
424
|
...(authorizedWait ? { approvedAction: { approvalId: approved.id, action: approved.action, oneShot: true } } : {}),
|
|
425
425
|
budgets: { ...deliveryBudgetState(run), continuations: run.continuations, failures: run.failures, maxFailures: run.maxFailures,
|
|
426
|
-
|
|
426
|
+
localExecutionLimits: false } };
|
|
427
427
|
const approvalInstruction = authorizedWait ? `SIMY recorded a matching, one-shot scoped user approval ${approved.id} for this exact selected action. The decision outcome "wait" describes its pre-approval assessment; the approved action is authorized now. Execute it once, then report what actually happened. ` : '';
|
|
428
428
|
const prompt = run.managed.phase === 'action_association' ? associationContract(run.managed.actionAssociation.job) : run.managed.phase === 'understanding' ? `Understand the ORIGINAL user request in this exact Codex conversation. This first/clarifying turn is read-only and must not perform task work. Context: ${JSON.stringify({ ...context, priorUnderstanding: run.managed.sessionUnderstanding, userAnswer: run.managed.understandingAnswer })}\n${understandingCheckpointContract(decision)} Do not write SIMY state directly.` : `Continue this existing SIMY managed task in the exact Codex conversation. Context: ${JSON.stringify(context)}\n${approvalInstruction}Execute ONLY the selected decision action. Do not perform any action from the NEXT plan in this turn. For a next local action within the enrolled task use the exact scope "task"; do not relabel external, broad, or destructive work as local. Preserve provider permissions. End with one JSON checkpoint. ${assessedCheckpointContract(decision)} Do not write SIMY state directly.`;
|
|
429
429
|
const initialSandbox = run.managed.providerSandbox || resumed.sandbox;
|
|
@@ -9,8 +9,8 @@ import { readCodexTitles, sessionTitle } from './session-titles.js';
|
|
|
9
9
|
function autoContinueState(record, isRunning, supported) {
|
|
10
10
|
if (record?.autoContinue !== true) return { autoContinueStatus: 'off', autoContinueReason: null };
|
|
11
11
|
const state = record.run?.state;
|
|
12
|
-
if (state === 'budget_exhausted') return { autoContinueStatus: '
|
|
13
|
-
if (['completed', 'reported_done'].includes(state)) return { autoContinueStatus: 'finished', autoContinueReason: '
|
|
12
|
+
if (state === 'budget_exhausted') return { autoContinueStatus: 'needs_user', autoContinueReason: 'Local execution limits have been removed. Resume this same conversation to continue.' };
|
|
13
|
+
if (['completed', 'reported_done'].includes(state)) return { autoContinueStatus: 'finished', autoContinueReason: 'Continuation finished for this conversation.' };
|
|
14
14
|
const managed = record.run?.managed;
|
|
15
15
|
const recoverableCause = record.run?.recoveryCause === 'process_exit' || managed?.resumeChannel === 'codex_app_server' && ['session_supervision', 'approved_action'].includes(record.run?.recoveryCause);
|
|
16
16
|
if (state === 'recovery_required' && (!managed?.autoRecovery || !recoverableCause || managed.pendingApproval)) return {
|
|
@@ -19,7 +19,7 @@ function autoContinueState(record, isRunning, supported) {
|
|
|
19
19
|
if (['paused', 'blocked', 'stopped', 'failed', 'verification_pending'].includes(state)) return { autoContinueStatus: 'needs_user', autoContinueReason: 'Existing supervision needs your attention before more work can continue.' };
|
|
20
20
|
if (!supported) return { autoContinueStatus: 'unavailable', autoContinueReason: 'The original provider continuation channel is unavailable.' };
|
|
21
21
|
return { autoContinueStatus: record.run ? 'supervising' : isRunning ? 'waiting' : 'watching',
|
|
22
|
-
autoContinueReason: record.run ? 'Continuing
|
|
22
|
+
autoContinueReason: record.run ? 'Continuing the original task with progress and approval checks.' : isRunning ? 'Waiting for the original turn to finish.' : 'Checking settled requests for worthwhile remaining work.' };
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function initialDiagnosisPlan(objective, contextBootstrap = false) {
|
|
@@ -147,7 +147,7 @@ export async function setSessionAutoContinue({ root, provider, session, enabled,
|
|
|
147
147
|
return updateRecord(root, provider, session, async previous => {
|
|
148
148
|
if (!previous) throw new Error('Refresh this conversation before changing automatic continuation.');
|
|
149
149
|
if (enabled && previous.run && !previous.run.managed) throw new Error('Automatic continuation is unavailable for this conversation.');
|
|
150
|
-
if (enabled && TERMINAL_RUNS.has(previous.run?.state)) throw new Error('
|
|
150
|
+
if (enabled && TERMINAL_RUNS.has(previous.run?.state)) throw new Error('This conversation is settled. Resume an eligible existing task after refreshing its status.');
|
|
151
151
|
if (enabled && provider === 'codex' && !previous.admission?.model) throw new Error('Automatic continuation is unavailable for this conversation.');
|
|
152
152
|
if (enabled && provider === 'claude' && !await liveClaudeHook(previous,
|
|
153
153
|
{ session, cwd: previous.cwd, observation: { source: previous.admission?.source } }, options.inspectProcess || processIdentity)) throw new Error('Claude needs a live matching SIMY SessionStart hook; cold CLI takeover is unavailable.');
|
|
@@ -224,7 +224,6 @@ export async function enrollSession({ root, provider, session, objective, accept
|
|
|
224
224
|
maxMinutes = 30, maxContinuations = 6, maxFailures = 3, maxNoProgressMinutes, maxTokens, continuationPolicy, hardMaxMinutes, hardMaxContinuations, providerTokenCeiling, autoContinueEpoch, openClient = openCodexAppServer,
|
|
225
225
|
readClaude = readClaudeSession, inspectProcess = processIdentity, now = Date.now() }) {
|
|
226
226
|
if (!['codex', 'claude'].includes(provider)) throw new Error('This provider does not have a verified exact-session enrollment channel.');
|
|
227
|
-
if (maxTokens !== undefined && provider !== 'codex') throw new Error('Token-budgeted supervision requires host-observed Codex usage; Claude live hooks cannot enforce this budget.');
|
|
228
227
|
if (typeof objective !== 'string' || !objective.trim() || objective.length > 4000) throw new Error('A concrete objective is required.');
|
|
229
228
|
if (!['autonomous', 'balanced', 'strict'].includes(autonomyPolicy)) throw new Error('Choose an autonomy policy.');
|
|
230
229
|
let metadata;
|
package/src/sqm/bundle-store.js
CHANGED
|
@@ -206,6 +206,7 @@ async function fetchBundle({ endpoint, repository, cliVersion, token, organizati
|
|
|
206
206
|
const error = new Error(`SQM Cloud returned HTTP ${response.status}${body ? `: ${body.slice(0, 300)}` : ""}.`);
|
|
207
207
|
error.status = response.status;
|
|
208
208
|
Object.assign(error, safeSqmDiagnostic({ status: response.status, code: parsed?.error?.code || parsed?.detail?.code,
|
|
209
|
+
reasonCode: parsed?.error?.reason_code || parsed?.detail?.reason_code,
|
|
209
210
|
requestId: response.headers?.get?.("x-request-id") }, "knowledge_bundle", "knowledge_bundle_http_error"));
|
|
210
211
|
error.fallbackAllowed = response.status >= 500 && response.status <= 599;
|
|
211
212
|
throw error;
|
package/src/sqm/diagnostics.js
CHANGED
|
@@ -8,16 +8,30 @@ export function safeSqmDiagnostic(error, stage = "check", code = "sqm_check_fail
|
|
|
8
8
|
return {
|
|
9
9
|
stage: machine(value.stage ?? value.errorStage ?? value.error_stage, stage),
|
|
10
10
|
code: machine(value.code ?? value.errorCode ?? value.error_code, code),
|
|
11
|
+
...(value.reasonCode || value.reason_code ? { reasonCode: machine(value.reasonCode ?? value.reason_code, "access_denied") } : {}),
|
|
11
12
|
...(Number.isInteger(status) && status >= 400 && status <= 599 ? { status } : {}),
|
|
12
13
|
...(typeof requestId === "string" && /^(?:[a-f0-9]{16,64}|[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$/i.test(requestId) ? { requestId } : {}),
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
const ACCESS_HINTS = {
|
|
18
|
+
entitlement_denied: "The selected organization lacks an active SQM entitlement. Ask its administrator to check the subscription and IT Engineer package.",
|
|
19
|
+
organization_inactive: "The selected organization is inactive. Ask its administrator to restore it or select an active organization.",
|
|
20
|
+
membership_required: "Your account is not an active member of the selected organization. Ask its administrator to restore membership.",
|
|
21
|
+
membership_inactive: "Your membership in the selected organization is inactive. Ask its administrator to reactivate it.",
|
|
22
|
+
permission_denied: "Your organization role does not permit SQM. Ask its administrator to grant SQM access.",
|
|
23
|
+
tenant_mismatch: "This resource belongs to another organization. Select the resource's organization before retrying.",
|
|
24
|
+
actor_forbidden: "Your account cannot use SQM in the selected organization. Ask its administrator to verify membership and permissions.",
|
|
25
|
+
resource_scope_mismatch: "The resource belongs to another organization. Select its original organization; do not retry under the current scope.",
|
|
26
|
+
};
|
|
27
|
+
|
|
16
28
|
export function formatSqmDiagnostic(value) {
|
|
17
|
-
const hint = value.status === 401 ? "Reconnect your SIMY session."
|
|
18
|
-
: value.
|
|
19
|
-
: value.code === "repository_scope_unresolved" ? "
|
|
29
|
+
const hint = ACCESS_HINTS[value.reasonCode] || (value.status === 401 ? "Reconnect your SIMY session."
|
|
30
|
+
: value.code === "sqm_entitlement_required" ? ACCESS_HINTS.entitlement_denied
|
|
31
|
+
: value.code === "repository_scope_unresolved" ? "SQM cannot resolve this repository to a connected system in the selected organization. Check the repository remote, select the correct organization and ask its administrator to connect the repository in SQM Systems. Then run min-check again."
|
|
32
|
+
: value.status === 403 ? "SQM access was denied in the selected organization. Ask its administrator to check active membership, organization status and SQM entitlement."
|
|
20
33
|
: value.code === "knowledge_bundle_changed" || value.code === "repository_identity_changed" ? "Run min-check again for the current repository and knowledge version."
|
|
21
|
-
: "
|
|
22
|
-
|
|
34
|
+
: value.status === 409 ? "The SQM request conflicts with the current repository, task or lease state. Refresh its current status before retrying."
|
|
35
|
+
: "Inspect the command diagnostics for this stage before retrying.");
|
|
36
|
+
return `${value.stage} · ${value.code}${value.reasonCode ? ` · ${value.reasonCode}` : ""}${value.status ? ` · HTTP ${value.status}` : ""}${value.requestId ? ` · Request ${value.requestId}` : ""}\n${hint}`;
|
|
23
37
|
}
|
|
@@ -22,6 +22,7 @@ export async function uploadRunEvidence({ endpoint, token, proof, signedEvidence
|
|
|
22
22
|
error.stage = "evidence_upload";
|
|
23
23
|
error.retryable = false;
|
|
24
24
|
Object.assign(error, safeSqmDiagnostic({ status: response.status, code: payload?.error?.code,
|
|
25
|
+
reasonCode: payload?.error?.reason_code,
|
|
25
26
|
requestId: response.headers?.get?.("x-request-id") }, "evidence_upload", "evidence_upload_failed"));
|
|
26
27
|
throw error;
|
|
27
28
|
}
|
package/src/sqm/review-worker.js
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
+
import { safeSqmDiagnostic, formatSqmDiagnostic } from "./diagnostics.js";
|
|
6
7
|
import { inspectProviderStatus } from "../desktop-status.js";
|
|
7
8
|
import { inspectDesktopExecutor } from "../desktop-executor.js";
|
|
8
9
|
import { safeProviderDiagnostic, PROVIDER_FAILURE_HINTS } from "./provider-diagnostics.js";
|
|
@@ -22,11 +23,12 @@ export const SQM_CODEX_MODELS = Object.freeze(["gpt-6-astra", "gpt-6.1-sol"]);
|
|
|
22
23
|
export const SQM_CODEX_MODEL = SQM_CODEX_MODELS[0];
|
|
23
24
|
|
|
24
25
|
export class SqmLocalReviewError extends Error {
|
|
25
|
-
constructor(message, { status = null, code = "sqm_local_review_error", stage = null, jobId = null, failureRecorded = false, validationFeedback = "", providerDiagnostic } = {}) {
|
|
26
|
+
constructor(message, { status = null, code = "sqm_local_review_error", stage = null, jobId = null, failureRecorded = false, validationFeedback = "", providerDiagnostic, reasonCode } = {}) {
|
|
26
27
|
super(message);
|
|
27
28
|
this.name = "SqmLocalReviewError";
|
|
28
29
|
this.status = status;
|
|
29
30
|
this.code = code;
|
|
31
|
+
if (reasonCode) this.reasonCode = safeSqmDiagnostic({ reasonCode }).reasonCode;
|
|
30
32
|
this.stage = stage; this.jobId = jobId; this.failureRecorded = failureRecorded;
|
|
31
33
|
this.validationFeedback = typeof validationFeedback === "string" ? validationFeedback.slice(0, 2000) : "";
|
|
32
34
|
if (providerDiagnostic) this.providerDiagnostic = safeProviderDiagnostic(providerDiagnostic);
|
|
@@ -75,7 +77,8 @@ export function createSqmLocalReviewApi({
|
|
|
75
77
|
if (!response.ok) {
|
|
76
78
|
throw new SqmLocalReviewError(webApiErrorMessage(payload, response.status), {
|
|
77
79
|
status: response.status,
|
|
78
|
-
code: machineCode(payload?.code || payload?.detail?.code),
|
|
80
|
+
code: machineCode(payload?.code || payload?.error?.code || payload?.detail?.code),
|
|
81
|
+
reasonCode: payload?.error?.reason_code || payload?.detail?.reason_code,
|
|
79
82
|
stage: payload?.detail?.stage, jobId: payload?.detail?.job_id, failureRecorded: payload?.detail?.failure_recorded === true,
|
|
80
83
|
validationFeedback: payload?.detail?.validation_feedback,
|
|
81
84
|
});
|
|
@@ -703,14 +706,15 @@ function reviewFailure(error, stage, provider) {
|
|
|
703
706
|
else if (providerSignInRequired(message)) code = "provider_sign_in_required";
|
|
704
707
|
else if (stage === "analysis" || stage === "adjudication") code = `${provider}_local_review_failed`;
|
|
705
708
|
}
|
|
706
|
-
return { stage: /^[a-z_]{1,40}$/.test(error?.stage || "") ? error.stage : stage, code,
|
|
709
|
+
return { ...safeSqmDiagnostic(error), stage: /^[a-z_]{1,40}$/.test(error?.stage || "") ? error.stage : stage, code,
|
|
707
710
|
status: Number.isInteger(error?.status) && error.status >= 400 && error.status <= 599 ? error.status : null,
|
|
708
711
|
providerDiagnostic: safeProviderDiagnostic(error?.providerDiagnostic) };
|
|
709
712
|
}
|
|
710
713
|
|
|
711
714
|
function formatFailure(failure, failureReported = null) {
|
|
712
715
|
if (!failure) return "SQM analysis failed. No failure diagnostics were recorded.";
|
|
713
|
-
const hint = failure.
|
|
716
|
+
const hint = [403, 409].includes(failure.status) ? formatSqmDiagnostic(failure).split("\n").slice(1).join("\n")
|
|
717
|
+
: failure.code === "provider_sign_in_required" ? "Sign in to the local AI app again."
|
|
714
718
|
: failure.code === "provider_model_unsupported" ? "The local Codex account rejected the configured SQM models. Update Codex or check this account's available models."
|
|
715
719
|
: failure.code === "analysis_timeout" ? "The local AI analysis exceeded its time limit."
|
|
716
720
|
: failure.code === "sqm_source_loading_failed" ? "SIMY could not load evidence for this analysis."
|