fix(desktop): scope observer feed by channel and add session-boundary dividers (#1634)

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Will Pfleger
2026-07-08 16:43:42 -04:00
committed by GitHub
parent 4f1a487ab4
commit fd5d04de05
21 changed files with 1785 additions and 44 deletions

View File

@@ -6,6 +6,7 @@ import {
flattenDisplayBlocks,
formatTurnSetupLabel,
} from "./agentSessionTranscriptGrouping.ts";
import { isObserverEventAfter } from "../observerRelayStore.ts";
const baseTimestamp = "2026-06-14T22:20:23.000Z";
@@ -639,3 +640,446 @@ function mkTool(id, label, renderClass = "generic", groupKey = label) {
channelId: "chan-1",
};
}
// ── Session-run splitting and session-boundary blocks ──────────────────────────
/**
* Build a minimal tool-call item stamped with a specific session.
*/
function sessionItem(id, sessionId, ts = "2026-07-08T00:00:00.000Z") {
return {
id,
type: "tool",
renderClass: "generic",
descriptor: {
renderClass: "generic",
label: id,
preview: id,
source: "harness",
groupKey: id,
},
title: id,
toolName: id,
buzzToolName: null,
status: "completed",
args: {},
result: "",
isError: false,
timestamp: ts,
startedAt: ts,
completedAt: ts,
turnId: `turn-${id}`,
sessionId,
channelId: "chan-1",
};
}
// ── Single session — no boundary injected ──────────────────────────────────────
test("buildTranscriptDisplayBlocks_singleSession_noBoundaryBlock", () => {
const items = [sessionItem("a", "sess-1"), sessionItem("b", "sess-1")];
const blocks = buildTranscriptDisplayBlocks(items);
const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary");
assert.equal(
boundaryBlocks.length,
0,
"no session-boundary blocks for a single session",
);
});
// ── Two sessions — one boundary between them ───────────────────────────────────
test("buildTranscriptDisplayBlocks_twoSessions_oneBoundaryBetween", () => {
// items ordered oldest-first: sess-1 then sess-2
const items = [
sessionItem("a", "sess-1", "2026-07-08T00:00:01.000Z"),
sessionItem("b", "sess-2", "2026-07-08T00:00:02.000Z"),
];
const blocks = buildTranscriptDisplayBlocks(items);
const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary");
assert.equal(
boundaryBlocks.length,
1,
"exactly one boundary for two sessions",
);
// The boundary is inserted BEFORE the newer run (sess-2).
const boundaryIndex = blocks.indexOf(boundaryBlocks[0]);
const prevBlock = blocks[boundaryIndex - 1];
const nextBlock = blocks[boundaryIndex + 1];
// Previous content block belongs to sess-1 items, next to sess-2.
const flatPrev = flattenDisplayBlocks([prevBlock]).map((i) => i.id);
const flatNext = flattenDisplayBlocks([nextBlock]).map((i) => i.id);
assert.ok(flatPrev.includes("a"), "content before boundary is sess-1");
assert.ok(flatNext.includes("b"), "content after boundary is sess-2");
});
// ── Newest session labeled correctly relative to latestLiveSessionId ──────────
test("buildTranscriptDisplayBlocks_newestMatchesLive_labelStateCurrent", () => {
const items = [
sessionItem("old", "sess-1", "2026-07-08T00:00:01.000Z"),
sessionItem("new", "sess-2", "2026-07-08T00:00:02.000Z"),
];
const blocks = buildTranscriptDisplayBlocks(items, "sess-2");
const boundary = blocks.find((b) => b.kind === "session-boundary");
assert.ok(boundary, "boundary present");
assert.equal(
boundary.labelState,
"current",
"labelState=current when newest session matches live id",
);
assert.equal(boundary.sessionId, "sess-2", "boundary sessionId is sess-2");
});
test("buildTranscriptDisplayBlocks_newestNoLive_labelStateMostRecent", () => {
const items = [
sessionItem("old", "sess-1", "2026-07-08T00:00:01.000Z"),
sessionItem("new", "sess-2", "2026-07-08T00:00:02.000Z"),
];
// latestLiveSessionId is null → newest session is "most-recent"
const blocksNoLive = buildTranscriptDisplayBlocks(items, null);
const boundaryNoLive = blocksNoLive.find(
(b) => b.kind === "session-boundary",
);
assert.equal(
boundaryNoLive.labelState,
"most-recent",
"labelState=most-recent when no live id (archived-only view)",
);
// latestLiveSessionId is a DIFFERENT session → newest is still "most-recent"
const blocksDiffLive = buildTranscriptDisplayBlocks(items, "sess-other");
const boundaryDiff = blocksDiffLive.find(
(b) => b.kind === "session-boundary",
);
assert.equal(
boundaryDiff.labelState,
"most-recent",
"labelState=most-recent when live id differs from newest visible",
);
});
// ── Three sessions — two boundaries ────────────────────────────────────────────
test("buildTranscriptDisplayBlocks_threeSessions_twoBoundaries", () => {
const items = [
sessionItem("a", "sess-1", "2026-07-08T00:00:01.000Z"),
sessionItem("b", "sess-2", "2026-07-08T00:00:02.000Z"),
sessionItem("c", "sess-3", "2026-07-08T00:00:03.000Z"),
];
const blocks = buildTranscriptDisplayBlocks(items);
const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary");
assert.equal(boundaryBlocks.length, 2, "two boundaries for three sessions");
// With no live id: newest (sess-3) boundary = "most-recent"; older = "earlier".
const newestBoundary = boundaryBlocks[boundaryBlocks.length - 1];
const olderBoundary = boundaryBlocks[0];
assert.equal(
newestBoundary.labelState,
"most-recent",
"newest boundary is most-recent when latestLiveSessionId is null",
);
assert.equal(
olderBoundary.labelState,
"earlier",
"older boundary is earlier when latestLiveSessionId is null",
);
});
// ── Null sessionId items stay in the current run ───────────────────────────────
test("buildTranscriptDisplayBlocks_nullSessionId_staysInCurrentRun", () => {
// An item with null sessionId should not start a new run.
const items = [
sessionItem("a", "sess-1", "2026-07-08T00:00:01.000Z"),
// null sessionId — stays in sess-1 run
{ ...sessionItem("b", null, "2026-07-08T00:00:02.000Z"), sessionId: null },
sessionItem("c", "sess-1", "2026-07-08T00:00:03.000Z"),
];
const blocks = buildTranscriptDisplayBlocks(items);
const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary");
assert.equal(
boundaryBlocks.length,
0,
"no boundary when null-sessionId items are present within a single session",
);
});
// ── flattenDisplayBlocks skips session-boundary blocks ────────────────────────
test("flattenDisplayBlocks_skipsSessionBoundaryBlocks", () => {
const items = [
sessionItem("a", "sess-1", "2026-07-08T00:00:01.000Z"),
sessionItem("b", "sess-2", "2026-07-08T00:00:02.000Z"),
];
const blocks = buildTranscriptDisplayBlocks(items);
assert.ok(
blocks.some((b) => b.kind === "session-boundary"),
"test setup: boundary must be present",
);
const flat = flattenDisplayBlocks(blocks);
const ids = flat.map((i) => i.id);
assert.ok(ids.includes("a"), "item a is in flattened output");
assert.ok(ids.includes("b"), "item b is in flattened output");
assert.equal(
flat.filter((i) => i.kind === "session-boundary").length,
0,
"session-boundary items are excluded from flatten",
);
});
// ── isObserverEventAfter — latest-live ordering ──────────────────────────────
test("isObserverEventAfter returns true when candidate has later timestamp", () => {
const stored = { timestamp: "2026-07-08T00:00:01.000Z", seq: 5 };
const candidate = { timestamp: "2026-07-08T00:00:02.000Z", seq: 1 };
assert.ok(isObserverEventAfter(candidate, stored));
});
test("isObserverEventAfter returns false when candidate has earlier timestamp", () => {
const stored = { timestamp: "2026-07-08T00:00:02.000Z", seq: 5 };
const candidate = { timestamp: "2026-07-08T00:00:01.000Z", seq: 10 };
assert.ok(!isObserverEventAfter(candidate, stored));
});
test("isObserverEventAfter returns true for same timestamp, higher seq — session B advances over session A", () => {
// This is the tiebreak case: timestamp equal, seq tiebreak must mirror
// compareObserverEvents so latest-live never drifts from transcript order.
const stored = { timestamp: "2026-07-08T00:00:01.000Z", seq: 3 };
const candidate = { timestamp: "2026-07-08T00:00:01.000Z", seq: 7 };
assert.ok(isObserverEventAfter(candidate, stored));
});
test("isObserverEventAfter returns false for same timestamp, same seq", () => {
const stored = { timestamp: "2026-07-08T00:00:01.000Z", seq: 3 };
const candidate = { timestamp: "2026-07-08T00:00:01.000Z", seq: 3 };
assert.ok(!isObserverEventAfter(candidate, stored));
});
test("isObserverEventAfter returns false for same timestamp, lower seq", () => {
const stored = { timestamp: "2026-07-08T00:00:01.000Z", seq: 7 };
const candidate = { timestamp: "2026-07-08T00:00:01.000Z", seq: 3 };
assert.ok(!isObserverEventAfter(candidate, stored));
});
// ── Pre-resolution null-session deferral (regression for first-turn bundle) ───
/**
* Builds the exact wire sequence the harness emits on the first turn:
* turn_started(null) → session/new(null) → session_resolved(sess) → session/prompt(sess)
*
* After processTranscriptEvent the TranscriptItems produced are:
* - lifecycle turn_started: sessionId=null, turnId="turn-001"
* - metadata session/new (system prompt): sessionId=null, turnId=null
* - lifecycle session_resolved: sessionId="session-001", turnId="turn-001"
* - message session/prompt:user: sessionId="session-001", turnId="turn-001"
*/
function firstTurnSequence() {
const ts = "2026-07-08T10:00:00.000Z";
return [
// turn_started: sessionId null, turnId present
{
id: "turn-started",
type: "lifecycle",
renderClass: "lifecycle",
title: "Turn started",
text: "",
timestamp: ts,
acpSource: "turn_started",
turnId: "turn-001",
sessionId: null,
channelId: "chan-1",
},
// session/new system prompt: sessionId null, turnId null (processTranscriptEvent forces turnId null)
{
id: "system-prompt:chan-1",
type: "metadata",
renderClass: "raw-rail",
title: "System prompt",
sections: [
{ title: "Base", body: "You are a helpful AI assistant." },
{ title: "System", body: "You are Observer Agent." },
],
timestamp: ts,
acpSource: "session/new",
turnId: null,
sessionId: null,
channelId: "chan-1",
},
// session_resolved: first item with a non-null sessionId
{
id: "session-resolved",
type: "lifecycle",
renderClass: "lifecycle",
title: "Session ready",
text: "",
timestamp: ts,
acpSource: "session_resolved",
turnId: "turn-001",
sessionId: "session-001",
channelId: "chan-1",
},
// session/prompt:user — the user message bubble
{
id: "user-prompt",
type: "message",
role: "user",
title: "Buzz event",
text: "@Observer Agent help me debug this",
timestamp: ts,
acpSource: "session/prompt:user",
turnId: "turn-001",
sessionId: "session-001",
channelId: "chan-1",
},
];
}
test("buildTranscriptDisplayBlocks_firstTurnSequence_noStandaloneSystemPrompt", () => {
// Regression for: pre-resolution null-session items (turn_started + session/new)
// were assigned to a synthetic "unknown" run, causing the system prompt to emit
// as a standalone "System prompt" row instead of staying in the prompt bundle.
const blocks = buildTranscriptDisplayBlocks(firstTurnSequence());
// (a) No standalone "System prompt" single block.
const systemPromptSingles = blocks.filter(
(b) => b.kind === "single" && b.item.acpSource === "session/new",
);
assert.equal(
systemPromptSingles.length,
0,
"system prompt must not appear as a standalone single block",
);
// (b) No session-boundary blocks — this is a single-session transcript.
const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary");
assert.equal(
boundaryBlocks.length,
0,
"must be zero session-boundary blocks for a first-turn single-session sequence",
);
// (c) System prompt is inside the turn group (consumed by the prompt bundle).
const turnBlocks = blocks.filter((b) => b.kind === "turn");
assert.ok(turnBlocks.length > 0, "at least one turn block must exist");
const allTurnItems = flattenDisplayBlocks(turnBlocks);
const systemPromptInTurn = allTurnItems.some(
(item) => item.acpSource === "session/new",
);
assert.ok(
systemPromptInTurn,
"system prompt item must be present inside a turn block (prompt bundle)",
);
});
test("buildTranscriptDisplayBlocks_genuineSecondSession_boundaryPreserved", () => {
// Regression guard: the deferral fix must not over-collapse two genuinely
// distinct sessions. A second session_resolved with a different sessionId
// still gets its own run and a session-boundary block.
const ts2 = "2026-07-08T11:00:00.000Z";
const items = [
// First session (may have pre-resolution preamble)
...firstTurnSequence(),
// Second session — starts fresh with its own turn_started (no pre-null preamble here)
{
id: "turn-started-2",
type: "lifecycle",
renderClass: "lifecycle",
title: "Turn started",
text: "",
timestamp: ts2,
acpSource: "turn_started",
turnId: "turn-002",
sessionId: "session-002",
channelId: "chan-1",
},
{
id: "user-prompt-2",
type: "message",
role: "user",
title: "Buzz event",
text: "second session message",
timestamp: ts2,
acpSource: "session/prompt:user",
turnId: "turn-002",
sessionId: "session-002",
channelId: "chan-1",
},
];
const blocks = buildTranscriptDisplayBlocks(items);
const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary");
assert.equal(
boundaryBlocks.length,
1,
"exactly one session-boundary block between two distinct sessions",
);
assert.equal(
boundaryBlocks[0].sessionId,
"session-002",
"boundary is labeled with the newer session id",
);
});
// ── Duplicate-key guard: non-contiguous runs of the same sessionId ────────────
test("buildTranscriptDisplayBlocks_nonContiguousRunsSameSession_distinctBoundaryKeys", () => {
// Scenario: sess-B frames, then sess-A frames, then sess-C frames, then
// sess-A frames re-resolve (e.g. same agent session re-observed in a new
// live subscription). splitIntoSessionRuns treats each session-id change as
// a new run, so sess-A appears twice — once at runIndex=1 and once at
// runIndex=3 — yielding two session-boundary blocks both with
// sessionId="sess-A".
//
// The React key for a session-boundary is
// `session-boundary:${sessionId}:${runIndex}`
// Without the runIndex tiebreaker the two sess-A boundaries share the same
// key, causing React to silently duplicate or omit children. This test
// asserts the runIndex tiebreaker keeps the keys distinct.
const items = [
sessionItem("b-item", "sess-B", "2026-07-08T00:00:01.000Z"),
sessionItem("a-item", "sess-A", "2026-07-08T00:00:02.000Z"),
sessionItem("c-item", "sess-C", "2026-07-08T00:00:03.000Z"),
sessionItem("a-item-2", "sess-A", "2026-07-08T00:00:04.000Z"),
];
const blocks = buildTranscriptDisplayBlocks(items, "sess-A");
const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary");
// Four distinct session runs → three boundaries (before sess-A, before
// sess-C, and before the re-occurring sess-A).
assert.equal(
boundaryBlocks.length,
3,
"three boundaries for four runs (sess-B, sess-A, sess-C, sess-A)",
);
// Two boundaries share sessionId="sess-A" (the collision case pre-fix).
const sessABoundaries = boundaryBlocks.filter(
(b) => b.sessionId === "sess-A",
);
assert.equal(
sessABoundaries.length,
2,
"two boundary blocks both carry sessionId=sess-A (the collision the fix guards)",
);
// The runIndex values must be distinct across ALL boundary blocks so that
// `session-boundary:${sessionId}:${runIndex}` keys are unique.
const runIndexSet = new Set(boundaryBlocks.map((b) => b.runIndex));
assert.equal(
runIndexSet.size,
boundaryBlocks.length,
"every session-boundary block has a distinct runIndex",
);
// The React keys derived from each boundary must also be distinct.
const keys = boundaryBlocks.map(
(b) => `session-boundary:${b.sessionId}:${b.runIndex}`,
);
const keySet = new Set(keys);
assert.equal(
keySet.size,
keys.length,
"all React keys derived from session-boundary blocks are unique",
);
});