os-me: the Stop hook records only a Prediction shown after the pending invocation, and skips held-out sessions #1762
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#1762
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
The Stop hook of os-me has two faults. It marks a new
/os-me:predictinvocation as recorded when the transcript holds only an older Prediction, so the new Prediction never gets a row in~/dev/os-me-store/prediction-log.jsonl. It also writes rows, with text, for held-out sessions, which the Store rules forbid. The code is Ruby, inplugins/os-me/hooks; the tests are Minitest.Evidence, from the source at commit
43bdc0800and from the Store on 2026-11-13:CliAppender#prediction_shown?(plugins/os-me/hooks/cli_appender.rb:11-14) reads all messages of the transcript.PredictionRecorder#stop(plugins/os-me/hooks/prediction_recorder.rb:19-25) uses it as thereadycheck. When a session has an older Prediction, the first Stop after a new invocation is ready before the new Prediction exists.appendreads the older Prediction again, the log drops it as a duplicate, andmark_recordedwrites the Stop marker. No later Stop reads the new Prediction.queue/invocations.jsonl: session f8d22c34 got its second invocation at 2026-11-05T14:45:20Z and the Stop marker at 14:45:32Z (12 s). Session 47ffa777 got its second invocation at 2026-11-05T18:25:56Z and the Stop marker at 18:26:05Z (9 s). Each session has 1 log row.atvalue (InvocationLog#append,plugins/os-me/hooks/invocation_log.rb:53). Each transcript entry has a timestamp (Entry#timestamp,plugins/os-me/lib/os_me/entry.rb:20). No code compares the two.plugins/os-me/hooks/callsSplit#held_out?. The log has one row each for the held-out sessions ae4be489 (2026-11-04) and 9d8c2e15 (2026-11-07). Each row has atextvalue copied from the transcript.~/dev/os-me-store/CHARTER.mdline 31 says "Never read a held-out session".~/dev/os-me-store/learning/loop.mdline 39 says "never copy text from one".Approach
The owner is
OsMe::PredictionRecorder. Its collaborators areInvocationLog,CliAppender, andOsMe::Split.Ready check.
PredictionRecorder#stopcalls thereadycheck with two arguments: the transcript path and theatvalue of the pending invocation.CliAppender#prediction_shown?(transcript_path, since = nil)is true only when a Prediction is in a transcript entry whose timestamp is later thansince.TranscriptReader#prediction_candidates(since: nil)gives only those entries. When the check is false, the hook returns and waits for the next Stop, as it does today.Entries with no timestamp. A transcript entry with no timestamp counts as later than
since. Whensinceis nil, all entries count. Thusprediction_shown?(path)with one argument, and the fixtures ofplugins/os-me/test/support/transcript_scenario.rb(which carry no timestamp), behave as today.Time comparison. The
atvalue is UTC with whole seconds (InvocationLog#appendwritesutc.iso8601,plugins/os-me/hooks/invocation_log.rb:53; example2026-11-05T14:45:20Z). The transcript timestamp is UTC with milliseconds (example2026-11-05T14:47:00.128Z). Compare the two values as times, not as strings. An entry whose timestamp is equal tosincecounts as later. An entry whose timestamp does not parse counts as later, the same as an entry with no timestamp. Known limit: theatvalue has no fraction of a second, so a Prediction shown in the same second as the invocation, before it, counts as later. The comparison runs inTranscriptReader, andplugins/os-me/lib/os_me.rbdoes not loadtime, sotranscript_reader.rbloads it.Held-out sessions. Ruling of 2026-11-13 (session 2bb44e9c, orchestrator): skip. When
OsMe::Split#held_out?is true for the session id of the Stop payload, and the session has a pending invocation, the hook does not call thereadycheck, does not call the appender, and callsmark_recorded. The marker stops a retry on each later Stop. Do this check before the hook reads the transcript.PredictionRecorder.newgets asplit:keyword with the defaultOsMe::Split.new.Wiring.
plugins/os-me/hooks/record_prediction.rbkeepsappender.method(:prediction_shown?)as thereadycheck; the method now takes the two arguments.Tests. Thirteen tests give
PredictionRecorderareadylambda with one argument and reach the check: ten inplugins/os-me/hooks/test/prediction_recorder_test.rb, one inplugins/os-me/test/prediction_recorder_provenance_test.rb, and two inplugins/os-me/test/prediction_recorder_payload_provenance_test.rb. Thereadycheck now gets two arguments, so these tests fail; they are under Retired tests. They fail only because of the lambda. The behaviors that they pin stay, and acceptance criteria 8 to 19 state them. The test for a Stop with no predict entry inprediction_recorder_test.rbstays valid, because it does not reach the check.plugins/os-me/hooks/test/cli_appender_test.rbstays valid: it callsprediction_shown?(path)with one argument.Put the held-out check in
PredictionRecorder. Do not put it inAppendRequest,CLI#append, orCliAppender: the CLI test fixtures use the session idssess-1andsess-2, andSplit#held_out?is true for the two ids.Required work:
PredictionRecorder#stopgives theatvalue of the pending invocation to thereadycheck, and skips a held-out session — check: presentheld_out?inplugins/os-me/hooks/prediction_recorder.rbCliAppender#prediction_shown?accepts the time and reads only later entries — check: presentsinceinplugins/os-me/hooks/cli_appender.rbTranscriptReader#prediction_candidatesacceptssince:— check: presentsinceinplugins/os-me/lib/os_me/transcript_reader.rbtranscript_reader.rbloadstimefor the time comparison — check: presentrequire 'time'inplugins/os-me/lib/os_me/transcript_reader.rbOrchestrator work: update the Stop hook text of
docs/implementation-status/os-me.md(the lines "A second Stop appends nothing. An append failure writes no marker, so the next Stop retries.") with the ready check after the invocation time and the held-out skip, and add one History row.Rejected: a held-out check in
AppendRequestorCLI#append— the fixturessess-1andsess-2are held-out ids, so the CLI tests would fail.Rejected: a
readycheck that keeps one argument — the check cannot know the invocation time, and onlyPredictionRecorderholds the pending invocation.Rejected: a marked log row with no text for a held-out session — the ruling of 2026-11-13 is skip.
Acceptance criteria
atvalue of a pending invocation, and no later Prediction, when the Stop hook runs, then the appender is not called and the queue gets no Stop marker.atvalue, when the Stop hook runs again, then the appender is called one time and the queue gets one Stop marker.atvalue, when the ready check runs, then the check is true.UserPromptExpansionevent with no arguments, and a true ready check, when the Stop hook runs, then the appender receives the transcript path of the Stop payload, the origintyped, and the autopilot flag false.PreToolUseevent, and a true ready check, when the Stop hook runs, then the appender receives the originagent.autopilot, and a true ready check, when the Stop hook runs, then the appender receives the autopilot flag true.autopilot, and a true ready check, when the Stop hook runs, then the appender receives the autopilot flag false.PreToolUseand notUserPromptExpansion, and a true ready check, when the Stop hook runs, then the hook fails with aKeyErrorand the queue gets no Stop marker.atvalue of the pending invocation.producervalue and amodelvalue, and a Store repository, when the Stop hook records the Prediction, then the log row has that producer, that model, thestore_revof the Store, and thebrief_revof the Store.modelvalue, and a transcript whose last assistant message has a model, when the Stop hook records the Prediction, then the log row has the model of that message.producervalue, and a transcript that has an assistant message, when the Stop hook records the Prediction, then the log row has the produceragent.Owner sketch
Non-goals
In scope: the ready check of the Stop hook, and the held-out skip in the Stop hook.
Out of scope:
os-me-eval append.Blocking edges: none. This ticket and #1758 can ship in either order.
Retired tests
plugins/os-me/hooks/test/prediction_recorder_test.rb#test_stop_after_typed_entry_records_typed
plugins/os-me/hooks/test/prediction_recorder_test.rb#test_stop_after_agent_entry_records_agent
plugins/os-me/hooks/test/prediction_recorder_test.rb#test_second_stop_appends_nothing
plugins/os-me/hooks/test/prediction_recorder_test.rb#test_failed_append_is_retried_on_the_next_stop
plugins/os-me/hooks/test/prediction_recorder_test.rb#test_failed_append_then_retry_marks_the_session_recorded_once
plugins/os-me/hooks/test/prediction_recorder_test.rb#test_stop_before_the_prediction_is_shown_waits_and_retries_on_the_next_stop
plugins/os-me/hooks/test/prediction_recorder_test.rb#test_ready_check_reads_the_transcript_path_of_the_stop_payload
plugins/os-me/hooks/test/prediction_recorder_test.rb#test_autopilot_word_sets_autopilot_true
plugins/os-me/hooks/test/prediction_recorder_test.rb#test_missing_autopilot_word_sets_false
plugins/os-me/hooks/test/prediction_recorder_test.rb#test_unknown_event_raises
plugins/os-me/test/prediction_recorder_provenance_test.rb#test_stop_writes_a_record_carrying_producer_model_store_rev_and_brief_rev
plugins/os-me/test/prediction_recorder_payload_provenance_test.rb#test_stop_without_model_in_payload_records_the_transcript_model
plugins/os-me/test/prediction_recorder_payload_provenance_test.rb#test_stop_without_producer_in_payload_records_agent
Origin
CliAppender#prediction_shown?reads the whole transcript, so an older Prediction makes the check true (plugins/os-me/hooks/cli_appender.rb:11-14) ← DESIGN (plugins/os-me/hooks/prediction_recorder.rb: thereadycheck gets only the transcript path). Held-out rows ← no file underplugins/os-me/hooks/callsSplit#held_out?← DESIGN (plugins/os-me/hooks/prediction_recorder.rb).VERDICT: CREATE (ticket-skeptic, 2026-11-13): observed faults; sessions f8d22c34 and 47ffa777 have a Stop marker 9 s to 12 s after a new invocation and one log row; held-out sessions ae4be489 and 9d8c2e15 have rows with copied text. No open ticket covers the ready check or the held-out skip.
Work started on 2026-11-13 through /os-sdlc:implement. Branch: ticket-1762. #1758 merged first as main
c5fd757db; TranscriptReader#prediction_candidates and CliAppender#prediction_shown? are still present after it.Implementation 293 failed on 2026-11-13 at the test-reviewer step (dispatch 2905), reason verdict:error. Cause: a workflow defect, not a ticket or test defect: the changed tests under plugins/os-me/hooks/test are outside the target's test_path, so the gate did not run them and the reviewer got no diff. Recorded on #1276. The ticket is finished by hand on branch ticket-1762 under the escalation procedure's hand-finish checks: full suite, lint, one design audit that also reviews the hook tests, then commit, merge, close.
Resolution
Done: The Stop hook records only a Prediction shown after the pending invocation. A held-out session with a pending invocation gets one marker and no append. An unparseable invocation time raises ArgumentError. Entry owns the before-time rule (Entry#before?). Finished by hand after implementation 293 failed at test-reviewer (tests under plugins/os-me/hooks/test are outside the target test path; root ticket #1276).
Evidence: Commit
3332c596eon branch ticket-1762, merged to main on 2026-11-13. os-me suite: 466 runs, 837 assertions, 0 failures (bundle exec ruby test/all.rb in plugins/os-me). Rubocop: 11 files, no offenses. One design audit ran on the diff; its 7 findings (2 must-fix, 5 advisory) are all fixed; red proofs exist for the criteria 2, 8 and 16 tests. Plugin cache refreshed by worktree finish.Follow-ups: #1276 (second occurrence recorded: hook tests outside test_path). #1763 (quoted Prediction blocks in agent reports become rows). Store data for Jared: two held-out records already in prediction-log.jsonl (sessions ae4be489, 9d8c2e15) are not removed. The append step still reads the whole transcript; only the ready check uses the invocation time: dropped, #1758's copy rule and id positions cover it.