os-me: the prediction parser reads every Prediction block shape and every block of a message #1758
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#1758
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
Some
/os-me:predictruns leave no row in~/dev/os-me-store/prediction-log.jsonl. A run with no row can never get a score, andbin/os-me-reportcounts too few predictions. The parser causes most of the loss: it reads one block shape, one block of a message, and one message of a transcript. The code is Ruby, inplugins/os-me; the tests are Minitest.This ticket was an investigation ticket. The body was rewritten on 2026-11-13 after the causes were found, and again in the Decide phase of 2026-11-13, which moved the Stop hook causes to #1762.
Evidence, from the source at commit
43bdc0800(#1233) and from non-held-out transcripts, checked on 2026-11-13:PredictionParser::YAML_BLOCK(plugins/os-me/lib/os_me/prediction_parser.rb:7) needsprediction:on the first line after the yaml fence. It rejects four shapes that sessions write (shapes 1 to 4 below). For these blocksprediction?is false and the session gets no row. Sessions: c2e6868b, 9e888d99, 208fbb3f, bbe5ea91 (shape 1); 19f78576, a48658d5 (shape 2); a48658d5 (shapes 3 and 4).PredictionParser#yaml_block(prediction_parser.rb:46) takes the last block of a message. Sessions f8d22c34 and b9b5b401 each have a message with threeprediction:blocks. The log has one row for each session, and that row holds the third block.PredictionParser#last_positioned_prediction(prediction_parser.rb:18-21) returns only the last message that has a Prediction.AppendRequest#append(plugins/os-me/lib/os_me/cli/append_request.rb:11-18) records only that message.plugins/os-me/skills/predict/SKILL.mdstep 2). The first copy is in a task-notification result. The second copy is in the next assistant message that has a Prediction.last_positioned_predictioncounts the two copies as positions 1 and 2. With the code of43bdc0800,os-me-eval appendover the transcript of session 6098efae (one Prediction, recorded assession:1in the Store log) gives the idsession:2:1on a scratch log. Sessions 974224e0, b9b5b401, bbe5ea91, and 5aee9255 givesession:2:1also.confidencevalues in the same sequence. Thetowardtext is not always equal (a48658d5, f8d22c34, 47ffa777).prediction:block that does not load (MalformedPrediction: prediction YAML does not load). The copy in the next assistant message loads.Reproduce (always pass a scratch
--log; never write test rows to the Store log):Today this prints "no Prediction with a Confidence line in " and exits 1.
Approach
The owner is
OsMe::PredictionParser.OsMe::CLI::AppendRequestis its caller. One change: the parser returns every shown Prediction of a transcript as positioned rows, andAppendRequest#appendgives all of them toPredictionLog#append. The dedupe ofPredictionLog#appendon[id, session_id]does not change.Block shapes. A Prediction block is a fenced yaml block whose first key is
prediction:,predictions:, ordecision_<n>:, in one of the shapes 0 to 4. Comment lines (# ...) can come between the fence and the first key. The text in these examples is invented.Shape 0, read today. One block is one Prediction:
Shape 1, comment lines before the first key. One Prediction:
Shape 2, a
predictions:list. Each item has aprediction:mapping. Each item is one Prediction:Shape 3,
decision_<n>:keys at the top level. Each value has aprediction:mapping. Each key is one Prediction:Shape 4, a
predictions:mapping withdecision_<n>:keys. Each value holds the fields directly. Each key is one Prediction:In each real example of shapes 2, 3, and 4, each Prediction has its own
action,toward, andconfidence. So the row of each Prediction comes from its field mapping with the rules ofYamlPrediction#row(plugins/os-me/lib/os_me/prediction_parser/yaml_prediction.rb:24-27): the same text, confidence check, triggers, and lever keys. The keysdecision,choice, andrulingare not row fields. TodayYamlPrediction#initializeloads the block body and reads itspredictionkey (yaml_prediction.rb:8-13). After the changeYamlPrediction.new(fields)takes one field mapping, and a newPredictionParser::YamlBlockloads the body, finds the shape, and returns oneYamlPredictionfor each field mapping fromYamlBlock#predictions.YamlBlockraises the two presentMalformedPredictionerrors: the YAML does not load, and the Prediction is not a mapping.Shape 5 is not a Prediction block. Its
decision_<n>:values have noprediction:mapping and noaction, so a row has no action text:The one real example (session a48658d5) is the assistant copy of a shape 4 block in a task-notification result, so its Predictions get their rows from shape 4.
prediction?is false for shape 5, and shape 5 gives no rows and no error.A yaml block whose first key is some other key, for example
plan:, is not a Prediction block, with or without comment lines before the key.Messages.
AppendRequest#appendgives the parser all prediction candidates of the transcript in transcript sequence, each with its source: assistant text or task-notification result.Entry#assistant_textandEntry#notification_text(plugins/os-me/lib/os_me/entry.rb:24-38) already separate the two.TranscriptReader#prediction_messagesreturns[source, text]pairs, where the source is:assistantor:notification.TranscriptReader#prediction_candidatesdoes not change, becauseCliAppender#prediction_shown?uses it.Predictions of a message. A message gives its Predictions in the sequence shown: each Prediction block from first to last, and in a block each list item or
decision_<n>:key from first to last. A message with a Confidence line and no Prediction block is one Prediction with one row for each ruling, as today (LegacyPrediction#rows).Copies. An assistant message is a copy when the nearest earlier message that gives rows is a task-notification result, and the rows of the two messages have the same
confidencevalues in the same sequence. A copy gives no rows and takes no position. Two assistant messages are never copies of each other.Blocks that do not load. A Prediction block that raises
MalformedPredictionin a task-notification result gives no rows, and that block is not an error. A Prediction block that raisesMalformedPredictionin an assistant message raises, as today. This rule is inpositioned_rowsonly, because onlypositioned_rowsknows the source.prediction?stays true for a block that does not load, andparsestill raises for it (plugins/os-me/test/prediction_yaml_test.rb:62-66pins the two results). The Stop hook path does not change for a task-notification result with a block that does not load and no assistant copy yet:appendfails, the hook writes no Stop marker, and the next Stop tries again.Id scheme. Number the Predictions of the transcript 1, 2, 3, and so on, in the sequence shown. Copies have no number. Prediction 1 keeps the id that
YamlPrediction#rowandLegacyPrediction#rowsgive today:session:1, orsession:<letter>for each lettered ruling. Prediction n, for n above 1, getssession:<n>:1, orsession:<n>:<letter>. This isat_position(ruling, n)(prediction_parser.rb:40-44) with no change.PredictionParser#parse(text, position:)stays.Why this scheme keeps the present ids: each of the 84
session:<n>rows of the Store log has the idsession:1(count of 2026-11-13; the other 111 rows have ids of one capital letter, from before the hook). No row has the formsession:<n>:<m>. A transcript with one Prediction, with or without its copy, givessession:1. A later Prediction never changes the number of an earlier one, so a second run gives the same ids, and the dedupe adds nothing.AppendRequest#appendraises the presentThor::Error("no Prediction with a Confidence line in ") only when the transcript gives no rows.PredictionParser#towardandPredictionParser#stated_confidenceread the last Prediction of the text: the last Prediction block, and in it the last item or key. After the change they read shapes 1 to 4 for each caller:plugins/os-me/lib/os_me/confidence_extractor.rb:10(used bymoment_builder.rbandconfidence_score.rb) andplugins/os-me/lib/os_me/moment_topic.rb:31,35. Shapes 1 to 4 behave for these callers as shape 0 does today. A block that does not load raisesMalformedPredictionfrom the two methods. A confidence that is not an integer 0 to 100 raisesMalformedPredictionfromstated_confidence;towarddoes not examine the confidence. The callers do not rescue the error today, and they do not rescue it after the change. A good shape 2, 3, or 4 block gives the callers a value and no error.These tests pin the present behavior of the parser and its callers and must stay green:
test/confidence_extractor_yaml_test.rb,test/confidence_extractor_test.rb,test/draft_extraction_test.rb,test/candidate_refresh_agent_toward_test.rb,test/candidate_refresh_topic_test.rb,test/prediction_parser_test.rb,test/lever_fields_test.rb, andtest/prediction_fields_test.rb(paths fromplugins/os-me).Required work:
PredictionParser#prediction?and the block reader accept shapes 1 to 4 and refuse shape 5 and other first keys.PredictionParser#positioned_rows(messages)returns the rows of every Prediction of the transcript, with copies removed and the ids of the id scheme — check: presentdef positioned_rowsinplugins/os-me/lib/os_me/prediction_parser.rbTranscriptReader#prediction_messagesreturns the candidates with their source — check: presentdef prediction_messagesinplugins/os-me/lib/os_me/transcript_reader.rbAppendRequest#appendappends all rows ofpositioned_rows;last_positioned_predictionhas no caller and is removed — check: absentlast_positioned_predictioninplugins/os-me/lib/os_me/**/*.rbPredictionParser::YamlBlockloads a block body and returns oneYamlPredictionfor each field mapping — check: presentclass YamlBlockinplugins/os-me/lib/os_me/prediction_parser/yaml_block.rbOrchestrator work: add one History row to
docs/implementation-status/os-me.md(the text "reads the last fencedprediction:yaml block" is in the History row of 2026-10-23, which stays as a record), and update theOsMe::PredictionParsercard indocs/decision-system/current-state.md.Rejected: positions that count messages, as
last_positioned_predictiondoes — each copy takes a position, so the recorded transcript of 6098efae getssession:2:1in place ofsession:1.Rejected: a copy rule that compares the
towardtext — the copies of a48658d5, f8d22c34, and 47ffa777 change the text.Rejected: a copy rule that compares only
confidencevalues, without the source — session c2e6868b has two different Predictions with confidence 80.Rejected: rows for shape 5 — it has no
action, and its one real example is a copy.Acceptance criteria
prediction:), whenos-me-eval appendruns, then the log gets one row with the idsession:1and the confidence of the block.os-me-eval appendruns, then the log gets two rows with the idssession:1andsession:2:1, and each row has the confidence and the text of its item.decision_<n>:keys, whenos-me-eval appendruns, then the log gets two rows with the idssession:1andsession:2:1.decision_<n>:keys, whenos-me-eval appendruns, then the log gets two rows with the idssession:1andsession:2:1.os-me-eval appendruns, then the log gets rows only for the Predictions of the shape 4 block, and the command exits 0.prediction:blocks, whenos-me-eval appendruns, then the log gets three rows with the idssession:1,session:2:1, andsession:3:1, in block sequence.prediction:block in a task-notification result and a block with the same confidence in the next assistant message, whenos-me-eval appendruns, then the log gets one row with the idsession:1.session:1for the session, and the transcript of criterion 7, whenos-me-eval appendruns, then the log still has one row for the session.prediction:block with the same confidence, and no task-notification result between them, whenos-me-eval appendruns, then the log gets two rows with the idssession:1andsession:2:1.prediction:block does not load, and aprediction:block that loads in the next assistant message, whenos-me-eval appendruns, then the log gets one row with the idsession:1, and the command exits 0.prediction:blocks, whenos-me-eval appendruns two times, then the log has four rows after the first run and the same four rows after the second run.plan:, whenos-me-eval appendruns, then the log gets no row and the command prints "no Prediction with a Confidence line".MalformedPredictionerror that names the confidence, as it does today for a shape 0 block.Owner sketch
Non-goals
In scope: the block shapes that the parser reads; one row for each Prediction of a transcript; the copy rule; the id scheme;
os-me-eval append.Out of scope:
session:1rows of f8d22c34 and b9b5b401, which hold the third Prediction of a message with three blocks. For these two sessions a new run drops Prediction 1 (the idsession:1is taken) and adds Prediction 3 again assession:3:1. That repair is a Store data decision for Jared.appendover the sessions that are in the Store log now is a Store data decision for Jared. This ticket adds no migration.~/dev/os-me-store/bin/rederive-wait-records. It is a one-time repair script of 2026-10-22 that was used; this ticket does not update it.os-me-eval appendthat names one invocation or one block (cause d of the first body). It is not necessary when one run records every Prediction.Blocking edges: none. This ticket and #1762 can ship in either order.
Origin
VERDICT: CREATE (ticket-skeptic, 2026-11-13): six one-invocation sessions have no prediction-log row; #1233 covers a different fault. Causes found and body rewritten on 2026-11-13.
os-me: find why predict runs leave no prediction-log rowto os-me: record every shown Prediction; fix the parser and Stop hook faults that lose prediction-log rowsWork started on 2026-11-13 through /os-sdlc:implement. Branch: ticket-1758. #1233 (positioned ids) merged first as main
43bdc0800.os-me: record every shown Prediction; fix the parser and Stop hook faults that lose prediction-log rowsto os-me: the prediction parser reads every Prediction block shape and every block of a messageDecide phase 2026-11-13: one change = the parser returns every shown Prediction as positioned rows; owner = PredictionParser. Hook scope moved to #1762. Rulings: held-out sessions are skipped; removal of the two present held-out rows waits for Jared.
Resolution
Done: The prediction parser reads four yaml block shapes (comment line before prediction:, predictions: list, decision_N: wrapper, predictions: mapping with decision_N: keys), yields every block of a message, numbers Predictions and not messages, and gives no row to an assistant copy of a task-notification Prediction. os-me-eval append records every Prediction of a transcript; a second run adds nothing. This also removes the #1233 regression that gave the first Prediction of a normal session the id session:2:1.
Evidence: os-sdlc implementation 292, 2026-11-13: 11 dispatches, suite gate green, survivor gate green, contract audit pass; plugin suite 456 runs, 0 failures. Real-data check on 14 non-held-out sessions with a scratch log: 13 correct (4 recorded sessions keep id session:1 and the Store confidence; 9 sessions that lost rows give 37 rows that match an independent count; a second run adds no row). Branch ticket-1758.
Follow-ups: #1763 (filed, skeptic verdict CREATE): the parser makes rows from Prediction blocks that an agent report only quotes; found on session 2bb44e9c in the same check. #1762 (already filed): Stop hook ready check and held-out skip. Store data decision for Jared, no ticket: sessions f8d22c34 and b9b5b401 hold Prediction 3 under session:1; bbe5ea91 has a second Prediction that the Store lacks. Docs (os-me leaf History row, PredictionParser card in current-state.md) are done in the same session as orchestrator work.