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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-20
23 changes: 23 additions & 0 deletions openspec/changes/direct-operation-receipt-lookup/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Context

Operation creation writes `memory_operation:<sha256(operation_id)>`, while `get` and `get_db` issue `SELECT ... WHERE operation_id = $id LIMIT 1`. The uncomfortable fact is that the unique secondary index did not keep deployed receipt reads within the worker's ten-second request budget under heavy memory pressure; one lookup took 10.64 seconds before restart and 3.21 seconds after warmup.

## Goals

- Make receipt retrieval address the record that submission already created.
- Preserve receipt conversion, missing-record behavior, replay semantics, and conflicts.
- Avoid schema or migration changes.

## Decision

Use the Surreal client record selection API with table `memory_operation` and the existing `record_key(operation_id)` value in both `get` and `get_db`. The record key is SHA-256 hex, so it is deterministic and safe as a Surreal record identifier. The stored `operation_id` and its unique index remain available for validation and older queries.

## Risks

Rows created outside the operation service under a noncanonical record key will no longer be discoverable through the API. Such a row violates the service's existing creation contract; migrations and the operation submit path already use canonical keys.

## Verification

- Run the operations unit and integration tests, including duplicate submission, conflict, restart, and receipt lookup cases.
- Measure the deployed GET endpoint before and after installing the rebuilt server.
- Run the learning worker against the durable backlog and record delivered counts and timeouts.
23 changes: 23 additions & 0 deletions openspec/changes/direct-operation-receipt-lookup/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Why

Operation receipts are stored under a deterministic record key derived from `operation_id`, but receipt reads filter the whole table by a duplicate field. On the deployed RocksDB service, a single lookup took 3–10 seconds and caused the durable learning worker to time out after only a few records.

## What Changes

- Read operation receipts and internal operation state by their deterministic record identity.
- Retain the existing `operation_id` field and unique index for contract validation and compatibility.
- Verify submit, replay, conflict, and receipt-read behavior through the operation integration suite.

## Capabilities

### New Capabilities

- `operation-receipt-lookup`: Deterministic receipt retrieval by the durable operation identity.

### Modified Capabilities

None.

## Impact

This changes two read paths in `src/operations.rs`. The HTTP schema, stored record format, migrations, and operation state machine remain unchanged.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Purpose

Ensure durable operation receipts can be retrieved by their stable identity without work proportional to unrelated ledger rows.

## ADDED Requirements

### Requirement: Receipt lookup uses durable operation identity
The ledger SHALL retrieve an operation by the deterministic record identity derived from the requested `operation_id`, and SHALL return only a record whose durable identity corresponds to that request.

#### Scenario: Existing operation is requested
- **WHEN** a caller requests a receipt for an `operation_id` that was durably accepted
- **THEN** the ledger returns that operation's current receipt without filtering unrelated operation rows

#### Scenario: Unknown operation is requested
- **WHEN** a caller requests an `operation_id` whose deterministic record does not exist
- **THEN** the ledger returns the existing not-found response

### Requirement: Submit replay uses the same identity lookup
The ledger SHALL use the same deterministic receipt lookup after submission and when resolving a concurrent or replayed operation ID.

#### Scenario: Existing ID is submitted again
- **WHEN** a caller submits an existing `operation_id` with the same payload hash
- **THEN** the ledger returns the authoritative existing receipt without creating a second operation

#### Scenario: Existing ID has another payload hash
- **WHEN** a caller submits an existing `operation_id` with a different payload hash
- **THEN** the ledger returns the existing conflict response
8 changes: 8 additions & 0 deletions openspec/changes/direct-operation-receipt-lookup/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## 1. Deterministic lookup

- [x] 1.1 Replace field-filtered receipt reads with deterministic record selection in both public and internal operation reads; verify the crate compiles

## 2. Verification

- [x] 2.1 Run the operations test targets and strict OpenSpec validation
- [ ] 2.2 Install the rebuilt server and measure receipt lookup plus learning-worker backlog progress
21 changes: 7 additions & 14 deletions src/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,24 +420,17 @@ impl OperationService {

pub async fn get(&self, operation_id: &str) -> Result<Option<OperationReceipt>> {
let db = self.surreal()?.db()?;
let rows: Vec<DbOperation> = db
.query("SELECT * FROM memory_operation WHERE operation_id = $id LIMIT 1")
.bind(("id", operation_id.to_owned()))
.await?
.check()?
.take(0)?;
rows.into_iter().next().map(TryInto::try_into).transpose()
let operation: Option<DbOperation> = db
.select(("memory_operation", record_key(operation_id)))
.await?;
operation.map(TryInto::try_into).transpose()
}

async fn get_db(&self, operation_id: &str) -> Result<Option<DbOperation>> {
let db = self.surreal()?.db()?;
let rows: Vec<DbOperation> = db
.query("SELECT * FROM memory_operation WHERE operation_id = $id LIMIT 1")
.bind(("id", operation_id.to_owned()))
.await?
.check()?
.take(0)?;
Ok(rows.into_iter().next())
Ok(db
.select(("memory_operation", record_key(operation_id)))
.await?)
}

async fn list_nonterminal_ids(&self) -> Result<Vec<String>> {
Expand Down
Loading