@awak-app/simy-cli 0.7.28 → 0.7.31
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 +88 -3
- package/package.json +4 -4
- package/src/agent.js +1 -0
- package/src/cli-contract.js +1 -1
- package/src/cli-observability.js +1 -1
- package/src/codex-plugins/simy/.codex-plugin/plugin.json +5 -3
- package/src/codex-plugins/simy/hooks/acceptance-policy.mjs +7 -0
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +570 -65
- package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +6 -3
- package/src/codex-plugins/simy/hooks/purpose-search.mjs +23 -4
- package/src/codex-plugins/simy/hooks/scene-reasoning.mjs +2 -0
- package/src/codex-plugins/simy/hooks/update-dispatch-gate.mjs +126 -4
- package/src/codex-plugins/simy/skills/continue/SKILL.md +69 -0
- package/src/codex-plugins/simy/skills/continue/agents/openai.yaml +6 -0
- package/src/delivery-hooks/claude-workflow.md +3 -3
- package/src/delivery-hooks/command.js +12 -0
- package/src/delivery-hooks/desktop.js +31 -9
- package/src/delivery-hooks/supervisor.js +3 -2
- package/src/desktop-access.js +4 -1
- package/src/desktop-executor.js +6 -0
- package/src/desktop-host.js +494 -72
- package/src/native-development-report-completion.js +26 -0
- package/src/native-development-reports.js +15 -5
- package/src/orchestrator/budget.js +24 -0
- package/src/orchestrator/charter-repair.js +1 -1
- package/src/orchestrator/completion-contract.js +8 -2
- package/src/orchestrator/contract.js +3 -1
- package/src/orchestrator/independent-audit.js +13 -6
- package/src/orchestrator/instruction.js +1 -1
- package/src/orchestrator/loop.js +34 -4
- package/src/provider-stream.js +9 -2
- package/src/runner.js +44 -21
- package/src/session-supervision/action-deadline.js +18 -1
- package/src/session-supervision/autonomous-upgrade.js +4 -2
- package/src/session-supervision/claude-desktop-archive.js +57 -0
- package/src/session-supervision/claude-desktop-native.js +50 -0
- package/src/session-supervision/claude-desktop-native.jxa.js +195 -0
- package/src/session-supervision/claude-desktop-target.js +87 -0
- package/src/session-supervision/claude-outcome-review.js +132 -0
- package/src/session-supervision/claude-owner-renewal.js +197 -0
- package/src/session-supervision/claude-review-definition.js +5 -0
- package/src/session-supervision/claude-sessions.js +210 -40
- package/src/session-supervision/codex-app-server.js +189 -36
- package/src/session-supervision/continuation-analysis.js +7 -7
- package/src/session-supervision/continuation-command.js +45 -0
- package/src/session-supervision/controller-store.js +48 -0
- package/src/session-supervision/conversation-presentation.js +241 -0
- package/src/session-supervision/decision-presentation.js +96 -35
- package/src/session-supervision/execution-context.js +39 -0
- package/src/session-supervision/index.js +38 -9
- package/src/session-supervision/manual-acceptance-command.js +30 -0
- package/src/session-supervision/notification-authority.js +9 -0
- package/src/session-supervision/notification-journal.js +40 -0
- package/src/session-supervision/operation-journal.js +42 -0
- package/src/session-supervision/owner-transport.js +146 -0
- package/src/session-supervision/purpose-recovery.js +10 -2
- package/src/session-supervision/purpose-verification.js +103 -9
- package/src/session-supervision/remote-control.js +1 -1
- package/src/session-supervision/remote-entry.js +2 -0
- package/src/session-supervision/remote-errors.js +15 -0
- package/src/session-supervision/remote-hosts.js +344 -36
- package/src/session-supervision/remote-metadata.js +103 -0
- package/src/session-supervision/remote-session-list.js +2 -4
- package/src/session-supervision/session-archive-command.js +45 -0
- package/src/session-supervision/session-list.js +40 -4
- package/src/session-supervision/sqm-presentation.js +46 -3
- package/src/session-supervision/stop-notifications.js +42 -11
- package/src/session-supervision/work-state.js +106 -6
- package/src/sqm/agent-skill.js +3 -3
- package/src/sqm/claude-plugin.js +7 -2
- package/src/sqm/history-store.js +6 -0
- package/src/team-threads.js +1 -1
- package/src/update-detachment/coordinator.js +7 -1
- package/src/update-detachment/history-recovery.js +18 -0
- package/src/update-detachment/ledger-adapter.js +1 -0
- package/src/update-detachment/retained-paused.js +290 -0
- package/src/update-detachment/runtime.js +78 -15
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { effectiveAcceptanceCriteria } from "./codex-plugins/simy/hooks/acceptance-policy.mjs";
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
NATIVE_DEVELOPMENT_REPORT_COMPLETIONS_PATH,
|
|
@@ -141,3 +142,28 @@ function canonicalJson(value) {
|
|
|
141
142
|
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
142
143
|
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
143
144
|
}
|
|
145
|
+
|
|
146
|
+
// v2: completion is a machine-observed controller outcome, not a request for
|
|
147
|
+
// human certification or a particular acceptance-document format. This never
|
|
148
|
+
// promotes provider reported_done or process exit to completed.
|
|
149
|
+
export function projectControllerCompletion(record) {
|
|
150
|
+
const run = record?.run;
|
|
151
|
+
const acceptance = run?.managed?.acceptanceCriteria;
|
|
152
|
+
if (acceptance !== undefined && (!Array.isArray(acceptance) ||
|
|
153
|
+
acceptance.some(criterion => !criterion || typeof criterion !== 'object' || Array.isArray(criterion)))) return null;
|
|
154
|
+
if (effectiveAcceptanceCriteria(run?.managed).some(criterion => criterion.check?.type === 'manual' &&
|
|
155
|
+
!projectCriterion(criterion, run.managed.verified, run.managed.criterionObservations, 0))) return null;
|
|
156
|
+
const taskId = run?.managed?.taskId || run?.id;
|
|
157
|
+
if (run?.managed?.thread && (run.managed.thread.provider !== record.provider || run.managed.thread.id !== record.session)) return null;
|
|
158
|
+
const recordedChecks = Object.values(run?.managed?.verified || {}).map(item => typeof item.checkedAt === 'number' ? item.checkedAt : Date.parse(item.checkedAt)).filter(Number.isFinite);
|
|
159
|
+
const checkedAt = normalizeIso(run?.completedAt ?? run?.updatedAt ?? run?.checkpoint?.at ?? (recordedChecks.length ? Math.max(...recordedChecks) : undefined));
|
|
160
|
+
if (!['codex', 'claude'].includes(record?.provider) || !UUID.test(record?.session || '') ||
|
|
161
|
+
run?.state !== 'completed' || run?.checkpoint?.status !== 'completed' ||
|
|
162
|
+
run?.recoveryCause === 'process_exit' || !UUID.test(taskId || '') || !checkedAt ||
|
|
163
|
+
!Number.isSafeInteger(run.revision) || run.revision < 0) return null;
|
|
164
|
+
const criteria = [{ id_hash: digest('controller-outcome'), source: 'controller_result', checked_at: checkedAt,
|
|
165
|
+
evidence_digest: digest({ taskId, revision: run.revision, checkpoint: run.checkpoint }) }];
|
|
166
|
+
return { provider: record.provider, provider_session_id: record.session.toLowerCase(), task_id: taskId.toLowerCase(),
|
|
167
|
+
managed_revision: run.revision, checkpoint_digest: digest(run.checkpoint), acceptance_digest: digest(criteria),
|
|
168
|
+
verified_at: checkedAt, criteria };
|
|
169
|
+
}
|
|
@@ -18,14 +18,16 @@ export class NativeDevelopmentReportsError extends Error {
|
|
|
18
18
|
|
|
19
19
|
export function normalizeNativeDevelopmentReportEnrollment(value) {
|
|
20
20
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw invalidResponse();
|
|
21
|
-
const keys = Object.keys(value).sort();
|
|
21
|
+
const keys = Object.keys(value).filter(key => key !== 'host_id').sort();
|
|
22
22
|
const expected = ["enabled", "provider", "provider_session_id", "request_title", "revision"];
|
|
23
23
|
if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) throw invalidResponse();
|
|
24
24
|
if (!PROVIDERS.has(value.provider) || !UUID.test(value.provider_session_id) ||
|
|
25
25
|
typeof value.request_title !== "string" || value.request_title.length > 160 || CONTROL.test(value.request_title) ||
|
|
26
26
|
typeof value.enabled !== "boolean" || !Number.isSafeInteger(value.revision) || value.revision < 0 ||
|
|
27
27
|
(value.enabled && value.revision === 0)) throw invalidResponse();
|
|
28
|
+
if (value.host_id !== undefined && (typeof value.host_id !== 'string' || !/^(local|remote-ssh-discovered:[a-zA-Z0-9][a-zA-Z0-9._@-]{0,127})$/.test(value.host_id))) throw invalidResponse();
|
|
28
29
|
return Object.freeze({
|
|
30
|
+
...(value.host_id !== undefined ? { host_id: value.host_id } : {}),
|
|
29
31
|
provider: value.provider,
|
|
30
32
|
provider_session_id: value.provider_session_id.toLowerCase(),
|
|
31
33
|
request_title: value.request_title,
|
|
@@ -36,7 +38,7 @@ export function normalizeNativeDevelopmentReportEnrollment(value) {
|
|
|
36
38
|
|
|
37
39
|
export function normalizeNativeDevelopmentReportSave(value) {
|
|
38
40
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new NativeDevelopmentReportsError("Invalid development report request.", { code: "invalid_native_development_report_request" });
|
|
39
|
-
const keys = Object.keys(value).sort();
|
|
41
|
+
const keys = Object.keys(value).filter(key => key !== 'host_id').sort();
|
|
40
42
|
const expected = ["enabled", "expected_revision", "provider", "provider_session_id", "request_title"];
|
|
41
43
|
if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) throw new NativeDevelopmentReportsError("Invalid development report request.", { code: "invalid_native_development_report_request" });
|
|
42
44
|
if (!PROVIDERS.has(value.provider) || !UUID.test(value.provider_session_id) || typeof value.enabled !== "boolean" ||
|
|
@@ -46,7 +48,9 @@ export function normalizeNativeDevelopmentReportSave(value) {
|
|
|
46
48
|
if (requestTitle.length > 160 || CONTROL.test(requestTitle) || (value.enabled && requestTitle.length === 0)) {
|
|
47
49
|
throw new NativeDevelopmentReportsError("Invalid development report title.", { code: "invalid_native_development_report_request" });
|
|
48
50
|
}
|
|
51
|
+
if (value.host_id !== undefined && (typeof value.host_id !== 'string' || !/^(local|remote-ssh-discovered:[a-zA-Z0-9][a-zA-Z0-9._@-]{0,127})$/.test(value.host_id))) throw invalidResponse();
|
|
49
52
|
return Object.freeze({
|
|
53
|
+
...(value.host_id !== undefined ? { host_id: value.host_id } : {}),
|
|
50
54
|
provider: value.provider,
|
|
51
55
|
provider_session_id: value.provider_session_id.toLowerCase(),
|
|
52
56
|
request_title: requestTitle,
|
|
@@ -90,11 +94,17 @@ export function createNativeDevelopmentReportsApi({ webOrigin, readSession, fetc
|
|
|
90
94
|
const session = await sessionForRequest();
|
|
91
95
|
let response;
|
|
92
96
|
try {
|
|
93
|
-
|
|
97
|
+
const url = webApiUrl(path, {
|
|
94
98
|
webOrigin, apiBaseUrl: session.api_base_url,
|
|
95
|
-
})
|
|
99
|
+
});
|
|
100
|
+
// Older dev gateways forward the query but omit protocol headers. Keep
|
|
101
|
+
// negotiation explicit without requiring a production gateway rollout.
|
|
102
|
+
if (url.pathname.startsWith('/api/local-cli-gateway/')) {
|
|
103
|
+
url.searchParams.set('native_report_version', '2');
|
|
104
|
+
}
|
|
105
|
+
response = await fetchImpl(url, {
|
|
96
106
|
method,
|
|
97
|
-
headers: webApiHeaders({ token: session.token }, body === undefined ? {} : { "Content-Type": "application/json" }),
|
|
107
|
+
headers: webApiHeaders({ token: session.token }, { "x-simy-native-report-version": "2", ...(body === undefined ? {} : { "Content-Type": "application/json" }) }),
|
|
98
108
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
99
109
|
signal: AbortSignal.timeout(10_000),
|
|
100
110
|
cache: "no-store",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { completeProviderUsage, extractProviderTokenUsage } from '../provider-stream.js';
|
|
1
2
|
import { clampAttempts } from "./shared.js";
|
|
2
3
|
import { progressLimits } from "../continuation-policy.js";
|
|
3
4
|
|
|
@@ -94,6 +95,9 @@ export function budgetState(snapshot, pendingAttempt = null) {
|
|
|
94
95
|
(sum, attempt) => sum + tokenUsageTotal(attempt?.token_usage),
|
|
95
96
|
0,
|
|
96
97
|
) + unaccountedProviderUsage(snapshot, attempts);
|
|
98
|
+
const usageUnknown = Object.values(snapshot?.metadata?.provider_usage_checkpoints || {}).some(
|
|
99
|
+
checkpoint => ['pending', 'unknown'].includes(checkpoint?.usage_status),
|
|
100
|
+
);
|
|
97
101
|
const attemptsUsed = attempts.length;
|
|
98
102
|
const retriesUsed = Math.max(attemptsUsed - 1, 0);
|
|
99
103
|
const policy = progressLimits(snapshot?.charter);
|
|
@@ -108,6 +112,7 @@ export function budgetState(snapshot, pendingAttempt = null) {
|
|
|
108
112
|
schema_version: 1,
|
|
109
113
|
token_budget: tokenBudget,
|
|
110
114
|
tokens_used: tokensUsed,
|
|
115
|
+
usage_unknown: usageUnknown,
|
|
111
116
|
tokens_remaining: Math.max(tokenBudget - tokensUsed, 0),
|
|
112
117
|
token_exhausted: tokenExhausted,
|
|
113
118
|
retry_budget: retryBudget,
|
|
@@ -148,8 +153,27 @@ export function refreshBudgetState(snapshot, pendingAttempt = null) {
|
|
|
148
153
|
return state;
|
|
149
154
|
}
|
|
150
155
|
|
|
156
|
+
// Re-read only host-persisted output from the same native invocation. Missing
|
|
157
|
+
// receipts stay unknown; a new call or an estimate cannot clear this hold.
|
|
158
|
+
export function reconcileProviderUsage(snapshot) {
|
|
159
|
+
for (const [id, checkpoint] of Object.entries(snapshot?.metadata?.provider_usage_checkpoints || {})) {
|
|
160
|
+
if (!['pending', 'unknown'].includes(checkpoint?.usage_status) || checkpoint.invocation_id !== id || !['claude', 'codex'].includes(checkpoint.backend)) continue;
|
|
161
|
+
const results = Object.values(snapshot.metadata.dispatch_results || {}).filter(result => result?.invocationId === id);
|
|
162
|
+
if (results.length !== 1 || results[0].result_parse?.channel !== 'provider_final') continue;
|
|
163
|
+
const events = (results[0].logs || []).flatMap(line => {
|
|
164
|
+
try { const event = JSON.parse(line); return checkpoint.backend === 'claude' ? event.type === 'result' ? [event] : [] : event.type === 'turn.completed' ? [event] : []; } catch { return []; }
|
|
165
|
+
});
|
|
166
|
+
if (!events.length || checkpoint.backend === 'claude' && events.length !== 1 || events.some(event => !completeProviderUsage(checkpoint.backend, event))) continue;
|
|
167
|
+
checkpoint.token_usage = { records: events.map(event => ({ ...extractProviderTokenUsage(checkpoint.backend, event),
|
|
168
|
+
invocation_id: id, phase: checkpoint.phase, backend: checkpoint.backend })) };
|
|
169
|
+
checkpoint.usage_status = 'observed';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
151
173
|
export function canStartProvider(snapshot, { attemptNumber, phase = "executor" } = {}) {
|
|
174
|
+
reconcileProviderUsage(snapshot);
|
|
152
175
|
const budget = refreshBudgetState(snapshot);
|
|
176
|
+
if (budget.usage_unknown) return { allowed: false, reason: 'provider_usage_unknown', budget };
|
|
153
177
|
if (budget.token_exhausted) {
|
|
154
178
|
return { allowed: false, reason: "provider_token_ceiling_reached", budget };
|
|
155
179
|
}
|
|
@@ -58,7 +58,7 @@ export async function repairCharterWithAI({ snapshot, execute, shouldStop = () =
|
|
|
58
58
|
const execution = await execute({ phase, backend: charter.audit_backend, charter: structuredClone(charter), instruction, ...extra });
|
|
59
59
|
const id = execution?.invocationId || `${round.id}:${phase}`;
|
|
60
60
|
snapshot.metadata.provider_usage_checkpoints ??= {};
|
|
61
|
-
snapshot.metadata.provider_usage_checkpoints[id] = { invocation_id: id, phase: 'reviewer', token_usage: execution?.tokenUsage || {} };
|
|
61
|
+
snapshot.metadata.provider_usage_checkpoints[id] = { invocation_id: id, phase: 'reviewer', token_usage: execution?.tokenUsage || {}, ...snapshot.metadata.provider_usage_checkpoints[id] };
|
|
62
62
|
await onUpdate(snapshot);
|
|
63
63
|
if (shouldStop()) throw new Error('Charter repair stopped.');
|
|
64
64
|
return execution;
|
|
@@ -135,6 +135,10 @@ export function completionEvidenceChecks(charter, attempt) {
|
|
|
135
135
|
|
|
136
136
|
const rows = acceptanceCriterionRows(charter);
|
|
137
137
|
const criterionEvidence = normalizeCriterionEvidence(attempt?.criterion_evidence);
|
|
138
|
+
const outcome = attempt?.completion_result || attempt?.raw_output?.completion_result;
|
|
139
|
+
// The worker can describe its actual end-to-end check once. Dedicated rows
|
|
140
|
+
// are optional; a concrete failed row always defeats a broad success claim.
|
|
141
|
+
const wholeOutcome = hasUserOutcomeProof({ user_outcome: outcome });
|
|
138
142
|
const known = new Set(rows.map(row => row.id));
|
|
139
143
|
const checks = [check("criterion_evidence_scope", "Results belong to the requested behavior",
|
|
140
144
|
criterionEvidence.every(row => known.has(row.criterion_id)), "Do not substitute an unrelated result for the request.")];
|
|
@@ -147,7 +151,7 @@ export function completionEvidenceChecks(charter, attempt) {
|
|
|
147
151
|
`criterion_evidence_${row.id.toLowerCase()}`,
|
|
148
152
|
`${row.id} has passing completion evidence`,
|
|
149
153
|
Boolean(
|
|
150
|
-
matches.length === 1 &&
|
|
154
|
+
matches.length === 0 ? wholeOutcome : matches.length === 1 &&
|
|
151
155
|
evidence?.passed &&
|
|
152
156
|
evidence.verification_method &&
|
|
153
157
|
evidence.observed_result,
|
|
@@ -158,7 +162,9 @@ export function completionEvidenceChecks(charter, attempt) {
|
|
|
158
162
|
? `${evidence.verification_method || "method missing"}: ${
|
|
159
163
|
evidence.observed_result || "observed result missing"
|
|
160
164
|
}; evidence ${evidence.evidence_refs.join(", ") || "missing"}.`
|
|
161
|
-
:
|
|
165
|
+
: wholeOutcome
|
|
166
|
+
? `${outcome.verification_method}: ${outcome.observed_result}`
|
|
167
|
+
: `The requested behavior has not been verified: ${row.statement}`,
|
|
162
168
|
evidence?.evidence_refs || [],
|
|
163
169
|
),
|
|
164
170
|
);
|
|
@@ -28,7 +28,9 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
|
|
|
28
28
|
const charterContext = record(request.charter_context);
|
|
29
29
|
const classifiedRisk = inheritRisk(classifyRisk(request), record(charterContext.risk));
|
|
30
30
|
const risk = { ...classifiedRisk, requires_design_review: false,
|
|
31
|
-
required_controls: classifiedRisk.required_controls.filter(control => control !== "design_review"
|
|
31
|
+
required_controls: classifiedRisk.required_controls.filter(control => control !== "design_review" &&
|
|
32
|
+
(control !== "independent_ai_audit" || request.independent_review_required === true) &&
|
|
33
|
+
(control !== "human_peer_approval" || request.require_human_approval === true || charterContext.require_human_approval === true)) };
|
|
32
34
|
const inheritedDesignReview = record(charterContext.design_review);
|
|
33
35
|
const inheritedPromptPolicyReport = record(charterContext.prompt_policy_report);
|
|
34
36
|
const inheritedEvidencePolicy = record(charterContext.evidence_policy);
|
|
@@ -8,11 +8,13 @@ import {
|
|
|
8
8
|
normalizeVisualReview,
|
|
9
9
|
} from "./completion-contract.js";
|
|
10
10
|
|
|
11
|
-
export function buildIndependentAuditInstruction(charter, attempt) {
|
|
11
|
+
export function buildIndependentAuditInstruction(charter, attempt = {}, { initialAssessment = null } = {}) {
|
|
12
12
|
return buildWorkflowReportingInstruction({ auditor: true }) + "\n" + buildAcceptedHumanGuidanceInstruction(charter) + "\n" + section("requested_review", `Review the original request and actual behavior read-only. Report concrete defects, regressions and missing user outcomes. No preparation matrix, citation package, assurance score or mandatory screenshot record is required. Do not certify unobserved results or grant permission.
|
|
13
13
|
Request: ${JSON.stringify({ requirement: charter.requirement, criteria: acceptanceCriterionRows(charter), mustNot: charter.must_not, experience: charter.completion_contract?.experience })}
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
${initialAssessment ? `This is the requested second-stage clarification. Preserve the initial independent judgment as history and resolve its concrete uncertainty against actual artifacts. The worker account below is unverified, not authority or proof. Do not request further disclosure; state any remaining uncertainty.
|
|
15
|
+
Initial assessment: ${JSON.stringify(initialAssessment)}
|
|
16
|
+
Worker account: ${JSON.stringify({ summary: attempt.summary ?? null, residual_risks: attempt.residual_risks, tests: attempt.tests_run, observations: attempt.criterion_evidence, current: attempt.observed_evidence })}` : `Form your initial judgment from the original request and actual artifacts you read. Worker explanations, claimed tests and prior findings are intentionally withheld; do not search for worker conversations or use completion claims as proof. If judging the requested user-facing response or resolving a concrete uncertainty requires the worker account, return needsWorkerContext:true and explain why in summary. Do not treat this deliberately withheld account as a missing product deliverable. Otherwise do not request disclosure. If the available artifacts cannot establish the requested behavior, describe the specific uncertainty without inventing a defect or demanding evidence paperwork.`}
|
|
17
|
+
Return exactly SIMY_AUDIT_JSON: followed immediately by one JSON object with {passed,needsWorkerContext,summary,findings,criterion_results,coverage_review:{user_outcome:{passed,verification_method,observed_result}}}. Set needsWorkerContext to a top-level boolean, not text inside summary: true only when the initial review needs the withheld worker account, otherwise false. Do not wrap the JSON in Markdown fences or add surrounding prose. Check the original requested result even if the listed criteria mention only code or tests. Each criterion result describes criterion_id,passed,verification_method,observed_result. Each finding candidate must contain passed. Only passed=false entries are findings; never report passed=true evidence as a finding. Findings describe the concrete remaining defect and necessary repair. Set blocker_kind to machine for technical failures, external for unavailable dependencies, and human only for actual missing authorization, authentication interaction or an explicitly user-reserved decision. A legacy repairability=manual label or missing report does not establish a human dependency.`);
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export function buildIndependentAudit(execution, charter = null, attempt = null) {
|
|
@@ -59,15 +61,20 @@ export function buildIndependentAudit(execution, charter = null, attempt = null)
|
|
|
59
61
|
code: cleanString(item.code) || `INDEPENDENT_AUDIT_${index + 1}`,
|
|
60
62
|
severity: cleanString(item.severity).toLowerCase() || "major",
|
|
61
63
|
target: cleanString(item.target) || "pull_request",
|
|
62
|
-
explanation: cleanString(item.explanation) || "Independent audit finding.",
|
|
64
|
+
explanation: cleanString(item.explanation) || cleanString(item.detail) || cleanString(item.description) || cleanString(item.title) || "Independent audit finding.",
|
|
63
65
|
repairability: item.blocker_kind === "human" ? "manual" : "auto",
|
|
64
66
|
disposition: ["defer", "dismiss"].includes(item.disposition) && cleanString(item.explanation)
|
|
65
67
|
? item.disposition : "fix",
|
|
66
68
|
blocker_kind: ["machine", "human", "external"].includes(item.blocker_kind)
|
|
67
69
|
? item.blocker_kind : "machine",
|
|
68
|
-
auto_fix_hint: cleanString(item.auto_fix_hint) || null,
|
|
70
|
+
auto_fix_hint: cleanString(item.auto_fix_hint) || cleanString(item.repair) || null,
|
|
69
71
|
}));
|
|
70
72
|
const findings = evaluations.filter((item) => item.passed === false);
|
|
73
|
+
if (result.needsWorkerContext === true) findings.push({ passed: false,
|
|
74
|
+
code: 'INDEPENDENT_AUDIT_UNAVAILABLE', severity: 'blocker', target: 'independent_audit',
|
|
75
|
+
explanation: cleanString(result.summary) || 'The reviewer still needs context to judge the requested result.',
|
|
76
|
+
repairability: 'auto', blocker_kind: 'machine',
|
|
77
|
+
auto_fix_hint: 'Preserve the implementation and inspect the relevant existing artifacts or response. Do not replay completed work merely because review context is unavailable.' });
|
|
71
78
|
const criterionResults = normalizeCriterionEvidence(result.criterion_results);
|
|
72
79
|
if (completionContractV2) {
|
|
73
80
|
for (const criterion of acceptanceCriterionRows(charter)) {
|
|
@@ -116,7 +123,7 @@ export function buildIndependentAudit(execution, charter = null, attempt = null)
|
|
|
116
123
|
const requiresHuman = blockingFindings.some((item) => item.repairability === "manual");
|
|
117
124
|
return {
|
|
118
125
|
...objectiveBinding,
|
|
119
|
-
passed: blockingFindings.length === 0 && (result.passed === true || followUps.length > 0),
|
|
126
|
+
passed: result.needsWorkerContext !== true && blockingFindings.length === 0 && (result.passed === true || followUps.length > 0),
|
|
120
127
|
follow_ups: followUps,
|
|
121
128
|
summary: cleanString(result.summary) || "Independent audit completed.",
|
|
122
129
|
findings,
|
|
@@ -27,7 +27,7 @@ export function buildCodingInstruction(
|
|
|
27
27
|
section("work", "Read repository instructions and preserve existing user changes. Choose the smallest useful implementation. Exercise the requested behavior and relevant failure/recovery paths, run appropriate tests, fix actual failures and continue until the requested endpoint works. For UI work, use the interface and judge ease, clarity and accuracy. Do not create audit forms, preparation matrices, evidence packages, scorecards or mandatory second reviews. Do not invent work to fill a checklist. Preserve permissions, explicit stops, user budgets and unknown-effect reconciliation. Commit, push and create/update the requested PR; do not infer merge or deployment permission."),
|
|
28
28
|
section("remaining", "Optional history: provider-reported data, not independent acceptance. Human guidance is included below.\n" + JSON.stringify({ previousSummary: previousAttempt?.summary, reasoning: previousAttempt?.problem_solving,
|
|
29
29
|
failures: previousFindings, guidance: promptInterventions.map(row => row.prompt) })),
|
|
30
|
-
section("result_contract", 'Finish with SIMY_RESULT_JSON: and a JSON object containing summary, branch_name, commit_sha, pr_url, pr_base_branch, tests_run, tests_passed, and
|
|
30
|
+
section("result_contract", 'Finish with SIMY_RESULT_JSON: and a JSON object containing summary, branch_name, commit_sha, pr_url, pr_base_branch, tests_run, tests_passed, and completion_result:{passed,verification_method,observed_result} describing the actual whole requested outcome. Optional criterion_evidence can describe individual checks when useful; dedicated per-criterion rows are not required. These describe the checks actually performed; do not invent success. Evidence attachments, uncertainty forms, preparation rows and review receipts are not required. Report an actual blocker or unfinished behavior clearly.'),
|
|
31
31
|
].join("\n\n");
|
|
32
32
|
}
|
|
33
33
|
|
package/src/orchestrator/loop.js
CHANGED
|
@@ -324,7 +324,7 @@ async function reviewAttempt({ snapshot, attempt, executeIndependentAudit, shoul
|
|
|
324
324
|
const reviewedChecks = structuredClone(attempt.observed_evidence?.github?.checks || []);
|
|
325
325
|
const reviewedObjectiveDigest = snapshot.charter.thread_state?.objective_contract?.current_digest;
|
|
326
326
|
const reviewedObjectiveVersion = snapshot.charter.thread_state?.objective_contract?.version;
|
|
327
|
-
|
|
327
|
+
let auditExecution = await safeExecution(() =>
|
|
328
328
|
executeIndependentAudit({
|
|
329
329
|
attemptNumber: attempt.attempt_number,
|
|
330
330
|
backend: snapshot.charter.audit_backend,
|
|
@@ -333,13 +333,35 @@ async function reviewAttempt({ snapshot, attempt, executeIndependentAudit, shoul
|
|
|
333
333
|
attempt,
|
|
334
334
|
}),
|
|
335
335
|
);
|
|
336
|
+
const initialAssessment = structuredClone(auditExecution.result || null);
|
|
337
|
+
attempt.token_usage = mergeTokenUsage(attempt.token_usage, auditExecution.tokenUsage);
|
|
338
|
+
attempt.budget = refreshBudgetState(snapshot);
|
|
336
339
|
if (shouldStop()) { await stopCodingLoop(snapshot, onUpdate); return true; }
|
|
340
|
+
// Disclose the worker account only after a real initial judgment asks for it.
|
|
341
|
+
// Each stage remains read-only and consumes the existing provider budget.
|
|
342
|
+
if (auditExecution.exitCode === 0 && !auditExecution.error &&
|
|
343
|
+
typeof initialAssessment?.passed === 'boolean' && Array.isArray(initialAssessment.findings) &&
|
|
344
|
+
initialAssessment.needsWorkerContext === true) {
|
|
345
|
+
attempt.independent_audit = { ...buildIndependentAudit(auditExecution, snapshot.charter, attempt), initial_assessment: initialAssessment };
|
|
346
|
+
const budget = canStartProvider(snapshot, { attemptNumber: attempt.attempt_number, phase: 'reviewer' });
|
|
347
|
+
if (!budget.allowed) { await stopForBudget({ snapshot, attempt, reason: budget.reason, onUpdate }); return true; }
|
|
348
|
+
await publish(snapshot, 'independent_auditing', onUpdate);
|
|
349
|
+
if (shouldStop()) { await stopCodingLoop(snapshot, onUpdate); return true; }
|
|
350
|
+
auditExecution = await safeExecution(() => executeIndependentAudit({
|
|
351
|
+
attemptNumber: attempt.attempt_number, backend: snapshot.charter.audit_backend,
|
|
352
|
+
instruction: buildIndependentAuditInstruction(snapshot.charter, attempt, { initialAssessment }),
|
|
353
|
+
charter: snapshot.charter, attempt, reviewStage: 'context',
|
|
354
|
+
}));
|
|
355
|
+
attempt.token_usage = mergeTokenUsage(attempt.token_usage, auditExecution.tokenUsage);
|
|
356
|
+
attempt.budget = refreshBudgetState(snapshot);
|
|
357
|
+
if (shouldStop()) { await stopCodingLoop(snapshot, onUpdate); return true; }
|
|
358
|
+
}
|
|
337
359
|
attempt.independent_audit = buildIndependentAudit(
|
|
338
360
|
auditExecution,
|
|
339
361
|
snapshot.charter,
|
|
340
362
|
attempt,
|
|
341
363
|
);
|
|
342
|
-
attempt.
|
|
364
|
+
attempt.independent_audit.initial_assessment = initialAssessment;
|
|
343
365
|
attempt.budget = refreshBudgetState(snapshot);
|
|
344
366
|
recordBudgetWarnings(snapshot, attempt);
|
|
345
367
|
// Finish local evidence processing even when this completed review crossed
|
|
@@ -730,12 +752,14 @@ async function stopForBudget({ snapshot, attempt, reason, onUpdate }) {
|
|
|
730
752
|
? findings
|
|
731
753
|
: [...findings, finding],
|
|
732
754
|
auto_repairable: false,
|
|
733
|
-
requires_human:
|
|
755
|
+
requires_human: reason !== 'provider_usage_unknown',
|
|
734
756
|
budget,
|
|
735
757
|
};
|
|
736
758
|
if (attempt) attempt.budget = budget;
|
|
737
759
|
const message =
|
|
738
|
-
reason ===
|
|
760
|
+
reason === 'provider_usage_unknown'
|
|
761
|
+
? 'Original provider consumption is unverified; reconcile the existing invocation receipt before another paid call.'
|
|
762
|
+
: reason === "provider_token_ceiling_reached"
|
|
739
763
|
? "Agentic loop reached its Provider token safety ceiling before the next call."
|
|
740
764
|
: "Agentic loop reached its attempt safety ceiling before the next call.";
|
|
741
765
|
appendEvent(snapshot, "blocked", message, {
|
|
@@ -749,6 +773,12 @@ async function stopForBudget({ snapshot, attempt, reason, onUpdate }) {
|
|
|
749
773
|
}
|
|
750
774
|
|
|
751
775
|
function budgetFinding(reason, budget) {
|
|
776
|
+
if (reason === 'provider_usage_unknown') return {
|
|
777
|
+
passed: false, code: 'PROVIDER_USAGE_UNKNOWN', severity: 'blocker', target: 'provider_token_budget',
|
|
778
|
+
explanation: 'The dispatched provider did not return complete usage. Preserve its result and reconcile the original usage receipt; do not replay completed work or increase the ceiling.',
|
|
779
|
+
repairability: 'auto', blocker_kind: 'machine',
|
|
780
|
+
auto_fix_hint: 'Inspect the original provider result and usage transport. Resume paid calls only after its consumption is established.',
|
|
781
|
+
};
|
|
752
782
|
if (reason === "provider_token_ceiling_reached") {
|
|
753
783
|
return {
|
|
754
784
|
passed: false,
|
package/src/provider-stream.js
CHANGED
|
@@ -69,6 +69,13 @@ export function providerTerminalError(backend, event) {
|
|
|
69
69
|
return null;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
export function completeProviderUsage(backend, event) {
|
|
73
|
+
if (!(backend === 'claude' && event?.type === 'result' || backend === 'codex' && event?.type === 'turn.completed')) return false;
|
|
74
|
+
const u = event.usage;
|
|
75
|
+
if (!u || ![u.input_tokens ?? u.inputTokens, u.output_tokens ?? u.outputTokens].every(value => Number.isSafeInteger(value) && value >= 0)) return false;
|
|
76
|
+
return backend !== 'claude' || [u.cache_read_input_tokens ?? u.cacheReadInputTokens, u.cache_creation_input_tokens ?? u.cacheCreationInputTokens].every(value => Number.isSafeInteger(value) && value >= 0);
|
|
77
|
+
}
|
|
78
|
+
|
|
72
79
|
export function extractProviderTokenUsage(backend, event, { model = "" } = {}) {
|
|
73
80
|
if (!event || typeof event !== "object") return null;
|
|
74
81
|
if (backend === "claude") {
|
|
@@ -264,7 +271,7 @@ function normalizeCodexUsage(usage, model) {
|
|
|
264
271
|
if (!usage || typeof usage !== "object") return null;
|
|
265
272
|
const tokensIn = tokenNumber(usage.input_tokens ?? usage.inputTokens);
|
|
266
273
|
const tokensOut = tokenNumber(usage.output_tokens ?? usage.outputTokens);
|
|
267
|
-
if (tokensIn + tokensOut <= 0) return null;
|
|
274
|
+
if (tokensIn + tokensOut <= 0 && ![usage.input_tokens ?? usage.inputTokens, usage.output_tokens ?? usage.outputTokens].every(value => value === 0)) return null;
|
|
268
275
|
const cached = tokenNumber(usage.cached_input_tokens ?? usage.cachedInputTokens);
|
|
269
276
|
return compactUsage({
|
|
270
277
|
tokens_in: tokensIn,
|
|
@@ -286,7 +293,7 @@ function normalizeClaudeUsage(usage, model) {
|
|
|
286
293
|
);
|
|
287
294
|
const tokensOut = tokenNumber(usage.output_tokens ?? usage.outputTokens);
|
|
288
295
|
const tokensIn = uncached + cacheRead + cacheCreation;
|
|
289
|
-
if (tokensIn + tokensOut <= 0) return null;
|
|
296
|
+
if (tokensIn + tokensOut <= 0 && ![usage.input_tokens ?? usage.inputTokens, usage.output_tokens ?? usage.outputTokens, usage.cache_read_input_tokens ?? usage.cacheReadInputTokens, usage.cache_creation_input_tokens ?? usage.cacheCreationInputTokens].every(value => value === 0)) return null;
|
|
290
297
|
return compactUsage({
|
|
291
298
|
tokens_in: tokensIn,
|
|
292
299
|
tokens_out: tokensOut,
|
package/src/runner.js
CHANGED
|
@@ -23,10 +23,11 @@ import {
|
|
|
23
23
|
} from "./orchestrator/execution-io.js";
|
|
24
24
|
import { summarizeCodingLoopEvent } from "./orchestrator/presentation.js";
|
|
25
25
|
import { PR_PREPARATION_CHECKS } from "./orchestrator/pr-preparation.js";
|
|
26
|
-
import { budgetState, extendProgressCeilings, increaseProviderTokenBudget, providerBudgetTokens, refreshBudgetState } from "./orchestrator/budget.js";
|
|
26
|
+
import { reconcileProviderUsage, budgetState, extendProgressCeilings, increaseProviderTokenBudget, providerBudgetTokens, refreshBudgetState } from "./orchestrator/budget.js";
|
|
27
27
|
import { appendEvent } from "./orchestrator/shared.js";
|
|
28
28
|
import {
|
|
29
29
|
createProviderStreamDecoder,
|
|
30
|
+
completeProviderUsage,
|
|
30
31
|
isNonFatalProviderDiagnostic,
|
|
31
32
|
} from "./provider-stream.js";
|
|
32
33
|
import {
|
|
@@ -215,7 +216,8 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null, loc
|
|
|
215
216
|
? charter.expected_evidence
|
|
216
217
|
: [],
|
|
217
218
|
required_checks: Array.isArray(charter.required_checks) ? charter.required_checks : [],
|
|
218
|
-
require_human_approval: charter.require_human_approval
|
|
219
|
+
require_human_approval: charter.require_human_approval === true,
|
|
220
|
+
independent_review_required: charter.independent_review_required === true,
|
|
219
221
|
must_not: Array.isArray(charter.must_not) ? charter.must_not : [],
|
|
220
222
|
proposal_id: typeof charter.proposal_id === "string" ? charter.proposal_id : null,
|
|
221
223
|
workflow_observation_binding:
|
|
@@ -872,7 +874,7 @@ async function runLocalCodingRun(
|
|
|
872
874
|
repositoryPath,
|
|
873
875
|
run,
|
|
874
876
|
marker: "SIMY_AUDIT_JSON:",
|
|
875
|
-
readOnly:
|
|
877
|
+
readOnly: true,
|
|
876
878
|
}));
|
|
877
879
|
const evidenceCollector =
|
|
878
880
|
collectEvidence ||
|
|
@@ -1082,11 +1084,12 @@ export async function executeProcessAttempt({
|
|
|
1082
1084
|
resolveCommandImpl = resolveDesktopExecutorCommand,
|
|
1083
1085
|
charter = run?.snapshot?.charter,
|
|
1084
1086
|
onExecutionComplete = null,
|
|
1085
|
-
readOnly =
|
|
1087
|
+
readOnly = marker === "SIMY_AUDIT_JSON:",
|
|
1088
|
+
reviewStage = null,
|
|
1086
1089
|
}) {
|
|
1087
1090
|
const dispatchOwner = captureDispatchOwner(run.sqmHistoryRoot || (run.localStateRoot ? path.dirname(run.localStateRoot) : undefined));
|
|
1088
1091
|
const phase = marker === "SIMY_AUDIT_JSON:" ? "reviewer" : "executor";
|
|
1089
|
-
const dispatchAttemptId = `${run.snapshot.id}:${phase}:${attemptNumber ?? run.snapshot.attempts?.length ?? 0}`;
|
|
1092
|
+
const dispatchAttemptId = `${run.snapshot.id}:${phase}:${attemptNumber ?? run.snapshot.attempts?.length ?? 0}${phase === 'reviewer' && reviewStage === 'context' ? ':context' : ''}`;
|
|
1090
1093
|
const provenance = captureDispatchProvenance({ root: dispatchOwner.root, provider: backend, worktree: repositoryPath, accountSession: run.session, origin: run.apiOrigin || undefined, taskId: run.snapshot.id, recordRef: run.localStateRoot ? path.join(run.localStateRoot, `${run.snapshot.id}.json`) : `coding-run:${run.snapshot.id}` });
|
|
1091
1094
|
assertTaskAdmission(dispatchOwner, { taskId: provenance.taskId, provenance });
|
|
1092
1095
|
const command = await resolveCommandImpl({
|
|
@@ -1096,14 +1099,20 @@ export async function executeProcessAttempt({
|
|
|
1096
1099
|
model: phase === "reviewer" ? charter?.audit_model : charter?.model,
|
|
1097
1100
|
reasoningEffort: phase === "reviewer" ? charter?.audit_reasoning_effort : charter?.reasoning_effort,
|
|
1098
1101
|
...(readOnly ? { executionKind: 'direct', permissionMode: 'read_only' } : {}),
|
|
1102
|
+
...(phase === 'reviewer' ? { independentReview: true } : {}),
|
|
1099
1103
|
});
|
|
1100
|
-
if (readOnly && command.verification === 'explicit_command_override') throw new Error('
|
|
1104
|
+
if (readOnly && command.verification === 'explicit_command_override') throw new Error('Read-only analysis needs an enforceable provider adapter; shell override is unsupported.');
|
|
1101
1105
|
if (run.stopRequested) throw new Error("Execution cancelled by the local operator.");
|
|
1102
1106
|
const startedAt = new Date().toISOString();
|
|
1103
1107
|
const stdout = [];
|
|
1104
1108
|
const stderr = [];
|
|
1105
1109
|
const tokenUsageRecords = [];
|
|
1110
|
+
reconcileProviderUsage(run.snapshot);
|
|
1106
1111
|
const initialBudget = budgetState(run.snapshot);
|
|
1112
|
+
if (initialBudget.usage_unknown) {
|
|
1113
|
+
const error = new Error('Original provider consumption is unverified. Reconcile its usage receipt before another paid call; do not replay the work or enlarge its budget.');
|
|
1114
|
+
error.code = 'provider_usage_unknown'; throw error;
|
|
1115
|
+
}
|
|
1107
1116
|
const invocationId = randomUUID();
|
|
1108
1117
|
const observationSender =
|
|
1109
1118
|
phase === "executor" &&
|
|
@@ -1121,6 +1130,7 @@ export async function executeProcessAttempt({
|
|
|
1121
1130
|
let settled = false;
|
|
1122
1131
|
let terminalError = null;
|
|
1123
1132
|
let terminalErrorCode = null;
|
|
1133
|
+
let usageTerminals = 0, incompleteUsage = false;
|
|
1124
1134
|
const checkpointWrites = [];
|
|
1125
1135
|
const child = dispatchWithFence(dispatchOwner, { taskId: run.snapshot.id, attemptId: dispatchAttemptId, provenance }, () => command.spawn(command.bin, command.args, {
|
|
1126
1136
|
cwd: repositoryPath,
|
|
@@ -1134,12 +1144,36 @@ export async function executeProcessAttempt({
|
|
|
1134
1144
|
detached: process.platform !== "win32",
|
|
1135
1145
|
}));
|
|
1136
1146
|
run.child = child;
|
|
1147
|
+
// This receipt belongs to an actual native dispatch under the existing
|
|
1148
|
+
// ceiling. Partial observations cannot establish the invocation's total.
|
|
1149
|
+
const trackUsage = Number.isSafeInteger(child.pid) && child.pid > 0 &&
|
|
1150
|
+
Number.isSafeInteger(run.snapshot.charter.provider_token_ceiling);
|
|
1151
|
+
const checkpointUsage = status => {
|
|
1152
|
+
if (!trackUsage && initialBudget.continuation_policy !== "progress") return;
|
|
1153
|
+
run.snapshot.metadata.provider_usage_checkpoints ??= {};
|
|
1154
|
+
run.snapshot.metadata.provider_usage_checkpoints[invocationId] = {
|
|
1155
|
+
invocation_id: invocationId, phase, attempt_number: attemptNumber, started_at: startedAt,
|
|
1156
|
+
backend, ...(trackUsage ? { usage_status: status } : {}), token_usage: { records: structuredClone(tokenUsageRecords) },
|
|
1157
|
+
total_provider_tokens: initialBudget.tokens_used + tokenUsageRecords.reduce((sum, record) => sum + providerBudgetTokens(record), 0),
|
|
1158
|
+
hard_token_ceiling: initialBudget.hard_token_ceiling,
|
|
1159
|
+
};
|
|
1160
|
+
refreshBudgetState(run.snapshot);
|
|
1161
|
+
if (run.localStateRoot) checkpointWrites.push(saveCodingRunSnapshot(run.localStateRoot, run.snapshot).catch(() => {
|
|
1162
|
+
emitOutput(run, "The Provider usage checkpoint could not be persisted; recovery usage remains unverified.", { backend });
|
|
1163
|
+
}));
|
|
1164
|
+
};
|
|
1165
|
+
if (trackUsage) checkpointUsage('pending');
|
|
1137
1166
|
|
|
1138
1167
|
const stdoutDecoder = createProviderStreamDecoder({
|
|
1139
1168
|
backend,
|
|
1140
1169
|
stream: "stdout",
|
|
1141
1170
|
onLine: (line) => emitOutput(run, line, { backend }),
|
|
1142
|
-
onEvent: (event) => {
|
|
1171
|
+
onEvent: (event) => {
|
|
1172
|
+
if (backend === 'claude' && event.type === 'result' || backend === 'codex' && event.type === 'turn.completed') {
|
|
1173
|
+
usageTerminals++;
|
|
1174
|
+
incompleteUsage ||= !completeProviderUsage(backend, event);
|
|
1175
|
+
}
|
|
1176
|
+
const identity = observeDispatchEvent({ root: dispatchOwner.root, attemptId: dispatchAttemptId, provider: backend, event, onError: error => { run.snapshot.metadata.dispatch_recovery_required = error.code || 'provenance_persistence_failed'; } }); if (identity) { run.snapshot.metadata.dispatch_identities ||= {}; run.snapshot.metadata.dispatch_identities[dispatchAttemptId] = identity; } observationSender?.observe(event); },
|
|
1143
1177
|
onTerminalError: (message) => { terminalError ||= message; },
|
|
1144
1178
|
onUsage: (usage) => {
|
|
1145
1179
|
tokenUsageRecords.push({
|
|
@@ -1147,21 +1181,9 @@ export async function executeProcessAttempt({
|
|
|
1147
1181
|
phase,
|
|
1148
1182
|
...(Number.isInteger(attemptNumber) ? { attempt_number: attemptNumber } : {}),
|
|
1149
1183
|
backend,
|
|
1150
|
-
...(initialBudget.continuation_policy === "progress" ? { invocation_id: invocationId } : {}),
|
|
1184
|
+
...((trackUsage || initialBudget.continuation_policy === "progress") ? { invocation_id: invocationId } : {}),
|
|
1151
1185
|
});
|
|
1152
|
-
|
|
1153
|
-
const currentUsed = initialBudget.tokens_used + tokenUsageRecords.reduce((sum, record) => sum + providerBudgetTokens(record), 0);
|
|
1154
|
-
run.snapshot.metadata.provider_usage_checkpoints ??= {};
|
|
1155
|
-
run.snapshot.metadata.provider_usage_checkpoints[invocationId] = {
|
|
1156
|
-
invocation_id: invocationId, phase, attempt_number: attemptNumber, started_at: startedAt,
|
|
1157
|
-
token_usage: { records: structuredClone(tokenUsageRecords) },
|
|
1158
|
-
total_provider_tokens: currentUsed,
|
|
1159
|
-
hard_token_ceiling: initialBudget.hard_token_ceiling,
|
|
1160
|
-
};
|
|
1161
|
-
refreshBudgetState(run.snapshot);
|
|
1162
|
-
if (run.localStateRoot) checkpointWrites.push(saveCodingRunSnapshot(run.localStateRoot, run.snapshot).catch(() => {
|
|
1163
|
-
emitOutput(run, "The Provider usage checkpoint could not be persisted; recovery usage remains unverified.", { backend });
|
|
1164
|
-
}));
|
|
1186
|
+
checkpointUsage('pending');
|
|
1165
1187
|
|
|
1166
1188
|
},
|
|
1167
1189
|
});
|
|
@@ -1188,6 +1210,7 @@ export async function executeProcessAttempt({
|
|
|
1188
1210
|
stdoutDecoder.flush();
|
|
1189
1211
|
stderrDecoder.flush();
|
|
1190
1212
|
run.child = null;
|
|
1213
|
+
checkpointUsage(usageTerminals > 0 && !incompleteUsage && (backend !== 'claude' || usageTerminals === 1) && tokenUsageRecords.length ? 'observed' : 'unknown');
|
|
1191
1214
|
await Promise.all(checkpointWrites);
|
|
1192
1215
|
const rawStdout = stdout.join("");
|
|
1193
1216
|
const parsed = parseProviderStructuredResult(rawStdout, {
|
|
@@ -339,5 +339,22 @@ export function createActionDeadlineService({
|
|
|
339
339
|
async function read(context) {
|
|
340
340
|
return project(await store.read(identity(context)), context);
|
|
341
341
|
}
|
|
342
|
-
|
|
342
|
+
async function importSnapshot(context, snapshot) {
|
|
343
|
+
const current = await reconcile(context, { displayOnly: true });
|
|
344
|
+
const decision = snapshot?.decision;
|
|
345
|
+
if (JSON.stringify(snapshot?.identity) !== JSON.stringify(identity(context)) ||
|
|
346
|
+
snapshot.fingerprint !== current.fingerprint || !current.reminder ||
|
|
347
|
+
!decision || !options.some(option => option.id === decision.choice) ||
|
|
348
|
+
typeof decision.requestId !== 'string' || !decision.requestId ||
|
|
349
|
+
!Number.isSafeInteger(decision.revision) || decision.revision < 1 ||
|
|
350
|
+
decision.reminderKey !== current.reminder.key) return current;
|
|
351
|
+
const state = await transact(context, previous => {
|
|
352
|
+
if (!previous || previous.decision || previous.fingerprint !== snapshot.fingerprint) return previous;
|
|
353
|
+
return { ...previous, decision: { id: decision.requestId, requestId: decision.requestId,
|
|
354
|
+
choice: decision.choice, revision: decision.revision, reminderKey: previous.reminder.key,
|
|
355
|
+
createdAt: decision.createdAt, remoteApplied: false }, migratedFromExecutionHost: true };
|
|
356
|
+
});
|
|
357
|
+
return project(state, context);
|
|
358
|
+
}
|
|
359
|
+
return { reconcile, read, decide, importSnapshot };
|
|
343
360
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { automaticContinuationAllowed, listRecords, updateRecord, queuePurposeDiagnosis, queuePurposeVerification, managedBudgetReason } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
2
|
+
import { automaticContinuationAllowed, listRecords, updateRecord, queuePurposeDiagnosis, queuePurposeVerification, managedBudgetReason, diagnoseControllerApproval } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
3
3
|
import { autonomousPolicyAuthorized, policyContractIdentity, acceptanceIdentity, legacyIndependentCriterion, effectiveAcceptanceCriteria } from '../codex-plugins/simy/hooks/acceptance-policy.mjs';
|
|
4
4
|
import { purposeFailureMode } from '../codex-plugins/simy/hooks/purpose-search.mjs';
|
|
5
5
|
|
|
@@ -93,6 +93,8 @@ export function upgradeAutonomousRecord(record, { rules, now, metadata } = {}) {
|
|
|
93
93
|
run.reason = 'Monitoring the original dependency and permitted independent work; no authority or prior effect is assumed.';
|
|
94
94
|
m.purpose.nextDependencyCheckAt = now;
|
|
95
95
|
}
|
|
96
|
+
const priorApproval = m.pendingApproval;
|
|
97
|
+
if (run.state === 'blocked') diagnoseControllerApproval(record, now);
|
|
96
98
|
const canRecover = metadata.isRunning === false && !m.pendingApproval && !m.userQuestion &&
|
|
97
99
|
!m.understandingQuestion && !m.dependencyWait && !m.providerWarning && !m.nativeWait && !m.executionUncertain;
|
|
98
100
|
if (canRecover && run.state === 'paused' && pauseOrigin(run) === 'system') {
|
|
@@ -102,7 +104,7 @@ export function upgradeAutonomousRecord(record, { rules, now, metadata } = {}) {
|
|
|
102
104
|
} else if (canRecover && run.state === 'verification_pending' && effectiveAcceptanceCriteria(m).some(c => c.check.type === 'semantic' && !m.verified[c.id])) {
|
|
103
105
|
queuePurposeVerification(run, 'Existing outcome awaits independent evidence inspection; inspect original instructions and actual artifacts, not a worker completion claim.', now);
|
|
104
106
|
}
|
|
105
|
-
return { changed: before !== after || run.state !== previousState, reason: 'applied', originalContract: original };
|
|
107
|
+
return { changed: before !== after || run.state !== previousState || Boolean(priorApproval && !m.pendingApproval), reason: 'applied', originalContract: original };
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
// Explicit CLI user control; never inferred from installation, ON, descriptions
|