From 205af23f55dab269d65583019bbc42bef7449c57 Mon Sep 17 00:00:00 2001 From: hyk <4408344+hhyykk@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:47:24 +0800 Subject: [PATCH 1/3] feat(quota): migrate void transaction to TypeScript Signed-off-by: hyk <4408344+hhyykk@users.noreply.github.com> --- .../control_plane/effect_runtime_handlers.ts | 2 + .../quota/accounting_artifact_transaction.ts | 910 ++++++++++++++++++ loopx/control_plane/quota/slot_accounting.py | 217 +---- loopx/control_plane/quota/spend_commit.ts | 844 ++++------------ loopx/control_plane/quota/void_commit.py | 243 +++++ loopx/control_plane/quota/void_commit.ts | 862 +++++++++++++++++ loopx/quota.py | 30 +- tsconfig.control-plane.json | 3 + 8 files changed, 2213 insertions(+), 898 deletions(-) create mode 100644 loopx/control_plane/quota/accounting_artifact_transaction.ts create mode 100644 loopx/control_plane/quota/void_commit.py create mode 100644 loopx/control_plane/quota/void_commit.ts diff --git a/loopx/control_plane/effect_runtime_handlers.ts b/loopx/control_plane/effect_runtime_handlers.ts index a782d199bf..0ebe92d432 100644 --- a/loopx/control_plane/effect_runtime_handlers.ts +++ b/loopx/control_plane/effect_runtime_handlers.ts @@ -44,6 +44,7 @@ import { } from "./governed_capability.ts"; import { evaluateDeliveryWorkspaceCausality } from "./quota/settlement_workspace_causality.ts"; import { evaluateQuotaSpendCommit } from "./quota/spend_commit.ts"; +import { evaluateQuotaVoidCommit } from "./quota/void_commit.ts"; import { readQuotaSettlement } from "./quota/settlement_readback.ts"; import { evaluateTurnEnvelope } from "./quota/turn_envelope.ts"; import { evaluateQuotaMonitorPollCommit } from "./quota/monitor_poll_commit.ts"; @@ -347,6 +348,7 @@ export function createEffectRuntimeHandlers( evaluateDeliveryWorkspaceCausality, ], ["quota.spend.commit", evaluateQuotaSpendCommit], + ["quota.void.commit", evaluateQuotaVoidCommit], ["quota.settlement.read", readQuotaSettlement], ["quota.turn_envelope.evaluate", evaluateTurnEnvelope], ["task_lease.acquire.decide", evaluateTaskLeaseAcquireDecision], diff --git a/loopx/control_plane/quota/accounting_artifact_transaction.ts b/loopx/control_plane/quota/accounting_artifact_transaction.ts new file mode 100644 index 0000000000..0047f2a292 --- /dev/null +++ b/loopx/control_plane/quota/accounting_artifact_transaction.ts @@ -0,0 +1,910 @@ +import { createHash } from "node:crypto"; +import { access, lstat, readFile } from "node:fs/promises"; +import { basename, dirname, extname, join, resolve } from "node:path"; + +import type { JsonObject } from "../effect_program.ts"; +import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts"; +import { + appendJsonLine, + atomicWriteJson, + atomicWriteText, + withFileMutationLock, +} from "../effect_runtime_io.ts"; +import { + jsonObject, + optionalNonEmptyString as optionalString, + requireInteger as requiredInteger, + requireJsonObject as requiredObject, + requireNonEmptyString as requiredString, + requireStringLiteral, +} from "../runtime_decode.ts"; + +export type QuotaAccountingArtifactKind = "spend" | "void"; + +interface QuotaAccountingArtifactContract { + receiptSchema: "quota_spend_commit_receipt_v0" | "quota_void_commit_receipt_v0"; + transactionDirectory: "quota-spend" | "quota-void"; + artifactSlug: "quota-slot-spent" | "quota-slot-voided"; + classification: "quota_slot_spent" | "quota_slot_voided"; + metadataField: "quota_spend_commit" | "quota_void_commit"; + label: "quota spend" | "quota void"; +} + +const QUOTA_ACCOUNTING_ARTIFACT_CONTRACTS = { + spend: { + receiptSchema: "quota_spend_commit_receipt_v0", + transactionDirectory: "quota-spend", + artifactSlug: "quota-slot-spent", + classification: "quota_slot_spent", + metadataField: "quota_spend_commit", + label: "quota spend", + }, + void: { + receiptSchema: "quota_void_commit_receipt_v0", + transactionDirectory: "quota-void", + artifactSlug: "quota-slot-voided", + classification: "quota_slot_voided", + metadataField: "quota_void_commit", + label: "quota void", + }, +} as const satisfies Record; + +export interface QuotaAccountingArtifactReceipt extends JsonObject { + schema_version: + | "quota_spend_commit_receipt_v0" + | "quota_void_commit_receipt_v0"; + effect_id: string; + request_digest: string; + status: "prepared" | "committed"; + json_path: string; + markdown_path: string; + index_path: string; + expected_index_digest: string | null; + expected_index_bytes: number; + record: JsonObject; + index_record: JsonObject; + markdown: string; + payload: JsonObject; +} + +export type QuotaAccountingEffectResolution = + | { kind: "absent" } + | { kind: "matched"; record: JsonObject } + | { kind: "conflict"; reason: string }; + +export interface QuotaAccountingArtifactPrepareContext { + jsonPath: string; + markdownPath: string; + indexPath: string; + indexDigest: string | null; + indexRecords: readonly JsonObject[]; +} + +export type QuotaAccountingArtifactPreparation = + | { + kind: "prepared"; + record: JsonObject; + indexRecord: JsonObject; + markdown: string; + payload: JsonObject; + } + | { + kind: "not_found"; + reason: string; + payload: JsonObject; + }; + +export interface QuotaAccountingArtifactCommitRequest { + kind: QuotaAccountingArtifactKind; + runsDir: string; + generatedAt: string; + effectId: string; + requestDigest: string; + expectedIndexDigest: string | null; + prepare: ( + context: QuotaAccountingArtifactPrepareContext, + ) => + | QuotaAccountingArtifactPreparation + | Promise; +} + +export type QuotaAccountingArtifactCommitOutcome = + | { + status: "written" | "replayed" | "repaired"; + receipt: QuotaAccountingArtifactReceipt; + indexDigest: string | null; + } + | { + status: "conflict"; + reason: string; + reasonCode: "effect_id_conflict" | "index_digest_conflict"; + indexDigest: string | null; + } + | { + status: "not_found"; + reason: string; + payload: JsonObject; + indexDigest: string | null; + }; + +function contractFor( + kind: QuotaAccountingArtifactKind, +): QuotaAccountingArtifactContract { + return QUOTA_ACCOUNTING_ARTIFACT_CONTRACTS[kind]; +} + +function stableValue(value: unknown): unknown { + if (Array.isArray(value)) return value.map(stableValue); + const object = jsonObject(value); + if (!object) return value; + return Object.fromEntries( + Object.entries(object) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, child]) => [key, stableValue(child)]), + ); +} + +function canonicalJson(value: unknown): string { + return JSON.stringify(stableValue(value)); +} + +function sha256(value: string): string { + return `sha256:${createHash("sha256").update(value, "utf8").digest("hex")}`; +} + +function sha256Bytes(value: Uint8Array): string { + return `sha256:${createHash("sha256").update(value).digest("hex")}`; +} + +function runStem(generatedAt: string): string { + const stem = generatedAt.replace(/[^0-9A-Za-z-]+/g, "-").replace(/^-+|-+$/g, ""); + if (!stem) { + throw new EffectRuntimeRequestError( + "generated_at cannot form a run artifact name", + ); + } + return stem; +} + +function isNodeErrorCode(error: unknown, code: string): boolean { + return error instanceof Error && "code" in error && error.code === code; +} + +async function pathExists(path: string): Promise { + try { + await access(path); + return true; + } catch (error) { + if (isNodeErrorCode(error, "ENOENT")) return false; + throw error; + } +} + +export async function nextQuotaAccountingArtifactPaths( + kind: QuotaAccountingArtifactKind, + runsDir: string, + generatedAt: string, + effectId: string, +): Promise<{ jsonPath: string; markdownPath: string }> { + const contract = contractFor(kind); + const effectDigest = sha256(effectId).slice( + "sha256:".length, + "sha256:".length + 24, + ); + const base = `${runStem(generatedAt)}-${contract.artifactSlug}-${effectDigest}`; + for (let index = 1; ; index += 1) { + const stem = index === 1 ? base : `${base}-${index}`; + const jsonPath = join(runsDir, `${stem}.json`); + const markdownPath = join(runsDir, `${stem}.md`); + if (!await pathExists(jsonPath) && !await pathExists(markdownPath)) { + return { jsonPath, markdownPath }; + } + } +} + +function transactionPath( + contract: QuotaAccountingArtifactContract, + runsDir: string, + effectId: string, +): string { + const digest = sha256(effectId).slice("sha256:".length, "sha256:".length + 24); + return join(runsDir, ".transactions", contract.transactionDirectory, `${digest}.json`); +} + +async function readOptionalText(path: string): Promise { + try { + return await readFile(path, "utf8"); + } catch (error) { + if (isNodeErrorCode(error, "ENOENT")) return null; + throw error; + } +} + +async function readOptionalBytes(path: string): Promise { + try { + return await readFile(path); + } catch (error) { + if (isNodeErrorCode(error, "ENOENT")) return null; + throw error; + } +} + +export async function quotaAccountingIndexDigest( + indexPath: string, +): Promise { + const content = await readOptionalBytes(indexPath); + return content === null ? null : sha256Bytes(content); +} + +export function parseQuotaAccountingIndex(content: string | null): JsonObject[] { + if (content === null) return []; + const records: JsonObject[] = []; + for (const [index, line] of content.split(/\r?\n/).entries()) { + if (!line.trim()) continue; + let value: unknown; + try { + value = JSON.parse(line); + } catch { + throw new EffectRuntimeRequestError( + `quota run index line ${index + 1} is malformed`, + "malformed_run_index", + ); + } + records.push(requiredObject(value, `quota run index line ${index + 1}`)); + } + return records; +} + +function pyValue(value: unknown): string { + if (value === true) return "True"; + if (value === false) return "False"; + if (value === null || value === undefined) return "None"; + return String(value); +} + +function markdownScalar(value: unknown): string { + return pyValue(value).replace(/\r/g, " ").replace(/\n/g, " ").replace(/\|/g, "\\|").trim(); +} + +export function renderQuotaSlotMarkdown( + payload: JsonObject, + defaultClassification: "quota_slot_spent" | "quota_slot_voided", +): string { + const before = jsonObject(payload.before) ?? {}; + const after = jsonObject(payload.after) ?? {}; + const beforeQuota = jsonObject(before.quota) ?? before; + const afterQuota = jsonObject(after.quota) ?? after; + const lines = [ + "# LoopX Quota Slot Preview", + "", + `- ok: \`${pyValue(payload.ok)}\``, + `- dry_run: \`${pyValue(payload.dry_run)}\``, + `- goal_id: \`${pyValue(payload.goal_id)}\``, + `- classification: \`${pyValue(payload.classification ?? defaultClassification)}\``, + `- agent_id: \`${pyValue(payload.agent_id ?? "")}\``, + `- slots: \`${pyValue(payload.slots)}\``, + `- appended: \`${pyValue(payload.appended)}\``, + `- registry_mutated: \`${pyValue(payload.registry_mutated)}\``, + `- would_throttle: \`${pyValue(payload.would_throttle)}\``, + ]; + if (payload.json_path) lines.push(`- json_path: \`${pyValue(payload.json_path)}\``); + if (payload.index_path) lines.push(`- index_path: \`${pyValue(payload.index_path)}\``); + if (payload.reason) lines.push(`- reason: ${pyValue(payload.reason)}`); + if (Object.keys(before).length) { + lines.push( + `- before: state=${pyValue(before.state)} should_run=${pyValue(before.should_run)} ` + + `slots=${pyValue(beforeQuota.spent_slots)}/${pyValue(beforeQuota.allowed_slots)}`, + ); + } + if (Object.keys(after).length) { + lines.push( + `- after: state=${pyValue(after.state)} should_run=${pyValue(after.should_run)} ` + + `slots=${pyValue(afterQuota.spent_slots)}/${pyValue(afterQuota.allowed_slots)}`, + ); + const summary = jsonObject(after.plan_summary); + if (summary) { + lines.push( + `- after_plan_next_automatic_turn: ${pyValue(summary.next_automatic_turn ?? "none")}`, + ); + } + } + if (payload.rolling_window_note) { + lines.push(`- rolling_window_note: ${pyValue(payload.rolling_window_note)}`); + } + const operatorAction = jsonObject(payload.operator_action); + if (operatorAction) { + if (payload.error_code) lines.push(`- error_code: \`${pyValue(payload.error_code)}\``); + if (payload.incident_channel) { + lines.push(`- incident_channel: \`${pyValue(payload.incident_channel)}\``); + } + lines.push( + "- operator_action: " + + `action=${markdownScalar(operatorAction.action ?? "")} ` + + `holder_pid=${markdownScalar(operatorAction.holder_pid ?? "") || "unknown"} ` + + `retry_mode=${markdownScalar(operatorAction.retry_mode ?? "")}`, + ); + if (Array.isArray(operatorAction.steps)) { + for (const step of operatorAction.steps) lines.push(` - ${markdownScalar(step)}`); + } + } + return `${lines.join("\n")}\n`; +} + +function repairedTruncatedTail( + content: Buffer, + expectedRecord: JsonObject, + expectedIndexDigest: string | null, + expectedIndexBytes: number, +): string | null { + if (content.length <= expectedIndexBytes) return null; + const validPrefix = content.subarray(0, expectedIndexBytes); + const truncatedTail = content.subarray(expectedIndexBytes); + const expectedLine = Buffer.from(`${JSON.stringify(expectedRecord)}\n`, "utf8"); + if ( + truncatedTail.length >= expectedLine.length || + !expectedLine.subarray(0, truncatedTail.length).equals(truncatedTail) + ) { + return null; + } + if ( + expectedIndexDigest === null + ? validPrefix.length !== 0 + : sha256Bytes(validPrefix) !== expectedIndexDigest + ) { + return null; + } + const validPrefixText = validPrefix.toString("utf8"); + parseQuotaAccountingIndex(validPrefixText); + return `${validPrefixText}${expectedLine.toString("utf8")}`; +} + +function effectIdentityValue( + value: unknown, +): { value: string | null; malformed: boolean } { + if (value === null || value === undefined || value === "") { + return { value: null, malformed: false }; + } + if (typeof value !== "string" || !value.trim()) { + return { value: null, malformed: true }; + } + return { value: value.trim(), malformed: false }; +} + +function resolveEffectIdentity( + contract: QuotaAccountingArtifactContract, + record: JsonObject, + expectedEffectId: string, +): QuotaAccountingEffectResolution { + const rawMetadata = record[contract.metadataField]; + const recordEffect = effectIdentityValue(record.effect_ref); + const recordReferencesExpected = recordEffect.value === expectedEffectId; + const metadataPresent = rawMetadata !== undefined; + const metadata = metadataPresent ? jsonObject(rawMetadata) : null; + if (metadataPresent && metadata === null) { + if (!recordReferencesExpected) return { kind: "absent" }; + return { + kind: "conflict", + reason: `${contract.label} index row has malformed effect metadata`, + }; + } + const metadataEffect = effectIdentityValue(metadata?.effect_id); + const referencesExpected = metadataEffect.value === expectedEffectId || + recordReferencesExpected; + if (!referencesExpected) return { kind: "absent" }; + if ( + metadataEffect.malformed || + recordEffect.malformed || + (metadataPresent && metadataEffect.value === null) + ) { + return { + kind: "conflict", + reason: `${contract.label} index row has malformed effect identity`, + }; + } + if ( + metadataEffect.value !== null && + recordEffect.value !== null && + metadataEffect.value !== recordEffect.value + ) { + return { + kind: "conflict", + reason: `${contract.label} index row has conflicting effect identities`, + }; + } + return { kind: "matched", record }; +} + +export function resolveQuotaAccountingEffect( + kind: QuotaAccountingArtifactKind, + records: readonly JsonObject[], + effectId: string, +): QuotaAccountingEffectResolution { + const contract = contractFor(kind); + for (const record of [...records].reverse()) { + if (record.classification !== contract.classification) continue; + const resolution = resolveEffectIdentity(contract, record, effectId); + if (resolution.kind !== "absent") return resolution; + } + return { kind: "absent" }; +} + +export async function lookupQuotaAccountingReplay( + kind: QuotaAccountingArtifactKind, + indexPath: string, + effectId: string, + readOnly: boolean, +): Promise<{ + resolution: QuotaAccountingEffectResolution; + indexDigest: string | null; +}> { + const lookup = async () => { + const content = await readOptionalText(indexPath); + return { + resolution: resolveQuotaAccountingEffect( + kind, + parseQuotaAccountingIndex(content), + effectId, + ), + indexDigest: await quotaAccountingIndexDigest(indexPath), + }; + }; + return readOnly ? await lookup() : await withFileMutationLock(indexPath, lookup); +} + +function receiptObject( + contract: QuotaAccountingArtifactContract, + value: unknown, +): QuotaAccountingArtifactReceipt { + const receipt = requiredObject(value, `${contract.label} transaction receipt`); + if (receipt.schema_version !== contract.receiptSchema) { + const capitalizedLabel = `${contract.label[0]?.toUpperCase()}${contract.label.slice(1)}`; + throw new EffectRuntimeRequestError( + `${capitalizedLabel} transaction receipt schema mismatch`, + ); + } + const status = requireStringLiteral( + receipt.status, + ["prepared", "committed"] as const, + "receipt.status", + ); + const expectedIndexBytes = requiredInteger( + receipt.expected_index_bytes, + "receipt.expected_index_bytes", + ); + if (expectedIndexBytes < 0) { + throw new EffectRuntimeRequestError( + "receipt.expected_index_bytes cannot be negative", + ); + } + return { + schema_version: contract.receiptSchema, + effect_id: requiredString(receipt.effect_id, "receipt.effect_id"), + request_digest: requiredString(receipt.request_digest, "receipt.request_digest"), + status, + json_path: requiredString(receipt.json_path, "receipt.json_path"), + markdown_path: requiredString(receipt.markdown_path, "receipt.markdown_path"), + index_path: requiredString(receipt.index_path, "receipt.index_path"), + expected_index_digest: optionalString( + receipt.expected_index_digest, + "receipt.expected_index_digest", + ), + expected_index_bytes: expectedIndexBytes, + record: requiredObject(receipt.record, "receipt.record"), + index_record: requiredObject(receipt.index_record, "receipt.index_record"), + markdown: requiredString(receipt.markdown, "receipt.markdown"), + payload: requiredObject(receipt.payload, "receipt.payload"), + }; +} + +function validateReceiptPaths( + contract: QuotaAccountingArtifactContract, + runsDir: string, + receipt: QuotaAccountingArtifactReceipt, +): void { + const resolvedRunsDir = resolve(runsDir); + const resolvedIndexPath = resolve(receipt.index_path); + if (resolvedIndexPath !== resolve(runsDir, "index.jsonl")) { + throw new EffectRuntimeRequestError( + `${contract.label} transaction receipt index path is outside its run directory`, + "malformed_transaction_receipt", + ); + } + for (const [label, path, extension] of [ + ["JSON", receipt.json_path, ".json"], + ["Markdown", receipt.markdown_path, ".md"], + ] as const) { + const resolvedPath = resolve(path); + if (dirname(resolvedPath) !== resolvedRunsDir || extname(resolvedPath) !== extension) { + throw new EffectRuntimeRequestError( + `${contract.label} transaction receipt ${label} path is outside its run directory`, + "malformed_transaction_receipt", + ); + } + } + const generatedAt = requiredString( + receipt.record.generated_at, + "receipt.record.generated_at", + ); + const effectDigest = sha256(receipt.effect_id).slice( + "sha256:".length, + "sha256:".length + 24, + ); + const base = `${runStem(generatedAt)}-${contract.artifactSlug}-${effectDigest}`; + const jsonName = basename(receipt.json_path); + const jsonStem = jsonName.slice(0, -".json".length); + const suffix = jsonStem.slice(base.length); + if ( + !jsonStem.startsWith(base) || + (suffix !== "" && !/^-(?:[2-9]|[1-9][0-9]+)$/.test(suffix)) || + basename(receipt.markdown_path) !== `${jsonStem}.md` + ) { + throw new EffectRuntimeRequestError( + `${contract.label} transaction receipt artifact names do not match its effect identity`, + "malformed_transaction_receipt", + ); + } + if ( + receipt.index_record.json_path !== receipt.json_path || + receipt.index_record.markdown_path !== receipt.markdown_path || + receipt.payload.json_path !== receipt.json_path || + receipt.payload.markdown_path !== receipt.markdown_path || + receipt.payload.index_path !== receipt.index_path + ) { + throw new EffectRuntimeRequestError( + `${contract.label} transaction receipt artifact paths do not match its projections`, + "malformed_transaction_receipt", + ); + } + const recordMetadata = requiredObject( + receipt.record[contract.metadataField], + `receipt.record.${contract.metadataField}`, + ); + const indexMetadata = requiredObject( + receipt.index_record[contract.metadataField], + `receipt.index_record.${contract.metadataField}`, + ); + for (const [label, projection, expected] of [ + ["record classification", receipt.record.classification, contract.classification], + ["index classification", receipt.index_record.classification, contract.classification], + ["record effect", recordMetadata.effect_id, receipt.effect_id], + ["index effect", indexMetadata.effect_id, receipt.effect_id], + ["record digest", recordMetadata.request_digest, receipt.request_digest], + ["index digest", indexMetadata.request_digest, receipt.request_digest], + ] as const) { + if (projection !== expected) { + throw new EffectRuntimeRequestError( + `${contract.label} transaction receipt ${label} does not match its identity`, + "malformed_transaction_receipt", + ); + } + } +} + +async function rejectSymlinkPath(path: string, label: string): Promise { + try { + if ((await lstat(path)).isSymbolicLink()) { + throw new EffectRuntimeRequestError( + `${label} must not be a symbolic link`, + "malformed_transaction_receipt", + ); + } + } catch (error) { + if (isNodeErrorCode(error, "ENOENT")) return; + throw error; + } +} + +async function readReceipt( + contract: QuotaAccountingArtifactContract, + path: string, + runsDir: string, +): Promise { + const content = await readOptionalText(path); + if (content === null) return null; + let value: unknown; + try { + value = JSON.parse(content); + } catch { + throw new EffectRuntimeRequestError( + `${contract.label} transaction receipt is malformed`, + "malformed_transaction_receipt", + ); + } + const receipt = receiptObject(contract, value); + validateReceiptPaths(contract, runsDir, receipt); + await Promise.all([ + rejectSymlinkPath(receipt.json_path, `${contract.label} JSON artifact`), + rejectSymlinkPath(receipt.markdown_path, `${contract.label} Markdown artifact`), + rejectSymlinkPath(receipt.index_path, `${contract.label} run index`), + ]); + return receipt; +} + +async function ensureJsonArtifact( + contract: QuotaAccountingArtifactContract, + path: string, + expected: JsonObject, +): Promise { + const existing = await readOptionalText(path); + if (existing === null) { + await atomicWriteJson(path, expected); + return true; + } + let actual: unknown; + try { + actual = JSON.parse(existing); + } catch { + throw new EffectRuntimeRequestError( + `${contract.label} JSON artifact is malformed`, + "artifact_conflict", + ); + } + if (canonicalJson(actual) !== canonicalJson(expected)) { + throw new EffectRuntimeRequestError( + `${contract.label} JSON artifact conflicts with its transaction receipt`, + "artifact_conflict", + ); + } + return false; +} + +async function ensureMarkdownArtifact( + contract: QuotaAccountingArtifactContract, + path: string, + expected: string, +): Promise { + const existing = await readOptionalText(path); + if (existing === null) { + await atomicWriteText(path, expected); + return true; + } + if (existing !== expected) { + throw new EffectRuntimeRequestError( + `${contract.label} Markdown artifact conflicts with its transaction receipt`, + "artifact_conflict", + ); + } + return false; +} + +async function readReceiptIndex( + receipt: QuotaAccountingArtifactReceipt, +): Promise<{ content: string | null; records: JsonObject[]; repaired: boolean }> { + const indexBytes = await readOptionalBytes(receipt.index_path); + let content = indexBytes === null ? null : indexBytes.toString("utf8"); + try { + return { + content, + records: parseQuotaAccountingIndex(content), + repaired: false, + }; + } catch (error) { + const recovered = indexBytes === null + ? null + : repairedTruncatedTail( + indexBytes, + receipt.index_record, + receipt.expected_index_digest, + receipt.expected_index_bytes, + ); + if (recovered === null) throw error; + await atomicWriteText(receipt.index_path, recovered); + content = recovered; + return { + content, + records: parseQuotaAccountingIndex(content), + repaired: true, + }; + } +} + +function assertReceiptIndexPrefix( + contract: QuotaAccountingArtifactContract, + receipt: QuotaAccountingArtifactReceipt, + content: string | null, +): void { + const current = Buffer.from(content ?? "", "utf8"); + const expectedBytes = receipt.expected_index_bytes; + const expectedDigest = receipt.expected_index_digest; + const prefixMatches = current.length >= expectedBytes && ( + expectedDigest === null + ? expectedBytes === 0 + : sha256Bytes(current.subarray(0, expectedBytes)) === expectedDigest + ); + if (!prefixMatches) { + throw new EffectRuntimeRequestError( + `${contract.label} run index no longer retains its transaction prefix`, + "artifact_conflict", + ); + } +} + +async function ensureReceiptArtifacts( + kind: QuotaAccountingArtifactKind, + contract: QuotaAccountingArtifactContract, + receipt: QuotaAccountingArtifactReceipt, +): Promise { + const index = await readReceiptIndex(receipt); + let repaired = index.repaired; + const matchResolution = resolveQuotaAccountingEffect( + kind, + index.records, + receipt.effect_id, + ); + if (matchResolution.kind === "conflict") { + throw new EffectRuntimeRequestError( + matchResolution.reason, + "effect_id_conflict", + ); + } + const match = matchResolution.kind === "matched" + ? matchResolution.record + : null; + if (match) { + const metadata = jsonObject(match[contract.metadataField]); + if (metadata?.request_digest !== receipt.request_digest) { + throw new EffectRuntimeRequestError( + `${contract.label} effect identity is already bound to a different request`, + "effect_id_conflict", + ); + } + if (canonicalJson(match) !== canonicalJson(receipt.index_record)) { + throw new EffectRuntimeRequestError( + `${contract.label} index record conflicts with its transaction receipt`, + "artifact_conflict", + ); + } + } else { + assertReceiptIndexPrefix(contract, receipt, index.content); + } + repaired = await ensureJsonArtifact(contract, receipt.json_path, receipt.record) || + repaired; + repaired = await ensureMarkdownArtifact( + contract, + receipt.markdown_path, + receipt.markdown, + ) || repaired; + if (!match) { + const prefix = index.content ?? ""; + if (prefix && !prefix.endsWith("\n")) { + await atomicWriteText( + receipt.index_path, + `${prefix}\n${JSON.stringify(receipt.index_record)}\n`, + ); + } else { + await appendJsonLine(receipt.index_path, receipt.index_record); + } + repaired = true; + } + return repaired; +} + +export async function commitQuotaAccountingArtifactTransaction( + request: QuotaAccountingArtifactCommitRequest, +): Promise { + const contract = contractFor(request.kind); + const indexPath = join(request.runsDir, "index.jsonl"); + return await withFileMutationLock(indexPath, async () => { + await rejectSymlinkPath(indexPath, `${contract.label} run index`); + const receiptPath = transactionPath(contract, request.runsDir, request.effectId); + const existingReceipt = await readReceipt(contract, receiptPath, request.runsDir); + if (existingReceipt) { + if ( + existingReceipt.effect_id !== request.effectId || + existingReceipt.request_digest !== request.requestDigest + ) { + return { + status: "conflict", + reason: `${contract.label} effect identity is already bound to a different request`, + reasonCode: "effect_id_conflict", + indexDigest: await quotaAccountingIndexDigest(indexPath), + }; + } + const repaired = await ensureReceiptArtifacts( + request.kind, + contract, + existingReceipt, + ); + const committedReceipt = { + ...existingReceipt, + status: "committed", + } satisfies QuotaAccountingArtifactReceipt; + if (existingReceipt.status !== "committed" || repaired) { + await atomicWriteJson(receiptPath, committedReceipt); + } + return { + status: repaired ? "repaired" : "replayed", + receipt: committedReceipt, + indexDigest: await quotaAccountingIndexDigest(indexPath), + }; + } + + const currentIndexBytes = await readOptionalBytes(indexPath); + const currentIndexContent = currentIndexBytes === null + ? null + : currentIndexBytes.toString("utf8"); + const currentDigest = currentIndexBytes === null + ? null + : sha256Bytes(currentIndexBytes); + if (request.expectedIndexDigest !== currentDigest) { + return { + status: "conflict", + reason: "quota run index compare-and-swap precondition failed", + reasonCode: "index_digest_conflict", + indexDigest: currentDigest, + }; + } + const currentRecords = parseQuotaAccountingIndex(currentIndexContent); + const duplicateResolution = resolveQuotaAccountingEffect( + request.kind, + currentRecords, + request.effectId, + ); + if (duplicateResolution.kind === "conflict") { + return { + status: "conflict", + reason: duplicateResolution.reason, + reasonCode: "effect_id_conflict", + indexDigest: currentDigest, + }; + } + if (duplicateResolution.kind === "matched") { + return { + status: "conflict", + reason: `${contract.label} effect identity already exists without a matching transaction receipt`, + reasonCode: "effect_id_conflict", + indexDigest: currentDigest, + }; + } + + const { jsonPath, markdownPath } = await nextQuotaAccountingArtifactPaths( + request.kind, + request.runsDir, + request.generatedAt, + request.effectId, + ); + const preparation = await request.prepare({ + jsonPath, + markdownPath, + indexPath, + indexDigest: currentDigest, + indexRecords: currentRecords, + }); + if (preparation.kind === "not_found") { + return { + status: "not_found", + reason: preparation.reason, + payload: preparation.payload, + indexDigest: currentDigest, + }; + } + const prepared = { + schema_version: contract.receiptSchema, + effect_id: request.effectId, + request_digest: request.requestDigest, + status: "prepared", + json_path: jsonPath, + markdown_path: markdownPath, + index_path: indexPath, + expected_index_digest: currentDigest, + expected_index_bytes: currentIndexBytes?.length ?? 0, + record: preparation.record, + index_record: preparation.indexRecord, + markdown: preparation.markdown, + payload: preparation.payload, + } satisfies QuotaAccountingArtifactReceipt; + validateReceiptPaths(contract, request.runsDir, prepared); + await atomicWriteJson(receiptPath, prepared); + await ensureReceiptArtifacts(request.kind, contract, prepared); + const committedReceipt = { + ...prepared, + status: "committed", + } satisfies QuotaAccountingArtifactReceipt; + await atomicWriteJson(receiptPath, committedReceipt); + return { + status: "written", + receipt: committedReceipt, + indexDigest: await quotaAccountingIndexDigest(indexPath), + }; + }); +} diff --git a/loopx/control_plane/quota/slot_accounting.py b/loopx/control_plane/quota/slot_accounting.py index 7ee59c250e..b3217d953b 100644 --- a/loopx/control_plane/quota/slot_accounting.py +++ b/loopx/control_plane/quota/slot_accounting.py @@ -10,12 +10,6 @@ build_delivery_workspace_guard, delivery_workspace_identity, ) -from ..runtime.run_artifacts import ( - next_run_artifact_paths, - reserve_run_artifact_paths, - run_file_stem, -) -from ..runtime.time import now_local_iso from ..todos.contract import ( normalize_todo_claimed_by, normalize_todo_id, @@ -25,7 +19,6 @@ normalize_delivery_outcome, qualifies_turn_scoped_settlement, ) -from .decision_summary import compact_quota_decision, quota_decision_agent_id from .monitor_poll import QUOTA_MONITOR_POLL_CLASSIFICATION from .scheduler_ack import QUOTA_SCHEDULER_ACK_CLASSIFICATION from .settlement import ( @@ -46,13 +39,17 @@ from .spend_sources import ( DEFAULT_SLOT_SPEND_SOURCE, TURN_SCOPED_SLOT_SPEND_SOURCES, - VALID_SLOT_SPEND_SOURCES, VISIBLE_GOAL_SLOT_SPEND_SOURCE, ) from .spend_commit import ( build_quota_slot_spend_event as build_quota_slot_spend_event, record_quota_slot_spend_from_preview as record_quota_slot_spend_from_preview, ) +from .void_commit import ( + build_quota_slot_void_event as build_quota_slot_void_event, + build_quota_slot_void_preview_for_decision as build_quota_slot_void_preview_for_decision, + record_quota_slot_void_from_preview as record_quota_slot_void_from_preview, +) QUOTA_SLOT_SPENT_CLASSIFICATION = "quota_slot_spent" QUOTA_SLOT_VOIDED_CLASSIFICATION = "quota_slot_voided" @@ -249,10 +246,6 @@ def _repair_settlement_workspace_causality( return repaired or causality -def _now_local() -> str: - return now_local_iso() - - def _validate_goal_id_path_segment(goal_id: str) -> str: value = goal_id.strip() if not value: @@ -885,26 +878,6 @@ def build_quota_slot_preview_for_decision( } -def _find_quota_spend_run( - runtime_root: Path, - *, - goal_id: str, - generated_at: str, -) -> tuple[dict[str, Any], dict[str, Any]] | None: - for run in reversed(_load_goal_run_index_records(runtime_root, goal_id)): - if str(run.get("goal_id") or goal_id) != goal_id: - continue - if str(run.get("generated_at") or "") != generated_at: - continue - if str(run.get("classification") or "") != QUOTA_SLOT_SPENT_CLASSIFICATION: - continue - event = load_quota_event_from_run(run) - if not event or str(event.get("event_type") or "") != QUOTA_SLOT_SPENT_CLASSIFICATION: - continue - return run, event - return None - - def load_quota_event_from_run(run: dict[str, Any]) -> dict[str, Any] | None: if str(run.get("classification") or "") not in { QUOTA_SLOT_SPENT_CLASSIFICATION, @@ -929,183 +902,3 @@ def load_quota_event_from_run(run: dict[str, Any]) -> dict[str, Any] | None: return None event = record.get("quota_event") if isinstance(record.get("quota_event"), dict) else None return event - - -def build_quota_slot_void_preview_for_decision( - status_payload: dict[str, Any], - *, - goal_id: str, - voided_run_generated_at: str, - before: dict[str, Any], -) -> dict[str, Any]: - safe_goal_id = _validate_goal_id_path_segment(str(goal_id or "")) - safe_voided_at = str(voided_run_generated_at or "").strip() - if not safe_voided_at: - return { - "ok": False, - "mode": "void-slot", - "dry_run": True, - "goal_id": safe_goal_id, - "appended": False, - "registry_mutated": False, - "reason": "`quota void-slot` requires --void-generated-at", - } - - raw_runtime_root = status_payload.get("runtime_root") - if not raw_runtime_root: - raise ValueError("status payload does not include runtime_root") - runtime_root = Path(str(raw_runtime_root)).expanduser() - target = _find_quota_spend_run(runtime_root, goal_id=safe_goal_id, generated_at=safe_voided_at) - if target is None: - return { - "ok": False, - "mode": "void-slot", - "dry_run": True, - "goal_id": safe_goal_id, - "voided_run_generated_at": safe_voided_at, - "appended": False, - "registry_mutated": False, - "reason": "target quota_slot_spent run was not found in the goal runtime index", - } - target_run, target_event = target - slots = max(1, _int_number(target_event.get("slots"), default=1)) - before_quota = before.get("quota") if isinstance(before.get("quota"), dict) else {} - after = deepcopy(before) - after_quota = deepcopy(before_quota) - after_quota["spent_slots"] = max(0, _int_number(before_quota.get("spent_slots"), default=0) - slots) - after["quota"] = after_quota - return { - "ok": True, - "mode": "void-slot", - "dry_run": True, - "goal_id": safe_goal_id, - "slots": slots, - "voided_run_generated_at": safe_voided_at, - "voided_run_classification": target_run.get("classification"), - "voided_run_json_path": target_run.get("json_path"), - "appended": False, - "registry_mutated": False, - "before": before, - "after": after, - "would_throttle": False, - "reason": ( - f"dry-run preview: voiding {slots} slot(s) from {safe_goal_id} " - f"quota spend run {safe_voided_at}" - ), - "rolling_window_note": ( - "quota void-slot appends a quota_slot_voided accounting event. It does not delete the " - "original spend event; rolling-window ledgers subtract the void only when the target " - "spend event is inside the same accounting window." - ), - "classification": QUOTA_SLOT_VOIDED_CLASSIFICATION, - } - - -def build_quota_slot_void_event( - preview: dict[str, Any], - *, - source: str = DEFAULT_SLOT_SPEND_SOURCE, - reason_summary: str | None = None, - generated_at: str | None = None, -) -> dict[str, Any]: - if not preview.get("ok"): - raise ValueError(preview.get("reason") or "quota slot void requires a valid preview") - safe_source = str(source or DEFAULT_SLOT_SPEND_SOURCE).strip() - if safe_source not in VALID_SLOT_SPEND_SOURCES: - raise ValueError(f"quota slot void source must be one of: {', '.join(sorted(VALID_SLOT_SPEND_SOURCES))}") - safe_reason = str(reason_summary or "").strip() or "void duplicate or invalid quota slot spend event" - before = preview.get("before") if isinstance(preview.get("before"), dict) else {} - after = preview.get("after") if isinstance(preview.get("after"), dict) else {} - safe_agent_id = quota_decision_agent_id(before) - record = { - "generated_at": generated_at or _now_local(), - "goal_id": preview.get("goal_id"), - "classification": QUOTA_SLOT_VOIDED_CLASSIFICATION, - "recommended_action": safe_reason, - "health_check": "quota slot void event public-safe; original spend preserved for audit", - "quota_event": { - "event_type": QUOTA_SLOT_VOIDED_CLASSIFICATION, - "source": safe_source, - "slots": max(1, _int_number(preview.get("slots"), default=1)), - "reason_summary": safe_reason, - "voided_run_generated_at": preview.get("voided_run_generated_at"), - "voided_run_classification": preview.get("voided_run_classification"), - "before": compact_quota_decision(before) if before else {}, - "after": compact_quota_decision(after) if after else {}, - }, - } - if safe_agent_id: - record["agent_id"] = safe_agent_id - record["quota_event"]["agent_id"] = safe_agent_id - return record - - -def record_quota_slot_void_from_preview( - preview: dict[str, Any], - status_payload: dict[str, Any], - *, - goal_id: str, - render_markdown: Callable[[dict[str, Any]], str], - execute: bool = False, - source: str = DEFAULT_SLOT_SPEND_SOURCE, - reason_summary: str | None = None, -) -> dict[str, Any]: - safe_goal_id = _validate_goal_id_path_segment(str(goal_id or "")) - if not preview.get("ok"): - return preview - - generated_at = _now_local() - record = build_quota_slot_void_event( - preview, - source=source, - reason_summary=reason_summary, - generated_at=generated_at, - ) - raw_runtime_root = status_payload.get("runtime_root") - if not raw_runtime_root: - raise ValueError("status payload does not include runtime_root") - runtime_root = Path(str(raw_runtime_root)).expanduser() - runs_dir = runtime_root / "goals" / safe_goal_id / "runs" - stem = run_file_stem(generated_at) - path_allocator = reserve_run_artifact_paths if execute else next_run_artifact_paths - json_path, markdown_path = path_allocator(runs_dir, stem, "quota-slot-voided") - index_path = runs_dir / "index.jsonl" - index_record = { - "generated_at": generated_at, - "goal_id": safe_goal_id, - "classification": QUOTA_SLOT_VOIDED_CLASSIFICATION, - "recommended_action": record["recommended_action"], - "health_check": record["health_check"], - "json_path": str(json_path), - "markdown_path": str(markdown_path), - } - if record.get("agent_id"): - index_record["agent_id"] = record["agent_id"] - payload = { - **preview, - "dry_run": not execute, - "appended": execute, - "registry_mutated": False, - "source": record["quota_event"]["source"], - "classification": QUOTA_SLOT_VOIDED_CLASSIFICATION, - "generated_at": generated_at, - "agent_id": record.get("agent_id"), - "quota_event": record["quota_event"], - "json_path": str(json_path), - "markdown_path": str(markdown_path), - "index_path": str(index_path), - "reason": ( - f"{'appended' if execute else 'dry-run preview'} quota slot void event: " - f"{safe_goal_id} voided {record['quota_event']['slots']} slot(s) from " - f"{record['quota_event']['voided_run_generated_at']}" - ), - } - if execute: - payload["before"] = record["quota_event"]["before"] - payload["after"] = record["quota_event"]["after"] - if execute: - json_path.write_text(json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") - markdown_path.write_text(render_markdown(payload) + "\n", encoding="utf-8") - with index_path.open("a", encoding="utf-8") as f: - f.write(json.dumps(index_record, ensure_ascii=False) + "\n") - return payload diff --git a/loopx/control_plane/quota/spend_commit.ts b/loopx/control_plane/quota/spend_commit.ts index a89426bb3b..825398542f 100644 --- a/loopx/control_plane/quota/spend_commit.ts +++ b/loopx/control_plane/quota/spend_commit.ts @@ -1,15 +1,15 @@ import { createHash } from "node:crypto"; -import { access, readFile } from "node:fs/promises"; import { basename, isAbsolute, join } from "node:path"; import type { JsonObject } from "../effect_program.ts"; import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts"; import { - appendJsonLine, - atomicWriteJson, - atomicWriteText, - withFileMutationLock, -} from "../effect_runtime_io.ts"; + commitQuotaAccountingArtifactTransaction, + lookupQuotaAccountingReplay, + nextQuotaAccountingArtifactPaths, + quotaAccountingIndexDigest, + renderQuotaSlotMarkdown, +} from "./accounting_artifact_transaction.ts"; import { jsonObject, optionalNonEmptyString as optionalString, @@ -103,22 +103,6 @@ type SpendDisposition = | "capability_repair" | "safe_bypass"; -interface QuotaSpendCommitReceipt extends JsonObject { - schema_version: typeof QUOTA_SPEND_COMMIT_RECEIPT_SCHEMA; - effect_id: string; - request_digest: string; - status: "prepared" | "committed"; - json_path: string; - markdown_path: string; - index_path: string; - expected_index_digest: string | null; - expected_index_bytes: number; - record: JsonObject; - index_record: JsonObject; - markdown: string; - payload: JsonObject; -} - export interface QuotaSpendCommitResult extends JsonObject { schema_version: typeof QUOTA_SPEND_COMMIT_RESULT_SCHEMA; effect_id: string; @@ -154,10 +138,6 @@ function sha256(value: string): string { return `sha256:${createHash("sha256").update(value, "utf8").digest("hex")}`; } -function sha256Bytes(value: Uint8Array): string { - return `sha256:${createHash("sha256").update(value).digest("hex")}`; -} - function safeGoalId(value: unknown): string { const goalId = requiredString(value, "goal_id").trim(); if (goalId === "." || goalId === ".." || goalId.includes("/") || goalId.includes("\\")) { @@ -519,191 +499,8 @@ function buildSpendRecord( return record; } -function runStem(generatedAt: string): string { - const stem = generatedAt.replace(/[^0-9A-Za-z-]+/g, "-").replace(/^-+|-+$/g, ""); - if (!stem) throw new EffectRuntimeRequestError("generated_at cannot form a run artifact name"); - return stem; -} - -async function pathExists(path: string): Promise { - try { - await access(path); - return true; - } catch (error) { - if (isNodeErrorCode(error, "ENOENT")) return false; - throw error; - } -} - -function isNodeErrorCode(error: unknown, code: string): boolean { - return error instanceof Error && "code" in error && error.code === code; -} - -async function nextArtifactPaths( - runsDir: string, - generatedAt: string, - effectId: string, -): Promise<{ jsonPath: string; markdownPath: string }> { - const effectDigest = sha256(effectId).slice( - "sha256:".length, - "sha256:".length + 24, - ); - const base = `${runStem(generatedAt)}-quota-slot-spent-${effectDigest}`; - for (let index = 1; ; index += 1) { - const stem = index === 1 ? base : `${base}-${index}`; - const jsonPath = join(runsDir, `${stem}.json`); - const markdownPath = join(runsDir, `${stem}.md`); - if (!await pathExists(jsonPath) && !await pathExists(markdownPath)) { - return { jsonPath, markdownPath }; - } - } -} - -function transactionPath(runsDir: string, effectId: string): string { - const digest = sha256(effectId).slice("sha256:".length, "sha256:".length + 24); - return join(runsDir, ".transactions", "quota-spend", `${digest}.json`); -} - -async function readOptionalText(path: string): Promise { - try { - return await readFile(path, "utf8"); - } catch (error) { - if (isNodeErrorCode(error, "ENOENT")) return null; - throw error; - } -} - -async function readOptionalBytes(path: string): Promise { - try { - return await readFile(path); - } catch (error) { - if (isNodeErrorCode(error, "ENOENT")) return null; - throw error; - } -} - export async function quotaSpendIndexDigest(indexPath: string): Promise { - const content = await readOptionalBytes(indexPath); - return content === null ? null : sha256Bytes(content); -} - -function indexRecords(content: string | null): JsonObject[] { - if (content === null) return []; - const records: JsonObject[] = []; - for (const [index, line] of content.split(/\r?\n/).entries()) { - if (!line.trim()) continue; - let value: unknown; - try { - value = JSON.parse(line); - } catch { - throw new EffectRuntimeRequestError( - `quota run index line ${index + 1} is malformed`, - "malformed_run_index", - ); - } - records.push(requiredObject(value, `quota run index line ${index + 1}`)); - } - return records; -} - -function repairedTruncatedTail( - content: Buffer, - expectedRecord: JsonObject, - expectedIndexDigest: string | null, - expectedIndexBytes: number, -): string | null { - if (content.length <= expectedIndexBytes) return null; - const validPrefix = content.subarray(0, expectedIndexBytes); - const truncatedTail = content.subarray(expectedIndexBytes); - const expectedLine = Buffer.from(`${JSON.stringify(expectedRecord)}\n`, "utf8"); - if ( - truncatedTail.length >= expectedLine.length || - !expectedLine.subarray(0, truncatedTail.length).equals(truncatedTail) - ) { - return null; - } - if ( - expectedIndexDigest === null - ? validPrefix.length !== 0 - : sha256Bytes(validPrefix) !== expectedIndexDigest - ) { - return null; - } - const validPrefixText = validPrefix.toString("utf8"); - indexRecords(validPrefixText); - return `${validPrefixText}${expectedLine.toString("utf8")}`; -} - -type EffectIdentityResolution = - | { kind: "absent" } - | { kind: "matched"; record: JsonObject } - | { kind: "conflict"; reason: string }; - -function effectIdentityValue( - value: unknown, -): { value: string | null; malformed: boolean } { - if (value === null || value === undefined || value === "") { - return { value: null, malformed: false }; - } - if (typeof value !== "string" || !value.trim()) { - return { value: null, malformed: true }; - } - return { value: value.trim(), malformed: false }; -} - -function resolveEffectIdentity( - record: JsonObject, - expectedEffectId: string, -): EffectIdentityResolution { - const rawMetadata = record.quota_spend_commit; - const recordEffect = effectIdentityValue(record.effect_ref); - const recordReferencesExpected = recordEffect.value === expectedEffectId; - const metadataPresent = rawMetadata !== undefined; - const metadata = metadataPresent ? jsonObject(rawMetadata) : null; - if (metadataPresent && metadata === null) { - if (!recordReferencesExpected) return { kind: "absent" }; - return { - kind: "conflict", - reason: "quota spend index row has malformed effect metadata", - }; - } - const metadataEffect = effectIdentityValue(metadata?.effect_id); - const referencesExpected = metadataEffect.value === expectedEffectId || - recordReferencesExpected; - if (!referencesExpected) return { kind: "absent" }; - if ( - metadataEffect.malformed || - recordEffect.malformed || - (metadataPresent && metadataEffect.value === null) - ) { - return { - kind: "conflict", - reason: "quota spend index row has malformed effect identity", - }; - } - if ( - metadataEffect.value !== null && - recordEffect.value !== null && - metadataEffect.value !== recordEffect.value - ) { - return { - kind: "conflict", - reason: "quota spend index row has conflicting effect identities", - }; - } - return { kind: "matched", record }; -} - -function matchingIndexRecord( - records: readonly JsonObject[], - effectId: string, -): EffectIdentityResolution { - for (const record of [...records].reverse()) { - if (record.classification !== QUOTA_SLOT_SPENT_CLASSIFICATION) continue; - const resolution = resolveEffectIdentity(record, effectId); - if (resolution.kind !== "absent") return resolution; - } - return { kind: "absent" }; + return await quotaAccountingIndexDigest(indexPath); } async function evaluateQuotaSpendReplay( @@ -717,113 +514,110 @@ async function evaluateQuotaSpendReplay( "runs", "index.jsonl", ); - const evaluate = async (): Promise => { - const content = await readOptionalText(indexPath); - const candidateResolution = matchingIndexRecord( - indexRecords(content), - request.effect_id, - ); - if (candidateResolution.kind === "conflict") { - return { - schema_version: QUOTA_SPEND_COMMIT_RESULT_SCHEMA, - effect_id: request.effect_id, - status: "conflict", - written: false, - replayed: false, - repaired: false, - conflict: true, - request_digest: sha256(canonicalJson(value)), - index_digest: await quotaSpendIndexDigest(indexPath), - reason: candidateResolution.reason, - record: null, - payload: { - ok: false, - appended: false, - replay_found: true, - goal_id: request.goal_id, - effect_ref: request.effect_id, - reason: candidateResolution.reason, - }, - reason_code: "effect_id_conflict", - }; - } - const candidate = candidateResolution.kind === "matched" - ? candidateResolution.record - : null; - const basePayload: JsonObject = { - ok: false, - appended: false, - replay_found: candidate !== null, - goal_id: request.goal_id, - effect_ref: request.effect_id, - }; - if (candidate === null) { - return { - schema_version: QUOTA_SPEND_COMMIT_RESULT_SCHEMA, - effect_id: request.effect_id, - status: "preview", - written: false, - replayed: false, - repaired: false, - conflict: false, - request_digest: sha256(canonicalJson(value)), - index_digest: await quotaSpendIndexDigest(indexPath), - reason: "quota spend replay was not found", - record: null, - payload: basePayload, - }; - } - const candidateGoalId = typeof candidate.goal_id === "string" - ? candidate.goal_id.trim() - : ""; - const candidateAgentId = typeof candidate.agent_id === "string" - ? candidate.agent_id.trim() - : ""; - if ( - candidateGoalId !== request.goal_id || - !request.resolved_agent_id || - candidateAgentId !== request.resolved_agent_id - ) { - return { - schema_version: QUOTA_SPEND_COMMIT_RESULT_SCHEMA, - effect_id: request.effect_id, - status: "preview", - written: false, - replayed: false, - repaired: false, - conflict: false, - request_digest: sha256(canonicalJson(value)), - index_digest: await quotaSpendIndexDigest(indexPath), - reason: "quota spend replay requires the same valid agent identity", - record: null, - payload: { ...basePayload, reason: "agent identity mismatch" }, - }; - } + const lookup = await lookupQuotaAccountingReplay( + "spend", + indexPath, + request.effect_id, + request.read_only, + ); + const requestFingerprint = sha256(canonicalJson(value)); + if (lookup.resolution.kind === "conflict") { return { schema_version: QUOTA_SPEND_COMMIT_RESULT_SCHEMA, effect_id: request.effect_id, - status: "replayed", + status: "conflict", written: false, - replayed: true, + replayed: false, repaired: false, - conflict: false, - request_digest: sha256(canonicalJson(value)), - index_digest: await quotaSpendIndexDigest(indexPath), - reason: "quota spend replayed for the same provider effect", - record: candidate, + conflict: true, + request_digest: requestFingerprint, + index_digest: lookup.indexDigest, + reason: lookup.resolution.reason, + record: null, payload: { - ...candidate, - ...basePayload, - ok: true, - idempotent_replay: true, - agent_id: candidateAgentId, - reason: "quota spend replayed for the same provider effect", + ok: false, + appended: false, + replay_found: true, + goal_id: request.goal_id, + effect_ref: request.effect_id, + reason: lookup.resolution.reason, }, + reason_code: "effect_id_conflict", }; + } + const candidate = lookup.resolution.kind === "matched" + ? lookup.resolution.record + : null; + const basePayload: JsonObject = { + ok: false, + appended: false, + replay_found: candidate !== null, + goal_id: request.goal_id, + effect_ref: request.effect_id, + }; + if (candidate === null) { + return { + schema_version: QUOTA_SPEND_COMMIT_RESULT_SCHEMA, + effect_id: request.effect_id, + status: "preview", + written: false, + replayed: false, + repaired: false, + conflict: false, + request_digest: requestFingerprint, + index_digest: lookup.indexDigest, + reason: "quota spend replay was not found", + record: null, + payload: basePayload, + }; + } + const candidateGoalId = typeof candidate.goal_id === "string" + ? candidate.goal_id.trim() + : ""; + const candidateAgentId = typeof candidate.agent_id === "string" + ? candidate.agent_id.trim() + : ""; + if ( + candidateGoalId !== request.goal_id || + !request.resolved_agent_id || + candidateAgentId !== request.resolved_agent_id + ) { + return { + schema_version: QUOTA_SPEND_COMMIT_RESULT_SCHEMA, + effect_id: request.effect_id, + status: "preview", + written: false, + replayed: false, + repaired: false, + conflict: false, + request_digest: requestFingerprint, + index_digest: lookup.indexDigest, + reason: "quota spend replay requires the same valid agent identity", + record: null, + payload: { ...basePayload, reason: "agent identity mismatch" }, + }; + } + return { + schema_version: QUOTA_SPEND_COMMIT_RESULT_SCHEMA, + effect_id: request.effect_id, + status: "replayed", + written: false, + replayed: true, + repaired: false, + conflict: false, + request_digest: requestFingerprint, + index_digest: lookup.indexDigest, + reason: "quota spend replayed for the same provider effect", + record: candidate, + payload: { + ...candidate, + ...basePayload, + ok: true, + idempotent_replay: true, + agent_id: candidateAgentId, + reason: "quota spend replayed for the same provider effect", + }, }; - return request.read_only - ? await evaluate() - : await withFileMutationLock(indexPath, evaluate); } function indexRecordFor( @@ -862,78 +656,6 @@ function indexRecordFor( return indexRecord; } -function pyValue(value: unknown): string { - if (value === true) return "True"; - if (value === false) return "False"; - if (value === null || value === undefined) return "None"; - return String(value); -} - -function markdownScalar(value: unknown): string { - return pyValue(value).replace(/\r/g, " ").replace(/\n/g, " ").replace(/\|/g, "\\|").trim(); -} - -function quotaSpendMarkdown(payload: JsonObject): string { - const before = jsonObject(payload.before) ?? {}; - const after = jsonObject(payload.after) ?? {}; - const beforeQuota = jsonObject(before.quota) ?? before; - const afterQuota = jsonObject(after.quota) ?? after; - const lines = [ - "# LoopX Quota Slot Preview", - "", - `- ok: \`${pyValue(payload.ok)}\``, - `- dry_run: \`${pyValue(payload.dry_run)}\``, - `- goal_id: \`${pyValue(payload.goal_id)}\``, - `- classification: \`${pyValue(payload.classification ?? QUOTA_SLOT_SPENT_CLASSIFICATION)}\``, - `- agent_id: \`${pyValue(payload.agent_id ?? "")}\``, - `- slots: \`${pyValue(payload.slots)}\``, - `- appended: \`${pyValue(payload.appended)}\``, - `- registry_mutated: \`${pyValue(payload.registry_mutated)}\``, - `- would_throttle: \`${pyValue(payload.would_throttle)}\``, - ]; - if (payload.json_path) lines.push(`- json_path: \`${pyValue(payload.json_path)}\``); - if (payload.index_path) lines.push(`- index_path: \`${pyValue(payload.index_path)}\``); - if (payload.reason) lines.push(`- reason: ${pyValue(payload.reason)}`); - if (Object.keys(before).length) { - lines.push( - `- before: state=${pyValue(before.state)} should_run=${pyValue(before.should_run)} ` + - `slots=${pyValue(beforeQuota.spent_slots)}/${pyValue(beforeQuota.allowed_slots)}`, - ); - } - if (Object.keys(after).length) { - lines.push( - `- after: state=${pyValue(after.state)} should_run=${pyValue(after.should_run)} ` + - `slots=${pyValue(afterQuota.spent_slots)}/${pyValue(afterQuota.allowed_slots)}`, - ); - const summary = jsonObject(after.plan_summary); - if (summary) { - lines.push( - `- after_plan_next_automatic_turn: ${pyValue(summary.next_automatic_turn ?? "none")}`, - ); - } - } - if (payload.rolling_window_note) { - lines.push(`- rolling_window_note: ${pyValue(payload.rolling_window_note)}`); - } - const operatorAction = jsonObject(payload.operator_action); - if (operatorAction) { - if (payload.error_code) lines.push(`- error_code: \`${pyValue(payload.error_code)}\``); - if (payload.incident_channel) { - lines.push(`- incident_channel: \`${pyValue(payload.incident_channel)}\``); - } - lines.push( - "- operator_action: " + - `action=${markdownScalar(operatorAction.action ?? "")} ` + - `holder_pid=${markdownScalar(operatorAction.holder_pid ?? "") || "unknown"} ` + - `retry_mode=${markdownScalar(operatorAction.retry_mode ?? "")}`, - ); - if (Array.isArray(operatorAction.steps)) { - for (const step of operatorAction.steps) lines.push(` - ${markdownScalar(step)}`); - } - } - return `${lines.join("\n")}\n`; -} - function payloadFor( request: QuotaSpendCommitRequest, record: JsonObject, @@ -1000,167 +722,6 @@ function result( }; } -function receiptObject(value: unknown): QuotaSpendCommitReceipt { - const receipt = requiredObject(value, "quota spend transaction receipt"); - if (receipt.schema_version !== QUOTA_SPEND_COMMIT_RECEIPT_SCHEMA) { - throw new EffectRuntimeRequestError("Quota spend transaction receipt schema mismatch"); - } - const status = requireStringLiteral( - receipt.status, - ["prepared", "committed"] as const, - "receipt.status", - ); - const expectedIndexBytes = requiredInteger( - receipt.expected_index_bytes, - "receipt.expected_index_bytes", - ); - if (expectedIndexBytes < 0) { - throw new EffectRuntimeRequestError("receipt.expected_index_bytes cannot be negative"); - } - return { - schema_version: QUOTA_SPEND_COMMIT_RECEIPT_SCHEMA, - effect_id: requiredString(receipt.effect_id, "receipt.effect_id"), - request_digest: requiredString(receipt.request_digest, "receipt.request_digest"), - status, - json_path: requiredString(receipt.json_path, "receipt.json_path"), - markdown_path: requiredString(receipt.markdown_path, "receipt.markdown_path"), - index_path: requiredString(receipt.index_path, "receipt.index_path"), - expected_index_digest: optionalString( - receipt.expected_index_digest, - "receipt.expected_index_digest", - ), - expected_index_bytes: expectedIndexBytes, - record: requiredObject(receipt.record, "receipt.record"), - index_record: requiredObject(receipt.index_record, "receipt.index_record"), - markdown: requiredString(receipt.markdown, "receipt.markdown"), - payload: requiredObject(receipt.payload, "receipt.payload"), - }; -} - -async function readReceipt(path: string): Promise { - const content = await readOptionalText(path); - if (content === null) return null; - let value: unknown; - try { - value = JSON.parse(content); - } catch { - throw new EffectRuntimeRequestError( - "quota spend transaction receipt is malformed", - "malformed_transaction_receipt", - ); - } - return receiptObject(value); -} - -async function ensureJsonArtifact( - path: string, - expected: JsonObject, -): Promise { - const existing = await readOptionalText(path); - if (existing === null) { - await atomicWriteJson(path, expected); - return true; - } - let actual: unknown; - try { - actual = JSON.parse(existing); - } catch { - throw new EffectRuntimeRequestError( - "quota spend JSON artifact is malformed", - "artifact_conflict", - ); - } - if (canonicalJson(actual) !== canonicalJson(expected)) { - throw new EffectRuntimeRequestError( - "quota spend JSON artifact conflicts with its transaction receipt", - "artifact_conflict", - ); - } - return false; -} - -async function ensureMarkdownArtifact( - path: string, - expected: string, -): Promise { - const existing = await readOptionalText(path); - if (existing === null) { - await atomicWriteText(path, expected); - return true; - } - if (existing !== expected) { - throw new EffectRuntimeRequestError( - "quota spend Markdown artifact conflicts with its transaction receipt", - "artifact_conflict", - ); - } - return false; -} - -async function readReceiptIndex( - receipt: QuotaSpendCommitReceipt, -): Promise<{ content: string | null; records: JsonObject[]; repaired: boolean }> { - const indexBytes = await readOptionalBytes(receipt.index_path); - let content = indexBytes === null ? null : indexBytes.toString("utf8"); - try { - return { content, records: indexRecords(content), repaired: false }; - } catch (error) { - const recovered = indexBytes === null - ? null - : repairedTruncatedTail( - indexBytes, - receipt.index_record, - receipt.expected_index_digest, - receipt.expected_index_bytes, - ); - if (recovered === null) throw error; - await atomicWriteText(receipt.index_path, recovered); - content = recovered; - return { content, records: indexRecords(content), repaired: true }; - } -} - -async function ensureReceiptArtifacts( - receipt: QuotaSpendCommitReceipt, -): Promise { - let repaired = false; - repaired = await ensureJsonArtifact(receipt.json_path, receipt.record) || repaired; - repaired = await ensureMarkdownArtifact(receipt.markdown_path, receipt.markdown) || repaired; - const index = await readReceiptIndex(receipt); - repaired = index.repaired || repaired; - const matchResolution = matchingIndexRecord(index.records, receipt.effect_id); - if (matchResolution.kind === "conflict") { - throw new EffectRuntimeRequestError( - matchResolution.reason, - "effect_id_conflict", - ); - } - const match = matchResolution.kind === "matched" - ? matchResolution.record - : null; - if (match) { - const metadata = jsonObject(match.quota_spend_commit); - if (metadata?.request_digest !== receipt.request_digest) { - throw new EffectRuntimeRequestError( - "quota spend effect identity is already bound to a different request", - "effect_id_conflict", - ); - } - } else { - const prefix = index.content ?? ""; - if (prefix && !prefix.endsWith("\n")) { - await atomicWriteText( - receipt.index_path, - `${prefix}\n${JSON.stringify(receipt.index_record)}\n`, - ); - } else { - await appendJsonLine(receipt.index_path, receipt.index_record); - } - repaired = true; - } - return repaired; -} - export async function evaluateQuotaSpendCommit( value: unknown, ): Promise { @@ -1191,7 +752,8 @@ export async function evaluateQuotaSpendCommit( const runsDir = join(request.runtime_root, "goals", request.goal_id, "runs"); const indexPath = join(runsDir, "index.jsonl"); if (!request.execute) { - const { jsonPath, markdownPath } = await nextArtifactPaths( + const { jsonPath, markdownPath } = await nextQuotaAccountingArtifactPaths( + "spend", runsDir, request.generated_at, request.effect_id, @@ -1215,147 +777,87 @@ export async function evaluateQuotaSpendCommit( ); } - return await withFileMutationLock(indexPath, async () => { - const receiptPath = transactionPath(runsDir, request.effect_id); - const existingReceipt = await readReceipt(receiptPath); - if (existingReceipt) { - if ( - existingReceipt.effect_id !== request.effect_id || - existingReceipt.request_digest !== fingerprint - ) { - const payload = { ...request.preview, ok: false, appended: false }; - return result( + const outcome = await commitQuotaAccountingArtifactTransaction({ + kind: "spend", + runsDir, + generatedAt: request.generated_at, + effectId: request.effect_id, + requestDigest: fingerprint, + expectedIndexDigest: request.expected_index_digest, + prepare: ({ jsonPath, markdownPath, indexPath: lockedIndexPath }) => { + const payload = payloadFor( + request, + record, + jsonPath, + markdownPath, + lockedIndexPath, + { appended: true, replayed: false, repaired: false }, + ); + return { + kind: "prepared", + record, + indexRecord: indexRecordFor( request, + record, + jsonPath, + markdownPath, fingerprint, - "conflict", - await quotaSpendIndexDigest(indexPath), - "quota spend effect identity is already bound to a different request", - null, + ), + markdown: renderQuotaSlotMarkdown( payload, - { reason_code: "effect_id_conflict" }, - ); - } - const repaired = await ensureReceiptArtifacts(existingReceipt); - const committedReceipt = { - ...existingReceipt, - status: "committed", - } satisfies QuotaSpendCommitReceipt; - if (existingReceipt.status !== "committed" || repaired) { - await atomicWriteJson(receiptPath, committedReceipt); - } - const replayPayload = { - ...existingReceipt.payload, - appended: repaired, - idempotent_replay: !repaired, - transaction_repaired: repaired, - reason: repaired - ? "quota spend commit repaired its prepared durable transaction" - : "quota spend commit replayed for the same effect identity", + QUOTA_SLOT_SPENT_CLASSIFICATION, + ), + payload, }; - return result( - request, - fingerprint, - repaired ? "repaired" : "replayed", - await quotaSpendIndexDigest(indexPath), - optionalString(replayPayload.reason, "replay payload reason") ?? "", - existingReceipt.record, - replayPayload, - ); - } - - const currentIndexBytes = await readOptionalBytes(indexPath); - const currentIndexContent = currentIndexBytes === null - ? null - : currentIndexBytes.toString("utf8"); - const currentDigest = currentIndexBytes === null - ? null - : sha256Bytes(currentIndexBytes); - if (request.expected_index_digest !== currentDigest) { - return result( - request, - fingerprint, - "conflict", - currentDigest, - "quota run index compare-and-swap precondition failed", - null, - { ...request.preview, ok: false, appended: false }, - { reason_code: "index_digest_conflict" }, - ); - } - const currentRecords = indexRecords(currentIndexContent); - const duplicateResolution = matchingIndexRecord(currentRecords, request.effect_id); - if (duplicateResolution.kind === "conflict") { - return result( - request, - fingerprint, - "conflict", - currentDigest, - duplicateResolution.reason, - null, - { ...request.preview, ok: false, appended: false }, - { reason_code: "effect_id_conflict" }, - ); - } - if (duplicateResolution.kind === "matched") { - return result( - request, - fingerprint, - "conflict", - currentDigest, - "quota spend effect identity already exists without a matching transaction receipt", - null, - { ...request.preview, ok: false, appended: false }, - { reason_code: "effect_id_conflict" }, - ); - } + }, + }); - const { jsonPath, markdownPath } = await nextArtifactPaths( - runsDir, - request.generated_at, - request.effect_id, - ); - const payload = payloadFor( - request, - record, - jsonPath, - markdownPath, - indexPath, - { appended: true, replayed: false, repaired: false }, - ); - const indexRecord = indexRecordFor( + if (outcome.status === "conflict") { + return result( request, - record, - jsonPath, - markdownPath, fingerprint, + "conflict", + outcome.indexDigest, + outcome.reason, + null, + { ...request.preview, ok: false, appended: false }, + { reason_code: outcome.reasonCode }, ); - const markdown = quotaSpendMarkdown(payload); - const prepared = { - schema_version: QUOTA_SPEND_COMMIT_RECEIPT_SCHEMA, - effect_id: request.effect_id, - request_digest: fingerprint, - status: "prepared", - json_path: jsonPath, - markdown_path: markdownPath, - index_path: indexPath, - expected_index_digest: currentDigest, - expected_index_bytes: currentIndexBytes?.length ?? 0, - record, - index_record: indexRecord, - markdown, - payload, - } satisfies QuotaSpendCommitReceipt; - await atomicWriteJson(receiptPath, prepared); - await ensureReceiptArtifacts(prepared); - await atomicWriteJson(receiptPath, { ...prepared, status: "committed" }); + } + if (outcome.status === "not_found") { + throw new EffectRuntimeRequestError( + "quota spend transaction preparation did not produce an artifact", + ); + } + if (outcome.status === "written") { return result( request, fingerprint, "written", - await quotaSpendIndexDigest(indexPath), + outcome.indexDigest, "quota spend transaction committed by TypeScript", - record, - payload, + outcome.receipt.record, + outcome.receipt.payload, ); - }); + } + + const repaired = outcome.status === "repaired"; + const replayPayload = { + ...outcome.receipt.payload, + appended: repaired, + idempotent_replay: !repaired, + transaction_repaired: repaired, + reason: repaired + ? "quota spend commit repaired its prepared durable transaction" + : "quota spend commit replayed for the same effect identity", + }; + return result( + request, + fingerprint, + outcome.status, + outcome.indexDigest, + optionalString(replayPayload.reason, "replay payload reason") ?? "", + outcome.receipt.record, + replayPayload, + ); } diff --git a/loopx/control_plane/quota/void_commit.py b/loopx/control_plane/quota/void_commit.py new file mode 100644 index 0000000000..04b946925c --- /dev/null +++ b/loopx/control_plane/quota/void_commit.py @@ -0,0 +1,243 @@ +from __future__ import annotations + +from collections.abc import Callable, Mapping +from pathlib import Path +from typing import Any +from uuid import uuid4 + +from ...file_lock import exclusive_file_lock +from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result +from ..runtime.time import now_local_iso +from .spend_commit import quota_spend_index_digest +from .spend_sources import DEFAULT_SLOT_SPEND_SOURCE + + +QUOTA_VOID_COMMIT_REQUEST_SCHEMA = "loopx_quota_void_commit_request_v0" +QUOTA_VOID_COMMIT_RESULT_SCHEMA = "loopx_quota_void_commit_result_v0" +QUOTA_VOID_COMMIT_STATUSES = frozenset( + {"preview", "not_found", "written", "replayed", "repaired", "conflict"} +) +_ECMASCRIPT_TRIM_CHARS = ( + "\u0009\u000a\u000b\u000c\u000d\u0020\u00a0\u1680" + "\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a" + "\u2028\u2029\u202f\u205f\u3000\ufeff" +) + + +def _ecmascript_trim(value: str) -> str: + return value.strip(_ECMASCRIPT_TRIM_CHARS) + + +def normalize_quota_void_goal_id(value: Any) -> str: + # Import lazily because runtime imports the quota compatibility surface + # while the package is initializing. + from ...runtime import validate_goal_id_path_segment + + return str(validate_goal_id_path_segment(_ecmascript_trim(str(value or "")))) + + +def _normalized_effect_id(value: str | None) -> str: + if value is None: + return f"quota-void:{uuid4().hex}" + if not isinstance(value, str) or not _ecmascript_trim(value): + raise ValueError("effect_id must be a non-empty string") + normalized = _ecmascript_trim(value) + if len(normalized.encode("utf-16-le", errors="surrogatepass")) // 2 > 256: + raise ValueError("effect_id exceeds 256 characters") + return normalized + + +def _void_result( + params: Mapping[str, Any], + *, + expected_goal_id: str | None = None, +) -> Mapping[str, Any]: + try: + result = effect_runtime_result("quota.void.commit", dict(params)) + except EffectRuntimeRejected as exc: + raise ValueError(str(exc)) from None + if ( + not isinstance(result, Mapping) + or result.get("schema_version") != QUOTA_VOID_COMMIT_RESULT_SCHEMA + ): + raise RuntimeError("TypeScript quota void commit result shape mismatch") + if result.get("status") not in QUOTA_VOID_COMMIT_STATUSES: + raise RuntimeError("TypeScript quota void commit result has an invalid status") + payload = result.get("payload") + if not isinstance(payload, Mapping): + raise RuntimeError("TypeScript quota void commit omitted its payload") + operation = params.get("operation") + if operation == "project_record": + if result.get("effect_id") is not None: + raise RuntimeError( + "TypeScript quota void projection returned an effect identity" + ) + else: + expected_effect_id = params.get("effect_id") + if result.get("effect_id") != expected_effect_id: + raise RuntimeError( + "TypeScript quota void commit result effect_id mismatch" + ) + payload_effect_id = payload.get("effect_id") + if ( + payload_effect_id is not None + and payload_effect_id != expected_effect_id + ): + raise RuntimeError( + "TypeScript quota void commit payload effect_id mismatch" + ) + if ( + result.get("status") in {"written", "replayed", "repaired"} + and payload_effect_id != expected_effect_id + ): + raise RuntimeError( + "TypeScript quota void commit payload omitted its effect identity" + ) + if ( + expected_goal_id is not None + and payload.get("goal_id") != expected_goal_id + ): + raise RuntimeError("TypeScript quota void commit payload goal_id mismatch") + if result.get("status") == "conflict": + raise ValueError(str(result.get("reason") or "quota void commit conflict")) + return result + + +def _result_payload(result: Mapping[str, Any]) -> dict[str, Any]: + payload = result.get("payload") + if not isinstance(payload, Mapping): + raise RuntimeError("TypeScript quota void commit omitted its payload") + return dict(payload) + + +def _runtime_root(status_payload: Mapping[str, Any]) -> Path: + raw_runtime_root = status_payload.get("runtime_root") + if not raw_runtime_root: + raise ValueError("status payload does not include runtime_root") + normalized = _ecmascript_trim(str(raw_runtime_root)) + if not normalized: + raise ValueError("status payload does not include runtime_root") + return Path(normalized).expanduser().resolve() + + +def commit_quota_slot_void( + status_payload: Mapping[str, Any], + *, + goal_id: str, + voided_run_generated_at: str, + before: Mapping[str, Any], + execute: bool = False, + source: str = DEFAULT_SLOT_SPEND_SOURCE, + reason_summary: str | None = None, + effect_id: str | None = None, + generated_at: str | None = None, + _operation: str = "commit", +) -> dict[str, Any]: + """Execute one TypeScript-owned quota void transaction.""" + + safe_goal_id = normalize_quota_void_goal_id(goal_id) + normalized_effect_id = _normalized_effect_id(effect_id) + runtime_root = _runtime_root(status_payload) + index_path = runtime_root / "goals" / safe_goal_id / "runs" / "index.jsonl" + params: dict[str, Any] = { + "schema_version": QUOTA_VOID_COMMIT_REQUEST_SCHEMA, + "operation": _operation, + "effect_id": normalized_effect_id, + "runtime_root": str(runtime_root), + "goal_id": safe_goal_id, + "voided_run_generated_at": str(voided_run_generated_at or "").strip(), + "source": str(source or DEFAULT_SLOT_SPEND_SOURCE).strip(), + "reason_summary": reason_summary, + "generated_at": generated_at or now_local_iso(), + "execute": execute, + "expected_index_digest": None, + "before": dict(before), + } + + if execute: + # Legacy Python run writers still use the kernel lock. Hold it across + # the one native transaction until every index writer is in-process TS. + with exclusive_file_lock(index_path, operation="quota_void_commit"): + params["expected_index_digest"] = quota_spend_index_digest(index_path) + result = _void_result(params, expected_goal_id=safe_goal_id) + else: + params["expected_index_digest"] = quota_spend_index_digest(index_path) + result = _void_result(params, expected_goal_id=safe_goal_id) + return _result_payload(result) + + +def build_quota_slot_void_preview_for_decision( + status_payload: dict[str, Any], + *, + goal_id: str, + voided_run_generated_at: str, + before: dict[str, Any], +) -> dict[str, Any]: + return commit_quota_slot_void( + status_payload, + goal_id=goal_id, + voided_run_generated_at=voided_run_generated_at, + before=before, + execute=False, + _operation="preview", + ) + + +def build_quota_slot_void_event( + preview: dict[str, Any], + *, + source: str = DEFAULT_SLOT_SPEND_SOURCE, + reason_summary: str | None = None, + generated_at: str | None = None, +) -> dict[str, Any]: + if not preview.get("ok"): + raise ValueError(preview.get("reason") or "quota slot void requires a valid preview") + result = _void_result( + { + "schema_version": QUOTA_VOID_COMMIT_REQUEST_SCHEMA, + "operation": "project_record", + "preview": dict(preview), + "source": source, + "reason_summary": reason_summary, + "generated_at": generated_at or now_local_iso(), + } + ) + record = result.get("record") + if not isinstance(record, Mapping): + raise RuntimeError("TypeScript quota void projection omitted its record") + return dict(record) + + +def record_quota_slot_void_from_preview( + preview: dict[str, Any], + status_payload: dict[str, Any], + *, + goal_id: str, + render_markdown: Callable[[dict[str, Any]], str], + execute: bool = False, + source: str = DEFAULT_SLOT_SPEND_SOURCE, + reason_summary: str | None = None, +) -> dict[str, Any]: + del render_markdown + if not preview.get("ok"): + return preview + safe_goal_id = normalize_quota_void_goal_id(goal_id) + preview_goal_id = normalize_quota_void_goal_id( + preview.get("goal_id") or safe_goal_id + ) + if preview_goal_id != safe_goal_id: + raise ValueError("quota void preview goal_id does not match commit goal_id") + before = preview.get("before") + if not isinstance(before, Mapping): + raise ValueError("quota void preview has no before decision") + return commit_quota_slot_void( + status_payload, + goal_id=safe_goal_id, + voided_run_generated_at=str( + preview.get("voided_run_generated_at") or "" + ), + before=before, + execute=execute, + source=source, + reason_summary=reason_summary, + ) diff --git a/loopx/control_plane/quota/void_commit.ts b/loopx/control_plane/quota/void_commit.ts new file mode 100644 index 0000000000..9d2448a70c --- /dev/null +++ b/loopx/control_plane/quota/void_commit.ts @@ -0,0 +1,862 @@ +import { createHash } from "node:crypto"; +import { readFile, realpath } from "node:fs/promises"; +import { basename, isAbsolute, join, relative, resolve, sep } from "node:path"; + +import type { JsonObject } from "../effect_program.ts"; +import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts"; +import { + commitQuotaAccountingArtifactTransaction, + nextQuotaAccountingArtifactPaths, + parseQuotaAccountingIndex, + quotaAccountingIndexDigest, + renderQuotaSlotMarkdown, + type QuotaAccountingArtifactPrepareContext, + type QuotaAccountingArtifactPreparation, +} from "./accounting_artifact_transaction.ts"; +import { + jsonObject, + optionalNonEmptyString as optionalString, + requireBoolean as requiredBoolean, + requireJsonObject as requiredObject, + requireNonEmptyString as requiredString, + requireStringLiteral, +} from "../runtime_decode.ts"; + +export const QUOTA_VOID_COMMIT_REQUEST_SCHEMA = + "loopx_quota_void_commit_request_v0"; +export const QUOTA_VOID_COMMIT_RESULT_SCHEMA = + "loopx_quota_void_commit_result_v0"; +export const QUOTA_VOID_COMMIT_RECEIPT_SCHEMA = + "quota_void_commit_receipt_v0"; +export const QUOTA_SLOT_VOIDED_CLASSIFICATION = "quota_slot_voided"; +const QUOTA_SLOT_SPENT_CLASSIFICATION = "quota_slot_spent"; +const QUOTA_VOID_SOURCES = [ + "heartbeat", + "controller", + "adapter", + "visible-goal", +] as const; +const ROLLING_WINDOW_NOTE = + "quota void-slot appends a quota_slot_voided accounting event. It does not delete the " + + "original spend event; rolling-window ledgers subtract the void only when the target " + + "spend event is inside the same accounting window."; + +type QuotaVoidSource = (typeof QUOTA_VOID_SOURCES)[number]; +type QuotaVoidCommitStatus = + | "preview" + | "not_found" + | "written" + | "replayed" + | "repaired" + | "conflict"; + +interface QuotaVoidCommitRequest { + schema_version: typeof QUOTA_VOID_COMMIT_REQUEST_SCHEMA; + operation: "commit" | "preview"; + effect_id: string; + runtime_root: string; + goal_id: string; + voided_run_generated_at: string; + source: QuotaVoidSource; + reason_summary: string | null; + generated_at: string; + execute: boolean; + expected_index_digest: string | null; + before: JsonObject; +} + +interface QuotaVoidProjectionRequest { + schema_version: typeof QUOTA_VOID_COMMIT_REQUEST_SCHEMA; + operation: "project_record"; + preview: JsonObject; + source: QuotaVoidSource; + reason_summary: string | null; + generated_at: string; +} + +export interface QuotaVoidCommitResult extends JsonObject { + schema_version: typeof QUOTA_VOID_COMMIT_RESULT_SCHEMA; + effect_id: string | null; + status: QuotaVoidCommitStatus; + written: boolean; + replayed: boolean; + repaired: boolean; + conflict: boolean; + request_digest: string; + index_digest: string | null; + reason: string; + record: JsonObject | null; + payload: JsonObject; + reason_code?: string; +} + +interface TargetSpend { + run: JsonObject; + event: JsonObject; +} + +interface QuotaVoidDecisionFacts { + shouldRun: boolean; + normalDeliveryAllowed: boolean; + recoveryDeliveryAllowed: boolean; + effectiveAction: string | null; + selfRepairAllowed: boolean; + capabilityRepairAllowed: boolean; + workspaceRepairAllowed: boolean; + state: string; + safeBypassAllowed: boolean; + safeBypassKind: string | null; + blockedActionScope: string | null; + quota: JsonObject; +} + +function stableValue(value: unknown): unknown { + if (Array.isArray(value)) return value.map(stableValue); + const object = jsonObject(value); + if (!object) return value; + return Object.fromEntries( + Object.entries(object) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, child]) => [key, stableValue(child)]), + ); +} + +function canonicalJson(value: unknown): string { + return JSON.stringify(stableValue(value)); +} + +function sha256(value: string): string { + return `sha256:${createHash("sha256").update(value, "utf8").digest("hex")}`; +} + +function safeGoalId(value: unknown): string { + const goalId = requiredString(value, "goal_id").trim(); + if ( + goalId === "." || + goalId === ".." || + goalId.includes("/") || + goalId.includes("\\") + ) { + throw new EffectRuntimeRequestError("goal_id must be a single path segment"); + } + if (basename(goalId) !== goalId) { + throw new EffectRuntimeRequestError("goal_id must not include path traversal"); + } + return goalId; +} + +function commitRequest(value: unknown): QuotaVoidCommitRequest { + const request = requiredObject(value, "quota.void.commit params"); + if (request.schema_version !== QUOTA_VOID_COMMIT_REQUEST_SCHEMA) { + throw new EffectRuntimeRequestError("Quota void commit request schema mismatch"); + } + const operation = request.operation === undefined + ? "commit" + : requireStringLiteral( + request.operation, + ["commit", "preview"] as const, + "operation", + ); + const runtimeRoot = requiredString(request.runtime_root, "runtime_root").trim(); + if (!isAbsolute(runtimeRoot)) { + throw new EffectRuntimeRequestError("runtime_root must be absolute"); + } + const effectId = requiredString(request.effect_id, "effect_id").trim(); + if (effectId.length > 256) { + throw new EffectRuntimeRequestError("effect_id exceeds 256 characters"); + } + const before = requiredObject(request.before, "before"); + decodeQuotaVoidDecision(before, "before"); + const execute = requiredBoolean(request.execute, "execute"); + if (operation === "preview" && execute) { + throw new EffectRuntimeRequestError( + "quota void preview operation cannot execute durable effects", + ); + } + return { + schema_version: QUOTA_VOID_COMMIT_REQUEST_SCHEMA, + operation, + effect_id: effectId, + runtime_root: runtimeRoot, + goal_id: safeGoalId(request.goal_id), + voided_run_generated_at: + typeof request.voided_run_generated_at === "string" + ? request.voided_run_generated_at.trim() + : "", + source: requireStringLiteral( + request.source, + QUOTA_VOID_SOURCES, + "source", + `quota slot void source must be one of: ${QUOTA_VOID_SOURCES.join(", ")}`, + ), + reason_summary: optionalString( + request.reason_summary, + "reason_summary", + )?.trim() ?? null, + generated_at: requiredString(request.generated_at, "generated_at").trim(), + execute, + expected_index_digest: optionalString( + request.expected_index_digest, + "expected_index_digest", + ), + before, + }; +} + +function projectionRequest(value: unknown): QuotaVoidProjectionRequest { + const request = requiredObject(value, "quota void record projection params"); + if (request.schema_version !== QUOTA_VOID_COMMIT_REQUEST_SCHEMA) { + throw new EffectRuntimeRequestError("Quota void commit request schema mismatch"); + } + return { + schema_version: QUOTA_VOID_COMMIT_REQUEST_SCHEMA, + operation: "project_record", + preview: requiredObject(request.preview, "preview"), + source: requireStringLiteral( + request.source, + QUOTA_VOID_SOURCES, + "source", + `quota slot void source must be one of: ${QUOTA_VOID_SOURCES.join(", ")}`, + ), + reason_summary: optionalString( + request.reason_summary, + "reason_summary", + )?.trim() ?? null, + generated_at: requiredString(request.generated_at, "generated_at").trim(), + }; +} + +function requestDigest(request: QuotaVoidCommitRequest): string { + return sha256(canonicalJson({ + schema_version: request.schema_version, + effect_id: request.effect_id, + runtime_root: request.runtime_root, + goal_id: request.goal_id, + voided_run_generated_at: request.voided_run_generated_at, + source: request.source, + reason_summary: request.reason_summary, + before: request.before, + })); +} + +function legacyInteger(value: unknown, fallback: number): number { + if (typeof value === "boolean") return fallback; + if (typeof value === "number" && Number.isFinite(value)) { + return Math.trunc(value); + } + if (typeof value === "string" && value.trim()) { + const normalized = value.trim(); + if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(normalized)) { + return fallback; + } + const parsed = Number(normalized); + if (Number.isFinite(parsed)) return Math.trunc(parsed); + } + return fallback; +} + +function cloneObject(value: JsonObject): JsonObject { + return JSON.parse(JSON.stringify(value)) as JsonObject; +} + +function nullableString(value: unknown, label: string): string | null { + if (value === null || value === undefined) return null; + if (typeof value !== "string") { + throw new EffectRuntimeRequestError(`${label} must be a string or null`); + } + return value; +} + +function decodeQuotaVoidDecision( + value: unknown, + label: string, +): QuotaVoidDecisionFacts { + const decision = requiredObject(value, label); + if (typeof decision.state !== "string") { + throw new EffectRuntimeRequestError(`${label}.state must be a string`); + } + return { + shouldRun: requiredBoolean(decision.should_run, `${label}.should_run`), + normalDeliveryAllowed: requiredBoolean( + decision.normal_delivery_allowed, + `${label}.normal_delivery_allowed`, + ), + recoveryDeliveryAllowed: requiredBoolean( + decision.recovery_delivery_allowed, + `${label}.recovery_delivery_allowed`, + ), + effectiveAction: nullableString( + decision.effective_action, + `${label}.effective_action`, + ), + selfRepairAllowed: requiredBoolean( + decision.self_repair_allowed, + `${label}.self_repair_allowed`, + ), + capabilityRepairAllowed: requiredBoolean( + decision.capability_repair_allowed, + `${label}.capability_repair_allowed`, + ), + workspaceRepairAllowed: requiredBoolean( + decision.workspace_repair_allowed, + `${label}.workspace_repair_allowed`, + ), + state: decision.state, + safeBypassAllowed: requiredBoolean( + decision.safe_bypass_allowed, + `${label}.safe_bypass_allowed`, + ), + safeBypassKind: nullableString( + decision.safe_bypass_kind, + `${label}.safe_bypass_kind`, + ), + blockedActionScope: nullableString( + decision.blocked_action_scope, + `${label}.blocked_action_scope`, + ), + quota: requiredObject(decision.quota, `${label}.quota`), + }; +} + +function normalizedAgentId(before: JsonObject): string | null { + const identity = jsonObject(before.agent_identity); + if (!identity || typeof identity.agent_id !== "string") return null; + const value = identity.agent_id.trim().toLowerCase().replace(/ +/g, "-"); + return /^[a-z][a-z0-9_.:@-]{0,79}$/.test(value) ? value : null; +} + +function compactDecision(value: unknown): JsonObject { + const decision = jsonObject(value) ?? {}; + const quota = jsonObject(decision.quota) ?? {}; + return { + should_run: Boolean(decision.should_run), + normal_delivery_allowed: Boolean(decision.normal_delivery_allowed), + recovery_delivery_allowed: Boolean(decision.recovery_delivery_allowed), + effective_action: decision.effective_action ?? null, + self_repair_allowed: Boolean(decision.self_repair_allowed), + capability_repair_allowed: Boolean(decision.capability_repair_allowed), + workspace_repair_allowed: Boolean(decision.workspace_repair_allowed), + state: String(decision.state ?? ""), + safe_bypass_allowed: Boolean(decision.safe_bypass_allowed), + safe_bypass_kind: decision.safe_bypass_kind ?? null, + blocked_action_scope: decision.blocked_action_scope ?? null, + compute: quota.compute ?? null, + window_hours: quota.window_hours ?? null, + slot_minutes: quota.slot_minutes ?? null, + spent_slots: quota.spent_slots ?? null, + allowed_slots: quota.allowed_slots ?? null, + }; +} + +async function readTargetEvent( + runsDir: string, + run: JsonObject, + goalId: string, +): Promise { + const inline = jsonObject(run.quota_event); + if (inline) return inline; + if (typeof run.json_path !== "string" || !run.json_path.trim()) return null; + let targetPath: string; + try { + targetPath = await realpath(resolve(run.json_path.trim())); + } catch (error) { + if (error instanceof Error && "code" in error && error.code === "ENOENT") { + return null; + } + throw error; + } + const relativePath = relative(await realpath(runsDir), targetPath); + if ( + !relativePath || + relativePath === ".." || + relativePath.startsWith(`..${sep}`) || + isAbsolute(relativePath) + ) { + throw new EffectRuntimeRequestError( + "quota spend json_path must stay inside the goal runs directory", + ); + } + let content: string; + try { + content = await readFile(targetPath, "utf8"); + } catch (error) { + if (error instanceof Error && "code" in error && error.code === "ENOENT") return null; + throw error; + } + let value: unknown; + try { + value = JSON.parse(content); + } catch { + throw new EffectRuntimeRequestError("quota spend JSON artifact is malformed"); + } + const record = requiredObject(value, "quota spend JSON artifact"); + if ( + record.classification !== QUOTA_SLOT_SPENT_CLASSIFICATION || + (record.goal_id !== undefined && record.goal_id !== goalId) + ) { + throw new EffectRuntimeRequestError( + "quota spend JSON artifact identity does not match its index row", + ); + } + return jsonObject(record.quota_event); +} + +async function findTargetSpend( + runsDir: string, + records: readonly JsonObject[], + goalId: string, + generatedAt: string, +): Promise { + for (const run of [...records].reverse()) { + if (String(run.goal_id || goalId) !== goalId) continue; + if (String(run.generated_at ?? "") !== generatedAt) continue; + if (run.classification !== QUOTA_SLOT_SPENT_CLASSIFICATION) continue; + const event = await readTargetEvent(runsDir, run, goalId); + if (event?.event_type !== QUOTA_SLOT_SPENT_CLASSIFICATION) continue; + return { run, event }; + } + return null; +} + +function missingTargetPayload( + goalId: string, + generatedAt: string, +): JsonObject { + if (!generatedAt) { + return { + ok: false, + mode: "void-slot", + dry_run: true, + goal_id: goalId, + appended: false, + registry_mutated: false, + reason: "`quota void-slot` requires --void-generated-at", + }; + } + return { + ok: false, + mode: "void-slot", + dry_run: true, + goal_id: goalId, + voided_run_generated_at: generatedAt, + appended: false, + registry_mutated: false, + reason: "target quota_slot_spent run was not found in the goal runtime index", + }; +} + +function previewFor( + request: QuotaVoidCommitRequest, + target: TargetSpend, +): JsonObject { + const slots = Math.max(1, legacyInteger(target.event.slots, 1)); + const before = cloneObject(request.before); + const beforeQuota = jsonObject(before.quota) ?? {}; + const after = cloneObject(before); + const afterQuota = { ...beforeQuota }; + afterQuota.spent_slots = Math.max( + 0, + legacyInteger(beforeQuota.spent_slots, 0) - slots, + ); + after.quota = afterQuota; + return { + ok: true, + mode: "void-slot", + dry_run: true, + goal_id: request.goal_id, + slots, + voided_run_generated_at: request.voided_run_generated_at, + voided_run_classification: target.run.classification, + voided_run_json_path: target.run.json_path ?? null, + appended: false, + registry_mutated: false, + before, + after, + would_throttle: false, + reason: + `dry-run preview: voiding ${slots} slot(s) from ${request.goal_id} ` + + `quota spend run ${request.voided_run_generated_at}`, + rolling_window_note: ROLLING_WINDOW_NOTE, + classification: QUOTA_SLOT_VOIDED_CLASSIFICATION, + }; +} + +function recordFor( + preview: JsonObject, + source: QuotaVoidSource, + reasonSummary: string | null, + generatedAt: string, + effectId: string | null, + fingerprint: string, +): JsonObject { + if (preview.ok !== true) { + throw new EffectRuntimeRequestError( + typeof preview.reason === "string" + ? preview.reason + : "quota slot void requires a valid preview", + ); + } + const safeReason = reasonSummary || + "void duplicate or invalid quota slot spend event"; + const before = jsonObject(preview.before) ?? {}; + const after = jsonObject(preview.after) ?? {}; + const agentId = normalizedAgentId(before); + const event: JsonObject = { + event_type: QUOTA_SLOT_VOIDED_CLASSIFICATION, + source, + slots: Math.max(1, legacyInteger(preview.slots, 1)), + reason_summary: safeReason, + voided_run_generated_at: preview.voided_run_generated_at ?? null, + voided_run_classification: preview.voided_run_classification ?? null, + before: Object.keys(before).length ? compactDecision(before) : {}, + after: Object.keys(after).length ? compactDecision(after) : {}, + }; + const record: JsonObject = { + generated_at: generatedAt, + goal_id: preview.goal_id ?? null, + classification: QUOTA_SLOT_VOIDED_CLASSIFICATION, + recommended_action: safeReason, + health_check: + "quota slot void event public-safe; original spend preserved for audit", + quota_event: event, + }; + if (agentId) { + record.agent_id = agentId; + event.agent_id = agentId; + } + if (effectId) { + record.quota_void_commit = { + schema_version: QUOTA_VOID_COMMIT_RECEIPT_SCHEMA, + effect_id: effectId, + request_digest: fingerprint, + }; + } + return record; +} + +function artifactsFor( + request: QuotaVoidCommitRequest, + fingerprint: string, + preview: JsonObject, + context: Pick< + QuotaAccountingArtifactPrepareContext, + "jsonPath" | "markdownPath" | "indexPath" + >, +): Extract { + const record = recordFor( + preview, + request.source, + request.reason_summary, + request.generated_at, + request.effect_id, + fingerprint, + ); + const event = requiredObject(record.quota_event, "record.quota_event"); + const payload: JsonObject = { + ...preview, + dry_run: !request.execute, + appended: request.execute, + registry_mutated: false, + source: event.source, + classification: QUOTA_SLOT_VOIDED_CLASSIFICATION, + generated_at: request.generated_at, + agent_id: record.agent_id ?? null, + effect_id: request.effect_id, + quota_event: event, + json_path: context.jsonPath, + markdown_path: context.markdownPath, + index_path: context.indexPath, + reason: + `${request.execute ? "appended" : "dry-run preview"} quota slot void event: ` + + `${request.goal_id} voided ${event.slots} slot(s) from ` + + `${event.voided_run_generated_at}`, + }; + if (request.execute) { + payload.before = event.before; + payload.after = event.after; + } + const indexRecord: JsonObject = { + generated_at: request.generated_at, + goal_id: request.goal_id, + classification: QUOTA_SLOT_VOIDED_CLASSIFICATION, + recommended_action: record.recommended_action, + health_check: record.health_check, + json_path: context.jsonPath, + markdown_path: context.markdownPath, + quota_void_commit: record.quota_void_commit, + }; + if (record.agent_id) indexRecord.agent_id = record.agent_id; + return { + kind: "prepared", + record, + indexRecord, + markdown: renderQuotaSlotMarkdown( + payload, + QUOTA_SLOT_VOIDED_CLASSIFICATION, + ), + payload, + }; +} + +async function prepareArtifacts( + request: QuotaVoidCommitRequest, + fingerprint: string, + runsDir: string, + context: QuotaAccountingArtifactPrepareContext, +): Promise { + const target = await findTargetSpend( + runsDir, + context.indexRecords, + request.goal_id, + request.voided_run_generated_at, + ); + if (!target) { + const payload = missingTargetPayload( + request.goal_id, + request.voided_run_generated_at, + ); + return { + kind: "not_found", + reason: String(payload.reason), + payload, + }; + } + return artifactsFor( + request, + fingerprint, + previewFor(request, target), + context, + ); +} + +function result( + effectId: string | null, + fingerprint: string, + status: QuotaVoidCommitStatus, + indexDigest: string | null, + reason: string, + record: JsonObject | null, + payload: JsonObject, + reasonCode?: string, +): QuotaVoidCommitResult { + return { + schema_version: QUOTA_VOID_COMMIT_RESULT_SCHEMA, + effect_id: effectId, + status, + written: status === "written", + replayed: status === "replayed", + repaired: status === "repaired", + conflict: status === "conflict", + request_digest: fingerprint, + index_digest: indexDigest, + reason, + record, + payload, + ...(reasonCode ? { reason_code: reasonCode } : {}), + }; +} + +async function previewCommit( + request: QuotaVoidCommitRequest, + fingerprint: string, + runsDir: string, +): Promise { + const indexPath = join(runsDir, "index.jsonl"); + let content: string | null; + try { + content = await readFile(indexPath, "utf8"); + } catch (error) { + if (error instanceof Error && "code" in error && error.code === "ENOENT") { + content = null; + } else { + throw error; + } + } + const records = parseQuotaAccountingIndex(content); + const indexDigest = await quotaAccountingIndexDigest(indexPath); + if (request.operation === "preview") { + const target = await findTargetSpend( + runsDir, + records, + request.goal_id, + request.voided_run_generated_at, + ); + if (!target) { + const payload = missingTargetPayload( + request.goal_id, + request.voided_run_generated_at, + ); + return result( + request.effect_id, + fingerprint, + "not_found", + indexDigest, + String(payload.reason), + null, + payload, + "target_not_found", + ); + } + const payload = previewFor(request, target); + return result( + request.effect_id, + fingerprint, + "preview", + indexDigest, + String(payload.reason), + null, + payload, + ); + } + const paths = await nextQuotaAccountingArtifactPaths( + "void", + runsDir, + request.generated_at, + request.effect_id, + ); + const preparation = await prepareArtifacts(request, fingerprint, runsDir, { + jsonPath: paths.jsonPath, + markdownPath: paths.markdownPath, + indexPath, + indexDigest, + indexRecords: records, + }); + if (preparation.kind === "not_found") { + return result( + request.effect_id, + fingerprint, + "not_found", + indexDigest, + preparation.reason, + null, + preparation.payload, + "target_not_found", + ); + } + return result( + request.effect_id, + fingerprint, + "preview", + indexDigest, + "quota void transaction preview evaluated by TypeScript", + preparation.record, + preparation.payload, + ); +} + +async function evaluateCommit( + request: QuotaVoidCommitRequest, +): Promise { + const fingerprint = requestDigest(request); + const runsDir = join( + request.runtime_root, + "goals", + request.goal_id, + "runs", + ); + if (!request.execute) { + return await previewCommit(request, fingerprint, runsDir); + } + const outcome = await commitQuotaAccountingArtifactTransaction({ + kind: "void", + runsDir, + generatedAt: request.generated_at, + effectId: request.effect_id, + requestDigest: fingerprint, + expectedIndexDigest: request.expected_index_digest, + prepare: async (context) => + await prepareArtifacts(request, fingerprint, runsDir, context), + }); + if (outcome.status === "conflict") { + return result( + request.effect_id, + fingerprint, + "conflict", + outcome.indexDigest, + outcome.reason, + null, + { + ok: false, + mode: "void-slot", + goal_id: request.goal_id, + effect_id: request.effect_id, + appended: false, + registry_mutated: false, + }, + outcome.reasonCode, + ); + } + if (outcome.status === "not_found") { + return result( + request.effect_id, + fingerprint, + "not_found", + outcome.indexDigest, + outcome.reason, + null, + outcome.payload, + "target_not_found", + ); + } + const replayed = outcome.status === "replayed"; + const repaired = outcome.status === "repaired"; + const responsePayload: JsonObject = { + ...outcome.receipt.payload, + appended: outcome.status === "written" || repaired, + idempotent_replay: replayed, + transaction_repaired: repaired, + reason: replayed + ? "quota void commit replayed for the same effect identity" + : repaired + ? "quota void commit repaired its prepared durable transaction" + : outcome.receipt.payload.reason, + }; + return result( + request.effect_id, + fingerprint, + outcome.status, + outcome.indexDigest, + String(responsePayload.reason ?? ""), + outcome.receipt.record, + responsePayload, + ); +} + +function evaluateProjection( + request: QuotaVoidProjectionRequest, +): QuotaVoidCommitResult { + const fingerprint = sha256(canonicalJson(request)); + const record = recordFor( + request.preview, + request.source, + request.reason_summary, + request.generated_at, + null, + fingerprint, + ); + return result( + null, + fingerprint, + "preview", + null, + "quota void record projected by TypeScript", + record, + request.preview, + ); +} + +export async function evaluateQuotaVoidCommit( + value: unknown, +): Promise { + const raw = requiredObject(value, "quota.void.commit params"); + if (raw.operation === "project_record") { + return evaluateProjection(projectionRequest(raw)); + } + return await evaluateCommit(commitRequest(raw)); +} + +export async function quotaVoidIndexDigest( + indexPath: string, +): Promise { + return await quotaAccountingIndexDigest(indexPath); +} diff --git a/loopx/quota.py b/loopx/quota.py index 2029316a3a..91d39a9789 100644 --- a/loopx/quota.py +++ b/loopx/quota.py @@ -41,7 +41,6 @@ ) from .presentation.renderers.quota_event_markdown import ( render_quota_monitor_poll_markdown as _render_quota_monitor_poll_markdown, - render_quota_slot_preview_markdown as _render_quota_slot_preview_markdown, render_quota_slot_preview_markdown as render_quota_slot_preview_markdown, ) from .presentation.renderers.quota_markdown import ( @@ -62,13 +61,17 @@ QUOTA_SLOT_VOIDED_CLASSIFICATION, build_quota_slot_preview_for_decision, build_quota_slot_spend_event as _build_quota_slot_spend_event, - build_quota_slot_void_event as build_quota_slot_void_event, - build_quota_slot_void_preview_for_decision, load_quota_event_from_run, record_quota_slot_spend_from_preview, - record_quota_slot_void_from_preview, ) from .control_plane.quota.spend_commit import replay_quota_spend_by_effect_ref +from .control_plane.quota.void_commit import ( + build_quota_slot_void_event as build_quota_slot_void_event, + build_quota_slot_void_preview_for_decision, + commit_quota_slot_void, + normalize_quota_void_goal_id as _normalize_quota_void_goal_id, + record_quota_slot_void_from_preview as record_quota_slot_void_from_preview, +) from .control_plane.quota.spend_sources import ( DEFAULT_SLOT_SPEND_SOURCE, TURN_SCOPED_SLOT_SPEND_SOURCES, @@ -116,7 +119,8 @@ "render_quota_markdown": "loopx.presentation.renderers.quota_markdown", "render_quota_scheduler_ack_markdown": "loopx.presentation.renderers.quota_markdown", "render_quota_should_run_markdown": "loopx.presentation.renderers.quota_markdown", - "build_quota_slot_void_event": "loopx.control_plane.quota.slot_accounting", + "build_quota_slot_void_event": "loopx.control_plane.quota.void_commit", + "record_quota_slot_void_from_preview": "loopx.control_plane.quota.void_commit", "render_quota_slot_preview_markdown": "loopx.presentation.renderers.quota_event_markdown", } @@ -1144,7 +1148,7 @@ def build_quota_slot_void_preview( agent_id: str | None = None, operator_inbox_urgency_projector: Callable[..., dict[str, Any]] | None = None, ) -> dict[str, Any]: - safe_goal_id = _validate_goal_id_path_segment(str(goal_id or "")) + safe_goal_id = _normalize_quota_void_goal_id(goal_id) before = build_quota_should_run( status_payload, goal_id=safe_goal_id, @@ -1170,22 +1174,18 @@ def void_quota_slot( agent_id: str | None = None, operator_inbox_urgency_projector: Callable[..., dict[str, Any]] | None = None, ) -> dict[str, Any]: - safe_goal_id = _validate_goal_id_path_segment(str(goal_id or "")) - preview = build_quota_slot_void_preview( + safe_goal_id = _normalize_quota_void_goal_id(goal_id) + before = build_quota_should_run( status_payload, goal_id=safe_goal_id, - voided_run_generated_at=voided_run_generated_at, agent_id=agent_id, operator_inbox_urgency_projector=operator_inbox_urgency_projector, ) - if not preview.get("ok"): - return preview - - return record_quota_slot_void_from_preview( - preview, + return commit_quota_slot_void( status_payload, goal_id=safe_goal_id, - render_markdown=_render_quota_slot_preview_markdown, + voided_run_generated_at=voided_run_generated_at, + before=before, execute=execute, source=source, reason_summary=reason_summary, diff --git a/tsconfig.control-plane.json b/tsconfig.control-plane.json index 4bbfd66289..6d6f4ca566 100644 --- a/tsconfig.control-plane.json +++ b/tsconfig.control-plane.json @@ -27,7 +27,9 @@ "loopx/control_plane/quota/settlement_workspace_causality.ts", "loopx/control_plane/quota/settlement_readback.ts", "loopx/control_plane/quota/monitor_poll_commit.ts", + "loopx/control_plane/quota/accounting_artifact_transaction.ts", "loopx/control_plane/quota/spend_commit.ts", + "loopx/control_plane/quota/void_commit.ts", "loopx/control_plane/quota/turn_envelope.ts", "loopx/control_plane/scheduler/state_store.ts", "loopx/control_plane/scheduler/heartbeat_commit.ts", @@ -60,6 +62,7 @@ "tests/control_plane_ts/quota_settlement_readback.test.ts", "tests/control_plane_ts/quota_monitor_poll_commit.test.ts", "tests/control_plane_ts/quota_spend_commit.test.ts", + "tests/control_plane_ts/quota_void_commit.test.ts", "tests/control_plane_ts/scheduler_state_store.test.ts", "tests/control_plane_ts/scheduler_heartbeat_commit.test.ts", "tests/control_plane_ts/scheduler_heartbeat_commit_cli.test.ts", From c85e702ab8115307e30e5a947e09917e3cf9b7d1 Mon Sep 17 00:00:00 2001 From: hyk <4408344+hhyykk@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:47:31 +0800 Subject: [PATCH 2/3] test(quota): cover native void transaction Signed-off-by: hyk <4408344+hhyykk@users.noreply.github.com> --- examples/control_plane/quota_plan_fixtures.py | 16 +- .../test_quota_void_commit_runtime.py | 743 ++++++++++++++++++ .../quota_spend_commit.test.ts | 64 ++ .../quota_void_commit.test.ts | 596 ++++++++++++++ 4 files changed, 1416 insertions(+), 3 deletions(-) create mode 100644 tests/control_plane/test_quota_void_commit_runtime.py create mode 100644 tests/control_plane_ts/quota_void_commit.test.ts diff --git a/examples/control_plane/quota_plan_fixtures.py b/examples/control_plane/quota_plan_fixtures.py index 68eeb3cbbd..c1245be307 100644 --- a/examples/control_plane/quota_plan_fixtures.py +++ b/examples/control_plane/quota_plan_fixtures.py @@ -847,9 +847,19 @@ def assert_slot_void_execute( assert forbidden.isdisjoint(record), record assert forbidden.isdisjoint(record["quota_event"]), record index_lines = index_path.read_text(encoding="utf-8").splitlines() - assert any('"classification": "quota_slot_spent"' in line for line in index_lines), index_lines - assert any('"classification": "quota_slot_voided"' in line for line in index_lines), index_lines - assert any(f'"agent_id": "{SCOPED_AGENT_ID}"' in line for line in index_lines), index_lines + index_records = [json.loads(line) for line in index_lines] + assert any( + item.get("classification") == "quota_slot_spent" + for item in index_records + ), index_records + assert any( + item.get("classification") == "quota_slot_voided" + for item in index_records + ), index_records + assert any( + item.get("agent_id") == SCOPED_AGENT_ID + for item in index_records + ), index_records assert next_should_run["goal_id"] == "near-limit-half", next_should_run assert next_should_run["should_run"] is True, next_should_run diff --git a/tests/control_plane/test_quota_void_commit_runtime.py b/tests/control_plane/test_quota_void_commit_runtime.py new file mode 100644 index 0000000000..671c7fb2f2 --- /dev/null +++ b/tests/control_plane/test_quota_void_commit_runtime.py @@ -0,0 +1,743 @@ +from __future__ import annotations + +import hashlib +import json +from collections.abc import Iterator +from contextlib import contextmanager +from pathlib import Path +from typing import Any + +import pytest + +import loopx.quota as quota_facade +from loopx.control_plane.quota import void_commit +from loopx.control_plane.quota.decision_summary import compact_quota_decision +from loopx.control_plane.quota.void_commit import ( + QUOTA_VOID_COMMIT_REQUEST_SCHEMA, + QUOTA_VOID_COMMIT_RESULT_SCHEMA, + build_quota_slot_void_event, + build_quota_slot_void_preview_for_decision, + commit_quota_slot_void, + record_quota_slot_void_from_preview, +) +from loopx.history import repair_index_duplicates +from loopx.presentation.renderers.quota_event_markdown import ( + render_quota_slot_preview_markdown, +) +from loopx.quota import record_quota_slot_void_from_preview as legacy_record_void + + +GOAL_ID = "quota-void-commit-runtime" +AGENT_ID = "codex-main-control" +TARGET_AT = "2026-08-31T09:00:00+08:00" +VOID_AT = "2026-08-31T09:05:00+08:00" +REASON = "void duplicate quota spend" + + +def test_legacy_void_commit_import_paths_remain_compatible() -> None: + from loopx.control_plane.quota import slot_accounting + + assert ( + slot_accounting.build_quota_slot_void_preview_for_decision + is build_quota_slot_void_preview_for_decision + ) + assert slot_accounting.build_quota_slot_void_event is build_quota_slot_void_event + assert ( + slot_accounting.record_quota_slot_void_from_preview + is record_quota_slot_void_from_preview + ) + assert ( + legacy_record_void is record_quota_slot_void_from_preview + ) + + +def _decision(spent_slots: int) -> dict[str, Any]: + return { + "should_run": True, + "normal_delivery_allowed": True, + "recovery_delivery_allowed": False, + "effective_action": "advance", + "self_repair_allowed": False, + "capability_repair_allowed": False, + "workspace_repair_allowed": False, + "state": "eligible", + "safe_bypass_allowed": False, + "safe_bypass_kind": None, + "blocked_action_scope": None, + "agent_identity": {"agent_id": AGENT_ID}, + "quota": { + "compute": 1.0, + "window_hours": 24, + "slot_minutes": 1, + "spent_slots": spent_slots, + "allowed_slots": 1440, + }, + } + + +def _write_spend_target(runtime_root: Path) -> tuple[Path, Path]: + runs_dir = runtime_root / "goals" / GOAL_ID / "runs" + runs_dir.mkdir(parents=True, exist_ok=True) + target_path = runs_dir / "target-quota-slot-spent.json" + target = { + "generated_at": TARGET_AT, + "goal_id": GOAL_ID, + "classification": "quota_slot_spent", + "agent_id": AGENT_ID, + "quota_event": { + "event_type": "quota_slot_spent", + "source": "heartbeat", + "slots": 2, + "reason_summary": "fixture spend", + "agent_id": AGENT_ID, + "before": compact_quota_decision(_decision(0)), + "after": compact_quota_decision(_decision(2)), + }, + } + target_path.write_text( + json.dumps(target, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + index_path = runs_dir / "index.jsonl" + # Cover the legacy bounded-artifact fallback instead of relying only on + # quota_event being embedded in the compact index row. + index_path.write_text( + json.dumps( + { + "generated_at": TARGET_AT, + "goal_id": GOAL_ID, + "classification": "quota_slot_spent", + "agent_id": AGENT_ID, + "json_path": str(target_path), + "markdown_path": str(target_path.with_suffix(".md")), + } + ) + + "\n", + encoding="utf-8", + ) + return index_path, target_path + + +def _preview(runtime_root: Path) -> dict[str, Any]: + return { + "ok": True, + "mode": "void-slot", + "dry_run": True, + "goal_id": GOAL_ID, + "slots": 2, + "voided_run_generated_at": TARGET_AT, + "voided_run_classification": "quota_slot_spent", + "voided_run_json_path": str( + runtime_root / "goals" / GOAL_ID / "runs" / "target-quota-slot-spent.json" + ), + "appended": False, + "registry_mutated": False, + "before": _decision(2), + "after": _decision(0), + "would_throttle": False, + "reason": ( + f"dry-run preview: voiding 2 slot(s) from {GOAL_ID} " + f"quota spend run {TARGET_AT}" + ), + "rolling_window_note": ( + "quota void-slot appends a quota_slot_voided accounting event. It does not delete the " + "original spend event; rolling-window ledgers subtract the void only when the target " + "spend event is inside the same accounting window." + ), + "classification": "quota_slot_voided", + } + + +def _record(preview: dict[str, Any]) -> dict[str, Any]: + return { + "generated_at": VOID_AT, + "goal_id": GOAL_ID, + "classification": "quota_slot_voided", + "recommended_action": REASON, + "health_check": "quota slot void event public-safe; original spend preserved for audit", + "agent_id": AGENT_ID, + "quota_event": { + "event_type": "quota_slot_voided", + "source": "heartbeat", + "slots": 2, + "reason_summary": REASON, + "voided_run_generated_at": TARGET_AT, + "voided_run_classification": "quota_slot_spent", + "before": compact_quota_decision(preview["before"]), + "after": compact_quota_decision(preview["after"]), + "agent_id": AGENT_ID, + }, + } + + +def _typed_result(params: dict[str, Any], runtime_root: Path) -> dict[str, Any]: + preview = _preview(runtime_root) + effect_id = ( + None + if params.get("operation") == "project_record" + else str(params.get("effect_id") or "quota-void:test") + ) + execute = bool(params.get("execute")) + runs_dir = runtime_root / "goals" / GOAL_ID / "runs" + payload = { + **preview, + **( + { + "dry_run": False, + "appended": True, + "source": "heartbeat", + "generated_at": VOID_AT, + "agent_id": AGENT_ID, + "quota_event": _record(preview)["quota_event"], + "json_path": str(runs_dir / "quota-slot-voided.json"), + "markdown_path": str(runs_dir / "quota-slot-voided.md"), + "index_path": str(runs_dir / "index.jsonl"), + "effect_id": effect_id, + } + if execute + else {} + ), + } + status = "written" if execute else "preview" + return { + "schema_version": QUOTA_VOID_COMMIT_RESULT_SCHEMA, + "effect_id": effect_id, + "status": status, + "written": execute, + "replayed": False, + "repaired": False, + "conflict": False, + "request_digest": "sha256:" + ("0" * 64), + "index_digest": params.get("expected_index_digest"), + "reason": "typed quota void test result", + "record": _record(preview), + "payload": payload, + } + + +def test_real_runtime_preserves_public_payloads_and_three_artifact_write( + tmp_path: Path, +) -> None: + runtime_root = tmp_path / "runtime" + index_path, _target_path = _write_spend_target(runtime_root) + status = {"runtime_root": str(runtime_root)} + + preview = build_quota_slot_void_preview_for_decision( + status, + goal_id=GOAL_ID, + voided_run_generated_at=TARGET_AT, + before=_decision(2), + ) + legacy_preview = _preview(runtime_root) + for key, expected in legacy_preview.items(): + assert preview[key] == expected, key + + record = build_quota_slot_void_event( + preview, + source="heartbeat", + reason_summary=REASON, + generated_at=VOID_AT, + ) + legacy_record = _record(legacy_preview) + for key, expected in legacy_record.items(): + assert record[key] == expected, key + + written = record_quota_slot_void_from_preview( + preview, + status, + goal_id=GOAL_ID, + render_markdown=render_quota_slot_preview_markdown, + execute=True, + source="heartbeat", + reason_summary=REASON, + ) + assert written["ok"] is True + assert written["appended"] is True + assert written["dry_run"] is False + assert written["classification"] == "quota_slot_voided" + assert written["quota_event"]["voided_run_generated_at"] == TARGET_AT + assert written["before"] == compact_quota_decision(_decision(2)) + assert written["after"] == compact_quota_decision(_decision(0)) + + json_path = Path(written["json_path"]) + markdown_path = Path(written["markdown_path"]) + persisted = json.loads(json_path.read_text(encoding="utf-8")) + assert persisted["quota_event"] == written["quota_event"] + assert markdown_path.read_text(encoding="utf-8") == ( + render_quota_slot_preview_markdown(written) + "\n" + ) + rows = [json.loads(line) for line in index_path.read_text(encoding="utf-8").splitlines()] + assert [row["classification"] for row in rows] == [ + "quota_slot_spent", + "quota_slot_voided", + ] + assert rows[-1]["json_path"] == str(json_path) + assert rows[-1]["markdown_path"] == str(markdown_path) + + +def test_real_runtime_normalizes_ecmascript_goal_and_effect_identity( + tmp_path: Path, +) -> None: + runtime_root = tmp_path / "runtime" + index_path, _target_path = _write_spend_target(runtime_root) + effect_id = "quota-void:bom-normalized" + + written = commit_quota_slot_void( + {"runtime_root": str(runtime_root)}, + goal_id=f"\ufeff{GOAL_ID}\ufeff", + voided_run_generated_at=TARGET_AT, + before=_decision(2), + execute=True, + source="heartbeat", + reason_summary=REASON, + effect_id=f"\ufeff{effect_id}\ufeff", + generated_at=VOID_AT, + ) + + rows = [ + json.loads(line) + for line in index_path.read_text(encoding="utf-8").splitlines() + ] + assert written["goal_id"] == GOAL_ID + assert written["effect_id"] == effect_id + assert rows[-1]["quota_void_commit"]["effect_id"] == effect_id + assert not (runtime_root / "goals" / f"\ufeff{GOAL_ID}\ufeff").exists() + + +def test_public_entrypoint_normalizes_goal_before_building_decision( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + runtime_root = tmp_path / "runtime" + _write_spend_target(runtime_root) + observed_goal_ids: list[str] = [] + + def should_run( + _status_payload: dict[str, Any], + *, + goal_id: str, + **_kwargs: Any, + ) -> dict[str, Any]: + observed_goal_ids.append(goal_id) + return _decision(2) + + monkeypatch.setattr(quota_facade, "build_quota_should_run", should_run) + + written = quota_facade.void_quota_slot( + {"runtime_root": str(runtime_root)}, + goal_id=f"\ufeff{GOAL_ID}\ufeff", + voided_run_generated_at=TARGET_AT, + execute=True, + source="heartbeat", + reason_summary=REASON, + ) + + assert observed_goal_ids == [GOAL_ID] + assert written["goal_id"] == GOAL_ID + assert not (runtime_root / "goals" / f"\ufeff{GOAL_ID}\ufeff").exists() + + +def test_replay_survives_supported_duplicate_index_repair(tmp_path: Path) -> None: + runtime_root = tmp_path / "runtime" + index_path, _target_path = _write_spend_target(runtime_root) + target_row = index_path.read_text(encoding="utf-8") + index_path.write_text(target_row + target_row, encoding="utf-8") + registry_path = tmp_path / "project" / ".loopx" / "registry.json" + registry_path.parent.mkdir(parents=True) + registry_path.write_text( + json.dumps( + { + "schema_version": "0.1", + "updated_at": VOID_AT, + "common_runtime_root": str(runtime_root), + "goals": [ + { + "id": GOAL_ID, + "repo": str(tmp_path / "project"), + "status": "active-read-only", + "adapter": { + "kind": "fixture", + "status": "connected-read-only", + }, + } + ], + } + ) + + "\n", + encoding="utf-8", + ) + arguments = { + "status_payload": {"runtime_root": str(runtime_root)}, + "goal_id": GOAL_ID, + "voided_run_generated_at": TARGET_AT, + "before": _decision(2), + "execute": True, + "source": "heartbeat", + "reason_summary": REASON, + "effect_id": "quota-void:supported-index-repair", + "generated_at": VOID_AT, + } + + written = commit_quota_slot_void(**arguments) + repair = repair_index_duplicates( + registry_path=registry_path, + runtime_root_override=str(runtime_root), + goal_id=GOAL_ID, + limit=10, + execute=True, + ) + replayed = commit_quota_slot_void(**arguments) + + assert written["appended"] is True + assert repair["removed_row_count"] == 1 + assert replayed["idempotent_replay"] is True + assert replayed["appended"] is False + assert len(index_path.read_text(encoding="utf-8").splitlines()) == 2 + + +def test_independent_invocations_against_one_target_append_twice( + tmp_path: Path, +) -> None: + runtime_root = tmp_path / "runtime" + index_path, _target_path = _write_spend_target(runtime_root) + status = {"runtime_root": str(runtime_root)} + preview = _preview(runtime_root) + + def append_once() -> dict[str, Any]: + return record_quota_slot_void_from_preview( + preview, + status, + goal_id=GOAL_ID, + render_markdown=render_quota_slot_preview_markdown, + execute=True, + source="heartbeat", + reason_summary=REASON, + ) + + first = append_once() + second = append_once() + + assert first["appended"] is True + assert second["appended"] is True + assert first["effect_id"] != second["effect_id"] + assert first["json_path"] != second["json_path"] + rows = [json.loads(line) for line in index_path.read_text(encoding="utf-8").splitlines()] + void_rows = [row for row in rows if row.get("classification") == "quota_slot_voided"] + assert len(void_rows) == 2 + for row in void_rows: + persisted = json.loads(Path(row["json_path"]).read_text(encoding="utf-8")) + assert persisted["quota_event"]["voided_run_generated_at"] == TARGET_AT + + +def test_each_python_facade_uses_one_typed_runtime_request( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + runtime_root = tmp_path / "runtime" + _write_spend_target(runtime_root) + status = {"runtime_root": str(runtime_root)} + preview = _preview(runtime_root) + requests: list[tuple[str, dict[str, Any]]] = [] + + def call(method: str, params: dict[str, Any]) -> dict[str, Any]: + requests.append((method, params)) + return _typed_result(params, runtime_root) + + monkeypatch.setattr(void_commit, "effect_runtime_result", call) + + build_quota_slot_void_preview_for_decision( + status, + goal_id=GOAL_ID, + voided_run_generated_at=TARGET_AT, + before=_decision(2), + ) + assert len(requests) == 1 + + build_quota_slot_void_event( + preview, + source="heartbeat", + reason_summary=REASON, + generated_at=VOID_AT, + ) + assert len(requests) == 2 + + record_quota_slot_void_from_preview( + preview, + status, + goal_id=GOAL_ID, + render_markdown=render_quota_slot_preview_markdown, + execute=True, + source="heartbeat", + reason_summary=REASON, + ) + assert len(requests) == 3 + + commit_quota_slot_void( + status, + goal_id=GOAL_ID, + voided_run_generated_at=TARGET_AT, + before=_decision(2), + execute=False, + source="heartbeat", + reason_summary=REASON, + generated_at=VOID_AT, + effect_id="quota-void:explicit-invocation", + ) + assert len(requests) == 4 + + assert all(method == "quota.void.commit" for method, _params in requests) + assert all( + params["schema_version"] == QUOTA_VOID_COMMIT_REQUEST_SCHEMA + for _method, params in requests + ) + assert requests[0][1]["operation"] == "preview" + assert requests[0][1]["execute"] is False + assert str(requests[0][1]["effect_id"]).startswith("quota-void:") + assert requests[1][1]["operation"] == "project_record" + assert requests[2][1]["operation"] == "commit" + assert requests[2][1]["execute"] is True + assert str(requests[2][1]["effect_id"]).startswith("quota-void:") + assert requests[3][1]["effect_id"] == "quota-void:explicit-invocation" + + +@pytest.mark.parametrize( + ("mutation", "message"), + [ + ( + lambda result: {**result, "schema_version": "wrong_schema"}, + "result shape mismatch", + ), + (lambda result: {**result, "status": "unknown"}, "status"), + ( + lambda result: { + **result, + "payload": {**result["payload"], "goal_id": "other-goal"}, + }, + "goal_id", + ), + ( + lambda result: {**result, "effect_id": "quota-void:other"}, + "result effect_id mismatch", + ), + ( + lambda result: { + **result, + "payload": { + **result["payload"], + "effect_id": "quota-void:other", + }, + }, + "payload effect_id mismatch", + ), + ], +) +def test_python_facade_rejects_mismatched_typed_results( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + mutation: Any, + message: str, +) -> None: + runtime_root = tmp_path / "runtime" + _write_spend_target(runtime_root) + + def call(_method: str, params: dict[str, Any]) -> dict[str, Any]: + return mutation(_typed_result(params, runtime_root)) + + monkeypatch.setattr(void_commit, "effect_runtime_result", call) + + with pytest.raises(RuntimeError, match=message): + commit_quota_slot_void( + {"runtime_root": str(runtime_root)}, + goal_id=GOAL_ID, + voided_run_generated_at=TARGET_AT, + before=_decision(2), + source="heartbeat", + ) + + +def test_real_runtime_preserves_typed_effect_conflicts( + tmp_path: Path, +) -> None: + runtime_root = tmp_path / "runtime" + _write_spend_target(runtime_root) + arguments = { + "status_payload": {"runtime_root": str(runtime_root)}, + "goal_id": GOAL_ID, + "voided_run_generated_at": TARGET_AT, + "before": _decision(2), + "execute": True, + "source": "heartbeat", + "effect_id": "quota-void:conflict-probe", + "generated_at": VOID_AT, + } + commit_quota_slot_void(**arguments, reason_summary=REASON) + + with pytest.raises(ValueError, match="already bound to a different request"): + commit_quota_slot_void( + **arguments, + reason_summary="different correction semantics", + ) + + +def test_unsafe_goal_is_rejected_before_runtime_or_filesystem_effect( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + calls = 0 + + def unexpected_call(_method: str, _params: dict[str, Any]) -> object: + nonlocal calls + calls += 1 + raise AssertionError("unsafe goal reached the TypeScript runtime") + + monkeypatch.setattr(void_commit, "effect_runtime_result", unexpected_call) + + with pytest.raises(ValueError, match="single path segment"): + commit_quota_slot_void( + {"runtime_root": str(tmp_path / "runtime")}, + goal_id="../outside", + voided_run_generated_at=TARGET_AT, + before=_decision(2), + execute=True, + source="heartbeat", + ) + + assert calls == 0 + assert not (tmp_path / "runtime" / "goals").exists() + + +@pytest.mark.parametrize( + ("effect_id", "expected"), + [ + (" quota-void:normalized ", "quota-void:normalized"), + ("😀" * 128, "😀" * 128), + ], +) +def test_explicit_effect_id_is_normalized_before_runtime( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + effect_id: str, + expected: str, +) -> None: + runtime_root = tmp_path / "runtime" + _write_spend_target(runtime_root) + captured: dict[str, Any] = {} + + def call(_method: str, params: dict[str, Any]) -> dict[str, Any]: + captured.update(params) + return _typed_result(params, runtime_root) + + monkeypatch.setattr(void_commit, "effect_runtime_result", call) + + commit_quota_slot_void( + {"runtime_root": str(runtime_root)}, + goal_id=GOAL_ID, + voided_run_generated_at=TARGET_AT, + before=_decision(2), + effect_id=effect_id, + ) + + assert captured["effect_id"] == expected + + +@pytest.mark.parametrize("effect_id", [" ", "x" * 257, "😀" * 129]) +def test_invalid_effect_id_is_rejected_before_runtime_or_filesystem_effect( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + effect_id: str, +) -> None: + calls = 0 + + def unexpected_call(_method: str, _params: dict[str, Any]) -> object: + nonlocal calls + calls += 1 + raise AssertionError("invalid effect identity reached the TypeScript runtime") + + monkeypatch.setattr(void_commit, "effect_runtime_result", unexpected_call) + + with pytest.raises(ValueError, match="effect_id"): + commit_quota_slot_void( + {"runtime_root": str(tmp_path / "runtime")}, + goal_id=GOAL_ID, + voided_run_generated_at=TARGET_AT, + before=_decision(2), + execute=True, + source="heartbeat", + effect_id=effect_id, + ) + + assert calls == 0 + assert not (tmp_path / "runtime" / "goals").exists() + + +def test_blank_normalized_runtime_root_is_rejected_before_filesystem_effect( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + calls = 0 + + def unexpected_call(_method: str, _params: dict[str, Any]) -> object: + nonlocal calls + calls += 1 + raise AssertionError("blank runtime root reached the TypeScript runtime") + + monkeypatch.chdir(tmp_path) + monkeypatch.setattr(void_commit, "effect_runtime_result", unexpected_call) + + with pytest.raises(ValueError, match="runtime_root"): + commit_quota_slot_void( + {"runtime_root": " \ufeff "}, + goal_id=GOAL_ID, + voided_run_generated_at=TARGET_AT, + before=_decision(2), + execute=True, + ) + + assert calls == 0 + assert not (tmp_path / "goals").exists() + + +def test_execute_holds_legacy_index_lock_and_sends_expected_digest( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + runtime_root = tmp_path / "runtime" + index_path, _target_path = _write_spend_target(runtime_root) + expected_digest = "sha256:" + hashlib.sha256(index_path.read_bytes()).hexdigest() + timeline: list[str] = [] + captured: dict[str, Any] = {} + + @contextmanager + def lock(path: Path, **kwargs: Any) -> Iterator[Path]: + assert path == index_path + assert kwargs["operation"] == "quota_void_commit" + timeline.append("lock-enter") + try: + yield path.with_name(f"{path.name}.lock") + finally: + timeline.append("lock-exit") + + def call(method: str, params: dict[str, Any]) -> dict[str, Any]: + assert method == "quota.void.commit" + timeline.append("runtime") + captured.update(params) + return _typed_result(params, runtime_root) + + monkeypatch.setattr(void_commit, "exclusive_file_lock", lock) + monkeypatch.setattr(void_commit, "effect_runtime_result", call) + + payload = record_quota_slot_void_from_preview( + _preview(runtime_root), + {"runtime_root": str(runtime_root)}, + goal_id=GOAL_ID, + render_markdown=render_quota_slot_preview_markdown, + execute=True, + source="heartbeat", + reason_summary=REASON, + ) + + assert payload["appended"] is True + assert timeline == ["lock-enter", "runtime", "lock-exit"] + assert captured["execute"] is True + assert captured["expected_index_digest"] == expected_digest + assert str(captured["effect_id"]).startswith("quota-void:") diff --git a/tests/control_plane_ts/quota_spend_commit.test.ts b/tests/control_plane_ts/quota_spend_commit.test.ts index 39766742b3..346c61af2c 100644 --- a/tests/control_plane_ts/quota_spend_commit.test.ts +++ b/tests/control_plane_ts/quota_spend_commit.test.ts @@ -534,6 +534,70 @@ test("prepared transaction repairs its own truncated final index row", async (t) ); }); +test("shared receipt replay rejects a replaced spend index prefix", async (t) => { + const runtimeRoot = await tempRuntime(t); + const first = await evaluateQuotaSpendCommit(request(runtimeRoot)); + const indexPath = String(first.payload.index_path); + const params = request(runtimeRoot, { + effect_id: "quota-spend-effect-prefix-fence", + generated_at: "2026-08-25T12:02:00+08:00", + expected_index_digest: await quotaSpendIndexDigest(indexPath), + }); + await evaluateQuotaSpendCommit(params); + const replacement = `${JSON.stringify({ + generated_at: "2026-08-25T12:03:00+08:00", + goal_id: "quota-spend-commit", + classification: "unrelated", + })}\n`; + await writeFile(indexPath, replacement, "utf8"); + + await assert.rejects( + () => evaluateQuotaSpendCommit(params), + /quota spend run index no longer retains its transaction prefix/, + ); + assert.equal(await readFile(indexPath, "utf8"), replacement); +}); + +test("shared receipt replay tolerates a legal prefix rewrite that preserves its row", async (t) => { + const runtimeRoot = await tempRuntime(t); + const first = await evaluateQuotaSpendCommit(request(runtimeRoot)); + const indexPath = String(first.payload.index_path); + const params = request(runtimeRoot, { + effect_id: "quota-spend-effect-preserved-after-repair", + generated_at: "2026-08-25T12:02:00+08:00", + expected_index_digest: await quotaSpendIndexDigest(indexPath), + }); + await evaluateQuotaSpendCommit(params); + const rows = (await readFile(indexPath, "utf8")).trim().split("\n"); + assert.equal(rows.length, 2); + + await writeFile(indexPath, `${rows[1]}\n`, "utf8"); + + const replayed = await evaluateQuotaSpendCommit(params); + assert.equal(replayed.status, "replayed"); + assert.equal(await readFile(indexPath, "utf8"), `${rows[1]}\n`); +}); + +test("shared receipt replay rejects a mutated matching spend index row", async (t) => { + const runtimeRoot = await tempRuntime(t); + const params = request(runtimeRoot); + const written = await evaluateQuotaSpendCommit(params); + const indexPath = String(written.payload.index_path); + const row = JSON.parse(await readFile(indexPath, "utf8")) as Record; + const mutated = `${JSON.stringify({ + ...row, + goal_id: "other-goal", + json_path: "bogus.json", + })}\n`; + await writeFile(indexPath, mutated, "utf8"); + + await assert.rejects( + () => evaluateQuotaSpendCommit(params), + /quota spend index record conflicts with its transaction receipt/, + ); + assert.equal(await readFile(indexPath, "utf8"), mutated); +}); + test("effect identity and index CAS reject drift and racing writers", async (t) => { const runtimeRoot = await tempRuntime(t); const params = request(runtimeRoot); diff --git a/tests/control_plane_ts/quota_void_commit.test.ts b/tests/control_plane_ts/quota_void_commit.test.ts new file mode 100644 index 0000000000..c81f6249ca --- /dev/null +++ b/tests/control_plane_ts/quota_void_commit.test.ts @@ -0,0 +1,596 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { + mkdtemp, + mkdir, + readFile, + readdir, + rm, + symlink, + unlink, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; + +import { + evaluateQuotaVoidCommit, + quotaVoidIndexDigest, + QUOTA_VOID_COMMIT_REQUEST_SCHEMA, +} from "../../loopx/control_plane/quota/void_commit.ts"; + +const goalId = "quota-void-transaction"; +const targetGeneratedAt = "2026-08-25T11:59:00+08:00"; +const voidGeneratedAt = "2026-08-25T12:00:00+08:00"; + +interface TargetFixture { + runtimeRoot: string; + runsDir: string; + indexPath: string; + indexContent: string; + targetJsonPath: string; +} + +function beforeDecision(spentSlots: unknown = 5): Record { + return { + should_run: false, + normal_delivery_allowed: false, + recovery_delivery_allowed: false, + effective_action: "wait_for_quota", + self_repair_allowed: false, + capability_repair_allowed: false, + workspace_repair_allowed: false, + state: "throttled", + safe_bypass_allowed: false, + safe_bypass_kind: null, + blocked_action_scope: "normal_delivery", + agent_identity: { agent_id: "codex-main-control" }, + quota: { + compute: 1, + window_hours: 24, + slot_minutes: 1, + spent_slots: spentSlots, + allowed_slots: 5, + }, + }; +} + +function quotaSpendEvent(slots: unknown): Record { + return { + event_type: "quota_slot_spent", + source: "heartbeat", + slots, + reason_summary: "accounted completed delivery", + before: { spent_slots: 3 }, + after: { spent_slots: 5 }, + }; +} + +async function tempRuntime(t: test.TestContext): Promise { + const runtimeRoot = await mkdtemp(join(tmpdir(), "loopx-quota-void-commit-")); + t.after(() => rm(runtimeRoot, { recursive: true, force: true })); + return runtimeRoot; +} + +async function targetFixture( + t: test.TestContext, + options: { + inline?: boolean; + slots?: unknown; + generatedAt?: string; + jsonPath?: string; + } = {}, +): Promise { + const runtimeRoot = await tempRuntime(t); + const runsDir = join(runtimeRoot, "goals", goalId, "runs"); + await mkdir(runsDir, { recursive: true }); + const indexPath = join(runsDir, "index.jsonl"); + const generatedAt = options.generatedAt ?? targetGeneratedAt; + const targetJsonPath = options.jsonPath ?? join( + runsDir, + "20260825-115900-quota-slot-spent.json", + ); + const event = quotaSpendEvent(options.slots ?? 2); + const indexRecord: Record = { + generated_at: generatedAt, + goal_id: goalId, + classification: "quota_slot_spent", + json_path: targetJsonPath, + }; + if (options.inline !== false) { + indexRecord.quota_event = event; + } else { + await writeFile( + targetJsonPath, + `${JSON.stringify({ + generated_at: generatedAt, + goal_id: goalId, + classification: "quota_slot_spent", + quota_event: event, + }, null, 2)}\n`, + "utf8", + ); + } + const indexContent = `${JSON.stringify(indexRecord)}\n`; + await writeFile(indexPath, indexContent, "utf8"); + return { runtimeRoot, runsDir, indexPath, indexContent, targetJsonPath }; +} + +async function rawIndexDigest(indexPath: string): Promise { + const value = await readFile(indexPath); + return `sha256:${createHash("sha256").update(value).digest("hex")}`; +} + +async function request( + fixture: TargetFixture, + extra: Record = {}, +): Promise> { + return { + schema_version: QUOTA_VOID_COMMIT_REQUEST_SCHEMA, + effect_id: "quota-void-effect-1", + runtime_root: fixture.runtimeRoot, + goal_id: goalId, + voided_run_generated_at: targetGeneratedAt, + source: "heartbeat", + reason_summary: "duplicate heartbeat spend", + generated_at: voidGeneratedAt, + execute: true, + expected_index_digest: await rawIndexDigest(fixture.indexPath), + before: beforeDecision(), + ...extra, + }; +} + +async function transactionReceipt( + runsDir: string, + effectId: string, +): Promise<{ path: string; value: Record }> { + const candidates: string[] = []; + async function collect(directory: string): Promise { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = join(directory, entry.name); + if (entry.isDirectory()) { + await collect(path); + } else if (entry.isFile() && entry.name.endsWith(".json")) { + candidates.push(path); + } + } + } + await collect(join(runsDir, ".transactions")); + for (const path of candidates) { + const value = JSON.parse(await readFile(path, "utf8")) as Record; + if (value.effect_id === effectId) return { path, value }; + } + throw new Error(`transaction receipt was not found for ${effectId}`); +} + +test("preview finds an inline spend event and preserves the legacy payload without writes", async (t) => { + const fixture = await targetFixture(t); + const filesBefore = (await readdir(fixture.runsDir)).sort(); + const params = await request(fixture, { execute: false }); + + assert.equal( + await quotaVoidIndexDigest(fixture.indexPath), + await rawIndexDigest(fixture.indexPath), + ); + const result = await evaluateQuotaVoidCommit(params); + + assert.equal(result.status, "preview"); + assert.equal(result.payload.ok, true); + assert.equal(result.payload.mode, "void-slot"); + assert.equal(result.payload.dry_run, true); + assert.equal(result.payload.appended, false); + assert.equal(result.payload.registry_mutated, false); + assert.equal(result.payload.slots, 2); + assert.equal(result.payload.voided_run_generated_at, targetGeneratedAt); + assert.equal( + ((result.payload.after as Record).quota as Record) + .spent_slots, + 3, + ); + assert.equal(result.record?.classification, "quota_slot_voided"); + assert.deepEqual((await readdir(fixture.runsDir)).sort(), filesBefore); + assert.equal(await readFile(fixture.indexPath, "utf8"), fixture.indexContent); +}); + +test("target lookup falls back to the bounded legacy JSON artifact", async (t) => { + const fixture = await targetFixture(t, { inline: false, slots: 4 }); + const params = await request(fixture, { execute: false }); + + const result = await evaluateQuotaVoidCommit(params); + + assert.equal(result.status, "preview"); + assert.equal(result.payload.voided_run_json_path, fixture.targetJsonPath); + const event = result.record?.quota_event as Record; + assert.equal(event.event_type, "quota_slot_voided"); + assert.equal(event.slots, 4); + assert.equal((event.after as Record).spent_slots, 1); +}); + +test("legacy index rows without goal identity use the requested goal", async (t) => { + const fixture = await targetFixture(t, { inline: false }); + const indexRecord = JSON.parse(fixture.indexContent) as Record; + delete indexRecord.goal_id; + await writeFile(fixture.indexPath, `${JSON.stringify(indexRecord)}\n`, "utf8"); + + const result = await evaluateQuotaVoidCommit(await request(fixture, { + execute: false, + expected_index_digest: await rawIndexDigest(fixture.indexPath), + })); + + assert.equal(result.status, "preview"); + assert.equal(result.payload.goal_id, goalId); +}); + +test("a missing spend target returns the legacy not-found payload and writes nothing", async (t) => { + const fixture = await targetFixture(t, { + generatedAt: "2026-08-25T11:58:00+08:00", + }); + const filesBefore = (await readdir(fixture.runsDir)).sort(); + const params = await request(fixture); + + const result = await evaluateQuotaVoidCommit(params); + + assert.equal(result.status, "not_found"); + assert.equal(result.payload.ok, false); + assert.equal(result.payload.appended, false); + assert.match(String(result.payload.reason), /target quota_slot_spent run was not found/); + assert.equal(result.record, null); + assert.deepEqual((await readdir(fixture.runsDir)).sort(), filesBefore); + assert.equal(await readFile(fixture.indexPath, "utf8"), fixture.indexContent); +}); + +test("invalid source and filesystem paths fail before mutation", async (t) => { + const fixture = await targetFixture(t); + const originalFiles = (await readdir(fixture.runsDir)).sort(); + const originalIndex = await readFile(fixture.indexPath, "utf8"); + + await assert.rejects( + async () => evaluateQuotaVoidCommit(await request(fixture, { source: "unknown" })), + /quota slot (spend|void) source must be one of/, + ); + await assert.rejects( + async () => evaluateQuotaVoidCommit(await request(fixture, { + operation: "preview", + execute: true, + })), + /preview operation cannot execute durable effects/, + ); + await assert.rejects( + async () => evaluateQuotaVoidCommit(await request(fixture, { + before: { ...beforeDecision(), should_run: "true" }, + })), + /before\.should_run must be a boolean/, + ); + await assert.rejects( + async () => evaluateQuotaVoidCommit(await request(fixture, { goal_id: "../escape" })), + /goal_id must be a single path segment|goal id must be a single path segment/, + ); + await assert.rejects( + async () => evaluateQuotaVoidCommit(await request(fixture, { + runtime_root: "relative/runtime", + })), + /runtime_root must be absolute/, + ); + + const outsidePath = join(fixture.runtimeRoot, "outside-spend.json"); + await writeFile( + outsidePath, + `${JSON.stringify({ + classification: "quota_slot_spent", + quota_event: quotaSpendEvent(1), + })}\n`, + "utf8", + ); + const escapedTarget = { + generated_at: targetGeneratedAt, + goal_id: goalId, + classification: "quota_slot_spent", + json_path: outsidePath, + }; + await writeFile(fixture.indexPath, `${JSON.stringify(escapedTarget)}\n`, "utf8"); + await assert.rejects( + async () => evaluateQuotaVoidCommit(await request(fixture, { + execute: false, + expected_index_digest: await rawIndexDigest(fixture.indexPath), + })), + /json_path|artifact path|runs directory/, + ); + + await writeFile(fixture.indexPath, originalIndex, "utf8"); + assert.deepEqual((await readdir(fixture.runsDir)).sort(), originalFiles); +}); + +test("commit atomically owns the JSON, Markdown, and index artifacts", async (t) => { + const fixture = await targetFixture(t); + const result = await evaluateQuotaVoidCommit(await request(fixture)); + + assert.equal(result.status, "written"); + assert.equal(result.payload.dry_run, false); + assert.equal(result.payload.appended, true); + assert.equal(result.payload.registry_mutated, false); + assert.equal(result.payload.classification, "quota_slot_voided"); + const jsonPath = String(result.payload.json_path); + const markdownPath = String(result.payload.markdown_path); + const persisted = JSON.parse(await readFile(jsonPath, "utf8")) as Record; + assert.equal(persisted.classification, "quota_slot_voided"); + assert.equal( + (persisted.quota_event as Record).voided_run_generated_at, + targetGeneratedAt, + ); + assert.match(await readFile(markdownPath, "utf8"), /LoopX Quota Slot Preview/); + assert.match(await readFile(markdownPath, "utf8"), /quota_slot_voided/); + const rows = (await readFile(fixture.indexPath, "utf8")).trim().split("\n"); + assert.deepEqual( + rows.map((line) => (JSON.parse(line) as Record).classification), + ["quota_slot_spent", "quota_slot_voided"], + ); +}); + +test("the same effect replays without appending a second void", async (t) => { + const fixture = await targetFixture(t); + const params = await request(fixture); + const written = await evaluateQuotaVoidCommit(params); + const indexAfterWrite = await readFile(fixture.indexPath, "utf8"); + + const replayed = await evaluateQuotaVoidCommit(params); + + assert.equal(written.status, "written"); + assert.equal(replayed.status, "replayed"); + assert.equal(replayed.replayed, true); + assert.equal(replayed.payload.appended, false); + assert.equal(replayed.payload.json_path, written.payload.json_path); + assert.equal(await readFile(fixture.indexPath, "utf8"), indexAfterWrite); +}); + +test("the same effect identity rejects semantic request drift", async (t) => { + const fixture = await targetFixture(t); + const params = await request(fixture); + await evaluateQuotaVoidCommit(params); + const indexAfterWrite = await readFile(fixture.indexPath, "utf8"); + + const conflict = await evaluateQuotaVoidCommit({ + ...params, + reason_summary: "a different accounting correction", + }); + + assert.equal(conflict.status, "conflict"); + assert.equal(conflict.reason_code, "effect_id_conflict"); + assert.equal(conflict.payload.goal_id, goalId); + assert.equal(conflict.payload.effect_id, params.effect_id); + assert.equal(await readFile(fixture.indexPath, "utf8"), indexAfterWrite); +}); + +test("distinct effects may append independent voids for the same spend target", async (t) => { + const fixture = await targetFixture(t); + const first = await evaluateQuotaVoidCommit(await request(fixture)); + const second = await evaluateQuotaVoidCommit(await request(fixture, { + effect_id: "quota-void-effect-2", + expected_index_digest: await quotaVoidIndexDigest(fixture.indexPath), + })); + + assert.equal(first.status, "written"); + assert.equal(second.status, "written"); + assert.notEqual(first.payload.json_path, second.payload.json_path); + assert.notEqual(first.payload.markdown_path, second.payload.markdown_path); + const rows = (await readFile(fixture.indexPath, "utf8")).trim().split("\n"); + assert.equal(rows.length, 3); + assert.deepEqual( + rows.map((line) => (JSON.parse(line) as Record).classification), + ["quota_slot_spent", "quota_slot_voided", "quota_slot_voided"], + ); +}); + +test("index CAS serializes distinct racing void effects", async (t) => { + const fixture = await targetFixture(t); + const [left, right] = await Promise.all([ + evaluateQuotaVoidCommit(await request(fixture, { effect_id: "quota-void-race-a" })), + evaluateQuotaVoidCommit(await request(fixture, { effect_id: "quota-void-race-b" })), + ]); + + assert.deepEqual( + [left.status, right.status].sort(), + ["conflict", "written"], + ); + const conflict = [left, right].find((result) => result.status === "conflict"); + assert.equal(conflict?.reason_code, "index_digest_conflict"); + assert.equal((await readFile(fixture.indexPath, "utf8")).trim().split("\n").length, 2); +}); + +test("a prepared transaction repairs missing artifacts and its absent index row", async (t) => { + const fixture = await targetFixture(t); + const params = await request(fixture); + const written = await evaluateQuotaVoidCommit(params); + const receipt = await transactionReceipt(fixture.runsDir, String(params.effect_id)); + receipt.value.status = "prepared"; + await writeFile(receipt.path, `${JSON.stringify(receipt.value, null, 2)}\n`, "utf8"); + await Promise.all([ + unlink(String(written.payload.json_path)), + unlink(String(written.payload.markdown_path)), + writeFile(fixture.indexPath, fixture.indexContent, "utf8"), + ]); + + const repaired = await evaluateQuotaVoidCommit(params); + + assert.equal(repaired.status, "repaired"); + assert.equal(repaired.repaired, true); + assert.equal(repaired.payload.transaction_repaired, true); + assert.equal( + (JSON.parse(await readFile(String(written.payload.json_path), "utf8")) as Record) + .classification, + "quota_slot_voided", + ); + assert.match(await readFile(String(written.payload.markdown_path), "utf8"), /quota_slot_voided/); + assert.equal((await readFile(fixture.indexPath, "utf8")).trim().split("\n").length, 2); + assert.equal((await evaluateQuotaVoidCommit(params)).status, "replayed"); +}); + +test("a prepared transaction repairs only its own truncated final index row", async (t) => { + const fixture = await targetFixture(t); + await evaluateQuotaVoidCommit(await request(fixture)); + const indexBeforeSecond = await readFile(fixture.indexPath, "utf8"); + const secondParams = await request(fixture, { + effect_id: "quota-void-effect-2", + generated_at: "2026-08-25T12:02:00+08:00", + expected_index_digest: await quotaVoidIndexDigest(fixture.indexPath), + }); + await evaluateQuotaVoidCommit(secondParams); + const receipt = await transactionReceipt(fixture.runsDir, String(secondParams.effect_id)); + receipt.value.status = "prepared"; + await writeFile(receipt.path, `${JSON.stringify(receipt.value, null, 2)}\n`, "utf8"); + const expectedLine = JSON.stringify(receipt.value.index_record); + assert.notEqual(expectedLine, undefined); + await writeFile( + fixture.indexPath, + `${indexBeforeSecond}${expectedLine.slice(0, Math.floor(expectedLine.length / 2))}`, + "utf8", + ); + + const repaired = await evaluateQuotaVoidCommit(secondParams); + + assert.equal(repaired.status, "repaired"); + const repairedIndex = await readFile(fixture.indexPath, "utf8"); + assert.equal(repairedIndex.startsWith(indexBeforeSecond), true); + const rows = repairedIndex.trim().split("\n"); + assert.equal(rows.length, 3); + assert.equal( + (JSON.parse(rows[2]) as Record).classification, + "quota_slot_voided", + ); + assert.equal((await evaluateQuotaVoidCommit(secondParams)).status, "replayed"); +}); + +test("receipt replay fails closed when its committed index prefix drifts", async (t) => { + const fixture = await targetFixture(t); + const params = await request(fixture); + await evaluateQuotaVoidCommit(params); + const replacement = `${JSON.stringify({ + generated_at: "2026-08-25T12:01:00+08:00", + goal_id: goalId, + classification: "unrelated", + })}\n`; + await writeFile(fixture.indexPath, replacement, "utf8"); + + await assert.rejects( + () => evaluateQuotaVoidCommit(params), + /quota void run index no longer retains its transaction prefix/, + ); + assert.equal(await readFile(fixture.indexPath, "utf8"), replacement); +}); + +test("receipt replay tolerates duplicate repair when its exact void row remains", async (t) => { + const fixture = await targetFixture(t); + await writeFile( + fixture.indexPath, + `${fixture.indexContent}${fixture.indexContent}`, + "utf8", + ); + const params = await request(fixture, { + expected_index_digest: await quotaVoidIndexDigest(fixture.indexPath), + }); + await evaluateQuotaVoidCommit(params); + const rows = (await readFile(fixture.indexPath, "utf8")).trim().split("\n"); + assert.equal(rows.length, 3); + + const repairedIndex = `${rows[1]}\n${rows[2]}\n`; + await writeFile(fixture.indexPath, repairedIndex, "utf8"); + + const replayed = await evaluateQuotaVoidCommit(params); + assert.equal(replayed.status, "replayed"); + assert.equal(await readFile(fixture.indexPath, "utf8"), repairedIndex); +}); + +test("receipt replay rejects a mutated matching void index row", async (t) => { + const fixture = await targetFixture(t); + const params = await request(fixture); + await evaluateQuotaVoidCommit(params); + const rows = (await readFile(fixture.indexPath, "utf8")) + .trim() + .split("\n") + .map((line) => JSON.parse(line) as Record); + rows[1] = { ...rows[1], goal_id: "other-goal", json_path: "bogus.json" }; + const mutated = `${rows.map((row) => JSON.stringify(row)).join("\n")}\n`; + await writeFile(fixture.indexPath, mutated, "utf8"); + + await assert.rejects( + () => evaluateQuotaVoidCommit(params), + /quota void index record conflicts with its transaction receipt/, + ); + assert.equal(await readFile(fixture.indexPath, "utf8"), mutated); +}); + +test("a committed receipt refuses to overwrite a conflicting artifact", async (t) => { + const fixture = await targetFixture(t); + const params = await request(fixture); + const written = await evaluateQuotaVoidCommit(params); + await writeFile(String(written.payload.json_path), "{}\n", "utf8"); + + await assert.rejects( + () => evaluateQuotaVoidCommit(params), + /JSON artifact conflicts with its transaction receipt/, + ); +}); + +test("receipt repair rejects paths outside the run directory and symbolic links", async (t) => { + const fixture = await targetFixture(t); + const params = await request(fixture); + const written = await evaluateQuotaVoidCommit(params); + const receipt = await transactionReceipt(fixture.runsDir, String(params.effect_id)); + const outsidePath = join(fixture.runtimeRoot, "outside-quota-void.json"); + const escapedReceipt = { + ...receipt.value, + status: "prepared", + json_path: outsidePath, + index_record: { + ...(receipt.value.index_record as Record), + json_path: outsidePath, + }, + payload: { + ...(receipt.value.payload as Record), + json_path: outsidePath, + }, + }; + await writeFile(receipt.path, `${JSON.stringify(escapedReceipt, null, 2)}\n`, "utf8"); + + await assert.rejects( + () => evaluateQuotaVoidCommit(params), + /receipt JSON path is outside its run directory/, + ); + await assert.rejects(() => readFile(outsidePath), /ENOENT/); + + await writeFile(receipt.path, `${JSON.stringify({ + ...receipt.value, + status: "prepared", + }, null, 2)}\n`, "utf8"); + await unlink(String(written.payload.json_path)); + await symlink(outsidePath, String(written.payload.json_path)); + await assert.rejects( + () => evaluateQuotaVoidCommit(params), + /JSON artifact must not be a symbolic link/, + ); + await assert.rejects(() => readFile(outsidePath), /ENOENT/); +}); + +test("legacy slot coercion truncates numeric strings and clamps both slot floors", async (t) => { + for (const testCase of [ + { slots: "3.9", spentSlots: "2.9", expectedSlots: 3, expectedAfter: 0 }, + { slots: 0, spentSlots: 4, expectedSlots: 1, expectedAfter: 3 }, + { slots: -4, spentSlots: "invalid", expectedSlots: 1, expectedAfter: 0 }, + { slots: "0x10", spentSlots: 4, expectedSlots: 1, expectedAfter: 3 }, + ]) { + const fixture = await targetFixture(t, { slots: testCase.slots }); + const result = await evaluateQuotaVoidCommit(await request(fixture, { + execute: false, + before: beforeDecision(testCase.spentSlots), + })); + const event = result.record?.quota_event as Record; + + assert.equal(result.status, "preview"); + assert.equal(event.slots, testCase.expectedSlots); + assert.equal( + (event.after as Record).spent_slots, + testCase.expectedAfter, + ); + } +}); From 1d32a5dee2bab4b19d52dd2eb8349df20eadf838 Mon Sep 17 00:00:00 2001 From: hyk <4408344+hhyykk@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:47:41 +0800 Subject: [PATCH 3/3] docs(rfc): record quota void migration receipt Signed-off-by: hyk <4408344+hhyykk@users.noreply.github.com> --- .../typescript-control-plane-migration-v0.md | 40 ++++++++++++++++--- ...script-control-plane-migration-v0.zh-CN.md | 36 ++++++++++++++--- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index f287b2bcc2..7136d12638 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -159,6 +159,7 @@ choice is now implemented rather than hypothetical. | Scheduler durable state ([#3440](https://github.com/huangruiteng/loopx/pull/3440)) | State normalization, persistence, replay, and one coarse transition are TS-owned | The Python compatibility path still pays a cross-runtime transport tax | | Scheduler heartbeat/state transaction | TypeScript owns receipt freshness, ACK and host-failure validation, state construction, failure-cache transitions, replay/CAS fencing, atomic writes, and the public JSON/Markdown projection | Generated, receipt-bound host follow-up runs through the native TS CLI; Python remains only for unbound/manual compatibility calls and external host mutation | | Quota spend commit transaction | TypeScript owns final spend-transition validation, typed event construction, effect replay/CAS fencing, crash repair, and the JSON/Markdown/index write set | Python still projects `should-run` and settlement readback facts, and holds the legacy cross-writer index lock until the CLI/index writers move in-process | +| Quota void commit transaction | TypeScript owns spend-target resolution, before/after reduction, canonical correction construction, effect replay/index CAS, prepared-receipt repair, and the JSON/Markdown/index write set | Python retains `should-run` facts, clock/effect identity, the legacy cross-writer index lock, one transport call, and compatibility entry points | | Quota monitor-poll commit transaction | TypeScript owns monitor admission revalidation, target/event/result construction, effect replay/index CAS, provider intent, and repairable JSON/Markdown/index persistence | Python projects compact `should-run` facts, invokes the real Todo provider between at most two reductions, reloads legacy status, and holds the cross-writer index lock | | Runtime decoders ([#3443](https://github.com/huangruiteng/loopx/pull/3443)) | Stable primitive decoding has one small shared module; domain decoders remain local | No larger schema framework is justified | | Transaction payoff ([#3464](https://github.com/huangruiteng/loopx/pull/3464), [#3481](https://github.com/huangruiteng/loopx/pull/3481), and Todo completion) | Turn settlement, quota delivery routing, and Todo completion each cross one coarse TS boundary; the Todo transaction owns identity, replay fencing, validation planning/result reduction, continuation/recovery, and completion metadata | Python still executes explicitly external providers and materializes legacy Markdown/event results; other domains still need their own bounded cutovers | @@ -242,7 +243,7 @@ domains would now increase total complexity. Select by deletion leverage and runtime traffic, not by ease of translation. The shipped Turn settlement, quota delivery-routing, Todo-completion, -scheduler-heartbeat, quota-spend commit, and task-lease acquire cutovers +scheduler-heartbeat, quota-spend commit, quota-void commit, and task-lease acquire cutovers establish the pattern. Subsequent candidates must name a remaining transaction and its deletion leverage; remaining quota settlement readback is eligible only when it can @@ -294,6 +295,18 @@ shipped Stage 2B cutovers are in place: Python retains `should-run`/settlement fact projection plus one coarse transport call and the legacy kernel index lock; it no longer constructs or writes the spend event. +- Quota void commit: TypeScript finds the referenced spend under the mutation + lock, reduces the before/after accounting decision, constructs the canonical + correction, and commits its JSON, Markdown, index row, and prepared receipt + through the closed spend/void accounting-artifact kernel. Same-effect retry + replays or repairs one transaction; a fresh CLI invocation remains a fresh + effect and therefore preserves the existing ability to append another + correction for the same spend target. Malformed index rows now fail closed + instead of being skipped. Void artifact names include an effect digest and + JSONL rows use compact JSON; public payload semantics remain stable. The + shared kernel also validates persisted receipt/path identity for spend + recovery. Python retains `should-run` facts, UUID/clock ownership, one coarse + transport call, and the legacy cross-writer index lock. - Local task-lease lifecycle: native TypeScript transactions now own acquire, renew, transfer, release, terminal verification, holder verification, and fence close. They own boundary decode, handoff and owner/Todo eligibility, @@ -328,11 +341,13 @@ shipped Stage 2B cutovers are in place: durability checks. Invalid identities stop before the provider, while a crash/retry after the provider re-enters its same-key idempotent path. -The quota-spend cutover removes the Python spend-event builder and three-file -writer. Its bounded facade exits when the quota CLI and remaining run-index -writers execute the transaction in-process; until then it supplies compact -projection facts and shares the legacy Python index lock with unmigrated -writers. The Todo cutover removes the Python state-evaluation dataclass, local identity +The quota-accounting cutovers remove the Python spend and void event builders +and their three-file writers. Their bounded facades exit when quota decision +and the top-level CLI execute in-process TypeScript, all run-index writers use +the native lock, and the legacy Python void API compatibility window closes. +Until then Python supplies compact projection facts, clock/effect identity, +result validation, and the shared legacy index lock. The Todo cutover removes +the Python state-evaluation dataclass, local identity projection, replay helper, and public runtime handlers for those implementation leaves. The remaining Python Todo facade owns transport, external command execution, source compare-and-swap, legacy response projection, and the actual @@ -359,6 +374,19 @@ retiring a lock. This is not an exactly-once guarantee for a timed-out handler that is still executing concurrently inside the same Node process; callers must not start a second independent operation while that handler may still be live. +#### Quota void commit migration economics + +| Field | Receipt | +| --- | --- | +| Canonical owner | Before: Python `slot_accounting.py` owned spend-target lookup, correction reduction, event/result construction, artifact allocation, and JSON/Markdown/index persistence. After: versioned TypeScript `quota.void.commit` owns those semantics plus effect fencing, index CAS, receipts, replay, and repair through the closed spend/void accounting kernel. | +| Legacy semantic code deleted | 212 Python product LOC covering the prior void lookup, transition, event/projection, path-allocation, and JSON/Markdown/index writer path. | +| Bridge code added | 263 Python diff LOC: the 243-line bounded `void_commit.py` transport/compatibility facade plus 20 import, re-export, normalization, and route-wiring lines in `loopx/quota.py` and the legacy `slot_accounting.py` surface. | +| Cross-runtime calls | The public execute and dry-run paths move from zero crossings to one coarse request/response. Exact-effect replay or repair also uses one request/response. Distinct CLI invocations remain distinct effects; the legacy two-step preview-plus-record compatibility surface uses one call per entry point. | +| Product-code net change | Product code is +2,210/−898 LOC, net +1,312. Tests/examples are +1,416/−3, net +1,413; build configuration is +3 and docs are excluded. The production shared kernel is already used by spend and void, replacing 671 lines in `spend_commit.ts` rather than creating a speculative framework. | +| Migration scaffolding | No migration-only worker, parity corpus, or temporary schema framework is added. Native boundary/invariant/replay/CAS/repair tests remain as shipped and persisted contracts; Python bridge tests exit with the compatibility facade. | +| Facade exit | Delete the Python void facade when quota decision and the top-level CLI run in-process TypeScript, all run-index writers use the native lock, and the legacy `build_*void*`/`record_*void*` Python API compatibility window closes. | +| Correctness and performance | Typed-decoder negatives, legacy target compatibility, effect isolation, index CAS, malformed receipts and paths, exact index-row identity, supported duplicate-index repair, concurrent mutation, truncated-tail repair, public CLI behavior, and clean wheel/sdist semantic probes pass. Across 16 cold starts, p50/p95 is 230.88/260.92 ms; 128 warm typed pings are 1.07/1.29 ms and warm void previews are 1.93/2.34 ms. Across 64 durable facade transactions, commit is 30.64/37.49 ms and exact-effect replay is 8.05/9.86 ms. Daemon RSS is 108.38 MiB idle and 109.80 MiB after 256 requests. In 64 interleaved full-CLI pairs, baseline/candidate p50/p95 is 736.51/828.68 versus 779.52/856.49 ms: p95 +27.81 ms (+3.36%). The absolute delta is the measured cost of one new managed-runtime fingerprint/request plus prepared-receipt durability; the percentage stays below the 5% material-regression gate, and Stage 3 removes that crossing. | + #### Task-lease acquire migration economics | Field | Receipt | diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md index 7d3f4ef11d..cb6b586f10 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md @@ -138,6 +138,7 @@ replay、receipt 与 settlement。这个架构选择已经落地,不再是假 | Scheduler durable state([#3440](https://github.com/huangruiteng/loopx/pull/3440)) | State normalization、persistence、replay 与一笔粗粒度 transition 由 TS 拥有 | Python compatibility path 仍承担跨 runtime transport 税 | | Scheduler heartbeat/state transaction | TypeScript 拥有 receipt freshness、ACK 与 host-failure validation、state construction、failure-cache transition、replay/CAS fencing、atomic write,以及 public JSON/Markdown projection | 生成的 receipt-bound host follow-up 直接进入 native TS CLI;Python 只处理 unbound/manual compatibility call 与 external host mutation | | Quota spend commit transaction | TypeScript 拥有最终 spend transition 校验、typed event 构造、effect replay/CAS fencing、crash repair,以及 JSON/Markdown/index write set | Python 仍投影 `should-run` 与 settlement readback facts,并在 CLI/index writer 进程内迁移前持有 legacy cross-writer index lock | +| Quota void commit transaction | TypeScript 拥有 spend-target resolution、before/after reduction、canonical correction 构造、effect replay/index CAS、prepared-receipt repair,以及 JSON/Markdown/index write set | Python 保留 `should-run` facts、clock/effect identity、legacy cross-writer index lock、一次 transport 与 compatibility entrypoint | | Quota monitor-poll commit transaction | TypeScript 拥有 monitor admission 复核、target/event/result 构造、effect replay/index CAS、provider intent,以及可修复的 JSON/Markdown/index persistence | Python 投影 compact `should-run` facts,在最多两次 reduction 之间调用真实 Todo provider,刷新 legacy status,并持有 cross-writer index lock | | Runtime decoder([#3443](https://github.com/huangruiteng/loopx/pull/3443)) | 稳定 primitive decoding 进入一个很小的共享模块;domain decoder 仍留在本地 | 没有理由建设更大的 schema framework | | Transaction 兑现([#3464](https://github.com/huangruiteng/loopx/pull/3464)、[#3481](https://github.com/huangruiteng/loopx/pull/3481) 与 Todo completion) | Turn settlement、quota delivery routing 与 Todo completion 均只跨一个粗粒度 TS boundary;Todo transaction 拥有 identity、replay fence、validation planning/result reduction、continuation/recovery 与 completion metadata | Python 仍执行显式 external provider,并物化 legacy Markdown/event result;其他 domain 仍需各自的 bounded cutover | @@ -211,7 +212,7 @@ leaf pattern 会增加总复杂度。 按删除杠杆与 runtime traffic 选切口,而不是按翻译难度选。已经交付的 Turn settlement、quota delivery routing、Todo completion、scheduler heartbeat、quota -spend commit 与 task-lease acquire cutover 建立了这一模式。后续候选必须明确剩余 +spend commit、quota void commit 与 task-lease acquire cutover 建立了这一模式。后续候选必须明确剩余 transaction 及其删除杠杆;剩余 quota settlement readback 只有在能退出或显著收窄 facade,而不是再增加 leaf handler 时才适合迁移。 @@ -253,6 +254,16 @@ window 仍需 differential proof 时才保留 characterization corpus;引入 截断 JSONL 尾行,其他损坏仍然 fail closed。 Python 只保留 `should-run`/settlement fact projection、一次 coarse transport call 与 legacy kernel index lock;它不再构造或写入 spend event。 +- Quota void commit:TypeScript 在 mutation lock 内定位被引用的 spend,归约 + before/after accounting decision,构造 canonical correction,并通过闭合的 + spend/void accounting-artifact kernel 提交 JSON、Markdown、index row 与 prepared + receipt。同一 effect 的 retry 会 replay 或修复同一 transaction;新的 CLI invocation + 仍是新的 effect,因此保留对同一 spend target 再追加 correction 的既有行为。 + Malformed index row 现在由静默跳过改为 fail closed。Void artifact 文件名加入 + effect digest,JSONL row 改用 compact JSON;public payload 语义保持稳定。共享 kernel + 同时加固既有 spend recovery 的持久化 receipt/path identity。Python 只保留 + `should-run` facts、UUID/clock、一次 coarse transport call 与 legacy cross-writer + index lock。 - 本地 task-lease lifecycle:native TypeScript transaction 现在拥有 acquire、renew、 transfer、release、terminal verification、holder verification 与 fence close。它们拥有 boundary decode、handoff 与 owner/Todo eligibility、同 Todo 与重叠 write scope @@ -280,10 +291,12 @@ window 仍需 differential proof 时才保留 characterization corpus;引入 compare-and-swap、idempotency 与 lease-file durability check。无效 identity 会在 provider 前停止;provider 后发生 crash/retry 时则重入同 key 的幂等路径。 -Quota-spend cutover 删除了 Python spend-event builder 与三文件 writer。它的 bounded -facade 会在 quota CLI 和剩余 run-index writer 进程内执行 transaction 后退出;在此 -之前,它只提供 compact projection facts,并与未迁 writer 共享 legacy Python index -lock。Todo cutover 删除了 Python state-evaluation dataclass、local identity projection、 +Quota-accounting cutover 删除了 Python spend/void event builder 与三文件 writer。 +当 quota decision 与顶层 CLI 在进程内执行 TypeScript、全部 run-index writer 改用 +native lock,并且 legacy Python void API compatibility window 结束时,它们的 bounded +facade 即可退出。在此之前,Python 只提供 compact projection facts、clock/effect +identity、result validation 与共享 legacy index lock。Todo cutover 删除了 Python +state-evaluation dataclass、local identity projection、 replay helper,以及这些 implementation leaf 的 public runtime handler。剩余 Python Todo facade 只拥有 transport、external command execution、source compare-and-swap、 legacy response projection 与实际 Markdown/event write;当 writer 与 CLI 进入 native @@ -304,6 +317,19 @@ managed Node server PID;stale reclaim 会先取得 token claim,并用抗路 核验后再退役 lock。这不构成“同一 Node 进程内 handler 超时后仍并行执行时”的 exactly-once 保证;原 handler 可能仍存活时,caller 不得启动第二笔独立 operation。 +#### Quota void commit 迁移经济账 + +| 字段 | 回执 | +| --- | --- | +| Canonical owner | 迁移前由 Python `slot_accounting.py` 拥有 spend-target lookup、correction reduction、event/result 构造、artifact 分配及 JSON/Markdown/index persistence。迁移后由版本化 TypeScript `quota.void.commit` 拥有这些语义,并通过闭合的 spend/void accounting kernel 拥有 effect fence、index CAS、receipt、replay 与 repair。 | +| 删除的旧语义代码 | 删除 212 行 Python 产品代码,包括原 void lookup、transition、event/projection、path allocation 与 JSON/Markdown/index writer 路径。 | +| 新增的 bridge 代码 | 新增 263 行 Python diff LOC,其中 243 行是有界的 `void_commit.py` transport/compatibility facade,另有 `loopx/quota.py` 与 legacy `slot_accounting.py` surface 中 20 行 import、re-export、normalization 与 route wiring。 | +| 跨 runtime 调用 | 公开 execute 与 dry-run 路径从零次 crossing 变为一次 coarse request/response。Exact-effect replay 或 repair 也使用一次。不同 CLI invocation 仍是不同 effect;legacy preview 加 record 两步 compatibility surface 的每个 entrypoint 各调用一次。 | +| 产品代码净增减 | 产品代码新增 2,210 行、删除 898 行,净增 1,312 行。Test/example 另计新增 1,416 行、删除 3 行,净增 1,413 行;build configuration 为 +3,docs 不计入。生产共享 kernel 已同时服务 spend 与 void,并替换 `spend_commit.ts` 中 671 行逻辑,不是预留的 speculative framework。 | +| 迁移 scaffolding | 没有新增 migration-only worker、parity corpus 或临时 schema framework。保留 native boundary/invariant/replay/CAS/repair 测试作为已交付和持久化 contract;Python bridge 测试随 compatibility facade 一起退出。 | +| Facade 退出 | 当 quota decision 与顶层 CLI 在进程内执行 TypeScript、全部 run-index writer 使用 native lock,并且 legacy `build_*void*`/`record_*void*` Python API compatibility window 结束时,删除 Python void facade。 | +| 正确性与性能 | Typed-decoder 负例、legacy target compatibility、effect isolation、index CAS、malformed receipt/path、exact index-row identity、受支持的 duplicate-index repair、concurrent mutation、truncated-tail repair、公开 CLI 行为,以及干净 wheel/sdist semantic probe 均通过。16 次 cold start 的 p50/p95 为 230.88/260.92 ms;128 次 warm typed ping 为 1.07/1.29 ms,warm void preview 为 1.93/2.34 ms。64 次 durable facade transaction 中,commit 为 30.64/37.49 ms,exact-effect replay 为 8.05/9.86 ms。Daemon RSS 在 idle 时为 108.38 MiB,256 次请求后为 109.80 MiB。64 对交错 full-CLI 样本中,baseline/candidate p50/p95 为 736.51/828.68 与 779.52/856.49 ms,p95 增量为 27.81 ms(3.36%)。这个绝对增量来自新增的一次 managed-runtime fingerprint/request 与 prepared-receipt durability;百分比低于 5% 物质回退门槛,Stage 3 会删除这次 crossing。 | + #### Task-lease acquire 迁移经济账 | 字段 | 回执 |