From 39857b0a829e91a58b8a8caa65957d54b763f190 Mon Sep 17 00:00:00 2001 From: qa-engineer Date: Wed, 14 Oct 2026 19:52:17 +0000 Subject: [PATCH 1/2] fix(routing): clarify disabled console delivery receipts --- changelog.d/disabled-console-receipt.fixed.md | 1 + src/framework.ts | 32 ++++++++++++++- src/types/agent.ts | 2 + test/explicit-prose-routing.test.ts | 40 ++++++++++++++++++- test/turn-trigger-provenance.test.ts | 38 ++++++++++++++++++ 5 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 changelog.d/disabled-console-receipt.fixed.md diff --git a/changelog.d/disabled-console-receipt.fixed.md b/changelog.d/disabled-console-receipt.fixed.md new file mode 100644 index 000000000..2f761f820 --- /dev/null +++ b/changelog.d/disabled-console-receipt.fixed.md @@ -0,0 +1 @@ +- Disabled prose routing now reports that suppressed prose was shown on an attending non-channel surface and that no channel publish was attempted, instead of claiming nothing was delivered. diff --git a/src/framework.ts b/src/framework.ts index 013277a59..6b5adb0d2 100644 --- a/src/framework.ts +++ b/src/framework.ts @@ -593,6 +593,8 @@ interface TurnRouting { deliveries: string[]; /** Plain-prose segments the turn suppressed, reported by its receipt. */ suppressed: number; + /** An operator-facing non-channel surface receives this logical turn's stream. */ + consoleAttended: boolean; /** * The messages that would re-pin the locus (repinChannel) that no tool * boundary of the turn weighed, in the order they were stored @@ -625,7 +627,7 @@ interface Arrival { function freshTurnRouting(): TurnRouting { return { engaged: new Set(), destination: null, hybridHold: false, - continuation: false, deliveries: [], suppressed: 0, arrivals: [], + continuation: false, deliveries: [], suppressed: 0, consoleAttended: false, arrivals: [], }; } @@ -640,6 +642,18 @@ function isTurnContinuation(reason: string): boolean { return reason === 'context_budget_restart' || reason === 'tool_results_ready' || reason === 'tool_result_guard_retry'; } + +function isConsoleAttendedEvent(event: ProcessEvent): boolean { + const channelId = (event as unknown as { channelId?: unknown }).channelId; + return !event.type.startsWith('mcpl:') + && (event.type === 'external-message' || event.type === 'api:message' + || event.type === 'api:inference-request') + && channelId === undefined; +} + +function isConsoleAttendedTurn(trigger: InferenceRequest | undefined): boolean { + return trigger?.consoleAttendedOrigin === true; +} const CONVERSATION_ROUTER_STATE_ID = 'framework/conversation-router'; const INFERENCE_LOG_ID = 'framework/inference-log'; const PROCESS_LOG_ID = 'framework/process-log'; @@ -8800,6 +8814,7 @@ export class AgentFramework { suppressProse: this.activeTurnTriggers.get(agent.name)?.suppressProse, ephemeralSystemPrompt: this.activeTurnTriggers.get(agent.name)?.ephemeralSystemPrompt, silentHeartbeat: this.activeTurnTriggers.get(agent.name)?.silentHeartbeat, + consoleAttendedOrigin: this.activeTurnTriggers.get(agent.name)?.consoleAttendedOrigin, }); } else if (currentState.stream) { // What this round appends is in the next call's prompt. If that @@ -8963,6 +8978,7 @@ export class AgentFramework { reason: event.type, source, timestamp: Date.now(), + consoleAttendedOrigin: isConsoleAttendedEvent(event), }); } } @@ -10482,6 +10498,12 @@ export class AgentFramework { suppressProse: silentOnly ? trigger?.suppressProse : undefined, ephemeralSystemPrompt: silentOnly ? trigger?.ephemeralSystemPrompt : undefined, silentHeartbeat: silentOnly ? trigger?.silentHeartbeat : undefined, + // Fresh batches are console-attended if ANY cause is shown on an + // operator surface. Continuations keep the original logical turn's + // provenance rather than adopting a newly-batched request. + consoleAttendedOrigin: budgetRestart + ? trigger?.consoleAttendedOrigin + : requests.some((request) => request.consoleAttendedOrigin === true) || undefined, channelId: channelReq?.channelId, addressed: addressedReq !== undefined, // A context-budget restart continues the same logical turn: it keeps @@ -10567,7 +10589,9 @@ export class AgentFramework { const text = shown.length > 0 ? `[delivered] plain speech → ${shown.join(' · ')}${suppressedNote ? ` · ${suppressedNote}` : ''}` - : `[delivered] nothing — ${suppressedNote}`; + : routing.consoleAttended && agent.proseRouting === 'disabled' + ? `[delivered] shown on the attending surface; no automatic prose publish attempted — ${suppressedNote}` + : `[delivered] nothing — ${suppressedNote}`; try { const mid = agent.getContextManager().addMessage( 'user', @@ -11491,6 +11515,7 @@ export class AgentFramework { } } this.turnRouting.set(agent.name, routing); + if (trigger?.consoleAttendedOrigin === true) routing.consoleAttended = true; if (resumesTurn) { const previousLogicalToolState = this.logicalTurnToolCalls.get(agent); @@ -11509,6 +11534,7 @@ export class AgentFramework { } if (opensTurn) { + routing.consoleAttended = trigger?.consoleAttendedOrigin === true; // The turn's trigger channel (item-3 redux): the channel of a // channel/DM-triggered turn, and none for a heartbeat / no-trigger // turn, which falls back to the global default rather than inheriting @@ -12897,6 +12923,7 @@ export class AgentFramework { await turnSpeechChain; if (!trigger?.suppressProse && turnProseRouting !== 'explicit') { this.appendProseDeliveryReceipt(agent, streamRouting); + } // A `!` continuation this turn asked for. This gives prose the @@ -13078,6 +13105,7 @@ export class AgentFramework { } else { this.requestProseContinuation(agent, streamRouting); } + } return; } diff --git a/src/types/agent.ts b/src/types/agent.ts index 708c7002f..d7185cf90 100644 --- a/src/types/agent.ts +++ b/src/types/agent.ts @@ -355,6 +355,8 @@ export interface InferenceRequest { reason: string; source: string; timestamp: number; + /** True when an operator-facing non-channel surface receives this turn's stream. */ + consoleAttendedOrigin?: boolean; /** * The MCPL channel whose message triggered this inference, if any (composite * id, e.g. `discord:guild:channel` / `discord:dm:id`). The framework routes diff --git a/test/explicit-prose-routing.test.ts b/test/explicit-prose-routing.test.ts index 5159458e4..5610ef1ab 100644 --- a/test/explicit-prose-routing.test.ts +++ b/test/explicit-prose-routing.test.ts @@ -25,6 +25,7 @@ import type { InferenceRequest, } from '../src/index.js'; import { AgentFramework } from '../src/index.js'; +import { ApiModule } from '../src/modules/api/index.js'; import { MockMembrane, createMockResponse } from './helpers/mock-membrane.js'; import { ProseStreamRouter } from '../src/mcpl/prose-stream-router.js'; import { MembraneError } from '@animalabs/membrane'; @@ -1590,7 +1591,7 @@ describe('explicit prose routing', () => { const texts = framework.getAgent('assistant')!.getContextManager().getAllMessages() .flatMap(m => m.content).filter(b => b.type === 'text').map(b => (b as { text: string }).text); assert.ok(texts.includes(authored), 'authored prose remains in Chronicle'); - assert.ok(texts.some(t => t.includes('[delivered] nothing') && t.includes('proseRouting=disabled')), + assert.ok(texts.some(t => t.includes('[delivered] shown on the attending surface; no automatic prose publish attempted') && t.includes('proseRouting=disabled')), 'private suppression receipt explains the unsent prose'); await framework.stop(); @@ -1615,7 +1616,7 @@ describe('explicit prose routing', () => { assert.equal(module.calls[0]!.name, 'say'); const texts = framework.getAgent('assistant')!.getContextManager().getAllMessages() .flatMap(m => m.content).filter(b => b.type === 'text').map(b => (b as { text: string }).text); - assert.ok(texts.some(t => t.includes('[delivered] nothing') && t.includes('2 plain-speech segment(s) suppressed')), + assert.ok(texts.some(t => t.includes('[delivered] shown on the attending surface; no automatic prose publish attempted') && t.includes('2 plain-speech segment(s) suppressed')), 'one receipt accounts for mid-turn and trailing prose'); await framework.stop(); @@ -1913,4 +1914,39 @@ describe('explicit prose routing', () => { }); } + it('disabled context-budget continuation preserves its attending-surface receipt', async () => { + membrane.pushResponse(createMockResponse([{ type: 'text', text: 'private continuation' }] as ContentBlock[])); + const framework = await createFramework('disabled'); + stubRegistry(framework); + const agent = framework.getAgent('assistant')!; + await (framework as unknown as { startAgentStream(agent: unknown, trigger: unknown): Promise }) + .startAgentStream(agent, { + agentName: 'assistant', reason: 'context_budget_restart', source: 'framework', timestamp: Date.now(), + consoleAttendedOrigin: true, + }); + await framework.runUntilIdle(); + const texts = agent.getContextManager().getAllMessages().flatMap(m => m.content) + .filter(b => b.type === 'text').map(b => (b as { text: string }).text); + assert.ok(texts.some(t => t.includes('shown on the attending surface') + && t.includes('no automatic prose publish attempted'))); + await framework.stop(); + }); + + it('disabled api:inference-request is console-attended', async () => { + membrane.pushResponse(createMockResponse([{ type: 'text', text: 'api reply' }] as ContentBlock[])); + const framework = await AgentFramework.create({ + storePath: join(tempDir, 'api-test.chronicle'), membrane: membrane.asMembrane(), + agents: [{ name: 'assistant', model: 'test-model', systemPrompt: 'test', proseRouting: 'disabled' }], + modules: [new ApiModule()], + }); + stubRegistry(framework); + framework.pushEvent({ type: 'api:inference-request', agentName: 'assistant' } as never); + await framework.runUntilIdle(); + const texts = framework.getAgent('assistant')!.getContextManager().getAllMessages().flatMap(m => m.content) + .filter(b => b.type === 'text').map(b => (b as { text: string }).text); + assert.ok(texts.some(t => t.includes('shown on the attending surface') + && t.includes('no automatic prose publish attempted'))); + await framework.stop(); + }); + }); diff --git a/test/turn-trigger-provenance.test.ts b/test/turn-trigger-provenance.test.ts index df75affc1..ce84c0001 100644 --- a/test/turn-trigger-provenance.test.ts +++ b/test/turn-trigger-provenance.test.ts @@ -144,6 +144,44 @@ describe('Turn trigger provenance', () => { await framework.stop(); }); + it('a fresh mixed batch keeps console attendance from any request', async () => { + membrane.pushResponse(createMockResponse([{ type: 'text', text: 'mixed reply' }])); + const framework = await makeFramework(); + const i = internals(framework); + const captured = spy(framework); + const t = Date.now(); + i.pendingRequests.push( + { agentName: 'scout', reason: 'mcpl:channel-incoming', source: 'discord', timestamp: t, + channelId: 'discord:g:room', addressed: true }, + { agentName: 'scout', reason: 'api:message', source: 'unknown', timestamp: t + 1, + consoleAttendedOrigin: true }, + ); + await i.processInferenceRequests(); + await framework.runUntilIdle(); + assert.equal(captured.handed?.consoleAttendedOrigin, true); + assert.equal(captured.handed?.channelId, 'discord:g:room'); + await framework.stop(); + }); + + it('a context-budget continuation preserves its original console-attended value', async () => { + membrane.pushResponse(createMockResponse([{ type: 'text', text: 'continuing' }])); + const framework = await makeFramework(); + const i = internals(framework); + const captured = spy(framework); + const t = Date.now(); + i.pendingRequests.push( + { agentName: 'scout', reason: 'api:message', source: 'unknown', timestamp: t, + consoleAttendedOrigin: true }, + { agentName: 'scout', reason: 'context_budget_restart', source: 'framework', timestamp: t + 1, + consoleAttendedOrigin: false }, + ); + await i.processInferenceRequests(); + await framework.runUntilIdle(); + assert.equal(captured.handed?.reason, 'context_budget_restart'); + assert.equal(captured.handed?.consoleAttendedOrigin, false); + await framework.stop(); + }); + it('a context-budget restart keeps the channel for routing but names no author', async () => { membrane.pushResponse(createMockResponse([{ type: 'text', text: 'continuing' }])); const framework = await makeFramework(); From af300cc774a78070717fa801d5763956e8471153 Mon Sep 17 00:00:00 2001 From: Abigail-2459 Date: Sun, 18 Oct 2026 06:45:24 -0400 Subject: [PATCH 2/2] fix(receipt): the disabled-mode receipt says where plain speech went, not who saw it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In disabled mode, routing posts no plain speech to any channel, and the words stay in the transcript. The receipt now says only that, as the second suggestion in #225 has it. A disabled-mode turn that wrote plain speech ends with: [delivered] plain speech → no channel — N plain-speech segment(s) kept in the transcript (proseRouting=disabled — post to a channel only with an explicit send tool) It names no surface, and it doesn't deny an explicit send made in the same turn, which reports itself in its tool result. This replaces the console-attended classification from this branch's first commit. That classification read attendance from the type of the event that woke the turn, and the type doesn't say whether anyone watches the stream: - connectome-host sends IPC wakes as an external-message with source "headless", and tells the resident that a plain-prose reply won't reach the sender. Its receipt said "shown on the attending surface". - An api:message whose metadata names a channel isn't a surface conversation by the framework's own rule (surfaceSourceFor reads metadata.channelId). Its receipt said "shown on the attending surface" too, since the check read a top-level channelId, which these events don't carry. Who watches is the host's to know. With the classification gone, the consoleAttendedOrigin field on InferenceRequest goes, and so does the flag carried through batches, continuation requests and TurnRouting, with the four tests of that plumbing. The receipt tests keep their cases with the new wording, and a disabled turn woken by a channel message gets the same receipt as one woken from a console. The changelog fragment, docs/disabled-prose-routing.md and the proseRouting doc on AgentConfig follow, and the comment above the receipt's call no longer says only locus and hybrid modes get it. The receipts of the other modes are unchanged. Co-Authored-By: Claude Opus 5.5 --- changelog.d/disabled-console-receipt.fixed.md | 2 +- docs/disabled-prose-routing.md | 5 +- src/framework.ts | 54 +++++-------------- src/types/agent.ts | 5 +- test/explicit-prose-routing.test.ts | 42 ++------------- test/prose-drafts.test.ts | 2 +- test/turn-trigger-provenance.test.ts | 38 ------------- 7 files changed, 23 insertions(+), 125 deletions(-) diff --git a/changelog.d/disabled-console-receipt.fixed.md b/changelog.d/disabled-console-receipt.fixed.md index 2f761f820..80485435c 100644 --- a/changelog.d/disabled-console-receipt.fixed.md +++ b/changelog.d/disabled-console-receipt.fixed.md @@ -1 +1 @@ -- Disabled prose routing now reports that suppressed prose was shown on an attending non-channel surface and that no channel publish was attempted, instead of claiming nothing was delivered. +- Under `proseRouting: "disabled"`, the turn-end receipt now reads `[delivered] plain speech → no channel — N plain-speech segment(s) kept in the transcript (…)` instead of `[delivered] nothing — N plain-speech segment(s) suppressed (…)`, which read to a resident talking with its operator through a console or Web UI as if its replies had reached no one (#225). diff --git a/docs/disabled-prose-routing.md b/docs/disabled-prose-routing.md index e39e3c971..7355c6687 100644 --- a/docs/disabled-prose-routing.md +++ b/docs/disabled-prose-routing.md @@ -7,8 +7,9 @@ want every external utterance to be an explicit tool action. - Ambient traffic and channel opens cannot establish a publication locus. - Outgoing prose streaming and typing indicators are disabled. - Explicit publish tools (Discord/Portal sends, Eidoverse `say`, etc.) are unchanged. -- Authored prose remains in Chronicle. At turn end, a private `[delivered] nothing` - receipt records how many prose segments were suppressed. +- Authored prose remains in Chronicle. An ordinary turn that wrote plain speech + ends with a private `[delivered] plain speech → no channel` receipt, which + records how many segments stayed in the transcript instead of going to a channel. - The mode injects no model-visible primer. - Sleep announcements remain private unless the resident sends one explicitly. diff --git a/src/framework.ts b/src/framework.ts index ed463519f..a086afb66 100644 --- a/src/framework.ts +++ b/src/framework.ts @@ -780,8 +780,6 @@ interface TurnRouting { deliveries: string[]; /** Plain-prose segments the turn suppressed without drafts (proseRouting=disabled), reported by its receipt. */ suppressed: number; - /** An operator-facing non-channel surface receives this logical turn's stream. */ - consoleAttended: boolean; /** * Drafts held during the turn (src/prose-drafts.ts), which its receipt * names by their state at its end, and which settleTurnDrafts records as @@ -828,7 +826,7 @@ function freshTurnRouting(): TurnRouting { return { turnId: ++lastTurnId, engaged: new Set(), destination: null, - continuation: false, deliveries: [], suppressed: 0, consoleAttended: false, drafts: [], keptPrivate: 0, arrivals: [], + continuation: false, deliveries: [], suppressed: 0, drafts: [], keptPrivate: 0, arrivals: [], }; } @@ -867,18 +865,6 @@ function isTurnContinuation(reason: string): boolean { return reason === 'context_budget_restart' || reason === 'tool_results_ready' || reason === 'tool_result_guard_retry'; } - -function isConsoleAttendedEvent(event: ProcessEvent): boolean { - const channelId = (event as unknown as { channelId?: unknown }).channelId; - return !event.type.startsWith('mcpl:') - && (event.type === 'external-message' || event.type === 'api:message' - || event.type === 'api:inference-request') - && channelId === undefined; -} - -function isConsoleAttendedTurn(trigger: InferenceRequest | undefined): boolean { - return trigger?.consoleAttendedOrigin === true; -} const CONVERSATION_ROUTER_STATE_ID = 'framework/conversation-router'; const INFERENCE_LOG_ID = 'framework/inference-log'; const PROCESS_LOG_ID = 'framework/process-log'; @@ -10516,7 +10502,6 @@ export class AgentFramework { suppressProse: this.activeTurnTriggers.get(agent.name)?.suppressProse, ephemeralSystemPrompt: this.activeTurnTriggers.get(agent.name)?.ephemeralSystemPrompt, silentHeartbeat: this.activeTurnTriggers.get(agent.name)?.silentHeartbeat, - consoleAttendedOrigin: this.activeTurnTriggers.get(agent.name)?.consoleAttendedOrigin, }); } else if (currentState.stream) { // What this round appends is in the next call's prompt. If that @@ -10719,7 +10704,6 @@ export class AgentFramework { reason: event.type, source, timestamp: Date.now(), - consoleAttendedOrigin: isConsoleAttendedEvent(event), }); } } @@ -12637,12 +12621,6 @@ export class AgentFramework { suppressProse: silentOnly ? trigger?.suppressProse : undefined, ephemeralSystemPrompt: silentOnly ? trigger?.ephemeralSystemPrompt : undefined, silentHeartbeat: silentOnly ? trigger?.silentHeartbeat : undefined, - // Fresh batches are console-attended if ANY cause is shown on an - // operator surface. Continuations keep the original logical turn's - // provenance rather than adopting a newly-batched request. - consoleAttendedOrigin: budgetRestart - ? trigger?.consoleAttendedOrigin - : requests.some((request) => request.consoleAttendedOrigin === true) || undefined, channelId: channelReq?.channelId, addressed: addressedReq !== undefined, // A context-budget restart continues the same logical turn: it keeps @@ -13505,7 +13483,7 @@ export class AgentFramework { if (kept > 0) notes.push(`${kept} plain-speech segment(s) kept private (skip_reply or a private think)`); if (suppressed > 0) { notes.push(agent.proseRouting === 'disabled' - ? `${suppressed} plain-speech segment(s) suppressed (proseRouting=disabled — publish only with an explicit send tool)` + ? `${suppressed} plain-speech segment(s) kept in the transcript (proseRouting=disabled — post to a channel only with an explicit send tool)` : `${suppressed} plain-speech segment(s) suppressed`); } const suppressedNote = notes.join(' · '); @@ -13514,19 +13492,17 @@ export class AgentFramework { // since the turn held it may have had such an attempt: its state is // unknown now. const uncertain = unconfirmed.length > 0 || dismissedAtRisk.length > 0 || letGo.length > 0; - // A disabled-mode turn that an operator-facing surface attended was shown - // there, and no prose was published automatically. Disabled mode holds no - // drafts, so such a turn is never uncertain; if one were, the uncertain - // wording would show, since a send whose outcome is unknown matters more - // to the resident than where the reply was shown. + // In disabled mode, routing posts no plain speech to any channel, and the + // words stay in the transcript. The receipt says only that. Who watched + // the turn's stream is for the host to know, and an explicit send + // reports itself in its tool result. Disabled mode holds no drafts, so + // it is never uncertain. const text = shown.length > 0 ? `[delivered] plain speech → ${shown.join(' · ')}${suppressedNote ? ` · ${suppressedNote}` : ''}` - : uncertain - ? `[delivered] nothing confirmed — ${suppressedNote}` - : routing.consoleAttended && agent.proseRouting === 'disabled' - ? `[delivered] shown on the attending surface; no automatic prose publish attempted — ${suppressedNote}` - : `[delivered] nothing — ${suppressedNote}`; + : agent.proseRouting === 'disabled' + ? `[delivered] plain speech → no channel — ${suppressedNote}` + : `[delivered] nothing${uncertain ? ' confirmed' : ''} — ${suppressedNote}`; try { const mid = agent.getContextManager().addMessage( 'user', @@ -14807,7 +14783,6 @@ export class AgentFramework { } } this.turnRouting.set(agent.name, routing); - if (trigger?.consoleAttendedOrigin === true) routing.consoleAttended = true; if (resumesTurn) { const previousLogicalToolState = this.logicalTurnToolCalls.get(agent); @@ -14829,7 +14804,6 @@ export class AgentFramework { } if (opensTurn) { - routing.consoleAttended = trigger?.consoleAttendedOrigin === true; // The turn's trigger channel (item-3 redux): the channel of a // channel/DM-triggered turn, and none for a heartbeat / no-trigger // turn, which falls back to the global default rather than inheriting @@ -16379,12 +16353,11 @@ export class AgentFramework { // receipt. Queued deliveries and holds settled before final // dispatch, even when the last response is empty, so the receipt // and the continuation see the last rounds' deliveries; - // trailing/text-only segments were awaited in-loop. Locus and - // hybrid modes only; explicit-mode envelopes acknowledge - // themselves through the prose gateway. + // trailing/text-only segments were awaited in-loop. Every mode + // but explicit, whose envelopes acknowledge themselves through + // the prose gateway. if (!trigger?.suppressProse && turnProseRouting !== 'explicit') { this.appendProseDeliveryReceipt(agent, streamRouting); - } // A `!` continuation this turn asked for. This gives prose the @@ -16604,7 +16577,6 @@ export class AgentFramework { } else { this.requestProseContinuation(agent, streamRouting); } - } return; } diff --git a/src/types/agent.ts b/src/types/agent.ts index 6eb7f227c..84ec5a023 100644 --- a/src/types/agent.ts +++ b/src/types/agent.ts @@ -192,7 +192,8 @@ export interface AgentConfig { * that publication envelope through the same authorized resolver. * See docs/hybrid-prose-routing.md. * - 'disabled': generated prose is never published. Only explicit tools may - * send externally; prose remains in Chronicle with a private suppression receipt. + * send externally; prose remains in Chronicle, and a turn that wrote plain + * speech ends with a private `[delivered] plain speech → no channel` receipt. */ proseRouting?: 'locus' | 'explicit' | 'hybrid' | 'disabled'; @@ -360,8 +361,6 @@ export interface InferenceRequest { reason: string; source: string; timestamp: number; - /** True when an operator-facing non-channel surface receives this turn's stream. */ - consoleAttendedOrigin?: boolean; /** * The MCPL channel whose message triggered this inference, if any (composite * id, e.g. `discord:guild:channel` / `discord:dm:id`). The framework routes diff --git a/test/explicit-prose-routing.test.ts b/test/explicit-prose-routing.test.ts index 75677f44d..4b01e0a78 100644 --- a/test/explicit-prose-routing.test.ts +++ b/test/explicit-prose-routing.test.ts @@ -25,7 +25,6 @@ import type { InferenceRequest, } from '../src/index.js'; import { AgentFramework } from '../src/index.js'; -import { ApiModule } from '../src/modules/api/index.js'; import { MockMembrane, createMockResponse } from './helpers/mock-membrane.js'; import { ProseStreamRouter } from '../src/mcpl/prose-stream-router.js'; import { draftState, type ProseDraftStore } from '../src/prose-drafts.js'; @@ -2203,8 +2202,8 @@ describe('explicit prose routing', () => { const texts = framework.getAgent('assistant')!.getContextManager().getAllMessages() .flatMap(m => m.content).filter(b => b.type === 'text').map(b => (b as { text: string }).text); assert.ok(texts.includes(authored), 'authored prose remains in Chronicle'); - assert.ok(texts.some(t => t.includes('[delivered] shown on the attending surface; no automatic prose publish attempted') && t.includes('proseRouting=disabled')), - 'private suppression receipt explains the unsent prose'); + assert.ok(texts.includes('[delivered] plain speech → no channel — 1 plain-speech segment(s) kept in the transcript (proseRouting=disabled — post to a channel only with an explicit send tool)'), + 'the private receipt says the prose went to no channel and stays in the transcript'); await framework.stop(); }); @@ -2228,7 +2227,7 @@ describe('explicit prose routing', () => { assert.equal(module.calls[0]!.name, 'say'); const texts = framework.getAgent('assistant')!.getContextManager().getAllMessages() .flatMap(m => m.content).filter(b => b.type === 'text').map(b => (b as { text: string }).text); - assert.ok(texts.some(t => t.includes('[delivered] shown on the attending surface; no automatic prose publish attempted') && t.includes('2 plain-speech segment(s) suppressed')), + assert.ok(texts.some(t => t.includes('[delivered] plain speech → no channel') && t.includes('2 plain-speech segment(s) kept in the transcript')), 'one receipt accounts for mid-turn and trailing prose'); await framework.stop(); @@ -2521,39 +2520,4 @@ describe('explicit prose routing', () => { }); } - it('disabled context-budget continuation preserves its attending-surface receipt', async () => { - membrane.pushResponse(createMockResponse([{ type: 'text', text: 'private continuation' }] as ContentBlock[])); - const framework = await createFramework('disabled'); - stubRegistry(framework); - const agent = framework.getAgent('assistant')!; - await (framework as unknown as { startAgentStream(agent: unknown, trigger: unknown): Promise }) - .startAgentStream(agent, { - agentName: 'assistant', reason: 'context_budget_restart', source: 'framework', timestamp: Date.now(), - consoleAttendedOrigin: true, - }); - await framework.runUntilIdle(); - const texts = agent.getContextManager().getAllMessages().flatMap(m => m.content) - .filter(b => b.type === 'text').map(b => (b as { text: string }).text); - assert.ok(texts.some(t => t.includes('shown on the attending surface') - && t.includes('no automatic prose publish attempted'))); - await framework.stop(); - }); - - it('disabled api:inference-request is console-attended', async () => { - membrane.pushResponse(createMockResponse([{ type: 'text', text: 'api reply' }] as ContentBlock[])); - const framework = await AgentFramework.create({ - storePath: join(tempDir, 'api-test.chronicle'), membrane: membrane.asMembrane(), - agents: [{ name: 'assistant', model: 'test-model', systemPrompt: 'test', proseRouting: 'disabled' }], - modules: [new ApiModule()], - }); - stubRegistry(framework); - framework.pushEvent({ type: 'api:inference-request', agentName: 'assistant' } as never); - await framework.runUntilIdle(); - const texts = framework.getAgent('assistant')!.getContextManager().getAllMessages().flatMap(m => m.content) - .filter(b => b.type === 'text').map(b => (b as { text: string }).text); - assert.ok(texts.some(t => t.includes('shown on the attending surface') - && t.includes('no automatic prose publish attempted'))); - await framework.stop(); - }); - }); diff --git a/test/prose-drafts.test.ts b/test/prose-drafts.test.ts index 210ced87f..031cb7996 100644 --- a/test/prose-drafts.test.ts +++ b/test/prose-drafts.test.ts @@ -557,7 +557,7 @@ describe('held prose drafts, end to end', () => { createMockResponse([text('More of it.')]), ]); assert.deepEqual(h.drafts(), []); - assert.ok(h.texts().some((t) => t.startsWith('[delivered] nothing — 2 plain-speech segment(s) suppressed (proseRouting=disabled'))); + assert.ok(h.texts().some((t) => t.startsWith('[delivered] plain speech → no channel — 2 plain-speech segment(s) kept in the transcript (proseRouting=disabled'))); } finally { await h.close(); } diff --git a/test/turn-trigger-provenance.test.ts b/test/turn-trigger-provenance.test.ts index ce84c0001..df75affc1 100644 --- a/test/turn-trigger-provenance.test.ts +++ b/test/turn-trigger-provenance.test.ts @@ -144,44 +144,6 @@ describe('Turn trigger provenance', () => { await framework.stop(); }); - it('a fresh mixed batch keeps console attendance from any request', async () => { - membrane.pushResponse(createMockResponse([{ type: 'text', text: 'mixed reply' }])); - const framework = await makeFramework(); - const i = internals(framework); - const captured = spy(framework); - const t = Date.now(); - i.pendingRequests.push( - { agentName: 'scout', reason: 'mcpl:channel-incoming', source: 'discord', timestamp: t, - channelId: 'discord:g:room', addressed: true }, - { agentName: 'scout', reason: 'api:message', source: 'unknown', timestamp: t + 1, - consoleAttendedOrigin: true }, - ); - await i.processInferenceRequests(); - await framework.runUntilIdle(); - assert.equal(captured.handed?.consoleAttendedOrigin, true); - assert.equal(captured.handed?.channelId, 'discord:g:room'); - await framework.stop(); - }); - - it('a context-budget continuation preserves its original console-attended value', async () => { - membrane.pushResponse(createMockResponse([{ type: 'text', text: 'continuing' }])); - const framework = await makeFramework(); - const i = internals(framework); - const captured = spy(framework); - const t = Date.now(); - i.pendingRequests.push( - { agentName: 'scout', reason: 'api:message', source: 'unknown', timestamp: t, - consoleAttendedOrigin: true }, - { agentName: 'scout', reason: 'context_budget_restart', source: 'framework', timestamp: t + 1, - consoleAttendedOrigin: false }, - ); - await i.processInferenceRequests(); - await framework.runUntilIdle(); - assert.equal(captured.handed?.reason, 'context_budget_restart'); - assert.equal(captured.handed?.consoleAttendedOrigin, false); - await framework.stop(); - }); - it('a context-budget restart keeps the channel for routing but names no author', async () => { membrane.pushResponse(createMockResponse([{ type: 'text', text: 'continuing' }])); const framework = await makeFramework();