Skip to content

feat: let a claim and a completion carry an attempt key - #77

Merged
sstriker merged 3 commits into
mainfrom
feat/claim-effects
Nov 12, 2026
Merged

sstriker merged 3 commits into
mainfrom
feat/claim-effects

Conversation

@sstriker

@sstriker sstriker commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

This is one more part of package 2 of PLAN.md.

The paper gives each task operation an attempt key from the caller, so a retry cannot claim two tasks or complete a task two times (Appendix A, Appendix C). The ledger had no such key. A caller that did not get the answer to claim_next and asked again got a second task.

What changes

  • The memory tools claim_next and complete take attempt. A key is 1 to 64 ASCII letters, digits, dots, hyphens or underscores.
  • claim_next with the same key gives the same claim, while that claim is in force. It does not give a second task. When the claim is over, the ledger refuses the key.
  • complete with the same key and the same handoff gives the done task. The ledger completes the task one time, and nothing else occurs for the second call. With a different handoff, the ledger refuses the key.
  • A key belongs to one session and one domain. The memory server keeps it as <session>:<key>. A different session with the same key makes its own claim. The ledger looks among claims for a claim and among completions for a completion, so one key can be used for each. A refusal names the key as the caller gave it.
  • The ledger looks for the key in the transaction that makes the claim, in the domain of the claim. It gives the claim back only to a caller whose name, handling and manifest fit the task.
  • The key is in the event of the claim or the completion, so a rebuild keeps it. The event of a claim now also has its generation.
  • local/README.md, README.md, PLAN.md and docs/terms.md have the attempt key.

Not in this change

An attempt key for the other operations. amem task has no attempt key, because a command has no session.

Checks

make check passed with 985 tests on 09bfe14. The ledger tests ran on SQLite and on the embedded PostgreSQL.

Adversarial pass

The pass ran on 98646df, the first form. It confirmed nine findings, and 295b01a corrects them:

  1. A key gave back a task above the handling of the caller, and from a different domain.
  2. A second session of the same author with the same key got the claim of the first.
  3. The refusal for a used key gave the id of a task that the caller could not read.
  4. The ledger looked for the key before the transaction of the claim. Two callers with one key claimed two tasks.
  5. SQLite looked in each domain, and PostgreSQL in one.
  6. A completion asked again ran the close of the continuation chain again, with the handoff of the second call.
  7. One key for a claim and for a completion of one task was refused.
  8. A key had no limit on its length or its characters.
  9. Three changes to the code left all tests green. Each now has a test.

A second pass ran on 295b01a. It ran its own tests and mutations on SQLite and on the embedded PostgreSQL. It confirmed no defect in behaviour. It confirmed six findings about tests and text, and 09bfe14 answers them:

  1. No test held "two connections with one key make one claim". The new test two_connections_under_one_key_make_one_claim fails on PostgreSQL when the lookup does not take the domain lock.
  2. No test held the lease term or the requirements term of same_claim. Each new assertion fails when its term is removed.
  3. No test held "nothing more happens" for a completion asked again. The new test a_completion_asked_again_closes_no_task_up_its_chain fails when the second call runs the chain.
  4. The operation prefix of the kept key had no test and no function. It is removed.
  5. The refusal showed the kept key, with the session id. It now shows the key of the caller, and the MCP test compares the full message.
  6. The text did not say that a key is ASCII. It now does.

The pass also named these points as plausible, without a test that shows them:

  • A second call of complete could read the task before the first call ended, and then get NotHolder. complete now reads the task with the ledger locked. No test holds this.
  • The key is for one domain, and the text did not say so. local/README.md now says so.
  • Open: if the first call of complete stops after the ledger completed the task and before the chain closed, the second call gives the done task and the parent stays open. Before this branch, the second call got WrongState, and the parent also stayed open.
  • Open: the lookup reads the events of the domain without an index on the key.

I stop at two passes.

Posted by Claude on behalf of @sstriker

🤖 Generated with Claude Code

sstriker and others added 3 commits November 12, 2026 08:53
The paper gives each task operation an attempt key from the caller, so a retry
cannot claim two tasks or complete a task two times (Appendix A, Appendix C).
The ledger had no such key. A caller that did not get the answer to
`claim_next` and asked again got a second task.

- `claim_next_once` takes a key. The same key asked again gives the same claim,
  while that claim is in force for that holder. When the claim is over, the
  ledger refuses the key with `Error::AttemptSpent`.
- `complete_once` takes a key. The same key asked again gives the done task,
  and the ledger appends no second event. A key that was used for a different
  task or a different operation is refused.
- The key is in the event of the claim or the completion. The ledger finds it
  in the events, so a rebuild keeps it. The event of a claim now also has its
  generation.
- The memory tools `claim_next` and `complete` take `attempt`.
  `amem task claim-next` and `amem task complete` take `--attempt`.
- A completion asked again reaches the ledger after the session forgot the
  claim. A test of the tools found that the session refused it first.

Not in this change: an attempt key for the other operations.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
… claim

This commit merges `main` and changes the attempt keys of 98646df. The
adversarial pass ran on 98646df and confirmed nine findings.

- A key was the same for each session of an author, and on SQLite for each
  domain. A second session with the same key got the claim of the first. A
  session with low handling got a restricted task. The memory server now
  keeps a key under its session and its operation. The ledger looks for a key
  in the domain of the claim. It gives the claim back only to a caller whose
  name, handling and manifest fit the task.
- The refusal for a used key gave the id of the task. It now gives only the
  key.
- The ledger looked for the key before the transaction of the claim. Two
  callers with one key claimed two tasks. Each backend now looks in that
  transaction. PostgreSQL takes the lock of the domain first.
- A completion asked again ran the close of the continuation chain again,
  with the handoff of the second call. It now changes nothing. With a
  different handoff, the ledger refuses the key.
- One key for a claim and for a completion of one task was refused. A key now
  belongs to one operation.
- A key had no limit. A key is now 1 to 64 letters, digits, dots, hyphens or
  underscores.
- `amem task claim-next` and `complete` no longer take `--attempt`. A command
  has no session, so its key could not be found again.
- Tests now hold the generation check of a key, the domain of a key, the
  handling of the caller, a second session, a bad key, and a completion
  asked again with a different handoff.

A test of this branch claimed the first of two tasks that were filed at one
time. The queue order of `main` is now by time and then by id, so the test
gives the two tasks different times.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The second adversarial pass found no defect in behaviour. It found rules
that no test held. Each new assertion failed against a mutation of the
code before this commit kept it.

- Two connections with one key make one claim. Without the domain lock
  in the PostgreSQL lookup, the new test fails.
- A lapsed lease ends the claim of a key. A caller without the
  requirements of the task does not get the claim back.
- A completion asked again closes no task up its chain.

Changes in behaviour:

- The kept key is the session and the caller's key. The operation prefix
  is gone, because the ledger looks among claims or among completions.
- A refusal names the key as the caller gave it.
- `complete` reads the task with the ledger locked, so a second call
  with the same key reads what the first call left.
- The message and the README say that a key is ASCII, and that a key
  belongs to one domain.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@sstriker
sstriker marked this pull request as ready for review November 12, 2026 12:30
Copilot AI balanced review requested due to automatic review settings November 12, 2026 12:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@sstriker
sstriker merged commit fff57d1 into main Nov 12, 2026
10 checks passed
@sstriker
sstriker deleted the feat/claim-effects branch November 12, 2026 16:51
sstriker added a commit that referenced this pull request Nov 12, 2026
Main has the attempt keys of #77. The plan keeps the rows of both, and
the MCP tests of both stand.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants