summaryrefslogtreecommitdiffhomepage
path: root/packages/conversation-store/src/reconcile.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/conversation-store/src/reconcile.ts')
-rw-r--r--packages/conversation-store/src/reconcile.ts176
1 files changed, 88 insertions, 88 deletions
diff --git a/packages/conversation-store/src/reconcile.ts b/packages/conversation-store/src/reconcile.ts
index ea33904..2d2b68d 100644
--- a/packages/conversation-store/src/reconcile.ts
+++ b/packages/conversation-store/src/reconcile.ts
@@ -1,107 +1,107 @@
import type { ChatMessage, ToolCallChunk, ToolResultChunk } from "@dispatch/kernel";
export interface ReconcileReport {
- readonly repairedCount: number;
- readonly repairedToolCallIds: readonly string[];
- /** Number of `error` chunks stripped from assistant messages. */
- readonly strippedErrorChunks: number;
- /** Number of assistant messages dropped after stripping left them empty. */
- readonly droppedEmptyMessages: number;
+ readonly repairedCount: number;
+ readonly repairedToolCallIds: readonly string[];
+ /** Number of `error` chunks stripped from assistant messages. */
+ readonly strippedErrorChunks: number;
+ /** Number of assistant messages dropped after stripping left them empty. */
+ readonly droppedEmptyMessages: number;
}
export interface ReconcileResult {
- readonly messages: ChatMessage[];
- readonly report: ReconcileReport;
+ readonly messages: ChatMessage[];
+ readonly report: ReconcileReport;
}
export function reconcileWithReport(messages: readonly ChatMessage[]): ReconcileResult {
- // Phase 1: strip `error` chunks from assistant messages. An error chunk is a
- // failed-generation marker, never valid provider content — removing it here
- // (on load, before any provider sees the messages) auto-repairs broken chats
- // with no DB surgery (append-only storage untouched).
- let strippedErrorChunks = 0;
- const stripped: ChatMessage[] = [];
- for (const msg of messages) {
- if (msg.role === "assistant" && msg.chunks.some((c) => c.type === "error")) {
- const filtered = msg.chunks.filter((chunk) => {
- if (chunk.type === "error") {
- strippedErrorChunks++;
- return false;
- }
- return true;
- });
- stripped.push({ role: msg.role, chunks: filtered });
- } else {
- stripped.push(msg);
- }
- }
+ // Phase 1: strip `error` chunks from assistant messages. An error chunk is a
+ // failed-generation marker, never valid provider content — removing it here
+ // (on load, before any provider sees the messages) auto-repairs broken chats
+ // with no DB surgery (append-only storage untouched).
+ let strippedErrorChunks = 0;
+ const stripped: ChatMessage[] = [];
+ for (const msg of messages) {
+ if (msg.role === "assistant" && msg.chunks.some((c) => c.type === "error")) {
+ const filtered = msg.chunks.filter((chunk) => {
+ if (chunk.type === "error") {
+ strippedErrorChunks++;
+ return false;
+ }
+ return true;
+ });
+ stripped.push({ role: msg.role, chunks: filtered });
+ } else {
+ stripped.push(msg);
+ }
+ }
- // Phase 2: drop assistant messages left with neither `text` nor `tool-call`
- // chunks after stripping (the now-empty error-only message). This is what
- // unblocks continuation: such a message serializes to nothing a provider
- // understands. Safe: it ends with no tool-call, so it is NEVER followed by a
- // `tool` message — no "tool-without-preceding-assistant-tool_calls" 400.
- let droppedEmptyMessages = 0;
- const pruned: ChatMessage[] = [];
- for (const msg of stripped) {
- if (msg.role === "assistant") {
- const hasContent = msg.chunks.some(
- (chunk) => chunk.type === "text" || chunk.type === "tool-call",
- );
- if (!hasContent) {
- droppedEmptyMessages++;
- continue;
- }
- }
- pruned.push(msg);
- }
+ // Phase 2: drop assistant messages left with neither `text` nor `tool-call`
+ // chunks after stripping (the now-empty error-only message). This is what
+ // unblocks continuation: such a message serializes to nothing a provider
+ // understands. Safe: it ends with no tool-call, so it is NEVER followed by a
+ // `tool` message — no "tool-without-preceding-assistant-tool_calls" 400.
+ let droppedEmptyMessages = 0;
+ const pruned: ChatMessage[] = [];
+ for (const msg of stripped) {
+ if (msg.role === "assistant") {
+ const hasContent = msg.chunks.some(
+ (chunk) => chunk.type === "text" || chunk.type === "tool-call" || chunk.type === "thinking",
+ );
+ if (!hasContent) {
+ droppedEmptyMessages++;
+ continue;
+ }
+ }
+ pruned.push(msg);
+ }
- // Phase 3: orphaned-tool-call synthesis (unchanged) on what remains.
- const resolvedIds = new Set<string>();
- for (const msg of pruned) {
- for (const chunk of msg.chunks) {
- if (chunk.type === "tool-result") {
- resolvedIds.add(chunk.toolCallId);
- }
- }
- }
+ // Phase 3: orphaned-tool-call synthesis (unchanged) on what remains.
+ const resolvedIds = new Set<string>();
+ for (const msg of pruned) {
+ for (const chunk of msg.chunks) {
+ if (chunk.type === "tool-result") {
+ resolvedIds.add(chunk.toolCallId);
+ }
+ }
+ }
- const orphaned: ToolCallChunk[] = [];
- for (const msg of pruned) {
- if (msg.role !== "assistant") continue;
- for (const chunk of msg.chunks) {
- if (chunk.type === "tool-call" && !resolvedIds.has(chunk.toolCallId)) {
- orphaned.push(chunk);
- }
- }
- }
+ const orphaned: ToolCallChunk[] = [];
+ for (const msg of pruned) {
+ if (msg.role !== "assistant") continue;
+ for (const chunk of msg.chunks) {
+ if (chunk.type === "tool-call" && !resolvedIds.has(chunk.toolCallId)) {
+ orphaned.push(chunk);
+ }
+ }
+ }
- const result: ChatMessage[] = [...pruned];
+ const result: ChatMessage[] = [...pruned];
- for (const call of orphaned) {
- const base = {
- type: "tool-result" as const,
- toolCallId: call.toolCallId,
- toolName: call.toolName,
- content: "interrupted: tool execution did not complete",
- isError: true,
- };
- const synthesized: ToolResultChunk =
- call.stepId !== undefined ? { ...base, stepId: call.stepId } : base;
- result.push({ role: "tool", chunks: [synthesized] });
- }
+ for (const call of orphaned) {
+ const base = {
+ type: "tool-result" as const,
+ toolCallId: call.toolCallId,
+ toolName: call.toolName,
+ content: "interrupted: tool execution did not complete",
+ isError: true,
+ };
+ const synthesized: ToolResultChunk =
+ call.stepId !== undefined ? { ...base, stepId: call.stepId } : base;
+ result.push({ role: "tool", chunks: [synthesized] });
+ }
- return {
- messages: result,
- report: {
- repairedCount: orphaned.length,
- repairedToolCallIds: orphaned.map((c) => c.toolCallId),
- strippedErrorChunks,
- droppedEmptyMessages,
- },
- };
+ return {
+ messages: result,
+ report: {
+ repairedCount: orphaned.length,
+ repairedToolCallIds: orphaned.map((c) => c.toolCallId),
+ strippedErrorChunks,
+ droppedEmptyMessages,
+ },
+ };
}
export function reconcile(messages: readonly ChatMessage[]): ChatMessage[] {
- return reconcileWithReport(messages).messages;
+ return reconcileWithReport(messages).messages;
}