diff options
| author | Adam Malczewski <[email protected]> | 2026-10-10 00:06:31 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-10-10 00:06:31 +0900 |
| commit | 9f2dcfaab421fa84837685a0b65678acf03d9ded (patch) | |
| tree | a1ef40fbbf98b136a471889c5b02795b739753be /src/adapters/idb | |
| parent | 591e2dbe5cbf2283d58debdf92943a075edc4569 (diff) | |
| download | dispatch-web-9f2dcfaab421fa84837685a0b65678acf03d9ded.tar.gz dispatch-web-9f2dcfaab421fa84837685a0b65678acf03d9ded.zip | |
fix(turn-metrics): exact per-turn placement via turnId, decode-only TPS, step-level retention
Placement
- Chunks carry their producing turn: StoredChunk.turnId (backend [email protected]) for
committed rows (persisted in the IDB cache; old cached rows are upgraded on
sync) and the live event's turnId for provisional ones (the optimistic echo is
tagged on turn-start).
- interleaveTurnMetrics attributes every group to its turn (turnId, else the
turn owning its stepId) and puts the turn total after the turn's LAST visible
group. No user-message counting: sending, streaming, steering, images, errored
turns and chat-limit unloading can no longer shift metrics between turns — at
any point during a turn, not just after it ends. A turn cut in half by the
chat limit keeps its total under its visible tail.
- Legacy rows (no turnId) fall back to an order-preserving, count-guarded match
that leaves a turn blank rather than show another turn's numbers.
- A turn that errors re-syncs metrics (the backend now persists them).
TPS
- Step TPS = outputTokens / decodeMs only (no genTotalMs fallback — it includes
time-to-first-token). Turn TPS sums tokens and decode time over timed steps
only. Tool execution is excluded by the backend.
Display
- Retention = this turn's first-step cache reads / previous turn's last-step
prompt (step-level; turn sums made it pin at 100%).
- Turn row shows output tokens + input processed (with cached), instead of a
misleading 'total tok' that summed every step's full prompt.
Diffstat (limited to 'src/adapters/idb')
| -rw-r--r-- | src/adapters/idb/index.test.ts | 10 | ||||
| -rw-r--r-- | src/adapters/idb/index.ts | 8 |
2 files changed, 17 insertions, 1 deletions
diff --git a/src/adapters/idb/index.test.ts b/src/adapters/idb/index.test.ts index c05c605..04e2177 100644 --- a/src/adapters/idb/index.test.ts +++ b/src/adapters/idb/index.test.ts @@ -30,6 +30,16 @@ describe("createIdbChunkStore", () => { expect(loaded[0]?.chunk).toEqual(textChunk("a")); }); + it("round-trips a chunk's turnId (and upserts it onto an older row)", async () => { + const store = createIdbChunkStore({ indexedDB: new IDBFactory() }); + await store.append("conv1", [makeChunk(1, "a"), makeChunk(2, "b")]); + expect((await store.load("conv1")).map((c) => c.turnId)).toEqual([undefined, undefined]); + await store.append("conv1", [{ ...makeChunk(1, "a"), turnId: "t1" }]); + const loaded = await store.load("conv1"); + expect(loaded.map((c) => c.turnId)).toEqual(["t1", undefined]); + expect("turnId" in (loaded[1] ?? {})).toBe(false); + }); + it("append out-of-order still loads seq-ordered", async () => { const store = createIdbChunkStore({ indexedDB: new IDBFactory() }); const chunks = [makeChunk(3, "c"), makeChunk(1, "a"), makeChunk(2, "b")]; diff --git a/src/adapters/idb/index.ts b/src/adapters/idb/index.ts index 96b2cbc..3dffef9 100644 --- a/src/adapters/idb/index.ts +++ b/src/adapters/idb/index.ts @@ -14,6 +14,7 @@ interface ChunkRecord { seq: number; role: StoredChunk["role"]; chunk: StoredChunk["chunk"]; + turnId?: string; } interface MetaRecord { @@ -70,7 +71,11 @@ function keyRangeFor(conversationId: string): IDBKeyRange { } function chunksToStoredChunks(records: ChunkRecord[]): StoredChunk[] { - return records.map((r) => ({ seq: r.seq, role: r.role, chunk: r.chunk })); + return records.map((r) => + typeof r.turnId === "string" + ? { seq: r.seq, role: r.role, chunk: r.chunk, turnId: r.turnId } + : { seq: r.seq, role: r.role, chunk: r.chunk }, + ); } export function createIdbChunkStore(opts?: CreateIdbChunkStoreOptions): ConversationChunkStore { @@ -113,6 +118,7 @@ export function createIdbChunkStore(opts?: CreateIdbChunkStoreOptions): Conversa seq: c.seq, role: c.role, chunk: c.chunk, + ...(c.turnId !== undefined ? { turnId: c.turnId } : {}), } satisfies ChunkRecord); } |
