codex-flow 2.1.13__py3-none-any.whl

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.
Files changed (113) hide show
  1. codex_flow/__init__.py +28 -0
  2. codex_flow/__main__.py +9 -0
  3. codex_flow/cli.py +242 -0
  4. codex_flow/data/LICENSE +21 -0
  5. codex_flow/data/README.en.md +303 -0
  6. codex_flow/data/README.md +305 -0
  7. codex_flow/data/VERSION +1 -0
  8. codex_flow/data/apps/chatgpt-mcp/README.md +86 -0
  9. codex_flow/data/apps/chatgpt-mcp/__init__.py +1 -0
  10. codex_flow/data/apps/chatgpt-mcp/adapter.py +458 -0
  11. codex_flow/data/apps/chatgpt-mcp/server.py +358 -0
  12. codex_flow/data/apps/chatgpt-mcp/widget.html +927 -0
  13. codex_flow/data/apps/macos-overlay/README.en.md +121 -0
  14. codex_flow/data/apps/macos-overlay/README.md +123 -0
  15. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayRuntimeState.swift +126 -0
  16. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayScreenGeometry.swift +82 -0
  17. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayWindowController.swift +1052 -0
  18. codex_flow/data/apps/macos-overlay/Sources/Localization.swift +197 -0
  19. codex_flow/data/apps/macos-overlay/Sources/Models/TelemetryData.swift +1557 -0
  20. codex_flow/data/apps/macos-overlay/Sources/Services/AccountSnapshotService.swift +1101 -0
  21. codex_flow/data/apps/macos-overlay/Sources/Services/FlowPilotInstanceLock.swift +153 -0
  22. codex_flow/data/apps/macos-overlay/Sources/Services/IPCServer.swift +298 -0
  23. codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryQueryEngine.swift +800 -0
  24. codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryWatcher.swift +135 -0
  25. codex_flow/data/apps/macos-overlay/Sources/Services/UpdateService.swift +610 -0
  26. codex_flow/data/apps/macos-overlay/Sources/Views/AccountView.swift +610 -0
  27. codex_flow/data/apps/macos-overlay/Sources/Views/AnalyticsView.swift +566 -0
  28. codex_flow/data/apps/macos-overlay/Sources/Views/AutostartView.swift +293 -0
  29. codex_flow/data/apps/macos-overlay/Sources/Views/BubbleView.swift +317 -0
  30. codex_flow/data/apps/macos-overlay/Sources/Views/HistoryView.swift +1124 -0
  31. codex_flow/data/apps/macos-overlay/Sources/Views/HoverRevealText.swift +165 -0
  32. codex_flow/data/apps/macos-overlay/Sources/Views/InspectorSkillsToolsView.swift +121 -0
  33. codex_flow/data/apps/macos-overlay/Sources/Views/LogoView.swift +182 -0
  34. codex_flow/data/apps/macos-overlay/Sources/Views/SleekSwitch.swift +117 -0
  35. codex_flow/data/apps/macos-overlay/Sources/Views/StrategyModeView.swift +561 -0
  36. codex_flow/data/apps/macos-overlay/Sources/Views/SummaryView.swift +1273 -0
  37. codex_flow/data/apps/macos-overlay/Sources/Views/UpdateView.swift +352 -0
  38. codex_flow/data/apps/macos-overlay/Sources/main.swift +340 -0
  39. codex_flow/data/apps/macos-overlay/Tests/OverlayScreenGeometryTests.swift +163 -0
  40. codex_flow/data/apps/macos-overlay/Tests/TelemetryPhase1ContractTests.swift +357 -0
  41. codex_flow/data/apps/macos-overlay/Tests/TelemetryQueryEngineConcurrencyTests.swift +221 -0
  42. codex_flow/data/apps/macos-overlay/Tests/TelemetryQuotaSelectionTests.swift +158 -0
  43. codex_flow/data/apps/macos-overlay/Tests/TelemetryWorkerTokenTests.swift +122 -0
  44. codex_flow/data/apps/macos-overlay/build.sh +75 -0
  45. codex_flow/data/benchmark/corpus.json +103 -0
  46. codex_flow/data/benchmark/manifest.example.json +41 -0
  47. codex_flow/data/benchmark/manifest.schema.json +137 -0
  48. codex_flow/data/benchmark/prices/gpt-5.6-2026-08-30.json +5 -0
  49. codex_flow/data/benchmark/profiles.json +90 -0
  50. codex_flow/data/benchmark/schema.json +77 -0
  51. codex_flow/data/benchmark/tasks.json +50 -0
  52. codex_flow/data/completions/codex-flow.bash +34 -0
  53. codex_flow/data/completions/codex-flow.zsh +52 -0
  54. codex_flow/data/glama.json +6 -0
  55. codex_flow/data/install-release.ps1 +126 -0
  56. codex_flow/data/install-release.sh +155 -0
  57. codex_flow/data/install.ps1 +349 -0
  58. codex_flow/data/install.sh +362 -0
  59. codex_flow/data/policy/benchmark.toml +49 -0
  60. codex_flow/data/policy/defaults.toml +70 -0
  61. codex_flow/data/scripts/analyze-benchmark.py +510 -0
  62. codex_flow/data/scripts/benchmark-local.py +171 -0
  63. codex_flow/data/scripts/check-recommendation.py +277 -0
  64. codex_flow/data/scripts/doctor.py +449 -0
  65. codex_flow/data/scripts/generate-release-manifest.py +74 -0
  66. codex_flow/data/scripts/localization.py +192 -0
  67. codex_flow/data/scripts/manage-hooks.py +448 -0
  68. codex_flow/data/scripts/manage-instructions.py +389 -0
  69. codex_flow/data/scripts/manage-shell.py +151 -0
  70. codex_flow/data/scripts/materialize-corpus.py +193 -0
  71. codex_flow/data/scripts/menu.py +646 -0
  72. codex_flow/data/scripts/migrations/0001_update_settings.py +80 -0
  73. codex_flow/data/scripts/package-release.py +132 -0
  74. codex_flow/data/scripts/render-benchmark-report.py +292 -0
  75. codex_flow/data/scripts/run-benchmark.py +829 -0
  76. codex_flow/data/scripts/strategies/__init__.py +28 -0
  77. codex_flow/data/scripts/strategies/balanced.py +115 -0
  78. codex_flow/data/scripts/strategies/base.py +363 -0
  79. codex_flow/data/scripts/strategies/efficient.py +158 -0
  80. codex_flow/data/scripts/strategies/lifecycle_runtime.py +590 -0
  81. codex_flow/data/scripts/strategies/quality.py +209 -0
  82. codex_flow/data/scripts/strategies/speed.py +108 -0
  83. codex_flow/data/scripts/strategies/task_budget_runtime.py +644 -0
  84. codex_flow/data/scripts/strategies/task_phase_runtime.py +341 -0
  85. codex_flow/data/scripts/strategies/work_unit_runtime.py +421 -0
  86. codex_flow/data/scripts/strategy_runtime.py +1091 -0
  87. codex_flow/data/scripts/telemetry.py +400 -0
  88. codex_flow/data/scripts/telemetry_core/__init__.py +192 -0
  89. codex_flow/data/scripts/telemetry_core/app_server.py +1192 -0
  90. codex_flow/data/scripts/telemetry_core/collector.py +1247 -0
  91. codex_flow/data/scripts/telemetry_core/common.py +421 -0
  92. codex_flow/data/scripts/telemetry_core/latency.py +593 -0
  93. codex_flow/data/scripts/telemetry_core/query.py +427 -0
  94. codex_flow/data/scripts/telemetry_core/quota_ledger.py +598 -0
  95. codex_flow/data/scripts/telemetry_core/render.py +460 -0
  96. codex_flow/data/scripts/telemetry_core/repair.py +223 -0
  97. codex_flow/data/scripts/ui.py +266 -0
  98. codex_flow/data/scripts/update-homebrew-formula.py +146 -0
  99. codex_flow/data/scripts/update_runtime_config.py +134 -0
  100. codex_flow/data/scripts/updater.py +1718 -0
  101. codex_flow/data/smithery.yaml +18 -0
  102. codex_flow/data/templates/agents/worker-explorer.toml +24 -0
  103. codex_flow/data/templates/agents/worker-implementer.toml +49 -0
  104. codex_flow/data/templates/agents/worker-reviewer.toml +25 -0
  105. codex_flow/data/templates/flow-pilot-instructions.md +35 -0
  106. codex_flow/data/templates/skills/flow-pilot/SKILL.md +577 -0
  107. codex_flow/mcp.py +35 -0
  108. codex_flow-2.1.13.dist-info/METADATA +342 -0
  109. codex_flow-2.1.13.dist-info/RECORD +113 -0
  110. codex_flow-2.1.13.dist-info/WHEEL +5 -0
  111. codex_flow-2.1.13.dist-info/entry_points.txt +3 -0
  112. codex_flow-2.1.13.dist-info/licenses/LICENSE +21 -0
  113. codex_flow-2.1.13.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1247 @@
1
+ """Telemetry hook event collector, worker correlation, maintenance, and notifications."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from .app_server import (
14
+ AppServer,
15
+ apply_participant_metadata,
16
+ find_session_transcript,
17
+ merge_thread_metadata,
18
+ merge_usage,
19
+ quota_delta,
20
+ quota_windows,
21
+ session_index_metadata,
22
+ transcript_turn_started_at,
23
+ transcript_turn_usage,
24
+ usage_delta,
25
+ usage_summary,
26
+ )
27
+ from .common import (
28
+ LAST_FILE,
29
+ WORKER_INDEX_FILE,
30
+ atomic_json,
31
+ fmt_duration_ms,
32
+ fmt_tokens,
33
+ iter_run_files,
34
+ load_run,
35
+ load_worker_index,
36
+ now_ms,
37
+ numeric_ms,
38
+ policy_bool,
39
+ read_json_object,
40
+ remember_worker_parent,
41
+ run_key,
42
+ run_path_for_key,
43
+ state_lock,
44
+ telemetry_notifications_enabled,
45
+ telemetry_retention_days,
46
+ worker_index_entry,
47
+ )
48
+ from .render import (
49
+ aggregate_usage_value,
50
+ render_summary,
51
+ run_context,
52
+ )
53
+
54
+
55
+ _USAGE_NUMERIC_FIELDS = (
56
+ "input_tokens",
57
+ "cached_input_tokens",
58
+ "net_new_input_tokens",
59
+ "output_tokens",
60
+ "reasoning_output_tokens",
61
+ "cache_write_input_tokens",
62
+ "total_tokens",
63
+ "estimated_credits_micros",
64
+ "estimated_usd_micros",
65
+ )
66
+
67
+
68
+ def _worker_execution_records(worker: dict[str, Any]) -> dict[str, dict[str, Any]]:
69
+ """Return distinct child executions, including the legacy single row."""
70
+ result: dict[str, dict[str, Any]] = {}
71
+ executions = worker.get("executions")
72
+ if isinstance(executions, dict):
73
+ for key, value in executions.items():
74
+ if isinstance(value, dict):
75
+ record = dict(value)
76
+ record.setdefault("turn_id", key)
77
+ result[str(key)] = record
78
+ elif isinstance(executions, list):
79
+ for value in executions:
80
+ if not isinstance(value, dict) or value.get("turn_id") is None:
81
+ continue
82
+ result[str(value["turn_id"])] = dict(value)
83
+
84
+ # Older runs only had one top-level worker turn. Preserve it as an
85
+ # execution when upgrading the row so a later resumed turn cannot absorb
86
+ # its usage or lifecycle into the new execution.
87
+ legacy_turn = worker.get("turn_id")
88
+ if legacy_turn is not None and str(legacy_turn) not in result:
89
+ result[str(legacy_turn)] = {
90
+ key: value
91
+ for key, value in worker.items()
92
+ if key not in {"executions", "service_usage_cumulative"}
93
+ }
94
+ return result
95
+
96
+
97
+ def _ensure_worker_execution_map(worker: dict[str, Any]) -> dict[str, dict[str, Any]]:
98
+ executions = _worker_execution_records(worker)
99
+ worker["executions"] = executions
100
+ return executions
101
+
102
+
103
+ def _sum_usage_values(usages: list[dict[str, Any]]) -> dict[str, Any] | None:
104
+ if not usages:
105
+ return None
106
+ result: dict[str, Any] = {}
107
+ for field in _USAGE_NUMERIC_FIELDS:
108
+ values = [
109
+ value.get(field)
110
+ for value in usages
111
+ if isinstance(value.get(field), (int, float))
112
+ and not isinstance(value.get(field), bool)
113
+ ]
114
+ if values:
115
+ result[field] = int(sum(values))
116
+ groups: list[dict[str, Any]] = []
117
+ for usage in usages:
118
+ value = usage.get("groups")
119
+ if isinstance(value, list):
120
+ groups.extend(group for group in value if isinstance(group, dict))
121
+ result["groups"] = groups
122
+ sources = {str(value.get("source")) for value in usages if value.get("source")}
123
+ if sources:
124
+ result["source"] = next(iter(sources)) if len(sources) == 1 else "+".join(sorted(sources))
125
+ return result
126
+
127
+
128
+ def _rebuild_worker_usage(worker: dict[str, Any]) -> None:
129
+ executions = _worker_execution_records(worker)
130
+ usages = [
131
+ execution["usage"]
132
+ for execution in executions.values()
133
+ if isinstance(execution.get("usage"), dict)
134
+ ]
135
+ aggregate = _sum_usage_values(usages)
136
+ if aggregate is not None:
137
+ worker["usage"] = aggregate
138
+ elif not usages and not isinstance(worker.get("usage"), dict):
139
+ worker["usage"] = None
140
+
141
+
142
+ def _refresh_worker_lifecycle(worker: dict[str, Any]) -> None:
143
+ executions = _worker_execution_records(worker)
144
+ statuses = [
145
+ execution.get("status")
146
+ for execution in executions.values()
147
+ if execution.get("status") in {"running", "observed", "completed"}
148
+ ]
149
+ if statuses:
150
+ order = {"running": 1, "observed": 2, "completed": 3}
151
+ # An execution still running keeps the UI row live. Otherwise expose
152
+ # the strongest terminal state seen for the distinct executions.
153
+ worker["status"] = (
154
+ "running"
155
+ if "running" in statuses
156
+ else max(statuses, key=lambda status: order[status])
157
+ )
158
+ elif worker.get("status") == "running":
159
+ worker["status"] = "observed"
160
+ started = [
161
+ numeric_ms(execution.get("started_at_ms"))
162
+ for execution in executions.values()
163
+ if numeric_ms(execution.get("started_at_ms")) is not None
164
+ ]
165
+ if started:
166
+ worker["started_at_ms"] = min(started)
167
+ finished = [
168
+ numeric_ms(execution.get("finished_at_ms"))
169
+ for execution in executions.values()
170
+ if numeric_ms(execution.get("finished_at_ms")) is not None
171
+ ]
172
+ if finished:
173
+ worker["finished_at_ms"] = max(finished)
174
+
175
+
176
+ def _merge_execution_usage(
177
+ existing: dict[str, Any] | None, incoming: dict[str, Any] | None
178
+ ) -> dict[str, Any] | None:
179
+ if incoming is None:
180
+ return existing
181
+ if existing is None:
182
+ return dict(incoming)
183
+ existing_source = str(existing.get("source") or "")
184
+ incoming_source = str(incoming.get("source") or "")
185
+ if incoming_source.startswith("transcript") and existing_source == "app-server":
186
+ # A delayed stop can provide exact per-turn transcript usage after a
187
+ # parent stop filled the running execution from cumulative service
188
+ # usage. Prefer the exact turn evidence even when its total is lower.
189
+ return dict(incoming)
190
+ old_total = existing.get("total_tokens")
191
+ new_total = incoming.get("total_tokens")
192
+ if (
193
+ isinstance(old_total, (int, float))
194
+ and isinstance(new_total, (int, float))
195
+ and not isinstance(old_total, bool)
196
+ and not isinstance(new_total, bool)
197
+ and int(new_total) <= int(old_total)
198
+ ):
199
+ return existing
200
+ return dict(incoming)
201
+
202
+
203
+ def _merge_execution_values(
204
+ existing: dict[str, Any] | None, incoming: dict[str, Any]
205
+ ) -> dict[str, Any]:
206
+ previous = dict(existing) if isinstance(existing, dict) else {}
207
+ result = dict(previous)
208
+ for key, value in incoming.items():
209
+ if value is not None:
210
+ result[key] = value
211
+ for field, reducer in (
212
+ ("started_at_ms", min),
213
+ ("finished_at_ms", max),
214
+ ):
215
+ values = [
216
+ numeric_ms(previous.get(field)),
217
+ numeric_ms(incoming.get(field)),
218
+ ]
219
+ values = [value for value in values if value is not None]
220
+ if values:
221
+ result[field] = reducer(values)
222
+ statuses = [previous.get("status"), incoming.get("status")]
223
+ status_order = {"running": 1, "observed": 2, "completed": 3}
224
+ valid_statuses = [status for status in statuses if status in status_order]
225
+ if valid_statuses:
226
+ result["status"] = max(valid_statuses, key=lambda status: status_order[status])
227
+ result["usage"] = _merge_execution_usage(
228
+ existing.get("usage") if isinstance(existing, dict) else None,
229
+ incoming.get("usage"),
230
+ )
231
+ if result.get("service_usage_cumulative") is None and incoming.get("service_usage_cumulative") is not None:
232
+ result["service_usage_cumulative"] = incoming["service_usage_cumulative"]
233
+ return result
234
+
235
+
236
+ def _service_usage_delta(
237
+ worker: dict[str, Any],
238
+ execution: dict[str, Any],
239
+ service_usage: dict[str, Any] | None,
240
+ execution_count: int,
241
+ session_id: Any = None,
242
+ agent_id: str | None = None,
243
+ ) -> dict[str, Any] | None:
244
+ if service_usage is None:
245
+ return None
246
+ if execution.get("service_usage_finalized"):
247
+ return None
248
+ if execution.get("service_usage_from_zero"):
249
+ return service_usage
250
+ previous = execution.get("service_usage_baseline")
251
+ if not isinstance(previous, dict):
252
+ current_turn = str(execution.get("turn_id") or "")
253
+ snapshots = [
254
+ item.get("service_usage_cumulative")
255
+ for item in _worker_execution_records(worker).values()
256
+ if str(item.get("turn_id") or "") != current_turn
257
+ and isinstance(item.get("service_usage_cumulative"), dict)
258
+ ]
259
+ if snapshots:
260
+ previous = snapshots[-1]
261
+ prior_execution_seen = False
262
+ if (
263
+ not isinstance(previous, dict)
264
+ and session_id is not None
265
+ and agent_id
266
+ and agent_id != "unknown"
267
+ ):
268
+ historical: list[dict[str, Any]] = []
269
+ current_turn = str(execution.get("turn_id") or "")
270
+ for path in iter_run_files():
271
+ run = read_json_object(path)
272
+ if run is None or str(run.get("session_id") or "") != str(session_id or ""):
273
+ continue
274
+ workers = run.get("workers")
275
+ candidate = workers.get(agent_id) if isinstance(workers, dict) else None
276
+ if not isinstance(candidate, dict):
277
+ continue
278
+ for item in _worker_execution_records(candidate).values():
279
+ if str(item.get("turn_id") or "") == current_turn:
280
+ continue
281
+ prior_execution_seen = True
282
+ snapshot = item.get("service_usage_cumulative")
283
+ if isinstance(snapshot, dict):
284
+ historical.append(item)
285
+ if historical:
286
+ historical.sort(
287
+ key=lambda item: max(
288
+ numeric_ms(item.get("updated_at_ms")) or 0,
289
+ numeric_ms(item.get("finished_at_ms")) or 0,
290
+ numeric_ms(item.get("started_at_ms")) or 0,
291
+ )
292
+ )
293
+ previous = historical[-1].get("service_usage_cumulative")
294
+ if isinstance(previous, dict):
295
+ execution["service_usage_baseline"] = dict(previous)
296
+ return usage_delta(previous, service_usage)
297
+ # Once this agent has more than one execution, a cumulative service value
298
+ # cannot be attributed to the current turn without a prior snapshot.
299
+ if execution_count > 1 or prior_execution_seen:
300
+ return None
301
+ execution["service_usage_from_zero"] = True
302
+ return service_usage
303
+
304
+
305
+ def _execution_match(
306
+ session_id: Any, agent_id: str, worker_turn_id: Any
307
+ ) -> str | None:
308
+ if worker_turn_id is None:
309
+ return None
310
+ target = str(worker_turn_id)
311
+ matches: set[str] = set()
312
+ for path in iter_run_files():
313
+ run = read_json_object(path)
314
+ if run is None or str(run.get("session_id") or "") != str(session_id or ""):
315
+ continue
316
+ workers = run.get("workers")
317
+ worker = workers.get(agent_id) if isinstance(workers, dict) else None
318
+ if not isinstance(worker, dict):
319
+ continue
320
+ executions = _worker_execution_records(worker)
321
+ if target in executions:
322
+ key = path.stem
323
+ merged = run.get("merged_into")
324
+ if isinstance(merged, str) and run_path_for_key(merged).is_file():
325
+ key = resolve_merged_run_key(merged)
326
+ matches.add(key)
327
+ return next(iter(matches)) if len(matches) == 1 else None
328
+
329
+
330
+ def _transcript_worker_started_at(event: dict[str, Any]) -> int | None:
331
+ turn_id = event.get("turn_id")
332
+ if turn_id is None:
333
+ return None
334
+ for field in ("agent_transcript_path", "transcript_path"):
335
+ started = transcript_turn_started_at(event.get(field), turn_id)
336
+ if started is not None:
337
+ return started
338
+ for field in ("task_started_at_ms", "worker_started_at_ms"):
339
+ value = numeric_ms(event.get(field))
340
+ if value is not None:
341
+ return value
342
+ return None
343
+
344
+
345
+ def _parent_key_for_worker_timestamp(session_id: Any, started_at_ms: int) -> str | None:
346
+ matches: list[str] = []
347
+ for key, _, parent in parent_candidates(session_id):
348
+ parent_started = numeric_ms(parent.get("started_at_ms"))
349
+ parent_finished = numeric_ms(parent.get("finished_at_ms"))
350
+ if parent_started is not None and started_at_ms < parent_started:
351
+ continue
352
+ if parent_finished is not None and started_at_ms > parent_finished:
353
+ continue
354
+ matches.append(key)
355
+ return matches[0] if len(matches) == 1 else None
356
+
357
+
358
+ def resolve_merged_run_key(key: str) -> str:
359
+ current = key
360
+ seen: set[str] = set()
361
+ for _ in range(8):
362
+ if current in seen:
363
+ break
364
+ seen.add(current)
365
+ run = read_json_object(run_path_for_key(current))
366
+ target = run.get("merged_into") if isinstance(run, dict) else None
367
+ if not isinstance(target, str) or not target or target == current:
368
+ break
369
+ if not run_path_for_key(target).is_file():
370
+ break
371
+ current = target
372
+ return current
373
+
374
+
375
+ def find_worker_record(
376
+ session_id: Any, agent_id: str
377
+ ) -> tuple[str, Path, dict[str, Any], dict[str, Any]] | None:
378
+ candidates: list[
379
+ tuple[
380
+ tuple[int, int, int, int],
381
+ str,
382
+ Path,
383
+ dict[str, Any],
384
+ dict[str, Any],
385
+ ]
386
+ ] = []
387
+ for path in iter_run_files():
388
+ run = read_json_object(path)
389
+ if run is None or str(run.get("session_id") or "") != str(session_id or ""):
390
+ continue
391
+ workers = run.get("workers")
392
+ worker = workers.get(agent_id) if isinstance(workers, dict) else None
393
+ if not isinstance(worker, dict):
394
+ continue
395
+ merged_workers = run.get("merged_workers")
396
+ if isinstance(merged_workers, dict) and agent_id in merged_workers:
397
+ continue
398
+ key = path.stem
399
+ status = worker.get("status")
400
+ status_score = 2 if status == "completed" else 1 if status else 0
401
+ parent_score = 1 if run.get("prompt_seen") is True else 0
402
+ finished = numeric_ms(worker.get("finished_at_ms")) or 0
403
+ started = numeric_ms(worker.get("started_at_ms")) or numeric_ms(
404
+ run.get("started_at_ms")
405
+ ) or 0
406
+ candidates.append(
407
+ ((parent_score, status_score, finished, started), key, path, run, worker)
408
+ )
409
+ if not candidates:
410
+ return None
411
+ _, key, path, run, worker = max(candidates, key=lambda item: item[0])
412
+ return key, path, run, worker
413
+
414
+
415
+ def parent_candidates(session_id: Any) -> list[tuple[str, Path, dict[str, Any]]]:
416
+ result: list[tuple[str, Path, dict[str, Any]]] = []
417
+ for path in iter_run_files():
418
+ run = read_json_object(path)
419
+ if run is None:
420
+ continue
421
+ if str(run.get("session_id") or "") != str(session_id or ""):
422
+ continue
423
+ if run.get("prompt_seen") is True:
424
+ result.append((path.stem, path, run))
425
+ return result
426
+
427
+
428
+ def worker_interval(
429
+ source_run: dict[str, Any], worker: dict[str, Any], event_time_ms: int
430
+ ) -> tuple[int | None, int]:
431
+ started = numeric_ms(worker.get("started_at_ms"))
432
+ if started is None:
433
+ started = numeric_ms(source_run.get("started_at_ms"))
434
+ finished = numeric_ms(worker.get("finished_at_ms")) or event_time_ms
435
+ return started, finished
436
+
437
+
438
+ def parent_match_score(
439
+ parent: dict[str, Any],
440
+ event_time_ms: int,
441
+ source_record: tuple[dict[str, Any], dict[str, Any]] | None = None,
442
+ ) -> tuple[int, int, int]:
443
+ parent_started = numeric_ms(parent.get("started_at_ms"))
444
+ parent_finished = numeric_ms(parent.get("finished_at_ms"))
445
+ active = (
446
+ (parent_started is None or parent_started <= event_time_ms)
447
+ and (parent_finished is None or event_time_ms <= parent_finished)
448
+ )
449
+ score = 0
450
+ overlap = 0
451
+ if active:
452
+ score += 20_000_000
453
+
454
+ if source_record is not None:
455
+ source_run, worker = source_record
456
+ worker_started, worker_finished = worker_interval(
457
+ source_run, worker, event_time_ms
458
+ )
459
+ if worker_started is not None:
460
+ start_inside = parent_started is None or (
461
+ parent_started <= worker_started
462
+ and (parent_finished is None or worker_started <= parent_finished)
463
+ )
464
+ if start_inside:
465
+ score += 1_000_000_000
466
+ if worker_finished is not None:
467
+ finish_inside = parent_started is None or (
468
+ parent_started <= worker_finished
469
+ and (parent_finished is None or worker_finished <= parent_finished)
470
+ )
471
+ if finish_inside:
472
+ score += 500_000_000
473
+ if parent_started is not None and worker_started is not None:
474
+ parent_end = parent_finished or max(event_time_ms, worker_finished)
475
+ overlap = max(
476
+ 0,
477
+ min(parent_end, worker_finished)
478
+ - max(parent_started, worker_started),
479
+ )
480
+ if overlap:
481
+ score += 100_000_000 + min(overlap, 86_400_000)
482
+ elif active:
483
+ score += 1_000_000_000
484
+
485
+ return score, overlap, parent_started or 0
486
+
487
+
488
+ def find_parent_run_key(
489
+ session_id: Any,
490
+ event_time_ms: int | None = None,
491
+ source_record: tuple[dict[str, Any], dict[str, Any]] | None = None,
492
+ ) -> str | None:
493
+ event_time_ms = event_time_ms or now_ms()
494
+ candidates = parent_candidates(session_id)
495
+ if not candidates:
496
+ return None
497
+ ranked = [
498
+ (parent_match_score(run, event_time_ms, source_record), key)
499
+ for key, _, run in candidates
500
+ ]
501
+ best = max(ranked, key=lambda item: (item[0], item[1]))
502
+ return best[1] if best[0][0] > 0 else None
503
+
504
+
505
+ def worker_run_key(event: dict[str, Any]) -> str:
506
+ agent_id = str(event.get("agent_id") or "unknown")
507
+ session_id = event.get("session_id")
508
+ kind = event.get("hook_event_name")
509
+ event_time_ms = now_ms()
510
+
511
+ # A child turn id is the stable identity for a resumed execution. Resolve
512
+ # it before consulting the legacy agent-only index, which otherwise points
513
+ # every later stop at the first parent that used this agent.
514
+ exact = _execution_match(session_id, agent_id, event.get("turn_id"))
515
+ if exact is not None:
516
+ return exact
517
+
518
+ # If the stop has a transcript, its exact task_started timestamp can place
519
+ # it in one parent interval even when the corresponding Start hook was
520
+ # absent. Require a unique interval; overlapping parents are ambiguous.
521
+ if kind == "SubagentStop":
522
+ started_at_ms = _transcript_worker_started_at(event)
523
+ if started_at_ms is not None:
524
+ timestamp_parent = _parent_key_for_worker_timestamp(session_id, started_at_ms)
525
+ if timestamp_parent is not None:
526
+ return timestamp_parent
527
+
528
+ # No exact execution or timestamp evidence means we cannot safely
529
+ # reassign this stop to the currently active parent. Keep it in its
530
+ # child-keyed run so later repair can make an evidence-based choice.
531
+ return run_key(event)
532
+
533
+ active_parent = find_parent_run_key(session_id, event_time_ms)
534
+ return active_parent or run_key(event)
535
+
536
+
537
+ def merge_worker_values(
538
+ existing: dict[str, Any] | None, incoming: dict[str, Any]
539
+ ) -> dict[str, Any]:
540
+ previous = dict(existing) if isinstance(existing, dict) else {}
541
+ result = dict(previous)
542
+ for key, value in incoming.items():
543
+ if value is not None:
544
+ result[key] = value
545
+ started_values = [
546
+ numeric_ms(previous.get("started_at_ms")),
547
+ numeric_ms(incoming.get("started_at_ms")),
548
+ ]
549
+ started_values = [value for value in started_values if value is not None]
550
+ if started_values:
551
+ result["started_at_ms"] = min(started_values)
552
+ finished_values = [
553
+ numeric_ms(previous.get("finished_at_ms")),
554
+ numeric_ms(incoming.get("finished_at_ms")),
555
+ ]
556
+ finished_values = [value for value in finished_values if value is not None]
557
+ if finished_values:
558
+ result["finished_at_ms"] = max(finished_values)
559
+ statuses = [previous.get("status"), incoming.get("status")]
560
+ status_order = {"running": 1, "observed": 2, "completed": 3}
561
+ statuses = [status for status in statuses if status in status_order]
562
+ if statuses:
563
+ result["status"] = max(statuses, key=lambda status: status_order[status])
564
+ existing_executions = _worker_execution_records(previous)
565
+ incoming_executions = _worker_execution_records(incoming)
566
+ if existing_executions or incoming_executions:
567
+ merged_executions: dict[str, dict[str, Any]] = {}
568
+ for execution_id in set(existing_executions) | set(incoming_executions):
569
+ merged_executions[execution_id] = _merge_execution_values(
570
+ existing_executions.get(execution_id),
571
+ incoming_executions.get(execution_id)
572
+ or {"turn_id": execution_id},
573
+ )
574
+ result["executions"] = merged_executions
575
+ _rebuild_worker_usage(result)
576
+ elif result.get("usage") is None and incoming.get("usage") is not None:
577
+ result["usage"] = incoming["usage"]
578
+ return result
579
+
580
+
581
+ def absorb_worker_source(
582
+ run: dict[str, Any], target_key: str, session_id: Any, agent_id: str
583
+ ) -> None:
584
+ source = find_worker_record(session_id, agent_id)
585
+ if source is None:
586
+ return
587
+ source_key, source_path, source_run, source_worker = source
588
+ if source_key == target_key or source_run.get("prompt_seen") is True:
589
+ return
590
+ workers = run.setdefault("workers", {})
591
+ workers[agent_id] = merge_worker_values(workers.get(agent_id), source_worker)
592
+ provenance = run.setdefault("worker_sources", {})
593
+ if not isinstance(provenance, dict):
594
+ provenance = {}
595
+ run["worker_sources"] = provenance
596
+ sources = provenance.setdefault(agent_id, [])
597
+ if not isinstance(sources, list):
598
+ sources = []
599
+ provenance[agent_id] = sources
600
+ if source_key not in sources:
601
+ sources.append(source_key)
602
+ merged_workers = source_run.setdefault("merged_workers", {})
603
+ if not isinstance(merged_workers, dict):
604
+ merged_workers = {}
605
+ source_run["merged_workers"] = merged_workers
606
+ merged_workers[agent_id] = target_key
607
+ source_worker_ids = set((source_run.get("workers") or {}).keys())
608
+ if source_worker_ids and source_worker_ids.issubset(merged_workers.keys()):
609
+ targets = set(str(value) for value in merged_workers.values())
610
+ if len(targets) == 1:
611
+ source_run["merged_into"] = target_key
612
+ source_run["merged_at_ms"] = now_ms()
613
+ source_run["merge_reason"] = "agent-index"
614
+ atomic_json(source_path, source_run)
615
+
616
+
617
+ def reconcile_orphan_workers() -> set[str]:
618
+ changed: set[str] = set()
619
+ with state_lock("telemetry-reconcile") as acquired:
620
+ if not acquired:
621
+ return changed
622
+ for source_path in iter_run_files():
623
+ source_run = read_json_object(source_path)
624
+ if source_run is None or source_run.get("prompt_seen") is True:
625
+ continue
626
+ if source_run.get("worker_correlation") == "unresolved":
627
+ continue
628
+ if source_run.get("merged_into"):
629
+ continue
630
+ workers = source_run.get("workers")
631
+ if not isinstance(workers, dict):
632
+ continue
633
+ merged_workers = source_run.get("merged_workers")
634
+ if not isinstance(merged_workers, dict):
635
+ merged_workers = {}
636
+ source_run["merged_workers"] = merged_workers
637
+ source_key = source_path.stem
638
+ source_changed = False
639
+ for agent_id, source_worker in list(workers.items()):
640
+ if agent_id in merged_workers or not isinstance(source_worker, dict):
641
+ continue
642
+ target_key = find_parent_run_key(
643
+ source_run.get("session_id"),
644
+ now_ms(),
645
+ (source_run, source_worker),
646
+ )
647
+ if target_key is None or target_key == source_key:
648
+ continue
649
+ target_path = run_path_for_key(target_key)
650
+ target_turn_id = None
651
+ with state_lock(target_key) as target_acquired:
652
+ if not target_acquired:
653
+ continue
654
+ target_run = read_json_object(target_path)
655
+ if target_run is None or target_run.get("prompt_seen") is not True:
656
+ continue
657
+ target_turn_id = target_run.get("turn_id")
658
+ target_workers = target_run.setdefault("workers", {})
659
+ target_workers[agent_id] = merge_worker_values(
660
+ target_workers.get(agent_id), source_worker
661
+ )
662
+ if target_workers[agent_id].get("status") == "running":
663
+ target_workers[agent_id]["status"] = "observed"
664
+ provenance = target_run.setdefault("worker_sources", {})
665
+ if not isinstance(provenance, dict):
666
+ provenance = {}
667
+ target_run["worker_sources"] = provenance
668
+ sources = provenance.setdefault(agent_id, [])
669
+ if not isinstance(sources, list):
670
+ sources = []
671
+ provenance[agent_id] = sources
672
+ if source_key not in sources:
673
+ sources.append(source_key)
674
+ atomic_json(target_path, target_run)
675
+ merged_workers[agent_id] = target_key
676
+ source_changed = True
677
+ changed.add(target_key)
678
+ remember_worker_parent(
679
+ agent_id,
680
+ source_run.get("session_id"),
681
+ target_key,
682
+ target_turn_id,
683
+ source_worker.get("started_at_ms") or source_run.get("started_at_ms"),
684
+ source_worker.get("turn_id"),
685
+ source_worker.get("finished_at_ms"),
686
+ source_worker.get("transcript_path"),
687
+ )
688
+ source_worker_ids = set(workers.keys())
689
+ if source_worker_ids and source_worker_ids.issubset(merged_workers.keys()):
690
+ targets = set(str(value) for value in merged_workers.values())
691
+ if len(targets) == 1:
692
+ source_run["merged_into"] = next(iter(targets))
693
+ source_run["merged_at_ms"] = now_ms()
694
+ source_run["merge_reason"] = "parent-time-window"
695
+ source_changed = True
696
+ if source_changed:
697
+ atomic_json(source_path, source_run)
698
+ return changed
699
+
700
+
701
+ def run_age_timestamp_ms(path: Path, run: dict[str, Any] | None) -> int | None:
702
+ if isinstance(run, dict):
703
+ for field in ("finished_at_ms", "started_at_ms", "merged_at_ms"):
704
+ value = numeric_ms(run.get(field))
705
+ if value is not None:
706
+ return value
707
+ try:
708
+ return int(path.stat().st_mtime * 1000)
709
+ except OSError:
710
+ return None
711
+
712
+
713
+ def run_maintenance() -> None:
714
+ with state_lock("telemetry-maintenance") as acquired:
715
+ if not acquired:
716
+ return
717
+ cutoff = now_ms() - telemetry_retention_days() * 86_400_000
718
+ for path in iter_run_files():
719
+ run = read_json_object(path)
720
+ timestamp = run_age_timestamp_ms(path, run)
721
+ if timestamp is None or timestamp >= cutoff:
722
+ continue
723
+ try:
724
+ path.unlink()
725
+ except OSError:
726
+ pass
727
+
728
+ last = read_json_object(LAST_FILE)
729
+ last_timestamp = run_age_timestamp_ms(LAST_FILE, last)
730
+ if last_timestamp is not None and last_timestamp < cutoff:
731
+ try:
732
+ LAST_FILE.unlink()
733
+ except OSError:
734
+ pass
735
+
736
+ if not WORKER_INDEX_FILE.exists():
737
+ return
738
+ with state_lock("worker-index") as index_acquired:
739
+ if not index_acquired:
740
+ return
741
+ index = load_worker_index()
742
+ workers = index.get("workers")
743
+ if not isinstance(workers, dict):
744
+ return
745
+ stale = [
746
+ agent_id
747
+ for agent_id, entry in workers.items()
748
+ if isinstance(entry, dict)
749
+ and numeric_ms(entry.get("updated_at_ms")) is not None
750
+ and numeric_ms(entry.get("updated_at_ms")) < cutoff
751
+ ]
752
+ if stale:
753
+ for agent_id in stale:
754
+ workers.pop(agent_id, None)
755
+ atomic_json(WORKER_INDEX_FILE, index)
756
+
757
+
758
+ def applescript_literal(value: Any) -> str:
759
+ text = " ".join(str(value).split())
760
+ return '"' + text.replace("\\", "\\\\").replace('"', '\\"') + '"'
761
+
762
+
763
+ def notification_body(run: dict[str, Any]) -> str:
764
+ session, project, branch = run_context(run)
765
+ label = project or session or "Codex task"
766
+ if branch and project:
767
+ label = f"{label} · {branch}"
768
+ workers = list((run.get("workers") or {}).values())
769
+ parent = run.get("parent") or {}
770
+ usages = [parent.get("usage_delta") if isinstance(parent, dict) else None]
771
+ usages.extend(
772
+ worker.get("usage") if isinstance(worker, dict) else None for worker in workers
773
+ )
774
+ total_tokens = aggregate_usage_value(usages, "total_tokens")
775
+ worker_count = f"{len(workers)} worker{'s' if len(workers) != 1 else ''}"
776
+ parts = [label, worker_count, f"{fmt_tokens(total_tokens)} tokens"]
777
+ started = numeric_ms(run.get("started_at_ms"))
778
+ finished = numeric_ms(run.get("finished_at_ms"))
779
+ if started is not None and finished is not None:
780
+ duration = fmt_duration_ms(finished - started)
781
+ if duration:
782
+ parts.append(duration)
783
+ return " · ".join(parts)
784
+
785
+
786
+ def send_system_notification(run: dict[str, Any]) -> None:
787
+ notify_overlay_if_active(run)
788
+ telemetry_mod = sys.modules.get("telemetry")
789
+ sub_mod = getattr(telemetry_mod, "subprocess", subprocess) if telemetry_mod else subprocess
790
+ shutil_mod = getattr(telemetry_mod, "shutil", shutil) if telemetry_mod else shutil
791
+ sys_mod = getattr(telemetry_mod, "sys", sys) if telemetry_mod else sys
792
+
793
+ if not telemetry_notifications_enabled() or sys_mod.platform != "darwin":
794
+ return
795
+ executable = shutil_mod.which("osascript")
796
+ if not executable:
797
+ return
798
+ script = (
799
+ f"display notification {applescript_literal(notification_body(run))} "
800
+ f"with title {applescript_literal('FlowPilot task finished')}"
801
+ )
802
+ try:
803
+ sub_mod.run(
804
+ [executable, "-e", script],
805
+ stdout=subprocess.DEVNULL,
806
+ stderr=subprocess.DEVNULL,
807
+ timeout=2.0,
808
+ check=False,
809
+ )
810
+ except (OSError, Exception):
811
+ return
812
+
813
+
814
+ def notify_overlay_if_active(run: dict[str, Any]) -> None:
815
+ """Send immediate update event to macos-overlay daemon if running."""
816
+ import socket
817
+ codex_home = os.environ.get("CODEX_HOME", os.path.expanduser("~/.codex"))
818
+ sock_path = os.path.join(codex_home, "codex-flow", "overlay.sock")
819
+ if not os.path.exists(sock_path):
820
+ return
821
+ try:
822
+ client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
823
+ client.settimeout(0.5)
824
+ client.connect(sock_path)
825
+ client.sendall(b"update\n")
826
+ client.close()
827
+ except Exception:
828
+ pass
829
+
830
+
831
+ def is_same_run(left: dict[str, Any] | None, right: dict[str, Any]) -> bool:
832
+ if not isinstance(left, dict):
833
+ return False
834
+ return (
835
+ str(left.get("session_id") or "") == str(right.get("session_id") or "")
836
+ and str(left.get("turn_id") or "") == str(right.get("turn_id") or "")
837
+ )
838
+
839
+
840
+ def write_stop_output(text: str) -> None:
841
+ if os.environ.get("CODEX_FLOW_TELEMETRY_TEST_PLAIN_OUTPUT") == "1":
842
+ sys.stdout.write(text)
843
+ return
844
+ sys.stdout.write(
845
+ json.dumps({"systemMessage": text}, ensure_ascii=False, separators=(",", ":"))
846
+ + "\n"
847
+ )
848
+
849
+
850
+ def collect_hook(event: dict[str, Any]) -> None:
851
+ kind = event.get("hook_event_name")
852
+ if kind not in {"UserPromptSubmit", "SubagentStart", "SubagentStop", "Stop"}:
853
+ return
854
+
855
+ key = worker_run_key(event) if kind in {"SubagentStart", "SubagentStop"} else run_key(event)
856
+
857
+ if kind == "UserPromptSubmit":
858
+ with state_lock(key) as acquired:
859
+ if not acquired:
860
+ return
861
+ run = load_run(event, key)
862
+ path = run_path_for_key(key)
863
+ transcript_path = event.get("transcript_path")
864
+ if not transcript_path:
865
+ transcript_path = find_session_transcript(event.get("session_id"))
866
+
867
+ is_system = False
868
+ cwd = event.get("cwd")
869
+ if cwd == "/":
870
+ is_system = True
871
+ prompt_text = str(event.get("user_prompt") or event.get("prompt") or "")
872
+ if (
873
+ "safety and compliance standards for Codex ambient" in prompt_text
874
+ or "hyperpersonalized suggestions" in prompt_text
875
+ ):
876
+ is_system = True
877
+
878
+ run_updates: dict[str, Any] = {
879
+ "started_at_ms": now_ms(),
880
+ "cwd": cwd,
881
+ "prompt_seen": True,
882
+ "transcript_path": transcript_path,
883
+ }
884
+ if is_system:
885
+ run_updates["is_system_task"] = True
886
+ if prompt_text and not run.get("summary"):
887
+ run_updates["summary"] = prompt_text[:200].strip()
888
+ run.update(run_updates)
889
+ if event.get("model") is not None:
890
+ run.setdefault("parent", {})["model"] = event.get("model")
891
+ with AppServer() as server:
892
+ sample_time_before = now_ms()
893
+ raw_before = quota_windows(server.rate_limits()) if server.available else []
894
+ run["quota_before"] = [
895
+ {**w, "sampled_at_ms": sample_time_before} for w in raw_before
896
+ ]
897
+ try:
898
+ from .quota_ledger import get_db, record_observation, resolve_account_id
899
+ resolved_account = resolve_account_id(event.get("account_id"))
900
+ for w in run["quota_before"]:
901
+ if w.get("window_duration_mins") == 10080 and isinstance(w.get("used_percent"), (int, float)):
902
+ with get_db() as db_conn:
903
+ record_observation(
904
+ conn=db_conn,
905
+ account_id=resolved_account,
906
+ bucket_id=w.get("slot") or "primary",
907
+ used_percent=float(w["used_percent"]),
908
+ sampled_at_ms=sample_time_before,
909
+ sample_source="turn_start",
910
+ resets_at_ms=w.get("resets_at"),
911
+ run_id=key,
912
+ )
913
+ except Exception:
914
+ pass
915
+ run.setdefault("parent", {})["usage_before"] = (
916
+ usage_summary(server.thread_usage(event.get("session_id")))
917
+ if server.available
918
+ else None
919
+ )
920
+ run["thread"] = merge_thread_metadata(
921
+ session_index_metadata(event.get("session_id")),
922
+ server.thread_metadata(event.get("session_id"))
923
+ if server.available
924
+ else None,
925
+ )
926
+ if not run.get("transcript_path"):
927
+ thread_path = (
928
+ run.get("thread", {}).get("path")
929
+ if isinstance(run.get("thread"), dict)
930
+ else None
931
+ )
932
+ if thread_path and Path(thread_path).is_file():
933
+ run["transcript_path"] = thread_path
934
+ else:
935
+ resolved = find_session_transcript(event.get("session_id"))
936
+ if resolved:
937
+ run["transcript_path"] = resolved
938
+ apply_participant_metadata(
939
+ run.setdefault("parent", {}),
940
+ event=event,
941
+ transcript_path=run.get("transcript_path"),
942
+ turn_id=run.get("turn_id"),
943
+ usage=run["parent"].get("usage_before"),
944
+ )
945
+ atomic_json(path, run)
946
+ return
947
+
948
+ if kind in {"SubagentStart", "SubagentStop"}:
949
+ agent_id = str(event.get("agent_id") or "unknown")
950
+ with state_lock(key) as acquired:
951
+ if not acquired:
952
+ return
953
+ run = load_run(event, key)
954
+ path = run_path_for_key(key)
955
+ if kind == "SubagentStop":
956
+ absorb_worker_source(run, key, event.get("session_id"), agent_id)
957
+ workers = run.setdefault("workers", {})
958
+ worker = workers.setdefault(agent_id, {"agent_id": agent_id})
959
+ executions = _ensure_worker_execution_map(worker)
960
+ worker_turn_id = event.get("turn_id")
961
+ execution = None
962
+ if worker_turn_id is not None:
963
+ execution_id = str(worker_turn_id)
964
+ execution = executions.setdefault(
965
+ execution_id, {"turn_id": worker_turn_id}
966
+ )
967
+ elif kind == "SubagentStop" and len(executions) == 1:
968
+ execution = next(iter(executions.values()))
969
+ worker["agent_id"] = agent_id
970
+ if event.get("agent_type") is not None:
971
+ worker["agent_type"] = event.get("agent_type")
972
+ if event.get("model") is not None:
973
+ worker["model"] = event.get("model")
974
+ if event.get("turn_id") is not None:
975
+ worker["turn_id"] = event.get("turn_id")
976
+ if kind == "SubagentStart":
977
+ started_at_ms = _transcript_worker_started_at(event) or now_ms()
978
+ previous_started = numeric_ms(worker.get("started_at_ms"))
979
+ worker["started_at_ms"] = (
980
+ started_at_ms
981
+ if previous_started is None
982
+ else min(previous_started, started_at_ms)
983
+ )
984
+ if worker.get("status") != "completed":
985
+ worker["status"] = "running"
986
+ if execution is not None:
987
+ execution["agent_id"] = agent_id
988
+ execution["started_at_ms"] = min(
989
+ value
990
+ for value in (
991
+ numeric_ms(execution.get("started_at_ms")),
992
+ started_at_ms,
993
+ )
994
+ if value is not None
995
+ )
996
+ execution["status"] = (
997
+ "completed"
998
+ if execution.get("status") == "completed"
999
+ else "running"
1000
+ )
1001
+ if event.get("agent_transcript_path") is not None:
1002
+ worker["transcript_path"] = event.get("agent_transcript_path")
1003
+ if execution is not None:
1004
+ execution["transcript_path"] = event.get("agent_transcript_path")
1005
+ else:
1006
+ agent_transcript_path = event.get("agent_transcript_path")
1007
+ finished_at_ms = now_ms()
1008
+ worker["finished_at_ms"] = max(
1009
+ value
1010
+ for value in (
1011
+ numeric_ms(worker.get("finished_at_ms")),
1012
+ finished_at_ms,
1013
+ )
1014
+ if value is not None
1015
+ )
1016
+ worker["status"] = "completed"
1017
+ if agent_transcript_path is not None:
1018
+ worker["transcript_path"] = agent_transcript_path
1019
+ if execution is not None:
1020
+ execution["agent_id"] = agent_id
1021
+ execution_started = _transcript_worker_started_at(event)
1022
+ if execution_started is None:
1023
+ execution_started = numeric_ms(execution.get("started_at_ms"))
1024
+ if execution_started is None:
1025
+ execution_started = finished_at_ms
1026
+ execution["started_at_ms"] = execution_started
1027
+ execution["finished_at_ms"] = max(
1028
+ value
1029
+ for value in (
1030
+ numeric_ms(execution.get("finished_at_ms")),
1031
+ finished_at_ms,
1032
+ )
1033
+ if value is not None
1034
+ )
1035
+ execution["status"] = "completed"
1036
+ if agent_transcript_path is not None:
1037
+ execution["transcript_path"] = agent_transcript_path
1038
+ last_message = event.get("last_assistant_message")
1039
+ if isinstance(last_message, str):
1040
+ last_message = last_message.strip()
1041
+ if last_message:
1042
+ worker["conclusion"] = last_message[:4000]
1043
+ transcript_usage = transcript_turn_usage(
1044
+ agent_transcript_path, event.get("turn_id")
1045
+ )
1046
+ with AppServer() as server:
1047
+ service_usage = (
1048
+ usage_summary(server.thread_usage(agent_id))
1049
+ if server.available
1050
+ else None
1051
+ )
1052
+ if execution is not None:
1053
+ service_delta = _service_usage_delta(
1054
+ worker,
1055
+ execution,
1056
+ service_usage,
1057
+ len(executions),
1058
+ event.get("session_id"),
1059
+ agent_id,
1060
+ )
1061
+ if service_usage is not None and not execution.get("service_usage_finalized"):
1062
+ execution["service_usage_cumulative"] = service_usage
1063
+ merged_usage = merge_usage(transcript_usage, service_delta)
1064
+ if service_usage is not None:
1065
+ execution["service_usage_finalized"] = True
1066
+ execution["usage"] = _merge_execution_usage(
1067
+ execution.get("usage"), merged_usage
1068
+ )
1069
+ else:
1070
+ merged_usage = merge_usage(transcript_usage, service_usage)
1071
+ if merged_usage is not None or worker.get("usage") is None:
1072
+ worker["usage"] = merged_usage
1073
+ _rebuild_worker_usage(worker)
1074
+ if not run.get("prompt_seen"):
1075
+ run["worker_correlation"] = "unresolved"
1076
+ _refresh_worker_lifecycle(worker)
1077
+ apply_participant_metadata(
1078
+ worker,
1079
+ event=event,
1080
+ transcript_path=worker.get("transcript_path"),
1081
+ turn_id=worker.get("turn_id"),
1082
+ usage=worker.get("usage"),
1083
+ )
1084
+ atomic_json(path, run)
1085
+
1086
+ last = read_json_object(LAST_FILE)
1087
+ if kind == "SubagentStop" and is_same_run(last, run):
1088
+ atomic_json(LAST_FILE, run)
1089
+ remember_worker_parent(
1090
+ agent_id,
1091
+ event.get("session_id"),
1092
+ key,
1093
+ run.get("turn_id"),
1094
+ worker.get("started_at_ms"),
1095
+ worker_turn_id,
1096
+ worker.get("finished_at_ms") if kind == "SubagentStop" else None,
1097
+ worker.get("transcript_path"),
1098
+ )
1099
+ return
1100
+
1101
+ with state_lock(key) as acquired:
1102
+ if not acquired:
1103
+ return
1104
+ run = load_run(event, key)
1105
+ path = run_path_for_key(key)
1106
+ run["finished_at_ms"] = now_ms()
1107
+ if event.get("model") is not None:
1108
+ run.setdefault("parent", {})["model"] = event.get("model")
1109
+ with AppServer() as server:
1110
+ run["thread"] = merge_thread_metadata(
1111
+ session_index_metadata(event.get("session_id")),
1112
+ run.get("thread"),
1113
+ server.thread_metadata(event.get("session_id"))
1114
+ if server.available
1115
+ else None,
1116
+ )
1117
+ sample_time_after = now_ms()
1118
+ raw_after = quota_windows(server.rate_limits()) if server.available else []
1119
+ quota_after = [
1120
+ {**w, "sampled_at_ms": sample_time_after} for w in raw_after
1121
+ ]
1122
+ parent_after = (
1123
+ usage_summary(server.thread_usage(event.get("session_id")))
1124
+ if server.available
1125
+ else None
1126
+ )
1127
+ run["quota_after"] = quota_after
1128
+ run["quota_change_during_run"] = quota_delta(
1129
+ run.get("quota_before", []), quota_after
1130
+ )
1131
+ try:
1132
+ from .quota_ledger import get_db, record_observation, export_quota_summary, resolve_account_id
1133
+ resolved_account = resolve_account_id(event.get("account_id"))
1134
+ for w in quota_after:
1135
+ if w.get("window_duration_mins") == 10080 and isinstance(w.get("used_percent"), (int, float)):
1136
+ with get_db() as db_conn:
1137
+ record_observation(
1138
+ conn=db_conn,
1139
+ account_id=resolved_account,
1140
+ bucket_id=w.get("slot") or "primary",
1141
+ used_percent=float(w["used_percent"]),
1142
+ sampled_at_ms=sample_time_after,
1143
+ sample_source="turn_finish",
1144
+ resets_at_ms=w.get("resets_at"),
1145
+ run_id=key,
1146
+ )
1147
+ export_quota_summary(db_conn)
1148
+ except Exception:
1149
+ pass
1150
+ run["parent"]["usage_after"] = parent_after
1151
+ service_delta = usage_delta(
1152
+ run["parent"].get("usage_before"), parent_after
1153
+ )
1154
+ transcript_path = run.get("transcript_path") or event.get("transcript_path")
1155
+ if not transcript_path or not Path(transcript_path).is_file():
1156
+ thread_path = (
1157
+ run.get("thread", {}).get("path")
1158
+ if isinstance(run.get("thread"), dict)
1159
+ else None
1160
+ )
1161
+ if thread_path and Path(thread_path).is_file():
1162
+ transcript_path = thread_path
1163
+ else:
1164
+ resolved = find_session_transcript(event.get("session_id"))
1165
+ if resolved:
1166
+ transcript_path = resolved
1167
+ if transcript_path:
1168
+ run["transcript_path"] = transcript_path
1169
+
1170
+ transcript_usage = transcript_turn_usage(
1171
+ transcript_path,
1172
+ event.get("turn_id"),
1173
+ )
1174
+ run["parent"]["usage_delta"] = merge_usage(
1175
+ transcript_usage, service_delta
1176
+ )
1177
+ apply_participant_metadata(
1178
+ run["parent"],
1179
+ event=event,
1180
+ transcript_path=run.get("transcript_path"),
1181
+ turn_id=event.get("turn_id"),
1182
+ usage=run["parent"].get("usage_delta"),
1183
+ )
1184
+ for agent_id, worker in run.get("workers", {}).items():
1185
+ if not isinstance(worker, dict):
1186
+ continue
1187
+ executions = _ensure_worker_execution_map(worker)
1188
+ service_usage = (
1189
+ usage_summary(server.thread_usage(agent_id))
1190
+ if server.available
1191
+ else None
1192
+ )
1193
+ for execution in executions.values():
1194
+ if not isinstance(execution, dict):
1195
+ continue
1196
+ if execution.get("usage") is None:
1197
+ execution_path = execution.get("transcript_path") or worker.get(
1198
+ "transcript_path"
1199
+ )
1200
+ execution_turn = execution.get("turn_id") or worker.get("turn_id")
1201
+ transcript_usage = transcript_turn_usage(
1202
+ execution_path,
1203
+ execution_turn,
1204
+ )
1205
+ service_delta = _service_usage_delta(
1206
+ worker,
1207
+ execution,
1208
+ service_usage,
1209
+ len(executions),
1210
+ event.get("session_id"),
1211
+ agent_id,
1212
+ )
1213
+ if service_usage is not None and not execution.get("service_usage_finalized"):
1214
+ execution["service_usage_cumulative"] = service_usage
1215
+ execution["usage"] = _merge_execution_usage(
1216
+ execution.get("usage"),
1217
+ merge_usage(transcript_usage, service_delta),
1218
+ )
1219
+ _rebuild_worker_usage(worker)
1220
+ if any(
1221
+ execution.get("status") == "running"
1222
+ for execution in executions.values()
1223
+ if isinstance(execution, dict)
1224
+ ):
1225
+ for execution in executions.values():
1226
+ if isinstance(execution, dict) and execution.get("status") == "running":
1227
+ execution["status"] = "observed"
1228
+ _refresh_worker_lifecycle(worker)
1229
+ apply_participant_metadata(
1230
+ worker,
1231
+ transcript_path=worker.get("transcript_path"),
1232
+ turn_id=worker.get("turn_id") or event.get("turn_id"),
1233
+ usage=worker.get("usage"),
1234
+ )
1235
+ atomic_json(path, run)
1236
+ atomic_json(LAST_FILE, run)
1237
+
1238
+ reconciled = reconcile_orphan_workers()
1239
+ if key in reconciled:
1240
+ refreshed = read_json_object(run_path_for_key(key))
1241
+ if refreshed is not None:
1242
+ run = refreshed
1243
+ atomic_json(LAST_FILE, run)
1244
+ run_maintenance()
1245
+ if policy_bool("telemetry", "summary", True):
1246
+ write_stop_output(render_summary(run))
1247
+ send_system_notification(run)