Skip to content

coord: fix the friction the twenty-agent demo found - #281

Merged
BHAWESHBHASKAR merged 300 commits into
mainfrom
ccr-94e2aa7d-5xfs9k
Nov 10, 2026
Merged

BHAWESHBHASKAR merged 300 commits into
mainfrom
ccr-94e2aa7d-5xfs9k

Conversation

@BHAWESHBHASKAR

Copy link
Copy Markdown
Collaborator

Summary

This PR fixes five frictions that the twenty-agent coordination demo (#280) surfaced in Phase 1 notices and context packs. It also includes the fixes from a Fable review of those changes. The demo now has nine claims; three are new and each checks a fix.

Why

The demo showed agents being told the wrong thing, or nothing:

  • A pack said [same_file] (your "src/api/handlers.rs") while the notice for the same overlap said [near].
  • The first agent of a duplicate pair was never told about the second.
  • Agents that never declared a session did not hear about lands.
  • A duplicate's land was not labelled as one.
  • Packs repeated the same owners and history rows for every co-changed file.

Changes

Friction fixes

  1. Packs rank overlaps by the agent's own paths. The level comes from the request's paths and the session's own paths. Work met only on a co-changed or searched file gets the new optional Notice.via: "related", ranks after the agent's own overlaps, and never counts toward a crowded path. The CLI renders it as "a file related to your task".
  2. The first of a duplicate pair is told. The task-key lookup now runs on creates, lands, /v1/notices and listings, not only on declaration. It is still said once, and an agent restarting under a new key is still not flagged.
  3. Undeclared sessions hear about lands.
    • A new optional request header, Leep-Target-Ref, on POST /v1/changesets.
    • The CLI sends its working copy's target ref, and leep_commit_and_land sends its target_ref.
    • An inferred session follows the ref each commit names. A declared target is never overridden.
  4. A duplicate's land is labelled. The notice reads by: task_key: "your duplicate landed this task; check whether your work is still needed".
  5. Quieter packs.
    • Owners and history rows are grouped across files (untrusted.paths), and bytes is measured after grouping.
    • MCP explains a carry rule once per pack.
    • leep_task_start and leep work start drop any notice the pack already shows.

Review fixes (Fable: ship after must-fixes, all applied)

  • Every land of a duplicate session is its own notice, and the session's live overlap still shows beside it.
  • A failed task-key listing drops only the pack's duplicates section; in_flight stays.
  • A land whose creator cannot be read is never tagged as a duplicate's. Without this, a forged agent.session could earn the tag.
  • A task key's live sessions (at most 40) and landed sessions (at most 64) are read separately, so landed history never hides a live duplicate.
  • A revived session forgets its task key and target ref.
  • New server setting --default-ref (LEEP_DEFAULT_REF, default main): sessions with no target are checked against it. The notice says so through the new optional Notice.landed_on, as in "landed on main".
  • Demo claims tightened:
    • claim 7 requires a pack line and a positive "related file" line;
    • claim 9 requires exactly one carry explanation;
    • claim 2 requires cy's warning to come from cy's own pack;
    • an e2e leep status check now retries instead of running once.

Docs: ADR 0095, 0096 and 0097 amended; FORMATS ("Notice and pack refinements"), ARCHITECTURE §12, CLI.md, MCP.md, OPERATIONS, DEMO-COORDINATION.md and the env example updated. Changelog fragment: changelog.d/coord-demo-friction.md.

Testing

  • Lint and checks: fmt check, clippy -D warnings, check-version.sh and changelog-assemble.sh --check pass on this head.
  • Full suite: cargo test --workspace against Postgres 16, including leep-e2e, passed on the fixer's tree before its last commit. That commit only added LEEP_DEFAULT_REF to the env example; leep-server and leep-cli were rerun after it. A full rerun on this head is in progress locally, and CI runs it too.
  • One test per fix, and each failed before its fix. One exception: the default-ref test needs the new setting to exist, so it can't run against the old code. Examples:
    • related_files_never_outrank_my_own
    • the_first_of_a_duplicate_pair_is_told_too
    • an_undeclared_session_hears_of_lands
    • a_duplicates_land_is_named_as_such
    • identical_owners_and_history_rows_are_one_item
    • each_land_of_a_duplicate_is_its_own_notice
    • a_failed_task_key_listing_keeps_live_work
    • landed_sessions_never_hide_a_live_duplicate
    • a_revived_session_forgets_its_task_and_ref
    • a_session_without_a_target_hears_of_the_default_ref
  • Demo: all nine claims pass on SQLite and Postgres, at the default search concurrency and with --read-concurrency search=1.

Compatibility

  • API: additive only. New optional fields Notice.via and Notice.landed_on, and the optional Leep-Target-Ref request header.
  • Older clients:
    • they read only untrusted.path, so they see the first file of a grouped item;
    • they don't send the header, so their inferred sessions are checked against --default-ref.
  • Visible changes:
    • the first of a duplicate pair now hears possible_duplicate where it used to hear claimed or same_file;
    • notices already shown in a task-start pack are not repeated.
  • No migration.
  • Known limits, left for the maintainer (in DEMO-COORDINATION.md):
    • leep_commit_and_land delivers create-time notices only after the land;
    • the MCP 30 s notice poll;
    • a shed pack at leep_task_start is not retried by the tool.
  • Branch history: the branch carries a no-op merge of its earlier history (e2e: twenty-agent coordination demo, and two fixes it found #280, squashed into main as 74c86f0), so this diff holds only this change.

Generated by Claude Code

claude added 30 commits November 1, 2026 22:42
Packs, metadata transactions and repack work per land with pipelining on
and off, the create p50 that rose as land p50 fell, and what bounds an
uncounted dropped attempt.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
In the 2,000-agent mixed read and land swarm lands ran at 52 a second
with pipelining on and off alike (land p99 384 ms against 442 ms):
reads limited that run, not commits. Pipelining stays available behind
--land-pipeline on and becomes the default only after a run where the
land leader is the bottleneck shows it gaining. The crash tests that
pipeline set the flag themselves.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
Co-Authored-By: Claude <noreply@anthropic.com>
A merge used one merge base, the newest common ancestor. In a
criss-cross history the sides have several best common ancestors, and a
three-way merge against any single one can take a side's version of a
path with no conflict that another base would raise: branches that
merged each other with different resolutions lost one of them silently
when their descendants merged. The server must never resolve a conflict
silently.

Repo::merge_bases finds every best common ancestor (free in the common
case through side_is_clear; otherwise git's paint-down walk, iterative
and batched). The merge is made against the first base as before, then
each path the bases disagree on is resolved against every base, and a
path whose answers differ becomes a conflict. Refusing was chosen over
git's recursive virtual base: it needs no synthetic trees or nested
conflict markers, and a merge every base resolves alike stays clean.
The review rules on merges check every base the same way. Lands replay
non-merge stacks against their first parent and pick no merge base.

See ADR 0071.

Fixes #160

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
…mselves

The nightly fuzz run hit a debug assertion: a node naming a writer's
pack by absolute reference, stored again in that writer's pack, made a
pack that refers to itself. In a release build the encoder wrote the
writer as index 0, so the reference silently read as a node of the
pack's own, by a relative part. Derived rows are read back from the
metadata store, and a damaged one must come back as an error that
verify reports and repair rederive rebuilds, never a panic or a wrong
answer.

A part whose id table names its own writer, or a node naming itself or
a later node of its part (which could make a walk loop), is now a
corrupt history node error. The encoder writes such a reference with
the writer in the table instead of asserting, so the pack fails to
decode. The fuzz harness stores a node again under a writer it does not
name, as a real pack does. The sibling skiplist decoder had the same
shape of hazard: a row whose generations do not descend, or that names
its own changeset, could send an ancestry walk round forever; both are
errors now.

Fixes #216

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
…mpotent

The nightly fuzz run found strip_comments disagreeing with itself: it
dropped whitespace at the start of the file, including the first code
line's indentation, so Ruby's ` =gin` came out at column 0, where a
second pass reads an `=begin`-style block and gives up. The lexer
decides comment_only auto-approval, and a lexer that reads its own
output differently may also read the language differently.

The file now starts a line like any other: blank lines and comments at
the top still vanish, but the first code line keeps its indentation.
As a safety net, strip_comments re-lexes its result and gives up unless
it reads back as itself, so any input class it handles this way never
auto-approves. A randomized test over every language checks the
property on the lexer itself, without the net (8 million cases ran
clean locally); the crash input joins the corpus.

Fixes #217

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
incremental_reach_rows_agree_with_full_reachability asserted that its
random history left something unreachable, and seeds 5020 and 6205 of
the nightly sweep generate histories that leave nothing (every tip
landed, named or young), failing the precondition without comparing
anything. Skipping such seeds would hide a sweep that tests nothing.

Each seed now abandons a two-changeset branch aged past grace, marks it
with one more run that may be killed, and checks that the oracle,
the incremental run and a full run all find it, so every seed compares
real results.

Fixes #218

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
replay_stack walked and rebased an unlanded stack of any length, and the
batch deadline was checked only before and after the whole replay. One
writer could create thousands of cheap changesets and land the tip on a
moved ref: the replay held the ref's leader, admission refused everyone
else, and the batch then answered 409 past-deadline, so a resend redid
all the work.

A stack longer than MAX_LAND_STACK (1,024, the landed-copy scan) is now
refused with Invalid (400) asking to land it in pieces, before anything
is replayed. The stack walk and both replay paths check a deadline at
every entry: past REPLAY_BUDGET (half of MAX_BATCH_LIFETIME) from the
batch's start, that land alone answers the retryable past-deadline 409,
and the lands replayed before it still have time to commit. rebase-onto
shares the replay and the bounds.

Fixes #158

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
Since #217, strip_comments gives up unless its result re-lexes to
itself, so the comment_lexer fuzz target's idempotence assertion on it
could never fail: the nightly fuzzer could no longer find the lexer
disagreeing with itself. strip_comments_unchecked (hidden) is one pass
of the lexer with the same prechecks; the fuzz target and the in-crate
property test now assert idempotence on it, and that the check in
strip_comments never fires.

Checking the lexer itself found three more inputs the check had been
hiding, each fixed at the root: GCC splices a backslash, blanks
including vertical tab or form feed, and a newline, which the lexer
did not count as a splice; a comment between a stray backslash and the
newline became a splice once stripped, so a backslash outside a literal
that is not a splice (and not at the end of the file) now makes the
lexer give up; and `import "C"` after a comment on its line was not
seen as cgo, so any `"C"` in a Go file now is.

Refs #217

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
Since ADR 0071 the review rules ask merge_bases for every changed path
and side, and merge_bases may walk (side_is_clear up to 10,000 steps,
then a paint-down walk in a criss-cross), where merge_base used to be
skiplist-cheap. A review now finds each (first parent, side) pair's
bases once and shares them between the dropped-change check and every
path's merge-parent check. A test counts the calls over a 41-path
criss-cross merge review.

Refs #160

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
The check that a skiplist row does not name its own changeset lived in
Repo::skiplist only. prefetch_skiplists decoded batched rows straight
into the derivation's skiplist cache, and skiplist_noting_generations
served them from there unchecked, so a one-entry row naming itself sent
on_first_parent_chain round forever; blame, the log filter walk, the
ancestry read-ahead and changeset creation decoded rows without it too.

Skiplist::decode_of(id, bytes) decodes and checks the row of `id`, and
every reader that knows whose row it holds uses it. A test drives the
prefetch path: the walk ends with a storage error and nothing is
cached (without the fix it hangs).

Refs #216

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
The stack cap was checked at the top of the walk, before the exits for
a parent already in the head's history or landed, which read as though
a stack of exactly MAX_LAND_STACK could be refused while the docs and
the error say "longer than". The check now sits where the walk is about
to take one more member, so the code states what it does: exactly the
cap lands, one more is refused. The test covers both sides of the
boundary.

Refs #158

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
Review asked for the cost of refusing over a virtual base to be stated:
once two lines resolved a file differently in a criss-cross, a later
edit to that file conflicts on every merge of the lines until one merge
of them lands and becomes the single base, where git's recursive
strategy would merge it cleanly.

Refs #160

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
Co-Authored-By: Claude <noreply@anthropic.com>
… cap

Serve the router on hyper's connection builders with a timer instead of
axum::serve, which builds them without one, so the header-read timeout
was never armed and no idle bound applied.

- --header-read-timeout (30s): a connection must send a complete request
  head in time, from when it opens and, on HTTP/1.1, from the end of its
  previous request.
- --idle-timeout (2m): a connection with no request in progress is
  closed gracefully.
- --body-idle-timeout (60s): a request body that stalls fails; a socket
  whose writes stall is closed; a large response the client reads nothing
  of is dropped by a watchdog. Read-class permits and per-token
  concurrency slots ride in response bodies, so they are now released
  within this bound when a client stops reading.
- --max-connections (10000): further connections wait in the backlog.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
…without auth

- POST bodies must be sent as application/json and blob uploads (PUT) as
  application/octet-stream; another type is 415 `invalid`. A request
  with no Content-Type is accepted when it carries an Authorization
  header, so older clients, which sent blob uploads without one, keep
  working against servers with tokens.
- Without --tokens-file, requests with an Origin header, or whose host
  is not localhost, an IP address or a name given with the new
  --allow-host (LEEP_ALLOW_HOSTS), are refused with 403 `forbidden`.
  The probes are exempt.
- leep, leep-soak, the e2e helpers and the API fuzz target send the
  types; leep-git and leep-mcp go through leep's client.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
…s address, match peer prefixes as origins

- A 500 answers with a generic message and the request id; the error
  text (a store message can name hosts, buckets or paths) is logged
  under that id. The `code` stays `internal`.
- --metrics-listen on a non-loopback address logs a startup warning;
  the docs say its labels include token names.
- --peer-url-prefix is parsed and compared on scheme, host and port,
  then path prefix, instead of as raw text. `http://host:` with no port
  allows any port of that host; a partial host is refused at startup.
  The env example uses a full origin.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
… read-only permissions

remote.yml uses the deploy SSH key; it now runs in the `production`
environment so a repository admin can require reviewers and keep the
secrets there (documented in OPERATIONS.md and DEPLOY.md). rust.yml gets
an explicit `contents: read` permissions block like the other workflows.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
leep-soak derived its server's tokens from --seed, which reports print
and CI sets to the run number. They now come from 32 random bytes made
for each run, or from LEEP_SOAK_TOKEN_SECRET to reproduce them. The
workload seed is unchanged and still replays a run's plan.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
The server now closes an HTTP/1.1 connection idle for
--header-read-timeout (30s by default). leep's client, which the git
mirror and leep-mcp use, and leep-soak's clients keep idle connections
for 20s, so a long-running client does not send a request on a
connection the server is closing.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
Since prefixes are matched as parsed origins, one like `https://leep-`,
which raw-text matching accepted for `https://leep-1.internal`, would
match nothing and stop every forward without an error. A prefix whose
host is not an IP address or a complete DNS name (empty labels, a label
starting or ending in `-`, a trailing dot) now stops the server at
startup with "does not name a whole host". The e2e startup test and the
docs are updated.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
Co-Authored-By: Claude <noreply@anthropic.com>
…head

A cold blame read the same main-chain skiplists one row at a time on
every request: they were cached per walk only. Skiplists read by queries
now go to one process-wide SkipReadCache on the Repo (32 MiB of the
derive cache budget, moved from the filtered log's own skiplist cache,
so the total is unchanged), used by FirstParents::list, plan_stretches
and filtered logs.

A blame answered by the newest changer's cached blame is now stored at
the requested head too, with the encoding a walk stores, so the next
blame there is one lookup. Stretches are planned only once the head walk
has not finished the blame.

On a 4,100-changeset linear history with files of 6 to 25 versions,
metadata calls per blame in a warm process fell from 119 to 95 for a
file's first blame, from 78 to 9 at a head 40 lands later, and from 68
to 2 for a repeated blame there.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
PERFORMANCE.md gains the 2,000-agent swarm runs that found the index-lag
collapse and each fix's effect (ADR 0073, ADR 0074), the agent sweep on
one 2-vCPU server (250, 500 and 1,000 agents pass every criterion; 2,000
is CPU-bound), the blame profile and the skiplist cache's effect, the
warm-blame result, sizing per vCPU, and what is not yet validated: more
than one server, lease handoff under load, a ref beyond about 220 lands
a second, and long histories.

OPERATIONS.md says to restart running servers after a repair rewrites
skiplist rows, since they keep decoded skiplists in memory once read.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
The cherry-pick onto main auto-merged them after the self-naming test
that #237 added, inside `mod tests`. The batched read in plan_stretches
also decodes with `decode_of`, so a cached list passes the same
self-naming check as every other read path.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
Co-Authored-By: Claude <noreply@anthropic.com>
Every read of a tree node deep-copied the cached node: a file lookup copied
each directory on its path to take one entry, and listings, diffs, creates
and the land batch's staged-node memo copied every directory they opened.
At 1,000 agents on one 2-core server, copying and freeing nodes was about a
third of a file read's CPU and a quarter of a land batch's, growing with
directory width. Root trees were read from the metadata store on every
listing and twice per diff, about 12% of all statements, although the
value is fixed by the changeset.

The decoded-node cache, the land memo, lazy-tree derivations and the read
paths now hand out Arc<TreeNode>; lookups borrow the entry they need, and
building a tree copies a node only where it changes it (Arc::make_mut).
read_tree and read_trees keep their types as copies of the new
read_tree_shared and read_trees_shared. Repo::root_tree keeps each root it
reads or a create computes in the ancestry cache, which already held
prefetched roots; verification and repair, which substitute recomputed
rows, read the row every time.

In a 1,000-file directory on memory stores, a warm lookup fell from 49 us
to 0.7 us, a one-file diff from 395 us to 194 us and a listing from 398 us
to 317 us. No persisted format or hash changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
Co-Authored-By: Claude <noreply@anthropic.com>
A blame walk found the first-parent ancestor at each changer's
generation by descending skiplists from the nearest known point, one
single-key read per halving of the distance, for every change. Where the
chain has no merge at or above that generation (the step's last_merge-v1
row, read with its index entry), every ancestor that high is on the
chain, so the changer is that ancestor and the walk goes straight to it,
reading its skiplist with its generation. Merge regions keep the
descent.

The blame walker's gathered reads of several derived kinds now go out as
one get_derived_kinds call (one Postgres statement per round instead of
one per kind).

On a 200,000-changeset linear history (memory stores), cold-blame
statements fell from 191 to 77 for a file of 18 versions, 728 to 372 for
75 versions and 9,406 to 3,764 for 14,743 versions. Adds a counting test
on deep chains and an ignored deep_blame_calls measurement.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
Co-Authored-By: Claude <noreply@anthropic.com>
POST /v1/context is a query, but the client retried only GETs, so a
rate limit or a Leep-Refused shed failed the call at once; with half
the cores as search permits a small server has one. The client now
sends it again on both, as it does a search.

leep_task_start and `leep work start` declare the session before they
ask for the pack. A pack the server cannot give then (shed with MCP's
retries off, rate limited, failing) no longer fails the call after the
session was made: the session is reported as started, with "context
unavailable" and when to ask leep_context or `leep context` again.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
`leep work start` asked for the context of its scope and claims
together, up to 128 paths against the request's limit of 64, so a full
declaration was refused after the session was made. It now sends the
claims, then the scope, at most 64.

leep_task_start sent its paths as the session's scope even when there
were none, so resuming a task without paths wiped the scope. It now
leaves the scope as it is then.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
One `search` read-class permit covers a whole pack: its path logs,
last-change reads, ref checks and culprits take no `log_filtered`
permit of their own. ADR 0097 and OPERATIONS now say so, with the
bounded cost under that permit (128 MiB scanned at most, against a
plain search's 256 MiB, and 2 s) and that with `search=off` only the
per-token rates bound packs. A router test checks that a full search
class sheds /v1/context with Leep-Refused and admits it once the
permit is free.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
Fixes from review of the context pack (ADR 0097):

- A history item copied agent.model and agent.session uncut, and an
  owners item listed every name, so one 10 KiB model emptied the
  history section. Names are cut to 80 bytes; owners items list at most
  16 names and 16 OWNERS files.
- Ref health is one item with a 5% quota; suspects could push it out
  and the red state with it. Suspects are now dropped first (reported
  as cut section `suspects`), and spare budget goes to ref health
  before anything else.
- When the caller's session lookup failed or timed out, the stand-in
  session found the caller as its own 100% duplicate. The stand-in now
  carries the caller's key, and rows of the same principal and key are
  never an overlap or a duplicate.
- A session both live on the files and a possible duplicate was charged
  twice; it is listed once, under duplicates, with its paths.
- The request's task key wins over the session's, as its task text does.
- `sized` no longer subtracts from a zero length.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
`leep review inbox` and leep_review_inbox asked for one page (50, or
20) and then dropped the requests the caller had already answered, so
when the newest page was all answered, older requests still waiting
were hidden. The shared `waiting_requests` reads on, at most ten pages,
until it has the limit of unanswered ones, and says when more follow.

leep_review_inbox no longer tells an MCP agent to run a CLI command it
has no tool for: it says the tools cannot approve or reject.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
The untrusted fence escaped line separators and bidirectional
controls but left C1 controls (U+0080 to U+009F) raw, which some
terminals read as escape sequences; they are escaped as \uXXXX now. A
red check named `ci/test` showed as `?` because it went through the
plain-word filter; it now goes through the name filter that allows
`/`, as refs do.

The server context test's OWNERS fixture held a line of code; it now
names an owner.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
The overlap notices work on this branch reached main as c844978, the
same tree as 2da46b1; keeping it as a parent lets the branch move
forward without rewriting what was pushed.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
leep_task_start declared its session without a base, and the server
finds `landed` notices only from a base (ADR 0096). An MCP agent, which
has no working copy, was therefore never told that another agent landed
a change to its files, even the twin given the same task key: the
coordination demo's duplicate pair showed `leep_overlaps` answering
"Nothing overlaps" right after the other agent landed the task.

A session without a base now takes the head of the pack it was given,
in one more declaration; a resumed session keeps its base.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
The pack's `history` section read `created-by-v1` of the landed
changeset only. A land the server rebased onto a moved ref is a copy
with no creator row, so on a busy ref most files' last change came back
with no `created_by`: in the coordination demo the newcomer saw the
model of the agent that last changed its file but not which agent it
was. Culprits already followed the rebase chain; both now use one
helper, `Repo::created_by_through_rebases`.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
The phase 1 demo of the coordination design: twenty scripted agents,
each with its own token and agent session, on one repository through
the real leep-server, leep and leep-mcp (five agents over MCP stdio).
It shows and asserts that `leep who src/auth/` lists the three agents
there, that two agents on one file are warned before either lands, that
two given one task key are told at `leep_task_start`, that the other
hears a `landed` notice naming the changeset (CLI and MCP), that a
newcomer's first call answers owners, history with models, live work and
a red ref's suspect, and that fourteen agents working alone, concurrently,
land and hear nothing louder than `near` about each other.

One scenario serves both: the `coord_demo` test asserts the claims on
SQLite and Postgres; the `coord_demo` example, run by
scripts/demo-coordination.sh, prints the narrated transcript with a
table of the notices each agent heard. Waits are bounded retries of a
command, never bounds on time.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
The context pack work on this branch reached main as b82b270, the same
tree as 6079653; keeping it as a parent lets the branch move forward
without rewriting what was pushed.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
leep_task_start took a new session's base from the head of its context
pack. On a busy server the pack is shed (503, all search permits in use)
and the session then had no base, so it never heard of a land on its
files: the coordination demo failed this way on a 2-core CI runner. With
no pack, the base is now the target ref's head, read directly.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
A context pack probed co-changed and searched files as if they were the
agent's own, so its in-flight levels contradicted the notices in the same
answer (rex's pack: `same_file` sol "(your handlers.rs)"; the notice:
`near`) and named related files "your" file. Levels are now ranked by the
request's and the session's own paths; work met only on a related file
carries the new optional `Notice.via = "related"` and is listed after
work on the agent's own files. ADR 0097 amended.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
Duplicates by task key were looked for only when a session was declared,
so only the second of a pair heard of the first; in the demo dee heard
eli as `same_file` with no task key. The task-key lookup (one indexed
listing, bounded like the other parts) now runs on every answer that
carries notices: creates, lands, `GET /v1/notices` and listings. Said
once, and still never about the same principal's own restart. ADR 0096
amended.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
A session inferred from commits (LEEP_AGENT_SESSION, no `work start`)
had no target ref and never heard a `landed` notice. Clients now send
the additive request header `Leep-Target-Ref` on `POST /v1/changesets`
(the CLI its working copy's target, `leep_commit_and_land` its
`target_ref`); inference records it when the session has none, a bad
name is ignored, and a session with no target ref is walked against
`main`. ADR 0095 and 0096 amended.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
eli's `landed` notice for dee's land of BILL-42 said only that a
changeset landed. The task-key listing now reads live and landed
sessions of the key (one indexed listing, at most 64), the `landed` walk
keeps each reported changeset's `agent.session` and creator, and a land
by a session with my task key (not my own principal) is tagged `by:
task_key`, naming that session and the key. The CLI and MCP tools add
"your duplicate landed this task; check whether your work is still
needed"; MCP keeps the key in the fence. ADR 0096 amended.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
Packs repeated the same owners and last change for every co-changed
file (the `carry identical` explanation four times in the demo), and
`leep_task_start` showed a duplicate twice, in the pack and as a notice.
An owners item now covers every file with the same owners answer and a
history item every file with the same last change, listing them in
`untrusted.paths` (first file keeps the id and `path`; sizes measured
after grouping). MCP explains a policy once, and `leep_task_start` and
`leep work start` drop notices the pack already lists
(`ContextPack::shows`). ADR 0097 amended.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
Three more claims: a pack ranks live work by the agent's own files as
its notices do (rex and sol call each other `near`; bo's pack never
calls ada's keys.rs bo's own); the first of a duplicate pair is told and
its twin hears that it landed the task; a task start says each thing
once. Claim 2 accepts cy's warning from its `work start` pack, which now
carries it instead of a repeated notice. The two design questions the
demo raised (a commit-only MCP tool, the 30 s notice poll) are listed as
known limits.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
A `landed` notice tagged `by: task_key` names the session that landed
it, and `one_per_session` kept one item per session id: of two lands by
my duplicate only the newest was said, and its live work folded away
too. A notice about a landed changeset now passes through untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
The in-flight section read the task-key sessions only to tag a
duplicate's land, yet a failed read omitted the whole section. The pack
now reads those sessions once for both sections: a failure leaves lands
untagged and omits only `duplicates`.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
A store error reading a landed changeset's creator counted as "no
creator", so the writer-chosen `agent.session` alone could tag a land
"your duplicate landed this task", and the walk cached it. On an error
the land now carries no landing session and is never tagged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
The newest 64 sessions of a task key, live or landed, could all be
landed under a reused key and hide an older live duplicate (and the
state is not in the task-key index). Live sessions (at most 40) and
landed ones (at most 64) are now two bounded listings run together.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
A session revived by a commit after its land kept its task key and
target ref, so an inferred session reused from review/a to review/b
kept walking review/a, and a stale task key could call a later land a
duplicate's. A revive now clears both, and while a session is inferred
the ref each commit names (`Leep-Target-Ref`) replaces the last one; a
declared target still stays. ADR 0095 amended.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
A session that named no target ref was walked against a fixed `main`,
and its `landed` notice still said "landed on your target ref". The
server gains `--default-ref` (`LEEP_DEFAULT_REF`, default `main`; it had
no such setting), and such a notice carries the new optional
`Notice.landed_on`, so the CLI and MCP tools say "landed on main".
ADR 0095 and 0096 amended.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
FORMATS says a client shows a `via` it does not know as it is, but the
CLI and MCP tools showed only `related` and dropped any other. The CLI
now prints it quoted and escaped, the MCP tools as a plain word or `?`.
ADR 0097 also notes that an older client sees only the first file of a
grouped owners or history item.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
Claim 7 now needs a pack line between rex and sol (sol starts first and
lands after rex has started, so it never depends on scheduling) and a
positive "a file related to your task" line in fay's pack, for the
maintainer's live work on docs/auth.md (now written with token.rs),
listed after cy. Claim 9 needs exactly one `carry` explanation in dee's
pack (asked again when shed), and claim 2 needs cy's warning to come from
its own `work start` pack. In e2e, a dead `warned` check goes and the
undeclared agent's `landed` notice is awaited by bounded retries.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
The env-example test checks every flag's variable is documented there;
`--default-ref` was missing. Also takes a test's slice by reference, as
clippy asks.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
The coordination demo on this branch reached main as 74c86f0, the same
tree as e195f97; keeping it as a parent lets the branch move forward
without rewriting what was pushed.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
@BHAWESHBHASKAR
BHAWESHBHASKAR merged commit 582a33e into main Nov 10, 2026
5 checks passed
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