cctally 1.78.0 → 1.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -64,6 +64,23 @@ _SEARCH_BADGE = {
64
64
  "tool_call": "tools",
65
65
  "tool_output": "tools",
66
66
  "event": "event",
67
+ "meta": "context",
68
+ }
69
+
70
+ _META_LABEL_TEXT = {
71
+ "agents": "Project instructions",
72
+ "context_bundle": "Session context",
73
+ "delegation": "Delegation context",
74
+ "environment": "Environment context",
75
+ "heartbeat": "Harness heartbeat",
76
+ "instructions": "User instructions",
77
+ "memory": "Memory context",
78
+ "mode": "Agent mode",
79
+ "model_switch": "Model switch",
80
+ "permissions": "Permissions",
81
+ "plugins": "Available plugins",
82
+ "role": "Harness role",
83
+ "skill": "Skill context",
67
84
  }
68
85
 
69
86
 
@@ -87,7 +104,15 @@ def codex_normalization_authoritative(conn: sqlite3.Connection) -> bool:
87
104
  "SELECT 1 FROM cache_meta "
88
105
  "WHERE key='conversation_rebuild_codex_pending'"
89
106
  ).fetchone() is not None
90
- return not pending
107
+ version = conn.execute(
108
+ "SELECT value FROM cache_meta "
109
+ "WHERE key='codex_conversation_contract_version'"
110
+ ).fetchone()
111
+ return (
112
+ not pending
113
+ and version is not None
114
+ and version[0] == kern.CODEX_CONVERSATION_CONTRACT_VERSION
115
+ )
91
116
  except sqlite3.OperationalError:
92
117
  pass
93
118
  try:
@@ -155,6 +180,14 @@ def _item_key_for_item(conversation_key: str, item: dict) -> str:
155
180
  content_digest=anchor.content_digest)
156
181
 
157
182
 
183
+ def _member_item_keys(conversation_key: str, item: dict) -> list[str]:
184
+ """Durable aliases for logical items folded by a later contract version."""
185
+ return [
186
+ _item_key_for_item(conversation_key, folded)
187
+ for folded in item.get("folded_items", [])
188
+ ]
189
+
190
+
158
191
  def codex_block_key(
159
192
  conversation_key: str,
160
193
  *,
@@ -203,6 +236,35 @@ def _load_conversation_rows(conn: sqlite3.Connection, conversation_key: str) ->
203
236
  ]
204
237
 
205
238
 
239
+ def _load_row_payloads(
240
+ conn: sqlite3.Connection, conversation_key: str,
241
+ ) -> dict[tuple[str, int], tuple[str | None, dict]]:
242
+ """Retained physical payloads for query-time card shaping.
243
+
244
+ The retained payload remains the authoritative source used for full-payload
245
+ readback and a defensive read-time re-shape; contract v3 also persists the
246
+ same bounded card so replay-derived rollups and logical item counts converge.
247
+ """
248
+ result: dict[tuple[str, int], tuple[str | None, dict]] = {}
249
+ for source_path, line_offset, record_type, payload_json in conn.execute(
250
+ "SELECT source_path,line_offset,record_type,payload_json "
251
+ "FROM codex_conversation_events WHERE conversation_key = ?",
252
+ (conversation_key,),
253
+ ):
254
+ try:
255
+ obj = json.loads(payload_json or "{}")
256
+ except (json.JSONDecodeError, TypeError):
257
+ continue
258
+ payload = obj.get("payload") if isinstance(obj, dict) else None
259
+ if isinstance(payload, dict):
260
+ result[(source_path, line_offset)] = (record_type, payload)
261
+ return result
262
+
263
+
264
+ def _row_payload(row, payloads: dict) -> tuple[str | None, dict] | None:
265
+ return payloads.get((row.source_path, row.line_offset))
266
+
267
+
206
268
  def _row_display(row) -> str:
207
269
  """The row's display/search text from whichever column carries it."""
208
270
  return row.text or row.search_thinking or row.search_tool or ""
@@ -217,6 +279,12 @@ def _parse_detail(detail_json: str | None):
217
279
  return None
218
280
 
219
281
 
282
+ def _public_detail(detail):
283
+ if not isinstance(detail, dict):
284
+ return detail
285
+ return {key: value for key, value in detail.items() if not key.startswith("_")}
286
+
287
+
220
288
  def _item_kind(item: dict) -> str:
221
289
  klass = item["klass"]
222
290
  if klass == "prompt":
@@ -225,10 +293,31 @@ def _item_kind(item: dict) -> str:
225
293
  return "assistant"
226
294
  if klass == "event":
227
295
  return "event"
296
+ if klass == "meta":
297
+ return "meta"
228
298
  return item["anchor_row"].kind # unturned: the row's own provider kind
229
299
 
230
300
 
231
- def _item_blocks_with_rows(item: dict) -> list[list]:
301
+ def _item_meta(item: dict) -> dict | None:
302
+ if item["klass"] != "meta":
303
+ return None
304
+ detail = _parse_detail(item["anchor_row"].detail_json)
305
+ if not isinstance(detail, dict):
306
+ return {"meta_kind": "context", "meta_label": "role", "skill_name": None}
307
+ meta = {
308
+ "meta_kind": detail.get("meta_kind") or "context",
309
+ "meta_label": detail.get("meta_label") or "role",
310
+ "skill_name": detail.get("skill_name"),
311
+ }
312
+ sections = detail.get("meta_sections")
313
+ if isinstance(sections, list) and all(isinstance(value, str) for value in sections):
314
+ meta["meta_sections"] = sections
315
+ return meta
316
+
317
+
318
+ def _item_blocks_with_rows(
319
+ item: dict, payloads: dict | None = None, *, preserve_marker_text: bool = False,
320
+ ) -> list[list]:
232
321
  """Assemble an item's blocks (the historical ``_build_item_blocks`` behaviour)
233
322
  AND expose each block's underlying rows, so the detail renderer and the payload
234
323
  locator (§3.4) share ONE folding rule. Each entry is a 3-list
@@ -240,6 +329,12 @@ def _item_blocks_with_rows(item: dict) -> list[list]:
240
329
  Every ``tool_call`` block additionally carries an opaque ``block_key`` (§3.4) —
241
330
  the payload-capable anchor. Non-tool blocks carry no ``block_key``."""
242
331
  rows = item["rows"]
332
+ payloads = payloads or {}
333
+ lifecycle_positions = {
334
+ (row.source_path, row.line_offset) for row in item.get("lifecycle_rows", [])
335
+ }
336
+ row_order = {(row.source_path, row.line_offset): index
337
+ for index, row in enumerate(rows)}
243
338
  call_owner_count: dict[str, int] = {}
244
339
  for r in rows:
245
340
  if r.kind == "tool_call" and r.call_id:
@@ -247,33 +342,234 @@ def _item_blocks_with_rows(item: dict) -> list[list]:
247
342
  entries: list[list] = []
248
343
  tool_entry_by_call: dict[str, int] = {}
249
344
  for r in rows:
345
+ if (r.source_path, r.line_offset) in lifecycle_positions:
346
+ continue
250
347
  text = _row_display(r)
251
- detail = _parse_detail(r.detail_json)
348
+ stored_detail = _parse_detail(r.detail_json)
349
+ detail = _public_detail(stored_detail)
350
+ retained = _row_payload(r, payloads)
351
+ payload = retained[1] if retained is not None else None
352
+ if (preserve_marker_text and isinstance(stored_detail, dict)
353
+ and stored_detail.get("markers") and isinstance(payload, dict)):
354
+ if retained[0] == "response_item":
355
+ text = kern._join_content_texts(payload.get("content"))
356
+ elif retained[0] == "event_msg":
357
+ text = kern._stringify(payload.get("message"))
358
+ if r.kind == "tool_call" and isinstance(payload, dict):
359
+ card = kern.decode_tool_call_card(payload)
360
+ if card is None:
361
+ card = kern.decode_secondary_tool_call_card(payload)
362
+ if card is not None:
363
+ detail = dict(detail) if isinstance(detail, dict) else {}
364
+ detail["card"] = card
365
+ output_card = (kern.decode_tool_output_card(payload)
366
+ if r.kind == "tool_output" and isinstance(payload, dict)
367
+ else None)
368
+ if output_card is not None:
369
+ card, text = output_card
370
+ detail = dict(detail) if isinstance(detail, dict) else {}
371
+ detail["card"] = card
372
+ if r.kind == "event" and isinstance(payload, dict):
373
+ card = kern.decode_patch_event_card(payload)
374
+ if card is None:
375
+ card = kern.decode_secondary_event_card(payload)
376
+ if card is not None:
377
+ detail = dict(detail) if isinstance(detail, dict) else {}
378
+ detail["card"] = card
252
379
  if (r.kind == "tool_output" and r.call_id
253
380
  and call_owner_count.get(r.call_id, 0) == 1
254
381
  and r.call_id in tool_entry_by_call):
255
382
  owner = entries[tool_entry_by_call[r.call_id]]
383
+ if isinstance(detail, dict) and isinstance(detail.get("card"), dict):
384
+ call_card = (owner[0].get("detail") or {}).get("card")
385
+ if (detail["card"].get("status") == "unknown"
386
+ and isinstance(call_card, dict)
387
+ and isinstance(call_card.get("status"), str)):
388
+ detail["card"]["status"] = call_card["status"]
389
+ detail["card"]["is_error"] = call_card["status"] in {
390
+ "failed", "error"}
256
391
  owner[0]["output"] = {"text": text, "detail": detail}
392
+ owner_card = (owner[0].get("detail") or {}).get("card")
393
+ if isinstance(owner_card, dict) and owner_card.get("type") in {
394
+ "plan", "agent"} and isinstance(payload, dict):
395
+ result = kern.decode_secondary_tool_result(payload)
396
+ if result is not None:
397
+ owner_card["result"] = result
257
398
  owner[2] = r
258
399
  continue
259
400
  block = {
260
401
  "kind": r.kind, "text": text, "detail": detail,
261
402
  "call_id": r.call_id, "timestamp_utc": r.timestamp_utc,
262
403
  }
404
+ if (r.kind == "event" and r.event_type in {
405
+ "web_search_end", "mcp_tool_call_end", "task_started", "task_complete"}
406
+ or isinstance(stored_detail, dict) and stored_detail.get("markers")):
407
+ block["block_key"] = _block_key_for_row(r)
408
+ block["payload_which"] = "event"
263
409
  if r.kind == "tool_call":
264
410
  block["block_key"] = _block_key_for_row(r)
265
411
  if r.call_id and call_owner_count.get(r.call_id, 0) == 1:
266
412
  tool_entry_by_call[r.call_id] = len(entries)
267
413
  entries.append([block, r, None])
414
+
415
+ # A native patch completion may carry an inner call id distinct from the
416
+ # outer custom-tool call. Correlate only on unique same-id ownership, or on
417
+ # one strictly bracketed single-patch call (call < event < its output).
418
+ # Anything ambiguous remains its own event card.
419
+ patch_calls = [
420
+ (index, call_owner_count.get(entry[1].call_id, 0))
421
+ for index, entry in enumerate(entries)
422
+ if entry[1].kind == "tool_call"
423
+ and isinstance((entry[0].get("detail") or {}).get("card"), dict)
424
+ and entry[0]["detail"]["card"].get("type") == "patch"
425
+ ]
426
+ matched_calls: set[int] = set()
427
+ suppress_events: set[int] = set()
428
+ for event_index, event_entry in enumerate(entries):
429
+ event_block, event_row, _unused = event_entry
430
+ event_card = (event_block.get("detail") or {}).get("card")
431
+ if not (event_row.kind == "event" and isinstance(event_card, dict)
432
+ and event_card.get("source") == "patch_apply_end"):
433
+ continue
434
+ event_key = _block_key_for_row(event_row)
435
+ event_block["block_key"] = event_key
436
+ event_block["payload_which"] = "event"
437
+ same_id = [
438
+ index for index, owner_count in patch_calls
439
+ if index not in matched_calls and event_row.call_id
440
+ and owner_count == 1
441
+ and entries[index][1].call_id == event_row.call_id
442
+ ]
443
+ candidates = same_id if len(same_id) == 1 else []
444
+ if not candidates:
445
+ event_pos = row_order[(event_row.source_path, event_row.line_offset)]
446
+ candidates = []
447
+ for index, _owner_count in patch_calls:
448
+ if index in matched_calls:
449
+ continue
450
+ _call_block, call_row, output_row = entries[index]
451
+ call_pos = row_order[(call_row.source_path, call_row.line_offset)]
452
+ output_pos = (row_order.get((output_row.source_path, output_row.line_offset))
453
+ if output_row is not None else None)
454
+ if (call_row.source_path == event_row.source_path
455
+ and output_row is not None
456
+ and output_row.source_path == event_row.source_path
457
+ and call_pos < event_pos and output_pos is not None
458
+ and event_pos < output_pos):
459
+ candidates.append(index)
460
+ if len(candidates) != 1:
461
+ continue
462
+ owner_index = candidates[0]
463
+ owner_card = entries[owner_index][0]["detail"]["card"]
464
+ completion = dict(event_card)
465
+ completion["event_block_key"] = event_key
466
+ owner_card["completion"] = completion
467
+ matched_calls.add(owner_index)
468
+ suppress_events.add(event_index)
469
+ if suppress_events:
470
+ entries = [entry for index, entry in enumerate(entries)
471
+ if index not in suppress_events]
472
+
473
+ # Web/MCP completion events fold only by one exact same-turn call id. The
474
+ # kernel already uses this proof for logical item count; repeat it here to
475
+ # produce the additive card body while preserving payload selectors.
476
+ calls_by_id: dict[str, list[int]] = {}
477
+ for index, entry in enumerate(entries):
478
+ row = entry[1]
479
+ if row.kind == "tool_call" and row.call_id:
480
+ calls_by_id.setdefault(row.call_id, []).append(index)
481
+ suppress_secondary: set[int] = set()
482
+ matched_secondary: set[int] = set()
483
+ for event_index, event_entry in enumerate(entries):
484
+ event_block, event_row, _unused = event_entry
485
+ event_card = (event_block.get("detail") or {}).get("card")
486
+ if not (event_row.kind == "event" and event_row.call_id
487
+ and isinstance(event_card, dict)
488
+ and event_card.get("type") in {
489
+ "web_search_completion", "mcp_completion"}):
490
+ continue
491
+ candidates = [
492
+ index for index in calls_by_id.get(event_row.call_id, [])
493
+ if index not in matched_secondary
494
+ and entries[index][1].turn_id == event_row.turn_id
495
+ ]
496
+ if event_card.get("type") == "web_search_completion":
497
+ candidates = [
498
+ index for index in candidates
499
+ if ((entries[index][0].get("detail") or {}).get("name")
500
+ == "web_search_call")
501
+ ]
502
+ if len(candidates) != 1:
503
+ event_block["block_key"] = _block_key_for_row(event_row)
504
+ event_block["payload_which"] = "event"
505
+ continue
506
+ owner_index = candidates[0]
507
+ owner_block = entries[owner_index][0]
508
+ owner_detail = owner_block.get("detail")
509
+ if not isinstance(owner_detail, dict):
510
+ owner_detail = {}
511
+ owner_block["detail"] = owner_detail
512
+ owner_card = owner_detail.get("card")
513
+ if event_card.get("type") == "mcp_completion":
514
+ if not isinstance(owner_card, dict) or owner_card.get("type") != "mcp":
515
+ owner_card = {
516
+ "schema_version": kern.CODEX_CARD_SCHEMA_VERSION,
517
+ "type": "mcp", "source": "function_call",
518
+ "call_status": "requested",
519
+ "name": owner_detail.get("name"),
520
+ }
521
+ owner_retained = _row_payload(entries[owner_index][1], payloads)
522
+ owner_payload = owner_retained[1] if owner_retained is not None else None
523
+ if isinstance(owner_payload, dict) and isinstance(
524
+ owner_payload.get("status"), str):
525
+ owner_card["call_status"] = owner_payload["status"]
526
+ owner_detail["card"] = owner_card
527
+ if not isinstance(owner_card, dict):
528
+ event_block["block_key"] = _block_key_for_row(event_row)
529
+ event_block["payload_which"] = "event"
530
+ continue
531
+ owner_card["completion"] = {
532
+ key: value for key, value in event_card.items()
533
+ if key not in {"schema_version", "type", "source"}
534
+ }
535
+ owner_card["completion"]["event_block_key"] = _block_key_for_row(event_row)
536
+ matched_secondary.add(owner_index)
537
+ suppress_secondary.add(event_index)
538
+ if suppress_secondary:
539
+ entries = [entry for index, entry in enumerate(entries)
540
+ if index not in suppress_secondary]
268
541
  return entries
269
542
 
270
543
 
271
- def _build_item_blocks(item: dict) -> list[dict]:
544
+ def _build_item_blocks(
545
+ item: dict, payloads: dict | None = None, *, preserve_marker_text: bool = False,
546
+ ) -> list[dict]:
272
547
  """Assemble an item's blocks, folding each ``tool_output`` into its
273
548
  ``tool_call`` block via ``call_id`` when that call_id has exactly one owner
274
549
  (§5.2). Physical order within the item is preserved. Thin projection of
275
550
  ``_item_blocks_with_rows`` — the single source of truth for the folding rule."""
276
- return [entry[0] for entry in _item_blocks_with_rows(item)]
551
+ return [entry[0] for entry in _item_blocks_with_rows(
552
+ item, payloads, preserve_marker_text=preserve_marker_text)]
553
+
554
+
555
+ def _item_lifecycle(item: dict) -> dict | None:
556
+ lifecycle = item.get("lifecycle")
557
+ if not isinstance(lifecycle, dict):
558
+ return None
559
+ result = {
560
+ key: (dict(value) if isinstance(value, dict) else value)
561
+ for key, value in lifecycle.items()
562
+ }
563
+ result["events"] = [
564
+ {
565
+ "event": (_parse_detail(row.detail_json) or {}).get(
566
+ "lifecycle", {}).get("event"),
567
+ "payload_which": "event",
568
+ "block_key": _block_key_for_row(row),
569
+ }
570
+ for row in item.get("lifecycle_rows", [])
571
+ ]
572
+ return result
277
573
 
278
574
 
279
575
  # ── tokens union (§5.6) ───────────────────────────────────────────────────────
@@ -305,44 +601,27 @@ def _tokens_union(tokens: dict) -> dict:
305
601
  # ── cost attribution (§5.4) ───────────────────────────────────────────────────
306
602
 
307
603
 
308
- def _file_boundaries(conn: sqlite3.Connection, source_path: str) -> list[tuple[int, str | None]]:
309
- """Ordered ``(line_offset, active_turn)`` boundaries for one file — a
310
- ``turn_context`` sets the effective turn, a ``session_meta`` resets it to
311
- ``None`` (a new un-turned segment). Read from the retained physical events so
312
- an accounting row that precedes the first normalized message still attributes
313
- to the turn ``turn_context`` already opened (§5.4)."""
314
- boundaries: list[tuple[int, str | None]] = []
315
- for off, rtype, tid, payload_json in conn.execute(
316
- "SELECT line_offset, record_type, turn_id, payload_json "
317
- "FROM codex_conversation_events "
318
- "WHERE source_path = ? AND record_type IN ('turn_context','session_meta') "
319
- "ORDER BY line_offset",
320
- (source_path,),
321
- ):
322
- if rtype == "session_meta":
323
- boundaries.append((off, None))
324
- continue
325
- turn = tid
326
- if turn is None:
327
- try:
328
- payload = (json.loads(payload_json or "{}").get("payload") or {})
329
- turn = payload.get("turn_id")
330
- except (json.JSONDecodeError, TypeError, AttributeError):
331
- turn = None
332
- boundaries.append((off, turn))
333
- return boundaries
334
-
335
-
336
- def _turn_at(boundaries: list[tuple[int, str | None]], offset: int) -> str | None:
337
- """Nearest preceding turn boundary by file offset (§5.4); ``None`` when the
338
- row precedes any turn (or sits in an un-turned segment)."""
339
- turn: str | None = None
340
- for boff, bturn in boundaries:
341
- if boff <= offset:
342
- turn = bturn
343
- else:
344
- break
345
- return turn
604
+ def _file_turn_map(conn: sqlite3.Connection, source_path: str) -> dict[int, str | None]:
605
+ """Exact physical-offset canonical logical turn for one retained file.
606
+
607
+ Cost attribution and normalized prose use the same pure lifecycle inference,
608
+ including resumed segments whose native proof arrives on task completion.
609
+ """
610
+ from _lib_jsonl import CodexPhysicalEvent
611
+
612
+ events = [
613
+ CodexPhysicalEvent(*row)
614
+ for row in conn.execute(
615
+ "SELECT source_path, line_offset, source_root_key, conversation_key, "
616
+ "native_thread_id, root_thread_id, parent_thread_id, timestamp_utc, "
617
+ "record_type, event_type, turn_id, call_id, payload_json "
618
+ "FROM codex_conversation_events WHERE source_path = ? "
619
+ "ORDER BY line_offset",
620
+ (source_path,),
621
+ )
622
+ ]
623
+ turns, _terminal = kern.infer_codex_event_turns(events)
624
+ return {event.line_offset: turn for event, turn in zip(events, turns)}
346
625
 
347
626
 
348
627
  def _attribute_costs(conn: sqlite3.Connection, conversation_key: str, effective_speed: str):
@@ -358,9 +637,9 @@ def _attribute_costs(conn: sqlite3.Connection, conversation_key: str, effective_
358
637
  "WHERE conversation_key = ? ORDER BY source_path, line_offset",
359
638
  (conversation_key,),
360
639
  ).fetchall()
361
- boundaries: dict[str, list] = {}
640
+ turn_maps: dict[str, dict[int, str | None]] = {}
362
641
  for source_path in {e[0] for e in entries}:
363
- boundaries[source_path] = _file_boundaries(conn, source_path)
642
+ turn_maps[source_path] = _file_turn_map(conn, source_path)
364
643
  turn_cost: dict[str, float] = {}
365
644
  turn_tokens: dict[str, dict] = {}
366
645
  unattr_cost = 0.0
@@ -372,7 +651,7 @@ def _attribute_costs(conn: sqlite3.Connection, conversation_key: str, effective_
372
651
  model or "", inp or 0, cin or 0, out or 0, rout or 0, speed=effective_speed)
373
652
  total += priced
374
653
  _add_tokens(conv_tokens, inp, out, cin, rout)
375
- turn = _turn_at(boundaries.get(source_path, []), offset)
654
+ turn = turn_maps.get(source_path, {}).get(offset)
376
655
  if turn is not None:
377
656
  turn_cost[turn] = turn_cost.get(turn, 0.0) + priced
378
657
  _add_tokens(turn_tokens.setdefault(turn, _zero_tokens()), inp, out, cin, rout)
@@ -547,6 +826,109 @@ def _parent_of(conn: sqlite3.Connection, conversation_key: str):
547
826
  return {"conversation_key": parent_ck, "title": _conversation_display_title(conn, parent_ck)}
548
827
 
549
828
 
829
+ def _consistent_meta_value(direct, nested):
830
+ present = [value for value in (direct, nested) if value is not None]
831
+ if any(not isinstance(value, str) or not value for value in present):
832
+ return False
833
+ values = present
834
+ if not values:
835
+ return None
836
+ return values[0] if len(set(values)) == 1 else False
837
+
838
+
839
+ def _agent_session_meta(payload: dict) -> dict | None:
840
+ """Extract current retained child facts without persisting or exposing paths."""
841
+ source = payload.get("source")
842
+ subagent = source.get("subagent") if isinstance(source, dict) else None
843
+ spawn = subagent.get("thread_spawn") if isinstance(subagent, dict) else None
844
+ spawn = spawn if isinstance(spawn, dict) else {}
845
+ parent = _consistent_meta_value(
846
+ payload.get("parent_thread_id"), spawn.get("parent_thread_id"))
847
+ agent_path = _consistent_meta_value(payload.get("agent_path"), spawn.get("agent_path"))
848
+ if parent is False or agent_path is False or not parent or not agent_path:
849
+ return None
850
+ return {
851
+ "parent_thread_id": parent,
852
+ "agent_path": agent_path,
853
+ "role": _consistent_meta_value(payload.get("agent_role"), spawn.get("agent_role")),
854
+ "nickname": _consistent_meta_value(
855
+ payload.get("agent_nickname"), spawn.get("agent_nickname")),
856
+ }
857
+
858
+
859
+ def _safe_agent_label(value) -> str | None:
860
+ if not isinstance(value, str):
861
+ return None
862
+ value = " ".join(value.split())
863
+ if not value or len(value) > 120 or "/" in value or "\\" in value:
864
+ return None
865
+ return value
866
+
867
+
868
+ def _spawn_child_link(
869
+ conn: sqlite3.Connection, conversation_key: str, canonical_task_name: str,
870
+ ) -> dict | None:
871
+ """Return one opaque same-root child only on exact retained identity proof."""
872
+ thread = _thread_facts(conn, conversation_key)
873
+ if thread is None:
874
+ return None
875
+ native, _root, _parent, source_root_key, _cwd, _git = thread
876
+ if not native or not source_root_key:
877
+ return None
878
+ matches: dict[str, list[dict]] = {}
879
+ for child_key, payload_json in conn.execute(
880
+ "SELECT conversation_key,payload_json FROM codex_conversation_events "
881
+ "WHERE source_root_key=? AND record_type='session_meta' "
882
+ "AND conversation_key IS NOT NULL AND conversation_key != ?",
883
+ (source_root_key, conversation_key),
884
+ ):
885
+ try:
886
+ record = json.loads(payload_json or "{}")
887
+ except (json.JSONDecodeError, TypeError):
888
+ continue
889
+ payload = record.get("payload") if isinstance(record, dict) else None
890
+ facts = _agent_session_meta(payload) if isinstance(payload, dict) else None
891
+ if not (facts and facts["parent_thread_id"] == native
892
+ and facts["agent_path"] == canonical_task_name):
893
+ continue
894
+ if not codex_conversation_exists(conn, child_key):
895
+ continue
896
+ matches.setdefault(child_key, []).append(facts)
897
+ if len(matches) != 1:
898
+ return None
899
+ child_key, facts_list = next(iter(matches.items()))
900
+ link = {"conversation_key": child_key}
901
+ for field in ("role", "nickname"):
902
+ labels = {_safe_agent_label(facts.get(field)) for facts in facts_list}
903
+ labels.discard(None)
904
+ if len(labels) == 1:
905
+ link[field] = next(iter(labels))
906
+ return link
907
+
908
+
909
+ def _attach_spawn_child_links(
910
+ conn: sqlite3.Connection, conversation_key: str, items: list[dict],
911
+ ) -> None:
912
+ cache: dict[str, dict | None] = {}
913
+ for item in items:
914
+ for block in item.get("blocks", []):
915
+ detail = block.get("detail")
916
+ card = detail.get("card") if isinstance(detail, dict) else None
917
+ if not (isinstance(card, dict) and card.get("type") == "agent"
918
+ and card.get("operation") == "spawn_agent"):
919
+ continue
920
+ result = card.get("result")
921
+ value = result.get("value") if isinstance(result, dict) else None
922
+ task_name = value.get("task_name") if isinstance(value, dict) else None
923
+ if not isinstance(task_name, str) or not task_name:
924
+ continue
925
+ if task_name not in cache:
926
+ cache[task_name] = _spawn_child_link(
927
+ conn, conversation_key, task_name)
928
+ if cache[task_name] is not None:
929
+ card["child_conversation"] = cache[task_name]
930
+
931
+
550
932
  def codex_conversation_exists(conn: sqlite3.Connection, conversation_key: str) -> bool:
551
933
  """Cheap existence probe (spec §5.2) — True iff any normalized
552
934
  ``codex_conversation_messages`` row carries ``conversation_key``. Used by the
@@ -611,11 +993,20 @@ def codex_conversation_source_paths(
611
993
 
612
994
  def _paginate_items(items: list[dict], *, after, before, tail, limit):
613
995
  keys = [it["item_key"] for it in items]
996
+ aliases = {
997
+ alias: index
998
+ for index, item in enumerate(items)
999
+ for alias in item.get("member_item_keys", [])
1000
+ }
614
1001
  lo, hi = 0, len(items)
615
1002
  if after is not None and after in keys:
616
1003
  lo = keys.index(after) + 1
1004
+ elif after is not None and after in aliases:
1005
+ lo = aliases[after] + 1
617
1006
  if before is not None and before in keys:
618
1007
  hi = keys.index(before)
1008
+ elif before is not None and before in aliases:
1009
+ hi = aliases[before]
619
1010
  window = items[lo:hi]
620
1011
  if tail is not None:
621
1012
  cap = min(tail, limit) if limit else tail
@@ -644,6 +1035,7 @@ def get_codex_conversation(
644
1035
  before: str | None = None,
645
1036
  tail: int | None = None,
646
1037
  limit: int = 200,
1038
+ legacy_export: bool = False,
647
1039
  ) -> dict:
648
1040
  """Detail envelope (§5.6): status ``ok`` | ``normalization_pending`` |
649
1041
  ``not_found``. ``ok`` carries canonical items (mirror-paired, tool-folded),
@@ -656,7 +1048,23 @@ def get_codex_conversation(
656
1048
  if not rows:
657
1049
  return {"status": "not_found", "conversation_key": conversation_key}
658
1050
  kept, _suppressed = kern.pair_mirrors(rows)
659
- items = kern.canonical_items(kept)
1051
+ items = kern.canonical_items(
1052
+ kept, fold_patch_completions=not legacy_export)
1053
+ # Detail/API callers receive the exact card display projection from retained
1054
+ # provider payloads. Export deliberately renders the byte-frozen legacy text
1055
+ # while retaining the same additive card metadata.
1056
+ payloads = _load_row_payloads(conn, conversation_key)
1057
+ if legacy_export:
1058
+ marker_positions = {
1059
+ (row.source_path, row.line_offset)
1060
+ for row in rows
1061
+ if isinstance(_parse_detail(row.detail_json), dict)
1062
+ and bool(_parse_detail(row.detail_json).get("markers"))
1063
+ }
1064
+ payloads = {
1065
+ position: retained for position, retained in payloads.items()
1066
+ if position in marker_positions
1067
+ }
660
1068
  turn_cost, turn_tokens, unattr_cost, unattr_tokens, total, conv_tokens = _attribute_costs(
661
1069
  conn, conversation_key, effective_speed)
662
1070
  # Carrier item per turn: prefer the response item, else the first item of the
@@ -682,15 +1090,25 @@ def get_codex_conversation(
682
1090
  if turn is not None and carriers.get(turn) == idx and turn in turn_cost:
683
1091
  cost = turn_cost[turn]
684
1092
  tokens = _tokens_union(turn_tokens[turn])
685
- built.append({
1093
+ item = {
686
1094
  "item_key": _item_key_for_item(conversation_key, it),
1095
+ "member_item_keys": _member_item_keys(conversation_key, it),
687
1096
  "kind": _item_kind(it),
688
1097
  "timestamp_utc": it["anchor_row"].timestamp_utc,
689
1098
  "model": it["anchor_row"].model,
690
- "blocks": _build_item_blocks(it),
1099
+ "blocks": _build_item_blocks(
1100
+ it, payloads, preserve_marker_text=legacy_export),
691
1101
  "cost_usd": cost,
692
1102
  "tokens": tokens,
693
- })
1103
+ }
1104
+ meta = _item_meta(it)
1105
+ if meta is not None:
1106
+ item.update(meta)
1107
+ lifecycle = _item_lifecycle(it)
1108
+ if lifecycle is not None:
1109
+ item["lifecycle"] = lifecycle
1110
+ built.append(item)
1111
+ _attach_spawn_child_links(conn, conversation_key, built)
694
1112
  page_items, page = _paginate_items(built, after=after, before=before, tail=tail, limit=limit)
695
1113
  return {
696
1114
  "status": "ok",
@@ -737,18 +1155,26 @@ def get_codex_conversation_outline(
737
1155
  turns: list[dict] = []
738
1156
  kind_totals: dict[str, int] = {}
739
1157
  for it in items:
1158
+ meta = _item_meta(it)
740
1159
  anchor_text = _row_display(it["anchor_row"])
741
- label = _first_nonblank_line(_strip_ansi(anchor_text)) if anchor_text else ""
1160
+ if meta is not None:
1161
+ label = _META_LABEL_TEXT.get(meta["meta_label"], "Harness context")
1162
+ else:
1163
+ label = _first_nonblank_line(_strip_ansi(anchor_text)) if anchor_text else ""
742
1164
  kinds: dict[str, int] = {}
743
1165
  for r in it["rows"]:
744
1166
  kinds[r.kind] = kinds.get(r.kind, 0) + 1
745
1167
  kind_totals[r.kind] = kind_totals.get(r.kind, 0) + 1
746
- turns.append({
1168
+ turn = {
747
1169
  "item_key": _item_key_for_item(conversation_key, it),
1170
+ "member_item_keys": _member_item_keys(conversation_key, it),
748
1171
  "label": label,
749
1172
  "timestamp_utc": it["anchor_row"].timestamp_utc,
750
1173
  "kinds": kinds,
751
- })
1174
+ }
1175
+ if meta is not None:
1176
+ turn.update(meta)
1177
+ turns.append(turn)
752
1178
  return {
753
1179
  "status": "ok",
754
1180
  "conversation_key": conversation_key,
@@ -1359,9 +1785,29 @@ def _reread_codex_full_content(conn: sqlite3.Connection, row):
1359
1785
  extracted = kern._extract(record_type, payload)
1360
1786
  if extracted is None:
1361
1787
  return None
1362
- content = extracted.content_text or ""
1788
+ card = None
1789
+ if record_type == "event_msg" and payload.get("type") in {
1790
+ "patch_apply_end", "web_search_end", "mcp_tool_call_end",
1791
+ "task_started", "task_complete"}:
1792
+ content = kern._canonical_json(payload)
1793
+ card = (kern.decode_patch_event_card(
1794
+ payload, text_cap=_FULL_PAYLOAD_CEILING)
1795
+ or kern.decode_secondary_event_card(
1796
+ payload, text_cap=_FULL_PAYLOAD_CEILING))
1797
+ else:
1798
+ content = (extracted.identity_text if extracted.identity_text is not None
1799
+ else extracted.content_text or "")
1800
+ if record_type == "response_item" and payload.get("type") in kern._RESPONSE_TOOL_CALLS:
1801
+ card = (kern.decode_tool_call_card(
1802
+ payload, text_cap=_FULL_PAYLOAD_CEILING)
1803
+ or kern.decode_secondary_tool_call_card(
1804
+ payload, text_cap=_FULL_PAYLOAD_CEILING))
1805
+ elif record_type == "response_item" and payload.get("type") in kern._RESPONSE_TOOL_OUTPUTS:
1806
+ shaped = kern.decode_tool_output_card(
1807
+ payload, text_cap=_FULL_PAYLOAD_CEILING)
1808
+ card = shaped[0] if shaped is not None else None
1363
1809
  truncated = len(content) > _FULL_PAYLOAD_CEILING
1364
- return content[:_FULL_PAYLOAD_CEILING], truncated
1810
+ return content[:_FULL_PAYLOAD_CEILING], truncated, card
1365
1811
 
1366
1812
 
1367
1813
  def _locate_payload_block(conn: sqlite3.Connection, conversation_key: str, block_key: str):
@@ -1372,14 +1818,27 @@ def _locate_payload_block(conn: sqlite3.Connection, conversation_key: str, block
1372
1818
  rows = _load_conversation_rows(conn, conversation_key)
1373
1819
  if not rows:
1374
1820
  return None
1821
+ # Event/marker payload keys are row-scoped and remain addressable even when
1822
+ # their bounded presentation folds into a proven owning interaction.
1823
+ for row in rows:
1824
+ detail = _parse_detail(row.detail_json)
1825
+ payload_addressable = (
1826
+ row.kind == "event" and row.event_type in {
1827
+ "patch_apply_end", "web_search_end", "mcp_tool_call_end",
1828
+ "task_started", "task_complete"}
1829
+ or isinstance(detail, dict) and bool(detail.get("markers"))
1830
+ )
1831
+ if payload_addressable and _block_key_for_row(row) == block_key:
1832
+ return {"event": row}
1375
1833
  kept, _suppressed = kern.pair_mirrors(rows)
1376
1834
  items = kern.canonical_items(kept)
1835
+ payloads = _load_row_payloads(conn, conversation_key)
1377
1836
  for item in items:
1378
- for _block, call_row, output_row in _item_blocks_with_rows(item):
1837
+ for _block, call_row, output_row in _item_blocks_with_rows(item, payloads):
1379
1838
  if call_row.kind != "tool_call":
1380
1839
  continue
1381
1840
  if _block_key_for_row(call_row) == block_key:
1382
- return call_row, output_row
1841
+ return {"call": call_row, "output": output_row}
1383
1842
  return None
1384
1843
 
1385
1844
 
@@ -1388,22 +1847,22 @@ def read_codex_payload(
1388
1847
  ) -> dict:
1389
1848
  """Locate + full re-read for a Codex detail payload block (§3.4).
1390
1849
 
1391
- Selector: ``block_key`` (required) + ``which ∈ {call, output}``. A call-id-less
1850
+ Selector: ``block_key`` (required) + ``which ∈ {call, output, event}``. A call-id-less
1392
1851
  (or unpaired) call is call-only — ``which=output`` for it → ``not_found`` (no
1393
1852
  adjacency pairing is introduced). Success envelope (pinned):
1394
1853
  ``{"status":"ok","block_key","which","content","truncated"}`` where ``content``
1395
- is the selected side's full text from the re-read record and ``truncated``
1396
- reflects ``_FULL_PAYLOAD_CEILING``. ``gone`` (→ HTTP 410) means the physical
1854
+ is the selected side's full text from the re-read record, patch events add a
1855
+ full bounded ``card``, and ``truncated`` reflects ``_FULL_PAYLOAD_CEILING``.
1856
+ ``gone`` (→ HTTP 410) means the physical
1397
1857
  record moved/mutated; ``not_found`` (→ 404) means no such block, no output
1398
1858
  partner, or a containment miss (a read is never attempted outside the root)."""
1399
1859
  miss = {"status": "not_found", "block_key": block_key, "which": which}
1400
- if which not in ("call", "output"):
1860
+ if which not in ("call", "output", "event"):
1401
1861
  return miss
1402
1862
  located = _locate_payload_block(conn, conversation_key, block_key)
1403
1863
  if located is None:
1404
1864
  return miss
1405
- call_row, output_row = located
1406
- target = call_row if which == "call" else output_row
1865
+ target = located.get(which)
1407
1866
  if target is None: # which=output for a call-id-less / unpaired call
1408
1867
  return miss
1409
1868
  # Containment guard (Codex-only; the Claude path has no equivalent) BEFORE any
@@ -1414,9 +1873,12 @@ def read_codex_payload(
1414
1873
  outcome = _reread_codex_full_content(conn, target)
1415
1874
  if outcome is None:
1416
1875
  return {"status": "gone", "block_key": block_key, "which": which}
1417
- content, truncated = outcome
1418
- return {"status": "ok", "block_key": block_key, "which": which,
1419
- "content": content, "truncated": truncated}
1876
+ content, truncated, card = outcome
1877
+ response = {"status": "ok", "block_key": block_key, "which": which,
1878
+ "content": content, "truncated": truncated}
1879
+ if card is not None:
1880
+ response["card"] = card
1881
+ return response
1420
1882
 
1421
1883
 
1422
1884
  # ── whole-conversation export (§3.3) ──────────────────────────────────────────
@@ -1430,7 +1892,8 @@ def get_codex_conversation_export(
1430
1892
  module. Status-tagged: ``ok`` (carries ``markdown``) | ``normalization_pending``
1431
1893
  | ``not_found`` — the dispatch/transport layers map those to bytes/HTTP."""
1432
1894
  detail = get_codex_conversation(
1433
- conn, conversation_key, effective_speed=effective_speed, limit=0)
1895
+ conn, conversation_key, effective_speed=effective_speed, limit=0,
1896
+ legacy_export=True)
1434
1897
  if detail.get("status") != "ok":
1435
1898
  return {"status": detail.get("status"), "conversation_key": conversation_key}
1436
1899
  from _lib_codex_conversation_export import render_codex_conversation_markdown