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
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,41 @@
All notable changes to TruthLease are documented here. The project follows
[Semantic Versioning](https://semver.org/) once its public contract stabilizes.

## 0.3.0-alpha.1 - 2026-07-22

Stale-safe durable resume vertical slice:

- optional LangGraph `1.2.x` integration with persistent approval, authoritative
revalidation, fenced execution, and explicit ambiguous-outcome recovery;
- digest-minimized checkpoint state containing references, bindings, durable
IDs, phases, and stable decision codes instead of raw effects, target tokens,
evidence payloads, or exception messages;
- SQLite schema v3 `lease_requests` binding for restart-safe, idempotent lease
acquisition across workflow-node replay;
- exact lease-intent conflict detection across artifact, effect, target,
operation, assurance, microsecond TTL, boot epoch, and target precondition;
- recovery of the original lease after consumption or invalidation so an
existing target attempt is resolved rather than replaced;
- public read-only lease and lease-to-attempt accessors for framework adapters;
- real `SqliteSaver` process-restart tests covering policy drift, target-token
drift, duplicate dispatch, response loss after commit, incomplete dependency
capture, reference drift, strict approval types, and raw-value redaction;
- explicit recovery approval bound to the current durable attempt status and
result digest, including pre-checkpoint crash tests;
- conservative same-attempt recovery when local persistence fails after a
possible target commit, while an already persisted receipt returns without a
redundant state read;
- durable terminal truth taking precedence over stale recovery responses and
payload resolvers after an attempt is resolved by another worker;
- migration tests for populated v1 and v2 stores with no synthetic request
backfill;
- architecture decision record, integration guide, updated threat model, and
current competitive boundary.

The alpha does not turn TruthLease into a workflow engine, authorize arbitrary
LangGraph state, encrypt checkpoints, discover undeclared dependencies, or make
external effects exactly once without a cooperating deduplicating target.

## 0.2.0-alpha.1 - 2026-07-20

Target-fencing vertical slice:
Expand Down
95 changes: 89 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ effect.**
- **Why it matters:** durable execution can resume an interrupted workflow, but
resuming reliably is different from checking that the saved decision is still
valid.
- **What the LangGraph integration adds:** a paused effect can survive a process
restart, re-read its authoritative dependencies, and either reuse one durable
lease and target attempt or stop with a stable reconciliation code. The
checkpoint stores references and digests, not the raw effect or target token.
- **Concrete example:** a release plan uses policy `v12` and test report `r7`.
Policy `v13` arrives while the workflow is paused. TruthLease blocks the old
plan, preserves test report `r7`, and identifies only the work that now needs
Expand All @@ -45,6 +49,7 @@ about what is reusable, stale, or still uncertain.
| Short-lived, single-use leases | A stale or substituted effect is rejected at the local gate. |
| Bound target preconditions and stable attempt IDs | A cooperating endpoint can reject stale state and deduplicate an ambiguous retry. |
| Post-commit fenced receipts | Operators can distinguish a local permit from a target-confirmed effect. |
| Optional LangGraph stale-safe resume | A persisted approval resumes only after authoritative revalidation; crash recovery reuses the same lease and target attempt. |
| Explicit assurance and completeness | Operators can see what the runtime actually knows instead of trusting a vague safety claim. |
| Lifecycle events and SQLite integrity checks | Accepted lifecycle changes are inspectable and the local store fails closed. Denials are returned decisions, not guaranteed journal entries. |

Expand All @@ -57,7 +62,7 @@ about what is reusable, stale, or still uncertain.
## Quick start

TruthLease requires Python 3.11 or newer and has no third-party runtime
dependencies.
dependencies in its core. The LangGraph adapter is an optional extra.

```bash
git clone https://github.com/aantenore/truthlease.git
Expand All @@ -66,6 +71,12 @@ uv sync --extra dev
uv run truthlease demo --reset
```

For persistent LangGraph approval and resume:

```bash
uv sync --extra langgraph
```

The demo constructs this graph:

```mermaid
Expand All @@ -85,6 +96,75 @@ It then proves that:
- an identical change event is idempotent;
- a new plan revision can acquire and consume a new lease.

## LangGraph: resume the workflow, re-check the decision

LangGraph checkpoints preserve workflow position; TruthLease separately checks
whether the saved action is still eligible at the durable-effect boundary. The
integration composes the two without making LangGraph a core dependency:

```mermaid
flowchart LR
Pause["Approval interrupt"] --> Checkpoint["LangGraph checkpoint"]
Checkpoint --> Resume["Process restart + resume"]
Resume --> Revalidate["Authoritative re-read"]
Revalidate -->|changed| Stop["Reconcile; no target effect"]
Revalidate -->|still current| Lease["Idempotent fenced lease"]
Lease --> Target["One durable target attempt"]
Target -->|response lost| Recover["Recovery interrupt"]
Recover --> Target
Target --> Receipt["Fenced receipt"]
```

The public surface is deliberately callback-driven: source revalidation, effect
resolution, target-precondition resolution, evidence production, target adapter,
and checkpointer are all replaceable.

```python
import sqlite3

from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.types import Command
from truthlease.integrations.langgraph import (
build_stale_safe_graph,
make_resume_state,
workflow_idempotency_key,
)

# `runtime`, `plan`, `effect`, `precondition`, `endpoint`, `adapter`, and the
# callback functions below are application-owned objects.
workflow_key = workflow_idempotency_key({"workflow": "release-2026.07"})
state = make_resume_state(
idempotency_key=workflow_key,
artifact=plan.ref,
effect_ref="effects/release-2026.07",
effect=effect,
precondition_ref="preconditions/deployment/revision-7",
target_precondition=precondition,
target=endpoint,
operation="promote",
)
checkpointer = SqliteSaver(sqlite3.connect("workflow.db", check_same_thread=False))
graph = build_stale_safe_graph(
runtime,
checkpointer=checkpointer,
revalidate=revalidate_sources,
resolve_effect=resolve_effect,
resolve_precondition=resolve_precondition,
evidence_provider=provide_evidence,
target_adapter=adapter,
)
config = {"configurable": {"thread_id": workflow_key}}
paused = graph.invoke(state, config=config)
resumed = graph.invoke(Command(resume=True), config=config)
```

The snippet shows the composition contract; the callbacks and fenced target must
be supplied by the host application. Use immutable, versioned references and
return the exact original effect and precondition during ambiguous-attempt
recovery. Node names and checkpoint fields are versioned persistence contracts.
See the complete [LangGraph integration guide](docs/langgraph-integration.md) and
[ADR 0003](docs/adr/0003-langgraph-stale-safe-resume.md).

## Python API

```python
Expand Down Expand Up @@ -297,6 +377,7 @@ the invalidation closure.

Read the
[architecture](https://github.com/aantenore/truthlease/blob/main/docs/architecture.md),
[LangGraph stale-safe resume guide](https://github.com/aantenore/truthlease/blob/main/docs/langgraph-integration.md),
[target-fencing contract](https://github.com/aantenore/truthlease/blob/main/docs/spec/coherence-v2.md),
[base coherence contract](https://github.com/aantenore/truthlease/blob/main/docs/spec/coherence-v1.md),
[threat model](https://github.com/aantenore/truthlease/blob/main/docs/threat-model.md), and
Expand All @@ -322,10 +403,12 @@ These public projects are complementary rather than one growing framework:
- distributed transactions, universal rollback, or exactly-once external effects;
- an authentication, authorization, or policy engine.

Use Restate, Temporal, or LangGraph for durable execution; Drasi or Debezium for
high-volume change capture; a policy engine for authorization; and target-side
idempotency plus fencing for durable effects. TruthLease connects their version
evidence to the artifacts that an agent may later use.
Use Restate, Temporal, or LangGraph for general durable execution; Drasi or
Debezium for high-volume change capture; a policy engine for authorization; and
target-side idempotency plus fencing for durable effects. TruthLease connects
their version evidence to the artifacts that an agent may later use. Its
LangGraph adapter is a narrow executable composition for one approval-to-effect
boundary, not a replacement workflow engine.

## Why a separate project

Expand Down Expand Up @@ -362,7 +445,7 @@ uv run ruff check .
uv run mypy
uv run pytest --cov=truthlease --cov-report=term-missing
uv run python benchmarks/benchmark_fence_correctness.py
uv build
uv build --no-sources
```

This is an executable alpha. See
Expand Down
147 changes: 147 additions & 0 deletions docs/adr/0003-langgraph-stale-safe-resume.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# ADR 0003: LangGraph stale-safe resume

- Status: accepted
- Date: 2026-07-22
- Release: `0.3.0-alpha.1`

## Context

LangGraph can checkpoint an interrupted workflow and restart its node later.
That durability does not imply that the policy, record, schema, approval, or
target token behind a saved action is still current. Its interrupt contract also
means the node restarts from the beginning, so side effects around interruption
must be idempotent.

TruthLease `0.2` could fence a target commit and reuse a prepared attempt, but a
workflow crash immediately after lease acquisition could rerun the node and
acquire a new lease before the lease ID itself reached the workflow checkpoint.
The framework integration therefore needs a durable identity below the graph
checkpoint boundary.

## Decision

Add two independent, composable pieces:

1. SQLite schema v3 stores a digest-only `lease_requests` binding from one
application idempotency key to one lease-intent digest and one lease ID.
2. An optional LangGraph graph composes approval, authoritative revalidation,
fenced lease acquisition, target execution, and same-attempt recovery.

The core remains free of mandatory third-party runtime dependencies. LangGraph
and its SQLite saver are installed through the `langgraph` extra.

## Lease acquisition identity

`TruthLease.acquire_lease()` accepts an optional algorithm-prefixed
`idempotency_key`. The stored intent digest binds the artifact reference, effect,
target, operation, assurance, TTL at microsecond precision, boot epoch, and
target-precondition descriptor.

- First use performs the normal freshness checks and atomically stores the lease
and request binding.
- Identical reuse returns the exact original lease, including terminal state.
- Reuse for different intent fails closed with `ConflictError`.
- No raw effect, target token, or workflow label is stored in the binding.

Returning a consumed lease is deliberate: its durable attempt is the only safe
identity for resolving an ambiguous target outcome.

## Graph structure

The graph has four versioned nodes:

```text
START -> approval_v1 -> authorize_v1 -> execute_fenced_v1
-> recover_indeterminate_v1
-> execute_fenced_v1
```

- `approval_v1` has no side effect before `interrupt()`.
- `authorize_v1` re-reads authoritative sources, verifies resolved digests, and
acquires the idempotent fenced lease. It performs no target effect.
- `execute_fenced_v1` resolves again, prepares one durable attempt, and invokes
the target outside the database transaction.
- `recover_indeterminate_v1` requires an explicit boolean before retrying that
same attempt.

Node names and state fields are a persistence contract. Incompatible evolution
requires versioned names and a checkpoint migration strategy.

## Checkpoint minimization

The graph state contains only references, digests, IDs, phase, approval, and
stable decision codes. Recovery approval is bound to a digest of the current
attempt status and result. Raw effects, opaque target tokens, evidence payloads,
and exception messages stay behind replaceable callbacks.

This is data minimization, not encryption or tamper resistance. The host and
checkpoint store remain trusted, references must not contain secrets, and
low-entropy identifiers need an application-owned keyed digest if guessing is a
concern.

## Crash boundaries

| Crash point | Recovery behavior |
| --- | --- |
| Before approval checkpoint | No lease and no target effect exist. |
| During authoritative read | The node reruns; source adapters must use their normal compare-and-swap discipline. |
| After lease acquisition but before graph checkpoint | The node reacquires by the same idempotency key and receives the original lease. |
| After attempt prepare but before target call | A prepared attempt is conservatively treated as potentially dispatched and requires recovery approval before the same request can be sent. |
| After target commit but before local result record | The prepared attempt is treated as potentially dispatched and requires recovery approval; the target then resolves the same attempt ID and request fingerprint. |
| After local receipt but before graph checkpoint | Re-execution returns the terminal receipt without redispatch. |

Recovery approval remains valid while resolving one unchanged durable attempt
basis, including across a crash of that approved node. If status or result digest
changes, the basis changes and the old approval is not reused. This authorizes
resolution of one logical attempt, not an unbounded sequence of new effect
identities; target-side deduplication remains required for repeated transport.
If the attempt becomes terminal while the recovery interrupt is paused, that
durable outcome takes precedence over the stale human response and is surfaced
without another effect/precondition resolution or target dispatch.

## Alternatives considered

### Store the full effect in LangGraph state

Rejected. It couples checkpoint retention to application payload sensitivity and
makes accidental prompt, credential, or target-token persistence likely.

### Trust the saved checkpoint without authoritative revalidation

Rejected. Durable position and current authority are different properties.

### Acquire a fresh lease on every node retry

Rejected. A crash between target commit and checkpoint could create a second
attempt and duplicate the durable effect.

### Put LangGraph in the core dependency set

Rejected. TruthLease is framework-neutral and its deterministic core remains
usable in small services and other workflow engines.

### Build a workflow engine in TruthLease

Rejected. Scheduling, general human-task management, compensation, and arbitrary
graph execution remain outside this project's boundary.

## Consequences

Positive:

- a real durable-orchestrator integration now exercises the commit-time contract;
- pause/restart and ambiguous-response recovery preserve one lease and attempt;
- framework checkpoints avoid raw effects and target tokens;
- callbacks and checkpointers remain replaceable.

Costs and limitations:

- callers must keep immutable effect and precondition references resolvable for
the full recovery horizon;
- TTL, boot epoch, and every other intent component must remain identical during
recovery;
- target deduplication retention must outlive that recovery horizon;
- SQLite schema version advances from 2 to 3;
- paused state version 1 and node names now require compatibility discipline;
- the initial adapter is synchronous and covers one fenced effect, not a general
multi-target workflow.
Loading
Loading