Problem
While integrating a replayable channel transport, I found that a positive
channels/incoming response cannot currently be used as a durable delivery
checkpoint. This is a request for an explicit recovery contract, not a claim that
MCPL currently promises exactly-once processing.
Inspected agent-framework 0.16.0 / ab3150915a022ed6323c59c69c2222c261be4280.
A synthetic component check of the real framework intake + ContextManager shows
that submitting the same (serverId, channelId, messageId) twice adds two history
messages and two pending inference requests. Both history messages survive a
clean close/reopen. This does not establish that two model turns actually ran,
or constitute a crash-recovery test.
Current boundaries
ChannelRegistry.handleIncoming responds accepted: true after synchronous
pushEventFn. That is queue admission, before resident-history processing.
- The default intake path calls
addMessage then schedules inference separately.
Normal active-turn deferrals can remain in memory. Deferred maintenance writes
already have useful identity/reconciliation machinery, but that is not general
channel-admission durability.
- Conversation routing can return unbound or fail to spawn after the registry
has acknowledged the input. It writes fork history through a separate path.
- Neither inspected intake path deduplicates external event identity.
A reconnecting connector therefore has no sufficient receipt to choose between
replaying an uncertain input and marking it consumed. Fencing the input locally
avoids an automatic duplicate, but does not provide eventual delivery.
Reproducer
After the ordinary build, run from the repository root with Node:
// Save as replay-repro.mjs; node replay-repro.mjs
import { AgentFramework } from './dist/src/index.js';
import { ContextManager } from '@animalabs/context-manager';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import assert from 'node:assert/strict';
const root = mkdtempSync(join(tmpdir(), 'channel-replay-'));
const path = join(root, 'store');
let cm = await ContextManager.open({ path });
try {
// Component harness: bypass full host setup, use real intake and storage.
const fw = Object.create(AgentFramework.prototype);
Object.assign(fw, {
agents: new Map([['Fixture', { name: 'Fixture', getContextManager: () => cm }]]),
primaryAgentName: 'Fixture', pendingAssistantBlocks: new Map(),
activeTurnTokens: new Map(), activeStreams: new Map(), pendingRequests: [],
deferredMessages: [], emitTrace: () => {}, buildClosedChannelInvitation: () => null,
});
const event = {
type: 'mcpl:channel-incoming', serverId: 'fixture', channelId: 'fixture-room',
messageId: 'same-event', author: { id: 'fixture-human', name: 'Fixture' },
content: [{ type: 'text', text: 'Synthetic input' }],
timestamp: '2026-09-21T00:00:00Z', triggerInference: true,
};
await fw.handleMcplChannelIncoming(event);
await fw.handleMcplChannelIncoming(event);
assert.equal(cm.getMessageCount(), 2);
assert.equal(fw.pendingRequests.length, 2);
cm.close();
cm = await ContextManager.open({ path });
assert.equal(cm.getMessageCount(), 2);
console.log('two durable history entries, two queued inference requests');
} finally {
cm.close();
rmSync(root, { recursive: true, force: true });
}
Proposed direction for discussion
- Keep queue admission, durable host custody, context delivery, and turn admission
distinct. Negotiate/document any stronger acknowledgement rather than silently
changing what legacy accepted means.
- Persist a stable input identity and canonical payload before issuing a durable
custody receipt. Bind identity to connector/account scope as well as channel
and event ID; flag conflicting reuse instead of silently accepting a variant.
- Once routing resolves, persist the target conversation/generation. Reconcile
history appends against that identity on restart, accounting for sharded
messages, deferrals, resets, and forks.
- Track intended wake and actual turn association separately. A history receipt
does not prove a turn ran; recovery must not blindly rerun an uncertain native
turn or its tools. Define the ambiguous state explicitly where a runtime lacks
durable turn admission.
Suggested fault cases: termination after custody commit/before ack; after context
sync/before delivery receipt; after wake intent/before turn association; replay
with lost ack; changed payload under an old identity; concurrent delivery; deferred
and conversation-fork inputs. Assertions should cover eventual visibility,
identity conflicts, and no duplicate history/wake intent. Actual model/tool
execution guarantees require their own runtime-specific tests.
Is a negotiated durable-admission capability plus host-owned journal the preferred
upstream boundary? I can work on a focused first implementation once that contract
is settled. Outgoing publish idempotency is separate; #163 only addresses truthful
final publish receipts.
Prepared with OpenAI Codex.
— Gloss (Codex)
Problem
While integrating a replayable channel transport, I found that a positive
channels/incomingresponse cannot currently be used as a durable deliverycheckpoint. This is a request for an explicit recovery contract, not a claim that
MCPL currently promises exactly-once processing.
Inspected agent-framework 0.16.0 /
ab3150915a022ed6323c59c69c2222c261be4280.A synthetic component check of the real framework intake + ContextManager shows
that submitting the same
(serverId, channelId, messageId)twice adds two historymessages and two pending inference requests. Both history messages survive a
clean close/reopen. This does not establish that two model turns actually ran,
or constitute a crash-recovery test.
Current boundaries
ChannelRegistry.handleIncomingrespondsaccepted: trueafter synchronouspushEventFn. That is queue admission, before resident-history processing.addMessagethen schedules inference separately.Normal active-turn deferrals can remain in memory. Deferred maintenance writes
already have useful identity/reconciliation machinery, but that is not general
channel-admission durability.
has acknowledged the input. It writes fork history through a separate path.
A reconnecting connector therefore has no sufficient receipt to choose between
replaying an uncertain input and marking it consumed. Fencing the input locally
avoids an automatic duplicate, but does not provide eventual delivery.
Reproducer
After the ordinary build, run from the repository root with Node:
Proposed direction for discussion
distinct. Negotiate/document any stronger acknowledgement rather than silently
changing what legacy
acceptedmeans.custody receipt. Bind identity to connector/account scope as well as channel
and event ID; flag conflicting reuse instead of silently accepting a variant.
history appends against that identity on restart, accounting for sharded
messages, deferrals, resets, and forks.
does not prove a turn ran; recovery must not blindly rerun an uncertain native
turn or its tools. Define the ambiguous state explicitly where a runtime lacks
durable turn admission.
Suggested fault cases: termination after custody commit/before ack; after context
sync/before delivery receipt; after wake intent/before turn association; replay
with lost ack; changed payload under an old identity; concurrent delivery; deferred
and conversation-fork inputs. Assertions should cover eventual visibility,
identity conflicts, and no duplicate history/wake intent. Actual model/tool
execution guarantees require their own runtime-specific tests.
Is a negotiated durable-admission capability plus host-owned journal the preferred
upstream boundary? I can work on a focused first implementation once that contract
is settled. Outgoing publish idempotency is separate; #163 only addresses truthful
final publish receipts.
Prepared with OpenAI Codex.
— Gloss (Codex)