Repository navigation
mcp: stop a land for overlaps the agent has not seen (pre-flight) - #282
Merged
Merged
Conversation
…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>
A tree that a changeset was created without (ADR 0069) is stored later, by the land that fast-forwards a ref to it or by the changeset sweep, nodes first and root last. When that happens after the backup's metadata snapshot (which already holds the changeset) and while sync_blobs is listing the source, the listing can pass a node's shard before the node is written and then reach the root's shard after the root is written. The pool then holds a root without its whole tree: the closure check fails with missing_tree (seen in soak_smoke), and a restore taken with --accept-failed-check breaks "a present root means a whole tree". sync_blobs now reads, exactly, each directory a copied tree node names that the destination lacks, and copies it, recursively. Children are written before their parents, so the source holds them. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
Co-Authored-By: Claude <noreply@anthropic.com>
At 1M changesets the server held 2.7 GB resident with 1.19 GB in use over 32 arenas. Cap them as the importer does, unless MALLOC_ARENA_MAX is set. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
On a cold restart at 1M changesets the 321 MB pack index load outlasted the ready timeout, so /v1/ready timed out. Warm the blob store in the background when the routes are built (new BlobStore::warm) and answer 503 blobs/loading, without probing, until it ends. Documents both this and the malloc arena limit in OPERATIONS and DEPLOY. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
Co-Authored-By: Claude <noreply@anthropic.com>
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
Notice::stops_land is the one rule of the MCP pre-flight check (ADR 0096 amendment): possible duplicates, a duplicate's land, a land on a changed file, a claim on or above a changed file, and another session naming a changed file itself stop; near, a scope directory and related files do not. So that the rule can read a notice's paths, the server now names the file two sessions share rather than a directory holding it, and a landed notice names a file of mine the land changed before a directory of mine. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
leep_commit_and_land and leep_resolve_and_land read the notices their create got before landing, and stop, unlanded, on one Notice::stops_land says to read first and the agent has not been shown. The answer names the changeset made and the notices (fenced) and says how to go on; force: true lands anyway, and a retry reuses the stopped changeset instead of making another. A clean call is still one create and one land. leep_task_start asks once more for a context pack shed with a short Retry-After. The demo shows dee stopped and landing with force; ADR 0096 is amended, and MCP.md, ARCHITECTURE.md and the demo notes say how it works. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
Said once was keyed by the other session and level, so a session first heard of through my scope directory (or a claim on another file, or a land on another file) never stopped my later commit of a file it does touch. Each notice now records the files it stops a land of, and is said again when it comes to stop one it did not; every notice that stops comes in one answer, past the cap of three. Notice::stops_land reads the other side's path, never which of mine it was paired with, and the server prefers, per session, the row that stops a land of the request's files (or the session's, on a notice read); a landed notice names the request's file the land changed. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
A stop is no longer an error result, and a plain retry of a stopped changeset (the same call, or the changeset by id) is stopped again with its notices and any new ones, never landed: only force lands it. Landing it by id without a ref lands it on the ref the stopped call named, with its create_ref and request, and the guidance names both. A stopped changeset is forgotten only once it lands, so a failed forced land keeps its request's land id. Resolution stops say to send new resolutions. The shed-pack test no longer sleeps. MCP.md, ADR 0096, ARCHITECTURE.md and the demo notes describe the rule and its limits. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpET1BMBSd4aNPHPVkQEbD
The demo friction fixes on this branch reached main as 582a33e, the same tree as ab82a0d; 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
leep_commit_and_landandleep_resolve_and_landnow run a pre-flight check. A call whose create answer carries a stopping warning the agent has not been shown creates the changeset but does not land it, and the result lists the warnings. A stopped changeset lands only withforce: true.The maintainer chose this option ("pre-flight check") to close the gap the twenty-agent demo found: the MCP tool created and landed in one call, so overlap warnings arrived only after the land.
Why
CLI agents run
leep commitand see its warnings before they runleep land. MCP agents had no such pause. Now the agents most likely to collide, those with a duplicate task, a claim or the same file, are told before they land, and a call with nothing to warn about stays a single call.Changes
The stop rule. It is decided in one place,
Notice::stops_land, against the files the changeset changes, using the other side's path. A land stops on:nearwarnings, scope directories, related files and unknown levels never stop a land.Only unseen warnings stop. The server says each warning once per session. It now remembers which files it said a warning as a stop for, and says it again when it would stop a file it did not stop before. For example: a non-stopping warning about ben's
src/b.rs, followed later by ben editing mysrc/a.rs, stops my commit ofsrc/a.rs. Per session, the server prefers the overlap row that stops for the request's files. Landed warnings name the request's file. Every stopping warning comes in one answer, beyond the usual cap of 3.A stopped call:
isError: falsewithstructuredContent {status: "stopped", landed: false, changeset, target_ref, files_changed, stopped_by, guidance}and the textNOT LANDED: stopped before landing;force: true(the guidance names the target ref), or to change its work.Retry and force:
GET /v1/notices, so a host that automatically retries failed tool calls cannot land blind.changeset: <id>withforce: truelands the remembered changeset on its own ref, with its owncreate_refand land request id. No new changeset is made.Other changes:
leep_task_start's context pack is shed, it is retried once ifRetry-Afteris 2 s or less.leep commitalready prints warnings beforeleep land.maindoesn't move, and dee then lands the same changeset withforce.Docs:
changelog.d/mcp-preflight.md.The documented limits are:
GET /v1/noticesanswer mean no stop, because warnings are advisory;LEEP_AGENT_SESSIONdon't share stops.Testing
-D warnings,check-version.shandchangelog-assemble.sh --checkpass.cargo test --workspaceagainst Postgres 16: 2,012 passed, 1 failed, 9 ignored. The failure isa_hanging_coordination_store_changes_no_answer, an existing wall-clock assert: 314 ms against its 300 ms limit while another build ran on the machine. It passed 5 of 5 runs alone. This PR doesn't touch it. A proper fix (a paused-clock bound test) is in progress separately.forcelands without making a second changeset;neardoes not stop;Retry-After: 0, so it doesn't sleep).coord_demopasses on SQLite and Postgres.Fable reviewed it ("ship after must-fixes"). The three must-fixes are included: wrong-ref guidance, said-once keyed by file, and the stop judged by the other side's path. All the should-fixes and nits are included too.
Compatibility
Notice::stops_landis client logic; no wire field was removed.force: trueto land past it.Generated by Claude Code