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:
- 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.
- Stale claim detection. PID liveness + age, similar to
src/core/lock.js's 5-minute reclaim, vs. TTL-only.
- 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.
- 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?
- 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
Problem
The full Learning Moments classifier is doing real semantic work (question generation, rubric, metadata) but it runs inline in
PostToolBatch. Metrics from the timetmp/feedback_5.mdwas written:classifier_budget_exhaustedearly-exitsSo 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:
moment-claimlock (two-phase claim) — parallel hooks with different fingerprints no longer serialize on each other.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:New event types likely needed (each gets a
ledger/control/telemetrydecision insrc/core/event-registry.js):candidate_pendingcandidate_claimedcandidate_failedmoment_readymoment_expiredOpen design questions
Worth a real design pass before writing code:
/learning-moments verifytriggers a sweep)? Each has different failure modes — orphan processes, missed wakes, manual-only.src/core/lock.js's 5-minute reclaim, vs. TTL-only.moment_readyis 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.candidate-store/moment-pipelinemodule live? Todaysrc/core/log.js+src/core/state.jscarry 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.mdargues — 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 list6e26bcd— two-phase moment claim0899334— gate classifier on prompt budget; secret-path default ignores