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,421 @@
|
|
|
1
|
+
"""Shared constants, policy configuration, locking, and formatting helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
import time
|
|
9
|
+
import unicodedata
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
CODEX_HOME = Path(os.environ.get("CODEX_HOME", Path.home() / ".codex"))
|
|
15
|
+
STATE_ROOT = CODEX_HOME / "codex-flow" / "telemetry"
|
|
16
|
+
RUNS_DIR = STATE_ROOT / "runs"
|
|
17
|
+
LAST_FILE = STATE_ROOT / "last.json"
|
|
18
|
+
QUOTA_LEDGER_DB = STATE_ROOT / "quota_ledger.db"
|
|
19
|
+
QUOTA_SUMMARY_FILE = STATE_ROOT / "quota_summary.json"
|
|
20
|
+
QUOTA_LOCK_KEY = "quota_ledger"
|
|
21
|
+
WORKER_INDEX_FILE = STATE_ROOT / "worker-index.json"
|
|
22
|
+
SESSION_INDEX_FILE = CODEX_HOME / "session_index.jsonl"
|
|
23
|
+
TIMEOUT = float(os.environ.get("CODEX_FLOW_TELEMETRY_TIMEOUT", "3.0"))
|
|
24
|
+
LOCK_TIMEOUT = float(os.environ.get("CODEX_FLOW_TELEMETRY_LOCK_TIMEOUT", "2.0"))
|
|
25
|
+
DEFAULT_RETENTION_DAYS = 30
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def display_width(s: str) -> int:
|
|
29
|
+
"""Calculate terminal display width considering fullwidth/CJK characters."""
|
|
30
|
+
width = 0
|
|
31
|
+
for ch in s:
|
|
32
|
+
if unicodedata.east_asian_width(ch) in ("W", "F"):
|
|
33
|
+
width += 2
|
|
34
|
+
else:
|
|
35
|
+
width += 1
|
|
36
|
+
return width
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def truncate_display(s: str, max_width: int) -> str:
|
|
40
|
+
"""Truncate a string to fit max_width visual columns."""
|
|
41
|
+
cur = 0
|
|
42
|
+
res: list[str] = []
|
|
43
|
+
for ch in s:
|
|
44
|
+
w = 2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1
|
|
45
|
+
if cur + w > max_width:
|
|
46
|
+
break
|
|
47
|
+
res.append(ch)
|
|
48
|
+
cur += w
|
|
49
|
+
return "".join(res)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def pad_display(s: str, target_width: int, align: str = "left") -> str:
|
|
53
|
+
"""Pad string to target display width."""
|
|
54
|
+
w = display_width(s)
|
|
55
|
+
pad = max(0, target_width - w)
|
|
56
|
+
if align == "right":
|
|
57
|
+
return (" " * pad) + s
|
|
58
|
+
return s + (" " * pad)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def policy_value(section_name: str, key_name: str) -> str | None:
|
|
62
|
+
"""Read one simple TOML scalar without adding a Python-version dependency."""
|
|
63
|
+
path = CODEX_HOME / "codex-flow.toml"
|
|
64
|
+
try:
|
|
65
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
66
|
+
except (OSError, UnicodeError):
|
|
67
|
+
return None
|
|
68
|
+
section: str | None = None
|
|
69
|
+
for raw in lines:
|
|
70
|
+
line = raw.split("#", 1)[0].strip()
|
|
71
|
+
if not line:
|
|
72
|
+
continue
|
|
73
|
+
if line.startswith("[") and line.endswith("]"):
|
|
74
|
+
section = line[1:-1].strip()
|
|
75
|
+
continue
|
|
76
|
+
if section != section_name or "=" not in line:
|
|
77
|
+
continue
|
|
78
|
+
key, value = (part.strip() for part in line.split("=", 1))
|
|
79
|
+
if key != key_name:
|
|
80
|
+
continue
|
|
81
|
+
return value.strip().strip('"')
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def policy_bool(section_name: str, key_name: str, default: bool) -> bool:
|
|
86
|
+
value = policy_value(section_name, key_name)
|
|
87
|
+
if value is None:
|
|
88
|
+
return default
|
|
89
|
+
normalized = value.lower()
|
|
90
|
+
if normalized == "true":
|
|
91
|
+
return True
|
|
92
|
+
if normalized == "false":
|
|
93
|
+
return False
|
|
94
|
+
return default
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def policy_int(section_name: str, key_name: str, default: int) -> int:
|
|
98
|
+
value = policy_value(section_name, key_name)
|
|
99
|
+
try:
|
|
100
|
+
parsed = int(value) if value is not None else default
|
|
101
|
+
except (TypeError, ValueError):
|
|
102
|
+
return default
|
|
103
|
+
return parsed if parsed > 0 else default
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def telemetry_notifications_enabled() -> bool:
|
|
107
|
+
value = os.environ.get("CODEX_FLOW_TELEMETRY_NOTIFICATIONS")
|
|
108
|
+
if value is not None:
|
|
109
|
+
normalized = value.strip().lower()
|
|
110
|
+
if normalized in {"0", "false", "no", "off"}:
|
|
111
|
+
return False
|
|
112
|
+
if normalized in {"1", "true", "yes", "on"}:
|
|
113
|
+
return True
|
|
114
|
+
return policy_bool("telemetry", "notifications", False)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def telemetry_retention_days() -> int:
|
|
118
|
+
value = os.environ.get("CODEX_FLOW_TELEMETRY_RETENTION_DAYS")
|
|
119
|
+
if value is not None:
|
|
120
|
+
try:
|
|
121
|
+
parsed = int(value)
|
|
122
|
+
if parsed > 0:
|
|
123
|
+
return parsed
|
|
124
|
+
except ValueError:
|
|
125
|
+
pass
|
|
126
|
+
return policy_int("telemetry", "retention_days", DEFAULT_RETENTION_DAYS)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def now_ms() -> int:
|
|
130
|
+
return int(time.time() * 1000)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def atomic_json(path: Path, value: Any) -> None:
|
|
134
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
fd, tmp = tempfile.mkstemp(prefix=path.name + ".", dir=str(path.parent))
|
|
136
|
+
try:
|
|
137
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
138
|
+
json.dump(value, handle, ensure_ascii=False, indent=2, sort_keys=True)
|
|
139
|
+
handle.write("\n")
|
|
140
|
+
os.replace(tmp, path)
|
|
141
|
+
finally:
|
|
142
|
+
if os.path.exists(tmp):
|
|
143
|
+
os.unlink(tmp)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@contextmanager
|
|
147
|
+
def state_lock(key: str):
|
|
148
|
+
STATE_ROOT.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
lock = STATE_ROOT / ("." + key.replace("/", "_") + ".lock")
|
|
150
|
+
deadline = time.monotonic() + LOCK_TIMEOUT
|
|
151
|
+
acquired = False
|
|
152
|
+
while True:
|
|
153
|
+
try:
|
|
154
|
+
lock.mkdir()
|
|
155
|
+
acquired = True
|
|
156
|
+
break
|
|
157
|
+
except FileExistsError:
|
|
158
|
+
if time.monotonic() >= deadline:
|
|
159
|
+
break
|
|
160
|
+
time.sleep(0.025)
|
|
161
|
+
try:
|
|
162
|
+
yield acquired
|
|
163
|
+
finally:
|
|
164
|
+
if acquired:
|
|
165
|
+
try:
|
|
166
|
+
lock.rmdir()
|
|
167
|
+
except OSError:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def read_json_object(path: Path) -> dict[str, Any] | None:
|
|
172
|
+
try:
|
|
173
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
174
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
175
|
+
return None
|
|
176
|
+
return value if isinstance(value, dict) else None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def numeric_ms(value: Any) -> int | None:
|
|
180
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
181
|
+
return int(value)
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def safe_key_part(value: Any) -> str:
|
|
186
|
+
return str(value).replace("/", "_").replace("\\", "_")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def run_key(event: dict[str, Any]) -> str:
|
|
190
|
+
session = safe_key_part(event.get("session_id") or "unknown")
|
|
191
|
+
turn = safe_key_part(event.get("turn_id") or "unknown")
|
|
192
|
+
return f"{session}--{turn}"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def run_path_for_key(key: str) -> Path:
|
|
196
|
+
return RUNS_DIR / f"{safe_key_part(key)}.json"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def run_path(event: dict[str, Any]) -> Path:
|
|
200
|
+
return run_path_for_key(run_key(event))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def load_run(event: dict[str, Any], key: str | None = None) -> dict[str, Any]:
|
|
204
|
+
resolved_key = key or run_key(event)
|
|
205
|
+
path = run_path_for_key(resolved_key)
|
|
206
|
+
existing = read_json_object(path) if path.exists() else None
|
|
207
|
+
if existing is not None:
|
|
208
|
+
return existing
|
|
209
|
+
return {
|
|
210
|
+
"schema_version": 1,
|
|
211
|
+
"session_id": event.get("session_id"),
|
|
212
|
+
"turn_id": event.get("turn_id"),
|
|
213
|
+
"cwd": event.get("cwd"),
|
|
214
|
+
"parent": {"model": event.get("model")},
|
|
215
|
+
"workers": {},
|
|
216
|
+
"started_at_ms": now_ms(),
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def iter_run_files() -> list[Path]:
|
|
221
|
+
try:
|
|
222
|
+
return sorted(path for path in RUNS_DIR.glob("*.json") if path.is_file())
|
|
223
|
+
except OSError:
|
|
224
|
+
return []
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def load_worker_index() -> dict[str, Any]:
|
|
228
|
+
value = read_json_object(WORKER_INDEX_FILE)
|
|
229
|
+
if value is None:
|
|
230
|
+
return {"schema_version": 1, "workers": {}}
|
|
231
|
+
workers = value.get("workers")
|
|
232
|
+
if not isinstance(workers, dict):
|
|
233
|
+
value["workers"] = {}
|
|
234
|
+
return value
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def worker_index_entry(
|
|
238
|
+
agent_id: str, session_id: Any, worker_turn_id: Any = None
|
|
239
|
+
) -> dict[str, Any] | None:
|
|
240
|
+
if not agent_id or agent_id == "unknown":
|
|
241
|
+
return None
|
|
242
|
+
index = load_worker_index()
|
|
243
|
+
workers = index.get("workers")
|
|
244
|
+
entry = workers.get(agent_id) if isinstance(workers, dict) else None
|
|
245
|
+
if not isinstance(entry, dict):
|
|
246
|
+
return None
|
|
247
|
+
if str(entry.get("session_id") or "") != str(session_id or ""):
|
|
248
|
+
return None
|
|
249
|
+
key = entry.get("run_key")
|
|
250
|
+
if worker_turn_id is not None:
|
|
251
|
+
target = str(worker_turn_id)
|
|
252
|
+
executions = entry.get("executions")
|
|
253
|
+
if isinstance(executions, dict):
|
|
254
|
+
execution = executions.get(target)
|
|
255
|
+
if isinstance(execution, dict):
|
|
256
|
+
execution_key = execution.get("run_key") or execution.get("parent_key")
|
|
257
|
+
if isinstance(execution_key, str) and run_path_for_key(execution_key).is_file():
|
|
258
|
+
result = dict(entry)
|
|
259
|
+
result["run_key"] = execution_key
|
|
260
|
+
result["parent_key"] = execution_key
|
|
261
|
+
result["parent_turn_id"] = execution.get("parent_turn_id")
|
|
262
|
+
result["execution"] = execution
|
|
263
|
+
return result
|
|
264
|
+
elif isinstance(executions, list):
|
|
265
|
+
for execution in executions:
|
|
266
|
+
if not isinstance(execution, dict) or str(execution.get("turn_id") or "") != target:
|
|
267
|
+
continue
|
|
268
|
+
execution_key = execution.get("run_key") or execution.get("parent_key")
|
|
269
|
+
if isinstance(execution_key, str) and run_path_for_key(execution_key).is_file():
|
|
270
|
+
result = dict(entry)
|
|
271
|
+
result["run_key"] = execution_key
|
|
272
|
+
result["parent_key"] = execution_key
|
|
273
|
+
result["parent_turn_id"] = execution.get("parent_turn_id")
|
|
274
|
+
result["execution"] = execution
|
|
275
|
+
return result
|
|
276
|
+
# A legacy index entry has no child-turn identity. Returning it here
|
|
277
|
+
# would silently route a delayed stop to whichever parent was latest.
|
|
278
|
+
return None
|
|
279
|
+
if not isinstance(key, str) or not run_path_for_key(key).is_file():
|
|
280
|
+
return None
|
|
281
|
+
return entry
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def remember_worker_parent(
|
|
285
|
+
agent_id: str,
|
|
286
|
+
session_id: Any,
|
|
287
|
+
parent_key: str,
|
|
288
|
+
parent_turn_id: Any,
|
|
289
|
+
started_at_ms: Any,
|
|
290
|
+
worker_turn_id: Any = None,
|
|
291
|
+
finished_at_ms: Any = None,
|
|
292
|
+
transcript_path: Any = None,
|
|
293
|
+
) -> None:
|
|
294
|
+
if not agent_id or agent_id == "unknown":
|
|
295
|
+
return
|
|
296
|
+
with state_lock("worker-index") as acquired:
|
|
297
|
+
if not acquired:
|
|
298
|
+
return
|
|
299
|
+
index = load_worker_index()
|
|
300
|
+
workers = index.setdefault("workers", {})
|
|
301
|
+
if not isinstance(workers, dict):
|
|
302
|
+
workers = {}
|
|
303
|
+
index["workers"] = workers
|
|
304
|
+
existing = workers.get(agent_id)
|
|
305
|
+
new_started = numeric_ms(started_at_ms)
|
|
306
|
+
if not isinstance(existing, dict) or str(existing.get("session_id") or "") != str(session_id or ""):
|
|
307
|
+
existing = None
|
|
308
|
+
entry = dict(existing) if existing is not None else {
|
|
309
|
+
"agent_id": agent_id,
|
|
310
|
+
"session_id": session_id,
|
|
311
|
+
}
|
|
312
|
+
old_started = numeric_ms(entry.get("started_at_ms"))
|
|
313
|
+
# Keep the legacy top-level pointer useful for a newly-started worker,
|
|
314
|
+
# while retaining every child execution for exact late-stop routing.
|
|
315
|
+
if old_started is None or (new_started is not None and new_started >= old_started):
|
|
316
|
+
entry.update(
|
|
317
|
+
{
|
|
318
|
+
"parent_key": parent_key,
|
|
319
|
+
"parent_turn_id": parent_turn_id,
|
|
320
|
+
"run_key": parent_key,
|
|
321
|
+
"started_at_ms": new_started,
|
|
322
|
+
}
|
|
323
|
+
)
|
|
324
|
+
executions = entry.get("executions")
|
|
325
|
+
if not isinstance(executions, dict):
|
|
326
|
+
executions = {}
|
|
327
|
+
entry["executions"] = executions
|
|
328
|
+
if worker_turn_id is not None:
|
|
329
|
+
execution_id = str(worker_turn_id)
|
|
330
|
+
execution = executions.get(execution_id)
|
|
331
|
+
if not isinstance(execution, dict):
|
|
332
|
+
execution = {"turn_id": worker_turn_id}
|
|
333
|
+
execution["run_key"] = parent_key
|
|
334
|
+
execution["parent_key"] = parent_key
|
|
335
|
+
execution["parent_turn_id"] = parent_turn_id
|
|
336
|
+
previous_started = numeric_ms(execution.get("started_at_ms"))
|
|
337
|
+
if previous_started is None or (new_started is not None and new_started < previous_started):
|
|
338
|
+
execution["started_at_ms"] = new_started
|
|
339
|
+
new_finished = numeric_ms(finished_at_ms)
|
|
340
|
+
previous_finished = numeric_ms(execution.get("finished_at_ms"))
|
|
341
|
+
if new_finished is not None and (previous_finished is None or new_finished > previous_finished):
|
|
342
|
+
execution["finished_at_ms"] = new_finished
|
|
343
|
+
if transcript_path is not None:
|
|
344
|
+
execution["transcript_path"] = transcript_path
|
|
345
|
+
execution["updated_at_ms"] = now_ms()
|
|
346
|
+
executions[execution_id] = execution
|
|
347
|
+
entry["updated_at_ms"] = now_ms()
|
|
348
|
+
workers[agent_id] = entry
|
|
349
|
+
atomic_json(WORKER_INDEX_FILE, index)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def text_value(value: Any) -> str | None:
|
|
353
|
+
if not isinstance(value, str):
|
|
354
|
+
return None
|
|
355
|
+
value = value.strip()
|
|
356
|
+
return value or None
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def compact_text(value: Any, limit: int = 52) -> str | None:
|
|
360
|
+
if value is None:
|
|
361
|
+
return None
|
|
362
|
+
text = " ".join(str(value).split())
|
|
363
|
+
if not text:
|
|
364
|
+
return None
|
|
365
|
+
if len(text) <= limit:
|
|
366
|
+
return text
|
|
367
|
+
return text[: max(1, limit - 1)] + "…"
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def fmt_tokens(value: Any) -> str:
|
|
371
|
+
if not isinstance(value, (int, float)):
|
|
372
|
+
return "n/a"
|
|
373
|
+
value = int(value)
|
|
374
|
+
if value >= 1_000_000:
|
|
375
|
+
return f"{value / 1_000_000:.2f}m"
|
|
376
|
+
if value >= 1_000:
|
|
377
|
+
return f"{value / 1_000:.1f}k"
|
|
378
|
+
return str(value)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def window_label(minutes: Any) -> str:
|
|
382
|
+
if minutes == 300:
|
|
383
|
+
return "5h"
|
|
384
|
+
if minutes == 10080:
|
|
385
|
+
return "7d"
|
|
386
|
+
if isinstance(minutes, int) and minutes > 0:
|
|
387
|
+
if minutes % 1440 == 0:
|
|
388
|
+
return f"{minutes // 1440}d"
|
|
389
|
+
return f"{minutes}m"
|
|
390
|
+
return "quota"
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def fmt_percent(value: Any) -> str | None:
|
|
394
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
395
|
+
return f"{value:g}"
|
|
396
|
+
if value is None:
|
|
397
|
+
return None
|
|
398
|
+
return str(value)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def fmt_local_timestamp(value: Any, *, milliseconds: bool = False) -> str | None:
|
|
402
|
+
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
|
403
|
+
return None
|
|
404
|
+
timestamp = float(value) / 1000 if milliseconds else float(value)
|
|
405
|
+
try:
|
|
406
|
+
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp))
|
|
407
|
+
except (OverflowError, OSError, ValueError):
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def fmt_duration_ms(value: Any) -> str | None:
|
|
412
|
+
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
|
413
|
+
return None
|
|
414
|
+
total_seconds = max(0, int(round(float(value) / 1000)))
|
|
415
|
+
hours, remainder = divmod(total_seconds, 3600)
|
|
416
|
+
minutes, seconds = divmod(remainder, 60)
|
|
417
|
+
if hours:
|
|
418
|
+
return f"{hours}h {minutes}m {seconds}s"
|
|
419
|
+
if minutes:
|
|
420
|
+
return f"{minutes}m {seconds}s"
|
|
421
|
+
return f"{seconds}s"
|