# v1.7 Deferred Queue Implementation Plan

> **STATUS: DEFERRED 2026-05-06** — superseded by `2026-05-06-v1.7-foundations.md`.
>
> This plan was rejected after `/plan-eng-review` + `/codex review --model gpt-5.5` found the underlying recall surface is too thin to bolt the four tracks onto:
> - **Track 1 (CLI parity)** assumed CLI `SearchResult` and API `RecallResultItem` are interchangeable. They are not. CLI uses `{entry, score, bm25, cosine, tokens, breakdown}` and downstream calls `markRetrieved(results.map(r=>r.entry))` + `explainMatch(query, r)`. CLI JSON output (`src/cli.ts:1348`) does not surface `isFreshTail / isSummary / substitutedFor / descendantCount`. `limit` semantics are also undefined: API can return `> limit`, CLI slices back. Prerequisite work needed before Track 1 is implementable.
> - **Track 2 (HTTP slashes)** is mostly churn — `ID_SEGMENT_RE` already rejects literal `/` and `rejectEncodedSlash` already rejects `%2F` pre-routing. The proposed `matchPathOrExtra` also misses the headline `/v1/sessions/foo/bar/assemble` case (extra segment is BEFORE the static suffix, not after). Drop.
> - **Track 3 (mean-of-children re-rank)** assumed `MemoryEntry.bm25_score` exists. It does not — `loadSearchEntries` returns plain `MemoryEntry[]`, `MEMORY_SELECT_COLUMNS` (src/store.ts:189) does not select `bm25(memories_fts)`. The formula is unimplementable until BM25 plumbing lands. Defer to v1.8.
> - **Track 4.3 (audit handle reuse)** is a real perf refactor (~5 helper rewrites, not 1). Out of release-debt scope.
> - **Track 4.1 (localeCompare → byte cmp)** and **Track 4.2 (tenant-wide warn)** survive — see foundations plan.
>
> Foundations plan: `docs/plans/2026-05-06-v1.7-foundations.md`. Reopen this plan ONLY after foundations land.
>
> **Keep below for archaeology — do NOT execute.**

---

**Goal:** Close the v1.6.x review-chain debt by wiring CLI `hippo recall` parity with `api.recall`, hardening the HTTP path matcher against slashes in `:id` segments, replacing the hardcoded `score: 0.5` for substituted summaries with a real re-rank, and tidying three small polish items (localeCompare, tenant-wide fresh-tail default, audit-handle reuse).

**Architecture:** All work is incremental on the existing `src/api.ts` `recall` / `assemble` surface plus `src/cli.ts` `cmdRecall` and `src/server.ts` `matchPath`. No schema migration. No new public API. Every behaviour change is opt-in via a flag, env-gated, or marked breaking with a CHANGELOG entry. We do NOT introduce a new `cmdRecallV2` — we route `cmdRecall` through `api.recall` for the new flags only when `--fresh-tail` or `--summarize-overflow` is passed, preserving the existing physics/hybrid/multihop default path bit-for-bit.

**Tech Stack:** TypeScript, Node 24, `node:sqlite`, vitest. Real DB for tests (project rule). No new deps.

**Reviewer chain (mandatory before publish):** `/codex review --base v1.6.4` + `/review` + `/self-review` + `/ship-check`. Skipping any of these is what created the v1.6.1-v1.6.3 retro debt; do not skip.

---

## Pre-flight

### Task 0: Set up worktree and baseline

**Files:** none

**Step 1: Confirm clean working tree on main, on tag v1.6.4**

Run: `git status && git describe --tags`
Expected: `working tree clean`, `v1.6.4` (or one commit past).

**Step 2: Cut feature branch**

Run: `git checkout -b feat/v1.7-deferred-queue`

**Step 3: Confirm test baseline**

Run: `npm test 2>&1 | tail -5`
Expected: `1337 passed` (matches v1.6.4 CHANGELOG).

**Step 4: Confirm CLI builds and self-tests**

Run: `npm run build && node dist/cli.js --version`
Expected: `1.6.4`

---

## Track 1 — CLI fresh-tail / summarize-overflow parity

**Why:** v1.6.0 + v1.6.2 + v1.6.3 wired `freshTailCount`, `freshTailSessionId`, and `summarizeOverflow` into `api.recall`, MCP `hippo_recall`, and HTTP `/v1/recall`. The CLI's `cmdRecall` (`src/cli.ts:758`) bypasses `api.recall` and calls `physicsSearch` / `hybridSearch` / `multihopSearch` directly, so users running `hippo recall "..." --fresh-tail 5` get nothing.

**Design constraint:** Do NOT regress the existing physics/hybrid/multihop default path. Route through `api.recall` ONLY when one of the new flags is passed.

### Task 1.1: Write failing test — `--fresh-tail N` surfaces last N raw rows

**Files:**
- Test: `tests/cli-recall-v17-fresh-tail.test.ts` (create)

**Step 1: Write the failing test**

```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';

const CLI = join(process.cwd(), 'dist/cli.js');

describe('cli recall --fresh-tail (v1.7)', () => {
  let root: string;
  beforeEach(() => {
    root = mkdtempSync(join(tmpdir(), 'hippo-v17-'));
    spawnSync('node', [CLI, 'init'], { env: { ...process.env, HIPPO_HOME: root } });
  });
  afterEach(() => rmSync(root, { recursive: true, force: true }));

  it('surfaces last N raw rows even when query does not match', () => {
    const env = { ...process.env, HIPPO_HOME: root, HIPPO_SESSION_ID: 'sess-A' };
    spawnSync('node', [CLI, 'remember', 'totally unrelated content alpha'], { env });
    spawnSync('node', [CLI, 'remember', 'totally unrelated content beta'], { env });
    spawnSync('node', [CLI, 'remember', 'totally unrelated content gamma'], { env });

    const r = spawnSync(
      'node',
      [CLI, 'recall', 'XYZ_NO_MATCH', '--fresh-tail', '2', '--json'],
      { env, encoding: 'utf8' },
    );
    const out = JSON.parse(r.stdout);
    const fresh = out.results.filter((x: { isFreshTail?: boolean }) => x.isFreshTail);
    expect(fresh.length).toBe(2);
    expect(fresh.map((x: { content: string }) => x.content)).toContain(
      'totally unrelated content gamma',
    );
  });
});
```

**Step 2: Run test to verify it fails**

Run: `npx vitest run tests/cli-recall-v17-fresh-tail.test.ts`
Expected: FAIL — `fresh.length` is 0 because `cmdRecall` ignores `--fresh-tail`.

**Step 3: Commit failing test**

```bash
git add tests/cli-recall-v17-fresh-tail.test.ts
git commit -m "test: failing case for cli recall --fresh-tail (v1.7)"
```

### Task 1.2: Wire `--fresh-tail`, `--fresh-tail-session`, `--summarize-overflow` flags into cmdRecall

**Files:**
- Modify: `src/cli.ts:758-870` (`cmdRecall`)

**Step 1: Read the surrounding code**

Run: `sed -n '758,870p' src/cli.ts`

**Step 2: Add the new flag plumbing**

After the existing flag parses (around line 832, after `recallActiveScope`), insert:

```typescript
  const freshTailCount = flags['fresh-tail'] !== undefined
    ? Math.max(0, parseInt(String(flags['fresh-tail']), 10) || 0)
    : 0;
  const freshTailSessionId = typeof flags['fresh-tail-session'] === 'string'
    ? String(flags['fresh-tail-session']).trim() || undefined
    : (typeof process.env.HIPPO_SESSION_ID === 'string' && process.env.HIPPO_SESSION_ID.trim())
      ? process.env.HIPPO_SESSION_ID.trim()
      : undefined;
  const summarizeOverflow = flags['summarize-overflow'] !== undefined
    ? flags['summarize-overflow'] === true || String(flags['summarize-overflow']) === '1' || String(flags['summarize-overflow']) === 'true'
    : false;

  // v1.7: when any DAG-aware recall flag is set, route through api.recall so
  // fresh-tail / summary-substitution semantics match MCP + HTTP.
  // Otherwise keep the legacy physics/hybrid/multihop path.
  const useApiRecall = freshTailCount > 0 || summarizeOverflow;
```

**Step 3: Branch on `useApiRecall`**

Wrap the existing `if (useMultihop) ... else if (usePhysics && !hasGlobal) ... else if (hasGlobal) ... else { hybridSearch(...) }` block in `if (!useApiRecall) { ... } else { ... api.recall path ... }`.

In the `else` branch:

```typescript
    const ctx: Context = {
      hippoRoot,
      tenantId,
      actor: process.env.HIPPO_ACTOR,
    };
    const recallResult = recall(ctx, {
      query,
      limit,
      scope: recallActiveScope,
      summarizeOverflow,
      freshTailCount,
      freshTailSessionId,
    });
    results = recallResult.results.map((r) => ({
      id: r.id,
      content: r.content,
      score: r.score,
      layer: r.layer,
      strength: r.strength,
      isSummary: r.isSummary,
      substitutedFor: r.substitutedFor,
      descendantCount: r.descendantCount,
      isFreshTail: r.isFreshTail,
    }));
```

Add `import { recall, type Context } from './api.js';` at top of file if not already present (`grep -n "from './api" src/cli.ts`).

**Step 4: Update the JSON renderer to surface `isFreshTail`, `isSummary`, `substitutedFor`, `descendantCount`**

Find the `--json` output block (search `flags['json']` or `JSON.stringify` near the bottom of `cmdRecall`). Pass the fields through unchanged.

**Step 5: Run the failing test**

Run: `npm run build && npx vitest run tests/cli-recall-v17-fresh-tail.test.ts`
Expected: PASS.

**Step 6: Run the full suite to confirm no regression**

Run: `npm test 2>&1 | tail -5`
Expected: `1338 passed` (one new test).

**Step 7: Commit**

```bash
git add src/cli.ts tests/cli-recall-v17-fresh-tail.test.ts
git commit -m "feat(cli): wire --fresh-tail / --summarize-overflow into hippo recall (v1.7)"
```

### Task 1.3: Add boundary tests — clamp, env fallback, both flags together

**Files:**
- Modify: `tests/cli-recall-v17-fresh-tail.test.ts`

**Step 1: Add cases**

```typescript
  it('clamps --fresh-tail to 200', () => {
    // ... 250 inserts skipped for size; assert recallResult.results.filter(isFreshTail).length <= 200
  });

  it('falls back to HIPPO_SESSION_ID env when --fresh-tail-session omitted', () => {
    // env HIPPO_SESSION_ID=sess-A; insert under sess-A and sess-B; assert only sess-A rows surface
  });

  it('combines --fresh-tail with --summarize-overflow without dedup loss', () => {
    // insert N+M raws under one DAG parent; expect both fresh-tail and summary in results, no duplicate ids
  });
```

**Step 2: Run and pass**

Run: `npx vitest run tests/cli-recall-v17-fresh-tail.test.ts`
Expected: 4 passed.

**Step 3: Commit**

```bash
git add tests/cli-recall-v17-fresh-tail.test.ts
git commit -m "test: cli recall v1.7 boundary cases (clamp, env fallback, combined flags)"
```

### Task 1.4: Update `hippo recall --help` text and CHANGELOG draft

**Files:**
- Modify: `src/cli.ts` (the `recall` help block — search `case 'recall':` then walk back to the help string)
- Modify: `CHANGELOG.md` (prepend a `## 1.7.0 (unreleased)` section with the new flags)

**Step 1: Find help block**

Run: `grep -n "hippo recall" src/cli.ts | head -5`

**Step 2: Add three lines**

```
  --fresh-tail N             surface last N kind='raw' rows in this session (capped 200)
  --fresh-tail-session <id>  override session id for fresh-tail (default: $HIPPO_SESSION_ID)
  --summarize-overflow       substitute level-2 parent summaries for overflow leaves
```

**Step 3: CHANGELOG draft entry**

Prepend to `CHANGELOG.md`:

```markdown
## 1.7.0 (unreleased)

Closes the v1.6.x deferred queue: CLI fresh-tail / summarize-overflow parity, HTTP path matcher slash hardening, real summary re-rank, and three polish items.

### Added
- **CLI: `hippo recall --fresh-tail N [--fresh-tail-session <id>] [--summarize-overflow]`.** Brings CLI to parity with MCP `hippo_recall` and HTTP `/v1/recall`. Default behaviour unchanged when none of the new flags are set.

### Changed
- (TBD as later tasks land)
```

**Step 4: Build + test**

Run: `npm run build && node dist/cli.js recall --help | grep fresh-tail`
Expected: three lines visible.

**Step 5: Commit**

```bash
git add src/cli.ts CHANGELOG.md
git commit -m "docs: hippo recall --help + v1.7.0 CHANGELOG draft"
```

---

## Track 2 — HTTP path matcher: slashes in `:id`

**Why:** `matchPath` (`src/server.ts:211-227`) splits the path on `/` then maps each segment to a single param. A `sessionId` containing a literal `/` (legal in some external session id schemes) collides with the segment count and silently 404s, even though `validateIdSegment` would accept the decoded form. v1.6.4 added `rejectEncodedSlash` for `%2F`, so any client that URL-encodes the slash gets a 400. The remaining failure mode is a session id that does not contain `/` at the framework caller, but a user-supplied id that does — currently a confusing 404.

**Decision (Karpathy "simplicity first"):** Do not extend `matchPath` to glob-match slashes. Instead, document the constraint in the `sessionId` / `:id` charset (already enforced by `validateIdSegment`: `[A-Za-z0-9_:.-]`, which excludes `/`), and improve the 404 → 400 distinction so the caller sees "invalid id" instead of a silent miss.

### Task 2.1: Failing test — slash in id returns 400, not 404

**Files:**
- Test: `tests/http-v17-slash-in-id.test.ts` (create)

**Step 1: Write test**

```typescript
import { describe, it, expect } from 'vitest';
import { spawnHippoServer } from './helpers/server-fixture.js'; // existing helper
// hit GET /v1/sessions/sess%2Fwith-slash/assemble — expect 400 with body { error: 'invalid id segment' }
// hit GET /v1/recall/drill/sum_a/b — same
// hit GET /v1/memories/mem%2Fa%2Fb — same (already 400 from v1.6.4 rejectEncodedSlash; this just locks it in)
```

**Step 2: Run test**

Run: `npx vitest run tests/http-v17-slash-in-id.test.ts`
Expected: FAIL — currently returns 404 for the un-encoded slash form because `pathParts.length !== patternParts.length`.

**Step 3: Commit**

```bash
git add tests/http-v17-slash-in-id.test.ts
git commit -m "test: failing case for HTTP slash-in-id 400 vs 404 (v1.7)"
```

### Task 2.2: Distinguish "extra path segments under a `:id` route" from "no route"

**Files:**
- Modify: `src/server.ts:211-227` (matchPath) and the route-dispatch block (~line 537-696)

**Design:** Add a sibling helper `matchPathPrefix(pattern, path)` that returns `'extra-segments'` when `pathParts.length > patternParts.length` AND every non-`:id` prefix matches. The dispatch then maps `'extra-segments'` to a 400 with `{ error: 'invalid id segment: contains /' }`.

**Step 1: Implement**

```typescript
function matchPathOrExtra(
  pattern: string,
  path: string,
): Record<string, string> | 'extra-segments' | null {
  const patternParts = pattern.split('/');
  const pathParts = path.split('/');
  if (pathParts.length < patternParts.length) return null;

  const params: Record<string, string> = {};
  for (let i = 0; i < patternParts.length; i++) {
    const pp = patternParts[i]!;
    const ap = pathParts[i]!;
    if (pp.startsWith(':')) {
      if (ap.length === 0) return null;
      params[pp.slice(1)] = decodeURIComponent(ap);
    } else if (pp !== ap) {
      return null;
    }
  }
  if (pathParts.length > patternParts.length) {
    // Prefix matched but trailing segments exist. Caller can decide whether
    // this is "id contained /" (400) or "wrong route" (404).
    return 'extra-segments';
  }
  return params;
}
```

**Step 2: Update `/v1/sessions/:id/assemble`, `/v1/recall/drill/:id`, `/v1/memories/:id` dispatch**

For each, wrap the existing `matchPath` call:

```typescript
const m = matchPathOrExtra('/v1/sessions/:id/assemble', path);
if (m === 'extra-segments') {
  res.statusCode = 400;
  res.setHeader('Content-Type', 'application/json');
  res.end(JSON.stringify({ error: 'invalid id segment: contains /' }));
  return;
}
if (m && typeof m === 'object') {
  // ... existing handler ...
}
```

**Step 3: Run test, expect PASS**

Run: `npm run build && npx vitest run tests/http-v17-slash-in-id.test.ts`

**Step 4: Run full suite**

Run: `npm test 2>&1 | tail -5`
Expected: `1342 passed` (cumulative).

**Step 5: Commit**

```bash
git add src/server.ts tests/http-v17-slash-in-id.test.ts
git commit -m "fix(server): 400 instead of 404 when :id contains /"
```

---

## Track 3 — Real re-rank for substituted summaries (replace hardcoded 0.5)

**Why:** `src/api.ts:340` sets `score: 0.5` for level-2 summary substitutions. Mid-rank by accident, not by design — the comment says "so they don't outrank top-N strong matches but stay above lowest-rank leaves." That is fine for the `summarize_overflow=true && limit=10` case where baseSlice has 10 strong matches, but breaks when baseSlice is sparse (e.g. limit=20, only 5 BM25 matches): the summary outranks the trailing 0-score leaves *and* ranks above the 5th BM25 hit which has score ~0.05. The chosen value is also undocumented in the public API.

**Decision:** Score each substituted summary as the **mean BM25 score of its overflow children**, clamped to `[0.05, 0.95]` so it never ties with a perfect match nor disappears below 0. This is honest re-ranking — the summary's "relevance" is exactly the average relevance of what it subsumes.

### Task 3.1: Failing test

**Files:**
- Test: `tests/api-v17-summary-rerank.test.ts` (create)

**Step 1: Write test**

```typescript
// Setup: 12 leaves under one level-2 parent.
//   Leaves 0-4: contain query term, BM25 ~ 0.8, 0.6, 0.5, 0.4, 0.3
//   Leaves 5-11: do NOT contain query term, BM25 ~ 0
// recall(query, limit=5, summarizeOverflow=true)
// Expected base slice: leaves 0-4 (top 5)
// Overflow: leaves 5-11 (7 rows under the parent)
// Substituted summary score should be mean(BM25 of leaves 5-11) ≈ 0
// → clamped to floor 0.05, NOT the legacy 0.5.
```

**Step 2: Run, expect FAIL**

Run: `npx vitest run tests/api-v17-summary-rerank.test.ts`
Expected: FAIL — current score is `0.5`.

**Step 3: Commit**

```bash
git add tests/api-v17-summary-rerank.test.ts
git commit -m "test: failing case for summary substitution real re-rank"
```

### Task 3.2: Implement mean-of-children re-rank

**Files:**
- Modify: `src/api.ts:300-346` (substitution block + summaryRanked map)

**Step 1: Capture BM25 scores per overflow child**

The current code in `recall()` ranks `baseRanked` items by `1 - idx/limit` — that is positional, not BM25. We need the actual BM25 score. Look upstream: `loadSearchEntries` returns entries pre-sorted by BM25 + has the `bm25_score` column populated when `physics.enabled` is false. Confirm by reading `src/store.ts::loadSearchEntries`.

Run: `grep -n "bm25_score\|loadSearchEntries" src/store.ts | head -10`

**Step 2: Carry BM25 score on `MemoryEntry` (already present) into the overflow grouping**

In the `overflowByParent` map building, store the BM25 score alongside the entry id:

```typescript
const overflowByParent = new Map<string, { id: string; bm25: number }[]>();
for (const e of overflowed) {
  if (!e.dag_parent_id) continue;
  const arr = overflowByParent.get(e.dag_parent_id) ?? [];
  arr.push({ id: e.id, bm25: e.bm25_score ?? 0 });
  overflowByParent.set(e.dag_parent_id, arr);
}
```

**Step 3: Compute mean-clamped score**

```typescript
const summaryRanked: RecallResultItem[] = substituted.map((s) => {
  const childScores = (overflowByParent.get(s.entry.id) ?? []).map((c) => c.bm25);
  const mean = childScores.length
    ? childScores.reduce((a, b) => a + b, 0) / childScores.length
    : 0;
  const clamped = Math.max(0.05, Math.min(0.95, mean));
  return {
    id: s.entry.id,
    content: s.entry.content,
    score: clamped,
    layer: s.entry.layer,
    strength: s.entry.strength,
    isSummary: true,
    substitutedFor: s.childIds,
    descendantCount: s.entry.descendant_count ?? s.childIds.length,
  };
});
```

**Step 4: Run targeted test**

Run: `npx vitest run tests/api-v17-summary-rerank.test.ts`
Expected: PASS.

**Step 5: Run full suite — watch for surprises**

Run: `npm test 2>&1 | tail -5`
Expected: most pass; a small number of existing tests that asserted `score === 0.5` will now fail. **Open each failing test, decide whether to update the assertion (the new value is correct) or whether the test was load-bearing.** Likely candidates: `tests/dag-recall.test.ts`, `tests/dag-v163-review-patch.test.ts`. Do NOT bulk-replace; read each one.

**Step 6: Update legitimate assertions**

For each test that asserted `score === 0.5` purely as "not zero, not full", change to `expect(score).toBeGreaterThanOrEqual(0.05)` or to the actual computed value if the test was checking a specific input.

**Step 7: Re-run, expect green**

Run: `npm test 2>&1 | tail -5`

**Step 8: Commit**

```bash
git add src/api.ts tests/
git commit -m "feat(recall): summary substitution scored by mean BM25 of children, clamped [0.05, 0.95]"
```

### Task 3.3: Document the new rule in MEMORY_ENVELOPE.md

**Files:**
- Modify: `MEMORY_ENVELOPE.md` (or `docs/api/recall.md` if it exists — `grep -rn "summarizeOverflow\|substitutedFor" docs MEMORY_ENVELOPE.md 2>/dev/null | head`)

Add one paragraph under the substitution section describing the score formula and the clamp range. Two sentences, no more.

**Step:** Commit.

```bash
git add MEMORY_ENVELOPE.md
git commit -m "docs: document summary substitution score formula"
```

---

## Track 4 — Polish

### Task 4.1: `localeCompare` → byte compare on ISO timestamps

**Files:**
- Modify: `src/api.ts:675-676`

**Why:** ISO 8601 timestamps in our schema are always lexicographically sortable (UTC, fixed-width). `localeCompare` does locale-aware Unicode collation, which is ~50x slower than `<`/`>` byte compare and has no semantic benefit here. Hot path on `assemble`.

**Step 1: Replace**

```typescript
olderItems.sort((a, b) => (a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0));
tailItems.sort((a, b) => (a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0));
```

Or extract a helper `cmpIso(a, b)` if you prefer DRY.

**Step 2: Run assemble tests**

Run: `npx vitest run tests/dag-assemble`
Expected: all pass (no semantic change for ISO).

**Step 3: Commit**

```bash
git add src/api.ts
git commit -m "perf(assemble): byte compare ISO timestamps instead of localeCompare"
```

### Task 4.2: Deprecation note on tenant-wide `loadFreshRawMemories`

**Files:**
- Modify: `src/store.ts` (the `loadFreshRawMemories` JSDoc)

**Why:** v1.6.2 added the `sessionId` parameter; v1.6.3 retroactive review noted that callers omitting it get tenant-wide rows tagged `isFreshTail=true`, which is rarely what you want. Don't break back-compat now — just emit a `console.warn` once per process when the parameter is omitted, gated by an env flag so prod isn't noisy.

**Step 1: Add JSDoc + warn**

```typescript
/**
 * Load the most recent N kind='raw' rows for `tenantId`.
 *
 * @deprecated Calling without `sessionId` returns tenant-wide rows. This
 * is rarely the right shape for "what did I just see in this conversation".
 * Pass `sessionId` (typically `ctx.sessionId` or `process.env.HIPPO_SESSION_ID`)
 * for the session-scoped behaviour. The tenant-wide default will become
 * an error in v2.0; set `HIPPO_ALLOW_TENANT_WIDE_FRESH_TAIL=1` to silence
 * this warning in the meantime.
 */
let warnedTenantWideFreshTail = false;
export function loadFreshRawMemories(
  hippoRoot: string,
  count: number,
  tenantId: string,
  sessionId?: string,
): MemoryEntry[] {
  if (sessionId === undefined && !warnedTenantWideFreshTail
      && process.env.HIPPO_ALLOW_TENANT_WIDE_FRESH_TAIL !== '1') {
    warnedTenantWideFreshTail = true;
    console.warn(
      '[hippo] loadFreshRawMemories called without sessionId — returning tenant-wide rows. '
      + 'See deprecation note. Set HIPPO_ALLOW_TENANT_WIDE_FRESH_TAIL=1 to silence.'
    );
  }
  // ... existing body ...
}
```

**Step 2: Test the warn fires once**

Add to `tests/store-fresh-tail.test.ts` (or create one) a case asserting `console.warn` is called exactly once across 5 invocations without sessionId.

**Step 3: Confirm no internal caller is hitting the warn path**

Run: `grep -rn "loadFreshRawMemories" src/ | grep -v "src/store.ts"`
For each hit, confirm the call site passes `sessionId`. If not, fix the call site (do not just silence the warn — it's there for a reason).

**Step 4: Commit**

```bash
git add src/store.ts tests/store-fresh-tail.test.ts
git commit -m "feat(store): deprecation warn on tenant-wide loadFreshRawMemories"
```

### Task 4.3: Audit DB handle reuse in `recall`

**Files:**
- Modify: `src/api.ts:255-440` (`recall`)

**Why:** `recall` opens its own DB handle on line 398 just to emit the audit row, then closes it at function end. `loadSearchEntries` already opens one on line 257. Two handles per recall, two `PRAGMA` round trips. Single connection is sufficient — `node:sqlite` is single-threaded per handle anyway.

**Step 1: Refactor**

Open the DB once at the top of `recall()`, pass it explicitly into `loadSearchEntries`, `appendAuditEvent`, and any other helper. Use a `try { ... } finally { closeHippoDb(db); }` around the whole body.

**Step 2: Confirm no test breaks**

Run: `npm test 2>&1 | tail -5`

**Step 3: Micro-benchmark before/after**

Run: `node benchmarks/a1/p99-recall.ts > /tmp/v17-after.txt && cat /tmp/v17-after.txt | tail -10`
Compare against the `current p99 = 58.42ms` baseline in TODOS.md. Expected: small improvement (5-10ms). Document actual delta in the commit message.

**Step 4: Commit**

```bash
git add src/api.ts
git commit -m "perf(recall): reuse DB handle across loadSearchEntries + audit emit (-Xms p99)"
```

---

## Pre-publish — Mandatory review chain

### Task 5.1: Self-review

Run: `/self-review`
Read every diff hunk. Look for: dead code, comments that lie, scope creep, missed test coverage, hardcoded values that should be config, semantic drift between CHANGELOG and code.

### Task 5.2: Codex cross-model review

Run: `/codex review --base v1.6.4`
This caught real bugs in v1.6.2 and v1.6.3 that the senior `/review` missed. Don't skip.

### Task 5.3: Senior `/review`

Run: `/review`
This caught real bugs codex missed. Don't skip.

### Task 5.4: Address all P0 + load-bearing P1 findings

Treat findings as blocking unless explicitly deferred to v1.8 with a TODOS.md entry. Each deferral needs a one-line justification.

### Task 5.5: Ship-check

Run: `/ship-check`
Verify: tests green, build clean, CHANGELOG updated, version bump pending, docs aligned.

### Task 5.6: Bump to 1.7.0 and tag

```bash
npm version 1.7.0
git push origin feat/v1.7-deferred-queue --tags
```

Open PR. After merge, `npm publish`.

---

## Out of scope (deferred to v1.8 or later)

- B3 dlPFC follow-ups (sequential-learning adapter contract, vlPFC interference, `--no-propagate`, `enforceDepthCap` refactor).
- 24h soak harness as a real release gate.
- A4 lifecycle compliance work (RTBF, secret-scrubbing).
- A7 observability dashboard.
- Hybrid embedding ON-by-default (separate plan).

---

## Success criteria (the only metrics that matter)

1. `hippo recall "..." --fresh-tail 5 --json` surfaces 5 fresh-tail items in the result, identical shape to MCP/HTTP.
2. `GET /v1/sessions/sess/with-slash/assemble` returns **400** with `{ error: 'invalid id segment: contains /' }`, not 404.
3. Substituted summary scores fall in `[0.05, 0.95]` and equal mean BM25 of their children to 3dp.
4. Test count grows from 1337 to ~1350+, all passing on Windows + Linux CI.
5. Full review chain (`/codex` + `/review` + `/self-review` + `/ship-check`) ran with all P0/P1 addressed or explicitly deferred.
6. `p99-recall.ts` baseline does not regress; ideally shaves 3-10ms via Task 4.3.
