From 20170f9dfa1f7c2b1167e74bef34b91ec3115050 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 18:24:49 -0400 Subject: [PATCH 1/8] Date the external events saved before event time became required #2011 made every transcript event carry its time but left 45,458 saved Claude tool events undated, so every rotate crashed decoding them. The carry takes each event's time from its Claude Code transcript (all 45,458 found, 6 s) and refuses if any stays undated; the unjudged query loses its undated branch. Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/external_transcript.py | 15 +---- tools/cutover/carry_external_event_times.py | 73 +++++++++++++++++++++ 2 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 tools/cutover/carry_external_event_times.py diff --git a/src/agent_comms/external_transcript.py b/src/agent_comms/external_transcript.py index d2eb3a25b0..d254b81736 100644 --- a/src/agent_comms/external_transcript.py +++ b/src/agent_comms/external_transcript.py @@ -212,19 +212,8 @@ def unjudged_query(cls, thread, todo): judged = tuple(verdict.tool_call_id for verdict in todo.verdicts) placeholders = lambda values: ",".join("?" for _ in values) since = todo.order.at.recorded_at("An order") - # SQL NULL is the original undated fact, decoded through FieldCodec - # as UnrecordedEventTime when a row is read. No time is substituted. - table = cls.declared_name - selected = f'u.thread=? AND u.kind=? AND u.tool IN ({placeholders(tools)})' - # Two ranges keep timestamp in the index search, rather than scanning - # every historical edit to evaluate an OR for each row. - where = ( - f'e.rowid IN (SELECT u.rowid FROM "{table}" u WHERE {selected} AND u.timestamp>=? ' - f'UNION ALL SELECT u.rowid FROM "{table}" u WHERE {selected} ' - 'AND u.timestamp IS NULL)' - ) - scope = (thread, ToolStartTranscript.declared_name, *tools) - parameters = (*scope, since, *scope) + where = f'e.thread=? AND e.kind=? AND e.tool IN ({placeholders(tools)}) AND e.timestamp>=?' + parameters = (thread, ToolStartTranscript.declared_name, *tools, since) if judged: where += f' AND e.call_id NOT IN ({placeholders(judged)})' parameters += judged diff --git a/tools/cutover/carry_external_event_times.py b/tools/cutover/carry_external_event_times.py new file mode 100644 index 0000000000..6355896a72 --- /dev/null +++ b/tools/cutover/carry_external_event_times.py @@ -0,0 +1,73 @@ +"""Date the external events saved before #2011 made every transcript event carry its time. + +Those events are Claude tool calls; their Claude Code transcripts record when each +tool_use and tool_result happened. Each undated event takes that time; an event the +transcripts cannot date stops the carry, since readers no longer accept undated events. +""" +from __future__ import annotations + +import json +import re +from contextlib import closing +from datetime import datetime +from pathlib import Path + +from agent_comms.coordination_database import CoordinationStore +from agent_comms.external_transcript import ExternalEventRow, ExternalTranscript +from agent_comms.transcript_events import ToolEndTranscript, ToolStartTranscript + +from .carry import Carried, StoreCarry + +TRANSCRIPTS = Path.home() / ".claude" / "projects" +CALL = re.compile(r'"(?:id|tool_use_id)":"(toolu_\w+)"') +# Which transcript content item records each saved event kind. +RECORDED_BY = {"tool_use": (ToolStartTranscript.declared_name, "id"), + "tool_result": (ToolEndTranscript.declared_name, "tool_use_id")} + + +def recorded_times(wanted: set[str]) -> dict[tuple[str, str], float]: + """(kind, call id) to the time its transcript line records, for the wanted calls.""" + times: dict[tuple[str, str], float] = {} + for path in TRANSCRIPTS.glob("**/*.jsonl"): + with path.open(errors="replace") as lines: + for line in lines: + if "toolu_" not in line or not wanted.intersection(CALL.findall(line)): + continue + record = json.loads(line) + at = datetime.fromisoformat(record["timestamp"]).timestamp() + for item in record["message"]["content"]: + kind, key = RECORDED_BY.get(item.get("type"), (None, None)) + if kind and item[key] in wanted: + times.setdefault((kind, item[key]), at) + return times + + +class ExternalEventTimes(StoreCarry): + store = ExternalTranscript + + @classmethod + def carry(cls, root: Path, *, apply: bool) -> Carried: + path = cls.store.path_in(root) + if not path.exists(): + return Carried(0) + table = ExternalEventRow.declared_name + with closing(CoordinationStore.writer(path)) as db: + db.execute("BEGIN IMMEDIATE") + undated = db.execute( + f'SELECT DISTINCT kind, call_id FROM "{table}" WHERE timestamp IS NULL').fetchall() + times = recorded_times({call for _, call in undated}) + missing = [event for event in undated if tuple(event) not in times] + if missing: + db.rollback() + raise ValueError(f"{len(missing)} saved events have no transcript time, e.g. {missing[:3]}") + for kind, call in undated: + db.execute(f'UPDATE "{table}" SET event=json_set(event, \'$.timestamp\', ?) ' + 'WHERE timestamp IS NULL AND kind=? AND call_id=?', + (times[kind, call], kind, call)) + db.commit() if apply else db.rollback() + return Carried(len(undated), {"events": len(undated)}, + (path.name,) if apply and undated else ()) + + +if __name__ == "__main__": + ExternalEventTimes.main() From c40e22c13eed1c4152dab1384dd9ba2312f795af Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 18:26:18 -0400 Subject: [PATCH 2/8] Cutover to Core's commander waits only on the review queue and control plane The story and the commander page follow the switch; they never gated it. Parity joins this list when #1992 lands. Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/plan_phases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent_comms/plan_phases.py b/src/agent_comms/plan_phases.py index 4fb954a3b9..2a78e0159f 100644 --- a/src/agent_comms/plan_phases.py +++ b/src/agent_comms/plan_phases.py @@ -95,7 +95,7 @@ class ControlPlanePhase(PlanPhase): class CutoverPhase(PlanPhase): - after = (ReviewQueuePhase, StoryPhase, CommanderPagePhase, ControlPlanePhase) + after = (ReviewQueuePhase, ControlPlanePhase) class SkillPhase(PlanPhase): From ab14d09354fbb3a31a983e7a94975e4566d41339 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 18:39:25 -0400 Subject: [PATCH 3/8] Carry reads tool blocks through Claude's decoders and dates all rows in one update Co-Authored-By: Claude Opus 5.5 --- tools/cutover/carry_external_event_times.py | 66 ++++++++++++++------- 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/tools/cutover/carry_external_event_times.py b/tools/cutover/carry_external_event_times.py index 6355896a72..647c258bff 100644 --- a/tools/cutover/carry_external_event_times.py +++ b/tools/cutover/carry_external_event_times.py @@ -1,17 +1,21 @@ """Date the external events saved before #2011 made every transcript event carry its time. -Those events are Claude tool calls; their Claude Code transcripts record when each -tool_use and tool_result happened. Each undated event takes that time; an event the -transcripts cannot date stops the carry, since readers no longer accept undated events. +Those events are Claude tool calls. Replaying each Claude Code transcript through the +bridge's own reader yields the same events with their recorded times; each undated row +takes its event's time. An event no transcript dates stops the carry, since readers no +longer accept undated events. """ + from __future__ import annotations +from contextlib import closing +from pathlib import Path + import json import re -from contextlib import closing from datetime import datetime -from pathlib import Path +from agent_comms.cli_bridges.claude_protocol import ToolResult, ToolUse from agent_comms.coordination_database import CoordinationStore from agent_comms.external_transcript import ExternalEventRow, ExternalTranscript from agent_comms.transcript_events import ToolEndTranscript, ToolStartTranscript @@ -20,13 +24,16 @@ TRANSCRIPTS = Path.home() / ".claude" / "projects" CALL = re.compile(r'"(?:id|tool_use_id)":"(toolu_\w+)"') -# Which transcript content item records each saved event kind. -RECORDED_BY = {"tool_use": (ToolStartTranscript.declared_name, "id"), - "tool_result": (ToolEndTranscript.declared_name, "tool_use_id")} +# Claude's own block decoders: which saved event each block dates. +BLOCKS = { + ToolUse.wire_tag: (ToolUse, ToolStartTranscript, lambda block: block.id), + ToolResult.wire_tag: (ToolResult, ToolEndTranscript, lambda block: block.tool_use_id), +} def recorded_times(wanted: set[str]) -> dict[tuple[str, str], float]: - """(kind, call id) to the time its transcript line records, for the wanted calls.""" + """(event kind, call id) to the time its transcript line records, for the wanted calls. + The bridge's reader replays only a recent window, so the full file is read here.""" times: dict[tuple[str, str], float] = {} for path in TRANSCRIPTS.glob("**/*.jsonl"): with path.open(errors="replace") as lines: @@ -34,11 +41,16 @@ def recorded_times(wanted: set[str]) -> dict[tuple[str, str], float]: if "toolu_" not in line or not wanted.intersection(CALL.findall(line)): continue record = json.loads(line) + content = record.get("message", {}).get("content") + if "timestamp" not in record or not isinstance(content, list): + continue # snapshots and queue lines quote ids but time nothing at = datetime.fromisoformat(record["timestamp"]).timestamp() - for item in record["message"]["content"]: - kind, key = RECORDED_BY.get(item.get("type"), (None, None)) - if kind and item[key] in wanted: - times.setdefault((kind, item[key]), at) + for item in content: + block, event, call_of = BLOCKS.get(item.get("type"), (None, None, None)) + if block is not None: + call = call_of(block.model_validate(item)) + if call in wanted: + times.setdefault((event.declared_name, call), at) return times @@ -54,19 +66,31 @@ def carry(cls, root: Path, *, apply: bool) -> Carried: with closing(CoordinationStore.writer(path)) as db: db.execute("BEGIN IMMEDIATE") undated = db.execute( - f'SELECT DISTINCT kind, call_id FROM "{table}" WHERE timestamp IS NULL').fetchall() + f'SELECT DISTINCT kind, call_id FROM "{table}" WHERE timestamp IS NULL' + ).fetchall() times = recorded_times({call for _, call in undated}) missing = [event for event in undated if tuple(event) not in times] if missing: db.rollback() - raise ValueError(f"{len(missing)} saved events have no transcript time, e.g. {missing[:3]}") - for kind, call in undated: - db.execute(f'UPDATE "{table}" SET event=json_set(event, \'$.timestamp\', ?) ' - 'WHERE timestamp IS NULL AND kind=? AND call_id=?', - (times[kind, call], kind, call)) + raise ValueError( + f"{len(missing)} saved events have no transcript time, e.g. {missing[:3]}" + ) + # One pass over the table: per-event updates would each scan it, since no index leads with call_id. + db.execute( + "CREATE TEMP TABLE dated (kind TEXT, call_id TEXT, at REAL, PRIMARY KEY (kind, call_id))" + ) + db.executemany( + "INSERT INTO dated VALUES (?, ?, ?)", + ((kind, call, at) for (kind, call), at in times.items()), + ) + db.execute( + f"UPDATE \"{table}\" SET event=json_set(event, '$.timestamp', (SELECT at FROM dated d " + f'WHERE d.kind="{table}".kind AND d.call_id="{table}".call_id)) WHERE timestamp IS NULL' + ) db.commit() if apply else db.rollback() - return Carried(len(undated), {"events": len(undated)}, - (path.name,) if apply and undated else ()) + return Carried( + len(undated), {"events": len(undated)}, (path.name,) if apply and undated else () + ) if __name__ == "__main__": From 7cb7590c12b48dd91f725b1d7c8481599f3210b9 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 18:49:05 -0400 Subject: [PATCH 4/8] Wrap the carry's SQL and look up block decoders by membership Co-Authored-By: Claude Opus 5.5 --- tools/cutover/carry_external_event_times.py | 32 ++++++++++++--------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/tools/cutover/carry_external_event_times.py b/tools/cutover/carry_external_event_times.py index 647c258bff..d18bcc2e99 100644 --- a/tools/cutover/carry_external_event_times.py +++ b/tools/cutover/carry_external_event_times.py @@ -8,12 +8,12 @@ from __future__ import annotations -from contextlib import closing -from pathlib import Path - import json import re +from collections.abc import Callable +from contextlib import closing from datetime import datetime +from pathlib import Path from agent_comms.cli_bridges.claude_protocol import ToolResult, ToolUse from agent_comms.coordination_database import CoordinationStore @@ -25,9 +25,11 @@ TRANSCRIPTS = Path.home() / ".claude" / "projects" CALL = re.compile(r'"(?:id|tool_use_id)":"(toolu_\w+)"') # Claude's own block decoders: which saved event each block dates. -BLOCKS = { - ToolUse.wire_tag: (ToolUse, ToolStartTranscript, lambda block: block.id), - ToolResult.wire_tag: (ToolResult, ToolEndTranscript, lambda block: block.tool_use_id), +BLOCKS: dict[str, tuple[tuple[type[ToolUse | ToolResult], str, Callable[..., str]], ...]] = { + ToolUse.wire_tag: ((ToolUse, ToolStartTranscript.declared_name, lambda block: block.id),), + ToolResult.wire_tag: ( + (ToolResult, ToolEndTranscript.declared_name, lambda block: block.tool_use_id), + ), } @@ -45,12 +47,11 @@ def recorded_times(wanted: set[str]) -> dict[tuple[str, str], float]: if "timestamp" not in record or not isinstance(content, list): continue # snapshots and queue lines quote ids but time nothing at = datetime.fromisoformat(record["timestamp"]).timestamp() - for item in content: - block, event, call_of = BLOCKS.get(item.get("type"), (None, None, None)) - if block is not None: + for item in (item for item in content if item["type"] in BLOCKS): + for block, kind, call_of in BLOCKS[item["type"]]: call = call_of(block.model_validate(item)) if call in wanted: - times.setdefault((event.declared_name, call), at) + times.setdefault((kind, call), at) return times @@ -75,17 +76,20 @@ def carry(cls, root: Path, *, apply: bool) -> Carried: raise ValueError( f"{len(missing)} saved events have no transcript time, e.g. {missing[:3]}" ) - # One pass over the table: per-event updates would each scan it, since no index leads with call_id. + # One pass over the table: per-event updates would each scan it, + # since no index leads with call_id. db.execute( - "CREATE TEMP TABLE dated (kind TEXT, call_id TEXT, at REAL, PRIMARY KEY (kind, call_id))" + "CREATE TEMP TABLE dated" + " (kind TEXT, call_id TEXT, at REAL, PRIMARY KEY (kind, call_id))" ) db.executemany( "INSERT INTO dated VALUES (?, ?, ?)", ((kind, call, at) for (kind, call), at in times.items()), ) db.execute( - f"UPDATE \"{table}\" SET event=json_set(event, '$.timestamp', (SELECT at FROM dated d " - f'WHERE d.kind="{table}".kind AND d.call_id="{table}".call_id)) WHERE timestamp IS NULL' + f'UPDATE "{table}" AS t SET event=json_set(event, \'$.timestamp\',' + " (SELECT at FROM dated d WHERE d.kind=t.kind AND d.call_id=t.call_id))" + " WHERE timestamp IS NULL" ) db.commit() if apply else db.rollback() return Carried( From b090b8a18e1bc349090916274bd1babbeb6770d2 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 18:53:18 -0400 Subject: [PATCH 5/8] Decode each transcript line with the bridge's record decoder Co-Authored-By: Claude Opus 5.5 --- tools/cutover/carry_external_event_times.py | 52 ++++++++++++--------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/tools/cutover/carry_external_event_times.py b/tools/cutover/carry_external_event_times.py index d18bcc2e99..93787efa27 100644 --- a/tools/cutover/carry_external_event_times.py +++ b/tools/cutover/carry_external_event_times.py @@ -12,10 +12,17 @@ import re from collections.abc import Callable from contextlib import closing -from datetime import datetime from pathlib import Path -from agent_comms.cli_bridges.claude_protocol import ToolResult, ToolUse +from pydantic import ValidationError + +from agent_comms.cli_bridges.claude_protocol import ( + CLAUDE_RECORD, + AssistantRecord, + ToolResult, + ToolUse, + UserRecord, +) from agent_comms.coordination_database import CoordinationStore from agent_comms.external_transcript import ExternalEventRow, ExternalTranscript from agent_comms.transcript_events import ToolEndTranscript, ToolStartTranscript @@ -24,34 +31,35 @@ TRANSCRIPTS = Path.home() / ".claude" / "projects" CALL = re.compile(r'"(?:id|tool_use_id)":"(toolu_\w+)"') -# Claude's own block decoders: which saved event each block dates. -BLOCKS: dict[str, tuple[tuple[type[ToolUse | ToolResult], str, Callable[..., str]], ...]] = { - ToolUse.wire_tag: ((ToolUse, ToolStartTranscript.declared_name, lambda block: block.id),), - ToolResult.wire_tag: ( - (ToolResult, ToolEndTranscript.declared_name, lambda block: block.tool_use_id), - ), -} +# Claude's own block types, and which saved event each one dates. +BLOCKS: tuple[tuple[type[ToolUse | ToolResult], str, Callable[..., str]], ...] = ( + (ToolUse, ToolStartTranscript.declared_name, lambda block: block.id), + (ToolResult, ToolEndTranscript.declared_name, lambda block: block.tool_use_id), +) def recorded_times(wanted: set[str]) -> dict[tuple[str, str], float]: """(event kind, call id) to the time its transcript line records, for the wanted calls. - The bridge's reader replays only a recent window, so the full file is read here.""" + The bridge's reader replays only a recent window, so every line is decoded here with + the bridge's record decoder.""" times: dict[tuple[str, str], float] = {} for path in TRANSCRIPTS.glob("**/*.jsonl"): with path.open(errors="replace") as lines: - for line in lines: + for position, line in enumerate(lines): if "toolu_" not in line or not wanted.intersection(CALL.findall(line)): continue - record = json.loads(line) - content = record.get("message", {}).get("content") - if "timestamp" not in record or not isinstance(content, list): - continue # snapshots and queue lines quote ids but time nothing - at = datetime.fromisoformat(record["timestamp"]).timestamp() - for item in (item for item in content if item["type"] in BLOCKS): - for block, kind, call_of in BLOCKS[item["type"]]: - call = call_of(block.model_validate(item)) - if call in wanted: - times.setdefault((kind, call), at) + try: + record = CLAUDE_RECORD.validate_python( + {**json.loads(line), "position": position} + ) + except ValidationError: + continue # snapshots and queue lines quote call ids but are no timed record + if not isinstance(record, AssistantRecord | UserRecord): + continue + for block in record.message.content: + for decoded, kind, call_of in BLOCKS: + if isinstance(block, decoded) and call_of(block) in wanted: + times.setdefault((kind, call_of(block)), record.timestamp) return times @@ -87,7 +95,7 @@ def carry(cls, root: Path, *, apply: bool) -> Carried: ((kind, call, at) for (kind, call), at in times.items()), ) db.execute( - f'UPDATE "{table}" AS t SET event=json_set(event, \'$.timestamp\',' + f"UPDATE \"{table}\" AS t SET event=json_set(event, '$.timestamp'," " (SELECT at FROM dated d WHERE d.kind=t.kind AND d.call_id=t.call_id))" " WHERE timestamp IS NULL" ) From 286219310945e7ab1fa010076efa20fe3292ea41 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 18:55:27 -0400 Subject: [PATCH 6/8] Practice installs read Claude Code's real transcripts Co-Authored-By: Claude Opus 5.5 --- tools/upgrade_bus.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/upgrade_bus.py b/tools/upgrade_bus.py index 62369f88d1..d0cbdefa56 100644 --- a/tools/upgrade_bus.py +++ b/tools/upgrade_bus.py @@ -256,6 +256,8 @@ def rehearsal_home(scratch: Path, root: Path, copy: Path) -> Path: record["root"] = str(copy) (home / route).write_text(json.dumps(record)) (home / route).chmod(0o600) + # Claude Code's transcripts are read-only input that carries date from; the copy reads the real ones. + (home / ".claude").symlink_to(Path.home() / ".claude") return home From 834fb0ba5a9de4b7ced2822fa26a60b690b4ad9a Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 18:56:49 -0400 Subject: [PATCH 7/8] Wrap the practice-home comment Co-Authored-By: Claude Opus 5.5 --- tools/upgrade_bus.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/upgrade_bus.py b/tools/upgrade_bus.py index d0cbdefa56..ebab0fbfbb 100644 --- a/tools/upgrade_bus.py +++ b/tools/upgrade_bus.py @@ -256,7 +256,8 @@ def rehearsal_home(scratch: Path, root: Path, copy: Path) -> Path: record["root"] = str(copy) (home / route).write_text(json.dumps(record)) (home / route).chmod(0o600) - # Claude Code's transcripts are read-only input that carries date from; the copy reads the real ones. + # Claude Code's transcripts are read-only input that carries date from; + # the copy reads the real ones. (home / ".claude").symlink_to(Path.home() / ".claude") return home From ec8d84df59fd5bc2fa9aa5a305e4425b2690b48c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 18:56:55 -0400 Subject: [PATCH 8/8] Budget the batch growth read in review Src-Budget: 112