# DashClaw Durable Execution Finality — Release Readiness Report

**Date:** 2026-05-13
**Auditor:** Claude Opus 4.7 (1M context)
**Scope:** Six-phase Durable Execution Finality update (commits `25599c35` → `5407b6ca` plus Dockerfile fix `0f07fc50`)
**Source goal:** `DASHCLAW_DURABLE_FINALITY_RELEASE_READINESS_GOAL.md`

---

## TL;DR

**Verdict: SHIP-READY** with three low-risk corrections applied during this audit. No P0/P1 architecture blockers found. All required quality gates pass. The new HTTP surface (`/api/actions/:id/outcome`, `/api/cron/outcome-sweep`), the SDK wrappers (Node + Python), the `/decisions` outcome filter/badge, and idempotency-key plumbing all line up with the implementation. The Vercel daily-cron tradeoff is documented honestly. Recommend Wes tag/release after a final glance at the file list below.

---

## What shipped across the six phases

| Phase | Commit | What landed |
|---|---|---|
| 1 | `25599c35` | `drizzle/0004_action_outcome_finality.sql` + `schema/schema.js` extension; `outcome_status` (5-state CHECK), `outcome_at`, `outcome_summary`, `outcome_error`, `outcome_progress` (jsonb), `idempotency_key`; partial index on pending rows; conditional unique idempotency index; `getActionOutcome` + `setActionOutcome` with one-shot CAS at the repo layer; `POST/GET /api/actions/[actionId]/outcome` with DLP redaction, 8 KB progress cap, sweep-state rejection; 22 unit tests; docs + nav + MethodEntry in `app/docs/page.js`. |
| 2 | `fdc45bf8` | `app/api/cron/outcome-sweep/route.js` (CRON_SECRET auth, per-org loop, `DASHCLAW_OUTCOME_TIMEOUT_MINUTES` clamped `[1, 1440]`, `signal.detected` fan-out, webhook delivery); `sweepLostOutcomesForOrg` + `listOrgsWithStaleOutcomes` helpers; `vercel.json` daily cron entry; 8 sweep route tests + 6 repo tests. |
| 3 | `9a5e7930` | Node SDK `reportActionOutcome`, `getActionOutcome`, `reportActionSuccess`/`Failure`/`Partial`; 6 SDK unit tests. |
| 4 | `1593e342` | Python SDK snake_case parity (`report_action_outcome`, `get_action_outcome`, three convenience wrappers); 8 Python SDK tests; README + parity matrix updates. |
| 5 | `63c8c73c` | `app/components/OutcomeBadge.js`; `listActions` SELECT + outcome filter; `GET /api/actions` accepts `outcome_status` query param; `/decisions` page outcome dropdown + row badge; `/decisions/[actionId]` Final Outcome badge + summary/error line. |
| 6 | `5407b6ca` | `idempotency_key` validated, persisted, and short-circuited in `POST /api/actions`; `getActionByIdempotencyKey` repo helper; Node `deriveIdempotencyKey` + Python `derive_idempotency_key` SHA-256 helpers; 4 Node + 4 Python helper tests + 3 route short-circuit tests. |
| Bonus | `0f07fc50` | Dockerfile fix: add `python3 make g++` to deps stage so `better-sqlite3@12.10.0` (no musl prebuilt) can compile from source. Unrelated to the finality work; uncovered when CI ran the demo image build. |

Spec: [`docs/architecture/durable-execution-finality.md`](docs/architecture/durable-execution-finality.md). Tracking issue: [#105](https://github.com/ucsandman/DashClaw/issues/105).

---

## Local repo state at audit start

Tracked changes from this audit are all in `docs/` and `public/downloads/`. Untracked or modified items that pre-existed:

| Path | Tracking | What it is | Audit action |
|---|---|---|---|
| `CLAUDE.md` (modified) | tracked | AgentLens appended an `agentlens:merged-sections` block after line 158. Not part of my changes. | **Documented — not touched.** Wes should decide if the appended block is desired. |
| `CLAUDE.NEW.md` | **untracked, NOT gitignored** | AgentLens-generated CLAUDE.md proposal, frontmatter says "Review before committing." | **Inspected — not touched.** Recommend Wes decide before tag/release. |
| `.agents/` | gitignored | Out-of-band agent scratch. | **Documented — not touched.** |
| `plugins/` | gitignored | Local-only Codex plugin snapshot. SKILL.md is STALE (lacks outcome routes) and api-surface.md says "174 active routes" — but because this directory is gitignored, no user receives it. | **Documented — not touched.** Wes's local Codex/Claude environment is the only consumer. |
| `.claude/skills/dashclaw-platform-intelligence/` | gitignored | Project-local mirror written by `livingcode:refresh`. Current after this audit. | Refreshed during audit. |

The shipped/downloadable skill bundle (`public/downloads/dashclaw-platform-intelligence/**`) IS tracked, IS regenerated by `livingcode:refresh`, and IS current as of this audit.

---

## Corrections applied during this audit

All edits are low-risk doc + reference updates; no code/schema/test changes.

1. **`docs/architecture/durable-execution-finality.md` — Cron sweep cadence claim.**
   - Was: "runs hourly via the existing Vercel cron mechanism."
   - Now: documents that **daily** is what shipped on Vercel Hobby, that operators on Pro can change `vercel.json` to `0 * * * *`, and that external operators can hit the endpoint at any cadence with `Authorization: Bearer $CRON_SECRET`. The tradeoff (slower sweep → only delays surfacing of `lost_confirmation`, never weakens correctness) is called out.

2. **`docs/architecture/durable-execution-finality.md` — Per-org setting key name.**
   - Was: "Per-org override via `outcome_timeout_minutes` in `settings`."
   - Now: "Per-org override via the `DASHCLAW_OUTCOME_TIMEOUT_MINUTES` key in `settings` (allow-listed in `app/lib/repositories/settings.repository.js`). The cron clamps the resolved value to `[1, 1440]` minutes." This is the actual key shipped in Phase 2.

3. **`docs/architecture/durable-execution-finality.md` — Open question #3 (sweep frequency).**
   - Replaced the "Lean: hourly" prose with what actually shipped (daily) plus operator escape hatches.

4. **`public/downloads/dashclaw-platform-intelligence/references/api-surface.md` — Curated narrative skill.**
   - Added `/api/actions/[actionId]/outcome` + `/api/cron/outcome-sweep` rows to the Action Recording table.
   - Added a "Durable execution finality (v2.13.3+)" prose block explaining the five-state machine, one-shot semantics, SDK helpers, idempotency-key short-circuit, sweep cadence, and `DASHCLAW_OUTCOME_TIMEOUT_MINUTES`.
   - Ran `npm run livingcode:refresh` to mirror the update to `.claude/skills/...` (project) and `~/.claude/skills/...` (global). `public/downloads/dashclaw-platform-intelligence.zip` was regenerated.

The auto-generated `SKILL.md` (in the same bundle) already listed the new routes from Phase 2's commit hook. Only the hand-curated narrative was lagging.

---

## Section-by-section findings

### 1. API surface and docs alignment

| Source | State |
|---|---|
| `docs/openapi/critical-stable.openapi.json` | ✓ Up to date — `npm run openapi:check` clean. |
| `docs/api-inventory.{json,md}` | ✓ Up to date — `npm run api:inventory:check` clean. Inventory includes both new routes. |
| `PROJECT_DETAILS.md` (route table) | ✓ Lists `POST/GET /api/actions/:actionId/outcome` and `/api/cron/outcome-sweep` with honest "Daily on Vercel free tier; operators can run hourly externally" cadence note. |
| `docs/architecture/durable-execution-finality.md` | ✓ After this audit's edits. |
| `sdk/README.md` + `sdk-python/README.md` | ✓ Both reference the actual method names and include retry-safe-poll code; both call out daily Vercel cron + hourly external option. |
| `docs/sdk-parity.md` | ✓ Both SDKs marked as Yes for this domain. |
| `README.md` / `QUICK-START.md` | The four-step Governance Loop in `QUICK-START.md` still uses the legacy `updateOutcome` (PATCH `/api/actions/:id`) flow. That is intentional — that's the canonical record-completion flow. The new `/outcome` route is the **terminal-state declaration** for retry safety, a distinct concept that doesn't belong in a "first agent in 8 minutes" walkthrough. **No change recommended.** |
| `public/downloads/dashclaw-platform-intelligence/SKILL.md` | ✓ Auto-generated; lists both new routes. |
| `public/downloads/dashclaw-platform-intelligence/references/api-surface.md` | ✓ After this audit's edits. |
| `public/downloads/dashclaw-platform-intelligence/references/troubleshooting.md` | Mentions legacy `updateOutcome` only. Acceptable — troubleshooting is about debugging a stuck/broken loop, not about idempotency. **No change recommended.** |
| `public/downloads/dashclaw-platform-intelligence/references/platform-knowledge.md` | Mentions "outcomes" once in passing. Acceptable. **No change recommended.** |

### 2. Migrations + fresh-install path

`drizzle/0004_action_outcome_finality.sql` (committed in Phase 1) does the right things:

- `ALTER TABLE … ADD COLUMN IF NOT EXISTS` for all six new columns.
- CHECK constraint added inside a `DO $$ … END $$` block guarded by `pg_constraint` lookup, so re-runs don't fail.
- Partial index on `pending` rows uses `IF NOT EXISTS`.
- Conditional unique idempotency index uses `IF NOT EXISTS`.
- All statements are separated by `--> statement-breakpoint` so `scripts/auto-migrate.mjs` splits them into one-shot DDL calls.

`scripts/auto-migrate.mjs` confirmed to:

- Read every `drizzle/*.sql` file in filename order.
- Apply each statement via `sql.unsafe`.
- Swallow `42P07 / 42P16 / 42701 / 42710 / 42P10 / 23505` and "already exists" message paths as expected idempotent re-runs.
- Run a second pass (Step 1b) that auto-`ALTER TABLE ADD COLUMN IF NOT EXISTS` for every column found in a CREATE TABLE statement, so old deploys backfill cleanly.

`schema/schema.js` carries the matching Drizzle definitions (`outcomeStatus`, `outcomeAt`, `outcomeSummary`, `outcomeError`, `outcomeProgress`, `idempotencyKey`, plus the CHECK constraint) so any future `drizzle-kit generate` keeps the model honest. Runtime queries in `actions.repository.js` use raw tagged-template SQL, not Drizzle ORM, so the schema.js edit is documentation-grade — but it's correct and consistent.

**`.env.example`:** no new env var is required. `DASHCLAW_OUTCOME_TIMEOUT_MINUTES` is a per-org **setting** (stored in the `settings` table, allow-listed in `app/lib/repositories/settings.repository.js`), not an env var, and the cron falls back to 15 if unset. `CRON_SECRET` was already documented (commented out in `.env.example`) for the existing cron routes — same secret unlocks the new sweep route. **No change needed.**

**`npm run db:migrate`:** documented in `CLAUDE.md` as a required step "after pulling changes that touch `schema/schema.js` or `drizzle/*.sql`." That paragraph was already in place from prior schema work and applies verbatim to this update. Confirmed.

**Live migration not run during this audit** — the goal forbids running migrations against any non-safe DB and our local `.env.local` is uninspected. Idempotency is verified by reading the migration code path, not by execution. ✅ within the safety rules.

### 3. Startup + one-command setup

- `scripts/setup.mjs` and `scripts/init-self-host-env.mjs` were not touched by Phase 1-6 and don't need a Durable Finality patch — there's no new user-facing prerequisite. Setup unchanged.
- `vercel.json` cron cadence is now consistent with what the docs say after fix #1 above.
- No hidden manual SQL required. No new env var required.
- `npm run startup:smoke` was not run during this audit because it boots a real local server and the goal says "do not leave long-running dev servers alive." Inspected `scripts/startup-smoke.mjs` and `scripts/lib/startup-smoke.mjs` — they do exit cleanly after `/api/setup/status` reports `configured`. Wes can run it any time; it doesn't exercise outcome-specific routes.

### 4. SDK parity and examples

| Surface | Node | Python | Tests |
|---|---|---|---|
| `reportActionOutcome` / `report_action_outcome` | ✓ `sdk/dashclaw.js` line ~810 | ✓ `sdk-python/dashclaw/client.py` line ~1910 | 4 Node + 4 Python |
| `getActionOutcome` / `get_action_outcome` | ✓ | ✓ | 2 Node + 1 Python |
| `reportActionSuccess` / `report_action_success` | ✓ | ✓ | 1 each |
| `reportActionFailure` / `report_action_failure` | ✓ | ✓ | 1 each |
| `reportActionPartial` / `report_action_partial` | ✓ | ✓ | 1 each |
| `deriveIdempotencyKey` / `derive_idempotency_key` | ✓ uses `import { createHash } from 'crypto'` (ESM) | ✓ `@staticmethod`, uses `hashlib` | 4 each — identical-hash, differs-on-change, order-independent, type-validation |

Naming convention split (camelCase Node, snake_case Python) matches `MEMORY.md` rule. Both READMEs include matching code-block examples; both reference the `/outcome` HTTP endpoint as a fallback for non-SDK callers. `docs/sdk-parity.md` matrix row is accurate.

**`npm run sdk:integration:python`**: ✅ 18 tests pass (4 skipped — those are the live-network tests, intentionally skipped without `DASHCLAW_BASE_URL`). The targeted `test_sdk_v2_surface.py` covers all 8 new outcome wrappers + 4 new idempotency-key helper cases — 49 tests, all pass.

**`npm run sdk:integration`** (Node): not run during this audit. Inspected `scripts/check-sdk-cross-integration.mjs` — it requires a live DashClaw instance (`DASHCLAW_BASE_URL` + `DASHCLAW_API_KEY`) to make real HTTP calls. **Skipped per safety rule 2** ("do not run live SDK calls against production unless [...] uses test/local env"). Vitest covers the SDK surface at the unit level — 124 tests passed when isolated, 1837 in the full suite.

### 5. OpenClaw plugin and packaged skills

- `packages/openclaw-plugin/package.json` (`@dashclaw/openclaw-plugin@1.2.5`) — name + version current, depends on `dashclaw ^2.11.1`. **No claim about Durable Execution Finality belongs here** — the plugin's job is intercepting OpenClaw tool calls and shipping them to DashClaw; outcome reporting is the agent-framework author's responsibility, not the plugin's. **No change needed.**
- `packages/openclaw-plugin/openclaw.plugin.json` — configSchema is current; all three accepted shapes (canonical / SDK-alias / env) documented honestly. No stale endpoint names. ✓
- `packages/openclaw-plugin/README.md` — install command + config keys all match the schema. ✓
- `plugins/dashclaw/.mcp.json` + `plugins/dashclaw/.codex-plugin/plugin.json` — these live under the gitignored `plugins/` tree (local-only). The committed equivalent for general users is the MCP server config snippet in `README.md` (line 87+), which is current.
- `plugins/dashclaw/skills/dashclaw-platform-intelligence/SKILL.md` — stale (lacks outcome routes) BUT local-only because `plugins/` is gitignored. Not a shipping concern. Wes can regenerate locally any time via `python -m livingcode emit skill` if he wants his local Codex environment current.
- `public/downloads/dashclaw-platform-intelligence/SKILL.md` — auto-generated, current, lists `/api/actions/[actionId]/outcome` and `/api/cron/outcome-sweep`.
- `public/downloads/dashclaw-platform-intelligence/references/api-surface.md` — **updated by this audit** to mention the new surface.

**`.agents/plugins/marketplace.json`** — file exists in the gitignored `.agents/` tree only, not in the shipping repo. Not a release concern.

### 6. `/decisions` UI

- `app/components/OutcomeBadge.js`: 5-state mapping is correct — `pending → info (with animate-pulse)`, `completed → success`, `partial → warning`, `failed → error`, `lost_confirmation → default` (neutral zinc). Labels short and non-alarmist: "Pending / Completed / Partial / Failed / Lost". The `lost_confirmation → "Lost"` label is intentionally not alarmist — the system can't *know* the action failed, only that confirmation didn't arrive. ✓
- All color tokens come from `app/components/ui/Badge.js` (`success-subtle`, `warning-subtle`, `error-subtle`, `info-subtle`) — token-first, no hardcoded hex. Matches `.impeccable.md` token rules. ✓
- `app/decisions/page.js`: new "All outcomes" `<select>` is a native element, keyboard-navigable and screen-reader-friendly by default. Sits between status and risk-min filters, consistent visual rhythm.
- Row badge appears only when `outcome_status !== 'pending'` to avoid noise on the still-running rows.
- `app/decisions/[actionId]/page.js`: outcome badge sits next to the legacy status display in the Final Outcome timeline cap. Small summary line beneath surfaces `outcome_summary` for success/partial, `outcome_error` for failed, and a custom "No outcome reported within timeout window (system sweep)" string for `lost_confirmation` — distinct copy per state, never misleadingly "completed."
- Filter wiring: `filterOutcome` state, query param `outcome_status`, fetch dep array all updated correctly. Repository validates the value against the 5-state enum before SQL interpolation.

**Recommendation:** consider `aria-label="Action outcome state"` on the OutcomeBadge wrapper element in a future polish pass. Not a release blocker — the visible text labels already convey state for assistive tech.

### 7. Idempotency keys

- `app/lib/validate.js` accepts `idempotency_key` as a max-256-char string on the ActionRecord schema.
- `app/api/actions/route.js` short-circuits on `(org_id, idempotency_key)` hit BEFORE quota, guard, signature verification, or insert — so a duplicated POST does zero downstream work and returns `{ idempotent_replay: true }`. Verified by 3 dedicated route tests.
- `getActionByIdempotencyKey` is repo-layer; no direct SQL in route.
- Unique index `action_records_idempotency_idx ON (org_id, idempotency_key) WHERE idempotency_key IS NOT NULL` (Phase 1 migration) prevents DB-level race-condition duplicate even if two requests slip past the lookup window.
- Node helper uses `createHash('sha256')` via top-level `import { createHash } from 'crypto'` (ESM-correct after my Phase 6 fix). Python helper uses `hashlib.sha256` inside a `@staticmethod`. Both helpers sort keys before hashing → order-independent ✓. Both reject non-dict input with TypeError ✓.

---

## Commands run

All required gates plus the targeted Python suite:

| Command | Result |
|---|---|
| `npm run lint` | ✓ pass |
| `npm run docs:check` | ✓ `docs validation passed` |
| `npm run route-sql:check` | ✓ `Route SQL guard passed: no direct SQL usage increases (current total 85, baseline total 90).` |
| `npm run openapi:check` | ✓ `OpenAPI artifact is up to date` |
| `npm run api:inventory:check` | ✓ `API inventory artifacts are up to date` |
| `npm test -- --run` | ✓ **228 files passed / 1 skipped (229) — 1837 pass / 5 skip (1842) — 34.25s** |
| `npm run sdk:integration:python` | ✓ 18 pass / 4 skipped (live-network) |
| `PYTHONPATH=sdk-python python -m unittest discover -s sdk-python/tests -p "test_sdk_v2_surface.py"` | ✓ 49 pass |
| `npm run livingcode:refresh` | ✓ mirrored updated api-surface.md to `.claude/skills/...` (project) and `~/.claude/skills/...` (global). Regenerated `public/downloads/dashclaw-platform-intelligence.zip`. |

**Not run, by design:**

- `npm run sdk:integration` (Node) — requires live `DASHCLAW_BASE_URL` + `DASHCLAW_API_KEY`. Skipped per safety rule 2. Vitest unit coverage for the SDK surface is comprehensive (124 tests in the SDK + outcome suites).
- `npm run db:migrate` — skipped per safety rule (local DB target uninspected for prod risk). Migration idempotency verified by reading `scripts/auto-migrate.mjs` and `drizzle/0004_action_outcome_finality.sql`.
- `npm run startup:smoke` — skipped per goal directive ("Do not leave long-running dev servers alive after verification."). Not needed: outcome feature doesn't change the boot path.

---

## Files changed by this audit

| File | Change |
|---|---|
| `docs/architecture/durable-execution-finality.md` | Cron-cadence claim + setting-key name + open-question #3 corrected. |
| `public/downloads/dashclaw-platform-intelligence/references/api-surface.md` | New row in Action Recording table + "Durable execution finality" prose block. |
| `.claude/skills/dashclaw-platform-intelligence/references/api-surface.md` | Auto-mirrored by `livingcode:refresh`. **GITIGNORED** — not committed. |
| `public/downloads/dashclaw-platform-intelligence.zip` | Regenerated by `livingcode:refresh` after the source update. |
| `public/downloads/dashclaw-platform-intelligence.zip.manifest` | Updated hash. |
| `DURABLE_FINALITY_RELEASE_READINESS_REPORT.md` | This report. |

`~/.claude/skills/dashclaw-platform-intelligence/references/api-surface.md` was also overwritten (operator-private global skill) — not part of the repo. No git impact.

**Nothing was committed during this audit.** All edits sit in the working tree for Wes's review.

---

## Confidence ratings

| Domain | Confidence | Why |
|---|---|---|
| Fresh-install / migration | **High** | Migration is `IF NOT EXISTS`-guarded throughout. `auto-migrate.mjs` swallows the standard idempotent-rerun error codes. No new env var required. |
| API contract | **High** | `openapi:check` + `api:inventory:check` clean. Both new routes appear in `docs/api-inventory.md`. PROJECT_DETAILS.md route table accurate. |
| SDK parity | **High** | Node + Python wrappers mirror each other; 49 Python tests + 124 Node SDK/outcome tests all pass; READMEs match implementation. |
| Skill / plugin accuracy | **Medium-High** | The shipped curated narrative now mentions the new surface (after this audit's edit). SKILL.md is auto-generated and current. Local-only `plugins/` snapshot is stale but ignored. |
| `/decisions` UI | **High** | OutcomeBadge tokens correct; labels non-alarmist; filter wiring validated; row badge hides on `pending` to avoid noise. Minor a11y polish recommended as P2. |
| Idempotency semantics | **High** | One-shot at repo layer + unique DB index + early route short-circuit. Verified by tests covering race / cross-org / no-key paths. |

---

## Remaining follow-ups

### P2 (nice-to-have polish, not blocking)

1. **OutcomeBadge `aria-label`.** Add `aria-label={\`Outcome: ${label}\`}` to the wrapping `<Badge>` so screen readers announce the semantic role distinct from the surrounding status display.
2. **Troubleshooting reference doc.** Add a short "Action outcome stuck in `pending`" entry to `public/downloads/dashclaw-platform-intelligence/references/troubleshooting.md` — points readers at `DASHCLAW_OUTCOME_TIMEOUT_MINUTES` and the sweep cadence.
3. **Mission Control instrument rail.** Per the original phase plan, outcome-state counts were called out as "open follow-up explicitly not shipped." Still open.
4. **`/operations/feed` surfacing.** `lost_confirmation` signals already flow through the existing signal pipeline so they're not invisible — but a dedicated row treatment in `/operations/feed` would help operator UX. Open follow-up.

### P1 (decide before tag, not blocking ship)

5. **`CLAUDE.NEW.md` decision.** Wes should accept, revise, or delete the AgentLens-generated CLAUDE.md proposal before tagging. The current `CLAUDE.md` has an `agentlens:merged-sections` block appended that isn't part of any code commit.

### P0

None.

---

## Ready to tag/release?

**Yes** — the six phases are functionally complete, all required gates pass, the API surface is consistent across code/docs/SDK/skill, and the daily-cron tradeoff is documented honestly. The corrections applied during this audit (architecture-doc accuracy + curated skill narrative) are doc-grade and ship-safe.

Before tagging:

1. Review the working-tree diffs from this audit (4 changed files).
2. Decide what to do with `CLAUDE.NEW.md` and the appended `CLAUDE.md` AgentLens block.
3. Run a final `git status` to confirm nothing else has crept in.
4. Commit the doc fixes with whatever commit message you prefer.
