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.
- package/CHANGELOG.md +13 -0
- package/README.md +2 -0
- package/bin/_cctally_cache.py +140 -2
- package/bin/_cctally_config.py +50 -0
- package/bin/_cctally_dashboard.py +175 -20
- package/bin/_cctally_dashboard_conversation.py +1 -1
- package/bin/_cctally_dashboard_envelope.py +12 -0
- package/bin/_cctally_dashboard_sources.py +15 -1
- package/bin/_cctally_doctor.py +16 -1
- package/bin/_cctally_milestone_history.py +845 -0
- package/bin/_cctally_tui.py +23 -0
- package/bin/_cctally_update.py +338 -15
- package/bin/_cctally_weekrefs.py +6 -2
- package/bin/_lib_codex_conversation.py +1342 -18
- package/bin/_lib_codex_conversation_export.py +21 -1
- package/bin/_lib_codex_conversation_query.py +530 -67
- package/bin/_lib_conversation_dispatch.py +2 -2
- package/bin/_lib_doctor.py +38 -0
- package/bin/_lib_milestone_history.py +158 -0
- package/bin/cctally +31 -0
- package/dashboard/static/assets/index-BwvAcS_k.js +92 -0
- package/dashboard/static/assets/index-CrIlpAlQ.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +3 -1
- package/dashboard/static/assets/index-CkTe6VJt.js +0 -87
- package/dashboard/static/assets/index-DsEXuMpq.css +0 -1
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
309
|
-
"""
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
640
|
+
turn_maps: dict[str, dict[int, str | None]] = {}
|
|
362
641
|
for source_path in {e[0] for e in entries}:
|
|
363
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1419
|
-
|
|
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
|