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
25 changes: 24 additions & 1 deletion .beads/issues.jsonl

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions polylogue/sources/origin_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,10 @@ def _aistudio_drive_spec() -> OriginSpec:
"runSettings (temperature/topP/topK/maxOutputTokens/thinkingLevel/safetySettings/enable* flags) "
"is read and stored verbatim as sessions.run_settings_json (polylogue-2qx.4 / polylogue-cgfy); "
"deliberately not decomposed into columns so the schema stays uncoupled from one provider's knobs.",
"chunkedPrompt.pendingInputs (unsent textbox drafts) is read and stored verbatim as "
"sessions.pending_drafts_json (polylogue-o4j2), deliberately as a session-row field rather than a "
"session_event: a draft is mutable current UI state, and session_events participate in "
"session_revision_projection's append-only comparison axes (polylogue-aggz Invariant 1).",
"drive_support_blocks.py's _SUCCESS_OUTCOMES ({'ok', 'success', "
"'succeeded', 'completed', 'outcome_ok'}) is not (yet) a "
"DroppedValueVocabulary (polylogue-2qx): Gemini's own committed "
Expand Down
9 changes: 9 additions & 0 deletions polylogue/sources/parsers/base_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,15 @@ class ParsedSession(BaseModel):
# maxOutputTokens/thinkingLevel/safetySettings/enable* flags). Stored
# verbatim as a JSON column -- see sessions.run_settings_json.
run_settings: dict[str, object] | None = None
# polylogue-o4j2: non-blank chunkedPrompt.pendingInputs entries (unsent
# AI Studio textbox drafts), each {"text": ..., "role": ..., optionally
# "token_count": ...}. Deliberately NOT a session_event: a draft is
# mutable current UI state -- edited in place, gone entirely once
# submitted -- and session_events participate in
# session_revision_projection's append-only comparison axes
# (polylogue-aggz Invariant 1). Stored verbatim as a JSON column, same
# pattern as run_settings -- see sessions.pending_drafts_json.
pending_drafts: list[dict[str, object]] = Field(default_factory=list)
# polylogue-2qx.4 / polylogue-cgfy: tracker-agnostic external references
# (pr-link today, issue refs generalize to the same relation).
session_refs: list[ParsedSessionRef] = Field(default_factory=list)
Expand Down
50 changes: 50 additions & 0 deletions polylogue/sources/parsers/drive.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,50 @@ def _model_config_event(
)


def _pending_drafts(pending_inputs: object) -> list[dict[str, object]]:
"""Extract non-blank ``chunkedPrompt.pendingInputs`` entries.

AI Studio's Drive-synced JSON carries the operator's not-yet-submitted
textbox content here -- draft prompts that never became a chunk and are
otherwise unrecoverable once overwritten (polylogue-o4j2). Entries with
blank/whitespace-only text are the overwhelmingly common case (the
textbox was empty when synced) and carry no evidence, so they are
skipped rather than kept as near-100%-empty noise.

Deliberately returned as plain dicts for ``ParsedSession.pending_drafts``,
NOT ``ParsedSessionEvent``s: a draft is mutable CURRENT state (the
operator edits the same textbox in place, and the entry disappears
entirely once submitted), not an append-only historical fact.
``session_events`` feeds ``session_revision_projection``'s
message/attachment/event comparison axes (polylogue-aggz Invariant 1),
which assume every axis only ever grows between two acquisitions of the
same session; a mutable, disappearing item there reproduces the exact
defect class polylogue-bu1i (acquisition state in identity) and
polylogue-nuec (provider-remeasurement in identity) were fixed for --
edits would compare as disjoint forks, and submission would shrink the
event axis while the message axis grows, both misclassifying revision
membership. ``pending_drafts`` stays outside every identity/hash
computation in ``pipeline/ids.py`` (see ``sessions.pending_drafts_json``).
"""
if not isinstance(pending_inputs, list):
return []
drafts: list[dict[str, object]] = []
for entry in pending_inputs:
entry_obj = json_document(entry)
text = entry_obj.get("text")
if not isinstance(text, str) or not text.strip():
continue
draft: dict[str, object] = {"text": text}
role_val = _string_field(entry_obj, "role")
if role_val is not None:
draft["role"] = role_val
token_count = _non_negative_int_field(entry_obj, "tokenCount", "token_count")
if token_count is not None:
draft["token_count"] = token_count
drafts.append(draft)
return drafts


def _delivery_status(chunk_obj: JSONDocument) -> str | None:
if _string_field(chunk_obj, "errorMessage", "error_message") is not None:
return "error"
Expand Down Expand Up @@ -380,6 +424,7 @@ def parse_chunked_prompt(provider: Provider | str, payload: JSONDocument, fallba
if payload.get("updateTime")
else _select_timestamp(observed_timestamps, latest=True)
)
pending_drafts = _pending_drafts(prompt.get("pendingInputs"))
active_leaf_message_provider_id = messages[-1].provider_message_id if messages else None
if active_leaf_message_provider_id is not None:
messages = [
Expand Down Expand Up @@ -409,6 +454,11 @@ def parse_chunked_prompt(provider: Provider | str, payload: JSONDocument, fallba
# the ``model_config`` session_event above; this is the same value
# landing on the session row itself.
run_settings=dict(run_settings) if run_settings else None,
# polylogue-o4j2: pendingInputs draft(s), kept off session_events on
# purpose -- see _pending_drafts' docstring for why (mutable current
# state must not enter session_revision_projection's comparison
# axes).
pending_drafts=pending_drafts,
)


Expand Down
16 changes: 15 additions & 1 deletion polylogue/storage/runtime/archive/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from polylogue.archive.session.branch_type import BranchType
from polylogue.core.enums import BlockType, MaterialOrigin, Origin, SemanticBlockType, SessionKind
from polylogue.core.hashing import hash_text
from polylogue.core.json import json_document
from polylogue.core.json import json_document, json_document_list
from polylogue.core.security import sanitize_path as _sanitize_path_helper
from polylogue.core.timestamps import canonical_timestamp_text
from polylogue.core.types import AttachmentId, ContentHash, MessageId, SessionEventId, SessionId
Expand Down Expand Up @@ -58,6 +58,12 @@ class SessionRecord(BaseModel):
# verbatim (aistudio-drive runSettings). None when the read path didn't
# select the column or the provider carries none.
run_settings: JSONObject | None = None
# polylogue-o4j2 (v47): non-blank chunkedPrompt.pendingInputs entries
# (unsent AI Studio textbox drafts), stored verbatim. Deliberately a
# plain session-row field, outside session_revision_projection's
# comparison axes -- see sessions.pending_drafts_json / drive.py's
# _pending_drafts docstring for why a draft cannot be a session_event.
pending_drafts: list[JSONObject] | None = None

@field_validator("origin", mode="before")
@classmethod
Expand All @@ -83,6 +89,14 @@ def non_empty_string(cls, v: str) -> str:
def coerce_json_document(cls, value: object) -> JSONObject | None:
return _coerce_json_object(value)

@field_validator("pending_drafts", mode="before")
@classmethod
def coerce_pending_drafts(cls, value: object) -> list[JSONObject] | None:
if value is None:
return None
documents = json_document_list(value)
return [dict(document) for document in documents] or None

@field_validator("created_at", "updated_at", mode="before")
@classmethod
def coerce_archive_timestamp(cls, value: object) -> str | None:
Expand Down
23 changes: 22 additions & 1 deletion polylogue/storage/sqlite/archive_tiers/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,17 @@
# A bump without a declaration is a policy violation, not a free rebuild:
# `devtools lab policy schema-versioning` fails, and the archive silently
# falls back to full raw replay. See polylogue-9rw0 / polylogue-b5l.
INDEX_SCHEMA_VERSION = 46
#
# polylogue-o4j2: v47 adds sessions.pending_drafts_json -- aistudio-drive's
# chunkedPrompt.pendingInputs non-blank entries (unsent textbox drafts, 7/397
# real sessions with draft text on the live archive). Landed as a session-row
# field rather than a session_event on purpose: a draft is mutable CURRENT
# state (edited in place, gone entirely once submitted), and session_events
# participate in session_revision_projection's append-only comparison axes
# (polylogue-aggz Invariant 1) -- putting mutable state there reproduces the
# exact defect class polylogue-bu1i (acquisition state) and polylogue-nuec
# (provider-remeasurement) were fixed for. See sessions table comment.
INDEX_SCHEMA_VERSION = 47

# 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 Expand Up @@ -193,6 +203,17 @@
-- into typed columns would couple this schema to one provider for no
-- query benefit; nothing here is queried across origins today.
run_settings_json TEXT CHECK ({json_object_check("run_settings_json", nullable=True)}),
-- polylogue-o4j2 (v47): non-blank chunkedPrompt.pendingInputs entries --
-- the operator's not-yet-submitted textbox draft(s) -- verbatim as a
-- JSON array of {{text, role, token_count}} objects. Deliberately a
-- session-row field, NOT a session_event: a draft is CURRENT mutable
-- UI state (edited in place, then disappears entirely on submit), not
-- an append-only historical fact, so it must stay outside
-- session_revision_projection's message/attachment/event comparison
-- axes (polylogue-aggz Invariant 1) -- exactly the shape polylogue-bu1i
-- and polylogue-nuec were fixed for, on a third axis (mutable session
-- state rather than acquisition state or provider-remeasurement).
pending_drafts_json TEXT CHECK ({json_array_check("pending_drafts_json", nullable=True)}),
git_branch TEXT,
git_repository_url TEXT,
provider_project_ref TEXT,
Expand Down
11 changes: 9 additions & 2 deletions polylogue/storage/sqlite/archive_tiers/write.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,15 +516,15 @@ def add_timing(name: str, started_at: float) -> None:
INSERT INTO sessions (
native_id, origin, raw_id, branch_type, active_leaf_message_id,
title, session_kind, title_source, title_ref, title_confidence,
display_name, run_settings_json,
display_name, run_settings_json, pending_drafts_json,
git_branch, git_repository_url, commit_hash,
instructions_text, reported_duration_ms, provider_project_ref,
message_count, word_count, tool_use_count, thinking_count,
paste_count, user_message_count, authored_user_message_count,
assistant_message_count, system_message_count,
tool_message_count, user_word_count, authored_user_word_count, assistant_word_count,
content_hash, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(origin, native_id) DO UPDATE SET
raw_id = excluded.raw_id,
branch_type = excluded.branch_type,
Expand All @@ -536,6 +536,12 @@ def add_timing(name: str, started_at: float) -> None:
title_confidence = COALESCE(excluded.title_confidence, sessions.title_confidence),
display_name = COALESCE(excluded.display_name, sessions.display_name),
run_settings_json = COALESCE(excluded.run_settings_json, sessions.run_settings_json),
-- Plain overwrite, NOT COALESCE like run_settings_json above:
-- a draft is current mutable state, so a reprocess that finds
-- no non-blank pendingInputs (submitted, or cleared) must
-- actually clear the stored value rather than preserving a
-- now-stale draft forever (polylogue-o4j2).
pending_drafts_json = excluded.pending_drafts_json,
git_branch = excluded.git_branch,
git_repository_url = excluded.git_repository_url,
commit_hash = excluded.commit_hash,
Expand All @@ -562,6 +568,7 @@ def add_timing(name: str, started_at: float) -> None:
session.title_confidence,
_sqlite_text(session.display_name),
_json_dumps(session.run_settings) if session.run_settings else None,
_json_dumps(session.pending_drafts) if session.pending_drafts else None,
_sqlite_text(session.git_branch),
_sqlite_text(session.git_repository_url),
_sqlite_text(session.git_commit_hash),
Expand Down
11 changes: 11 additions & 0 deletions polylogue/storage/sqlite/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,17 @@ class IndexDeltaDeclarationReport(TypedDict):
# behaviour.
classes=(DerivedDeltaClass.SEMANTIC_REPARSE,),
),
IndexDeltaDeclaration(
version=47,
# polylogue-o4j2: sessions.pending_drafts_json -- aistudio-drive
# pendingInputs draft text, moved off the session_events axis (see
# index.py's v47 header comment). Values depend on parser semantics
# (the new column is populated only by re-parsing the drive.py
# payload), so a shape-only copy-forward would leave every row NULL
# -- the same v42/v44/v45/v46 precedent. SEMANTIC_REPARSE routes
# through `polylogue ops reset --index && polylogued run`.
classes=(DerivedDeltaClass.SEMANTIC_REPARSE,),
),
)


Expand Down
4 changes: 4 additions & 0 deletions polylogue/storage/sqlite/queries/mappers_archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
)
from polylogue.storage.sqlite.queries.mappers_support import (
_json_object,
_json_object_list,
_parse_json,
_row_float,
_row_get,
Expand Down Expand Up @@ -68,6 +69,9 @@ def _row_to_session(row: sqlite3.Row) -> SessionRecord:
run_settings=_json_object(
_parse_json(_row_get(row, "run_settings_json"), field="run_settings_json", record_id=row["session_id"])
),
pending_drafts=_json_object_list(
_parse_json(_row_get(row, "pending_drafts_json"), field="pending_drafts_json", record_id=row["session_id"])
),
)


Expand Down
12 changes: 12 additions & 0 deletions polylogue/storage/sqlite/queries/mappers_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,18 @@ def _json_object(value: JSONValue | None) -> JSONObject | None:
return result


def _json_object_list(value: JSONValue | None) -> list[JSONObject] | None:
"""Parse a JSON array of objects, e.g. ``sessions.pending_drafts_json``."""
if not isinstance(value, list):
return None
documents: list[JSONObject] = []
for item in value:
document = json_document(item)
if document:
documents.append(dict(document))
return documents or None


def _json_text_tuple(value: JSONValue | None) -> tuple[str, ...]:
if not isinstance(value, list):
return ()
Expand Down
6 changes: 4 additions & 2 deletions polylogue/storage/sqlite/queries/sessions_reads.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
git_repository_url,
provider_project_ref,
display_name,
run_settings_json
run_settings_json,
pending_drafts_json
"""


Expand Down Expand Up @@ -60,7 +61,8 @@ def _session_record_select(alias: str | None = None) -> str:
{prefix}git_repository_url AS git_repository_url,
{prefix}provider_project_ref AS provider_project_ref,
{prefix}display_name AS display_name,
{prefix}run_settings_json AS run_settings_json
{prefix}run_settings_json AS run_settings_json,
{prefix}pending_drafts_json AS pending_drafts_json
"""


Expand Down
Loading