Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .beads/issues.jsonl

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions browser-extension/src/backfill/providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ function envelope({ provider, nativeId, title, createdAt, updatedAt, turns, rawP
provider_session_id: nativeId,
session_kind: sessionKind === "temporary" ? "temporary" : "standard",
title: title || nativeId,
title_source: title ? "provider" : "session-id",
created_at: createdAt,
updated_at: updatedAt,
provider_meta: { capture_fidelity: captureFidelity, backfill: attribution },
Expand Down
5 changes: 4 additions & 1 deletion browser-extension/src/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@
? "temporary"
: "standard";
const now = new Date().toISOString();
const sessionTitle = title || document.title || stableProviderSessionId;
const sessionTitleSource = title ? "provider" : document.title ? "page" : "session-id";
const envelope = {
polylogue_capture_kind: CAPTURE_KIND,
schema_version: SCHEMA_VERSION,
Expand All @@ -126,7 +128,8 @@
provider,
provider_session_id: stableProviderSessionId,
session_kind: stableSessionKind,
title: title || document.title || stableProviderSessionId,
title: sessionTitle,
title_source: sessionTitleSource,
created_at: createdAt,
updated_at: updatedAt || now,
model,
Expand Down
1 change: 1 addition & 0 deletions browser-extension/tests/backfill.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ describe("background backfill coordinator", () => {
expect(status.progress.complete).toBe(2);
expect(h.adapter.fetchCalls).toEqual(["one", "two"]);
expect(h.receiver).toHaveBeenCalledTimes(2);
expect(h.receiver.mock.calls[0][0].session.title_source).toBe("provider");
});

it("recovers the durable job, queue, revision, and ACK ledgers from real IndexedDB after a worker restart", async () => {
Expand Down
34 changes: 34 additions & 0 deletions browser-extension/tests/common_envelope_boundary.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,40 @@ function maximalTurn() {
}

describe("common.js buildEnvelope boundary contract (real source, not a copy)", () => {
it("types provider, page, and session-id title evidence", () => {
const providerDom = installCommon();
const providerEnvelope = providerDom.window.polylogueCapture.buildEnvelope({
provider: "chatgpt",
adapterName: "chatgpt-native-v1",
turns: [maximalTurn()],
providerSessionId: "conversation-1",
title: "Provider conversation title",
});
expect(providerEnvelope.session.title).toBe("Provider conversation title");
expect(providerEnvelope.session.title_source).toBe("provider");

const pageDom = installCommon();
const pageEnvelope = pageDom.window.polylogueCapture.buildEnvelope({
provider: "chatgpt",
adapterName: "chatgpt-dom-v1",
turns: [maximalTurn()],
providerSessionId: "conversation-1",
});
expect(pageEnvelope.session.title).toBe("Boundary contract fixture");
expect(pageEnvelope.session.title_source).toBe("page");

const sessionIdDom = installCommon();
sessionIdDom.window.document.title = "";
const sessionIdEnvelope = sessionIdDom.window.polylogueCapture.buildEnvelope({
provider: "chatgpt",
adapterName: "chatgpt-dom-v1",
turns: [maximalTurn()],
providerSessionId: "conversation-1",
});
expect(sessionIdEnvelope.session.title).toBe("conversation-1");
expect(sessionIdEnvelope.session.title_source).toBe("session-id");
});

it("carries every BrowserCaptureTurn field from a maximal turn into the envelope", () => {
const dom = installCommon();
const envelope = dom.window.polylogueCapture.buildEnvelope({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ source material:
blob_sha256: 9fd0dbdb080058070935924534a903cc63a8dcba571f6b2734f92a96576b59d7

completion-claim experiment:
sample manifest: a7a39bdcced3d950886d5b555d2ceec7b5f776d961fa726a76946c857f863d13
sample manifest: 014380e82576a22360db0b18c25a984b62fdb057e535aadd1c9b448cf40f2466
denominator: 2
unsupported by structural evidence: 0 (0.0%)
neutral prior outcome: 0 (0.0%)
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/demo-tour/transcript.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ source material:
blob_sha256: 9fd0dbdb080058070935924534a903cc63a8dcba571f6b2734f92a96576b59d7

completion-claim experiment:
sample manifest: a7a39bdcced3d950886d5b555d2ceec7b5f776d961fa726a76946c857f863d13
sample manifest: 014380e82576a22360db0b18c25a984b62fdb057e535aadd1c9b448cf40f2466
denominator: 2
unsupported by structural evidence: 0 (0.0%)
neutral prior outcome: 0 (0.0%)
Expand Down
2 changes: 2 additions & 0 deletions polylogue/browser_capture/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
BROWSER_CAPTURE_EXTENSION_ORIGIN_WILDCARD: Literal["chrome-extension://*"] = "chrome-extension://*"
BrowserCaptureArchiveLifecycle = Literal["missing", "spooled_only", "ingest_pending", "archived", "stale", "failed"]
BrowserCaptureSessionKind = Literal["standard", "temporary"]
BrowserCaptureTitleSource = Literal["provider", "page", "session-id"]


class BrowserCaptureAttachment(BaseModel):
Expand Down Expand Up @@ -168,6 +169,7 @@ class BrowserCaptureSession(BaseModel):
provider_session_id: str
session_kind: BrowserCaptureSessionKind = "standard"
title: str | None = None
title_source: BrowserCaptureTitleSource | None = None
created_at: str | None = None
updated_at: str | None = None
model: str | None = None
Expand Down
3 changes: 2 additions & 1 deletion polylogue/sources/parsers/antigravity.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from polylogue.archive.message.artifacts import classify_material_origin
from polylogue.archive.message.roles import Role
from polylogue.archive.message.types import MessageType
from polylogue.core.enums import BlockType, Provider
from polylogue.core.enums import BlockType, Provider, TitleSource
from polylogue.core.json import JSONDocument, dumps_bytes, loads

from .base import (
Expand Down Expand Up @@ -308,6 +308,7 @@ def parse_markdown_export(
source_name=Provider.ANTIGRAVITY,
provider_session_id=summary.cascade_id,
title=summary.title,
title_source=TitleSource.ORIGIN if summary.title else None,
created_at=None,
updated_at=summary.last_modified_time,
messages=messages,
Expand Down
43 changes: 38 additions & 5 deletions polylogue/sources/parsers/browser_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
BrowserCaptureTurn,
looks_like_browser_capture,
)
from polylogue.core.enums import BlockType, Provider, SessionKind
from polylogue.core.enums import BlockType, Provider, SessionKind, TitleSource
from polylogue.core.timestamps import parse_timestamp
from polylogue.sources.parsers.base_models import (
ParsedAttachment,
Expand Down Expand Up @@ -329,6 +329,34 @@ def _merge_envelope_attachments(parsed: ParsedSession, envelope: BrowserCaptureE
return parsed.model_copy(update={"attachments": list(merged.values())})


def _trusted_envelope_title(envelope: BrowserCaptureEnvelope) -> str | None:
"""Return only a provider-authored browser title.

Current producers declare the source explicitly. Older v1 envelopes are
accepted conservatively only when their title differs from both fallback
values the extension is known to synthesize.
"""
title = envelope.session.title
if not title:
return None
if envelope.session.title_source == "provider":
return title
if envelope.session.title_source in {"page", "session-id"}:
return None
if title in {envelope.provenance.page_title, envelope.session.provider_session_id}:
return None
return title


def _merge_envelope_title(parsed: ParsedSession, envelope: BrowserCaptureEnvelope) -> ParsedSession:
"""Fill only a missing native title from trusted envelope evidence."""
trusted_title = _trusted_envelope_title(envelope)
fallback_titles = {None, "", parsed.provider_session_id, envelope.session.provider_session_id}
if parsed.title not in fallback_titles or trusted_title is None:
return parsed
return parsed.model_copy(update={"title": trusted_title, "title_source": TitleSource.ORIGIN})


def _merge_envelope_native_metadata(parsed: ParsedSession, envelope: BrowserCaptureEnvelope) -> ParsedSession:
"""Use browser-envelope fields only when the native payload omitted them.

Expand All @@ -338,10 +366,8 @@ def _merge_envelope_native_metadata(parsed: ParsedSession, envelope: BrowserCapt
title/model/timestamp fields from the embedded payload.
"""

parsed = _merge_envelope_title(parsed, envelope)
updates: dict[str, object] = {}
fallback_titles = {None, "", parsed.provider_session_id, envelope.session.provider_session_id}
if parsed.title in fallback_titles and envelope.session.title:
updates["title"] = envelope.session.title
if parsed.created_at is None and envelope.session.created_at is not None:
updates["created_at"] = envelope.session.created_at
if parsed.updated_at is None and envelope.session.updated_at is not None:
Expand Down Expand Up @@ -461,10 +487,12 @@ def _parse_claude_fallback_envelope(
fidelity_flag = (
COMPACT_BROWSER_CAPTURE_INGEST_FLAG if _is_compact_native_capture(envelope) else DOM_FALLBACK_INGEST_FLAG
)
trusted_title = _trusted_envelope_title(envelope)
return ParsedSession(
source_name=Provider.CLAUDE_AI,
provider_session_id=provider_session_id,
title=envelope.session.title or envelope.provenance.page_title or provider_session_id,
title_source=TitleSource.ORIGIN if trusted_title is not None else None,
session_kind=_session_kind_for_browser_capture(envelope, provider_session_id),
created_at=created_at,
updated_at=updated_at,
Expand Down Expand Up @@ -640,7 +668,10 @@ def parse(payload: object, fallback_id: str) -> ParsedSession:

return _merge_envelope_session_events(
_apply_browser_capture_session_kind(
_merge_envelope_attachments(parse_chatgpt(raw_provider_payload, provider_session_id), envelope),
_merge_envelope_attachments(
_merge_envelope_title(parse_chatgpt(raw_provider_payload, provider_session_id), envelope),
envelope,
),
envelope,
provider_session_id,
has_native_payload=True,
Expand Down Expand Up @@ -725,10 +756,12 @@ def parse(payload: object, fallback_id: str) -> ParsedSession:
)
for message in messages
]
trusted_title = _trusted_envelope_title(envelope)
return ParsedSession(
source_name=provider,
provider_session_id=provider_session_id,
title=envelope.session.title or envelope.provenance.page_title or provider_session_id,
title_source=TitleSource.ORIGIN if trusted_title is not None else None,
session_kind=_session_kind_for_browser_capture(envelope, provider_session_id),
created_at=envelope.session.created_at,
updated_at=envelope.session.updated_at,
Expand Down
6 changes: 4 additions & 2 deletions polylogue/sources/parsers/grok.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
from polylogue.archive.message.artifacts import classify_material_origin
from polylogue.archive.message.roles import Role
from polylogue.archive.message.types import MessageType
from polylogue.core.enums import BlockType, Provider
from polylogue.core.enums import BlockType, Provider, TitleSource
from polylogue.core.timestamps import canonical_timestamp_text

from .base import (
Expand Down Expand Up @@ -132,7 +132,8 @@ def parse_conversation(payload: Mapping[str, object], fallback_id: str) -> Parse
responses = responses_raw if isinstance(responses_raw, list) else []

title_raw = conversation.get("title")
title = title_raw if isinstance(title_raw, str) and title_raw else fallback_id
provider_title = title_raw if isinstance(title_raw, str) and title_raw else None
title = provider_title or fallback_id
created_at = _timestamp_text(conversation.get("create_time"))

messages: list[ParsedMessage] = []
Expand Down Expand Up @@ -186,6 +187,7 @@ def parse_conversation(payload: Mapping[str, object], fallback_id: str) -> Parse
source_name=Provider.GROK,
provider_session_id=fallback_id,
title=title,
title_source=TitleSource.ORIGIN if provider_title else None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Trigger a semantic replay for title provenance

When upgrading an existing v66 archive whose raw records have already been parsed, these parser-only assignments leave INDEX_SCHEMA_VERSION unchanged, so normal startup accepts the existing index.db and the parse backlog excludes rows with parsed_at_ms; consequently the stored title_source remains NULL and the public title stays suppressed for all previously ingested Grok, Antigravity, browser-capture, and Hermes sessions. Increment the index version and declare the delta as SEMANTIC_REPARSE so those sessions are rebuilt from their retained raw evidence rather than fixing only new or explicitly reprocessed imports.

AGENTS.md reference: AGENTS.md:L193-L200

Useful? React with 👍 / 👎.

created_at=created_at,
updated_at=updated_at,
messages=messages,
Expand Down
6 changes: 4 additions & 2 deletions polylogue/sources/parsers/hermes_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from polylogue.archive.message.roles import Role
from polylogue.archive.session.branch_type import BranchType
from polylogue.core.enums import BlockType, MaterialOrigin, Provider
from polylogue.core.enums import BlockType, MaterialOrigin, Provider, TitleSource
from polylogue.core.json import JSONDocument, json_document

from .base import ParsedContentBlock, ParsedMessage, ParsedSession, ParsedSessionEvent, fill_linear_parent_chain
Expand Down Expand Up @@ -695,10 +695,12 @@ def _parse_session_row(
(message.provider_message_id for message in reversed(messages) if message.is_active_path),
None,
)
provider_title = _optional_text(_row_value(row, "title"))
return ParsedSession(
source_name=Provider.HERMES,
provider_session_id=session_id,
title=_optional_text(_row_value(row, "title")) or raw_session_id,
title=provider_title or raw_session_id,
title_source=TitleSource.ORIGIN if provider_title else None,
created_at=_epoch_iso(row["started_at"]),
updated_at=_epoch_iso(_row_value(row, "ended_at")) or _latest_message_timestamp(messages),
messages=messages,
Expand Down
6 changes: 5 additions & 1 deletion polylogue/storage/sqlite/archive_tiers/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,11 @@
# positional coordinates in the generated message_id expression. Existing
# derived rows remain readable as opaque legacy ids, but new materialization
# must replay raw sessions to regenerate message/block/reference identities.
INDEX_SCHEMA_VERSION = 66
# polylogue-o5smo: v67 assigns typed origin provenance to provider-authored
# Grok, Antigravity, browser-capture, and Hermes titles. Existing parsed rows
# require raw replay because an in-place schema operation cannot recover title
# authorship from the stored untyped title text.
INDEX_SCHEMA_VERSION = 67

# polylogue-v6i3: shared WHEN-clause fragment gating the blocks_command_trigram
# trigger BODIES on the same dedicated bulk-build guard row messages_fts's
Expand Down
8 changes: 8 additions & 0 deletions polylogue/storage/sqlite/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,14 @@ class IndexDeltaDeclarationReport(TypedDict):
# by raw replay under the new expression.
classes=(DerivedDeltaClass.SEMANTIC_REPARSE,),
),
IndexDeltaDeclaration(
version=67,
# Parser-authored title provenance changes stored session semantics.
# Existing untyped title text cannot distinguish provider evidence
# from page-title and native-id fallbacks, so only raw replay can
# produce trustworthy title_source values.
classes=(DerivedDeltaClass.SEMANTIC_REPARSE,),
),
)


Expand Down
5 changes: 4 additions & 1 deletion tests/unit/sources/parsers/test_antigravity.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from pathlib import Path

from polylogue.archive.message.roles import Role
from polylogue.core.enums import BlockType, Provider
from polylogue.core.enums import BlockType, Provider, TitleSource
from polylogue.core.json import JSONDocument
from polylogue.pipeline.ids import session_revision_projection
from polylogue.sources.parsers.antigravity import (
Expand Down Expand Up @@ -43,6 +43,7 @@ def test_parse_markdown_export_splits_known_sections() -> None:
assert session.source_name is Provider.ANTIGRAVITY
assert session.provider_session_id == "cascade-1"
assert session.title == "Focused checks"
assert session.title_source is TitleSource.ORIGIN
assert session.updated_at == "2026-03-05T04:21:34Z"
assert [message.role for message in session.messages] == [Role.USER, Role.ASSISTANT]
assert [message.text for message in session.messages] == [
Expand Down Expand Up @@ -113,6 +114,7 @@ def test_parse_markdown_export_falls_back_to_single_export_message() -> None:
assert session.messages[0].position == 0
assert session.messages[0].is_active_leaf is True
assert session.active_leaf_message_provider_id == session.messages[0].provider_message_id
assert session.title_source is None


def test_parse_brain_metadata_reads_adjacent_artifact(tmp_path: Path) -> None:
Expand All @@ -134,6 +136,7 @@ def test_parse_brain_metadata_reads_adjacent_artifact(tmp_path: Path) -> None:
assert session.source_name is Provider.ANTIGRAVITY
assert session.provider_session_id == "session-1:implementation_plan.md"
assert session.title == "Implementation Plan"
assert session.title_source is None
assert session.updated_at == "2026-01-07T19:08:15.216541610Z"
assert session.messages[0].role is Role.ASSISTANT
assert session.messages[0].text == "# Implementation Plan\n\nDo the work.\n"
Expand Down
4 changes: 3 additions & 1 deletion tests/unit/sources/parsers/test_grok.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Any

from polylogue.archive.message.roles import Role
from polylogue.core.enums import BlockType, Origin, Provider
from polylogue.core.enums import BlockType, Origin, Provider, TitleSource
from polylogue.core.sources import origin_from_provider
from polylogue.pipeline.ids import session_revision_projection
from polylogue.sources.dispatch import detect_provider, parse_payload
Expand Down Expand Up @@ -89,6 +89,7 @@ def test_parse_conversation_nested_response_shape() -> None:
assert session.source_name is Provider.GROK
assert session.provider_session_id == "grok-1"
assert session.title == "Debugging a React hook"
assert session.title_source is TitleSource.ORIGIN
assert session.created_at == "2024-04-01T19:33:20+00:00"
assert len(session.messages) == 2
assert [m.role for m in session.messages] == [Role.USER, Role.ASSISTANT]
Expand Down Expand Up @@ -118,6 +119,7 @@ def test_parse_conversation_missing_title_falls_back_to_id() -> None:
payload: dict[str, Any] = {"conversation": {}, "responses": []}
session = grok.parse_conversation(payload, "fallback-id")
assert session.title == "fallback-id"
assert session.title_source is None


def test_parse_conversation_empty_responses_yields_no_messages() -> None:
Expand Down
12 changes: 11 additions & 1 deletion tests/unit/sources/parsers/test_hermes_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import sqlite3
from pathlib import Path

from polylogue.core.enums import BlockType
from polylogue.core.enums import BlockType, TitleSource
from polylogue.sources.parsers.base import ParsedContentBlock
from polylogue.sources.parsers.hermes_state import parse_state_db

Expand Down Expand Up @@ -83,6 +83,16 @@ def test_exit_code_zero_is_not_an_error(tmp_path: Path) -> None:
assert blocks[0].exit_code == 0


def test_state_db_explicit_title_is_provider_provenance(tmp_path: Path) -> None:
path = tmp_path / "state.db"
_write_state_db(path, tool_contents=[json.dumps({"output": "ok", "exit_code": 0})])

sessions = parse_state_db(path)

assert sessions[0].title == "Outcome fixture"
assert sessions[0].title_source is TitleSource.ORIGIN


def test_nonzero_exit_code_is_an_error(tmp_path: Path) -> None:
blocks = _tool_result_blocks(
tmp_path / "state.db", tool_contents=[json.dumps({"output": "boom", "exit_code": 127})]
Expand Down
Loading