From 5a264c255cfce03205737a5cebcacc3ef3010a91 Mon Sep 17 00:00:00 2001 From: veil-chow-fyaic <247294299+veil-chow-fyaic@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:40:07 +0800 Subject: [PATCH] feat: project bounded m5.2 failure progress --- README.md | 2 +- ...26-09-02-m5-2-real-effects-live-attempt.md | 13 ++++ .../2026-09-02-mainline-checkpoint.md | 23 ++++-- docs/10-planning/mainline-plan.md | 7 +- docs/10-planning/project-status.md | 11 ++- scripts/run-m5-2-event-pump-gate.mjs | 4 + .../coordinator-driven-no-plan-scenario.mjs | 75 +++++++++++++++++++ src/validation/m5-2-event-pump-codex-gate.mjs | 66 +++++++++++++++- ...ordinator-driven-no-plan-scenario.test.mjs | 34 +++++++++ test/m5-2-event-pump-codex-gate.test.mjs | 42 +++++++++++ 10 files changed, 262 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4997b97..df312fb 100644 --- a/README.md +++ b/README.md @@ -342,7 +342,7 @@ peer content or as a production security boundary. install Ajv and native `better-sqlite3`. - **Reference runtime:** authenticated JSON-RPC + SQLite coordinator for local, trusted-process experiments. -- **Validation:** 384 tests, plus 55 schema cases and 7 transition cases; +- **Validation:** 385 tests, plus 55 schema cases and 7 transition cases; documentation lint passes. These are separate counts, not one combined total. - **Default:** proactive coordination remains off unless a maintainer explicitly opts into the bounded experimental profile. diff --git a/docs/09-reviews/2026-09-02-m5-2-real-effects-live-attempt.md b/docs/09-reviews/2026-09-02-m5-2-real-effects-live-attempt.md index ee43b77..e1eb946 100644 --- a/docs/09-reviews/2026-09-02-m5-2-real-effects-live-attempt.md +++ b/docs/09-reviews/2026-09-02-m5-2-real-effects-live-attempt.md @@ -71,3 +71,16 @@ Do not repeat the run blindly. First retain a public, SQLite-derived partial manifest that exposes the exact phase, durable dispatch/action counts, bounded reconciliation reason, and cleanup without raw prompts, receipts, paths, or session identifiers. Then correct the observed boundary and run once. + +## Follow-up implementation + +The requested failure manifest is now implemented for the next run. It is +captured from the live coordinator database before cleanup and then passed +through an exact public projector. The output is restricted to a schema +version, source marker, derived stage, nine bounded counts, and—only for an +ambiguous recovery—one fixed-enum reason code. Any extra key, unknown reason, +string, impossible count, or stage/count mismatch suppresses the projection. + +The historical aggregates above remain operator observations from attempt 11; +they are not retroactively upgraded into machine evidence. The next live run is +the first one that can produce the new machine-verifiable partial projection. diff --git a/docs/09-reviews/2026-09-02-mainline-checkpoint.md b/docs/09-reviews/2026-09-02-mainline-checkpoint.md index 1d3330f..d9fb6ba 100644 --- a/docs/09-reviews/2026-09-02-mainline-checkpoint.md +++ b/docs/09-reviews/2026-09-02-mainline-checkpoint.md @@ -10,8 +10,9 @@ critical path. ## Repository state -- branch: `main`, clean and synchronized with `origin/main`; -- commit: `0c7165ead39499497827737c17e94e95f0f3286b`; +- branch base: `main`, synchronized with `origin/main` before this diagnostic + slice; +- base commit: `e8b8ce62dd177dde09557b7311939f871b3e3954`; - latest product work: real-effects integration #133 and product-proof update #134; - public discovery: one star, zero forks, and no external operator report; @@ -21,9 +22,9 @@ critical path. The following passed on the exact checkpoint commit: -- 384/384 unit and subtests; +- 385/385 unit and subtests; - 55 schema cases and 7 transition cases; -- 112 Markdown files with zero lint issues; +- 114 Markdown files with zero lint issues; - focused autonomous real-effects scenario: 9/9; - one-command attention-router demo: `state=passed`; - modeled manual lower bound: 9 user actions; @@ -57,11 +58,19 @@ and zero remaining journals. The exact empty caller-owned artifacts directory was then removed. This is a failed partial attempt, not a product pass. See the [attempt record](2026-09-02-m5-2-real-effects-live-attempt.md). +The follow-up diagnostic slice now captures a strict pre-cleanup SQLite +projection on scenario failure. It exposes only a fixed stage enum, nine +bounded aggregate counts, and an allowlisted ambiguous-reconciliation reason; +raw prompts, receipts, paths, thread IDs, and journal contents are not included. +Schema drift, excess counts, inconsistent stages, and unsafe reason strings are +rejected by the CLI projector. Failure-stage, tamper, privacy, and exact cleanup +tests pass. + ## Ordered next gates -1. Explain the reviewer reconciliation failure with a bounded partial-stage - manifest, correct the observed blocker, and retain one successful traversal - of the merged real-effects Codex path. +1. Run the merged real-effects Codex path once with the new partial-stage + manifest, use its exact bounded reconciliation reason to correct the + observed blocker, and retain one successful traversal. 2. One same-condition manual/ThreadMesh comparison with action, elapsed, usage, interruption, duplicate-delivery, and cleanup measurements. 3. The complete M5.3 matrix: relevant 3/3, irrelevant, stale/unverified, diff --git a/docs/10-planning/mainline-plan.md b/docs/10-planning/mainline-plan.md index ea17102..7c87ef6 100644 --- a/docs/10-planning/mainline-plan.md +++ b/docs/10-planning/mainline-plan.md @@ -48,9 +48,10 @@ Execute in this order: 1. Treat the real behavioral chain as passed and retain its exact evidence; do not rerun it merely to polish counts or prose. -2. Diagnose the fresh reviewer admitted-turn reconciliation failure from - bounded product/coordinator evidence, retain a public partial-stage manifest, - and rerun the integrated bounded Git and child-verifier path once. Keep the +2. The public SQLite-derived partial-stage manifest is implemented and rejects + raw or inconsistent failure data. Rerun the integrated bounded Git and + child-verifier path once, use the emitted reconciliation reason to fix the + exact reviewer boundary, then retain one successful traversal. Keep the process-scoped proxy, certificate validation, product version, and exact cleanup boundary unchanged. 3. Retain the measured manual/live baseline. The executable demo already diff --git a/docs/10-planning/project-status.md b/docs/10-planning/project-status.md index 0cd9e4e..f59e68c 100644 --- a/docs/10-planning/project-status.md +++ b/docs/10-planning/project-status.md @@ -49,7 +49,7 @@ setup attempts are retained. | Research and problem framing | Codex deep dive, community signals, ecosystem comparison, ADRs | Established | | Community adoption | One GitHub star; zero forks and zero external issue comments or independent setup results as of 2026-09-02 | Positive discovery hint only; adoption unvalidated | | Active product outcome | One-command lifecycle-event and dependency-handoff loop with inspector, manual-action accounting, active-receiver checkpoint negative, and 76-second walkthrough | Real Codex A/R/same-A/V/dependent behavioral chain completed after one kickoff; real Git/verifier path merged; combined network-valid rerun pending | -| Protocol draft | 14 JSON Schemas; 55 schema cases; 7 transition cases; 384 tests | Executable draft; counts are reported separately | +| Protocol draft | 14 JSON Schemas; 55 schema cases; 7 transition cases; 385 tests | Executable draft; counts are reported separately | | Minimal adapter SDK | `@fyaic/threadmesh` `0.1.0-alpha.0`; six bounded client methods, per-turn proactive bridge, about 20 kB tarball, packed-consumer execution | Real Pi clean-consumer pass; not published to npm | | Local binding | Schema-validated JSON-RPC, transport-derived principals, typed errors | Executable local reference | | Local persistence | SQLite v10 registry, lifecycle state, append-only Git evidence, and durable per-dispatch event-pump selection/publication checkpoints | Experimental; global cross-dispatch pump chain absent | @@ -312,7 +312,7 @@ Git effects and verifier custody were simulated; the merged integration is not silently combined with that earlier run. The 2026-09-02 mainline check retained a clean deterministic regression: -384/384 unit and subtests, 55 schema cases, 7 transition cases, 112 Markdown +385/385 unit and subtests, 55 schema cases, 7 transition cases, 114 Markdown files with zero lint issues, and a passing one-command demo. Initial direct connectivity failed after system DNS returned unexpected non-provider addresses. A process-scoped local proxy then restored certificate-verified @@ -324,6 +324,13 @@ journals, and removed the caller-owned empty artifacts directory. See the [checkpoint](../09-reviews/2026-09-02-mainline-checkpoint.md) and [attempt record](../09-reviews/2026-09-02-m5-2-real-effects-live-attempt.md). +The next-run diagnostic is now implemented: scenario failures snapshot a +strict SQLite-derived stage and aggregate counts before cleanup, while the CLI +publishes only an exact allowlisted projection. Ambiguous reconciliation may +include one fixed-enum reason code; raw prompts, receipts, paths, session IDs, +and journal data are excluded. The next real run is therefore diagnostic rather +than blind: its reason code selects the reviewer-boundary correction. + Community reports are now grouped into three public product backlogs rather than one issue per upstream symptom: correlated handoff state [#135](https://github.com/fyaic/threadmesh/issues/135), durable diff --git a/scripts/run-m5-2-event-pump-gate.mjs b/scripts/run-m5-2-event-pump-gate.mjs index 512debc..c7aa3a1 100644 --- a/scripts/run-m5-2-event-pump-gate.mjs +++ b/scripts/run-m5-2-event-pump-gate.mjs @@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url"; import { projectM52EventPumpFailureCleanup, + projectM52EventPumpFailureProgress, runM52EventPumpCodexGate, runM52OperatorSuppliedCodexEventPumpGate, } from @@ -129,12 +130,15 @@ try { const preflight = preflightCodes.has(error?.code); const cleanup = error?.cleanup === undefined ? null : projectM52EventPumpFailureCleanup(error.cleanup); + const partialProgress = error?.partialProgress === undefined + ? null : projectM52EventPumpFailureProgress(error.partialProgress); console.error(JSON.stringify({ state: preflight ? "not-run" : "failed", code: shutdownSignal === null ? (error?.code ?? "threadmesh_m52_event_pump_runner_failed") : `threadmesh_m52_event_pump_runner_${shutdownSignal.toLowerCase()}`, liveAck: LIVE_ACK, + ...(partialProgress === null ? {} : { partialProgress }), ...(cleanup === null ? {} : { cleanup }), }, null, 2)); process.exitCode = preflight ? 3 : 1; diff --git a/src/validation/coordinator-driven-no-plan-scenario.mjs b/src/validation/coordinator-driven-no-plan-scenario.mjs index 0555f62..40f7a0b 100644 --- a/src/validation/coordinator-driven-no-plan-scenario.mjs +++ b/src/validation/coordinator-driven-no-plan-scenario.mjs @@ -49,6 +49,76 @@ function scenarioError(code) { return error; } +const FAILURE_PROGRESS_COUNT_QUERIES = Object.freeze({ + tasks: "SELECT COUNT(*) AS count FROM tasks", + dispatches: "SELECT COUNT(*) AS count FROM event_pump_dispatches", + turnIntents: "SELECT COUNT(*) AS count FROM turn_execution_intents", + toolActions: "SELECT COUNT(*) AS count FROM turn_tool_actions", + lifecyclePublications: "SELECT COUNT(*) AS count FROM lifecycle_action_publications", + gitEvidenceRecords: "SELECT COUNT(*) AS count FROM git_evidence_records", + dependencyFinalizations: + "SELECT COUNT(*) AS count FROM git_evidence_dependency_finalizations", + dependencySatisfactions: "SELECT COUNT(*) AS count FROM dependency_satisfactions", + cursorCommits: "SELECT COUNT(*) AS count FROM attention_cursor_commits", +}); + +export const COORDINATOR_FAILURE_RECONCILIATION_REASONS = Object.freeze([ + "codex-native-turn-identity-mismatch", + "codex-native-turn-thread-not-idle", + "codex-native-turn-baseline-truncated", + "codex-native-turn-baseline-mutated", + "codex-native-turn-no-observable-delta", + "codex-native-turn-multiple-new-turns", + "codex-native-turn-client-id-mismatch", + "codex-native-turn-client-id-missing", + "codex-native-turn-completed-observation-only", + "codex-native-turn-still-in-progress", + "codex-native-turn-started-id-mismatch", +]); + +export function deriveCoordinatorDrivenFailureStage(counts) { + const durableStages = Math.min( + counts.lifecyclePublications ?? 0, + counts.gitEvidenceRecords ?? 0, + ); + if ((counts.dependencyFinalizations ?? 0) > 0 || + (counts.dependencySatisfactions ?? 0) > 0) { + return "dependency-finalized"; + } + if (durableStages >= 4) return "verification-published"; + if (durableStages >= 3) return "fix-published"; + if (durableStages >= 2) return "review-published"; + if (durableStages >= 1 && (counts.turnIntents ?? 0) >= 3) { + return "reviewer-admitted-turn-partial"; + } + if (durableStages >= 1) return "implementation-published"; + if ((counts.turnIntents ?? 0) > 0) return "implementation-turn-partial"; + if ((counts.tasks ?? 0) >= 5) return "roles-registered"; + if ((counts.tasks ?? 0) > 0) return "roles-registering"; + return "coordinator-ready"; +} + +export function captureCoordinatorDrivenFailureProgress(database, error) { + const counts = Object.fromEntries(Object.entries(FAILURE_PROGRESS_COUNT_QUERIES) + .map(([key, query]) => [key, database.prepare(query).get().count])); + const recovery = error?.recovery; + const reasonCode = recovery?.reasonCode; + const reconciliation = recovery?.state === "ambiguous" && + COORDINATOR_FAILURE_RECONCILIATION_REASONS.includes(reasonCode) + ? { + state: "ambiguous", + reasonCode, + } + : null; + return Object.freeze({ + schemaVersion: 1, + source: "sqlite-pre-cleanup", + stage: deriveCoordinatorDrivenFailureStage(counts), + counts: Object.freeze(counts), + reconciliation: reconciliation === null ? null : Object.freeze(reconciliation), + }); +} + function throwIfShutdownRequested(signal) { if (signal?.aborted === true) { throw scenarioError("threadmesh_coordinator_driven_shutdown_requested"); @@ -872,6 +942,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ let adapter; let result; let failure; + let failureProgress = null; let evidenceRevision = 0; let evidenceHead = null; let verification = null; @@ -2179,6 +2250,9 @@ export async function runCoordinatorDrivenNoPlanScenario({ }, }; } catch (error) { + try { + failureProgress = captureCoordinatorDrivenFailureProgress(coordinator.db, error); + } catch {} if (injectFinalizationFailure || injectPreverifiedTamper) { let edgeStatus = "unavailable"; let taskState = "unavailable"; @@ -2338,6 +2412,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ }; if (failure) { failure.cleanup = cleanup; + if (failureProgress !== null) failure.partialProgress = failureProgress; if (failure.failureEvidence) failure.failureEvidence.cleanupComplete = cleanup.complete; throw failure; } diff --git a/src/validation/m5-2-event-pump-codex-gate.mjs b/src/validation/m5-2-event-pump-codex-gate.mjs index b0d1289..e5a176a 100644 --- a/src/validation/m5-2-event-pump-codex-gate.mjs +++ b/src/validation/m5-2-event-pump-codex-gate.mjs @@ -1,8 +1,11 @@ import path from "node:path"; import { canonicalJson, sha256Digest } from "../canonical-json.mjs"; -import { runCoordinatorDrivenNoPlanScenario } from - "./coordinator-driven-no-plan-scenario.mjs"; +import { + COORDINATOR_FAILURE_RECONCILIATION_REASONS, + deriveCoordinatorDrivenFailureStage, + runCoordinatorDrivenNoPlanScenario, +} from "./coordinator-driven-no-plan-scenario.mjs"; import { CodexLiveAgentRuntime, isCodexLiveAgentRuntime, @@ -161,6 +164,65 @@ export function projectM52EventPumpFailureCleanup(value) { }); } +const FAILURE_PROGRESS_COUNT_LIMITS = Object.freeze({ + tasks: 5, + dispatches: 5, + turnIntents: 16, + toolActions: 32, + lifecyclePublications: 4, + gitEvidenceRecords: 4, + dependencyFinalizations: 1, + dependencySatisfactions: 1, + cursorCommits: 5, +}); +export function projectM52EventPumpFailureProgress(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + const expectedKeys = [ + "schemaVersion", "source", "stage", "counts", "reconciliation", + ]; + if (canonicalJson(Object.keys(value).sort()) !== canonicalJson(expectedKeys.sort()) || + value.schemaVersion !== 1 || value.source !== "sqlite-pre-cleanup" || + !value.counts || typeof value.counts !== "object" || + Array.isArray(value.counts)) { + return null; + } + const countKeys = Object.keys(FAILURE_PROGRESS_COUNT_LIMITS); + if (canonicalJson(Object.keys(value.counts).sort()) !== + canonicalJson([...countKeys].sort())) { + return null; + } + const counts = {}; + for (const [key, limit] of Object.entries(FAILURE_PROGRESS_COUNT_LIMITS)) { + const count = value.counts[key]; + if (!Number.isSafeInteger(count) || count < 0 || count > limit) return null; + counts[key] = count; + } + if (value.stage !== deriveCoordinatorDrivenFailureStage(counts)) return null; + let reconciliation = null; + if (value.reconciliation !== null) { + const source = value.reconciliation; + const reconciliationKeys = ["state", "reasonCode"]; + if (!source || typeof source !== "object" || Array.isArray(source) || + canonicalJson(Object.keys(source).sort()) !== + canonicalJson(reconciliationKeys.sort()) || + source.state !== "ambiguous" || + !COORDINATOR_FAILURE_RECONCILIATION_REASONS.includes(source.reasonCode)) { + return null; + } + reconciliation = Object.freeze({ + state: source.state, + reasonCode: source.reasonCode, + }); + } + return Object.freeze({ + schemaVersion: 1, + source: "sqlite-pre-cleanup", + stage: value.stage, + counts: Object.freeze(counts), + reconciliation, + }); +} + function exactObject(value, keys, label) { if (!value || typeof value !== "object" || Array.isArray(value) || canonicalJson(Object.keys(value).sort()) !== canonicalJson([...keys].sort())) { diff --git a/test/coordinator-driven-no-plan-scenario.test.mjs b/test/coordinator-driven-no-plan-scenario.test.mjs index 36ba8c8..d5bdef5 100644 --- a/test/coordinator-driven-no-plan-scenario.test.mjs +++ b/test/coordinator-driven-no-plan-scenario.test.mjs @@ -353,6 +353,23 @@ test("failed trusted finalization starts no dependent business turn", async (t) ], cleanupComplete: true, }); + assert.deepEqual(error.partialProgress, { + schemaVersion: 1, + source: "sqlite-pre-cleanup", + stage: "fix-published", + counts: { + tasks: 5, + dispatches: 5, + turnIntents: 8, + toolActions: 11, + lifecyclePublications: 4, + gitEvidenceRecords: 3, + dependencyFinalizations: 0, + dependencySatisfactions: 0, + cursorCommits: 3, + }, + reconciliation: null, + }); assert.equal(error.cleanup?.complete, true); return true; }, @@ -528,4 +545,21 @@ test("a bounded shutdown after role bootstrap cleans every created role and run assert.equal(failure.cleanup?.remainingJournalCount, 0); assert.equal(failure.cleanup?.coordinatorRemoved, true); assert.equal(failure.cleanup?.runRootRemoved, true); + assert.deepEqual(failure.partialProgress, { + schemaVersion: 1, + source: "sqlite-pre-cleanup", + stage: "coordinator-ready", + counts: { + tasks: 0, + dispatches: 0, + turnIntents: 0, + toolActions: 0, + lifecyclePublications: 0, + gitEvidenceRecords: 0, + dependencyFinalizations: 0, + dependencySatisfactions: 0, + cursorCommits: 0, + }, + reconciliation: null, + }); }); diff --git a/test/m5-2-event-pump-codex-gate.test.mjs b/test/m5-2-event-pump-codex-gate.test.mjs index 20de7b3..4af3f39 100644 --- a/test/m5-2-event-pump-codex-gate.test.mjs +++ b/test/m5-2-event-pump-codex-gate.test.mjs @@ -14,6 +14,7 @@ import { } from "../src/validation/live-agent-scenario.mjs"; import { projectM52EventPumpFailureCleanup, + projectM52EventPumpFailureProgress, projectM52OperatorSuppliedCodexEventPumpGateResult, projectOperatorSuppliedCodexProbe, projectM52EventPumpCodexGateResult, @@ -413,3 +414,44 @@ test("failure cleanup projection is bounded and omits raw role and path data", ( ).includes("synthetic"), false); } }); + +test("failure progress projection exposes only bounded SQLite-derived stage data", () => { + const source = { + schemaVersion: 1, + source: "sqlite-pre-cleanup", + stage: "reviewer-admitted-turn-partial", + counts: { + tasks: 5, + dispatches: 1, + turnIntents: 3, + toolActions: 4, + lifecyclePublications: 1, + gitEvidenceRecords: 1, + dependencyFinalizations: 0, + dependencySatisfactions: 0, + cursorCommits: 0, + }, + reconciliation: { + state: "ambiguous", + reasonCode: "codex-native-turn-completed-observation-only", + }, + }; + assert.deepEqual(projectM52EventPumpFailureProgress(source), source); + + for (const mutate of [ + (value) => { value.rawThreadId = "raw-thread-id"; }, + (value) => { value.counts.prompt = 1; }, + (value) => { value.counts.turnIntents = 17; }, + (value) => { value.stage = "review-published"; }, + (value) => { value.reconciliation.reasonCode = "/private/raw/path"; }, + (value) => { value.reconciliation.reasonCode = "codex-native-turn-secret-id"; }, + ]) { + const tampered = structuredClone(source); + mutate(tampered); + assert.equal(projectM52EventPumpFailureProgress(tampered), null); + } + const encoded = JSON.stringify(projectM52EventPumpFailureProgress(source)); + assert.equal(encoded.includes("threadId"), false); + assert.equal(encoded.includes("prompt"), false); + assert.equal(encoded.includes("/private/"), false); +});