# Code Quality Critical Fixes Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Fix three correctness-blocking findings from the 2026-05-21 code-quality review: subdir-tier aliasing in DictStore, four divergent SSE parsers, and unbounded per-session caches that leak SQLite handles.

**Architecture:** Three independent file sets — `src/dict/` (C1), `src/codec/` (C2 new canonical parser + 4-site migration), `src/proxy/` (C3 new SessionCache class + handler refactor). C2 lands first because Important spec Wave B depends on `sseParser` exports. C1 and C3 land in parallel.

**Tech Stack:** TypeScript strict, Bun runtime, `bun:sqlite`, TransformStream / ReadableStream pipelines, vitest for tests.

**Source spec:** `/home/user/Projects/fewtok/docs/superpowers/specs/code-quality-critical-design.md`

---

## Wave Plan

| Wave | Tasks | Files touched | Safe to parallelize? |
|------|-------|---------------|----------------------|
| 1 | Task 1 (C1 paths helpers) | `src/dict/paths.ts` | single task |
| 1 | Task 2 (C2 sseParser module) | `src/codec/sseParser.ts` (new) | ✅ no overlap with Task 1 |
| 1 | Task 3 (C3 SessionCache module) | `src/proxy/sessionCache.ts` (new) | ✅ no overlap with Task 1/2 |
| 2 | Task 4 (C1 fileFor branch) | `src/dict/DictStore.ts` | depends on Task 1 |
| 2 | Task 5 (C2 migrate rtkRouteStream) | `src/codec/rtkRouteStream.ts` | depends on Task 2 |
| 2 | Task 6 (C2 migrate KbResponseInterceptor) | `src/kb/KbResponseInterceptor.ts` | depends on Task 2 |
| 2 | Task 7 (C2 migrate sse-usage) | `src/proxy/sse-usage.ts` | depends on Task 2 |
| 3 | Task 8 (C2 migrate sseRewriteStream — boundary only) | `src/codec/sseRewriteStream.ts` | depends on Task 2; cross-spec: holds lock on this file through I2 + M3 |
| 3 | Task 9 (C3 wire SessionCache into handler) | `src/proxy/handler.ts` | depends on Task 3 |
| 4 | Task 10 (C3 dispose() bootstrap wiring) | `src/proxy/server.ts` | depends on Task 9 |

`src/codec/sseRewriteStream.ts` is touched by Task 8 (C2) and downstream by Important I2 + Minor M3. Serialization: ship Task 8 first, then I2 plan runs, then M3 plan runs. Never start I2 while Task 8 open.

---

## File Structure

**New files:**
- `src/dict/paths.ts` — extend with `subdirPaths(projectRoot)` and `subdirMacros(projectRoot)` helpers (file already exists; add two exports).
- `src/codec/sseParser.ts` — canonical SSE primitives: `SseEvent` interface, `splitEvents`, `parseEvent`, `encodeEvent`.
- `src/proxy/sessionCache.ts` — `SessionCache<V>` class: LRU + idle eviction + onEvict callback.

**Modified files:**
- `src/dict/DictStore.ts` — add `if (tier === "subdir")` branch in `fileFor`.
- `src/codec/rtkRouteStream.ts` — replace local `splitEvents/extractDataLine/readDataJson/encodeSseEvent` with imports from `sseParser`.
- `src/kb/KbResponseInterceptor.ts` — replace local `parseSseStream/encodeSseEvent` with `sseParser` imports.
- `src/proxy/sse-usage.ts` — replace local `parseSseFrame/dispatchFrame` boundary logic with `splitEvents/parseEvent`.
- `src/codec/sseRewriteStream.ts` — replace inline `indexOf("\n\n")` boundary scan with `splitEvents`; keep byte-level `rewriteSlice` + `pendingTail` sigil rewrite intact.
- `src/proxy/handler.ts` — replace three `Map` caches with `SessionCache`; refactor `buildFetchHandler` to return `{ handler, dispose }`.
- `src/proxy/server.ts` — wire `dispose()` into SIGHUP/SIGTERM lifecycle.

**Tests (created, NEVER committed — project rule):**
- `tests/codec/sseParser.test.ts`
- `tests/proxy/sessionCache.test.ts`
- `tests/dict/DictStore.subdir.test.ts`

---

## Pre-flight

Before any task, run:

```bash
git status
bun tsc --noEmit 2>&1 | head -30
```

`tsc` must be clean. If errors exist, stop and surface — do not commit on top of a broken baseline.

---

### Task 1: subdirPaths / subdirMacros helpers

**Wave:** 1
**Blocks:** Task 4
**Blocked by:** —

**Files:**
- Modify: `src/dict/paths.ts` (add two exports at end of file)
- Test: `tests/dict/paths.subdir.test.ts` (new, not committed)

- [ ] **Step 1: Read existing paths.ts to find pattern**

```bash
grep -n "export function" src/dict/paths.ts
```

Note the existing `projectPaths` / `projectMacros` / `globalPaths` / `globalMacros` signatures — mirror them exactly.

- [ ] **Step 2: Write failing test**

`tests/dict/paths.subdir.test.ts`:

```ts
import { describe, it, expect } from "bun:test";
import { subdirPaths, subdirMacros, projectPaths, projectMacros } from "../../src/dict/paths";

describe("subdir tier paths", () => {
  it("subdirPaths distinct from projectPaths", () => {
    expect(subdirPaths("/x")).not.toBe(projectPaths("/x"));
    expect(subdirPaths("/x")).toMatch(/subdir-path\.json$/);
  });
  it("subdirMacros distinct from projectMacros", () => {
    expect(subdirMacros("/x")).not.toBe(projectMacros("/x"));
    expect(subdirMacros("/x")).toMatch(/subdir-macro\.json$/);
  });
});
```

- [ ] **Step 3: Run test → expect FAIL**

```bash
bun test tests/dict/paths.subdir.test.ts
```

Expected: ReferenceError / module-not-found for `subdirPaths`.

- [ ] **Step 4: Implement helpers**

Append to `src/dict/paths.ts`:

```ts
export function subdirPaths(projectRoot: string): string {
  return join(projectRootDir(projectRoot), "subdir-path.json");
}

export function subdirMacros(projectRoot: string): string {
  return join(projectRootDir(projectRoot), "subdir-macro.json");
}
```

If `projectRootDir` isn't the existing helper name, use the same primitive `projectPaths` uses — read the file first to find it.

- [ ] **Step 5: Run test → expect PASS**

```bash
bun test tests/dict/paths.subdir.test.ts
bun tsc --noEmit 2>&1 | head -20
```

Both green.

- [ ] **Step 6: Commit (source only — never tests/)**

```bash
git add src/dict/paths.ts
git commit -m "feat(dict): add subdirPaths/subdirMacros helpers for subdir tier"
```

---

### Task 2: sseParser canonical module

**Wave:** 1
**Blocks:** Tasks 5, 6, 7, 8
**Blocked by:** —

**Files:**
- Create: `src/codec/sseParser.ts`
- Test: `tests/codec/sseParser.test.ts` (new, not committed)

- [ ] **Step 1: Write failing tests covering all edge cases from spec C2**

`tests/codec/sseParser.test.ts`:

```ts
import { describe, it, expect } from "bun:test";
import { splitEvents, parseEvent, encodeEvent } from "../../src/codec/sseParser";

describe("splitEvents", () => {
  it("splits at \\n\\n boundaries", () => {
    const r = splitEvents("event: a\ndata: 1\n\nevent: b\ndata: 2\n\n");
    expect(r.events.length).toBe(2);
    expect(r.remainder).toBe("");
  });

  it("preserves partial frame in remainder", () => {
    const r = splitEvents("event: foo\ndata: bar");
    expect(r.events).toEqual([]);
    expect(r.remainder).toBe("event: foo\ndata: bar");
  });

  it("normalizes \\r\\n to \\n", () => {
    const r = splitEvents("event: a\r\ndata: 1\r\n\r\n");
    expect(r.events.length).toBe(1);
  });
});

describe("parseEvent", () => {
  it("multi-line data joined by \\n", () => {
    const ev = parseEvent("data: a\ndata: b\n\n");
    expect(ev.dataRaw).toBe("a\nb");
    expect(ev.eventType).toBeNull();
  });

  it("[DONE] sentinel", () => {
    const ev = parseEvent("data: [DONE]\n\n");
    expect(ev.isDone).toBe(true);
    expect(ev.dataJson).toBeNull();
  });

  it("missing event: → eventType null, JSON parsed", () => {
    const ev = parseEvent('data: {"x":1}\n\n');
    expect(ev.eventType).toBeNull();
    expect(ev.dataJson).toEqual({ x: 1 });
  });

  it("event: + JSON data", () => {
    const ev = parseEvent('event: message_delta\ndata: {"v":2}\n\n');
    expect(ev.eventType).toBe("message_delta");
    expect(ev.dataJson).toEqual({ v: 2 });
  });

  it("invalid JSON → dataJson null, dataRaw preserved", () => {
    const ev = parseEvent("data: not-json\n\n");
    expect(ev.dataJson).toBeNull();
    expect(ev.dataRaw).toBe("not-json");
  });

  it("empty event yields empty fields", () => {
    const ev = parseEvent("\n\n");
    expect(ev.eventType).toBeNull();
    expect(ev.dataRaw).toBe("");
  });
});

describe("encodeEvent", () => {
  it("eventType + data round-trips", () => {
    const out = encodeEvent("message_delta", { v: 2 });
    expect(out).toContain("event: message_delta");
    expect(out).toContain('data: {"v":2}');
    expect(out.endsWith("\n\n")).toBe(true);
  });

  it("null eventType emits data-only", () => {
    const out = encodeEvent(null, { v: 2 });
    expect(out).not.toContain("event:");
    expect(out).toContain('data: {"v":2}');
  });
});

describe("streaming integrity", () => {
  it("1-byte slices reassemble identically to one-shot", () => {
    const full = 'event: a\ndata: {"x":1}\n\nevent: b\ndata: {"y":2}\n\n';
    let carry = "";
    const collected: string[] = [];
    for (const ch of full) {
      const r = splitEvents(carry + ch);
      collected.push(...r.events);
      carry = r.remainder;
    }
    expect(collected.length).toBe(2);
  });
});
```

- [ ] **Step 2: Run tests → expect FAIL**

```bash
bun test tests/codec/sseParser.test.ts
```

Expected: module not found.

- [ ] **Step 3: Implement sseParser.ts**

Create `src/codec/sseParser.ts`:

```ts
export interface SseEvent {
  eventType: string | null;
  dataRaw: string;
  isDone: boolean;
  dataJson: unknown | null;
  raw: string;
}

export function splitEvents(buf: string): { events: string[]; remainder: string } {
  const normalized = buf.replace(/\r\n/g, "\n");
  const parts = normalized.split("\n\n");
  const remainder = parts.pop() ?? "";
  const events = parts.map(p => p + "\n\n");
  return { events, remainder };
}

export function parseEvent(rawEvent: string): SseEvent {
  const lines = rawEvent.replace(/\n\n$/, "").split("\n");
  let eventType: string | null = null;
  const dataLines: string[] = [];
  for (const line of lines) {
    if (line.startsWith("event:")) {
      eventType = line.slice(6).trim();
    } else if (line.startsWith("data:")) {
      dataLines.push(line.slice(5).replace(/^ /, ""));
    }
  }
  const dataRaw = dataLines.join("\n");
  const isDone = dataRaw === "[DONE]";
  let dataJson: unknown | null = null;
  if (!isDone && dataRaw.length > 0) {
    try { dataJson = JSON.parse(dataRaw); } catch { dataJson = null; }
  }
  return { eventType, dataRaw, isDone, dataJson, raw: rawEvent };
}

export function encodeEvent(eventType: string | null, data: unknown): string {
  const lines: string[] = [];
  if (eventType !== null) lines.push(`event: ${eventType}`);
  const serialized = typeof data === "string" ? data : JSON.stringify(data);
  for (const dl of serialized.split("\n")) {
    lines.push(`data: ${dl}`);
  }
  return lines.join("\n") + "\n\n";
}
```

- [ ] **Step 4: Run tests + tsc → expect PASS**

```bash
bun test tests/codec/sseParser.test.ts
bun tsc --noEmit 2>&1 | head -20
```

All green.

- [ ] **Step 5: Commit**

```bash
git add src/codec/sseParser.ts
git commit -m "feat(codec): add canonical sseParser (splitEvents/parseEvent/encodeEvent)"
```

---

### Task 3: SessionCache module

**Wave:** 1
**Blocks:** Task 9
**Blocked by:** —

**Files:**
- Create: `src/proxy/sessionCache.ts`
- Test: `tests/proxy/sessionCache.test.ts` (new, not committed)

- [ ] **Step 1: Write failing tests**

`tests/proxy/sessionCache.test.ts`:

```ts
import { describe, it, expect, mock } from "bun:test";
import { SessionCache } from "../../src/proxy/sessionCache";

describe("SessionCache", () => {
  it("LRU evicts oldest beyond maxEntries", () => {
    const evicted: string[] = [];
    const c = new SessionCache<number>({
      maxEntries: 2, idleMs: 1e9, sweepIntervalMs: 0,
      onEvict: (k) => evicted.push(k),
    });
    c.set("a", 1); c.set("b", 2); c.set("c", 3);
    expect(c.size()).toBe(2);
    expect(evicted).toEqual(["a"]);
    c.dispose();
  });

  it("get bubbles entry to tail (LRU recency)", () => {
    const evicted: string[] = [];
    const c = new SessionCache<number>({
      maxEntries: 2, idleMs: 1e9, sweepIntervalMs: 0,
      onEvict: (k) => evicted.push(k),
    });
    c.set("a", 1); c.set("b", 2);
    expect(c.get("a")).toBe(1); // bubble a to tail
    c.set("c", 3);              // should evict b, not a
    expect(evicted).toEqual(["b"]);
    c.dispose();
  });

  it("manual sweep evicts idle entries", async () => {
    const evicted: string[] = [];
    const c = new SessionCache<number>({
      maxEntries: 100, idleMs: 10, sweepIntervalMs: 0,
      onEvict: (k) => evicted.push(k),
    });
    c.set("a", 1);
    await new Promise(r => setTimeout(r, 20));
    const n = c.sweep();
    expect(n).toBe(1);
    expect(evicted).toEqual(["a"]);
    c.dispose();
  });

  it("dispose fires onEvict for all and clears timer", () => {
    const evicted: string[] = [];
    const c = new SessionCache<number>({
      maxEntries: 100, idleMs: 1e9, sweepIntervalMs: 0,
      onEvict: (k) => evicted.push(k),
    });
    c.set("a", 1); c.set("b", 2);
    c.dispose();
    expect(evicted.sort()).toEqual(["a", "b"]);
    expect(c.size()).toBe(0);
  });

  it("onEvict exception isolated", () => {
    const evicted: string[] = [];
    const c = new SessionCache<number>({
      maxEntries: 2, idleMs: 1e9, sweepIntervalMs: 0,
      onEvict: (k) => { if (k === "a") throw new Error("boom"); evicted.push(k); },
    });
    c.set("a", 1); c.set("b", 2); c.set("c", 3); // evicts a, throws, must continue
    c.set("d", 4); // evicts b
    expect(evicted).toEqual(["b"]);
    c.dispose();
  });
});
```

- [ ] **Step 2: Run → FAIL**

```bash
bun test tests/proxy/sessionCache.test.ts
```

- [ ] **Step 3: Implement SessionCache**

Create `src/proxy/sessionCache.ts`:

```ts
export interface SessionCacheOpts<V> {
  maxEntries: number;
  idleMs: number;
  sweepIntervalMs: number;
  onEvict?(key: string, value: V): void;
}

interface Entry<V> { value: V; lastAccessed: number; }

export class SessionCache<V> {
  private readonly map = new Map<string, Entry<V>>();
  private readonly opts: SessionCacheOpts<V>;
  private timer: ReturnType<typeof setInterval> | null = null;

  constructor(opts: SessionCacheOpts<V>) {
    this.opts = opts;
    if (opts.sweepIntervalMs > 0) {
      this.timer = setInterval(() => this.sweep(), opts.sweepIntervalMs);
      // Bun: unref the timer so it doesn't pin event loop
      if (typeof (this.timer as { unref?: () => void }).unref === "function") {
        (this.timer as { unref: () => void }).unref();
      }
    }
  }

  get(key: string): V | undefined {
    const entry = this.map.get(key);
    if (entry === undefined) return undefined;
    entry.lastAccessed = Date.now();
    // Bubble to tail: delete + re-insert preserves Map insertion order = LRU recency.
    this.map.delete(key);
    this.map.set(key, entry);
    return entry.value;
  }

  set(key: string, value: V): void {
    if (this.map.has(key)) this.map.delete(key);
    this.map.set(key, { value, lastAccessed: Date.now() });
    while (this.map.size > this.opts.maxEntries) {
      const oldestKey = this.map.keys().next().value as string | undefined;
      if (oldestKey === undefined) break;
      this.evict(oldestKey);
    }
  }

  sweep(): number {
    const now = Date.now();
    const cutoff = now - this.opts.idleMs;
    let count = 0;
    for (const [k, e] of this.map) {
      if (e.lastAccessed < cutoff) { this.evict(k); count++; }
    }
    return count;
  }

  dispose(): void {
    if (this.timer !== null) { clearInterval(this.timer); this.timer = null; }
    for (const k of [...this.map.keys()]) this.evict(k);
  }

  size(): number { return this.map.size; }

  private evict(key: string): void {
    const entry = this.map.get(key);
    if (entry === undefined) return;
    this.map.delete(key);
    if (this.opts.onEvict !== undefined) {
      try { this.opts.onEvict(key, entry.value); }
      catch (err) { console.error(`[SessionCache] onEvict threw for ${key}:`, err); }
    }
  }
}
```

- [ ] **Step 4: Run tests + tsc → PASS**

```bash
bun test tests/proxy/sessionCache.test.ts
bun tsc --noEmit 2>&1 | head -20
```

- [ ] **Step 5: Commit**

```bash
git add src/proxy/sessionCache.ts
git commit -m "feat(proxy): add SessionCache (LRU + idle eviction + onEvict)"
```

---

### Task 4: DictStore.fileFor subdir branch

**Wave:** 2
**Blocks:** —
**Blocked by:** Task 1

**Files:**
- Modify: `src/dict/DictStore.ts` (production-mode branch in `fileFor`)
- Test: `tests/dict/DictStore.subdir.test.ts` (new, not committed)

- [ ] **Step 1: Audit dev machine for stray subdir entries (read-only)**

```bash
STRAY=$(ls ~/.fewtok/projects/*/project-path.json 2>/dev/null | while read f; do
  jq '[.aliases[]? | select(.tier == "subdir")] | length' "$f" 2>/dev/null
done | awk '{s+=$1} END {print s+0}')
echo "stray subdir entries: ${STRAY}"
```

Record `${STRAY}` for Step 9 commit body. If `${STRAY} > 0`, migration code required (Step 6 conditional). If `${STRAY} = 0`, skip migration.

- [ ] **Step 2: Write failing test**

`tests/dict/DictStore.subdir.test.ts`:

```ts
import { describe, it, expect } from "bun:test";
import { DictStore } from "../../src/dict/DictStore";

describe("DictStore.fileFor subdir tier", () => {
  it("subdir tier resolves to distinct file from project tier (production mode)", () => {
    const store = new DictStore({});
    const subdirPath = (store as unknown as { fileFor: (t: string, k: string, p: string) => string }).fileFor("subdir", "path", "/x");
    const projectPath = (store as unknown as { fileFor: (t: string, k: string, p: string) => string }).fileFor("project", "path", "/x");
    expect(subdirPath).not.toBe(projectPath);
    expect(subdirPath).toMatch(/subdir-path\.json$/);
  });

  it("subdir tier macro file distinct from project macro file", () => {
    const store = new DictStore({});
    const sub = (store as unknown as { fileFor: (t: string, k: string, p: string) => string }).fileFor("subdir", "macro", "/x");
    const proj = (store as unknown as { fileFor: (t: string, k: string, p: string) => string }).fileFor("project", "macro", "/x");
    expect(sub).not.toBe(proj);
    expect(sub).toMatch(/subdir-macro\.json$/);
  });
});
```

- [ ] **Step 3: Run → FAIL**

```bash
bun test tests/dict/DictStore.subdir.test.ts
```

Expected: subdir branch returns project path → assertion fails.

- [ ] **Step 4: Locate fileFor in DictStore.ts**

```bash
grep -n "fileFor\|subdir\|globalPaths\|projectPaths" src/dict/DictStore.ts | head -30
```

- [ ] **Step 5: Add subdir branch + new imports**

In `src/dict/DictStore.ts`, find the production-mode branch of `fileFor` (around line 262-269 per spec). Add the `subdir` import at the top of the file alongside existing `paths` imports, then insert the new branch:

```ts
// Imports — extend existing paths.* import line:
import { globalPaths, globalMacros, projectPaths, projectMacros, subdirPaths, subdirMacros } from "./paths";
```

Replace the production-mode branch body with:

```ts
if (tier === "global") return kind === "path" ? globalPaths() : globalMacros();
if (tier === "subdir") return kind === "path" ? subdirPaths(projectRoot) : subdirMacros(projectRoot);
return kind === "path" ? projectPaths(projectRoot) : projectMacros(projectRoot);
```

Verify self-contained mode (baseDir branch around line 247-248) already handles subdir; no change there.

- [ ] **Step 6: (Conditional) Migration if audit count > 0**

If Step 1 audit found stray `subdir` entries in `project-*.json`, create `src/dict/migrate-subdir-tier.ts`:

```ts
import { readFileSync, writeFileSync, existsSync } from "fs";
import { join } from "path";
import { homedir } from "os";

const SENTINEL = join(homedir(), ".fewtok", ".subdir-migrated");

export function migrateSubdirTierOnce(): void {
  if (existsSync(SENTINEL)) return;
  // Walk ~/.fewtok/projects/*/project-{path,macro}.json, move tier==subdir entries
  // into subdir-{path,macro}.json (create the file if absent).
  // … audit-driven code; only ship if audit found stray entries.
  writeFileSync(SENTINEL, new Date().toISOString());
}
```

Call from `DictStore` constructor before any read:

```ts
constructor(opts: DictStoreOpts) {
  migrateSubdirTierOnce();
  // … existing constructor body
}
```

Add unit test for migration in `tests/dict/migrate-subdir-tier.test.ts` (not committed).

If audit count = 0, skip Step 6 entirely.

- [ ] **Step 7: Run tests + tsc → PASS**

```bash
bun test tests/dict/DictStore.subdir.test.ts
bun test src/dict/ 2>&1 | tail -20
bun tsc --noEmit 2>&1 | head -20
```

- [ ] **Step 8: Integration check — GC walk writes 6 distinct files**

```bash
bun test tests/dict 2>&1 | grep -E "pruneBelowFreq|gcUnused" | head
```

Run any existing GC tests (`pruneBelowFreq`, `gcUnused`). Confirm no regression. If no test exists, write a one-off scratch script (not committed) that constructs a fixture DictStore with one entry per (tier, kind), runs `pruneBelowFreq`, and asserts 6 byte-distinct files via `stat`.

- [ ] **Step 9: Commit**

If migration code shipped, include audit count + sentinel path in commit body.

```bash
git add src/dict/DictStore.ts
# if migration shipped: git add src/dict/migrate-subdir-tier.ts
git commit -m "fix(dict): add subdir-tier branch in fileFor (was aliasing project files)

Closes C1 from code-quality-critical spec. Audit on dev machine
found ${STRAY} stray subdir entries in project-*.json (substitute the captured number)."
```

---

### Task 5: Migrate rtkRouteStream to sseParser

**Wave:** 2
**Blocks:** —
**Blocked by:** Task 2

**Files:**
- Modify: `src/codec/rtkRouteStream.ts`

- [ ] **Step 1: Locate duplicate primitives**

```bash
grep -n "splitEvents\|extractDataLine\|readDataJson\|encodeSseEvent" src/codec/rtkRouteStream.ts
```

- [ ] **Step 2: Replace imports**

Add at top of `src/codec/rtkRouteStream.ts`:

```ts
import { splitEvents, parseEvent, encodeEvent } from "./sseParser";
```

- [ ] **Step 3: Delete local copies**

Delete the local `splitEvents`, `extractDataLine`, `readDataJson`, `encodeSseEvent` function definitions in this file. Callers within the file:
- Replace `encodeSseEvent(type, data)` calls with `encodeEvent(type, data)`.
- Replace `extractDataLine(raw)` + `readDataJson(raw)` patterns with `parseEvent(raw).dataJson`.
- `splitEvents` is identical-named — just imported now.

- [ ] **Step 4: Run RtkRouteStream test suite**

```bash
bun test src/codec/tests/RtkRouteStream.test.ts
bun tsc --noEmit 2>&1 | head -20
```

PASS expected (behavior must not change — same wire output).

- [ ] **Step 5: Commit**

```bash
git add src/codec/rtkRouteStream.ts
git commit -m "refactor(codec): migrate rtkRouteStream to canonical sseParser"
```

---

### Task 6: Migrate KbResponseInterceptor to sseParser

**Wave:** 2
**Blocks:** —
**Blocked by:** Task 2

**Files:**
- Modify: `src/kb/KbResponseInterceptor.ts`

- [ ] **Step 1: Locate duplicate primitives**

```bash
grep -n "parseSseStream\|encodeSseEvent" src/kb/KbResponseInterceptor.ts
```

- [ ] **Step 2: Replace imports**

```ts
import { splitEvents, parseEvent, encodeEvent } from "../codec/sseParser";
```

- [ ] **Step 3: Delete local `parseSseStream` async generator**

Rewrite the caller. Where the file currently does `for await (const parsed of parseSseStream(reader))`, swap to a manual loop reading chunks + `splitEvents` + `parseEvent`:

```ts
const decoder = new TextDecoder();
let carry = "";
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  const { events, remainder } = splitEvents(carry + decoder.decode(value, { stream: true }));
  carry = remainder;
  for (const raw of events) {
    const ev = parseEvent(raw);
    // existing per-event logic …
  }
}
```

- [ ] **Step 4: Replace local encodeSseEvent**

The local `encodeSseEvent(parsed)` derived type from payload. Migrate to `encodeEvent(parsed.type ?? null, parsed)` at each call site.

- [ ] **Step 5: Delete the now-dead `encodeSseEvent` and `parseSseStream` function bodies**

- [ ] **Step 6: Run KB interceptor tests + tsc**

```bash
bun test src/kb/tests/KbResponseInterceptor.test.ts
bun tsc --noEmit 2>&1 | head -20
```

Both green. Behavior must be identical (output bytes unchanged).

- [ ] **Step 7: Commit**

```bash
git add src/kb/KbResponseInterceptor.ts
git commit -m "refactor(kb): migrate KbResponseInterceptor to canonical sseParser"
```

---

### Task 7: Migrate sse-usage to sseParser

**Wave:** 2
**Blocks:** —
**Blocked by:** Task 2

**Files:**
- Modify: `src/proxy/sse-usage.ts`

- [ ] **Step 1: Locate duplicate primitives**

```bash
grep -n "parseSseFrame\|dispatchFrame" src/proxy/sse-usage.ts
```

- [ ] **Step 2: Replace boundary detection + frame parsing**

Import:

```ts
import { splitEvents, parseEvent } from "../codec/sseParser";
```

In the wrapping TransformStream's `transform`, replace inline `\n\n` scanning with `splitEvents(carry + chunkStr)`. For each event, call `parseEvent(raw)`; pass `(parsed.eventType, parsed.dataRaw, parsed.dataJson)` to the existing `dispatchFrame` accumulator (keep `dispatchFrame` — it's the usage-accounting fold, not parsing).

- [ ] **Step 3: Delete local `parseSseFrame`**

`dispatchFrame` stays (different responsibility — usage accounting).

- [ ] **Step 4: Run sse-usage tests + tsc**

```bash
bun test src/proxy/tests/sse-usage.test.ts
bun tsc --noEmit 2>&1 | head -20
```

- [ ] **Step 5: Commit**

```bash
git add src/proxy/sse-usage.ts
git commit -m "refactor(proxy): migrate sse-usage boundary parsing to sseParser"
```

---

### Task 8: Migrate sseRewriteStream boundary detection only

**Wave:** 3
**Blocks:** —
**Blocked by:** Task 2

**CROSS-SPEC FILE LOCK:** This file (`src/codec/sseRewriteStream.ts`) is also touched by Important I2 and Minor M3. **Ship Task 8 first.** Do not start I2 or M3 plans until Task 8 commit is on `main`.

**Files:**
- Modify: `src/codec/sseRewriteStream.ts` (boundary detection only — keep byte-level sigil rewrite intact)

- [ ] **Step 1: Read the file**

```bash
bun -e "console.log(require('fs').readFileSync('src/codec/sseRewriteStream.ts','utf8').length, 'bytes')"
```

Locate (a) the inline `indexOf("\n\n")` scan that finds event boundaries, (b) `rewriteSlice` byte-level char-by-char regex match, (c) `carryStr` + `pendingTail` state.

- [ ] **Step 2: Import sseParser**

```ts
import { splitEvents } from "./sseParser";
```

- [ ] **Step 3: Replace boundary detection ONLY**

Where the code does inline `indexOf("\n\n")` + slicing, replace with:

```ts
const { events, remainder } = splitEvents(carryStr + chunkStr);
carryStr = remainder;
for (const eventRaw of events) {
  const rewritten = rewriteSlice(eventRaw, /* existing args */);
  controller.enqueue(encoder.encode(rewritten));
}
```

**Do NOT touch:** `rewriteSlice` body, `pendingTail` per-event-index logic, sigil regex compilation, sigil substitution character-by-character.

- [ ] **Step 4: Run sseRewriteStream test suite**

```bash
bun test src/codec 2>&1 | tail -30
bun tsc --noEmit 2>&1 | head -20
```

Output bytes through the stream must be identical to pre-change for the same input. If not, the boundary swap diverged — fix the swap, not the test.

- [ ] **Step 5: Defer E2E smoke**

Worktree policy: no live-proxy run. This task's verification is `bun tsc --noEmit` + `bun test src/codec` (Step 4). Live `cld-test` round-trip moves to final-verification phase under user approval.

- [ ] **Step 6: Commit**

```bash
git add src/codec/sseRewriteStream.ts
git commit -m "refactor(codec): migrate sseRewriteStream boundary detection to sseParser

Keeps byte-level rewriteSlice + pendingTail sigil rewrite intact. Only
indexOf(\"\\n\\n\") boundary scan migrates; the byte-level path remains
because the canonical parser doesn't model char-by-char sigil walk."
```

- [ ] **Step 7: Defer dist rebuild + hot-swap**

Worktree policy: no live-proxy hot-swap. Dist rebuild + `kill -USR1` + live `cld-test` move to final-verification phase under user approval. Do NOT signal the running `ft start` process.

---

### Task 9: Wire SessionCache into handler.ts

**Wave:** 3
**Blocks:** Task 10
**Blocked by:** Task 3

**Files:**
- Modify: `src/proxy/handler.ts`

- [ ] **Step 1: Read current cache definitions**

```bash
grep -n "resultIndexerCache\|kbStoreCache\|readCacheLayerMap\|buildFetchHandler\|new Map" src/proxy/handler.ts
```

Spec reference: lines 110-114 hold the three `new Map<…>()` instances.

- [ ] **Step 2: Import SessionCache and KbStore.close type**

```ts
import { SessionCache } from "./sessionCache";
```

- [ ] **Step 3: Replace the three Maps**

Replace:

```ts
const resultIndexerCache = new Map<string, ReturnType<typeof makeResultIndexerLayer>>();
const kbStoreCache = new Map<string, KbStore>();
const readCacheLayerMap = new Map<string, Layer>();
```

With:

```ts
const resultIndexerCache = new SessionCache<ReturnType<typeof makeResultIndexerLayer>>({
  maxEntries: 1000,
  idleMs: 60 * 60 * 1000,
  sweepIntervalMs: 5 * 60 * 1000,
});
const kbStoreCache = new SessionCache<KbStore>({
  maxEntries: 1000,
  idleMs: 60 * 60 * 1000,
  sweepIntervalMs: 5 * 60 * 1000,
  onEvict: (_k, store) => { store.close(); }, // KbStore.close is idempotent
});
const readCacheLayerMap = new SessionCache<Layer>({
  maxEntries: 1000,
  idleMs: 60 * 60 * 1000,
  sweepIntervalMs: 5 * 60 * 1000,
});
```

- [ ] **Step 4: Update all `.get(k)` / `.set(k, v)` / `.has(k)` call sites**

`SessionCache` exposes `get`, `set`, `size`, `dispose`, `sweep`. `.has(k)` callers convert to `cache.get(k) !== undefined`. Audit for any `Map.delete(k)`, `Map.clear()`, `Map.entries()` calls and adapt — `SessionCache` does not expose these; if any caller needs them, it's a smell, surface in the PR.

- [ ] **Step 5: Refactor buildFetchHandler return shape**

Current:

```ts
export function buildFetchHandler(opts: …): (req: Request) => Promise<Response> {
  // … set up the three caches …
  return async (req) => { /* … */ };
}
```

After:

```ts
export interface FetchHandlerBundle {
  handler: (req: Request) => Promise<Response>;
  dispose(): void;
}

export function buildFetchHandler(opts: …): FetchHandlerBundle {
  // … set up the three caches …
  const handler = async (req: Request) => { /* … */ };
  const dispose = () => {
    resultIndexerCache.dispose();
    kbStoreCache.dispose();
    readCacheLayerMap.dispose();
  };
  return { handler, dispose };
}
```

- [ ] **Step 6: Update callers in tests + server**

```bash
grep -rn "buildFetchHandler(" src/ tests/
```

Each call site that uses the returned function destructures: `const { handler, dispose } = buildFetchHandler(opts)`. Tests adapt the same way.

- [ ] **Step 7: Run tsc + proxy unit tests**

```bash
bun tsc --noEmit 2>&1 | head -30
bun test src/proxy 2>&1 | tail -20
```

- [ ] **Step 8: Commit**

```bash
git add src/proxy/handler.ts
git commit -m "feat(proxy): bound session caches via SessionCache; expose dispose

Replaces three unbounded Maps in handler.ts with SessionCache (LRU
1000 entries + 1h idle + 5min sweep). KbStore.close() wired into
onEvict. buildFetchHandler now returns { handler, dispose } so the
server can release SQLite handles on SIGHUP/SIGTERM."
```

---

### Task 10: Wire dispose into server lifecycle

**Wave:** 4
**Blocks:** —
**Blocked by:** Task 9

**Files:**
- Modify: `src/proxy/server.ts`

- [ ] **Step 1: Locate buildFetchHandler usage in server.ts**

```bash
grep -n "buildFetchHandler\|SIGTERM\|SIGHUP\|process.on" src/proxy/server.ts
```

- [ ] **Step 2: Capture dispose, attach to signal handlers**

Replace:

```ts
const handler = buildFetchHandler(opts);
```

With:

```ts
let bundle = buildFetchHandler(opts);

const reload = () => {
  bundle.dispose();
  bundle = buildFetchHandler(opts);
};

const shutdown = () => {
  bundle.dispose();
  process.exit(0);
};

process.on("SIGHUP", reload);
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
```

Routes that previously read `handler` now read `bundle.handler`.

- [ ] **Step 3: Run tsc + server tests if any**

```bash
bun tsc --noEmit 2>&1 | head -30
```

- [ ] **Step 4: Defer E2E lifecycle smoke**

Worktree policy: no scratch proxy spawn. Lifecycle smoke (SIGHUP/SIGTERM behavior, `lsof` handle audit) moves to final-verification phase under user approval. This task's verification is tsc clean only (Step 3).

- [ ] **Step 5: Commit**

```bash
git add src/proxy/server.ts
git commit -m "feat(proxy): dispose session caches on SIGHUP/SIGTERM"
```

---

## Cross-Spec Handoff

After Task 8 commits:
- Important Wave B (I1, I2) is unblocked — start `Docs/plans/2026-05-21-code-quality-important.md`.

After this whole plan commits:
- Minor M3 (sseRewriteStream comment) can run — last writer on this file.

## Self-Review Summary

- **Spec coverage:** C1 → Tasks 1, 4. C2 → Tasks 2, 5, 6, 7, 8. C3 → Tasks 3, 9, 10. All sections covered.
- **Placeholders:** none. All commands and code blocks are concrete.
- **Type consistency:** `SessionCache`, `SessionCacheOpts`, `FetchHandlerBundle`, `SseEvent`, `splitEvents`, `parseEvent`, `encodeEvent` — signatures consistent across tasks.
- **Wave plan check:** Task 4 (DictStore) blocked by Task 1 (paths). Tasks 5-8 blocked by Task 2 (sseParser). Task 9 blocked by Task 3 (SessionCache). Task 10 blocked by Task 9. No same-wave file overlap.
- **Cross-spec:** `src/codec/sseRewriteStream.ts` lock noted on Task 8. `src/proxy/handler.ts` lock relevant to Minor M1 + Important I8 — note carried into those plans.
