decision-system S7b: extract question_answer, correction, direction and over_escalation moments #1277
Labels
No labels
P0
P1
P2
P3
bug
category/code-change
category/cross-repo
category/eval-run
category/investigation
category/no-test-target
category/owner-ruling
category/prose-artifact
category/recurring-chore
category/structure
category/umbrella
crc-refactor
create
delete
enhancement
filed-by/agent
filed-by/user
frozen
lint-rule
needs-info
needs-triage
next
plugin/cc-architect
plugin/os
plugin/os-adr
plugin/os-aidd-lint
plugin/os-backlog
plugin/os-context
plugin/os-doc-hygiene
plugin/os-sdlc
plugin/os-vault
project/cc-os
ready-for-agent
ready-for-human
recurring
review
rule-proposal
update
waiting
wayfinder:grilling
wayfinder:map
wayfinder:map
wayfinder:research
wayfinder:task
wayfinder:task
wontfix
worklist/deviations
worklist/lint-rule
worklist/new-implement-build
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jared/cc-os#1277
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
MomentExtractorwrites onlymemo_replymoments. It drops each pair thatMemoDetectordoes not find. Question-answer, correction, direction, and over-escalation moments hold tier and ping-cost signals (docs/decision-system/extraction.md, "Lever → training data"), and no run stores them. S7a (#1249) addsMomentKindandActiveStretch, but no code callsMomentKindyet.Approach
Ruling (2026-10-23, /os-me:predict 80%, AFK): budget per model call; stop mid-session; no new cursor; ids of classified exchanges,
noneincluded, are the resume point.MomentExtractorsends each new non-memo pair toMomentKindand gives each result to a consumer that writes oneDecisionMomentof that kind,noneincluded.Per pair, in transcript order:
DecisionMoment.id_for(pair)) is in the log already: skip it. No model call.MemoDetector#memo?is true: the current memo path (ReplyShape, one call).MomentKind#classify(one call). Each answer,noneincluded, goes toDecisionMoment.of_kind.The consumer is one factory,
DecisionMoment.of_kind(pair, kind:, split:, extracted_at:). It accepts each key ofMomentKind::CRITERIA(the four kinds andnone). It fills the same pointer, time, and latency fields asmemo_reply(with the S7aActiveStretchfilter), setskind, and leavesheadlineaspair.headline.stated_percent,band,outcome, andoutcome_pstay null, because these kinds have no recommendation forReplyShapeto read. It raisesArgumentErrorfor a kind that is not a key ofMomentKind::CRITERIA,memo_replyincluded.labeled_by(model)sets the labeler, as for memo moments.noneis recorded as a row withkindnonein the log of the session split (moments.jsonloreval/moments-held-out.jsonl). No consumer output changes:CalibrationReport(andos-me-eval calibration) skips rows withoutstated_percent;MomentLogreads only ids forknown_ids;RowLabels,os-me-eval report,score,exchanges, andbackfilldo not read the moments log; theReplyShapeeval harness (eval/reply_shape/run.rb) reads its own labels file. Thusknown_idsskips each pair already classified,noneincluded, and there is no new cursor object.The budget counts model calls, not rows: each
ReplyShapecall and eachMomentKindcall counts 1, and anoneanswer counts too. Todayextractreturnsbatch.size; it must return the number of calls. The check happens before each call. When the run stops in the middle of a session, it appends the rows of that session so far and does not advance the extract cursor for that session, so the next run reads the session again and makes calls only for pairs whose ids are not in the log. A session advances the cursor only when no pair in it still needs a call. Held-out sessions go only toeval/moments-held-out.jsonl, as today.ActiveStretchdoes not choose which pairs to classify, and no pair is dropped past a cap.MomentExtractor.newgets a requiredmoment_kind:keyword.MomentExtractor.buildgivesMomentKind.new(client: LabelerClient.for(adapter))with the same Sonnet adapter asReplyShape. Existing tests that callMomentExtractor.newgive aMomentKindon aDecision::Model::Adapters::Fixedadapter. Tests with a fakeclaudeget akind_answers(choice)helper inGemReplies, next toshape_answers.Required work:
MomentKindin the extractor — check: presentmoment_kindinplugins/os-me/lib/os_me/moment_extractor.rbnone— check: presentdef self.of_kindinplugins/os-me/lib/os_me/decision_moment.rbdef kind_answersinplugins/os-me/test/gem_replies.rbThe fixture pairs are the five pairs in the S7a (#1249) Approach table. The tests put each pair at
ai_at2026-10-01T10:00:00Zandreply_at2026-10-01T10:05:00Zunless a criterion gives other times. New tests go only underplugins/os-me/test.Acceptance criteria
question_answer,correction,direction,over_escalation, a train session whose only pair is that kind's fixture pair, and a fakeclaudeonPATHthat answerskindwith that kind, whenos-me-extract --store <store> --transcript-root <root> --budget 20runs, then it exits 0 andmoments.jsonlholds exactly one row for that session withkindequal to that kind,id"<session_id>:0",splittrain,stated_percentnull,outcomenull,latency_s300, andlabeler.modelsonnet.nonefixture pair and a fakeclaudethat answerskindnone, whenos-me-extract --budget 20runs, then the fakeclaudeis called exactly once,moments.jsonlholds exactly one row withkindnone,id"<session_id>:0",stated_percentnull, andoutcomenull, andqueue/extract-cursor.jsonholds that session id.yes, go ahead, and a fakeclaudethat answers shapeaccept, whenos-me-extract --budget 20runs, then the fakeclaudeis called exactly once andmoments.jsonlholds one row withkindmemo_replyandoutcomeaccept.correctionfixture pair second, and a fakeclaudethat answers shapeaccepton call 1 andkindcorrectionon call 2, whenos-me-extract --budget 20runs, thenmoments.jsonlholds two rows whosekindvalues are['memo_reply', 'correction']in that order.nonefixture, and a fakeclaudethat always answerskindnone, whenos-me-extract --budget 2runs, then the fakeclaudeis called exactly 2 times,moments.jsonlholds exactly 2 rows, both withkindnone, andqueue/extract-cursor.jsonholds exactly 1 session id.correction,none, anddirectionfixture pairs in that order, and a fakeclaudethat answerskindcorrectionon call 1,noneon call 2, anddirectionon call 3, whenos-me-extract --budget 2runs, then the fakeclaudeis called exactly 2 times,moments.jsonlholds two rows whosekindvalues are['correction', 'none']in that order, andqueue/extract-cursor.jsonhas no entry for that session id.claudethat answerskinddirection, whenos-me-extract --budget 20runs again, then the fakeclaudeis called exactly once in that run,moments.jsonlholds three rows whosekindvalues are['correction', 'none', 'direction']in that order, andqueue/extract-cursor.jsonholds that session id.held_out_id) whose only pair is thecorrectionfixture pair and a fakeclaudethat answerskindcorrection, whenos-me-extract --budget 20runs, theneval/moments-held-out.jsonlholds one row with that session id,kindcorrection, andsplitheld_out, and no row inmoments.jsonlhas that session id.correctionfixture pair withai_at2026-10-01T10:00:00Zandreply_at2026-10-01T12:00:00Z, whenos-me-extract --budget 20runs, then its row haslatency_snullandreply_chars59.correctionfixture pair, whenOsMe::DecisionMoment.of_kind(pair, kind: 'memo_reply', split: 'train', extracted_at: '2026-10-01T13:00:00Z')runs, then it raisesArgumentError; withkind: 'none'it returns a moment withkindnoneandoutcomenil.Owner sketch
Non-goals
MomentKindcriteria. The S7 gate is orchestrator work: an agent checks a sample of each new kind from train sessions only, and each kind must reach 80% agreement.ReplyShape,AnswerSource, orWrongCost::Assessorlabels on the new kinds.MomentKind.ActiveStretchto choose pairs.CalibrationReport. It skips rows withoutstated_percent, so the new kinds andnonerows do not change its output.Blocked by: #1249
Origin
DESIGN(docs/decision-system/build-plan.mdS7).VERDICT: CREATE (user-approved; backlog search only)
Work started on branch ticket-1277 (os-sdlc implement, S7b).
Resolution
Done: MomentExtractor classifies each non-memo exchange with MomentKind and writes one row per exchange, none included. The budget counts model calls, is checked per call, and a partial session leaves the cursor in place.
Evidence: Commits
0d21c538,0551d5f3,06668695on ticket-1277. Suite: 285 runs, 0 failures. Rubocop clean. Opus design audit: pass; F6 fixed. Hand-finished after escalation 68 (test defects); implementation 138 abandoned with that reason.Follow-ups: S7 gate (80% agreement on a checked sample of each new kind) is recorded as open in the build-plan S7 row. Dropped: uncounted calls when a transcript vanishes after classification (pre-existing, rare, overrun is at most one session). Dropped: pair-labeler extraction from MomentExtractor (audit F8 says acceptable at current size).