fix: separate worker completion from durable session delivery

This commit is contained in:
amosblomqvist
2026-10-26 01:55:52 +02:00
parent fc3673466d
commit e851432f4c
19 changed files with 646 additions and 326 deletions
+13 -9
View File
@@ -17,7 +17,7 @@ import { createExtractionPipeline } from "../agent/subagents/extraction.ts";
import type { ModelRole } from "../../config.ts";
import {
deliverJobResult,
noteSteerVisible,
noteSteersDurable,
reconcileUserSteers,
registerLiveAgent,
restoreUncommittedAutomatic,
@@ -58,7 +58,7 @@ import {
type TurnInput,
} from "../core/input-events.ts";
import { steerDelivered } from "../core/contracts.ts";
import type {CallUsage, SteerMessage} from "../core/contracts.ts";
import type {CallUsage} from "../core/contracts.ts";
/**
* One chat turn over the server-owned native record. Framework-free (plain
@@ -259,7 +259,8 @@ export async function handleChatInput(
chatId,
);
const compactedRecord = stores.chatStore.getChat(chatId);
if (compactedRecord) record = compactedRecord;
if (!compactedRecord) return Response.json({ error: "Unknown chat" }, { status: 404 });
record = compactedRecord;
// The input's native appends (pure; the honest 400 for an answer with no
// pending question call). Computed BEFORE beginTurn so a refused input
@@ -329,9 +330,7 @@ export async function handleChatInput(
// Visibility bookkeeping is not the canonical save. Its failure must
// neither misreport a saved input as lost nor prevent the turn running.
try {
for (const appended of stagedAutomatic ? [] : appends) {
if ((appended as { role?: string }).role === "custom") noteSteerVisible(appended as SteerMessage);
}
if (!stagedAutomatic) noteSteersDurable(chatId, merged);
} catch (err) {
console.error(`[chat ${chatId}] input visibility bookkeeping failed:`, err);
}
@@ -383,7 +382,11 @@ export async function handleChatInput(
foregroundOwed: !callPurpose.background,
purpose: callPurpose,
scopeCurrent: (scope) => connectionScopeCurrent(conversation.provider, scope),
commitStaged: () => { stores.chatStore.saveMessages(chatId, merged); persistBaseCount = merged.length; },
commitStaged: () => {
stores.chatStore.saveMessages(chatId, merged);
persistBaseCount = merged.length;
noteSteersDurable(chatId, merged);
},
restoreAutomatic: (automaticIds, retainedIds) => restoreUncommittedAutomatic(chatId, constructed, automaticIds, retainedIds),
});
const agentOpts: AgentBuildOptions = {
@@ -481,7 +484,7 @@ export async function handleChatInput(
// run's first LLM call (the native replacement for continuation
// coalescing). Settlement still cannot beat the flow below: it needs
// a streamed response, which never happens inside the constructor.
registerLiveAgent(chatId, constructed);
registerLiveAgent(chatId, constructed, input.type !== "steers" && input.type !== "continue");
// Debug recorder run marker (owner, 2026-10-04): correlates the
// JSONL event stream with the record's turns; no-op unless
// LH_PROVIDER_EVENTS=1.
@@ -522,12 +525,13 @@ export async function handleChatInput(
// merely because usage/extraction/other settlement work failed.
const persisted = stores.chatStore.getChat(chatId)?.messages ?? [];
reconcileUserSteers(chatId, persisted);
settleSteers(chatId, persisted);
settleSteers(chatId, persisted, true);
},
onFinish: (turnMessages, isAborted) => {
// Wave B: the record write is the agent's own final state —
// the new messages are the slice past the merged base.
const stamped = persistTurn(stores.chatStore, chatId, persistBaseCount, turnMessages, isAborted);
noteSteersDurable(chatId, stores.chatStore.getChat(chatId)?.messages ?? stamped);
// usage log append (idempotent per turn id) — the stats bar
// refolds this on the client after the turn settles. The
// provenance message is the turn's FIRST new assistant message.