1. How Session Routing Works

  Command arrives at Session::execute()

  ├─ Transaction lifecycle?
  │   TxnBegin / TxnCommit / TxnRollback / TxnInfo / TxnIsActive
  │   └─ Dedicated handler (handle_begin, handle_commit, etc.)

  ├─ Explicitly non-transactional? (24 commands)
  │   Branch, Vector, Database, Retention, version history
  │   └─ Always → executor.execute(cmd)
  │      Regardless of whether a transaction is active.

  └─ Everything else (18 commands)
      ├─ Transaction active?
      │   └─ YES → dispatch_in_txn(cmd)
      │           ├─ 13 commands: handled via TransactionContext
      │           └─ 5 commands: catch-all → executor.execute(cmd)

      └─ NO → executor.execute(cmd)

Location: crates/executor/src/session.rs:70-118

2. Complete Command Routing Table

Transaction lifecycle (5) — dedicated handlers

CommandHandlerNotes
TxnBeginhandle_begin()Creates TransactionContext, stores in session
TxnCommithandle_commit()Validates read-set, WAL, apply writes
TxnRollbackhandle_abort()Returns context to pool without applying
TxnInfohandle_txn_info()Returns txn_id and status
TxnIsActiveInlineReturns Bool(in_transaction())

Handled inside transaction (13) — via TransactionContext

These commands use the transaction’s write-set and snapshot for read-your-writes semantics.

CommandMechanismRead-your-writes?
KvGetctx.get() → write_set → delete_set → snapshotYes
KvListctx.scan_prefix() → merged viewYes
KvPutTransaction::kv_put() → ctx.put()Yes
KvDeletectx.exists() + ctx.delete()Yes
StateGetctx.get() + JSON deserializeYes
StateInitTransaction::state_init()Yes
StateCasTransaction::state_cas()Yes
JsonGetRoot: ctx.get() + JSON deserialize. Path: Transaction::json_get_path()Yes
JsonSetTransaction::json_set()Yes
JsonDeleteTransaction::json_delete()Yes
EventAppendTransaction::event_append() (hash chaining)Yes
EventGetTransaction::event_get()Yes
EventLenTransaction::event_len()Yes

Location: crates/executor/src/session.rs:214-361

Explicitly routed to executor (24) — bypass transaction silently

These commands go to executor.execute() regardless of whether a transaction is active. No error or warning is returned.

CommandHas side effects?Sees uncommitted writes?
BranchCreateWRITE — creates branchNo
BranchGetReadNo
BranchListReadNo
BranchExistsReadNo
BranchDeleteWRITE — deletes branch + dataNo
VectorUpsertWRITE — inserts/updates embeddingNo
VectorGetReadNo
VectorDeleteWRITE — deletes embeddingNo
VectorSearchReadNo
VectorCreateCollectionWRITE — creates collectionNo
VectorDeleteCollectionWRITE — deletes collection + dataNo
VectorListCollectionsReadNo
PingNoneN/A
InfoRead (metadata)N/A
FlushNone (TODO)N/A
CompactNone (TODO)N/A
RetentionApplyReturns error (“not yet implemented”)N/A
RetentionStatsReturns error (“not yet implemented”)N/A
RetentionPreviewReturns error (“not yet implemented”)N/A
KvGetvRead (version history)No
StateGetvRead (version history)No
JsonGetvRead (version history)No
JsonListRead (document listing)No
EventGetByTypeRead (type-filtered events)No

Location: crates/executor/src/session.rs:81-108

Catch-all in dispatch_in_txn (5) — escape to executor during active transaction

These commands are NOT in the explicit outer routing list, so they reach dispatch_in_txn when a transaction is active. But they have no explicit handler there, so the catch-all at line 365 sends them to executor.execute().

CommandWhat executor doesProblem
StateSetWrites state via implicit single-op transactionWRITE bypasses session transaction
SearchRuns HybridSearch across all primitivesRead only — doesn’t see uncommitted writes
BranchExportExports branch to fileFile I/O — safe
BranchImportImports branch from fileWRITE bypasses session transaction
BranchBundleValidateValidates bundle fileFile I/O only — safe

Location: crates/executor/src/session.rs:363-365

3. Problems Found

Problem 1: StateSet bypasses transaction scope

Severity: High Existing issue: #837

TxnBegin
  KvPut("key", 1)        ← buffered in transaction
  StateSet("cell", "x")  ← IMMEDIATELY committed to storage
TxnRollback
  "key" → rolled back     ✓
  "cell" → still "x"      ✗ NOT rolled back

StateSet is the only write command for a transactional primitive (State) that bypasses the session transaction. StateInit and StateCas are handled correctly inside the transaction. StateSet falls through the catch-all to executor.execute(), which creates its own implicit single-operation transaction.

Root cause: dispatch_in_txn has explicit handlers for StateInit and StateCas but not StateSet. The comment at line 363 says “includes batch operations, history, CAS, scan, incr, etc.” suggesting StateSet was overlooked.

Problem 2: Vector writes silently bypass transaction — no error

Severity: Medium

TxnBegin
  KvPut("key", 1)                    ← buffered in transaction
  VectorUpsert("vec", [0.1, 0.2])    ← IMMEDIATELY committed
TxnRollback
  "key" → rolled back     ✓
  "vec" → still exists     ✗ NOT rolled back

All 7 vector commands are explicitly routed to executor at the outer dispatch level (session.rs:87-93). The user receives a success response with no indication that the operation is outside the transaction scope.

Vector operations are non-transactional by design (the vector backend cannot participate in transactions — see issue #937). But the session should either:

  • Return an error when vector writes are attempted inside a transaction, OR
  • Document the behavior explicitly in the response

Problem 3: Branch writes silently bypass transaction — no error

Severity: Medium

TxnBegin
  KvPut("key", 1)         ← buffered in transaction
  BranchCreate("new")     ← IMMEDIATELY committed
TxnRollback
  "key" → rolled back     ✓
  "new" branch → exists   ✗ NOT rolled back

BranchCreate and BranchDelete are write operations that are explicitly routed to executor (session.rs:82-86). Like vectors, branches are not currently transactional, but the user has no way to know this.

Problem 4: Read commands inside transaction don’t see uncommitted writes

Severity: Medium

TxnBegin
  JsonSet("doc", "$", {"a": 1})    ← buffered in transaction
  JsonList()                        ← reads from committed store
                                      "doc" NOT in results
  EventAppend("type1", payload)    ← buffered in transaction
  EventGetByType("type1")         ← reads from committed store
                                      event NOT in results
TxnCommit

Five read commands for transactional primitives bypass the transaction context:

CommandTransactional equivalent exists?Why bypassed
JsonListNo ctx.scan_prefix equivalent for JSONNo JSON-specific list in TransactionContext
EventGetByTypeNo type-filtered read in TransactionContextRequires scan across all events
KvGetvNo version history in TransactionContextRequires storage-layer version chains
StateGetvNo version history in TransactionContextRequires storage-layer version chains
JsonGetvNo version history in TransactionContextRequires storage-layer version chains

The version history commands (Getv) are inherently non-transactional — they read the committed version chain, which is a reasonable design. But JsonList and EventGetByType are regular read commands whose transactional counterparts (JsonGet, EventGet) DO use the transaction context. The inconsistency is confusing.

Problem 5: BranchImport bypasses transaction via catch-all

Severity: Low

BranchImport falls through dispatch_in_txn’s catch-all to executor.execute(). Unlike the other branch commands (which are explicitly routed at the outer level), Import reaches the catch-all accidentally — it was not listed in the explicit non-transactional block at lines 82-86.

This works correctly (the executor handles it fine), but the routing is inconsistent. BranchExport and BranchBundleValidate have the same issue.

4. Routing Consistency Matrix

Write operations

PrimitiveWrite commandsIn dispatch_in_txn?Consistent?
KVKvPut, KvDeleteYes, YesYes
StateStateInit, StateCas, StateSetYes, Yes, NoNo — StateSet escapes
JSONJsonSet, JsonDeleteYes, YesYes
EventEventAppendYesYes
VectorVectorUpsert, VectorDelete, Create/DeleteCollectionNo (all 4)Yes (consistently non-transactional)
BranchBranchCreate, BranchDelete, BranchImportNo (all 3)Yes (consistently non-transactional)

Read operations

PrimitiveRead commandsIn dispatch_in_txn?Consistent?
KVKvGet, KvList, KvGetvYes, Yes, NoPartial — Getv intentionally excluded
StateStateGet, StateGetvYes, NoPartial — Readv intentionally excluded
JSONJsonGet, JsonList, JsonDelete, JsonGetvYes, No, Yes, NoNo — JsonList should be transactional
EventEventGet, EventLen, EventGetByTypeYes, Yes, NoNo — ReadByType should be transactional
VectorVectorGet, VectorSearch, ListCollectionsNo (all 3)Yes (consistently non-transactional)
BranchBranchGet, BranchList, BranchExistsNo (all 3)Yes (consistently non-transactional)

5. TransactionContext Capability Gaps

The Transaction wrapper (engine’s TransactionOps trait) supports these operations:

OperationSupportedUsed by dispatch_in_txn
kv_getYesYes (via ctx.get)
kv_putYesYes
kv_deleteYesYes (via ctx.delete)
kv_existsYesYes (via ctx.exists)
kv_listYesYes (via ctx.scan_prefix)
event_appendYesYes
event_getYesYes
event_rangeYesNot used
event_lenYesYes
state_getYesYes (via ctx.get)
state_initYesYes
state_casYesYes
json_createYesNot used directly
json_getYesYes
json_get_pathYesYes
json_setYesYes
json_deleteYesYes
json_existsYesNot used directly
json_destroyYesNot used directly
vector_insertStub — returns errorNo
vector_getStub — returns errorNo
vector_deleteStub — returns errorNo
vector_searchStub — returns errorNo
vector_existsStub — returns errorNo
branch_metadataStub — returns errorNo
branch_update_statusStub — returns errorNo

Location: crates/engine/src/transaction/context.rs:158-662

Notable gaps:

  • No state_set (unconditional write) — only init and CAS
  • No json_list (document enumeration)
  • No event_get_by_type (type-filtered scan)
  • No version history methods (getv) — by design, these read committed chains

6. Summary

#ProblemSeverityType
1StateSet bypasses transaction scope (issue #837)HighWrite escapes transaction
2Vector writes silently bypass transaction — no errorMediumSilent non-transactional writes
3Branch writes silently bypass transaction — no errorMediumSilent non-transactional writes
4JsonList doesn’t see uncommitted JSON documents in transactionMediumInconsistent read visibility
5EventGetByType doesn’t see uncommitted events in transactionMediumInconsistent read visibility
6BranchExport/Import/Validate route inconsistently (catch-all vs explicit)LowInconsistent routing