From 0b66d484980fc69644989a0a9b8d53e8d34402db Mon Sep 17 00:00:00 2001 From: Sinity Date: Sat, 1 Aug 2026 19:29:44 +0200 Subject: [PATCH 1/4] fix(storage): let a raw rewrite its own accepted revision head Problem polylogue-buq8/i415/lkos each independently flagged codex-session rows with message_count=0 despite 996 KB-3.3 MB of real event_msg/ response_item content in the raw bytes. Direct reproduction against the live archive's flagship sample (native_id 0199fada-d8bd-7fc0-997b-d23d3a6849c7) falsifies both prior hypotheses: running the current parse_stream_payload directly against the exact live raw bytes extracts 1,496 real messages correctly, so the content loss is not a codex parser defect in polylogue/sources/ (i415's "old rollout envelope" theory) and is not gated by raw_sessions. revision_authority='quarantined' either -- 1,742 of 1,757 quarantined codex-session rows in the live archive already have non-zero message counts, so that column is orthogonal to whether materialization ran (falsifying buq8/lkos's "materialization never runs" framing). The actual mechanism, confirmed via source.db/index.db inspection: each affected session's raw_revision_heads.accepted_raw_id equals its own sessions.raw_id, with decided_at_ms (the governance decision) months after sessions.updated_at_ms (the original, defective write). A bookkeeping-only backfill later recorded the raw as authoritative without re-running message extraction against it. revision_authority_refuses_write's "governed" check then refused every subsequent write for that session_id unconditionally -- including the very raw the backfill had just declared authoritative -- because it only checked whether any raw_revision_heads row existed for the session_id, never which raw it named. Whatever originally produced the zero-message content (a long-since-fixed parser bug, an interrupted write, or another historical defect this checkout can no longer observe) was permanently frozen: no future re-ingest tick could ever pass the gate to correct it. Solution storage/sqlite/archive_tiers/ingest_precedence.py: revision_authority_refuses_write now compares the incoming raw_id against raw_revision_heads.accepted_raw_id and refuses only when they differ (a losing/competing raw trying to overwrite the winner). A write for the accepted raw itself -- the winner re-asserting its own content -- is no longer refused, closing the corrective-rewrite gap without reopening it to arbitrary last-writer-wins (a different raw_id is still refused, pinned by a new regression test). tests/unit/pipeline/test_ingest_batch.py adds two regression tests: test_write_session_allows_rewrite_of_its_own_accepted_revision_head reproduces the exact defect shape (governed session, zero existing messages, same raw_id, real incoming content) and asserts the write now succeeds; test_write_session_still_refuses_a_different_raw_than_ the_accepted_head pins that the existing invariant (a different raw for a governed session_id stays refused) is untouched. Verification devtools test tests/unit/pipeline/test_ingest_batch.py tests/unit/storage/test_revision_replay.py tests/unit/storage/test_revision_application.py tests/unit/storage/test_raw_revision_authority.py tests/unit/sources/test_live_batch_support.py tests/unit/sources/test_revision_backfill.py tests/unit/storage/test_repair.py tests/unit/storage/test_raw_retention.py tests/unit/storage/test_raw_authority_ledger.py tests/unit/storage/test_quarantined_accepted_raw_repair.py tests/unit/storage/test_duplicate_raw_identity_repair.py tests/unit/storage/test_incremental_rebuild_equivalence.py tests/unit/daemon/test_raw_materialization_parse_stage_equivalence.py tests/unit/storage/test_browser_capture_origin_repair.py tests/unit/cli/test_status.py tests/unit/daemon/test_daemon_status.py tests/unit/maintenance/test_raw_authority_reset.py tests/integration/test_append_cohort_memory.py tests/unit/sources/test_dispatch_payloads.py tests/unit/sources/test_dispatch_ordering.py tests/unit/sources/test_source_laws.py -> 2 new tests pass; every other failure (24 in the raw-authority/ repair/append-cohort cluster, 1 in test_dispatch_payloads.py's unrelated repeated-session_meta message-count assertion) is reproduced identically on the unmodified baseline (verified via git stash), so this change adds zero regressions. New regression test verified to fail without the fix (reverted the production diff, reran, confirmed `assert False is True`, then reapplied). devtools verify --quick -> exit 0 (ruff format/check, mypy --strict, render all --check, topology/layering/closure-matrix/schema-policy lab checks all pass) Residual / follow-up This fix unblocks *future* re-ingest of the affected sessions but does not retroactively repair already-materialized rows: the live archive's zero-message sessions were written before this fix existed, so they still need a session-scoped reparse (a normal daemon reprocess tick touching those raw_ids, or `polylogue ops reset --index && polylogued run` for a full rebuild) to actually pick up the corrected behavior. This is not a schema change (no index.db structural delta, so no lifecycle.py SEMANTIC_REPARSE declaration applies) -- it is an application-logic fix in the write-precedence layer, and the repair step is an ordinary reprocess of the specific affected raw_ids, not an index generation bump. polylogue-buq8 and polylogue-lkos's framing ("materialization never runs" / "message-extraction defect distinct from the gate") does not match what the live data shows; i415's "old rollout envelope" parser theory is also not supported -- direct reproduction shows the current parser handles the flagship sample correctly. All three describe the same underlying write-gate defect fixed here, not three independent problems, and not the systemic raw-authority absorbing-state issue tracked separately (and still open) in polylogue-u19l. Ref polylogue-buq8, polylogue-i415, polylogue-lkos Co-Authored-By: Claude --- .../sqlite/archive_tiers/ingest_precedence.py | 33 ++++- tests/unit/pipeline/test_ingest_batch.py | 137 ++++++++++++++++++ 2 files changed, 165 insertions(+), 5 deletions(-) diff --git a/polylogue/storage/sqlite/archive_tiers/ingest_precedence.py b/polylogue/storage/sqlite/archive_tiers/ingest_precedence.py index 93c17e2d04..20c6f99a45 100644 --- a/polylogue/storage/sqlite/archive_tiers/ingest_precedence.py +++ b/polylogue/storage/sqlite/archive_tiers/ingest_precedence.py @@ -166,8 +166,31 @@ def revision_authority_refuses_write( Two independent refusals, checked in order: - an ACCEPTED revision-authority head already exists for this - ``session_id`` (``raw_revision_heads``) -- some other raw in this - cohort already won, so this write is redundant, never authoritative. + ``session_id`` (``raw_revision_heads``) for a *different* raw than the + one being written -- some other raw in this cohort already won, so + this write is redundant, never authoritative. A write whose own + ``raw_id`` **is** the accepted head is never refused here: it is the + winning raw attempting to (re)establish its own content, not a + competing/losing raw trying to overwrite the winner (polylogue-buq8/ + i415/lkos: ``raw_revision_heads`` can be populated by a bookkeeping- + only backfill pass -- e.g. ``backfill_historical_revision_evidence`` + recomputing authority for pre-governance sessions -- that records + *which* raw is authoritative without re-running message extraction + against it. Before this fix, that left a session's original, + pre-governance ``sessions``/``messages`` rows -- possibly written by a + long-since-fixed parser bug, an interrupted write, or any other + historical defect -- permanently frozen: every later ingest tick for + the very raw the backfill just named authoritative hit this same + unconditional "governed, skip" branch and could never re-parse or + correct it. Measured live: 11 ``codex-session`` rows with + ``message_count=0`` despite 996 KB-3.3 MB of real ``event_msg``/ + ``response_item`` content in the raw bytes, each one's + ``raw_revision_heads.accepted_raw_id`` equal to its own ``sessions. + raw_id`` -- direct reproduction against the live raw bytes confirms + the current parser already extracts the real messages correctly, so + the content loss was never in ``sources/`` parsing; it was this gate + refusing to let a corrective write through for a raw that was already + "won"). - this raw's own recorded membership decision for this ``provider_session_id`` is ``'ambiguous'`` -- ``classify_membership_ revisions`` genuinely refused to arbitrate a winner for this cohort, @@ -185,11 +208,11 @@ def revision_authority_refuses_write( "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'raw_revision_heads'" ).fetchone() if has_revision_heads is not None: - governed = conn.execute( - "SELECT 1 FROM raw_revision_heads WHERE session_id = ? LIMIT 1", + governed_row = conn.execute( + "SELECT accepted_raw_id FROM raw_revision_heads WHERE session_id = ? LIMIT 1", (session_id,), ).fetchone() - if governed is not None: + if governed_row is not None and str(governed_row[0]) != raw_id: return True if source_conn is None or not raw_id: return False diff --git a/tests/unit/pipeline/test_ingest_batch.py b/tests/unit/pipeline/test_ingest_batch.py index 07ba84e7a1..034a367b02 100644 --- a/tests/unit/pipeline/test_ingest_batch.py +++ b/tests/unit/pipeline/test_ingest_batch.py @@ -1765,6 +1765,143 @@ def test_write_session_allows_existing_upsert_even_without_messages(tmp_path: Pa assert counts["skipped_sessions"] == 0 +def test_write_session_allows_rewrite_of_its_own_accepted_revision_head(tmp_path: Path) -> None: + """polylogue-buq8/i415/lkos: a write for the raw ``raw_revision_heads`` + itself names as the accepted authority for this session must never be + refused, even though the session already has a governed head. + + Live-archive reproduction (2026-07-31): 11 ``codex-session`` rows carry + ``message_count=0`` despite 996 KB-3.3 MB of real ``event_msg``/ + ``response_item`` content in their raw bytes. Every one has + ``raw_revision_heads.accepted_raw_id`` equal to its own + ``sessions.raw_id`` -- ``decided_at_ms`` for the governance row postdates + ``sessions.updated_at_ms`` by months, proving a later bookkeeping-only + backfill (recomputing authority for a pre-governance session) recorded + the raw as authoritative without re-running message extraction against + it. Direct reproduction of ``parse_stream_payload`` against the exact + live raw bytes of the flagship sample (native_id + ``0199fada-d8bd-7fc0-997b-d23d3a6849c7``) confirms the current codex + parser already extracts 1,496 real messages correctly -- the content + loss was never a parser defect in ``sources/``. It was + ``revision_authority_refuses_write``'s ``governed`` check refusing + *every* write once ``raw_revision_heads`` had any row for the + ``session_id``, with no comparison against which raw the row actually + names -- permanently freezing whatever content the pre-governance write + happened to leave behind, including zero messages, because the very raw + the backfill just declared authoritative could never pass its own gate. + + Mutation that fails this: reverting the ``accepted_raw_id`` comparison + back to a bare existence check (``governed is not None: return True``). + """ + with open_connection(tmp_path / "index.db") as conn: + # Simulate the historical defect: a session was written (long ago, + # ``force_write=True`` standing in for whatever historical write + # path/bug left this content behind) with zero messages for raw + # "raw-accepted", then a bookkeeping-only backfill later recorded + # that same raw as the accepted revision head without ever + # re-writing the session's content. + stub = _session_data( + "codex-session:frozen-empty", + content_hash="hash-stub-empty", + raw_id="raw-accepted", + message_tuples=[], + ) + _write_session(conn, stub, force_write=True) + conn.execute( + "INSERT INTO raw_revision_heads (logical_source_key, session_id, accepted_raw_id, " + "accepted_source_revision, accepted_content_hash, accepted_frontier_kind, accepted_frontier, " + "acquisition_generation, decided_at_ms) VALUES " + "('codex:frozen-empty','codex-session:frozen-empty','raw-accepted','sr',?,'byte',1,0,1)", + (b"\x09" * 32,), + ) + conn.commit() + + real_msg = _message_tuple( + "msg-real", + "codex-session:frozen-empty", + role="user", + text="the real conversation content", + content_hash="hash-real", + sort_key=1.0, + ) + corrective = _session_data( + "codex-session:frozen-empty", + content_hash="hash-corrected", + raw_id="raw-accepted", + message_tuples=[real_msg], + ) + changed, counts = _write_session(conn, corrective) + conn.commit() + + stored = conn.execute( + "SELECT message_count FROM sessions WHERE session_id = ?", + ("codex-session:frozen-empty",), + ).fetchone() + + assert changed is True + assert counts["skipped_sessions"] == 0 + assert stored["message_count"] == 1 + + +def test_write_session_still_refuses_a_different_raw_than_the_accepted_head(tmp_path: Path) -> None: + """The mirror of the fix above: a *different*, non-accepted raw for a + governed session_id must still be refused -- the accepted-raw carve-out + must not reopen the door to an arbitrary competing/losing raw + overwriting the winner.""" + with open_connection(tmp_path / "index.db") as conn: + winner = _session_data( + "codex-session:governed", + content_hash="hash-winner", + raw_id="raw-winner", + message_tuples=[ + _message_tuple( + "msg-winner", + "codex-session:governed", + role="user", + text="winning content", + content_hash="hash-winner-msg", + sort_key=1.0, + ) + ], + ) + _write_session(conn, winner) + conn.execute( + "INSERT INTO raw_revision_heads (logical_source_key, session_id, accepted_raw_id, " + "accepted_source_revision, accepted_content_hash, accepted_frontier_kind, accepted_frontier, " + "acquisition_generation, decided_at_ms) VALUES " + "('codex:governed','codex-session:governed','raw-winner','sr',?,'byte',1,0,1)", + (b"\x0a" * 32,), + ) + conn.commit() + + loser = _session_data( + "codex-session:governed", + content_hash="hash-loser", + raw_id="raw-loser", + message_tuples=[ + _message_tuple( + "msg-loser", + "codex-session:governed", + role="user", + text="losing content", + content_hash="hash-loser-msg", + sort_key=1.0, + ) + ], + ) + changed, counts = _write_session(conn, loser) + conn.commit() + + stored = conn.execute( + "SELECT raw_id, message_count FROM sessions WHERE session_id = ?", + ("codex-session:governed",), + ).fetchone() + + assert changed is False + assert counts["skipped_sessions"] == 1 + assert dict(stored) == {"raw_id": "raw-winner", "message_count": 1} + + def test_write_session_refuses_a_raw_recorded_ambiguous_membership(tmp_path: Path) -> None: """``_write_session`` -- the daemon's default batch-ingest write path, used for most non-drive origins -- must refuse a session whose OWN From fc3fcdcbf7ab0324e62501fbfc1e4c19d11cea7e Mon Sep 17 00:00:00 2001 From: Sinity Date: Sat, 1 Aug 2026 19:32:09 +0200 Subject: [PATCH 2/4] chore(beads): record joint investigation findings on buq8/i415/lkos Left implementation-trail comments on all three beads: the root cause is one shared write-precedence gate defect (fixed in this branch), not three independent problems, and none of the beads' original root-cause framing (quarantine blocking materialization / old-format parser gap / distinct extraction defect) matches what direct reproduction against the live raw bytes shows. Co-Authored-By: Claude --- .beads/issues.jsonl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 65ad275e96..cc3ebb2b21 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,4 +1,4 @@ -{"_type":"issue","id":"polylogue-kadx3","title":"Daemon UDS socket path is machine-wide, not archive-scoped — CLI silently talks to the wrong archive","description":"Discovered live during PR #3517 perf work (g3jk lane, 2026-08-01): the daemon Unix domain socket path is derived from $XDG_RUNTIME_DIR/polylogue/daemon.sock with no archive-root component. A test/dev daemon pointed at a different archive is never actually reached by the CLI — any polylogue invocation on the same machine finds and talks to the live production daemon regardless of POLYLOGUE_ARCHIVE_ROOT or --archive-root. Worse: even explicit --no-daemon local execution through the real polylogue binary was observed ignoring POLYLOGUE_ARCHIVE_ROOT and serving results from the live personal archive during the lane agent's measurement work — real archive content leaked into agent shell output twice before the agent caught it and switched to an in-process/mocked-daemon harness. This extends the already-tracked polylogue-z9gh/polylogue-tas4 finding (same family: archive-root resolution not respected in some code path) but is a distinct, more severe manifestation: it means ANY agent or test running local devtools/CLI commands on this machine, believing it is isolated to POLYLOGUE_ARCHIVE_ROOT, may in fact be silently reading (and via other commands, potentially writing) the live production archive. No fix attempted by the discovering lane (out of its scope); needs dedicated investigation into (1) socket path derivation — should key off resolved archive root, not just XDG_RUNTIME_DIR, (2) the --no-daemon local path specifically, tracing why POLYLOGUE_ARCHIVE_ROOT was not honored there. Ref polylogue-z9gh, polylogue-tas4.","status":"open","priority":0,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-08-01T13:01:00Z","created_by":"Sinity","updated_at":"2026-08-01T13:01:00Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"polylogue-kadx3","title":"Daemon UDS socket path is machine-wide, not archive-scoped — CLI silently talks to the wrong archive","description":"Discovered live during PR #3517 perf work (g3jk lane, 2026-08-01): the daemon Unix domain socket path is derived from $XDG_RUNTIME_DIR/polylogue/daemon.sock with no archive-root component. A test/dev daemon pointed at a different archive is never actually reached by the CLI — any polylogue invocation on the same machine finds and talks to the live production daemon regardless of POLYLOGUE_ARCHIVE_ROOT or --archive-root. Worse: even explicit --no-daemon local execution through the real polylogue binary was observed ignoring POLYLOGUE_ARCHIVE_ROOT and serving results from the live personal archive during the lane agent's measurement work — real archive content leaked into agent shell output twice before the agent caught it and switched to an in-process/mocked-daemon harness. This extends the already-tracked polylogue-z9gh/polylogue-tas4 finding (same family: archive-root resolution not respected in some code path) but is a distinct, more severe manifestation: it means ANY agent or test running local devtools/CLI commands on this machine, believing it is isolated to POLYLOGUE_ARCHIVE_ROOT, may in fact be silently reading (and via other commands, potentially writing) the live production archive. No fix attempted by the discovering lane (out of its scope); needs dedicated investigation into (1) socket path derivation — should key off resolved archive root, not just XDG_RUNTIME_DIR, (2) the --no-daemon local path specifically, tracing why POLYLOGUE_ARCHIVE_ROOT was not honored there. Ref polylogue-z9gh, polylogue-tas4.","status":"in_progress","priority":0,"issue_type":"bug","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-08-01T13:01:00Z","created_by":"Sinity","updated_at":"2026-08-01T17:28:42Z","started_at":"2026-08-01T17:28:42Z","lease_expires_at":"2026-08-01T17:33:42Z","heartbeat_at":"2026-08-01T17:28:42Z","comments":[{"id":"63d2c93e-8f9e-5884-af9c-1697bd14ff56","issue_id":"polylogue-kadx3","author":"Sinity","text":"PR #3526 (feature/fix/daemon-socket-archive-scope, not yet merged) closes the socket-scoping half of this bead.\n\nScope understood: (1) archive-scope the daemon UDS socket path so two daemons for different archives never collide, (2) investigate whether --no-daemon genuinely ignores POLYLOGUE_ARCHIVE_ROOT.\n\nWhat changed: new polylogue/daemon/socket_path.py derives $XDG_RUNTIME_DIR/polylogue/\u003csha256-key-of-resolved-archive-root\u003e/daemon.sock instead of the old unscoped $XDG_RUNTIME_DIR/polylogue/daemon.sock. Updated daemon/cli.py (startup), cli/archive_query.py, cli/click_app.py, cli/commands/facets.py (all 3 CLI daemon-probe call sites) to pass config.archive_root / archive_root_path -- the same value already used for /api/health probe matching, so probe and bind now use identical scoping.\n\n--no-daemon finding: could NOT reproduce as a separate defect. Live-reproduced against the actual CLI entry point (python3 with the worktree's own source on PYTHONPATH, avoiding the shared-venv editable-hijack hazard) with POLYLOGUE_ARCHIVE_ROOT pointed at a scratch dir, both with and without --no-daemon: in every case the CLI correctly looked for the scratch archive's own index.db and failed cleanly, never fell through to a real archive. This is consistent with polylogue/paths/_roots.py's existing archive-root scoping (polylogue-4ma3, polylogue-o7hx) and the hundreds of existing --no-daemon tests already isolated per-archive-root. My assessment: the observed leak in the discovering lane's harness is fully explained by the socket collision alone -- the harness must have reached the daemon-fallback path (not --no-daemon), and pre-fix that path could reach whichever daemon last stole the shared socket, i.e. the real production polylogued. No separate --no-daemon regression test was added since I could not confirm a real defect there (per the bead's own conditional instruction).\n\nVerification: new tests/unit/daemon/test_uds_socket_scoping.py (5 tests, including a live two-daemon-two-archive-roots-one-runtime-dir integration test proving no cross-talk/socket theft); devtools test across golden-parity + facets/click_app/archive_query/daemon_cli suites, 326 passed; devtools verify --quick exit 0 (had to register the new hashlib.sha256 call site in docs/plans/hash-boundary-registry.yaml as `identifier` classification, and regenerate docs/plans/topology-target.yaml for the new module).\n\nAcceptance criteria: (1) socket-path archive-scoping -- satisfied, PR #3526. (2) --no-daemon POLYLOGUE_ARCHIVE_ROOT-honoring gap -- investigated, not reproduced as a real defect; treating as explained by (1) rather than a distinct bug requiring its own fix.\n\nNot merging this PR myself per repo convention (agent-opened PRs still go through the merge-gate check before squash-merge).","created_at":"2026-08-01T17:29:04Z"}],"dependency_count":0,"dependent_count":0,"comment_count":1} {"_type":"issue","id":"polylogue-ov5r","title":"Full schema regeneration (replace_provider_packages) silently narrows committed packages -- no monotonic merge","description":"devtools schema-generate / SchemaRegistry.replace_provider_packages (polylogue/schemas/runtime_registry.py:543) unconditionally deletes a provider's entire versions/ tree and rewrites it from a fresh full-corpus generation, with NO merge against the committed prior schema. This is a live, silent-narrowing regression -- distinct from and NOT caught by devtools schema-audit or polylogue.schemas.promotion_audit (privacy/secrets scanner only).\n\ntests/unit/schemas/test_promotion_monotonicity.py already documents and guards against exactly this failure mode for the OTHER promotion surface (SchemaRegistry.promote_cluster / merge_observed_structure_schemas), citing a real 2026-07-29 incident where a codex/claude-code promotion \"narrowed 33 field types and dropped 173 fields.\" That guard is wired into promote_cluster only -- generate_provider_schema -\u003e persist_generated_provider_bundle -\u003e replace_provider_packages has no equivalent, and is the path `devtools lab schema generate --provider X --output-dir polylogue/schemas/providers` (the documented, sanctioned regeneration entrypoint) actually calls.\n\nREPRODUCED 2026-08-01 while executing polylogue-2qx.3 AC1 (regenerate schema packages for every provider from the live archive). Ran `devtools schema-generate` (full corpus, no sample cap) for all 8 corpus-driven providers against the live /realm/db/polylogue archive, then diffed the OLD (git HEAD) committed schema.json.gz files against the NEW regenerated ones by JSON-Schema leaf-path type union (same method test_promotion_monotonicity.py's `_types_by_path` uses):\n\n| provider | old distinct typed paths | new distinct typed paths | paths ENTIRELY LOST | paths with narrowed type union |\n|---|---|---|---|---|\n| claude-code | 944 | 250 | 722 | 735 |\n| codex | 188 | 1095 | 0 | 3 (reproduces the literal named incident: `.timestamp` `[\"number\",\"string\"]` -\u003e `[\"string\"]`) |\n| chatgpt | 2209 | 2242 | 10 | 25 |\n| claude-ai (claude-ai-export) | 592 | 488 | 109 | 109 |\n| gemini (aistudio-drive) | 200 | 191 | 9 | 9 |\n| gemini-cli | 127 | 124 | 3 | 3 |\n| hermes | 1314 | 1288 | 26 | 26 |\n\nRoot cause hypothesis (not fully diagnosed): the current clustering/package-selection algorithm (`_build_package_candidates`, `selection_rationale` in catalog.json) fragments what used to be one large amalgamated package (e.g. claude-code's old single v1 \"session_record_stream\" element with sample_count=2,171,910) into many small structurally-distinct packages plus large \"orphan_adjunct_counts\" buckets that never get individually retained as elements at all -- so most raw structural diversity observed in a full-corpus run never reaches any committed .schema.json.gz, and what's committed is strictly narrower than the March 2026 baseline even though the underlying corpus grew.\n\nNone of this generated output was committed -- reverted in full (`git checkout -- polylogue/schemas/providers/ \u0026\u0026 git clean -fd`) before opening any PR, per the same monotonicity concern this bead's own sibling test enforces elsewhere.\n\nFix direction (not designed here): either (a) route replace_provider_packages through the same merge_observed_structure_schemas monotonic-merge helper promote_cluster already uses, keyed per (provider, element_kind) rather than per exact version, or (b) change persist_generated_provider_bundle to merge new element schemas into the existing committed ones before writing rather than deleting versions/ wholesale. Whichever direction is chosen needs its own before/after leaf-path-union regression test analogous to test_promotion_monotonicity.py, scoped to the generate path specifically since that suite currently only covers promote_cluster.\n\nBlocks polylogue-2qx.3 AC1 from being safely satisfied via the current `devtools lab schema generate`/`schema-generate` mechanism until fixed.\n","status":"closed","priority":0,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-08-01T09:51:22Z","created_by":"Sinity","updated_at":"2026-08-01T10:24:11Z","closed_at":"2026-08-01T10:24:11Z","close_reason":"Merged PR #3502 (a7a576535): merge_observed_structure_schemas wired into replace_provider_packages via _merge_element_schema_with_existing, so a full-corpus regen merges against committed history instead of destructively replacing it (8 new monotonicity tests, 71 total passed). Two real regressions in the fix itself (nested annotation stripping, unobserved-element-kind loss) found by post-merge review and tracked separately as polylogue-46kg (P1) — do not rerun 2qx.3's promotion attempt until 46kg lands too. Force-closed: the bd dependency direction (ov5r blocked-by 2qx.3) is backwards — ov5r's own scope is independently complete; 2qx.3's AC1 is what actually depends on ov5r+46kg.","labels":["area:ingest","area:sources"],"dependencies":[{"issue_id":"polylogue-ov5r","depends_on_id":"polylogue-2qx.3","type":"blocks","created_at":"2026-08-01T11:51:21Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-6mpy","title":"Claude Code content-classification gate refuses genuine no-OriginSpec session records","description":"Discovered while verifying polylogue-fpid on fresh worktree from origin/master\n(2026-07-31, commit 5798b3dd1 + literal_check restore).\n\ntests/unit/sources/test_revision_backfill.py::test_parse_one_still_replays_real_claude_code_sessions_with_no_path_rule\nfails on a clean checkout with no relation to polylogue-fpid's changes\n(confirmed by running the identical test against the untouched HEAD copy of\npolylogue/sources/revision_backfill.py -- same failure, byte-identical\nassertion):\n\n E assert 0 == 1\n + where 0 = len([])\n tests/unit/sources/test_revision_backfill.py:265: assert 0 == 1\n\nThe test guards against the regression-direction failure mode for the\ncontent-classification gate added for polylogue-9ykn: a genuine Claude Code\nsession record (role=user, real timestamp, sessionId) at a path carrying no\nmatching OriginSpec path rule should still parse to 1 session via _parse_one.\nCurrently it parses to 0 -- the gate is refusing content it must accept.\n\nAlso affects (same root cause, confirmed failing identically on a clean\nworktree independent of any fpid change):\n - tests/unit/sources/test_live_batch_support.py::test_full_ingest_skips_durably_excised_content_without_aborting_batch\n - tests/unit/sources/test_live_batch_support.py::test_append_multi_session_payload_is_rejected_before_index_write\n - tests/unit/sources/test_live_batch_support.py::test_full_ingest_writes_archive_with_route_observability\n - tests/unit/pipeline/test_ingest_batch.py::test_primary_mode_projects_revision_after_allowed_durable_receipt\n - tests/unit/pipeline/test_ingest_batch.py::test_primary_mode_keeps_unconfirmed_revision_out_of_index_and_fts\n\nLikely landed in one of today's merges to master (5798b3dd1's cluster, or an\nadjacent same-day PR touching sources/dispatch.py's content-classification\ngate / origin_specs.py path-rule matching) -- not bisected further here since\nit is out of scope for polylogue-fpid.\n\nImpact: blocks a clean `devtools test tests/unit/sources` / `tests/unit/pipeline`\nrun on current master; every fresh worktree inherits it.","status":"closed","priority":0,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T15:19:18Z","created_by":"Sinity","updated_at":"2026-07-31T22:54:41Z","closed_at":"2026-07-31T22:54:41Z","close_reason":"Merged PR #3497 (c6190d7db): record content now overrides the analysis/-dir path guess, so genuine Claude Code session records with no OriginSpec path rule classify correctly; the failing master test is green.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-7qfq","title":"literal_check deleted by #3458 despite live call sites in archive_tiers/index.py","description":"PR #3458 (commit 5798b3dd1, \"refactor: collapse duplicate implementations,\ndead shims, and split vocabularies\") deleted\npolylogue/storage/sqlite/archive_tiers/common.py:literal_check(), claiming\n\"the helper had zero call sites\" (citing bead polylogue-u6tl).\n\nThat was true when polylogue-u6tl was filed, but PR #3451\n(\"feat(storage): wire literal_check into DDL, fix a drift CHECK gap\",\nmerged earlier the same day as #3458 landed) had already added two live\ncall sites at polylogue/storage/sqlite/archive_tiers/index.py:1642 and\n:1657 (DelegationMappingState / DelegationResultStatus CHECK generation).\n#3458's audit was stale by the time it merged -- a duplication sweep and a\nnew-feature PR raced, and the sweep's \"zero call sites\" grep predated the\nnew usage.\n\nImpact: every import of polylogue.storage.sqlite.archive_tiers.index (and\neverything downstream -- archive.py, embeddings, most of devtools, most of\nthe test suite via tests/infra fixtures) raises\n ImportError: cannot import name 'literal_check' from\n 'polylogue.storage.sqlite.archive_tiers.common'\non current master. This is a full verification-blocking regression, not a\ncosmetic one -- `devtools test`, `pytest`, and `devtools status` all fail\nimmediately.\n\nDiscovered while verifying polylogue-4ma3 (archive_root resolution fix) --\ndevtools test/verify could not run at all until this was fixed.","status":"closed","priority":0,"issue_type":"bug","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T14:22:09Z","created_by":"Sinity","updated_at":"2026-07-31T14:36:09Z","started_at":"2026-07-31T14:22:19Z","closed_at":"2026-07-31T14:36:09Z","close_reason":"Duplicate — PR #3464 (aeea9c4c9) already fixed this identically, merged before mine. Rebased feature/fix/archive-root-resolution onto post-#3464 master and dropped the duplicate commit.","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -109,15 +109,15 @@ {"_type":"issue","id":"polylogue-jc4q","title":"claude-code-session fork/subagent/resume files collide with parent's provider_session_id in revision membership","description":"Measured while verifying polylogue-oycw's fix (#3401/#3405) against real\n'ambiguous-only' cohorts. Reparsed all 185 real claude-code-session\nambiguous cohorts with the CURRENT set-based classifier (read-only\nsimulation against /realm/db/polylogue source.db + blob store, no writes):\nonly 56/185 (30.3%) resolve cleanly; 125/185 (67.6%) still hit a genuine\n`conflict` verdict -- much higher than chatgpt-export (7.4%) or\nclaude-ai-export (6.5%), and worth investigating on its own.\n\nThis is NOT the same shape of defect as the other two follow-ups\n(polylogue-uqwd, and the claude-ai content_blocks bead). Deep-dived one\ncohort in detail: logical_source_key grouping raw ids whose stored\nprovider_session_id is 0213d48f-5b7a-4241-b77a-eb714672dc3b has 7 members\nfrom source paths:\n\n .../drive-cache/gemini/0213d48f-5b7a-4241-b77a-eb714672dc3b.jsonl.txt.json\n .../.claude/projects/-realm-project-sinex/0213d48f-...jsonl (x2, different acquisitions)\n .../.claude/projects/-realm-project-sinex/a3a274a2-02cc-456a-b4f5-30f1e60229e5.jsonl (x2)\n .../.claude/projects/-realm-project-sinex/cbea0c3a-6cee-4906-a2b8-1499b2b8809a.jsonl (x2)\n\nThree of these files carry a UUID in their OWN filename (a3a274a2...,\ncbea0c3a...) that is DIFFERENT from the reported provider_session_id\n(0213d48f...) -- and when parsed, they yield only 3 and 5 messages\nrespectively (frontier (3,0,0,0) / (5,0,0,0)) versus 213-214 messages for\nthe three 0213d48f-named files. The parser is asserting these tiny,\ndifferently-named files share session identity with the large session --\nalmost certainly Claude Code fork/resume/subagent files whose early\nrecords (`summary`/leaf pointers) still reference the ROOT ancestor's\nsession id.\n\nThe trio of large, same-provider-session-id revisions (0964ee2c/b8282869/\n608c1916, 213-214 msgs each) DOES resolve correctly under the fixed\nrelation (a_contains_b/b_contains_a chain) -- this is the specific pairwise\nrelation a sibling investigation verified independently. The problem is\nthe tiny a3a274a2/cbea0c3a files being folded into the SAME cohort at all:\ncomparing a 3-message fork snippet against a 213-message parent under one\nshared identity is exactly the \"genuinely divergent content under one\nidentity\" case that must stay visible as ambiguous rather than being\ncoerced -- and it correctly does -- but the identity assignment upstream\n(what makes these count as \"the same session\" in the first place) looks\nwrong. `session_links`/lineage normalization (branch_point_message_id,\ninheritance: prefix-sharing/spawned-fresh) exists specifically to model a\nforked/resumed child as ITS OWN session with a recorded relationship to\nthe parent, not as a same-identity revision of the parent -- if these\nfiles were resolved through that path instead of colliding on\nprovider_session_id, revision membership would never see them as a cohort\nat all.\n\nNeeds a proper investigation of how claude-code JSONL parsing derives\nprovider_session_id for forked/resumed/subagent files, and whether that\nderivation should route through session-lineage assignment instead of (or\nbefore) raw revision-membership grouping. This is materially larger than\npolylogue-oycw's positional-prefix fix and belongs to the lineage/identity\nlayer, not the comparison-relation layer -- filed as its own investigation\nrather than folded into either.\n\nRef polylogue-oycw, polylogue-aggz","notes":"Fixed. Root cause confirmed by reading real ~/.claude/projects files (not inferred from the parser): Claude Code re-stamps a handful of records with an ANCESTOR session's sessionId even inside a file that is otherwise entirely a different session's own content -- either a leading resume/fork boundary record, or (the specific 0213d48f/a3a274a2/cbea0c3a case measured live) a mid-file quirk where a `/exit` sent right after \"usage limit reached\" gets tagged with the ancestor's id even though it chains straight off the file's own preceding record. Both shapes verified directly in tests/fixtures and a live corpus read.\n\nFix: dispatch.py's Claude Code grouping (_claude_code_grouped_record_specs eager, _claude_code_stream_sessions streaming) now identifies each file's own real content (the largest sessionId-grouped run) as \"primary\" and detects a carryover run via two structural signals (occurs before primary's first record, or its root parentUuid resolves into a uuid primary already produced) -- not content-shape heuristics. A carryover run's identity is qualified (f\"{ancestor_id}:{fallback_id}\") so siblings off one ancestor never collide with each other or the ancestor; the ancestor id becomes parent_session_id, routing through session_links/lineage exactly as the bead's own framing called for (\"model a forked/resumed child as ITS OWN session with a recorded relationship to the parent, not as a same-identity revision of the parent\") instead of re-deriving identity or adding a resolution heuristic to the classifier. New explicit trust_fallback_id flag threaded through LoweredPayloadSpec/parse_code/parse_code_stream/_parse_code_records makes this override the parser's default \"trust the record's own sessionId\" ONLY for these dispatch-proven fragments; subagent/self-compaction files (agent-* fallback_id) keep their untouched existing scheme.\n\nSEMANTIC_REPARSE: INDEX_SCHEMA_VERSION bumped to 53 (lifecycle.py declaration added) -- changes sessions.native_id and session_links edges for affected raw acquisitions.\n\nVerification: devtools verify --quick green (mypy/lint/layering/schema-versioning policy). devtools test across tests/unit/sources/{test_dispatch_payloads,test_parsers_claude_code_artifacts,test_claude_code_sidecar_evidence,test_tool_result_sidecars,test_claude_code_normalization_laws,test_source_laws}.py, tests/unit/pipeline/test_archive_ingest_shared_raw.py, tests/unit/storage/{test_revision_replay,test_index_fast_forward_lifecycle,test_schema_policy_contracts,test_archive_tiers_ddl}.py: 320 passed, 2 pre-existing failures confirmed unrelated via a throwaway detached origin/master worktree with zero jc4q changes applied (filed as polylogue-yl8t and polylogue-ihro).\n\nBefore/after resolution rate: not re-measured against the live archive in this PR (would require polylogue ops reset --index \u0026\u0026 polylogued run, out of scope for a code-only PR) -- the new sibling-carryover regression test in test_archive_ingest_shared_raw.py directly proves the collision no longer occurs for the exact measured shape.\n","status":"closed","priority":1,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T14:43:10Z","created_by":"Sinity","updated_at":"2026-07-31T16:28:34Z","closed_at":"2026-07-31T16:28:34Z","close_reason":"Fixed via PR #3472 (merged 39d72ad5). dispatch.py's Claude Code grouping now identifies each file's real content as primary and qualifies carryover-fragment identity instead of colliding on the ancestor's bare provider_session_id; ancestor reference routes through parent_session_id/session_links. SEMANTIC_REPARSE index v53. Regression test added. Live before/after resolution-rate re-measurement deferred (needs polylogue ops reset --index \u0026\u0026 polylogued run against the real archive, tracked separately if operator wants it run).","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-jsxj","title":"test_daemon_cli whale-pass tests broken by #3455's load_polylogue_config signature","description":"Discovered 2026-07-31 while verifying polylogue-u19l/w32w. tests/unit/daemon/test_daemon_cli.py::test_maybe_run_raw_materialization_whale_pass_no_candidate_skips_writer and test_maybe_run_raw_materialization_whale_pass_runs_scoped_pass_and_emits_events both fail on origin/master HEAD (5798b3dd1) with: TypeError: \u003clambda\u003e() got an unexpected keyword argument '_bootstrap', raised from polylogue/config.py:2173 (archive_root() -\u003e load_polylogue_config(_bootstrap=bootstrap)). Both tests monkeypatch polylogue.config.load_polylogue_config with a lambda that doesn't accept _bootstrap. Root cause is almost certainly #3455 (refactor(config): delete two inert config keys and the whale off-switch) since it touched both config.py and this exact test file in the same commit and the failing tests are literally named after that PR's 'whale pass' feature. Unrelated to raw_reconciler.py/archive_tiers/common.py; reproduces before and after the u19l/w32w fix. Needs the two lambdas updated to accept **kwargs or an explicit _bootstrap param.","status":"closed","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T14:36:20Z","created_by":"Sinity","updated_at":"2026-07-31T22:34:30Z","closed_at":"2026-07-31T22:34:30Z","close_reason":"Merged PR #3489 (test-side fix): whale-pass tests patch polylogue.paths.archive_root/render_root directly (the seam ~20 sibling tests use) instead of a zero-arg load_polylogue_config lambda incompatible with #3455's signature; production behavior was correct (documented at paths/_roots.py:133).","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-gb4e","title":"Consumer-reachability gate: per-PR check that new surfaces have production callers","description":"Anti-vacuity is currently prose-only. Natural-experiment evidence from the 2026-07-30 fanout: tests asserting guessed field names absent from any corpus; a test-local reimplementation of the unit under test; 1.8M rows written by code nothing reads; polylogue-nua7 (unread-wire batch landed 3 tables + reader chains with zero surface consumers). Generative cause: acceptance criteria terminate at the producer and no gate can see a missing consumer. Build a bounded per-PR gate: for every module/table/tool the diff ADDS, require a reachable production caller (import-graph walk from entrypoints; for tables, a reader outside tests) or an explicit waiver line in the PR body. Intersects polylogue-h75b (vulture+coverage+affordance dead-code lane) - this is the per-PR incremental variant of that whole-repo lane.","status":"open","priority":1,"issue_type":"feature","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T13:39:55Z","created_by":"Sinity","updated_at":"2026-07-31T22:35:46Z","dependencies":[{"issue_id":"polylogue-gb4e","depends_on_id":"polylogue-h75b","type":"blocks","created_at":"2026-07-31T15:39:55Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"polylogue-c831","title":"message_type classification drifts from persisted rows: 1919 live candidates the ingest path never re-stamps","description":"Evidence (2026-07-31, read-only scan of the live index.db): running the exact message_type_backfill classifier pass (storage/message_type_backfill.py: _message_text_by_id_sql + classify_text_message_type) over all 1,219,627 message_type='message' rows finds 1,919 rows the current classifier would flip to context/protocol.\n\nWhy this is a bug and not just pending maintenance: the invariant (#839) is that persisted message_type is the single source of truth and the ingest materialization path assigns it at write time. If every row had been written by the current classifier, candidates would be 0 by construction. 1,919 candidates means classifier semantics changed after those rows were materialized WITHOUT a SEMANTIC_REPARSE index delta (storage/sqlite/lifecycle.py), so the automatic path silently diverged from the declared regime.\n\nPer the no-break-glass policy, the manual 'ops doctor --repair --target message_type_backfill' surface cannot be deleted while it has live work; the real fix is in the automatic path:\n1. Determine which classifier change created the drift (git log over polylogue/archive/message/artifacts.py vs the affected rows' ingest dates).\n2. Decide: either classifier changes are declared SEMANTIC_REPARSE deltas (schema-versioning policy applies to classifier semantics), or the daemon owns a bounded convergence pass that re-stamps message_type when the classifier fingerprint changes.\n3. Once the automatic path provably converges this, delete the message_type_backfill manual target (same shape as the session_timestamp_backfill removal in the escape-hatch sweep PR).\n\nFound during the escape-hatch/defensive-scaffolding sweep (worktree agent lane).","status":"open","priority":1,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T13:10:53Z","created_by":"Sinity","updated_at":"2026-07-31T13:10:53Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"polylogue-c831","title":"message_type classification drifts from persisted rows: 1919 live candidates the ingest path never re-stamps","description":"Evidence (2026-07-31, read-only scan of the live index.db): running the exact message_type_backfill classifier pass (storage/message_type_backfill.py: _message_text_by_id_sql + classify_text_message_type) over all 1,219,627 message_type='message' rows finds 1,919 rows the current classifier would flip to context/protocol.\n\nWhy this is a bug and not just pending maintenance: the invariant (#839) is that persisted message_type is the single source of truth and the ingest materialization path assigns it at write time. If every row had been written by the current classifier, candidates would be 0 by construction. 1,919 candidates means classifier semantics changed after those rows were materialized WITHOUT a SEMANTIC_REPARSE index delta (storage/sqlite/lifecycle.py), so the automatic path silently diverged from the declared regime.\n\nPer the no-break-glass policy, the manual 'ops doctor --repair --target message_type_backfill' surface cannot be deleted while it has live work; the real fix is in the automatic path:\n1. Determine which classifier change created the drift (git log over polylogue/archive/message/artifacts.py vs the affected rows' ingest dates).\n2. Decide: either classifier changes are declared SEMANTIC_REPARSE deltas (schema-versioning policy applies to classifier semantics), or the daemon owns a bounded convergence pass that re-stamps message_type when the classifier fingerprint changes.\n3. Once the automatic path provably converges this, delete the message_type_backfill manual target (same shape as the session_timestamp_backfill removal in the escape-hatch sweep PR).\n\nFound during the escape-hatch/defensive-scaffolding sweep (worktree agent lane).","notes":"Root cause found: (b), a genuine ingest-time/backfill-time classification divergence, not a classifier-semantics-changed-after-materialization scenario.\n\nstorage/message_type_backfill.py's `_message_text_by_id_sql()` reconstructs a message's classification input from ONLY persisted TEXT-type `blocks` rows (via `message_prose_sql(block_types=(\"text\",))`), deliberately excluding thinking/tool_use/tool_result content.\n\nBut the Claude Code ingest path (polylogue/sources/parsers/claude/code_parser.py, via extract_message_text -\u003e claude/common.py:extract_text_from_segments) builds `_message_type_from_code_record`'s classification input from a COMBINED string that also folds in THINKING (wrapped `\u003cthinking\u003e...\u003c/thinking\u003e`) and TOOL_USE/TOOL_RESULT (JSON-dumped) segment content, even though those are split into their own separate ParsedContentBlock rows before being persisted. When a THINKING/TOOL_USE segment happens to contain a classifier marker (e.g. a `\u003cfile path=` line, a `\u003csystem\u003e` block) that the message's own TEXT block does not carry, ingest-time classification and a later backfill re-run over the persisted TEXT-only blocks disagree -- this is exactly the 1,919-row drift found in the bead's evidence scan; Codex (extract_codex_text pulls only text/input_text/output_text fields) and ChatGPT parsers were checked and do not exhibit the same combined-text pattern.\n\nFix (PR, not yet merged): added `text_blocks_prose()` (polylogue/sources/parsers/base_support.py, re-exported via base.py) -- the parse-time twin of `message_prose_sql(block_types=(\"text\",))` -- and changed code_parser.py's `_message_type_from_code_record` call site to classify from `text_blocks_prose(content_blocks)` (the message's own already-split TEXT blocks) instead of the combined `extract_message_text` string. Added a regression test (tests/unit/sources/test_parsers_claude_code_artifacts.py::test_parse_code_classifies_message_type_from_text_blocks_only) with a THINKING block carrying a `\u003cfile path=` marker + a plain TEXT reply block; verified it fails (misclassifies as CONTEXT) without the fix and passes with it.\n\nNot done as part of this fix (explicitly out of scope, no bd evidence of drift there): `classify_material_origin`'s `text=text` argument in code_parser.py still receives the combined multi-segment string, which has the same latent-divergence shape for OPERATOR_COMMAND/GENERATED_*_PACK markers. Left unchanged since the bead's 1,919-row evidence scan was message_type-specific; a follow-up bead may be warranted if material_origin drift is independently measured.\n\nRemaining scope per the bead's own 3-step list: step 1 (git-log dating which classifier change caused this) is now answered differently than assumed -- there was no dated classifier-semantics change; the divergence has existed since the Claude Code parser and the TEXT-only backfill reconstruction were both written, they just used different logic. Step 3 (retiring the manual `message_type_backfill` maintenance target) is NOT done here -- the existing 1,919 already-materialized rows still need one operational backfill run (`polylogue ops doctor --repair --target message_type_backfill` or equivalent) to converge; this PR only stops NEW drift from accumulating on future Claude Code ingests. Filing that as separate operational follow-up is appropriate rather than folding a live-archive repair into this PR.\nPR opened: https://github.com/Sinity/polylogue/pull/3525 (fix/parsers/claude-code-message-type-text-blocks-only). Not merged -- awaiting CI/review per standard workflow. Bead left in_progress.","status":"in_progress","priority":1,"issue_type":"task","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T13:10:53Z","created_by":"Sinity","updated_at":"2026-08-01T17:27:44Z","started_at":"2026-08-01T17:24:51Z","lease_expires_at":"2026-08-01T17:29:51Z","heartbeat_at":"2026-08-01T17:24:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-sd9s","title":"Drift-sentinel CHECK rejects known_field_unread and the writer swallows the whole batch","description":"Kind-proliferation audit finding (exact precedent of the pattern this repo already hit once).\n\nDriftClassification (polylogue/schemas/drift_sentinel.py:48) has 4 members: unseen_shape, new_field, field_changed, known_field_unread. The ops-tier DDL hand-writes the CHECK with only 3: polylogue/storage/sqlite/archive_tiers/ops.py:295 'CHECK(classification IN (unseen_shape, new_field, field_changed))'.\n\nclassify() returns KNOWN_FIELD_UNREAD (drift_sentinel.py:113) and it is in RISKY_CLASSIFICATIONS (line 65) — the exact class the sentinel exists to surface. When such an observation reaches record_schema_drift_sample, the INSERT violates the CHECK, raises IntegrityError, and the sole caller (schemas/drift_sentinel_sampling.py:81-83) catches 'except sqlite3.Error: logger.debug(...); return 0' — silently discarding not just that row but the ENTIRE batch of observations in the same call.\n\nLive evidence: sqlite3 file:/realm/db/polylogue/ops.db?mode=ro 'SELECT classification, count(*) FROM schema_drift_samples GROUP BY 1' -\u003e unseen_shape 313 only.\n\nFix shape: (1) generate the CHECK from the Literal (literal_check('classification', *get_args(DriftClassification))) so Python type and SQL constraint cannot drift — this is the repo's own stated pattern (CLAUDE.md 'CHECK constraints are generated from Python types') that this table bypassed; (2) ops.db is disposable but DDL is CREATE TABLE IF NOT EXISTS — an existing live table keeps the stale CHECK, so add ops-bootstrap convergence (detect stale CHECK via sqlite_master.sql, drop+recreate the telemetry table; 313 rows, disposable tier); (3) narrow the except in drift_sentinel_sampling so a constraint violation is at least per-row and logged above debug.","status":"open","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T13:08:06Z","created_by":"Sinity","updated_at":"2026-07-31T13:08:06Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-rlvj","title":"FTS freshness ledger: targeted repair overwrites global ready with unmeasured missing_rows","description":"Surface-coherence audit 2026-07-31: live archive shows messages_fts reporting state=ready, missing_rows=0 while an independent count shows 12,659 blocks (blocks.search_text != '' minus messages_fts_docsize rows) are unindexed. Root cause: daemon/convergence_stages.py's _mark_message_fts_ready_after_targeted_repair() called message_fts_readiness_sync(conn, verify_total_rows=False) -- a cheap existence check (any indexed row AND any indexable row) that is almost always true -- and then wrote state=READY, missing_rows=0 unconditionally over the single global fts_freshness_state row for messages_fts, discarding whatever accurate missing_rows an earlier exact snapshot (fts_invariant_snapshot_sync) had recorded. threads_fts does not have this bug: it is only ever written from the exact archive-wide invariant, so it correctly reports stale/10 for the same archive. Fix: make the targeted-repair marker always source its row from fts_invariant_snapshot_sync (same anti-join query the hourly fts_orphan_audit sweep already runs), and add a table-level CHECK (state='ready' implies missing_rows=0 AND excess_rows=0 AND duplicate_rows=0 AND source_rows=indexed_rows) to fts_freshness_state (index schema v51, CONSTRAINT_ONLY) so the contradiction is unconstructible going forward. Distinct from polylogue-8zzs/polylogue-oitx (the fabricated-100%-default class): this is a correct measurement of the wrong (scoped, not global) population, not a hard-coded default over a NULL.","status":"closed","priority":1,"issue_type":"bug","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T13:07:02Z","created_by":"Sinity","updated_at":"2026-07-31T13:56:58Z","started_at":"2026-07-31T13:36:54Z","closed_at":"2026-07-31T13:56:58Z","close_reason":"Fixed in PR #3461 (merged 7d30cc497): daemon/convergence_stages.py's _mark_message_fts_ready_after_targeted_repair now sources its row from the real archive-wide fts_invariant_snapshot_sync instead of a cheap existence check, and index schema v52 adds a CHECK constraint (state='ready' implies balanced counters) making the ledger's specific contradiction unconstructible, with a fast-forward sanitizer for archives already poisoned by the bug. session_work_events_fts independently verified fine (27,034/27,034, 0 anti-join gap). 12,659-block gap cause: mix of expected hot-session catch-up lag plus genuine static-session drift (chatgpt-export/claude-ai-export, ~3,558 blocks) that fts_orphan_audit's existing repair path closes once a daemon build with this fix runs; this PR does not itself backfill rows.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-6krh","title":"Deliberate convergence deferrals are recorded as status='failed'; the 'deferred' value is never written","description":"Audit 2026-07-31 (debt-taxonomy report, /realm/inbox/polylogue-audits-2026-07-31/debt-taxonomy.html).\n\nMEASURED on live ops.db:\n SELECT status, COUNT(*) FROM convergence_debt GROUP BY 1;\n failed | 325\n SELECT last_error, COUNT(*) FROM convergence_debt GROUP BY 1;\n 'live membership ingest deferred FTS to preserve writer availability' | 324\n 'live full ingest deferred FTS to preserve writer availability' | 1\n\nEvery live row is a SUCCESSFUL, INTENTIONAL deferral (the false_means_pending\ncontract doing exactly what it is designed to do) filed under status='failed'.\nThe schema's CHECK admits ('failed','deferred') and 'deferred' has never been\nwritten.\n\nConsequence: every health/alert/status surface that counts status='failed'\nreports correct bounded-work behaviour as failure --\n polylogue/daemon/health.py:908\n polylogue/daemon/status.py:2241\n polylogue/cli/commands/status.py:916\n polylogue/api/archive.py:1452 (status IN ('failed','deferred'))\n polylogue/daemon/metrics.py:379 (polylogue_convergence_debt_count gauge)\n\nThis is the inverse of the usual pathology: not a defect hiding behind a\nlegitimate-sounding ledger state, but a legitimate mechanism wearing a defect's\nlabel. It makes convergence_debt unusable as an alerting signal, because a real\nfailure and a designed deferral are indistinguishable.\n\nFIX: the deferral path (cursor.record_convergence_debt from a\nfalse_means_pending StageState.PENDING) should write status='deferred'; only a\ngenuine stage exception should write 'failed'. Then health surfaces can alert on\n'failed' and merely report 'deferred'.\n\nNOTE the audit's positive verdict on the mechanism itself: convergence_debt is\nthe one debt category that passes every test -- it has a reader that ACTS,\nexponential backoff, and DELETEs on success (cursor.py:473,499;\nrepair.py:4856,4896). Live population is 325 rows, all created within 3.5h, all\nattempts=1. It shrinks. Do not collapse this bead into 'remove convergence_debt'.","status":"open","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T12:46:26Z","created_by":"Sinity","updated_at":"2026-07-31T12:46:26Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-x1gd","title":"Rebuild index after tool-result-sidecar session-scope fix, characterize residual debt","description":"polylogue/sources/live/tool_result_sidecars.py + dispatch.py + code_parser.py now join Claude Code tool-results/ sidecars session-wide (parent + all subagent .jsonl) instead of per-transcript, and stamp occurred_at_ms from the sidecar file's own mtime. This is a derived-tier (index.db) SEMANTIC_REPARSE change per storage/sqlite/lifecycle.py -- it only takes effect on `polylogue ops reset --index \u0026\u0026 polylogued run`.\n\nBefore fix (measured live, 2026-07-30): 556,871 claude_tool_result_sidecar debt events, occurred_at_ms NULL on all of them. Root cause: subagent transcripts share ONE session-level tool-results/ dir with their parent but the join only saw each transcript's own tool_use_id index, so every sibling-owned file got double/triple/N-counted as debt once per subagent that didn't own it. Deduped by (session, filename): only 14,209 physically distinct debt files. Sampling (3 hand-picked + 25 random sessions, ~480 physical files) found the session-wide union-index join resolves ~99.6% of them; 2 files in the 25-session sample stayed unresolved even against the full session union (likely compaction-pruned turns -- genuinely gone).\n\nFollow-up once the operator schedules the index rebuild:\n1. Re-run the same debt query (session_events WHERE event_type='claude_tool_result_sidecar' AND acquisition_status='debt') and confirm the count drops from ~556K to roughly the true physical-file count (~14K order of magnitude, exact number depends on corpus growth since the audit).\n2. Confirm occurred_at_ms is now populated (no longer NULL) and check whether new debt is still accruing (via min/max occurred_at_ms) or was purely historical.\n3. For whatever debt remains after rebuild, partition it by cause using file shape (toolu_-shaped stem = resolvable via union index bug; other-shape mirror files = need a \"saved to\" pointer to resolve) and report an honest, non-single-bucket residue count -- don't just report a smaller undifferentiated number.\n4. If a meaningful cohort remains genuinely orphaned (no owner anywhere in the session, e.g. compaction pruned the referencing turn), consider whether the raw JSONL bytes are still worth acquiring into source.db even without a parsed owner, as a separate follow-up.","notes":"CORRECTION (2026-07-31): the numbers in the original description were\nevent-counted and overstate the problem. A direct disk cross-check\n(match sidecar basenames against files physically present under\n~/.claude/projects/*/*/tool-results/) found: ~12,000 distinct debt\nfiles, ~1.4GB, 100% still present on disk. There is no data loss and no\nrotation risk -- everything is recoverable. The original 14,209/2.02GB\nfigure in the first commit's message double-counted a subset of files\ndue to a session_id-prefix-stripping bug for agent-*.meta.json\ncompanion sessions (fixed in the branch's second commit, which also\nfound and fixed that .meta.json companions were an independent second\nsource of the same fanout bug).\n\nDebt unit decision (made in code): per PHYSICAL FILE, not per event.\njoin_tool_result_sidecars_session_scoped reports a file as debt at most\nonce (attributed to the root/parent transcript), never once per\nreplay/subagent. This is what \"drive to zero\" should be measured\nagainst after rebuild -- expect the archive-wide debt count to land\nnear the true distinct-file count (order 12,000, modulo corpus growth\nsince the audit), not the current 556,871.\n\nDocstring recheck: NOT changed to \"fix\" the 1-5%-vs-98% discrepancy,\nbecause it wasn't wrong -- the original 1-5% was file-counted (sampled\n80 sessions), the archive-wide 98% was event-counted; different\ndenominators, not a contradiction. Confirmed by dedup: per-file the\narchive-wide rate is much closer to 1-5% than to 98%.\n\nPR: feature/fix/tool-result-sidecar-debt-scope\nREBUILD-BATCH COORDINATION 2026-07-31 (coordinator): the live archive is at index v46; master declares v47-v50, all SEMANTIC_REPARSE, so ONE 'polylogue ops reset --index \u0026\u0026 polylogued run' pass unblocks the entire fixed-pending-rebuild cohort: r39b + mctu + 8b10 (reasoning/thinking visibility, PR #3447), b508 phantom-sidecar purge (PR #3403), gt1z/shnc cost columns (PR #3446), plus this bead's sidecar session-scope characterization. Sequencing: run AFTER the currently in-flight raw-authority lane (9dxn/5q2u/f57q/hjpx) merges so lineage-ordering and fingerprint gating ride the same pass. Post-rebuild verification checklist is in each cohort bead's notes (e.g. 8b10's sum(thinking_count) query).","status":"open","priority":1,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T11:44:46Z","created_by":"Sinity","updated_at":"2026-07-31T21:22:34Z","comments":[{"id":"61e31689-5088-5464-8ad8-5dd2d86db981","issue_id":"polylogue-x1gd","author":"Sinity","text":"PENDING-REBUILD (storage triage 2026-07-31): fix landed on origin/master (commit 04cb44ce9, PR #3448, merged 2026-07-31) -- Claude Code tool-result sidecar join is now session-wide (union index across parent + all subagent .jsonl) instead of per-transcript, and occurred_at_ms is now sourced from the sidecar file's own mtime. NOT yet visible on the live archive: this only changes materialization logic exercised during parse/reprocess, and affected sessions were already ingested under the old semantics. Live measured today: 566,885 claude_tool_result_sidecar events flagged debt (json_extract(payload_json,$.acquisition_status), up from the bead's 556,871 baseline via corpus growth), occurred_at_ms NULL for 578,241 (~100%, essentially unchanged). Once 'polylogue ops reset --index \u0026\u0026 polylogued run' completes the full raw replay (already required for the unrelated v47-53 SEMANTIC_REPARSE chain -- this fix rides along with that same replay), re-run this exact query: expect debt to drop from ~566K to roughly the true physical-file count (~14,209, per this bead's own dedup-by-(session,filename) measurement) and occurred_at_ms to populate for resolved events. CAVEAT for the operator: PR #3448 did NOT add a storage/sqlite/lifecycle.py IndexDeltaDeclaration (empty diff there) -- it rides along with the v47-53 replay but was not itself formally declared as reparse-requiring, an instance of the exact schema-versioning-lint blind spot polylogue-gucv describes. Do not close until the rebuild has run and the query above is re-verified.","created_at":"2026-07-31T21:26:38Z"}],"dependency_count":0,"dependent_count":0,"comment_count":1} {"_type":"issue","id":"polylogue-qsb4","title":"Delegation is a tree, not one level: no arbitrary-depth ancestry/subtree query surface","description":"SCOPE CLARIFICATION on polylogue-1vpm.7 (operator, 2026-07-31, mid-session):\ndelegation in this archive is a tree, not one level -- several subagents\ndispatched in real sessions launch their own subagents. delegation_facts\nalready models this IMPLICITLY (each row is one parent_session_id -\u003e\nchild_session_id edge; a child that itself dispatches subagents gets its\nown delegation_facts rows keyed by its own session_id as parent), so\narbitrary depth already exists in the DATA. What is missing is a single\nquery surface that returns a whole ancestry chain or subtree in one call,\ndepth-annotated, without N+1 queries or client-side reassembly -- and any\nUX built on top of it.\n\nWHY THIS MATTERS: session claude-code-session:38baa1de-9715-48fa-8175-\nf2a29d92800e dispatches ~20 subagents via the \"Agent\" tool (see\npolylogue-1vpm.7's companion fix in archive/viewport/tools.py); some of\nthose subagents dispatch their own subagents (nested Agent-tool calls are\nvisible in the corpus -- verify exact depth/count live before designing).\nA report describing this session's fan-out needs \"whose child is this at\nevery level\", \"what did agent X ultimately spawn\", and \"who ultimately\nasked for this work\" -- none of which delegation_facts' flat per-session\nrows answer without recursive client-side stitching today.\n\nCURRENT STATE (verified 2026-07-31, read-only against\nfile:/realm/db/polylogue/index.db):\n- delegation_facts / delegations (storage/sqlite/archive_tiers/archive.py):\n get_delegation_attempt/get_delegation_card resolve ONE edge by identity\n (instruction_tool_use_block_id, or parent+child pair). query_delegations\n is a flat filtered list, no recursion, no depth column.\n- session_links already has the EXACT precedent to reuse: it persists\n every parent reference a parser asserts even when the parent isn't\n ingested yet, keyed (src_session_id, dst_origin, dst_native_id,\n link_type), resolved on each save, with TopologyEdgeStatus =\n unresolved/resolved/repaired/quarantined (quarantined = the cycle-break,\n #866/#1260). delegation_facts_source already excludes quarantined\n session_links edges (`l.status IS NULL OR l.status != 'quarantined'`),\n so cycle-break precedent is already inherited at the edge level -- a\n recursive CTE walking delegation_facts should still carry an explicit\n visited-path guard defensively, but should not need to invent a second\n cycle vocabulary.\n- work_evidence_nodes/work_evidence_edges (index v46+) already hold a\n generic directed graph (edge_kind: invoked/claimed/mentioned/produced/\n retried/unresolved) that DOES support arbitrary-depth traversal via\n recursive CTE by construction -- but it is populated only from Workflow\n orchestration runs today (verified live: 7 runs, 122 calls, 164\n attempts, 128 structured-results, 0 rows sourced from Claude Code\n subagent dispatch). polylogue-1vpm's own tracking notes call this graph\n \"structurally hollow\" (authority/confidence constant, no time/actor).\n Whether delegation should PROJECT INTO this graph (one edge_kind=\n 'delegated' per delegation_facts row) rather than growing a second,\n parallel recursive-CTE surface is an open design question this bead\n must answer, not assume either way.\n\nACCEPTANCE CRITERIA:\n1. A single call returns the full ancestry chain (root-to-node) for a\n given session/delegation, depth-annotated, in one query -- no N+1.\n2. A single call returns the full subtree (node-to-all-descendants) for a\n given session/delegation, depth-annotated, in one query -- no N+1.\n3. Cycles/orphans reuse session_links' TopologyEdgeStatus vocabulary and\n quarantine precedent rather than inventing a second one; state\n explicitly whether a defensive visited-path guard is still needed in\n the recursive CTE despite quarantine already excluding cycle edges at\n the source.\n4. Explicit design decision, argued from evidence: does this live as new\n recursive-CTE methods on ArchiveStore (delegation_facts-native), or as\n a projection into work_evidence_nodes/edges (join existing \"invoked\"\n graph), or both with one clearly designated as source of truth? Read\n polylogue-1vpm and polylogue-1vpm.6 first -- this may already be a\n settled architectural decision this bead is unaware of.\n5. At least one production surface (MCP tool, CLI verb, or existing\n `get`/`query` dispatcher extension) exposes the tree/subtree query --\n not merely a new ArchiveStore method with no caller. cli/commands/\n analyze* and archive/query/ are owned by other lanes per this session's\n scope -- coordinate or use the MCP `get`/`query` dispatcher instead.\n6. State what UX the html-report skill's delegation-tree CSS pattern\n (references/patterns.md) is meant to consume from this surface, even\n if the actual report/HTML rendering is out of this bead's scope --\n the query surface's shape should not require a second redesign once a\n renderer is built against it.\n7. Live re-measurement: exact max delegation depth and fan-out width in\n the corpus today (after the companion \"Agent\" tool_name -\u003e SUBAGENT\n classification fix lands and, if the operator runs it, a reindex) --\n confirm the \"~20 subagents, some nested\" claim with real numbers before\n finalizing the design.\n\nNON-GOALS (unless folded in explicitly): rewriting delegation_facts'\nidentity-matching mechanism (that's polylogue-1vpm.7, already fixed);\nbuilding the actual HTML/report rendering (that's the report-writing\ntask this bead's design should unblock, not perform).","status":"open","priority":1,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:34:56Z","created_by":"Sinity","updated_at":"2026-07-31T10:34:56Z","labels":["area:insights","area:storage","lane:analytics-experiments"],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"polylogue-f1ie","title":"Hook sidecar path mismatch: writer and reader use different directories, paste ground truth discarded live","description":"MEASURED 2026-07-31 (conversation-fidelity audit, audit-only pass). Live, currently-active pipeline break -- not historical debt.\n\nTHE MISMATCH:\n WRITER: ~/.claude/settings.json invokes 'polylogue-hook \u003cevent\u003e --sidecar-dir /home/sinity/.local/share/polylogue/hooks' on UserPromptSubmit / PreToolUse / PostToolUse and others. That directory currently holds ~197,485 files.\n READER: polylogue/sources/live/hook_paste_enrichment.py resolves its sidecar directory through polylogue/config.py:2199-2206 (hook_sidecar_dir setting, falling back to archive_root/hooks) -\u003e /realm/db/polylogue/hooks. That directory is empty apart from an unused pending/ subdir.\nThe daemon's paste-enrichment step therefore never sees any hook sidecar. Hook ground truth accumulates in a directory nothing consumes.\n\nOBSERVABLE CONSEQUENCE: messages.has_paste = 1 for 4 rows out of 4,908,097 archive-wide. paste_boundary is 'projected' on those same 4 and NULL everywhere else.\n select has_paste, count(*) from messages group by 1;\nCross-check via FTS finds 1,424 blocks whose text contains 'pasted' and 'text' within 3 tokens:\n select count(*) from messages_fts where messages_fts match 'NEAR(pasted text, 3)';\nTwo to three orders of magnitude more candidate pastes than flagged messages. has_paste / paste_count are effectively inert columns.\n\nNOT FULLY DISAMBIGUATED (be honest): a second, independent detection path exists -- polylogue/archive/message/paste_detection.py:has_paste_marker looks for a literal '[Pasted text #N]' marker in message text at parse time, and does not depend on the hook sidecar. The FTS-vs-has_paste gap could therefore be (a) that path also not firing, or (b) most of those 1,424 hits predating the paste-detection feature and never having been reprocessed, since materialization runs at ingest/reprocess time and not retroactively. This audit could not separate the two. Whichever it is, the path mismatch above is independently real and worth fixing first because it is cheap.\n\nRELATED, NOT THE SAME: attachment_refs.upload_origin='paste' has 69 real rows, so pasted ATTACHMENTS are recorded (just under-acquired like every other attachment channel). It is paste TEXT detection that is dead.\n\nFIX: point the two at the same directory -- either set hook_sidecar_dir in polylogue.toml to ~/.local/share/polylogue/hooks, or change the --sidecar-dir the sinnix-managed hook command passes. Then decide whether a one-off reprocess is warranted to backfill has_paste on historical sessions.\n\nRE-RUN:\n grep -c sidecar-dir ~/.claude/settings.json\n ls /realm/db/polylogue/hooks; ls ~/.local/share/polylogue/hooks | wc -l\n sqlite3 \"file:/realm/db/polylogue/index.db?mode=ro\" \"select has_paste, count(*) from messages group by 1;\"","status":"open","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:27:01Z","created_by":"Sinity","updated_at":"2026-07-31T10:27:01Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"polylogue-f1ie","title":"Hook sidecar path mismatch: writer and reader use different directories, paste ground truth discarded live","description":"MEASURED 2026-07-31 (conversation-fidelity audit, audit-only pass). Live, currently-active pipeline break -- not historical debt.\n\nTHE MISMATCH:\n WRITER: ~/.claude/settings.json invokes 'polylogue-hook \u003cevent\u003e --sidecar-dir /home/sinity/.local/share/polylogue/hooks' on UserPromptSubmit / PreToolUse / PostToolUse and others. That directory currently holds ~197,485 files.\n READER: polylogue/sources/live/hook_paste_enrichment.py resolves its sidecar directory through polylogue/config.py:2199-2206 (hook_sidecar_dir setting, falling back to archive_root/hooks) -\u003e /realm/db/polylogue/hooks. That directory is empty apart from an unused pending/ subdir.\nThe daemon's paste-enrichment step therefore never sees any hook sidecar. Hook ground truth accumulates in a directory nothing consumes.\n\nOBSERVABLE CONSEQUENCE: messages.has_paste = 1 for 4 rows out of 4,908,097 archive-wide. paste_boundary is 'projected' on those same 4 and NULL everywhere else.\n select has_paste, count(*) from messages group by 1;\nCross-check via FTS finds 1,424 blocks whose text contains 'pasted' and 'text' within 3 tokens:\n select count(*) from messages_fts where messages_fts match 'NEAR(pasted text, 3)';\nTwo to three orders of magnitude more candidate pastes than flagged messages. has_paste / paste_count are effectively inert columns.\n\nNOT FULLY DISAMBIGUATED (be honest): a second, independent detection path exists -- polylogue/archive/message/paste_detection.py:has_paste_marker looks for a literal '[Pasted text #N]' marker in message text at parse time, and does not depend on the hook sidecar. The FTS-vs-has_paste gap could therefore be (a) that path also not firing, or (b) most of those 1,424 hits predating the paste-detection feature and never having been reprocessed, since materialization runs at ingest/reprocess time and not retroactively. This audit could not separate the two. Whichever it is, the path mismatch above is independently real and worth fixing first because it is cheap.\n\nRELATED, NOT THE SAME: attachment_refs.upload_origin='paste' has 69 real rows, so pasted ATTACHMENTS are recorded (just under-acquired like every other attachment channel). It is paste TEXT detection that is dead.\n\nFIX: point the two at the same directory -- either set hook_sidecar_dir in polylogue.toml to ~/.local/share/polylogue/hooks, or change the --sidecar-dir the sinnix-managed hook command passes. Then decide whether a one-off reprocess is warranted to backfill has_paste on historical sessions.\n\nRE-RUN:\n grep -c sidecar-dir ~/.claude/settings.json\n ls /realm/db/polylogue/hooks; ls ~/.local/share/polylogue/hooks | wc -l\n sqlite3 \"file:/realm/db/polylogue/index.db?mode=ro\" \"select has_paste, count(*) from messages group by 1;\"","status":"closed","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:27:01Z","created_by":"Sinity","updated_at":"2026-08-01T17:10:04Z","closed_at":"2026-08-01T17:10:04Z","close_reason":"Duplicate of polylogue-swqu (sinnix settings.json fix) + polylogue-k8wv (backlog drain), both filed alongside already-merged PR #3418 which added the drift detector. No polylogue code defect remains; verified reader/writer resolution is correct and drift-detection is live.","comments":[{"id":"cb8e6430-27ae-52cc-b996-a0213b98106c","issue_id":"polylogue-f1ie","author":"Claude","text":"Investigated. This is not a live polylogue code bug -- it's a duplicate\ndiscovery of two beads already filed alongside PR #3418 (merged\n2026-07-31T08:04:01Z, 23 min before this bead was created):\n\n- polylogue-swqu: the actual root cause (sinnix's ~/.claude/settings.json\n template bakes a stale --sidecar-dir from before the archive root moved\n to /realm/db/polylogue). Confirmed still live 2026-08-01:\n `grep -c sidecar-dir ~/.claude/settings.json` = 5, all pointing at\n /home/sinity/.local/share/polylogue/hooks. This is a sinnix-repo fix\n (dots/claude/settings.json template), out of scope for a polylogue PR.\n- polylogue-k8wv: migrating the legacy flat-file backlog (~197K files now,\n was 108,956 at filing) into source.db via the existing idempotent\n drain_hook_event_spool() mechanism, once the archive root is corrected\n and the daemon isn't mid-restart.\n\nVerified there is no reader-side code defect: archive_root()\n(polylogue/paths/_roots.py) correctly resolves /realm/db/polylogue from\npolylogue.toml's [archive].root, and hooks_sidecar_dir() =\narchive_root()/\"hooks\" tracks it -- this matches the bead's own\n\"reader\" observation exactly. hook_install_sidecar_drift() (added in\n#3418, polylogue/hooks/__init__.py) already detects this exact drift\nclass and polylogue/daemon/cli.py's 15-min heartbeat already logs it as\na warning. No further polylogue code change would do anything the\nalready-merged PR doesn't. Not opening a redundant PR.\n\nAside (found while investigating, not actioned): the live polylogued.service\nis currently refusing to start its watcher --\n\"tier user_version mismatch: index.db:46!=53\" -- unrelated to this bead,\nworth its own look.\n\nClosing as duplicate of polylogue-swqu + polylogue-k8wv, which already\ncarry the full remaining scope (sinnix settings fix + backlog drain).\n","created_at":"2026-08-01T17:10:24Z"}],"dependency_count":0,"dependent_count":0,"comment_count":1} {"_type":"issue","id":"polylogue-ksgg","title":"Branch/thread structure is unreconstructible: 5 of 9 origins carry no message parent links; read surface omits the columns","description":"MEASURED 2026-07-31 (conversation-fidelity audit, audit-only pass).\n\n(A) PARENT LINKS MISSING PER ORIGIN. Sampled 60 sessions per origin (all, where fewer exist), counting messages with parent_message_id set:\n claude-code-session 21048 msgs 96.7% parented\n chatgpt-export 6574 msgs 91.6%\n claude-ai-export 1123 msgs 87.6%\n codex-session 18373 msgs 0.0%\n hermes-session 7507 msgs 0.0%\n aistudio-drive 3108 msgs 0.0%\n gemini-cli-session 652 msgs 0.0%\n grok-export 16 msgs 0.0%\nFive of nine origins store no message-level parent at all. Sessions there are a flat ordered list; the tree the data model documents (sessions -\u003e messages -\u003e blocks with parent links) is not populated.\n\n(B) VARIANTS NEVER RECORDED for the two coding origins: variant_index\u003e0 count is 0 for claude-code-session and 0 for codex-session in the same samples. Retries/regenerations, where they occurred, are not distinguishable.\n\n(C) ACTIVE-PATH CONTRADICTION. 665 sessions archive-wide contain variant_index\u003e0 rows. In 25 of them (490 variant messages) there is not a single is_active_path=0 row -- every variant is marked as being on the active path, so the branch the user actually saw cannot be recovered for those sessions.\n select session_id, count(*), sum(variant_index\u003e0) v, sum(is_active_path=0) inactive from messages group by 1 having v\u003e0;\n\n(D) THE READ SURFACE DOES NOT EXPOSE ANY OF IT. The message payload from has these keys and no others:\n actions, anchor, attachment_refs, branch_index, cache_read_tokens, cache_write_tokens, content_blocks, has_paste_evidence, has_thinking, has_tool_use, id, input_tokens, material_origin, message_type, output_tokens, role, session_id, target_ref, text, timestamp\nAbsent: position, variant_index, is_active_path, is_active_leaf, parent_message_id. So even for claude-code and chatgpt, where the columns ARE populated, a consumer of the JSON read surface cannot reconstruct ordering-by-position or which branch was live. Verified against three real sessions across two origins.\n\nCONSEQUENCE: 'does the archive reconstruct the branch the user actually saw' is answerable only by direct SQL, and on five origins not at all.\n\nSUGGESTED SPLIT: (D) is cheap and self-contained -- add the columns to the messages payload. (A)/(B) are per-origin parser work. (C) is a writer-side active-path assignment bug worth isolating first since it is small and bounded (25 sessions).","status":"open","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:22:02Z","created_by":"Sinity","updated_at":"2026-07-31T10:22:02Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"polylogue-buq8","title":"Eleven codex sessions with multi-MB real content materialize as zero messages (quarantine consequence)","description":"MEASURED 2026-07-31 (conversation-fidelity audit, audit-only pass). User-visible consequence of polylogue-u19l; filed separately because the symptom is content loss in the read model, not a convergence metric.\n\nQUERY: select count(*) from sessions s where s.origin='codex-session' and not exists(select 1 from messages m where m.session_id=s.session_id); -\u003e 17\n\nOf those 17, raw files were inspected directly:\n - 6 are genuinely empty (raw is session_meta plus at most a bare task_started event). Correctly represented.\n - 11 hold real substantial conversations, 996 KB to 3.3 MB, 694 to 3,688 lines each. Example native_id 019a2e27-2596-7f22-b6f5-e26acd721d57 (3.3 MB / 3,685 lines) raw inner-type census: message:50, user_message:24, agent_reasoning:478, reasoning:479, function_call:444, function_call_output:444, custom_tool_call:67, custom_tool_call_output:67. ZERO of this reached messages, blocks, or session_events.\n\nROOT CAUSE (confirmed in source.db): all 11 raw rows have parse_error=NULL, validation_status='passed', blob_size matching the real file -- the bytes were acquired and parsed fine. They carry revision_authority='quarantined', revision_kind='unknown', source_index=0, no predecessor_raw_id/baseline_raw_id: a single uncontested acquisition whose authority classification never resolved to byte_proven, so materialization into index.db never runs. This is the absorbing-state mechanism diagnosed in polylogue-u19l.\n\nSCOPE: select revision_authority, count(*) from raw_sessions where origin='codex-session' group by 1; -\u003e byte_proven 3951, quarantined 5202 (57%).\n\nWHY THIS MATTERS SEPARATELY FROM u19l: sessions.message_count=0 reads as 'nothing happened here' on every surface. Nothing distinguishes a genuinely empty rollout from 3.3 MB that never materialized. The audit brief's warning applies exactly -- this stayed invisible because everything downstream trusted the parser's verdict.\n\nRESIDUAL / INFERRED, not measured: the 11 are only the sessions where EVERY raw row was quarantined. With 5,202 quarantined rows overall, sessions where some rows resolved and others did not would lose content while still reporting message_count\u003e0, and would never appear in this query. Detecting those needs a per-session reconciliation of raw item counts against archive row counts. Recommend that as the acceptance check for u19l's fix rather than 'no more empty sessions'.","status":"open","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:21:16Z","created_by":"Sinity","updated_at":"2026-07-31T10:21:16Z","dependencies":[{"issue_id":"polylogue-buq8","depends_on_id":"polylogue-u19l","type":"relates-to","created_at":"2026-07-31T14:40:08Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"polylogue-buq8","title":"Eleven codex sessions with multi-MB real content materialize as zero messages (quarantine consequence)","description":"MEASURED 2026-07-31 (conversation-fidelity audit, audit-only pass). User-visible consequence of polylogue-u19l; filed separately because the symptom is content loss in the read model, not a convergence metric.\n\nQUERY: select count(*) from sessions s where s.origin='codex-session' and not exists(select 1 from messages m where m.session_id=s.session_id); -\u003e 17\n\nOf those 17, raw files were inspected directly:\n - 6 are genuinely empty (raw is session_meta plus at most a bare task_started event). Correctly represented.\n - 11 hold real substantial conversations, 996 KB to 3.3 MB, 694 to 3,688 lines each. Example native_id 019a2e27-2596-7f22-b6f5-e26acd721d57 (3.3 MB / 3,685 lines) raw inner-type census: message:50, user_message:24, agent_reasoning:478, reasoning:479, function_call:444, function_call_output:444, custom_tool_call:67, custom_tool_call_output:67. ZERO of this reached messages, blocks, or session_events.\n\nROOT CAUSE (confirmed in source.db): all 11 raw rows have parse_error=NULL, validation_status='passed', blob_size matching the real file -- the bytes were acquired and parsed fine. They carry revision_authority='quarantined', revision_kind='unknown', source_index=0, no predecessor_raw_id/baseline_raw_id: a single uncontested acquisition whose authority classification never resolved to byte_proven, so materialization into index.db never runs. This is the absorbing-state mechanism diagnosed in polylogue-u19l.\n\nSCOPE: select revision_authority, count(*) from raw_sessions where origin='codex-session' group by 1; -\u003e byte_proven 3951, quarantined 5202 (57%).\n\nWHY THIS MATTERS SEPARATELY FROM u19l: sessions.message_count=0 reads as 'nothing happened here' on every surface. Nothing distinguishes a genuinely empty rollout from 3.3 MB that never materialized. The audit brief's warning applies exactly -- this stayed invisible because everything downstream trusted the parser's verdict.\n\nRESIDUAL / INFERRED, not measured: the 11 are only the sessions where EVERY raw row was quarantined. With 5,202 quarantined rows overall, sessions where some rows resolved and others did not would lose content while still reporting message_count\u003e0, and would never appear in this query. Detecting those needs a per-session reconciliation of raw item counts against archive row counts. Recommend that as the acceptance check for u19l's fix rather than 'no more empty sessions'.","status":"in_progress","priority":1,"issue_type":"bug","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:21:16Z","created_by":"Sinity","updated_at":"2026-08-01T17:31:14Z","started_at":"2026-08-01T17:31:14Z","lease_expires_at":"2026-08-01T17:36:14Z","heartbeat_at":"2026-08-01T17:31:14Z","dependencies":[{"issue_id":"polylogue-buq8","depends_on_id":"polylogue-u19l","type":"relates-to","created_at":"2026-07-31T14:40:08Z","created_by":"Sinity","metadata":"{}"}],"comments":[{"id":"5ea20fc8-4556-53bb-8f6b-628e6b16c58a","issue_id":"polylogue-buq8","author":"Sinity","text":"Investigated jointly with polylogue-i415/polylogue-lkos (same live-archive symptom, three inconsistent theories). Root cause is NOT quarantine blocking materialization: live query shows 1,742/1,757 quarantined codex-session rows already have real message counts, so revision_authority is orthogonal to whether materialization ran. Actual bug: raw_revision_heads.accepted_raw_id for these 11 sessions equals their own sessions.raw_id, with decided_at_ms months after sessions.updated_at_ms -- a bookkeeping-only backfill retroactively declared the raw authoritative without re-running message extraction, and revision_authority_refuses_write's unconditional governed-check then refused every subsequent write attempt for that session_id forever, including the accepted raw's own corrective rewrite. Direct reproduction confirms the current codex parser extracts the real content correctly (1,496 messages from the 3.3MB flagship sample) -- this was never a parser defect. Fixed in PR #3527 (revision_authority_refuses_write now compares accepted_raw_id, only refusing a genuinely different/losing raw). Fix unblocks future re-ingest but does not retroactively repair the already-materialized rows -- those need an ordinary session-scoped reparse after merge, not a schema/index bump.","created_at":"2026-08-01T17:31:35Z"}],"dependency_count":0,"dependent_count":0,"comment_count":1} {"_type":"issue","id":"polylogue-mvq8","title":"\u003e8MiB browser captures stamped unknown-export by the 1MiB provider probe: 641MB of ChatGPT captures unparseable, 8 conversations wholly absent, lane re-captures them forever","description":"STAGE-2 (detection defect at acquire time) - rebuild does NOT fix: origin is stamped on the raw row; needs probe fix + re-detection of stored unknown-export rows. From the 2026-07-31 acquisition-completeness audit.\n\nMechanism (file:line, verified against a live blob): captures \u003e _STREAMING_FULL_INGEST_BYTES = 8MiB (polylogue/sources/live/batch_support.py:26) take _browser_capture_prefix_probe, which reads only _BROWSER_CAPTURE_PREFIX_PROBE_BYTES = 1MiB (batch_support.py:31, read at :464). The capture envelope orders raw_provider_payload BEFORE session.provider, so for any conversation big enough the provider regex (:469) finds nothing and the row falls back to unknown-export (:506-509). A correct bounded ijson reader already exists (_stream_browser_capture_provider, polylogue/sources/source_acquisition_components.py:355-381) but is not used on this route.\n\nMeasured: 23 distinct browser-capture paths / 641,613,073 bytes of raw rows sit under origin='unknown-export' (repro: select count(distinct source_path),sum(blob_size) from raw_sessions where origin='unknown-export' and source_path like '%browser-capture%'). Union-find vs index: 8 conversations (108.8MB) wholly unrepresented; the rest exist only as stale truncated pre-8MiB versions. Compounding: the unsatisfied cursor re-acquires the growing conversation repeatedly - one path has 12 raw rows of ~23MB each. ACTIVE: the chatgpt browser-capture lane is live (acquired same-day as audit).\n\nRelated: polylogue-t0ta (chatgpt detection tightness), polylogue-erf3 (claude.ai zip container-level unknown-export), polylogue-01fe (unknown-export unfilterable).\n\nAC: (1) provider detection for streaming-size captures uses the bounded ijson envelope reader (or equivalent) - a \u003e8MiB capture with provider after a multi-MB payload detects correctly, with test; (2) the 23 stored unknown-export capture rows re-detected/re-originated and parsed - the 8 absent conversations reach the index; (3) re-capture churn stops (cursor satisfied after successful parse).","status":"open","priority":1,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:10:18Z","created_by":"Sinity","updated_at":"2026-07-31T10:10:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-pebu","title":"Recover unimported provider exports: 2026-07-29 ChatGPT ZIP holds 181 conversations absent from the archive; 5 legacy inbox ZIPs (2.29GB) permanently excluded","description":"STAGE-1 ACQUISITION LEAK - index rebuild does NOT recover any of this; the bytes were never captured. From the 2026-07-31 acquisition-completeness audit (report: /realm/inbox/polylogue-audits-2026-07-31/acquisition-completeness.html).\n\n1) /realm/data/exports/chatlog/raw/chatgpt/chatgpt-data-2026-07-29-03-22-34.zip (16.08GB, 2836 conversations) has ZERO raw_sessions rows referencing it. Browser-capture independently covers 2291/2472 dated conversations (92.7%), but 181 conversations exist ONLY inside this unimported ZIP. (Related: polylogue-geop - newer exports are not supersets, so older bundles must not be pruned on import.)\n2) 5 ZIPs under the legacy ~/.local/share/polylogue/inbox/ ({chatgpt,claude-ai}-data-*.zip, largest 2.07GB, total 2.29GB) have zero raw_sessions AND zero raw_artifacts rows; their ingest cursors are excluded=1 with failure_count 689/864/975/1001/2018 (crash-looped past the design ceiling of 5 - see the failure-accounting bead). Cross-check against /realm/data/exports/chatlog/raw originals before re-import; at least chatgpt-data-2026-04-23 exists there and IS imported, so dedupe by content, not by path.\n\nRepro (mode=ro):\n sqlite3 \"file:/realm/db/polylogue/source.db?mode=ro\" \"select count(*) from raw_sessions where source_path like '%chatgpt-data-2026-07-29%'\" -- 0\n sqlite3 \"file:/realm/db/polylogue/ops.db?mode=ro\" \"select source_path,failure_count from ingest_cursor where failure_count\u003e100\" -- the 5 ZIPs\n\nAC: (1) 2026-07-29 export imported; the 181 absent conversations present in index (verify by native_id sample); (2) each of the 5 excluded ZIPs either imported from a verified-good copy or explicitly closed as corrupt/duplicate WITH a durable record of that disposition; (3) no double-ingest of conversations already present via browser-capture (content-hash idempotency should handle this - verify counts before/after).","status":"open","priority":1,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:10:16Z","created_by":"Sinity","updated_at":"2026-07-31T10:10:16Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-7eo7","title":"Health verdict contradicts its own data: 'ok (6 alerts)', 23-min-stale heartbeat still 'running', cursor_lag_samples never populated, health loop absent when schema-blocked","description":"Audit 2026-07-31, four observability defects in one verdict pipeline: (1) polylogued status prints 'Health: ok (6 alerts)' while hook_flow [error] fires every tick (journal, dozens/day) — alerts don't feed the verdict. (2) 'Status: running (heartbeat 1369.8s ago)' — a 23-min-stale heartbeat (15-min interval) produces no staleness verdict. (3) ops.db cursor_lag_samples has 0 rows EVER — the cursor-lag SLO check reads a table nothing produces (detector without producer). (4) When the watcher is schema-blocked, periodic health checks are never started at all (daemon/cli.py:2114-2165) — the daemon is blind exactly when blocked. Also cosmetic: SIGTERM stop exits 143 so every clean stop logs \"Failed with result 'exit-code'\", training operators to ignore 'failed'. Fix: alerts must drive the verdict; heartbeat staleness threshold; wire the lag sampler; start fast health checks in schema-blocked mode; SuccessExitStatus=143.","status":"open","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:09:49Z","created_by":"Sinity","updated_at":"2026-07-31T10:09:49Z","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -134,7 +134,7 @@ {"_type":"issue","id":"polylogue-b629","title":"Leak audit L3: 17 live-archive session identifiers committed at tip","description":"AUDIT 2026-07-31 (leak-surfaces). VERDICT: EXPOSED.\n\nMethod: collected every UUID appearing in tests/, docs/ and .agent/ (47 distinct), then resolved each against the live archive read-only (SELECT origin FROM sessions WHERE native_id = ?, file:/realm/db/polylogue/index.db?mode=ro). 17 matched real sessions across codex-session, claude-code-session and chatgpt-export origins.\n\nContent at risk: identifiers only, no text. Locations include test fixtures, docs, demo evidence files and .beads records. The identifiers are deliberately NOT reproduced in the audit report or in this bead; regenerate with the query above.\n\nFix: replace with synthetic ids in tests and docs; decide separately whether the historical occurrences are worth scrubbing.\n\nReport: /realm/inbox/polylogue-audits-2026-07-31/leak-surfaces.html","status":"open","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:08:28Z","created_by":"Sinity","updated_at":"2026-07-31T10:08:28Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-8zzs","title":"CLI status fabricates 'FTS: 100.0% indexed' from readiness boolean when coverage_pct is null","description":"Surface-coherence audit 2026-07-31 — the live 'ops status says FTS 100% while query path says incomplete' incident, CLI-render site. polylogue/cli/commands/status.py:1273-1276: `pct = _safe_float(fts.get(\"coverage_pct\"), default=100.0 if fts.get(\"messages_ready\") else 0.0)` then prints `FTS: [green]100.0% indexed`. Live evidence: `polylogue ops status --json --full` has fts_readiness.coverage_pct=null, message_indexed_count=null, message_indexable_count=null, coverage_exact=false, surfaces.messages_fts source_rows=1 indexed_rows=1 (index.db fts_freshness_state row: detail='bounded global messages_fts repair completed; exact counts skipped') — yet the human status line asserts the precise measured-looking claim \"FTS: 100.0% indexed\" fabricated from the messages_ready boolean. Same snapshot: component_readiness.search.counts all None, search.collection.state=stale. Sibling of polylogue-oitx (daemon/fts_status.py fabricated coverage class — filed by the 2026-07-31 silent-degradation audit); this bead covers the CLI presentation layer: when coverage_pct is null/not measured, render 'structurally ready (coverage not measured)' or similar — never a fabricated percentage.\n","status":"closed","priority":1,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T08:40:26Z","created_by":"Sinity","updated_at":"2026-07-31T21:11:02Z","closed_at":"2026-07-31T21:11:02Z","close_reason":"Duplicate/superseded: fixed by PR #3429 (eb5796f49, merged 2026-07-31), which landed under tracking id polylogue-roax (also closed). status.py:1327-1336 now prints 'coverage unknown' instead of fabricating 100% when fts.coverage_pct is None. Verified via /realm/tmp/bead-audit verify-first triage 2026-07-31.","labels":["cli","surface-coherence"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-hnl7","title":"MCP query tool silently drops origin/tag/repo/since/until/sort for default projection","description":"Surface-coherence audit 2026-07-31 (live archive, in-process build_server()). MCP `query`'s input schema accepts origin/tag/repo/since/until/sort, but the default (query_units) projection path passes only (expression, limit, continuation) — polylogue/mcp/server_cutover.py ~L620-630: `hooks.get_polylogue().query_units(expression, limit=limit, continuation=continuation)`. Live repro: query(expression='messages where role:user | count', origin='claude-code-session') -\u003e count=208055, which is the ALL-origin count (SQL `select count(*) from messages where role='user'` = 208055; claude-code-session alone = 141646 via sessions.user_message_count rollup and via join). CLI with the same root filter returns the correct 141646 (`polylogue --origin claude-code-session --json find 'messages where role:user | count'`). MCP also accepts origin='bogus-origin' without error (returns the unfiltered aggregate) where CLI raises UsageError listing valid origins. Filters ARE honored for projection='sessions' and insight projections — only the default unit-query path drops them. Fix: lower the args into the unit expression, or reject the combination loudly (invalid_argument) the way continuation is rejected for other projections. An agent surface silently returning wrong-scope numbers is the worst MCP failure shape.\n","notes":"Fixed via PR #3445 (fix(mcp): repair query-filter, facet-default, and prompt-tool integrity), commit bb800174a. query()'s default projection now forwards origin/tag/repo/since/until/min_messages/max_messages/min_words to query_units. Unrecognised origin now rejected (invalid_argument, against core.sources.CORE_SCHEMA_ORIGINS). sort on default projection now rejected loudly instead of silently ignored. New test tests/unit/mcp/test_query_default_projection_filters.py (3 tests). Live-archive verified: origin=claude-code-session -\u003e 141652 (CLI parity, was 208061 whole-archive before fix); origin=bogus-origin -\u003e invalid_argument.","status":"closed","priority":1,"issue_type":"task","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T08:40:24Z","created_by":"Sinity","updated_at":"2026-07-31T21:17:51Z","started_at":"2026-07-31T09:26:28Z","closed_at":"2026-07-31T21:17:51Z","close_reason":"Fixed via PR #3445 (a8f74103e): MCP query() default projection now forwards origin/tag/repo/since/until filters and rejects unknown origin/sort loudly; regression tests on master.","labels":["mcp","surface-coherence"],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"polylogue-i415","title":"Silent parse loss: 11 codex rollouts (up to 3.3MB, mostly 2025-10/11 era) parsed to zero messages despite real content","description":"Forensics 2026-07-31. 17 codex-session rows have message_count=0; 11 of them have blob_size 19KB-3.3MB. Verified sample rollout 0199fada-d8bd-7fc0-997b-d23d3a6849c7 (3.3MB, 2025-10-19): jq type histogram = 1543 event_msg + 1496 response_item (incl 55 message, 440 function_call+440 outputs, 44 custom_tool_call pairs, 473 reasoning) + 513 turn_context — archive shows ZERO messages. This is silent data loss for old-format rollouts, not 'genuinely empty'. 9/11 are 2025-10..11 native ids; 2 are 2026-07-17. The other 6 empties are legit (single session_meta record, blob \u003c=5KB).\nRepro: ATTACH index.db from source.db side or join; SELECT s.native_id, r.blob_size FROM sessions s JOIN raw_sessions r ON r.raw_id=s.raw_id WHERE s.origin='codex-session' AND s.message_count=0 ORDER BY r.blob_size DESC;\nAC: parser handles the old rollout envelope (or a dated schema variant is added), the 11 sessions re-parse with non-zero messages, and a fixture from a synthesized old-format rollout protects it.","status":"open","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T08:20:22Z","created_by":"Sinity","updated_at":"2026-07-31T08:20:22Z","comments":[{"id":"019fb743-7795-756b-a460-10373103be45","issue_id":"polylogue-i415","author":"Sinity","text":"Code trace (audit): HEAD still parses these to zero. codex.py looks_like (1944-1970) accepts state-record-dominated files; _parse_records emits messages only for _message_record shapes (385-391) and drops role-less/text-less records (2344-2347); session_meta/turn_context/world_state/compacted only ever emit events — compacted deliberately does not re-parse replacement_history. Related: polylogue-dhil (whale anatomy, open); f969cf93b pins only the multi-session_meta case. NOTE: sampled file has 55 response_item payload.type='message' records that still produced 0 messages — the old-envelope inner shape apparently fails _message_record; a fixture from that exact era file is the AC.","created_at":"2026-07-31T08:21:20Z"},{"id":"019fb7b8-5707-76ed-b7fa-c380803f5447","issue_id":"polylogue-i415","author":"Sinity","text":"Investigated for PR #3441. Re-parsed the exact archived raw bytes (verified identical to the live on-disk rollout files via blob_size match) for all 17 codex-session rows with message_count=0 using the CURRENT (unmodified) codex.py: 11 now produce real non-trivial message counts (3 to 1073 messages, e.g. 1023 for the 3.3MB 0199fada-... sample cited in this bead's forensic note), confirming this is a stale-materialization issue from an older parser version, not a live parser defect -- the operator's planned index rebuild will resolve these 11 with no code change. The remaining 6 are genuinely near-empty stubs (\u003c=5KB, 1-4 records), matching this bead's own classification. Added a regression fixture (tests/unit/sources/test_silent_ingest_loss.py::test_codex_dense_reasoning_and_tool_call_rollout_yields_messages) built from real record shapes (prose redacted) so this shape cannot silently regress. No codex.py change needed or made.","created_at":"2026-07-31T10:28:59Z"}],"dependency_count":0,"dependent_count":0,"comment_count":2} +{"_type":"issue","id":"polylogue-i415","title":"Silent parse loss: 11 codex rollouts (up to 3.3MB, mostly 2025-10/11 era) parsed to zero messages despite real content","description":"Forensics 2026-07-31. 17 codex-session rows have message_count=0; 11 of them have blob_size 19KB-3.3MB. Verified sample rollout 0199fada-d8bd-7fc0-997b-d23d3a6849c7 (3.3MB, 2025-10-19): jq type histogram = 1543 event_msg + 1496 response_item (incl 55 message, 440 function_call+440 outputs, 44 custom_tool_call pairs, 473 reasoning) + 513 turn_context — archive shows ZERO messages. This is silent data loss for old-format rollouts, not 'genuinely empty'. 9/11 are 2025-10..11 native ids; 2 are 2026-07-17. The other 6 empties are legit (single session_meta record, blob \u003c=5KB).\nRepro: ATTACH index.db from source.db side or join; SELECT s.native_id, r.blob_size FROM sessions s JOIN raw_sessions r ON r.raw_id=s.raw_id WHERE s.origin='codex-session' AND s.message_count=0 ORDER BY r.blob_size DESC;\nAC: parser handles the old rollout envelope (or a dated schema variant is added), the 11 sessions re-parse with non-zero messages, and a fixture from a synthesized old-format rollout protects it.","status":"in_progress","priority":1,"issue_type":"bug","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T08:20:22Z","created_by":"Sinity","updated_at":"2026-08-01T17:31:14Z","started_at":"2026-08-01T17:31:14Z","lease_expires_at":"2026-08-01T17:36:14Z","heartbeat_at":"2026-08-01T17:31:14Z","comments":[{"id":"019fb743-7795-756b-a460-10373103be45","issue_id":"polylogue-i415","author":"Sinity","text":"Code trace (audit): HEAD still parses these to zero. codex.py looks_like (1944-1970) accepts state-record-dominated files; _parse_records emits messages only for _message_record shapes (385-391) and drops role-less/text-less records (2344-2347); session_meta/turn_context/world_state/compacted only ever emit events — compacted deliberately does not re-parse replacement_history. Related: polylogue-dhil (whale anatomy, open); f969cf93b pins only the multi-session_meta case. NOTE: sampled file has 55 response_item payload.type='message' records that still produced 0 messages — the old-envelope inner shape apparently fails _message_record; a fixture from that exact era file is the AC.","created_at":"2026-07-31T08:21:20Z"},{"id":"019fb7b8-5707-76ed-b7fa-c380803f5447","issue_id":"polylogue-i415","author":"Sinity","text":"Investigated for PR #3441. Re-parsed the exact archived raw bytes (verified identical to the live on-disk rollout files via blob_size match) for all 17 codex-session rows with message_count=0 using the CURRENT (unmodified) codex.py: 11 now produce real non-trivial message counts (3 to 1073 messages, e.g. 1023 for the 3.3MB 0199fada-... sample cited in this bead's forensic note), confirming this is a stale-materialization issue from an older parser version, not a live parser defect -- the operator's planned index rebuild will resolve these 11 with no code change. The remaining 6 are genuinely near-empty stubs (\u003c=5KB, 1-4 records), matching this bead's own classification. Added a regression fixture (tests/unit/sources/test_silent_ingest_loss.py::test_codex_dense_reasoning_and_tool_call_rollout_yields_messages) built from real record shapes (prose redacted) so this shape cannot silently regress. No codex.py change needed or made.","created_at":"2026-07-31T10:28:59Z"},{"id":"6a3ca1cb-a0ae-5e8f-83ac-01e4753a0eea","issue_id":"polylogue-i415","author":"Sinity","text":"Investigated jointly with polylogue-buq8/polylogue-lkos. The 'old rollout envelope the parser can't handle' theory does not hold: direct reproduction of parse_stream_payload against the exact live raw bytes of the flagship sample (native_id 0199fada-d8bd-7fc0-997b-d23d3a6849c7, 3.3MB) extracts 1,496 real messages with the CURRENT parser -- no parser fix needed in polylogue/sources/. The actual defect is one layer downstream: raw_revision_heads.accepted_raw_id for this and the other 10 affected sessions equals their own sessions.raw_id, but decided_at_ms (the governance decision) is months after sessions.updated_at_ms (the original write) -- a bookkeeping-only backfill declared the raw authoritative without re-triggering message extraction, and revision_authority_refuses_write's unconditional 'governed' check then refused every future write for that session_id, including the accepted raw's own corrective rewrite. Fixed in PR #3527. AC as originally framed ('parser handles the old rollout envelope, fixture protects it') is misframed -- no parser change was needed or made; closing via the write-gate fix instead, with a regression test in test_ingest_batch.py rather than a codex-parser fixture.","created_at":"2026-08-01T17:31:46Z"}],"dependency_count":0,"dependent_count":0,"comment_count":3} {"_type":"issue","id":"polylogue-shnc","title":"Cost accounting: 100% of codex session_model_usage unpriced; 5,016 rows claim provenance='priced' with NULL cost/catalog; all 3,417 origin_reported rows carry no value","description":"Forensics 2026-07-31, live index.db (verifies and extends the existing NULL-cost report):\n- ALL 3,153 codex-session session_model_usage rows have cost_usd NULL and priced_with NULL (gpt-5.5 617, gpt-5.4 531, gpt-5-codex 454, gpt-5.3-codex 405, gpt-5.6-sol 313, gpt-5.6-terra 306, ...) despite the vendored LiteLLM catalog nominally covering gpt-5.x. Only 1 price_catalogs row is loaded.\n- Contradictory state: cost_provenance='priced' but cost_usd IS NULL AND priced_with IS NULL on 5,016 rows (70.1M tokens). 'priced' with no catalog and no price is a semantic lie; the other 10,222 priced rows are consistent.\n- cost_provenance='origin_reported' has cost_usd NULL on 3,417/3,417 rows (7.58B tokens) — the label exists but the origin-reported value was never stored.\n- claude-code NULLs: \u003csynthetic\u003e 1,140 (fine) + claude-sonnet-5 705 (catalog gap) + 7 misc.\n- session_provider_usage_events: 4,002,046 rows, estimated_cost_usd populated on 103, actual_cost_usd on 0.\nRepro: SELECT cost_provenance, cost_usd IS NULL, priced_with IS NULL, count(*) FROM session_model_usage GROUP BY 1,2,3;\nAC: pricing pass covers codex models + claude-sonnet-5; provenance constraint (priced =\u003e cost_usd AND priced_with NOT NULL; origin_reported =\u003e cost_usd NOT NULL) enforced or the states renamed honestly; re-materialization backfills existing rows.","notes":"RECONCILE 2026-07-31: returned to open (stale in_progress, no assignee). Write-path fix merged in PR #3446 (ed17421f7); remaining work is re-materialization to backfill existing unpriced/contradictory rows on the live archive — an ops run, not code.","status":"open","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T08:20:22Z","created_by":"Sinity","updated_at":"2026-07-31T21:19:28Z","started_at":"2026-07-31T11:02:39Z","comments":[{"id":"019fb743-7112-71ce-a091-fc8a3ff2c669","issue_id":"polylogue-shnc","author":"Sinity","text":"Code trace (audit): NOT a catalog gap — gpt-5.5/5.4/5-codex ARE in vendored litellm_model_prices.json. Root cause in storage/sqlite/archive_tiers/write.py: (a) _upsert/_increment_provider_usage_model_rollup (~3721-3794) hardcode cost_provenance='origin_reported' AND cost_usd=NULL/priced_with=NULL — pricing never attempted on the Codex cumulative-rollup path; (b) _aggregate_message_tokens_into_model_usage (~3847-3964), the only pricer, has a WHERE NOT guard (~3942-3950) refusing to overwrite origin_reported rows with nonzero tokens — structurally barred from pricing Codex; (c) the 'priced' label is written unconditionally by the INSERT literal even when 'normalized in PRICING and billable\u003e0' is false — hence 5,016 priced-with-NULL rows. 'origin_reported' means token provenance, not that a cost exists (session_reported_costs table was dropped in polylogue-v2mg).","created_at":"2026-07-31T08:21:18Z"},{"id":"019fb7d7-7ad8-7dae-ade1-29a6a254ef45","issue_id":"polylogue-shnc","author":"Sinity","text":"PR #3446 fixes the write-path root cause (Codex rollup writer + message-aggregator provenance bug) and adds enforcing CHECK constraints. Re-materialization (polylogue ops reset --index) still needed to backfill existing rows on the live archive.","created_at":"2026-07-31T11:03:00Z"}],"dependency_count":0,"dependent_count":0,"comment_count":2} {"_type":"issue","id":"polylogue-f5tq","title":"Untested shipped defaults: _archive_facet_buckets(include_deferred=True) plus a 17-item sweep","description":"FALSE-GREEN AUDIT 2026-07-31 (findings F4 + F13). Generalises the correlation_view github_api defect.\n\nTHE SEED DEFECT'S SHAPE: run_correlation_view(github_api=True) at\npolylogue/insights/correlation_view.py:14 shipped a NameError on its DEFAULT path because\nevery test (tests/unit/cli/test_correlate_view.py:60,80,90) passed github_api=False.\n\nI built an AST sweep to find the class mechanically: walk every polylogue/ function with a\nboolean default param, walk every tests/ call site, and flag params where the DEFAULT value is\nnever passed and never omitted while the opposite value IS passed.\n 388 production functions carry bool defaults\n 265 of them are called from tests\n 17 have a default that is never exercised\n 2 of those 17 have default=True (i.e. the SHIPPED behaviour is the untested one)\nThe sweep rediscovered run_correlation_view without being told it existed -- that is the\ncalibration proving it detects the class.\n\nNEW FINDING, the second default=True case:\n polylogue/api/archive.py:735 _archive_facet_buckets(..., include_deferred: bool = True)\n tests/unit/api/test_facade_contracts.py:738 is the only test, and passes include_deferred=False.\n The False branch returns HARD-CODED EMPTY DICTS for repos/role_counts/material_origins/\n message_types/action_types/has_flags. The True branch (the shipped default) calls\n _archive_aggregate_facet_families(archive._conn, ...) and does all the real SQL work.\n The test constructs its archive stub with _conn=None -- so it STRUCTURALLY CANNOT exercise\n the default; passing True would crash on the None connection.\n Production callers at api/archive.py:4771-4774 all forward an operator-supplied\n include_deferred, so the default path is live in real use.\n\nThe remaining 15 are default=False with tests passing only True (force, detail,\nrequire_overlays, exclude_none, include_rows, ...). Lower risk -- the untested default is\nusually the inert path -- but each is an untested shipped default and worth a triage pass.\n\nA mirror sweep found 235 flags never passed explicitly by ANY test (the non-default branch\nuntested). That list is noisy: matching is by bare function name, so generic names (list,\ncount, to_payload, model_copy) collide across classes. Treat it as a candidate pool.\n\nAC:\n- A test exercises _archive_facet_buckets with include_deferred=True against a real\n connection, asserting the SQL facet families are populated.\n- The 17-item list is triaged: each either gets default-path coverage or a recorded reason\n the default is not worth testing.\n- Consider whether this sweep is worth a devtools lab policy check. NOTE the operator's\n standing 'no completeness-check theater' rule: only add the gate if the known debt is\n migrated first, not as a substitute for migrating it.","notes":"Fixed via PR #3445, commit a9eed07fd. Added test_archive_facet_buckets_include_deferred_default_populates_sql_families exercising _archive_facet_buckets(include_deferred=True) against a real seeded ArchiveStore connection; asserts role_counts/message_types are SQL-populated, not the include_deferred=False branch's hardcoded empty dicts. Anti-vacuity verified: inverting the include_deferred branch condition makes both facet-bucket tests fail (AttributeError on None conn / AssertionError on empty role_counts). Triaged the remaining ~15-item sweep in the commit body rather than adding 15 individual tests: reproduced the AST sweep locally and confirmed it is structurally noisy exactly as the bead's own text warns -- it false-flags storage/blob_gc.py's run_blob_gc(dry_run=False) as untested even though a dozen tests exercise that default by omitting the kwarg. Spot-checked the bead-named examples (exclude_none, detail, require_overlays, include_rows) and found cosmetic serialization/reporting-detail toggles, not a second confirmed defect. No devtools lab policy gate added, per operator's standing no-completeness-check-theater rule -- this pass did not surface a second migratable defect to justify one.","status":"closed","priority":1,"issue_type":"bug","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T08:20:16Z","created_by":"Sinity","updated_at":"2026-07-31T21:17:51Z","started_at":"2026-07-31T09:26:29Z","closed_at":"2026-07-31T21:17:51Z","close_reason":"Fixed via PR #3445 (a9eed07fd): real-connection test exercising _archive_facet_buckets(include_deferred=True); 17-item bool-default sweep triaged without completeness-theater lint.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-eo81","title":"Antigravity origin inverted: 116 metadata sidecars ingested as sessions; all 44 real conversations (314MB .pb) never acquired","description":"Forensics 2026-07-31. Every antigravity-session row (116/116) is a 1-message session materialized from ~/.gemini/antigravity/brain/\u003cuuid\u003e/*.md.metadata.json — artifact metadata, not conversations (producer stopped 2026-07-18; 232 raws, 116 sessions). Meanwhile ~/.gemini/antigravity/conversations/ holds 44 real conversation .pb files (314MB) and raw_sessions/raw_artifacts contain ZERO rows for that directory: the actual conversations were never acquired. The origin is 100% noise, 0% signal.\nRepro: SELECT count(*) FROM raw_sessions WHERE source_path LIKE '%antigravity/conversations%'; -- 0\nAC: (1) purge/reclassify the 116 metadata sessions; (2) decide+implement .pb conversation acquisition (or explicitly document the format as out of scope with the gap tracked); (3) metadata.json becomes sidecar artifact kind.","notes":"2026-07-31 acquisition-completeness audit cross-check (report: /realm/inbox/polylogue-audits-2026-07-31/acquisition-completeness.html): full-tree recount across BOTH roots (~/.gemini/antigravity + antigravity-cli) = 55 .pb files / 339,774,849 bytes with zero raw_sessions/raw_artifacts rows (this bead's 44/314MB was the conversations dir of one root). Sidecar rows: 232 raw rows over 116 distinct *.md.metadata.json paths, 61KB total = 0.008% of antigravity's 383.6MB captured. All 114 antigravity ingest cursors excluded=1 failure_count=5 since the 2026-07-18 bulk give-up incident. Dormant: newest mtime under either root is 2026-07-16 - static residue, not an active drip. STAGE-1: index rebuild recovers none of it.\nRECONCILE 2026-07-31: unclaimed (stale claim). Acquisition fix merged (PR #3441/7b4f881d0); retroactive purge of 116 phantom sessions + live redeploy remain. Note polylogue-msia covers the same shape — consolidate before provisioning a lane.","status":"open","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T08:19:56Z","created_by":"Sinity","updated_at":"2026-07-31T21:18:32Z","comments":[{"id":"019fb743-7558-7b63-a3f0-f099cd82dded","issue_id":"polylogue-eo81","author":"Sinity","text":"Code trace (audit): parse_brain_metadata (sources/parsers/antigravity.py:245-288) documents the 1-session-per-metadata-file shape as a DELIBERATE tagged compromise — sessions carry flag 'degraded:brain-metadata-fragment' meant to exclude them from primary counts; tracked upstream as GH issue #1764. Still wired unconditionally at HEAD (dispatch.py:1080,1207). The .pb conversations gap (44 files / 314MB, zero raw rows) is the part with no tracking at all.","created_at":"2026-07-31T08:21:19Z"},{"id":"019fb7b8-5162-7672-924a-b0e1f650371e","issue_id":"polylogue-eo81","author":"Sinity","text":"Fixed acquisition half in PR #3441 (branch feature/sources/antigravity-conversation-acquisition): the language-server export path was gated on a nonexistent 'sessions/' dir (real dir is 'conversations/') and cascade discovery relied on SearchConversations, which only surfaces ~10/44 real conversations -- switched to disk-truth glob of conversations/*.pb, still enriching metadata from search when available. Verified against real ~/.gemini/antigravity data: exported a cascade absent from SearchConversations directly via ConvertTrajectoryToMarkdown (82KB real markdown), and ran the fixed iter_source_sessions_with_raw end-to-end producing all 44 sessions / 44 raw blob snapshots / 2162 messages into a scratch blob store (no live-archive writes). Also reclassified *.md.metadata.json as a non-session sidecar (AGENT_SIDECAR_META) in the generic walk so future ingest stops fragmenting brain metadata into noise sessions -- parse_brain_metadata remains wired as an explicit fallback only when the language server truly cannot be reached. NOT done: retroactive purge/reclassification of the existing 116 already-materialized fragment sessions (deletion-adjacent, deliberately left for a separate follow-up); live-archive acquisition itself, since polylogued.service runs a separately-deployed Nix package that won't pick up this fix until merge+redeploy -- see PR body for the exact operator action needed.","created_at":"2026-07-31T10:28:58Z"}],"dependency_count":0,"dependent_count":0,"comment_count":2} @@ -558,7 +558,7 @@ {"_type":"issue","id":"polylogue-gvr2","title":"delegation_facts is a TABLE despite its own code comment saying it should be a VIEW (100% derivable, matching actions precedent)","description":"Split from polylogue-cuxz.9 (verified 2026-08-01): polylogue/storage/sqlite/archive_tiers/index.py:1771 defines delegation_facts_source as a VIEW explicitly commented '100% derivable from existing tables -- VIEW, not a table, matching the actions precedent' -- then the very next statement (:1656) creates delegation_facts as a TABLE anyway. The view's WHERE clause is gated on membership in delegation_refresh_scope, a mutable side-table callers must populate before querying (delegation_facts.py:34-39) and always empty in steady state -- querying delegation_facts_source directly returns 0 rows (confirmed live). Needs a real design decision, not a mechanical fix: either (a) make the view self-standing/indexed well enough to query directly at actions-view cost (converting the table to a true view, matching the code comment's stated intent), or (b) document the scope-table gating as an intentional query-parameterization idiom and keep the table, updating the misleading comment. Ref polylogue-cuxz.9.","status":"open","priority":2,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-08-01T11:34:41Z","created_by":"Sinity","updated_at":"2026-08-01T11:34:41Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-20c8","title":"master-red: material-protocol origin-vocab drift latch (fixed via PR #3500)","description":"PR #3422 (2026-07-31) added claude-design-session to core.enums.Origin without bumping CURRENT_ORIGIN_VOCABULARY_VERSION, tripping origin_vocab.py's deliberate drift latch on every encode — failed all 22 material_protocol/v1 tests plus downstream encoders since 2026-07-31 10:23, invisible to per-PR CI's heavy-test skip. Fixed same-day via PR #3500 (version 2-\u003e3, frozen digest, fixture regenerated). Filed retroactively as a durable record of the incident and root cause for future origin-enum changes: bumping Origin members requires bumping CURRENT_ORIGIN_VOCABULARY_VERSION in the same PR.","status":"closed","priority":2,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-08-01T09:01:57Z","created_by":"Sinity","updated_at":"2026-08-01T09:02:22Z","closed_at":"2026-08-01T09:02:22Z","close_reason":"Fixed same-day via merged PR #3500 (origin_vocab.py version 2-\u003e3 + frozen digest + fixture regen). Retroactive record only.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-d4kq","title":"Refusal path cannot feed claude_parse_coverage: no session-scoped sink for refused records","description":"Residual from PR #3497 (polylogue-9ykn): the new positive-evidence refusal path records reasons but cannot feed the claude_parse_coverage event family because no session-scoped sink exists for records that never become sessions. Design where refused-record coverage evidence lives (source-tier taxonomy row? ops-tier event?) so the after-the-fact detector (PR #3419) and the refusal gate share one coverage picture. Ref polylogue-9ykn.","status":"open","priority":2,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T22:54:42Z","created_by":"Sinity","updated_at":"2026-07-31T22:54:42Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"polylogue-lkos","title":"Codex message-extraction defect: 2 large sessions materialize zero messages","description":"Residual from PR #3497's empty-session reconciliation (polylogue-9ykn): 2 large codex-session rows materialize with zero messages despite substantial raw content — a message-extraction defect in the codex parse path, distinct from the envelope-only/sidecar classes the gate now refuses. Identify the two sessions from the reconciliation evidence in PR #3497, reproduce extraction against their raws, fix the extraction gap.","status":"open","priority":2,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T22:54:42Z","created_by":"Sinity","updated_at":"2026-07-31T22:54:42Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"polylogue-lkos","title":"Codex message-extraction defect: 2 large sessions materialize zero messages","description":"Residual from PR #3497's empty-session reconciliation (polylogue-9ykn): 2 large codex-session rows materialize with zero messages despite substantial raw content — a message-extraction defect in the codex parse path, distinct from the envelope-only/sidecar classes the gate now refuses. Identify the two sessions from the reconciliation evidence in PR #3497, reproduce extraction against their raws, fix the extraction gap.","status":"in_progress","priority":2,"issue_type":"bug","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T22:54:42Z","created_by":"Sinity","updated_at":"2026-08-01T17:31:14Z","started_at":"2026-08-01T17:31:14Z","lease_expires_at":"2026-08-01T17:36:14Z","heartbeat_at":"2026-08-01T17:31:14Z","comments":[{"id":"a8df4338-b418-5b79-998d-d9b02fbee465","issue_id":"polylogue-lkos","author":"Sinity","text":"Investigated jointly with polylogue-buq8/polylogue-i415. Identified the '2 large sessions' from PR #3497's residual note (996KB/1.4MB raw blobs) -- both are among the same 11-session set buq8/i415 already flag, not a separate/distinct defect. This is NOT a message-extraction defect in the codex parser: direct reproduction of the current parse_stream_payload against live raw bytes for the largest sample in this cluster extracts real messages correctly. The actual mechanism: raw_revision_heads.accepted_raw_id for each of these sessions equals its own sessions.raw_id, decided by a bookkeeping-only backfill pass that ran months after the session's original (defective) write, without re-running message extraction. revision_authority_refuses_write's unconditional 'governed' check then permanently refused any corrective rewrite for that raw. Fixed in PR #3527 (accepted_raw_id comparison added to the shared write-refusal gate). All three of buq8/i415/lkos describe one root cause, not three; none of the three beads' original framing ('materialization never runs' / 'old rollout envelope' / 'distinct message-extraction defect') is what the live data actually shows.","created_at":"2026-08-01T17:31:55Z"}],"dependency_count":0,"dependent_count":0,"comment_count":1} {"_type":"issue","id":"polylogue-jtek","title":"rebuild perf: overlap the up-front census classification pass with replay","description":"Residual from polylogue-2cuv's close (PR #3496): the spill_load decode is now fully pipelined behind apply (#3478 _ReplaySpillPrefetcher, 72.7% of the old serial spill_load measured concurrent), but the up-front census classification pass still strictly precedes the replay loop — cohort membership must resolve before cohorts can be ordered, so overlapping census-page N+1 with replay-page N's apply needs a materially larger redesign (paged census with incremental cohort release). Worth doing only if the next real rebuild receipt shows census as a dominant remaining phase (selection_s + census timings now measured via #3494/#3469). Ref polylogue-2cuv polylogue-o56w.","status":"open","priority":2,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T22:43:38Z","created_by":"Sinity","updated_at":"2026-07-31T22:43:38Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-s0ug","title":"test_incremental_restart_and_fresh_generation_rebuild_are_equivalent fails after #3484 CONTINUATION lineage fix","description":"Discovered while rebasing perf/rebuild-deadline-and-phase-receipts (polylogue-uhgm/polylogue-6mvg) onto master past commit 0d27e3da7 (fix(sources): require structural evidence for codex CONTINUATION lineage, #3484). tests/unit/storage/test_incremental_rebuild_equivalence.py::test_incremental_restart_and_fresh_generation_rebuild_are_equivalent fails deterministically in isolation on origin/master HEAD (90d4df67e): assertion at line ~600 compares session_links rows and finds the synthetic lineage-child raw no longer carries a ('codex-session:lineage-parent', 'continuation') link -- exactly the shape #3484 tightened (a bare second session_meta record is no longer sufficient evidence for CONTINUATION; the parser now requires structural evidence such as forked_from_id). The test's hand-built Codex payload fixture (_codex_session helper, parent_native_id kwarg) predates that tightening and needs updating to supply the new required structural marker. Confirmed unrelated to polylogue-uhgm/polylogue-6mvg's changes: git diff origin/master...HEAD for that branch touches neither codex.py, revision_backfill.py's classification logic, nor this test file.","status":"closed","priority":2,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T22:36:26Z","created_by":"Sinity","updated_at":"2026-07-31T22:53:54Z","closed_at":"2026-07-31T22:53:54Z","close_reason":"Merged PR #3498 (fixture-side): the equivalence test's synthetic _codex_session predated #3484's evidence tightening and never set cwd/git on its session_metas; empirical validation over 494 real multi-meta rollouts (99.2% share cwd, 100% timestamp-consistent, the 4 exceptions genuinely unrelated) confirms the production rule is correct. Fixture now models real continuation structure; #3484's negative case still passes.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-c379","title":"HTTP _archive_filter_kwargs_from_spec missing root key breaks test_archive_filter_kwargs_cover_every_storage_lowerable_spec_field","description":"Discovered as a follow-up during 2026-07-31 verify-first triage of polylogue-bsi7 (which described a different, no-longer-reproducing order-dependent failure in the same test file). The real, deterministic (order-independent) failure is: tests/unit/daemon/test_web_reader.py::test_archive_filter_kwargs_cover_every_storage_lowerable_spec_field fails because polylogue/daemon/http.py:635-653 (_archive_filter_kwargs_from_spec) is missing a 'root' key that all four ArchiveStore query methods (count_sessions, list_summaries, search_summaries, count_search_sessions) now accept (confirmed via inspect.signature). This appears to date from commit 7f494b8d5 ('finish typed session-PR evidence + wire root: filter'), which wired 'root:' into the storage layer but not into the HTTP kwarg builder. Fix: add 'root' to the kwarg dict built by _archive_filter_kwargs_from_spec so the HTTP-facing filter path stays in parity with the storage layer.","acceptance_criteria":"test_archive_filter_kwargs_cover_every_storage_lowerable_spec_field passes; _archive_filter_kwargs_from_spec includes 'root' alongside the other lowerable spec fields.","status":"open","priority":2,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T21:13:07Z","created_by":"Sinity","updated_at":"2026-07-31T21:13:07Z","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -614,7 +614,7 @@ {"_type":"issue","id":"polylogue-awy5","title":"Acquisition failure has no durable representation: zero 'failed' rows ever, exceptions only in logger.warning, no source.db trace pre-acquire, batch path re-increments excluded cursors to failure_count=2018","description":"MECHANISM finding (enables every silent STAGE-1 leak; the operator requirement is that nothing is skipped without being accounted for). From the 2026-07-31 acquisition-completeness audit; all file:line verified on master.\n\nMeasured absences (all-time, ops.db mode=ro):\n- ingest_attempts: completed 2127 / interrupted 25 / failed 0 - the 'failed' status is never used. For failing batches, error_message holds the semicolon-joined source_path list, not the exception; the real exception (zipfile.BadZipFile etc.) goes only to logger.warning (polylogue/sources/live/batch.py:2571) and is lost.\n- daemon_stage_events: only 'running'/'completed' ever. daemon_events: zero error/fail kinds ever.\n- A file that fails BEFORE acquisition completes leaves no source.db row at all: raw_artifacts.raw_id is NOT NULL REFERENCES raw_sessions, and _insert_artifact (polylogue/storage/sqlite/archive_tiers/source_write.py:1046-1075) only runs post-acquire. Verified: the 5 crash-looped inbox ZIPs have 0 rows in both tables.\n- Give-up loop bug: _MAX_CURSOR_FAILURES_BEFORE_EXCLUDE=5 (sources/live/cursor.py:51); mark_failed (cursor.py:1110-1174) sets excluded, clears next_retry_at (:1162). The watcher gates on excluded (watcher.py:780), but the batch/full-ingest path calls mark_failed via _record_failed_cursor (batch.py:938-978; call sites :567,:702,:765) with no excluded pre-check - failure_count on the 5 ZIPs reached 689/864/975/1001/2018, i.e. the scan crash-loops on permanently-excluded sources, redoing work and capturing nothing.\n- No aggregate skip ledger: raw_artifacts.support_status has per-path lookup only (import_explain); ops status aggregates cursor exclusions (daemon/status.py:1170-1240) but not artifact statuses, and nothing can enumerate never-cursored classes (e.g. tool-results, antigravity .pb - see polylogue-rujy / polylogue-eo81).\n\nAC: (1) acquisition/parse failures produce a durable record carrying the actual exception (ingest_attempts status='failed' or equivalent event) - kill-based test; (2) files that fail pre-acquire leave a durable accounted-for row (artifact-level, not cursor-only); (3) batch path consults cursor.excluded before re-queueing (failure_count stops growing past threshold); (4) an aggregate accounting surface: counts by disposition for every observed-but-unarchived path class, so 'is everything accounted for?' is answerable with one query.","status":"open","priority":2,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:11:34Z","created_by":"Sinity","updated_at":"2026-07-31T10:11:34Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-2qrx","title":"Stalled append-cursor backlog: 211 live files 414MB behind, 206 of them cold for days-to-weeks (top: 94.8MB lag on one codex rollout, 329h stale)","description":"STAGE-1 ACQUISITION LEAK (content still on disk, so recoverable - but only by acquisition catch-up; an index rebuild replays source.db and recovers none of it). From the 2026-07-31 acquisition-completeness audit.\n\n211 non-excluded ingest cursors have byte_offset \u003c current file size: codex-session 104 files / 252.1MB lag, claude-code-session 106 / 126.9MB, unknown-export 1 / 35.3MB (the live /realm/db/polylogue/inbox/claude-ai-data-2026-07-30 zip). Only 5 are hot (mtime\u003c1h); 206 are stalled - file cold for days-to-weeks yet the cursor never caught up. Top offenders: rollout-2026-07-15T20-11-43-019f66fa (94.8MB lag on 118MB, 329h stale), rollout-2026-06-29T11-29-04-019f12b5 (30.3MB lag on 428MB, 625h stale), rollout-2026-07-11T22-26-14-019f52db (22.5MB lag, 422h).\n\nThese are exactly the tails of large multi-hundred-MB sessions - the newest content of the biggest working sessions is what's missing. Distinct from the excluded/give-up class (those are 93-100% content-accounted via full-reacquire; see audit report) and from the interrupted-ingest bead (polylogue-61jg) though plausibly the same daemon-interruption incidents left both residues.\n\nRepro (mode=ro): sqlite3 \"file:/realm/db/polylogue/ops.db?mode=ro\" \"select origin,count(*),sum(stat_size-byte_offset) from ingest_cursor where excluded=0 and byte_offset\u003cstat_size group by origin\" then re-stat paths on disk for current sizes.\n\nRelated: polylogue-aex0 (cursor continuity anchored to source.db), polylogue-1xc.13 (expose freshness/excluded degradation).\n\nAC: (1) the 206 stalled files drained to byte_offset==size (or a recorded per-file disposition); (2) a freshness signal exists that would have flagged a 329h-stale 94MB lag (ties into polylogue-1xc.13); (3) whatever stalls append catch-up on multi-hundred-MB files is root-caused.","status":"open","priority":2,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:11:33Z","created_by":"Sinity","updated_at":"2026-07-31T10:11:33Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-5iz4","title":"Codex 90.8MB session unindexable: 'membership replay cannot replace an unconvertible byte head' crash reproduces on every parse attempt","description":"STAGE-2 PARSE LEAK, rebuild does NOT fix (the parser crash reproduces on retry; bytes ARE complete in source.db). From the 2026-07-31 acquisition-completeness audit.\n\nCodex session native_id 019f49d8-0185-7c43-8793-db6e57db13e1 (rollout-2026-07-10T04-25-20-...jsonl) never entered the index. 804 raw_sessions rows share this source_path (incremental full-snapshot captures as the live file grew). The largest revision (90,822,451 bytes) has parsed_at_ms=NULL; the second largest (90,156,590 bytes) has parse_error='RuntimeError: membership replay cannot replace an unconvertible byte head'. index.db has zero sessions for this native_id. These 804 rows also account for 781 of codex's 'genuine gap' unparsed raw rows - one session, massive revision churn.\n\nRelated: polylogue-rgh2 (closed - accepted semantic head in membership replay authority) evidently did not cover this byte-head case; polylogue-1k9l tracks the broader 111-row parse_error ledger.\n\nRepro (mode=ro): sqlite3 \"file:/realm/db/polylogue/source.db?mode=ro\" \"select count(*), max(blob_size) from raw_sessions where source_path like '%019f49d8-0185-7c43-8793-db6e57db13e1%'\"\n\nAC: (1) root-cause the unconvertible-byte-head replay failure on this session's actual revision chain (fixture from the real blob shapes, content redacted); (2) the session parses and reaches the index with plausible message_count; (3) regression test for the replay path; (4) the 804-row churn compacts per normal revision authority rules.","status":"open","priority":2,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:10:54Z","created_by":"Sinity","updated_at":"2026-07-31T10:10:54Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"polylogue-p3b2","title":"Silent materialize drops: 97 chatgpt-export groups (211MB) parse OK but produce zero index sessions; ~3 of 47 claude-ai zero-message sessions hold real turns","description":"STAGE-2 PARSE LEAK, rebuild-fixes-it UNKNOWN until root-caused (if materialize has a deterministic drop it reproduces on rebuild). From the 2026-07-31 acquisition-completeness audit.\n\n1) 97 chatgpt-export union-find groups (211,881,173 bytes) have parsed_at_ms set, NO parse_error, yet no index.db session matches by raw_id or (origin,native_id). Concentrated under source paths containing 'inbox/\u003cuuid\u003e-\u003chash\u003e.json' (staging copies of browser captures). Parse claims success; materialization yields nothing; nothing records the drop.\n2) claude-ai-export zero-message sessions: of 47 total with index message_count=0, a 15-session sample found 14 genuinely-empty stubs (~233B raw, chat_messages:0) and 1 real drop: session claude-ai-export:44810a60-201b-4ea9-9db5-a46b21302bbc has chat_messages:4 in raw JSON but message_count=0 in index. Extrapolates to ~3/47; a full 47-session sweep is cheap and should be step one.\n\nRepro sketch (mode=ro): join raw_sessions latest revisions per (origin,native_id) against index sessions; for (2) decode the blob at /realm/db/polylogue/blob/\u003c2hex\u003e/\u003c62hex\u003e and count chat_messages vs sessions.message_count.\n\nAC: (1) the 97-group drop root-caused with the exact materialize decision named (and, if by-design e.g. duplicate-of-existing-session, that decision recorded durably per raw row rather than silent); (2) recoverable groups reach the index; (3) full sweep of the 47 zero-message claude-ai sessions, real-content ones re-materialized.","status":"open","priority":2,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:10:51Z","created_by":"Sinity","updated_at":"2026-07-31T10:10:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"polylogue-p3b2","title":"Silent materialize drops: 97 chatgpt-export groups (211MB) parse OK but produce zero index sessions; ~3 of 47 claude-ai zero-message sessions hold real turns","description":"STAGE-2 PARSE LEAK, rebuild-fixes-it UNKNOWN until root-caused (if materialize has a deterministic drop it reproduces on rebuild). From the 2026-07-31 acquisition-completeness audit.\n\n1) 97 chatgpt-export union-find groups (211,881,173 bytes) have parsed_at_ms set, NO parse_error, yet no index.db session matches by raw_id or (origin,native_id). Concentrated under source paths containing 'inbox/\u003cuuid\u003e-\u003chash\u003e.json' (staging copies of browser captures). Parse claims success; materialization yields nothing; nothing records the drop.\n2) claude-ai-export zero-message sessions: of 47 total with index message_count=0, a 15-session sample found 14 genuinely-empty stubs (~233B raw, chat_messages:0) and 1 real drop: session claude-ai-export:44810a60-201b-4ea9-9db5-a46b21302bbc has chat_messages:4 in raw JSON but message_count=0 in index. Extrapolates to ~3/47; a full 47-session sweep is cheap and should be step one.\n\nRepro sketch (mode=ro): join raw_sessions latest revisions per (origin,native_id) against index sessions; for (2) decode the blob at /realm/db/polylogue/blob/\u003c2hex\u003e/\u003c62hex\u003e and count chat_messages vs sessions.message_count.\n\nAC: (1) the 97-group drop root-caused with the exact materialize decision named (and, if by-design e.g. duplicate-of-existing-session, that decision recorded durably per raw row rather than silent); (2) recoverable groups reach the index; (3) full sweep of the 47 zero-message claude-ai sessions, real-content ones re-materialized.","notes":"INVESTIGATION 2026-08-01 (scope: AC(1)/(2) only, the 97 chatgpt-export groups -- AC(3), the claude-ai zero-message sweep, was NOT investigated this session and remains open).\n\nVERDICT: not a materialize-time drop, and not \"stale index that a rebuild fixes\" either -- it's a false positive in the 2026-07-31 audit's own join methodology. No pipeline bug exists; no code change made.\n\nEvidence (read-only queries against the live archive, source.db + index.db):\n\n1. Sampled the exact raw rows the audit describes (origin='chatgpt-export', parsed_at_ms IS NOT NULL, parse_error IS NULL, source_path under .../inbox/\u003cuuid\u003e-\u003chash\u003e.json). Example raw_id f19944c8fe19cd59...: content decodes to a real browser-capture envelope (polylogue_capture_kind=browser_llm_session) for ChatGPT conversation 6a4433f6-8604-83eb-a40c-7cc2f641e158, 21 turns, 129955 bytes.\n\n2. raw_sessions.native_id is NULL on this row and on every other row in this class -- BY DESIGN. write_raw_and_parsed_result (pipeline/services/archive_ingest.py) sets native_id from the parsed session's provider_session_id, but the live daemon watcher's full-record path uses write_raw_payload (storage/sqlite/archive_tiers/revision_governance.py:445, called from sources/live/batch.py:1968) which never receives/sets native_id at all -- see the standing code comment at pipeline/services/archive_ingest.py:137-149: \"The live daemon watcher instead writes ONE raw per file (write_raw_payload, no native_id) and defers session identity to membership-census classification.\" So (origin, native_id) and raw_id joins against raw_sessions can never succeed for this class -- that's expected, not a bug.\n\n3. The actual per-raw materialize decision IS recorded durably, just in a different table the audit didn't query: raw_session_memberships (source.db), keyed by raw_id, with a provider_session_id, decision (applied / superseded_equivalent / superseded_prefix / ambiguous / NULL=undecided), and decided_at_ms. For raw_id f19944c8...: decision='superseded_prefix', provider_session_id='6a4433f6-...', decided_at_ms=1785387771453 (2026-07-30). Its 3 sibling raws for the same uuid (under browser-capture/chatgpt/, near-simultaneous acquisition, likely from the known archive-root consolidation event) show 'superseded_equivalent' x2 and 'applied' x1 -- the 'applied' raw (c1e1669e...) is exactly the one index.db session chatgpt-export:6a4433f6-... points at (raw_id column), message_count=859.\n\n4. This generalizes across the WHOLE class, not just the hand-picked sample: re-ran the audit's own join (origin='chatgpt-export', parsed_at_ms set, no parse_error, no index.db session matching by (origin,native_id)) with no other filter -- 4936 raw rows match (\"unrepresented\" by the naive join). For every single one of the 4936, raw_session_memberships has a row, and every one of those rows' provider_session_id resolves to a real index.db session (join on 4936/4936). Decision breakdown: superseded_equivalent=3025, applied=1852, superseded_prefix=59. Zero ambiguous, zero undecided (NULL decision), zero missing a corresponding session. Checked the 5 largest-by-blob_size rows in this set individually too (25.3MB/25.4MB/24MB blobs) -- same story, sessions with 2264 and 1713 real messages exist.\n\n5. The bead's own 97-\"group\" count (vs. my 4936-row count) reflects the audit's union-find over (logical_source_key, (origin,native_id), (origin,source_path)) -- all three keys are NULL/non-shared for this daemon-acquired class, so the audit's grouping degenerates to ~1 group per raw_id for anything that doesn't share a literal source_path, undercounting the true redundancy but still reporting \"no match\" for content that IS present, because the join never looks at raw_session_memberships.provider_session_id.\n\nCONCLUSION: no fix landed. Content is not missing; it is fully accounted for via the membership-census mechanism (sources/live/batch.py's _apply_membership_sessions/replace_raw_membership_census path + raw_session_memberships table), which is working as designed. polylogue ops reset --index \u0026\u0026 polylogued run is not needed for this finding -- there is nothing for a rebuild to recover. The real gap is purely in how the audit joins raw acquisition state to index state: it should resolve identity via raw_session_memberships.provider_session_id (or index.db sessions.raw_id) for any row where raw_sessions.native_id is NULL, not treat NULL-native_id rows as unrepresented. Recommend closing AC(1)/(2) as \"verified non-finding\" if/when someone re-runs a completeness audit, and keeping this bead open only for AC(3) (the claude-ai-export zero-message sweep, e.g. 44810a60-201b-4ea9-9db5-a46b21302bbc), which this session did not touch.","status":"open","priority":2,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:10:51Z","created_by":"Sinity","updated_at":"2026-08-01T17:23:12Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-l1qg","title":"POLYLOGUE_ARCHIVE_ROOT silently redirects ops/maintenance CLI to a scratch archive during recovery flows","description":"Audit 2026-07-31, reproduced live: with POLYLOGUE_ARCHIVE_ROOT=/tmp/polylogue-archive inherited from the repo devshell env, 'polylogue ops maintenance raw-authority-frontier' reported census:1 accepted=0 plans=0 (an empty scratch archive) instead of the live archive's census 932 with 17,384 plans — no warning that the root came from an env override. An operator running break-glass maintenance in the wrong shell would conclude the frontier is clean. Matches the 2026-07-28 archive-root precedence scare (memory note). Needs: ops/maintenance commands should print the resolved archive root + its provenance (env/config/default) on every invocation, and arguably refuse env-derived roots for break-glass apply subcommands without an explicit flag.","status":"open","priority":2,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:09:54Z","created_by":"Sinity","updated_at":"2026-07-31T10:09:54Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-mia3","title":"Parse-pool result wait has no watchdog: zero-completion hang re-loops on 15s heartbeat forever (p0pw residual)","description":"Audit 2026-07-31. The forkserver deadlock cause behind polylogue-p0pw is already mitigated (process_pool.py:40 uses spawn), but the consumer loop remains unguarded: _iter_ingest_results_chunk (pipeline/services/ingest_batch/_core.py:965-974) does wait(futures, timeout=15, FIRST_COMPLETED) and on empty 'done' just heartbeats and continues — no worker-liveness probe, no escalation, no sequential fallback once submission succeeded, and terminate_process_pool (process_pool.py:117-132) is never wired as a watchdog. Any future worker hang (resource exhaustion, silent worker death without future resolution) stalls ingest indefinitely while heartbeats keep the attempt looking alive. Needs: bounded total-stall detection (N heartbeats with zero completions and zero running workers → terminate pool, fall back sequential, record attempt failure).","status":"open","priority":2,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:09:51Z","created_by":"Sinity","updated_at":"2026-07-31T10:09:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-ix5r","title":"Excluded ingest cursors are permanent-unless-file-replaced; status mislabels them 'retry due' and hides age","description":"Audit 2026-07-31. 1,446 cursor rows excluded=1 (5-failure cap, cursor.py:51,1147-1164); revive_replaced_exclusion requires the FILE to change identity (size/dev/inode/mtime), so a parser fix never revives them — they stay dark until manual re-ingest. Pre-exclusion history shows the cost of the old regime: export zips reached failure_count 2,018/1,001/975/864/689 (thousands of full acquire+parse+crash cycles). polylogued status prints 'Live cursor: 1241 failed, 1446 excluded, 1241 retry due' — for excluded rows the retry never comes — and 'Failing files: 50 shown, 1397 omitted' with no ages. Needs: parser-fingerprint-aware revival (retry excluded files when the parser fingerprint changes), age/oldest surfacing, and honest labeling.","status":"open","priority":2,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:09:47Z","created_by":"Sinity","updated_at":"2026-07-31T10:09:47Z","dependency_count":0,"dependent_count":0,"comment_count":0} From 623c51a9a618d241da4b3ed2e7a8c8cdc0662819 Mon Sep 17 00:00:00 2001 From: Sinity Date: Sat, 1 Aug 2026 19:32:51 +0200 Subject: [PATCH 3/4] chore(beads): release buq8/i415/lkos claims pending merge + reparse Comments left; fix is in PR #3527 (unmerged) and the live archive still needs an ordinary reparse of the affected raw_ids after merge -- leaving these open rather than closing prematurely. Co-Authored-By: Claude --- .beads/issues.jsonl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index cc3ebb2b21..b042058349 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -117,7 +117,7 @@ {"_type":"issue","id":"polylogue-qsb4","title":"Delegation is a tree, not one level: no arbitrary-depth ancestry/subtree query surface","description":"SCOPE CLARIFICATION on polylogue-1vpm.7 (operator, 2026-07-31, mid-session):\ndelegation in this archive is a tree, not one level -- several subagents\ndispatched in real sessions launch their own subagents. delegation_facts\nalready models this IMPLICITLY (each row is one parent_session_id -\u003e\nchild_session_id edge; a child that itself dispatches subagents gets its\nown delegation_facts rows keyed by its own session_id as parent), so\narbitrary depth already exists in the DATA. What is missing is a single\nquery surface that returns a whole ancestry chain or subtree in one call,\ndepth-annotated, without N+1 queries or client-side reassembly -- and any\nUX built on top of it.\n\nWHY THIS MATTERS: session claude-code-session:38baa1de-9715-48fa-8175-\nf2a29d92800e dispatches ~20 subagents via the \"Agent\" tool (see\npolylogue-1vpm.7's companion fix in archive/viewport/tools.py); some of\nthose subagents dispatch their own subagents (nested Agent-tool calls are\nvisible in the corpus -- verify exact depth/count live before designing).\nA report describing this session's fan-out needs \"whose child is this at\nevery level\", \"what did agent X ultimately spawn\", and \"who ultimately\nasked for this work\" -- none of which delegation_facts' flat per-session\nrows answer without recursive client-side stitching today.\n\nCURRENT STATE (verified 2026-07-31, read-only against\nfile:/realm/db/polylogue/index.db):\n- delegation_facts / delegations (storage/sqlite/archive_tiers/archive.py):\n get_delegation_attempt/get_delegation_card resolve ONE edge by identity\n (instruction_tool_use_block_id, or parent+child pair). query_delegations\n is a flat filtered list, no recursion, no depth column.\n- session_links already has the EXACT precedent to reuse: it persists\n every parent reference a parser asserts even when the parent isn't\n ingested yet, keyed (src_session_id, dst_origin, dst_native_id,\n link_type), resolved on each save, with TopologyEdgeStatus =\n unresolved/resolved/repaired/quarantined (quarantined = the cycle-break,\n #866/#1260). delegation_facts_source already excludes quarantined\n session_links edges (`l.status IS NULL OR l.status != 'quarantined'`),\n so cycle-break precedent is already inherited at the edge level -- a\n recursive CTE walking delegation_facts should still carry an explicit\n visited-path guard defensively, but should not need to invent a second\n cycle vocabulary.\n- work_evidence_nodes/work_evidence_edges (index v46+) already hold a\n generic directed graph (edge_kind: invoked/claimed/mentioned/produced/\n retried/unresolved) that DOES support arbitrary-depth traversal via\n recursive CTE by construction -- but it is populated only from Workflow\n orchestration runs today (verified live: 7 runs, 122 calls, 164\n attempts, 128 structured-results, 0 rows sourced from Claude Code\n subagent dispatch). polylogue-1vpm's own tracking notes call this graph\n \"structurally hollow\" (authority/confidence constant, no time/actor).\n Whether delegation should PROJECT INTO this graph (one edge_kind=\n 'delegated' per delegation_facts row) rather than growing a second,\n parallel recursive-CTE surface is an open design question this bead\n must answer, not assume either way.\n\nACCEPTANCE CRITERIA:\n1. A single call returns the full ancestry chain (root-to-node) for a\n given session/delegation, depth-annotated, in one query -- no N+1.\n2. A single call returns the full subtree (node-to-all-descendants) for a\n given session/delegation, depth-annotated, in one query -- no N+1.\n3. Cycles/orphans reuse session_links' TopologyEdgeStatus vocabulary and\n quarantine precedent rather than inventing a second one; state\n explicitly whether a defensive visited-path guard is still needed in\n the recursive CTE despite quarantine already excluding cycle edges at\n the source.\n4. Explicit design decision, argued from evidence: does this live as new\n recursive-CTE methods on ArchiveStore (delegation_facts-native), or as\n a projection into work_evidence_nodes/edges (join existing \"invoked\"\n graph), or both with one clearly designated as source of truth? Read\n polylogue-1vpm and polylogue-1vpm.6 first -- this may already be a\n settled architectural decision this bead is unaware of.\n5. At least one production surface (MCP tool, CLI verb, or existing\n `get`/`query` dispatcher extension) exposes the tree/subtree query --\n not merely a new ArchiveStore method with no caller. cli/commands/\n analyze* and archive/query/ are owned by other lanes per this session's\n scope -- coordinate or use the MCP `get`/`query` dispatcher instead.\n6. State what UX the html-report skill's delegation-tree CSS pattern\n (references/patterns.md) is meant to consume from this surface, even\n if the actual report/HTML rendering is out of this bead's scope --\n the query surface's shape should not require a second redesign once a\n renderer is built against it.\n7. Live re-measurement: exact max delegation depth and fan-out width in\n the corpus today (after the companion \"Agent\" tool_name -\u003e SUBAGENT\n classification fix lands and, if the operator runs it, a reindex) --\n confirm the \"~20 subagents, some nested\" claim with real numbers before\n finalizing the design.\n\nNON-GOALS (unless folded in explicitly): rewriting delegation_facts'\nidentity-matching mechanism (that's polylogue-1vpm.7, already fixed);\nbuilding the actual HTML/report rendering (that's the report-writing\ntask this bead's design should unblock, not perform).","status":"open","priority":1,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:34:56Z","created_by":"Sinity","updated_at":"2026-07-31T10:34:56Z","labels":["area:insights","area:storage","lane:analytics-experiments"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-f1ie","title":"Hook sidecar path mismatch: writer and reader use different directories, paste ground truth discarded live","description":"MEASURED 2026-07-31 (conversation-fidelity audit, audit-only pass). Live, currently-active pipeline break -- not historical debt.\n\nTHE MISMATCH:\n WRITER: ~/.claude/settings.json invokes 'polylogue-hook \u003cevent\u003e --sidecar-dir /home/sinity/.local/share/polylogue/hooks' on UserPromptSubmit / PreToolUse / PostToolUse and others. That directory currently holds ~197,485 files.\n READER: polylogue/sources/live/hook_paste_enrichment.py resolves its sidecar directory through polylogue/config.py:2199-2206 (hook_sidecar_dir setting, falling back to archive_root/hooks) -\u003e /realm/db/polylogue/hooks. That directory is empty apart from an unused pending/ subdir.\nThe daemon's paste-enrichment step therefore never sees any hook sidecar. Hook ground truth accumulates in a directory nothing consumes.\n\nOBSERVABLE CONSEQUENCE: messages.has_paste = 1 for 4 rows out of 4,908,097 archive-wide. paste_boundary is 'projected' on those same 4 and NULL everywhere else.\n select has_paste, count(*) from messages group by 1;\nCross-check via FTS finds 1,424 blocks whose text contains 'pasted' and 'text' within 3 tokens:\n select count(*) from messages_fts where messages_fts match 'NEAR(pasted text, 3)';\nTwo to three orders of magnitude more candidate pastes than flagged messages. has_paste / paste_count are effectively inert columns.\n\nNOT FULLY DISAMBIGUATED (be honest): a second, independent detection path exists -- polylogue/archive/message/paste_detection.py:has_paste_marker looks for a literal '[Pasted text #N]' marker in message text at parse time, and does not depend on the hook sidecar. The FTS-vs-has_paste gap could therefore be (a) that path also not firing, or (b) most of those 1,424 hits predating the paste-detection feature and never having been reprocessed, since materialization runs at ingest/reprocess time and not retroactively. This audit could not separate the two. Whichever it is, the path mismatch above is independently real and worth fixing first because it is cheap.\n\nRELATED, NOT THE SAME: attachment_refs.upload_origin='paste' has 69 real rows, so pasted ATTACHMENTS are recorded (just under-acquired like every other attachment channel). It is paste TEXT detection that is dead.\n\nFIX: point the two at the same directory -- either set hook_sidecar_dir in polylogue.toml to ~/.local/share/polylogue/hooks, or change the --sidecar-dir the sinnix-managed hook command passes. Then decide whether a one-off reprocess is warranted to backfill has_paste on historical sessions.\n\nRE-RUN:\n grep -c sidecar-dir ~/.claude/settings.json\n ls /realm/db/polylogue/hooks; ls ~/.local/share/polylogue/hooks | wc -l\n sqlite3 \"file:/realm/db/polylogue/index.db?mode=ro\" \"select has_paste, count(*) from messages group by 1;\"","status":"closed","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:27:01Z","created_by":"Sinity","updated_at":"2026-08-01T17:10:04Z","closed_at":"2026-08-01T17:10:04Z","close_reason":"Duplicate of polylogue-swqu (sinnix settings.json fix) + polylogue-k8wv (backlog drain), both filed alongside already-merged PR #3418 which added the drift detector. No polylogue code defect remains; verified reader/writer resolution is correct and drift-detection is live.","comments":[{"id":"cb8e6430-27ae-52cc-b996-a0213b98106c","issue_id":"polylogue-f1ie","author":"Claude","text":"Investigated. This is not a live polylogue code bug -- it's a duplicate\ndiscovery of two beads already filed alongside PR #3418 (merged\n2026-07-31T08:04:01Z, 23 min before this bead was created):\n\n- polylogue-swqu: the actual root cause (sinnix's ~/.claude/settings.json\n template bakes a stale --sidecar-dir from before the archive root moved\n to /realm/db/polylogue). Confirmed still live 2026-08-01:\n `grep -c sidecar-dir ~/.claude/settings.json` = 5, all pointing at\n /home/sinity/.local/share/polylogue/hooks. This is a sinnix-repo fix\n (dots/claude/settings.json template), out of scope for a polylogue PR.\n- polylogue-k8wv: migrating the legacy flat-file backlog (~197K files now,\n was 108,956 at filing) into source.db via the existing idempotent\n drain_hook_event_spool() mechanism, once the archive root is corrected\n and the daemon isn't mid-restart.\n\nVerified there is no reader-side code defect: archive_root()\n(polylogue/paths/_roots.py) correctly resolves /realm/db/polylogue from\npolylogue.toml's [archive].root, and hooks_sidecar_dir() =\narchive_root()/\"hooks\" tracks it -- this matches the bead's own\n\"reader\" observation exactly. hook_install_sidecar_drift() (added in\n#3418, polylogue/hooks/__init__.py) already detects this exact drift\nclass and polylogue/daemon/cli.py's 15-min heartbeat already logs it as\na warning. No further polylogue code change would do anything the\nalready-merged PR doesn't. Not opening a redundant PR.\n\nAside (found while investigating, not actioned): the live polylogued.service\nis currently refusing to start its watcher --\n\"tier user_version mismatch: index.db:46!=53\" -- unrelated to this bead,\nworth its own look.\n\nClosing as duplicate of polylogue-swqu + polylogue-k8wv, which already\ncarry the full remaining scope (sinnix settings fix + backlog drain).\n","created_at":"2026-08-01T17:10:24Z"}],"dependency_count":0,"dependent_count":0,"comment_count":1} {"_type":"issue","id":"polylogue-ksgg","title":"Branch/thread structure is unreconstructible: 5 of 9 origins carry no message parent links; read surface omits the columns","description":"MEASURED 2026-07-31 (conversation-fidelity audit, audit-only pass).\n\n(A) PARENT LINKS MISSING PER ORIGIN. Sampled 60 sessions per origin (all, where fewer exist), counting messages with parent_message_id set:\n claude-code-session 21048 msgs 96.7% parented\n chatgpt-export 6574 msgs 91.6%\n claude-ai-export 1123 msgs 87.6%\n codex-session 18373 msgs 0.0%\n hermes-session 7507 msgs 0.0%\n aistudio-drive 3108 msgs 0.0%\n gemini-cli-session 652 msgs 0.0%\n grok-export 16 msgs 0.0%\nFive of nine origins store no message-level parent at all. Sessions there are a flat ordered list; the tree the data model documents (sessions -\u003e messages -\u003e blocks with parent links) is not populated.\n\n(B) VARIANTS NEVER RECORDED for the two coding origins: variant_index\u003e0 count is 0 for claude-code-session and 0 for codex-session in the same samples. Retries/regenerations, where they occurred, are not distinguishable.\n\n(C) ACTIVE-PATH CONTRADICTION. 665 sessions archive-wide contain variant_index\u003e0 rows. In 25 of them (490 variant messages) there is not a single is_active_path=0 row -- every variant is marked as being on the active path, so the branch the user actually saw cannot be recovered for those sessions.\n select session_id, count(*), sum(variant_index\u003e0) v, sum(is_active_path=0) inactive from messages group by 1 having v\u003e0;\n\n(D) THE READ SURFACE DOES NOT EXPOSE ANY OF IT. The message payload from has these keys and no others:\n actions, anchor, attachment_refs, branch_index, cache_read_tokens, cache_write_tokens, content_blocks, has_paste_evidence, has_thinking, has_tool_use, id, input_tokens, material_origin, message_type, output_tokens, role, session_id, target_ref, text, timestamp\nAbsent: position, variant_index, is_active_path, is_active_leaf, parent_message_id. So even for claude-code and chatgpt, where the columns ARE populated, a consumer of the JSON read surface cannot reconstruct ordering-by-position or which branch was live. Verified against three real sessions across two origins.\n\nCONSEQUENCE: 'does the archive reconstruct the branch the user actually saw' is answerable only by direct SQL, and on five origins not at all.\n\nSUGGESTED SPLIT: (D) is cheap and self-contained -- add the columns to the messages payload. (A)/(B) are per-origin parser work. (C) is a writer-side active-path assignment bug worth isolating first since it is small and bounded (25 sessions).","status":"open","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:22:02Z","created_by":"Sinity","updated_at":"2026-07-31T10:22:02Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"polylogue-buq8","title":"Eleven codex sessions with multi-MB real content materialize as zero messages (quarantine consequence)","description":"MEASURED 2026-07-31 (conversation-fidelity audit, audit-only pass). User-visible consequence of polylogue-u19l; filed separately because the symptom is content loss in the read model, not a convergence metric.\n\nQUERY: select count(*) from sessions s where s.origin='codex-session' and not exists(select 1 from messages m where m.session_id=s.session_id); -\u003e 17\n\nOf those 17, raw files were inspected directly:\n - 6 are genuinely empty (raw is session_meta plus at most a bare task_started event). Correctly represented.\n - 11 hold real substantial conversations, 996 KB to 3.3 MB, 694 to 3,688 lines each. Example native_id 019a2e27-2596-7f22-b6f5-e26acd721d57 (3.3 MB / 3,685 lines) raw inner-type census: message:50, user_message:24, agent_reasoning:478, reasoning:479, function_call:444, function_call_output:444, custom_tool_call:67, custom_tool_call_output:67. ZERO of this reached messages, blocks, or session_events.\n\nROOT CAUSE (confirmed in source.db): all 11 raw rows have parse_error=NULL, validation_status='passed', blob_size matching the real file -- the bytes were acquired and parsed fine. They carry revision_authority='quarantined', revision_kind='unknown', source_index=0, no predecessor_raw_id/baseline_raw_id: a single uncontested acquisition whose authority classification never resolved to byte_proven, so materialization into index.db never runs. This is the absorbing-state mechanism diagnosed in polylogue-u19l.\n\nSCOPE: select revision_authority, count(*) from raw_sessions where origin='codex-session' group by 1; -\u003e byte_proven 3951, quarantined 5202 (57%).\n\nWHY THIS MATTERS SEPARATELY FROM u19l: sessions.message_count=0 reads as 'nothing happened here' on every surface. Nothing distinguishes a genuinely empty rollout from 3.3 MB that never materialized. The audit brief's warning applies exactly -- this stayed invisible because everything downstream trusted the parser's verdict.\n\nRESIDUAL / INFERRED, not measured: the 11 are only the sessions where EVERY raw row was quarantined. With 5,202 quarantined rows overall, sessions where some rows resolved and others did not would lose content while still reporting message_count\u003e0, and would never appear in this query. Detecting those needs a per-session reconciliation of raw item counts against archive row counts. Recommend that as the acceptance check for u19l's fix rather than 'no more empty sessions'.","status":"in_progress","priority":1,"issue_type":"bug","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:21:16Z","created_by":"Sinity","updated_at":"2026-08-01T17:31:14Z","started_at":"2026-08-01T17:31:14Z","lease_expires_at":"2026-08-01T17:36:14Z","heartbeat_at":"2026-08-01T17:31:14Z","dependencies":[{"issue_id":"polylogue-buq8","depends_on_id":"polylogue-u19l","type":"relates-to","created_at":"2026-07-31T14:40:08Z","created_by":"Sinity","metadata":"{}"}],"comments":[{"id":"5ea20fc8-4556-53bb-8f6b-628e6b16c58a","issue_id":"polylogue-buq8","author":"Sinity","text":"Investigated jointly with polylogue-i415/polylogue-lkos (same live-archive symptom, three inconsistent theories). Root cause is NOT quarantine blocking materialization: live query shows 1,742/1,757 quarantined codex-session rows already have real message counts, so revision_authority is orthogonal to whether materialization ran. Actual bug: raw_revision_heads.accepted_raw_id for these 11 sessions equals their own sessions.raw_id, with decided_at_ms months after sessions.updated_at_ms -- a bookkeeping-only backfill retroactively declared the raw authoritative without re-running message extraction, and revision_authority_refuses_write's unconditional governed-check then refused every subsequent write attempt for that session_id forever, including the accepted raw's own corrective rewrite. Direct reproduction confirms the current codex parser extracts the real content correctly (1,496 messages from the 3.3MB flagship sample) -- this was never a parser defect. Fixed in PR #3527 (revision_authority_refuses_write now compares accepted_raw_id, only refusing a genuinely different/losing raw). Fix unblocks future re-ingest but does not retroactively repair the already-materialized rows -- those need an ordinary session-scoped reparse after merge, not a schema/index bump.","created_at":"2026-08-01T17:31:35Z"}],"dependency_count":0,"dependent_count":0,"comment_count":1} +{"_type":"issue","id":"polylogue-buq8","title":"Eleven codex sessions with multi-MB real content materialize as zero messages (quarantine consequence)","description":"MEASURED 2026-07-31 (conversation-fidelity audit, audit-only pass). User-visible consequence of polylogue-u19l; filed separately because the symptom is content loss in the read model, not a convergence metric.\n\nQUERY: select count(*) from sessions s where s.origin='codex-session' and not exists(select 1 from messages m where m.session_id=s.session_id); -\u003e 17\n\nOf those 17, raw files were inspected directly:\n - 6 are genuinely empty (raw is session_meta plus at most a bare task_started event). Correctly represented.\n - 11 hold real substantial conversations, 996 KB to 3.3 MB, 694 to 3,688 lines each. Example native_id 019a2e27-2596-7f22-b6f5-e26acd721d57 (3.3 MB / 3,685 lines) raw inner-type census: message:50, user_message:24, agent_reasoning:478, reasoning:479, function_call:444, function_call_output:444, custom_tool_call:67, custom_tool_call_output:67. ZERO of this reached messages, blocks, or session_events.\n\nROOT CAUSE (confirmed in source.db): all 11 raw rows have parse_error=NULL, validation_status='passed', blob_size matching the real file -- the bytes were acquired and parsed fine. They carry revision_authority='quarantined', revision_kind='unknown', source_index=0, no predecessor_raw_id/baseline_raw_id: a single uncontested acquisition whose authority classification never resolved to byte_proven, so materialization into index.db never runs. This is the absorbing-state mechanism diagnosed in polylogue-u19l.\n\nSCOPE: select revision_authority, count(*) from raw_sessions where origin='codex-session' group by 1; -\u003e byte_proven 3951, quarantined 5202 (57%).\n\nWHY THIS MATTERS SEPARATELY FROM u19l: sessions.message_count=0 reads as 'nothing happened here' on every surface. Nothing distinguishes a genuinely empty rollout from 3.3 MB that never materialized. The audit brief's warning applies exactly -- this stayed invisible because everything downstream trusted the parser's verdict.\n\nRESIDUAL / INFERRED, not measured: the 11 are only the sessions where EVERY raw row was quarantined. With 5,202 quarantined rows overall, sessions where some rows resolved and others did not would lose content while still reporting message_count\u003e0, and would never appear in this query. Detecting those needs a per-session reconciliation of raw item counts against archive row counts. Recommend that as the acceptance check for u19l's fix rather than 'no more empty sessions'.","status":"open","priority":1,"issue_type":"bug","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:21:16Z","created_by":"Sinity","updated_at":"2026-08-01T17:32:39Z","started_at":"2026-08-01T17:31:14Z","dependencies":[{"issue_id":"polylogue-buq8","depends_on_id":"polylogue-u19l","type":"relates-to","created_at":"2026-07-31T14:40:08Z","created_by":"Sinity","metadata":"{}"}],"comments":[{"id":"5ea20fc8-4556-53bb-8f6b-628e6b16c58a","issue_id":"polylogue-buq8","author":"Sinity","text":"Investigated jointly with polylogue-i415/polylogue-lkos (same live-archive symptom, three inconsistent theories). Root cause is NOT quarantine blocking materialization: live query shows 1,742/1,757 quarantined codex-session rows already have real message counts, so revision_authority is orthogonal to whether materialization ran. Actual bug: raw_revision_heads.accepted_raw_id for these 11 sessions equals their own sessions.raw_id, with decided_at_ms months after sessions.updated_at_ms -- a bookkeeping-only backfill retroactively declared the raw authoritative without re-running message extraction, and revision_authority_refuses_write's unconditional governed-check then refused every subsequent write attempt for that session_id forever, including the accepted raw's own corrective rewrite. Direct reproduction confirms the current codex parser extracts the real content correctly (1,496 messages from the 3.3MB flagship sample) -- this was never a parser defect. Fixed in PR #3527 (revision_authority_refuses_write now compares accepted_raw_id, only refusing a genuinely different/losing raw). Fix unblocks future re-ingest but does not retroactively repair the already-materialized rows -- those need an ordinary session-scoped reparse after merge, not a schema/index bump.","created_at":"2026-08-01T17:31:35Z"}],"dependency_count":0,"dependent_count":0,"comment_count":1} {"_type":"issue","id":"polylogue-mvq8","title":"\u003e8MiB browser captures stamped unknown-export by the 1MiB provider probe: 641MB of ChatGPT captures unparseable, 8 conversations wholly absent, lane re-captures them forever","description":"STAGE-2 (detection defect at acquire time) - rebuild does NOT fix: origin is stamped on the raw row; needs probe fix + re-detection of stored unknown-export rows. From the 2026-07-31 acquisition-completeness audit.\n\nMechanism (file:line, verified against a live blob): captures \u003e _STREAMING_FULL_INGEST_BYTES = 8MiB (polylogue/sources/live/batch_support.py:26) take _browser_capture_prefix_probe, which reads only _BROWSER_CAPTURE_PREFIX_PROBE_BYTES = 1MiB (batch_support.py:31, read at :464). The capture envelope orders raw_provider_payload BEFORE session.provider, so for any conversation big enough the provider regex (:469) finds nothing and the row falls back to unknown-export (:506-509). A correct bounded ijson reader already exists (_stream_browser_capture_provider, polylogue/sources/source_acquisition_components.py:355-381) but is not used on this route.\n\nMeasured: 23 distinct browser-capture paths / 641,613,073 bytes of raw rows sit under origin='unknown-export' (repro: select count(distinct source_path),sum(blob_size) from raw_sessions where origin='unknown-export' and source_path like '%browser-capture%'). Union-find vs index: 8 conversations (108.8MB) wholly unrepresented; the rest exist only as stale truncated pre-8MiB versions. Compounding: the unsatisfied cursor re-acquires the growing conversation repeatedly - one path has 12 raw rows of ~23MB each. ACTIVE: the chatgpt browser-capture lane is live (acquired same-day as audit).\n\nRelated: polylogue-t0ta (chatgpt detection tightness), polylogue-erf3 (claude.ai zip container-level unknown-export), polylogue-01fe (unknown-export unfilterable).\n\nAC: (1) provider detection for streaming-size captures uses the bounded ijson envelope reader (or equivalent) - a \u003e8MiB capture with provider after a multi-MB payload detects correctly, with test; (2) the 23 stored unknown-export capture rows re-detected/re-originated and parsed - the 8 absent conversations reach the index; (3) re-capture churn stops (cursor satisfied after successful parse).","status":"open","priority":1,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:10:18Z","created_by":"Sinity","updated_at":"2026-07-31T10:10:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-pebu","title":"Recover unimported provider exports: 2026-07-29 ChatGPT ZIP holds 181 conversations absent from the archive; 5 legacy inbox ZIPs (2.29GB) permanently excluded","description":"STAGE-1 ACQUISITION LEAK - index rebuild does NOT recover any of this; the bytes were never captured. From the 2026-07-31 acquisition-completeness audit (report: /realm/inbox/polylogue-audits-2026-07-31/acquisition-completeness.html).\n\n1) /realm/data/exports/chatlog/raw/chatgpt/chatgpt-data-2026-07-29-03-22-34.zip (16.08GB, 2836 conversations) has ZERO raw_sessions rows referencing it. Browser-capture independently covers 2291/2472 dated conversations (92.7%), but 181 conversations exist ONLY inside this unimported ZIP. (Related: polylogue-geop - newer exports are not supersets, so older bundles must not be pruned on import.)\n2) 5 ZIPs under the legacy ~/.local/share/polylogue/inbox/ ({chatgpt,claude-ai}-data-*.zip, largest 2.07GB, total 2.29GB) have zero raw_sessions AND zero raw_artifacts rows; their ingest cursors are excluded=1 with failure_count 689/864/975/1001/2018 (crash-looped past the design ceiling of 5 - see the failure-accounting bead). Cross-check against /realm/data/exports/chatlog/raw originals before re-import; at least chatgpt-data-2026-04-23 exists there and IS imported, so dedupe by content, not by path.\n\nRepro (mode=ro):\n sqlite3 \"file:/realm/db/polylogue/source.db?mode=ro\" \"select count(*) from raw_sessions where source_path like '%chatgpt-data-2026-07-29%'\" -- 0\n sqlite3 \"file:/realm/db/polylogue/ops.db?mode=ro\" \"select source_path,failure_count from ingest_cursor where failure_count\u003e100\" -- the 5 ZIPs\n\nAC: (1) 2026-07-29 export imported; the 181 absent conversations present in index (verify by native_id sample); (2) each of the 5 excluded ZIPs either imported from a verified-good copy or explicitly closed as corrupt/duplicate WITH a durable record of that disposition; (3) no double-ingest of conversations already present via browser-capture (content-hash idempotency should handle this - verify counts before/after).","status":"open","priority":1,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:10:16Z","created_by":"Sinity","updated_at":"2026-07-31T10:10:16Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-7eo7","title":"Health verdict contradicts its own data: 'ok (6 alerts)', 23-min-stale heartbeat still 'running', cursor_lag_samples never populated, health loop absent when schema-blocked","description":"Audit 2026-07-31, four observability defects in one verdict pipeline: (1) polylogued status prints 'Health: ok (6 alerts)' while hook_flow [error] fires every tick (journal, dozens/day) — alerts don't feed the verdict. (2) 'Status: running (heartbeat 1369.8s ago)' — a 23-min-stale heartbeat (15-min interval) produces no staleness verdict. (3) ops.db cursor_lag_samples has 0 rows EVER — the cursor-lag SLO check reads a table nothing produces (detector without producer). (4) When the watcher is schema-blocked, periodic health checks are never started at all (daemon/cli.py:2114-2165) — the daemon is blind exactly when blocked. Also cosmetic: SIGTERM stop exits 143 so every clean stop logs \"Failed with result 'exit-code'\", training operators to ignore 'failed'. Fix: alerts must drive the verdict; heartbeat staleness threshold; wire the lag sampler; start fast health checks in schema-blocked mode; SuccessExitStatus=143.","status":"open","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:09:49Z","created_by":"Sinity","updated_at":"2026-07-31T10:09:49Z","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -134,7 +134,7 @@ {"_type":"issue","id":"polylogue-b629","title":"Leak audit L3: 17 live-archive session identifiers committed at tip","description":"AUDIT 2026-07-31 (leak-surfaces). VERDICT: EXPOSED.\n\nMethod: collected every UUID appearing in tests/, docs/ and .agent/ (47 distinct), then resolved each against the live archive read-only (SELECT origin FROM sessions WHERE native_id = ?, file:/realm/db/polylogue/index.db?mode=ro). 17 matched real sessions across codex-session, claude-code-session and chatgpt-export origins.\n\nContent at risk: identifiers only, no text. Locations include test fixtures, docs, demo evidence files and .beads records. The identifiers are deliberately NOT reproduced in the audit report or in this bead; regenerate with the query above.\n\nFix: replace with synthetic ids in tests and docs; decide separately whether the historical occurrences are worth scrubbing.\n\nReport: /realm/inbox/polylogue-audits-2026-07-31/leak-surfaces.html","status":"open","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T10:08:28Z","created_by":"Sinity","updated_at":"2026-07-31T10:08:28Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-8zzs","title":"CLI status fabricates 'FTS: 100.0% indexed' from readiness boolean when coverage_pct is null","description":"Surface-coherence audit 2026-07-31 — the live 'ops status says FTS 100% while query path says incomplete' incident, CLI-render site. polylogue/cli/commands/status.py:1273-1276: `pct = _safe_float(fts.get(\"coverage_pct\"), default=100.0 if fts.get(\"messages_ready\") else 0.0)` then prints `FTS: [green]100.0% indexed`. Live evidence: `polylogue ops status --json --full` has fts_readiness.coverage_pct=null, message_indexed_count=null, message_indexable_count=null, coverage_exact=false, surfaces.messages_fts source_rows=1 indexed_rows=1 (index.db fts_freshness_state row: detail='bounded global messages_fts repair completed; exact counts skipped') — yet the human status line asserts the precise measured-looking claim \"FTS: 100.0% indexed\" fabricated from the messages_ready boolean. Same snapshot: component_readiness.search.counts all None, search.collection.state=stale. Sibling of polylogue-oitx (daemon/fts_status.py fabricated coverage class — filed by the 2026-07-31 silent-degradation audit); this bead covers the CLI presentation layer: when coverage_pct is null/not measured, render 'structurally ready (coverage not measured)' or similar — never a fabricated percentage.\n","status":"closed","priority":1,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T08:40:26Z","created_by":"Sinity","updated_at":"2026-07-31T21:11:02Z","closed_at":"2026-07-31T21:11:02Z","close_reason":"Duplicate/superseded: fixed by PR #3429 (eb5796f49, merged 2026-07-31), which landed under tracking id polylogue-roax (also closed). status.py:1327-1336 now prints 'coverage unknown' instead of fabricating 100% when fts.coverage_pct is None. Verified via /realm/tmp/bead-audit verify-first triage 2026-07-31.","labels":["cli","surface-coherence"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-hnl7","title":"MCP query tool silently drops origin/tag/repo/since/until/sort for default projection","description":"Surface-coherence audit 2026-07-31 (live archive, in-process build_server()). MCP `query`'s input schema accepts origin/tag/repo/since/until/sort, but the default (query_units) projection path passes only (expression, limit, continuation) — polylogue/mcp/server_cutover.py ~L620-630: `hooks.get_polylogue().query_units(expression, limit=limit, continuation=continuation)`. Live repro: query(expression='messages where role:user | count', origin='claude-code-session') -\u003e count=208055, which is the ALL-origin count (SQL `select count(*) from messages where role='user'` = 208055; claude-code-session alone = 141646 via sessions.user_message_count rollup and via join). CLI with the same root filter returns the correct 141646 (`polylogue --origin claude-code-session --json find 'messages where role:user | count'`). MCP also accepts origin='bogus-origin' without error (returns the unfiltered aggregate) where CLI raises UsageError listing valid origins. Filters ARE honored for projection='sessions' and insight projections — only the default unit-query path drops them. Fix: lower the args into the unit expression, or reject the combination loudly (invalid_argument) the way continuation is rejected for other projections. An agent surface silently returning wrong-scope numbers is the worst MCP failure shape.\n","notes":"Fixed via PR #3445 (fix(mcp): repair query-filter, facet-default, and prompt-tool integrity), commit bb800174a. query()'s default projection now forwards origin/tag/repo/since/until/min_messages/max_messages/min_words to query_units. Unrecognised origin now rejected (invalid_argument, against core.sources.CORE_SCHEMA_ORIGINS). sort on default projection now rejected loudly instead of silently ignored. New test tests/unit/mcp/test_query_default_projection_filters.py (3 tests). Live-archive verified: origin=claude-code-session -\u003e 141652 (CLI parity, was 208061 whole-archive before fix); origin=bogus-origin -\u003e invalid_argument.","status":"closed","priority":1,"issue_type":"task","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T08:40:24Z","created_by":"Sinity","updated_at":"2026-07-31T21:17:51Z","started_at":"2026-07-31T09:26:28Z","closed_at":"2026-07-31T21:17:51Z","close_reason":"Fixed via PR #3445 (a8f74103e): MCP query() default projection now forwards origin/tag/repo/since/until filters and rejects unknown origin/sort loudly; regression tests on master.","labels":["mcp","surface-coherence"],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"polylogue-i415","title":"Silent parse loss: 11 codex rollouts (up to 3.3MB, mostly 2025-10/11 era) parsed to zero messages despite real content","description":"Forensics 2026-07-31. 17 codex-session rows have message_count=0; 11 of them have blob_size 19KB-3.3MB. Verified sample rollout 0199fada-d8bd-7fc0-997b-d23d3a6849c7 (3.3MB, 2025-10-19): jq type histogram = 1543 event_msg + 1496 response_item (incl 55 message, 440 function_call+440 outputs, 44 custom_tool_call pairs, 473 reasoning) + 513 turn_context — archive shows ZERO messages. This is silent data loss for old-format rollouts, not 'genuinely empty'. 9/11 are 2025-10..11 native ids; 2 are 2026-07-17. The other 6 empties are legit (single session_meta record, blob \u003c=5KB).\nRepro: ATTACH index.db from source.db side or join; SELECT s.native_id, r.blob_size FROM sessions s JOIN raw_sessions r ON r.raw_id=s.raw_id WHERE s.origin='codex-session' AND s.message_count=0 ORDER BY r.blob_size DESC;\nAC: parser handles the old rollout envelope (or a dated schema variant is added), the 11 sessions re-parse with non-zero messages, and a fixture from a synthesized old-format rollout protects it.","status":"in_progress","priority":1,"issue_type":"bug","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T08:20:22Z","created_by":"Sinity","updated_at":"2026-08-01T17:31:14Z","started_at":"2026-08-01T17:31:14Z","lease_expires_at":"2026-08-01T17:36:14Z","heartbeat_at":"2026-08-01T17:31:14Z","comments":[{"id":"019fb743-7795-756b-a460-10373103be45","issue_id":"polylogue-i415","author":"Sinity","text":"Code trace (audit): HEAD still parses these to zero. codex.py looks_like (1944-1970) accepts state-record-dominated files; _parse_records emits messages only for _message_record shapes (385-391) and drops role-less/text-less records (2344-2347); session_meta/turn_context/world_state/compacted only ever emit events — compacted deliberately does not re-parse replacement_history. Related: polylogue-dhil (whale anatomy, open); f969cf93b pins only the multi-session_meta case. NOTE: sampled file has 55 response_item payload.type='message' records that still produced 0 messages — the old-envelope inner shape apparently fails _message_record; a fixture from that exact era file is the AC.","created_at":"2026-07-31T08:21:20Z"},{"id":"019fb7b8-5707-76ed-b7fa-c380803f5447","issue_id":"polylogue-i415","author":"Sinity","text":"Investigated for PR #3441. Re-parsed the exact archived raw bytes (verified identical to the live on-disk rollout files via blob_size match) for all 17 codex-session rows with message_count=0 using the CURRENT (unmodified) codex.py: 11 now produce real non-trivial message counts (3 to 1073 messages, e.g. 1023 for the 3.3MB 0199fada-... sample cited in this bead's forensic note), confirming this is a stale-materialization issue from an older parser version, not a live parser defect -- the operator's planned index rebuild will resolve these 11 with no code change. The remaining 6 are genuinely near-empty stubs (\u003c=5KB, 1-4 records), matching this bead's own classification. Added a regression fixture (tests/unit/sources/test_silent_ingest_loss.py::test_codex_dense_reasoning_and_tool_call_rollout_yields_messages) built from real record shapes (prose redacted) so this shape cannot silently regress. No codex.py change needed or made.","created_at":"2026-07-31T10:28:59Z"},{"id":"6a3ca1cb-a0ae-5e8f-83ac-01e4753a0eea","issue_id":"polylogue-i415","author":"Sinity","text":"Investigated jointly with polylogue-buq8/polylogue-lkos. The 'old rollout envelope the parser can't handle' theory does not hold: direct reproduction of parse_stream_payload against the exact live raw bytes of the flagship sample (native_id 0199fada-d8bd-7fc0-997b-d23d3a6849c7, 3.3MB) extracts 1,496 real messages with the CURRENT parser -- no parser fix needed in polylogue/sources/. The actual defect is one layer downstream: raw_revision_heads.accepted_raw_id for this and the other 10 affected sessions equals their own sessions.raw_id, but decided_at_ms (the governance decision) is months after sessions.updated_at_ms (the original write) -- a bookkeeping-only backfill declared the raw authoritative without re-triggering message extraction, and revision_authority_refuses_write's unconditional 'governed' check then refused every future write for that session_id, including the accepted raw's own corrective rewrite. Fixed in PR #3527. AC as originally framed ('parser handles the old rollout envelope, fixture protects it') is misframed -- no parser change was needed or made; closing via the write-gate fix instead, with a regression test in test_ingest_batch.py rather than a codex-parser fixture.","created_at":"2026-08-01T17:31:46Z"}],"dependency_count":0,"dependent_count":0,"comment_count":3} +{"_type":"issue","id":"polylogue-i415","title":"Silent parse loss: 11 codex rollouts (up to 3.3MB, mostly 2025-10/11 era) parsed to zero messages despite real content","description":"Forensics 2026-07-31. 17 codex-session rows have message_count=0; 11 of them have blob_size 19KB-3.3MB. Verified sample rollout 0199fada-d8bd-7fc0-997b-d23d3a6849c7 (3.3MB, 2025-10-19): jq type histogram = 1543 event_msg + 1496 response_item (incl 55 message, 440 function_call+440 outputs, 44 custom_tool_call pairs, 473 reasoning) + 513 turn_context — archive shows ZERO messages. This is silent data loss for old-format rollouts, not 'genuinely empty'. 9/11 are 2025-10..11 native ids; 2 are 2026-07-17. The other 6 empties are legit (single session_meta record, blob \u003c=5KB).\nRepro: ATTACH index.db from source.db side or join; SELECT s.native_id, r.blob_size FROM sessions s JOIN raw_sessions r ON r.raw_id=s.raw_id WHERE s.origin='codex-session' AND s.message_count=0 ORDER BY r.blob_size DESC;\nAC: parser handles the old rollout envelope (or a dated schema variant is added), the 11 sessions re-parse with non-zero messages, and a fixture from a synthesized old-format rollout protects it.","status":"open","priority":1,"issue_type":"bug","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T08:20:22Z","created_by":"Sinity","updated_at":"2026-08-01T17:32:39Z","started_at":"2026-08-01T17:31:14Z","comments":[{"id":"019fb743-7795-756b-a460-10373103be45","issue_id":"polylogue-i415","author":"Sinity","text":"Code trace (audit): HEAD still parses these to zero. codex.py looks_like (1944-1970) accepts state-record-dominated files; _parse_records emits messages only for _message_record shapes (385-391) and drops role-less/text-less records (2344-2347); session_meta/turn_context/world_state/compacted only ever emit events — compacted deliberately does not re-parse replacement_history. Related: polylogue-dhil (whale anatomy, open); f969cf93b pins only the multi-session_meta case. NOTE: sampled file has 55 response_item payload.type='message' records that still produced 0 messages — the old-envelope inner shape apparently fails _message_record; a fixture from that exact era file is the AC.","created_at":"2026-07-31T08:21:20Z"},{"id":"019fb7b8-5707-76ed-b7fa-c380803f5447","issue_id":"polylogue-i415","author":"Sinity","text":"Investigated for PR #3441. Re-parsed the exact archived raw bytes (verified identical to the live on-disk rollout files via blob_size match) for all 17 codex-session rows with message_count=0 using the CURRENT (unmodified) codex.py: 11 now produce real non-trivial message counts (3 to 1073 messages, e.g. 1023 for the 3.3MB 0199fada-... sample cited in this bead's forensic note), confirming this is a stale-materialization issue from an older parser version, not a live parser defect -- the operator's planned index rebuild will resolve these 11 with no code change. The remaining 6 are genuinely near-empty stubs (\u003c=5KB, 1-4 records), matching this bead's own classification. Added a regression fixture (tests/unit/sources/test_silent_ingest_loss.py::test_codex_dense_reasoning_and_tool_call_rollout_yields_messages) built from real record shapes (prose redacted) so this shape cannot silently regress. No codex.py change needed or made.","created_at":"2026-07-31T10:28:59Z"},{"id":"6a3ca1cb-a0ae-5e8f-83ac-01e4753a0eea","issue_id":"polylogue-i415","author":"Sinity","text":"Investigated jointly with polylogue-buq8/polylogue-lkos. The 'old rollout envelope the parser can't handle' theory does not hold: direct reproduction of parse_stream_payload against the exact live raw bytes of the flagship sample (native_id 0199fada-d8bd-7fc0-997b-d23d3a6849c7, 3.3MB) extracts 1,496 real messages with the CURRENT parser -- no parser fix needed in polylogue/sources/. The actual defect is one layer downstream: raw_revision_heads.accepted_raw_id for this and the other 10 affected sessions equals their own sessions.raw_id, but decided_at_ms (the governance decision) is months after sessions.updated_at_ms (the original write) -- a bookkeeping-only backfill declared the raw authoritative without re-triggering message extraction, and revision_authority_refuses_write's unconditional 'governed' check then refused every future write for that session_id, including the accepted raw's own corrective rewrite. Fixed in PR #3527. AC as originally framed ('parser handles the old rollout envelope, fixture protects it') is misframed -- no parser change was needed or made; closing via the write-gate fix instead, with a regression test in test_ingest_batch.py rather than a codex-parser fixture.","created_at":"2026-08-01T17:31:46Z"}],"dependency_count":0,"dependent_count":0,"comment_count":3} {"_type":"issue","id":"polylogue-shnc","title":"Cost accounting: 100% of codex session_model_usage unpriced; 5,016 rows claim provenance='priced' with NULL cost/catalog; all 3,417 origin_reported rows carry no value","description":"Forensics 2026-07-31, live index.db (verifies and extends the existing NULL-cost report):\n- ALL 3,153 codex-session session_model_usage rows have cost_usd NULL and priced_with NULL (gpt-5.5 617, gpt-5.4 531, gpt-5-codex 454, gpt-5.3-codex 405, gpt-5.6-sol 313, gpt-5.6-terra 306, ...) despite the vendored LiteLLM catalog nominally covering gpt-5.x. Only 1 price_catalogs row is loaded.\n- Contradictory state: cost_provenance='priced' but cost_usd IS NULL AND priced_with IS NULL on 5,016 rows (70.1M tokens). 'priced' with no catalog and no price is a semantic lie; the other 10,222 priced rows are consistent.\n- cost_provenance='origin_reported' has cost_usd NULL on 3,417/3,417 rows (7.58B tokens) — the label exists but the origin-reported value was never stored.\n- claude-code NULLs: \u003csynthetic\u003e 1,140 (fine) + claude-sonnet-5 705 (catalog gap) + 7 misc.\n- session_provider_usage_events: 4,002,046 rows, estimated_cost_usd populated on 103, actual_cost_usd on 0.\nRepro: SELECT cost_provenance, cost_usd IS NULL, priced_with IS NULL, count(*) FROM session_model_usage GROUP BY 1,2,3;\nAC: pricing pass covers codex models + claude-sonnet-5; provenance constraint (priced =\u003e cost_usd AND priced_with NOT NULL; origin_reported =\u003e cost_usd NOT NULL) enforced or the states renamed honestly; re-materialization backfills existing rows.","notes":"RECONCILE 2026-07-31: returned to open (stale in_progress, no assignee). Write-path fix merged in PR #3446 (ed17421f7); remaining work is re-materialization to backfill existing unpriced/contradictory rows on the live archive — an ops run, not code.","status":"open","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T08:20:22Z","created_by":"Sinity","updated_at":"2026-07-31T21:19:28Z","started_at":"2026-07-31T11:02:39Z","comments":[{"id":"019fb743-7112-71ce-a091-fc8a3ff2c669","issue_id":"polylogue-shnc","author":"Sinity","text":"Code trace (audit): NOT a catalog gap — gpt-5.5/5.4/5-codex ARE in vendored litellm_model_prices.json. Root cause in storage/sqlite/archive_tiers/write.py: (a) _upsert/_increment_provider_usage_model_rollup (~3721-3794) hardcode cost_provenance='origin_reported' AND cost_usd=NULL/priced_with=NULL — pricing never attempted on the Codex cumulative-rollup path; (b) _aggregate_message_tokens_into_model_usage (~3847-3964), the only pricer, has a WHERE NOT guard (~3942-3950) refusing to overwrite origin_reported rows with nonzero tokens — structurally barred from pricing Codex; (c) the 'priced' label is written unconditionally by the INSERT literal even when 'normalized in PRICING and billable\u003e0' is false — hence 5,016 priced-with-NULL rows. 'origin_reported' means token provenance, not that a cost exists (session_reported_costs table was dropped in polylogue-v2mg).","created_at":"2026-07-31T08:21:18Z"},{"id":"019fb7d7-7ad8-7dae-ade1-29a6a254ef45","issue_id":"polylogue-shnc","author":"Sinity","text":"PR #3446 fixes the write-path root cause (Codex rollup writer + message-aggregator provenance bug) and adds enforcing CHECK constraints. Re-materialization (polylogue ops reset --index) still needed to backfill existing rows on the live archive.","created_at":"2026-07-31T11:03:00Z"}],"dependency_count":0,"dependent_count":0,"comment_count":2} {"_type":"issue","id":"polylogue-f5tq","title":"Untested shipped defaults: _archive_facet_buckets(include_deferred=True) plus a 17-item sweep","description":"FALSE-GREEN AUDIT 2026-07-31 (findings F4 + F13). Generalises the correlation_view github_api defect.\n\nTHE SEED DEFECT'S SHAPE: run_correlation_view(github_api=True) at\npolylogue/insights/correlation_view.py:14 shipped a NameError on its DEFAULT path because\nevery test (tests/unit/cli/test_correlate_view.py:60,80,90) passed github_api=False.\n\nI built an AST sweep to find the class mechanically: walk every polylogue/ function with a\nboolean default param, walk every tests/ call site, and flag params where the DEFAULT value is\nnever passed and never omitted while the opposite value IS passed.\n 388 production functions carry bool defaults\n 265 of them are called from tests\n 17 have a default that is never exercised\n 2 of those 17 have default=True (i.e. the SHIPPED behaviour is the untested one)\nThe sweep rediscovered run_correlation_view without being told it existed -- that is the\ncalibration proving it detects the class.\n\nNEW FINDING, the second default=True case:\n polylogue/api/archive.py:735 _archive_facet_buckets(..., include_deferred: bool = True)\n tests/unit/api/test_facade_contracts.py:738 is the only test, and passes include_deferred=False.\n The False branch returns HARD-CODED EMPTY DICTS for repos/role_counts/material_origins/\n message_types/action_types/has_flags. The True branch (the shipped default) calls\n _archive_aggregate_facet_families(archive._conn, ...) and does all the real SQL work.\n The test constructs its archive stub with _conn=None -- so it STRUCTURALLY CANNOT exercise\n the default; passing True would crash on the None connection.\n Production callers at api/archive.py:4771-4774 all forward an operator-supplied\n include_deferred, so the default path is live in real use.\n\nThe remaining 15 are default=False with tests passing only True (force, detail,\nrequire_overlays, exclude_none, include_rows, ...). Lower risk -- the untested default is\nusually the inert path -- but each is an untested shipped default and worth a triage pass.\n\nA mirror sweep found 235 flags never passed explicitly by ANY test (the non-default branch\nuntested). That list is noisy: matching is by bare function name, so generic names (list,\ncount, to_payload, model_copy) collide across classes. Treat it as a candidate pool.\n\nAC:\n- A test exercises _archive_facet_buckets with include_deferred=True against a real\n connection, asserting the SQL facet families are populated.\n- The 17-item list is triaged: each either gets default-path coverage or a recorded reason\n the default is not worth testing.\n- Consider whether this sweep is worth a devtools lab policy check. NOTE the operator's\n standing 'no completeness-check theater' rule: only add the gate if the known debt is\n migrated first, not as a substitute for migrating it.","notes":"Fixed via PR #3445, commit a9eed07fd. Added test_archive_facet_buckets_include_deferred_default_populates_sql_families exercising _archive_facet_buckets(include_deferred=True) against a real seeded ArchiveStore connection; asserts role_counts/message_types are SQL-populated, not the include_deferred=False branch's hardcoded empty dicts. Anti-vacuity verified: inverting the include_deferred branch condition makes both facet-bucket tests fail (AttributeError on None conn / AssertionError on empty role_counts). Triaged the remaining ~15-item sweep in the commit body rather than adding 15 individual tests: reproduced the AST sweep locally and confirmed it is structurally noisy exactly as the bead's own text warns -- it false-flags storage/blob_gc.py's run_blob_gc(dry_run=False) as untested even though a dozen tests exercise that default by omitting the kwarg. Spot-checked the bead-named examples (exclude_none, detail, require_overlays, include_rows) and found cosmetic serialization/reporting-detail toggles, not a second confirmed defect. No devtools lab policy gate added, per operator's standing no-completeness-check-theater rule -- this pass did not surface a second migratable defect to justify one.","status":"closed","priority":1,"issue_type":"bug","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T08:20:16Z","created_by":"Sinity","updated_at":"2026-07-31T21:17:51Z","started_at":"2026-07-31T09:26:29Z","closed_at":"2026-07-31T21:17:51Z","close_reason":"Fixed via PR #3445 (a9eed07fd): real-connection test exercising _archive_facet_buckets(include_deferred=True); 17-item bool-default sweep triaged without completeness-theater lint.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-eo81","title":"Antigravity origin inverted: 116 metadata sidecars ingested as sessions; all 44 real conversations (314MB .pb) never acquired","description":"Forensics 2026-07-31. Every antigravity-session row (116/116) is a 1-message session materialized from ~/.gemini/antigravity/brain/\u003cuuid\u003e/*.md.metadata.json — artifact metadata, not conversations (producer stopped 2026-07-18; 232 raws, 116 sessions). Meanwhile ~/.gemini/antigravity/conversations/ holds 44 real conversation .pb files (314MB) and raw_sessions/raw_artifacts contain ZERO rows for that directory: the actual conversations were never acquired. The origin is 100% noise, 0% signal.\nRepro: SELECT count(*) FROM raw_sessions WHERE source_path LIKE '%antigravity/conversations%'; -- 0\nAC: (1) purge/reclassify the 116 metadata sessions; (2) decide+implement .pb conversation acquisition (or explicitly document the format as out of scope with the gap tracked); (3) metadata.json becomes sidecar artifact kind.","notes":"2026-07-31 acquisition-completeness audit cross-check (report: /realm/inbox/polylogue-audits-2026-07-31/acquisition-completeness.html): full-tree recount across BOTH roots (~/.gemini/antigravity + antigravity-cli) = 55 .pb files / 339,774,849 bytes with zero raw_sessions/raw_artifacts rows (this bead's 44/314MB was the conversations dir of one root). Sidecar rows: 232 raw rows over 116 distinct *.md.metadata.json paths, 61KB total = 0.008% of antigravity's 383.6MB captured. All 114 antigravity ingest cursors excluded=1 failure_count=5 since the 2026-07-18 bulk give-up incident. Dormant: newest mtime under either root is 2026-07-16 - static residue, not an active drip. STAGE-1: index rebuild recovers none of it.\nRECONCILE 2026-07-31: unclaimed (stale claim). Acquisition fix merged (PR #3441/7b4f881d0); retroactive purge of 116 phantom sessions + live redeploy remain. Note polylogue-msia covers the same shape — consolidate before provisioning a lane.","status":"open","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T08:19:56Z","created_by":"Sinity","updated_at":"2026-07-31T21:18:32Z","comments":[{"id":"019fb743-7558-7b63-a3f0-f099cd82dded","issue_id":"polylogue-eo81","author":"Sinity","text":"Code trace (audit): parse_brain_metadata (sources/parsers/antigravity.py:245-288) documents the 1-session-per-metadata-file shape as a DELIBERATE tagged compromise — sessions carry flag 'degraded:brain-metadata-fragment' meant to exclude them from primary counts; tracked upstream as GH issue #1764. Still wired unconditionally at HEAD (dispatch.py:1080,1207). The .pb conversations gap (44 files / 314MB, zero raw rows) is the part with no tracking at all.","created_at":"2026-07-31T08:21:19Z"},{"id":"019fb7b8-5162-7672-924a-b0e1f650371e","issue_id":"polylogue-eo81","author":"Sinity","text":"Fixed acquisition half in PR #3441 (branch feature/sources/antigravity-conversation-acquisition): the language-server export path was gated on a nonexistent 'sessions/' dir (real dir is 'conversations/') and cascade discovery relied on SearchConversations, which only surfaces ~10/44 real conversations -- switched to disk-truth glob of conversations/*.pb, still enriching metadata from search when available. Verified against real ~/.gemini/antigravity data: exported a cascade absent from SearchConversations directly via ConvertTrajectoryToMarkdown (82KB real markdown), and ran the fixed iter_source_sessions_with_raw end-to-end producing all 44 sessions / 44 raw blob snapshots / 2162 messages into a scratch blob store (no live-archive writes). Also reclassified *.md.metadata.json as a non-session sidecar (AGENT_SIDECAR_META) in the generic walk so future ingest stops fragmenting brain metadata into noise sessions -- parse_brain_metadata remains wired as an explicit fallback only when the language server truly cannot be reached. NOT done: retroactive purge/reclassification of the existing 116 already-materialized fragment sessions (deletion-adjacent, deliberately left for a separate follow-up); live-archive acquisition itself, since polylogued.service runs a separately-deployed Nix package that won't pick up this fix until merge+redeploy -- see PR body for the exact operator action needed.","created_at":"2026-07-31T10:28:58Z"}],"dependency_count":0,"dependent_count":0,"comment_count":2} @@ -558,7 +558,7 @@ {"_type":"issue","id":"polylogue-gvr2","title":"delegation_facts is a TABLE despite its own code comment saying it should be a VIEW (100% derivable, matching actions precedent)","description":"Split from polylogue-cuxz.9 (verified 2026-08-01): polylogue/storage/sqlite/archive_tiers/index.py:1771 defines delegation_facts_source as a VIEW explicitly commented '100% derivable from existing tables -- VIEW, not a table, matching the actions precedent' -- then the very next statement (:1656) creates delegation_facts as a TABLE anyway. The view's WHERE clause is gated on membership in delegation_refresh_scope, a mutable side-table callers must populate before querying (delegation_facts.py:34-39) and always empty in steady state -- querying delegation_facts_source directly returns 0 rows (confirmed live). Needs a real design decision, not a mechanical fix: either (a) make the view self-standing/indexed well enough to query directly at actions-view cost (converting the table to a true view, matching the code comment's stated intent), or (b) document the scope-table gating as an intentional query-parameterization idiom and keep the table, updating the misleading comment. Ref polylogue-cuxz.9.","status":"open","priority":2,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-08-01T11:34:41Z","created_by":"Sinity","updated_at":"2026-08-01T11:34:41Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-20c8","title":"master-red: material-protocol origin-vocab drift latch (fixed via PR #3500)","description":"PR #3422 (2026-07-31) added claude-design-session to core.enums.Origin without bumping CURRENT_ORIGIN_VOCABULARY_VERSION, tripping origin_vocab.py's deliberate drift latch on every encode — failed all 22 material_protocol/v1 tests plus downstream encoders since 2026-07-31 10:23, invisible to per-PR CI's heavy-test skip. Fixed same-day via PR #3500 (version 2-\u003e3, frozen digest, fixture regenerated). Filed retroactively as a durable record of the incident and root cause for future origin-enum changes: bumping Origin members requires bumping CURRENT_ORIGIN_VOCABULARY_VERSION in the same PR.","status":"closed","priority":2,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-08-01T09:01:57Z","created_by":"Sinity","updated_at":"2026-08-01T09:02:22Z","closed_at":"2026-08-01T09:02:22Z","close_reason":"Fixed same-day via merged PR #3500 (origin_vocab.py version 2-\u003e3 + frozen digest + fixture regen). Retroactive record only.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-d4kq","title":"Refusal path cannot feed claude_parse_coverage: no session-scoped sink for refused records","description":"Residual from PR #3497 (polylogue-9ykn): the new positive-evidence refusal path records reasons but cannot feed the claude_parse_coverage event family because no session-scoped sink exists for records that never become sessions. Design where refused-record coverage evidence lives (source-tier taxonomy row? ops-tier event?) so the after-the-fact detector (PR #3419) and the refusal gate share one coverage picture. Ref polylogue-9ykn.","status":"open","priority":2,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T22:54:42Z","created_by":"Sinity","updated_at":"2026-07-31T22:54:42Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"polylogue-lkos","title":"Codex message-extraction defect: 2 large sessions materialize zero messages","description":"Residual from PR #3497's empty-session reconciliation (polylogue-9ykn): 2 large codex-session rows materialize with zero messages despite substantial raw content — a message-extraction defect in the codex parse path, distinct from the envelope-only/sidecar classes the gate now refuses. Identify the two sessions from the reconciliation evidence in PR #3497, reproduce extraction against their raws, fix the extraction gap.","status":"in_progress","priority":2,"issue_type":"bug","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T22:54:42Z","created_by":"Sinity","updated_at":"2026-08-01T17:31:14Z","started_at":"2026-08-01T17:31:14Z","lease_expires_at":"2026-08-01T17:36:14Z","heartbeat_at":"2026-08-01T17:31:14Z","comments":[{"id":"a8df4338-b418-5b79-998d-d9b02fbee465","issue_id":"polylogue-lkos","author":"Sinity","text":"Investigated jointly with polylogue-buq8/polylogue-i415. Identified the '2 large sessions' from PR #3497's residual note (996KB/1.4MB raw blobs) -- both are among the same 11-session set buq8/i415 already flag, not a separate/distinct defect. This is NOT a message-extraction defect in the codex parser: direct reproduction of the current parse_stream_payload against live raw bytes for the largest sample in this cluster extracts real messages correctly. The actual mechanism: raw_revision_heads.accepted_raw_id for each of these sessions equals its own sessions.raw_id, decided by a bookkeeping-only backfill pass that ran months after the session's original (defective) write, without re-running message extraction. revision_authority_refuses_write's unconditional 'governed' check then permanently refused any corrective rewrite for that raw. Fixed in PR #3527 (accepted_raw_id comparison added to the shared write-refusal gate). All three of buq8/i415/lkos describe one root cause, not three; none of the three beads' original framing ('materialization never runs' / 'old rollout envelope' / 'distinct message-extraction defect') is what the live data actually shows.","created_at":"2026-08-01T17:31:55Z"}],"dependency_count":0,"dependent_count":0,"comment_count":1} +{"_type":"issue","id":"polylogue-lkos","title":"Codex message-extraction defect: 2 large sessions materialize zero messages","description":"Residual from PR #3497's empty-session reconciliation (polylogue-9ykn): 2 large codex-session rows materialize with zero messages despite substantial raw content — a message-extraction defect in the codex parse path, distinct from the envelope-only/sidecar classes the gate now refuses. Identify the two sessions from the reconciliation evidence in PR #3497, reproduce extraction against their raws, fix the extraction gap.","status":"open","priority":2,"issue_type":"bug","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T22:54:42Z","created_by":"Sinity","updated_at":"2026-08-01T17:32:39Z","started_at":"2026-08-01T17:31:14Z","comments":[{"id":"a8df4338-b418-5b79-998d-d9b02fbee465","issue_id":"polylogue-lkos","author":"Sinity","text":"Investigated jointly with polylogue-buq8/polylogue-i415. Identified the '2 large sessions' from PR #3497's residual note (996KB/1.4MB raw blobs) -- both are among the same 11-session set buq8/i415 already flag, not a separate/distinct defect. This is NOT a message-extraction defect in the codex parser: direct reproduction of the current parse_stream_payload against live raw bytes for the largest sample in this cluster extracts real messages correctly. The actual mechanism: raw_revision_heads.accepted_raw_id for each of these sessions equals its own sessions.raw_id, decided by a bookkeeping-only backfill pass that ran months after the session's original (defective) write, without re-running message extraction. revision_authority_refuses_write's unconditional 'governed' check then permanently refused any corrective rewrite for that raw. Fixed in PR #3527 (accepted_raw_id comparison added to the shared write-refusal gate). All three of buq8/i415/lkos describe one root cause, not three; none of the three beads' original framing ('materialization never runs' / 'old rollout envelope' / 'distinct message-extraction defect') is what the live data actually shows.","created_at":"2026-08-01T17:31:55Z"}],"dependency_count":0,"dependent_count":0,"comment_count":1} {"_type":"issue","id":"polylogue-jtek","title":"rebuild perf: overlap the up-front census classification pass with replay","description":"Residual from polylogue-2cuv's close (PR #3496): the spill_load decode is now fully pipelined behind apply (#3478 _ReplaySpillPrefetcher, 72.7% of the old serial spill_load measured concurrent), but the up-front census classification pass still strictly precedes the replay loop — cohort membership must resolve before cohorts can be ordered, so overlapping census-page N+1 with replay-page N's apply needs a materially larger redesign (paged census with incremental cohort release). Worth doing only if the next real rebuild receipt shows census as a dominant remaining phase (selection_s + census timings now measured via #3494/#3469). Ref polylogue-2cuv polylogue-o56w.","status":"open","priority":2,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T22:43:38Z","created_by":"Sinity","updated_at":"2026-07-31T22:43:38Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-s0ug","title":"test_incremental_restart_and_fresh_generation_rebuild_are_equivalent fails after #3484 CONTINUATION lineage fix","description":"Discovered while rebasing perf/rebuild-deadline-and-phase-receipts (polylogue-uhgm/polylogue-6mvg) onto master past commit 0d27e3da7 (fix(sources): require structural evidence for codex CONTINUATION lineage, #3484). tests/unit/storage/test_incremental_rebuild_equivalence.py::test_incremental_restart_and_fresh_generation_rebuild_are_equivalent fails deterministically in isolation on origin/master HEAD (90d4df67e): assertion at line ~600 compares session_links rows and finds the synthetic lineage-child raw no longer carries a ('codex-session:lineage-parent', 'continuation') link -- exactly the shape #3484 tightened (a bare second session_meta record is no longer sufficient evidence for CONTINUATION; the parser now requires structural evidence such as forked_from_id). The test's hand-built Codex payload fixture (_codex_session helper, parent_native_id kwarg) predates that tightening and needs updating to supply the new required structural marker. Confirmed unrelated to polylogue-uhgm/polylogue-6mvg's changes: git diff origin/master...HEAD for that branch touches neither codex.py, revision_backfill.py's classification logic, nor this test file.","status":"closed","priority":2,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T22:36:26Z","created_by":"Sinity","updated_at":"2026-07-31T22:53:54Z","closed_at":"2026-07-31T22:53:54Z","close_reason":"Merged PR #3498 (fixture-side): the equivalence test's synthetic _codex_session predated #3484's evidence tightening and never set cwd/git on its session_metas; empirical validation over 494 real multi-meta rollouts (99.2% share cwd, 100% timestamp-consistent, the 4 exceptions genuinely unrelated) confirms the production rule is correct. Fixture now models real continuation structure; #3484's negative case still passes.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-c379","title":"HTTP _archive_filter_kwargs_from_spec missing root key breaks test_archive_filter_kwargs_cover_every_storage_lowerable_spec_field","description":"Discovered as a follow-up during 2026-07-31 verify-first triage of polylogue-bsi7 (which described a different, no-longer-reproducing order-dependent failure in the same test file). The real, deterministic (order-independent) failure is: tests/unit/daemon/test_web_reader.py::test_archive_filter_kwargs_cover_every_storage_lowerable_spec_field fails because polylogue/daemon/http.py:635-653 (_archive_filter_kwargs_from_spec) is missing a 'root' key that all four ArchiveStore query methods (count_sessions, list_summaries, search_summaries, count_search_sessions) now accept (confirmed via inspect.signature). This appears to date from commit 7f494b8d5 ('finish typed session-PR evidence + wire root: filter'), which wired 'root:' into the storage layer but not into the HTTP kwarg builder. Fix: add 'root' to the kwarg dict built by _archive_filter_kwargs_from_spec so the HTTP-facing filter path stays in parity with the storage layer.","acceptance_criteria":"test_archive_filter_kwargs_cover_every_storage_lowerable_spec_field passes; _archive_filter_kwargs_from_spec includes 'root' alongside the other lowerable spec fields.","status":"open","priority":2,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T21:13:07Z","created_by":"Sinity","updated_at":"2026-07-31T21:13:07Z","dependency_count":0,"dependent_count":0,"comment_count":0} From ad8d74d96f79ce9108c8ef7ba295db9d5e935671 Mon Sep 17 00:00:00 2001 From: Sinity Date: Sat, 1 Aug 2026 19:55:35 +0200 Subject: [PATCH 4/4] fix(storage): stop selecting an arbitrary raw_revision_heads row Problem: revision_authority_refuses_write's governed-head check used `SELECT accepted_raw_id FROM raw_revision_heads WHERE session_id = ? LIMIT 1`. Historical drift or an interrupted repair can leave more than one raw_revision_heads row for a session (storage/repair.py's parallel_session_heads shape), so an incoming raw matching whichever row LIMIT 1 happened to select was allowed through even when a different parallel head accepted another raw -- and the same write could be refused or allowed depending on row insertion/scan order alone. Flagged by the CodeRabbit-equivalent bot review on PR #3527 as a P2 finding. Solution: check for the existence of any head whose accepted_raw_id differs from the incoming raw, instead of comparing against one arbitrarily selected row. Order-independent by construction. Verification: new regression test builds two parallel heads (one accepting the incoming raw, one accepting a different raw) and asserts the write is still refused; confirmed to fail on the unmodified code (git-stash-verified) and pass with the fix. devtools test tests/unit/pipeline/test_ingest_batch.py tests/unit/storage/test_raw_revision_authority.py tests/unit/storage/test_revision_application.py tests/unit/archive/test_session_revision_membership.py (134 passed); devtools verify --quick (exit 0). The review's other (P1) finding -- accepted-raw rewrites don't reissue raw_revision_applications receipts, so a later formal replay-plan validation could see a stale accepted_content_hash -- is a real gap in the authoritative-replay ledger's write path, not this narrow comparison bug. It touches both ordinary write call sites (revision_governance.py, ingest_batch/_core.py) and the receipt synthesis contract in revision_application.py; fixing it correctly needs its own scoped design rather than a same-lane addendum. Tracked as a follow-up (see PR comment). Co-Authored-By: Claude --- .../sqlite/archive_tiers/ingest_precedence.py | 17 ++++-- tests/unit/pipeline/test_ingest_batch.py | 59 +++++++++++++++++++ 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/polylogue/storage/sqlite/archive_tiers/ingest_precedence.py b/polylogue/storage/sqlite/archive_tiers/ingest_precedence.py index 20c6f99a45..398d166494 100644 --- a/polylogue/storage/sqlite/archive_tiers/ingest_precedence.py +++ b/polylogue/storage/sqlite/archive_tiers/ingest_precedence.py @@ -208,11 +208,20 @@ def revision_authority_refuses_write( "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'raw_revision_heads'" ).fetchone() if has_revision_heads is not None: - governed_row = conn.execute( - "SELECT accepted_raw_id FROM raw_revision_heads WHERE session_id = ? LIMIT 1", - (session_id,), + # Historical drift or an interrupted repair can leave more than one + # raw_revision_heads row for a session (storage/repair.py's + # ``parallel_session_heads`` shape). A bare ``LIMIT 1`` examined an + # arbitrary one of those rows, so an incoming raw matching whichever + # row happened to be selected was allowed through even when a + # different parallel head accepted another raw -- and the same write + # could be refused or allowed depending on row order alone. Checking + # for the *existence* of any head that names a different raw makes + # the refusal decision independent of row order. + conflicting_head = conn.execute( + "SELECT 1 FROM raw_revision_heads WHERE session_id = ? AND accepted_raw_id != ? LIMIT 1", + (session_id, raw_id), ).fetchone() - if governed_row is not None and str(governed_row[0]) != raw_id: + if conflicting_head is not None: return True if source_conn is None or not raw_id: return False diff --git a/tests/unit/pipeline/test_ingest_batch.py b/tests/unit/pipeline/test_ingest_batch.py index 034a367b02..a4f007a6b6 100644 --- a/tests/unit/pipeline/test_ingest_batch.py +++ b/tests/unit/pipeline/test_ingest_batch.py @@ -1902,6 +1902,65 @@ def test_write_session_still_refuses_a_different_raw_than_the_accepted_head(tmp_ assert dict(stored) == {"raw_id": "raw-winner", "message_count": 1} +def test_write_session_refuses_when_a_parallel_head_accepts_a_different_raw(tmp_path: Path) -> None: + """P2 bot finding on PR #3527: with more than one ``raw_revision_heads`` + row for the same ``session_id`` (historical drift or an interrupted + repair -- ``storage/repair.py``'s ``parallel_session_heads`` shape), the + refusal decision must not depend on which row a bare ``LIMIT 1`` + happened to select. Two heads for this session: one already accepts + the incoming raw, the other accepts a different raw -- the write must + still be refused, regardless of which row is inserted (and therefore + selected) first. + + Mutation that fails this: reverting to ``SELECT accepted_raw_id ... + LIMIT 1`` and comparing only that single arbitrary row. + """ + with open_connection(tmp_path / "index.db") as conn: + conn.execute( + "INSERT INTO raw_revision_heads (logical_source_key, session_id, accepted_raw_id, " + "accepted_source_revision, accepted_content_hash, accepted_frontier_kind, accepted_frontier, " + "acquisition_generation, decided_at_ms) VALUES " + "('codex:parallel-a','codex-session:parallel','raw-incoming','sr',?,'byte',1,0,1)", + (b"\x0b" * 32,), + ) + conn.execute( + "INSERT INTO raw_revision_heads (logical_source_key, session_id, accepted_raw_id, " + "accepted_source_revision, accepted_content_hash, accepted_frontier_kind, accepted_frontier, " + "acquisition_generation, decided_at_ms) VALUES " + "('codex:parallel-b','codex-session:parallel','raw-other','sr',?,'byte',1,0,1)", + (b"\x0c" * 32,), + ) + conn.commit() + + incoming = _session_data( + "codex-session:parallel", + content_hash="hash-incoming", + raw_id="raw-incoming", + message_tuples=[ + _message_tuple( + "msg-incoming", + "codex-session:parallel", + role="user", + text="incoming content", + content_hash="hash-incoming-msg", + sort_key=1.0, + ) + ], + ) + changed, counts = _write_session(conn, incoming) + conn.commit() + + stored = conn.execute( + "SELECT message_count FROM sessions WHERE session_id = ?", + ("codex-session:parallel",), + ).fetchone() + + assert changed is False + assert counts["skipped_sessions"] == 1 + # Refused before ever inserting: this was the session's first write attempt. + assert stored is None + + def test_write_session_refuses_a_raw_recorded_ambiguous_membership(tmp_path: Path) -> None: """``_write_session`` -- the daemon's default batch-ingest write path, used for most non-drive origins -- must refuse a session whose OWN