Skip to content

fix(prose): the turn's receipt names what kept its plain speech private - #953

Open
nissa-seru wants to merge 4 commits into
anima-research:mainfrom
nissa-seru:nigel/317-receipt-causes-squash
Open

nissa-seru wants to merge 4 commits into
anima-research:mainfrom
nissa-seru:nigel/317-receipt-causes-squash

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

Why. When a resident's plain speech is kept private, main's [delivered] receipt lists all three possible causes, whichever applied, so it can name causes that didn't happen.

This makes a turn's [delivered] receipt name what kept its plain speech private, where it listed every possible cause (#317). A resident whose short reply skip_reply kept private read (skip_reply, a drafts call or a private think), two causes that didn't apply (#928).

The receipt of a turn whose plain speech was kept private

  • base (main): it gives one count and all three possible causes, whichever applied: 1 plain-speech segment(s) kept private (skip_reply, a drafts call or a private think).
  • head: with one cause it names that one, such as 1 plain-speech segment(s) kept private (skip_reply). With two or more it gives each with its count, such as 2 plain-speech segment(s) kept private (1 by skip_reply, 1 by a private think). Prose held after a hybrid >>>skip_reply line is named >>>skip_reply.

What counts as one segment is unchanged, so the body of a live hybrid >>>skip_reply envelope is still kept private with no count, and the receipt's line for suppressed plain speech is as on main. This names every 'private' hold as skip_reply's, and a silent request's hold is 'private' too, so by reading, a silent turn's kept prose that reaches a receipt would be named (skip_reply). No test or run covers a receipt after a silent request's hold.

  • Changelog fragment added: changelog.d/317-receipt-names-why.changed.md.
Credits

Nigel-2405 made this change, and Oscar-1808 and Ada-1017 made the calls on it. Ada-1017 wrote #317. Alfred-2570 read the change word for word, its merge with main included, and our internal review read its first version, then its fixes. Langston-2479 wrote its description, and Myrtle-2415 put it in this shape.

Details for agents

At 96c016a34f, which merges main 6c7810995c34 into e58fd932b625, this change's head before it, whose merge base with main was a64cfc010f. Beyond what Git merged on its own, the merge changes only two tests' keptPrivate literals, from 0 to {}. Problem's run is at a64cfc010f, and the red runs in Tests use its src/framework.ts. As of November 7, main is 0c570d8f2d: from a64cfc010f to there, no changed line of src/framework.ts names keptPrivate, holdSilencedProse, routeSilencedHybrid or deliverHybridEnvelope, and the one that says "plain-speech" is a new log line for prose held as drafts. At 0c570d8f2d the receipt's line is ${kept} plain-speech segment(s) kept private (skip_reply, a drafts call or a private think) (src/framework.ts:21516), and its other line ${suppressed} plain-speech segment(s) suppressed (:21520).

Problem

At a64cfc010f, the receipt counts the plain-speech segments kept private in one number, and gives all three possible causes for it: ${kept} plain-speech segment(s) kept private (skip_reply, a drafts call or a private think) (src/framework.ts:21092). main's own tests assert that wording for a round that writes prose and calls skip_reply (test/prose-drafts.test.ts:678–:686), for a private think beside a send (:694), and for a drafts round (:1439). So a resident whose words skip_reply kept private reads two causes that didn't apply.

#928 reports that case: a resident writes a short reply and calls skip_reply in the same round, and nothing is posted. Run at a64cfc010f, a round with the prose "ok, thanks" and skip_reply got the receipt the test at :678–:686 asserts: [delivered] nothing — 1 plain-speech segment(s) kept private (skip_reply, a drafts call or a private think).

Changes

Line numbers are e58fd932b625's.

  • The count by cause. TurnRouting.keptPrivate is now a count per cause, from KEPT_PRIVATE_CAUSES (src/framework.ts:1541), whose order is the receipt's.
    • holdSilencedProse (:20066) counts a round with the 'private' silence that skip_reply gives under skip_reply, a same-round private think's segments under a private think, and a drafts round's under a drafts call (:20087). A round that calls skip_reply and holds a private think counts once, under skip_reply.
    • Hybrid prose held after a >>>skip_reply line counts under >>>skip_reply, in routeSilencedHybrid (:20192) and deliverHybridEnvelope (:22769). A drafts round's undeliberate parts there count under a drafts call (:20200).
  • The receipt (:21102–:21105) names the one cause, or each cause with its count.
  • The receipt's other line stays as main has it. N plain-speech segment(s) suppressed (:21109) names no cause. Under proseRouting: "disabled" the receipt words it as kept in the transcript (proseRouting=disabled — …) instead (:21108). Otherwise its count comes only from a code_execution round withheld because its turn moved on before its script calls returned (:25432), as 0.24.0's changelog entry says (CHANGELOG.md:4612–:4616). Not verified below says what we know of a receipt that shows it.
  • Doc comment. The one on TurnRouting.keptPrivate goes.
  • Changelog. changelog.d/317-receipt-names-why.changed.md.

What counts as one segment is unchanged. holdSilencedProse counts prose runs, and the hybrid paths count envelopes.

Tests

One run, from a clean checkout of e58fd932b625, on Linux with Node 24.22.2 and npm 11.19.1. It deleted node_modules and package-lock.json, ran npm install --no-audit --no-fund and npx tsc --noEmit, then npm run build and npm run pretest, then each of the five test files this change touches, and code-execution, alone, with node --import ./dist/test/helpers/force-exit-blocking-stdout.js --test --test-force-exit. Then it put back a64cfc010f's src/framework.ts, the only file under src/ this change touches, and built and ran the same six files again.

npm ls --depth=0 printed @animalabs/chronicle 0.5.1, @animalabs/context-manager 0.24.2, @animalabs/mcpl-core 0.4.0, @animalabs/membrane 0.8.0, @babel/parser 7.29.12, @modelcontextprotocol/client 2.4.0, @modelcontextprotocol/server 2.4.0, @types/node 22.20.5, @types/ws 8.18.2, chokidar 4.0.3, discord.js 14.28.0, tsx 4.23.15, typescript 7.0.2 and ws 8.22.0. This repository commits no root lockfile, so these are the versions a fresh install chose on October 30.

  • e58fd932b625: tsc reported no errors, and every test in the six files passed: prose-drafts 257, explicit-prose-routing 120, tool-result-guard 156, code-execution 99, present-while-acting 93 and send-into-closed-channel 24.
  • The same tests on a64cfc010f's src/framework.ts: 12 failed.
    • prose-drafts, 9. Six assert a receipt that now names its cause, among them "deliberate privacy is never drafted: skip_reply and a same-round private think". Three are new: "a turn whose plain speech two causes kept private names each with its count", "hybrid mode: a turn whose plain speech all four causes kept private names each with its count, in order", and "a skip_reply round with a private think in it counts its plain speech once, as skip_reply's".
    • explicit-prose-routing, 2. "hybrid: after >>>skip_reply, the next unprefixed envelope stays in the context and counts as kept private", and "hybrid: a retried turn's text waits for the replaced stream's late deliveries, and follows what they set", whose receipt now names (>>>skip_reply).
    • tool-result-guard, 1. "a fresh turn starts new routing state in every mode, and leaves the earlier turn's as it was", for the per-cause count's empty start.
    • code-execution, present-while-acting and send-into-closed-channel passed.
  • 96c016a34f, after the merge: a second run, from a clean checkout, on Linux with Node 24.22.2 and npm 11.19.1, with a fresh npm install, which chose @animalabs/context-manager 0.27.0 and @animalabs/membrane 0.10.2. npm run typecheck reported no errors, and every test passed in the six test files this change touches at 96c016a34f, locked-prose-routing now among them since the merge changed its literal: prose-drafts 267, explicit-prose-routing 121, present-while-acting 96, send-into-closed-channel 24, tool-result-guard 159 and locked-prose-routing 31. On main 6c7810995c34, the same files passed with the same counts, except prose-drafts with 264, without the three tests this change adds.

Not verified

Line numbers are e58fd932b625's.

  • Whether any route that withholds a round then writes a receipt that reads the suppressed count. No test covers it, and probes in four shapes found no such route. Run at 48f26ce56c, an earlier main, a stream that completes while the round's script runs and a provider error retried once withheld nothing: each round was routed when its code_execution call answered. Run at 96c016a34f, a round that calls skip_reply beside a script still running wasn't withheld either: skip_reply's silence kept it private, and the receipt, written once the script returned, reads [delivered] nothing — 1 plain-speech segment(s) kept private (skip_reply). A framework stopped while the script runs withholds the round, and its store closes with it, so no receipt follows.
  • A silent request's 'private' hold. holdSilencedProse's doc comment names "a silent turn" among the deliberate silences (src/framework.ts:20056), and this change labels every 'private' hold skip_reply. Probes in three shapes found no route by which a silent turn's 'private' reaches a receipt. With 48f26ce56c's src/, a silent heartbeat tick's held retry merged with a wake that brings no message: the merged start ran neither silent nor 'private'. At 96c016a34f, the same with an addressed message arriving during the hold: the merged start posted its line, and the receipt names the room. Also at 96c016a34f, a silent tick's context-budget restart while an addressed message waits: the restart ran alone and silent, writing no receipt, and the message got a turn of its own. By reading at 96c016a34f: a batch that resumes a turn starts with the turn's continuations alone (src/framework.ts:22947–:22955), and a silent start opens no logical turn (:28120–:28128), so a start that isn't silent opens one (:28130–:28137) and drops the carry (:28151). This change doesn't touch those paths. No test covers a receipt after a silent request's hold.
  • Private speech that neither main nor this change counts. Two kinds:
    • a mid-turn round with a private think and no send, whose prose :25354–:25357 logs as not routed, with no count;
    • the body of a live hybrid >>>skip_reply envelope, which deliverHybridEnvelope logs as kept in context and returns from with no count (:22705–:22712), as the comment at test/prose-drafts.test.ts:2833 says.
  • The rest of the suite, and other Node versions. We ran the test files Tests names, on Node 24.22.2.

Nigel-2405 added 3 commits October 30, 2026 05:15
…te (anima-research#317)

The [delivered] receipt counted the plain-speech segments its hold paths
keep private in one number, and named every possible cause: "kept private
(skip_reply, a drafts call or a private think)". Each place that counts
one knows its cause, so the turn's routing now counts them by cause, and
the receipt names the cause that applied: "1 plain-speech segment(s) kept
private (skip_reply)", or, with two or more, each with its count in the
order skip_reply, >>>skip_reply, a drafts call, a private think: "2
plain-speech segment(s) kept private (1 by skip_reply, 1 by a private
think)".

- holdSilencedProse counts a 'private' round's segments as skip_reply's, a
  same-round private think's as the think's, and a drafts round's (the
  'kept' cause without a think) as the drafts call's. A skip_reply round
  that also holds a private think counts once, under skip_reply.
- Hybrid prose held after a >>>skip_reply (routeSilencedHybrid and
  deliverHybridProse) counts as >>>skip_reply's, and a drafts round's
  undeliberate parts as the drafts call's.
- A 'private' hold is labelled skip_reply. A silent request's own turns
  write no receipt, and its held retry merged into a start that isn't
  silent doesn't bring its 'private' there: a silent start opens no
  logical turn, so that start opens a new one, which takes no carry.

A code_execution round withheld because its turn moved on before its
script calls returned no longer adds to the turn's suppressed count; its
log line stays. No route that withholds one writes a receipt that reads
the count: a new logical turn takes a fresh TurnRouting, a continuation of
the same turn starts only once the round's tool results are in, and the
stream-end withhold runs after any receipt of a turn a tool ended. The
withhold's doc no longer says a later stream's receipt counts it. With
that, only disabled routing feeds the suppressed count: an agent's
proseRouting "disabled", or a turn that suppresses prose, which writes no
receipt. So a receipt's suppressed line is always the disabled wording,
which reads as before.

The doc comments on TurnRouting.suppressed and TurnRouting.keptPrivate and
on recordProseSuppression go, as doc comments on internals do.

Tests: anima-research#928's case reads "(skip_reply)" and a private think's "(a private
think)"; two causes in one turn each show with their counts, and a hybrid
turn with all four shows them in order, one with a count of two; the retry
test's receipt names "(>>>skip_reply)"; a skip_reply round with a private
think counts once; the drafts-round and >>>skip_reply receipts name their
causes; the three withheld-round tests now find the suppressed count
unchanged; routing literals in tests take the new shape. Disabled mode's
receipt tests are unchanged and pass.
test/present-while-acting.test.ts conflicted in two tests that main removed
in c78a573 ("a silent start that throws leaves an open prose turn
alone" and "a silent turn's over-budget boundary leaves no arrival on an
open prose turn", both building a state the framework doesn't enter). This
branch had touched them only to change their routing literal's
keptPrivate from 0 to {}, so main's removal is taken.
…-research#317)

agent-framework 0.24.0's changelog states that a withheld round's
segments are counted as suppressed in the turn's delivery receipt, so
taking that count out would change a documented rule and can't go in this
pull request. The withhold's recordProseSuppression call, the receipt's
non-disabled "suppressed" line, the comments that describe them and
code-execution's withheld assertion are as main has them, and the
changelog fragment no longer mentions the withheld round. What remains is
the receipt naming the cause that kept plain speech private.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

@anima-review

anima-review Bot commented Nov 3, 2026

Copy link
Copy Markdown
About these findings

The findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits.

Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase.

Automatic review of e58fd932b625.

Findings, most severe first:

@nissa-seru

Copy link
Copy Markdown
Contributor Author

Finding 1 (should fix) isn't changed here. This pull request only names the cause of counts its base, a64cfc010f, already makes, and the body of a live hybrid >>>skip_reply envelope isn't counted at the base either (src/framework.ts:22705–22712 at e58fd932b6), as the description's Not verified says.

Nigel-2405 wrote this reply, and Ada-1017 made the call.

— Nigel-2405

@nissa-seru

Copy link
Copy Markdown
Contributor Author

Lines for the automatic review's minor findings 2 to 5 on e58fd932b6, which this pull request leaves as they are. Its counting is its base's, a64cfc010f: it names the cause of counts the base already makes.

  • 2. True by reading at both commits: the silenced hybrid path counts every >>>skip_reply envelope, whatever its body (src/framework.ts:20136–20140 at e58fd932b6). We haven't run it.
  • 3. It's in the description's Not verified: a mid-turn round with a private think and no send keeps its prose with no count (:25354–:25357).
  • 4. This pull request doesn't touch the non-live XML path (withoutRoundsProse, :25769), and we haven't checked the finding.
  • 5. The retry-ordering test and its fixed 20 ms wait predate this pull request, which changes only its receipt assertion. By reading, it could pass without showing the wait. We haven't run it under load, and it stays as it is.

Each of 2 to 4 would put a receipt's count off by one segment on a rare path. One count across the paths would touch four routing paths and change the number a resident reads, so it would be a change of its own, which we aren't proposing.

Nigel-2405 checked each finding against the code and wrote this reply, and Ada-1017 made the calls.

— Nigel-2405

@nissa-seru
nissa-seru requested a review from antra-tess November 4, 2026 23:41
test/present-while-acting.test.ts conflicted in one hunk, where main
rewrote "the start that continues the turn weighs a message its own flush
lands, and a delivery assembled for it" as two cases of one loop. This
branch had touched the file only to change its routing literals'
keptPrivate from 0 to {}, so the file is main's, with its four routing
literals' keptPrivate written {}.

test/locked-prose-routing.test.ts, which main added since a64cfc0,
builds a routing record with keptPrivate: 0 for "a turn whose lock moved
while its words waited for its end holds its speech on every path". Its
path holds the words as a draft and doesn't count them as kept private,
so the literal is written {}, the record's shape here, rather than left
as a number nothing reads.

From a64cfc0 to 6c78109, main's src adds no keptPrivate site and no
receipt wording: the eight sites and the receipt line are main's at the
merge base, moved. The receipt assertions in test/prose-drafts.test.ts
merged without conflict.
@anima-review

anima-review Bot commented Nov 11, 2026

Copy link
Copy Markdown

Automatic context view of 96c016a34f7f merged onto 36b6dfe9de39.

What a context view runs and what its checks judge: CHECKS.md.

The records of 6 of the 80 runs differ between the base and this head.

In the files profiled hosts loaded, 3 changed source lines that map to code ran in no profiled drive: src/framework.ts lines 23535, 23543 and 26988. 5 changed source lines map to nothing the host runs: 5 of src/framework.ts's. The whole list is in the fold below.

Usage: the agents' requests, subagents' included, in the 80 runs, base → head: 206,047 → 206,045 tokens sent a request, 72.4% of them read from the cache, 138,567,998 → 138,567,104 written to it, 4,043,512 output, 24 thinking blocks dropped, USD 0.5446 at list price a priced request and 123 requests with no list price. The figures of 6 runs moved, for any kind of request; they're under "Usage that differs", below. The figures are the stand-in's model of each provider's published caching and token rules, not a measurement; the stand-in answers the same every time, so a difference between the arms comes from the change. A list price is what each arm's host prices those figures at with its own table (connectome-host's priceCall), where it gives one, so it rests on the same model, with the stand-in's token estimate in place of the provider's own count, cache hits as the script's pacing gives them, and scripted replies.

No model read these records for this view, which started on its own. A view asked for with a /context-deltas comment by nissa-seru gives a model's account of the records of the pull request's head as it is when that view runs.

Runs whose records differ: 6 of 80

H4b/W1, H5/W12, H5b/W12, H5c/W12, H1r/W16 and H1/W17.

Usage that differs: 6 of 80 runs, base → head
H4b/W1 kind requests input read from the cache written to it output thinking dropped list price, USD unpriced
agent 212 1,146,418 47,744,444 → 47,743,996 2,663,640 → 2,663,512 241,037 0 62.2666 → 62.2651 0
summariser 85 → 86 1,449,942 → 1,461,143 3,820,686 → 3,878,997 179,974 170,000 → 172,000 0 15.2098 → 15.3450 0
H5/W12 kind requests input read from the cache written to it output thinking dropped list price, USD unpriced
agent 111 117,917 13,749,938 → 13,748,802 1,307,390 → 1,307,278 93,489 0 18.6342 → 18.6330 0
concepts 38 120,268 0 0 2,685 0 0.1337 0
judge 5 19,162 134,017 87,223 167 0 0.0142 0
naming 1 617 0 0 9 0 0.0007 0
relevance 2 10,035 0 0 42 0 0.0102 0
review 1 1,352 0 0 55 0 0.0002 0
H5b/W12 kind requests input read from the cache written to it output thinking dropped list price, USD unpriced
agent 72 46,801 4,918,188 → 4,917,868 3,601,086 → 3,600,654 64,733 0 27.1498 → 27.1469 0
concepts 19 65,532 0 0 1,605 0 0.0736 0
judge 2 1,492 684 684 67 0 0.0003 0
naming 1 617 0 0 9 0 0.0007 0
relevance 2 11,495 0 0 42 0 0.0117 0
review 1 1,924 0 0 55 0 0.0002 0
H5c/W12 kind requests input read from the cache written to it output thinking dropped list price, USD unpriced
agent 70 54,958 7,603,165 → 7,602,541 752,821 → 752,757 70,609 0 10.8810 → 10.8803 0
concepts 18 70,788 0 0 1,536 0 0.0785 0
naming 1 617 0 0 9 0 0.0007 0
relevance 2 13,372 0 0 42 0 0.0136 0
review 1 1,930 0 0 55 0 0.0002 0
H1r/W16 kind requests input read from the cache written to it output thinking dropped list price, USD unpriced
agent 11 1,978 245,157 → 245,029 33,701 → 33,685 9,528 0 0.7077 → 0.7075 0
H1/W17 kind requests input read from the cache written to it output thinking dropped list price, USD unpriced
agent 34 11,604 1,552,647 → 1,549,978 69,784 → 69,642 30,540 0 2.2957 → 2.2929 0
naming 1 598 0 0 9 0 0.0006 0

The figures of the other 74 runs are the same in the base and this head.

Upgraded runs beside their fresh twins
On the base requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,911 58.3% 857,419 20,328 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.20.0 15 155,632 63.2% 715,480 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed
On the head requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,911 58.3% 857,419 20,328 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.20.0 15 155,632 63.2% 715,480 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed

A run that writes its own history first, as a fresh twin does, is shown without that history's requests, so its figures are its workload's alone, as the upgraded runs' are.

Changed source lines and deletions not seen to run, by file

src/framework.ts: lines 23535, 23543 and 26988 ran in no profiled drive; lines 1603, 1656–1657 and 1660–1661 map to nothing the host runs.

Held the same in base and head: connectome-host 00d56c540b27, chronicle 0.6.0, context-manager 0.27.0 (the view chose it for both arms, since connectome-host's lockfile holds 0.25.3, which doesn't meet ^0.27.0 from the base), membrane 0.8.1.

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