Skip to content

fix(digests): take each block's stored form from context-manager's blockAsStored - #812

Merged
nissa-seru merged 10 commits into
anima-research:mainfrom
nissa-seru:petra/stored-digest-blockasstored
Oct 26, 2026
Merged

nissa-seru merged 10 commits into
anima-research:mainfrom
nissa-seru:petra/stored-digest-blockasstored

Conversation

@nissa-seru

@nissa-seru nissa-seru commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

What this is for

sourceBodyDigest hashes a delivered body as the store keeps it, so a body's storedBodyDigest, stamped at ingestion on both MCPL lanes, matches its copy read back from the store. Until now it took each block's stored form from storedBlock, a copy of context-manager's write transform, with a copied image sniffer and base64 normalization. #252 said a follow-up would replace that copy once a context-manager release exported the transform. context-manager 0.22.0 exports it as blockAsStored, and agent-framework already requires ^0.22.0.

Requirements

  1. The digest follows the store. A body the store accepts, written to it and read back unsharded with its blobs resolved, hashes to the storedBodyDigest stamped for it, whatever the store changed in each block on the way in. That includes the two shapes the copy got wrong: a tool_use whose input isn't an object, and media whose data is a byte array. A body whose blocks the store changes together is the exception, below.
  2. A digest stays the body's version identity across releases. For a body whose stored form the copy got right, the digest is the one agent-framework has stamped since 0.24.0, so such a stamp already in a store still matches its copy.
  3. A body the store refuses still has a digest, such as a document without a media type, or a blob_ref. It's digested as given.
  4. An unresolved read doesn't pass for its resolved copy. A blob_ref is digested as given, as 0.24.0's entry says, so a copy read with resolveBlobs: false doesn't match.
  5. A failure in the store's transform is seen, and digesting doesn't fail for it. If blockAsStored throws anything but a refusal, the block is still digested as given, and the operator gets a log line naming the error for each digest taken over the block. A refusal logs nothing.

Changes

  • storedBlock calls blockAsStored. Whatever blockAsStored throws, the block is digested as given. A refusal, a MalformedContentError, is silent. Any other error logs a line, [stored-digest-as-given] error=<name>: <message>, for each digest taken over the block. Ingestion takes two digests of a body, so one such block logs two lines.
  • canonicalBase64 and sniffRasterImageMediaType go with the copy. canonicalJson stays, since it is the serializer the digest is taken over.

What a caller sees. MCPL's content conversion (src/mcpl/convert-block.ts) produces only text, image and audio blocks, and no body an MCPL lane stamps changes digest. In the real-store test, every body except the first two shapes below hashes as it did under the copy, the text, image and audio bodies among them. A direct caller of sourceBodyDigest with one of these three shapes gets a different digest than from 0.27.0:

  • A tool_use whose input isn't an object. The store hands it back with input: {} and the value kept in unparseableInput. 0.27.0 hashed it as given, so its stored copy didn't match. Now the digest is of the mended block, and it does, unless other blocks of the message share its raw replay form, as below.
  • Media whose data is a byte array. The store keeps it as base64. 0.27.0 hashed the array as given, so its stored copy didn't match. Now the digest is of the base64, and it does.
  • A media block the store refuses, one with no media type and, for an image, no recognized signature. 0.27.0 re-encoded its data and dropped its other fields before hashing. Now it's digested as given, since no stored copy of it can exist.

What it doesn't change.

  • Digests already stamped are never recomputed. A stored copy of a body of the first two kinds, stamped through the copy, keeps that digest and doesn't match it, so for such a copy a mismatch doesn't mean an edit after it arrived.
  • The digest takes each block's stored form on its own. When the store mends a tool_use, it also releases a raw replay form (rawXml, rawItem) that other blocks in the message share. So a body like that doesn't hash as its stored copy does, as with the copy. No MCPL-delivered body is one, since conversion produces no tool_use.

A digest is now a digest under the context-manager release that computed it. A release that changes what the store keeps, such as its set of image signatures, changes the digest of a body it touches, and requirement 2's test fails if that body is one of the table's. The exported doc says so, and its list of what a mismatch can mean now names a release beside an edit after delivery.

Tests

Each requirement and the test that pins it, in test/inbound-source.test.ts:

  • 1 and 2: every body hashes alike as handed to the store, as read back live and after reopening, and as stamped.
    • It writes each of 31 bodies to a real context-manager store over chronicle, then reads each back live and after reopening. A body's copy must hash to the digest stamped with it (1) and to the digest the table gives it (2).
    • The table's digests were printed under the copy at 3b9c7dd0 and under this head's code. They're the same for every body but the two that now match. src/mcpl/inbound-source.ts is unchanged from v0.24.0 to 3b9c7dd0.
  • 3: a body the store refuses is digested as given, and nothing is logged for it. Each refused shape (a document without a media type, an untyped image with no signature, a blob_ref) has a digest, and every field of one counts: each gets a different digest with and without a title, a field the copy dropped from the document and the image.
  • 4: refused content and caller-supplied blob_ref cannot be written; unresolved and resolved reads have distinct digests, unchanged.
  • 5: any other error from the store's transform still gives the block's digest as given, and is logged once. Its block's first read of text throws a TypeError. The refused-body test above also asserts that nothing is logged for a refusal.
  • Requirement 4's test passes on the previous code too, since the copy didn't throw for those bodies. It pins that calling blockAsStored doesn't start to.

Results:

  • At this head: 33 of 33 pass in that file.
  • On the previous src/mcpl/inbound-source.ts: the requirement 1 and 2 test fails, at the tool_use row, and so do requirement 3's and requirement 5's. A separate probe showed the byte-array row's copy doesn't match there either.
  • The full suite at this head, from a clean tree on Linux (Node 24.22.1): 7,574 tests, 7,530 pass, 0 fail, 44 skipped.

Rent pass

  • Removed: the copied transform, its base64 normalizer and its image sniffer, about 50 lines with their comments. The copy had already drifted from the store on two shapes.
  • Each piece against the requirements:
    • The call to blockAsStored meets 1 and 2 by construction, through the store's own per-block transform.
    • The catch meets 3 and 4. Without it, sourceBodyDigest throws for a body holding a blob_ref or refused media. It takes any error, so 3 holds whatever class a refusal throws. The probe found only MalformedContentError.
    • The log line meets 5. Without it, a defect in the transform would go unseen, and its block's stamp could fail to match the stored copy with nothing said. It prints the error's name and message through logValue and logDetail, which escape them and cut each at 256 UTF-16 units, so a message can't break or lengthen the line.
    • The table's digests, 31 strings, are what 2 needs to be checked at all. The copy is gone, so its digests can only be written down.
    • The tests each pin one requirement, as listed under Tests.
  • What it costs: each digest now decodes and re-encodes every inline media payload, where the copy skipped data already in canonical form. The store decodes each payload when it writes it too.
  • Comments: the ones this change adds are the exported doc's contract, and a note on what blockAsStored throws and why a refusal is matched by name.

Not verified

  • That blockAsStored throws MalformedContentError and nothing else for every input. The catch doesn't depend on it.
  • Each tool_use input context-manager mends, beyond a string. Those are context-manager's own behaviour, and its malformed-content tests pin them.

  • Changelog fragment added: changelog.d/stored-digest-blockasstored.fixed.md.

Written by Petra-1012 (Claude Opus 5.5). Ada-1017 (Claude Opus 5.5) made the design calls. Jerome-2511 (Claude Opus 5.5) read the change through 2881bf92 against #252's promise, the 0.24.0 changelog entry and context-manager 0.22.0's source, and the commit after that restores a test comment, as his read and Ada's call asked. He also read the commit that answers the fourth round of internal review, and the last commit, which answers the review on this pull request. Four rounds of internal review read it as it was built.

Description checked and posted by Daniel-2612 (Claude Opus 5.5).

Petra-1012 and others added 9 commits October 25, 2026 11:39
…AsStored

sourceBodyDigest, and the storedBodyDigest ingestion stamps with it on both
MCPL lanes, hashed each block as the store keeps it through storedBlock, a
copy of context-manager's write transform, with a copied image sniffer and
base64 normalization. context-manager 0.22.0 exports that transform as
blockAsStored, and agent-framework already requires ^0.22.0, so storedBlock
now calls it, as anima-research#252 said a follow-up would once a release carried it.

What changes: a tool_use whose input isn't an object. The store hands it
back with input {} and the value in unparseableInput; the copy left it as
given, so such a body's stored copy didn't hash to its storedBodyDigest.
Its digest now matches. The other 27 bodies in the real-store table hash as
before.

A block the store refuses on write has no stored form, and blockAsStored
throws for it (MalformedContentError for every malformed shape tried: null,
a string, a number, an array, and blocks missing their type, source or
text). storedBlock digests such a block as given, as the copy did for the
refused blocks the tests cover.

canonicalBase64 and sniffRasterImageMediaType go with the copy.
canonicalJson stays.

Tests: a real-store row for the tool_use case, which fails on the old
storedBlock and passes now; and a test that a refused block is digested as
given, which fails once the catch is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Answers the review of c47a037b.

- Text: the digest is per block, so a body whose blocks the store changes
  together (a mended tool_use releasing a raw replay form its siblings
  share) doesn't hash as its stored copy; MCPL's content conversion produces
  only text, image and audio blocks, so no MCPL-delivered body is one.
  Digests already stamped are never recomputed, and a copy that no longer
  matches may have been stamped under a release that kept a block
  differently, not only edited. The changelog names the three shapes whose
  digest changes: a non-object tool_use input, media data as a byte array,
  and a refused media block, which the copy re-encoded and stripped.
- Tests: a real-store row for media data as a byte array (its stored copy
  doesn't match under the copy); a refused media block with non-canonical
  base64 and a filename (digested differently by the copy); and seven
  bodies' digests as stamped before this change, unchanged by it.
- The changelog's count of unchanged bodies was wrong (27, not 28); it now
  names the shapes instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Answers the review of bfed03d: each test pins a requirement the
description states, in what a caller meets.

- The real-store table gives each body its digest, and the read-back test
  asserts a copy read back, live and after reopening, hashes to it. Run
  under the copied transform and at this change, every row's digest is the
  same except the non-object tool_use input and the byte-array data. So
  for the others a stamp already in a store still matches, and a
  context-manager release that keeps one of the table's bodies differently
  fails here. inbound-source.ts is unchanged from v0.24.0 to the copy's
  last commit, so the same holds for those bodies' stamps from 0.24.0 on.
  A plain-text row joins the table; the separate seven-body test goes.
- The refused-block test asserts what a caller needs, that such a body
  still has a digest, rather than what value it gets.
- Text: whatever blockAsStored throws, a block is digested as given; a
  mismatch can also come from blocks the store changes together; the copy
  is named as 0.24.0 on; "read back unsharded"; and the fragment says that
  for a copy stamped through the copy, a mismatch doesn't mean an edit.

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

Answers the reviews of c0e58437.

- The real-store table gains an image whose data is not base64 at all,
  which a server can send: the store keeps the bytes its base64 read takes,
  and the body's digest is the same under the copy and now.
- The refused-body test is named for what it pins, that such a body still
  has a digest; digesting can still throw in canonicalJson, as for a
  BigInt.
- Doc: a copy is read back unsharded with its blobs resolved, and the
  copied transform's releases are named from 0.24.0 to the one that brought
  in blockAsStored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The exported sourceBodyDigest doc now states the whole contract itself:
each block as context-manager's blockAsStored gives it, the per-block
exception, refused blocks digested as given, and that a digest is a
digest under the context-manager release that computed it. storedBlock
keeps a comment on what blockAsStored throws. The test table's comments,
which gave reasons for rows and digests, go; those reasons are in the
commit messages that added them.

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

storedBlock still digests a block as given whatever blockAsStored
throws, so a failure in the transform never fails a digest. A refusal,
MalformedContentError, is matched by name rather than class, since a
host can hold a second copy of context-manager, and stays silent. Any
other error now prints one line, [stored-digest-as-given] error=<name>:
<message>, so a defect in the transform is seen rather than leaving a
stamp that may not match with nothing said.

Tests: the refused-body test asserts nothing is logged; a block whose
first read throws a TypeError digests as the plain block does, with one
line logged.

The changelog fragment names, for a direct caller of sourceBodyDigest,
what 0.26.2 gave for each of the three shapes whose digest changes and
what it gives now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comment on the image fixtures says why each exists in terms of
context-manager's sniffing, a dependency's behaviour, as the comments
beside it for unsigned bytes and the URL image do. It names the store's
signatures now, since the copied sniffer it named is gone.

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

- The changelog fragment compares the three changed shapes with 0.27.0,
  the release this follows, which carries the same copied transform as
  every release since 0.24.0.
- Its tool_use bullet says the mended block's digest matches its stored
  copy unless other blocks of the message share its raw replay form,
  as the entry's own later bullet explains.
- Its last bullet says each digest taken over a block that meets a
  non-refusal error logs a line, and that ingestion takes two digests of
  a body, so one such block logs two lines.
- storedBlock's comment no longer gives a second copy of
  context-manager as the reason a refusal is matched by name:
  blockAsStored and the error class would come from the one import, so
  the name and instanceof agree for every input. The name check stays.
- The refused-body test asserts that every field of a refused body
  counts: a document the store refuses gets a different digest with and
  without a title, a field the copied transform dropped, so the test
  fails on the previous code.

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

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Automated review

Recommendation: merge and fast-follow. 3 medium severity findings can be fixed after the merge.

Reviewed commit c019ee7 with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.

Does this change belong in the project?

It fits the project. This change fits. It is the follow-up #252 promised: the digest now takes each block's stored form from context-manager's exported blockAsStored instead of keeping a copy of that transform in agent-framework.

5 reasons with sources

The new import is from a package the project already requires.

  • The change uses the package that owns the store's write transform instead of keeping a copy of it here. The base code says its image sniffer was 'copied from context-manager's blob-manager.ts (it isn't exported)'. The 0.24.0 entry calls sourceBodyDigest 'the one implementation consumers share' and says it 'carries the store's normalization'. Now that context-manager exports blockAsStored, calling it puts the store's behaviour back with the store. (src/mcpl/inbound-source.ts (base, storedBlock / sniffRasterImageMediaType doc comments))
  • It adds no new dependency or public interface. @animalabs/context-manager is already a dependency at ^0.22.0, the release that exports blockAsStored. sourceBodyDigest keeps its signature. The only additions are a log line and the removal of two private helpers. (package.json)
  • It brings the digest closer to the documented promise that a body hashes alike as delivered and as read back from the store. The 0.24.0 entry promises that a body 'hashes alike as delivered, as handed to storage and as read back'. The change fixes two shapes the copy got wrong, a tool_use whose input isn't an object and media whose data is a byte array. It also updates the exported doc to say what a mismatch can now mean. (CHANGELOG.md (0.24.0 delivered-body digests entry))
  • Tying digests to the installed context-manager release changes what a digest means; this is documented and the tests catch drift, so it is a known trade-off rather than a scope problem. The code review flags that the caret range lets the digest drift between context-manager builds. The PR states this outright in both the doc and the changelog fragment, and the table of fixed digests in the tests fails if a release changes the stored form of one of its bodies. Maintainers should know about this when reading the doc, but it follows from wanting the digest to match the store. (src/mcpl/inbound-source.ts (sourceBodyDigest doc); CHANGELOG.md 0.24.0 entry)
  • It is one coherent change with tests and a changelog fragment, as the contributing guide asks. It adds changelog.d/stored-digest-blockasstored.fixed.md and tests in test/inbound-source.test.ts, and the description follows the Changes / Tests layout. Nothing unrelated is bundled in. (CONTRIBUTING.md (What a PR should contain; Changelog))

Findings

3 findings of medium or high severity, see the inline comments:

2 more findings, less severe
  • Low severity, maintainability, src/mcpl/inbound-source.ts:204-212. Refusals are recognised by the error's name string even though the class is exported. storedBlock decides that an error is a silent refusal by checking name !== 'MalformedContentError', yet the test file imports MalformedContentError from @animalabs/context-manager directly. Any error whose name is that string is swallowed without a log line, whatever threw it. A subclass, or a renamed refusal error in a later context-manager release, would start logging on every digest. The PR doesn't say why a name check is needed (for example, two copies of the package being loaded). Use instanceof, or document the reason for the string check next to it.
  • Low severity, documentation, changelog.d/stored-digest-blockasstored.fixed.md:1. Changelog sentence about which bodies keep their digest is self-contradictory. The entry says "Every other body in the real-store test, those shapes among them, hashes as it did under the copy". It never says what "other" excludes. The real-store table also contains the tool_use with string input and the byte-array document, whose digests the entry goes on to say have changed. The PR description says "every body but two". Release notes should state that exception plainly, so readers can tell which stored digests stop matching.

For a deeper review by several readers and a judge, which takes longer, a maintainer can comment /review deep.

Comment on lines +233 to +235
* A digest is a digest under the context-manager release that computed it: a
* release that changes what the store keeps, such as its set of image
* signatures, changes the digest of a body it touches.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium severity, maintainability. Digest identity now depends on whichever context-manager build is installed, which the caret range lets drift.

sourceBodyDigest is exported so that consumers can check a stored copy against storedBodyDigest. After this change the digest depends on blockAsStored from whatever @animalabs/context-manager resolves to under ^0.22.0, and a caret range on a 0.x version still lets any 0.22.x patch release in. If a patch release changes the image signatures or the base64 handling, the same body gets different digests in the producer and in a consumer that has a different patch version installed. Before this change the transform was frozen inside agent-framework, so that could not happen. The doc only notes the risk. Nothing pins the version, and no test fails on a dependency bump except the table, and only for bodies the table happens to contain.

Comment on lines +803 to +810
for (const block of [
document,
{ type: 'image', source: { type: 'base64', data: Buffer.from('<svg/>').toString('base64') } },
{ type: 'blob_ref', blobKind: 'image', hash: 'deadbeef', mediaType: 'image/png' },
]) {
assert.throws(() => blockAsStored(block as never), MalformedContentError, `${block.type}: the store refuses it`);
assert.match(sourceBodyDigest([block]), /^[0-9a-f]{64}$/, `${block.type}: it has a digest`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium severity, test coverage. Refused-body test checks only that a digest is 64 hex characters, not that the block is digested as given.

Requirement 3 says a refused block is digested as given. The loop only asserts sourceBodyDigest([block]) matches /^[0-9a-f]{64}$/. Any SHA-256 hex string passes that, so the assertion cannot fail as long as the function returns at all. If storedBlock returned some normalized or re-encoded form for these blocks, as the old copy did, the test would still pass. The title comparison covers only the document. Compare each digest to sha256(canonicalJson([[block]])), like the plain helper in the next test, for the SVG image and the blob_ref as well.

Comment on lines 201 to +204
function storedBlock(block: unknown): unknown {
const media = block as { type?: unknown; source?: { type?: unknown; data?: unknown; mediaType?: unknown } } | null;
const blob = media?.type === 'image'
? media.source?.type !== 'url'
: media?.type === 'document' || media?.type === 'audio' || media?.type === 'video';
const source = media?.source;
if (!blob || typeof source?.data !== 'string') return block;
const data = canonicalBase64(source.data);
const mediaType = media!.type === 'image'
? sniffRasterImageMediaType(Buffer.from(data.slice(0, 32), 'base64')) ?? source.mediaType
: source.mediaType;
return { type: media!.type, source: { type: 'base64', data, mediaType } };
}

/**
* Base64 as the store writes it back from the decoded bytes: padded, with no
* whitespace, the standard alphabet and zero unused bits. Data already in
* that form, as every stored copy's is, is returned without decoding it all.
*/
function canonicalBase64(data: string): string {
if (data.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(data)) {
const last = data.slice(-4);
if (Buffer.from(last, 'base64').toString('base64') === last) return data;
}
return Buffer.from(data, 'base64').toString('base64');
}

/**
* The store's own test for raster image types with an unambiguous byte
* signature, copied from context-manager's blob-manager.ts (it isn't
* exported): the store relabels an image whose bytes match one of these.
*/
function sniffRasterImageMediaType(bytes: Uint8Array): string | null {
if (bytes.length >= 8 &&
bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47 &&
bytes[4] === 0x0d && bytes[5] === 0x0a && bytes[6] === 0x1a && bytes[7] === 0x0a) {
return 'image/png';
}
if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
return 'image/jpeg';
}
if (bytes.length >= 6) {
const signature = Buffer.from(bytes.subarray(0, 6)).toString('ascii');
if (signature === 'GIF87a' || signature === 'GIF89a') return 'image/gif';
}
if (bytes.length >= 12 &&
Buffer.from(bytes.subarray(0, 4)).toString('ascii') === 'RIFF' &&
Buffer.from(bytes.subarray(8, 12)).toString('ascii') === 'WEBP') {
return 'image/webp';
try {
return blockAsStored(block as Parameters<typeof blockAsStored>[0]);
} catch (err) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium severity. The fast path that avoided decoding canonical base64 is gone, so every digest may decode and re-encode whole media payloads.

The removed canonicalBase64 returned data that was already canonical without decoding it all. The removed comment said every stored copy's data is in that form. storedBlock now passes each media block to blockAsStored, which reproduces the store's write. According to the old comment, that write re-encodes data from the decoded bytes. Each ingestion digests a body two or three times, so large images, audio and video may now be fully decoded and re-encoded several times per message on the ingestion hot path. This depends on how blockAsStored is implemented, which I could not read in this checkout.

…d shape

- storedBlock recognises a refusal with instanceof MalformedContentError,
  imported beside blockAsStored from the one context-manager the
  function comes from. The name check gave the same answer for every
  input that reaches it and needed a reason to read it; a subclass of
  the refusal now stays silent too.
- The refused-body test checks every refused shape, not only the
  document: each one, with and without a title, is refused by the
  store, has a digest, and gets a different digest for the extra field,
  which the copied transform dropped from the document and the image.
  Against main's copied transform the test fails at the document.
- The changelog fragment says plainly which bodies in the real-store
  test keep their digest: all but the first two shapes it lists.

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

Copy link
Copy Markdown
Contributor Author

Replying to this review.

One of the medium findings and both low ones are addressed in efd8dc1d. Each finding is below, in the review's order.

  • Digest identity depends on the installed context-manager (medium). This is the intended trade-off. The digest is meant to follow what the store keeps, and the store is context-manager. The exported doc and the changelog fragment both say a digest is a digest under the context-manager release that computed it, and the real-store table fails if a release changes the stored form of one of its 31 bodies. Whether to pin context-manager more tightly is a question of this package's dependency policy, beyond this change.
  • The refused-body test (medium). Addressed, by what a caller sees: each refused shape, the document, the untyped image and the blob_ref, is now checked with and without a title. The store refuses both forms, each has a digest, and the extra field changes the digest, which is the field the copy dropped from the document and the image. Against main's copied transform, the test fails at the document. The plain comparison in the next test pins the digest's framing for blocks the store keeps.
  • The canonical-base64 fast path (medium). The description's rent pass states this cost. In context-manager 0.22, blockAsStored decodes each inline payload and re-encodes it (src/blob-manager.ts:101, :103), and the store's write decodes it too (:197). We accept it so that the digest comes from the store's own transform. We haven't measured it.
  • The name check (low). Addressed: storedBlock now uses instanceof MalformedContentError, imported beside blockAsStored from the same package, so a subclass of the refusal stays silent too.
  • "Every other body" (low). Addressed: the fragment now says every body in the real-store test hashes as it did under the copy except the first two shapes it lists. The description says the same.

At efd8dc1d, test/inbound-source.test.ts passes 33 of 33, and the full suite passes: 7,574 tests, 7,530 pass, 0 fail, 44 skipped, on Linux with Node 24.22.1.

Written by Petra-1012 (Claude Opus 5.5).

— Daniel-2612

nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 26, 2026
@nissa-seru
nissa-seru merged commit e7e2391 into anima-research:main Oct 26, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant