One history table outlives its subject and the other does not

Task Orchestrator

An MCP work-item server that verifies an agent's actor claim against a did:web JWKS before admitting a note, records the verdict on the row, and never consults it again on any read.

Carries 1 of 7 rubric mechanisms. Most systems here carry none or one (44%), and a dash means the mechanism was not found at this commit — not that the system needed it. Each mark is one LLM reviewer's reading of the code at this commit rather than a run of it — known limits.

  • Tombstone
  • Trust state
  • Bi-temporal
  • Scope enforced
  • Mutation audit
  • Human review
  • Negative evals

1. Executive Summary

Task Orchestrator is an MCP server giving agents a persistent work-item graph — items, notes, dependencies, role transitions — with the rules enforced at the tool boundary rather than in a prompt. Its own framing is the clearest summary: "Prompt-based frameworks hope the LLM follows instructions. This one blocks the call if it doesn't." MIT, Kotlin, 523 files.

It earns a place here because its notes are durable agent-authored content, full-text indexed and read back across sessions, and because of what it does to them on the way in.

One mark: negative_eval. Three mechanisms that look like more are worth reading for exactly where they stop.

An actor claim is cryptographically verified. ActorParsing.kt:96 calls context.actorVerifier().verify(claim), and the verifier resolves a did:web document, extracts its JWKS and checks the proof. The result is a five-value VerificationStatusABSENT, UNCHECKED, VERIFIED, REJECTED, UNAVAILABLE — stored on the note and on the transition.

It gates the write and never the read. The ladder at ActorParsing.kt:112-161 distinguishes a fresh verification from a stale-cache one from a JWKS fetch that failed, and refuses the call when the status is not VERIFIED. Nothing filters a query on it afterwards: a note recorded REJECTED comes back like any other.

And one audit table outlives its subject while the other does not. ResourceLeaseHistoryTable deliberately carries no foreign key so it survives ON DELETE CASCADE and deletion of the holder itself. RoleTransitionsTable, which records the state changes of the work items, is removed with them by deleteByItemId.

2. Mental Model

Work items hold notes; notes are what an agent writes down and a later agent reads. Every transition between roles is recorded with who caused it and what the server made of their identity. The gates — required notes, dependency ordering, actor attribution — live in the tool handlers, so an agent that does not comply gets an error rather than a silently accepted call.

3. Architecture

Diagram — an agent writing a note or advancing an item supplies an actor claim which the server resolves through a did:web document to a JWKS and verifies, refusing the call unless the status is verified while distinguishing a stale cache from a failed fetch; the verdict is stored on the note and on the role transition and no query filters on it afterwards, and of the two history tables the lease history deliberately carries no foreign key so it survives deletion of the item it describes while the transitions log is cascaded away with that item
Diagram source
%% caption: an agent writing a note or advancing an item supplies an actor claim which the server resolves through a did:web document to a JWKS and verifies, refusing the call unless the status is verified while distinguishing a stale cache from a failed fetch; the verdict is stored on the note and on the role transition and no query filters on it afterwards, and of the two history tables the lease history deliberately carries no foreign key so it survives deletion of the item it describes while the transitions log is cascaded away with that item
flowchart TD
    AG(("agent")) --> CALL["manage_notes(upsert) · advance_item · claim_item"]
    CALL --> AC["ActorParsing: actorVerifier().verify(claim)"]
    AC --> DID["did:web resolver → DID document"]
    DID --> JWKS["JWKS extractor"]
    JWKS --> VS{"VerificationStatus"}
    VS --> V1["VERIFIED — fresh, or stale cache"]
    VS --> V2["UNAVAILABLE + verifiedFromCache"]
    VS --> V3["UNAVAILABLE — JWKS fetch failed"]
    VS --> V4["REJECTED / UNCHECKED / ABSENT"]
    V4 -->|"not VERIFIED"| ERR["the call is refused"]
    V1 --> ADMIT
    V2 --> ADMIT
    ADMIT["write admitted"]

    ADMIT --> N[("notes<br/>body · actorId · actorKind · actorParent<br/>actorProof · verificationStatus · verificationVerifier")]
    ADMIT --> RT[("role_transitions<br/>from/to role · status labels · trigger<br/>summary · actor · verification")]
    N --> FTS["FTS5 index"]
    FTS --> READ["later sessions read the notes"]
    READ -.->|"no query filters on verificationStatus"| N
    READ --> AG

    RT -.->|"deleteByItemId — cascaded away<br/>with the work item"| GONE1["history gone"]
    LH[("resource_lease_history<br/>NO foreign key, deliberately")] -.->|"survives ON DELETE CASCADE<br/>and deletion of the holder"| KEPT["history kept"]

    GATES["server-enforced gates:<br/>required notes · dependency ordering<br/>actor attribution"] --> CALL

4. Essential Implementation Paths

The verification ladderActorParsing.kt:112-161. Three documented outcomes in order: VERIFIED, whether fresh or from a stale cache; UNAVAILABLE carrying metadata["verifiedFromCache"] == "true"; and UNAVAILABLE without that flag, meaning the JWKS could not be fetched at all. :143 computes isVerified = verification.status == VerificationStatus.VERIFIED and :129 records that anything else "the caller must surface... as an" error. Distinguishing "I verified this earlier and cannot re-check now" from "I have never been able to check" is the distinction most implementations skip.

Where the verdict landsSQLiteNoteRepository.kt:103-105 and :119-121, writing actorProof, verificationStatus and verificationVerifier onto the row, and SQLiteRoleTransitionRepository.kt:51 doing the same for a transition.

Where it does not — nothing. A search of the main source for verificationStatus outside the schema, the repositories and the parsing ladder returns only ActorParsing's own admission decision. No read path narrows on it.

The two historiesResourceLeaseHistoryTable.kt:12-15: "Deliberately carries NO foreign key on holderItemId to WorkItemsTable... this table is an audit trail that must survive both ON DELETE CASCADE of the live ResourceLeasesTable rows and deletion of the holder work item itself, so holderItemId may reference a work item that no longer exists." Its interval semantics are equally explicit: one row per hold interval, a same-holder TTL refresh extends the open row in place, and stealing an expired lease closes the prior interval with release_reason = "expired" before opening a new one.

SQLiteRoleTransitionRepository.kt:121-123 is the contrast: deleteByItemId issues RoleTransitionsTable.deleteWhere { itemId eq itemId }.

The attribution hookclaude-plugins/task-orchestrator/hooks/enforce-actor-attribution.mjs is a PreToolUse hook that, when actor_authentication is enabled, "blocks advance_item and manage_notes(upsert) calls that are missing an actor object" — a client-side pre-check in front of the server-side verification, not a substitute for it.

5. Memory Data Model

work_items, notes with an FTS5 index, dependencies, role_transitions, resource_leases and resource_lease_history. A note carries its body, an actor claim — id, kind, parent, proof — and the server's verification of that claim. There is no supersession pointer, no validity window and no status the note itself can hold beyond the verification verdict.

6. Retrieval Mechanics

Full-text search over notes, plus graph traversal across dependencies. Neither consults the verification verdict.

7. Write Mechanics

A write is refused unless the actor verifies, unless the required notes for a transition exist, and unless the dependency ordering permits it. Schemas are opt-in: "Without schemas, all 14 tools work in schema-free mode — no gates, no required notes."

8. Agent Integration

Fourteen MCP tools, plus a Claude plugin carrying hooks for actor attribution, plan capture, and a retrospective trigger with a marker, a cooldown and a Stop backstop.

9. Reliability, Safety, and Trust

negative_eval — section 10.

trust_state is withheld, and this is the finding. The vocabulary is right: five discrete values on the row including REJECTED, derived from a cryptographic check rather than claimed. It gates admission and nothing else. A note stored when verification was UNAVAILABLE is indistinguishable, at every read path, from one stored VERIFIED — the field is present in the result and absent from every predicate. The rubric asks for a state that withholds a memory from being treated as true; this one decides whether the memory is written.

human_review is withheld. The verification establishes who wrote a note, not that anyone else approved it. There is no state a note waits in and no second actor who resolves it. The identity work is real and it is authorship attestation rather than review.

audit_log is withheld, on the contrast inside the codebase. role_transitions is never updated and records every state change with its actor and verification — and deleteByItemId removes an item's entire history with the item. The table beside it, resource_lease_history, was deliberately denormalised so that exact thing could not happen to it. When one project has written both, the second is the standard the first is being measured against.

tombstone, bitemporal and scope_enforced are withheld. No rejected value is recorded against content, there is one clock, and the boundary is a project root rather than a key filtered on a read.

10. Tests, Evals, and Benchmarks

A substantial Kotlin suite plus JavaScript hook tests. Nothing was installed and nothing was run.

The mark rests on cases that assert refusals and non-effects:

  • DidVerificationIntegrationTest.kt:434"Expected REJECTED: loose-kid must not apply to multi-key DID documents (single-key guard)". A relaxed key-id match is asserted not to apply when the DID document holds more than one key, which is the case where relaxing it would let the wrong key verify.
  • IdempotencyToolsTest.kt:427"Cached call must not create a duplicate dependency".
  • :682"Without actor, requestId must not enable caching": the idempotency cache is asserted inert when there is no actor to key it to.
  • :838"Repository must not be called — validation rejected before execution", which asserts the absence of a side effect rather than the presence of an error.

The last is the shape worth copying: checking that the repository was never reached proves the rejection happened before anything could be written, which an error-code assertion alone does not.

No benchmark and no paper.

11. For Your Own Build

Steal

  • Distinguish "verified from a stale cache" from "could not verify at all." Collapsing both into a failure makes an offline moment look like an attack; collapsing both into success makes an attack look like an offline moment.
  • Assert that the repository was not called. An error assertion proves the caller was told no; a never-reached assertion proves nothing was written.
  • Build the audit trail so it survives the deletion of its subject. The lease-history table's missing foreign key is three lines of decision and a paragraph of rationale, and it is why that history answers questions after an incident.
  • Put the gates in the tool handler. A required note that advance_item checks is a rule; the same sentence in a prompt is a hope.

Avoid

  • Storing a verdict you never read. Five verification states land on every note and no query mentions them. The check is doing work at admission, and the column is doing none afterwards.
  • Two history tables with opposite deletion semantics. One survives its subject by design and one is cascaded away; a reader who finds the second first will assume the first behaves the same way.

Fit

Take it if several agents share a work graph and you want the ordering and the documentation requirements enforced where they cannot be talked out of. The actor verification is worth having on its own, and worth extending to the read path if you intend to trust what the notes say.

12. Open Questions

  • A note's verification verdict is stored and never filtered on. Is a verified-only read mode intended, or is admission considered sufficient?
  • role_transitions is deleted with its work item while the lease history is built to outlive its holder. Is the difference deliberate?
  • Schemas are opt-in and without them there are "no gates, no required notes." What fraction of installs runs with them, and is the ungated default the one most agents meet?
  • The retrospective machinery has a marker, a cooldown and a Stop backstop. Is anything written down as a result, or is the retrospective only triggered?

Appendix: File Index

Path What it holds
current/src/main/kotlin/.../tools/ActorParsing.kt the verification call and the three-outcome ladder
.../infrastructure/config/JwksActorVerifier.kt, DidWebResolver.kt, DidDocumentJwksExtractor.kt did:web resolution to a JWKS and the proof check
.../database/schema/ResourceLeaseHistoryTable.kt the audit trail with no foreign key, and why
.../database/schema/RoleTransitionsTable.kt the transition record, its actor columns and verification columns
.../repository/SQLiteRoleTransitionRepository.kt create, the finders, and deleteByItemId
.../repository/SQLiteNoteRepository.kt where the verdict is written onto a note
claude-plugins/task-orchestrator/hooks/enforce-actor-attribution.mjs the client-side pre-check
current/src/test/.../DidVerificationIntegrationTest.kt the single-key guard case

Appendix: Recorded Searches

Run from the root of the checkout at the pinned commit.

Claim Command Result at this pin
The actor claim is really verified, not just stored grep -rn "\.verify(" --include='*.kt' current/src/main ActorParsing.kt:96 calls context.actorVerifier().verify(claim); JwksActorVerifier implements it over a did:web-resolved JWKS. A first pass that looked only at the repositories saw actorProof being persisted and would have reported it unverified
No read path filters on the verdict grep -rn "verificationStatus" --include='*.kt' current/src/main excluding the schema, the repositories' row mapping and VerificationResult.kt Only ActorParsing's own admission ladder
The two history tables differ on deletion read ResourceLeaseHistoryTable.kt:12-15; grep -n "deleteWhere" .../SQLiteRoleTransitionRepository.kt The first deliberately carries no foreign key so it survives; the second is deleted by deleteByItemId at :123
Transitions are never updated grep -rn "RoleTransitionsTable" --include='*.kt' current/src/main | grep -i update Nothing; the repository exposes create, three finders and the delete
The gates are opt-in read README.md:287 "Without schemas, all 14 tools work in schema-free mode — no gates, no required notes"
The licence carries no rider head -3 LICENSE; grep -n -i 'anthropic|may not' LICENSE Stock MIT, no match

History

2026-09-209f228716… — first reading, at 523 files. Screened before reading; nothing was installed, built or run. MIT. One mark, negative_eval. It is included here rather than excluded as workflow tooling because its notes are durable agent-authored content, full-text indexed and read back across sessions. trust_state is withheld although a five-value verification verdict sits on every note, because it gates the write and no read consults it; human_review because verification establishes authorship rather than approval; and audit_log on a contrast the codebase makes with itself — one history table is deliberately built to survive deletion of its subject and the one recording work-item transitions is cascaded away with it. One claim was checked and inverted before publication: the repositories persist an actorProof straight from the caller's object, which reads as an unverified claim until the parsing layer shows a did:web JWKS check standing in front of them.