Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
16a6392
fix(surgery): return once marks are scheduled, not after Discord acce…
Oct 7, 2026
82a267a
fix(surgery): report unresolved marker bookkeeping as unresolved
Oct 7, 2026
0254342
docs(surgery): describe delivery as started without awaiting, not run…
Oct 7, 2026
287eef2
docs(surgery): say what every receipt outcome guarantees, not 'durabl…
Oct 7, 2026
80c74c8
docs(changelog): state the incident's drain duration as projected, no…
Oct 7, 2026
acd95b6
fix(awareness): new gates wait for running drains, not only pending m…
Oct 7, 2026
8b22d1f
docs(surgery): `none` also covers a disabled awareness outbox
Oct 7, 2026
8b530ee
test(surgery): require both surgeries' reactions before traffic, by e…
Oct 7, 2026
30dd6f1
fix(awareness): no operator command waits for Discord; a gate waits f…
Oct 11, 2026
0fb707e
fix(awareness): with no gate up, an inbound event is still handled in…
Oct 11, 2026
535e0ee
Merge main into fix/surgery-marks-receipt
Oct 13, 2026
b099ed1
test(awareness): pin each branch move's delivery start, the bookkeepi…
Oct 13, 2026
e1f8e61
docs(awareness): say where a mark is deferred rather than attempted; …
Oct 13, 2026
1f60e57
docs(awareness): channel traffic admitted before the gate may now be …
Oct 13, 2026
0de75ec
test(surgery): pin that a turn for channel traffic admitted before th…
Oct 13, 2026
4c65258
docs(awareness): a reconnect's traces follow its re-registration atte…
Oct 13, 2026
181fd99
docs(changelog): startDiscordAwarenessDelivery is a new public method…
Oct 13, 2026
d81c7da
docs(api): the web switch used to await the awareness sync, not every…
Oct 13, 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
49 changes: 49 additions & 0 deletions changelog.d/fix-surgery-marks-receipt.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
- Live `rollbackToMessage` and `suppressMessages` (and message-granular
`host/command` undo, which rides on rollback) return once the body change
has landed, with a marker-scheduling receipt, instead of after every
awareness reaction (💤 by default) has been delivered. Previously the store
stayed reserved, and the operator got no answer, for the whole serial
drain. At the reported pace of about one reaction per second, 918 queued
marks would take roughly 15 minutes; the household's client timed out
after 60 seconds. Delivery now continues in the background, behind the
same MCPL data-plane gate as before, which holds channel traffic that
arrives after it until the marks are attempted, or deferred when their
server isn't connected. A turn may now run during delivery when it carries
no channel traffic, such as a scheduler timer or a provider admission, and
also when its channel traffic was admitted before the gate went up (queued
when the surgery began, or arriving during its switch or fork). Such a
turn used to wait for the whole drain, since the store stayed reserved;
now it can answer before the marks show.
- The other branch moves no longer wait for Discord either: the web API's
`branch.switch`, `undo` and `redo`, and `host/command` turn undo, return
once the branch has moved, and their marks follow behind the same gate.
Each starts its delivery through
`startDiscordAwarenessDelivery(after, agentName?)`, a new public method
that a host can call after its own branch moves. A delivery that fails to
start raises a `discord-awareness-delivery` ops alert instead of failing
the command, and the next sync, reconnect or start delivers what is still
pending. (The web switch used to await the awareness sync before
answering.)
- A gate installed while a drain is still running (restoring the source
branch mid-delivery, say) waits for that drain and then drains again from
the ledger. So before traffic is released, every mark pending when the
gate was armed is attempted, or deferred until its server is connected,
as before: the drain's remaining reactions, the removals they need, and an
add re-pended during the drain (a rollback, its undo, then a redo).
Inbound MCPL handlers wait out a generation installed while they wait, not
only the one they first saw, and a reconnect's gate opens only once both
its drain and the server's re-registration attempt have settled. So a
reconnect's `mcpl:server-reconnected` and `module:added` traces now come
once that attempt has settled, granted or not, where they came at once
when no marks were pending.
- The result's new `markers` field (`none` | `queued` | `not-scheduled` |
`unresolved`, the exported `SurgeryMarkerReceipt` type) says whether marks
were scheduled, never that Discord accepted them. The `[operator]` stderr
line gains `marks=…` and the operator-actions log entry gains `markers`.
If the ledger write after the switch fails, the rollback that already
applied is no longer reported as failed: the batch is retired
(`not-scheduled`, with a `discord-awareness-not-scheduled` ops alert), or,
if even that write fails, the result says the batch may still be
delivered (`unresolved`, with a `discord-awareness-unresolved` ops alert). A
suppression whose redactions all committed is no longer undone by that
failure.
15 changes: 11 additions & 4 deletions src/api/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,8 +543,13 @@ export class ApiServer {
this.currentBranch = params.name;

// Reversible Discord awareness markers are part of the branch projection.
// Do not acknowledge the switch until add/remove operations were attempted.
await this.framework.syncDiscordAwarenessMarkers();
// The switch is acknowledged once it has landed: delivery of the marks it
// adds and removes starts behind the MCPL data-plane gate, which holds
// channel traffic that arrives after it until they are attempted, or
// deferred while their server is disconnected, and a failure is an ops
// alert rather than a failed switch. (It used to await the awareness
// sync, which after a large rollback took the drain's whole length.)
this.framework.startDiscordAwarenessDelivery('branch switch');

// Materialize config files from the new branch
const ws = this.framework.getModule('workspace');
Expand Down Expand Up @@ -598,7 +603,8 @@ export class ApiServer {

if (result.undone) {
this.currentBranch = result.toBranch!;
await this.framework.syncDiscordAwarenessMarkers();
// Marks follow behind the data-plane gate; the undo doesn't wait for Discord.
this.framework.startDiscordAwarenessDelivery('undo', params.agentName);
this.broadcast('branch:switched', {
from: result.fromBranch,
to: result.toBranch,
Expand All @@ -618,7 +624,8 @@ export class ApiServer {

if (result.redone) {
this.currentBranch = result.toBranch!;
await this.framework.syncDiscordAwarenessMarkers();
// Marks follow behind the data-plane gate; the redo doesn't wait for Discord.
this.framework.startDiscordAwarenessDelivery('redo', params.agentName);
this.broadcast('branch:switched', {
from: result.fromBranch,
to: result.toBranch,
Expand Down
Loading
Loading