Skip to content

Move classifier off the user-facing hook path (background candidate pipeline) #14

Description

@raghubetina

Problem

The full Learning Moments classifier is doing real semantic work (question generation, rubric, metadata) but it runs inline in PostToolBatch. Metrics from the time tmp/feedback_5.md was written:

  • 52 classifier calls
  • avg classifier wall duration: ~23.5s
  • hook p95: ~17.1s
  • 4 moments injected, 27 silenced, 20 declined, 233 classifier_budget_exhausted early-exits

So the user pays full hook latency on every change event in the window where the classifier runs, even though only ~5% of those classifications turn into asked questions. The two recent mitigations help but don't solve it:

  • 0.5.0 lifted the classifier call out of the moment-claim lock (two-phase claim) — parallel hooks with different fingerprints no longer serialize on each other.
  • 0.5.1 gates the classifier on the immediate-prompt budget upstream — skips the model call when prompt budget is already exhausted.

Both reduce wasted classifications. Neither reduces the hook latency for the calls that do run.

Proposal

Move full classification off the blocking hook path. Sketch from tmp/feedback_5.md:

PostToolBatch
  -> deterministic gates (paused, budget, baseline, file count)
  -> compute candidate fingerprint
  -> enqueue candidate_pending
  -> spawn (or wake) background worker (best-effort)
  -> return

Background worker (e.g. `learning-moments worker --once`)
  -> claim pending candidate (under lock)
  -> build/redact context
  -> run classifier
  -> write moment_ready / moment_silenced / classifier_failed_open

Later opportunistic injection (PostToolBatch / UserPromptSubmit / UserPromptExpansion)
  -> read ready moments
  -> check prompt budget + freshness
  -> inject with stored question + redacted context

New event types likely needed (each gets a ledger/control/telemetry decision in src/core/event-registry.js):

  • candidate_pending
  • candidate_claimed
  • candidate_failed
  • moment_ready
  • moment_expired

Open design questions

Worth a real design pass before writing code:

  1. Worker invocation model. Spawned-from-hook (detached process per hook), wake-on-demand (one long-running worker per session), or run-on-slash-command (/learning-moments verify triggers a sweep)? Each has different failure modes — orphan processes, missed wakes, manual-only.
  2. Stale claim detection. PID liveness + age, similar to src/core/lock.js's 5-minute reclaim, vs. TTL-only.
  3. Delayed-moment context retention. Storing a redacted diff excerpt on moment_ready is essentially required (the user needs context to answer a delayed question fairly). But excerpts have a stricter privacy profile than durable learning metadata — separate retention from the ledger.
  4. Working tree drift. Between enqueue and classification, the user can keep editing. Do we still classify based on the original diff? Re-snapshot? Mark stale?
  5. Where does the candidate-store / moment-pipeline module live? Today src/core/log.js + src/core/state.js carry the existing query helpers; this would centralize the queue/lifecycle semantics in one place. Worth a refactor or layer it on top?

Why not just optimize the classifier?

tmp/feedback_5.md argues — and I agree — that model selection / prompt shortening / caching / inline triage are all secondary. The biggest architectural win is "don't block the user on the classifier." Until that's true, faster-classifier work is fighting the wrong fight.

Why not fast inline triage?

Also from feedback_5: building a small fast classifier to predict the big classifier's verdict without labeled data would just encode intuition. The right order is to first move full classification background, then log candidate features + outcomes for a while, then maybe build inline triage if the data supports a measurable target.

References

  • tmp/feedback_5.md — full audit, recommended architecture, data-to-collect list
  • Findings 1, 3, 6 in that document fold into this issue
  • Recent commits implementing related but smaller wins:
    • 6e26bcd — two-phase moment claim
    • 0899334 — gate classifier on prompt budget; secret-path default ignores

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions