codex-autorunner 1.0.0__py3-none-any.whl → 1.2.0__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_autorunner/__init__.py +12 -1
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/runtime.py +59 -18
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +36 -7
- codex_autorunner/bootstrap.py +226 -4
- codex_autorunner/cli.py +5 -1174
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +20 -0
- codex_autorunner/core/about_car.py +119 -1
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_threads.py +17 -2
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +6 -2
- codex_autorunner/core/config.py +433 -4
- codex_autorunner/core/context_awareness.py +38 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/drafts.py +58 -4
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +96 -2
- codex_autorunner/core/flows/models.py +13 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +134 -0
- codex_autorunner/core/flows/runtime.py +57 -4
- codex_autorunner/core/flows/store.py +142 -7
- codex_autorunner/core/flows/transition.py +27 -15
- codex_autorunner/core/flows/ux_helpers.py +272 -0
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +291 -20
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +496 -0
- codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
- codex_autorunner/core/pma_lifecycle.py +527 -0
- codex_autorunner/core/pma_queue.py +367 -0
- codex_autorunner/core/pma_safety.py +221 -0
- codex_autorunner/core/pma_state.py +115 -0
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
- codex_autorunner/core/prompt.py +0 -80
- codex_autorunner/core/prompts.py +56 -172
- codex_autorunner/core/redaction.py +0 -4
- codex_autorunner/core/review_context.py +11 -9
- codex_autorunner/core/runner_controller.py +35 -33
- codex_autorunner/core/runner_state.py +147 -0
- codex_autorunner/core/runtime.py +829 -0
- codex_autorunner/core/sqlite_utils.py +13 -4
- codex_autorunner/core/state.py +7 -10
- codex_autorunner/core/state_roots.py +62 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/templates/__init__.py +39 -0
- codex_autorunner/core/templates/git_mirror.py +234 -0
- codex_autorunner/core/templates/provenance.py +56 -0
- codex_autorunner/core/templates/scan_cache.py +120 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +218 -0
- codex_autorunner/core/ticket_manager_cli.py +494 -0
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/update.py +4 -5
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +125 -15
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
- codex_autorunner/flows/ticket_flow/definition.py +52 -3
- codex_autorunner/integrations/agents/__init__.py +11 -19
- codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +177 -25
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +305 -32
- codex_autorunner/integrations/agents/runner.py +86 -0
- codex_autorunner/integrations/agents/wiring.py +279 -0
- codex_autorunner/integrations/app_server/client.py +7 -60
- codex_autorunner/integrations/app_server/env.py +2 -107
- codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
- codex_autorunner/integrations/telegram/adapter.py +65 -0
- codex_autorunner/integrations/telegram/config.py +46 -0
- codex_autorunner/integrations/telegram/constants.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
- codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
- codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +22 -1
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +45 -10
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
- codex_autorunner/integrations/telegram/transport.py +13 -4
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/routes/__init__.py +37 -76
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +2 -238
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -596
- codex_autorunner/routes/file_chat.py +4 -833
- codex_autorunner/routes/flows.py +4 -977
- codex_autorunner/routes/messages.py +4 -456
- codex_autorunner/routes/repos.py +2 -196
- codex_autorunner/routes/review.py +2 -147
- codex_autorunner/routes/sessions.py +2 -175
- codex_autorunner/routes/settings.py +2 -168
- codex_autorunner/routes/shared.py +2 -275
- codex_autorunner/routes/system.py +4 -193
- codex_autorunner/routes/usage.py +2 -86
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +2 -270
- codex_autorunner/server.py +4 -4
- codex_autorunner/static/agentControls.js +61 -16
- codex_autorunner/static/app.js +126 -14
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +7 -7
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/dashboard.js +224 -171
- codex_autorunner/static/docChatCore.js +185 -13
- codex_autorunner/static/fileChat.js +68 -40
- codex_autorunner/static/fileboxUi.js +159 -0
- codex_autorunner/static/hub.js +114 -131
- codex_autorunner/static/index.html +375 -49
- codex_autorunner/static/messages.js +568 -87
- codex_autorunner/static/notifications.js +255 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +128 -6
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9798 -6143
- codex_autorunner/static/tabs.js +152 -11
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminal.js +18 -0
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +137 -15
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +821 -98
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +39 -0
- codex_autorunner/static/workspace.js +389 -82
- codex_autorunner/static/workspaceFileBrowser.js +15 -13
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +2534 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2223 -0
- codex_autorunner/surfaces/web/hub_jobs.py +192 -0
- codex_autorunner/surfaces/web/middleware.py +587 -0
- codex_autorunner/surfaces/web/pty_session.py +370 -0
- codex_autorunner/surfaces/web/review.py +6 -0
- codex_autorunner/surfaces/web/routes/__init__.py +82 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +284 -0
- codex_autorunner/surfaces/web/routes/app_server.py +132 -0
- codex_autorunner/surfaces/web/routes/archive.py +357 -0
- codex_autorunner/surfaces/web/routes/base.py +615 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +1354 -0
- codex_autorunner/surfaces/web/routes/messages.py +490 -0
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +197 -0
- codex_autorunner/surfaces/web/routes/review.py +148 -0
- codex_autorunner/surfaces/web/routes/sessions.py +176 -0
- codex_autorunner/surfaces/web/routes/settings.py +169 -0
- codex_autorunner/surfaces/web/routes/shared.py +277 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/routes/usage.py +89 -0
- codex_autorunner/surfaces/web/routes/voice.py +120 -0
- codex_autorunner/surfaces/web/routes/workspace.py +271 -0
- codex_autorunner/surfaces/web/runner_manager.py +25 -0
- codex_autorunner/surfaces/web/schemas.py +469 -0
- codex_autorunner/surfaces/web/static_assets.py +490 -0
- codex_autorunner/surfaces/web/static_refresh.py +86 -0
- codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
- codex_autorunner/tickets/__init__.py +8 -1
- codex_autorunner/tickets/agent_pool.py +53 -4
- codex_autorunner/tickets/files.py +37 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +6 -1
- codex_autorunner/tickets/outbox.py +50 -2
- codex_autorunner/tickets/runner.py +396 -57
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1949
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -586
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -376
- codex_autorunner/web/static_assets.py +4 -441
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/paths.py +49 -33
- codex_autorunner-1.2.0.dist-info/METADATA +150 -0
- codex_autorunner-1.2.0.dist-info/RECORD +339 -0
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -2653
- codex_autorunner/core/static_assets.py +0 -55
- codex_autorunner-1.0.0.dist-info/METADATA +0 -246
- codex_autorunner-1.0.0.dist-info/RECORD +0 -251
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Analytics summary routes.
|
|
2
|
+
|
|
3
|
+
This module aggregates run/ticket/message metadata for the analytics dashboard
|
|
4
|
+
without relying on legacy autorunner endpoints. It intentionally reads from the
|
|
5
|
+
filesystem-backed ticket_flow store and ticket files to keep the UI consistent
|
|
6
|
+
with the rest of the app.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from fastapi import APIRouter
|
|
16
|
+
|
|
17
|
+
from ....core.flows.models import FlowEventType, FlowRunRecord, FlowRunStatus
|
|
18
|
+
from ....core.flows.store import FlowStore
|
|
19
|
+
from ....core.utils import find_repo_root
|
|
20
|
+
from ....tickets.files import list_ticket_paths, read_ticket, ticket_is_done
|
|
21
|
+
from ....tickets.outbox import parse_dispatch, resolve_outbox_paths
|
|
22
|
+
from ....tickets.replies import resolve_reply_paths
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _flows_db_path(repo_root: Path) -> Path:
|
|
26
|
+
return repo_root / ".codex-autorunner" / "flows.db"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _select_primary_run(records: list[FlowRunRecord]) -> Optional[FlowRunRecord]:
|
|
30
|
+
"""Select the primary run for analytics display.
|
|
31
|
+
|
|
32
|
+
Only considers the newest run (records[0]). If it's active or paused, return it.
|
|
33
|
+
If the newest run is terminal (completed/stopped/failed), return None to show idle.
|
|
34
|
+
This matches the backend's _active_or_paused_run() logic and prevents showing
|
|
35
|
+
stale data from old paused runs when newer runs have completed.
|
|
36
|
+
"""
|
|
37
|
+
if not records:
|
|
38
|
+
return None
|
|
39
|
+
newest = records[0]
|
|
40
|
+
if (
|
|
41
|
+
FlowRunStatus(newest.status).is_active()
|
|
42
|
+
or FlowRunStatus(newest.status).is_paused()
|
|
43
|
+
):
|
|
44
|
+
return newest
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _parse_timestamp(value: Optional[str]) -> Optional[datetime]:
|
|
49
|
+
if not value:
|
|
50
|
+
return None
|
|
51
|
+
try:
|
|
52
|
+
if value.endswith("Z"):
|
|
53
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
54
|
+
return datetime.fromisoformat(value)
|
|
55
|
+
except ValueError:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _duration_seconds(
|
|
60
|
+
started_at: Optional[str], finished_at: Optional[str], status: str
|
|
61
|
+
) -> Optional[float]:
|
|
62
|
+
start_dt = _parse_timestamp(started_at)
|
|
63
|
+
if not start_dt:
|
|
64
|
+
return None
|
|
65
|
+
end_dt = _parse_timestamp(finished_at)
|
|
66
|
+
if not end_dt and status in {
|
|
67
|
+
FlowRunStatus.RUNNING.value,
|
|
68
|
+
FlowRunStatus.PAUSED.value,
|
|
69
|
+
FlowRunStatus.PENDING.value,
|
|
70
|
+
}:
|
|
71
|
+
end_dt = datetime.now(timezone.utc)
|
|
72
|
+
if not end_dt:
|
|
73
|
+
return None
|
|
74
|
+
return (end_dt - start_dt).total_seconds()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _ticket_counts(ticket_dir: Path) -> dict[str, int]:
|
|
78
|
+
total = 0
|
|
79
|
+
done = 0
|
|
80
|
+
for path in list_ticket_paths(ticket_dir):
|
|
81
|
+
total += 1
|
|
82
|
+
try:
|
|
83
|
+
if ticket_is_done(path):
|
|
84
|
+
done += 1
|
|
85
|
+
except Exception:
|
|
86
|
+
# Treat unreadable/invalid tickets as not-done but still count them.
|
|
87
|
+
continue
|
|
88
|
+
todo = max(total - done, 0)
|
|
89
|
+
return {"todo": todo, "done": done, "total": total}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _count_history_dirs(history_dir: Path) -> int:
|
|
93
|
+
if not history_dir.exists() or not history_dir.is_dir():
|
|
94
|
+
return 0
|
|
95
|
+
count = 0
|
|
96
|
+
try:
|
|
97
|
+
for child in history_dir.iterdir():
|
|
98
|
+
try:
|
|
99
|
+
if child.is_dir() and len(child.name) == 4 and child.name.isdigit():
|
|
100
|
+
count += 1
|
|
101
|
+
except OSError:
|
|
102
|
+
continue
|
|
103
|
+
except OSError:
|
|
104
|
+
return count
|
|
105
|
+
return count
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _aggregate_diff_stats(dispatch_history_dir: Path) -> Dict[str, int]:
|
|
109
|
+
"""Aggregate diff stats from all turn summaries in dispatch history.
|
|
110
|
+
|
|
111
|
+
Returns dict with insertions, deletions, files_changed totals.
|
|
112
|
+
"""
|
|
113
|
+
totals = {"insertions": 0, "deletions": 0, "files_changed": 0}
|
|
114
|
+
if not dispatch_history_dir.exists() or not dispatch_history_dir.is_dir():
|
|
115
|
+
return totals
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
for entry_dir in dispatch_history_dir.iterdir():
|
|
119
|
+
if not entry_dir.is_dir():
|
|
120
|
+
continue
|
|
121
|
+
if not (len(entry_dir.name) == 4 and entry_dir.name.isdigit()):
|
|
122
|
+
continue
|
|
123
|
+
dispatch_path = entry_dir / "DISPATCH.md"
|
|
124
|
+
if not dispatch_path.exists():
|
|
125
|
+
continue
|
|
126
|
+
try:
|
|
127
|
+
dispatch, _errors = parse_dispatch(dispatch_path)
|
|
128
|
+
if dispatch and dispatch.extra:
|
|
129
|
+
diff_stats = dispatch.extra.get("diff_stats")
|
|
130
|
+
if isinstance(diff_stats, dict):
|
|
131
|
+
totals["insertions"] += int(diff_stats.get("insertions") or 0)
|
|
132
|
+
totals["deletions"] += int(diff_stats.get("deletions") or 0)
|
|
133
|
+
totals["files_changed"] += int(
|
|
134
|
+
diff_stats.get("files_changed") or 0
|
|
135
|
+
)
|
|
136
|
+
except Exception:
|
|
137
|
+
continue
|
|
138
|
+
except OSError:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
return totals
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _build_summary(repo_root: Path) -> Dict[str, Any]:
|
|
145
|
+
from ....core.config import load_repo_config
|
|
146
|
+
|
|
147
|
+
ticket_dir = repo_root / ".codex-autorunner" / "tickets"
|
|
148
|
+
db_path = _flows_db_path(repo_root)
|
|
149
|
+
records: list[FlowRunRecord] = []
|
|
150
|
+
if db_path.exists():
|
|
151
|
+
try:
|
|
152
|
+
with FlowStore(
|
|
153
|
+
db_path, durable=load_repo_config(repo_root).durable_writes
|
|
154
|
+
) as store:
|
|
155
|
+
records = store.list_flow_runs(flow_type="ticket_flow")
|
|
156
|
+
except Exception:
|
|
157
|
+
records = []
|
|
158
|
+
|
|
159
|
+
run_record = _select_primary_run(records)
|
|
160
|
+
|
|
161
|
+
default_run = {
|
|
162
|
+
"id": None,
|
|
163
|
+
"short_id": None,
|
|
164
|
+
"status": "idle",
|
|
165
|
+
"started_at": None,
|
|
166
|
+
"finished_at": None,
|
|
167
|
+
"duration_seconds": None,
|
|
168
|
+
"current_step": None,
|
|
169
|
+
"created_at": None,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
run_data: Dict[str, Any] = default_run
|
|
173
|
+
turns: Dict[str, Optional[int]] = {
|
|
174
|
+
"total": None,
|
|
175
|
+
"current_ticket": None,
|
|
176
|
+
"dispatches": 0,
|
|
177
|
+
"replies": 0,
|
|
178
|
+
}
|
|
179
|
+
current_ticket: Optional[str] = None
|
|
180
|
+
agent_id: Optional[str] = None
|
|
181
|
+
|
|
182
|
+
if run_record:
|
|
183
|
+
run_data = {
|
|
184
|
+
"id": run_record.id,
|
|
185
|
+
"short_id": run_record.id.split("-")[0] if run_record.id else None,
|
|
186
|
+
"status": run_record.status.value,
|
|
187
|
+
"started_at": run_record.started_at,
|
|
188
|
+
"finished_at": run_record.finished_at,
|
|
189
|
+
"duration_seconds": _duration_seconds(
|
|
190
|
+
run_record.started_at, run_record.finished_at, run_record.status.value
|
|
191
|
+
),
|
|
192
|
+
"current_step": run_record.current_step,
|
|
193
|
+
"created_at": run_record.created_at,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
state = run_record.state if isinstance(run_record.state, dict) else {}
|
|
197
|
+
ticket_state = state.get("ticket_engine") if isinstance(state, dict) else {}
|
|
198
|
+
if isinstance(ticket_state, dict):
|
|
199
|
+
turns["total"] = ticket_state.get("total_turns") # type: ignore[index]
|
|
200
|
+
turns["current_ticket"] = ticket_state.get("ticket_turns") # type: ignore[index]
|
|
201
|
+
current_ticket = ticket_state.get("current_ticket") # type: ignore[assignment]
|
|
202
|
+
agent_id = ticket_state.get("last_agent_id") # type: ignore[assignment]
|
|
203
|
+
|
|
204
|
+
workspace_value = run_record.input_data.get("workspace_root")
|
|
205
|
+
workspace_root = Path(workspace_value) if workspace_value else repo_root
|
|
206
|
+
runs_dir = Path(
|
|
207
|
+
run_record.input_data.get("runs_dir") or ".codex-autorunner/runs"
|
|
208
|
+
)
|
|
209
|
+
outbox_paths = resolve_outbox_paths(
|
|
210
|
+
workspace_root=workspace_root, runs_dir=runs_dir, run_id=run_record.id
|
|
211
|
+
)
|
|
212
|
+
reply_paths = resolve_reply_paths(
|
|
213
|
+
workspace_root=workspace_root, runs_dir=runs_dir, run_id=run_record.id
|
|
214
|
+
)
|
|
215
|
+
turns["dispatches"] = _count_history_dirs(outbox_paths.dispatch_history_dir)
|
|
216
|
+
turns["replies"] = _count_history_dirs(reply_paths.reply_history_dir)
|
|
217
|
+
# Diff stats are now stored in FlowStore as DIFF_UPDATED events.
|
|
218
|
+
# Fallback to legacy dispatch history parsing if FlowStore query fails.
|
|
219
|
+
try:
|
|
220
|
+
with FlowStore(
|
|
221
|
+
db_path, durable=load_repo_config(repo_root).durable_writes
|
|
222
|
+
) as store:
|
|
223
|
+
events = store.get_events_by_type(
|
|
224
|
+
run_record.id, FlowEventType.DIFF_UPDATED
|
|
225
|
+
)
|
|
226
|
+
totals = {"insertions": 0, "deletions": 0, "files_changed": 0}
|
|
227
|
+
for ev in events:
|
|
228
|
+
data = ev.data or {}
|
|
229
|
+
totals["insertions"] += int(data.get("insertions") or 0)
|
|
230
|
+
totals["deletions"] += int(data.get("deletions") or 0)
|
|
231
|
+
totals["files_changed"] += int(data.get("files_changed") or 0)
|
|
232
|
+
turns["diff_stats"] = totals
|
|
233
|
+
except Exception:
|
|
234
|
+
turns["diff_stats"] = _aggregate_diff_stats(
|
|
235
|
+
outbox_paths.dispatch_history_dir
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# If current ticket is known, read its frontmatter to pick agent id when available.
|
|
239
|
+
if current_ticket:
|
|
240
|
+
current_path = (workspace_root / current_ticket).resolve()
|
|
241
|
+
try:
|
|
242
|
+
doc, _errors = read_ticket(current_path)
|
|
243
|
+
if doc and doc.frontmatter and getattr(doc.frontmatter, "agent", None):
|
|
244
|
+
agent_id = doc.frontmatter.agent
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
ticket_counts = _ticket_counts(ticket_dir)
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
"run": run_data,
|
|
252
|
+
"tickets": {
|
|
253
|
+
"todo_count": ticket_counts["todo"],
|
|
254
|
+
"done_count": ticket_counts["done"],
|
|
255
|
+
"total_count": ticket_counts["total"],
|
|
256
|
+
"current_ticket": current_ticket,
|
|
257
|
+
},
|
|
258
|
+
"turns": {
|
|
259
|
+
"total": turns.get("total"),
|
|
260
|
+
"current_ticket": turns.get("current_ticket"),
|
|
261
|
+
"dispatches": turns.get("dispatches"),
|
|
262
|
+
"replies": turns.get("replies"),
|
|
263
|
+
"diff_stats": turns.get("diff_stats"),
|
|
264
|
+
},
|
|
265
|
+
"agent": {
|
|
266
|
+
"id": agent_id,
|
|
267
|
+
"model": None,
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def build_analytics_routes() -> APIRouter:
|
|
273
|
+
router = APIRouter(prefix="/api/analytics", tags=["analytics"])
|
|
274
|
+
|
|
275
|
+
@router.get("/summary")
|
|
276
|
+
def get_analytics_summary():
|
|
277
|
+
repo_root = find_repo_root()
|
|
278
|
+
data = _build_summary(repo_root)
|
|
279
|
+
return data
|
|
280
|
+
|
|
281
|
+
return router
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
__all__ = ["build_analytics_routes"]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
App-server support routes (thread registry).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
8
|
+
from fastapi.responses import FileResponse, StreamingResponse
|
|
9
|
+
|
|
10
|
+
from ....core.app_server_threads import normalize_feature_key
|
|
11
|
+
from ....core.utils import is_within
|
|
12
|
+
from ....integrations.app_server.client import CodexAppServerError
|
|
13
|
+
from ..schemas import (
|
|
14
|
+
AppServerThreadArchiveRequest,
|
|
15
|
+
AppServerThreadArchiveResponse,
|
|
16
|
+
AppServerThreadResetAllResponse,
|
|
17
|
+
AppServerThreadResetRequest,
|
|
18
|
+
AppServerThreadResetResponse,
|
|
19
|
+
AppServerThreadsResponse,
|
|
20
|
+
)
|
|
21
|
+
from .shared import SSE_HEADERS
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_app_server_routes() -> APIRouter:
|
|
25
|
+
router = APIRouter()
|
|
26
|
+
|
|
27
|
+
@router.get("/api/app-server/turns/{turn_id}/events")
|
|
28
|
+
async def stream_app_server_turn_events(
|
|
29
|
+
turn_id: str, request: Request, thread_id: str
|
|
30
|
+
):
|
|
31
|
+
events = getattr(request.app.state, "app_server_events", None)
|
|
32
|
+
if events is None:
|
|
33
|
+
raise HTTPException(status_code=404, detail="App-server events unavailable")
|
|
34
|
+
if not thread_id:
|
|
35
|
+
raise HTTPException(status_code=400, detail="thread_id is required")
|
|
36
|
+
return StreamingResponse(
|
|
37
|
+
events.stream(thread_id, turn_id),
|
|
38
|
+
media_type="text/event-stream",
|
|
39
|
+
headers=SSE_HEADERS,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@router.get("/api/app-server/threads", response_model=AppServerThreadsResponse)
|
|
43
|
+
def app_server_threads(request: Request):
|
|
44
|
+
registry = request.app.state.app_server_threads
|
|
45
|
+
return registry.feature_map()
|
|
46
|
+
|
|
47
|
+
@router.get("/api/app-server/models")
|
|
48
|
+
async def app_server_models(request: Request):
|
|
49
|
+
engine = request.app.state.engine
|
|
50
|
+
supervisor = request.app.state.app_server_supervisor
|
|
51
|
+
try:
|
|
52
|
+
client = await supervisor.get_client(engine.repo_root)
|
|
53
|
+
return await client.model_list()
|
|
54
|
+
except CodexAppServerError as exc:
|
|
55
|
+
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
|
56
|
+
|
|
57
|
+
@router.post(
|
|
58
|
+
"/api/app-server/threads/reset", response_model=AppServerThreadResetResponse
|
|
59
|
+
)
|
|
60
|
+
def reset_app_server_thread(request: Request, payload: AppServerThreadResetRequest):
|
|
61
|
+
registry = request.app.state.app_server_threads
|
|
62
|
+
try:
|
|
63
|
+
key = normalize_feature_key(payload.key)
|
|
64
|
+
except ValueError as exc:
|
|
65
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
66
|
+
cleared = registry.reset_thread(key)
|
|
67
|
+
return {"status": "ok", "key": key, "cleared": cleared}
|
|
68
|
+
|
|
69
|
+
@router.post(
|
|
70
|
+
"/api/app-server/threads/archive",
|
|
71
|
+
response_model=AppServerThreadArchiveResponse,
|
|
72
|
+
)
|
|
73
|
+
async def archive_app_server_thread(
|
|
74
|
+
request: Request, payload: AppServerThreadArchiveRequest
|
|
75
|
+
):
|
|
76
|
+
thread_id = payload.thread_id.strip()
|
|
77
|
+
if not thread_id:
|
|
78
|
+
raise HTTPException(status_code=400, detail="thread_id is required")
|
|
79
|
+
engine = request.app.state.engine
|
|
80
|
+
supervisor = request.app.state.app_server_supervisor
|
|
81
|
+
try:
|
|
82
|
+
client = await supervisor.get_client(engine.repo_root)
|
|
83
|
+
await client.thread_archive(thread_id)
|
|
84
|
+
except CodexAppServerError as exc:
|
|
85
|
+
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
|
86
|
+
return {"status": "ok", "thread_id": thread_id, "archived": True}
|
|
87
|
+
|
|
88
|
+
@router.post(
|
|
89
|
+
"/api/app-server/threads/reset-all",
|
|
90
|
+
response_model=AppServerThreadResetAllResponse,
|
|
91
|
+
)
|
|
92
|
+
def reset_app_server_threads(request: Request):
|
|
93
|
+
registry = request.app.state.app_server_threads
|
|
94
|
+
registry.reset_all()
|
|
95
|
+
return {"status": "ok", "cleared": True}
|
|
96
|
+
|
|
97
|
+
@router.get("/api/app-server/threads/backup")
|
|
98
|
+
def download_app_server_threads_backup(request: Request):
|
|
99
|
+
registry = request.app.state.app_server_threads
|
|
100
|
+
notice = registry.corruption_notice() or {}
|
|
101
|
+
backup_path = notice.get("backup_path")
|
|
102
|
+
if not isinstance(backup_path, str) or not backup_path:
|
|
103
|
+
raise HTTPException(status_code=404, detail="No backup available")
|
|
104
|
+
path = Path(backup_path)
|
|
105
|
+
engine = request.app.state.engine
|
|
106
|
+
if not is_within(engine.repo_root, path):
|
|
107
|
+
raise HTTPException(status_code=400, detail="Invalid backup path")
|
|
108
|
+
if not path.exists():
|
|
109
|
+
raise HTTPException(status_code=404, detail="Backup not found")
|
|
110
|
+
return FileResponse(path, filename=path.name)
|
|
111
|
+
|
|
112
|
+
@router.get("/api/app-server/account")
|
|
113
|
+
async def app_server_account(request: Request):
|
|
114
|
+
engine = request.app.state.engine
|
|
115
|
+
supervisor = request.app.state.app_server_supervisor
|
|
116
|
+
try:
|
|
117
|
+
client = await supervisor.get_client(engine.repo_root)
|
|
118
|
+
return await client.account_read()
|
|
119
|
+
except CodexAppServerError as exc:
|
|
120
|
+
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
|
121
|
+
|
|
122
|
+
@router.get("/api/app-server/rate-limits")
|
|
123
|
+
async def app_server_rate_limits(request: Request):
|
|
124
|
+
engine = request.app.state.engine
|
|
125
|
+
supervisor = request.app.state.app_server_supervisor
|
|
126
|
+
try:
|
|
127
|
+
client = await supervisor.get_client(engine.repo_root)
|
|
128
|
+
return await client.rate_limits_read()
|
|
129
|
+
except CodexAppServerError as exc:
|
|
130
|
+
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
|
131
|
+
|
|
132
|
+
return router
|