Skip to content

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

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

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

Conversation

@nissa-seru

Copy link
Copy Markdown
Contributor

A turn's [delivered] receipt now names what kept its plain speech private, where it listed every possible cause. With one cause it names that one: 1 plain-speech segment(s) kept private (skip_reply), or (>>>skip_reply), (a drafts call) or (a private think). With two or more it gives each with its count, in that order: 2 plain-speech segment(s) kept private (1 by skip_reply, 1 by a private think).

@antra-tess, we're asking you to review this before it's merged, since it changes what an agent reads in its turn's receipt.

Closes #317.

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 this head'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.

Open pull requests

By git merge-tree at 22:15 UTC on October 30, this branch merges cleanly into main at a64cfc010f. Of the open pull requests that merge cleanly into a64cfc010f, four conflict with this branch: #940 and #931 in src/framework.ts, #844 in test/present-while-acting.test.ts, and #190 in src/framework.ts and test/explicit-prose-routing.test.ts.

Tests

One run, from a clean checkout of this head, 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 six test files this change touches 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.

  • This head: 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.

Not verified

Line numbers are this head's.

  • Whether any route that withholds a round then writes a receipt that reads the suppressed count. No test covers it. Two probes covered two shapes of it: a stream that completes while the round's script runs, and a provider error retried once. Run at 48f26ce56c, an earlier main, each with a probe test added, neither withheld the round at all: each round was routed when its code_execution call answered.
  • 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. A probe with 48f26ce56c's src/, through a silent heartbeat tick whose held retry merged into a start that wasn't silent, found that the merged start ran neither silent nor 'private'. This change doesn't touch that path. 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.
  • Node 22.12.0, which CI's test job uses, and the rest of the suite. We ran the six test files this change touches. CI runs the whole suite when this pull request opens.

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

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 this description.

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.
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.

The turn's delivery receipt gives one reason for every suppressed prose segment

1 participant