Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,9 +474,10 @@ revision and configuration digest prefixes.
`POST /api/analysis-runs` records a Pending run on a new authorized
cutoff capture (ADR 0017): snapshot, counts, frozen membership, run,
scope, and the first status in one transaction.
`POST /api/analysis-runs/{id}/start` then runs ThreadWeave on that
frozen bag and persists run-scoped edges (ADR 0021), or submits TEPP
through `tepp_client` (ADR 0022). It does not invent a TEPP score.
`POST /api/analysis-runs/{id}/start` then commits Running plus a
durable outbox row, wakes Valkey, and delivers ThreadWeave on that
frozen bag (ADR 0021 / ADR 0023) or submits TEPP through
`tepp_client` (ADR 0022). It does not invent a TEPP score.
Request a lineage reconstruction from the home list, open the Pending
row, then start reconstruction. A Pending TEPP row starts a
measurement; a missing transport stays Failed /
Expand Down Expand Up @@ -506,7 +507,8 @@ payload is lookup labels plus non-negative aggregate counts -- never
source SQL, a DSN, a raw record, or a provider body. After `make seed`,
Demo Analyst and Demo Admin see "Lineage reconstruction · Succeeded ·
Demo Corp" with "3 documents" and Pending / Running / Succeeded times,
and "TEPP measurement · Failed · Demo Corp" whose detail history ends
the designed A-100 fork as clickable reconstructed edges, and
"TEPP measurement · Failed · Demo Corp" whose detail history ends
in Failed / `tepp_not_available`.
A run-bearing registry is emptied only after an unrevoked
`analysis_run_retention_grant` and `GRANT analysis_run_retention_admin`,
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.d/0.93.0-seed-analysis-run-reconstruction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# 0.93.0 Seed analysis-run reconstruction

make seed persists the designed A-100 fork on the Demo Corp Succeeded
lineage run. Open that run and click the revised-quote child.
5 changes: 5 additions & 0 deletions CHANGELOG.d/0.94.0-analysis-run-outbox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 0.94.0 Analysis-run start outbox

Start commits Running plus a durable outbox row, wakes Valkey, then
delivers ThreadWeave or tepp_client. A crash no longer loses the work
item. No TEPP theta is invented.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ All notable changes to this project are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.94.0] - 2026-08-17

### Added

- `POST /api/analysis-runs/{id}/start` now commits Running plus one
durable outbox row, wakes Valkey (`analysis-run-outbox`), then
delivers ThreadWeave or `tepp_client` (ADR 0023). A crash after
Start leaves the work item; refresh finishes it. Period-report
stays 422. No TEPP theta is invented.

## [0.93.0] - 2026-08-17

### Added

- `make seed` now persists the designed A-100 fork on the Demo Corp
Succeeded lineage run. Open that run: the revised quote and delivery
question follow the pricing follow-up and are buttons. Start is
unchanged. No TEPP theta is invented.

## [0.92.0] - 2026-08-17

### Added
Expand Down
11 changes: 6 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ after cutoff were rewritten after the run; compare those bodies
before treating them as reconstructed evidence (ADR 0016).
`POST /api/analysis-runs` records Pending on an authorized
cutoff capture (ADR 0017). `POST /api/analysis-runs/{id}/start`
reconstructs that frozen cutoff bag (ADR 0021) or submits TEPP
through `tepp_client` (ADR 0022). A missing transport or unused
accepted envelope is Failed. Failed TEPP is terminal — request a
new run, then start. Do not invent a theta. Hover the Result
prefix to read the parent-choice digest.
commits Running plus a durable outbox row, then reconstructs that
frozen cutoff bag (ADR 0021 / ADR 0023) or submits TEPP through
`tepp_client` (ADR 0022). A missing transport or unused accepted
envelope is Failed. Failed TEPP is terminal — request a new run,
then start. Do not invent a theta. Hover the Result prefix to read
the parent-choice digest.
6 changes: 3 additions & 3 deletions backend/app/analysis_run_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

``create_pending_analysis_run`` (ADR 0017) writes snapshot, counts, frozen
membership, run, scope, and the first Pending event atomically.
``start_pending_analysis_run`` later reconstructs lineage (ADR 0021)
or submits TEPP through ``tepp_client`` (ADR 0022). Neither path
invents a TEPP score.
``enqueue_pending_analysis_run`` then ``deliver_queued_analysis_run``
later reconstruct lineage (ADR 0021 / ADR 0023) or submit TEPP through
``tepp_client`` (ADR 0022). Neither path invents a TEPP score.
"""

from __future__ import annotations
Expand Down
95 changes: 95 additions & 0 deletions backend/app/analysis_run_outbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Durable start-work outbox. PostgreSQL is truth; Valkey is the wake-up.

ADR 0023. Start writes Running plus one immutable outbox row, then a
worker claims that row and runs ThreadWeave or ``tepp_client``. A crash
after enqueue leaves the work item; it does not invent a theta.
"""

from __future__ import annotations

import hashlib
import json
from datetime import datetime, timezone
from typing import Any

import redis.asyncio as redis

OUTBOX_STREAM_KEY = "analysis-run-outbox"
_CLAIMED = "analysis_outbox_claimed"
_DELIVERED = "analysis_outbox_delivered"


def outbox_request_digest(
*,
analysis_run_id: str,
work_kind_code: str,
snapshot_sha256: str,
knowledge_cutoff: datetime,
) -> str:
"""SHA-256 of the frozen start request. Never hashes a post body."""
cutoff = knowledge_cutoff
if cutoff.tzinfo is None:
cutoff = cutoff.replace(tzinfo=timezone.utc)
material = json.dumps(
{
"analysis_run_id": str(analysis_run_id),
"knowledge_cutoff": cutoff.astimezone(timezone.utc).strftime(
"%Y-%m-%dT%H:%M:%SZ"
),
"snapshot_sha256": snapshot_sha256,
"work_kind_code": work_kind_code,
},
separators=(",", ":"),
sort_keys=True,
)
return hashlib.sha256(material.encode()).hexdigest()


def outbox_stream_fields(
*,
analysis_run_id: str,
work_kind_code: str,
request_sha256: str,
) -> dict[str, str]:
"""Valkey XADD fields for one start-work wake-up. No body, no theta."""
return {
"analysis_run_id": str(analysis_run_id),
"request_sha256": request_sha256,
"work_kind_code": work_kind_code,
}


async def publish_outbox_event(
client: redis.Redis | None,
*,
analysis_run_id: str,
work_kind_code: str,
request_sha256: str,
) -> str | None:
"""``XADD`` the wake-up. A missing Valkey leaves PostgreSQL durable."""
if client is None:
return None
try:
entry_id = await client.xadd(
OUTBOX_STREAM_KEY,
outbox_stream_fields(
analysis_run_id=analysis_run_id,
work_kind_code=work_kind_code,
request_sha256=request_sha256,
),
maxlen=1000,
approximate=True,
)
except redis.RedisError:
return None
return str(entry_id)


def latest_outbox_delivery_is_delivered(status_code: str | None) -> bool:
"""True when the newest delivery event already finished the work."""
return status_code == _DELIVERED


def latest_outbox_delivery_is_claimed(status_code: str | None) -> bool:
"""True when a worker already claimed the row and may retry."""
return status_code == _CLAIMED
Loading