Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3a423a1
fix: exempt chat:edited/deleted/reaction-remove markers from conversa…
ajaniramon Sep 21, 2026
54bea64
Merge remote-tracking branch 'origin/main' into fix/157-nonconversati…
ajaniramon Sep 25, 2026
843e4c0
Merge remote-tracking branch 'origin/main' into fix/157-nonconversati…
ajaniramon Oct 6, 2026
67586bc
Merge main (24826254) into fix/157-nonconversational-tags
Oct 17, 2026
5d58d94
fix(changelog), test: narrow the marker claim to what holds, and pin …
Oct 17, 2026
0236f40
docs, test: say what holds for markers, and pin re-pin, the open repa…
Oct 17, 2026
26afe3b
fix: count an edit the agent sees first as the message it replaced (R…
Oct 18, 2026
7dc9ce0
docs, test: re-pin wording, channel_publish for forks, injection asse…
Oct 18, 2026
044a80d
fix: count a replacement of a message stored outside the deferred que…
Oct 18, 2026
bf35192
fix: end a message's direct window when a compile assembles it
Oct 18, 2026
9cac203
test: pin which compiles end a message's direct window
Oct 18, 2026
03420e7
Merge main (07563022) into fix/157-nonconversational-tags
Oct 18, 2026
44e9ce7
fix: judge a first view by a shown watermark, not a flag on the place…
Oct 18, 2026
772f70a
fix: first views after a failed compile, in the deferred lane, and in…
Oct 18, 2026
db15e9b
test: pin the three first-view pieces no test reached
Oct 18, 2026
2ed67d2
Merge main (0d19f7c8) into fix/157-nonconversational-tags
Oct 18, 2026
1789e8c
fix(mcpl): a notice that may take the agent's first view clears it on…
Oct 18, 2026
cb487a2
Merge main (c0155d39) into fix/157-nonconversational-tags
Oct 18, 2026
f40c54a
fix(mcpl): a channel retry answered from its receipt keeps its first …
Oct 18, 2026
7e13dd9
fix(mcpl): end a subject's first view where an occupant that may have…
Oct 18, 2026
28c2561
Merge main (b82ba630) into fix/157-nonconversational-tags
Oct 19, 2026
b8dc07a
refactor(mcpl): name the coalescer's first-view mark `firstView` (R4-3)
Oct 19, 2026
fb3389d
docs(mcpl): say what the default publish channel, a failed compile's …
Oct 19, 2026
dcd0478
fix(mcpl): keep a first view's end through a crash; say where channel…
Oct 19, 2026
e257911
Merge main (9c6964c9) into fix/157-nonconversational-tags
Oct 19, 2026
a10eb51
fix(coalescing): a crash-window record ends its subject's first view …
Oct 19, 2026
bf29f26
Merge main (4fd2bfb9) into fix/157-nonconversational-tags
Oct 19, 2026
13d2bba
fix(mcpl): a run's first view needs nothing of the subject read, and …
Oct 19, 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
1 change: 1 addition & 0 deletions changelog.d/nonconversational-markers.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **Edit/delete/reaction-remove markers now preserve explicit-send suppression, like reaction markers** (#157). `isConversationalInjection` exempted only `chat:reaction`, so the RFC-001 marker tags `chat:edited`, `chat:deleted` and `chat:reaction-remove` (now delivered as synthetic incoming messages by e.g. zulip_mcp) cleared explicit-send suppression — an edit notice arriving right after a `send_message` re-opened auto-routing for the same round's prose. The four tags live in a shared `NON_CONVERSATIONAL_TAGS` set (`src/mcpl/tags.ts`). All four marker tags also leave ChannelRegistry's default publish channel and `context.incoming`'s message/thread anchor unchanged. An edit the agent sees first still counts as the message it replaced: it clears explicit-send suppression, can move the turn's speech channel at a tool boundary, and, for a channel message the coalescer delivers when it accepts it, moves the default publish channel and `context.incoming` as that message would. The host knows that case when RFC-006 coalescing delivers a version of a subject whose producer marked its first occurrence `initial` and of which no version may have been shown to the agent yet, whether the version is a plain occurrence that replaced an unread one, a deferred notice's render, or a plain occurrence that replaced a pending notice; an edit of such an edit is the agent's first view too. A doubt counts as shown, so the host errs toward a marker: a message counts as shown once a compile of any agent that can read it has begun to assemble it (a compile that throws gives that back, unless a tool boundary or another compile has moved the agent's position since) or a tool boundary of one may have injected it (residents sharing a message slot can all read it), and so does any message stored before one that was, since the host keeps one position per agent. `channel_publish`'s description now says that an omitted `channelId` goes to a conversation agent's own channel when it is bound to one, and otherwise to the channel of the most recent incoming message the host received on any channel, whether or not the agent has been shown it, apart from markers, and that a message pushed to the agent never sets it.
64 changes: 62 additions & 2 deletions src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -932,11 +932,27 @@ export class Agent {
/** Counts marks: a compile writes its watermark back only if no mark was
* made while it ran (a tool boundary's, or another compile's). */
private watermarkMarks = 0;
/**
* The newest stored message a compile or a tool boundary may have put in
* front of the agent, per branch. It moves where the consumed watermark
* does, and sooner: when a committing compile takes its value, before it
* awaits, and at every tool boundary that injects, whether or not the
* stream says it carries injected messages. So nothing above it can have
* been shown. A compile that throws assembled nothing, and gives its move
* back where it can (unshowFailedCompile). A scalar counts a prefix, so a
* message stored below it without having been shown counts as shown, which
* errs toward a marker.
*/
private shownWatermark: { branch: string; sequence: number } | null = null;

getConsumedWatermark(): { branch: string; sequence: number } | null {
return this.consumedWatermark;
}

getShownWatermark(): { branch: string; sequence: number } | null {
return this.shownWatermark;
}

/** Mark everything currently stored as consumed (the agent's creation, at
* boot or later, and a tool boundary that injected messages into the live
* stream). */
Expand All @@ -945,6 +961,32 @@ export class Agent {
// stubs): without a watermark nothing is ever treated as unread.
this.consumedWatermark = this.pendingWatermark();
this.watermarkMarks++;
this.shownWatermark = this.consumedWatermark;
}

/** Mark everything currently stored as possibly shown (a tool boundary
* that injected messages, whatever its stream says it carries). Without
* a watermark everything counts as shown, as markContextConsumed counts
* it read. */
markContextShown(): void {
this.shownWatermark = this.pendingWatermark();
}

/** A committing compile that threw assembled nothing, so its own move of
* the shown watermark goes back, unless a boundary or another compile has
* moved the watermark since. It goes back to the watermark as the compile
* found it, which can be another compile's claim that has failed as well.
* Of two overlapping committing compiles, if the first to start throws
* first, it gives nothing back (the second's claim is current), and if
* the second then throws, it restores the first's failed claim. Messages
* up to that claim then count as shown, although no request held those
* above the watermark the first compile found, which errs toward a marker
* until the next compile or boundary moves the watermark. */
private unshowFailedCompile(
watermark: { branch: string; sequence: number } | null,
before: { branch: string; sequence: number } | null,
): void {
if (watermark && this.shownWatermark === watermark) this.shownWatermark = before;
}

/**
Expand Down Expand Up @@ -992,8 +1034,16 @@ export class Agent {
// fails assembled nothing, so the watermark moves only on success.
const watermark = this.pendingWatermark();
const marksBefore = this.watermarkMarks;
const shownBefore = this.shownWatermark;
if (watermark) this.shownWatermark = watermark;
const compile = () => this.contextManager.compile(this.compileBudget(budget));
const result = budget ? await compile() : await this.compileAtLiveBudget(compile);
let result: CompileResult;
try {
result = budget ? await compile() : await this.compileAtLiveBudget(compile);
} catch (error) {
this.unshowFailedCompile(watermark, shownBefore);
throw error;
}
if (watermark) this.applyCompileWatermark(watermark, marksBefore);
return result;
}
Expand Down Expand Up @@ -1034,8 +1084,18 @@ export class Agent {
// any compile before it selects.
const watermark = opts?.dryRun ? null : this.pendingWatermark();
const marksBefore = this.watermarkMarks;
// Shown from the compile's start: what it assembles is in front of the
// agent before the consumed watermark moves, once it returns.
const shownBefore = this.shownWatermark;
if (watermark) this.shownWatermark = watermark;
const compile = () => this.contextManager.compile(this.compileBudget(budget), injections, opts as never);
const result = budget || opts?.dryRun ? await compile() : await this.compileAtLiveBudget(compile);
let result: CompileResult;
try {
result = budget || opts?.dryRun ? await compile() : await this.compileAtLiveBudget(compile);
} catch (error) {
this.unshowFailedCompile(watermark, shownBefore);
throw error;
}
if (watermark) this.applyCompileWatermark(watermark, marksBefore);
return result;
}
Expand Down
Loading
Loading