@awak-app/simy-cli 0.7.27 → 0.7.29
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/package.json +3 -3
- package/src/cli-contract.js +1 -1
- package/src/cli-observability.js +1 -1
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +229 -22
- package/src/codex-plugins/simy/hooks/purpose-search.mjs +23 -4
- package/src/codex-plugins/simy/hooks/result-cycle.mjs +8 -1
- package/src/codex-plugins/simy/hooks/scene-reasoning.mjs +2 -0
- package/src/delivery-hooks/command.js +4 -0
- package/src/delivery-hooks/lifecycle-recovery.js +1 -1
- package/src/delivery-hooks/supervisor.js +14 -2
- package/src/desktop-executor.js +6 -0
- package/src/orchestrator/budget.js +24 -0
- package/src/orchestrator/charter-repair.js +1 -1
- package/src/orchestrator/independent-audit.js +13 -6
- package/src/orchestrator/loop.js +34 -4
- package/src/provider-stream.js +9 -2
- package/src/runner.js +42 -20
- package/src/session-supervision/automatic-continuation.js +8 -1
- package/src/session-supervision/claude-outcome-review.js +132 -0
- package/src/session-supervision/claude-review-definition.js +5 -0
- package/src/session-supervision/claude-sessions.js +67 -6
- package/src/session-supervision/codex-app-server.js +170 -35
- package/src/session-supervision/codex-native-connection.js +101 -0
- package/src/session-supervision/continuation-analysis.js +1 -1
- package/src/session-supervision/index.js +5 -2
- package/src/session-supervision/native-handoff.js +251 -0
- package/src/session-supervision/purpose-recovery.js +1 -0
- package/src/session-supervision/purpose-verification.js +103 -9
- package/src/sqm/agent-skill.js +3 -1
- package/src/sqm/claude-plugin.js +2 -0
- package/src/sqm/command.js +17 -1
- package/src/sqm/hypothesis-knowledge.js +31 -10
- package/src/sqm/knowledge-recall.js +66 -0
- package/src/sqm/review-worker.js +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awak-app/simy-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.29",
|
|
4
4
|
"description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
"test:e2e:console:real-provider": "node ./scripts/real-provider-console-e2e.js",
|
|
40
40
|
"test:e2e:console:fullstack": "node ./scripts/fullstack-cli-smoke.js",
|
|
41
41
|
"check": "npm run check:auto-update && node --check ./src/desktop-executor.js && node --check ./src/local-task.js && node --check ./scripts/desktop-executor-e2e.js && node --check ./scripts/pre-guardrail-e2e.js && node --check ./scripts/fixtures/fake-desktop-provider.js && npm run check:syntax && npm run check:session-supervision-syntax && npm run check:real-provider-syntax && npm test && npm run check:package",
|
|
42
|
-
"check:session-supervision-syntax": "node --check ./src/session-supervision/session-list.js && node --check ./src/console/sessions.js && node --check ./src/session-supervision/remote-hosts.js && node --check ./src/session-supervision/remote-entry.js && node --check ./src/session-supervision/index.js && node --check ./src/session-supervision/discovery-window.js && node --check ./src/session-supervision/codex-app-server.js && node --check ./src/session-supervision/claude-sessions.js && node --check ./src/session-supervision/original-session-target.js && node --check ./src/session-supervision/action-association.js && node --check ./src/session-supervision/action-association-host.js && node --check ./src/session-supervision/action-association-api.js && node --check ./src/session-supervision/association-store.js && node --check ./src/session-supervision/action-deadline.js && node --check ./src/session-supervision/action-deadline-store.js && node --check ./src/codex-plugins/simy/hooks/action-association.mjs && node --check ./src/session-supervision/purpose-instruction.js && node --check ./src/session-supervision/purpose-recovery.js && node --check ./src/session-supervision/purpose-candidate.js && node --check ./src/codex-plugins/simy/hooks/scene-reasoning.mjs && node --check ./src/codex-plugins/simy/hooks/purpose-cycle.mjs && node --check ./src/codex-plugins/simy/hooks/purpose-rule-review.mjs && node --check ./src/codex-plugins/simy/hooks/purpose-search.mjs && node --check ./src/codex-plugins/simy/hooks/purpose-evidence.mjs && node --check ./src/session-supervision/purpose-observer.js && node --check ./src/session-supervision/purpose-verification.js && node --check ./src/codex-plugins/simy/hooks/purpose-workspace.mjs && node --check ./scripts/purpose-search-live-e2e.js && node --check ./src/session-supervision/remote-managed-delivery.js",
|
|
42
|
+
"check:session-supervision-syntax": "node --check ./src/session-supervision/session-list.js && node --check ./src/console/sessions.js && node --check ./src/session-supervision/remote-hosts.js && node --check ./src/session-supervision/remote-entry.js && node --check ./src/session-supervision/index.js && node --check ./src/session-supervision/discovery-window.js && node --check ./src/session-supervision/codex-app-server.js && node --check ./src/session-supervision/claude-sessions.js && node --check ./src/session-supervision/claude-outcome-review.js && node --check ./src/session-supervision/original-session-target.js && node --check ./src/session-supervision/action-association.js && node --check ./src/session-supervision/action-association-host.js && node --check ./src/session-supervision/action-association-api.js && node --check ./src/session-supervision/association-store.js && node --check ./src/session-supervision/action-deadline.js && node --check ./src/session-supervision/action-deadline-store.js && node --check ./src/codex-plugins/simy/hooks/action-association.mjs && node --check ./src/session-supervision/purpose-instruction.js && node --check ./src/session-supervision/purpose-recovery.js && node --check ./src/session-supervision/purpose-candidate.js && node --check ./src/codex-plugins/simy/hooks/scene-reasoning.mjs && node --check ./src/codex-plugins/simy/hooks/purpose-cycle.mjs && node --check ./src/codex-plugins/simy/hooks/purpose-rule-review.mjs && node --check ./src/codex-plugins/simy/hooks/purpose-search.mjs && node --check ./src/codex-plugins/simy/hooks/purpose-evidence.mjs && node --check ./src/session-supervision/purpose-observer.js && node --check ./src/session-supervision/purpose-verification.js && node --check ./src/codex-plugins/simy/hooks/purpose-workspace.mjs && node --check ./scripts/purpose-search-live-e2e.js && node --check ./src/session-supervision/remote-managed-delivery.js",
|
|
43
43
|
"check:auto-update": "node --check ./src/auto-update.js && node --check ./scripts/auto-update-e2e.js && npm run check:sqm",
|
|
44
44
|
"check:workflow-observation-sender": "node --check ./src/workflow-observation-sender.js",
|
|
45
45
|
"check:sqm": "node --check ./src/sqm/provider-plugin-repair.js && node --check ./src/sqm/provider-plugin-status.js && node --check ./src/sqm/advisory-guidance.js && node --check ./src/sqm/canonical.js && node --check ./src/sqm/validation.js && node --check ./src/sqm/reliability.js && node --check ./src/sqm/repository.js && node --check ./src/sqm/checkers.js && node --check ./src/sqm/bundle-store.js && node --check ./src/sqm/proof.js && node --check ./src/sqm/evidence-client.js && node --check ./src/sqm/session.js && node --check ./src/sqm/index.js && node --check ./src/sqm/command.js && node --check ./src/sqm/agent-skill.js && node --check ./src/sqm/workflow-onboarding.js && node --check ./src/sqm/workflow-update.js && node --check ./src/sqm/history-store.js && node --check ./src/sqm/review-worker.js && node --check ./scripts/sqm-complete-loop-e2e.js && node --check ./scripts/sqm-local-review-e2e.js",
|
|
46
|
-
"check:real-provider-syntax": "node --check ./scripts/real-codex-probe.js && node --check ./scripts/real-claude-probe.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/claude-parity-runtime-e2e.js && node --check ./scripts/claude-plugin-runtime-e2e.js && node --check ./scripts/codex-plugin-runtime-e2e.js",
|
|
46
|
+
"check:real-provider-syntax": "node --check ./scripts/real-codex-probe.js && node --check ./scripts/real-claude-probe.js && node --check ./scripts/claude-delivery-fault-live-e2e.mjs && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/claude-parity-runtime-e2e.js && node --check ./scripts/claude-plugin-runtime-e2e.js && node --check ./scripts/codex-plugin-runtime-e2e.js",
|
|
47
47
|
"check:syntax": "node --check ./src/purpose-local-proof.js && node --check ./src/device-signing-identity.js && node --check ./src/index.js && node --check ./src/install-update.js && node --check ./src/agent.js && node --check ./src/connection-discovery.js && node --check ./src/connection-diagnostics.js && node --check ./src/launch-authorization.js && node --check ./src/codex-plugins/simy/hooks/connection-reminder.mjs && node --check ./src/codex-plugins/simy/hooks/dev-work.mjs && node --check ./src/dev-work-events.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/durable-local-task-worker.js && node --check ./src/durable-local-task-steps-contract.js && node --check ./src/bounded-local-task-subtasks-contract.js && node --check ./src/bounded-local-task-subtask-pool.js && node --check ./src/local-task-artifact-contract.js && node --check ./src/execution-capability-contract.js && node --check ./src/shutdown.js && node --check ./src/run-registry.js && node --check ./src/direct-executor.js && node --check ./src/execution-guardrail.js && node --check ./src/backend-executable.js && node --check ./src/login-shell-path.js && node --check ./src/hook-runtime.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/completion-contract.js && node --check ./src/orchestrator/pr-preparation.js && node --check ./src/orchestrator/minimum-completion.js && node --check ./src/orchestrator/delivery/scripts/check-minimum-completion.mjs && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./src/coding-run-store.js && node --check ./scripts/fixtures/completion-contract-fixture.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/completion-contract-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/local-task-default-workspace-e2e.js && node --check ./scripts/local-task-idempotency-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/console-recovery-e2e-fixture.js && node --check ./scripts/console-recovery-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js && node --check ./src/codex-plugins/simy/hooks/delivery-loop.mjs && node --check ./src/delivery-hooks/command.js && node --check ./src/delivery-hooks/desktop.js && node --check ./src/delivery-hooks/supervisor.js && node --check ./src/delivery-hooks/recovery.js && node --check ./src/delivery-hooks/supervisor-owner.js && node --check ./src/delivery-hooks/supervisor-watchdog.js && node --check ./src/delivery-hooks/supervisor-service.js && node --check ./scripts/supervisor-watchdog-live-e2e.js && node --check ./src/codex-plugins/simy/hooks/recovery-incident.mjs && node --check ./src/session-supervision/automatic-continuation.js && node --check ./src/session-supervision/stop-notifications.js",
|
|
48
48
|
"check:package": "node ./scripts/check-package-contents.js",
|
|
49
49
|
"build:macos-installer:dev": "bash packaging/build-macos-arm64.sh --unsigned-dev",
|
package/src/cli-contract.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "./durable-local-task-steps-contract.js";
|
|
15
15
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
16
16
|
|
|
17
|
-
export const CLI_VERSION = "0.7.
|
|
17
|
+
export const CLI_VERSION = "0.7.29";
|
|
18
18
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
19
19
|
|
|
20
20
|
export function narrativeImageCapabilityMetadata() {
|
package/src/cli-observability.js
CHANGED
|
@@ -7,7 +7,7 @@ import { execFile } from 'node:child_process';
|
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
8
|
import { webApiHeaders, webApiUrl } from './web-api.js';
|
|
9
9
|
// Keep this leaf module package-safe: hooks load it before the full CLI contract.
|
|
10
|
-
const CLI_VERSION = '0.7.
|
|
10
|
+
const CLI_VERSION = '0.7.29';
|
|
11
11
|
|
|
12
12
|
const STORE = 'cli-observability-state.json';
|
|
13
13
|
const LEGACY_CONFIG = 'cli-observability.json';
|
|
@@ -432,14 +432,14 @@ function assessedPlan(value, managed = null) {
|
|
|
432
432
|
if (!alternatives.some(option => option.id === recommendation.optionId) || (!flexible && !recommendation.reason)) throw new Error('Plan recommendation must identify an assessed option and reason.');
|
|
433
433
|
return { diagnosis: text(value.diagnosis, 2000), alternatives, selectedOptionId, recommendation, userQuestion: text(value.userQuestion, 1000) || null };
|
|
434
434
|
}
|
|
435
|
-
function missingAcceptanceDiagnosis(managed, missing) {
|
|
435
|
+
export function missingAcceptanceDiagnosis(managed, missing) {
|
|
436
436
|
const names = missing.map(item => item.id).join(', ');
|
|
437
437
|
const scope = managed.decision.action.scope;
|
|
438
438
|
const action = { kind: 'read', scope, summary: `Inspect missing acceptance checks: ${names}` };
|
|
439
|
-
return assessedPlan({ diagnosis: `Worker claimed completion, but independent
|
|
439
|
+
return assessedPlan({ diagnosis: `Worker claimed completion, but independent acceptance checks still fail: ${names}. Diagnose the discrepancy before any repair.`,
|
|
440
440
|
alternatives: [{ id: 'inspect_missing_acceptance', action,
|
|
441
441
|
assessment: { risk: 'low', reversibility: 'reversible', externalEffect: 'none', blastRadius: 'single_file', uncertainty: 'low', inObjective: true, userChoice: false },
|
|
442
|
-
expectedVerification: `Report
|
|
442
|
+
expectedVerification: `Report actual outcome and unmet conditions for ${names}`, rollback: 'No changes to undo' }],
|
|
443
443
|
selectedOptionId: 'inspect_missing_acceptance', recommendation: { optionId: 'inspect_missing_acceptance', reason: 'Independent checks have not verified completion.' } });
|
|
444
444
|
}
|
|
445
445
|
function rejectedActionDiagnosis(managed, rejected) {
|
|
@@ -503,6 +503,18 @@ export function canResearchReplan(run, provider, now) {
|
|
|
503
503
|
!m.userQuestion && !['understanding', 'needs_clarification', 'action_association'].includes(m.phase) &&
|
|
504
504
|
!m.nativeWait && !m.providerWarning && !m.usageBlocked && !m.purpose?.nativeBoundary && !run.blocker && !deliveryStop(run, now, { checkProgress: false }));
|
|
505
505
|
}
|
|
506
|
+
// A legacy worker can mislabel a filesystem write as effect-free. Keep that
|
|
507
|
+
// write held; only diagnose its assessment after the original turn has settled.
|
|
508
|
+
function canDiagnoseLegacyLocalEffect(run, provider, now) {
|
|
509
|
+
const m = run?.managed, d = m?.decision, pending = m?.pendingApproval;
|
|
510
|
+
if (!['active', 'blocked'].includes(run?.state) || searchEnabled(m) || m?.capability !== 'live_hook' || m.autonomyPolicy !== 'autonomous' ||
|
|
511
|
+
d?.action?.kind !== 'local_edit' || d.assessment?.externalEffect !== 'none' ||
|
|
512
|
+
d.assessment.reversibility !== 'reversible' || d.reason !== 'Local write has inconsistent effect boundaries.' ||
|
|
513
|
+
pending?.id !== d.id || pending.denied || m.plan?.userQuestion ||
|
|
514
|
+
m.rejectedActionDigests?.includes(digest(JSON.stringify(d.action)))) return false;
|
|
515
|
+
return canResearchReplan({ ...run, state: 'blocked', managed: { ...m, pendingApproval: null,
|
|
516
|
+
decision: { ...d, waitKind: 'research' } } }, provider, now);
|
|
517
|
+
}
|
|
506
518
|
export function evaluateAssessedPlan(plan, policy, scopes, now = Date.now(), { allowLocalTestWrites = false, llmDelegated = false, allowReversibleExternal = false } = {}) {
|
|
507
519
|
const option = plan.alternatives.find(item => item.id === plan.selectedOptionId);
|
|
508
520
|
const { action } = option;
|
|
@@ -514,9 +526,10 @@ export function evaluateAssessedPlan(plan, policy, scopes, now = Date.now(), { a
|
|
|
514
526
|
if (!scopes.includes(action.scope)) { reason = 'Action scope is outside the task enrollment.'; waitKind = 'authority'; }
|
|
515
527
|
else if (facts.inObjective !== true) reason = 'Establish objective fit or choose work within the original task.';
|
|
516
528
|
else if (facts.reversibility === 'irreversible') { reason = 'Irreversible consequences require an exact user decision.'; waitKind = 'human'; }
|
|
517
|
-
else if (facts.reversibility !== 'reversible' || facts.externalEffect === 'unknown' || facts.blastRadius === 'unknown') reason =
|
|
529
|
+
else if (facts.reversibility !== 'reversible' || facts.externalEffect === 'unknown' || facts.blastRadius === 'unknown') reason = `Investigate unresolved assessment fields before execution: ${['reversibility', 'externalEffect', 'blastRadius'].filter(key => key === 'reversibility' ? facts[key] !== 'reversible' : facts[key] === 'unknown').map(key => `${key}=${facts[key]}`).join(', ')}. Inspect the actual target and operation contract; preserve facts that remain unknown.`;
|
|
518
530
|
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.';
|
|
519
531
|
else if (!option.expectedVerification) reason = 'Expected verification is missing; acquire a verification method.';
|
|
532
|
+
else if (autonomous && allowReversibleExternal && action.kind === 'external' && facts.externalEffect !== 'external') reason = 'External action and declared effects disagree. Inspect the target and correct the assessment without executing it; API/service mutations have external effects even for a loopback service, while local filesystem work uses a local action.';
|
|
520
533
|
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'; }
|
|
521
534
|
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.';
|
|
522
535
|
else if (localWrite && facts.externalEffect !== 'local') reason = 'Local write has inconsistent effect boundaries.';
|
|
@@ -542,11 +555,11 @@ export function canExecuteAutonomousExternal(managed) {
|
|
|
542
555
|
// contract identical across transports; the host still verifies acceptance.
|
|
543
556
|
export function assessedCheckpointContract(decision, managed = null) {
|
|
544
557
|
const observationField = flexibleReasoning(managed) ? 'Plan diagnosis, recommendation and its reason are optional when the selected action is clear. Optional observations may be omitted; do not create evidence files, citations or extra reports. Actual tool results are captured by the controller.' : 'Return the observed result; evidence attachments and extra reports are optional.';
|
|
545
|
-
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"} for THIS selected action. ${observationField} ${actionReversibilityInstruction} 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. Successful diagnosis or repair is only an action result, not completion of the original user task. If original work remains, return in_progress with remainingWork and the next assessed plan, preserving the original objective, corrections and acceptance criteria. Do not repeat effects already performed.
|
|
558
|
+
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"} for THIS selected action. ${observationField} ${actionReversibilityInstruction} This is provider-reported, not independent verification. When the requested outcome is a conversational answer or correction, put the actual answer text in summary. Saying it was prepared or delivered is insufficient; do not place the answer only in an undeclared JSON field. For an answer-only request, returning that text here is the conversational result: do not invent a separate message-delivery action unless the original user requested a separate destination. This does not verify implementation or external effects. 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. For blastRadius, single_file and repository describe filesystem scope; multi_system is the conservative category for a known non-filesystem boundary, including an API/service or conversation. State the actual affected targets in the plan; this category grants no additional target or permission. Use unknown for missing scope facts, not merely because a known target is not a file. Missing facts are unknown. Successful diagnosis or repair is only an action result, not completion of the original user task. If original work remains, return in_progress with remainingWork and the next assessed plan, preserving the original objective, corrections and acceptance criteria. Do not repeat effects already performed. Report completed only when the original requested outcome is achieved; keep unfinished checks as remaining work. Do not add audit reports, certification, or evidence-submission tasks. 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. ${informationWaitInstruction} external_dependency requires waitCondition:{condition,owner,nextCheck} with the observable resumption condition, dependency owner, and bounded recheck instruction, plus nextStep.`;
|
|
546
559
|
|
|
547
560
|
}
|
|
548
561
|
export function understandingCheckpointContract(decision) {
|
|
549
|
-
return `${minimalSufficientChangeInstruction({ turn: 'understanding' })}\n${correctiveFollowupInstruction}\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. Write objective and criteria in the original user's language. Lead objective with the intended user experience (who can do what in which situation), when grounded in the request. Criteria must describe observable outcomes for that experience, not merely code written or tests passing. Keep technical-only requests technical; do not invent new UX obligations. Preserve done and remaining as the recorded stages of the whole requested work, distinguishing reported implementation from verified outcomes. 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.`;
|
|
562
|
+
return `${minimalSufficientChangeInstruction({ turn: 'understanding' })}\n${correctiveFollowupInstruction}\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 blastRadius, single_file and repository describe filesystem scope; multi_system is the conservative category for a known non-filesystem boundary, including an API/service or conversation. State the actual affected targets in the plan; this category grants no additional target or permission. Use unknown for missing scope facts, not merely because a known target is not a file. 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. Write objective and criteria in the original user's language. Lead objective with the intended user experience (who can do what in which situation), when grounded in the request. Criteria must describe observable outcomes for that experience, not merely code written or tests passing. Keep technical-only requests technical; do not invent new UX obligations. Preserve done and remaining as the recorded stages of the whole requested work, distinguishing reported implementation from verified outcomes. 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. If and only if the original user requests ONLY a conversational answer or design (no implementation, files, tests, deployment or external effects), no independent review/audit or human acceptance is requested anywhere in the user context, and the existing answer already satisfies every requested point, you may include sessionUnderstanding.conversationOutcome:{kind:"answer_only",independentReviewRequested:false,requestTurnRefs:["exact user turn refs"],answerTurnRefs:["exact assistant answer turn refs"]}. Include the latest user and latest answer references, and exact user excerpts in the criteria basis. Read and judge the actual answer, including factual errors and unsupported claims; its mere existence or a worker saying done is insufficient. If the answer is wrong or incomplete, use clear with the next read action to formulate the missing/corrected answer, not already_done. Omit conversationOutcome for uncertain scope, missing context or requested effects; a design that describes unimplemented work is complete only when the user asked for design alone. This optional classification ends only an initially uncontracted conversational task as reported_done; it does not waive a frozen acceptance contract or assert 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.`;
|
|
550
563
|
}
|
|
551
564
|
function boundedContextList(value, maxItems = 8, maxLength = 300) {
|
|
552
565
|
if (!Array.isArray(value) || value.length > maxItems) throw new Error('Session understanding has too many items.');
|
|
@@ -588,13 +601,40 @@ function sessionUnderstanding(value) {
|
|
|
588
601
|
}) : [];
|
|
589
602
|
const result = { state: value.state, objective: contextText(value.objective, 1000), constraints: boundedContextList(value.constraints),
|
|
590
603
|
done: boundedContextList(value.done), remaining: boundedContextList(value.remaining), nextAction: value.nextAction ? actionDescriptor(value.nextAction) : null,
|
|
591
|
-
criteria, matchingContext: value.matchingContext && typeof value.matchingContext === 'object' ? boundedMatchingContext(value.matchingContext) : null, ambiguities: boundedContextList(value.ambiguities, 3), question: contextText(value.question, 500) || null, basis };
|
|
604
|
+
criteria, conversationOutcome: normalizeConversationOutcome(value.conversationOutcome), matchingContext: value.matchingContext && typeof value.matchingContext === 'object' ? boundedMatchingContext(value.matchingContext) : null, ambiguities: boundedContextList(value.ambiguities, 3), question: contextText(value.question, 500) || null, basis };
|
|
592
605
|
if (result.state === 'clear' && (!result.objective || !result.remaining.length || !result.nextAction || result.ambiguities.length || result.question)) throw new Error('Clear understanding needs an objective, remaining work, and an unambiguous next action.');
|
|
593
606
|
if (result.state === 'already_done' && (!result.objective || result.remaining.length || result.nextAction)) throw new Error('Already-finished understanding must have no remaining action.');
|
|
594
607
|
if (['clear', 'already_done'].includes(result.state) && !basis.some(item => item.source === 'user')) throw new Error('Inferred session goal needs a user-message basis.');
|
|
595
608
|
if (['ambiguous', 'context_missing'].includes(result.state) && (!result.question || result.ambiguities.length !== 1)) throw new Error('Unclear understanding needs one focused question.');
|
|
596
609
|
return result;
|
|
597
610
|
}
|
|
611
|
+
function normalizeConversationOutcome(value) {
|
|
612
|
+
if (value?.kind !== 'answer_only' || value.independentReviewRequested !== false) return null;
|
|
613
|
+
const refs = key => Array.isArray(value[key]) && value[key].length > 0 && value[key].length <= 32 &&
|
|
614
|
+
value[key].every(ref => typeof ref === 'string' && ref.length > 0 && ref.length <= 160) ? [...new Set(value[key])] : null;
|
|
615
|
+
const requestTurnRefs = refs('requestTurnRefs'), answerTurnRefs = refs('answerTurnRefs');
|
|
616
|
+
return requestTurnRefs && answerTurnRefs ? { kind: 'answer_only', independentReviewRequested: false, requestTurnRefs, answerTurnRefs } : null;
|
|
617
|
+
}
|
|
618
|
+
function conversationUnderstandingComplete(run, understanding, context) {
|
|
619
|
+
const managed = run.managed, outcome = understanding.conversationOutcome;
|
|
620
|
+
if (!managed.contextBootstrap || managed.acceptanceCriteria.length || managed.independentReviewRequired ||
|
|
621
|
+
understanding.state !== 'already_done' || !outcome || context?.proseComplete !== true || context.proseOnly !== true || context.incomplete || context.awaitingTurn ||
|
|
622
|
+
!understanding.criteria.length || understanding.criteria.some(c => c.source !== 'user_grounded' || c.check.type !== 'semantic') ||
|
|
623
|
+
managed.executionUncertain || managed.purpose?.nativeBoundary) return false;
|
|
624
|
+
if ([...(managed.workHistory || []), ...(managed.purpose?.evidence?.units || [])].some(row => row.action?.kind && row.action.kind !== 'read') ||
|
|
625
|
+
managed.purpose?.evidence?.receipts?.some(row => row.kind === 'fileChange')) return false;
|
|
626
|
+
const messages = context.messages || [], latestUser = messages.findLastIndex(row => row.role === 'user');
|
|
627
|
+
const latestAnswer = messages.findLastIndex(row => row.role === 'assistant');
|
|
628
|
+
if (latestUser < 0 || latestAnswer <= latestUser || !outcome.requestTurnRefs.includes(messages[latestUser].turnRef) ||
|
|
629
|
+
!outcome.answerTurnRefs.includes(messages[latestAnswer].turnRef)) return false;
|
|
630
|
+
if (outcome.requestTurnRefs.some(ref => !messages.some(row => row.role === 'user' && row.turnRef === ref)) ||
|
|
631
|
+
outcome.answerTurnRefs.some(ref => !messages.some((row, index) => index > latestUser && row.role === 'assistant' && row.turnRef === ref && row.text?.trim()))) return false;
|
|
632
|
+
return understanding.criteria.every(criterion => {
|
|
633
|
+
const basis = understanding.basis[criterion.basisIndex];
|
|
634
|
+
return basis?.source === 'user' && outcome.requestTurnRefs.includes(basis.turnRef) && basis.snippet &&
|
|
635
|
+
messages.some(row => row.role === 'user' && row.turnRef === basis.turnRef && row.text.includes(basis.snippet));
|
|
636
|
+
});
|
|
637
|
+
}
|
|
598
638
|
function recordPurposeReview(managed, review, now) {
|
|
599
639
|
const purpose = managed.purpose;
|
|
600
640
|
if (searchEnabled(managed)) reconcileRequirements(managed, review, now);
|
|
@@ -790,15 +830,83 @@ export function pendingAcceptance(run) {
|
|
|
790
830
|
if (enrollment && (enrollment.objective !== run.objective || enrollment.acceptanceDigest !== digest(JSON.stringify(run.managed.acceptanceCriteria)) ||
|
|
791
831
|
JSON.stringify(enrollment.deliveryLevel) !== JSON.stringify(run.managed.deliveryLevel || null))) return [{ id: 'enrollment', description: 'Original objective, level and acceptance contract',
|
|
792
832
|
check: 'manual', detail: 'Enrollment changed unexpectedly; stop and inspect before enrolling a revised task' }];
|
|
793
|
-
|
|
833
|
+
const pending = effectiveAcceptanceCriteria(run.managed).filter(item => !Object.hasOwn(run.managed.verified, item.id))
|
|
794
834
|
.map(item => ({ id: item.id, description: item.description, check: item.check.type,
|
|
795
835
|
detail: run.managed.criterionObservations?.[item.id]?.observed_result || (item.check.type === 'manual' ? 'Explicit user verification required' : 'Independent check not yet passed') }));
|
|
836
|
+
// File predicates and a worker claim cannot waive an explicit review.
|
|
837
|
+
// The host checkpoint path supplies an independently bound result when available.
|
|
838
|
+
if (run.managed?.independentReviewRequired && !searchEnabled(run.managed) && run.managed.liveOutcomeReview?.passed !== true) pending.push({
|
|
839
|
+
id: 'independent-review', description: 'Required independent outcome review', check: 'semantic',
|
|
840
|
+
detail: 'Independent outcome review has not passed; file checks alone cannot complete this task' });
|
|
841
|
+
return pending;
|
|
796
842
|
}
|
|
797
843
|
function acceptancePendingReason(run) {
|
|
798
844
|
return `Acceptance verification pending: ${pendingAcceptance(run).map(item => `${item.id} (${item.description}): ${item.detail}`).join('; ')}`;
|
|
799
845
|
}
|
|
846
|
+
// Only the host review executor calls this; worker checkpoints cannot supply a
|
|
847
|
+
// passing verdict. Recheck the enrollment, holds, and actual workspace at commit.
|
|
848
|
+
export async function settleLiveOutcomeReview({ root, provider, session, snapshot, review }) {
|
|
849
|
+
return updateRecord(root, provider, session, async record => {
|
|
850
|
+
if (!liveOutcomeReviewEligible(record) || record.run.id !== snapshot.run.id || record.run.revision !== snapshot.run.revision || record.run.controlRevision !== snapshot.run.controlRevision ||
|
|
851
|
+
JSON.stringify(record.run.managed.enrollment) !== JSON.stringify(snapshot.run.managed.enrollment) ||
|
|
852
|
+
hasPendingUserStop(root, record) || !(await providerLinkState(root, provider)).connected) return record;
|
|
853
|
+
const run = record.run, managed = run.managed;
|
|
854
|
+
const owner = managed.liveHookOwner;
|
|
855
|
+
if (owner && await processIdentity(owner.pid) !== owner.identity) return record;
|
|
856
|
+
if (review.inspectionAvailable !== false && await workspaceIdentity(taskDirectory(record)).catch(() => null) !== review.workspaceDigest)
|
|
857
|
+
review = { passed: false, inspectionAvailable: false, summary: 'Workspace changed during independent review; reread the current outcome before retrying.', findings: [], source: 'controller_review_unavailable', checkedAt: Date.now() };
|
|
858
|
+
managed.liveOutcomeReview = review;
|
|
859
|
+
if (review.nativeRequest) {
|
|
860
|
+
managed.nativeOutcomeReview = review.nativeRequest;
|
|
861
|
+
const plan = missingAcceptanceDiagnosis(managed, [{ id: 'native-independent-review', description: review.summary }]);
|
|
862
|
+
plan.alternatives[0].action.summary = `Use native Agent ${review.nativeRequest.agentType} with the exact managed.nativeOutcomeReview.prompt from simy delivery status. No extra context, fork, resume, model override or tools. Wait for its result, then submit a completed checkpoint. Keep the original budget. If unavailable, report not_run; do not retry or launch another Claude process.`;
|
|
863
|
+
plan.diagnosis = review.summary;
|
|
864
|
+
recordDecision(managed, plan, Date.now());
|
|
865
|
+
run.state = 'active';
|
|
866
|
+
run.checkpoint = { ...run.checkpoint, status: 'in_progress', reportedStatus: 'completed', acceptanceMet: false, nextStep: plan.alternatives[0].action.summary };
|
|
867
|
+
run.nextStep = run.checkpoint.nextStep;
|
|
868
|
+
return record;
|
|
869
|
+
}
|
|
870
|
+
if (review.budgetBoundary) {
|
|
871
|
+
// Do not spend the original worker allowance on retrying an executor that
|
|
872
|
+
// cannot share its cap. Keep acceptance pending at the genuine Stop.
|
|
873
|
+
run.reason = review.summary;
|
|
874
|
+
return record;
|
|
875
|
+
}
|
|
876
|
+
if (review.passed) {
|
|
877
|
+
for (const criterion of effectiveAcceptanceCriteria(managed)) if (criterion.check.type === 'semantic')
|
|
878
|
+
managed.verified[criterion.id] = { source: 'independent_claude_review', checkedAt: Date.now(), workspaceDigest: review.workspaceDigest };
|
|
879
|
+
await verifyAutomaticCriteria(record, Date.now());
|
|
880
|
+
// Keep the original writer lease until its genuine Stop boundary.
|
|
881
|
+
if (!pendingAcceptance(run).length) { run.state = 'active'; run.reason = 'Independent review passed; awaiting original Stop readback.'; }
|
|
882
|
+
} else if (managed.autonomyPolicy && managed.decision) {
|
|
883
|
+
const missing = [{ id: 'independent-review', description: review.summary }];
|
|
884
|
+
const plan = missingAcceptanceDiagnosis(managed, missing);
|
|
885
|
+
plan.diagnosis = [review.summary, ...(review.findings || [])].join(' ').slice(0, 4000);
|
|
886
|
+
if (review.inspectionAvailable === false) {
|
|
887
|
+
plan.alternatives[0].action.summary = 'Inspect independent review availability and missing observations without changing the deliverable';
|
|
888
|
+
plan.alternatives[0].expectedVerification = 'Identify the concrete observation or provider failure and an authorized way to retry verification of the original outcome.';
|
|
889
|
+
plan.diagnosis = `Independent inspection unavailable; no product defect has been established. ${plan.diagnosis}`;
|
|
890
|
+
run.failures += 1;
|
|
891
|
+
}
|
|
892
|
+
recordDecision(managed, plan, Date.now());
|
|
893
|
+
run.state = 'active';
|
|
894
|
+
run.checkpoint = { ...run.checkpoint, status: 'in_progress', reportedStatus: 'completed', acceptanceMet: false, nextStep: plan.alternatives[0].action.summary };
|
|
895
|
+
run.nextStep = run.checkpoint.nextStep; managed.remainingWork = plan.diagnosis;
|
|
896
|
+
}
|
|
897
|
+
return record;
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
export function liveOutcomeReviewEligible(record) {
|
|
901
|
+
const run = record?.run, m = run?.managed;
|
|
902
|
+
return Boolean(record?.provider === 'claude' && m?.capability === 'live_hook' && !searchEnabled(m) &&
|
|
903
|
+
run.state === 'verification_pending' && run.checkpoint?.status === 'completed' &&
|
|
904
|
+
(m.independentReviewRequired || effectiveAcceptanceCriteria(m).some(c => c.check.type === 'semantic')) &&
|
|
905
|
+
!m.liveOutcomeReview && !m.pendingApproval && !m.nativeWait && !m.executionUncertain && !m.purpose?.nativeBoundary &&
|
|
906
|
+
!managedBudgetReason(run) && automaticContinuationAllowed(record));
|
|
907
|
+
}
|
|
800
908
|
export function deliveryAcceptanceContext(run) {
|
|
801
|
-
return
|
|
909
|
+
return `${run.managed?.capability === 'live_hook' && run.managed?.thread?.provider === 'claude' ? 'When the requested artifact is ready or repaired, submit a completed work checkpoint to request independent review. This does not grant acceptance. If nativeOutcomeReview.prompt is absent, submit that checkpoint first to obtain a fresh review prompt; never reuse a pre-repair prompt. ' : ''}Original objective: ${run.managed?.enrollment?.objective || run.objective}. Mode: ${run.managed ? 'managed acceptance tracking' : 'standard checkpoint tracking; completion reports are not verified acceptance'}.` +
|
|
802
910
|
`${run.managed?.deliveryLevel ? ` Frozen delivery level: ${JSON.stringify(run.managed.deliveryLevel)}.` : ''}` +
|
|
803
911
|
` 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.`;
|
|
804
912
|
}
|
|
@@ -1065,7 +1173,8 @@ export function queuePurposeDiagnosis(run, reason, key, now = Date.now(), recove
|
|
|
1065
1173
|
export const informationWaitInstruction = 'Use blocked/missing_information only with a nonempty question field containing a concrete question that requires the user\'s answer. Missing verification evidence, zero applicable SQM rules/scenarios, or unfinished checks alone are remaining work: inspect applicability and available evidence, then propose authorized repair or verification. Keep unmet acceptance criteria unverified; do not invent rules or lower acceptance. If investigation finds a real user-only choice or unavailable input, ask that specific question. Preserve native permissions, explicit stops, refusals and budgets.';
|
|
1066
1174
|
const informationWaitDiagnosis = 'Read the current task context and available verification evidence to determine whether progress really requires a user answer. Identify an authorized next investigation or verification step, or return blocked/missing_information with the specific user question. Do not repeat prior side effects or claim that incomplete verification passed.';
|
|
1067
1175
|
|
|
1068
|
-
export async function checkpointDelivery({ root = rootDirectory(), provider, session, cwd = process.cwd(), checkpoint, expectedLease, toolEvidence = [], sourceContext, controllerEvidence, now = Date.now() }) {
|
|
1176
|
+
export async function checkpointDelivery({ root = rootDirectory(), provider, session, cwd = process.cwd(), checkpoint, expectedLease, expectedNativeHandoff, toolEvidence = [], sourceContext, controllerEvidence, now = Date.now() }) {
|
|
1177
|
+
if (expectedNativeHandoff && checkpoint?.kind) throw new Error('Native handoff requires an ordinary action checkpoint.');
|
|
1069
1178
|
if (checkpoint?.kind === 'action_association') return checkpointActionAssociation({ root, provider, session, cwd, checkpoint, expectedLease, now });
|
|
1070
1179
|
if (checkpoint?.kind === 'session_understanding') return checkpointUnderstanding({ root, provider, session, cwd, checkpoint, expectedLease, sourceContext, controllerEvidence, now });
|
|
1071
1180
|
try { cwd = await realpath(cwd); } catch { throw new Error('No matching active Delivery Loop.'); }
|
|
@@ -1081,6 +1190,16 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
1081
1190
|
let nextStep = text(checkpoint.nextStep);
|
|
1082
1191
|
let repairPlan = null;
|
|
1083
1192
|
if (!run || !checkpointDirectoryMatches(record, cwd)) throw new Error('No matching active Delivery Loop.');
|
|
1193
|
+
if (expectedNativeHandoff) {
|
|
1194
|
+
const h = run.managed?.nativeHandoff, expected = expectedNativeHandoff;
|
|
1195
|
+
if (h?.id !== expected.id || h.turnId !== expected.turnId || h.phase !== 'finished' || h.turnStatus !== 'completed' || run.id !== expected.runId ||
|
|
1196
|
+
h.decisionId !== expected.decisionId || h.decisionId !== run.managed.decision?.id || run.revision !== expected.revision ||
|
|
1197
|
+
run.controlRevision !== expected.controlRevision || record.autoContinueEpoch !== expected.epoch ||
|
|
1198
|
+
!record.autoContinue || !automaticContinuationAllowed(record) || hasPendingUserStop(root, record) ||
|
|
1199
|
+
run.stopReason || run.state !== 'recovery_required') throw new Error('Native handoff changed; checkpoint discarded.');
|
|
1200
|
+
if (h.checkpointRevision !== undefined) return record;
|
|
1201
|
+
run.state = 'active';
|
|
1202
|
+
}
|
|
1084
1203
|
if (run.managed?.phase === 'understanding' || run.managed?.phase === 'needs_clarification') throw new Error('Session understanding needs its separate structured checkpoint.');
|
|
1085
1204
|
if (expectedLease && (run.state !== 'auto_resuming' || run.managed?.resumeLease?.id !== expectedLease || run.revision !== run.managed.resumeLease.checkpointRevision)) throw new Error('Managed recovery lease changed; checkpoint discarded.');
|
|
1086
1205
|
if (TERMINAL.has(run.state) || run.state === 'blocked' || run.state === 'recovery_required') throw new Error('Loop is stopped or awaiting a human resume.');
|
|
@@ -1266,6 +1385,10 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
1266
1385
|
...(remainingWork ? { remainingWork } : {}), ...(incompleteResult ? { reportedStatus: checkpoint.status } : {}),
|
|
1267
1386
|
evidence, acceptanceMet: !unsupportedWait && !incompleteResult && checkpoint.acceptanceMet === true };
|
|
1268
1387
|
if (!run.managed && effectiveStatus !== 'completed') delete run.completionCheck;
|
|
1388
|
+
if (run.managed?.liveOutcomeReview) {
|
|
1389
|
+
delete run.managed.liveOutcomeReview;
|
|
1390
|
+
for (const criterion of effectiveAcceptanceCriteria(run.managed)) if (criterion.check.type === 'semantic') delete run.managed.verified[criterion.id];
|
|
1391
|
+
}
|
|
1269
1392
|
run.revision += 1; run.updatedAt = now;
|
|
1270
1393
|
recordManagedOutcomeProgress(run, now);
|
|
1271
1394
|
// File predicates were reread above; a prior semantic verdict can belong
|
|
@@ -1296,6 +1419,14 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
1296
1419
|
sourceId: expectedLease || `checkpoint:${run.revision}`, attemptId: expectedLease,
|
|
1297
1420
|
resultRef: `checkpoint:${run.revision}`, now }));
|
|
1298
1421
|
notifyState(run, now);
|
|
1422
|
+
if (expectedNativeHandoff) {
|
|
1423
|
+
run.managed.nativeHandoff.checkpointRevision = run.revision;
|
|
1424
|
+
const attempt = run.managed.executionAttempt;
|
|
1425
|
+
if (attempt?.decisionId === expectedNativeHandoff.decisionId) {
|
|
1426
|
+
attempt.phase = 'checkpointed'; attempt.checkpointRevision = run.revision;
|
|
1427
|
+
}
|
|
1428
|
+
if (run.state === 'active') { run.state = 'recovery_required'; run.recoveryCause = 'session_supervision'; }
|
|
1429
|
+
}
|
|
1299
1430
|
return record;
|
|
1300
1431
|
});
|
|
1301
1432
|
return settled;
|
|
@@ -1326,6 +1457,16 @@ export function externalReviewIsCurrent(managed,review,now) {
|
|
|
1326
1457
|
.every(link=>observations.some(item=>item.id===link.ref && item.status==='observed' && currentPurposeObservation(item,now,review.observationWindow)));
|
|
1327
1458
|
|
|
1328
1459
|
}
|
|
1460
|
+
function verifiedConversationResult(managed, review) {
|
|
1461
|
+
const inspection = review.semanticInspection;
|
|
1462
|
+
if (inspection?.source !== 'controller_conversation_answer' || inspection.digest !== review.answerContext?.digest ||
|
|
1463
|
+
!/^[a-f0-9]{64}$/.test(inspection.digest || '') || !review.inspectionPerformed) return null;
|
|
1464
|
+
const answer = (managed.purpose?.evidence?.receipts || []).filter(row => row.kind === 'agentMessage' && row.ref?.startsWith('answer:')).at(-1);
|
|
1465
|
+
if (!answer || answer.ref !== inspection.answerRefs?.at(-1) || answer.id !== inspection.answerDigest ||
|
|
1466
|
+
answer.observation?.truncated || answer.observation?.redacted || typeof answer.observation?.text !== 'string' ||
|
|
1467
|
+
createHash('sha256').update(answer.observation.text).digest('hex') !== inspection.answerDigest) return null;
|
|
1468
|
+
return answer.observation.text;
|
|
1469
|
+
}
|
|
1329
1470
|
export async function settlePurposeVerification({ root, provider, session, expectedLease, review, verifySnapshot, now = Date.now() }) {
|
|
1330
1471
|
const started = Date.now(), initialNow = now;
|
|
1331
1472
|
const snapshot = (await listRecords(root)).find(record => record.provider === provider && record.session === session);
|
|
@@ -1426,7 +1567,7 @@ export async function settlePurposeVerification({ root, provider, session, expec
|
|
|
1426
1567
|
if (review.passed && !pending.length && !managed.purpose.nativeBoundary && !managed.executionUncertain && pendingAcceptance(run).length === 0) {
|
|
1427
1568
|
for (const unit of evidenceLedger(managed).units || []) if (unit.status === 'observed' && unit.criterionIds.every(id => managed.verified[id])) unit.status = 'verified';
|
|
1428
1569
|
run.state = 'completed'; run.reason = review.summary; state.phase = 'completed';
|
|
1429
|
-
run.checkpoint = { status: 'completed', summary: review.summary, evidence: review.criteria.map(c => c.evidence) };
|
|
1570
|
+
run.checkpoint = { status: 'completed', summary: verifiedConversationResult(managed, review) || review.summary, evidence: review.criteria.map(c => c.evidence) };
|
|
1430
1571
|
bindControllerCompletion(run, now);
|
|
1431
1572
|
managed.autoRecovery = false;
|
|
1432
1573
|
settleCompletionHandoffs(run, now, review);
|
|
@@ -1497,6 +1638,7 @@ async function checkpointUnderstanding({ root, provider, session, cwd, checkpoin
|
|
|
1497
1638
|
const result = checkpoint.actionResult;
|
|
1498
1639
|
if (decision?.action.kind !== 'read' || result?.decisionId !== decision.id || !['observed', 'failed', 'not_run'].includes(result.status) || !text(result.summary, 300)) throw new Error('Understanding result must match the selected read decision.');
|
|
1499
1640
|
const understanding = sessionUnderstanding(checkpoint.sessionUnderstanding);
|
|
1641
|
+
const conversationalCompletion = Boolean(expectedLease) && conversationUnderstandingComplete(run, understanding, sourceContext);
|
|
1500
1642
|
if (['clear', 'already_done'].includes(understanding.state) && result.status !== 'observed') throw new Error('Clear understanding requires an observed read result.');
|
|
1501
1643
|
const plan = understanding.state === 'clear' ? assessedPlan(checkpoint.plan, run.managed) : null;
|
|
1502
1644
|
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.');
|
|
@@ -1548,8 +1690,16 @@ async function checkpointUnderstanding({ root, provider, session, cwd, checkpoin
|
|
|
1548
1690
|
run.managed.phase = 'active';
|
|
1549
1691
|
run.state = 'reported_done';
|
|
1550
1692
|
run.reason = 'Original conversation reports no remaining work; independent completion was not verified.';
|
|
1551
|
-
if (
|
|
1552
|
-
|
|
1693
|
+
if (conversationalCompletion) {
|
|
1694
|
+
// These are inferred expectations, not a user-enrolled verification
|
|
1695
|
+
// contract. Preserve them in sessionUnderstanding without promoting
|
|
1696
|
+
// them into new audit obligations or claiming independent acceptance.
|
|
1697
|
+
run.reason = 'The requested conversational answer was checked against its user context; no independent acceptance was requested.';
|
|
1698
|
+
run.managed.autoRecovery = false;
|
|
1699
|
+
run.checkpoint = { ...run.checkpoint, status: 'completed', summary: checkpoint.summary };
|
|
1700
|
+
bindControllerCompletion(run, now);
|
|
1701
|
+
} else if (purposeEnabled(run)) {
|
|
1702
|
+
if (!run.managed.acceptanceCriteria.length) run.managed.acceptanceCriteria = understanding.criteria.filter(item => item.source === 'user_grounded').map(item => ({ id: item.id, description: item.description,
|
|
1553
1703
|
check: item.check.type === 'file_equals' ? { type: 'file_equals', exactPath: item.check.path, text: item.check.text } : item.check }));
|
|
1554
1704
|
if (searchEnabled(run.managed)) queuePurposeVerification(run, 'Original conversation reports completion.', now);
|
|
1555
1705
|
else queuePurposeDiagnosis(run, 'The conversation reports completion. Check the original purpose against actual artifacts and outcomes before accepting that claim.', 'reported_completion', now);
|
|
@@ -1583,6 +1733,11 @@ function newVerifiedCriterion(run, id, now) {
|
|
|
1583
1733
|
}
|
|
1584
1734
|
export async function verifyAutomaticCriteria(record, now) {
|
|
1585
1735
|
const managed = record.run.managed;
|
|
1736
|
+
if (managed.liveOutcomeReview?.passed && managed.liveOutcomeReview.workspaceDigest !== await workspaceIdentity(taskDirectory(record)).catch(() => null)) {
|
|
1737
|
+
managed.liveOutcomeReview = { ...managed.liveOutcomeReview, passed: false, summary: 'Workspace changed after independent review; inspect the current result.' };
|
|
1738
|
+
delete managed.nativeOutcomeReview;
|
|
1739
|
+
for (const criterion of effectiveAcceptanceCriteria(managed)) if (criterion.check.type === 'semantic') delete managed.verified[criterion.id];
|
|
1740
|
+
}
|
|
1586
1741
|
managed.criterionObservations = {};
|
|
1587
1742
|
const observe = (criterion, passed, detail, sha256 = null) => {
|
|
1588
1743
|
managed.criterionObservations[criterion.id] = { criterion_id: criterion.id, passed,
|
|
@@ -2154,20 +2309,52 @@ async function handleHookState({ input, provider, root = rootDirectory(), now =
|
|
|
2154
2309
|
run.reason = 'Prior live action stopped without a settled checkpoint; explicitly resume read-only result diagnosis.';
|
|
2155
2310
|
}
|
|
2156
2311
|
}
|
|
2312
|
+
if (event === 'Stop' && !suppressDispatch && automaticContinuationAllowed(record) &&
|
|
2313
|
+
canDiagnoseLegacyLocalEffect(run, provider, now)) {
|
|
2314
|
+
const latestLink = await providerLinkState(root, provider);
|
|
2315
|
+
if (latestLink.connected && latestLink.epoch === link.epoch) {
|
|
2316
|
+
recordDecision(run.managed, researchWaitDiagnosis(run.managed), now);
|
|
2317
|
+
run.state = 'active'; run.reason = 'The inconsistent local write remains unexecuted; inspect its assessment first.';
|
|
2318
|
+
run.nextStep = run.managed.decision.action.summary;
|
|
2319
|
+
run.checkpoint = null; run.consumedRevision = run.revision;
|
|
2320
|
+
run.managed.escalation = null;
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2157
2323
|
if (!run || TERMINAL.has(run.state)) return record;
|
|
2158
2324
|
if (event === 'Interrupt') { run.state = 'stopped'; run.reason = 'User interrupted the provider.'; }
|
|
2159
2325
|
else if (run.state === 'auto_resuming' && ['Stop', 'SessionEnd', 'StopFailure'].includes(event)) {
|
|
2160
2326
|
// The supervisor owns this exact attempt and consumes its structured
|
|
2161
2327
|
// final result once. Provider lifecycle hooks must not race that lease.
|
|
2162
2328
|
}
|
|
2163
|
-
else if (
|
|
2329
|
+
else if (run.state !== 'paused' && (event === 'PermissionRequest' || event === 'Notification' && ['permission_prompt', 'elicitation_dialog'].includes(input.notification_type))) {
|
|
2330
|
+
// A native permission wait is still authoritative while independent
|
|
2331
|
+
// acceptance is pending; lifecycle bookkeeping must not hide it.
|
|
2332
|
+
run.state = 'blocked'; run.blocker = 'authorization'; run.reason = 'Resolve the provider approval or input request, then explicitly resume Delivery Loop.';
|
|
2333
|
+
}
|
|
2334
|
+
else if (['paused', 'verification_pending'].includes(run.state) || run.state === 'blocked' &&
|
|
2335
|
+
(run.managed?.dependencyWait || run.managed?.pendingApproval || run.managed?.nativeWait ||
|
|
2336
|
+
['authorization', 'execution_uncertain'].includes(run.blocker))) {
|
|
2164
2337
|
// A provider lifecycle event cannot override an explicit pause or a
|
|
2165
2338
|
// completion candidate awaiting independent acceptance checks.
|
|
2166
2339
|
}
|
|
2167
|
-
else if (event === '
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2340
|
+
else if (event === 'SessionEnd') {
|
|
2341
|
+
const managed = run.managed, lastAttempt = managed?.attempts?.at(-1);
|
|
2342
|
+
// Ending the native writer must not erase a previously authorized retry.
|
|
2343
|
+
// A matching undispatched busy receipt also recovers this queue when an
|
|
2344
|
+
// older hook already replaced its cause with session_end.
|
|
2345
|
+
const undispatchedBusy = lastAttempt?.outcome === 'busy' && lastAttempt.id === managed?.executionAttempt?.id &&
|
|
2346
|
+
managed.executionAttempt.phase === 'claimed' && !managed.executionAttempt.dispatchedAt;
|
|
2347
|
+
const queued = provider === 'codex' && record.autoContinue === true && automaticContinuationAllowed(record) &&
|
|
2348
|
+
managed?.resumeChannel === 'codex_app_server' && managed.autoRecovery === true &&
|
|
2349
|
+
!managed.pendingApproval && !managed.executionUncertain && run.state === 'recovery_required' &&
|
|
2350
|
+
(['session_supervision', 'approved_action'].includes(run.recoveryCause) || run.recoveryCause === 'session_end' && undispatchedBusy);
|
|
2351
|
+
if (queued) {
|
|
2352
|
+
// Keep nextAttemptAt, the selected decision, approvals and all budgets.
|
|
2353
|
+
if (run.recoveryCause === 'session_end') run.recoveryCause = 'session_supervision';
|
|
2354
|
+
run.reason = 'The native session ended. Previously authorized automatic continuation is waiting for the writer lock to become available.';
|
|
2355
|
+
} else {
|
|
2356
|
+
run.state = 'recovery_required'; run.recoveryCause = 'session_end'; run.reason = 'Provider session ended. Check why it ended before resuming.';
|
|
2357
|
+
}
|
|
2171
2358
|
} else if (event === 'StopFailure') {
|
|
2172
2359
|
run.failures += 1;
|
|
2173
2360
|
run.state = failureLimitReached(run) ? 'failed' : 'recovery_required'; run.recoveryCause = 'provider_error';
|
|
@@ -2178,6 +2365,10 @@ async function handleHookState({ input, provider, root = rootDirectory(), now =
|
|
|
2178
2365
|
// turn. Lease settlement and runtime observation above still run.
|
|
2179
2366
|
const latestLink = await providerLinkState(root, provider);
|
|
2180
2367
|
if (!automaticContinuationAllowed(record) || !latestLink.connected || latestLink.epoch !== link.epoch) return record;
|
|
2368
|
+
// Preserve the reason for a held action, including uncertainty observed
|
|
2369
|
+
// while settling this Stop's writer lease. Acceptance is not a wait release.
|
|
2370
|
+
if (run.state !== 'active' || run.managed?.nativeWait || run.managed?.purpose?.nativeBoundary ||
|
|
2371
|
+
run.managed?.pendingApproval && run.checkpoint?.status === 'completed') return record;
|
|
2181
2372
|
// Codex turn IDs make re-delivered Stop events idempotent. Claude has no
|
|
2182
2373
|
// stable event ID; never dedupe on text (identical text can be a new turn).
|
|
2183
2374
|
const key = input.turn_id ? `${input.turn_id}:${Boolean(input.stop_hook_active)}:${run.revision}` : null;
|
|
@@ -2185,7 +2376,18 @@ async function handleHookState({ input, provider, root = rootDirectory(), now =
|
|
|
2185
2376
|
if (run.managed && run.checkpoint?.status === 'completed' && run.revision > run.consumedRevision) {
|
|
2186
2377
|
await verifyAutomaticCriteria(record, now);
|
|
2187
2378
|
const pending = pendingAcceptance(run);
|
|
2188
|
-
|
|
2379
|
+
const missingFiles = pending.filter(item => ['file_exists', 'file_contains', 'file_equals'].includes(item.check) || run.managed.liveOutcomeReview?.passed === false && item.check === 'semantic');
|
|
2380
|
+
if (missingFiles.length && run.managed.autonomyPolicy && !run.managed.pendingApproval &&
|
|
2381
|
+
!run.managed.nativeWait && !run.managed.purpose?.nativeBoundary && !run.managed.executionUncertain) {
|
|
2382
|
+
// Completion reads at checkpoint time can become stale before Stop.
|
|
2383
|
+
// Use the same assessed diagnosis as a failed checkpoint predicate;
|
|
2384
|
+
// a new file mismatch is useful work, not human acceptance waiting.
|
|
2385
|
+
recordDecision(run.managed, missingAcceptanceDiagnosis(run.managed, missingFiles), now);
|
|
2386
|
+
run.checkpoint = { ...run.checkpoint, status: 'in_progress', reportedStatus: 'completed', acceptanceMet: false,
|
|
2387
|
+
nextStep: `Diagnose failed acceptance checks: ${missingFiles.map(item => item.id).join(', ')}` };
|
|
2388
|
+
run.nextStep = run.checkpoint.nextStep;
|
|
2389
|
+
run.managed.remainingWork = run.nextStep;
|
|
2390
|
+
} else if (pending.length) {
|
|
2189
2391
|
run.state = 'verification_pending'; run.reason = acceptancePendingReason(run);
|
|
2190
2392
|
run.lastStopKey = key; notifyState(run, now); return record;
|
|
2191
2393
|
}
|
|
@@ -2302,7 +2504,7 @@ export function deliveryDecisionRevision(run) {
|
|
|
2302
2504
|
return Number.isSafeInteger(revision) ? revision : undefined;
|
|
2303
2505
|
}
|
|
2304
2506
|
export async function controlDelivery({ root = rootDirectory(), provider, session, taskId, expectedRunId, expectedRevision, approvalId, autonomyPolicy, continuationPolicy, questionId, waitId, answer, purposeUpdate, resumeObservation, action, inspectProcess = processIdentity, now = Date.now() }) {
|
|
2305
|
-
|
|
2507
|
+
const updated = await updateRecord(root, provider, session, async record => {
|
|
2306
2508
|
const run = record?.run;
|
|
2307
2509
|
if (!run) throw new Error('Delivery Loop not found.');
|
|
2308
2510
|
if ((expectedRunId !== undefined || expectedRevision !== undefined) &&
|
|
@@ -2413,7 +2615,7 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
|
|
|
2413
2615
|
else if (action === 'pause') {
|
|
2414
2616
|
const queuedAppServer = run.state === 'recovery_required' && ['session_supervision', 'approved_action'].includes(run.recoveryCause) &&
|
|
2415
2617
|
run.managed?.resumeChannel === 'codex_app_server' && run.managed.autoRecovery;
|
|
2416
|
-
if (!run.managed || !['active', 'auto_resuming'].includes(run.state) && !queuedAppServer) throw new Error('Only a running or queued managed task can pause.');
|
|
2618
|
+
if (!run.managed || !['active', 'auto_resuming'].includes(run.state) && !queuedAppServer && !liveOutcomeReviewEligible(record)) throw new Error('Only a running or queued managed task can pause.');
|
|
2417
2619
|
if (queuedAppServer || run.state === 'auto_resuming' && run.managed.resumeChannel === 'codex_app_server') run.managed.pausedRecoveryCause = run.recoveryCause || 'session_supervision';
|
|
2418
2620
|
run.state = 'paused'; run.reason = 'Paused by user.';
|
|
2419
2621
|
run.stopReason = { actor: 'user', code: 'explicit_pause', at: now };
|
|
@@ -2534,7 +2736,8 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
|
|
|
2534
2736
|
run.checkpoint = { status: 'in_progress', summary: run.reason, nextStep: run.nextStep, evidence: [], acceptanceMet: false };
|
|
2535
2737
|
run.revision += 1;
|
|
2536
2738
|
run.managed.approvedAction = { id: pending.id, nextStep: run.nextStep, action: { kind: pending.kind, scope: pending.scope, summary: pending.summary } };
|
|
2537
|
-
if (
|
|
2739
|
+
if (run.managed.resumeChannel === 'codex_app_server' &&
|
|
2740
|
+
(['read', 'local_edit', 'local_test'].includes(pending.kind) || searchEnabled(run.managed) && pending.kind === 'external' && run.managed.decision?.assessment?.reversibility === 'reversible')) run.managed.autoRecovery = true;
|
|
2538
2741
|
} else {
|
|
2539
2742
|
pending.denied = true; run.reason = `User rejected ${pending.scope}.`; run.blocker = 'authorization';
|
|
2540
2743
|
rememberRejectedAction(run.managed, pending);
|
|
@@ -2546,6 +2749,10 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
|
|
|
2546
2749
|
}
|
|
2547
2750
|
run.updatedAt = now; notifyState(run, now); return commitControl();
|
|
2548
2751
|
});
|
|
2752
|
+
if (['stop', 'pause'].includes(action) && updated?.run?.managed?.nativeHandoff) {
|
|
2753
|
+
void import('../../../session-supervision/native-handoff.js').then(({ reconcileNativeHandoff }) => reconcileNativeHandoff({ root, record: updated })).catch(() => {});
|
|
2754
|
+
}
|
|
2755
|
+
return updated;
|
|
2549
2756
|
}
|
|
2550
2757
|
export async function reconcileRecords({ root = rootDirectory(), now = Date.now(), inspectProcess = processIdentity, records: suppliedRecords } = {}) {
|
|
2551
2758
|
const records = suppliedRecords || await listRecords(root);
|
|
@@ -34,14 +34,32 @@ export function searchState(managed) {
|
|
|
34
34
|
const actionKey = action => hash([action.kind, action.scope, bounded(action.summary).replace(/\s+/g, ' ')]);
|
|
35
35
|
const experimentContext = run => hash([run.managed.verifiedRevision, run.managed.purpose.evidence?.current?.digest || null]);
|
|
36
36
|
|
|
37
|
+
// Actual failed operations survive changes to the worker's prose. Keep only a
|
|
38
|
+
// digest here: captured output may contain sensitive data and is not authority.
|
|
39
|
+
function failedOperationKey(toolEvidence) {
|
|
40
|
+
const operations = [];
|
|
41
|
+
for (const item of toolEvidence || []) {
|
|
42
|
+
if (item?.source !== 'codex_tool_event' || item.kind !== 'commandExecution' ||
|
|
43
|
+
!Number.isInteger(item.exitCode) || item.exitCode === 0 || item.observation?.truncated) continue;
|
|
44
|
+
let captured;
|
|
45
|
+
try { captured = JSON.parse(item.observation?.text); } catch { continue; }
|
|
46
|
+
const command = item.command || captured.command;
|
|
47
|
+
if (typeof command !== 'string' || typeof captured.output !== 'string') continue;
|
|
48
|
+
operations.push([command, item.exitCode, captured.output]);
|
|
49
|
+
}
|
|
50
|
+
return operations.length ? hash(operations) : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
// Compact history survives the short conversational window and process restart.
|
|
38
54
|
// It is a search hint, never a permission or an acceptance receipt.
|
|
39
55
|
export function repeatedSearchAction(run) {
|
|
40
56
|
if (!searchEnabled(run.managed)) return null;
|
|
41
57
|
const action = run.managed.decision?.action;
|
|
42
58
|
if (!action || !['read', 'local_edit', 'local_test'].includes(action.kind)) return null;
|
|
43
|
-
|
|
44
|
-
|
|
59
|
+
const state = searchState(run.managed), context = experimentContext(run);
|
|
60
|
+
return state.experimentIndex?.find(item => item.context === context && item.failures >= 2 &&
|
|
61
|
+
(item.actionKey === actionKey(action) || item.observedOperation && item.lastCycle === state.cycle &&
|
|
62
|
+
item.action.kind === action.kind && item.action.scope === action.scope)) || null;
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
// These are leads for the next experiment, not completion or authority receipts.
|
|
@@ -50,9 +68,10 @@ export function recordSearchExperiment(run, performed, result, review, toolEvide
|
|
|
50
68
|
const state = searchState(run.managed);
|
|
51
69
|
state.cycle += 1;
|
|
52
70
|
state.experimentIndex ||= [];
|
|
53
|
-
const
|
|
71
|
+
const observedOperation = result.status === 'failed' ? failedOperationKey(toolEvidence) : null;
|
|
72
|
+
const key = observedOperation ? hash([performed.action.kind, performed.action.scope, observedOperation]) : actionKey(performed.action), context = experimentContext(run);
|
|
54
73
|
const prior = state.experimentIndex.find(item => item.actionKey === key && item.context === context);
|
|
55
|
-
const entry = prior || { actionKey: key, context, action: performed.action, attempts: 0, failures: 0, firstCycle: state.cycle };
|
|
74
|
+
const entry = prior || { actionKey: key, context, ...(observedOperation ? { observedOperation } : {}), action: performed.action, attempts: 0, failures: 0, firstCycle: state.cycle };
|
|
56
75
|
entry.attempts += 1;
|
|
57
76
|
entry.failures = result.status === 'failed' ? entry.failures + 1 : 0;
|
|
58
77
|
Object.assign(entry, { lastCycle: state.cycle, result: result.status, finding: bounded(result.summary, 300), refutation: bounded(review?.refutation, 300) });
|