Skip to content

Retain large event outputs, render generated images, and migrate legacy rows - #2920

Open
ymichael wants to merge 9 commits into
mainfrom
bb/pr-incrementally-migrate-legacy-event-outputs-thr_9mfhzth64b
Open

Retain large event outputs, render generated images, and migrate legacy rows#2920
ymichael wants to merge 9 commits into
mainfrom
bb/pr-incrementally-migrate-legacy-event-outputs-thr_9mfhzth64b

Conversation

@ymichael

@ymichael ymichael commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Human comments

What was wrong

Large completed outputs lived directly in events.data, so ordinary timeline reads loaded and parsed megabytes that the UI did not need. Image-generation completions were worse: they were stored as unhandled provider envelopes with multi-megabyte encoded results, so a useful “Generated image” event could exceed the 4 MiB timeline limit and render as an unexplained generic placeholder. Existing databases also removed inline output in one bulk synchronous sweep, which could stall the Node event loop.

What changed

User-facing result

  • Normal timeline pages carry only small previews and metadata, not full command/tool/search/fetch/image results.
  • Generated images now render as compact Generating image / Generated image rows using prompt/path metadata. The encoded result is omitted, the row has no details expander, and rendering makes no second request.
  • Explicit raw/detail reads restore the exact full output while it is retained. Fork/copy preserves retained values and their original expiry without materializing them in Node memory.
  • Existing databases migrate legacy inline values incrementally: at most one row per synchronous advance, followed by a setImmediate yield. Cursor/window progress survives restart.
  • Timeline/projection caches are invalidated whenever migration or expiry rewrites history.

Output lifecycle

  1. A completed supported output above 32,768 JavaScript characters is replaced atomically with a Unicode-safe 2 KiB head + marker + 2 KiB tail preview in events.data. The exact JSON-encoded string goes into retained_event_outputs.
  2. Bounded timeline/event reads use the preview. Timeline details expose whether the full value is available, detail-limited, or retention-expired.
  3. Raw/detail reads hydrate the full value only when their response budget allows it. Public event pages accept at most 100 rows; hydrated raw responses above 8 MiB return an explicit 413 so callers can paginate.
  4. Full values expire at event.createdAt + 7 days. The preview then becomes authoritative and sidecars are deleted in bounded, yielding sweeps.
  5. Historical inline rows are scanned in ordered 250-row windows. One eligible row is migrated per advance; malformed, mismatched, already-previewed, or over-8-MiB rows are skipped without blocking later rows. Completion is persisted so history is not rescanned daily.

The authoritative output contract now has five pairs: commandExecution.aggregatedOutput, imageGeneration.result, toolCall.result, webFetch.resultText, and webSearch.resultText. The existing four pairs and all sidecar semantics are unchanged; no caller redeclares the list.

Generated-image boundary

  • The Codex provider plugin translates new imageGeneration items into the existing provider-bridge delta union.
  • The provider-neutral assembler persists a canonical imageGeneration item. The provider-bridge envelope remains protocol version 2 because this is additive vocabulary; its grammar snapshot and protocol documentation are updated.
  • Core DB/timeline code does not branch on a provider ID. For old rows, it recognizes the exact validated provider/unhandled item/completed image-generation envelope shape, preserves that envelope, and sidecarizes only its nested result.
  • The host-daemon protocol moves from current-main 179 to 180 because the new canonical item can cross the server/daemon wire.
  • Drizzle migration 0113_puzzling_black_knight.sql creates the sidecar table and expiry index and adds the partial (created_at, id) index used by legacy image scans. It was generated once from the final combined schema.

Everything remains on the Node main event loop; this PR adds no worker.

What the measurements say

For the original four output paths, bounded migration trades total drain time for responsiveness. On 36 KiB rows, p95 synchronous occupancy fell from 110.23 ms to 1.59 ms idle and from 247.96 ms to 2.47 ms under controlled CPU load. A 4,000-row / 147 MB backlog takes about 10.5 minutes on production cadence, with a yield between rows.

The generated-image benchmark matches the observed production population: 39 rows across 5 threads, including four 4,555,236-character results, for 56,720,944 result bytes. The exact parent cannot drain this backlog. HEAD drains it in 40 advances (39 migrations plus cursor completion):

Load Sync p50 / p95 / max Timer delay p50 / p95 / max Wall time p50 / p95 / max
Idle 4.31 / 20.19 / 26.91 ms 3.34 / 19.23 / 25.99 ms 260.39 / 270.00 / 270.00 ms
CPU 4.39 / 20.89 / 23.70 ms 3.42 / 19.94 / 22.75 ms 269.12 / 269.81 / 269.81 ms

Each image run migrates 39 rows / 56,720,944 bytes, scans 39 rows, executes 239 statements, and performs 40 ticks / 40 yields. The largest stored event falls from 4,555,638 bytes to 4,713 bytes. The database grows from 57,524,224 to 57,831,424 bytes while retained values coexist with previews (+307,200 bytes, about 0.53%). Expected and hydrated full-result SHA-256 are identical: 825d4731616b8bb2a9caae08316fc48f95eb7846cc692dce062a428bbeb25d89.

Benchmark baselines and raw artifacts

New-write/read sidecar benchmark

BEFORE is inline baseline eeaaa3e8db7b3aeb3c4ab46873816c84cb6ea513; AFTER is sidecar runtime b78bf6038f15c081c8274f3e8141ccc6afa8f5b2. Foundation head 259f937f7b327ea73f56b69ace6ed35645c8045e only adds the target export and has the same runtime/schema behavior. Same harness, Apple M4 Max, Node v22.23.1, 12 warmups + 60 read iterations, 8 warmups + 40 write iterations.

Main-loop operation, p95 Inline Sidecar Result
Normal bounded event read 42.382 ms 0.902 ms 47× faster
Normal bounded timeline read 42.752 ms 5.857 ms 7.3× faster
Raw full-output read 7.127 ms 26.562 ms explicit hydration costs more
Four-row write transaction 10.157 ms 5.929 ms 1.7× faster
Event data materialized by bounded reads 2,327,734 B 331,678 B 85.8% less

Artifacts: comparison, inline JSON, sidecar JSON, harness, SHA-256 manifest.

Original four-path legacy migration benchmark

BEFORE is foundation 259f937f7b327ea73f56b69ace6ed35645c8045e; AFTER is migration runtime f89baf1d0384d3f59cba5e655a1a65d083160a51. Same harness, two warmups, 10 measured iterations, Apple M4 Max, Node v22.23.1. CPU runs add one continuously busy child; migration stays on the main loop.

Fixture/load Metric Bulk sweep Bounded migration
36 KiB / idle sync occupancy 80.057 / 110.226 / 110.226 ms 0.335 / 1.587 / 36.072 ms
36 KiB / idle timer delay 80.107 / 110.267 / 110.267 ms 1.505 / 2.662 / 40.194 ms
36 KiB / CPU sync occupancy 91.183 / 247.957 / 247.957 ms 0.430 / 2.468 / 63.656 ms
36 KiB / CPU timer delay 91.230 / 248.036 / 248.036 ms 1.525 / 4.307 / 88.012 ms
4 MiB / idle sync occupancy 32.915 / 94.668 / 94.668 ms 4.560 / 14.589 / 22.070 ms
4 MiB / idle timer delay 32.949 / 94.702 / 94.702 ms 4.816 / 14.622 / 22.112 ms
4 MiB / CPU sync occupancy 30.997 / 165.865 / 165.865 ms 5.162 / 13.308 / 15.954 ms
4 MiB / CPU timer delay 33.529 / 172.354 / 172.354 ms 5.320 / 13.486 / 16.004 ms

Every 4,000-row drain migrated 147,456,000 bytes. Persisted windows reduced scan rows from 875,500 to 4,000 (99.5%). Expected, hydrated, and raw-event hashes match.

Artifacts: comparison, BEFORE idle, AFTER idle, BEFORE CPU, AFTER CPU, SHA-256 manifest.

Legacy generated-image benchmark

BEFORE is exact pre-extension head c9ed7ed4a4f1d88a1cca1b26bfac54a06e5c4c18; AFTER runtime is cf98aab80691b9d39dc908707fa063618314d6f7. The rebased runtime patch is range-diff equivalent; only the host protocol number and generated migration packaging changed. Same harness and 39-row dataset, two warmups, 10 measured iterations per load condition.

Artifacts: comparison, harness, BEFORE idle, BEFORE CPU, AFTER idle, AFTER CPU, SHA-256 manifest.

Exact combined file list (70 files)
apps/app/src/components/thread/timeline/TimelineRowDetails.output-preview.test.tsx
apps/app/src/components/thread/timeline/TimelineRowDetails.tsx
apps/app/src/components/thread/timeline/useTimelineWorkRowFullOutput.ts
apps/cli/src/__tests__/command-output/thread-log.test.ts
apps/cli/src/commands/thread/show.ts
apps/server/scripts/benchmark-completed-event-output-migration.mjs
apps/server/src/routes/threads/data.ts
apps/server/src/services/system/event-loop-work.ts
apps/server/src/services/system/periodic-sweeps.ts
apps/server/src/services/threads/thread-data.ts
apps/server/src/services/threads/timeline-cache.ts
apps/server/src/services/threads/timeline-latest-rows-cache.ts
apps/server/src/services/threads/timeline-output-preview.ts
apps/server/src/services/threads/timeline.ts
apps/server/test/public/public-thread-data.test.ts
apps/server/test/public/public-thread-timeline-output-preview.test.ts
apps/server/test/services/periodic-sweeps.test.ts
apps/server/test/services/threads/timeline-cache.test.ts
apps/server/test/services/threads/timeline-in-turn-window.test.ts
apps/server/test/services/threads/timeline-latest-rows-cache.test.ts
docs/api_to_audit.md
docs/provider-bridge-protocol.md
packages/client-core/src/timeline/timeline-auto-expand.ts
packages/client-core/src/timeline/timelineRowSignatures.ts
packages/db/drizzle/0113_puzzling_black_knight.sql
packages/db/drizzle/meta/0113_snapshot.json
packages/db/drizzle/meta/_journal.json
packages/db/src/data/event-output-truncation.ts
packages/db/src/data/events.ts
packages/db/src/data/index.ts
packages/db/src/data/retained-event-outputs.ts
packages/db/src/data/sweeps.ts
packages/db/src/retained-event-output.ts
packages/db/src/schema.ts
packages/db/test/data/completed-event-output-migration.test.ts
packages/db/test/data/retained-event-outputs.test.ts
packages/db/test/data/sweeps.test.ts
packages/db/test/helpers/migrated-connection.ts
packages/db/test/migrate.test.ts
packages/db/test/query-plans.test.ts
packages/domain/package.json
packages/domain/src/provider-event.ts
packages/domain/src/utf16.ts
packages/host-daemon-contract/src/protocol.ts
packages/host-daemon-contract/test/contract.test.ts
packages/provider-bridge-protocol/src/assembler/delta-assembler.ts
packages/provider-bridge-protocol/src/contract-tests/provider-bridge-grammar.v2.snapshot.json
packages/provider-bridge-protocol/src/thread-delta.ts
packages/server-contract/src/api/threads.ts
packages/server-contract/src/common.ts
packages/server-contract/src/thread-timeline.ts
packages/thread-view/src/build-event-projection.ts
packages/thread-view/src/build-thread-timeline.ts
packages/thread-view/src/event-projection-message.ts
packages/thread-view/src/event-projection-types.ts
packages/thread-view/src/format-timeline-text.ts
packages/thread-view/src/timeline-row-title.ts
packages/thread-view/src/timeline-view.ts
packages/thread-view/src/timeline-work-row-glyph.ts
packages/thread-view/src/tool-activity-cells.ts
packages/thread-view/src/tool-activity-projection.ts
packages/thread-view/src/tool-activity-web-projection.ts
packages/thread-view/src/web-activity-lifecycle.ts
packages/thread-view/test/build-thread-timeline.test.ts
plugins/provider-codex/src/delta-translation.test.ts
plugins/provider-codex/src/delta-translation.ts
plugins/provider-codex/src/presentation.ts
plugins/provider-codex/src/schemas.ts
tests/integration/helpers/api.ts
tests/integration/helpers/assertions.ts

Residual tradeoffs

  • Explicit raw/detail reads are slower because they fetch full retained values; normal timeline reads avoid that work.
  • Full values intentionally disappear after seven days. The compact preview remains.
  • A raw event response above 8 MiB returns 413, so callers must paginate; an individual legacy event above 8 MiB is not migrated and can still render as oversized.
  • The database temporarily grows while preview and full sidecar coexist. SQLite file compaction remains separate maintenance policy.
  • Historical migration takes longer overall and uses more small statements in exchange for bounded main-loop occupancy and a yield between rows.
  • The generated-image p95 is about 21 ms because the known production-sized 4.35 MiB JSON row must still be parsed once on the main loop. The 8 MiB migration cap bounds the pathological case.
  • An out-of-band historical row inserted behind a permanently completed cursor requires an explicit future cursor-version change or maintenance action.

How you verified

Focused regressions use real in-memory SQLite without mocks and fail on the relevant parent behavior:

  • new large image results are previewed/sidecarized atomically and hydrate byte-identically;
  • a 4 MiB+ legacy raw image event migrates into a compact Generated image row without exposing its result or showing the generic oversized placeholder;
  • legacy image migration handles restart, independent cursor state, expiry, malformed/future shapes, query-plan index use, cache invalidation, and one-row yielding;
  • the provider bridge translates started/completed image generation without a provider-unhandled fallback;
  • image timeline rows are non-expandable and omit encoded results;
  • all original sidecar/migration regressions remain covered, including UTF-16 boundaries, raw/detail/copy, 100-row pagination, 8 MiB response limits, restart/window/completion, and sidecar expiry.

Validation on rebased head 07fa5fd0ea:

  • affected Turbo build/typecheck graph: 16/16 tasks passed;
  • @bb/db: 469/469 tests passed, including migration replay, snapshot-chain, query-plan, cursor, and sidecar regressions;
  • provider bridge: 238/238, Codex plugin: 260/260, thread view: 370/370, client core: 253/253, domain: 195/195, server contract: 64/64, host-daemon contract: 51/51;
  • full app suite: 3,867 passed, 3 skipped;
  • full server suite: 2,217/2,217 passed across 227 passing test files, with 1 file skipped;
  • full integration suite: 77/77 passed with controlled four-worker concurrency;
  • generated bridge grammar snapshot was retained and the single Drizzle migration/snapshot was regenerated mechanically from origin/main's 0112 state;
  • range-diff confirms the runtime patches are unchanged apart from the required host protocol increment; the obsolete SDK inventory update disappeared because current main removed that inventory.
  • rebased-head CI run 33837383447: all required jobs passed; the unrelated app-shard jsdom teardown flake passed on its isolated rerun.

Fixes: no linked issue.

AGENT GENERATED

@ymichael
ymichael force-pushed the bb/pr-incrementally-migrate-legacy-event-outputs-thr_9mfhzth64b branch from 214378b to 1f46348 Compare September 2, 2026 09:11
@ymichael
ymichael changed the base branch from bb/pr-retained-event-output-sidecar-storage-thr_pkwpa3qag4 to main September 2, 2026 17:49
@ymichael ymichael changed the title Migrate legacy completed outputs incrementally Retain large completed outputs and migrate legacy rows incrementally Sep 2, 2026
@ymichael

ymichael commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

@slopcop The PR description has been rewritten to lead with the user-facing benefits and benchmark results, with the detailed lifecycle, validation, and raw artifacts kept in expandable sections.

@bb-slop-cop

bb-slop-cop Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🚨 SLOP COP 🚨 · review

I am the Slop Cop, and I am reviewing this change now. I will check security, code quality, performance, architecture, and the main user flow.

runEventLoopWorkSync("sweep:retained-event-output-expiry:delete", () =>
deleteExpiredRetainedEventOutputs(deps.db, {
expiredAtOrBefore: now,
limit: RETAINED_EVENT_OUTPUT_EXPIRY_BATCH_SIZE,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — The expiry worker removes only one full output each ten-second sweep.

That permits 8,640 removals each day. A faster write rate creates an endless backlog. Expired outputs can remain as plaintext past seven days. Please use a bounded row or time budget, and yield between delete groups.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 28f2efe. The 10-second expiry sweep now performs up to 256 one-row deletes, yielding with setImmediate after every successful delete and stopping early when empty. The regression seeds two expired sidecars and observes both deletion plus the intermediate one-row state between yields; it failed before this fix and now passes.

});
const hydratedEventRows =
detailsInlineOutputLimit === null
? hydrateRetainedEventOutputRows(db, eventRowsWithBackgroundTaskState)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — This byte check occurs after the code loads every full sidecar.

The earlier floor sees only previews. One large result can cause an unlimited database read, JSON parse, and JSON write before fallback. Please query lengths first, then hydrate only values that fit the 4 MiB budget.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 28f2efe. Bounded timeline detail hydration now preflights sidecar byte metadata before selecting values. If raw bytes already exceed the 4 MiB budget it returns previews without materializing the full strings; otherwise it checks JSON-escaped size and hydrates only when the completed response fits. The new 5 MiB regression stays previewed at a 4 MiB budget and hydrates exactly at 8 MiB.

Comment thread packages/db/src/data/sweeps.ts Outdated
json_type(data, ?) = 'text'
AND json_type(data, ?) IS NULL
AND json_extract(data, ?) = ?
AND length(json_extract(data, ?)) > ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — This migration size check does not match the new-write size check.

SQLite counts Unicode code points. JavaScript counts UTF-16 units. Thus, 20,000 emoji count as 20,000 here and 40,000 in JavaScript. New writes retain this output, but the legacy migration skips it forever. Please use one unit and add an astral Unicode test.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 28f2efe. The SQL predicate is now only a cheap UTF-8-byte candidate filter; prepareCompletedEventOutputData remains the canonical JavaScript UTF-16 threshold check. False-positive multibyte candidates advance the cursor safely. A 20,000-emoji value (40,000 JS UTF-16 units) now migrates and hydrates byte-identically.

let migratedBytes = 0;
let migratedRows = 0;
const startedAt = performance.now();
while (migratedRows < expectedRows) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — This loop measures continuous advances, not the production drain time.

Production stops after 64 advances and waits for the next ten-second sweep. Therefore, 4,000 rows need at least 63 sweeps, or approximately 10.5 minutes. The PR reports 4.6–5.5 seconds. Please benchmark the real schedule or correct the stated drain time.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 28f2efe. The harness now reports both continuous active work and a production projection using the actual 10-second cadence, one foundation call versus at most 64 bounded advances per sweep. It also adds four 4 MiB single-row paths. For 4,000 rows / 147 MB, bounded migration uses about 5.1 s active work but about 630 s from startup; standard idle p95 occupancy is 69.779 -> 0.378 ms and the 4 MiB-row p95 is 23.347 -> 4.308 ms. Raw artifacts and the corrected comparison are linked in the PR.

return { inserted: false };
}
if (prepared.retainedOutput !== null) {
insertPreparedRetainedEventOutput(db, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — This helper can save the preview without the retained full output.

insertEvents accepts a plain connection. If this second write fails, the preview remains. A retry ignores the existing event and never restores the sidecar. Please make both writes atomic, or require a transaction. Add a failure test for the sidecar write.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 28f2efe. insertEvents now accepts DbConnection and wraps the complete event batch plus retained-sidecar inserts in one immediate transaction; notification happens only after commit. The regression installs a real SQLite BEFORE INSERT trigger that rejects the sidecar insert and verifies that no preview event is left behind. It failed before the transaction and passes now.

@bb-slop-cop bb-slop-cop Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 SLOP COP 🚨 · review

Plain English summary: This PR moves large completed outputs into a side table for seven days. Normal timeline reads receive small previews. Explicit reads can still receive the full output. A background job converts old rows in small steps.

I found five material issues.

  • High — Expiry cannot keep pace. The worker removes one output every ten seconds. It can remove only 8,640 rows each day. A faster write rate keeps plaintext outputs past seven days and grows the database.
  • High — The 4 MiB guard runs too late. Detail reads load and rebuild all full values before the size check. One very large result can block the server or exhaust memory.
  • Medium — Unicode size rules differ. SQLite counts code points, but JavaScript counts UTF-16 units. The migration can skip legacy emoji-heavy outputs that new writes retain.
  • Medium — The drain benchmark does not model production. It runs advances continuously. Production runs 64 advances every ten seconds. A 4,000-row backlog needs at least 10.5 minutes, not 4.6–5.5 seconds. The 36 KiB fixture also does not test a very large single row.
  • Medium — One exported write path is not atomic. insertEvents can save a preview before a sidecar write fails. A retry ignores the event and cannot restore the full output.

The architecture scan found a smaller maintenance issue. The output paths exist in both retained-event-outputs.ts and event-output-truncation.ts. The retention module and sweep module also import each other. Move the policy constants and target contract into retained-event-output.ts. Derive both path lists from that contract.

Verification completed:

  • The full @bb/db suite passed: 455 tests.
  • The five focused server files passed: 55 tests.
  • The @bb/db and @bb/server Turbo typechecks passed.
  • The local app loaded through doobie. The project view and prompt interface worked without console errors.
  • The current GitHub checks are green.

The core design is useful, but these limits need correction before the retention promise is reliable.

@ymichael

ymichael commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

@slopcop The five inline findings are addressed in 28f2efe with focused regressions, refreshed production-schedule and 4 MiB-row benchmarks, corrected raw artifacts, and an updated PR description. Please re-review.

@bb-slop-cop

bb-slop-cop Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🚨 SLOP COP 🚨 · review

I am the Slop Cop. I am reviewing this pull request for security, code quality, performance, architecture, and end-to-end behavior.

@ymichael
ymichael force-pushed the bb/pr-incrementally-migrate-legacy-event-outputs-thr_9mfhzth64b branch from 28f2efe to 193561b Compare September 2, 2026 19:07
@ymichael

ymichael commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

@slopcop Follow-up: current main introduced a conflict in the shared sweep files, so the branch is now rebased and the content-equivalent review-fix head is 193561ba77cd4d1965652fa02c662ff4bef91f7c. Both destroyed-environment maintenance and retained-output maintenance are preserved. Fresh exact-head artifacts are linked in the updated description; 460 DB tests, 53 focused DB regressions, 57 focused server regressions, both typechecks, the server build, and the 21-test provider ratchet pass. Please review this head.

Comment thread packages/db/src/data/sweeps.ts Outdated
if (cursor.lastCreatedAt === 0 && cursor.lastEventId === "") {
return emptyCompletedEventOutputMigrationResult("idle", 0);
}
advanceCompletedEventOutputMigrationCursor(db, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — A completed migration restarts forever.

This reset makes the next 10-second sweep scan from the first event again.

After migration completes, each sweep can still run 64 scans of 250 rows and evaluate JSON conditions.

Please store a completed-pass state and reduce later scans.

Reset that state after an import or a policy change.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in b73deba. A full cursor pass now persists an explicit completed state; ordinary 10-second sweeps then return with zero scanned rows. A bounded daily wrap catches out-of-band/imported legacy rows inserted behind the cursor, and a cursor-version change restarts immediately. The regression covers completion, a no-scan follow-up, and discovery of a behind-cursor row after the rescan interval.


const expiresAt = args.createdAt + COMPLETED_EVENT_OUTPUT_RETENTION_MS;
const truncation = isJsonObject(existingTruncation) ? existingTruncation : {};
item[target.outputPath] = truncateOutput(value);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — The output size becomes incorrect for retained values.

This assignment replaces a 50,000-character output with a 4,173-character storage preview.

The timeline preview then uses row.output.length as outputPreview.totalChars.

The UI can report 4,173 characters while the retained detail contains 50,000 characters.

Please carry truncation.originalLength into the timeline row and use it for totalChars.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in b73deba. Timeline projection now derives the authoritative original output length from the completed-event truncation metadata and preserves it when applying an additional timeline preview. The normal and detail timeline regressions use a 5 MiB retained value and assert bounded preview text plus the full original totalChars.

}
const remainingDataBytes = maxDataBytes - storedDataBytes;
if (
retainedOutputSizeTotal(rawSizes, rowCountsByEventId) > remainingDataBytes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — The byte check rejects retained output that fits the limit.

The check adds the retained value size to storedDataBytes.

It does not subtract the storage preview that hydration replaces.

I reproduced this with a 40,000-character retained value.

The hydrated rows used 45,489 bytes, but this function returned the 9,805-byte previews at that exact limit.

Please calculate the projected hydrated JSON size before the decision.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in b73deba. The preflight now computes the exact projected serialized JSON byte count: it removes the stored preview and truncation metadata from the small event template, then adds SQLite octet_length(json_quote(value)) metadata for the retained string. It still does not select/materialize the full value before acceptance. The regression proves an exact-limit response hydrates and a limit one byte smaller stays a preview.

if (!result.threadId) {
throw new Error("Migrated completed output has no thread");
}
deps.hub.notifyThread(result.threadId, ["history-rewritten"]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — The migration can cause repeated client refreshes.

Each migrated row sends an immediate history-rewritten message.

A sweep can send 64 messages, and one thread can receive many messages.

The client handles this change immediately and invalidates several queries.

Please collect changed thread IDs and send one message for each thread after the sweep.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in b73deba. The periodic sweep now collects affected thread IDs in a Set and emits cache/history invalidation once per thread in finally, so a later advance failure cannot lose invalidation. The regression migrates two rows belonging to one thread and now expects exactly one history-rewritten notification.

@bb-slop-cop bb-slop-cop Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 SLOP COP 🚨 · review

This PR moves large completed outputs into a second database table. Normal reads keep a short preview. Detail reads keep full text for seven days. A background job converts old rows in small steps.

I found four medium issues:

  • The timeline can report the storage preview length as the full output length.
  • The detail byte check can reject a full output that fits its limit.
  • The migration restarts a complete scan every ten seconds forever.
  • The migration can send many immediate refresh messages for one thread.

I left line comments with the evidence and proposed corrections.

The security review found no access, privacy, retention, or transaction defect. The central target list also removes path duplication cleanly. I found no other required architecture refactor.

The database suite passed 458 tests. The focused server suites passed 36 tests. The security checks passed 13 focused database tests.

I also tested the real application with Doobie. A 40,000-character command stored a 4.8 KB preview and a full retained value. The detail view restored all 40,000 characters.

GitHub currently reports a merge conflict with main.

@bb-slop-cop

bb-slop-cop Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🚨 SLOP COP 🚨 · review

I am the SlopCop. I am reviewing this pull request for security, code quality, performance, architecture, and end-to-end behavior.

types: args.types,
});
return rows.map((row) => parseStoredEventRow(row));
return hydrateRetainedEventOutputRows(db, rows).map((row) =>

@bb-slop-cop bb-slop-cop Bot Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — This raw read has no output byte limit.

The route accepts any numeric row limit. This line loads every full sidecar before JSON serialization.

Twenty 1 MiB outputs took 286 ms locally. One request can use unbounded time and memory.

Please set a row limit and a total-byte limit, or stream bounded pages.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 6aba4c1. The raw event-list route now clamps pages to 100 rows and performs the exact projected hydrated-data byte preflight before selecting any full sidecar value. Pages over 8 MiB return 413; callers can use the existing sequence/limit pagination, and a smaller page still returns the byte-identical retained value. The same byte guard protects /events/wait. The in-memory integration regressions cover a requested 1,000-row page being capped at 100 and a 9 MiB retained page returning 413 while a one-row page hydrates exactly; both failed before this fix and now pass.

@ymichael
ymichael force-pushed the bb/pr-incrementally-migrate-legacy-event-outputs-thr_9mfhzth64b branch from 193561b to b73deba Compare September 2, 2026 19:26

@bb-slop-cop bb-slop-cop Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 SLOP COP 🚨 · review

Plain-English summary

This change moves large completed outputs into a separate database table for seven days.

Normal timeline pages keep small previews. A background job converts old database rows in small steps.

Findings

I found five medium issues. I added one new inline comment and did not repeat four open comments.

  1. Raw event reads can load unlimited full outputs into memory.
  2. Timeline previews report the storage preview length instead of the full output length.
  3. The detail byte check rejects some full outputs that fit the response limit.
  4. The migration cursor restarts after each complete pass and scans all completed history forever.
  5. Migration notices can cause 64 immediate client refreshes for one thread in one sweep.

I found no security defect. The central output-target contract also removes the old path-list duplication.

Verification

  • The database suite passed all 460 tests.
  • The database and server typechecks passed.
  • All current GitHub checks passed.
  • A browser check returned the exact 50,026-character retained output without storage-only truncation data.
  • git diff --check passed.

Please address the five findings before merge.

@ymichael

ymichael commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

@slopcop The four follow-up findings are addressed on b73debadfb: completed scans no longer rescan every 10 seconds, timeline previews preserve the authoritative original length, exact-fit hydration uses the exact projected serialized size without selecting the large value, and same-thread migration notifications are coalesced. I added failing-before/passing-after regressions for each, refreshed the exact-head idle/CPU artifacts, and updated the PR description. Please take another look.

@bb-slop-cop bb-slop-cop Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 SLOP COP 🚨 · review

I am reviewing this pull request now. I will check security, code quality, performance, architecture, and the main user path.

@ymichael
ymichael force-pushed the bb/pr-incrementally-migrate-legacy-event-outputs-thr_9mfhzth64b branch from b73deba to 6aba4c1 Compare September 2, 2026 19:40
@ymichael

ymichael commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

@slopcop The additional raw-read finding is now addressed on 6aba4c150f: 100-row cap, exact 8 MiB preflight before full-value selection, 413 for oversized pages, and exact retained output on smaller pages. The new integration regressions failed before and pass now; the exact-head benchmark artifacts and PR description are refreshed. Please re-review this head.


function truncateOutput(value: string): string {
return (
value.slice(0, COMPLETED_EVENT_OUTPUT_RETAINED_HEAD_CHARS) +

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — Preserve Unicode characters at both preview boundaries

slice() works on UTF-16 code units. A boundary can divide an emoji or another astral character. The stored preview then contains an unpaired surrogate. After retention expires, UTF-8 output replaces that character with U+FFFD permanently. I reproduced this with 2,047 ASCII characters followed by 😀; value.slice(0, 2048).isWellFormed() returned false.

Please move each boundary when it falls between a high and low surrogate. Add tests for both the head and tail boundaries.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 9e58d94. The retained preview now detects a head or tail boundary between a UTF-16 high/low surrogate pair and moves that boundary inward, so the preview never introduces an unpaired surrogate. The in-memory SQLite regression places an emoji across each boundary and verifies both stored previews remain well formed after round-tripping through SQLite; it failed before this fix and passes now.

@bb-slop-cop bb-slop-cop Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 SLOP COP 🚨 · review

Plain-English summary

This change moves large completed outputs from busy event rows into a separate table for seven days.

Normal timeline pages use small previews. Detail and raw reads can restore the full text while it remains available.

A background job moves old rows in small steps, which reduces server pauses.

Findings

I found six open issues. Four already had open SlopCop comments, and I added two new comments.

  1. High: bb thread log --json --all stops after 100 events. The server caps a page at 100 rows, but the CLI expects 1,000 rows.
  2. Medium: The migration reads overlapping 250-row windows. A focused benchmark scanned 125,500 rows to move 1,000 rows.
  3. Medium: The second timeline preview can split an emoji. The result contains an unpaired UTF-16 surrogate and can show a broken character.
  4. Medium: The interface reports expired output as too large. After retention ends, the output does not exist and cannot load.
  5. Medium: The Unicode boundary fix stores incorrect preview lengths. A direct check reported 2,048 units when the stored head had 2,047 units.
  6. Low: The benchmark artifact records private machine details. It includes the host name and the absolute repository path.

Architecture

The shared output-target contract removes the old path duplication. I found no need for a larger storage refactor.

The two preview functions should share one safe UTF-16 boundary helper. This change would prevent the current second-preview defect and later drift.

The benchmark also repeats the production cadence and advance limit. Inputs or one shared source would keep its projections accurate.

Verification

  • A browser test ran a real Codex command with 40,000 output characters.
  • The database stored a 6,843-character preview and a sidecar.
  • The expanded interface restored all 40,000 characters without a preview notice.
  • The CLI test used 162 events and returned only sequences 1 through 100.
  • The focused database checks passed 15 of 15 tests and 40 of 40 tests.
  • The 4 MiB migration benchmark measured approximately 4.9 ms at p95 for one synchronous step.
  • All GitHub checks pass, and GitHub reports that the pull request can merge.

The security review found no access, injection, cross-thread, expiry, path, or transaction defect.

I recommend corrections for the high and medium issues before merge.

@ymichael
ymichael force-pushed the bb/pr-incrementally-migrate-legacy-event-outputs-thr_9mfhzth64b branch 2 times, most recently from 4e71ff6 to 1eba401 Compare September 2, 2026 20:45
@ymichael

ymichael commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

@slopcop All six findings from the latest review are addressed on head 1eba4014f6a6c7a75c359a9d9d2e2e4478fc0819, with focused regressions and refreshed sanitized before/after artifacts linked in the PR body. Please re-review this exact head.

@bb-slop-cop

bb-slop-cop Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🚨 SLOP COP 🚨 · review

I am the SlopCop. I am reviewing this pull request now.

I will check security, code quality, architecture, performance, and the main user flow.

afterSeq: parseOptionalInteger(query.afterSeq, "afterSeq"),
beforeSeq: parseOptionalInteger(query.beforeSeq, "beforeSeq"),
limit: parseOptionalInteger(query.limit, "limit") ?? 100,
limit: parseBoundedPositiveOptionalInteger({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — Do not silently cap the public event list

This route previously accepted a larger limit.

The response is an array without page data, and the contract still accepts any positive integer.

A client can request 1,000 rows, receive 100 rows, and incorrectly stop because the result appears complete.

Reject values above 100, or add page data that tells clients to continue.

@ymichael ymichael Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in f89baf1. The public query schema now rejects limit values above the shared 100-row page size with HTTP 400, so callers never mistake a silently truncated response for the final page. The CLI continues to paginate 100-row requests when all events are requested.

const previews = retainedOutputPreviewsByCallId(
events,
availablePreview,
Date.now(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — Do not cache a time-based retention state without an expiry

Availability uses Date.now() during the build, but the cache key does not include time.

The expiry sweep deletes sidecars without a thread notification.

An unchanged thread can show available after the retained output expires.

Add a cache expiry, compute availability after cache access, or notify affected threads during deletion.

@ymichael ymichael Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in f89baf1. Expiry selection now returns the affected thread IDs, and the sweep coalesces a history-rewritten notification per thread after deletion, including on the error path. That invalidates timeline and projection caches when time-based full-output availability changes.

if (!target) {
throw new Error("Expected completed output migration target");
}
const result = runEventLoopWorkSync(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — Move unlimited output work off the server event loop

This synchronous frame parses, serializes, and writes one output with no byte limit.

The included benchmark used 16 MiB outputs.

It measured 146.9 ms median delay and 366.1 ms maximum timer delay.

The row limit cannot limit work for one large row.

Use a worker or a byte limit that prevents a long server stall.

@ymichael ymichael Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in f89baf1 with a byte bound while preserving the explicit no-worker and main-loop constraint. Candidate selection short-circuits before JSON parsing when serialized event data exceeds 8 MiB; that legacy row remains byte-identical inline, the cursor advances past it, and later bounded rows still migrate. The new-write sidecar path is unchanged. A focused 8 MiB regression covers skip-and-continue behavior.

Comment thread packages/db/src/data/sweeps.ts Outdated
}
const state = getCompletedEventOutputScanState(db, args);
const cursor = state.cursor;
if (cursor.lastCreatedAt === COMPLETED_EVENT_OUTPUT_MIGRATION_COMPLETED_AT) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — Avoid a full history rescan every day

After completion, this code resets each target cursor to the first event every 24 hours.

New events already create sidecars when the server writes them.

One million completed rows need at least 625 seconds at the current scan limit.

Keep a permanent migration cursor, or start a bounded rescan only after an import.

@ymichael ymichael Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in f89baf1. Completion is now permanent for the cursor version; the daily wrap was removed, so an already-drained database does zero history scans on later sweeps. A future import path must deliberately bump or restart the cursor or invoke explicit maintenance rather than imposing a permanent daily rescan on every database.

Comment thread packages/db/src/data/sweeps.ts Outdated
updatedAt: args.migratedAt,
}
: window;
const scanRows =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — Remove the unused production count query

This COUNT runs on each advance to fill scanRows.

The server does not read scanRows.

The benchmark saw 12,000 statements for 2,000 migrated rows, and this query supplied 2,000 statements.

Remove the field from production, or collect it through benchmark instrumentation.

@ymichael ymichael Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in f89baf1. The per-advance count query and its types were removed. The scanRows metric now records only rows actually fetched when opening a persisted 250-row window and is zero while reusing that window. In the refreshed 4,000-row drain this removed 3,984 statements, from 24,000 to 20,016, while scan rows remained 4,000.

@bb-slop-cop bb-slop-cop Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 SLOP COP 🚨 · review

Plain English summary

This PR changes how BB stores large output from completed commands and tools.

BB keeps a short preview in the event row. It keeps the full output in a separate row for seven days.

A small periodic job moves old event rows to this format. The UI and CLI can then request the full output when necessary.

Review results

I found five issues:

  • The event route silently reduces larger client limits to 100 rows. The response has no page data.
  • The timeline cache can show full output as available after the retention period ends.
  • One 16 MiB legacy output can block the server event loop for hundreds of milliseconds.
  • Each completed migration starts a full history scan again after 24 hours.
  • Each migration step runs a count query that the server does not use.

I left one inline comment for each issue. I recommend that the author resolve them before merge.

Architecture and security

I found no authorization bypass, cross-thread lookup, SQL injection risk, or broken transaction boundary.

I found no useful duplicate code to remove. The new UTF-16 helper has different rules from the existing byte-window helper.

Checks

  • The database suite passed all 463 tests.
  • The changed application test passed all 6 tests.
  • The focused server run passed 139 of 140 tests. The one time-based assertion passed its isolated rerun.
  • The affected timeline tests passed their focused rerun after they exceeded limits in the full concurrent run.
  • Type checks passed for the database, server, application, and CLI packages.
  • A live doobie check created a thread with 40,000 output characters.
  • The database stored a 4,870-character preview and the full retained value.
  • The UI loaded and displayed the complete output after the row opened.
  • A 16 MiB benchmark measured 146.9 ms median delay and 366.1 ms maximum timer delay.

I posted a comment review. I did not use an approval or a change-request state.

@ymichael
ymichael force-pushed the bb/pr-incrementally-migrate-legacy-event-outputs-thr_9mfhzth64b branch 2 times, most recently from f89baf1 to c9ed7ed Compare September 2, 2026 21:26
@ymichael ymichael changed the title Retain large completed outputs and migrate legacy rows incrementally Retain large event outputs, render generated images, and migrate legacy rows Sep 3, 2026
@ymichael
ymichael force-pushed the bb/pr-incrementally-migrate-legacy-event-outputs-thr_9mfhzth64b branch from 76aa2ab to 07fa5fd Compare September 4, 2026 04:35
@ymichael

ymichael commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator Author

@slopcop The PR has been rebased onto current main, the two Drizzle migrations have been regenerated as a single migration, and the rebased CI is green. Ready for another look.

@bb-slop-cop

bb-slop-cop Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🚨 SLOP COP 🚨 · review

I’m reviewing this now. I’m checking security, general code quality and architecture, performance, and the end-to-end behavior in parallel, then I’ll leave one consolidated review with the findings.

outputPath: RetainedEventOutputPath,
): number {
const payload: unknown = JSON.parse(data);
if (!isJsonObject(payload) || !isJsonObject(payload.item)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — High: migrated legacy image sidecars crash bounded hydration

prepareLegacyImageGenerationOutputData stores the hydratable item under rawEvent.params.item, but this byte preflight only accepts payload.item. I reproduced a valid legacy image migration followed by canHydrateRetainedEventOutputRowsWithinDataByteLimit() throwing Retained output event payload is not an item object. The raw events route and turn-detail reads call this preflight, so affected threads return HTTP 500 until the retained value expires. Please pass the row into this calculation and reuse hydratableOutputItem(row, payload), then cover both the bounded-hydration helper and public routes with a migrated legacy envelope.

...row,
output: buildTimelineOutputPreview(row.output),
outputPreview: { totalChars: row.output.length },
outputPreview: row.outputPreview ?? {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — Medium: the new output-availability state is omitted from row memo signatures

This makes outputPreview.experimental_fullOutputAvailability user-visible state, but command/tool render signatures still omit both it and totalChars. A history-rewritten refetch after migration or expiry can therefore return a new row at the same sequence whose signature compares equal, causing the memoized component to keep the old availability/detail state (including a previously loaded full value after retention expiry). Please add the preview fields to the command/tool signatures and cover the available → retention-expired transition in the signature/memo tests.

const page = await sdk.threads.events.list({
threadId,
limit: String(THREAD_LOG_ALL_EVENTS_PAGE_SIZE),
limit: String(THREAD_EVENT_LIST_PAGE_SIZE),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 slopcop/review — Medium: bb thread log --all cannot page output-heavy histories

The raw event endpoint now intentionally returns event_data_too_large when one hydrated page exceeds 8 MiB, but --all always asks for 100 rows and has no retry with a smaller page. I reproduced an output-heavy thread returning 413 for limit=100, after which bb thread log --all --json aborts instead of printing the thread. Please make this path byte-aware (for example, catch the typed error and reduce the page size until it fits) and add a CLI regression with several retained outputs totaling more than 8 MiB.

@bb-slop-cop bb-slop-cop Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 SLOP COP 🚨 · review

Plain-English summary: this PR moves large command, tool, search, fetch, and generated-image results out of the normal timeline record. The timeline keeps a small preview so everyday reads stay fast, while explicit detail/raw reads can recover the full value for seven days. It also teaches the app to show generated-image activity and incrementally migrates older databases without one long event-loop stall.

I found three issues that should be fixed before merge:

  1. High — migrated legacy image outputs can make reads return 500. The migration stores old Codex image results under rawEvent.params.item, but the response-size preflight only understands payload.item. A valid migrated row therefore throws before hydration on raw-event and turn-detail paths. This is also a low-severity request-level availability/security issue because one provider-produced legacy image can keep those reads broken until retention expiry.
  2. Medium — retention changes can leave stale output UI mounted. Command and tool render signatures do not include the new full-output availability or character count. After a history rewrite, React can treat available and retention-expired rows as identical and retain stale details.
  3. Medium — bb thread log --all does not honor the new byte-bounded pagination contract. It always requests 100 raw events. If retained outputs push that page over 8 MiB, the endpoint returns 413 and the CLI aborts instead of retrying with a smaller page.

Architecture note: the central retained-output target table is a good consolidation. The two incremental migration drivers in packages/db/src/data/sweeps.ts now duplicate most of their cursor/window/update machinery; a shared internal driver with strategy-specific selectors/preparers would reduce drift, but I consider that optional once the correctness issues above are covered.

Performance review found no additional regressions. The partial indexes, bounded response hydration, cursor windows, expiry index, and yields are all exercised; a local migration benchmark measured 36 KiB rows at 0.40 ms p50 / 0.96 ms p95 synchronous work per advance and 1 MiB rows at 1.31 ms p50 (2.60 ms max).

Verification on 07fa5fd0: @bb/db passed 469/469, @bb/client-core 253/253, @bb/cli 530/530, and 52 focused server tests passed. A full server run passed 2,214 tests with three unrelated five-second timeouts. In the live dev app, I created a real thread, inserted a 4 MiB generated-image completion through the DB data layer, confirmed the stored event shrank to 4,531 bytes with the exact 4,194,328-byte value retained in the sidecar, and used Chrome/doobie to expand the turn. The UI rendered a compact non-expandable Generated image row and did not put the encoded image prefix in the DOM. I separately reproduced the 8 MiB raw-page failure and the CLI abort.

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