refactor(server): replace stream() with subscribe() + startTurn() across all providers

Replace three competing event paths (foreground stream, live event pump,
JSONL history poller) with a single push-based subscribe() + startTurn()
contract. This fixes duplicate user messages and stuck running state caused
by timing-based routing between concurrent event sources.

Key changes:
- AgentSession interface: remove stream(), add subscribe() and startTurn()
- All providers (Claude, Codex, OpenCode): single subscribers Set with
  notifySubscribers() for push-based event delivery and turnId stamping
- Agent manager: identity-based turn ownership via activeForegroundTurnId
  replacing pendingRun async generator
- Delete: dual queues, routeSdkMessageFromPump, startLiveHistoryPolling,
  snapHistoryOffsetToEnd, liveEventBacklog, Pushable
- Fix Codex provider not clearing activeForegroundTurnId on turn completion
- Add real-provider integration tests for event stream invariants
This commit is contained in:
Mohamed Boudra
2026-03-24 21:22:00 +07:00
parent 6cc81e3ab4
commit a54687d3ef
23 changed files with 2501 additions and 1708 deletions
@@ -199,10 +199,7 @@ function startAgentRun(
logger: Logger,
options?: { replaceRunning?: boolean },
): void {
const snapshot = agentManager.getAgent(agentId);
const shouldReplace =
options?.replaceRunning &&
Boolean(snapshot && (snapshot.lifecycle === "running" || snapshot.pendingRun));
const shouldReplace = Boolean(options?.replaceRunning && agentManager.hasInFlightRun(agentId));
const iterator = shouldReplace
? agentManager.replaceAgentRun(agentId, prompt)
: agentManager.streamAgent(agentId, prompt);
@@ -696,7 +693,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
throw new Error(`Agent ${agentId} not found`);
}
if (snapshot.lifecycle === "running" || snapshot.pendingRun) {
if (agentManager.hasInFlightRun(agentId)) {
waitTracker.cancel(agentId, "Agent run interrupted by new prompt");
}