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.
- codex_flow/__init__.py +28 -0
- codex_flow/__main__.py +9 -0
- codex_flow/cli.py +242 -0
- codex_flow/data/LICENSE +21 -0
- codex_flow/data/README.en.md +303 -0
- codex_flow/data/README.md +305 -0
- codex_flow/data/VERSION +1 -0
- codex_flow/data/apps/chatgpt-mcp/README.md +86 -0
- codex_flow/data/apps/chatgpt-mcp/__init__.py +1 -0
- codex_flow/data/apps/chatgpt-mcp/adapter.py +458 -0
- codex_flow/data/apps/chatgpt-mcp/server.py +358 -0
- codex_flow/data/apps/chatgpt-mcp/widget.html +927 -0
- codex_flow/data/apps/macos-overlay/README.en.md +121 -0
- codex_flow/data/apps/macos-overlay/README.md +123 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayRuntimeState.swift +126 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayScreenGeometry.swift +82 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayWindowController.swift +1052 -0
- codex_flow/data/apps/macos-overlay/Sources/Localization.swift +197 -0
- codex_flow/data/apps/macos-overlay/Sources/Models/TelemetryData.swift +1557 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/AccountSnapshotService.swift +1101 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/FlowPilotInstanceLock.swift +153 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/IPCServer.swift +298 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryQueryEngine.swift +800 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryWatcher.swift +135 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/UpdateService.swift +610 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AccountView.swift +610 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AnalyticsView.swift +566 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AutostartView.swift +293 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/BubbleView.swift +317 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/HistoryView.swift +1124 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/HoverRevealText.swift +165 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/InspectorSkillsToolsView.swift +121 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/LogoView.swift +182 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/SleekSwitch.swift +117 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/StrategyModeView.swift +561 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/SummaryView.swift +1273 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/UpdateView.swift +352 -0
- codex_flow/data/apps/macos-overlay/Sources/main.swift +340 -0
- codex_flow/data/apps/macos-overlay/Tests/OverlayScreenGeometryTests.swift +163 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryPhase1ContractTests.swift +357 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryQueryEngineConcurrencyTests.swift +221 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryQuotaSelectionTests.swift +158 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryWorkerTokenTests.swift +122 -0
- codex_flow/data/apps/macos-overlay/build.sh +75 -0
- codex_flow/data/benchmark/corpus.json +103 -0
- codex_flow/data/benchmark/manifest.example.json +41 -0
- codex_flow/data/benchmark/manifest.schema.json +137 -0
- codex_flow/data/benchmark/prices/gpt-5.6-2026-08-30.json +5 -0
- codex_flow/data/benchmark/profiles.json +90 -0
- codex_flow/data/benchmark/schema.json +77 -0
- codex_flow/data/benchmark/tasks.json +50 -0
- codex_flow/data/completions/codex-flow.bash +34 -0
- codex_flow/data/completions/codex-flow.zsh +52 -0
- codex_flow/data/glama.json +6 -0
- codex_flow/data/install-release.ps1 +126 -0
- codex_flow/data/install-release.sh +155 -0
- codex_flow/data/install.ps1 +349 -0
- codex_flow/data/install.sh +362 -0
- codex_flow/data/policy/benchmark.toml +49 -0
- codex_flow/data/policy/defaults.toml +70 -0
- codex_flow/data/scripts/analyze-benchmark.py +510 -0
- codex_flow/data/scripts/benchmark-local.py +171 -0
- codex_flow/data/scripts/check-recommendation.py +277 -0
- codex_flow/data/scripts/doctor.py +449 -0
- codex_flow/data/scripts/generate-release-manifest.py +74 -0
- codex_flow/data/scripts/localization.py +192 -0
- codex_flow/data/scripts/manage-hooks.py +448 -0
- codex_flow/data/scripts/manage-instructions.py +389 -0
- codex_flow/data/scripts/manage-shell.py +151 -0
- codex_flow/data/scripts/materialize-corpus.py +193 -0
- codex_flow/data/scripts/menu.py +646 -0
- codex_flow/data/scripts/migrations/0001_update_settings.py +80 -0
- codex_flow/data/scripts/package-release.py +132 -0
- codex_flow/data/scripts/render-benchmark-report.py +292 -0
- codex_flow/data/scripts/run-benchmark.py +829 -0
- codex_flow/data/scripts/strategies/__init__.py +28 -0
- codex_flow/data/scripts/strategies/balanced.py +115 -0
- codex_flow/data/scripts/strategies/base.py +363 -0
- codex_flow/data/scripts/strategies/efficient.py +158 -0
- codex_flow/data/scripts/strategies/lifecycle_runtime.py +590 -0
- codex_flow/data/scripts/strategies/quality.py +209 -0
- codex_flow/data/scripts/strategies/speed.py +108 -0
- codex_flow/data/scripts/strategies/task_budget_runtime.py +644 -0
- codex_flow/data/scripts/strategies/task_phase_runtime.py +341 -0
- codex_flow/data/scripts/strategies/work_unit_runtime.py +421 -0
- codex_flow/data/scripts/strategy_runtime.py +1091 -0
- codex_flow/data/scripts/telemetry.py +400 -0
- codex_flow/data/scripts/telemetry_core/__init__.py +192 -0
- codex_flow/data/scripts/telemetry_core/app_server.py +1192 -0
- codex_flow/data/scripts/telemetry_core/collector.py +1247 -0
- codex_flow/data/scripts/telemetry_core/common.py +421 -0
- codex_flow/data/scripts/telemetry_core/latency.py +593 -0
- codex_flow/data/scripts/telemetry_core/query.py +427 -0
- codex_flow/data/scripts/telemetry_core/quota_ledger.py +598 -0
- codex_flow/data/scripts/telemetry_core/render.py +460 -0
- codex_flow/data/scripts/telemetry_core/repair.py +223 -0
- codex_flow/data/scripts/ui.py +266 -0
- codex_flow/data/scripts/update-homebrew-formula.py +146 -0
- codex_flow/data/scripts/update_runtime_config.py +134 -0
- codex_flow/data/scripts/updater.py +1718 -0
- codex_flow/data/smithery.yaml +18 -0
- codex_flow/data/templates/agents/worker-explorer.toml +24 -0
- codex_flow/data/templates/agents/worker-implementer.toml +49 -0
- codex_flow/data/templates/agents/worker-reviewer.toml +25 -0
- codex_flow/data/templates/flow-pilot-instructions.md +35 -0
- codex_flow/data/templates/skills/flow-pilot/SKILL.md +577 -0
- codex_flow/mcp.py +35 -0
- codex_flow-2.1.13.dist-info/METADATA +342 -0
- codex_flow-2.1.13.dist-info/RECORD +113 -0
- codex_flow-2.1.13.dist-info/WHEEL +5 -0
- codex_flow-2.1.13.dist-info/entry_points.txt +3 -0
- codex_flow-2.1.13.dist-info/licenses/LICENSE +21 -0
- 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)
|