Skip to content

fix(ingest): persist per-turn authoring model for Claude Code and Codex chunks (Refs #655) - #1639

Merged
EtanHey merged 1 commit into
mainfrom
claude/orc-final-credit-batch-z2uwd0
Nov 5, 2026
Merged

EtanHey merged 1 commit into
mainfrom
claude/orc-final-credit-batch-z2uwd0

Conversation

@EtanHey

@EtanHey EtanHey commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

TASK MARKER: cloud-credit-20261105-brainlayer-final-issue-655

Draft, partial. Refs #655, not Fixes. This fixes Part A (transcript ingestion) and corrects the Codex path that #655 called "working". Part B (self-reported caller_model on BrainBar brain_store) is not done. See Unresolved acceptance below.

Change and evidence

Base and head

Trace on the current active surfaces

  • Realtime watcher (the active ingestion path for Claude and Codex). watcher.normalize_provider_entry keeps Claude user/assistant entries verbatim, including message.model. The entry then goes through watcher_bridge.flush_to_db, classify_content, chunk_content and the metadata JSON written to chunks.metadata. On main, pipeline/classify._extract_entry_metadata read only sessionId, timestamp and sender, so the model was dropped. The batch brainlayer index path uses the same classify_content, so this fix covers it too (source-level; not separately exercised).
  • The Codex batch parser (ingest/codex.py) was not actually working. On a session_meta line it set model = payload.get("model_provider", "openai"), and a turn_context model was used only if not model. So every real Codex chunk stored "model": "openai", which is the provider, not the model. The first value also stuck for the whole session, and user and tool chunks were labelled with it as well.
  • BrainBar brain_store. The served schema in brain-bar/Sources/BrainBar/MCPRouter.swift is content, tags, importance and project. handleBrainStore then calls storeOrQueueWithinBudget or the pending-queue replay. There is no model field anywhere on it. The Python mcp/store_handler.py path named in the issue is not served.

What changed

  • pipeline/classify.py: new transcript_author_model(raw). It returns the transcript's own model string, stripped, or None. Nothing is inferred. These values are treated as unknown: absent, non-string, empty or blank, Claude Code's "<synthetic>" placeholder (written on turns no model produced), longer than 128 characters, or containing non-printable/control characters. _extract_entry_metadata sets meta["model"] for assistant entries only. A user entry, a tool_result carried on a user entry, or a task-notification result never gets a model.
  • ingest/codex.py: each turn_context.model, passed through the same validator, becomes the author of the turns that follow it. model_provider is now recorded separately on every chunk. model appears only on assistant chunks, and is None when no turn_context named one.
  • Authoring vs enrichment. Nothing writes enrichment_model or enrichment_backend, and a test asserts both stay NULL while metadata.model is set.
  • No schema migration. This reuses the existing chunks.metadata JSON, the same key Codex already used. Existing rows are untouched: old Claude rows have no model key (unknown), and old Codex rows keep "openai".

Commands and observed output (Linux cloud container, Python 3.13.18, editable venv)

Fail-first, old code (git stash push src/, new tests kept):

python -m pytest tests/test_author_model_provenance.py -q -p no:cacheprovider
6 failed, 10 passed
  mixed-model Claude via watcher -> scratch DB: KeyError: 'model'
  codex per-turn: AssertionError: assert 'openai' == 'codex-fixture-model-a'
  codex no turn_context: assert 'openai' is None
  parity: claude {A: None, B: None} != codex {A: 'openai', B: 'openai'}
  codex user/tool chunks: carried 'model'

New code: 16 passed.

Mutant controls (each one applied to HEAD, then restored):

Mutant Result
drop the <synthetic> filter 1 failed ([synthetic])
attribute the model on user entries too 1 failed (test_user_entry_claiming_a_model_is_not_attributed)
Codex first-turn-wins (model = model or …) 2 failed (per-turn and parity)
Codex provider used as model 1 failed (…unknown_not_the_provider)

Focused neighbours: tests/test_author_model_provenance.py test_classify.py test_ingest_codex.py test_watcher_bridge.py test_watcher_provenance_ingest.py test_watcher.py test_jsonl_watcher.py test_chunk.py test_chunker.py test_context_pipeline.py test_prompt_classification.py test_precompact_chunk_origin.py gave 342 passed.

ruff check and ruff format --check pass on all three files.

Repo gate: BRAINLAYER_PREPUSH_SCOPE=changed-only BRAINLAYER_CHANGED_FILES_RANGE=origin/main..HEAD scripts/run_tests.sh fell back to the full suite because src/brainlayer/ingest/codex.py is unmapped, and it hit my 500s timeout at about 42%. Up to that point there were 4,860 PASSED and 65 FAILED, in test_backup_*, test_brainbar_service_render, test_brainbar_update_script and test_audit_recursion_filter. All 65 fail identically on a clean origin/main worktree (65 failed), so they are Linux-container environment failures (zstd, macOS service rendering), not this diff. The rest of the full suite (about 58%) is UNRUN.

The test fixtures are synthetic: scratch SQLite under pytest tmp_path, made-up model strings (claude-fixture-model-a, codex-fixture-model-b), no real transcripts, no network, and no model loads.

Unresolved acceptance (why this is Refs #655)

  1. Part B, BrainBar brain_store caller_model: NOT implemented. The served surface is Swift. Doing it needs:

    • an optional caller_model string in the brain_store inputSchema, plus a length cap in the limitedInputSchema map;
    • threading it through handleBrainStore, then storeOrQueueWithinBudget/queueBrainStore, the pending-store queue JSON, and replay;
    • persisting it as metadata.model with metadata.model_source = "caller_self_reported", and absent when not supplied (never inferred from agent_id or the session);
    • surfacing it in retrieval.

    This container has no Swift toolchain and BrainBar is a macOS app, so I could not compile or test any of it. I would rather leave it out than ship an untested change to the store/queue write path. Owner: BrainBar lane.

  2. Codex sessions read by the realtime watcher still carry no model. normalize_provider_entry rebuilds each Codex response_item without context, and the model lives on earlier turn_context lines. Fixing it needs per-file state across polls (offsets restart mid-file). That is out of scope here, and nothing tests it.

  3. Retrieval surfacing. metadata.model is persisted, but no BrainBar search formatter shows it. Answering "which model wrote this?" today means querying chunks.metadata (json_extract(metadata,'$.model')). There is no chunks.model column; this PR deliberately adds none.

  4. The arbitrated (writer-queue) watcher path is source-level only. It passes the same metadata dict to enqueue_chunk. The test exercises arbitrated=False.

  5. Native, installed or live proof is UNRUN. No Mac, no BrainBar, no production DB, no installed watcher. Proof here is source and fixture only.

Risks

  • Rows ingested by the Codex batch parser after this merges hold the real model (or None) instead of "openai". A query on metadata.model = 'openai' would split between old and new rows. I found no reader of that key in src/.
  • When a valid model string arrives, it is stored as recorded (stripped). Values from a hostile or broken transcript are capped at 128 printable characters.

Owner handoff, @brainlayer lead

  • Review this draft. It is not marked ready and has no auto-merge.
  • Decide whether to take Part B (Swift brain_store caller_model) as a follow-up in the BrainBar lane, and whether retrieval should show metadata.model.
  • Run the full Python suite on a Mac or CI on the exact head a03bca7.
  • Before merge, run the pre-merge live check against a copy of the real DB, per AGENTS.md. That check is UNRUN here. Suggested: ingest one recent mixed-model Claude JSONL into a DB copy via the watcher path, then SELECT json_extract(metadata,'$.model'), count(*) FROM chunks WHERE source='realtime_watcher' GROUP BY 1.
  • Report target: orchestrator/docs.local/plans/2026-11-04-claude-cloud-batch/collab.md. That file is not mounted in the cloud, so this body is the handoff for the dispatcher to import.

Implementer/harness: Claude Code cloud worker (Anthropic Claude, effort high, fast mode off). There were no subagents and no paid API calls. A cost meter was not available in-session; the work ran from 02:11Z to about 02:35Z, well inside the 07:00Z bound.

Loop-record: implementer=anthropic/claude-code-cloud/high; reviewer=none; rounds=0; defects=0


Generated by Claude Code

Note

Persist per-turn author model for Claude Code and Codex ingest

  • Adds shared validator classify.transcript_author_model that trims whitespace and rejects non-strings, blank values, synthetic placeholders, strings over 128 chars, and non-printable characters
  • _extract_entry_metadata in classify.py now stores the validated model on assistant entries only; user entries never get an author model
  • Codex parser in codex.py attributes each assistant response to the model of its most recent turn_context, and tracks the session model_provider separately on user, tool, and assistant chunks
  • Adds integration and parser tests in test_author_model_provenance.py, including a Claude/Codex parity test
  • Behavioral Change: previously parsed Codex assistant entries used the session provider or the first model seen; now a missing or untrustworthy model stays unknown and the provider is never substituted

Macroscope summarized a03bca7.

@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 22 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 55b939b and 1dd44ef.

Files selected for processing: 3

  • src/brainlayer/ingest/codex.py
  • src/brainlayer/pipeline/classify.py
  • tests/test_author_model_provenance.py
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@deepsource-io

deepsource-io Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in 55b939b...1dd44ef on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

Important

Some issues found as part of this review are outside of the diff in this pull request and aren't shown in the inline review comments due to GitHub's API limitations. You can see those issues on the DeepSource dashboard.

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
Python Nov 5, 2026 7:28a.m. Review ↗
Swift Nov 5, 2026 7:28a.m. Review ↗
JavaScript Nov 5, 2026 7:28a.m. Review ↗
Shell Nov 5, 2026 7:28a.m. Review ↗
Secrets Nov 5, 2026 7:28a.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

BrainLayer ratchet

Every Value below was measured by this run. A row this machine cannot measure says n/a — <reason> instead of a number; baselines in Notes name their own machine, method and date and were not measured here.

Hosted success gives no verdict on mapped bytes, search p50/p95, idle CPU, hotlane backlog CPU, or fallback replay debt.

Row Status Value (measured by this run) Method Notes
commit provenance 🟢 GREEN measured 1dd44efb7e86 == PR head · checkout 1dd44efb7e86 commit graph + live PR head · in-process · runner Which commit this whole table is about. On a pull_request event the checkout is GitHub's synthetic merge ref, whose sha is not on the PR — #759's table printed 13fa724278bf while that PR's head was 4632f979 — so this row names the PR-head parent instead, the sha a reviewer can actually see. The comparison sha is read live from repos/{owner}/{repo}/pulls/{n} when the table is collected, not taken from the event payload, because the payload cannot know the run has been overtaken. Residual window, stated rather than papered over: a push landing between that read and the comment being posted is not caught here — the run for that push refreshes the table.
baseline attestation 🟢 GREEN baseline f421d1a7c5e6 matches the main attestation (run 41987607789 · main b3135a98474f · 2026-11-05T07:30:12Z) main attestation artifact via Actions API · in-process · runner What every comparison is measured AGAINST, and who says so. The baseline fields of tests/fixtures/sprint_gate/corpus.json (queries, latency_baseline_ms, thresholds) are compared to the ratchet-attestation artifact of the latest successful push or (no-input) workflow_dispatch run of ratchet-attest.yml on main, fetched through the Actions API — a PR run cannot write to another run's artifacts. A field that differs is RED unless that main run measured the new value. The calibrated socket collector can license p50/p95; every absent measured path stays locked, so missing collection never passes as permission for a hand edit. Boundary: the comparator is this PR's checkout of ci_ratchet_table.py, diff-reviewable, not tamper-proof.
provenance 🟢 GREEN stamped 1dd44efb7e86 == HEAD, tree clean wheel stamp · in-process · runner Sha half of #749 keg-mode provenance: a keg built from this wheel can answer __build_sha__. The helper-age and served-process predicates need a running BrainBar and are measured only by scripts/sprint_gate.py on an installed Mac. The sha here is the checkout's — the explicit PR head in ratchet.yml — because that is what publish.yml stamps at release time; the PR-head sha this table describes is the one in commit provenance above.
fallback replay debt ⚪ n/a n/a — no fallback queue on this machine: the pending memories live in ~/Gits/*/docs.local/decisions, and docs.local/ is gitignored, so a runner checkout has no copy of them to count docs.local walk · machine with the fallback queue local-only; not enforced by hosted CI. intended_brain_store: true with no chunk_id means a memory reached disk and never reached the DB, so it answers no brain_search. Budget: 0. Any pending or unparseable file is a finding, never a band -- 122 of these sat from 2026-06-28 to 2026-09-05 because nothing counted them where a reader would look. Measured by walking the tree, so it is only ever measured on a machine that HAS the tree.
pre-push: delete-only push runs no suite 🟢 GREEN 0 suite starts · exit 0 real checkout hook · delete-only stdin · sentinel suite runner Bug SHA: 44ecbf5ba6f1ada3b965797559eee90c94b45595 (RED replay, 2026-10-06). Fix: #1084; measured at 1dd44efb7e8691de5631fd9d8201cb6ed7edf659. Ceiling: 0 suite starts; hook must exit 0 and announce the delete-only skip.
mapped bytes ⚪ n/a n/a — no BrainBar daemon at /tmp/brainbar.sock: this row needs the daemon, its hybrid helper and the indexed corpus running together, and no GitHub-hosted runner has them (macOS included) — only a self-hosted Darwin/arm64 runner on an installed Mac would socket · installed Mac local-only; not enforced by hosted CI. Baseline 26.2 GB — installed Mac, socket, 2026-09-03, after R2 drained 15,070 → 0. Up from 16.8 GB because the drain left more vectors mapped under the same cap: the change is the drain, not a leak. Not measured by this run.
search p50/p95 ⚪ n/a n/a — no BrainBar daemon at /tmp/brainbar.sock: this row needs the daemon, its hybrid helper and the indexed corpus running together, and no GitHub-hosted runner has them (macOS included) — only a self-hosted Darwin/arm64 runner on an installed Mac would socket · installed Mac local-only; not enforced by hosted CI. Margin p50: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Margin p95: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Calibrated on MacBook-Pro.local at 2026-09-01T08:42:22Z under active_sprint_load (tests/fixtures/sprint_gate/corpus.json). Not measured by this run.
idle CPU ⚪ n/a n/a — no BrainBar daemon at /tmp/brainbar.sock: this row needs the daemon, its hybrid helper and the indexed corpus running together, and no GitHub-hosted runner has them (macOS included) — only a self-hosted Darwin/arm64 runner on an installed Mac would ps sampling · installed Mac local-only; not enforced by hosted CI. Ceiling: average CPU < 30% over a 60 s window (resource_budget in scripts/sprint_gate.py), ratified and kept as a hard budget. Margin daemon: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Margin helper: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Margin watcher: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Needs the BrainBar daemon, helper and watcher actually running. Not measured by this run.
hotlane backlog CPU ⚪ n/a n/a — live hotlane CPU evidence requires an installed Mac CPU/wall for backlog-encoding cycles · rolling 600 s · installed Mac local-only; not enforced by hosted CI. Default budget 60% of one core; BRAINLAYER_HOTLANE_CPU_BUDGET_PERCENT overrides. At least 30s of active backlog cycles required; yielded time excluded. Measures installed code.
signature_valid ⚪ n/a n/a — the macOS signature-parity job is trigger-gated and did not run on this PR: it touches no release or signing path (pyproject.toml, scripts/release-*, scripts/brainlayer-version-check.sh, publish.yml, ratchet.yml) and carries no ratchet:signatures label — a GitHub macOS runner bills at ~10× Linux minutes and rebuilds the keg venv from source codesign · installed keg scripts/release-verify-signatures.sh <keg> codesign-verifies every *.so/*.dylib under libexec/venv. The macOS parity job installs the published tap formula (etanhey/layers/brainlayer), so this row measures the release path — formula, published sdist and Homebrew's relocation — and not this PR's tree. Release-time baseline for the same keg on a different machine: 442 valid / 0 invalid — installed Mac (M4 Max), brew --prefix brainlayer 1.5.11, 2026-09-03.
BrainBar renders no enrichment status 🟢 GREEN 19 actual-view captures; active pending-store/replay retained macOS DEBUG binary · AppKit actual views · Vision OCR + icon RGBA comparison · private synthetic state Missing render capability/evidence is RED. Historical data is retained; this is source-build proof, not installed-app proof.
Watcher heartbeat freshness and recovery ⚪ n/a n/a — macOS native probe trigger-gated: no affected path or ratchet:heartbeat label private Python producer -> compiled Swift consumer Preserves genuine silence; independent agent stores cannot revive the watcher. Not a load or installed-UI measurement.
maintenance quiesce: services stay down under a reviving watchdog ⚪ n/a n/a — macOS replay trigger-gated: no affected path or ratchet:quiesce label real launchd · GitHub macOS runner · real maintenance + fleet-watchdog Fix: #1076 (bc6d690f4331387485678d0247e19e14e6ef8d97). Ceiling: 0 revivals; supervisor held, both resumed, no hold marker.
no cloud model call reachable anywhere 🟢 GREEN 520 source files; 460 installed imports; 3 native tests; wheel a21598b14afc; 0 candidate attempts immutable installed wheel (default + dev) + AST source + actual native BrainBar Synthetic private fixtures; SDK absence gated on merged R10b. Exact-head candidate proof, not deployment.

🟢 GREEN measured, within budget · 🔴 RED measured, out of budget — a finding to clear before merge · ⚪ n/a not measurable on this machine, never guessed.

No RED rows.

Measured on Linux/x86_64 · measured 1dd44efb7e86 · PR head 1dd44efb7e86 · checkout 1dd44efb7e86 · run · updated 2026-11-05 08:37:58 UTC

…ex chunks (#655)

Claude Code transcripts record message.model on every assistant turn, but
_extract_entry_metadata dropped it, so realtime-watcher and batch-index rows
could not answer 'which model wrote this?'. Read it into chunk metadata
['model'] for assistant entries only, verbatim, and leave it unknown when it
is absent, non-string, malformed, or Claude Code's '<synthetic>' placeholder.

The Codex parser, cited as the working path, actually stored model_provider
('openai') under 'model' and kept the first value for the whole session.
It now takes each turn_context model as the author of the following turn,
records model_provider separately, and stops attributing user and tool
chunks to a model.

Authoring provenance stays distinct from enrichment_model/enrichment_backend.

Refs #655

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3NoZDshC2wAT1TQqUTtFa
@EtanHey
EtanHey force-pushed the claude/orc-final-credit-batch-z2uwd0 branch from a03bca7 to 1dd44ef Compare November 5, 2026 07:28
@EtanHey
EtanHey merged commit f84e5be into main Nov 5, 2026
28 of 29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants