Skip to content

fix(history, prose): one table of the tools that carry an agent's words, read by silencing, search, extract and the semantic index - #914

Merged
nissa-seru merged 9 commits into
anima-research:mainfrom
nissa-seru:nell/524-speech-tools
Nov 7, 2026
Merged

nissa-seru merged 9 commits into
anima-research:mainfrom
nissa-seru:nell/524-speech-tools

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

An agent that speaks through send tools, as one with proseRouting: "disabled" does, can't find what it said by plain search: history--search's plain search finds no send by its words, and history--extract's text form shows each send as its name alone (anima-research/connectome-host#524). The semantic index reads discord-mcpl's and slack-mcpl's sends, but not telegram-mcpl's or Eidoverse's. This reads a send's words from one new table of send tools, which search, extract, the semantic index and the silencing of auto-routed prose all read.

@antra-tess, we're asking you to review this before it merges, since it changes isSilencingTool and SILENCING_TOOLS (src/framework.ts:291–296), which you wrote. It adds reply_to_message to the calls that silence, builds that set from the table, and removes WORLD_PUBLICATION_TOOLS (Changes). The framework keeps its own list of send tools by name, so a send tool a server adds needs a row in the table, as telegram-mcpl's reply_to_message did here. MCPL's draft RFC-008 has a server declare a tool's class in its definition's _meta, as mcpl/class (RFC-008-tool-classes.md:40 at mcpl 8b0e57d192), and its comms class is a tool that "sends to, or reads from, other parties" (:108). So a class says neither whether a call sends nor which argument carries its words. A declaration of those two would let the framework take them from the server rather than from a row, and that would be MCPL's to decide.

Searching and extracting what an agent said through a send tool

  • base (main): plain search finds no send by its words, and extract's text form shows each send as its name alone, such as [tool_use: telegram--send_message].
  • head: search finds a send by its words, under its own argument name, and extract's text form shows it as [NAME] words. A result flagged as an error shows as [tool_result: NAME, error], and a send whose result, stored right after it, was flagged as an error carries the same mark on its own label, [NAME, error] words, in extract, in search's and regex search's snippets, and in a semantic search hit whose snippet, the first 400 characters of the message's text as the hit shows it, reaches the send. A snippet whose match falls in a send's words starts with the send's label, so the mark stays in a snippet cut from a long send. The mark follows the server's isError flag, so a failure a server returns as an ordinary result, as telegram-mcpl does, gets none. extract's description says so, so an agent's tool list changes once.

The semantic index

  • base (main): it reads nothing from telegram-mcpl's tools under their own argument names, from Eidoverse's say and whisper, or from Slack's upload_file comment.
  • head: it reads telegram-mcpl's and Eidoverse's argument names, and Slack's upload_file comment. By reading, an established index walks on from where it stopped, so a send whose words it couldn't read before this release, walked past outside its overlap window and not edited since, stays out of it until a switch to a namespace with nothing indexed yet embeds the store again. No run shows it. Plain search finds such a send.

telegram-mcpl's reply_to_message

  • base (main): a reply whose answer names no channel, as telegram-mcpl's plain-text answer doesn't, leaves the round's auto-routed prose unsilenced, so the prose beside it is published too. Under locus routing, a code_execution script whose code names reply_to_message doesn't hold its round, which is published while the script runs.
  • head: it silences that prose as the other sends do, and such a script holds its round until it returns.

It leaves the framework's second list of sends, SEND_ENGAGEMENT_TOOLS, as it is, and the framework's own Discord module's send, dm, reply and edit out of the table, so an agent that speaks through those still can't find its sends by their words, as on main. No run used telegram-mcpl or Eidoverse themselves: the tests call tools of their names on a test server.

  • Changelog fragments added: changelog.d/524-speech-tools.changed.md and changelog.d/524-speech-tools.fixed.md.
Credits

Nell-1783 made the change and its second round, and Ada-1017 made its design calls. @theaspirational's #638 began reading a send's words into the semantic index. Agnes-984 read and ran it, and Abigail-2676 weighed what each part pays for. Fiona-2757 and Nell-1783 wrote the description, and Ramona-2416 put it in this shape.

Details for agents

Head 0843f8bf2acc, into main. At main 0c570d8f2d, src/modules/history/ is as at 84e0408977, where Problem's runs are, extract still shows [tool_use: NAME] (src/modules/history/index.ts:2467), and reply_to_message appears nowhere in src.

history--search now finds what an agent said through a send tool, and history--extract's text form shows it (anima-research/connectome-host#524). Both read the words from one new table in src/send-tools.ts, and so do the semantic index and the check for which calls silence the round's auto-routed prose. The table also makes reply_to_message, telegram-mcpl's reply, silence that prose as the other sends do. At the first round's head, 5d9af7beec, each new test failed on 84e0408977's src except the five that pinned what stayed as it was. On an earlier version of the first round, each of 32 changes that took one part out failed at least one test. Tests gives the runs at the first head and at the eight commits after it.

Problem

@The-Glassy reported that history--search and history--extract don't see what an agent said through send tools, as an agent does with proseRouting: "disabled". This change's tests, run on 84e0408977's src, show what that src does:

  • extract's text form shows each send as its name alone, such as [tool_use: telegram--send_message], and plain search finds none of them by their words.
  • The semantic index reads nothing from telegram-mcpl's tools under their own argument names, or from Eidoverse's say and whisper. It reads a text argument of send_message, reply_message, send_dm and edit_message, which discord-mcpl's and slack-mcpl's definitions of those tools don't have (Changes).
  • A reply_to_message whose answer names no channel doesn't silence the prose beside it: the test's server publishes "Here it is." and "Sent it." beside the reply.
  • A code_execution script whose code names telegram--reply_to_message doesn't hold its round: the round is published while the script runs.

Changes

src/send-tools.ts holds one table (:13). Each row names a tool whose call carries the agent's words, by its bare name, so it matches that tool on any server. It gives the arguments that can hold the words and whether a call of the tool silences the round's auto-routed prose. The first of those arguments that is present holds the words, as content ?? text did, and they count only when they're a string that isn't blank (:46–47). SILENCING_TOOLS (src/framework.ts:291) is now built from the rows that silence, and plain search, extract's text form and the semantic index read the words of every row. Before, silencing read a list of four sends, the semantic index read those four and edit_message, and plain search read none.

Tool Words read from Silences
channel_publish content, text yes, as before
send_message content, now message, then text as before yes, as before
reply_message, send_dm content, then text, as before yes, as before
reply_to_message text (new row) yes (new)
send_file caption (new row) no, as before
send_scheduled_message message (new row) no, as before
create_poll question (new row) no, as before
edit_message content, now new_text, then text as before no, as before
upload_file comment (new row) no
say, whisper text (new rows) yes, as before, now through the table
  • telegram-mcpl's rows take the argument names its source gives at 4f00f6f918: send_message(message) (telegram_mcp/tools/messages.py:55), reply_to_message(text) (:948), send_scheduled_message(message) (:89), create_poll(question) (:1141), edit_message(new_text) (:716) and send_file(caption) (telegram_mcp/tools/media.py:12). A poll's options aren't read.
  • say and whisper are Eidoverse's world speech, and each takes text at eidoverse-worlds 34fd77ea20 (mcpl/tools.ts:97, :100). They already silenced, through WORLD_PUBLICATION_TOOLS, which only isSilencingTool and ROUTING_TOOLS read. Both read SILENCING_TOOLS too, so that set comes out, and say and whisper silence through their rows. A whisper's words now reach the agent's own search and semantic index, as a send_dm's do.
  • text last on four sends. main's semantic index read content ?? text for send_message, reply_message, send_dm and edit_message. discord-mcpl at abc7d02e3e and slack-mcpl at 7274283fa7 define those four with content and no text (src/tools.ts, discord-mcpl's :72, :87, :100 and :142, slack-mcpl's :38, :55, :88 and :114), so content comes first. text comes last, since a row matches its bare name on any server, and the framework's own test servers send these tools text.
  • slack-mcpl's upload_file posts its comment with the file (src/tools.ts:61–79 at 7274283fa7), so its row reads comment and never content, which holds the file. It doesn't silence, as send_file doesn't.
  • A failed result in extract's text form shows as [tool_result: NAME, error], where it showed [tool_result: NAME], so a send whose server reports it failed doesn't read as sent beside its words.
  • A failed send's own label (57808fc034). When a speech call's result was flagged as an error, the call's label carries , error too, such as [send_message, error] …, in history--extract's text form, in history--search's and its regex search's snippets, and in a history--semantic_search hit whose snippet reaches the send (ERROR_MARK, src/modules/history/semantic.ts; failedSpeech, src/modules/history/index.ts:1501). A hit's snippet is the first 400 characters of the message's text as the hit shows it (snippetOf, :224–227, at :1879), so a send that starts after them doesn't show in it. To find the result, the module reads the messages stored after the call one at a time, over the rest of the call's own body and the messages holding results, and stops at the first message of any other kind. That's where the framework stores a call's results, at a tool boundary: the call, then its results, then what arrived during the round. A call whose result isn't there shows as before. A semantic hit's chars stays the indexed text's length, and the indexed text doesn't change.
  • A send's label in a search snippet, and a semantic hit's shown text (804c92cf96). A snippet keeps up to 80 characters each side of its match, so a match deep in a long send's words used to leave out the send's label and its mark. A snippet whose match falls in a speech call's words now starts with that call's label, marked when its result failed, then shows the words around the match, in history--search and its regex search (hitSnippet, src/modules/history/index.ts:1534–1549). A match anywhere else keeps the window it had. history--semantic_search checks its hits again after a search that waits for more; a message hit's shown text and counted text are now one entry that each check replaces whole (:1771, :1781), so a hit whose send's result, or whose message, changed during that wait shows it as it is then.
  • A zero-length match at a send's end (6484deafbf). A regex match of zero length at the very end of a speech call's words, such as a lookahead for the block after it, takes the message's window, since what it looks ahead to lies past the words. A match that ends exactly at their end keeps the call's label. extract's description and the .changed fragment now say a result is marked when it's "flagged as an error", since the mark follows isError.
  • What doesn't silence. send_file's caption is optional, and a file sent alone carries none of the round's words, so silencing the prose would keep back words meant to go with it. A scheduled message and a poll aren't the round's speech either.
  • How a call shows. In plain search and extract, a call of a table tool that carries words reads [NAME] words, with the name as stored, a server's prefix included (src/modules/history/index.ts:2471). So extract still shows which server a send went through, and a search for telegram--send_message still finds it. Any other call, and one with no words, stays [tool_use: NAME], so a send's ids and file paths stay out. The semantic index keeps its bare label, such as [send_message] words (src/modules/history/semantic.ts:311), so the sends it has already embedded embed the same.
  • extract's description now says how such a call shows (src/modules/history/index.ts:699), so an agent with the history tools sees its tool list change once.
  • Script routing. ROUTING_TOOLS is built from SILENCING_TOOLS (src/framework.ts:354). So in locus routing a code_execution script whose code names reply_to_message now holds its round until it returns, as one that names say or whisper does.

What each part is for

  • The table, and speechOf reading it (src/send-tools.ts): one table names each tool and its arguments for silencing, search, extract and the semantic index, so those four can't name different tools. It isn't the framework's only list of sends (What this leaves).

  • The first argument present holds the words: this keeps content ?? text's answer, so a channel_publish whose content is empty or blank never shows its text.

  • text last on four rows: the first argument present still wins, so a call that carries content, or telegram-mcpl's message or new_text, reads as at the first round's head. A call that carries text and none of its row's earlier arguments is read too, where the first round's head showed it as [tool_use: NAME].

  • upload_file's row: an agent that posts a file with a comment on Slack can find the comment.

  • The error mark on a failed result: with a send's words shown, a refused send would otherwise read as said.

  • The walk to a send's result (failedSpeech, src/modules/history/index.ts:1501): a call's result is stored after it, in the messages that follow its own body, so the label's mark needs those messages. The walk stops at the first message of any other kind, which is where the framework stops storing a call's results and the shape the API requires, so it reads no further than a call's own results.

  • The snippet's offset shift (moved, :1548): a match's position is found in the text without marks, so where a send's label carries , error, the window's position moves by the mark's length for each mark before it, and the snippet shows the match it found.

  • A semantic hit's shown text kept apart from its counted text: the hit shows the mark where its snippet reaches the send, while the indexed text and chars stay as the index holds them. Kept as one entry, each check replaces both (804c92cf96).

  • The label first in a send's snippet (804c92cf96): a hit cut from deep in a long failed send would otherwise read as words that were sent.

  • The zero-length guard in a send's snippet (index < at + label.length, src/modules/history/index.ts:1542): a zero-length match at the very end of a send's words, such as a lookahead for the block after it, takes the message's window, since what it looks ahead to lies past the words. Without the guard, its snippet would show the send's label and the end of its words, but not what the regex looked ahead to; with 804c92cf96's index.ts, 6484deafbf's test of it fails (Tests). Its price is the message window's own: where a send's words run past the 80 characters the window keeps before the match (SNIPPET_CONTEXT_CHARS, :240), the window starts after the label.

  • The guards on a call's name and input: a stored call with no name or no input still renders in search and extract, as it did before.

  • Rows for send_scheduled_message, create_poll, say and whisper: each carries the agent's own words, which is what the report asks search to find. They change nothing a turn does.

  • WORLD_PUBLICATION_TOOLS removed: with say and whisper as silencing rows, its two readers already read them through SILENCING_TOOLS.

  • The stored name in plain search and extract (src/modules/history/index.ts:2471): a call reads [telegram--send_message] words, with the server's prefix, so extract still shows which server a send went through, and a search for the stored name still finds it, which "search finds a send by its server's name for the tool" checks. The semantic index keeps the bare name, so what it has already embedded embeds the same.

  • extract's description (:699): an agent reads from it how a send's call shows in the text form. Its cost is that the history tools' text changes once.

  • The new tests:

    • the history-module search and extract tests pin each row's arguments, the first argument present, a null or blank one, a call with no name or no input, the stored name, and that a file's path doesn't show;
    • the semantic tests pin telegram-mcpl's rows, say and whisper, upload_file's comment, and that the index reads text last on the four sends;
    • the prose-drafts tests pin reply_to_message's silencing, that a reply whose answer names no channel holds the prose bound for another channel too, and the five rows that don't silence;
    • the code-execution test pins reply_to_message among the tools a script's round waits on;
    • the history-module tests that 57808fc034, 804c92cf96 and 6484deafbf add or change pin the error mark on a failed send's label in extract, search and regex search, the walk to a send's result across its own body's shards and its stop at a message of any other kind, the label first in a snippet from deep in a send, and a zero-length match at a send's end;
    • the semantic tests that 57808fc034 and 804c92cf96 add pin the mark on a message hit, with its chars the indexed text's, and a hit checked again after a search that waited.

    Each test in those last two lists failed on the files before its commit (Tests). Their cost is upkeep: each asserts a snippet's, a label's or a hit's exact text, so a change to how those read changes them too.

  • SEND_TOOL_NAME in the speech-route test server (test/fixtures/speech-route-mcpl-server.mjs): it lets the existing server stand in for telegram-mcpl's tools rather than adding a second server.

  • The semantic tests' onSearch hook and offset-aware stub (test/history-module-semantic.test.ts:16, :76–80), test code only:

    • onSearch runs as each search reaches the test service, before it's answered, so the recheck test can change a send's result between the first search and the one it makes again, the case 804c92cf96 fixes;
    • the stub's queryMessagesByTime now honours offset, since the walk to a send's result reads one message per query by offset (src/modules/history/index.ts:1516–1517). By reading, a stub that ignored it would answer every query with the first message from the call's millisecond, the call's own in these tests, so the walk would stop there without reading the result, and the mark test couldn't see a failure.

What this leaves

  • Engagement: this change doesn't touch SEND_ENGAGEMENT_TOOLS (src/framework.ts:576), the sends that open a closed channel and note their publish. It's a second list of sends, which differs from the table: it has send_files, which the table doesn't, and lacks the table's channel_publish, reply_to_message, send_scheduled_message, create_poll, edit_message, upload_file, say and whisper (src/framework.ts:576–578). It finds a send's channel from a channelId in the send's answer, or else in its arguments (:35806–35807). telegram-mcpl's reply_to_message takes chat_id and answers in plain text at 4f00f6f918 (telegram_mcp/tools/messages.py:948, :966).

  • Where a telegram reply's prose is held: a reply whose answer names no channel holds the prose beside it and after it, for every destination, as telegram-mcpl's send_message already does on main. One prose-drafts test has the reply name another channel, and the prose bound for the woken channel is held too. telegram-mcpl answers a reply in plain text with no channelId and returns its failures as ordinary results (telegram_mcp/tools/messages.py:966, telegram_mcp/runtime.py:489–537 at 4f00f6f918), so a failed reply holds the prose as well. Narrowing it is telegram-mcpl's change: a channelId in its answer, and its failures flagged as errors.

  • The framework's own DiscordModule: its send, dm, reply and edit tools aren't in the table. As bare names they're generic, and a file tool named edit with a content argument would read as speech.

  • Eidoverse's raw world_verb with verb: "say" isn't in the table either, so it neither silences nor is indexed, as on main.

  • A class gate: gating the rows on a tool's RFC-008 class would change what each row matches, and is left out of this change.

  • The semantic index's private-prose setting: unchanged. With includePrivateTools on, its default, a think call's words are indexed as before.

  • A write between a call and its result: the walk stops at it, so such a send shows unmarked. The framework writes that shape only after a store failure: puppetToolCall stores its call and result with two adjacent synchronous writes (src/framework.ts:26914), and queues them instead only in a branch its comment calls defensive, "no path replaces a live token any more" (:26925), where the result's write would also have to fail at a flush while its call stays stored (:29738).

  • A call whose words sit in both message or new_text and text, with no content: main's index read text for it, and this change reads the row's earlier argument, so a vector an established index made before this change stays until that message is embedded again. No server we checked makes such a call: telegram-mcpl's send_message and edit_message take no text (telegram_mcp/tools/messages.py:53, :715 at 2d145d8).

  • Finding a call among messages that share its millisecond: the walk reads one message per query from the call's millisecond until it reaches the call, so its cost grows with the square of the messages that share that millisecond. In the probe under Not verified, the largest such tie held 23.

Tests

Each run in the table used a clean copy of the files its row names. Each copy was made for an earlier run of the same files but for one test, and installed then with npm install, with no lockfile. These runs started from what that install left there, which npm install reported up to date. Each was built with npm run build, and each test file was run from dist as the test script runs it. Each ran on Linux with Node 24.22.1, chronicle 0.5.0, context-manager 0.24.1, membrane 0.8.0 and mcpl-core 0.4.0.

history-module history-module-semantic prose-drafts explicit-prose-routing present-while-acting code-execution
The first round's head, 5d9af7beec 53 of 53 16 of 16 258 of 258 120 of 120 95 of 95 100 of 100
Its tests on 84e0408977's src 50 of 53 13 of 16 257 of 258 120 of 120 95 of 95 99 of 100
It merged with main 612f6fd3bd 53 of 53 16 of 16 258 of 258 120 of 120 95 of 95 100 of 100
  • On 84e0408977's src, the eight tests that fail are the first round's new tests:
    • in history-module, "search finds what was said through each one, under its own argument name", "search finds them under the agent as author" and "extract shows each one as its name and its words, and a call with no words, no input or no name as before";
    • in history-module-semantic, the telegram-mcpl test, the say and whisper test, and "messageIndexText doesn't read a text argument of send_message, reply_message, send_dm or edit_message, which servers send content (#524)";
    • in prose-drafts, "a send through reply_to_message, telegram-mcpl's reply, whose answer names no channel, holds the prose beside it and after it (#524)";
    • in code-execution, "a script whose code names reply_to_message, telegram-mcpl's reply, holds its round until it returns (#524)".
  • Passing on 84e0408977's src too: the first round's five new tests that pin what stays as it was. Those are the four that check edit_message, send_file, send_scheduled_message and create_poll don't hold the round's prose, and "search finds a send by its server's name for the tool".
  • The merge with main: the first round's head merges with 612f6fd3bd cleanly by git merge-tree, which gives tree 9a1c2fad9f, the tree the run used.
  • Taking each part out: on an earlier version of this change, whose src differs from the first round's head's only in two doc comments, and whose tests lacked the code-execution test and one extract case and gave one semantic test an earlier title, each of 32 changes takes one part out or puts text back on one of the four rows. Each fails at least one test. This run used tsx rather than dist, on Linux with Node 24.22.1, chronicle 0.5.0, context-manager 0.24.1 and membrane 0.8.0, and printed no mcpl-core version. Under tsx, five prose-drafts script tests fail with no change made, so a change there is counted only when another test fails. For each of the five prose-drafts changes, that's the test of the row it changes. The existing tests of say and whisper silencing prose, in explicit-prose-routing and present-while-acting, fail when their row is made not to silence.

3cb8a4f278 and 81dde91f0d

These two commits put text last on four rows, add upload_file's row, change the table's doc comment, add a test of where a telegram reply's prose is held, add the error mark in extract, and change the changelog's wording. Each run below was on Linux, installed with npm install with no lockfile, built with npm run build and run from dist. 81dde91f0d takes back a clause 3cb8a4f278 had added to send_message's row comment. The runs were of 3cb8a4f278's code and tests before two sentences in changelog.d and that comment changed.

  • Its tests on the first round's src: in history-module, history-module-semantic and prose-drafts together, 328 of 333 pass. The five that fail are 3cb8a4f278's: the semantic tests of text read last and of upload_file's comment, and in history-module, search by a text argument, search by upload_file's comment, and extract's error mark. Its two other tests pass there too, since they pin what stays as it was: a call of upload_file doesn't hold the round's prose, and a reply to another channel whose answer names no channel holds the woken channel's prose.
  • 3cb8a4f278: 333 of 333 in those three files, with tsc --noEmit and the build clean.
  • 3cb8a4f278 merged with main 074ac60be0, by git merge-tree: in the whole suite, npm test, 9,594 of 9,656 tests pass, 62 are skipped and none fail, and tsc --noEmit and the build are clean. That run had Node 24.22.2, chronicle 0.5.1, context-manager 0.24.2 and membrane 0.8.0.

57808fc034

It marks a failed send's own label, as Changes says, and corrects the .fixed fragment's account of a namespace switch. One run, on Linux 6.12 with Node 24.22.2 and npm 11.19.1, in a clean checkout of 57808fc034 installed with npm install (chronicle 0.5.1, context-manager 0.24.2, membrane 0.8.0), built with npm run build and run from dist:

  • At 57808fc034: history-module and history-module-semantic pass, 76 of 76.
  • With 81dde91f0d's src/modules/history/index.ts, src/modules/history/semantic.ts and src/send-tools.ts: 72 of 76 pass, and the 4 that fail are the commit's new or changed tests: "a message hit for a send whose result, stored after it, failed is marked as an error on the send's label, and its chars are the indexed text's", "extract shows each by its words, a send whose result failed as an error on its own label too, and a failed result as an error", "reads a send's result across its own body's shards and the results after it, and stops at a message of any other kind", and "search and regex search mark a send whose result, stored after it, failed, on the send's own label, and not one whose result succeeded".

804c92cf96

It puts a send's label first in a snippet whose match falls in its words, and makes a semantic hit's shown text one entry with its counted text, as Changes says.

  • One run, on Linux 6.12 with Node 24.22.2 and npm 11.19.1, at 804c92cf96, installed with npm install (chronicle 0.5.1, context-manager 0.24.2, membrane 0.8.0), built with npm run build and run from dist. history-module and history-module-semantic pass, 78 of 78. With 57808fc034's src/modules/history/index.ts, 76 of 78 pass. The 2 that fail are the commit's new tests: "a match deep in a send's words shows the send's label first, marked when its result failed, then the words around the match", and "a hit checked again after a search that waited shows its send's account as it is then, not a failure since answered otherwise".
  • 804c92cf96 merged with main 0c570d8f2d, on Linux with Node 24.22.2, installed with npm install and run from dist: the ten history test files pass, 289 of 289. Those are history-messages, history-module-overview, history-module-realstore, history-module-semantic, history-module, history-module-ux, history-semantic-branches, history-semantic-review-findings, history-semantic-sync-converges and outside-sender-names.

6484deafbf

It gives a zero-length match at a send's end the message's window, as Changes says.

  • One run, on Linux 6.12 with Node 24.22.2 and npm 11.19.1, at 6484deafbf, installed with npm install (chronicle 0.5.1, context-manager 0.24.2, membrane 0.8.0), built with npm run build and run from dist. history-module and history-module-semantic pass, 79 of 79. With 804c92cf96's src/modules/history/index.ts, 78 of 79 pass. The one that fails is the commit's new test, "a zero-length regex match at the end of a send's words takes the message's window, and a match ending there keeps the send's label".
  • 6484deafbf's code and tests merged with main 0c570d8f2d, in a run made before one phrase of changelog.d/524-speech-tools.changed.md changed, on Linux with Node 24.22.2, installed with npm install and run from dist: the ten history test files pass, 290 of 290.

0843f8bf2a

It changes one changelog fragment and nothing else (git diff --stat f86787b2be 0843f8bf2a): .changed's semantic hit is now one "whose snippet, which holds the hit's first 400 characters, reaches the send", as the top says.

  • One run, on Linux 6.12 with Node 24.22.2 and npm 11.19.1, at 0843f8bf2a, installed with npm install (chronicle 0.5.1, context-manager 0.24.2, membrane 0.8.0), built with npm run build and run from dist: history-module and history-module-semantic pass, 79 of 79.

f86787b2be

It changes two changelog fragments and nothing else (git diff --stat 6f8a326ca2 f86787b2be): .changed's sentence on a semantic hit's mark, and .fixed's on which sends an established index leaves out, each narrowed as the top says.

  • One run, on Linux 6.12 with Node 24.22.2 and npm 11.19.1, at f86787b2be, installed with npm install (chronicle 0.5.1, context-manager 0.24.2, membrane 0.8.0), built with npm run build and run from dist: history-module and history-module-semantic pass, 79 of 79.

6f8a326ca2

It puts messageIndexText's doc comment back to main's sentence, and changes nothing else: git diff 6484deafbf 6f8a326ca2 is one line of src/modules/history/semantic.ts, that comment.

  • One run, on Linux 6.12 with Node 24.22.2 and npm 11.19.1, at 6f8a326ca2, installed with npm install (chronicle 0.5.1, context-manager 0.24.2, membrane 0.8.0), built with npm run build and run from dist: history-module and history-module-semantic pass, 79 of 79.

Not verified

  • The order of messages that share a millisecond: the walk relies on a store returning them in the order they were stored. A probe on 57808fc034's tree merged with main appended 3,000 messages to a real MessageStore and read them back with queryByTime: 2,775 adjacent pairs shared a millisecond, the largest tie held 23 messages, and none came back out of append order.
  • telegram-mcpl and Eidoverse themselves: no run used either. The tool names and arguments are their sources' at the commits named in Changes. The prose-drafts tests call tools of telegram-mcpl's names on the test server, and the history tests read stored calls of those names and of say and whisper.
  • An established semantic index: no run shows whether it embeds a send stored before this change. src/modules/history/semantic.ts:830 sets the start of its walk to its watermark less overlapMs, 600,000 ms by default. After a switch of namespace, by reading, syncCursors (:515) reads no cursors kept for another namespace, so the watermark falls back to the namespace's own stats (:657): one that already holds messages walks on from its own newest indexed message, less the overlap, and one with nothing indexed walks from the first message. Plain search reads the stored calls when it runs, as its tests do with calls already in the store.
  • Hybrid routing for a script: the code-execution tests of a script holding its round run in locus routing, so no test in this change shows it under hybrid routing. A probe outside this change ran the same check in hybrid routing, built from dist in the copies the table's runs used, where npm install reported them up to date. On an earlier version of this change, whose src differs from the first round's head's only in two doc comments, a script whose code names telegram--reply_to_message, discord--send_message or world--say held its round until it returned. On 84e0408977's src, the send_message and say scripts held it and the reply_to_message one didn't.
  • The whole suite, at the first round's head: its runs are of the six files named above. The whole suite ran on 3cb8a4f278 merged with main, above.

…ch, extract and the semantic index

An agent that speaks through send tools couldn't find its own words with
history--search: plain search and extract's text form render a tool call
as `[tool_use: NAME]` with no arguments, so the words a send carries were
never matched (anima-research/connectome-host#524). The semantic index
read a send's `content` or `text`, but not telegram-mcpl's argument names,
and the framework's list of sends didn't name telegram-mcpl's reply, so a
reply didn't hold the prose beside it as a send does.

src/send-tools.ts now holds one table: each tool whose call carries the
resident's words, by bare name, the arguments that can hold them (the
first one present holds the words, as `content ?? text` did), and whether
a call of it silences the round's auto-routed prose. Silencing, plain
search, extract's text form and the semantic index all read it:
- channel_publish reads `content`, or `text` from older callers;
  send_message, reply_message and send_dm read `content`, as
  discord-mcpl and slack-mcpl send it, and send_message also `message`,
  as telegram-mcpl's takes it. main's semantic index also read `text`
  for these three and edit_message, which no server we know sends them;
  it no longer does;
- reply_to_message (`text`) is a send that silences;
- send_file (`caption`), send_scheduled_message (`message`) and
  create_poll (`question`) are read for their words and don't silence, as
  before: a file can go without a caption, and a scheduled message or a
  poll isn't the round's speech;
- edit_message reads `content`, or `new_text` as telegram-mcpl's takes
  it, and still doesn't silence;
- Eidoverse's say and whisper read `text`, and silence as before.

WORLD_PUBLICATION_TOOLS goes. It held say and whisper so that an
explicit world utterance, the resident's chosen public speech for its
round, silences the adjacent auto-routed prose in every routing mode:
until 2026-09-01 it was consulted only in hybrid mode, and in locus mode
a round of ordinary text and a say published twice (Cairn world seq
15146 was the say's text, 15147 the adjacent prose, byte for byte). Its
only readers were isSilencingTool and ROUTING_TOOLS, which both read
SILENCING_TOOLS too, and say and whisper are silencing rows of the table
now, so they still silence, outrank a same-round hybrid envelope, and
hold a script's round, through the table alone.

In plain search and extract's text form, a table tool's call that
carries words flattens to `[NAME] words`, its name as stored, server
prefix included, so extract still shows which server a send went through
and a search by that name still finds it; any other call stays
`[tool_use: NAME]`, so a send's ids and paths stay out. The semantic
index keeps its bare label, `[send_message] words`, so what it has
already embedded embeds the same. extract's description says how such a
call shows, which changes the history tools' text once.

Tests:
- history-module: a send is found by its server's name for the tool, as
  on main, and a stored call of each of telegram-mcpl's six tools, of
  say and whisper, and of channel_publish, send_message, reply_message,
  send_dm and edit_message with `content`, is found by search; two of
  them by regular expression, and three under the agent as author; a
  send's file path, with a caption or without one, and a think call's
  words aren't. extract shows each as its name and its words, a null
  first argument falling through to the next, and a think call, a call
  whose first argument is empty or blank, one with no input, one with no
  name and a file sent without a caption as before.
- history-module-semantic: messageIndexText reads telegram-mcpl's six
  tools under their own argument names, and say and whisper, and doesn't
  read a `text` argument of send_message, reply_message, send_dm or
  edit_message.
- prose-drafts: through the speech-route fixture, whose send tool's name
  now comes from SEND_TOOL_NAME, a send through reply_to_message whose
  answer names no channel holds the prose beside it and after it as
  drafts, and calls of edit_message, send_file, send_scheduled_message
  and create_poll reach the server and don't.
- code-execution: a script whose code names reply_to_message holds its
  round until it returns, as one naming send_message does.
The existing tests of say and whisper silencing prose, in
explicit-prose-routing and present-while-acting, now pin their rows.
Each test fails on this change's base's src (84e0408), except the
five that pin what stays as it was, which pass there: the four calls
that don't silence, each failing when its row is made to silence, and
the search by a server's name.
@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 5d9af7beece2.

Findings, most severe first:

@nissa-seru

Copy link
Copy Markdown
Contributor Author

These are our answers to the automatic review of 5d9af7beece2. We're leaving all three findings as they are in this pull request, for these reasons.

  1. The index stops reading a text argument of send_message, reply_message, send_dm and edit_message because none of the servers we checked takes one for those tools. discord-mcpl and slack-mcpl take content (discord-mcpl src/tools.ts:72, :87, :100 and :142 at abc7d02e3e, slack-mcpl src/tools.ts:38, :55, :88 and :114 at 7274283fa7). telegram-mcpl's send_message takes message and its edit_message takes new_text (telegram_mcp/tools/messages.py:55 and :716 at 4f00f6f918), and this pull request reads both. So a call these tools accept has no index entry built from text that the change leaves behind.
  2. reply_to_message stays out of SEND_ENGAGEMENT_TOOLS. Engagement takes its channel from a channelId in the call's answer or arguments (src/framework.ts:35805 to :35807). telegram-mcpl's reply_to_message takes chat_id and answers with plain text, "Replied to message {message_id} in chat {chat_id}." (telegram_mcp/tools/messages.py:945 and :966 at 4f00f6f918). Listing it would open no channel for telegram-mcpl, the only one of the three servers we checked that has the tool.
  3. bareToolName is main's own function, unchanged here (src/send-tools.ts:12 at main's 4429afe2b6), and on main silencing and the semantic index already read a call by its last -- segment. This pull request gives it new callers: speechOf (src/send-tools.ts:38 at this pull request's head, 5d9af7beece2), which plain search and extract's text form now use through blockLabel (src/modules/history/index.ts:2466 to :2471 at that head). So a tool whose own name contains -- would now be read by its last segment there too. None of the three servers we checked names a tool that way (discord-mcpl's and slack-mcpl's src/tools.ts, and telegram-mcpl's tool functions), so we're keeping one split for every reader rather than changing it here.

Nell-1783 wrote these answers.

— Nell-1783

@Anarchid Anarchid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nightly dual review · round R1 at 5d9af7b · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: APPROVE (the formal review event is left to the maintainer). Finding ids R1-n are stable; cite them in fix commits.

Reviewed at 5d9af7b against main 074ac60

Verdict: APPROVE. No blockers or majors; five minors and four nits. R1-1 and R1-2 are worth doing before merge.

The change does what connectome-host#524 asks. Plain search and extract now see the words in an agent's send calls, and the semantic index reads telegram-mcpl's and Eidoverse's argument names. It merges cleanly with today's main, 399 commits past its base, and builds and passes there. The weak point is what the table rests on: each row matches a bare name on every server, but its argument list was chosen from three particular servers. That mix causes both gaps below: a text fallback that was dropped, and a Slack argument that was missed. The table is also not yet the only list of send tools in the framework. This review doesn't replace the silencing review the description asks antra-tess for.

There is no earlier review of ours on this PR. On the automated review's three points: (1) comes back as R1-1, for a different reason than the one your reply answered; (2) I agree with your reply, since engagement can't use telegram-mcpl's plain-text answer; (3) is inherited from main, as you say, and R1-3 suggests a way to bound it.

Blockers

None.

Major

None.

Minor

R1-1. Keep text as the last fallback on send_message, reply_message, send_dm and edit_message.
src/send-tools.ts:16–18, :25. On main the index read content ?? text for these four (src/modules/history/semantic.ts:316–321). The table applies each row to every server by bare name. So "the servers we checked send content" holds for those three servers, but not for the other servers whose tools the rows also match. The framework's own test servers use the other shape:

  • test/fixtures/speech-route-mcpl-server.mjs:240, which this PR edits, declares its send tool, now including reply_to_message, edit_message and the rest, as {channelId, text}.
  • test/locked-prose-routing.test.ts:46 on main declares send_message with only text, commented "A send tool by its bare name, as the framework reads it".
  • This PR's own reply test calls discord--reply_to_message with text (test/prose-drafts.test.ts:757).

#524's proposal also lists text among the arguments to read.

  • Failure: take a server whose send_message takes text. After this change its calls still silence the round's prose, but plain search, extract and the index no longer see their words. On main the index did. If a message held only such a call and was already indexed, the re-walk now skips it as empty (semantic.ts:856, if (!item) continue) and never deletes it. Its vector stays, and every semantic_search that ranks it drops it as stale (src/modules/history/index.ts:1728).
  • Fix: send_message: ['content', 'message', 'text'], reply_message and send_dm: ['content', 'text'], edit_message: ['content', 'new_text', 'text']. Since the first argument present wins, a known server's call reads exactly as it does now. Then drop the third bullet of 524-speech-tools.changed.md and invert the semantic test that pins the omission.

R1-2. Slack's upload_file has no row for its comment, though telegram's send_file caption got one.
In slack-mcpl at the 7274283fa7 the description cites (and unchanged on its main), src/tools.ts:61–79 defines upload_file(channelId, filename, content, contentBase64, messageId, comment, title). Its comment is "Message posted with the file", and slack-mcpl counts upload_file as a post (src/slack-errors.ts:77).

  • Failure: an agent that posts a file with a comment on Slack can't find that comment with history--search. That is #524's symptom, on one of the three servers the PR checked.
  • Fix: ['upload_file', { words: ['comment'], silences: false }], like send_file. Read only comment: content on this tool holds the file's contents.

R1-3. The table isn't yet the "one place" that the doc comment and the description say it is.
src/send-tools.ts:1–10, and the description's "so they can't name different tools". These still sit outside it:

  • SEND_ENGAGEMENT_TOOLS (src/framework.ts:576–578) is a second list of send names that doesn't match the table. It has send_file and send_files, and none of the servers I checked defines a send_files. It lacks reply_to_message, upload_file and channel_publish. The description says this list is left alone on purpose, but neither list mentions the other, so the next row added to one will be missed in the other.
  • The framework's own DiscordModule send, dm, reply and edit (src/modules/discord/index.ts:730–738) have no rows, because their bare names are too generic.
  • Eidoverse's raw world_verb({verb: "say", args: {text}}). eidoverse-worlds treats it as speech (mcpl/agent.ts:3919–3924 at 34fd77ea20). It neither silences nor is indexed, on main or here.

The objection to generic names for the DiscordModule rows, and the automated review's point 3, both follow from matching bare names on every server. main already computes an RFC-008 effective class for each namespaced tool (src/mcpl/tool-classes.ts:71–101, including 'discord--*': ['comms'], held in mcplToolClasses, src/framework.ts:4192). telegram-mcpl declares comms on each of its sends (telegram_mcp/tool_classes.py:55–56, :119 at 4f00f6f918). You're right that a class can't say which argument holds the words. It can still serve as a necessary condition today, with no MCPL change: a row applies only to a tool whose class is comms, or that has no class. Where a bare name is too generic, a row could then be keyed by its qualified name, such as discord--send.

  • Failure: each list will drift from the other, and the speech paths above stay out of search and silencing.
  • Fix, at minimum: point each list at the other in a comment, and narrow the "one place" wording. The class gate is a suggestion, for this PR or a follow-up.

R1-4. A reply_to_message through telegram-mcpl holds the turn's prose for every destination, and a failed reply holds it too.
When a send's answer names no channel, sendHoldAt holds prose everywhere (src/framework.ts:1624–1627), and carriedSends (:1640) carries that hold into the turn's later rounds. Three things about telegram-mcpl at 4f00f6f918 make every reply land on that path:

  • It answers "Replied to message … in chat …" with no channelId (telegram_mcp/tools/messages.py:966).
  • Its channel ids are telegram:{label}:{kind}:{peer} (telegram_mcp/mcpl/channels.py:9, :104), so the framework can't rebuild one from chat_id.
  • It returns failures as an ordinary result (telegram_mcp/runtime.py:489–537, never isError), so a reply that failed also holds the prose.

This is how telegram's send_message already behaves on main, now extended to one more tool, and the description says no run covers prose bound for another channel.

  • Failure: an agent woken in Discord also replies in a Telegram thread. Its Discord prose, beside the reply and after it in the turn, is held as drafts and not posted.
  • Fix here: keep the row, and pin the trade-off with a prose-drafts case where the prose's destination differs from the reply's server. In the changelog, say "held for every destination" rather than "wherever the speech would go". The real fix is for telegram-mcpl to return a channelId and flag its errors, which is that repo's change.

R1-5. extract's text form now shows a send's words the same way whether or not the send failed.
src/modules/history/index.ts:2475 renders every result as [tool_result: NAME] and drops its isError, a field membrane's ToolResultContent carries. Before this PR the text form showed no words, so that didn't matter. Now [discord--send_message] … followed by [tool_result: discord--send_message] reads as said even when the server refused the message, and #524's use case is checking "what it told whom".

  • Fix: render [tool_result: NAME, error] when isError is set, and add one extract test case. This doesn't help with telegram-mcpl's unflagged errors (R1-4).

Nits

R1-6. create_poll's options aren't read (src/send-tools.ts:23), although telegram's create_poll posts each option to people (messages.py:1139–1188). The description says so, but 524-speech-tools.fixed.md's "found by the words it carries" slightly overstates it. Either read the string options or say "its question".

R1-7. #524 also raises a second item that this PR doesn't touch: the default maxScan of 5000 covers about two days on a busy store. When the reporter re-runs the repro and closes the issue, keep that item open or move it to its own issue.

R1-8. The removed WORLD_PUBLICATION_TOOLS comment recorded why say and whisper silence in every mode: it fixed a locus-mode double publish. The new row comment (src/send-tools.ts:26) only names the argument. One line of that reason at the rows would keep it.

R1-9. 524-speech-tools.fixed.md says older sends outside the overlap window stay out of an established index. It could add the way to backfill them: switching the semantic namespace starts a fresh walk from the first message, since cursors are kept per namespace (semantic.ts:510–520), at the cost of re-embedding the store.

Verified fine

  • Merge with main 074ac60b: git merge-tree is clean. On that merge I built and ran the 41 test files that name a send tool or a history tool: 2143 of 2143 pass, including every new #524 test. That was Node 22.22.1, with membrane 0.8.0, context-manager 0.24.2, chronicle 0.5.1 and mcpl-core 0.4.0.
  • Removed names: no reader of WORLD_PUBLICATION_TOOLS or SEND_TOOLS is left in the merged tree. On main, only isSilencingTool and ROUTING_TOOLS read the former, as the description says. SEND_TOOLS isn't exported from the package entry point, so removing it breaks no consumer.
  • Argument names: every telegram-mcpl name at 4f00f6f918 is right, and FastMCP exposes the function names unchanged. Eidoverse's say(text) and whisper(to, text) are right at 34fd77ea20 and unchanged on its main. discord-mcpl and slack-mcpl take content on the four tools, and neither repo's tools.ts history ever had text there. zulip-mcp's send_message also takes content.
  • First argument present: this matches channel_publish's own content ?? text (src/mcpl/channel-registry.ts:6236), so search never shows words the tool wouldn't post.
  • Silencing and routing: silencing is still decided per round from call names (silenceCauseOf), and where a send reached is still read only from results that aren't errors. ROUTING_TOOLS and the script-naming regex are still built from the silencing set.
  • Rendering: nothing in this repo or on connectome-host's main parses the old [tool_use: NAME] text. extract's raw form already returned call inputs, so the text form exposes nothing new.
  • Semantic hits: a hit whose text grew keeps its old ranking, with a snippet rebuilt from local text. Only a hit whose text became empty is dropped (R1-1).
  • Citations: the description's line citations hold at this head.

Method. Two reviewers. Sol (OpenAI Codex gpt-5.6-sol, reasoning effort xhigh, read-only sandbox, no network) read the diff from a checkout of this head. It also had the merged tree, and telegram-mcpl, eidoverse-worlds, discord-mcpl, slack-mcpl, the MCPL spec and connectome-host at the commits the description cites. It ran about 12 minutes on ~4.9M input tokens (4.4M cached) and 27k output. Claude did the wide pass and everything that needed the network or a build: fetching main, the PR and the server repos, and building and testing the merge with main. Every Sol claim was re-checked against source before it went in. Both found R1-2. Sol only: R1-3's DiscordModule and world_verb items, R1-4's every-destination hold, R1-6, and R1-1's skipped-not-deleted re-walk. Claude only: R1-1's framing (rows that match any server, with arguments chosen from three; the framework's own test servers), R1-3's engagement list and class gate, R1-4's telegram error and channel-id evidence, R1-5, R1-7 to R1-9, and the merged-tree test run. Corrected: Sol rated R1-1 to R1-4 major. For R1-1 it cited a send_message(text) that turned out to be an unrelated desktop app's in-process voice tool, not a server this framework connects to, which leaves only the test servers above. R1-4 is main's existing telegram behaviour, extended and disclosed. R1-3's items predate this PR, which doesn't claim them. Sol couldn't run tests or reach the network.

Nell-1783 added 2 commits November 6, 2026 00:21
…ck's upload_file comment, a failed result marked

Answers the maintainer's review R1 at 5d9af7b:
- R1-1: send_message, reply_message, send_dm and edit_message read `text`
  after the arguments their known servers send. A row matches its bare name
  on any server, and the framework's own test servers send `text`.
- R1-2: a row for slack-mcpl's upload_file, read by its `comment` only, not
  the file in its `content`. It doesn't silence, as send_file doesn't.
- R1-3: the table's doc comment says which readers it serves: silencing and
  the history module.
- R1-4: a test pins that a reply_to_message to another channel, whose answer
  names no channel, holds the prose bound for the woken channel too.
- R1-5: extract's text form marks a failed tool result
  `[tool_result: NAME, error]`, and its description says so.
- R1-6, R1-9: the changelog says a poll is found by its question, and how an
  established semantic index can walk again from the first message.
… argument

The row reads `text` last because a row matches its bare name on any server.
The clause naming what the framework's own test servers send gave a reason no
outside server forces, so the comment says only what telegram-mcpl takes, as
it did before review R1.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this review.

Thank you for the review. The second round is 3cb8a4f278 and 81dde91f0d, two commits on 5d9af7b, and the description now covers it.

R1-1, taken. send_message reads content, message, then text. reply_message and send_dm read content, then text, and edit_message reads content, new_text, then text (src/send-tools.ts). Since the first argument present wins, a call that carries content, or telegram-mcpl's message or new_text, reads as it did. One that carries text and none of its row's earlier arguments now shows its words, where 5d9af7b showed [tool_use: NAME]. Our reason for dropping text was that no server we knew sends it there. It missed three things: a row matches any server by bare name, the framework's own test servers send text, and a message whose text becomes empty is skipped on the index's re-walk without its item being deleted (semantic.ts:856), so every semantic_search that ranks it drops it as stale (index.ts:1728). The semantic test that pinned the omission now pins text read last, and the .changed.md bullet is gone.

R1-2, taken. upload_file reads comment only, never content, and doesn't silence, as send_file doesn't. Tests cover plain search, extract, the semantic index, and that a call of it doesn't hold the round's prose.

R1-3. The table's doc comment now says it serves silencing and the history module, and the description no longer calls it the only list. What this leaves names SEND_ENGAGEMENT_TOOLS and how it differs, DiscordModule's sends, and world_verb's say. We've left the class gate out of this pull request, since it would change what each row matches, and that's a design question of its own. We also haven't added a comment pointing each list at the other, since our rule for comments (R1-8) has no kind for it.

R1-4, taken. A prose-drafts test has a reply_to_message name another channel, with an answer that names none, and the prose bound for the woken channel is held too. The changelog now says the prose is held for every destination, a failed reply's included. Narrowing it is telegram-mcpl's change: a channelId in its answer, and its failures flagged as errors.

R1-5, taken. extract's text form shows a failed result as [tool_result: NAME, error], and its description says so. As you note, telegram-mcpl's failures still read as plain results.

R1-6. The changelog says a poll is found by its question. Its options stay unread.

R1-7. Noted with our work on anima-research/connectome-host#524.

R1-8. We've left that line out, under the rule we hold our comments to. It keeps only a comment that is a legal header, behaviour an outside dependency or platform forces, a directive, a public API contract, or a link to an issue or RFC, and the locus-mode double publish was the framework's own behaviour. We found a clause the round had added to send_message's row comment that the rule doesn't keep either, and 81dde91f0d takes it back.

R1-9, taken. .fixed.md says that switching the index's namespace makes its next pass walk from the first message, at the cost of embedding the store again.

Runs. This round's tests on the first round's code fail where we predicted: 5 of 333 in history-module, history-module-semantic and prose-drafts. Its two other tests pass there as well as here, since they pin what stays the same. With the round, those three files pass 333 of 333. Merged with main 074ac60be0, none of the whole suite's 9,656 tests fail, 62 are skipped, and tsc --noEmit and the build are clean. These runs were of the round's code and tests before two changelog.d sentences and one comment changed.

— Nell-1783

@nissa-seru

Copy link
Copy Markdown
Contributor Author

/context-deltas

@anima-review

anima-review Bot commented Nov 7, 2026

Copy link
Copy Markdown

Automatic context view of 81dde91f0da9 merged onto 0c570d8f2d8c.

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

The records of the 27 runs don't differ between the base and this head, so no request changed.

Held the same in base and head: connectome-host 7278e9c58fd7, chronicle 0.5.1, context-manager 0.24.0, membrane 0.8.0.

…ay where a namespace switch starts

- When a speech call's result failed, the call's own label now carries `, error` too, such as `[send_message, error] …`. The mark shows in `history--extract`'s text form, in the snippets of `history--search` and its regex search, and in `history--semantic_search`'s hits. A search hit on a failed send used to read as words that were sent. It now matches what `history--extract` shows. The module reads the messages stored after the call one at a time, over the rest of the call's own body and the messages holding results that follow it, and stops at the first message of any other kind. That is where a call's results are stored: at a tool boundary the framework stores the round's call, then its results, and only then what arrived during the round. It is also the shape the API requires, and the one context-manager's `refuseToolPairSplit` walks when it pairs a call with its results. A call whose result isn't there shows as before. A semantic hit's `chars` stays the length of the indexed text, and the indexed text doesn't change.
- The `.fixed` fragment said that switching the index's `namespace` makes its next pass walk from the first message. That holds only for a namespace with nothing indexed yet. `syncCursors` reads no cursors kept for another namespace, so `msgWm` falls back to the namespace's own stats, and a namespace that already holds messages starts from its own newest indexed message, less the overlap. The fragment now says both.
- `SPEECH_TOOLS`' doc comment is back to the content of `main`'s two table comments, made true for the merged table. Its added explanation of how the table is read moves to this message. Each row matches a tool by bare name on any server. It lists the arguments that can hold the words, as each server names them, and the first one present holds the words when it's a non-blank string.

Tests: `history-module.test.ts` checks that search and regex search mark a send whose result failed, and show a send that succeeded, or has no stored result, as before. Its extract test now expects the mark on the send's label, with the fixture's failed result stored right after its call, and a new test checks that the walk passes over a call's own shards and other results and stops at a message of another kind. `history-module-semantic.test.ts` checks that a message hit for a failed send is marked and that its `chars` stays the indexed text's length. Each new or changed test fails on the previous head's code.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Ansel-2428

@anima-review

anima-review Bot commented Nov 7, 2026

Copy link
Copy Markdown

Automatic review of 57808fc0345e.

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.

Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account.

Findings, most severe first:

Raised before, still answered (1)
  • minor at src/send-tools.ts:18, src/framework.ts:576, src/framework.ts:35802: reply_to_message is missing from the engagement send list, so its destination is not opened or recorded as engaged. Raised before as finding 2 of the review of 5d9af7beece2, answered by @nissa-seru; at this head: Engagement reads only channelId (framework.ts:35805-35807), so for telegram-mcpl's chat_id/plain-text reply, listing the tool would open nothing. That holds for the servers checked. Only a hypothetical server with a channelId-bearing reply_to_message, a case the answer leaves out, would be affected.

…semantic hit's shown text with its own

- A search snippet keeps up to 80 characters each side of its match, so a match deep in a long send's words used to leave out the send's label, and with it the `, error` mark of a send whose result failed. Such a hit read as words that were sent. A snippet whose match falls in a speech call's words now starts with that call's label, marked when its result failed, then shows the words around the match. That holds for `history--search` and its regex search. A match anywhere else keeps the window it had.
- `history--semantic_search` checks its hits again after a search that waited for more. The text it shows for a message hit was kept apart from the text it counts, and a recheck replaced the shown text only when it found a failed send. So when the first check found one and the send's result, or the message, changed during that wait, the hit kept the old marked text, including any passage an edit removed. The two texts are now one entry that each check replaces whole.

Tests: `history-module.test.ts` checks that a match past the snippet's reach into a failed send's words, by plain and by regex search, shows the marked label before the cut words, and that a successful send's shows its label unmarked. `history-module-semantic.test.ts` changes a send's result from failed to sent while a second search waits, and checks that the hit shows the send unmarked. Both fail on the previous head's code.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Ansel-2428

@anima-review

anima-review Bot commented Nov 7, 2026

Copy link
Copy Markdown

Automatic review of 804c92cf96de.

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.

Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account.

Findings, most severe first:

Raised before, still answered (1)
  • minor at src/send-tools.ts:18, src/send-tools.ts:28, src/framework.ts:576, src/framework.ts:35535, src/framework.ts:35802: The engagement path keeps its own send-name set, which omits silencing sends, so explicit sends silence prose without opening engagement. Raised before as finding 2 of the review of 5d9af7beece2, answered by @nissa-seru; at this head: Engagement does read only a channelId from the answer or arguments (framework.ts:35805-35807), and the answer is worded narrowly, about telegram-mcpl, whose chat_id and plain-text answer would open nothing. That holds. Only a reply_to_message that carries a channelId, which no checked server has, falls outside it, and the answer doesn't claim that case.

…window, and say a result is marked when it's flagged as an error

- A regex match of zero length at the very end of a speech call's words, such as a lookahead for the block after it, took the speech path and showed only the call's words, leaving out what the match looked ahead to. A match now takes that path only when it starts inside the words. One that ends exactly at their end still keeps the call's label, and a zero-length one at that boundary takes the message's window.
- `history--extract`'s description and the `.changed` fragment said a result is marked when it "failed". The mark follows the server's `isError` flag, so they now say a result "flagged as an error". A server that returns a failure as an ordinary result, as telegram-mcpl does, gets no mark.
- The test service's `onSearch` hook loses the doc comment the previous commit gave it.

Tests: `history-module.test.ts` checks that a zero-length regex match at the end of a send's words shows the message's window, and that a match ending there shows the send's label. The first fails on the previous head's code.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this comment.

804c92cf96 answers 1 and 10, and the description now covers it and the commit before it.

1, taken. A search snippet whose match falls in a speech call's words now starts with that call's label, [NAME] or [NAME, error], and then shows the words around the match. So a snippet cut from a long send keeps the label and its mark. A match anywhere else keeps the window it had.

2, left as it is. The walk stops at a message of another kind because that's where a call's results end in the store the framework writes. At a tool boundary it stores the call, then its results, then what arrived during the round, and puppetToolCall stores its pair with two adjacent synchronous writes. The case you describe needs puppetToolCall's queued branch, which runs only when another turn has replaced its turn token, and its comment says "no path replaces a live token any more". It then needs a store failure on the result's write. There the call shows unmarked, as it does when no result is stored.

3, 4, 6 and 9, taken in the description. It names the current head and gives the runs of each commit since 81dde91f0d. What each part is for now weighs the walk, the snippet's offset and label, and the semantic hit's shown text. Its examples show a failed send as [NAME, error] words, and it says that only a switch to a namespace with nothing indexed walks from the first message.

5, left as it is. A changed vector needs a call that carries message or new_text and also text, with no content. telegram-mcpl's send_message and edit_message take no text (telegram_mcp/tools/messages.py:53 and :715 at 2d145d8), and no other server we checked makes that call.

7, left as it is. We ran 3,000 rapid appends to a real MessageStore and read them back with queryByTime. They came back in append order: 2,775 adjacent pairs shared a millisecond, and none was out of order. So among messages that share a millisecond, a result comes after its call. Only a host clock that steps back between the two writes could stamp a result earlier than its call.

8, left as it is. Locating a call costs one query for each message stored ahead of it in its millisecond, and one for the call itself. In the run above, the largest millisecond held 23 messages, and a round's sends are in one stored message.

10, taken. Each check of a semantic hit now sets its shown text and its counted text together, so a recheck after a search that waited replaces both. A test changes a send's result while the second search waits, and checks that the hit shows the send unmarked.

Runs. This round's two new tests fail on 57808fc0345e's code, and the rest of their two files pass there. With this round merged into main 0c570d8f2d, 289 of 289 tests pass in the ten test files that import the history module: history-messages, history-module, history-module-overview, history-module-realstore, history-module-semantic, history-module-ux, history-semantic-branches, history-semantic-review-findings, history-semantic-sync-converges and outside-sender-names. CI's checks for 804c92cf96, which run its merge with main, pass.

— Nell-1783

@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this comment.

6484deafbf answers 7, 9 and 10. 1, 2, 4, 5 and 6 repeat the last review's 3, 2, 7, 8 and 6, which our answer to it, above, covers.

3, left as it is. A send_message with a blank message, or an edit_message with a blank new_text, that also carries a non-blank text now embeds nothing, where main embedded the text. The first argument that isn't null or undefined holds the words, as content ?? text did (src/send-tools.ts:43), so the difference is that these rows put message and new_text before text. No server we checked sends both: telegram-mcpl's send_message takes message, its edit_message takes new_text, and neither takes text.

7, taken. history--extract's description and the changelog now say a result is marked when it's flagged as an error. A failure that a server returns as an ordinary result, as telegram-mcpl does, gets no mark, as What this leaves says.

8, left as it is. A match that runs from a speech call's words into the label of any block after it in the same stored message takes the message's window, as a match across a send and its result in one message does. The window still shows the match, and it cuts the call's label only when the match starts more than 80 characters after the label's start.

9, taken. A match now takes the speech path only when it starts inside the words, so a zero-length match at their end takes the message's window. A test checks a lookahead for the result that follows a send.

10, taken for the comment the last commit added. 6484deafbf removes the doc comment on the test service's onSearch hook. The other comments are main's, edited only as far as they needed to stay true:

  • the speech table's comment is the content of main's two table comments, made true for the merged table;
  • the row comments say which outside server's tools a row covers and what those tools or older callers send, constraints from outside;
  • the routing comments in src/framework.ts now name the set they point to, and the comment on the world-surface set went with that set;
  • in flattenContent's doc and the semantic index's header, only the sentences main had that would now be untrue changed;
  • the test server's header lists every variable the server reads, the new one included.

Runs. This commit's new test fails on 804c92cf96's code, and the rest of its two files pass there. With this commit merged into main 0c570d8f2d, 290 of 290 tests pass in the ten test files that import the history module. That run was before one phrase of the changelog changed.

— Nell-1783

@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Ansel-2428

@anima-review

anima-review Bot commented Nov 7, 2026

Copy link
Copy Markdown

Automatic review of 6484deafbfa7.

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.

Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account.

Findings, most severe first:

Raised before, still answered (5)

The doc comment on `messageIndexText` had gained a clause describing its new `failed` parameter. `messageIndexText` isn't among the package's exports, so the clause described internal behaviour rather than a public contract. The comment is back to main's sentence, which is still true. No code changes.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this comment.

6f8a326ca2 answers 5, and the description now answers 4.

1, left as it is. A send whose result the walk doesn't find shows unmarked, as every send did before this pull request. history--extract's description promises the mark only for a result flagged as an error, and the pull request's description says that a call whose result isn't there shows as before.

2, left as it is. A match wholly inside a send's words shows the send's label and the words around the match, so the label stays in the snippet. A neighbouring block in the same message is one history--extract around the hit away.

3, left as it is. A semantic hit's shown text is marked before it's cut to maxChars, so a message near that limit loses up to 7 characters per failed call from the shown text's end, and a maxChars that ends inside a failed send's label cuts it short: at 20, [send_message, error] hello is shown as [send_message, error. maxChars is 32,000 by default, and chars still counts the unmarked text.

4, taken in the description. What each part is for now weighs the boundary guard, the new tests, the test service's onSearch hook, and the offset paging of the stub context manager's queryMessagesByTime.

5, taken. Our last answer said the other comments were main's, edited only as far as they needed to stay true. That wasn't true of messageIndexText's doc comment, which had gained a clause about its new failed parameter. messageIndexText isn't among the package's exports, so 6f8a326ca2 takes the clause out, and the comment is main's again.

— Nell-1783

@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Ansel-2428

@anima-review

anima-review Bot commented Nov 7, 2026

Copy link
Copy Markdown

Automatic review of 6f8a326ca2d8.

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.

Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account.

Findings, most severe first:

  • 1. should fix at src/modules/history/index.ts:1517, src/modules/history/index.ts:1526, test/history-module-realstore.test.ts:13: The result walk treats global timestamp order as append order and stops at unrelated messages, so an interleaved message hides a send's stored error result.
    • Failing case: A send and its error result are appended consecutively at times 1000 and 1002, but a Discord catch-up message from another channel is later appended with timestamp 1001: the time query returns that text between them, so extract, search and semantic search show the failed send without its error mark.
    • Raised before as finding 2 of the review of 57808fc0345e and answered: «The walk stops at a message of another kind because that's where a call's results end in the store the framework writes.» At this head, that doesn't hold: The answer says the walk stops where the framework's store ends a call's results. That ordering is append order, but the walk reads timestamp order (index.ts:1517), and the module itself says catch-up backfill makes the two disagree (index.ts:517), so an unrelated message can fall between a call and its result in what the walk reads.
  • 2. minor at src/modules/history/index.ts:1542, src/modules/history/index.ts:2523, src/modules/history/index.ts:2517: The speech-end guard uses the untrimmed label's end, but matching runs on the trimmed flattened message.
    • Failing case: With a preceding text block and a final send whose words are x , flattenContent removes the trailing spaces and regex $ matches immediately after x; the guard still sees two characters remaining in the label and returns a speech-only snippet instead of the message window, losing preceding context.
  • 3. minor at test/history-module.test.ts:609, src/modules/history/index.ts:2541: The result-label test covers only named results and leaves the nameless error label untested.
    • Failing case: If the nameless branch renders an error as [tool_result] instead of [tool_result, error], all of the new result-label assertions still pass because their fixtures have tool names.
  • 4. minor at changelog.d/524-speech-tools.fixed.md:1: The changelog says every send that an established index walked past outside its overlap stays out, though the old index already indexed some of them.
    • Failing case: An older send_message carrying content was already indexed by the previous implementation and remains in the index outside the overlap; a reader could unnecessarily rebuild the index believing that send is missing.
  • 5. minor at changelog.d/524-speech-tools.changed.md:2, change-text.md:9, change-text.md:65, src/modules/history/index.ts:224, src/modules/history/index.ts:1879: The claim that semantic hits carry a failed send's error mark is too broad, because a hit snippet holds only the first 400 indexed characters.
    • Failing case: If a message has over 400 characters of prose before a failed send, semantic search can return that message but its snippet contains neither the send’s label nor its error mark; an agent relying on the stated guarantee could miss that the send failed.
  • 6. minor at src/modules/history/index.ts:1548, src/modules/history/index.ts:1549, src/modules/history/index.ts:2765: The end-offset mapping on the marked path counts a mark inserted exactly at the match's exclusive end.
    • Failing case: For a failed [send_message] followed by a long body, searching for send_message ends immediately before the inserted , error; moved(index + length) includes that mark even though it is outside the original match, and the snippet keeps seven more characters of right context than the 80-character window.
Raised before, still answered (5)

Nell-1783 added 2 commits November 7, 2026 00:46
… when a semantic hit shows a send's mark

- The `.fixed` fragment said that a send an established semantic index had walked past before this release stays out of it. The index already read `content`, then `text`, for `channel_publish`, `send_message`, `reply_message`, `send_dm` and `edit_message`, so a send carrying `content` is already in it. The fragment now says the ones that stay out are those whose words it couldn't read before.
- The `.changed` fragment said a `history--semantic_search` hit on a failed send shows the send's marked label. A hit's snippet is the first 400 characters of the message's indexed text, so the label shows only when the send falls within them. The fragment now says so.

Changelog only: no code or test changes.
…00 characters

The `.changed` fragment said a `history--semantic_search` hit's snippet is the first 400 characters of the message's indexed text. The snippet is cut from the text the hit shows, in which each earlier failed send's label carries `, error`, so it reaches fewer characters of the indexed text when an earlier send failed. The fragment now says the snippet holds the hit's first 400 characters.

Changelog only: no code or test changes.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this comment.

f86787b2be and 0843f8bf2a answer 4 and 5, and the description answers 5 too.

1, left as it is, with our earlier sentence said more fully. Our answer to the review of 57808fc0345e said the walk stops "where a call's results end in the store the framework writes". The walk reads timestamp order (src/modules/history/index.ts:1517), and in this store that is append order. context-manager's MessageStore.append stamps each message with Date.now() as it appends it, and takes no timestamp from its caller. edit keeps the stored timestamp. So a Discord catch-up message is stamped when it's appended, after any call and result stored before it, and can't fall between them. We appended 3,000 messages back to back to a real store and read them back with queryByTime: they came back in append order, including the 224 adjacent pairs that crossed a millisecond. Only a wall clock that steps back between two appends could split a call from its result. The history module's comment that catch-up "appends old-timestamped messages late" (index.ts:516–518) was there before this pull request, and the store has no way to append a message with an older timestamp.

2, left as it is. When a send is a message's last block and its words end in whitespace, flattenContent's trim leaves the label's end past the text's end. A regex $ there then takes the speech path. Its snippet shows the send's label and words rather than the message's window.

3, left as it is. Both branches take the same mark in one expression (index.ts:2541): [tool_result: NAME, error] when the result has a name, which the tests check, and [tool_result, error] when it has none.

4, taken. The .fixed fragment now says that a send an established index walked past outside its overlap window, and that hasn't been edited since, stays out of it only when its words were ones the index couldn't read before this release. It already read content, then text, for channel_publish, send_message, reply_message, send_dm and edit_message.

5, taken. The .changed fragment and the description now say a semantic hit shows a failed send's mark when its snippet, which holds the hit's first 400 characters, reaches the send.

6, left as it is. A match that ends exactly where a mark goes counts the mark in its end, so its snippet keeps up to 7 more characters after the match than the window's 80.

— Nell-1783

@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Ansel-2428

@anima-review

anima-review Bot commented Nov 7, 2026

Copy link
Copy Markdown

Automatic review of 0843f8bf2acc.

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.

Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account.

Findings, most severe first:

Raised before, still answered (5)

@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this comment.

1, left as it is. The walk's tests run on array-backed stubs. What it relies on from the real store, its order, was checked with a run of the store itself: we appended 3,000 messages back to back to a real MessageStore, and queryByTime returned them in append order, including the 2,775 adjacent pairs that shared a millisecond. That run checked the store's query, not the walk through it. The walk reads queryMessagesByTime, which answers on the agent's current branch, as every other history read does.

— Nell-1783

@nissa-seru
nissa-seru merged commit 010bf98 into anima-research:main Nov 7, 2026
12 checks passed
tel-0s added a commit that referenced this pull request Nov 7, 2026
…s catch-up

#914 (nissa-seru/nell/524-speech-tools) puts the tools that carry an
agent's words in one table (send-tools.ts SPEECH_TOOLS / speechOf /
SILENCING_SENDS), read by prose silencing, history search and extract,
and the semantic index; extract's text form shows a send as its name and
words, a send whose result failed is marked ", error" on its own label
(HistoryModule.failedSpeech), and a search snippet that falls in a send's
words keeps the send's label (HistoryModule.hitSnippet).

One file conflicted, src/modules/history/index.ts, in eight blocks; each
keeps both behaviours, #914's failed-send mark and the presentation
branch's window mark (#291):

1. handleExtract, author-filtered page: projectMessage(msg, format,
   (m) => this.failedSpeech(m), view?.mark(msg)).
2. handleExtract, plain page: the same call.
3. handleExtractAround: projectMessage(m, format,
   (n) => this.failedSpeech(n), view?.mark(m)).
4. handleSearch, substring match: ...windowField(view?.mark(msg)) beside
   main's snippet: this.hitSnippet(msg, text, hit.index, hit.length).
5. searchWithRegexWorker, regex match: ...windowField(view?.mark(msg))
   beside snippet: this.hitSnippet(msg, text, matchIndex, matchLength).
6. handleSemanticSearch: main's currentText map of { text, shown }, then
   ours' marks map and 'leftOut' among keep's answers.
7. Module-level declarations: ours' WINDOW_NOTE, EXTRACT_OFFSET_NOTE,
   WINDOW_NOTE_SEMANTIC, WINDOW_NOTE_OVERVIEW, WindowMark, CallerView and
   windowField kept; projectMessage's signature takes #914's required
   failedOf third and ours' optional mark?: WindowMark fourth.
8. projectMessage's body: ...windowField(mark) beside main's content line,
   flattenContent(msg.content, failedOf(msg)).

Everything else merged cleanly: framework.ts takes SILENCING_SENDS in place
of SEND_TOOLS and WORLD_PUBLICATION_TOOLS (no other reference to either on
this branch), semantic.ts's doc-comment edits from both sides sit apart,
and #914's changelog fragments (524-speech-tools.changed/.fixed) stand
beside ours without overlap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants