Skip to content

Merge main (97ca7eba) into fix/glob-mount-prefixed-patterns, and a **/ after a brace group - #1

Closed
nissa-seru wants to merge 1466 commits into
Anarchid:fix/glob-mount-prefixed-patternsfrom
nissa-seru:gordon/af401-merge-main
Closed

nissa-seru wants to merge 1466 commits into
Anarchid:fix/glob-mount-prefixed-patternsfrom
nissa-seru:gordon/af401-merge-main

Conversation

@nissa-seru

Copy link
Copy Markdown

TL;DR: this merges main (97ca7eba) into anima-research#401's branch, which no longer merged cleanly. One of the conflicts needed a decision, not only a resolution: where a mount-prefixed pattern's walk starts, now that main starts each search at the directory its pattern names (anima-research#511). Each reading of the pattern walks from its own start, so the pattern as written walks exactly as it does on main. A second commit fixes a **/ right after a brace group. It's a pull request into your branch rather than a push to it, so the change stays yours to take.

The decision

anima-research#511 made glob start its walk at the directory its pattern names before the first wildcard, beneath its path, and grep at the deeper of its path and its filter's directory. A search on a mount past its walk cap then still reaches what its pattern names. This branch gives a pattern whose first segment is a mount's name a second reading in that mount, its rest. Taken as main has it, the walk would start at the first reading's directory. For notes/diary/*.md in the notes mount, that's notes/diary inside the mount. It isn't there, so walkStart stops at the mount's root, and on a mount past its cap the walk can stop before diary. That's the failure anima-research#511 fixed, back for exactly the patterns this branch makes work.

So:

  • Each reading walks from its own start, for its own matches (searchWalks). Where two walks run, their entries are given once each, in path order, and a directory both name keeps the first walk's account (mergedWalks).
    • The pattern as written walks exactly as on main: its start, the ignore patterns its named scope sets aside, and its cap.
    • The prefixed reading walks from the literal directory of the rest of the pattern, from the mount's root and beneath path. prefixedGlob gives it beside its regex, in the place of this branch's mountGlobs.
  • One walk from where both readings' matches lie would lose what main finds in a mount that holds a directory named like itself. Past the cap, it would start higher. On a direct mount, even within the cap, a directory each reading names would stay ignored. work/node_modules/pkg/*.js would find nothing, where main finds work/work/node_modules/pkg/a.js.
  • In a mount where the prefixed reading applies, the pattern as written walks where walkStart reaches its whole directory, or, on a 'chronicle' mount, where the workspace tracks something beneath it.
    • What's tracked: a pass reports what the store's tree, the disk agreement and the branch's intents track beneath its scope, whatever disk shows, such as a draft written where disk has no directory.
    • A reading walkStart stops short of, before a segment that isn't there spelled exactly so as a directory, a link, a directory the mount passes over or one it can't read, with nothing tracked beneath, has nothing a walk can list there. Its walk would start above it, often at the mount's root.
    • trackedPaths, taken out of gather in reconcile.ts, is the one list of those tracked paths, within the mount's include. gather takes its candidates through it, and searchWalks asks it (tracksBeneath). A direct mount tracks nothing.
  • What it gives up: a pattern as written that falls short there with nothing tracked doesn't name the regions its walk would have left incomplete. That's the usual case, a mount holding no directory named like itself, where main's walk would be of the whole mount from its root, and otherwise one beneath such a directory that disk doesn't hold whole. The tracked check is read outside the mount's turn, as walkStart reads disk, so a branch selected between the two can leave such a reading unwalked.

The other conflicts

  • glob's description: this branch's sentence, then main's note for a direct mount.
  • grep's glob description: this branch's, with main's integer types for the context counts.
  • The handlers: main's, with the prefixed reading's walk added. main's regex for the pattern as written is built first, as main builds it, so a pattern globToRegex refuses is still refused before the path is read.
  • walkCap's doc and grep's comment name the second walk.

The brace-group fix

A **/ right after a brace group was always read as not starting a path segment. So after {src/,lib/} it still needed a directory, though each alternative ends one and {src,lib}/**/*.ts finds the same files with none. Whether it starts a segment there depends on the alternative that matched, so a **/ at the start of any part of a pattern now reads by what comes before it in the path. Where the path so far is empty or ends with a /, it matches zero or more directories, and otherwise any run of characters and then a directory, as before. So {src/,draft}**/*.ts matches src/main.ts and draftx/y.ts, draft{s,ing}**/ still needs a directory, and an empty alternative ends a directory only where the group starts one. The changelog fragment's **/ sentence now says "at the start of a path segment".

Tests

  • test/workspace-glob-mount-prefix.test.ts, as on this branch, plus:
    • a **/ after a brace group, after alternatives that end a directory, one that doesn't, and an empty one, at a segment's start and not;
    • a **/ that starts a brace alternative, at a segment's start and not;
    • a pattern's +, . and parentheses matching themselves, against names their regex reading would also take.
  • In test/workspace-three-way.test.ts, beside the cases fix(workspace): glob and grep start where their pattern does anima-research/agent-framework#511 added:
    • past the walk cap, a mount-prefixed pattern walks from the directory the rest of it names, in glob and in grep's filter, and names no region outside it, both where the mount has no directory named like itself and where it has part of one;
    • beneath such a directory, past the cap, each reading finds its own;
    • on a direct mount, each reading finds what lies beneath the ignored directory it names;
    • a draft the workspace holds beneath such a directory that disk lacks is listed, past the cap too;
    • where both readings walk, a directory both walks name is named once, with main's account of it, and a file both reach is given once, as a match, a result and a skip;
    • the pattern also matches as written beneath such a directory.

Checks

On Linux x86_64 with Node 24.22.0, at this pull request's head:

  • npm run typecheck and npm run build are clean, and npm test passes, 5,984 tests, 5,940 passing, 44 skipped, none failing.
  • The workspace* test files pass, 964 tests, 920 passing, 44 skipped. The merge commit alone passes workspace-three-way.test.ts and workspace-glob-mount-prefix.test.ts, 373 of 373.
  • This branch's own file at main (97ca7eba): all 7 of its cases fail. A mount-prefixed pattern finds nothing in glob or in grep's filter, and **/*.md misses each mount's top-level .md files. At this branch's head before the merge (92092ab2), the same file passes 7 of 7.
  • Each part's test fails without it. Each of these is one change at the head, run over those two files:
    • no prefixed walk: 16 fail, this branch's cases among them;
    • each walk starting at the search's own path: 11 fail, among them the direct-mount case and the past-the-cap cases fix(workspace): glob and grep start where their pattern does anima-research/agent-framework#511 added;
    • the pattern as written always walking: the past-the-cap test of mount-prefixed patterns fails, since a walk from the root names regions outside the rest's directory;
    • nothing counted as tracked: both draft tests fail;
    • gather not taking its candidates from trackedPaths: 47 fail, since gather then takes no tracked paths at all. That shows gather needs them, not that the list is shared;
    • two walks' entries not deduplicated: the tests that a file both readings reach is given once and that a directory both walks name is named once fail;
    • the prefixed walk's entries kept first, and a region kept once per path and reason, not per path: the directory test fails each time;
    • a **/ at a part's start always matching zero or more directories: 3 tests fail, the brace-group ones;
    • a **/ at a part's start always needing a directory: 5 fail, among them this branch's **/ tests;
    • no escaping: the escaping test fails;
    • main's source under this head's tests: 16 fail.
  • The **/ reading against brace expansion: Beth-2709 compared glob's pattern reading with the union of each pattern's brace expansions, read without braces, over 1,680 patterns and 2,379 paths: 3,996,720 answers. This head differs from the expansion in 100 of them, against 456 for this fix's first version. All 100 come after an alternative that ends in **, such as {a/**,b}**/x, whose expansion puts **** inside one segment. Patterns without braces gave the same answers before and after the change, all 478,296 of them.

What each part is for

This weighs what the merge and the brace-group fix add. This branch's own parts are weighed in anima-research#401.

  • searchWalks: the one place both handlers take their walks from, so glob and grep can't disagree about where a mount-prefixed pattern's matches lie.
  • A walk per reading: the pattern as written keeps main's walk whole, so nothing main finds is lost. The second walk is made only in a mount where both readings walk.
  • Walking the pattern as written only where its directory is reached or something is tracked beneath it: without the first condition, a mount-prefixed pattern would walk the whole mount from its root. Without the second, a draft the agent wrote beneath a directory named like the mount would be dropped from the result without a word.
  • trackedPaths shared with gather: the check counts what a pass would report, from the same list, so the two can't drift apart. A run comparing gather's candidates before and after the change, members and order, found no difference in 1,062 gathers across the workspace tests.
  • prefixedGlob: the prefixed reading, with its directory. The pattern as written keeps main's regex.
  • mergedWalks: two walks' entries, each once, in path order, a directory both name keeping the relative reading's account. One walk's are left exactly as it gave them, at no cost.
  • grep's once-only search where two walks run: a text both walks hand on is searched once, not twice. One walk searches as main does.
  • The brace-group fix: {src/,lib/}**/*.ts and {src,lib}/**/*.ts give the same files, and {src/,draft}**/ reads each alternative as its place in the path does. A part no longer carries whether it starts a segment, since the regex reads that from the path.
  • The escaping test: a pattern's +, . and parentheses had no test that they match only themselves, which the brace-group code builds its regex around.
  • Left out:
    • one walk from where both readings' matches lie, for the reasons above;
    • checking what's tracked inside the mount's turn, which would move the walk's start into the pass, for a race walkStart's disk read already has.

Written by Gordon-2414 (Claude Opus 5.5). An internal review run read the merge's first shape, one walk, and found the miss that led to a walk per reading. A second run read this shape, and its findings judged worth fixing are fixed here. Beth-2709 (Claude Opus 5.5) read this shape and approved it, with runs of her own, and then the fixes she asked for and the second run's code fixes.

Description checked and posted by Lottie-2661 (Claude Opus 5.5).

🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling.

Meg-2646 and others added 30 commits October 19, 2026 03:28
…whatever its age

restoreReceipts skipped every record older than the receipt retry window
(an hour by default) before it read anything else, so a host that came back
more than an hour after a crash in the snapshot throttle's window forgot a
subject's ended first view, and the subject's next batch or birth could be
granted one the agent may already have had. A record whose subject the
restore didn't hold dropped the end the same way. The end is a fact about
the subject, not about the receipt: each record that carries it now ends the
subject's first view whatever its age, and a subject the restore doesn't
hold yet comes back with that end and an unknown history. The retry window
still bounds the receipt itself and the rest of the record, as before.

Tests: the crash-window test now also ends the first view by a plain
occurrence that displaces the shown render, beside the retraction; a
snapshot taken before the end, with the records after it, restores the end
onto the snapshot's subject; and records restored two hours later still end
it, so the edit of a later initial plain occurrence is no first view.

Text: five comments that name the default publish channel, the incoming
anchor or the refused agent's locus as the latest message that isn't a
reaction, reaction removal, edit or deletion marker now add "apart from an
edit the agent sees first", which sets them like the message it replaced.
The changelog says a compile that throws gives back its move of the agent's
shown position unless something has moved it since.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ved only to hide whether a name is taken goes (R1-1)

The include exists to keep files out of a mount's content: what it syncs,
stores, serves and writes back. This change had also tried to make every
answer the same whether a name the include leaves out was taken. That grew
a walk asked before every answer the kernel gave, at about twenty sites,
and each merge of main had to carry it to new ones, while existence still
showed by simpler routes. Whether a name is taken was never what the
include protects, so the guarantee narrows to content:

- No workspace path reads, stores, serves or writes back a file the include
  leaves out, a nested mount's included, and no tool lists, matches or
  names one. A refusal may show that something occupies a path the caller
  named. On a mount that follows links, it may also say whether the path
  leads out of the mount or into a nested mount that covers it, with the
  path to use there.
- Gone: landing and landedFor and every site that asked them, realBeneath's
  out-parameter and the real roots kept for it, grep's check of a path
  before it asks disk whether the path names a file, and locateDir and
  parentPlacement withholding a nested place. Each site that names a place
  in a nested mount's directory names it only where it's a directory or a
  file that mount's include covers (namesNestedPlace), judged at the place
  realpath gives. The file tools' parse also leaves a place beneath a file
  there unnamed, since its path to use would name the file. locateDir takes
  a place it doesn't name as missing, and parentPlacement as gone, as for
  any file.
- A link the include doesn't cover is outside the mount itself. On a mount
  that doesn't follow links, its refusal names nothing of where it leads
  (shownLeads), and its code is the one for a link where it stands: symlink
  at the path itself, judged on the parsed path whatever its spelling, and
  parent_symlink on its way.
- A `..` takes back the segment before it on a mount with an include too,
  so the include judges exactly the path every tool acts on. The include's
  own refusal of a `..` goes.
- resolveAbsolutePath compares a path as given with a nested mount's
  directory loosely at every level, as it already did before the first
  start, so the measurement of each level at each start goes (foldsAtEach,
  LevelFolds). Its one cost is a null for a directory of the outer mount's
  own spelled like a nested one in another case, on a disk that tells case
  apart.
- Kept: the push asks where a file lies before it hashes it, so no file in
  a nested mount's directory is read for a hash; a walk passes over a
  nested mount's directory by its real path and by identity; and covered
  history beneath anything the include passes over that isn't a directory
  or a link, a FIFO included, is taken as absent.
- Texts: the include's doc and fragment state the guarantee, how it holds,
  and the three cases outside it, where nesting is matched by spelling or
  placed at each start. The codes' doc, followSymlinks' doc and the nested
  roots' and parent links' fragments say what a link the include doesn't
  cover gets.

Tests: the walk's tests go, with those that pinned answers alike. The
nested-naming tests name a place only where that mount covers it, and
nothing beneath a file there. New: a covered link reads through its own
name, history beneath an uncovered FIFO is absent, a link at the path
itself is symlink however the path is spelled, and a covered directory
link names its target where an uncovered one doesn't.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es no turn of the mount (R1-2)

handleFsChanges took the mount's turn and ran a pass for every batch, and
only gather then dropped the paths outside the include. On a narrow include
over a busy directory, each batch held the mount's turn against the agent's
own tool calls and changed nothing. It now drops what insideMount refuses
first, and a batch with nothing else takes no turn. A covered path's own
change still comes as its own event, and gather still drops the rest.

Tests: with a real watcher, a write outside the include takes no pass and
wakes no one, and a covered write after it is taken in, as a control.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reaking (R1-3)

It asks operators and hosts to act. A draft stored under an earlier
spelling that never reached disk has to be recovered by hand. A host that
matched the traversal refusal's text meets new words. A path with a
separator right after the mount's name, which readFileFromDisk read, is
refused. workspace-path-spellings.breaking.md says so for each audience,
with what to do and what's unchanged, and the fixed fragment keeps the bug
fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An agent couldn't see a mount's include until a path outside it was
refused, though the refusal already prints the patterns. The list of
mounts now gives each mount's include where it has one, and the tool's
description of its path says so.

Tests: ls without a path lists a mount with its include and one without.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ler resolves the root first

This change had made isContainedPath resolve the root it was given, so a
mount path written with a trailing separator would hold its own root. Every
caller already passes a resolved root, or one realpath gave, so that never
ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a nested mount's already is

On a mount that follows links, the include was judged only where a path
is named, so a link the include covers read and wrote whatever file it led
to: with `include: ["*.ts"]` and `cov.ts -> secret.txt`, a read served
secret.txt, a sync stored it, grep matched it, and a materialize of a
write to cov.ts wrote into secret.txt. The include is the operator's
statement of which files' content the workspace holds, and a link is a
path, not a grant of content, so a file the include leaves out is no file
of the mount's, however a path reaches it.

- coversLanding judges the mount's own include at the canonical place a
  path lands, as namesNestedPlace judges a nested mount's place, beside
  the nested check at each site that asks nestedAtReal where a followed
  path lands to a file: the file tools' parse, so read, write, edit and
  delete refuse such a path before anything is stored; observePath, for a
  link at the path and, through the parent's real path parentPlacement now
  returns, for one on its way, since a pass takes a copy whose fingerprint
  its stat vouches for without opening it; openContained; the push's
  location check; unlinkContained, before the hash, so the file isn't read
  for one; and readFileFromDisk. A file a write would create is
  judged where it lands before anything is made, and a walk of a
  directory a link put elsewhere than its path judges each file where it
  lies.
- The place is judged as disk spells it, as a path given directly is: a
  path that meets no link lands where its parse named it, but through a
  link, realpath keeps the spelling the link's target gives, which on a
  disk that folds names need not be disk's. So only a path that went
  through a link reads its directories' spelling, and a walk reads it
  once for each directory it lists elsewhere.
- Such a path is refused as one that leaves the mount, in the words a
  place a nested mount's include leaves out gets (escape). A mount without
  an include judges nothing more.
- Texts: the include's and followSymlinks' docs and the fragment say so,
  with a link to a directory under a `dir/**` include as the example. They
  also give the full reach of a nested case outside the guarantee: on a
  disk that folds names, through a link that spells a nested mount's
  directory otherwise than disk does, the outer mount reads, lists,
  searches and writes there as its own, creating files too.

Tests: the test that a covered link reads through its own name turns
around. A covered link to a covered file reads through, while a covered
path that reaches a file the include leaves out, through a link at it or
on its way, is refused by read, write, edit, delete and readFileFromDisk,
isn't stored, matched or listed from where the link leads, and where disk
puts the link after a change was taken, no materialize, forced or not,
writes through it, creates a file where it leads, or removes it. A copy
history took in before the include, at such a path, is neither vouched
for nor searched. The checks at the act, called directly, refuse it too.
On a disk that folds
names, the landing is judged as disk spells it, both ways, and in a
listing from where a link leads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main brought the change that gives a draft one spelling on a disk that
folds case: the parse respells a path with the workspace's entries, so a
path that names an entry keeps the entry's spelling. Where both changed
the same code:

- parsePath respells with the workspace's entries, as main does, then
  keeps the rest of a path as given past a nested mount's directory, as
  this change does. With `.`, `..` and empty segments gone before the
  respelling, each segment keeps its place, so the two compose. Its doc
  states both rules. nestedDepth and entriesOf both stand.
- The observe import takes both sides' names, and this change no longer
  uses linkLeads there.
- workspace-path-spelling.test.ts takes main's counts in its header, with
  this change's test of an absolute path run on any disk, and both sides'
  tests.
- readNesting returns main's real roots alone, since this change no
  longer keeps its own.

The include's check of where a followed path lands respells without the
workspace's entries, so it judges disk's spelling of the place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… whatever spelling realpath gives

The include's check of where a followed path lands took a landing spelled
otherwise than its path for a link it had followed. That holds where
realpath keeps each name as given unless it's a link, as on Linux. On
macOS and Windows, realpath gives disk's stored names, and the parse can
now keep a workspace entry's spelling that differs from disk's, so a path
with no link would have been judged as disk spells it: refused where the
include covers the entry's spelling and not disk's, in words that name a
link that isn't there, and judged differently by platform.

- judgedAt gives the path the include judges: the path as named where the
  two spellings agree, or where no link lies on the way (linkOnTheWay's
  walk, asked only where they differ), and otherwise the landing as disk
  spells it. coversLanding asks it, and a walk of a directory a link put
  elsewhere asks it once for the listing, so the rule sits in one place.

Tests: with a landing spelled otherwise and no link on the way, the path
is judged as named; through a link, where it lands; and without an
include, every file is covered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…judged where the include says

The include still met a file it leaves out, or named one, in these
places.

- An unlink judges where a regular file reached through a link among its
  directories lands, before it hashes or removes it. The check sat only
  in the branch for a link at the path, so a forced deletion through a
  followed directory link removed a file the include leaves out.
- A pass judges history in a nested mount's directory by that mount's
  include, so what the outer mount took in there before the mount was
  nested, and that mount's include leaves out, is inert history that no
  listing, sync or grep names.
- A link's target is judged and named as disk spells it: realpath keeps
  the spelling the target gives, so on a disk that folds names a covered
  link's refusal could name a file disk spells outside the include.
- What an answer shows keeps the caller's spelling from the first segment
  on a path's way that disk has as something other than a directory and
  that the include doesn't cover. A link refusal, a pass's note on a file
  tool's path, and what a sync of a path says it skipped name such a
  segment the same way, so no answer names it as disk spells it. Every
  check still judges the path as disk spells it, and directories keep
  disk's spelling, as a listing shows them.
- A create judges where the file would land, a nested mount's place
  included, before anything is made: one function, which the opened
  file's check shares, judges a landing for both.
- judgedAt takes a path as named only where its landing is spelled
  otherwise only as folding makes it and no link lies on the way, so a
  link swapped for a directory between the realpath and the walk can only
  make the landing judged, and a link to a directory spelled otherwise
  only in case, on a disk that tells case apart, is judged where it
  lands.
- The entries the spelling step reads leave out keys no spelling names,
  which are outside the mount.
- Where a nested mount's holder has no include, a place there is named
  whatever its kind, so the stat that would tell its kind isn't made.
- A refusal shows a configured bigint as JavaScript writes it, where
  JSON.stringify threw.
- Texts: the include's doc and fragment give the spelling rules on a disk
  that folds names and four nested cases outside the guarantee, a place
  reached through a link being judged in its target's spelling among them;
  the follow advice holds where the include covers what a link leads to;
  the codes' doc, the payload saver's and the trace's comments, and the
  parent-link test's header say what holds.

Tests: an unlink through a followed directory link, forced or expecting
other bytes, and a write expecting other bytes, are refused before any
hash; a create leaves nothing at a nested mount's absent root; history in
a nested directory that mount's include leaves out isn't listed, synced
or matched; a covered directory through a link is a directory to the file
tools; a landing spelled beyond folding with no link left on the way is
judged where it lands, and so is a link to a directory spelled otherwise
only in case where disk tells case apart; the watcher's test waits for its batch's handling
and counts the mount's turns. On a disk that folds names: a covered name
whose file disk spells otherwise is outside the include; an uncovered
file or link on a path's way keeps the caller's spelling in a read's,
a delete's and a sync's answer and a disk read's refusal, while the
include still judges disk's spelling; a
link's target is named as disk spells it, both ways; two nested levels
keep the path as given past the innermost; and a key no spelling names
spells no write. The validator refuses `*.js*` and shows a bigint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ys absent

accumulateUsage dropped a tally's estimatedCost when an inference was
priced in a second currency, but the next priced inference found no cost
and started the sum again from itself. On main, after USD 1, EUR 2 and
USD 3, the totals and the agent's entry both report 3 USD. That held for
the totals and every entry, and the shared ephemeral entry, which pools
every subagent, would meet it whenever two subagents' models were priced
in different currencies, as a host's own model registry can price them.

A tally now records that two currencies met with mixedCurrencies: true,
and its estimatedCost stays absent from then on. The mark is stored and
restored with the tally, a stored mark wins over a cost stored beside it,
and the fold at restore carries a stored entry's mark, or a mix among the
entries it folds, into the shared entry. At restore, the totals are also
marked when the restored entries' costs are in two currencies, or against
the totals' own, or an entry is marked, since the totals counted every
entry's inferences; a record written before the mark shows a mix only so.
An inference membrane couldn't price still adds only its tokens, so a
tally's cost is a lower bound when any were unpriced, as the totals'
already was; the doc says so.

Tests: the fold tests seed the stored totals and inference count with
their entries, and check that the fold leaves them as stored and that the
next inference adds to them. The currency tests check tokens as well as
cost, an unpriced inference into a priced entry, the fold of a mix, and
the totals' mark at restore. A subconscious configured under its own
name, disabled and then enabled, keeps its stored entry. The end-turn
test compares the shared cost within 1e-10 rather than rounded to cents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kind refuses '.', '..' and a file's trailing '/'

Takes out the path normalization 42e0533 gave every mount: anima-research#536 already
normalizes every mount's paths in parsePathAsGiven, with rules of its own,
so that rule is anima-research#536's to make (Ada-1017's call). parsePathAsGiven and parsePath
are as 6aadccb has them, the two existing tests' spelling edits are
undone, and changelog.d/workspace-path-one-spelling.changed.md goes.

In its place, the direct kind refuses these spellings itself: parsePath
refuses a direct mount's path with a '.' or '..' segment, which a
store-backed mount's store refuses as an invalid tree path, and
parseFilePath, which the file operations parse with, a direct mount's path
ending in '/', where a store-backed mount's read finds no file. Both read
what parsePathAsGiven gives, so once anima-research#536's parse lands they see no such
path. A directory's path may still end in '/'.

From Esther-2584's read of 6aadccb..42e0533:
- The alert that a mount configured direct runs as 'chronicle' counts an
  intent the workspace can't read as one, not as a change, for one mount
  and for several.
- ls without a path and status give a mount configured direct its backend
  as it runs this session ('chronicle' for the fallback) and the ignore
  list it walks by, which no config shows.
- The fragment says one ops alert names every such mount.

Tests: the normalization test is replaced by one of the direct kind's
refusals; the fallback test checks its ls and status; the held-kinds, ten-
at-a-time and two-mount tests check the alert's counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… arrives, and a retraction or plain occurrence takes the whole run

A render describes what changed since the server's previous render
(RFC-006 §5.2), so a later render can't stand in for an earlier one the
model hasn't read (§3.3's last paragraph, vector 49). The coalescer kept
one occupant per subject and treated a render as it treats a plain
occurrence:

- a render settling while an earlier render was still unread took the
  subject's slot, so a retraction withdrew it alone and left the earlier
  one queued (anima-research#548);
- a notice arriving after a render had landed unread displaced it, as
  §5.1 has a notice displace an unread plain occurrence, so the next render
  covered only the new notice and the earlier span reached no one.

The occupant now marks a render (`rendered`) and carries the earlier
renders it builds on (`before`, oldest first, each kept whole). A render
settling while such a run is there joins it. A notice leaves an unread
render in place, and its batch follows the run's agent (deliverTo); so
does a batch opened while the render was under way, once the render lands
unread, and its wakes go with it (wakeForBatch), so every render of a run
lands in one agent's context. The receipt doesn't name the unread render
as a prior, since it was neither displaced nor found consumed. A
retraction and a plain occurrence remove the run newest first
(removeOccupant), stopping at a member the host can't remove, which keeps
the renders it builds on; what the walk leaves becomes history
('unknown'), as an occupant whose removal failed always did.
refreshOccupant checks each member: one read or gone becomes history, and
the run keeps those still unread, so the walk removes nothing the model
has read.

snapshot() writes the occupant's eventId and placement, which is all
restore() reads, so the stored shape is main's; after a restart nothing of
the subject is tracked as unread, as for any stored occupant. The README's
coalescing paragraph says that neither a later render nor a notice
replaces an unread render.

Tests: mcpl-coalescer-render-run, over a stub host whose deliveries wait
unread in one agent's deferred queue, covers anima-research#548's sequence, a batch
opened during a render, a notice between two renders, a plain occurrence
over a run, removals that fail at the newest and at an earlier render, a
render read before a retraction, a three-render run, a gone render in the
middle of one, a lone render that stays a render and stays removable,
and the snapshot. In mcpl-coalescing, a new test has a notice arrive
after a render landed in a live turn's queue, and one call reads the first
render, then the second. Two tests where a newer batch was
open while a render waited in the primary's queue are turned round: that
batch, with its wake, now follows the primary, and in one of them the
other agent's assembly no longer renders it. The changelog fragment says
what agents see.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Answers an internal review of e566cc6.

The fallback alert:
- Its words of fixed length come first: what's happening and the two ways
  to start a mount direct, then the mounts with what each holds, then the
  held list, so the webhook's kept head (about 346 of 500 characters) holds
  the ways out however many mounts it names and however long their names.
- The materialize route reads as steps: configure the mount 'chronicle'
  and read-write, materialize it, and follow materialize's answer.
- With several mounts it says to settle each change held at a mount to
  start that mount direct.
- An intent the workspace can't read is named with the way that settles
  it, a sync of the path, since materialize's selection leaves such an
  intent with no workspace entry unselected.

The decision:
- chooseKinds decides every mount before it sets any, so a read of the
  store that throws leaves the earlier decisions and fails the start.
- A mount configured direct that ran as 'chronicle' starts store-backed in
  initStore, and runs direct only once a decision finds nothing held.
- Tests in the host's order (start, then initStore) and the store-first
  order, and of a decision that can't read the store.

readBytes: every refusal from the open asks observePath, so a
link to a directory it can't open gets the listing's reason, and a regular
file it can't open keeps the open's.

Tests: the grep order test holds its first file's reads until
fifteen others are read, and asserts it; the inOrder test settles its works
in reverse by hand; each held kind is settled by a sync of its path and the
next start is direct.

Text: a mount that ran direct and runs as
'chronicle' again takes in disk's changes as its listings, searches and
syncs reach them; a direct read that finds its root replaced as it opens
the file is refused; watch 'never''s doc says a read observes only a path
the store doesn't hold; MountState.direct's doc names both decisions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…error

Under Bun, a failed WebSocket dial's message quotes the url as dialed:
its user, password, path and query, and the token, static or an access
provider's. WebSocketTransport.open carried that message on in its own
error, which reaches the caller of connectMcplServer as raised. Its log
line was masked; the error wasn't.

dialErrorText shows each ws(s) url in the client's text as redactUrl does
and replaces the configuration's and its dials' credentials
(redactServerText), for the 'error' path and for a client that throws as
the socket is constructed. The error is a new one with no cause. Bun
emits an ErrorEvent, not an Error, so its text is read from `message`.

Raised in review of connectome-host#343 (item 2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efore its credentials

From review:
- A credential in the url with a parenthesis, quote or bracket was cut by
  the url matcher before redactServerText could replace it whole. Now the
  url as dialed and as configured are replaced first, then the known
  credentials, then any other ws(s) url, matched to a space or quote.
- An error the WebSocket client emits after the socket opened went to the
  connection's listeners and logs as raised: now masked too
  (clientErrorText), as a new error with no cause.
- The Bun test asserts a refused dial carrying the client's own text, not
  the open timeout, and CI's pack-check job, which has bun, runs it with
  AF_TEST_REQUIRE_BUN so a missing bun fails rather than skips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t be read

From a verification review: clientErrorText read config.url outside a
guard, so a configuration whose url getter throws made the socket's
'error' listener throw, with nothing to catch it. It now returns
WITHHELD_TEXT, as redactServerText does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion

WebSocketTransport's constructor takes the configuration and the url it
dialed, which its errors are masked with (clientErrorText), in place of
the server id alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ends the run as failed, and a settle says when its result was dropped

From the round review of the engine's last round:
- An exception that escaped the script's promise chain once it was running, from a callback it
  gave queueMicrotask or a FinalizationRegistry (whose jobs pass a throw on) or from one of
  QuickJS's own jobs failing, was answered as rejected. Nothing told it from the script's own
  rejection, and after a rejection the binding had kept, the failure was never answered at all,
  since both had the same state code. It now has its own state, ENGINE_FAILED, and answer,
  `failed`, which every later call gives. `rejected` stays the script's own: its completion
  promise rejected, or the prelude or the script threw as it started.
- A settle whose result never reached the script, because the run had already ended (a stop or
  a failure) or the engine failed before it could deliver it, now says so: ENGINE_DROPPED, and
  `dropped: true` in its answer. The early return after a failed read of the call's capability
  also keeps ENGINE_UNFIT now.
- Comments and docs: the stack limit's account is scoped to recursion through calls, and names
  the recursion in C it doesn't hold (the parser, chains of iterator helpers, JSON); the
  interrupt slot's doc names the stop after a whole ending; engine_settle's comment gives the
  depths measured under default settings and says when the unfit error's copy is freed; run_jobs
  says which jobs pass a throw on; call() and dispose() say why any unexpected exception leaves
  the module as it is.

Tests, under Bun 1.4.3: with the last round's binding and module, the two new behaviour tests
fail, and so do the three whose settles after a stop now carry `dropped`; with only the module
taken back, the same five fail. Taking out each new piece fails its test: a job's failure given
the rejected state, or answered as rejected; the dropped flag in the module or in the binding;
the final release of the copies (2,000 settles, half for ids no call has, would take far more
room); the check of a slot's failure after a read; dispose()'s catch and its mark; the ending
bound's comparison, now pinned at exactly MAX_ENDING_BYTES; and the error copy's retry, whose
test now checks that the first copy was refused. Not pinned: freeing the unfit error's copy
before the script's code runs rather than as engine_settle returns, which leaks nothing either
way.


At the clean head, on Linux x86_64: tsc is clean; all 12 code_execution test files pass, 287 of
287, under Node 24.22.0 and Node 20.20.2 file by file and under Bun 1.4.3; and a rebuild from a
fresh toolchain fetch gives the committed module, 599,900 bytes, sha256 23cb4945.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd, a link among the patches and an engine/patches that isn't a directory, and puts the committed copy back if stopped

From the round review of the engine's last round:
- A patch that left a listed file executable, a link or a submodule passed the name check, and the
  copy wrote it as a plain file. Each listed file must now have mode 100644 after the patches.
- A link in engine/patches, which -f followed, was applied; and an engine/patches that was a file
  or a dangling link was passed over, so the release was copied unpatched. Both are refused now.
- If the script stops between moving the committed copy aside and renaming the new one in, its
  exit trap puts the committed copy back and says so; once the new copy is in place, the trap
  removes the old one.
- The restore command it suggests names the repository with git -C, and quotes both paths.

Checked in a scratch clone with quickjs-ng v0.17.0: a patch making list.h executable, one
turning it into a link, a link to a real patch, and engine/patches as a file or as a dangling
link are each refused with engine/quickjs-ng unchanged; a copy of the script that signals itself
between the two renames put the committed copy back; the restore command, run from /, restored
the copy after it was removed; and clean runs match the committed copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an hold

An earlier version could store an entry under a key with an empty segment
or a trailing `/`, such as `a//b.txt` or `c/`, but never one with a `.` or
`..` segment: the store refuses those keys, which is why a write to
`work/./a.txt` failed with "Invalid tree path". So no operator's history
holds a `./` key for this change to leave outside the mount, and the note
for operators names `//` and a trailing `/` alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nfigured

From review of tokenTransport 'header':
- A header-mode credential must be printable ASCII (U+0020 to U+007E) with
  no space at either end. Node and Bun send exactly those bytes, so the
  server checks the credential that was configured, and the redactor's
  patterns match what it echoes. Outside that set the bytes differed. Node
  sent a trailing space or TAB and its HTTP parser stripped it, Bun
  stripped it before sending, and Node sent é as the byte e9 where Bun sent
  c3 a9. serverConfigProblems refuses a static token outside the set, so
  it no longer fails every background redial, and the dial refuses an
  access provider's. Neither refusal quotes the credential.
- 'header' with a url that carries userinfo is refused too, beside the
  url-token refusal in tokenTransportProblem, so the configuration check
  and every dial refuse it. ws sends the userinfo as the request's Basic
  credential, and a request has one Authorization header, so whenever
  header mode's Bearer header carries a credential it replaces the
  userinfo's. Query mode sends both, as before.
- The catch around new WebSocket goes. Under the character rule no
  credential the runtimes refuse reaches the constructor, and neither
  runtime's refusal quoted the value.
- The tokenTransport refusal follows config-checks.ts. null counts as not
  set, and the refusal names the value's kind (kindOf), never the value,
  which could be a credential pasted into the wrong field.
- Tests cover the rule's edges at the dial, the static token's refusal, the
  userinfo refusal, a header-mode dial with no credential, and a
  header-mode server's text before any dial, its static token and its
  url's pairs as dialed. The failed-dial test drops a console capture that
  no line on its path reached.
- The fragment says which refusals a dial repeats. Two comments and the
  pending redaction fragment now say a dial that sends its token in the
  query.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Answers an internal review of a25a425, and Esther-2584's read of it.

The fallback alert:
- The materialize route ends by restoring the mount's direct config, after
  "then start it again"; it left the mount configured 'chronicle' and
  read-write before, so its next start ran as configured (found by Esther-2584).
- Where an intent the workspace can't read is held, the ways out say only a
  sync settles one, past the ten paths named too, and its label says only a
  sync of the path settles it: materialize doesn't, and a sync does where it
  can read disk's copy.
- The lead no longer says why it keeps the mount store-backed, which the
  mounts and their counts say after the ways out, so with several mounts
  the fixed head runs 332 characters, inside the webhook's kept start.

The direct kind's refusals say "doesn't take" and "give the
path ... without it", since ls, glob and grep name directories and write,
edit and delete don't read; parsePath's comment gives their reason, the
store-backed kind's own refusals, which an empty segment isn't among.

Text: types.ts says the refusals are the tools' and the
host's reads', and that a generated file is answered, and
resolveAbsolutePath maps a path, as on any mount.

Tests: a decision whose read of a second mount fails leaves
the first as it was; the same module started again on a branch that holds
a draft runs as 'chronicle'; the intent sentence past the ten paths named,
and within the webhook's kept start beside long mount names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s of the mount

The test counted every turn of the mount taken after the uncovered write,
so a turn that any other batch took in that window failed it as well. On
macOS one run failed with one turn, and the test couldn't say whether the
uncovered file's batch or another took it. The handler asks for its turn,
if it takes one, before its first await, so the turns counted across its
call are its batch's own. The test now asserts no turn for a batch of
nothing but the uncovered file, and no pass naming it whichever batch
hands it over, and a failure names each batch with the turns it took.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is reported once, and an unreaped launch is held first

Anarchid's review of anima-research#540 found four gaps in the two MCP connection
classes
(anima-research#540 (review)).
anima-research#556 holds the two it set aside; the review itself gave R1-1 and R1-2.

anima-research#556's first: a call whose arguments JSON can't write (a BigInt, a
cyclic object) put nothing on the wire, but the modern engine read it as
no-response, "whether it took effect is unknown". ObservedTransport.send
marked the request handed off before the inner send, and undid the mark
only for NotWrittenError, while the serializer's TypeError came from
inside the inner send. It now writes an observed request's JSON before
the mark, so a call whose first leg JSON can't write reads not-sent over
stdio and over HTTP, and the transport is still handed the caller's
message. The legacy engine already refused such a request as not sent,
but said "Try again later"; sendRequest now serializes it before it
keeps anything, and refuses it in the modern engine's words: "If calling
it again fails, ask whoever runs this host."

anima-research#556's second: the SDK's HTTP send reports a failure through the
transport's onerror and then throws the same object. So a failed
request reached whoever sent it, and also raised the connection's
'error', which the framework traces as mcpl:server-error, or with no
'error' listener logged a second line. While a request's or a
response's send is in flight, ObservedTransport now holds what that
send's transport reports, and once the send settles it forwards all of
it but the error the send threw, which its sender reports: a call
through its outcome, the connect through its failure, the SDK through
its own report. A notification holds nothing, since its sender may
swallow its failure, as the SDK's listen teardown does. The scope is per
send and names its transport, and a settled send's scope lets go of it.

R1-1: when a stdio child's pipe failed and the child outlived SIGKILL,
the modern engine held the launch only once the reap's close had failed,
after that close had ended the pending calls. So they, and a call made
as the connection closed, read "this host is reconnecting to it" one
tick before reconnecting halted for good. The transport's 'unreaped' now
passes the reap's failure, and handleLost records the hold once it has
started the reap and before it emits 'close', as the legacy engine holds
it first. It reports the hold after 'close', where the other route
reports one too, in a finally. The permanent reconnect-failed comes from
the reap's continuation, as on the route where the reap finds the child
alive, and not when the host has closed the connection meanwhile, whose
close() then reports the failure itself. A listener that throws can't
skip the reap, the hold, 'close', the hold's report or the permanent
failure.

R1-2: a legacy connection redacted each error answer's log line with a
redactor built for that line, and for a stdio server the framework
launched, the configuration's env getter reads the Discord awareness
ledger from disk. The connection now keeps one redactor, made at its
first error answer; serverTextRedactor builds again only after a build
that failed, which withholds that line, or once a dial notes something
new.

Also: each legacy refusal as not sent, for these arguments, for a failed
pipe and for a write the transport refuses at once, rejects before it
logs, so a host logger that throws can't replace the refusal.
holdsUnreapedChild's doc says it may not read true yet while a reap
runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ender-runs

fix(coalescing): an unread render stays when a later render or notice arrives, and a retraction or plain occurrence takes the whole run
…turn's locus

An addressed re-pin at a tool boundary, or the agent's own channel_open,
moves a locus or hybrid turn's locus between rounds, and the next round's
prose is delivered there. driveStream computed the channel it shows once,
when the stream started, so for the rest of the stream the
inference:tokens and inference:content_block traces named the old channel,
and the typing indicator stayed there (anima-research#555). The turn-start comment still
said the locus is frozen.

driveStream now keeps that channel as a variable, and followTurnLocus
compares it with the turn's locus at the model's content in each round (its
tokens and block events, not the tool results a boundary reports as
content) and at a refusal retry. When the locus has moved, in locus or
hybrid mode, it moves the indicator there, and the traces, which read the
same value, name the new channel. A response with nothing in it moves
nothing, so a channel_open just before an empty final response doesn't
start typing in a turn that is ending. Explicit mode keeps its trigger
channel.

A stream that hands its turn to a successor (a budget restart, a guard
retry) leaves its indicator running. TurnRouting now records the channel
the turn's indicator was last shown in, and showTurnTyping, which every
start of the indicator and followTurnLocus go through, stops that one first
when it shows the indicator elsewhere. So a re-pin at such a boundary no
longer leaves the old channel typing until the turn ends.

Both name the conversation the turn is attending: in hybrid mode they
follow the locus whether or not a >>> envelope sends the text elsewhere.
The live outgoing stream is unchanged.

Tests: the re-pin rig's locus and hybrid re-pins, a pinned hybrid turn, a
stream with no block events, a refusal before the retried round's content,
a budget restart that re-pins, channel_open, an empty final response with
and without its tool's result reported as content, and a locus turn with
nowhere to speak at its start; an explicit turn's typing and traces across
its rounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node's ws names only the address in a failed dial's error, so under
Node no test failed without the masking at the dial. A test now has
ws raise Bun's words there. A request that throws as ws constructs the
socket reaches the catch around the constructor, and a url quoted with
an escape's hex in lower case reaches the last step, which masks any
other ws(s) url. The special-character cases gain a password with
brackets, and each now checks that its text was masked rather than
withheld. This drops the Bun probe's stderr check. Its configurations
don't reconnect, so a failed connect writes no log line to check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test counts each batch's turns of the mount across the handler's
call, which holds because the handler asks for its turn before its first
await. Nothing checked that. A handler that awaited first would leave
every batch's count at 0, and the assertion that the uncovered file's
batch takes no turn would pass whatever the handler did. The covered
change's control now asserts that its own batch's count sees its turn,
so such a handler fails the test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ixteen are under way

The test counted the most opens (for grep) and lstats (for the listing)
under way at once, holding each for a turn. Before its file's lstat, each
of the listing's observations resolves the file's parent directory, so the
count depended on how those steps interleaved: CI's macOS Node 20 leg
counted 15 for the listing, and holding every sixteenth realpath for
200 ms gives 15 on Linux too. inOrder starts all sixteen observations
before it awaits the first.

The helper now counts only the files' own calls, and holds them until
sixteen are under way, and 50 ms longer, in which a reader wider than
sixteen would start more. So the count shows the reader's width however
the steps before each call interleave: with that same hold, it counts 16.
A narrower reader is let go after 5 seconds, so it fails rather than
hangs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru and others added 26 commits October 20, 2026 03:04
…limits

fix(release): limit the CommonJS bin's --check and the bun probe
…p-ends-live-forks

fix(stop): end each live conversation fork as its release would
main moved to 0644506 (anima-research#616). In test/route-failure-targeting.test.ts
both sides added tests after the same test, and both are kept, this
branch's first. src/framework.ts merged without a conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ase, fetched, not a copy

engine/build.sh now clones quickjs-ng at the pinned tag into its own
scratch directory, checks the clone's commit against the pin, applies
the patches in engine/patches in the byte order of their names, and
compiles from there with the same relative paths as before. So the 19
files of quickjs-ng's source that engine/quickjs-ng held, 82,874 lines,
leave the repository, and engine/vendor.sh with them.

Its git runs with none of the machine's configuration: no global or
system config or attributes file, no hooks, templates from an empty
directory, and no GIT_ variable from the environment but the network's
(GIT_SSL_*, GIT_HTTP_*, GIT_PROXY_*). Any of the rest could rewrite the
checkout or a patch, or write elsewhere, while HEAD still names the pin.
So a proxy or a CA reaches the clone from the environment alone, and a
mirror a builder's git configuration names isn't taken.

Nothing that uses the package needs the C, since it runs
src/code-execution/quickjs.wasm, which is committed. What the copy
bought was CI's check that it was the pinned release with the patches,
which a build that checks the commit it fetches makes unneeded, and a
rebuild offline once the toolchain was cached, which the fetch gives up:
a rebuild comes only with an engine change. The engine workflow's
rebuild and compare stays, and builds the committed module's bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… still ends

The engine asked the child nothing more once it had a stop, and the child
looked for its parent only when asked whether to stop. So a child whose
script kept QuickJS from taking a stop, as a stack hook or an iterator's
return can, ran on once its parent went, with nothing left to end it.

The module gets a fourth import, host_supervise, which the interrupt
handler calls at every check, before it asks host_interrupt and after a
stop. The binding calls the child's supervise() whatever has happened, a
failed slot included, while a stopped guest is still asked nothing more
and a failed slot is still not called again. The child's look for its
parent moves from interrupt() to supervise(), which looks at most once a
stop check, so an orphan ends its process at the first check that looks
once its parent has gone, whether its stop came before its parent went
or not, on Linux and macOS, where an orphan's parent pid changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s left out

Reading the parts of what ended a run, error_part checked for a stop only
before each read, and error_json before the JSON or string form it makes
in place of a missing message. QuickJS drops a stop that lands in a stack
hook or an iterator's return, so a getter, a toJSON or a toString the
read runs could go on past the stop and give its text afterwards, which
then reached the ending. Each now checks again after the code returns,
and what's given or thrown after a stop is dropped, with its value or
its exception.

Tests hold each, and that a stack that isn't a string is left out with
its string form never made, beside a string stack that's kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as ended

__host and __output refuse a call or output once a stop has come, or
while the script's ending is read, and said "the script has ended, so its
calls no longer reach the host". The script's own code can read that
while it still runs, in a getter the ending's read runs or after a stop
it kept from landing, so each now says only "the script's calls no longer
reach the host", or its output, and a test holds both texts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d data ends its run

The docs said a script's own JSON nested about 10,000 deep (5,000 under
V8's optimizing tier alone) crashed its run under Node. Measured under
Node 24.22, the guest's JSON.stringify ends the run from about 5,000 deep
(4,000 with the optimizing tier alone), a call's arguments included, and
its JSON.parse from about 8,000 (5,000). Under Bun 1.4.3, QuickJS's
catchable RangeError comes first from about 15,000, though a parse nested
22,000 deep still runs Bun's stack out. MAX_RESULT_DEPTH's 2,000 keeps its
margin in every tier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… are cut alike

The changelog said each return's output keeps its first and last 8,000
characters cut alike to fit the agent's inline cap. They are, where the
rest of the return leaves them room; where it doesn't, the return is cut
as any long tool result is, as the tool's own text says. The changelog
now says so too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s first call in either order

The lifecycle test of a call still running when its script is stopped
asserted that the inner call starts before the run call completes. The
run call completes when its script yields, at 500 ms, which a child slow
to start beat on a loaded machine, failing the test with the code right.
It now takes those two in either order, and still holds the first event
to the run call's start and the last to the inner call's completion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…its call carried

puppetToolCall gives its call the agent's inline cap as resultMaxChars,
then stored the pair under the cap read again after the call. A
code_execution return fits its output to the cap its call carries, so a
puppeted script that lowered its agent's cap during the call rendered to
the old cap and was cut to the new one from its start, losing its
output's tail. The pair is now stored under the cap its call carried,
the agent's at the call, so what it renders and what it stores both use
that cap, and a test puppets such a script and finds its return stored
whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…trip

fix(mcpl): store a server's metadata without the keys only the host writes
Brings 0.23.2's release commit back to main: the ## 0.23.2 section of CHANGELOG.md and package.json's version. Source and tests merge to main's own text, since the release's change is a port of anima-research#622, which main already holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0.23.2's section carries it. The merge leaves main's copy in place, since v0.23.1 never had it, and without this main's next release would list the fix a second time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
messageIndexText took text blocks and private-prose tool arguments only.
A resident with proseRouting disabled speaks only through send tools, so
none of its own replies reached the index: it could find what others said
to it, not what it answered.

The message argument (`content`, or channel_publish's older `text`) of
the send tools and edit_message, under any MCPL prefix, is now indexed as
`[<tool>] …`, with or without includePrivateTools since it is public.
Other arguments (ids, topics, names) are not. The send tools come from
one set, src/send-tools.ts, which the framework's SILENCING_TOOLS now
builds on, so the two lists can't drift apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtZAC1EbTdNGzQrCCvaihA
The index takes what the resident wrote through a send tool, whether or
not it was delivered, and a send_dm reaches one person. So the comments
no longer call it public or everything the resident says, and the test
reasons from what isn't a private note. The test now covers send_dm.

The changelog fragment drops "drop the namespace to index them again".
That procedure hasn't been run, and a review traced a case where a
pending edit upserted first would leave the reset's later syncs starting
from a recent watermark.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ic-index-sent-messages

fix(history/semantic): index what the resident sent through a send tool
Brings anima-research#252 up to main (c4ba80b). Three conflicts, each kept from both
sides: tsconfig keeps main's `types: ["node"]` (TypeScript 7 build) and this
branch's ES2024.String lib; framework.ts keeps both sides' imports (this
branch's sourceBodyDigest, main's conversation-router facts); and
inbound-source.test.ts keeps main's two push-edit envelope tests in the
envelope suite, ahead of this branch's digest suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…review

Review pull requests with RevEngine
…stamps

feat(mcpl): stamp each inbound item's delivered-body and stored-copy digests
…cution-isolate

feat(code-execution)!: code_execution runs JavaScript in the isolate, and the Python runner goes
Since this branch's last merge with main, anima-research#511 made glob start its walk at
the directory its pattern names before the first wildcard, beneath its
path, and grep at the deeper of its path and its filter's directory, so a
search on a mount past its walk cap still reaches what its pattern names.
This branch gives a pattern whose first segment is a mount's name a second,
prefixed reading in that mount. Taken as main has it, the walk would start
at the relative reading's directory, which for notes/diary/*.md in the
notes mount is notes/diary inside it: not there, so walkStart stops at the
root, and the walk of the whole mount can stop at the cap before diary.

So each reading walks from its own walkStart, for its own matches
(searchWalks). Where two walks ran, their entries are given once each, in
path order, and a directory both name keeps the relative reading's account
(mergedWalks); one walk's are given as main gives them.
The relative reading's walk is main's: its start, the ignore patterns its
named scope sets aside, its cap. The prefixed one's starts at the rest of
the pattern's literal directory, from the mount's root and beneath the
path (the new prefixedGlob gives it beside its regex), and only adds. One
walk started where both readings' matches lie would lose what main finds
in a mount holding a directory named like itself: past the cap, by
starting higher, and on a direct mount within it, by keeping ignored a
directory each reading names (node_modules in work/node_modules/pkg/*.js).

Where a prefixed reading applies, the relative one walks only where
walkStart reaches its whole directory, or, on a 'chronicle' mount, where
the workspace tracks something beneath it. A pass reports what the store's
tree, the disk agreement and the branch's intents track beneath its scope
whatever disk shows, such as a draft written where disk has no directory,
so a relative reading walkStart stops short of can still match there. One
that falls short with nothing tracked has nothing a walk can list beneath
it, and its walk would start above it, often at the root. trackedPaths,
taken out of gather, is the one list of those paths, within the mount's
include: gather takes its candidates through it, and searchWalks asks it
(tracksBeneath). A direct mount tracks nothing. What's tracked is read
outside the mount's turn, as walkStart reads disk, so a branch selected in
between can leave such a reading unwalked.

Outside that race, a search gives every match main gives, and in a mount
where the prefixed reading applies, it makes a second walk only where both
readings walk. What it gives up: a relative reading that falls short
there with nothing tracked doesn't name the regions its walk would have
left incomplete. That's the usual case, a mount holding no directory named
like itself, where that walk would be of the whole mount from its root,
and otherwise one beneath such a directory that disk doesn't hold whole.

main's relative regex is built first, as main builds it, so a pattern
globToRegex refuses is refused before the path is read. prefixedGlob takes
the place of mountGlobs, whose relative half that regex is.

The other conflicts: glob's description takes this branch's sentence and
main's direct-mount note; grep's glob description is this branch's, with
main's integer context types. walkCap's doc and grep's comment name the
second walk.

Tests, beside anima-research#511's: past the walk cap, a mount-prefixed pattern walks
from the directory the rest of it names, in glob and in grep's filter,
naming no region outside it, where the mount has no directory named like
itself and where it has part of one; beneath such a directory, past the
cap, each reading finds its own; on a direct mount, each reading finds
what lies beneath the ignored directory it names; a draft the workspace
holds beneath such a directory that disk lacks is listed, past the cap
too; where both readings walk, a directory both walks name is named once,
as main names it, and a file both reach is given once, as a match, a
result and a skip; and the pattern matches as written beneath such a
directory. glob and grep each run on a mount of their own in these tests,
so neither answers from what the other's pass took into the store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ro or more

A `**/` right after a brace group was always read as not starting a path
segment, so after `{src/,lib/}` it still needed a directory, though each
alternative ends one and `{src,lib}/**/*.ts` finds the same files with
none. Whether it starts a segment there depends on the alternative that
matched, so a `**/` at the start of any part of a pattern now reads by
what comes before it in the path: zero or more directories where the path
so far is empty or ends with a `/`, and otherwise any run of characters
and then a directory, as before. So `{src/,draft}**/*.ts` matches
`src/main.ts` and `draftx/y.ts`, `draft{s,ing}**/` still needs a
directory, and an empty alternative ends a directory only where the group
starts one. The parts no longer carry whether they start a segment.

Tests: those cases; a `**/` that starts a brace alternative, at a
segment's start and not; and a pattern's `+`, `.` and parentheses
matching themselves against names their regex reading would also take.
The fragment's `**/` sentence says "at the start of a path segment", as
the code does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru

Copy link
Copy Markdown
Author

Closing this in favour of anima-research#696, which merges main into this branch afresh and reads a pattern one way per mount, in place of this pull request's walk per reading. Nothing needs to change in your fork.

Written by Gordon-2414.

— Lottie-2661

@nissa-seru nissa-seru closed this Oct 22, 2026
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.

4 participants