@awak-app/simy-cli 0.6.22 → 0.6.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -0
- package/package.json +3 -3
- package/src/backend-executable.js +75 -7
- package/src/cli-contract.js +9 -1
- package/src/codex-plugins/simy/hooks/acceptance-policy.mjs +5 -0
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +533 -74
- package/src/codex-plugins/simy/hooks/dev-work.mjs +23 -1
- package/src/codex-plugins/simy/hooks/purpose-cycle.mjs +63 -0
- package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +154 -0
- package/src/codex-plugins/simy/hooks/purpose-rule-policy.mjs +34 -0
- package/src/codex-plugins/simy/hooks/purpose-rule-review.mjs +65 -0
- package/src/codex-plugins/simy/hooks/purpose-search.mjs +123 -0
- package/src/codex-plugins/simy/hooks/purpose-workspace.mjs +97 -0
- package/src/codex-plugins/simy/skills/delivery-loop/SKILL.md +19 -5
- package/src/codex-plugins/simy/skills/sqm-loop/SKILL.md +19 -4
- package/src/delivery-hooks/command.js +48 -0
- package/src/delivery-hooks/consistency-monitor.js +126 -0
- package/src/delivery-hooks/desktop-runtime.js +70 -0
- package/src/delivery-hooks/desktop.js +13 -3
- package/src/delivery-hooks/legacy-hook-routing.js +61 -0
- package/src/delivery-hooks/local-runtime.js +123 -0
- package/src/delivery-hooks/local-supervisor.js +25 -0
- package/src/delivery-hooks/runtime-watchdog.js +101 -0
- package/src/delivery-hooks/supervisor.js +90 -22
- package/src/desktop-host.js +45 -4
- package/src/desktop-status.js +4 -2
- package/src/dev-work-events.js +32 -3
- package/src/durable-local-task-worker.js +57 -12
- package/src/fp-narrative-image.js +180 -0
- package/src/index.js +1 -1
- package/src/local-task.js +4 -0
- package/src/orchestrator/audit.js +1 -1
- package/src/orchestrator/charter-repair.js +93 -0
- package/src/orchestrator/charter-validation.js +14 -6
- package/src/orchestrator/checklist-continuation.js +1 -1
- package/src/orchestrator/contract.js +1 -3
- package/src/orchestrator/delivery/WORKFLOW.md +33 -60
- package/src/orchestrator/delivery/controls/harness-controls.json +1326 -0
- package/src/orchestrator/delivery/references/delivery-levels.md +17 -44
- package/src/orchestrator/delivery/references/pr-closure.md +2 -3
- package/src/orchestrator/delivery/references/state-and-receipts.md +14 -5
- package/src/orchestrator/delivery/references/ux-specification.md +1 -2
- package/src/orchestrator/delivery-level.js +4 -5
- package/src/orchestrator/independent-audit.js +9 -5
- package/src/orchestrator/instruction.js +1 -0
- package/src/orchestrator/loop.js +55 -18
- package/src/orchestrator/risk.js +1 -1
- package/src/orchestrator/ux-specification.js +2 -2
- package/src/runner.js +22 -1
- package/src/session-supervision/analysis-manager.js +2 -2
- package/src/session-supervision/automatic-continuation.js +6 -5
- package/src/session-supervision/autonomous-upgrade.js +55 -0
- package/src/session-supervision/codex-app-server.js +200 -24
- package/src/session-supervision/consistency-auditor.js +113 -0
- package/src/session-supervision/index.js +38 -11
- package/src/session-supervision/purpose-candidate.js +44 -0
- package/src/session-supervision/purpose-instruction.js +47 -0
- package/src/session-supervision/purpose-observer.js +172 -0
- package/src/session-supervision/purpose-recovery.js +264 -0
- package/src/session-supervision/purpose-verification.js +154 -0
- package/src/sqm/agent-skill.js +6 -3
- package/src/sqm/artifact-paths.js +39 -0
- package/src/sqm/claude-plugin.js +7 -0
- package/src/sqm/command.js +95 -27
- package/src/sqm/history-store.js +3 -3
- package/src/sqm/repository.js +19 -5
- package/src/sqm/workflow-update.js +14 -0
- package/src/workflow-reporting.js +2 -0
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { retainApplicableRules } from './purpose-rule-policy.mjs';
|
|
3
|
+
import { reconcileRequirements, retainControllerEvidence, reconcileFindings, findingText, evidenceLedger, currentPurposeObservation } from './purpose-evidence.mjs';
|
|
4
|
+
import { workspaceIdentity } from './purpose-workspace.mjs';
|
|
5
|
+
import { searchEnabled, failureLimitReached, searchState, recordSearchExperiment, rememberSearchWait, searchNeedsPivot, markSearchPivot, repeatedSearchAction } from './purpose-search.mjs';
|
|
2
6
|
// Standalone by design: this exact module is shipped to both provider plugins.
|
|
3
7
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
8
|
import { mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
@@ -7,6 +11,9 @@ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
|
7
11
|
import { execFile } from 'node:child_process';
|
|
8
12
|
import { promisify } from 'node:util';
|
|
9
13
|
import { acquireWorkspaceLease, releaseWorkspaceLease } from './workspace-lease.mjs';
|
|
14
|
+
import { purposeEnabled, purposeContract, purposeDiagnosisPlan, validatePurposeReview } from './purpose-cycle.mjs';
|
|
15
|
+
import { effectiveAcceptanceCriteria } from './acceptance-policy.mjs';
|
|
16
|
+
import { assessRuleReview } from './purpose-rule-review.mjs';
|
|
10
17
|
import { associationContract, validateAssociationResult } from './action-association.mjs';
|
|
11
18
|
import { minimalSufficientChangeInstruction } from './minimal-sufficient.mjs';
|
|
12
19
|
import { continuationMode } from './continuation-policy.mjs';
|
|
@@ -14,7 +21,7 @@ import { evaluateOutcomeProgress, restartOutcomeProgressWindow } from './outcome
|
|
|
14
21
|
|
|
15
22
|
export const TERMINAL = new Set(['completed', 'reported_done', 'stopped', 'failed', 'budget_exhausted']);
|
|
16
23
|
const EVENTS = new Set(['SessionStart', 'UserPromptSubmit', 'Stop', 'StopFailure', 'SessionEnd', 'Interrupt', 'PermissionRequest', 'Notification']);
|
|
17
|
-
const MAX_BYTES =
|
|
24
|
+
const MAX_BYTES = 256 * 1024;
|
|
18
25
|
const RETRYABLE = new Set(['overloaded', 'server_error', 'rate_limit']);
|
|
19
26
|
const APPROVAL_DECISIONS = new Set(['automatic', 'proceed_and_report', 'wait']);
|
|
20
27
|
const LOCAL_ACTIONS = new Set(['read', 'local_edit', 'local_test']);
|
|
@@ -190,7 +197,7 @@ export async function updateRecord(root, provider, session, mutate, { inspectPro
|
|
|
190
197
|
}
|
|
191
198
|
}
|
|
192
199
|
const serialized = JSON.stringify(next);
|
|
193
|
-
if (Buffer.byteLength(serialized) > MAX_BYTES) throw new Error('Delivery state exceeds
|
|
200
|
+
if (Buffer.byteLength(serialized) > MAX_BYTES) throw new Error('Delivery state exceeds 256 KiB; reduce task details.');
|
|
194
201
|
if (serialized === previousBytes) return next;
|
|
195
202
|
temporary = `${file}.${randomUUID()}.tmp`;
|
|
196
203
|
await writeFile(temporary, serialized, { mode: 0o600 });
|
|
@@ -289,7 +296,12 @@ function assessedPlan(value) {
|
|
|
289
296
|
const alternatives = value.alternatives.map(option => {
|
|
290
297
|
const id = text(option?.id, 80);
|
|
291
298
|
if (!/^[A-Za-z0-9_-]{1,80}$/.test(id)) throw new Error('Invalid plan option ID.');
|
|
292
|
-
|
|
299
|
+
const frontier = {};
|
|
300
|
+
for (const key of ['criterionIds','dependsOn','readResources','writeResources','requiredCapabilities','requiredPermissions']) {
|
|
301
|
+
if (option[key] !== undefined && (!Array.isArray(option[key]) || option[key].length > 32 || option[key].some(value => typeof value !== 'string' || !value.trim() || value.length > 200))) throw new Error('Invalid work unit metadata.');
|
|
302
|
+
if (option[key]) frontier[key] = [...new Set(option[key])];
|
|
303
|
+
}
|
|
304
|
+
return { id, ...frontier, ...(option.independent === true && text(option.independenceReason, 300) ? { independent: true, independenceReason: text(option.independenceReason, 300) } : {}), action: actionDescriptor(option.action), assessment: assessment(option.assessment), expectedVerification: text(option.expectedVerification, 1000), rollback: text(option.rollback, 1000) };
|
|
293
305
|
});
|
|
294
306
|
if (new Set(alternatives.map(option => option.id)).size !== alternatives.length) throw new Error('Duplicate plan option ID.');
|
|
295
307
|
const selectedOptionId = text(value.selectedOptionId, 80);
|
|
@@ -318,22 +330,64 @@ function rejectedActionDiagnosis(managed, rejected) {
|
|
|
318
330
|
expectedVerification: 'Report the rejection reason and a concrete, different assessed next option.', rollback: 'No changes to undo.' }],
|
|
319
331
|
selectedOptionId: 'diagnose_rejected_action', recommendation: { optionId: 'diagnose_rejected_action', reason: 'Read the recorded rejection before choosing different work.' } });
|
|
320
332
|
}
|
|
321
|
-
|
|
333
|
+
function researchWaitDiagnosis(managed) {
|
|
334
|
+
const previous = managed.decision;
|
|
335
|
+
const action = { kind: 'read', scope: previous.action.scope, summary: 'Inspect the recorded assessment and local evidence; propose a corrected assessed plan without executing the waiting action' };
|
|
336
|
+
return assessedPlan({ diagnosis: `Research wait: ${previous.reason} Waiting action remains unexecuted: ${previous.action.summary}. Inspect its scope, effect boundaries and verification method before proposing another plan.`,
|
|
337
|
+
alternatives: [{ id: 'diagnose_research_wait', action,
|
|
338
|
+
assessment: { risk: 'low', reversibility: 'reversible', externalEffect: 'none', blastRadius: 'single_file', uncertainty: 'low', inObjective: true, userChoice: false },
|
|
339
|
+
expectedVerification: 'Report observed assessment evidence and a concrete, corrected assessed next option.', rollback: 'No changes to undo.' }],
|
|
340
|
+
selectedOptionId: 'diagnose_research_wait', recommendation: { optionId: 'diagnose_research_wait', reason: 'Acquire local evidence before reassessing the waiting action.' } });
|
|
341
|
+
}
|
|
342
|
+
// This is only an unexecuted enrollment repair, not recovery of later work.
|
|
343
|
+
// Retain even settled attempt history as a boundary for the dedicated recovery path.
|
|
344
|
+
function canResearchReplan(run, provider, now) {
|
|
345
|
+
const m = run?.managed, d = m?.decision;
|
|
346
|
+
const channel = m?.resumeChannel === 'codex_app_server' && provider === 'codex' ||
|
|
347
|
+
m?.capability === 'live_hook' && ['codex', 'claude'].includes(provider);
|
|
348
|
+
return Boolean(channel && m.autonomyPolicy && m.approvalScopes?.includes(d?.action?.scope) &&
|
|
349
|
+
run.state === 'blocked' && run.revision === 0 && d?.outcome === 'wait' && d.waitKind === 'research' &&
|
|
350
|
+
LOCAL_ACTIONS.has(d.action.kind) && d.assessment?.inObjective === true && d.assessment.userChoice === false &&
|
|
351
|
+
d.assessment.reversibility === 'reversible' && ['none', 'local'].includes(d.assessment.externalEffect) &&
|
|
352
|
+
!m.pendingApproval && !m.approvedAction && !m.rejectedActionOverflow && !m.executionUncertain &&
|
|
353
|
+
!m.executionAttempt && !m.liveWorkspaceLease && !m.resumeLease && !m.dependencyWait &&
|
|
354
|
+
!m.userQuestion && !['understanding', 'needs_clarification', 'action_association'].includes(m.phase) &&
|
|
355
|
+
!m.nativeWait && !m.providerWarning && !m.usageBlocked && !m.purpose?.nativeBoundary && !run.blocker && !deliveryStop(run, now, { checkProgress: false }));
|
|
356
|
+
}
|
|
357
|
+
export function evaluateAssessedPlan(plan, policy, scopes, now = Date.now(), { allowLocalTestWrites = false, llmDelegated = false, allowReversibleExternal = false } = {}) {
|
|
322
358
|
const option = plan.alternatives.find(item => item.id === plan.selectedOptionId);
|
|
323
359
|
const { action } = option;
|
|
324
360
|
const facts = assessment(option.assessment);
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
else if (
|
|
331
|
-
else if (
|
|
332
|
-
else if (
|
|
333
|
-
else if (
|
|
334
|
-
else if (
|
|
335
|
-
else if (
|
|
336
|
-
|
|
361
|
+
const localWrite = action.kind === 'local_edit' || allowLocalTestWrites && action.kind === 'local_test' && facts.externalEffect === 'local';
|
|
362
|
+
const autonomous = policy === 'autonomous' && llmDelegated;
|
|
363
|
+
const external = autonomous && allowReversibleExternal && action.kind === 'external' && facts.externalEffect === 'external';
|
|
364
|
+
let reason = null, waitKind = 'research';
|
|
365
|
+
if (!scopes.includes(action.scope)) { reason = 'Action scope is outside the task enrollment.'; waitKind = 'authority'; }
|
|
366
|
+
else if (facts.inObjective !== true) reason = 'Establish objective fit or choose work within the original task.';
|
|
367
|
+
else if (facts.reversibility === 'irreversible') { reason = 'Irreversible consequences require an exact user decision.'; waitKind = 'human'; }
|
|
368
|
+
else if (facts.reversibility !== 'reversible' || facts.externalEffect === 'unknown' || facts.blastRadius === 'unknown') reason = 'Investigate effect boundaries and reversibility before execution.';
|
|
369
|
+
else if (!autonomous && (facts.userChoice !== false || Object.values(facts).includes('unknown') || facts.userChoice === null)) reason = 'Action assessment or user choice needs review under the selected policy.';
|
|
370
|
+
else if (!option.expectedVerification) reason = 'Expected verification is missing; acquire a verification method.';
|
|
371
|
+
else if ((!LOCAL_ACTIONS.has(action.kind) || facts.externalEffect === 'external') && !external) { reason = 'This action needs existing external authority or a reversible supported alternative.'; waitKind = 'authority'; }
|
|
372
|
+
else if ((localWrite || external) && (!option.rollback || external && /^(?:no changes|none|n\/a)/i.test(option.rollback.trim()))) reason = 'Effectful work needs a concrete restoration plan.';
|
|
373
|
+
else if (localWrite && facts.externalEffect !== 'local') reason = 'Local write has inconsistent effect boundaries.';
|
|
374
|
+
else if (!localWrite && !external && facts.externalEffect !== 'none') reason = 'Read or test action has unexpected side effects.';
|
|
375
|
+
else if (!autonomous && (facts.blastRadius === 'multi_system' || facts.risk === 'high' || facts.uncertainty === 'high')) reason = 'Selected policy requires a narrower or lower-risk action.';
|
|
376
|
+
else if (policy === 'strict' && (localWrite || facts.risk !== 'low' || facts.uncertainty !== 'low')) reason = 'Strict policy requires low-risk read or local test.';
|
|
377
|
+
else if (policy === 'balanced' && (facts.risk !== 'low' || facts.uncertainty !== 'low' || localWrite && facts.blastRadius !== 'single_file')) reason = 'Balanced policy requires a low-risk, narrow action.';
|
|
378
|
+
const report = external || autonomous && (facts.risk !== 'low' || facts.uncertainty !== 'low' || facts.userChoice !== false || facts.blastRadius === 'multi_system') || policy === 'autonomous' && facts.risk === 'medium';
|
|
379
|
+
return { id: digest(JSON.stringify({ plan, policy, llmDelegated, allowReversibleExternal })), outcome: reason ? 'wait' : report ? 'report' : 'proceed', ...(reason ? { waitKind } : {}), ...(external && !reason ? { autonomousExternal: true } : {}), reason: reason || 'AI selected reversible work within the enrolled objective and provider authority.', selectedOptionId: option.id, action, assessment: facts, expectedVerification: option.expectedVerification, rollback: option.rollback, policy, at: now };
|
|
380
|
+
}
|
|
381
|
+
// Recompute from the frozen plan, not an arbitrary checkpoint flag. Provider
|
|
382
|
+
// permissions are checked independently by the adapter at actual dispatch.
|
|
383
|
+
export function canExecuteAutonomousExternal(managed) {
|
|
384
|
+
if (!searchEnabled(managed) || managed.resumeChannel !== 'codex_app_server' || managed.autonomyPolicy !== 'autonomous' || managed.executionUncertain || managed.purpose?.nativeBoundary || managed.pendingApproval?.denied || managed.rejectedActionOverflow) return false;
|
|
385
|
+
const decision = managed.decision;
|
|
386
|
+
if (!decision?.autonomousExternal || decision.outcome !== 'report' || decision.action.kind !== 'external' || !managed.plan?.alternatives?.length) return false;
|
|
387
|
+
const selected = managed.plan.alternatives.find(row => row.id === managed.plan.selectedOptionId);
|
|
388
|
+
if (!selected || JSON.stringify(selected.action) !== JSON.stringify(decision.action) || selected.rollback !== decision.rollback || JSON.stringify(assessment(selected.assessment)) !== JSON.stringify(decision.assessment) || managed.rejectedActionDigests?.includes(digest(JSON.stringify(decision.action)))) return false;
|
|
389
|
+
const current = evaluateAssessedPlan(managed.plan, managed.autonomyPolicy, managed.approvalScopes, Date.now(), { llmDelegated: true, allowReversibleExternal: true, allowLocalTestWrites: true });
|
|
390
|
+
return current.autonomousExternal === true && current.outcome === 'report';
|
|
337
391
|
}
|
|
338
392
|
// Shared by live Stop hooks and cold exact-session recovery. Keep the result
|
|
339
393
|
// contract identical across transports; the host still verifies acceptance.
|
|
@@ -341,7 +395,7 @@ export function assessedCheckpointContract(decision) {
|
|
|
341
395
|
return `${minimalSufficientChangeInstruction({ turn: 'assessed' })}\nReturn ONLY one JSON object with top-level status:"in_progress|completed|blocked|error" and summary:"what happened". Use the key status, NEVER kind, for an ordinary work checkpoint. Record actionResult:{decisionId:"${decision.id}",status:"observed|failed|not_run",summary:"what actually happened",evidence:["concrete observation"]} for THIS selected action. This is provider-reported, not independent verification. For in_progress/error include nextStep, remainingWork, and plan:{diagnosis,alternatives:[{id,action:{kind,scope,summary},assessment:{risk,reversibility,externalEffect,blastRadius,uncertainty,inObjective,userChoice},expectedVerification,rollback}],selectedOptionId,recommendation:{optionId,reason},userQuestion?} for the NEXT action; select one assessed option but do not execute it in this turn. Valid action.kind values are exactly read, local_edit, local_test, external, destructive, scope_change, unknown; use local_edit for writing local files. Risk is low|medium|high|unknown; reversibility reversible|irreversible|unknown; externalEffect none|local|external|unknown; blastRadius single_file|repository|multi_system|unknown; uncertainty low|medium|high|unknown. Missing facts are unknown. Completed requires evidence, and SIMY independently checks frozen acceptance. Blocked requires blocker set to exactly authorization, missing_information, or external_dependency, plus nextStep. For authorization, include an assessed plan and set approval to an exact copy of the selected plan alternative action object: kind, scope, and summary strings MUST be identical, with no paraphrasing. missing_information requires a specific question. external_dependency requires waitCondition:{condition,owner,nextCheck} with the observable resumption condition, dependency owner, and bounded recheck instruction, plus nextStep.`;
|
|
342
396
|
}
|
|
343
397
|
export function understandingCheckpointContract(decision) {
|
|
344
|
-
return `${minimalSufficientChangeInstruction({ turn: 'understanding' })}\nThis is an UNDERSTANDING turn, not a work turn. Read the existing conversation context and available read-only evidence; do not edit files, call external services, or start the proposed next action. Return ONLY JSON {kind:"session_understanding",summary:"...",actionResult:{decisionId:"${decision.id}",status:"observed|failed|not_run",summary:"...",evidence:["..."]},sessionUnderstanding:{state:"clear|ambiguous|context_missing|already_done",objective:"current user goal",constraints:["..."],done:["..."],remaining:["..."],nextAction:{kind:"read|local_edit|local_test|external|destructive|scope_change|unknown",scope:"task",summary:"..."},criteria:[{description:"...",source:"user_grounded|proposed",basisIndex:0,check:{type:"manual|file_exists|file_contains|file_equals",path:"relative/path",text:"expected text"}}],ambiguities:["..."],question:"one focused user question if unclear",basis:[{source:"user|assistant|artifact",turnRef:"if available",snippet:"short relevant excerpt"}]},plan:{diagnosis:"...",alternatives:[{id:"...",action:{kind,scope,summary},assessment:{risk,reversibility,externalEffect,blastRadius,uncertainty,inObjective,userChoice},expectedVerification:"...",rollback:"..."}],selectedOptionId:"...",recommendation:{optionId:"...",reason:"..."}}}. For clear, choose one NEXT action but do not execute it. Set sessionUnderstanding.nextAction to an exact copy of the selected plan alternative's action object: kind, scope, and summary strings MUST be identical, with no paraphrasing. Complete every known assessment field: risk low|medium|high|unknown; reversibility reversible|irreversible|unknown; externalEffect none|local|external|unknown; blastRadius single_file|repository|multi_system|unknown; uncertainty low|medium|high|unknown; inObjective and userChoice booleans. For a read-only single-file action with no effects, use reversible, none, single_file when those facts are known; unknown facts require
|
|
398
|
+
return `${minimalSufficientChangeInstruction({ turn: 'understanding' })}\nThis is an UNDERSTANDING turn, not a work turn. Read the existing conversation context and available read-only evidence; ${decision.purpose ? "Authorized read-only API and web research is allowed within existing provider permissions; do not send private material to external services. Do not edit files, write to external services, or start the proposed next action." : "do not edit files, call external services, or start the proposed next action."} Return ONLY JSON {kind:"session_understanding",summary:"...",actionResult:{decisionId:"${decision.id}",status:"observed|failed|not_run",summary:"...",evidence:["..."]},sessionUnderstanding:{state:"clear|ambiguous|context_missing|already_done",objective:"current user goal",constraints:["..."],done:["..."],remaining:["..."],nextAction:{kind:"read|local_edit|local_test|external|destructive|scope_change|unknown",scope:"task",summary:"..."},criteria:[{description:"...",source:"user_grounded|proposed",basisIndex:0,check:{type:"manual|semantic|file_exists|file_contains|file_equals",path:"relative/path",text:"expected text"}}],ambiguities:["..."],question:"one focused user question if unclear",basis:[{source:"user|assistant|artifact",turnRef:"if available",snippet:"short relevant excerpt"}]},plan:{diagnosis:"...",alternatives:[{id:"...",action:{kind,scope,summary},assessment:{risk,reversibility,externalEffect,blastRadius,uncertainty,inObjective,userChoice},expectedVerification:"...",rollback:"..."}],selectedOptionId:"...",recommendation:{optionId:"...",reason:"..."}}}. For clear, choose one NEXT action but do not execute it. Set sessionUnderstanding.nextAction to an exact copy of the selected plan alternative's action object: kind, scope, and summary strings MUST be identical, with no paraphrasing. Complete every known assessment field: risk low|medium|high|unknown; reversibility reversible|irreversible|unknown; externalEffect none|local|external|unknown; blastRadius single_file|repository|multi_system|unknown; uncertainty low|medium|high|unknown; inObjective and userChoice booleans. For a read-only single-file action with no effects, use reversible, none, single_file when those facts are known; unknown facts require AI investigation; reversible preferences are delegated under autonomous policy. Use only the original user's goal; constraints[] must contain only constraints from the original user, not these SIMY turn instructions. Do not invent new work or acceptance obligations. Include a user-grounded criterion only with a basisIndex pointing to a user message. Use file_equals for a user-requested file whose complete UTF-8 contents must be exact (including whitespace/newlines), file_contains only for a requested substring, and file_exists only for existence. ${decision.search ? "For semantic outcomes use check.type semantic; reserve manual for an explicit requirement for a human decision. Missing verification tooling is work to do, not a manual condition." : "For semantic conditions use manual."} Never emit file_equals with null, omitted, guessed, or placeholder text. If exact expected contents are unknown, choose read to learn them and use semantic for purpose search or manual otherwise. Do not invent a file check from repository hints alone. If the original work is already done, choose already_done with remaining:[], nextAction:null, no plan, and no new goal. First recover missing context with authorized reads and infer reversible defaults from the original request. Select read investigation when facts remain unknown. Only if the original goal cannot be grounded at all or irreversible personal consequences require a decision, set one ambiguity and one focused question, nextAction:null, and omit plan. Provider statements about past work and semantic success are not independent verification. Optionally include sessionUnderstanding.matchingContext:{deliverables:["concrete requested output"],projectHints:["project name"],repositoryUrls:[],relatedUrls:[],actionIds:[],sourceRefs:[{platform:"slack",externalThreadId:"exact thread",channelExternalId:"exact channel"}]}. Extract only explicit conversation evidence; IDs and sources are retrieval signals, never authenticated identity. Do not infer URLs or identifiers.`;
|
|
345
399
|
}
|
|
346
400
|
function boundedContextList(value, maxItems = 8, maxLength = 300) {
|
|
347
401
|
if (!Array.isArray(value) || value.length > maxItems) throw new Error('Session understanding has too many items.');
|
|
@@ -372,12 +426,12 @@ function sessionUnderstanding(value) {
|
|
|
372
426
|
const basisIndex = Number.isSafeInteger(item.basisIndex) && item.basisIndex >= 0 && item.basisIndex < basis.length ? item.basisIndex : null;
|
|
373
427
|
if (item.source === 'user_grounded' && (basisIndex === null || basis[basisIndex].source !== 'user')) throw new Error('User-grounded criterion needs a user-message basis.');
|
|
374
428
|
const check = item.check?.type || 'manual';
|
|
375
|
-
if (!['manual', 'file_exists', 'file_contains', 'file_equals'].includes(check)) throw new Error('Unsupported inferred acceptance check.');
|
|
376
|
-
if (
|
|
429
|
+
if (!['manual', 'semantic', 'file_exists', 'file_contains', 'file_equals'].includes(check)) throw new Error('Unsupported inferred acceptance check.');
|
|
430
|
+
if (!['manual', 'semantic'].includes(check) && (!text(item.check.path, 500) || isAbsolute(item.check.path) || item.check.path.split(/[\\/]/).includes('..'))) throw new Error('Inferred file check needs a relative task path.');
|
|
377
431
|
if (check === 'file_contains' && !text(item.check.text, 1000)) throw new Error('Inferred file check needs expected text.');
|
|
378
432
|
if (check === 'file_equals' && exactText(item.check.text) === null) throw new Error('Inferred file_equals needs bounded exact UTF-8 text.');
|
|
379
433
|
return { id: `inferred-${index + 1}`, description, source: item.source, basisIndex,
|
|
380
|
-
check:
|
|
434
|
+
check: ['manual', 'semantic'].includes(check) ? { type: check } : { type: check, path: text(item.check.path, 500), ...(check === 'file_contains' ? { text: contextText(item.check.text, 1000) } : {}), ...(check === 'file_equals' ? { text: exactText(item.check.text) } : {}) } };
|
|
381
435
|
}) : [];
|
|
382
436
|
const result = { state: value.state, objective: contextText(value.objective, 1000), constraints: boundedContextList(value.constraints),
|
|
383
437
|
done: boundedContextList(value.done), remaining: boundedContextList(value.remaining), nextAction: value.nextAction ? actionDescriptor(value.nextAction) : null,
|
|
@@ -388,19 +442,92 @@ function sessionUnderstanding(value) {
|
|
|
388
442
|
if (['ambiguous', 'context_missing'].includes(result.state) && (!result.question || result.ambiguities.length !== 1)) throw new Error('Unclear understanding needs one focused question.');
|
|
389
443
|
return result;
|
|
390
444
|
}
|
|
445
|
+
function recordPurposeReview(managed, review, now) {
|
|
446
|
+
const purpose = managed.purpose;
|
|
447
|
+
if (searchEnabled(managed)) reconcileRequirements(managed, review, now);
|
|
448
|
+
purpose.lastReview = { ...review, at: now, source: 'provider_reported' };
|
|
449
|
+
}
|
|
450
|
+
// Prose is planning context, never a progress receipt. Remember every attained
|
|
451
|
+
// criterion identity so a failure/recovery oscillation cannot renew the clock.
|
|
452
|
+
export function purposeProgressAt(run, now = Date.now()) {
|
|
453
|
+
const managed = run.managed, purpose = managed?.purpose;
|
|
454
|
+
if (purpose?.progressVersion === 2) return purpose.lastEvidenceAt || run.startedAt;
|
|
455
|
+
// Migration can retain actual controller/user acceptance receipts, but never
|
|
456
|
+
// a v1 provider-reported observation timestamp.
|
|
457
|
+
const timestamps = (managed?.acceptanceCriteria || []).flatMap(criterion => {
|
|
458
|
+
const receipt = managed.verified?.[criterion.id];
|
|
459
|
+
const expectedSource = criterion.check.type === 'manual' ? 'user' : criterion.check.type;
|
|
460
|
+
return receipt?.source === expectedSource && Number.isFinite(receipt.checkedAt) && receipt.checkedAt <= now
|
|
461
|
+
? [receipt.checkedAt] : [];
|
|
462
|
+
});
|
|
463
|
+
return Math.max(run.startedAt, ...timestamps);
|
|
464
|
+
}
|
|
465
|
+
function recordPurposeProgress(run, now, countAttempt = true) {
|
|
466
|
+
if (!purposeEnabled(run)) return;
|
|
467
|
+
const managed = run.managed, purpose = managed.purpose;
|
|
468
|
+
if (purpose.progressVersion !== 2) {
|
|
469
|
+
const baseline = purposeProgressAt(run, now);
|
|
470
|
+
purpose.progressVersion = 2; purpose.noLearning = 0;
|
|
471
|
+
purpose.lastEvidenceAt = baseline; delete purpose.observationDigest;
|
|
472
|
+
}
|
|
473
|
+
purpose.seenVerifiedCriteria ||= [];
|
|
474
|
+
const fresh = managed.acceptanceCriteria.filter(criterion => Object.hasOwn(managed.verified, criterion.id))
|
|
475
|
+
.map(criterion => digest(JSON.stringify(criterion))).filter(key => !purpose.seenVerifiedCriteria.includes(key));
|
|
476
|
+
if (fresh.length && purpose.seenVerifiedCriteria.length + fresh.length <= 128) {
|
|
477
|
+
purpose.seenVerifiedCriteria.push(...fresh);
|
|
478
|
+
purpose.noLearning = 0; purpose.lastEvidenceAt = now;
|
|
479
|
+
} else {
|
|
480
|
+
if (countAttempt) purpose.noLearning += 1;
|
|
481
|
+
if (fresh.length) purpose.progressCapacityReached = true;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
391
484
|
function recordDecision(managed, plan, now) {
|
|
392
|
-
|
|
485
|
+
let decision = evaluateAssessedPlan(plan, managed.autonomyPolicy, managed.approvalScopes, now, { allowLocalTestWrites: searchEnabled(managed), llmDelegated: searchEnabled(managed), allowReversibleExternal: searchEnabled(managed) && managed.resumeChannel === 'codex_app_server' });
|
|
486
|
+
if (searchEnabled(managed) && decision.outcome === 'wait') {
|
|
487
|
+
rememberSearchWait(managed, 'authorization', { action: decision.action, reason: decision.reason }, now);
|
|
488
|
+
const priorities = new Map((managed.purpose.lastReview?.priorities || []).map(row => [row.optionId, row.rank]));
|
|
489
|
+
for (const candidate of [...plan.alternatives].sort((a, b) => (priorities.get(a.id) ?? 99) - (priorities.get(b.id) ?? 99))) {
|
|
490
|
+
if (candidate.id === plan.selectedOptionId || !candidate.independent) continue;
|
|
491
|
+
const alternative = { ...plan, selectedOptionId: candidate.id, recommendation: { optionId: candidate.id, reason: 'Continue the highest listed eligible independent alternative.' } };
|
|
492
|
+
const assessed = evaluateAssessedPlan(alternative, managed.autonomyPolicy, managed.approvalScopes, now, { allowLocalTestWrites: searchEnabled(managed), llmDelegated: searchEnabled(managed), allowReversibleExternal: searchEnabled(managed) && managed.resumeChannel === 'codex_app_server' });
|
|
493
|
+
if (assessed.outcome !== 'wait' && !managed.rejectedActionOverflow && !managed.rejectedActionDigests?.includes(digest(JSON.stringify(assessed.action)))) {
|
|
494
|
+
plan = alternative; decision = assessed; break;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
393
498
|
if (managed.rejectedActionOverflow || managed.rejectedActionDigests?.includes(digest(JSON.stringify(decision.action)))) {
|
|
394
499
|
decision.outcome = 'wait';
|
|
395
|
-
decision.reason = 'A previously rejected action requires a fresh exact user decision.';
|
|
500
|
+
decision.reason = 'A previously rejected action requires a fresh exact user decision.'; decision.waitKind = 'human';
|
|
501
|
+
}
|
|
502
|
+
if (managed.purpose?.nativeBoundary && decision.action.kind !== 'read' && decision.outcome !== 'wait') {
|
|
503
|
+
decision.nativeIsolationEligible = true;
|
|
504
|
+
decision.outcome = 'wait';
|
|
505
|
+
decision.reason = 'Native operation remains pending; shared writes need confirmed ownership or an authorized isolated candidate workspace.';
|
|
396
506
|
}
|
|
397
507
|
managed.decisionGeneration = (managed.decisionGeneration || 0) + 1;
|
|
398
508
|
decision.id = digest(`${managed.taskId}:${managed.decisionGeneration}:${decision.id}`);
|
|
509
|
+
if (managed.purpose?.enabled) decision.purpose = true;
|
|
510
|
+
if (searchEnabled(managed)) decision.search = true;
|
|
511
|
+
if (searchEnabled(managed)) {
|
|
512
|
+
const ledger = evidenceLedger(managed);
|
|
513
|
+
ledger.units ||= [];
|
|
514
|
+
const option = plan.alternatives.find(row => row.id === decision.selectedOptionId);
|
|
515
|
+
const criterionIds = option?.criterionIds?.length ? option.criterionIds : managed.acceptanceCriteria.map(row => row.id);
|
|
516
|
+
if (criterionIds.some(id => !managed.acceptanceCriteria.some(row => row.id === id))) throw new Error('Work unit references unknown criterion.');
|
|
517
|
+
const dependsOn = option?.dependsOn || [];
|
|
518
|
+
if (dependsOn.some(id => !ledger.units.some(row => row.id === id && row.status === 'verified' && row.purposeRevision === ledger.purposeRevision))) { decision.outcome = 'wait'; decision.reason = 'Required work-unit dependency has not been independently verified.'; }
|
|
519
|
+
ledger.units.push({ id: decision.id, criterionIds, dependsOn, readResources: option?.readResources || ['task'],
|
|
520
|
+
writeResources: option?.writeResources || (decision.action.kind === 'read' ? [] : ['task']),
|
|
521
|
+
requiredCapabilities: option?.requiredCapabilities || [decision.action.kind], requiredPermissions: option?.requiredPermissions || [decision.action.scope],
|
|
522
|
+
status: decision.outcome === 'wait' ? 'waiting' : 'planned', evidenceRefs: [], purposeRevision: ledger.purposeRevision || 1, action: decision.action, createdAt: now });
|
|
523
|
+
if (ledger.units.length > 128) { ledger.unitsOmitted = true; ledger.units = ledger.units.filter(row => ['waiting','planned','unknown'].includes(row.status)).concat(ledger.units.filter(row => !['waiting','planned','unknown'].includes(row.status)).slice(-64)); }
|
|
524
|
+
if (ledger.units.length > 192) throw new Error('Work frontier needs reconciliation before more dispatch.');
|
|
525
|
+
}
|
|
399
526
|
managed.plan = plan; managed.decision = decision; managed.proposedAction = decision.action;
|
|
400
527
|
managed.decisionHistory ||= [];
|
|
401
528
|
if (managed.decisionHistory.at(-1)?.id !== decision.id) managed.decisionHistory.push({ id: decision.id, at: now, outcome: decision.outcome, reason: decision.reason, selectedOptionId: decision.selectedOptionId, action: decision.action });
|
|
402
529
|
managed.decisionHistory = managed.decisionHistory.slice(-10);
|
|
403
|
-
managed.pendingApproval = decision.outcome === 'wait' ? { id: decision.id, ...decision.action, reason: decision.reason } : null;
|
|
530
|
+
managed.pendingApproval = decision.outcome === 'wait' && !(searchEnabled(managed) && decision.waitKind === 'research') ? { id: decision.id, ...decision.action, reason: decision.reason } : null;
|
|
404
531
|
return decision;
|
|
405
532
|
}
|
|
406
533
|
function rememberRejectedAction(managed, pending) {
|
|
@@ -437,11 +564,11 @@ function managedSpec(spec, session, provider, observedProcess, admission, now) {
|
|
|
437
564
|
const id = text(item.id, 80) || `criterion-${index + 1}`;
|
|
438
565
|
if (!/^[A-Za-z0-9_-]+$/.test(id) || ['USER-GOAL', '__proto__', 'constructor', 'prototype', 'toString', 'valueOf', 'hasOwnProperty'].includes(id)) throw new Error('Invalid acceptance criterion ID.');
|
|
439
566
|
const check = item.check || { type: 'manual' };
|
|
440
|
-
if (!['manual', 'file_exists', 'file_contains', 'file_equals'].includes(check.type)) throw new Error('Unsupported acceptance check.');
|
|
441
|
-
if (check.type
|
|
567
|
+
if (!['manual', 'semantic', 'file_exists', 'file_contains', 'file_equals'].includes(check.type)) throw new Error('Unsupported acceptance check.');
|
|
568
|
+
if (!['manual', 'semantic'].includes(check.type) && (!text(check.path, 500) || isAbsolute(check.path) || check.path.split(/[\\/]/).includes('..'))) throw new Error('Acceptance file must be relative to the task directory.');
|
|
442
569
|
if (check.type === 'file_contains' && !text(check.text, 1000)) throw new Error('file_contains needs expected text.');
|
|
443
570
|
if (check.type === 'file_equals' && exactText(check.text) === null) throw new Error('file_equals needs bounded exact UTF-8 text.');
|
|
444
|
-
return { id, description: text(item.description, 500), check: { type: check.type, ...(check.type === 'file_equals' ? { exactPath: check.path } : check.type
|
|
571
|
+
return { id, description: text(item.description, 500), check: { type: check.type, ...(check.type === 'file_equals' ? { exactPath: check.path } : ['manual', 'semantic'].includes(check.type) ? {} : { path: check.path }), ...(check.type === 'file_contains' ? { text: text(check.text, 1000) } : {}), ...(check.type === 'file_equals' ? { text: exactText(check.text) } : {}) } };
|
|
445
572
|
});
|
|
446
573
|
if (new Set(criteria.map(item => item.id)).size !== criteria.length) throw new Error('Duplicate acceptance criterion ID.');
|
|
447
574
|
const approvalPolicy = config.approvalPolicy || 'wait';
|
|
@@ -462,6 +589,7 @@ function managedSpec(spec, session, provider, observedProcess, admission, now) {
|
|
|
462
589
|
const appServerChannel = config.resumeChannel === 'codex_app_server' && admission?.source === 'codex_app_server' && admission.session === session;
|
|
463
590
|
const autoRecovery = config.autoRecovery === true && exactCodexSession && Boolean(resumeModel) &&
|
|
464
591
|
(appServerChannel || config.resumeChannel === 'codex_exec' && Boolean(observedProcess?.identity));
|
|
592
|
+
if (config.purpose && config.maxTokens !== undefined && !(provider === 'codex' && appServerChannel)) throw new Error('Provider token budgets require the host-observed Codex app-server channel.');
|
|
465
593
|
let deliveryLevel = null;
|
|
466
594
|
if (config.deliveryLevel !== undefined) {
|
|
467
595
|
const value = config.deliveryLevel;
|
|
@@ -470,10 +598,11 @@ function managedSpec(spec, session, provider, observedProcess, admission, now) {
|
|
|
470
598
|
value.numberingVersion !== 'assurance-ascending-v2' || !text(value.selectionRef, 200)) throw new Error('Delivery level needs the user selection reference, requested/effective levels 1-5, and assurance-ascending-v2 numbering.');
|
|
471
599
|
deliveryLevel = { requested: value.requested, effective: value.effective, numberingVersion: value.numberingVersion, selectionRef: text(value.selectionRef, 200) };
|
|
472
600
|
}
|
|
473
|
-
const managed = { taskId: randomUUID(), deliveryLevel, thread: { provider, id: session },
|
|
601
|
+
const managed = { ...(config.purpose ? { purpose: structuredClone(config.purpose) } : {}), taskId: randomUUID(), deliveryLevel, thread: { provider, id: session },
|
|
474
602
|
actionId: text(config.actionId, 200) || null, actionDueAt, acceptanceCriteria: criteria, verified: {}, remainingWork: text(config.remainingWork, 4000) || null,
|
|
475
603
|
initialAction, proposedAction: initialAction, approvalPolicy, approvalScopes: [...new Set(scopes)].slice(0, 20),
|
|
476
604
|
grants: Object.fromEntries(scopes.map(scope => [scope, { decision: approvalPolicy }])), maxNoProgress: positive(config.maxNoProgress, 3, 20),
|
|
605
|
+
...(config.purpose ? { maxTokens: config.maxTokens === undefined ? null : positive(config.maxTokens, 1, 1_000_000_000), tokensUsed: 0, usageAvailable: null } : {}),
|
|
477
606
|
maxNoProgressMinutes: positive(config.maxNoProgressMinutes, 30, 1440), lastVerifiedProgressAt: now,
|
|
478
607
|
noProgress: 0, verifiedRevision: 0, consumedVerifiedRevision: 0, notification: null, notificationKey: null,
|
|
479
608
|
continuationPolicy: continuationMode(config.continuationPolicy),
|
|
@@ -488,6 +617,11 @@ function managedSpec(spec, session, provider, observedProcess, admission, now) {
|
|
|
488
617
|
}
|
|
489
618
|
function freezeEnrollment(run, now) {
|
|
490
619
|
if (!run.managed || run.managed.enrollment) return;
|
|
620
|
+
if (run.managed.purpose) {
|
|
621
|
+
const ledger = evidenceLedger(run.managed);
|
|
622
|
+
ledger.purposeRevision ||= 1;
|
|
623
|
+
ledger.charter ||= { objective: run.objective, criteria: structuredClone(run.managed.acceptanceCriteria), approvalScopes: [...run.managed.approvalScopes], frozenAt: now };
|
|
624
|
+
}
|
|
491
625
|
run.managed.enrollment = { objective: run.objective, acceptanceDigest: digest(JSON.stringify(run.managed.acceptanceCriteria)),
|
|
492
626
|
deliveryLevel: structuredClone(run.managed.deliveryLevel || null), frozenAt: now };
|
|
493
627
|
}
|
|
@@ -496,7 +630,7 @@ export function pendingAcceptance(run) {
|
|
|
496
630
|
if (enrollment && (enrollment.objective !== run.objective || enrollment.acceptanceDigest !== digest(JSON.stringify(run.managed.acceptanceCriteria)) ||
|
|
497
631
|
JSON.stringify(enrollment.deliveryLevel) !== JSON.stringify(run.managed.deliveryLevel || null))) return [{ id: 'enrollment', description: 'Original objective, level and acceptance contract',
|
|
498
632
|
check: 'manual', detail: 'Enrollment changed unexpectedly; stop and inspect before enrolling a revised task' }];
|
|
499
|
-
return (run.managed
|
|
633
|
+
return effectiveAcceptanceCriteria(run.managed).filter(item => !Object.hasOwn(run.managed.verified, item.id))
|
|
500
634
|
.map(item => ({ id: item.id, description: item.description, check: item.check.type,
|
|
501
635
|
detail: run.managed.criterionObservations?.[item.id]?.observed_result || (item.check.type === 'manual' ? 'Explicit user verification required' : 'Independent check not yet passed') }));
|
|
502
636
|
}
|
|
@@ -508,7 +642,7 @@ export function deliveryAcceptanceContext(run) {
|
|
|
508
642
|
`${run.managed?.deliveryLevel ? ` Frozen delivery level: ${JSON.stringify(run.managed.deliveryLevel)}.` : ''}` +
|
|
509
643
|
` Pending acceptance (last recorded checks): ${JSON.stringify(pendingAcceptance(run))}. Continue only within the original request and permissions; manual checks require the user. SQM is optional and 0 applicable checks do not establish quality acceptance.`;
|
|
510
644
|
}
|
|
511
|
-
export async function startDelivery({ root = rootDirectory(), provider, session, cwd = process.cwd(), executionCwd, upgradeRunId, objective, nextStep, maxFailures, continuationPolicy, managed, admission,
|
|
645
|
+
export async function startDelivery({ root = rootDirectory(), provider, session, cwd = process.cwd(), executionCwd, upgradeRunId, objective, nextStep, maxMinutes, maxContinuations, maxFailures, continuationPolicy, managed, admission,
|
|
512
646
|
liveHookAdmission, autoContinueEpoch, providerLinkEpoch, inspectProcess = processIdentity, now = Date.now() }) {
|
|
513
647
|
const link = await providerLinkState(root, provider);
|
|
514
648
|
if (providerLinkEpoch !== undefined && providerLinkEpoch !== link.epoch) throw Object.assign(new Error("SIMY provider connection changed before enrollment."), { code: "provider_link_changed" });
|
|
@@ -551,7 +685,8 @@ export async function startDelivery({ root = rootDirectory(), provider, session,
|
|
|
551
685
|
const waiting = managedState?.decision?.outcome === 'wait';
|
|
552
686
|
const queued = managedState?.capability === 'codex_app_server';
|
|
553
687
|
const run = { id: randomUUID(), objective: text(objective), state: waiting ? 'blocked' : queued ? 'recovery_required' : 'active', nextStep: text(nextStep), startedAt: now, updatedAt: now,
|
|
554
|
-
|
|
688
|
+
...(managedState?.purpose ? { ...(maxMinutes === undefined ? {} : { deadline: now + positive(maxMinutes, 120, 1440) * 60_000 }), ...(maxContinuations === undefined ? {} : { maxContinuations: positive(maxContinuations, 50, 1000) }) } : {}),
|
|
689
|
+
maxFailures: positive(maxFailures, 3, 10), maxFailuresSource: maxFailures === undefined ? 'default' : 'explicit',
|
|
555
690
|
continuations: 0, failures: 0, revision: 0, consumedRevision: -1, checkpoint: null, reason: waiting ? managedState.decision.reason : queued ? 'Exact-session supervision is queued; SIMY will acquire the provider writer lock before dispatch.' : 'started',
|
|
556
691
|
...(queued ? { recoveryCause: 'session_supervision' } : {}), managed: managedState };
|
|
557
692
|
if (executionCwd || previous.run?.executionCwd) run.executionCwd = executionCwd || previous.run.executionCwd;
|
|
@@ -579,9 +714,94 @@ export async function startDelivery({ root = rootDirectory(), provider, session,
|
|
|
579
714
|
return { ...previous, run };
|
|
580
715
|
});
|
|
581
716
|
}
|
|
582
|
-
|
|
717
|
+
// Explicit UI opt-in upgrades the existing run without replacing its identity or budgets.
|
|
718
|
+
export async function enablePurposeDelivery({ root = rootDirectory(), provider, session, admission, purpose, expectedRunId, expectedRevision, now = Date.now() }) {
|
|
719
|
+
return updateRecord(root, provider, session, record => {
|
|
720
|
+
const run = record?.run;
|
|
721
|
+
if (!run || run.id !== expectedRunId || run.revision !== expectedRevision || provider !== 'codex' ||
|
|
722
|
+
admission?.source !== 'codex_app_server' || admission.session !== session || admission.cwd !== record.cwd || !admission.model ||
|
|
723
|
+
record.admission?.source !== admission.source || record.admission?.model !== admission.model) throw new Error('Exact session changed before automatic continuation.');
|
|
724
|
+
if (TERMINAL.has(run.state) || run.state === 'auto_resuming' || run.managed?.resumeLease || run.managed?.liveWorkspaceLease ||
|
|
725
|
+
['pending', 'transferred', 'pending_received'].includes(run.managed?.handoff?.status)) throw new Error('This task cannot change supervision while completed, executing, or handed off.');
|
|
726
|
+
if (managedBudgetReason(run, now)) throw new Error('Original budget exhausted; automatic continuation cannot reset it.');
|
|
727
|
+
if (!run.managed) {
|
|
728
|
+
const plan = purposeDiagnosisPlan({ approvalScopes: ['task'] }, 'Read the original conversation, reconstruct its purpose and completion conditions, then choose necessary work.');
|
|
729
|
+
run.managed = managedSpec({ acceptanceCriteria: [], contextBootstrap: true, autonomyPolicy: 'autonomous', approvalScopes: ['task'], approvalPolicy: 'wait',
|
|
730
|
+
initialPlan: plan, initialAction: plan.alternatives[0].action, autoRecovery: true, resumeChannel: 'codex_app_server', resumeModel: admission.model, purpose },
|
|
731
|
+
session, provider, record.process, admission, now);
|
|
732
|
+
run.managed.purpose.previousCheckpoint = run.checkpoint;
|
|
733
|
+
run.checkpoint = null;
|
|
734
|
+
} else {
|
|
735
|
+
if (run.managed.resumeChannel !== 'codex_app_server' || run.managed.resumeModel !== admission.model) throw new Error('Automatic continuation requires the original verified provider channel.');
|
|
736
|
+
run.managed.purpose = { ...run.managed.purpose, ...purpose };
|
|
737
|
+
run.managed.autonomyPolicy = 'autonomous';
|
|
738
|
+
if (run.managed.plan && !run.managed.pendingApproval) recordDecision(run.managed, run.managed.plan, now);
|
|
739
|
+
}
|
|
740
|
+
run.managed.purpose.enabled = true;
|
|
741
|
+
// Preserve pending approvals/questions. The supervisor may investigate them,
|
|
742
|
+
// but enabling this switch does not grant external effects or native rights.
|
|
743
|
+
if (!run.managed.pendingApproval && !run.managed.dependencyWait && run.managed.phase !== 'needs_clarification' && !run.managed.userQuestion) {
|
|
744
|
+
if (run.managed.executionUncertain) prepareRecoveryDiagnosis(run, now);
|
|
745
|
+
run.managed.autoRecovery = true; run.state = 'recovery_required'; run.recoveryCause = 'session_supervision';
|
|
746
|
+
run.reason = 'Purpose-driven automatic continuation is enabled.';
|
|
747
|
+
} else if (run.state === 'paused') { run.state = 'blocked'; }
|
|
748
|
+
run.updatedAt = now;
|
|
749
|
+
return record;
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Called by the owning supervisor, never by an LLM. Each unchanged obstacle gets
|
|
754
|
+
// one bounded diagnostic attempt. It is not permission to replay the old action.
|
|
755
|
+
export function queuePurposeDiagnosis(run, reason, key, now = Date.now()) {
|
|
756
|
+
if (!purposeEnabled(run) || run.managed.resumeChannel !== 'codex_app_server' ||
|
|
757
|
+
['paused', 'stopped', 'completed', 'budget_exhausted'].includes(run.state) || managedBudgetReason(run, now) ||
|
|
758
|
+
(!searchEnabled(run.managed) && run.managed.pendingApproval?.denied) || run.managed.providerWarning && !run.managed.nativeWait || run.managed.usageBlocked ||
|
|
759
|
+
['pending', 'transferred', 'pending_received'].includes(run.managed.handoff?.status)) return false;
|
|
760
|
+
const purpose = run.managed.purpose;
|
|
761
|
+
purpose.recoveries ||= [];
|
|
762
|
+
if (searchEnabled(run.managed)) {
|
|
763
|
+
// Back off the same obstacle; a newly observed obstacle or independently
|
|
764
|
+
// verified progress should not inherit an unrelated fifteen-minute delay.
|
|
765
|
+
const retryKey = digest(JSON.stringify([key, run.managed.verifiedRevision]));
|
|
766
|
+
if (purpose.searchRetryKey !== retryKey) {
|
|
767
|
+
purpose.searchRetryKey = retryKey;
|
|
768
|
+
purpose.searchRetries = 0;
|
|
769
|
+
purpose.nextSearchAt = 0;
|
|
770
|
+
}
|
|
771
|
+
if (now < (purpose.nextSearchAt || 0)) return false;
|
|
772
|
+
rememberSearchWait(run.managed, run.blocker || 'verification', {
|
|
773
|
+
action: run.managed.pendingApproval ? { kind: run.managed.pendingApproval.kind, scope: run.managed.pendingApproval.scope, summary: run.managed.pendingApproval.summary } : null,
|
|
774
|
+
reason, question: run.managed.userQuestion, condition: run.managed.dependencyWait?.condition,
|
|
775
|
+
owner: run.managed.dependencyWait?.owner,
|
|
776
|
+
}, now);
|
|
777
|
+
purpose.nextSearchAt = now + Math.min(15 * 60_000, 15_000 * 2 ** Math.min(6, purpose.searchRetries || 0));
|
|
778
|
+
purpose.searchRetries = (purpose.searchRetries || 0) + 1;
|
|
779
|
+
} else {
|
|
780
|
+
if (purpose.recoveries.includes(key)) return false;
|
|
781
|
+
if (purpose.recoveries.length >= 20) return false;
|
|
782
|
+
}
|
|
783
|
+
purpose.recoveries.push(key);
|
|
784
|
+
purpose.recoveries = purpose.recoveries.slice(-20);
|
|
785
|
+
if (run.managed.nativeWait) purpose.nativeBoundary ||= { ...run.managed.nativeWait, provider: run.managed.thread?.provider, state: 'pending', writer: 'unknown' };
|
|
786
|
+
purpose.lastRecovery = { reason: contextText(reason, 1000), at: now, previousBlocker: run.blocker || null,
|
|
787
|
+
pendingApproval: run.managed.pendingApproval ? { ...run.managed.pendingApproval } : null,
|
|
788
|
+
question: run.managed.understandingQuestion?.text || run.managed.userQuestion || null,
|
|
789
|
+
dependency: run.managed.dependencyWait || null };
|
|
790
|
+
recordDecision(run.managed, purposeDiagnosisPlan(run.managed, reason), now);
|
|
791
|
+
if (run.managed.executionUncertain) prepareRecoveryDiagnosis(run, now);
|
|
792
|
+
if (run.managed.phase === 'needs_clarification') run.managed.phase = 'understanding';
|
|
793
|
+
run.managed.userQuestion = null; run.managed.understandingQuestion = null; run.managed.dependencyWait = null;
|
|
794
|
+
run.managed.autoRecovery = true; run.managed.nextAttemptAt = now;
|
|
795
|
+
run.blocker = null; run.blockerSource = null;
|
|
796
|
+
run.checkpoint = null; run.consumedRevision = run.revision;
|
|
797
|
+
run.state = 'recovery_required'; run.recoveryCause = 'session_supervision';
|
|
798
|
+
run.nextStep = run.managed.decision.action.summary; run.reason = reason; run.updatedAt = now;
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export async function checkpointDelivery({ root = rootDirectory(), provider, session, cwd = process.cwd(), checkpoint, expectedLease, toolEvidence = [], sourceContext, controllerEvidence, now = Date.now() }) {
|
|
583
803
|
if (checkpoint?.kind === 'action_association') return checkpointActionAssociation({ root, provider, session, cwd, checkpoint, expectedLease, now });
|
|
584
|
-
if (checkpoint?.kind === 'session_understanding') return checkpointUnderstanding({ root, provider, session, cwd, checkpoint, expectedLease, now });
|
|
804
|
+
if (checkpoint?.kind === 'session_understanding') return checkpointUnderstanding({ root, provider, session, cwd, checkpoint, expectedLease, sourceContext, controllerEvidence, now });
|
|
585
805
|
try { cwd = await realpath(cwd); } catch { throw new Error('No matching active Delivery Loop.'); }
|
|
586
806
|
const statuses = ['in_progress', 'completed', 'blocked', 'error'];
|
|
587
807
|
if (!checkpoint || !statuses.includes(checkpoint.status)) throw new Error('Checkpoint status must be in_progress, completed, blocked, or error.');
|
|
@@ -601,6 +821,8 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
601
821
|
if (TERMINAL.has(run.state) || run.state === 'blocked' || run.state === 'recovery_required') throw new Error('Loop is stopped or awaiting a human resume.');
|
|
602
822
|
if (['paused', 'verification_pending'].includes(run.state)) throw new Error('Managed task is paused or awaiting acceptance verification.');
|
|
603
823
|
if (run.managed) {
|
|
824
|
+
if (searchEnabled(run.managed) && expectedLease && sourceContext) run.managed.purpose.originalContext ||= sourceContext;
|
|
825
|
+
if (searchEnabled(run.managed) && expectedLease) retainControllerEvidence(run.managed, controllerEvidence, now);
|
|
604
826
|
const performed = run.managed.autonomyPolicy ? structuredClone(run.managed.decision) : null;
|
|
605
827
|
let actionResult = null;
|
|
606
828
|
if (performed) {
|
|
@@ -609,11 +831,29 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
609
831
|
actionResult = { decisionId: performed.id, status: result.status, summary: text(result.summary, 300), evidence: Array.isArray(result.evidence) ? result.evidence.map(item => text(item, 300)).filter(Boolean).slice(0, 5) : [], source: 'provider_reported' };
|
|
610
832
|
if (result.status === 'observed' && !actionResult.evidence.length) throw new Error('Observed action result needs concrete evidence.');
|
|
611
833
|
}
|
|
834
|
+
if (purposeEnabled(run)) {
|
|
835
|
+
const workerPlan = checkpoint.plan ? assessedPlan(checkpoint.plan) : null;
|
|
836
|
+
const review = validatePurposeReview(checkpoint.purposeReview, workerPlan, run.managed.purpose.ruleIds, searchEnabled(run.managed) ? run.managed.purpose.lastReview?.rules || [] : [], run.managed);
|
|
837
|
+
recordPurposeReview(run.managed, review, now);
|
|
838
|
+
if (!searchEnabled(run.managed) && checkpoint.status === 'completed' && review.rules.some(rule => ['failed', 'unverified'].includes(rule.status))) {
|
|
839
|
+
effectiveStatus = 'in_progress';
|
|
840
|
+
repairPlan = purposeDiagnosisPlan(run.managed, 'Completion is unproven: resolve failed/unverified workflow rules against the original purpose.');
|
|
841
|
+
nextStep = repairPlan.alternatives[0].action.summary;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
612
844
|
const previouslyVerified = Object.keys(run.managed.verified).length;
|
|
613
845
|
run.managed.remainingWork = text(checkpoint.remainingWork, 4000) || run.managed.remainingWork;
|
|
614
846
|
await verifyAutomaticCriteria(record, now);
|
|
847
|
+
recordPurposeProgress(run, now);
|
|
848
|
+
if (searchEnabled(run.managed) && performed && actionResult) {
|
|
849
|
+
const unit = evidenceLedger(run.managed).units?.find(row => row.id === performed.id);
|
|
850
|
+
if (unit) { unit.status = actionResult.status === 'failed' ? 'failed' : actionResult.status === 'not_run' ? 'waiting' : 'observed'; unit.evidenceRefs = (toolEvidence || []).map(row => row.ref || row.digest); unit.updatedAt = now;
|
|
851
|
+
if (unit.status === 'observed' && unit.criterionIds.length && unit.criterionIds.every(id=>run.managed.verified[id] && !['semantic','manual'].includes(run.managed.acceptanceCriteria.find(row=>row.id===id)?.check.type))) {unit.status='verified';unit.verificationSource='controller_automatic_criteria';}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (searchEnabled(run.managed) && performed && actionResult) recordSearchExperiment(run, performed, actionResult, run.managed.purpose.lastReview, expectedLease ? toolEvidence : [], now);
|
|
615
855
|
if (run.managed.autonomyPolicy && checkpoint.status === 'completed') {
|
|
616
|
-
const missing = run.managed.acceptanceCriteria.filter(item => item.check.type
|
|
856
|
+
const missing = run.managed.acceptanceCriteria.filter(item => !['manual', 'semantic'].includes(item.check.type) && !Object.hasOwn(run.managed.verified, item.id));
|
|
617
857
|
if (missing.length) {
|
|
618
858
|
effectiveStatus = 'in_progress';
|
|
619
859
|
repairPlan = checkpoint.plan && nextStep ? assessedPlan(checkpoint.plan) : missingAcceptanceDiagnosis(run.managed, missing);
|
|
@@ -633,7 +873,9 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
633
873
|
const selected = plan.alternatives.find(item => item.id === plan.selectedOptionId);
|
|
634
874
|
if (checkpoint.proposedAction && JSON.stringify(actionDescriptor(checkpoint.proposedAction)) !== JSON.stringify(selected.action)) throw new Error('Proposed action differs from selected assessed option.');
|
|
635
875
|
recordDecision(run.managed, plan, now);
|
|
636
|
-
if (
|
|
876
|
+
if (purposeEnabled(run) && !searchEnabled(run.managed) && ['failed', 'not_run'].includes(actionResult.status)) {
|
|
877
|
+
recordDecision(run.managed, purposeDiagnosisPlan(run.managed, `Investigate the ${actionResult.status} action before further changes: ${actionResult.summary}`), now);
|
|
878
|
+
} else if (!searchEnabled(run.managed) && (actionResult.status === 'not_run' || actionResult.status === 'failed' && run.managed.decision.action.kind !== 'read')) {
|
|
637
879
|
run.managed.decision.outcome = 'wait'; run.managed.decision.reason = actionResult.status === 'not_run' ? 'Previous selected action was not run.' : 'Failed action requires a read-only diagnosis before another change.';
|
|
638
880
|
run.managed.pendingApproval = { id: run.managed.decision.id, ...run.managed.decision.action, reason: run.managed.decision.reason };
|
|
639
881
|
}
|
|
@@ -648,8 +890,12 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
648
890
|
const plan = assessedPlan(checkpoint.plan);
|
|
649
891
|
if (JSON.stringify(plan.alternatives.find(item => item.id === plan.selectedOptionId).action) !== JSON.stringify(proposed)) throw new Error('Authorization request must match the selected assessed option.');
|
|
650
892
|
const decision = recordDecision(run.managed, plan, now);
|
|
651
|
-
|
|
652
|
-
|
|
893
|
+
if (purposeEnabled(run) && run.managed.autonomyPolicy === 'autonomous' && decision.outcome !== 'wait') {
|
|
894
|
+
effectiveStatus = 'in_progress';
|
|
895
|
+
} else {
|
|
896
|
+
decision.outcome = 'wait'; decision.reason = 'Provider authorization requires an exact user decision.';
|
|
897
|
+
run.managed.pendingApproval = { id: decision.id, ...proposed, reason: decision.reason };
|
|
898
|
+
}
|
|
653
899
|
} else {
|
|
654
900
|
run.managed.proposedAction = proposed;
|
|
655
901
|
run.managed.pendingApproval = { id: digest(`${run.managed.taskId}:${run.revision + 1}:${JSON.stringify(proposed)}`), ...proposed };
|
|
@@ -665,10 +911,22 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
665
911
|
if (!text(wait?.condition, 1000) || !text(wait?.owner, 200) || !text(wait?.nextCheck, 1000) || !nextStep) throw new Error('External dependency needs waitCondition {condition, owner, nextCheck} and a nextStep.');
|
|
666
912
|
run.managed.dependencyWait = { id: randomUUID(), condition: text(wait.condition, 1000), owner: text(wait.owner, 200), nextCheck: text(wait.nextCheck, 1000), createdAt: now };
|
|
667
913
|
}
|
|
914
|
+
if (searchEnabled(run.managed) && checkpoint.status === 'blocked') {
|
|
915
|
+
rememberSearchWait(run.managed, checkpoint.blocker, { reason: checkpoint.summary, question: checkpoint.question,
|
|
916
|
+
condition: checkpoint.waitCondition?.condition, owner: checkpoint.waitCondition?.owner, action: checkpoint.approval || null }, now);
|
|
917
|
+
if (checkpoint.plan && checkpoint.blocker !== 'authorization' && checkpoint.plan.alternatives?.find(o => o.id === checkpoint.plan.selectedOptionId)?.independent === true) recordDecision(run.managed, assessedPlan(checkpoint.plan), now);
|
|
918
|
+
if (checkpoint.plan && run.managed.decision?.outcome !== 'wait' && (checkpoint.blocker === 'authorization' || run.managed.plan.alternatives.find(o => o.id === run.managed.plan.selectedOptionId)?.independent)) {
|
|
919
|
+
effectiveStatus = 'in_progress'; nextStep = run.managed.decision.action.summary;
|
|
920
|
+
run.managed.userQuestion = null; run.managed.dependencyWait = null;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
run.blockerSource = effectiveStatus === 'blocked' ? 'checkpoint' : null;
|
|
668
924
|
run.checkpoint = { status: effectiveStatus, summary: text(checkpoint.summary), nextStep, blocker: checkpoint.blocker, evidence, acceptanceMet: checkpoint.acceptanceMet === true };
|
|
669
925
|
run.revision += 1; run.updatedAt = now;
|
|
670
926
|
recordManagedOutcomeProgress(run, now);
|
|
671
|
-
if (run.managed && effectiveStatus === 'completed') {
|
|
927
|
+
if (searchEnabled(run.managed) && effectiveStatus === 'completed') {
|
|
928
|
+
queuePurposeVerification(run, checkpoint.summary, now);
|
|
929
|
+
} else if (run.managed && effectiveStatus === 'completed') {
|
|
672
930
|
const pending = pendingAcceptance(run);
|
|
673
931
|
if (pending.length) { run.state = 'verification_pending'; run.reason = acceptancePendingReason(run); }
|
|
674
932
|
else if (run.managed.contextBootstrap && !run.managed.acceptanceCriteria.length) {
|
|
@@ -684,11 +942,121 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
684
942
|
delete run.managed.executionUncertain;
|
|
685
943
|
delete run.managed.recoveryDiagnosisDecisionId;
|
|
686
944
|
}
|
|
945
|
+
for (const request of run.managed?.consistencyOutbox || []) if (request.decisionId && request.decisionId === checkpoint.actionResult?.decisionId) {
|
|
946
|
+
request.stage = checkpoint.actionResult.status === 'observed' ? 'inspected' : 'inspection_failed';
|
|
947
|
+
request.observedAt = now; request.result = checkpoint.actionResult.status;
|
|
948
|
+
}
|
|
687
949
|
notifyState(run, now);
|
|
688
950
|
return record;
|
|
689
951
|
});
|
|
690
952
|
return settled;
|
|
691
953
|
}
|
|
954
|
+
export function queuePurposeVerification(run, claim, now) {
|
|
955
|
+
const managed = run.managed, state = searchState(managed);
|
|
956
|
+
recordDecision(managed, purposeDiagnosisPlan(managed, 'Independently inspect the requested outcome and return specific findings.'), now);
|
|
957
|
+
state.verificationDecisionId = managed.decision.id;
|
|
958
|
+
state.completionClaim = text(claim, 1000);
|
|
959
|
+
state.phase = 'verifying';
|
|
960
|
+
run.checkpoint = null; run.state = 'recovery_required'; run.recoveryCause = 'session_supervision';
|
|
961
|
+
run.nextStep = 'Independently verify the requested outcome.';
|
|
962
|
+
run.reason = run.nextStep; managed.autoRecovery = true;
|
|
963
|
+
}
|
|
964
|
+
function externalReviewIsCurrent(managed,review,now) {
|
|
965
|
+
const refs=[...(review.criteria||[]).filter(row=>row.passed),...(review.rules||[]).filter(row=>row.status==='satisfied'),...(review.workUnits||[])].flatMap(row=>row.evidenceRefs||[]).filter(link=>/^[a-f0-9]{64}$/.test(link.ref));
|
|
966
|
+
const ledger=managed.purpose.evidence;
|
|
967
|
+
const observations=[...(ledger?.observations||[]),...(ledger?.reviewObservationActionId===(review.decisionId||managed.decision.id) ? ledger.reviewObservations||[] : [])];
|
|
968
|
+
return refs.every(link=>observations.some(item=>item.id===link.ref && item.status==='observed' && currentPurposeObservation(item,now,review.observationWindow)));
|
|
969
|
+
}
|
|
970
|
+
export async function settlePurposeVerification({ root, provider, session, expectedLease, review, verifySnapshot, now = Date.now() }) {
|
|
971
|
+
const started = Date.now(), initialNow = now;
|
|
972
|
+
const snapshot = (await listRecords(root)).find(record => record.provider === provider && record.session === session);
|
|
973
|
+
if (!snapshot || snapshot.run?.state !== 'auto_resuming' || snapshot.run.managed?.resumeLease?.id !== expectedLease) throw new Error('Independent review lease changed.');
|
|
974
|
+
// Keep potentially large IO outside the state lock so pause/stop can persist.
|
|
975
|
+
const currentDigest = await (verifySnapshot ? verifySnapshot(snapshot) : workspaceIdentity(taskDirectory(snapshot), root, undefined, { excludedPaths: review?.manifest?.source === 'controller_certified_independent_proposal' ? review.manifest.exclusions.map(row => row.path) : [] }));
|
|
976
|
+
return updateRecord(root, provider, session, async record => {
|
|
977
|
+
now = initialNow + Math.max(0, Date.now() - started);
|
|
978
|
+
const run = record?.run, managed = run?.managed;
|
|
979
|
+
if (!searchEnabled(managed) || !expectedLease || run.state !== 'auto_resuming' || managed.resumeLease?.id !== expectedLease ||
|
|
980
|
+
managed.purpose.search?.verificationDecisionId !== managed.decision.id || run.revision !== managed.resumeLease.checkpointRevision || now >= run.deadline) throw new Error('Independent review lease changed.');
|
|
981
|
+
const state = searchState(managed);
|
|
982
|
+
if (review?.source !== 'independent_codex_session' || !review.session || review.session === session || !review.workspaceDigest || typeof review.passed !== 'boolean') throw new Error('Independent review provenance is missing.');
|
|
983
|
+
retainApplicableRules(managed, review.rules || []);
|
|
984
|
+
review = assessRuleReview(review, managed);
|
|
985
|
+
if (currentDigest !== review.workspaceDigest) {
|
|
986
|
+
review = { ...review, passed: false, needsHuman: false, findings: [...review.findings, 'Workspace changed before review settlement; verify the new revision.'] };
|
|
987
|
+
}
|
|
988
|
+
const nativeResultReview=managed.purpose.nativeBoundary?.state==='writer_completed_requires_verification' && managed.purpose.nativeBoundary.completedTurnId===managed.purpose.nativeBoundary.turnId;
|
|
989
|
+
if ((managed.purpose.nativeBoundary && !nativeResultReview) || managed.executionUncertain || pendingAcceptance(run).some(row => row.id === 'enrollment')) {
|
|
990
|
+
review = { ...review, passed: false, needsHuman: false, findings: [...review.findings,
|
|
991
|
+
'Required operation, effect reconciliation or original-purpose contract remains unresolved. Preserve the waiting unit and continue independent authorized work.'] };
|
|
992
|
+
}
|
|
993
|
+
if(!externalReviewIsCurrent(managed,review,now)) review={...review,passed:false,needsHuman:false,findings:[...review.findings,'Controller external evidence expired before settlement; reacquire the same authorized observations and independently re-evaluate without replaying writes.']};
|
|
994
|
+
const openFindings = reconcileFindings(managed, review, now);
|
|
995
|
+
const units=evidenceLedger(managed).units;
|
|
996
|
+
for (const result of (review.passed || review.needsHuman) ? review.workUnits || [] : []) {
|
|
997
|
+
const unit=units.find(row=>row.id===result.id);
|
|
998
|
+
if (!unit || !['verified','superseded'].includes(result.status) || !result.evidenceRefs?.length ||
|
|
999
|
+
unit.criterionIds.some(id=>!review.criteria.some(row=>row.id===id && row.passed))) throw new Error('Required unit resolution needs independent criterion-linked evidence.');
|
|
1000
|
+
if (unit.purposeRevision !== evidenceLedger(managed).purposeRevision) throw new Error('Unit belongs to an old purpose revision.');
|
|
1001
|
+
unit.status=result.status;unit.evidenceRefs=result.evidenceRefs;unit.verifiedAt=now;
|
|
1002
|
+
}
|
|
1003
|
+
state.review = review; delete state.verificationDecisionId;
|
|
1004
|
+
run.revision += 1; run.updatedAt = now;
|
|
1005
|
+
await verifyAutomaticCriteria(record, now);
|
|
1006
|
+
if (review.passed || review.needsHuman) {
|
|
1007
|
+
for (const criterion of effectiveAcceptanceCriteria(managed)) {
|
|
1008
|
+
if (criterion.check.type === 'semantic' && review.criteria.some(row => row.id === criterion.id && row.passed)) {
|
|
1009
|
+
managed.verified[criterion.id] = { source: 'independent_codex_session', checkedAt: now, workspaceDigest: review.workspaceDigest, session: review.session };
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
for (const unit of units) if (unit.id.startsWith('criterion:') && unit.criterionIds.every(id=>managed.verified[id])) {unit.status='verified';unit.evidenceRefs=review.criteria.filter(row=>unit.criterionIds.includes(row.id)).flatMap(row=>row.evidenceRefs || []);}
|
|
1013
|
+
const requiredPending=units.filter(row=>row.required && row.purposeRevision === evidenceLedger(managed).purposeRevision && !['verified','superseded'].includes(row.status));
|
|
1014
|
+
const pending = effectiveAcceptanceCriteria(managed).filter(c => !Object.hasOwn(managed.verified, c.id));
|
|
1015
|
+
if (requiredPending.length && (!pending.every(c=>c.check.type==='manual') || !pending.length)) review={...review,passed:false,needsHuman:false,findings:[...review.findings,'Required work units still waiting: '+requiredPending.map(row=>row.id).join(', ')]};
|
|
1016
|
+
if((review.passed || review.needsHuman) && nativeResultReview){delete managed.purpose.nativeBoundary;managed.nativeWait=null;}
|
|
1017
|
+
if (review.passed && !pending.length && !managed.purpose.nativeBoundary && !managed.executionUncertain && pendingAcceptance(run).length === 0) {
|
|
1018
|
+
for (const unit of evidenceLedger(managed).units || []) if (unit.status === 'observed' && unit.criterionIds.every(id => managed.verified[id])) unit.status = 'verified';
|
|
1019
|
+
run.state = 'completed'; run.reason = review.summary; state.phase = 'completed';
|
|
1020
|
+
run.checkpoint = { status: 'completed', summary: review.summary, evidence: review.criteria.map(c => c.evidence) };
|
|
1021
|
+
managed.autoRecovery = false;
|
|
1022
|
+
for (const request of managed.consistencyOutbox || []) {
|
|
1023
|
+
request.stage = 'resolved'; request.verifiedAt = now; request.reviewerSession = review.session;
|
|
1024
|
+
request.workspaceDigest = review.workspaceDigest;
|
|
1025
|
+
}
|
|
1026
|
+
} else if (pending.length && pending.every(c => c.check.type === 'manual')) {
|
|
1027
|
+
run.state = 'verification_pending'; run.reason = 'Only explicitly human acceptance remains.'; state.phase = 'waiting';
|
|
1028
|
+
run.checkpoint = { status: 'completed', summary: review.summary }; managed.autoRecovery = false;
|
|
1029
|
+
} else review = { ...review, passed: false, needsHuman: false, findings: ['Local acceptance changed during review; investigate and recheck.'] };
|
|
1030
|
+
}
|
|
1031
|
+
state.review={...review,decisionId:managed.decision.id};
|
|
1032
|
+
if (!review.passed && !review.needsHuman && openFindings.some(item => item.observation)) {
|
|
1033
|
+
const ledger = evidenceLedger(managed);
|
|
1034
|
+
const lookup = digest(JSON.stringify(openFindings.filter(item => item.observation).map(item => [item.id, item.observation])));
|
|
1035
|
+
if (state.observerLookup !== lookup) {
|
|
1036
|
+
state.observerLookup = lookup;
|
|
1037
|
+
queuePurposeVerification(run, 'Acquire the requested scoped external observations and independently re-evaluate the outcome.', now);
|
|
1038
|
+
notifyState(run, now); return record;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (!review.passed && !review.needsHuman && openFindings.length && openFindings.every(item => item.owner === 'controller')) {
|
|
1042
|
+
const ledger = evidenceLedger(managed);
|
|
1043
|
+
const lookup = createHash('sha256').update(JSON.stringify({ findings: openFindings.map(item => item.id).sort(), receipts: ledger.receipts.map(item => item.id), baseline: ledger.baseline?.digest, workspace: review.workspaceDigest })).digest('hex');
|
|
1044
|
+
if (state.controllerLookup !== lookup && (ledger.baseline || ledger.receipts.length)) {
|
|
1045
|
+
state.controllerLookup = lookup;
|
|
1046
|
+
queuePurposeVerification(run, 'Controller retrieved existing baseline and tool receipts; reconcile the missing evidence against the supplied ledger before requesting worker repairs.', now);
|
|
1047
|
+
notifyState(run, now); return record;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
if (!review.passed && !review.needsHuman) {
|
|
1051
|
+
const reason = `Outcome review found remaining work: ${[review.summary, ...openFindings.map(findingText)].join(' ').slice(0, 1800)}`;
|
|
1052
|
+
recordDecision(managed, purposeDiagnosisPlan(managed, reason), now);
|
|
1053
|
+
state.phase = 'repairing'; managed.purpose.lastRecovery = { reason, at: now };
|
|
1054
|
+
run.checkpoint = null; run.state = 'recovery_required'; run.recoveryCause = 'session_supervision';
|
|
1055
|
+
run.nextStep = managed.decision.action.summary; run.reason = reason; managed.autoRecovery = true;
|
|
1056
|
+
}
|
|
1057
|
+
notifyState(run, now); return record;
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
692
1060
|
export async function checkpointActionAssociation({ root, provider, session, cwd, checkpoint, expectedLease, now = Date.now() }) {
|
|
693
1061
|
cwd = await realpath(cwd);
|
|
694
1062
|
return updateRecord(root, provider, session, record => {
|
|
@@ -711,12 +1079,12 @@ export async function checkpointActionAssociation({ root, provider, session, cwd
|
|
|
711
1079
|
run.checkpoint = job.saved.checkpoint || null;
|
|
712
1080
|
run.revision += 1; run.updatedAt = now; managed.associationCheckpointRevision = run.revision;
|
|
713
1081
|
association.previousCheckpoint = job.saved.checkpoint || null;
|
|
714
|
-
if (expectedLease) run.continuations += 1;
|
|
1082
|
+
if (expectedLease && !searchEnabled(run.managed)) run.continuations += 1;
|
|
715
1083
|
run.reason = 'Action matching finished; original selected work remains unchanged.';
|
|
716
1084
|
return record;
|
|
717
1085
|
});
|
|
718
1086
|
}
|
|
719
|
-
async function checkpointUnderstanding({ root, provider, session, cwd, checkpoint, expectedLease, now }) {
|
|
1087
|
+
async function checkpointUnderstanding({ root, provider, session, cwd, checkpoint, expectedLease, sourceContext, controllerEvidence, now }) {
|
|
720
1088
|
try { cwd = await realpath(cwd); } catch { throw new Error('No matching active Delivery Loop.'); }
|
|
721
1089
|
if (!text(checkpoint.summary, 1000)) throw new Error('Understanding checkpoint needs a summary.');
|
|
722
1090
|
return updateRecord(root, provider, session, async record => {
|
|
@@ -732,6 +1100,10 @@ async function checkpointUnderstanding({ root, provider, session, cwd, checkpoin
|
|
|
732
1100
|
if (['clear', 'already_done'].includes(understanding.state) && result.status !== 'observed') throw new Error('Clear understanding requires an observed read result.');
|
|
733
1101
|
const plan = understanding.state === 'clear' ? assessedPlan(checkpoint.plan) : null;
|
|
734
1102
|
if (plan && JSON.stringify(plan.alternatives.find(item => item.id === plan.selectedOptionId).action) !== JSON.stringify(understanding.nextAction)) throw new Error('Understood next action differs from selected assessed option.');
|
|
1103
|
+
if (purposeEnabled(run)) recordPurposeReview(run.managed,
|
|
1104
|
+
validatePurposeReview(checkpoint.purposeReview, plan, run.managed.purpose.ruleIds, searchEnabled(run.managed) ? run.managed.purpose.lastReview?.rules || [] : [], run.managed), now);
|
|
1105
|
+
if (searchEnabled(run.managed) && expectedLease && sourceContext) run.managed.purpose.originalContext ||= sourceContext;
|
|
1106
|
+
if (searchEnabled(run.managed) && expectedLease) retainControllerEvidence(run.managed, controllerEvidence, now);
|
|
735
1107
|
run.managed.sessionUnderstanding = understanding;
|
|
736
1108
|
run.managed.understandingRevision = (run.managed.understandingRevision || 0) + 1;
|
|
737
1109
|
run.managed.understandingAnswer = null;
|
|
@@ -742,9 +1114,9 @@ async function checkpointUnderstanding({ root, provider, session, cwd, checkpoin
|
|
|
742
1114
|
run.managed.workHistory = run.managed.workHistory.slice(-10);
|
|
743
1115
|
run.checkpoint = { kind: 'session_understanding', status: 'in_progress', summary: contextText(checkpoint.summary, 1000), evidence: [], acceptanceMet: false };
|
|
744
1116
|
run.revision += 1; run.updatedAt = now;
|
|
745
|
-
if (expectedLease) run.continuations += 1;
|
|
1117
|
+
if (expectedLease && !searchEnabled(run.managed)) run.continuations += 1;
|
|
746
1118
|
if (result.status !== 'observed') run.failures += 1;
|
|
747
|
-
if (run
|
|
1119
|
+
if (failureLimitReached(run)) {
|
|
748
1120
|
run.state = 'failed'; run.reason = 'Repeated session understanding failures; inspect the original conversation.';
|
|
749
1121
|
notifyState(run, now); return record;
|
|
750
1122
|
}
|
|
@@ -769,6 +1141,12 @@ async function checkpointUnderstanding({ root, provider, session, cwd, checkpoin
|
|
|
769
1141
|
run.managed.phase = 'active';
|
|
770
1142
|
run.state = 'reported_done';
|
|
771
1143
|
run.reason = 'Original conversation reports no remaining work; independent completion was not verified.';
|
|
1144
|
+
if (purposeEnabled(run)) {
|
|
1145
|
+
run.managed.acceptanceCriteria = understanding.criteria.filter(item => item.source === 'user_grounded').map(item => ({ id: item.id, description: item.description,
|
|
1146
|
+
check: item.check.type === 'file_equals' ? { type: 'file_equals', exactPath: item.check.path, text: item.check.text } : item.check }));
|
|
1147
|
+
if (searchEnabled(run.managed)) queuePurposeVerification(run, 'Original conversation reports completion.', now);
|
|
1148
|
+
else queuePurposeDiagnosis(run, 'The conversation reports completion. Check the original purpose against actual artifacts and outcomes before accepting that claim.', 'reported_completion', now);
|
|
1149
|
+
}
|
|
772
1150
|
} else {
|
|
773
1151
|
run.managed.phase = 'needs_clarification';
|
|
774
1152
|
run.managed.understandingQuestion = { id: randomUUID(), text: understanding.question, revision: run.managed.understandingRevision };
|
|
@@ -806,7 +1184,11 @@ export async function verifyAutomaticCriteria(record, now) {
|
|
|
806
1184
|
};
|
|
807
1185
|
const failed = (criterion, detail) => { delete managed.verified[criterion.id]; observe(criterion, false, detail); };
|
|
808
1186
|
for (const criterion of managed.acceptanceCriteria) {
|
|
809
|
-
if (criterion.check.type === 'manual')
|
|
1187
|
+
if (criterion.check.type === 'manual') {
|
|
1188
|
+
if (managed.verified[criterion.id]?.source !== 'user') delete managed.verified[criterion.id];
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
if (criterion.check.type === 'semantic') continue;
|
|
810
1192
|
const path = criterion.check.type === 'file_equals' ? criterion.check.exactPath || criterion.check.path : criterion.check.path;
|
|
811
1193
|
if (!path) { failed(criterion, 'Frozen acceptance file path is absent'); continue; }
|
|
812
1194
|
const directory = taskDirectory(record);
|
|
@@ -828,7 +1210,17 @@ export async function verifyAutomaticCriteria(record, now) {
|
|
|
828
1210
|
} catch { failed(criterion, 'Acceptance file could not be independently read'); }
|
|
829
1211
|
}
|
|
830
1212
|
}
|
|
1213
|
+
function purposeCompletionBarrier(run) {
|
|
1214
|
+
const managed=run.managed;
|
|
1215
|
+
if(!searchEnabled(managed)) return null;
|
|
1216
|
+
if(managed.purpose.nativeBoundary || managed.executionUncertain) return 'Required original operation or effect reconciliation is still unresolved.';
|
|
1217
|
+
const ledger=evidenceLedger(managed);
|
|
1218
|
+
for(const unit of ledger.units) if(unit.purposeRevision===ledger.purposeRevision && unit.id.startsWith('criterion:') && unit.criterionIds.every(id=>managed.verified[id])) unit.status='verified';
|
|
1219
|
+
const pending=ledger.units.filter(unit=>unit.required && unit.purposeRevision===ledger.purposeRevision && !['verified','superseded'].includes(unit.status));
|
|
1220
|
+
return pending.length ? 'Required work units still waiting: '+pending.map(unit=>unit.id).join(', ') : null;
|
|
1221
|
+
}
|
|
831
1222
|
export async function verifyDelivery({ root = rootDirectory(), provider, session, taskId, criterionId, now = Date.now() }) {
|
|
1223
|
+
const started=Date.now(), initialNow=now;
|
|
832
1224
|
return updateRecord(root, provider, session, async record => {
|
|
833
1225
|
const run = record?.run;
|
|
834
1226
|
if (!run?.managed || run.state !== 'verification_pending') throw new Error('Managed task is not awaiting acceptance verification.');
|
|
@@ -837,10 +1229,22 @@ export async function verifyDelivery({ root = rootDirectory(), provider, session
|
|
|
837
1229
|
if (!criterion || criterion.check.type !== 'manual') throw new Error('Only manual criteria can be verified by the user.');
|
|
838
1230
|
newVerifiedCriterion(run, criterionId, now);
|
|
839
1231
|
run.managed.verified[criterionId] = { source: 'user', checkedAt: now };
|
|
1232
|
+
recordPurposeProgress(run, now, false);
|
|
1233
|
+
if (searchEnabled(run.managed)) {
|
|
1234
|
+
const review = run.managed.purpose.search?.review;
|
|
1235
|
+
const currentDigest = await workspaceIdentity(taskDirectory(record), root, undefined, { excludedPaths: review?.manifest?.source === 'controller_certified_independent_proposal' ? review.manifest.exclusions.map(row => row.path) : [] }).catch(() => null);
|
|
1236
|
+
now=initialNow+Math.max(0,Date.now()-started);
|
|
1237
|
+
if (review?.source !== 'independent_codex_session' || !review.session || review.session===session || (!review.passed && !review.needsHuman) || !review.workspaceDigest || currentDigest !== review.workspaceDigest || !externalReviewIsCurrent(run.managed,review,now)) {
|
|
1238
|
+
for (const c of run.managed.acceptanceCriteria) if (c.check.type === 'semantic') delete run.managed.verified[c.id];
|
|
1239
|
+
queuePurposeVerification(run, 'Human decision recorded; recheck the changed workspace before completing.', now);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
840
1242
|
if (run.state === 'verification_pending' && run.checkpoint?.status === 'completed') {
|
|
841
1243
|
await verifyAutomaticCriteria(record, now);
|
|
842
1244
|
if (!pendingAcceptance(run).length) {
|
|
843
|
-
|
|
1245
|
+
const barrier=purposeCompletionBarrier(run);
|
|
1246
|
+
if(barrier) queuePurposeVerification(run, 'Human decision recorded; '+barrier+' Independently reconcile the required operation before completing.', now);
|
|
1247
|
+
else {run.state = 'completed'; run.reason = run.checkpoint.summary;}
|
|
844
1248
|
}
|
|
845
1249
|
}
|
|
846
1250
|
notifyState(run, now);
|
|
@@ -884,13 +1288,14 @@ export function prepareRecoveryDiagnosis(run, now = Date.now()) {
|
|
|
884
1288
|
export const STOP_CODES = Object.freeze({ failureLimit: 'failure_limit', noProgressTime: 'no_progress_time', noProgressAttempts: 'no_progress_attempts' });
|
|
885
1289
|
export function deliveryStop(run, now = Date.now(), { checkProgress = true } = {}) {
|
|
886
1290
|
const managed = run.managed;
|
|
887
|
-
if (run
|
|
1291
|
+
if (failureLimitReached(run)) return { code: STOP_CODES.failureLimit,
|
|
888
1292
|
reason: `Stopped after ${run.failures} failed attempts. Open the original conversation to see what went wrong.` };
|
|
889
|
-
if (
|
|
1293
|
+
if (purposeEnabled(run) && (now >= run.deadline || run.continuations >= run.maxContinuations || managed.usageBlocked || managed.maxTokens && managed.tokensUsed >= managed.maxTokens)) return { code: 'purpose_budget', reason: 'Original purpose continuation budget exhausted.' };
|
|
1294
|
+
if (!checkProgress || !managed || searchEnabled(managed)) return null;
|
|
890
1295
|
const minutes = managed.maxNoProgressMinutes ?? 30;
|
|
891
|
-
if (now - (managed.lastVerifiedProgressAt ?? run.startedAt) >= minutes * 60_000) return { code: STOP_CODES.noProgressTime,
|
|
892
|
-
reason: `Paused because no progress was confirmed in ${minutes} minutes. Review the original conversation, then resume.` };
|
|
893
|
-
if (managed.noProgress >= managed.maxNoProgress) return { code: STOP_CODES.noProgressAttempts,
|
|
1296
|
+
if (now - (purposeEnabled(run) ? purposeProgressAt(run, now) : (managed.lastVerifiedProgressAt ?? run.startedAt)) >= minutes * 60_000) return { code: STOP_CODES.noProgressTime,
|
|
1297
|
+
reason: purposeEnabled(run) ? `No verified acceptance progress in ${minutes} minutes. Review the original conversation, then resume.` : `Paused because no progress was confirmed in ${minutes} minutes. Review the original conversation, then resume.` };
|
|
1298
|
+
if ((purposeEnabled(run) ? managed.purpose.noLearning || 0 : managed.noProgress) >= managed.maxNoProgress) return { code: STOP_CODES.noProgressAttempts,
|
|
894
1299
|
reason: `Paused after ${managed.noProgress} steps without confirmed progress. Review the original conversation, then resume.` };
|
|
895
1300
|
return null;
|
|
896
1301
|
}
|
|
@@ -920,7 +1325,7 @@ function continueAfterHumanDecision(run, now) {
|
|
|
920
1325
|
run.managed.outcomeProgress = restartOutcomeProgressWindow(run.managed.outcomeProgress);
|
|
921
1326
|
run.managed.lastVerifiedProgressAt = now;
|
|
922
1327
|
}
|
|
923
|
-
const stoppedState = stop => stop.code
|
|
1328
|
+
const stoppedState = stop => [STOP_CODES.failureLimit, 'purpose_budget'].includes(stop.code) ? 'budget_exhausted' : 'paused';
|
|
924
1329
|
// Context-first tasks usually have no host-checkable acceptance, so worker
|
|
925
1330
|
// reports can never reset noProgress. Reaching that limit pauses for review
|
|
926
1331
|
// instead of failing permanently and ending Auto-continue for the conversation.
|
|
@@ -956,7 +1361,7 @@ function recordManagedOutcomeProgress(run, now, assessmentAvailable = true) {
|
|
|
956
1361
|
}
|
|
957
1362
|
// Manual/legacy runs have no marker and retain their existing controls.
|
|
958
1363
|
export function automaticContinuationAllowed(record) {
|
|
959
|
-
if (record?.providerArchived === true) return false;
|
|
1364
|
+
if (record?.providerArchived === true || record?.run?.managed?.purpose && record.autoContinue === false) return false;
|
|
960
1365
|
const epoch = record?.run?.managed?.autoContinueEpoch;
|
|
961
1366
|
return !epoch || record.autoContinue === true && record.autoContinueEpoch === epoch;
|
|
962
1367
|
}
|
|
@@ -1002,7 +1407,7 @@ export function decideStop(run, now = Date.now()) {
|
|
|
1002
1407
|
if (!fresh || cp.status === 'error') next.failures += 1;
|
|
1003
1408
|
else next.failures = 0;
|
|
1004
1409
|
}
|
|
1005
|
-
if (next
|
|
1410
|
+
if (failureLimitReached(next)) return finish('failed', 'Repeated failure or missing progress checkpoint; human review required.');
|
|
1006
1411
|
if (next.managed) {
|
|
1007
1412
|
if (next.managed.autonomyPolicy && next.managed.decision?.outcome === 'wait' && !approvedHook) {
|
|
1008
1413
|
escalate(next, next.managed.decision.reason);
|
|
@@ -1027,7 +1432,20 @@ export function decideStop(run, now = Date.now()) {
|
|
|
1027
1432
|
else if (next.managed.associationCheckpointRevision !== next.revision && !initialHook && !replanHook && !understandingHook && !approvedHook && !legacyApprovedHook && !['session_understanding', 'action_association'].includes(cp?.kind)) next.managed.noProgress += 1;
|
|
1028
1433
|
if (reportedStep) next.managed.reportedProgress = (next.managed.reportedProgress || 0) + 1;
|
|
1029
1434
|
next.managed.consumedVerifiedRevision = next.managed.verifiedRevision;
|
|
1030
|
-
|
|
1435
|
+
const repeated = repeatedSearchAction(next);
|
|
1436
|
+
if (repeated && !next.managed.pendingApproval && !next.managed.executionUncertain &&
|
|
1437
|
+
queuePurposeDiagnosis(next, `This action failed ${repeated.failures} times against the same observed workspace and acceptance state. Investigate the failure, choose a different method or acquire new evidence before retrying. Prior result: ${repeated.finding}`, `repeated:${repeated.actionKey}:${repeated.context}`, now)) {
|
|
1438
|
+
markSearchPivot(next.managed, 'Repeated failed experiment; select a discriminating alternative.', now);
|
|
1439
|
+
return { run: next, output: {} };
|
|
1440
|
+
}
|
|
1441
|
+
if (searchNeedsPivot(next, now)) {
|
|
1442
|
+
const reason = 'Change the search strategy: review experiments, identify what was ruled out and select a different discriminating method. Lack of final acceptance is not failure.';
|
|
1443
|
+
markSearchPivot(next.managed, reason, now);
|
|
1444
|
+
next.managed.purpose.lastRecovery = { reason, at: now };
|
|
1445
|
+
}
|
|
1446
|
+
if (!searchEnabled(next.managed) && purposeEnabled(next) && next.managed.purpose.noLearning === next.managed.maxNoProgress - 1 &&
|
|
1447
|
+
queuePurposeDiagnosis(next, 'No acceptance progress: revisit the purpose, gather discriminating evidence and choose a different method.', `progress:${next.managed.verifiedRevision}`, now)) return { run: next, output: {} };
|
|
1448
|
+
if (!searchEnabled(next.managed) && next.managed.noProgress >= next.managed.maxNoProgress) {
|
|
1031
1449
|
const stop = deliveryStop(next, now);
|
|
1032
1450
|
escalate(next, stop.reason);
|
|
1033
1451
|
return finish(pausesWithoutProgress(next.managed) ? 'paused' : 'failed', stop.reason);
|
|
@@ -1038,7 +1456,7 @@ export function decideStop(run, now = Date.now()) {
|
|
|
1038
1456
|
if (replanHook) next.managed.awaitingReplanHook = false;
|
|
1039
1457
|
if (understandingHook) { next.managed.awaitingUnderstandingHook = false; next.managed.awaitingAssociationHook = false; }
|
|
1040
1458
|
if (cp?.nextStep) next.nextStep = cp.nextStep;
|
|
1041
|
-
if (!(next.managed?.resumeChannel === 'codex_app_server' && (next.managed.associationCheckpointRevision === next.revision || next.managed.contextBootstrap && cp?.kind === 'session_understanding'))) next.continuations += 1;
|
|
1459
|
+
if (!searchEnabled(next.managed) && !(next.managed?.resumeChannel === 'codex_app_server' && (next.managed.associationCheckpointRevision === next.revision || next.managed.contextBootstrap && cp?.kind === 'session_understanding'))) next.continuations += 1;
|
|
1042
1460
|
next.updatedAt = now; next.state = 'active';
|
|
1043
1461
|
if (approvedHook || legacyApprovedHook) {
|
|
1044
1462
|
const approval = next.managed.approvedAction;
|
|
@@ -1051,7 +1469,7 @@ export function decideStop(run, now = Date.now()) {
|
|
|
1051
1469
|
next.reason = fresh ? 'continue_pending_work' : 'checkpoint_missing';
|
|
1052
1470
|
const selected = next.managed?.autonomyPolicy ? next.managed.decision : null;
|
|
1053
1471
|
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 ? `${minimalSufficientChangeInstruction()}\n${next.nextStep}` : `${minimalSufficientChangeInstruction()}\nCheck 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}`;
|
|
1054
|
-
return { run: next, output: { decision: 'block', reason: `SIMY Delivery Loop (${next.continuations} continuations). ${next.executionCwd ? `Use task execution directory ${JSON.stringify(next.executionCwd)} for task work and acceptance. ` : ''}${deliveryAcceptanceContext(next)} ${instruction}\nStay within the original request and permissions. If authorization or information
|
|
1472
|
+
return { run: next, output: { decision: 'block', reason: `SIMY Delivery Loop (${next.continuations} continuations). ${next.executionCwd ? `Use task execution directory ${JSON.stringify(next.executionCwd)} for task work and acceptance. ` : ''}${deliveryAcceptanceContext(next)} ${instruction}\n${purposeContract(next.managed)}\nStay within the original request and permissions. If one action lacks authorization or information, preserve that dependency and select independent authorized work. Resume this same task; do not start a replacement loop.` } };
|
|
1055
1473
|
}
|
|
1056
1474
|
// A process signature protects against PID reuse. Never treat a stale timer alone
|
|
1057
1475
|
// as a crash; an idle provider may be waiting for its user.
|
|
@@ -1112,7 +1530,7 @@ export async function handleHook({ input, provider, root = rootDirectory(), now
|
|
|
1112
1530
|
if (!TERMINAL.has(record.run.state) && !['paused', 'verification_pending'].includes(record.run.state)) {
|
|
1113
1531
|
if (!(record.run.state === 'blocked' && record.run.managed.pendingApproval)) record.run.state = 'recovery_required';
|
|
1114
1532
|
record.run.recoveryCause = 'hook_owner_mismatch';
|
|
1115
|
-
record.run.reason = 'A different or unverified
|
|
1533
|
+
record.run.reason = 'A different or unverified provider process reported this session. Inspect and explicitly resume in the original conversation.';
|
|
1116
1534
|
record.run.managed.autoRecovery = false; record.run.updatedAt = now; notifyState(record.run, now);
|
|
1117
1535
|
}
|
|
1118
1536
|
return record;
|
|
@@ -1179,7 +1597,7 @@ export async function handleHook({ input, provider, root = rootDirectory(), now
|
|
|
1179
1597
|
run.state = 'recovery_required'; run.recoveryCause = 'session_end'; run.reason = 'Provider session ended. Check why it ended before resuming.';
|
|
1180
1598
|
} else if (event === 'StopFailure') {
|
|
1181
1599
|
run.failures += 1;
|
|
1182
|
-
run.state = run
|
|
1600
|
+
run.state = failureLimitReached(run) ? 'failed' : 'recovery_required'; run.recoveryCause = 'provider_error';
|
|
1183
1601
|
run.reason = RETRYABLE.has(input.error) ? 'Transient provider error. Resume the existing conversation after the provider recovers.' : 'Provider error requires attention before resuming.';
|
|
1184
1602
|
run.errorCode = text(input.error, 100); run.retryable = RETRYABLE.has(input.error);
|
|
1185
1603
|
} else if (event === 'Stop') {
|
|
@@ -1256,7 +1674,7 @@ export function canReplanDelivery(run, provider, now = Date.now()) {
|
|
|
1256
1674
|
const managed = run?.managed;
|
|
1257
1675
|
const supported = managed?.resumeChannel === 'codex_app_server' && provider === 'codex' ||
|
|
1258
1676
|
managed?.capability === 'live_hook' && provider === 'claude' && Boolean(managed.liveHookOwner?.identity);
|
|
1259
|
-
return Boolean(supported && managed.autonomyPolicy && managed.approvalScopes?.length && !managed.rejectedActionOverflow &&
|
|
1677
|
+
return canResearchReplan(run, provider, now) || Boolean(supported && managed.autonomyPolicy && managed.approvalScopes?.length && !managed.rejectedActionOverflow &&
|
|
1260
1678
|
run.state === 'blocked' && managed.pendingApproval?.denied === true &&
|
|
1261
1679
|
!deliveryStopBlocksResume(run, now));
|
|
1262
1680
|
}
|
|
@@ -1265,7 +1683,7 @@ export function canChangeDeliveryContinuation(run) {
|
|
|
1265
1683
|
!['pending', 'pending_received', 'transferred'].includes(run.managed.handoff?.status));
|
|
1266
1684
|
}
|
|
1267
1685
|
function reopenLegacyLimitStop(run, now) {
|
|
1268
|
-
if (run?.state !== 'budget_exhausted' ||
|
|
1686
|
+
if (purposeEnabled(run) || run?.state !== 'budget_exhausted' ||
|
|
1269
1687
|
deliveryStop(run, now)?.code === STOP_CODES.failureLimit ||
|
|
1270
1688
|
// Reasons persisted by older versions for stops that were not obsolete caps.
|
|
1271
1689
|
/No verified|failure limit/i.test(run.reason || '') ||
|
|
@@ -1277,9 +1695,9 @@ function reopenLegacyLimitStop(run, now) {
|
|
|
1277
1695
|
}
|
|
1278
1696
|
run.state = 'paused'; run.reason = 'SIMY no longer limits time or turns. Resume this task to continue.';
|
|
1279
1697
|
}
|
|
1280
|
-
export async function controlDelivery({ root = rootDirectory(), provider, session, taskId, approvalId, autonomyPolicy, continuationPolicy, questionId, waitId, answer, action, now = Date.now() }) {
|
|
1698
|
+
export async function controlDelivery({ root = rootDirectory(), provider, session, taskId, approvalId, autonomyPolicy, continuationPolicy, questionId, waitId, answer, purposeUpdate, action, inspectProcess = processIdentity, now = Date.now() }) {
|
|
1281
1699
|
for (let retry = 0; ; retry += 1) {
|
|
1282
|
-
try { return await updateRecord(root, provider, session, record => {
|
|
1700
|
+
try { return await updateRecord(root, provider, session, async record => {
|
|
1283
1701
|
const run = record?.run;
|
|
1284
1702
|
if (!run) throw new Error('Delivery Loop not found.');
|
|
1285
1703
|
if (record.providerArchived === true && !['stop', 'pause'].includes(action)) throw new Error('Restore the original provider conversation before changing Delivery Loop.');
|
|
@@ -1287,16 +1705,44 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
|
|
|
1287
1705
|
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.');
|
|
1288
1706
|
if (action !== 'stop' && run.managed?.handoff?.status === 'transferred') throw new Error('Task handoff is locked; continue in the received target session.');
|
|
1289
1707
|
reopenLegacyLimitStop(run, now);
|
|
1290
|
-
if (action === '
|
|
1708
|
+
if (action === 'revise_purpose') {
|
|
1709
|
+
if (!searchEnabled(run.managed) || run.managed.resumeLease || run.state !== 'paused' || !text(purposeUpdate?.sourceRef, 500) || !text(purposeUpdate?.objective, 2000)) throw new Error('Pause the task and supply an explicit user purpose revision with sourceRef, objective and acceptanceCriteria.');
|
|
1710
|
+
const revised=managedSpec({acceptanceCriteria:purposeUpdate.acceptanceCriteria,approvalScopes:run.managed.approvalScopes,initialAction:{kind:'read',scope:run.managed.approvalScopes[0],summary:'Reassess the explicitly revised purpose'}},session,provider,null,null,now);
|
|
1711
|
+
const ledger=evidenceLedger(run.managed);
|
|
1712
|
+
ledger.purposeRevisions ||= [];
|
|
1713
|
+
if (ledger.purposeRevisions.length >= 8) throw new Error('Purpose revision history requires archival before another revision.');
|
|
1714
|
+
ledger.purposeRevisions.push({revision:ledger.purposeRevision,charter:ledger.charter,sourceRef:text(purposeUpdate.sourceRef,500),changedAt:now});
|
|
1715
|
+
if(run.managed.purpose.candidate){
|
|
1716
|
+
run.managed.purpose.retainedCandidates ||= [];
|
|
1717
|
+
if(run.managed.purpose.retainedCandidates.length>=8) throw new Error('Retained candidates require explicit archival before another purpose revision.');
|
|
1718
|
+
run.managed.purpose.retainedCandidates.push({...run.managed.purpose.candidate,status:'retained_old_purpose',purposeRevision:ledger.purposeRevision});
|
|
1719
|
+
delete run.managed.purpose.candidate;
|
|
1720
|
+
}
|
|
1721
|
+
ledger.purposeRevision++;
|
|
1722
|
+
for(const unit of ledger.units) if (!['verified','superseded'].includes(unit.status)) unit.status='superseded_by_user_revision';
|
|
1723
|
+
ledger.requirements=ledger.requirements.filter(row=>row.id.startsWith('rule:'));
|
|
1724
|
+
ledger.findings.forEach(row=>{row.status='superseded_by_user_revision';});
|
|
1725
|
+
run.objective=text(purposeUpdate.objective,2000);run.managed.acceptanceCriteria=revised.acceptanceCriteria;run.managed.verified={};
|
|
1726
|
+
ledger.charter={objective:run.objective,criteria:structuredClone(revised.acceptanceCriteria),approvalScopes:[...run.managed.approvalScopes],frozenAt:now,sourceRef:text(purposeUpdate.sourceRef,500)};
|
|
1727
|
+
delete run.managed.enrollment;freezeEnrollment(run,now);
|
|
1728
|
+
run.managed.pendingApproval=null;delete run.managed.approvedAction;delete run.managed.purpose.lastReview;
|
|
1729
|
+
run.managed.purpose.search={cycle:0,pivots:0,experiments:[],waits:[],observed:[],lastPivotCycle:0};
|
|
1730
|
+
run.revision++;run.updatedAt=now;run.reason='User purpose revision recorded; prior evidence and exact approvals require re-evaluation. Resume explicitly within the existing budget.';
|
|
1731
|
+
run.managed.autoRecovery=false;return record;
|
|
1732
|
+
} else if (action === 'continuation_policy') {
|
|
1291
1733
|
if (!canChangeDeliveryContinuation(run)) throw new Error('Pause a managed task before changing its continuation policy.');
|
|
1292
1734
|
if (!['bounded', 'progress'].includes(continuationPolicy)) throw new Error('Select an explicit continuation policy.');
|
|
1293
1735
|
run.managed.continuationPolicy = continuationMode(continuationPolicy);
|
|
1294
1736
|
run.continuationPolicyHistory ||= [];
|
|
1295
1737
|
run.continuationPolicyHistory.push({ policy: continuationPolicy, at: now, source: 'explicit_controller_selection' });
|
|
1738
|
+
run.updatedAt = now; return record;
|
|
1739
|
+
} else if (action === 'disable_auto_continue') {
|
|
1740
|
+
if (!run.managed?.purpose) throw new Error('Purpose continuation is not enabled.');
|
|
1741
|
+
run.managed.purpose.enabled = false; run.managed.autoRecovery = false;
|
|
1742
|
+
if (!TERMINAL.has(run.state)) { run.state = 'paused'; run.reason = 'Automatic continuation paused by user.'; }
|
|
1296
1743
|
run.updatedAt = now;
|
|
1297
1744
|
return record;
|
|
1298
|
-
}
|
|
1299
|
-
if (action === 'dependency_ready') {
|
|
1745
|
+
} else if (action === 'dependency_ready') {
|
|
1300
1746
|
const wait = run.managed?.dependencyWait;
|
|
1301
1747
|
if (run.state !== 'blocked' || run.blocker !== 'external_dependency' || !wait || wait.id !== waitId ||
|
|
1302
1748
|
typeof answer !== 'string' || !text(answer, 1000) || answer.length > 1000) throw new Error('Dependency wait changed or confirmation is empty; refresh its status.');
|
|
@@ -1340,6 +1786,7 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
|
|
|
1340
1786
|
else { run.state = 'recovery_required'; run.recoveryCause = 'policy_change'; run.reason = 'Policy changed; resume the original conversation for the selected action.'; run.managed.autoRecovery = false; }
|
|
1341
1787
|
}
|
|
1342
1788
|
else if (action === 'stop') {
|
|
1789
|
+
run.stopReason = { actor: 'user', code: 'explicit_stop', at: now };
|
|
1343
1790
|
if (TERMINAL.has(run.state)) return record;
|
|
1344
1791
|
run.state = 'stopped'; run.reason = 'Stopped by user.';
|
|
1345
1792
|
}
|
|
@@ -1349,34 +1796,44 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
|
|
|
1349
1796
|
if (!run.managed || !['active', 'auto_resuming'].includes(run.state) && !queuedAppServer) throw new Error('Only a running or queued managed task can pause.');
|
|
1350
1797
|
if (queuedAppServer || run.state === 'auto_resuming' && run.managed.resumeChannel === 'codex_app_server') run.managed.pausedRecoveryCause = run.recoveryCause || 'session_supervision';
|
|
1351
1798
|
run.state = 'paused'; run.reason = 'Paused by user.';
|
|
1799
|
+
run.stopReason = { actor: 'user', code: 'explicit_pause', at: now };
|
|
1352
1800
|
}
|
|
1353
1801
|
else if (action === 'replan') {
|
|
1354
|
-
if (!canReplanDelivery(run, provider, now)) throw new Error('Replanning needs a rejected assessed approval
|
|
1355
|
-
|
|
1802
|
+
if (!canReplanDelivery(run, provider, now)) throw new Error('Replanning needs a supported original-session channel, a rejected assessed approval or safe research wait, and available budget.');
|
|
1803
|
+
const research = canResearchReplan(run, provider, now);
|
|
1804
|
+
if (research && run.managed.capability === 'live_hook') {
|
|
1805
|
+
const owner = run.managed.liveHookOwner || record.hookSessionProcess;
|
|
1806
|
+
if (run.managed.thread?.provider !== provider || run.managed.thread.id !== session ||
|
|
1807
|
+
!owner?.identity || !record.hookStartedAt || record.hookEndedAt ||
|
|
1808
|
+
record.process?.pid !== owner.pid || record.process?.identity !== owner.identity ||
|
|
1809
|
+
await inspectProcess(owner.pid) !== owner.identity) throw new Error('Research replanning requires the original verified live hook owner.');
|
|
1810
|
+
run.managed.liveHookOwner ||= { ...owner, hookStartedAt: record.hookStartedAt };
|
|
1811
|
+
}
|
|
1812
|
+
if (!research) continueAfterHumanDecision(run, now);
|
|
1356
1813
|
const rejected = run.managed.pendingApproval;
|
|
1357
|
-
//
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
const plan = rejectedActionDiagnosis(run.managed, rejected);
|
|
1814
|
+
// Denied records remain denied; research waits never create an approval.
|
|
1815
|
+
if (!research) rememberRejectedAction(run.managed, rejected);
|
|
1816
|
+
const plan = research ? researchWaitDiagnosis(run.managed) : rejectedActionDiagnosis(run.managed, rejected);
|
|
1361
1817
|
const decision = recordDecision(run.managed, plan, now);
|
|
1362
|
-
if (decision.outcome !== 'proceed' || decision.action.kind !== 'read') throw new Error('Safe read diagnosis is unavailable; keep the
|
|
1818
|
+
if (decision.outcome !== 'proceed' || decision.action.kind !== 'read') throw new Error('Safe read diagnosis is unavailable; keep the waiting action blocked.');
|
|
1363
1819
|
delete run.managed.approvedAction;
|
|
1364
1820
|
run.managed.userQuestion = null;
|
|
1365
1821
|
run.managed.escalation = null;
|
|
1366
1822
|
run.blocker = null;
|
|
1367
|
-
run.nextStep = 'Review the recorded rejection and propose a different assessed option without executing it.';
|
|
1823
|
+
run.nextStep = research ? 'Inspect local assessment evidence and propose a corrected assessed option without executing the waiting action.' : 'Review the recorded rejection and propose a different assessed option without executing it.';
|
|
1368
1824
|
run.checkpoint = null; run.consumedRevision = run.revision;
|
|
1369
1825
|
if (run.managed.resumeChannel === 'codex_app_server') {
|
|
1370
1826
|
run.managed.autoRecovery = true;
|
|
1371
1827
|
run.state = 'recovery_required'; run.recoveryCause = 'session_supervision';
|
|
1372
|
-
run.reason = 'Rejected action remains denied; safe read diagnosis is queued in the exact Codex session.';
|
|
1828
|
+
run.reason = research ? 'Research wait remains unexecuted; safe read diagnosis is queued in the exact Codex session.' : 'Rejected action remains denied; safe read diagnosis is queued in the exact Codex session.';
|
|
1373
1829
|
} else {
|
|
1374
1830
|
run.managed.awaitingReplanHook = true;
|
|
1375
1831
|
run.state = 'active';
|
|
1376
|
-
run.reason = 'Rejected action remains denied; safe read diagnosis awaits the original Claude Stop boundary.';
|
|
1832
|
+
run.reason = research ? 'Research wait remains unexecuted; safe read diagnosis awaits the original provider Stop boundary.' : 'Rejected action remains denied; safe read diagnosis awaits the original Claude Stop boundary.';
|
|
1377
1833
|
}
|
|
1378
1834
|
}
|
|
1379
1835
|
else if (action === 'resume') {
|
|
1836
|
+
delete run.stopReason;
|
|
1380
1837
|
if (run.managed?.dependencyWait) throw new Error('Confirm the exact external dependency condition before resuming.');
|
|
1381
1838
|
if (run.managed?.phase === 'needs_clarification') throw new Error('Answer the current session question before resuming.');
|
|
1382
1839
|
if (!['blocked', 'recovery_required', 'paused'].includes(run.state)) throw new Error('Only a blocked, paused, or recoverable loop can resume.');
|
|
@@ -1397,7 +1854,7 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
|
|
|
1397
1854
|
const selectedKind = run.managed?.decision?.action?.kind;
|
|
1398
1855
|
const approvedWait = run.managed?.decision?.outcome === 'wait' && run.managed.approvedAction?.id === run.managed.decision.id && !run.managed.approvedAction.dispatchedAt;
|
|
1399
1856
|
const queuedAppServer = run.managed?.resumeChannel === 'codex_app_server' &&
|
|
1400
|
-
['read', 'local_edit', 'local_test'].includes(selectedKind) &&
|
|
1857
|
+
['read', 'local_edit', 'local_test', ...(searchEnabled(run.managed) ? ['external'] : [])].includes(selectedKind) &&
|
|
1401
1858
|
(run.managed.decision.outcome !== 'wait' || approvedWait);
|
|
1402
1859
|
if (queuedAppServer) run.managed.autoRecovery = true;
|
|
1403
1860
|
run.state = queuedAppServer ? 'recovery_required' : 'active';
|
|
@@ -1418,6 +1875,7 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
|
|
|
1418
1875
|
run.checkpoint = { status: 'in_progress', summary: run.reason, nextStep: run.nextStep, evidence: [], acceptanceMet: false };
|
|
1419
1876
|
run.revision += 1;
|
|
1420
1877
|
run.managed.approvedAction = { id: pending.id, nextStep: run.nextStep, action: { kind: pending.kind, scope: pending.scope, summary: pending.summary } };
|
|
1878
|
+
if (searchEnabled(run.managed) && run.managed.resumeChannel === 'codex_app_server' && pending.kind === 'external' && run.managed.decision?.assessment?.reversibility === 'reversible') run.managed.autoRecovery = true;
|
|
1421
1879
|
} else {
|
|
1422
1880
|
pending.denied = true; run.reason = `User rejected ${pending.scope}.`; run.blocker = 'authorization';
|
|
1423
1881
|
rememberRejectedAction(run.managed, pending);
|
|
@@ -1457,6 +1915,7 @@ export async function reconcileRecords({ root = rootDirectory(), now = Date.now(
|
|
|
1457
1915
|
const stop = deliveryStop(run, now, { checkProgress: !activeTurn });
|
|
1458
1916
|
if (stop && !TERMINAL.has(run.state) && !['verification_pending', 'paused'].includes(run.state)) {
|
|
1459
1917
|
run.state = stoppedState(stop);
|
|
1918
|
+
run.stopReason = { actor: 'system', code: stop.code, at: now, evidence: stop.reason };
|
|
1460
1919
|
run.reason = stop.reason; run.updatedAt = now;
|
|
1461
1920
|
}
|
|
1462
1921
|
else if (run.state === 'active' && observed !== null && observed !== current.process?.identity && current.process?.identity === record.process?.identity) {
|