mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-16 22:59:04 +02:00
The transcript dialog opened from a running task's row showed only a one-shot snapshot taken at open time: TranscriptButton fetched once via api.listTaskMessages and cached it locally, never subscribing to the shared ["task-messages", taskId] cache that the WS task:message stream already seeds. New tool calls / thinking / text never appeared until the task finished or the page reloaded. Add a live-cache mode to the shared TranscriptButton: when isLive and the parent provides no items and the task id is a persisted UUID, render from the shared task-messages cache so the open dialog grows in real time. On open (and again on the running→terminal transition) force a backfill via api.listTaskMessages and merge it into the cache by seq — taskMessagesOptions is staleTime:Infinity, so a plain subscription never heals a WS reconnect gap. The cache observer is read-only (enabled:false) so React Query never blind-replaces the cache; only the WS handler and the seq-merged backfill write it. The subscription mounts only while the dialog is open, so closed live rows add no baseline requests; terminal tasks keep the lazy one-shot fetch. Covers issue execution-log and agent activity. Autopilot issue-less run_only live log is out of scope: the backend doesn't broadcast task:message for tasks with no issue/chat session, so there's nothing to subscribe to — backend broadcast unchanged. Extract mergeTaskMessagesBySeq into packages/core/chat/queries.ts and route both the realtime task:message handler and the new backfill through it, so there is one seq-merge semantics for that cache instead of two. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
62 lines
1.9 KiB
TypeScript
62 lines
1.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import type { TaskMessagePayload } from "../types/events";
|
|
import {
|
|
isTaskMessageTaskId,
|
|
mergeTaskMessagesBySeq,
|
|
taskMessagesOptions,
|
|
} from "./queries";
|
|
|
|
const msg = (seq: number): TaskMessagePayload => ({
|
|
task_id: "task-1",
|
|
issue_id: "issue-1",
|
|
seq,
|
|
type: "text",
|
|
content: `m${seq}`,
|
|
});
|
|
|
|
describe("taskMessagesOptions", () => {
|
|
it("fetches task messages for persisted UUID task ids", () => {
|
|
const taskId = "4a2e8d1c-7f9b-4e2a-9c1d-123456789abc";
|
|
|
|
expect(isTaskMessageTaskId(taskId)).toBe(true);
|
|
expect(taskMessagesOptions(taskId).enabled).toBe(true);
|
|
});
|
|
|
|
it("does not fetch task messages for optimistic task ids", () => {
|
|
const taskId = "optimistic-optimistic-1778739487737";
|
|
|
|
expect(isTaskMessageTaskId(taskId)).toBe(false);
|
|
expect(taskMessagesOptions(taskId).enabled).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("mergeTaskMessagesBySeq", () => {
|
|
it("backfills missing seqs and keeps the list seq-ordered", () => {
|
|
const existing = [msg(1), msg(3)];
|
|
const merged = mergeTaskMessagesBySeq(existing, [msg(2), msg(4)]);
|
|
|
|
expect(merged.map((m) => m.seq)).toEqual([1, 2, 3, 4]);
|
|
});
|
|
|
|
it("drops duplicate seqs and lets the existing entry win", () => {
|
|
const existing = [{ ...msg(1), content: "ws" }];
|
|
const merged = mergeTaskMessagesBySeq(existing, [
|
|
{ ...msg(1), content: "refetch" },
|
|
msg(2),
|
|
]);
|
|
|
|
expect(merged.map((m) => m.seq)).toEqual([1, 2]);
|
|
expect(merged.find((m) => m.seq === 1)?.content).toBe("ws");
|
|
});
|
|
|
|
it("preserves the array reference when nothing new arrives", () => {
|
|
const existing = [msg(1), msg(2)];
|
|
|
|
// Empty incoming and fully-duplicate incoming must both no-op so React
|
|
// Query observers don't re-render on replayed events.
|
|
expect(mergeTaskMessagesBySeq(existing, [])).toBe(existing);
|
|
expect(mergeTaskMessagesBySeq(existing, [msg(1), msg(2)])).toBe(existing);
|
|
});
|
|
});
|