Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
3f7aafe
feat(mcpl): stamp each accepted inbound item with its source envelope
Oct 7, 2026
f7e14f6
feat(chronicle): RecordJournal — typed records that don't follow bran…
Oct 7, 2026
2153e3d
fix(chronicle): a RecordJournal write that fails after reaching the s…
Oct 7, 2026
eb2a588
fix(mcpl): freeze ordinary inbound envelopes at admission; add lane f…
Oct 7, 2026
7c3d230
Merge Basil-1007's RecordJournal (2153e3d) as the shared base for the…
Oct 7, 2026
7144970
feat(mcpl): type MCPL request failures by what reached the server
Oct 7, 2026
0a8e9c7
Merge typed MCPL request failures (7144970)
Oct 7, 2026
2c591a7
feat(mcpl): one publish executor with structured delivery outcomes
Oct 7, 2026
7a91066
feat(drafts): hold suppressed plain speech as resendable drafts
Oct 7, 2026
15ec398
fix(mcpl): refuse invalid publish selectors; a contradictory receipt …
Oct 7, 2026
8e4146d
fix(drafts): keep uncertainty across attempts; resends own and re-che…
Oct 7, 2026
191ab09
fix(drafts): a re-bounce keeps the whole text and its risk; notices d…
Oct 7, 2026
6c5f054
fix(drafts): owner-scoped notice identity; true risk evidence; idempo…
Oct 7, 2026
e9099e5
fix(drafts): held envelopes keep hybrid state, receipts read draft st…
Oct 7, 2026
467a48f
fix(drafts): a held-draft notice gives a copied draft its risk; "noth…
Oct 7, 2026
a14e25a
fix(drafts): "not sent" only over words not at risk; exact-text claim…
Oct 7, 2026
6c927de
feat(mcpl): stamp each inbound item's delivered-body and stored-copy …
Oct 7, 2026
711b400
fix(mcpl): take the stored-copy digest at each storage site, over the…
Oct 7, 2026
d36a5ad
fix(mcpl): every stored push gets the host's delivered-body digest; e…
Oct 7, 2026
abc8b4d
feat(routing): speech goes to a deliberate or triggering route, never…
Oct 7, 2026
e354622
fix(routing): threads are never routes; unknown servers stay absent; …
Oct 7, 2026
6756af2
feat(routing): publish only where MCPL RFC-011 declares the place; th…
Oct 7, 2026
05bff07
fix(routing): receipts keep the destination as delivered; channel_pub…
Oct 7, 2026
cb93817
fix(routing): every receipt names its destination's server; gate thre…
Oct 7, 2026
42c3f5a
fix(gate): the frozen envelope names a batched candidate's conversati…
Oct 7, 2026
1d6d2bb
Merge main (02adf27) into feat/prose-drafts: bring #247 onto current …
Oct 8, 2026
88a24e6
Merge the integrated #247 (1d6d2bb) into feat/ingestion-stamps: bring…
Oct 8, 2026
56827f5
fix(routing): a fork's route answers the message that woke it in its …
Oct 8, 2026
ff899f7
fix(mcpl): publish rechecks its destination after opening a closed ch…
Oct 8, 2026
ff29945
fix(gate): coalesced items are gated with the host envelope their coa…
Oct 8, 2026
eaba9a8
fix(routing): speech goes where the route stood when its words were w…
Oct 8, 2026
ae42e95
fix(streams): outgoing streams carry only published speech
Oct 8, 2026
c899120
Merge #252's integrated head (88a24e6) into feat/speech-routes: bring…
Oct 8, 2026
2287051
docs(push-handler): handlePushEvent's step list matches the handler
Oct 8, 2026
4180ccd
fix(lifecycle): late tool results and MCPL arrivals at stop() settle …
Oct 8, 2026
315d12c
fix(drafts): refuse a dismissal while its draft's attempt is out; the…
Oct 8, 2026
4c73d3f
Merge #247's 315d12c into feat/ingestion-stamps
Oct 8, 2026
a2accc1
fix(mcpl): digest a body as the store keeps it, so an untouched store…
Oct 8, 2026
1bbf3f7
docs(routing): Breaking names scheduled wakes' lost fallback; comment…
Oct 8, 2026
2c83659
fix(streams): a later message on a channel opens a paragraph in its s…
Oct 8, 2026
8f29e62
Merge #252's a2accc1 into feat/speech-routes: #247's 315d12c and the …
Oct 8, 2026
d981e49
test(mcpl): pin every branch of the copied raster sniffer with a misl…
Oct 9, 2026
378c2b3
test(drafts): pin that a draft's sending mark ends with its own attem…
Oct 10, 2026
e041853
fix(routing): no guessed server for a shared channel id; the fork rul…
Oct 10, 2026
4e19f02
fix(routing): routing names each conversation with its server; a shar…
Oct 10, 2026
2c198b5
fix(drafts): notices count on the branch they're on, the catch-up nam…
Oct 11, 2026
548071d
fix(publish): a publish always has a deadline, a refused send opens n…
Oct 11, 2026
d80210e
fix(drafts): a refused {{unsent}} holds its envelope, dismissals name…
Oct 11, 2026
9feee54
fix(envelope): only the framework writes the envelope, conversation k…
Oct 11, 2026
7cc9c21
test(publish): the refused-before-open case uses a grant that could o…
Oct 11, 2026
97d860d
test(digests): every storage site and store normalization is pinned, …
Oct 11, 2026
e0e11fd
Merge #247's sweep round (7cc9c21) into feat/ingestion-stamps
Oct 11, 2026
f288525
Merge #252's review round (e0e11fd, carrying #247's 7cc9c21) into fea…
Oct 11, 2026
7ce228e
test(digests): the store refuses an undefined array element, and cano…
Oct 11, 2026
f9f54bf
fix(routing): conversation keys can't collide, labels show as given, …
Oct 11, 2026
a8e3470
Merge #252's 7ce228e into feat/speech-routes
Oct 11, 2026
fdca2b9
fix(routing): forks own their channels' unroutable candidates, pins k…
Oct 11, 2026
535d44f
fix(routing): an evicted event keeps its route candidate, and a mid-t…
Oct 11, 2026
5065142
fix(routing): a silent wake's channel_open sets no route, typing show…
Oct 11, 2026
5036af4
fix(publish): one reading of a thread id, acceptance after the trigge…
Oct 11, 2026
a84c6e5
fix(stop): stop() settles the speech chains, and the tests wait for w…
Oct 11, 2026
11236a5
docs(changelog): #256's fragments say what the review round changed
Oct 11, 2026
fd8cfa4
fix(routing): a coalesced item carries its trigger inputs, and the re…
Oct 11, 2026
6aaeb85
test(routing): explicit-prose-routing and channel-label-history expec…
Oct 11, 2026
9dc8239
fix(routing): defaultOutgoing names its place as its publish does: nu…
Oct 11, 2026
2690edb
feat(routing): a wake that names no conversation keeps the fallback r…
Oct 12, 2026
f1dde7f
fix(routing): only a wake that names no conversation takes the fallback
Oct 12, 2026
3f09c91
fix(routing): every lane says whether a wake names a conversation; re…
Oct 12, 2026
6114926
fix(routing): a wake that continues a turn names that turn's conversa…
Oct 12, 2026
698c31a
fix(routing): the fork rule judges a route at turn start; a bounce wa…
Oct 12, 2026
5ba5d8c
fix(routing): a fallback turn's continuation takes the fallback afres…
Oct 12, 2026
45cfe46
fix(routing): a continuation reads its turn's record when its own tur…
Oct 12, 2026
501fa63
fix(routing): a silent tick's record holds through its restarts, an u…
Oct 12, 2026
fabdc81
fix(routing): a conversation from a candidate marked unroutable stays…
Oct 12, 2026
0bae78b
fix(routing): a silent tick's restart that a batched wake makes ordin…
Oct 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions changelog.d/inbound-source-envelope.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
- **Inbound source envelope.** Every inbound item the host accepts now carries
`metadata.inboundSource`, stamped once where it was accepted: the
conversation it belongs to (`channel`: MCPL server, endpoint binding,
canonical channel id, thread, message, reply edge and the channel's label at
receipt; `unscoped` for a push that names no channel; `surface` for console
or API input from a module), the host acceptance time and the adapter's own
timestamp. It covers `channels/incoming` and `push/event` on both the
ordinary and the RFC-006 coalesced paths; a coalesced occurrence freezes its
envelope at acceptance, so deferral, fan-out and replay deliver it unchanged
and a later rename never rewrites it. Adapter-supplied metadata cannot set
the field, and one a module's messages carry is dropped (an adapter's own
`origin.source` string is untouched). `readInboundSource`,
`conversationKey` (a JSON-encoded key, so no server-chosen part can run
into another) and `INBOUND_SOURCE_KEY` are exported.
- **Inbound source envelope: admission facts.** An ordinary `channels/incoming`
message or `push/event` now has its envelope frozen at admission, before it
is queued or acknowledged, so a rename or rebind while it waits cannot
change it. Every channel and unscoped envelope names the admission `lane`
(`channels/incoming` or `push/event`), and one that came through RFC-006
coalesced admission has `coalesced: true` (others have no `coalesced`
field): facts a consumer needs to know what an `eventId` is worth.
`readInboundSource` validates every field it returns.
32 changes: 32 additions & 0 deletions changelog.d/ingestion-body-digests.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
- **Delivered-body digests on every stored MCPL inbound item.** Both MCPL
lanes stamp two fields, outside the frozen source envelope (console and API
input, and the host's own notices, carry neither):
- `metadata.sourceBodyDigest` is the version identity of the body as
delivered, taken at ingestion, after MCPL conversion and before any host
decoration or storage sharding. It is the SHA-256 (hex) of `[blocks]`
as canonical JSON (keys sorted at every level, `undefined` object entries
dropped, and an `undefined` array element written as `null` as JSON
writes it, though the store refuses a body holding one), with the blocks
as the store keeps them: inline media as
the store hands it back (base64 re-encoded from its bytes, an image's
media type taken from its bytes' signature, only `type` and `source`
kept), strings well-formed (a lone surrogate as U+FFFD) and an entry
whose key isn't well-formed dropped, as the store's append drops it. So
the body hashes alike as delivered, as handed to storage and as read
back, and an undecorated, unsharded stored copy hashes to it.
- `metadata.storedBodyDigest` uses the same function over exactly the
blocks handed to storage, decorations included, taken where each path
stores.

Both are always the host's own: values an adapter supplies are replaced.
A stored copy, read back with its blobs resolved, that no longer hashes to
its `storedBodyDigest` was changed after it arrived (`editMessage` keeps
metadata). That check is of an unsharded copy: a body the store shards
(context-manager's adaptive ingress chunking) carries the whole body's two
digests on every shard (`bodyGroupId` and `shardIndex` mark them), and no
shard hashes to them. A `blob_ref` block is digested as given, while the
store resolves it on read, so a copy holding one doesn't match; the MCPL
lanes never store one. `sourceBodyDigest(blocks)` is exported from the
package root as the one implementation consumers share; it carries the
framing and the store's normalization, so check a copy with it rather than
hashing your own serialization.
10 changes: 10 additions & 0 deletions changelog.d/mcpl-request-outcome.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
- **`McplRequestError`: why an MCPL request produced no result.** A request
to an MCPL server that fails now rejects with `McplRequestError` (exported),
whose `outcome` says which of three things happened: `not-sent` (refused
before anything was written: the connection was already closed),
`error-response` (the server answered with a JSON-RPC error; `code` and the
error's `data` are kept), or `no-response` (the request was handed to the
transport and no answer came back: a timeout, or the connection closed
while awaiting; it may or may not have reached the server). Only
`not-sent` proves the server never saw the request. Messages are unchanged,
and it is still an `Error`, so existing handling keeps working.
78 changes: 78 additions & 0 deletions changelog.d/prose-drafts.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
- **Held prose drafts: suppressed speech is kept, not destroyed.** Plain
speech that is not sent is now held as a draft with its exact words,
instead of surviving only as a count: speech suppressed by an
explicit send in the same round (locus and hybrid modes, mid-turn, trailing
and text-only, native and XML tool modes), hybrid prose left without a
destination after a failed envelope, and explicit/hybrid bounces (which
replace the in-memory latest-wins clipboard; `{{unsent}}` keeps meaning the
latest bounce). A draft held beside a send keeps its run exactly as
written, indentation and surrounding newlines included; routing still
publishes trimmed segments. Deliberate privacy is never drafted:
`skip_reply` (the tool, or a hybrid `>>>skip_reply` envelope, which keeps
the rest private until a destination is named), a same-round private
`think` in a native round (where that policy applies: XML tool mode routes
such prose as ordinary speech, so a send holds it as a draft), silent turns
and `proseRouting: disabled` narration stay private and are counted in the
receipt.
- In hybrid mode a send withholds publication, not what an envelope does: a
held `>>>` envelope moves the routing state as live routing would (a
destination it names becomes the sticky target), `>>>skip_reply {{unsent}}`
still sets the latest bounce aside, and the draft holds the message the
envelope would have published, with `{{unsent}}` expanded under the
re-bounce rule (a bare `{{unsent}}` leaves the bounce as the one draft).
- Drafts live in typed Chronicle records (`RecordJournal`) that do not follow
branch switches: undo, rollback and checkout never change them, and they
survive restarts. Nothing is ever sent from them on its own. Each
resident's 100 newest drafts are kept, and an older one for 7 days after
anything last happened to it (held, named, attempted, answered, dismissed);
the journal checkpoints what it keeps every 500 entries, so opening it
replays only what follows. A request naming a draft that was let go is
refused, and nothing is sent.
- A private notice names held drafts at the next tool boundary when the live
stream presents mid-turn messages (a draft holding words copied from one
that may already have been posted is named with that risk and the
`confirmDuplicate` its resend needs), and the turn-end `[delivered]` receipt
names every draft held in the turn by its state at turn end: still held
("held as drafts d-… (not sent …)"), delivered by the resident's resend
(listed where it landed), unconfirmed, or dismissed. A dismissed draft
whose own attempt may already have been posted is named with that risk,
and counts like an unconfirmed one: a receipt that names one, with nothing
confirmed delivered, reads "nothing confirmed", not "nothing". A draft
counts as named while a message naming it is on the current branch, so one
no notice reached (a crash, an aborted turn), or whose notices a rollback
or branch move took away, is named at the next turn's start: every such
draft by id, the newest eight with a preview.
- A draft is never published except by its resident's explicit resend (or
`{{unsent}}`), and only that resident can list or act on it. Its notices go
into the resident's own history, which residents sharing one message slot
already share, as they share the assistant turn the words came from.
- New `drafts` tool (whenever MCPL channels are configured): `list`, `read`,
`resend` and `dismiss`, scoped to the calling resident. Every such
resident's tool list gains it, so each one's first request after upgrading
misses the prompt cache once. `resend` publishes
drafts verbatim, in order, to one destination through the normal publish
path, journaling each attempt durably before the request leaves; it counts
as an explicit send. A delivered draft returns its historical receipt
instead of sending again; a draft with any attempt whose outcome is
unknown (and none confirmed) needs `confirmDuplicate: true`, and a resend
owns its drafts until it finishes, re-checking each before it is sent, and
a draft it doesn't attempt is told why the batch stopped. Dismissing a
draft (with the tool or `>>skip_reply {{unsent}}`) is refused while that
draft's attempt is out, since a dismissal can't call back words already on
their way; the resident is told to wait for that result. A draft only
queued in a resend can still be dismissed, and is then not sent. A
dismissal of a draft that may already have been posted names that risk.
Unused fields may be passed as null.
- `{{unsent}}` whose draft is being sent, queued in a resend, or unconfirmed
sends nothing, and the envelope's own words are held as a bounce draft
with `{{unsent}}` written out and the risk carried, so nothing the
resident wrote has to be typed again.
- Prose segments also break at XML tool mode's refused attempts and their
notices (`tool_attempt`, `tool_notice`): the words before an all-refused
call and after its notice are two messages, routed or held separately.
- `ChannelRegistry.deliverSpeech` (routeSpeech with its `PublishOutcome`) and
`ChannelRegistry.resolveDestination` are new. `routeSpeech` now delegates
to `deliverSpeech` and returns null unless delivery was confirmed, so a
missing receipt and a channel id two servers share now return null (see
the publish-outcomes entry).
- The bounce notice now goes to the bouncing resident rather than the primary.
23 changes: 23 additions & 0 deletions changelog.d/publish-outcomes.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
- **Speech delivery reports what a publish actually established.** Plain
speech now goes through one publish executor (`ChannelRegistry.publish`,
returning a `PublishOutcome`: `delivered`, `failed` or `unknown`, with the
destination the registry resolved). Only a connector's `delivered: true`
confirms a post; a missing or malformed receipt is no longer counted as
delivered (#163's rule). `failed` means nothing was posted: refused before
dispatch, or `delivered: false` naming no message. An error response, a
timeout or a lost connection after dispatch is `unknown`, keeping the
connector's own words (a multi-part send may already be partly posted), and
so is a contradictory receipt (`delivered: false` beside a message id,
valid or not).
The resident's `[discord-send-failed]` marker now says which: "could not be
delivered … Nothing was posted" for a failure, and "was not confirmed … may
or may not have been posted; check the channel before sending it again"
for an uncertain outcome. The marker's metadata and the
`mcpl:speech-route-failed` trace carry `outcome`. A channel id registered
by more than one MCPL server is refused rather than routed through
whichever server registered it first, and a supplied but empty or
non-string server selector is refused rather than read as omitted. A send
refused for its server or grant opens no closed channel. A
`channels/publish` request always has a deadline: with `requestTimeoutMs`
0 it keeps the 60 s default, since its outcome is told to the resident and
recorded for its draft; its expiry reads as unknown.
30 changes: 30 additions & 0 deletions changelog.d/speech-routes.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
- **Hosts and module authors using `ChannelRegistry` directly:** the registry
no longer decides where speech goes. It still remembers where the last
message a channel delivered came in, which the framework uses only for a
turn whose wake names no conversation. `getDefaultPublishChannel()`
returns its channel id, and `getLastInbound()` its server, channel and
thread. A reaction or a withdrawal doesn't count as a message.
- **Removed:** the `activeChannelResolver` option. The new
`speechRouteResolver(agentName)` option replaces it, and the framework
supplies it.
- **Changed:** `resolveLocus(conversationId)`, which the framework calls
with an agent's name, returns only a conversation fork's home channel,
or null. `getChannelServerId(id)` names a server only when
exactly one registered the id, and returns null for an id several
servers share, where it used to return the first registrant. `buildChannelContext(agent)` advertises the
agent's own route and reply edge, and nothing for a held, surface-only or
absent route.
- **Migration:** read a route from the framework's turn rather than the
registry, and pass `channelId` explicitly when publishing outside a turn.
`routeSpeech`/`deliverSpeech` also accept `{ serverId, channelId }` to
publish to an exact server.
- **Also changed:** `resolveProseTarget` returns the `serverId` its
target resolved to beside the channel id. `startTyping` and
`stopTyping` take `{ serverId, channelId }`. A bare id still works:
`startTyping` resolves it only when one server registers it, and
`stopTyping` stops that id's typing on every server that has it. The registry no longer
decides a coalesced message's trigger before `handleCoalescedIncoming`:
the event carries the filter's inputs as `triggerInputs`, and a host's
own handler decides from them once it admits the occurrence.
- **Unchanged:** fork homes, explicit sends naming a channel, and
`resolveDestination`. No sibling package version changes.
Loading
Loading