Skip to content

Slice 3.3 — periodic classifier: extraction, reconciliation, provenance, barrier - #406

Open
Pento95 wants to merge 37 commits into
mainfrom
slice/m3.3-periodic-classifier
Open

Slice 3.3 — periodic classifier: extraction, reconciliation, provenance, barrier#406
Pento95 wants to merge 37 commits into
mainfrom
slice/m3.3-periodic-classifier

Conversation

@Pento95

@Pento95 Pento95 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

Slice 3.3 — the periodic classifier: the background pass that populates the
structured graph retrieval queries against. Ships the periodic-classifier
pipeline declaration, its cadence scheduler and auto-retry policy over
per-branch classifier_status, the extraction → reconciliation → planner
chain, per-fact survival-anchor provenance, and the C3 prose-reversal barrier
that Slice 3.9
and Slice 3.10
build on.

Slice doc: 03-classifier.md.

What lands

  • Pipeline + scheduler. periodic-classifier (no-gate, self-blocking
    and blocked by chapter-close, pill-only affordance), a pure per-branch
    scheduler wired off pipelineEventBus's run_complete, 30s → 2m → 5m
    backoff, cadence suspension in failed-persistent, and an exported manual
    run as the only escape from it.
  • Extraction → writes. One structured call over the window past
    processedThrough; happenings with involvements and severity-judged
    awareness, relationship UPSERT-merge, status flips, first-introduction
    descriptions. Nothing embeds on the write path.
  • Disambiguation. Name index, then a transient in-memory similarity check
    against every namesake, τ-banded into create / promote / flag.
  • Barrier (C3). awaitRunTerminal moved into the generation store;
    bracketProseReversal as the single sanctioned entry to the reversal sweep,
    carrying the classifier drain and the processedThrough clamp.
  • Cascade. deleteHappening now drops and restores its involvement and
    awareness rows through registry hooks, on undo and redo alike.

Review pass

A critical review of the finished branch found four defects that shipped green
under the existing tests; the last two commits fix them and cover them.

  • The window was read from entriesStore, which holds only the last 50
    entries.
    Any backlog older than the reader's window was skipped, and the
    watermark then advanced past it — prose permanently never classified. Now
    read from SQLite. Unit tests that hydrate the store are structurally unable
    to catch this, so it is covered end to end.
  • A newCharacters handle colliding with a live placeholder silently
    misattributed facts.
    The return trip substitutes refs before the planner
    runs, so a model naming a new character c1 had every ref to it rewritten
    into the existing entity c1 — no unresolved ref, no warning. The prompt now
    reserves a new: namespace and the walker refuses to rewrite any ref a
    declared handle claims, so a non-compliant model cannot reach the failure.
  • Pre-flight failures never armed the retry lifecycle. An unassigned
    classifier agent halts before phase 0, so the phase's failure bookkeeping
    never ran: status stayed idle, no backoff, no lastError, and the cadence
    re-fired the doomed run on every committed turn.
  • Disambiguation had two silent-wrong-answer paths. cosine truncated to
    the shorter vector, fabricating a similarity from a shared prefix when the
    two sides came back on different models mid embedder-swap; and the namesake
    index was first-match-wins, which degrades exactly as create-with-flag
    manufactures more namesakes.

The slice doc is reconciled against this: two of its Implementation notes had
recorded properties the code no longer has, including a "coherent by
construction" claim about watermark-after that rested on a boot recovery path
that does not exist for this pipeline.

Known gap (filed, not fixed)

A crash between the delta burst and the watermark write leaves the deltas on
disk with processedThrough unmoved, and the replay duplicates the window's
happenings — there is no run marker and no idempotency key. Closing it touches
the orchestrator's transactional contract, so it is a
followup rather than a fix here.

Test Plan

  • pnpm typecheck
  • pnpm lint
  • pnpm vitest run --project unit — 1865 passing
  • pnpm test:e2e — 25 passing (rebuild the bundle first: the dev
    project launches Electron against a static dist/, so
    pnpm build:web && pnpm electron:compile must run or the suite
    silently exercises stale code)
  • Manual smoke, still outstanding per the slice doc: real provider,
    classifierCadence 2, three turns — confirm graph population and watch
    whether classifier.window_head_fallback warnings dominate the log
    (that is the signal for the M7.5 tuning pass, not a bug)

Summary by CodeRabbit

  • New Features
    • Added automatic periodic story classification for characters, happenings, relationships, awareness, and status changes.
    • Added configurable processing windows, retry handling, and manual classifier runs.
    • Added embedding-assisted character matching and staged-entity promotion.
  • Bug Fixes
    • Improved status messaging and cancellation for foreground versus background activity.
    • Undo and redo now fully restore or remove related story data.
    • Rollbacks keep classifier progress synchronized with restored content.
    • Redundant updates no longer create unnecessary history entries.
    • Generation prompts now receive only approved entity details.

Pento added 30 commits July 30, 2026 13:00
C3 needs it reachable from lib/actions' reversal path without importing
lib/pipeline. Mirrors M2.2's gate move.
One bracket owns the classifier drain and reversalInProgress window; the
clamp ships as ops spliced into the sweep transaction so it can never land
without the reversal. Slices 3.9/3.10 consume this, not the sweep.

B is the target entry itself on both arms: rollbackToEntry removes the
clicked entry (02-entry-arms.md), not just its successors.
json_set on one key, not a read-modify-write of classifier_status: the
classifier writes state/retryCount on the same blob, including on the
teardown of the run the bracket just aborted. WHERE guard subsumes both
no-op cases, so the clamp needs no ctx. Also: bracket rejects re-entry,
extra ops survive an empty row set, concurrency uses the shared kind.
The M1.5 arm orphaned both link tables; the periodic classifier's
reconcile path is the first consumer that can produce the orphans.
The reverse-replay engine now stays generic: tables that cascade deletes
declare a restoreCascade hook in their registration to extract and
re-insert children on undo. This avoids hardcoding domain-specific
behavior into the generic engine and prevents accidental key deletion
for unrelated tables.
Adds cascadeDeleteOps hook for redo path (was orphaning children on
delete→undo→redo). Inverts CascadeRestore to return cascadeKeys so
engine strips them (prevents accidental forgetting). Single
buildChildDeleteOps helper shared by forward delete and redo.
Round-trip delete→undo→redo test verifies cascaded child deletes sync
to stores. CascadeDeleteOps now returns child rows so redo can emit
proper delete patches. Test confirms fix catches the bug.
Cosine errors no longer silently read as no-signal. Adds exact
TAU_HIGH/TAU_LOW boundary tests, wrong-vector-count and
null-description edge cases, and documents cosine's rounding on
the export itself.
Every emitted delta carries its own survival anchor: single-turn to that
turn, awareness to the turn that narrated the learning, sub-rows to the
parent, unattributed to the window head.
occurred_at_entry_id no longer head-falls back: a bogus handle is a
story-time claim, so it degrades to temporal and is recorded instead.
Awareness and relationship refs now require a character; involvements
stay polymorphic. The entity snapshot became a mutable index, so flip
guards see same-reply promotes/creates and duplicate flips collapse.
retryDelayForStatus owns the attempt-count to delay mapping; the scheduler
re-reads persisted status rather than indexing BACKOFF_MS a second time, so
the reducer and the scheduler cannot drift by one.
inFlight/timer were module-global despite a per-branch API: a retry
pending for branch A was destroyed by a cadence tick for branch B.
Key both under a per-branch map. runNow() now returns
StartRunOutcome | {outcome:'busy'} instead of silently declining, so
[Run classifier now] can report why nothing happened.
buildClassifierContext built lore/definition/calendarVocabulary that
the template never read; removed from the builder and VARIABLES so
the group matches template consumption. The parity test now asserts
set equality instead of one-directional presence, since renderTemplate
does no context validation. Template also fixes nil-unsafe `size == 0`
checks and tightens Liquid whitespace control.
One structured call, one abort-free commit burst, watermark written after
the deltas so a crash leaves recovery's reverse-replay consistent.
An empty window can still be advanceable: coversThrough counts capped
candidates before the system-entry filter, so returning early left the
cadence re-firing forever. Also match narrativePhase's storyId guard.
PLACEHOLDER_FIELDS mirrors plan.ts's resolveRef sites, so it belongs
beside the schema; a schema-derived partition test is the link the two
lists lacked. Covers the success status write and the reconcile seam,
both of which survived mutation.
Narrows the reader's isGenerating to foreground kinds: a no-gate
background classifier run was lighting up the streaming placeholder
and gating the composer. Persists classifier_status.state: 'running'
at run start (key-scoped json_set, restored on abort) so the M7.2
status panel gets its Running state and shouldCadenceFire's guard
is live.
state: 'running' has no reporting path and no way out in v1 (the
manual-run UI is M7.2), so a crash mid-pass would silently stop the
classifier forever — the exact memory-decay failure this milestone
exists to prevent. Reset it to idle as a boot-time orphan sweep,
key-scoped to $.state so retrying/failed-persistent (real, reported
errors) and processedThrough are untouched.
Two specs: the graph-population happy path, and a disambiguation-seam
regression test for the "nobody calls configureClassifierEmbedder"
defect, invisible to unit tests because each half (injected embedder,
boot wiring) passes while the composition is broken. Verified red
before the bootstrap fix, green after.
Records the three decisions that constrain future slices: the unbuilt
field-scoped entities.status gate, watermark-after-burst and why it is
correct against boot recovery, and bracketProseReversal as the only
entry to the sweep. Involvement drift outlived the slice and moves to
followups; the Storybook full-suite flake is filed there too, confirmed
on main.
The pass read its window from entriesStore, which holds only the last
ENTRIES_WINDOW_SIZE entries, so older backlog was skipped and then buried
under the advancing watermark; it now reads story_entries. Also: handle /
placeholder collisions rebound refs, reconciliation scored one namesake on
possibly mismatched dims, and pre-flight failures armed no backoff.
Two notes asserted properties the code no longer has: watermark-after was
called "coherent by construction" on the premise that boot reverse-replays
an interrupted pass (nothing does), and the cosine truncation plus
first-match namesake index were recorded as deliberate. Adds the window
source, the reserved handle namespace, and pre-flight failure recording.
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Pento95, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 27 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 20488842-2f98-48bf-b46d-1ebf09734bb8

📥 Commits

Reviewing files that changed from the base of the PR and between 7d667c1 and d6f7612.

⛔ Files ignored due to path filters (4)
  • docs/followups.md is excluded by !docs/**
  • docs/implementation/lessons-learned/README.md is excluded by !docs/**
  • docs/implementation/lessons-learned/staleness-flags-are-cleared-by-the-drain.md is excluded by !docs/**
  • docs/implementation/triage.md is excluded by !docs/**
📒 Files selected for processing (22)
  • app/reader-composer/[branchId].tsx
  • app/story-settings/[storyId].tsx
  • components/compounds/generation-status-pill.tsx
  • e2e/harness/mock-llm.ts
  • e2e/tests/classifier-real-provider.smoke.spec.ts
  • lib/actions/delta/reverse-replay.test.ts
  • lib/actions/delta/reverse-replay.ts
  • lib/actions/index.ts
  • lib/actions/suggestions/refresh-suggestions.test.ts
  • lib/db/app-settings/app-settings-defaults.ts
  • lib/pipeline/definitions/generation-context.test.ts
  • lib/pipeline/definitions/generation-context.ts
  • lib/pipeline/definitions/periodic-classifier.test.ts
  • lib/pipeline/definitions/periodic-classifier.ts
  • lib/pipeline/index.ts
  • lib/pipeline/runtime/orchestrator.ts
  • lib/prompts/bundled/index.ts
  • lib/prompts/ids.ts
  • lib/prompts/templateContextMap.ts
  • lib/stores/generation/generation.ts
  • lib/stores/index.ts
  • scripts/e2e.ts
📝 Walkthrough

Walkthrough

The PR adds periodic classifier extraction, reconciliation, scheduling, retries, provenance tracking, and E2E coverage. It also adds cascade-aware undo and redo handling, classifier watermark clamping, and separate foreground and background generation state.

Changes

Periodic classifier and pipeline

Layer / File(s) Summary
Classifier contracts, planning, and execution
lib/classifier/*, lib/actions/classifier/*, lib/pipeline/definitions/periodic-classifier.ts
Adds extraction schemas, bounded windows, ID substitution, embedding-based entity reconciliation, action planning, status transitions, retries, watermark persistence, and structured classifier execution.
Classifier scheduling and boot wiring
lib/classifier/scheduler.ts, lib/boot/bootstrap.ts, lib/boot/classifier-scheduler.test.ts
Adds per-branch cadence scheduling, manual runs, retry timers, in-flight protection, shutdown handling, embedder setup, and recovery of stuck runs.
Classifier prompts and context
lib/prompts/*, lib/pipeline/definitions/classifier-context.ts
Adds the periodic classifier prompt, context mapping, field projection, provenance handles, and placeholder substitution.
Classifier tests and E2E coverage
lib/classifier/*.test.ts, lib/pipeline/definitions/periodic-classifier.test.ts, e2e/tests/*classifier*
Adds unit, integration, mock-provider, and real-provider coverage for classifier extraction, reconciliation, scheduling, window limits, failures, and persisted output.
Classifier configuration
lib/db/app-settings/*, lib/stores/app-settings/app-settings.ts
Adds the positive-integer classifierWindowMaxEntries setting with a default of 20.

Cascade-aware undo and redo

Layer / File(s) Summary
Cascade replay and happening deletion
lib/actions/delta/*, lib/actions/happenings/register-happenings.ts
Adds cascade restore and delete handlers. Redo and reverse replay now process child rows and emit child patch events. Happening deletion includes involvement and awareness rows in delete and restore flows.
Prose reversal and watermark integration
lib/actions/story-entries/*
Adds reversal bracketing, classifier cancellation and draining, nested-reversal protection, and conditional watermark clamping during rollback and undo operations.

Generation state and UI

Layer / File(s) Summary
Foreground and background run state
lib/stores/generation/*, lib/stores/index.ts
Separates foreground generation from periodic classifier activity by branch. Terminal waiting and cancellation now use a required branch ID.
Generation status UI
components/compounds/generation-status-pill.tsx, app/reader-composer/[branchId].tsx, app/story-settings/[storyId].tsx
Displays classifier activity separately and exposes cancellation only for foreground generation.

Pipeline hardening and exports

Layer / File(s) Summary
Pipeline contracts and exports
lib/pipeline/index.ts, lib/pipeline/runtime/*, lib/actions/index.ts, lib/classifier/index.ts
Exports classifier APIs and shared constants. No-op action rejections now complete without delta writes.
E2E build validation
scripts/e2e.ts
Adds stale or missing build-output checks with an explicit environment-variable bypass.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ForegroundTurn
  participant Bootstrap
  participant ClassifierScheduler
  participant PeriodicClassifier
  participant StructuredGeneration
  participant Database
  ForegroundTurn->>Bootstrap: emit completed turn
  Bootstrap->>ClassifierScheduler: trigger branch evaluation
  ClassifierScheduler->>Database: read cadence and classifier state
  ClassifierScheduler->>PeriodicClassifier: start classifier run
  PeriodicClassifier->>Database: load window and context
  PeriodicClassifier->>StructuredGeneration: request structured extraction
  StructuredGeneration-->>PeriodicClassifier: return extraction
  PeriodicClassifier->>Database: persist actions, status, and watermark
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: failerko

Poem

A rabbit checks the classifier queue,
Maps each handle to a name that is true.
Watermarks advance, then safely rewind,
Background runs stay out of the cancel line.
The story grows steady beneath moonlight blue.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the periodic classifier slice and its main capabilities, including extraction, reconciliation, provenance, and the reversal barrier.
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

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.

@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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/actions/story-entries/undo.ts (1)

72-99: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Wrap redo inside bracketProseReversal.

applyRedo writes deltas and patched story rows, but redoLastAction only sets reversalInProgress; it does not drain the in-flight periodic classifier first. Keep the manual guard/finally around the helper, and run the actual applyRedo() block under the prose-reversal barrier so redo cannot race a live classifier pass.

🤖 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 `@lib/actions/story-entries/undo.ts` around lines 72 - 99, The actual redo
application in redoLastAction must run under bracketProseReversal so it cannot
race the periodic classifier. Preserve the existing generation guard and
finally-based reversalInProgress cleanup, but wrap the applyRedo call and its
committed-error handling in the prose-reversal barrier.
🧹 Nitpick comments (12)
lib/classifier/reconcile.ts (2)

23-35: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider rejecting mismatched vector lengths inside cosine.

cosine scores the shared prefix when the two vectors differ in length. reconcileNewCharacter prevents that case through its usable guard, so the current behavior is safe. cosine is also exported from lib/classifier/index.ts, so a future caller can reintroduce the fabricated-similarity failure mode that the guard exists to block. Return 0 on a length mismatch to make the invariant local to the function.

♻️ Proposed change
 export function cosine(a: Float32Array, b: Float32Array): number {
-  const n = Math.min(a.length, b.length)
+  if (a.length !== b.length) return 0
+  const n = a.length
🤖 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 `@lib/classifier/reconcile.ts` around lines 23 - 35, Update the exported cosine
function to return 0 immediately when the input Float32Array lengths differ,
before calculating the shared-length loop; preserve the existing zero-norm
handling and similarity calculation for equal-length vectors.

58-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Record the embedder failure before degrading to no-signal.

The catch block discards the error. A sustained embedder outage then surfaces only as flagged creates on the collision-review surface, with no signal that reconciliation never had vectors. Log the error at warn level so the cause is diagnosable.

♻️ Proposed change
   let vectors: Float32Array[] | null = null
   try {
     const result = await deps.embedDescriptions([
       candidate.description,
       ...namesakes.map((n) => n.description ?? ''),
     ])
     vectors = result.vectors
-  } catch {
+  } catch (error) {
+    console.warn('[classifier] namesake embedding failed; flagging create', error)
     vectors = null
   }
🤖 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 `@lib/classifier/reconcile.ts` around lines 58 - 67, Update the catch block
around embedDescriptions in the reconciliation flow to capture the thrown error
and log it at warn level before setting vectors to null. Preserve the existing
no-signal fallback while including enough context and the original error for
diagnosis.
lib/prompts/templateContextMap.ts (1)

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

Update the documented turns type to match what the context actually carries.

The registry declares turns as WindowTurn[], which includes entryId and position. buildClassifierContext in lib/pipeline/definitions/classifier-context.ts projects each turn to { handle, content } only — the same narrowing pattern already applied to happenings here. A reader of this registry can wrongly assume entryId or position are available to the template.

📝 Proposed fix
     {
       name: 'turns',
-      type: 'WindowTurn[]',
+      type: '{ handle, content }[]',
       category: 'Story',
       description:
         'Unclassified prose window, each turn carrying the provenance handle (t1..tN) the model must tag its facts with.',
       required: true,
     },
🤖 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 `@lib/prompts/templateContextMap.ts` around lines 78 - 86, Update the
`classifierContext` registry entry’s `turns` type from `WindowTurn[]` to the
projected `{ handle: string; content: string }[]` shape produced by
`buildClassifierContext`, matching the existing narrowed `happenings`
declaration and removing the implication that `entryId` or `position` are
available.
lib/classifier/window.ts (1)

19-29: 🩺 Stability & Availability | 🔵 Trivial

Guard against a non-positive maxEntries.

If maxEntries is 0 or negative, candidates.slice(0, maxEntries) always returns an empty array. coversThrough then falls back to floor on every call and never advances past it. The classifier keeps reprocessing the same window forever without making progress.

Add an explicit guard so a misconfigured maxEntries fails loudly instead of stalling silently.

🛡️ Proposed guard
 export function buildClassifierWindow(args: {
   entries: readonly StoryEntry[]
   processedThrough: number | null
   maxEntries: number
 }): ClassifierWindow {
   const { entries, processedThrough, maxEntries } = args
+  if (maxEntries <= 0) {
+    throw new RangeError(`maxEntries must be positive, got ${maxEntries}`)
+  }
   const floor = processedThrough ?? 0

Please confirm whether the caller (app settings schema) already enforces a minimum of 1 for this value; if so, this guard is still cheap insurance against a future caller that does not.

🤖 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 `@lib/classifier/window.ts` around lines 19 - 29, Add an explicit validation at
the start of buildClassifierWindow that rejects maxEntries values less than 1,
using the project’s established error type or assertion style so
misconfiguration fails immediately before candidates.slice is reached.
lib/db/app-settings/app-settings-schema.ts (1)

86-89: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider an upper bound for classifierWindowMaxEntries.

The field accepts any positive safe integer. A large value makes readWindowEntries load that many rows and buildClassifierContext render them all into one prompt, which can exceed the model context window. An explicit .max() keeps the truncation cap self-limiting.

♻️ Proposed bound
-  classifierWindowMaxEntries: z.number().int().positive().default(20),
+  classifierWindowMaxEntries: z.number().int().positive().max(200).default(20),
🤖 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 `@lib/db/app-settings/app-settings-schema.ts` around lines 86 - 89, Update the
classifierWindowMaxEntries schema definition to enforce an explicit maximum
value in addition to its existing positive-integer validation and default, using
the project’s intended safe prompt-size bound.
e2e/harness/seed.ts (1)

110-113: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add row-existence preconditions to the three new seed helpers. Each helper casts a query result to a non-optional shape without checking that the target row exists. A wrong id then either throws an opaque TypeError inside beforeAll or silently produces a setup the spec cannot detect.

  • e2e/harness/seed.ts#L110-L113: type the stories row as optional and throw an error that names the missing storyId.
  • e2e/harness/seed.ts#L138-L159: assert that branchId exists before the insert loop, because COALESCE(MAX(position), 0) and the UPDATE branches both succeed for an unknown branch.
  • e2e/harness/seed.ts#L169-L176: type the app_settings row as optional and throw an error when the singleton row is absent.
🤖 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 `@e2e/harness/seed.ts` around lines 110 - 113, Add row-existence checks to the
three seed helpers in e2e/harness/seed.ts: at lines 110-113, type the stories
query result as optional and throw an error naming the missing storyId before
parsing settings; at lines 138-159, verify branchId exists before the insert
loop; at lines 169-176, type the app_settings query result as optional and throw
when the singleton row is absent.
lib/pipeline/definitions/periodic-classifier.test.ts (1)

67-84: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the clamp(0) call at line 74.

At line 74 classifier_status is still NULL, because the branches insert at line 60 does not set it. json_set returns NULL when its first argument is NULL, so the statement writes NULL and changes nothing. The next statement at line 75 replaces the whole column anyway. The call reads as a baseline seed that it is not.

♻️ Proposed cleanup
-  clamp(0)
   sqlite.prepare(`UPDATE branches SET classifier_status = ? WHERE id = 'b1'`).run(
🤖 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 `@lib/pipeline/definitions/periodic-classifier.test.ts` around lines 67 - 84,
Remove the clamp helper and its clamp(0) invocation from the test setup before
the classifier_status initialization; the subsequent UPDATE that seeds the
complete classifier_status JSON should remain unchanged.
lib/actions/delta/redo.ts (1)

34-34: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Key cascadeInfo by table and target ID.

cascadeInfo uses only delta.targetId as the key. A single redo batch can contain delete deltas for more than one registered table. If two such deltas ever share a targetId, the second set overwrites the first, and the post-commit loop emits child delete patches for the wrong parent. Current ID prefixes make this collision unlikely, so this is a hardening change rather than a live defect.

♻️ Proposed composite key
-      if (entry.cascadeDeleteOps) {
+      if (entry.cascadeDeleteOps) {
         const { ops: childOps, children } = await entry.cascadeDeleteOps(
           delta.branchId,
           delta.targetId,
           ctx,
         )
         ops.push(...childOps)
-        cascadeInfo.set(delta.targetId, children)
+        cascadeInfo.set(`${delta.targetTable}:${delta.targetId}`, children)
       }
-        const children = cascadeInfo.get(delta.targetId)
+        const children = cascadeInfo.get(`${delta.targetTable}:${delta.targetId}`)

Also applies to: 69-70

🤖 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 `@lib/actions/delta/redo.ts` at line 34, Update cascadeInfo and its access
sites in the redo flow to use a composite key containing both the registered
table and delta.targetId, ensuring delete cascades from different tables cannot
overwrite each other. Preserve the existing child-delete patch behavior by using
the same composite key when populating and reading cascadeInfo.
lib/actions/happenings/register-happenings.ts (1)

153-192: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hoist the repeated child predicates.

The same and(eq(branchId), eq(happeningId)) expression appears four times, twice per child table. Bind one predicate per table and reuse it for the select and the delete. This keeps the read filter and the delete filter provably identical.

♻️ Proposed refactor
 async function buildChildDeleteOps(branchId: string, happeningId: string, ctx: DbCtx) {
-  const involvements = await ctx.db
-    .select()
-    .from(happeningInvolvements)
-    .where(
-      and(
-        eq(happeningInvolvements.branchId, branchId),
-        eq(happeningInvolvements.happeningId, happeningId),
-      ),
-    )
-  const awareness = await ctx.db
-    .select()
-    .from(happeningAwareness)
-    .where(
-      and(
-        eq(happeningAwareness.branchId, branchId),
-        eq(happeningAwareness.happeningId, happeningId),
-      ),
-    )
+  const invWhere = and(
+    eq(happeningInvolvements.branchId, branchId),
+    eq(happeningInvolvements.happeningId, happeningId),
+  )
+  const awareWhere = and(
+    eq(happeningAwareness.branchId, branchId),
+    eq(happeningAwareness.happeningId, happeningId),
+  )
+  const involvements = await ctx.db.select().from(happeningInvolvements).where(invWhere)
+  const awareness = await ctx.db.select().from(happeningAwareness).where(awareWhere)
 
   const ops = [
-    ctx.db
-      .delete(happeningInvolvements)
-      .where(
-        and(
-          eq(happeningInvolvements.branchId, branchId),
-          eq(happeningInvolvements.happeningId, happeningId),
-        ),
-      )
-      .toSQL(),
-    ctx.db
-      .delete(happeningAwareness)
-      .where(
-        and(
-          eq(happeningAwareness.branchId, branchId),
-          eq(happeningAwareness.happeningId, happeningId),
-        ),
-      )
-      .toSQL(),
+    ctx.db.delete(happeningInvolvements).where(invWhere).toSQL(),
+    ctx.db.delete(happeningAwareness).where(awareWhere).toSQL(),
   ]
🤖 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 `@lib/actions/happenings/register-happenings.ts` around lines 153 - 192, Update
buildChildDeleteOps to hoist one branchId/happeningId predicate for
happeningInvolvements and one for happeningAwareness, then reuse each
corresponding predicate in both the select and delete queries so their filters
remain identical.
lib/actions/story-entries/prose-reversal.test.ts (1)

99-109: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case for a present status with processedThrough: null.

The suite covers processedThrough above the clamp, below the clamp, and a fully absent classifierStatus. It does not cover a stored status whose processedThrough is null. The status helper already accepts null, so the case is one call. For that row json_extract(classifier_status, '$.processedThrough') yields SQL NULL, the > comparison yields NULL, and the update must not match. Pinning that behavior protects the branch that has a status blob but no watermark yet.

💚 Proposed test
   it('no-ops when the branch has no classifier status yet', async () => {
     const { runInTransaction, read } = await seedBranch(null)
     await runInTransaction(classifierWatermarkClampOps('b1', 5))
     expect(await read()).toBeNull()
   })
+
+  it('no-ops when the status exists but has no watermark yet', async () => {
+    const { runInTransaction, read } = await seedBranch(status(null))
+    await runInTransaction(classifierWatermarkClampOps('b1', 5))
+    expect(await read()).toEqual(status(null))
+  })
 })
🤖 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 `@lib/actions/story-entries/prose-reversal.test.ts` around lines 99 - 109, Add
a test beside the existing classifier watermark clamp no-op cases using
seedBranch(status(null)), invoke classifierWatermarkClampOps('b1', 5), and
assert the stored status remains status(null). This should specifically cover a
present classifier status whose processedThrough value is null without changing
the existing absent-status or numeric watermark cases.
lib/actions/delta/registry.ts (1)

19-37: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider one shape for cascaded child rows.

CascadeRestore returns children as an array of { table, rows }. CascadeDeleteOps returns children as Record<table, rows>. Both express the same concept: child table name to rows. Two shapes force each consumer to handle a different traversal (for..of in reverse-replay, Object.entries in applyRedo). Aligning both on the record form would simplify the engine code and reduce the chance of mismatched handling in future cascade domains.

🤖 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 `@lib/actions/delta/registry.ts` around lines 19 - 37, Unify cascaded child-row
output on the record shape used by CascadeDeleteOps: update CascadeRestore to
return children keyed by table name, then adjust its consumers to iterate the
record consistently while preserving the existing rows and cascadeKeys behavior.
lib/actions/happenings/register-happenings.test.ts (1)

215-259: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated seed block.

Four tests repeat the same setup: create hap_1 through applyDeltaAction, then insert inv_1 and haw_1. The only variation is decayResistance (null versus 0.5). Extract one helper, for example seedHappeningWithChildren(db, ctx, { decayResistance }), and call it from each test. This removes about 80 duplicated lines and keeps the child-row fixture in one place.

Also applies to: 261-312, 314-409, 447-517

🤖 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 `@lib/actions/happenings/register-happenings.test.ts` around lines 215 - 259,
Extract the repeated happening setup from the affected tests into a shared
helper such as seedHappeningWithChildren, accepting db, ctx, and
decayResistance. Move the createHappening action plus happeningInvolvements and
happeningAwareness inserts into that helper, then replace each duplicated setup
block in the referenced tests with helper calls while preserving each test’s
decayResistance value.
🤖 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 `@lib/actions/classifier/deps.ts`:
- Around line 70-85: Update recordClassifierPreflightFailure to persist the
complete status returned by nextStatusOnFailure as a single JSON value, using
Drizzle’s JSON serialization rather than json_set path updates. Replace the
partial-field update while preserving the existing branch lookup and
failure-state calculation, so classifier_status always contains all required
ClassifierStatus fields.

In `@lib/actions/happenings/register-happenings.ts`:
- Around line 153-200: Update buildChildDeleteOps and its
deleteHappening/applyRedo callers so child-row reads and deletes are serialized
per happening: either acquire the same happening-specific lock used by
applyDeltaAction before reading, or move both SELECTs and DELETE operations into
one transaction/critical section. Ensure concurrent creation of
happeningInvolvements or happeningAwareness rows cannot occur between building
the undo payload and deleting the rows.

In `@lib/actions/story-entries/operational.test.ts`:
- Around line 314-316: Update the no-op rollback test around rollbackToEntry to
capture its return value and assert the successful status before checking
branches.processedThrough. Follow the sibling test’s success-status assertion
pattern so a rejection such as STORY_ENTRY_REJECTION.inFlight cannot satisfy the
clamp assertion.

In `@lib/actions/story-entries/prose-reversal.ts`:
- Around line 13-23: The awaitRunTerminal call in bracketProseReversal searches
globally by PERIODIC_CLASSIFIER_KIND and may cancel a classifier run on a
different branch. Either scope the classifier wait to the current branch before
calling awaitRunTerminal, or document that global classifier shutdown is
intentional behavior. Additionally, add a bounded wait with a timeout when
awaiting run.terminal since the current unbounded await has no visible timeout
protection while classifier commit and abort operations lack explicit timeout
guarantees.

In `@lib/pipeline/definitions/classifier-context.ts`:
- Around line 15-21: Update classifierContextMap’s context construction so
entities are mapped to only id, kind, name, description, status, and
injectionMode before being passed to substituteIds. Preserve the existing turns
and happenings projections, and prevent additional Entity row fields from
reaching the model prompt.

In `@lib/stores/generation/generation.ts`:
- Around line 134-138: Update awaitRunTerminal to require branchId and select
the run matching both branchId and kind before aborting or awaiting it.
Propagate the new argument through the reader, story settings, and
prose-reversal callers, and add a regression test proving waits and cancellation
remain isolated between two branches.

---

Outside diff comments:
In `@lib/actions/story-entries/undo.ts`:
- Around line 72-99: The actual redo application in redoLastAction must run
under bracketProseReversal so it cannot race the periodic classifier. Preserve
the existing generation guard and finally-based reversalInProgress cleanup, but
wrap the applyRedo call and its committed-error handling in the prose-reversal
barrier.

---

Nitpick comments:
In `@e2e/harness/seed.ts`:
- Around line 110-113: Add row-existence checks to the three seed helpers in
e2e/harness/seed.ts: at lines 110-113, type the stories query result as optional
and throw an error naming the missing storyId before parsing settings; at lines
138-159, verify branchId exists before the insert loop; at lines 169-176, type
the app_settings query result as optional and throw when the singleton row is
absent.

In `@lib/actions/delta/redo.ts`:
- Line 34: Update cascadeInfo and its access sites in the redo flow to use a
composite key containing both the registered table and delta.targetId, ensuring
delete cascades from different tables cannot overwrite each other. Preserve the
existing child-delete patch behavior by using the same composite key when
populating and reading cascadeInfo.

In `@lib/actions/delta/registry.ts`:
- Around line 19-37: Unify cascaded child-row output on the record shape used by
CascadeDeleteOps: update CascadeRestore to return children keyed by table name,
then adjust its consumers to iterate the record consistently while preserving
the existing rows and cascadeKeys behavior.

In `@lib/actions/happenings/register-happenings.test.ts`:
- Around line 215-259: Extract the repeated happening setup from the affected
tests into a shared helper such as seedHappeningWithChildren, accepting db, ctx,
and decayResistance. Move the createHappening action plus happeningInvolvements
and happeningAwareness inserts into that helper, then replace each duplicated
setup block in the referenced tests with helper calls while preserving each
test’s decayResistance value.

In `@lib/actions/happenings/register-happenings.ts`:
- Around line 153-192: Update buildChildDeleteOps to hoist one
branchId/happeningId predicate for happeningInvolvements and one for
happeningAwareness, then reuse each corresponding predicate in both the select
and delete queries so their filters remain identical.

In `@lib/actions/story-entries/prose-reversal.test.ts`:
- Around line 99-109: Add a test beside the existing classifier watermark clamp
no-op cases using seedBranch(status(null)), invoke
classifierWatermarkClampOps('b1', 5), and assert the stored status remains
status(null). This should specifically cover a present classifier status whose
processedThrough value is null without changing the existing absent-status or
numeric watermark cases.

In `@lib/classifier/reconcile.ts`:
- Around line 23-35: Update the exported cosine function to return 0 immediately
when the input Float32Array lengths differ, before calculating the shared-length
loop; preserve the existing zero-norm handling and similarity calculation for
equal-length vectors.
- Around line 58-67: Update the catch block around embedDescriptions in the
reconciliation flow to capture the thrown error and log it at warn level before
setting vectors to null. Preserve the existing no-signal fallback while
including enough context and the original error for diagnosis.

In `@lib/classifier/window.ts`:
- Around line 19-29: Add an explicit validation at the start of
buildClassifierWindow that rejects maxEntries values less than 1, using the
project’s established error type or assertion style so misconfiguration fails
immediately before candidates.slice is reached.

In `@lib/db/app-settings/app-settings-schema.ts`:
- Around line 86-89: Update the classifierWindowMaxEntries schema definition to
enforce an explicit maximum value in addition to its existing positive-integer
validation and default, using the project’s intended safe prompt-size bound.

In `@lib/pipeline/definitions/periodic-classifier.test.ts`:
- Around line 67-84: Remove the clamp helper and its clamp(0) invocation from
the test setup before the classifier_status initialization; the subsequent
UPDATE that seeds the complete classifier_status JSON should remain unchanged.

In `@lib/prompts/templateContextMap.ts`:
- Around line 78-86: Update the `classifierContext` registry entry’s `turns`
type from `WindowTurn[]` to the projected `{ handle: string; content: string
}[]` shape produced by `buildClassifierContext`, matching the existing narrowed
`happenings` declaration and removing the implication that `entryId` or
`position` are available.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b4efed80-437a-4016-b5a0-3f8bcfb3ba47

📥 Commits

Reviewing files that changed from the base of the PR and between 5452859 and b992a07.

⛔ Files ignored due to path filters (3)
  • docs/followups.md is excluded by !docs/**
  • docs/implementation/milestones/03-memory-floor/slices/03-classifier.md is excluded by !docs/**
  • lib/prompts/bundled/__snapshots__/periodic-classifier.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (64)
  • app/reader-composer/[branchId].tsx
  • app/story-settings/[storyId].tsx
  • components/compounds/generation-status-pill.tsx
  • e2e/harness/mock-llm.ts
  • e2e/harness/seed.ts
  • e2e/tests/periodic-classifier.spec.ts
  • lib/actions/classifier/deps.test.ts
  • lib/actions/classifier/deps.ts
  • lib/actions/classifier/run-now.test.ts
  • lib/actions/classifier/run-now.ts
  • lib/actions/delta/redo.ts
  • lib/actions/delta/registry.ts
  • lib/actions/delta/reverse-replay.test.ts
  • lib/actions/delta/reverse-replay.ts
  • lib/actions/embedder-swap/index.ts
  • lib/actions/happenings/register-happenings.test.ts
  • lib/actions/happenings/register-happenings.ts
  • lib/actions/index.ts
  • lib/actions/story-entries/operational.test.ts
  • lib/actions/story-entries/operational.ts
  • lib/actions/story-entries/prose-reversal.test.ts
  • lib/actions/story-entries/prose-reversal.ts
  • lib/actions/story-entries/undo.test.ts
  • lib/actions/story-entries/undo.ts
  • lib/boot/bootstrap.ts
  • lib/boot/classifier-scheduler.test.ts
  • lib/classifier/index.ts
  • lib/classifier/kind.ts
  • lib/classifier/plan.test.ts
  • lib/classifier/plan.ts
  • lib/classifier/reconcile.test.ts
  • lib/classifier/reconcile.ts
  • lib/classifier/scheduler.test.ts
  • lib/classifier/scheduler.ts
  • lib/classifier/schema.test.ts
  • lib/classifier/schema.ts
  • lib/classifier/status.test.ts
  • lib/classifier/status.ts
  • lib/classifier/substitute.test.ts
  • lib/classifier/substitute.ts
  • lib/classifier/window.test.ts
  • lib/classifier/window.ts
  • lib/db/app-settings/app-settings-defaults.ts
  • lib/db/app-settings/app-settings-schema.test.ts
  • lib/db/app-settings/app-settings-schema.ts
  • lib/pipeline/__tests__/concurrency-entry.test.ts
  • lib/pipeline/__tests__/preflight-orchestration.test.ts
  • lib/pipeline/definitions/classifier-context.test.ts
  • lib/pipeline/definitions/classifier-context.ts
  • lib/pipeline/definitions/periodic-classifier.test.ts
  • lib/pipeline/definitions/periodic-classifier.ts
  • lib/pipeline/index.ts
  • lib/pipeline/runtime/concurrency.ts
  • lib/pipeline/runtime/orchestrator.ts
  • lib/prompts/bundled/index.ts
  • lib/prompts/bundled/periodic-classifier.test.ts
  • lib/prompts/bundled/periodic-classifier.ts
  • lib/prompts/ids.ts
  • lib/prompts/templateContextMap.ts
  • lib/prompts/types.ts
  • lib/stores/app-settings/app-settings.ts
  • lib/stores/generation/generation.test.ts
  • lib/stores/generation/generation.ts
  • lib/stores/index.ts
💤 Files with no reviewable changes (1)
  • lib/pipeline/runtime/orchestrator.ts

Comment thread lib/actions/classifier/deps.ts
Comment thread lib/actions/happenings/register-happenings.ts
Comment thread lib/actions/story-entries/operational.test.ts Outdated
Comment thread lib/actions/story-entries/prose-reversal.ts Outdated
Comment thread lib/pipeline/definitions/classifier-context.ts
Comment thread lib/stores/generation/generation.ts Outdated
The key-scoped json_set writers patched onto a bare '{}', so a branch whose
classifier_status was still NULL persisted an object missing the keys that
writer does not own — the column type-lied about ClassifierStatus. They now
seed a complete idle status, which keeps the writes total without making any
writer touch a key it doesn't own. awaitRunTerminal matched on kind alone,
letting a reversal abort another branch's run; branchId is now required.
@Pento95

Pento95 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Four of six actioned in bc180bb.

Partial ClassifierStatus write — valid, and it also affects the pipeline's own writeStatus, which predates this PR. Fixed differently, though: this column has two independent writers (the reversal clamp owns $.processedThrough, the pipeline owns the lifecycle keys), so the suggested whole-blob db.update().set() would silently revert the clamp — the exact race cadence.md → Concurrency bans, and the reason every write here is key-scoped. The real defect was the seed document, not the scoping: all writers now coalesce onto a complete idle status, so writes are total without touching keys they don't own.

awaitRunTerminal branch scoping — valid, fixed; branchId is required rather than optional, with two-branch regression tests. Skipped the bounded wait: a timeout that proceeds breaks the barrier's premise, and one that throws makes every rollback and CTRL-Z failable to mask a hang with no known cause.

No-op clamp test assertion — valid, fixed.

Happening cascade race — premise accurate, conclusion not. It needs a concurrent writer attaching a child to an already existing happening; the only dispatcher of createHappeningInvolvement / upsertHappeningAwareness in the repo is lib/classifier/plan.ts, which attaches children solely to happenings it allocates in the same plan. Also note locking deleteHappening alone wouldn't close it — the inserting handlers would need the same key. Left as a comment on buildChildDeleteOps, since a Plot-screen "add involvement" affordance would make it real.

Entity projection — premise incorrect. The template references five fields and Liquid renders only what's referenced, so nothing extra reaches the prompt (see the committed snapshot). The hygiene point is fair but repo-wide: generation-context.ts passes full Entity rows the same way. Projecting one of two sibling builders is its own change.

@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: 1

🤖 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 `@app/story-settings/`[storyId].tsx:
- Around line 117-122: Update the generation-state selection around
generatingBranchId to consider only foreground per-turn runs, excluding
periodic-classifier activity; use the existing kind-scoped helper such as
isForegroundGenerating(...) or filter for PER_TURN_KIND before deriving the
branch ID and isGenerating state. Keep classifier status separate so the active
phase and cancellation flow only use the foreground generation run.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5e852a79-2e63-44ec-bbb5-4d91bef8178b

📥 Commits

Reviewing files that changed from the base of the PR and between b992a07 and bc180bb.

📒 Files selected for processing (16)
  • app/reader-composer/[branchId].tsx
  • app/story-settings/[storyId].tsx
  • lib/actions/classifier/deps.ts
  • lib/actions/classifier/run-now.test.ts
  • lib/actions/happenings/register-happenings.ts
  • lib/actions/story-entries/operational.test.ts
  • lib/actions/story-entries/operational.ts
  • lib/actions/story-entries/prose-reversal.test.ts
  • lib/actions/story-entries/prose-reversal.ts
  • lib/actions/story-entries/undo.ts
  • lib/classifier/index.ts
  • lib/classifier/status.ts
  • lib/pipeline/__tests__/concurrency-entry.test.ts
  • lib/pipeline/definitions/periodic-classifier.ts
  • lib/stores/generation/generation.test.ts
  • lib/stores/generation/generation.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • lib/actions/story-entries/operational.test.ts
  • lib/pipeline/tests/concurrency-entry.test.ts
  • lib/stores/generation/generation.ts
  • lib/actions/story-entries/operational.ts
  • app/reader-composer/[branchId].tsx
  • lib/actions/story-entries/undo.ts
  • lib/actions/happenings/register-happenings.ts
  • lib/classifier/index.ts
  • lib/actions/classifier/deps.ts
  • lib/pipeline/definitions/periodic-classifier.ts
  • lib/actions/classifier/run-now.test.ts
  • lib/actions/story-entries/prose-reversal.test.ts

Comment thread app/story-settings/[storyId].tsx Outdated
Pento added 2 commits July 31, 2026 16:01
The retry policy could only act on a call that returned, so a provider that
never answered left 'running' persisted — gating both the cadence and runNow,
with no cancel affordance and no way out before the next boot. The expiry now
routes to nextStatusOnFailure so it burns a retry. Separately, E2E launches
Electron against build output, so a stale bundle passed green while testing old
code; the runner compares mtimes per output and refuses to run.
Both context builders passed whole drizzle rows, so every column was template
surface whether the bundled template rendered it or not — and packs are
user-authored, which makes a column reachable today undroppable tomorrow.
templateContextMap's field lists are now enforced rather than descriptive;
classifierContext stays narrower than generationContext, since a prose
classifier has no use for injectionMode.

@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: 2

🤖 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 `@lib/pipeline/definitions/periodic-classifier.ts`:
- Around line 78-100: Update boundedSignal’s relay handler to clear the timeout
before aborting the controller when the outer signal fires. Keep dispose’s
cleanup and the timer’s expired flag behavior unchanged, ensuring outer
cancellation cannot later be classified as a timeout.

In `@scripts/e2e.ts`:
- Line 31: Update the tracked inputs in the freshness check within
scripts/e2e.ts to include package.json, babel.config.js, metro.config.js,
app.json, and the project’s dependency lockfile alongside the existing source
directories, so build configuration and dependency changes invalidate stale dist
output.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 88f03893-6836-4624-a323-f569dc4a2d77

📥 Commits

Reviewing files that changed from the base of the PR and between bc180bb and 258c3ea.

⛔ Files ignored due to path filters (1)
  • docs/implementation/milestones/03-memory-floor/slices/03-classifier.md is excluded by !docs/**
📒 Files selected for processing (7)
  • lib/pipeline/definitions/classifier-context.test.ts
  • lib/pipeline/definitions/classifier-context.ts
  • lib/pipeline/definitions/generation-context.test.ts
  • lib/pipeline/definitions/generation-context.ts
  • lib/pipeline/definitions/periodic-classifier.test.ts
  • lib/pipeline/definitions/periodic-classifier.ts
  • scripts/e2e.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • lib/pipeline/definitions/classifier-context.ts
  • lib/pipeline/definitions/classifier-context.test.ts
  • lib/pipeline/definitions/periodic-classifier.test.ts

Comment thread lib/pipeline/definitions/periodic-classifier.ts
Comment thread scripts/e2e.ts Outdated
Handlers have marked benign rejections code: 'noop' since M2, but nothing ever
read it: any rejection threw ActionRejectedError, so a state patch restating an
entity's current values — or a promotion the other writer already landed — took
the user's whole turn down with the reversal sweep. Found by the slice 3.3 real
provider smoke, where a 4B model hit it on most turns. Also lands that smoke as
an opt-in spec; it skips unless SMOKE_LLM_URL is set.

@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: 1

🧹 Nitpick comments (2)
e2e/tests/classifier-real-provider.smoke.spec.ts (2)

279-282: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider asserting that the pass wrote at least one happening.

Every hard invariant below is a COUNT(*) == 0 check. All of them pass when the classifier wrote nothing. written is printed only. If a real provider returns an empty extraction, the test still passes while the graph stays empty.

If an empty extraction is an acceptable real-model outcome, keep this observational and state that in the comment. Otherwise assert written[0][0] is greater than 0.

🤖 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 `@e2e/tests/classifier-real-provider.smoke.spec.ts` around lines 279 - 282,
Ensure the classifier smoke test validates that the current run wrote at least
one happening by asserting written[0][0] is greater than zero after the queryApp
call. Keep the existing diagnostic output, and preserve the current COUNT(*)
checks for the other invariants.

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

Align the test title and comments with TARGET_TURNS = 1.

The title states "three turns" and Line 189 states "Cadence 2 over three turns", but TARGET_TURNS is 1 and Lines 75-78 explain that one committed turn is intentional. Update the title and the two comments so the stated coverage matches the code.

Also applies to: 142-146

🤖 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 `@e2e/tests/classifier-real-provider.smoke.spec.ts` at line 121, Align the test
title and both turn-count comments with TARGET_TURNS = 1, including the cadence
comment near the later assertion. Preserve the intentional single committed-turn
behavior described near TARGET_TURNS and update only the misleading “three
turns” wording.
🤖 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 `@e2e/tests/classifier-real-provider.smoke.spec.ts`:
- Around line 190-209: Capture the deterministic parked watermark in the test’s
beforeAll setup and store it in a shared variable. Update the startWatermark
baseline used by the classifier polling assertion to reuse that parked value
instead of querying the database after the turn commits; keep the existing
greater-than polling behavior unchanged.

---

Nitpick comments:
In `@e2e/tests/classifier-real-provider.smoke.spec.ts`:
- Around line 279-282: Ensure the classifier smoke test validates that the
current run wrote at least one happening by asserting written[0][0] is greater
than zero after the queryApp call. Keep the existing diagnostic output, and
preserve the current COUNT(*) checks for the other invariants.
- Line 121: Align the test title and both turn-count comments with TARGET_TURNS
= 1, including the cadence comment near the later assertion. Preserve the
intentional single committed-turn behavior described near TARGET_TURNS and
update only the misleading “three turns” wording.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b10c65f-94e2-4428-b968-990cf6691c6d

📥 Commits

Reviewing files that changed from the base of the PR and between 258c3ea and 7d667c1.

⛔ Files ignored due to path filters (1)
  • docs/implementation/milestones/03-memory-floor/slices/03-classifier.md is excluded by !docs/**
📒 Files selected for processing (4)
  • e2e/tests/classifier-real-provider.smoke.spec.ts
  • lib/actions/entities/state-patch-actions.ts
  • lib/pipeline/__tests__/orchestrator-hardening.test.ts
  • lib/pipeline/runtime/orchestrator.ts

Comment thread e2e/tests/classifier-real-provider.smoke.spec.ts Outdated
Pento added 3 commits July 31, 2026 18:02
The slice named window_head_fallback as M7.5's tuning signal; the first real
run contradicts it, so the correction goes to the triage inbox rather than a
slice nobody has authored yet. The staleness-flag race is a test-design trap,
not a product bug, so it goes to lessons-learned.
…-classifier

Nine conflicts, all from the two slices touching the same seams. The two that
were not additive: buildGenerationContext keeps 3.7a's branch filter and this
branch's field projection (branchEntities.map(promptEntity)), and the pill's
kind split gains the classifier's phase plus the branchId awaitRunTerminal now
requires — 3.7a's suggestion-refresh call sites were still on the 2-arg form.
The merge applied isBackgroundKind to the reader but left story-settings on
3.7a's selector, so a classifier pass read as narrative there and offered a
cancel targeting PER_TURN_KIND, which matches no run. Also: the classifier's
timeout timer stayed armed after an outer abort, letting a late fire record a
cancel as a timeout; the E2E freshness check ignored root build config and the
lockfile; and the smoke read its watermark baseline after the pass could land.
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