Skip to content

fix(wiki-worker): bound the session-events DB fallback#328

Merged
khustup2 merged 2 commits into
mainfrom
fix/wiki-worker-bounded-fallback
Jul 21, 2026
Merged

fix(wiki-worker): bound the session-events DB fallback#328
khustup2 merged 2 commits into
mainfrom
fix/wiki-worker-bounded-fallback

Conversation

@khustup2

@khustup2 khustup2 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Problem

The wiki summary workers fall back to a DB read whenever the local session-event cache is absent (session resumed on another machine, started before the cache feature, or TTL-evicted). That fallback was:

SELECT message, creation_date FROM sessions
WHERE path LIKE '%<sessionId>%' ORDER BY creation_date ASC   -- no LIMIT

— an unindexed full scan that materializes the entire fat message column. On a mega-session that's 87–105 MB of cold S3 I/O, ~30s per trigger, re-paid every ~50 events by the periodic trigger. Measured on org 1ad8b15e (Examen AI, plugin 0.7.13x) with a local pg-deeplake against the same S3 root: cold fetch of the 105 MB message column ≈ 30s; warm ≈ 0.35s; compute is not the cost. The pi/codex workers have no local cache at all, so they hit this on every run.

Fix

Bound the fallback: a cheap count(*) probe (reads only path/creation_date, never the fat column) gives the true total, then the newest WIKI_FALLBACK_MAX_ROWS (2000) rows are fetched with ORDER BY creation_date DESC LIMIT N and reversed to chronological. The summarizer only ever consumes the newest un-summarized rows anyway (capLinesByBytes discards the rest), so this changes nothing it feeds.

The offset math is the subtle part. prevOffset indexes the FULL history, so a plain slice(prevOffset) breaks on a bounded window. New pure helper newRowsFromWindow(window, total, prevOffset) maps it correctly — the last total - prevOffset rows, or the whole window when the session grew past it (those older-new rows are exactly what the byte cap drops anyway). The stamped offset high-water uses the true total, not the window length, so the next run doesn't regress. The local-cache path (full session) keeps its exact slice(prevOffset).

Applied to all five workers: base, hermes, pi, cursor, codex. Option 1 (the local event cache) already shipped in 82d3ae3e; this is the "don't re-scan the whole fat column on a cache miss" half.

Tests

  • New newRowsFromWindow unit suite (tests/shared/wiki-offset.test.ts) covering the full-window == plain-slice equivalence, bounded-window tail selection, session-grew-past-window, prevOffset 0, and copy-not-alias.
  • Every worker's test suite updated for the probe→bounded-fetch dispatch pair and DESC ordering; a count(*)/DESC LIMIT 2000/never ASC assertion added on the fallback.
  • Green: typecheck; all six worker suites + the offset suite. Verified zero net-new failures vs main (the 19 pre-existing failures are unrelated capture/plugin-version tests).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Performance

    • Wiki summarization now uses bounded “recent events” DB fallback (with a count probe) to avoid loading full session histories.
    • Preserves accurate JSONL offset progress while limiting fetched rows to the newest window.
  • Bug Fixes

    • Improved correctness when local session cache is missing/incomplete by refetching and resetting offset inputs.
    • Fixed “new since last summary” selection during bounded DB fallback to prevent missed or duplicate updates.
  • Tests

    • Expanded mocks and added coverage for bounded fetching, retry/count-probe flow, offset math, and cache recovery across multiple workers.

The wiki summary workers fall back to a DB read whenever the local
session-event cache is absent (session resumed on another machine,
started before the cache feature, or TTL-evicted). That fallback was an
unbounded `SELECT message, creation_date FROM sessions WHERE path LIKE
'%<sid>%' ORDER BY creation_date ASC` — an unindexed full scan that
materializes the ENTIRE fat `message` column. On a mega-session that is
tens of MB (measured 87–105 MB) of cold S3 I/O, ~30s per trigger, and the
periodic trigger re-pays it every ~50 events. The pi/codex workers have
no local cache at all, so they hit this on every run.

Bound it: a cheap `count(*)` probe (reads no fat column) gives the true
total, then the newest `WIKI_FALLBACK_MAX_ROWS` rows are fetched with
`ORDER BY creation_date DESC LIMIT N` and reversed to chronological. The
summarizer only ever consumes the newest un-summarized rows anyway
(`capLinesByBytes` discards the rest), so this changes nothing it feeds.

The offset math is the subtle part: `prevOffset` indexes the FULL
history, so a plain `slice(prevOffset)` breaks on a bounded window. New
pure helper `newRowsFromWindow(window, total, prevOffset)` maps it
correctly (last `total - prevOffset` rows; whole window when the session
grew past it — those older-new rows are what the byte cap drops anyway).
The stamped offset high-water uses the true `total`, not the window
length, so the next run doesn't regress. The local-cache path (full
session) keeps its exact `slice(prevOffset)`.

Applied to all five workers (base, hermes, pi, cursor, codex). Option-1
(the local event cache) already shipped; this is the "don't re-scan the
whole fat column on a cache miss" half.
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Wiki workers now count session events separately, fetch only the newest 2,000 database rows, reverse them chronologically, and preserve global offsets. Shared offset utilities map persisted offsets onto bounded windows, while incomplete cache paths refetch from the database.

Changes

Bounded wiki event processing

Layer / File(s) Summary
Windowed offset contract
src/hooks/wiki-offset.ts, tests/shared/wiki-offset.test.ts
Adds WIKI_FALLBACK_MAX_ROWS and newRowsFromWindow, with tests covering bounded-window offset slicing and returned-array copying.
Bounded worker database fetches
src/hooks/codex/wiki-worker.ts, src/hooks/pi/wiki-worker.ts, src/hooks/wiki-worker.ts, tests/claude-code/*, tests/codex/*, tests/pi/*
Workers count matching events, fetch newest rows with LIMIT 2000, reverse them chronologically, use true totals for offsets, and update SQL mocks for the new query flow.
Cache-aware fallback recovery
src/hooks/cursor/wiki-worker.ts, src/hooks/hermes/wiki-worker.ts, src/hooks/wiki-worker.ts, tests/cursor/*, tests/hermes/*, tests/claude-code/wiki-worker-local-cache.test.ts
Incomplete local caches trigger bounded database refetches, while cache and database paths derive new rows using their respective offset logic and expanded tests cover error and retry paths.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WikiWorker
  participant SessionsDB
  participant OffsetHelper
  participant SummaryProcess
  WikiWorker->>SessionsDB: count matching session events
  SessionsDB-->>WikiWorker: total event count
  WikiWorker->>SessionsDB: fetch newest 2000 events
  SessionsDB-->>WikiWorker: newest-first event window
  WikiWorker->>OffsetHelper: compute rows after persisted offset
  OffsetHelper-->>WikiWorker: chronological new rows
  WikiWorker->>SummaryProcess: generate summary input
Loading

Possibly related PRs

Suggested reviewers: efenocchi, kaghni

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it does not use the required Summary, Version Bump, and Test plan sections. Rewrite it to match the template, adding Summary, Version Bump, and a Test plan with checkbox items.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately captures the main change: bounding the wiki-worker DB fallback.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/wiki-worker-bounded-fallback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Scope: files changed in this PR. Enforced threshold: 90% per metric (per file via vitest.config.ts).

Status Category Percentage Covered / Total
🟢 Lines 98.50% (🎯 90%) 655 / 665
🟢 Statements 97.94% (🎯 90%) 713 / 728
🟢 Functions 100.00% (🎯 90%) 67 / 67
🟢 Branches 93.40% (🎯 90%) 396 / 424
File Coverage — 6 files changed
File Stmts Branches Functions Lines
src/hooks/codex/wiki-worker.ts 🟢 99.3% 🟢 96.6% 🟢 100.0% 🟢 99.2%
src/hooks/cursor/wiki-worker.ts 🟢 98.6% 🟢 92.6% 🟢 100.0% 🟢 99.2%
src/hooks/hermes/wiki-worker.ts 🟢 98.6% 🟢 92.6% 🟢 100.0% 🟢 99.2%
src/hooks/pi/wiki-worker.ts 🟢 98.3% 🟢 93.7% 🟢 100.0% 🟢 99.1%
src/hooks/wiki-offset.ts 🟢 97.5% 🟢 90.0% 🟢 100.0% 🟢 100.0%
src/hooks/wiki-worker.ts 🟢 95.5% 🟢 92.3% 🟢 100.0% 🟢 95.8%

Generated for commit c338f2c.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
tests/shared/wiki-offset.test.ts (1)

78-116: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Solid coverage; consider adding a "stale total" regression case.

All existing cases check out against the implementation. One gap: none of these cases cover total < window.length (window "ahead of" total) — the exact shape produced when a caller's count(*) probe races with its row fetch (see the dbFetch finding in src/hooks/cursor/wiki-worker.ts/src/hooks/hermes/wiki-worker.ts). Locking in the expected behavior here would make the contract explicit for that scenario.

✅ Suggested additional case
+  it("total is stale/behind the window (window.length > total) — does not silently drop rows", () => {
+    // Simulates a count(*) probe racing with the row fetch: the window already
+    // contains more rows than the earlier count reported.
+    const w = rows(115); // full session fetched at SELECT time
+    const out = newRowsFromWindow(w, /* stale */ 110, 100);
+    // Expect all rows from prevOffset onward, not just (total - prevOffset).
+    expect(out).toEqual(w.slice(100));
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/shared/wiki-offset.test.ts` around lines 78 - 116, Add a regression
test in the newRowsFromWindow test suite covering a stale total where total is
less than the fetched window length, matching the race between count probing and
row fetching in dbFetch. Assert the expected offset behavior and returned rows
for that scenario, making the contract explicit without changing the
implementation.
tests/cursor/cursor-wiki-worker.test.ts (1)

229-234: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer a specific-value assertion over separate substring checks.

The three checks here (toContain("ORDER BY creation_date DESC"), toContain("LIMIT 2000"), includes("ORDER BY creation_date ASC")) are generic substring matches. As per path instructions, tests should prefer asserting on specific values over generic substrings. A single regex anchored on the trailing clause would pin down the exact expected SQL shape while still tolerating the dynamic escaped session-id prefix.

♻️ Suggested tightening
-    expect(sqls.some(s => s.startsWith("SELECT count(*) AS n"))).toBe(true);
-    const fetchSql = sqls.find(s => s.startsWith("SELECT message, creation_date"))!;
-    expect(fetchSql).toContain("ORDER BY creation_date DESC");
-    expect(fetchSql).toContain("LIMIT 2000");
-    expect(sqls.some(s => s.includes("ORDER BY creation_date ASC"))).toBe(false);
+    expect(sqls.some(s => s.startsWith("SELECT count(*) AS n"))).toBe(true);
+    const fetchSql = sqls.find(s => s.startsWith("SELECT message, creation_date"))!;
+    expect(fetchSql).toMatch(/ORDER BY creation_date DESC LIMIT 2000$/);
+    expect(sqls.some(s => s.includes("ORDER BY creation_date ASC"))).toBe(false);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/cursor/cursor-wiki-worker.test.ts` around lines 229 - 234, In the
cursor SQL assertions, replace the separate generic checks on fetchSql and the
ASC absence check with one regex-based assertion anchored to the trailing ORDER
BY creation_date DESC LIMIT 2000 clause. Preserve the existing count-query
assertion and allow the dynamic escaped session-id prefix while enforcing the
exact expected SQL shape.

Source: Path instructions

tests/hermes/hermes-wiki-worker.test.ts (1)

234-240: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the exact SQL string instead of substrings.

These new assertions use startsWith/toContain/includes substring checks on the SQL text. As per path instructions for tests/**, prefer asserting on specific values over generic substrings.

✅ Suggested tightening
-    expect(sqls.some(s => s.startsWith("SELECT count(*) AS n"))).toBe(true);
-    const fetchSql = sqls.find(s => s.startsWith("SELECT message, creation_date"))!;
-    expect(fetchSql).toContain("ORDER BY creation_date DESC");
-    expect(fetchSql).toContain("LIMIT 2000");
-    expect(sqls.some(s => s.includes("ORDER BY creation_date ASC"))).toBe(false);
+    expect(sqls).toContainEqual(expect.stringContaining("SELECT count(*) AS n"));
+    const fetchSql = sqls.find(s => s.startsWith("SELECT message, creation_date"))!;
+    expect(fetchSql).toBe(
+      `SELECT message, creation_date FROM "${cfg.sessionsTable}" WHERE path LIKE '${expectedLikePattern}' ORDER BY creation_date DESC LIMIT 2000`
+    );

As per path instructions, "Prefer asserting on specific values (paths, messages) over generic substrings."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/hermes/hermes-wiki-worker.test.ts` around lines 234 - 240, Strengthen
the SQL assertions in the hermes wiki worker test by comparing the generated
count and fetch statements against their exact expected SQL strings rather than
using startsWith, toContain, or includes. Keep the assertion that no ascending
creation-date query is generated, but express it through exact statement values
where possible.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/hooks/codex/wiki-worker.ts`:
- Around line 138-158: Update the LIKE predicates in the session-event queries
to use plain SQL string quotes around likePat instead of E-string literals.
Apply this in src/hooks/codex/wiki-worker.ts lines 138-158 and
src/hooks/pi/wiki-worker.ts lines 123-143, covering both count and fetch queries
while preserving the existing esc() result and query behavior.

In `@src/hooks/cursor/wiki-worker.ts`:
- Around line 135-145: The dbFetch implementations can undercount rows because
count(*) runs before the bounded SELECT. In src/hooks/cursor/wiki-worker.ts
lines 135-145 and src/hooks/hermes/wiki-worker.ts lines 135-145, update dbFetch
to fetch the bounded rows first, then query the count, and set total to the
maximum of the count and returned row length; apply the same correction to the
matching dbFetch patterns in src/hooks/wiki-worker.ts,
src/hooks/codex/wiki-worker.ts, and src/hooks/pi/wiki-worker.ts.

In `@src/hooks/wiki-worker.ts`:
- Around line 212-214: Prevent offset stamping from skipping backlog beyond the
bounded fetch window: in src/hooks/wiki-worker.ts:212-214,
src/hooks/codex/wiki-worker.ts:212-212, and src/hooks/pi/wiki-worker.ts:197-197,
update the flows around newRowsFromWindow so truncated windows advance only
through processed rows (prevOffset + newRows.length) and emit a wlog warning, or
loop fetching windows until the backlog is fully processed before recording the
full total.

---

Nitpick comments:
In `@tests/cursor/cursor-wiki-worker.test.ts`:
- Around line 229-234: In the cursor SQL assertions, replace the separate
generic checks on fetchSql and the ASC absence check with one regex-based
assertion anchored to the trailing ORDER BY creation_date DESC LIMIT 2000
clause. Preserve the existing count-query assertion and allow the dynamic
escaped session-id prefix while enforcing the exact expected SQL shape.

In `@tests/hermes/hermes-wiki-worker.test.ts`:
- Around line 234-240: Strengthen the SQL assertions in the hermes wiki worker
test by comparing the generated count and fetch statements against their exact
expected SQL strings rather than using startsWith, toContain, or includes. Keep
the assertion that no ascending creation-date query is generated, but express it
through exact statement values where possible.

In `@tests/shared/wiki-offset.test.ts`:
- Around line 78-116: Add a regression test in the newRowsFromWindow test suite
covering a stale total where total is less than the fetched window length,
matching the race between count probing and row fetching in dbFetch. Assert the
expected offset behavior and returned rows for that scenario, making the
contract explicit without changing the implementation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b4d5f082-f461-4c2f-90db-c2c3dd752cb6

📥 Commits

Reviewing files that changed from the base of the PR and between 87c285d and 0035bff.

📒 Files selected for processing (14)
  • src/hooks/codex/wiki-worker.ts
  • src/hooks/cursor/wiki-worker.ts
  • src/hooks/hermes/wiki-worker.ts
  • src/hooks/pi/wiki-worker.ts
  • src/hooks/wiki-offset.ts
  • src/hooks/wiki-worker.ts
  • tests/claude-code/wiki-worker-local-cache.test.ts
  • tests/claude-code/wiki-worker-plugin-version.test.ts
  • tests/claude-code/wiki-worker.test.ts
  • tests/codex/codex-wiki-worker.test.ts
  • tests/cursor/cursor-wiki-worker.test.ts
  • tests/hermes/hermes-wiki-worker.test.ts
  • tests/pi/pi-wiki-worker.test.ts
  • tests/shared/wiki-offset.test.ts

Comment on lines +138 to +158
// 1. Fetch session events from the sessions table — BOUNDED to the newest
// WIKI_FALLBACK_MAX_ROWS rows (reversed to chronological) plus the true
// total. The old unbounded `ORDER BY ASC` materialized the whole fat
// `message` column (tens of MB, ~30s cold on a mega-session) even though
// only the newest un-summarized rows are consumed. `count(*)` reads no fat
// column, so `total` is cheap and keeps the offset math + stamped offset right.
wlog("fetching session events");
const rows = await query(
`SELECT message, creation_date FROM "${cfg.sessionsTable}" ` +
`WHERE path LIKE E'${esc(`/sessions/%${cfg.sessionId}%`)}' ORDER BY creation_date ASC`
);

if (rows.length === 0) {
const likePat = esc(`/sessions/%${cfg.sessionId}%`);
const cntRows = await query(`SELECT count(*) AS n FROM "${cfg.sessionsTable}" WHERE path LIKE E'${likePat}'`);
const total = Number(cntRows[0]?.["n"] ?? 0);
if (total === 0) {
wlog("no session events found — exiting");
return;
}
const fetched = await query(
`SELECT message, creation_date FROM "${cfg.sessionsTable}" ` +
`WHERE path LIKE E'${likePat}' ORDER BY creation_date DESC LIMIT ${WIKI_FALLBACK_MAX_ROWS}`
);
const rows = fetched.reverse();

const jsonlLines = rows.length;
const jsonlLines = total;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate esc() to confirm whether it accounts for E''-literal backslash semantics.
rg -nP -A8 'function\s+esc\s*\(' --type=ts
rg -nP "LIKE E'" --type=ts -C2

Repository: activeloopai/hivemind

Length of output: 159


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files 'src/hooks/**/wiki-worker.ts' 'src/hooks/**/utils.ts' 'src/hooks/**/shared.ts' 'src/hooks/**/index.ts' | sed -n '1,120p'

echo
echo "== outline codex/wiki-worker.ts =="
ast-grep outline src/hooks/codex/wiki-worker.ts --view expanded || true

echo
echo "== outline pi/wiki-worker.ts =="
ast-grep outline src/hooks/pi/wiki-worker.ts --view expanded || true

echo
echo "== search esc definitions/usages =="
rg -n "esc\s*\(" src/hooks -g '*.ts' -C 2 || true

echo
echo "== search LIKE E' patterns =="
rg -n "LIKE E'" src/hooks -g '*.ts' -C 2 || true

Repository: activeloopai/hivemind

Length of output: 16901


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== read relevant sections =="
for f in src/hooks/codex/wiki-worker.ts src/hooks/pi/wiki-worker.ts src/hooks/wiki-worker.ts; do
  echo "--- $f ---"
  wc -l "$f"
done

echo
echo "== src/hooks/codex/wiki-worker.ts around queries =="
sed -n '1,240p' src/hooks/codex/wiki-worker.ts | nl -ba | sed -n '1,220p'

echo
echo "== src/hooks/pi/wiki-worker.ts around queries =="
sed -n '1,220p' src/hooks/pi/wiki-worker.ts | nl -ba | sed -n '1,220p'

echo
echo "== src/hooks/wiki-worker.ts around queries =="
sed -n '1,220p' src/hooks/wiki-worker.ts | nl -ba | sed -n '1,220p'

Repository: activeloopai/hivemind

Length of output: 486


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
for path in [
    Path('src/hooks/codex/wiki-worker.ts'),
    Path('src/hooks/pi/wiki-worker.ts'),
    Path('src/hooks/wiki-worker.ts'),
]:
    print(f'--- {path} ---')
    lines = path.read_text().splitlines()
    for i, line in enumerate(lines, 1):
        if 'function esc' in line or 'LIKE E\'' in line or "LIKE '" in line:
            start = max(1, i-3)
            end = min(len(lines), i+8)
            for j in range(start, end+1):
                print(f'{j:4}: {lines[j-1]}')
            print()
PY

Repository: activeloopai/hivemind

Length of output: 7317


🌐 Web query:

PostgreSQL LIKE operator backslash default escape E'' standard_conforming_strings documentation

💡 Result:

In PostgreSQL, the backslash character () serves two distinct functions that often cause confusion: one as a literal string escape mechanism and another as an escape character specifically for the LIKE operator's pattern matching [1][2][3]. 1. String Literal Parsing and standard_conforming_strings: The configuration parameter standard_conforming_strings controls how backslashes are interpreted within ordinary SQL string literals ('...'). - When standard_conforming_strings is on (the default in modern PostgreSQL), backslashes in ordinary string literals are treated as literal backslash characters [2][4]. - When standard_conforming_strings is off, backslashes in ordinary string literals are treated as escape characters (e.g., '\n' becomes a newline) [2][4]. - To represent backslash escape sequences reliably, you should use E-prefixed string constants (e.g., E'\'), which treat backslashes as escape characters regardless of the standard_conforming_strings setting [2][5]. 2. LIKE Operator Escape Behavior: Regardless of the standard_conforming_strings setting, the LIKE operator always uses the backslash as its default escape character for pattern matching [1][3]. This means that within a LIKE pattern, a backslash is used to escape wildcard characters like '%' and '' [1][6]. - To match a literal backslash in a LIKE clause, you must escape it with another backslash (e.g., '\') [1][5]. - Because of the two-layer interpretation (string parsing + LIKE pattern matching), searching for a literal backslash often requires multiple backslashes, such as '\\' in an ordinary string literal, or '\\' in an E-string literal [3]. - You can override the default LIKE escape character using the ESCAPE clause [1][6]. For example, LIKE '%#%' ESCAPE '#' treats '#' as the escape character instead of the backslash [1][3]. To disable the LIKE escape mechanism entirely, you can use ESCAPE '' [1][7]. In summary, standard_conforming_strings governs how string constants are parsed, whereas the LIKE operator's use of the backslash as an escape character is a fixed behavior of the pattern matching engine [1][3]. Applications requiring reliable handling of backslashes should use E-prefixed string constants and explicitly specify an ESCAPE character for LIKE patterns if needed [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path, ranges in {
    'src/hooks/codex/wiki-worker.ts': [(50, 170)],
    'src/hooks/pi/wiki-worker.ts': [(60, 155)],
    'src/hooks/wiki-worker.ts': [(55, 170)],
    'src/hooks/upload-summary.ts': [(52, 65)],
}.items():
    print(f'--- {path} ---')
    lines = Path(path).read_text().splitlines()
    for start, end in ranges:
        for i in range(start, min(end, len(lines)) + 1):
            print(f'{i:4}: {lines[i-1]}')
        print()
PY

Repository: activeloopai/hivemind

Length of output: 17833


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path, start, end in [
    ('src/hooks/codex/wiki-worker.ts', 54, 160),
    ('src/hooks/pi/wiki-worker.ts', 64, 150),
    ('src/hooks/wiki-worker.ts', 59, 165),
]:
    print(f'--- {path} ---')
    lines = Path(path).read_text().splitlines()
    for i in range(start, min(end, len(lines)) + 1):
        print(f'{i:4}: {lines[i-1]}')
    print()
PY

Repository: activeloopai/hivemind

Length of output: 15321


🌐 Web query:

PostgreSQL E string literal backslash LIKE pattern default escape documentation

💡 Result:

In PostgreSQL, there are two distinct mechanisms involving backslashes: string constant escaping (lexical level) and LIKE pattern escaping (pattern matching level). LIKE Pattern Escape In a LIKE pattern, the default escape character is the backslash () [1][2]. This allows you to match literal underscore (_) or percent sign (%) characters by preceding them with the backslash (e.g., '%') [1][2]. To match a literal backslash, you write two backslashes ('\') [1][2]. You can change this default behavior by using the ESCAPE clause (e.g., LIKE 'pattern' ESCAPE '!') or disable it entirely by using ESCAPE '' [1][2]. PostgreSQL's default of using the backslash as an escape character is a nonstandard extension to the SQL standard, which specifies that there should be no default escape character [1][2]. E String Literal An E string literal (e.g., E'string') is a lexical extension that enables C-style backslash escape sequences (such as \n for newline or \xHH for hexadecimal bytes) [3][4]. Because these sequences are interpreted by the parser when the query is read, a backslash intended to be part of a LIKE pattern must be doubled if it is inside an E string literal [3][4]. For example, to match a literal percent sign using an E string literal, you would write E'\%'—the first backslash escapes the second one for the string constant, leaving a single backslash for the LIKE operator to use as an escape character [1][3]. Important Configuration Note Since PostgreSQL 9.1, the default setting for the configuration parameter standard_conforming_strings is ON [3][5]. This means that backslashes in standard (non-E) string constants are treated as literal characters rather than escape sequences [3][5]. Consequently, it is recommended to use E string literals only when backslash escape sequences are actually needed [3][5].

Citations:


Use plain quotes for these LIKE patterns
esc() already returns a string for a plain SQL literal; E'...' adds an extra backslash-parse step before LIKE sees the pattern, so backslash-containing session IDs/paths can match differently here. Use the plain '${likePat}' form to match the other queries in these files.

  • src/hooks/codex/wiki-worker.ts#L145-L154
  • src/hooks/pi/wiki-worker.ts#L130-L139
📍 Affects 2 files
  • src/hooks/codex/wiki-worker.ts#L138-L158 (this comment)
  • src/hooks/pi/wiki-worker.ts#L123-L143
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/codex/wiki-worker.ts` around lines 138 - 158, Update the LIKE
predicates in the session-event queries to use plain SQL string quotes around
likePat instead of E-string literals. Apply this in
src/hooks/codex/wiki-worker.ts lines 138-158 and src/hooks/pi/wiki-worker.ts
lines 123-143, covering both count and fetch queries while preserving the
existing esc() result and query behavior.

Comment on lines +135 to +145
const dbFetch = async (): Promise<{ rows: Record<string, unknown>[]; total: number }> => {
const like = esc(`/sessions/%${cfg.sessionId}%`);
const cnt = await query(`SELECT count(*) AS n FROM "${cfg.sessionsTable}" WHERE path LIKE E'${like}'`);
const total = Number(cnt[0]?.["n"] ?? 0);
if (total === 0) return { rows: [], total: 0 };
const r = await query(
`SELECT message, creation_date FROM "${cfg.sessionsTable}" ` +
`WHERE path LIKE E'${like}' ORDER BY creation_date DESC LIMIT ${WIKI_FALLBACK_MAX_ROWS}`
);
return { rows: r.reverse(), total };
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

dbFetch's count(*)-then-SELECT ordering is a TOCTOU race that can silently drop new session events.

Both workers implement dbFetch as two independent query() round trips: count(*) first, then the bounded SELECT ... ORDER BY creation_date DESC LIMIT 2000. query() itself can retry with up to ~30s of backoff on transient 401/403/429/5xx responses, so the gap between the two queries can be substantial while the session's capture hook keeps appending rows. When that happens — especially for the common case where the whole session fits in one fetch (rows.length < WIKI_FALLBACK_MAX_ROWS) — the earlier total understates the row count that actually existed by the time the SELECT ran. newRowsFromWindow(rows, dbTotal, prevOffset) then returns a window that starts strictly after prevOffset, skipping the rows added during the race, while the offset is still stamped from that same stale dbTotal — high enough to mark those skipped rows as already summarized. They are never fed to the agent and never revisited on a later run: a silent, permanent loss of session events from the wiki summary.

  • src/hooks/cursor/wiki-worker.ts#L135-L145: swap the order in dbFetch — fetch the bounded rows first, then count(*), and set total = Math.max(count, r.length) so total can never understate the returned window.
  • src/hooks/hermes/wiki-worker.ts#L135-L145: apply the identical reordering + Math.max clamp to its dbFetch.

The same count(*)-then-SELECT pattern also appears in src/hooks/wiki-worker.ts, src/hooks/codex/wiki-worker.ts, and src/hooks/pi/wiki-worker.ts per the supplied graph evidence; those weren't in this review's file set but should get the same fix.

📍 Affects 2 files
  • src/hooks/cursor/wiki-worker.ts#L135-L145 (this comment)
  • src/hooks/hermes/wiki-worker.ts#L135-L145
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/cursor/wiki-worker.ts` around lines 135 - 145, The dbFetch
implementations can undercount rows because count(*) runs before the bounded
SELECT. In src/hooks/cursor/wiki-worker.ts lines 135-145 and
src/hooks/hermes/wiki-worker.ts lines 135-145, update dbFetch to fetch the
bounded rows first, then query the count, and set total to the maximum of the
count and returned row length; apply the same correction to the matching dbFetch
patterns in src/hooks/wiki-worker.ts, src/hooks/codex/wiki-worker.ts, and
src/hooks/pi/wiki-worker.ts.

Comment thread src/hooks/wiki-worker.ts
Comment on lines +212 to +214
// The offset high-water (stamped into the summary) must be the TRUE total, not the
// bounded window length — else the next run's offset regresses and re-summarizes.
let jsonlLines = usedLocalCache ? rows.length : dbTotal;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Offset advances past unsummarized backlog when it exceeds the 2,000-row fetch window (silent, permanent history loss).

All three workers share one root cause: newRowsFromWindow(rows, total/dbTotal, prevOffset) can only return rows already present in the bounded 2,000-row window, but the code then stamps the offset to the full total/jsonlLines regardless of whether the backlog since prevOffset exceeded that window. Whenever total - prevOffset > WIKI_FALLBACK_MAX_ROWS (e.g., a session's first-ever summarization on a large pre-existing history, or a worker outage), the middle slice of events between prevOffset and (total - 2000) is never fed to the LLM, yet the persisted offset jumps straight to total, so that slice can never be recovered on a later run — silently, with no log line (unlike the analogous, explicitly-logged capLinesByBytes byte-budget drop).

  • src/hooks/wiki-worker.ts#L212-L214: stop stamping jsonlLines = dbTotal unconditionally; either loop the bounded fetch until the backlog is fully covered before stamping, or stamp only prevOffset + newRows.length when the window was truncated, plus a wlog warning mirroring the byte-budget drop message.
  • src/hooks/codex/wiki-worker.ts#L212-L212: apply the same fix to jsonlLines/offset stamping around newRowsFromWindow(rows, total, prevOffset).
  • src/hooks/pi/wiki-worker.ts#L197-L197: apply the same fix to jsonlLines/offset stamping around newRowsFromWindow(rows, total, prevOffset).
📍 Affects 3 files
  • src/hooks/wiki-worker.ts#L212-L214 (this comment)
  • src/hooks/codex/wiki-worker.ts#L212-L212
  • src/hooks/pi/wiki-worker.ts#L197-L197
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/wiki-worker.ts` around lines 212 - 214, Prevent offset stamping
from skipping backlog beyond the bounded fetch window: in
src/hooks/wiki-worker.ts:212-214, src/hooks/codex/wiki-worker.ts:212-212, and
src/hooks/pi/wiki-worker.ts:197-197, update the flows around newRowsFromWindow
so truncated windows advance only through processed rows (prevOffset +
newRows.length) and emit a wlog warning, or loop fetching windows until the
backlog is fully processed before recording the full total.

Lift the four agent workers (hermes/cursor/pi/codex) over the 90%
per-file coverage bar (were ~83–85% statements / ~84% branches). Adds
tests for the bounded-fetch behaviors this PR introduced and the
surrounding error/edge paths that were previously uncovered:

- no-new-events skip (resume offset covers every row)
- local-cache-shorter-than-offset → bounded DB refetch (hermes/cursor)
- sidecar count preferred over a smaller parsed offset
- NULL embedding on embed-daemon failure
- existing-summary lookup throws → skip (no overwrite)
- sidecar update failure / releaseLock throw → still release, swallow
- capLinesByBytes drop + single-giant-row truncation
- exec throws after a partial summary write → skip upload
- fatal query error → release lock; retryable (503) → retry then succeed
- unknown server path fallback (empty DISTINCT path)
- codex formatExecFailure field branches (code/stderr/stdout, truncation)

Changed-file coverage now 97.9% statements / 93.4% branches / 100%
functions / 98.5% lines (all six files ≥90 per metric). No production
code change; zero net-new test failures vs main.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/cursor/cursor-wiki-worker.test.ts (1)

272-272: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the LIMIT 2000 bound in the "refetches from the bounded DB" tests. These tests exist to prove the fallback fetch is bounded, but they only assert ORDER BY creation_date DESC on the newest-events query and never verify the row cap. A regression that drops LIMIT 2000 (unbounded fetch) would pass unnoticed. Asserting the specific LIMIT 2000 value closes that gap.

  • tests/cursor/cursor-wiki-worker.test.ts#L272-L272: also assert the resolved message, creation_date SQL contains LIMIT 2000.
  • tests/hermes/hermes-wiki-worker.test.ts#L300-L302: extend the fetchSql assertions to include LIMIT 2000 alongside the existing ORDER BY creation_date DESC check.

As per path instructions, prefer asserting on specific values (paths, messages) over generic substrings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/cursor/cursor-wiki-worker.test.ts` at line 272, Strengthen the bounded
DB fallback assertions by verifying the resolved newest-events SQL includes the
specific LIMIT 2000 value alongside ORDER BY creation_date DESC. Update the
assertion at tests/cursor/cursor-wiki-worker.test.ts:272 and extend the fetchSql
assertions at tests/hermes/hermes-wiki-worker.test.ts:300-302; no other behavior
changes are needed.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/cursor/cursor-wiki-worker.test.ts`:
- Line 272: Strengthen the bounded DB fallback assertions by verifying the
resolved newest-events SQL includes the specific LIMIT 2000 value alongside
ORDER BY creation_date DESC. Update the assertion at
tests/cursor/cursor-wiki-worker.test.ts:272 and extend the fetchSql assertions
at tests/hermes/hermes-wiki-worker.test.ts:300-302; no other behavior changes
are needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 06ebb3b7-d418-417e-88b3-1c8df5efbe6a

📥 Commits

Reviewing files that changed from the base of the PR and between 0035bff and 426cc11.

📒 Files selected for processing (4)
  • tests/codex/codex-wiki-worker.test.ts
  • tests/cursor/cursor-wiki-worker.test.ts
  • tests/hermes/hermes-wiki-worker.test.ts
  • tests/pi/pi-wiki-worker.test.ts

@khustup2
khustup2 merged commit 451ec0c into main Jul 21, 2026
11 checks passed
@khustup2
khustup2 deleted the fix/wiki-worker-bounded-fallback branch July 21, 2026 23:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant