Repository navigation
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
Closed
nissa-seru wants to merge 1466 commits into
nissa-seru wants to merge 1466 commits into
Conversation
…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>
…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>
…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>
…merge-back Release 0.23.2
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>
Author
|
Closing this in favour of anima-research#696, which merges Written by Gordon-2414. — Lottie-2661 |
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.
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 thatmainstarts 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 onmain. 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
globstart its walk at the directory its pattern names before the first wildcard, beneath itspath, andgrepat the deeper of itspathand 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 asmainhas it, the walk would start at the first reading's directory. Fornotes/diary/*.mdin thenotesmount, that'snotes/diaryinside the mount. It isn't there, sowalkStartstops at the mount's root, and on a mount past its cap the walk can stop beforediary. That's the failure anima-research#511 fixed, back for exactly the patterns this branch makes work.So:
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).main: its start, the ignore patterns its named scope sets aside, and its cap.path.prefixedGlobgives it beside its regex, in the place of this branch'smountGlobs.mainfinds 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/*.jswould find nothing, wheremainfindswork/work/node_modules/pkg/a.js.walkStartreaches its whole directory, or, on a 'chronicle' mount, where the workspace tracks something beneath it.walkStartstops 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 ofgatherinreconcile.ts, is the one list of those tracked paths, within the mount's include.gathertakes its candidates through it, andsearchWalksasks it (tracksBeneath). A direct mount tracks nothing.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, aswalkStartreads disk, so a branch selected between the two can leave such a reading unwalked.The other conflicts
glob's description: this branch's sentence, thenmain's note for a direct mount.grep'sglobdescription: this branch's, withmain'sintegertypes for the context counts.main's, with the prefixed reading's walk added.main's regex for the pattern as written is built first, asmainbuilds it, so a patternglobToRegexrefuses is still refused before the path is read.walkCap's doc andgrep'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}/**/*.tsfinds 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}**/*.tsmatchessrc/main.tsanddraftx/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:**/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;**/that starts a brace alternative, at a segment's start and not;+,.and parentheses matching themselves, against names their regex reading would also take.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:globand ingrep'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;main's account of it, and a file both reach is given once, as a match, a result and a skip;Checks
On Linux x86_64 with Node 24.22.0, at this pull request's head:
npm run typecheckandnpm run buildare clean, andnpm testpasses, 5,984 tests, 5,940 passing, 44 skipped, none failing.workspace*test files pass, 964 tests, 920 passing, 44 skipped. The merge commit alone passesworkspace-three-way.test.tsandworkspace-glob-mount-prefix.test.ts, 373 of 373.main(97ca7eba): all 7 of its cases fail. A mount-prefixed pattern finds nothing inglobor ingrep's filter, and**/*.mdmisses each mount's top-level.mdfiles. At this branch's head before the merge (92092ab2), the same file passes 7 of 7.gathernot taking its candidates fromtrackedPaths: 47 fail, sincegatherthen takes no tracked paths at all. That showsgatherneeds them, not that the list is shared;**/at a part's start always matching zero or more directories: 3 tests fail, the brace-group ones;**/at a part's start always needing a directory: 5 fail, among them this branch's**/tests;main's source under this head's tests: 16 fail.**/reading against brace expansion: Beth-2709 comparedglob'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, soglobandgrepcan't disagree about where a mount-prefixed pattern's matches lie.main's walk whole, so nothingmainfinds is lost. The second walk is made only in a mount where both readings walk.trackedPathsshared withgather: the check counts what a pass would report, from the same list, so the two can't drift apart. A run comparinggather'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 keepsmain'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 asmaindoes.{src/,lib/}**/*.tsand{src,lib}/**/*.tsgive 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.+,.and parentheses had no test that they match only themselves, which the brace-group code builds its regex around.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.