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
6 changes: 3 additions & 3 deletions docs/evidence/schema-disposition-2026-08-31.json
Original file line number Diff line number Diff line change
Expand Up @@ -19354,7 +19354,7 @@
{
"campaign_action": "retain and verify the named producer/consumer",
"consumer": "index.db production readers",
"definition_sha256": "bf68c416f26bd43418c8f184698717a04674be167ce7021dc8b3863422db4a1e",
"definition_sha256": "a9cb8929e7a41413deac7dc8e25c650974b487963e93c6df91efa14802d5d0d3",
"disposition": "KEEP",
"evidence": "reachable canonical declaration; live counts are evidence only",
"generated_kind": null,
Expand Down Expand Up @@ -39851,15 +39851,15 @@
"schema_fingerprints": {
"audit": "39a2cff1dfbc0d8721f8a29229e6104abbdfa37d6f432d822b412bc580f2b5e9",
"embeddings": "822d7ca7d00c658690337f521b0ad03d912903d95435e71a9a5703480e82dc5f",
"index": "a3f4d367d17b1f22d7ba8b35b5a6397a29715d7e97ca0c9aed4b9270322002ac",
"index": "e9060c44f803e3427c973eadca10a12b39fafeb89a5c9ec4c9c4731082ede8eb",
"ops": "4443709d4b8fa6706cd0f2e350cb34fe7660c734dcd2407da202a425027e2d80",
"source": "b6ea0b8cd6f926106f1de584f5a4c1b114ea6749d7375b256c6f91c85f4d7814",
"user": "e87ac873dd6e228784e367ac4252c76bdd18569d9af0dc38bf2ee33128134559"
},
"schema_versions": {
"audit": 2,
"embeddings": 5,
"index": 93,
"index": 94,
"ops": 1,
"source": 41,
"user": 11
Expand Down
2 changes: 1 addition & 1 deletion docs/schema-disposition-2026-08-19.md
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,7 @@ Generated by `devtools render schema-disposition` from the canonical SQLite decl
| index:column:session_latency_profiles.tool_call_count_by_category_json | column | KEEP | canonical schema owner | index.db canonical writer route | index.db production readers | SELECT COUNT(*) FROM session_latency_profiles | retain and verify the named producer/consumer | none | 5bd2c51099f561917fc6dd963e913c27368728dc178b8fa875a8d7eb733a236a |
| index:column:session_latency_profiles.evidence_payload_json | column | KEEP | canonical schema owner | index.db canonical writer route | index.db production readers | SELECT COUNT(*) FROM session_latency_profiles | retain and verify the named producer/consumer | none | a2dadaf710c92a289869e9abf0a57a3badcb705b2fdd100447bfa28d73b999cd |
| index:column:session_latency_profiles.search_text | column | KEEP | canonical schema owner | index.db canonical writer route | index.db production readers | SELECT COUNT(*) FROM session_latency_profiles | retain and verify the named producer/consumer | none | 24d3295e25781ccaa1cd9889351fa302ff4d8dd011fb6c17988e60f458a3bf7a |
| index:table:session_links | table | KEEP | canonical schema owner | index.db canonical writer route | index.db production readers | SELECT COUNT(*) FROM session_links | retain and verify the named producer/consumer | none | bf68c416f26bd43418c8f184698717a04674be167ce7021dc8b3863422db4a1e |
| index:table:session_links | table | KEEP | canonical schema owner | index.db canonical writer route | index.db production readers | SELECT COUNT(*) FROM session_links | retain and verify the named producer/consumer | none | a9cb8929e7a41413deac7dc8e25c650974b487963e93c6df91efa14802d5d0d3 |
| index:column:session_links.src_session_id | column | KEEP | canonical schema owner | index.db canonical writer route | index.db production readers | SELECT COUNT(*) FROM session_links | retain and verify the named producer/consumer | none | 3bb16e757b931df8ff63a86d54c474423ca2304af7fd9eae70ef0da6bcead52b |
| index:column:session_links.dst_origin | column | KEEP | canonical schema owner | index.db canonical writer route | index.db production readers | SELECT COUNT(*) FROM session_links | retain and verify the named producer/consumer | none | 1ea6d5022d11a16d49aa2d5c726c43e301ac2fd56dfd1d1e3b3bb71ee94acd43 |
| index:column:session_links.dst_native_id | column | KEEP | canonical schema owner | index.db canonical writer route | index.db production readers | SELECT COUNT(*) FROM session_links | retain and verify the named producer/consumer | none | da9604bfa49c87da3e36e04e7b48a0391089ffab1408b89b282496bfe4fb6249 |
Expand Down
20 changes: 20 additions & 0 deletions polylogue/archive/topology/edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from datetime import datetime, timezone
from typing import Literal, get_args

from pydantic import BaseModel, ConfigDict, Field, field_validator

Expand Down Expand Up @@ -40,6 +41,25 @@ def _now_isoformat() -> str:
return datetime.now(timezone.utc).isoformat()


# Why ``session_links.parent_tool_use_block_id`` is NULL on a resolved edge.
# Persisted under ``evidence_json.dispatch_reason``; removed once a block
# binds. Every member is a refusal to guess, never an ordinal fallback.
DispatchResolutionReason = Literal[
# The origin declares no parent-side dispatch identity.
"origin-no-dispatch-identity",
# The parent carries no dispatch observation naming this child.
"dispatch-evidence-absent",
# Evidence names a tool id the parent has no tool_use block for.
"dispatch-block-missing",
# More than one parent tool_use block carries the named tool id.
"dispatch-tool-id-duplicate",
# Witnesses name different tool ids for this child, or one tool id
# names children that resolve to different sessions.
"dispatch-identity-contradiction",
]
DISPATCH_RESOLUTION_REASONS: frozenset[str] = frozenset(get_args(DispatchResolutionReason))


class TopologyEdgeRecord(BaseModel):
"""Runtime row for the ``session_links`` table.

Expand Down
2 changes: 2 additions & 0 deletions polylogue/sources/origin_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1230,8 +1230,10 @@ def _claude_code_spec() -> OriginSpec:
parent_dispatch=TopologyCapability(
"positive-derived",
(
"claude_code user.toolUseResult.agentId + tool_result.tool_use_id (Agent/Task result)",
"claude_code progress.data.type=agent_progress.parentToolUseID",
"claude_code progress.data.childSessionId/agentId when present",
"claude_code subagents/agent-*.meta.json toolUseId (source tier, child-bound)",
),
"parent-side dispatch evidence is retained only when the wire carries an exact child identity",
),
Expand Down
5 changes: 5 additions & 0 deletions polylogue/sources/parsers/base_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,11 @@ class ParsedDispatchObservation(BaseModel):
child_provider_id: str | None = None
child_identity_namespace: str = "provider-session"
observation_kind: Literal["parent_dispatch"] = "parent_dispatch"
# Display metadata the dispatching side recorded about the child
# (Claude Code ``toolUseResult.agentType`` / ``description``). Never an
# identity input to the resolver.
agent_type: str | None = None
description: str | None = None
first_seen: str | None = None
last_seen: str | None = None
resolution_reason: (
Expand Down
84 changes: 75 additions & 9 deletions polylogue/sources/parsers/claude/code_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,9 +584,70 @@ def _sidecar_evidence_payload(record_type: str, item: dict[str, object]) -> dict
@dataclass
class _DelegationProgressStats:
count: int = 0
result_count: int = 0
first_seen: str | None = None
last_seen: str | None = None
child_provider_ids: set[str] = field(default_factory=set)
agent_type: str | None = None
description: str | None = None


def _subagent_transcript_stem(agent_id: str) -> str:
"""Return the provider name a dispatched child claims for itself.

Claude Code writes a subagent's transcript to
``<session>/subagents/agent-<agentId>.jsonl`` and the child parser claims
that file stem as its provider alias; parent-side records carry the bare
``agentId``. The stem is the provider's own naming, not an inference.
"""
return agent_id if agent_id.startswith("agent-") else f"agent-{agent_id}"


def _accumulate_dispatch_result(
item: dict[str, object],
timestamp: str | None,
accumulator: dict[str, _DelegationProgressStats],
) -> bool:
"""Fold a ``user`` record's ``toolUseResult.agentId`` into its dispatch edge.

The Agent/Task tool's result record names the spawned child (``agentId``)
and, through its ``tool_result`` content segment, the dispatching
``tool_use_id``: exact parent-side evidence binding one tool-use block to
one child. Returns whether the record carried such evidence.
"""
if item.get("type") != "user":
return False
tool_result = item.get("toolUseResult")
if not isinstance(tool_result, dict):
return False
agent_id = _string_field(tool_result, "agentId")
if not agent_id:
return False
message = item.get("message")
content = message.get("content") if isinstance(message, dict) else None
if not isinstance(content, list):
return False
tool_use_ids = {
str(segment["tool_use_id"])
for segment in content
if isinstance(segment, dict)
and segment.get("type") == "tool_result"
and isinstance(segment.get("tool_use_id"), str)
and segment["tool_use_id"]
}
if len(tool_use_ids) != 1:
return False
entry = accumulator.setdefault(next(iter(tool_use_ids)), _DelegationProgressStats())
entry.child_provider_ids.add(_subagent_transcript_stem(agent_id))
entry.result_count += 1
entry.agent_type = _string_field(tool_result, "agentType") or entry.agent_type
entry.description = _string_field(tool_result, "description") or entry.description
if timestamp:
if entry.first_seen is None or timestamp < entry.first_seen:
entry.first_seen = timestamp
if entry.last_seen is None or timestamp > entry.last_seen:
entry.last_seen = timestamp
return True


def _accumulate_delegation_progress(
Expand Down Expand Up @@ -615,10 +676,14 @@ def _accumulate_delegation_progress(
# The dispatched child is named only inside the progress payload. The
# record envelope's identity fields name the transcript that emitted the
# tick -- its own session, which is the dispatching parent.
for key in ("childSessionId", "child_session_id", "agentId", "agent_id"):
for key in ("childSessionId", "child_session_id"):
value = _string_field(data, key)
if value:
entry.child_provider_ids.add(value)
for key in ("agentId", "agent_id"):
value = _string_field(data, key)
if value:
entry.child_provider_ids.add(_subagent_transcript_stem(value))
entry.count += 1
if timestamp:
if entry.first_seen is None or timestamp < entry.first_seen:
Expand Down Expand Up @@ -1697,6 +1762,7 @@ def _fold_code_record(acc: _SessionAccumulator, index: int, item: dict[str, obje
timestamp=timestamp,
)
)
_accumulate_dispatch_result(item, timestamp, acc.delegation_progress)
tool_execution_payload = _tool_execution_result_payload(item)
if tool_execution_payload is not None:
acc.session_events.append(
Expand Down Expand Up @@ -1855,6 +1921,8 @@ def _finalize_code_session(acc: _SessionAccumulator) -> ParsedSession:
observation = ParsedDispatchObservation(
provider_tool_id=parent_tool_use_id,
child_provider_id=child_provider_ids[0] if len(child_provider_ids) == 1 else None,
agent_type=stats.agent_type,
description=stats.description,
first_seen=stats.first_seen,
last_seen=stats.last_seen,
resolution_reason=(
Expand All @@ -1865,12 +1933,11 @@ def _finalize_code_session(acc: _SessionAccumulator) -> ParsedSession:
else None
),
)
observation_payload = observation.model_dump()
observation_payload["parent_tool_use_id"] = parent_tool_use_id
observation_payload["progress_tick_count"] = stats.count
observation_payload["summary"] = (
f"delegated work under tool_use {parent_tool_use_id} ({stats.count} progress ticks)"
summary = (
f"delegated work under tool_use {parent_tool_use_id} "
f"({stats.count} progress ticks, {stats.result_count} dispatch results)"
)
observation_payload = observation.model_dump()
if len(child_provider_ids) > 1:
observation_payload["child_provider_ids"] = list(child_provider_ids)
acc.session_events.append(
Expand All @@ -1881,10 +1948,9 @@ def _finalize_code_session(acc: _SessionAccumulator) -> ParsedSession:
payload={
"parent_tool_use_id": parent_tool_use_id,
"progress_tick_count": stats.count,
"first_seen": stats.first_seen,
"last_seen": stats.last_seen,
"dispatch_result_count": stats.result_count,
**observation_payload,
"summary": f"delegated work under tool_use {parent_tool_use_id} ({stats.count} progress ticks)",
"summary": summary,
},
)
)
Expand Down
23 changes: 23 additions & 0 deletions polylogue/sources/parsers/claude/orchestration.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@
"adopted_session_id",
"unresolved",
"error",
# agent-*.meta.json dispatch sidecar: the dispatching tool_use id is
# the exact join key to the parent block; agentType/description are
# display metadata.
"toolUseId",
"tool_use_id",
"agentType",
"agent_type",
"description",
"spawnDepth",
"spawn_depth",
}
)
_JOURNAL_FIELDS = _DOCUMENT_FIELDS | frozenset({"type", "event", "key", "ordinal", "retryOf", "retry_of"})
Expand Down Expand Up @@ -122,6 +132,19 @@ def transcript_path(self) -> str | None:
def meta_path(self) -> str | None:
return _first_string(self.payload, "metaPath", "meta_path")

@property
def tool_use_id(self) -> str | None:
"""The parent tool_use that dispatched this agent (sidecar ``toolUseId``)."""
return _first_string(self.payload, "toolUseId", "tool_use_id")

@property
def agent_type(self) -> str | None:
return _first_string(self.payload, "agentType", "agent_type")

@property
def description(self) -> str | None:
return _first_string(self.payload, "description")

@property
def is_result(self) -> bool:
return any(
Expand Down
6 changes: 6 additions & 0 deletions polylogue/storage/sqlite/archive_tiers/archive_tiers_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,12 @@ def _make_table_spec(
-- child inherited, and `inheritance` records whether the child shares the
-- parent's leading prefix ('prefix-sharing') or is a fresh spawn that merely
-- references the parent ('spawned-fresh'). NULL until the parent is resolved.
-- Positional content ancestry only: the prefix is aligned by message
-- position and content signature, and composition orders inherited
-- prefix then child tail by position. The child's tail may carry
-- occurred_at_ms earlier than this message (auto-compaction replays
-- original timestamps; observer clocks skew); no reader may treat the
-- branch point as a timestamp lower bound for the tail.
-- Deliberately NOT a FK: message_id is deterministic, so a parent full-replace
-- re-ingest re-creates the same id. An `ON DELETE SET NULL` FK would instead
-- null this during the parent's DELETE step and permanently break the child's
Expand Down
10 changes: 9 additions & 1 deletion polylogue/storage/sqlite/archive_tiers/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,15 @@
# SEMANTIC_REPARSE: links materialized under v92 recorded the emitting
# session's own name as a competing child identity, and no clone-safe SQL
# delta can recover the dispatch join key those rows refused.
INDEX_SCHEMA_VERSION = 93
# v94 lowers the Agent/Task tool result (``toolUseResult.agentId`` +
# ``tool_result.tool_use_id``) into the parent-side dispatch observation and
# names the child by its transcript stem; the resolver joins that
# observation, the child's ``agent-*.meta.json`` sidecar ``toolUseId``, and the
# exact parent tool_use block, recording a typed ``dispatch_reason`` when it
# refuses. SEMANTIC_REPARSE: parents materialized under v93 carry no
# observation for result-only dispatches (the live wire shape), so the join
# key cannot be derived from stored rows.
INDEX_SCHEMA_VERSION = 94

# 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
Loading