codex-autorunner 1.1.0__py3-none-any.whl → 1.2.1__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/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +17 -7
- codex_autorunner/bootstrap.py +219 -1
- codex_autorunner/core/__init__.py +17 -1
- codex_autorunner/core/about_car.py +124 -11
- codex_autorunner/core/app_server_threads.py +6 -0
- codex_autorunner/core/config.py +238 -3
- codex_autorunner/core/context_awareness.py +39 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +71 -1
- codex_autorunner/core/flows/reconciler.py +4 -1
- codex_autorunner/core/flows/runtime.py +22 -0
- codex_autorunner/core/flows/store.py +61 -9
- codex_autorunner/core/flows/transition.py +23 -16
- codex_autorunner/core/flows/ux_helpers.py +18 -3
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/hub.py +198 -41
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +683 -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/agent_backend.py +2 -5
- codex_autorunner/core/ports/run_event.py +1 -4
- 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 +5 -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/ticket_linter_cli.py +17 -0
- codex_autorunner/core/ticket_manager_cli.py +154 -92
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/utils.py +34 -6
- codex_autorunner/flows/review/service.py +23 -25
- codex_autorunner/flows/ticket_flow/definition.py +43 -1
- codex_autorunner/integrations/agents/__init__.py +2 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +18 -0
- codex_autorunner/integrations/agents/codex_backend.py +19 -8
- codex_autorunner/integrations/agents/runner.py +3 -8
- codex_autorunner/integrations/agents/wiring.py +8 -0
- codex_autorunner/integrations/telegram/adapter.py +1 -1
- codex_autorunner/integrations/telegram/config.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- 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 +346 -58
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +202 -45
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +18 -7
- codex_autorunner/integrations/telegram/handlers/messages.py +34 -3
- codex_autorunner/integrations/telegram/helpers.py +1 -3
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +30 -0
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +10 -4
- codex_autorunner/integrations/telegram/transport.py +10 -3
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/server.py +2 -2
- codex_autorunner/static/agentControls.js +21 -5
- codex_autorunner/static/app.js +115 -11
- codex_autorunner/static/archive.js +274 -81
- codex_autorunner/static/archiveApi.js +21 -0
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/constants.js +1 -1
- 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 +46 -81
- codex_autorunner/static/index.html +303 -24
- codex_autorunner/static/messages.js +82 -4
- codex_autorunner/static/notifications.js +288 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/settings.js +3 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9141 -6742
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminalManager.js +22 -3
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +41 -13
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +69 -19
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +28 -0
- codex_autorunner/static/workspace.js +258 -44
- codex_autorunner/static/workspaceFileBrowser.js +6 -4
- codex_autorunner/surfaces/cli/cli.py +1465 -155
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/web/app.py +253 -49
- codex_autorunner/surfaces/web/routes/__init__.py +4 -0
- codex_autorunner/surfaces/web/routes/analytics.py +29 -22
- codex_autorunner/surfaces/web/routes/archive.py +197 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +297 -36
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +219 -29
- codex_autorunner/surfaces/web/routes/messages.py +70 -39
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +1 -1
- codex_autorunner/surfaces/web/routes/shared.py +0 -3
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/runner_manager.py +2 -2
- codex_autorunner/surfaces/web/schemas.py +81 -18
- codex_autorunner/tickets/agent_pool.py +27 -0
- codex_autorunner/tickets/files.py +33 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +3 -0
- codex_autorunner/tickets/outbox.py +41 -5
- codex_autorunner/tickets/runner.py +350 -69
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/METADATA +15 -19
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/RECORD +132 -101
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -3302
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/top_level.txt +0 -0
|
@@ -2,6 +2,7 @@ import asyncio
|
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
4
|
import shlex
|
|
5
|
+
import sys
|
|
5
6
|
import threading
|
|
6
7
|
from contextlib import ExitStack, asynccontextmanager
|
|
7
8
|
from dataclasses import dataclass
|
|
@@ -18,6 +19,7 @@ from starlette.types import ASGIApp
|
|
|
18
19
|
|
|
19
20
|
from ...agents.opencode.supervisor import OpenCodeSupervisor
|
|
20
21
|
from ...agents.registry import validate_agent_id
|
|
22
|
+
from ...bootstrap import ensure_hub_car_shim
|
|
21
23
|
from ...core.app_server_threads import (
|
|
22
24
|
AppServerThreadRegistry,
|
|
23
25
|
default_app_server_threads_path,
|
|
@@ -34,7 +36,6 @@ from ...core.config import (
|
|
|
34
36
|
load_repo_config,
|
|
35
37
|
resolve_env_for_root,
|
|
36
38
|
)
|
|
37
|
-
from ...core.engine import Engine, LockError
|
|
38
39
|
from ...core.flows.models import FlowRunStatus
|
|
39
40
|
from ...core.flows.reconciler import reconcile_flow_runs
|
|
40
41
|
from ...core.flows.store import FlowStore
|
|
@@ -42,6 +43,7 @@ from ...core.hub import HubSupervisor
|
|
|
42
43
|
from ...core.logging_utils import safe_log, setup_rotating_logger
|
|
43
44
|
from ...core.optional_dependencies import require_optional_dependencies
|
|
44
45
|
from ...core.request_context import get_request_id
|
|
46
|
+
from ...core.runtime import LockError, RuntimeContext
|
|
45
47
|
from ...core.state import load_state, persist_session_registry
|
|
46
48
|
from ...core.usage import (
|
|
47
49
|
UsageError,
|
|
@@ -56,6 +58,7 @@ from ...core.utils import (
|
|
|
56
58
|
set_repo_root_context,
|
|
57
59
|
)
|
|
58
60
|
from ...housekeeping import run_housekeeping_once
|
|
61
|
+
from ...integrations.agents import build_backend_orchestrator
|
|
59
62
|
from ...integrations.agents.wiring import (
|
|
60
63
|
build_agent_backend_factory,
|
|
61
64
|
build_app_server_supervisor_factory,
|
|
@@ -65,7 +68,8 @@ from ...integrations.app_server.env import build_app_server_env
|
|
|
65
68
|
from ...integrations.app_server.event_buffer import AppServerEventBuffer
|
|
66
69
|
from ...integrations.app_server.supervisor import WorkspaceAppServerSupervisor
|
|
67
70
|
from ...manifest import load_manifest
|
|
68
|
-
from ...tickets.files import safe_relpath
|
|
71
|
+
from ...tickets.files import list_ticket_paths, safe_relpath, ticket_is_done
|
|
72
|
+
from ...tickets.models import Dispatch
|
|
69
73
|
from ...tickets.outbox import parse_dispatch, resolve_outbox_paths
|
|
70
74
|
from ...voice import VoiceConfig, VoiceService
|
|
71
75
|
from .hub_jobs import HubJobManager
|
|
@@ -77,6 +81,8 @@ from .middleware import (
|
|
|
77
81
|
SecurityHeadersMiddleware,
|
|
78
82
|
)
|
|
79
83
|
from .routes import build_repo_router
|
|
84
|
+
from .routes.filebox import build_hub_filebox_routes
|
|
85
|
+
from .routes.pma import build_pma_routes
|
|
80
86
|
from .routes.system import build_system_routes
|
|
81
87
|
from .runner_manager import RunnerManager
|
|
82
88
|
from .schemas import (
|
|
@@ -101,7 +107,7 @@ from .terminal_sessions import parse_tui_idle_seconds, prune_terminal_registry
|
|
|
101
107
|
class AppContext:
|
|
102
108
|
base_path: str
|
|
103
109
|
env: Mapping[str, str]
|
|
104
|
-
engine:
|
|
110
|
+
engine: RuntimeContext
|
|
105
111
|
manager: RunnerManager
|
|
106
112
|
app_server_supervisor: Optional[WorkspaceAppServerSupervisor]
|
|
107
113
|
app_server_prune_interval: Optional[float]
|
|
@@ -135,6 +141,10 @@ class HubAppContext:
|
|
|
135
141
|
job_manager: HubJobManager
|
|
136
142
|
app_server_supervisor: Optional[WorkspaceAppServerSupervisor]
|
|
137
143
|
app_server_prune_interval: Optional[float]
|
|
144
|
+
app_server_threads: AppServerThreadRegistry
|
|
145
|
+
app_server_events: AppServerEventBuffer
|
|
146
|
+
opencode_supervisor: Optional[OpenCodeSupervisor]
|
|
147
|
+
opencode_prune_interval: Optional[float]
|
|
138
148
|
static_dir: Path
|
|
139
149
|
static_assets_context: Optional[object]
|
|
140
150
|
asset_version: str
|
|
@@ -309,6 +319,7 @@ def _build_opencode_supervisor(
|
|
|
309
319
|
env: Mapping[str, str],
|
|
310
320
|
subagent_models: Optional[Mapping[str, str]] = None,
|
|
311
321
|
session_stall_timeout_seconds: Optional[float] = None,
|
|
322
|
+
max_text_chars: Optional[int] = None,
|
|
312
323
|
) -> tuple[Optional[OpenCodeSupervisor], Optional[float]]:
|
|
313
324
|
supervisor = build_opencode_supervisor(
|
|
314
325
|
opencode_command=opencode_command,
|
|
@@ -319,6 +330,7 @@ def _build_opencode_supervisor(
|
|
|
319
330
|
max_handles=config.max_handles,
|
|
320
331
|
idle_ttl_seconds=config.idle_ttl_seconds,
|
|
321
332
|
session_stall_timeout_seconds=session_stall_timeout_seconds,
|
|
333
|
+
max_text_chars=max_text_chars,
|
|
322
334
|
base_env=env,
|
|
323
335
|
subagent_models=subagent_models,
|
|
324
336
|
)
|
|
@@ -349,12 +361,11 @@ def _build_app_context(
|
|
|
349
361
|
if base_path is not None
|
|
350
362
|
else config.server_base_path
|
|
351
363
|
)
|
|
352
|
-
|
|
364
|
+
backend_orchestrator = build_backend_orchestrator(config.root, config)
|
|
365
|
+
engine = RuntimeContext(
|
|
353
366
|
config.root,
|
|
354
367
|
config=config,
|
|
355
|
-
|
|
356
|
-
app_server_supervisor_factory=build_app_server_supervisor_factory(config),
|
|
357
|
-
agent_id_validator=validate_agent_id,
|
|
368
|
+
backend_orchestrator=backend_orchestrator,
|
|
358
369
|
)
|
|
359
370
|
manager = RunnerManager(engine)
|
|
360
371
|
voice_config = VoiceConfig.from_raw(config.voice, env=env)
|
|
@@ -484,6 +495,7 @@ def _build_app_context(
|
|
|
484
495
|
env=env,
|
|
485
496
|
subagent_models=subagent_models,
|
|
486
497
|
session_stall_timeout_seconds=config.opencode.session_stall_timeout_seconds,
|
|
498
|
+
max_text_chars=config.opencode.max_text_chars,
|
|
487
499
|
)
|
|
488
500
|
voice_service: Optional[VoiceService]
|
|
489
501
|
if voice_missing_reason:
|
|
@@ -653,6 +665,7 @@ def _build_hub_context(
|
|
|
653
665
|
config,
|
|
654
666
|
backend_factory_builder=build_agent_backend_factory,
|
|
655
667
|
app_server_supervisor_factory_builder=build_app_server_supervisor_factory,
|
|
668
|
+
backend_orchestrator_builder=build_backend_orchestrator,
|
|
656
669
|
agent_id_validator=validate_agent_id,
|
|
657
670
|
)
|
|
658
671
|
logger = setup_rotating_logger(f"hub[{config.root}]", config.server_log)
|
|
@@ -669,10 +682,42 @@ def _build_hub_context(
|
|
|
669
682
|
logging.INFO,
|
|
670
683
|
f"Hub app ready at {config.root}",
|
|
671
684
|
)
|
|
685
|
+
try:
|
|
686
|
+
ensure_hub_car_shim(config.root, python_executable=sys.executable)
|
|
687
|
+
except Exception as exc:
|
|
688
|
+
safe_log(
|
|
689
|
+
logger,
|
|
690
|
+
logging.WARNING,
|
|
691
|
+
"Failed to ensure hub car shim",
|
|
692
|
+
exc=exc,
|
|
693
|
+
)
|
|
694
|
+
app_server_events = AppServerEventBuffer()
|
|
672
695
|
app_server_supervisor, app_server_prune_interval = _build_app_server_supervisor(
|
|
673
696
|
config.app_server,
|
|
674
697
|
logger=logger,
|
|
675
698
|
event_prefix="hub.app_server",
|
|
699
|
+
notification_handler=app_server_events.handle_notification,
|
|
700
|
+
)
|
|
701
|
+
app_server_threads = AppServerThreadRegistry(
|
|
702
|
+
default_app_server_threads_path(config.root)
|
|
703
|
+
)
|
|
704
|
+
opencode_command = config.agent_serve_command("opencode")
|
|
705
|
+
try:
|
|
706
|
+
opencode_binary = config.agent_binary("opencode")
|
|
707
|
+
except ConfigError:
|
|
708
|
+
opencode_binary = None
|
|
709
|
+
agent_config = config.agents.get("opencode")
|
|
710
|
+
subagent_models = agent_config.subagent_models if agent_config else None
|
|
711
|
+
opencode_supervisor, opencode_prune_interval = _build_opencode_supervisor(
|
|
712
|
+
config.app_server,
|
|
713
|
+
workspace_root=config.root,
|
|
714
|
+
opencode_binary=opencode_binary,
|
|
715
|
+
opencode_command=opencode_command,
|
|
716
|
+
logger=logger,
|
|
717
|
+
env=resolve_env_for_root(config.root),
|
|
718
|
+
subagent_models=subagent_models,
|
|
719
|
+
session_stall_timeout_seconds=config.opencode.session_stall_timeout_seconds,
|
|
720
|
+
max_text_chars=config.opencode.max_text_chars,
|
|
676
721
|
)
|
|
677
722
|
static_dir, static_context = materialize_static_assets(
|
|
678
723
|
config.static_assets.cache_root,
|
|
@@ -699,6 +744,10 @@ def _build_hub_context(
|
|
|
699
744
|
job_manager=HubJobManager(logger=logger),
|
|
700
745
|
app_server_supervisor=app_server_supervisor,
|
|
701
746
|
app_server_prune_interval=app_server_prune_interval,
|
|
747
|
+
app_server_threads=app_server_threads,
|
|
748
|
+
app_server_events=app_server_events,
|
|
749
|
+
opencode_supervisor=opencode_supervisor,
|
|
750
|
+
opencode_prune_interval=opencode_prune_interval,
|
|
702
751
|
static_dir=static_dir,
|
|
703
752
|
static_assets_context=static_context,
|
|
704
753
|
asset_version=asset_version(static_dir),
|
|
@@ -713,9 +762,14 @@ def _apply_hub_context(app: FastAPI, context: HubAppContext) -> None:
|
|
|
713
762
|
app.state.job_manager = context.job_manager
|
|
714
763
|
app.state.app_server_supervisor = context.app_server_supervisor
|
|
715
764
|
app.state.app_server_prune_interval = context.app_server_prune_interval
|
|
765
|
+
app.state.app_server_threads = context.app_server_threads
|
|
766
|
+
app.state.app_server_events = context.app_server_events
|
|
767
|
+
app.state.opencode_supervisor = context.opencode_supervisor
|
|
768
|
+
app.state.opencode_prune_interval = context.opencode_prune_interval
|
|
716
769
|
app.state.static_dir = context.static_dir
|
|
717
770
|
app.state.static_assets_context = context.static_assets_context
|
|
718
771
|
app.state.asset_version = context.asset_version
|
|
772
|
+
app.state.hub_supervisor = context.supervisor
|
|
719
773
|
|
|
720
774
|
|
|
721
775
|
def _app_lifespan(context: AppContext):
|
|
@@ -1097,6 +1151,11 @@ def create_hub_app(
|
|
|
1097
1151
|
app.state.static_assets_lock = threading.Lock()
|
|
1098
1152
|
app.state.hub_static_assets = None
|
|
1099
1153
|
app.mount("/static", static_files, name="static")
|
|
1154
|
+
raw_config = getattr(context.config, "raw", {})
|
|
1155
|
+
pma_config = raw_config.get("pma", {}) if isinstance(raw_config, dict) else {}
|
|
1156
|
+
if isinstance(pma_config, dict) and pma_config.get("enabled"):
|
|
1157
|
+
app.include_router(build_pma_routes())
|
|
1158
|
+
app.include_router(build_hub_filebox_routes())
|
|
1100
1159
|
mounted_repos: set[str] = set()
|
|
1101
1160
|
mount_errors: dict[str, str] = {}
|
|
1102
1161
|
repo_apps: dict[str, ASGIApp] = {}
|
|
@@ -1322,6 +1381,53 @@ def create_hub_app(
|
|
|
1322
1381
|
repo_dict["mounted"] = False
|
|
1323
1382
|
return repo_dict
|
|
1324
1383
|
|
|
1384
|
+
def _get_ticket_flow_summary(repo_path: Path) -> Optional[dict]:
|
|
1385
|
+
"""Get ticket flow summary for a repo (status, done/total, step).
|
|
1386
|
+
|
|
1387
|
+
Returns None if no ticket flow exists or repo is not initialized.
|
|
1388
|
+
"""
|
|
1389
|
+
db_path = repo_path / ".codex-autorunner" / "flows.db"
|
|
1390
|
+
if not db_path.exists():
|
|
1391
|
+
return None
|
|
1392
|
+
try:
|
|
1393
|
+
config = load_repo_config(repo_path)
|
|
1394
|
+
with FlowStore(db_path, durable=config.durable_writes) as store:
|
|
1395
|
+
# Get the latest ticket_flow run (any status)
|
|
1396
|
+
runs = store.list_flow_runs(flow_type="ticket_flow")
|
|
1397
|
+
if not runs:
|
|
1398
|
+
return None
|
|
1399
|
+
latest = runs[0] # Already sorted by created_at DESC
|
|
1400
|
+
|
|
1401
|
+
# Count tickets
|
|
1402
|
+
ticket_dir = repo_path / ".codex-autorunner" / "tickets"
|
|
1403
|
+
total = 0
|
|
1404
|
+
done = 0
|
|
1405
|
+
for path in list_ticket_paths(ticket_dir):
|
|
1406
|
+
total += 1
|
|
1407
|
+
try:
|
|
1408
|
+
if ticket_is_done(path):
|
|
1409
|
+
done += 1
|
|
1410
|
+
except Exception:
|
|
1411
|
+
continue
|
|
1412
|
+
|
|
1413
|
+
if total == 0:
|
|
1414
|
+
return None
|
|
1415
|
+
|
|
1416
|
+
# Extract current step from ticket_engine state
|
|
1417
|
+
state = latest.state if isinstance(latest.state, dict) else {}
|
|
1418
|
+
engine = state.get("ticket_engine") if isinstance(state, dict) else {}
|
|
1419
|
+
engine = engine if isinstance(engine, dict) else {}
|
|
1420
|
+
current_step = engine.get("total_turns")
|
|
1421
|
+
|
|
1422
|
+
return {
|
|
1423
|
+
"status": latest.status.value,
|
|
1424
|
+
"done_count": done,
|
|
1425
|
+
"total_count": total,
|
|
1426
|
+
"current_step": current_step,
|
|
1427
|
+
}
|
|
1428
|
+
except Exception:
|
|
1429
|
+
return None
|
|
1430
|
+
|
|
1325
1431
|
initial_snapshots = context.supervisor.scan()
|
|
1326
1432
|
for snap in initial_snapshots:
|
|
1327
1433
|
if snap.initialized and snap.exists_on_disk:
|
|
@@ -1372,6 +1478,24 @@ def create_hub_app(
|
|
|
1372
1478
|
)
|
|
1373
1479
|
|
|
1374
1480
|
asyncio.create_task(_app_server_prune_loop())
|
|
1481
|
+
opencode_supervisor = getattr(app.state, "opencode_supervisor", None)
|
|
1482
|
+
opencode_prune_interval = getattr(app.state, "opencode_prune_interval", None)
|
|
1483
|
+
if opencode_supervisor is not None and opencode_prune_interval:
|
|
1484
|
+
|
|
1485
|
+
async def _opencode_prune_loop():
|
|
1486
|
+
while True:
|
|
1487
|
+
await asyncio.sleep(opencode_prune_interval)
|
|
1488
|
+
try:
|
|
1489
|
+
await opencode_supervisor.prune_idle()
|
|
1490
|
+
except Exception as exc:
|
|
1491
|
+
safe_log(
|
|
1492
|
+
app.state.logger,
|
|
1493
|
+
logging.WARNING,
|
|
1494
|
+
"Hub opencode prune task failed",
|
|
1495
|
+
exc,
|
|
1496
|
+
)
|
|
1497
|
+
|
|
1498
|
+
asyncio.create_task(_opencode_prune_loop())
|
|
1375
1499
|
mount_lock = await _get_mount_lock()
|
|
1376
1500
|
async with mount_lock:
|
|
1377
1501
|
for prefix in list(mount_order):
|
|
@@ -1398,6 +1522,17 @@ def create_hub_app(
|
|
|
1398
1522
|
"Hub app-server shutdown failed",
|
|
1399
1523
|
exc,
|
|
1400
1524
|
)
|
|
1525
|
+
opencode_supervisor = getattr(app.state, "opencode_supervisor", None)
|
|
1526
|
+
if opencode_supervisor is not None:
|
|
1527
|
+
try:
|
|
1528
|
+
await opencode_supervisor.close_all()
|
|
1529
|
+
except Exception as exc:
|
|
1530
|
+
safe_log(
|
|
1531
|
+
app.state.logger,
|
|
1532
|
+
logging.WARNING,
|
|
1533
|
+
"Hub opencode shutdown failed",
|
|
1534
|
+
exc,
|
|
1535
|
+
)
|
|
1401
1536
|
static_context = getattr(app.state, "static_assets_context", None)
|
|
1402
1537
|
if static_context is not None:
|
|
1403
1538
|
static_context.close()
|
|
@@ -1501,6 +1636,27 @@ def create_hub_app(
|
|
|
1501
1636
|
history_dir = outbox_paths.dispatch_history_dir
|
|
1502
1637
|
if not history_dir.exists() or not history_dir.is_dir():
|
|
1503
1638
|
return None
|
|
1639
|
+
|
|
1640
|
+
def _dispatch_dict(dispatch: Dispatch) -> dict:
|
|
1641
|
+
return {
|
|
1642
|
+
"mode": dispatch.mode,
|
|
1643
|
+
"title": dispatch.title,
|
|
1644
|
+
"body": dispatch.body,
|
|
1645
|
+
"extra": dispatch.extra,
|
|
1646
|
+
"is_handoff": dispatch.is_handoff,
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
def _list_files(dispatch_dir: Path) -> list[str]:
|
|
1650
|
+
files: list[str] = []
|
|
1651
|
+
for child in sorted(dispatch_dir.iterdir(), key=lambda p: p.name):
|
|
1652
|
+
if child.name.startswith("."):
|
|
1653
|
+
continue
|
|
1654
|
+
if child.name == "DISPATCH.md":
|
|
1655
|
+
continue
|
|
1656
|
+
if child.is_file():
|
|
1657
|
+
files.append(child.name)
|
|
1658
|
+
return files
|
|
1659
|
+
|
|
1504
1660
|
seq_dirs: list[Path] = []
|
|
1505
1661
|
for child in history_dir.iterdir():
|
|
1506
1662
|
if not child.is_dir():
|
|
@@ -1510,40 +1666,74 @@ def create_hub_app(
|
|
|
1510
1666
|
seq_dirs.append(child)
|
|
1511
1667
|
if not seq_dirs:
|
|
1512
1668
|
return None
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1669
|
+
|
|
1670
|
+
seq_dirs = sorted(seq_dirs, key=lambda p: p.name, reverse=True)
|
|
1671
|
+
handoff_candidate: Optional[dict] = None
|
|
1672
|
+
non_summary_candidate: Optional[dict] = None
|
|
1673
|
+
turn_summary_candidate: Optional[dict] = None
|
|
1674
|
+
error_candidate: Optional[dict] = None
|
|
1675
|
+
|
|
1676
|
+
for seq_dir in seq_dirs:
|
|
1677
|
+
seq = int(seq_dir.name)
|
|
1678
|
+
dispatch_path = seq_dir / "DISPATCH.md"
|
|
1679
|
+
dispatch, errors = parse_dispatch(dispatch_path)
|
|
1680
|
+
if errors or dispatch is None:
|
|
1681
|
+
if error_candidate is None:
|
|
1682
|
+
error_candidate = {
|
|
1683
|
+
"seq": seq,
|
|
1684
|
+
"dir": seq_dir,
|
|
1685
|
+
"errors": errors,
|
|
1686
|
+
}
|
|
1530
1687
|
continue
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1688
|
+
candidate = {"seq": seq, "dir": seq_dir, "dispatch": dispatch}
|
|
1689
|
+
if dispatch.is_handoff and handoff_candidate is None:
|
|
1690
|
+
handoff_candidate = candidate
|
|
1691
|
+
if (
|
|
1692
|
+
dispatch.mode != "turn_summary"
|
|
1693
|
+
and non_summary_candidate is None
|
|
1694
|
+
):
|
|
1695
|
+
non_summary_candidate = candidate
|
|
1696
|
+
if (
|
|
1697
|
+
dispatch.mode == "turn_summary"
|
|
1698
|
+
and turn_summary_candidate is None
|
|
1699
|
+
):
|
|
1700
|
+
turn_summary_candidate = candidate
|
|
1701
|
+
if (
|
|
1702
|
+
handoff_candidate
|
|
1703
|
+
and non_summary_candidate
|
|
1704
|
+
and turn_summary_candidate
|
|
1705
|
+
):
|
|
1706
|
+
break
|
|
1707
|
+
|
|
1708
|
+
selected = (
|
|
1709
|
+
handoff_candidate or non_summary_candidate or turn_summary_candidate
|
|
1710
|
+
)
|
|
1711
|
+
if not selected:
|
|
1712
|
+
if error_candidate:
|
|
1713
|
+
return {
|
|
1714
|
+
"seq": error_candidate["seq"],
|
|
1715
|
+
"dir": safe_relpath(error_candidate["dir"], repo_root),
|
|
1716
|
+
"dispatch": None,
|
|
1717
|
+
"errors": error_candidate["errors"],
|
|
1718
|
+
"files": [],
|
|
1719
|
+
}
|
|
1720
|
+
return None
|
|
1721
|
+
|
|
1722
|
+
selected_dir = selected["dir"]
|
|
1723
|
+
dispatch = selected["dispatch"]
|
|
1724
|
+
result = {
|
|
1725
|
+
"seq": selected["seq"],
|
|
1726
|
+
"dir": safe_relpath(selected_dir, repo_root),
|
|
1727
|
+
"dispatch": _dispatch_dict(dispatch),
|
|
1544
1728
|
"errors": [],
|
|
1545
|
-
"files":
|
|
1729
|
+
"files": _list_files(selected_dir),
|
|
1546
1730
|
}
|
|
1731
|
+
if turn_summary_candidate is not None:
|
|
1732
|
+
result["turn_summary_seq"] = turn_summary_candidate["seq"]
|
|
1733
|
+
result["turn_summary"] = _dispatch_dict(
|
|
1734
|
+
turn_summary_candidate["dispatch"]
|
|
1735
|
+
)
|
|
1736
|
+
return result
|
|
1547
1737
|
except Exception:
|
|
1548
1738
|
return None
|
|
1549
1739
|
|
|
@@ -1561,11 +1751,11 @@ def create_hub_app(
|
|
|
1561
1751
|
if not db_path.exists():
|
|
1562
1752
|
continue
|
|
1563
1753
|
try:
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1754
|
+
config = load_repo_config(repo_root)
|
|
1755
|
+
with FlowStore(db_path, durable=config.durable_writes) as store:
|
|
1756
|
+
paused = store.list_flow_runs(
|
|
1757
|
+
flow_type="ticket_flow", status=FlowRunStatus.PAUSED
|
|
1758
|
+
)
|
|
1569
1759
|
except Exception:
|
|
1570
1760
|
continue
|
|
1571
1761
|
if not paused:
|
|
@@ -1603,11 +1793,18 @@ def create_hub_app(
|
|
|
1603
1793
|
safe_log(app.state.logger, logging.INFO, "Hub list_repos")
|
|
1604
1794
|
snapshots = await asyncio.to_thread(context.supervisor.list_repos)
|
|
1605
1795
|
await _refresh_mounts(snapshots)
|
|
1796
|
+
|
|
1797
|
+
def _enrich_repo(snap):
|
|
1798
|
+
repo_dict = _add_mount_info(snap.to_dict(context.config.root))
|
|
1799
|
+
if snap.initialized and snap.exists_on_disk:
|
|
1800
|
+
repo_dict["ticket_flow"] = _get_ticket_flow_summary(snap.path)
|
|
1801
|
+
else:
|
|
1802
|
+
repo_dict["ticket_flow"] = None
|
|
1803
|
+
return repo_dict
|
|
1804
|
+
|
|
1606
1805
|
return {
|
|
1607
1806
|
"last_scan_at": context.supervisor.state.last_scan_at,
|
|
1608
|
-
"repos": [
|
|
1609
|
-
_add_mount_info(repo.to_dict(context.config.root)) for repo in snapshots
|
|
1610
|
-
],
|
|
1807
|
+
"repos": [_enrich_repo(snap) for snap in snapshots],
|
|
1611
1808
|
}
|
|
1612
1809
|
|
|
1613
1810
|
@app.get("/hub/version")
|
|
@@ -1619,11 +1816,18 @@ def create_hub_app(
|
|
|
1619
1816
|
safe_log(app.state.logger, logging.INFO, "Hub scan_repos")
|
|
1620
1817
|
snapshots = await asyncio.to_thread(context.supervisor.scan)
|
|
1621
1818
|
await _refresh_mounts(snapshots)
|
|
1819
|
+
|
|
1820
|
+
def _enrich_repo(snap):
|
|
1821
|
+
repo_dict = _add_mount_info(snap.to_dict(context.config.root))
|
|
1822
|
+
if snap.initialized and snap.exists_on_disk:
|
|
1823
|
+
repo_dict["ticket_flow"] = _get_ticket_flow_summary(snap.path)
|
|
1824
|
+
else:
|
|
1825
|
+
repo_dict["ticket_flow"] = None
|
|
1826
|
+
return repo_dict
|
|
1827
|
+
|
|
1622
1828
|
return {
|
|
1623
1829
|
"last_scan_at": context.supervisor.state.last_scan_at,
|
|
1624
|
-
"repos": [
|
|
1625
|
-
_add_mount_info(repo.to_dict(context.config.root)) for repo in snapshots
|
|
1626
|
-
],
|
|
1830
|
+
"repos": [_enrich_repo(snap) for snap in snapshots],
|
|
1627
1831
|
}
|
|
1628
1832
|
|
|
1629
1833
|
@app.post("/hub/jobs/scan", response_model=HubJobResponse)
|
|
@@ -26,6 +26,7 @@ from .app_server import build_app_server_routes
|
|
|
26
26
|
from .archive import build_archive_routes
|
|
27
27
|
from .base import build_base_routes, build_frontend_routes
|
|
28
28
|
from .file_chat import build_file_chat_routes
|
|
29
|
+
from .filebox import build_filebox_routes
|
|
29
30
|
from .flows import build_flow_routes
|
|
30
31
|
from .messages import build_messages_routes
|
|
31
32
|
from .repos import build_repos_routes
|
|
@@ -33,6 +34,7 @@ from .review import build_review_routes
|
|
|
33
34
|
from .sessions import build_sessions_routes
|
|
34
35
|
from .settings import build_settings_routes
|
|
35
36
|
from .system import build_system_routes
|
|
37
|
+
from .templates import build_templates_routes
|
|
36
38
|
from .terminal_images import build_terminal_image_routes
|
|
37
39
|
from .usage import build_usage_routes
|
|
38
40
|
from .voice import build_voice_routes
|
|
@@ -59,6 +61,7 @@ def build_repo_router(static_dir: Path) -> APIRouter:
|
|
|
59
61
|
router.include_router(build_app_server_routes())
|
|
60
62
|
router.include_router(build_workspace_routes())
|
|
61
63
|
router.include_router(build_flow_routes())
|
|
64
|
+
router.include_router(build_filebox_routes())
|
|
62
65
|
router.include_router(build_file_chat_routes())
|
|
63
66
|
router.include_router(build_messages_routes())
|
|
64
67
|
router.include_router(build_repos_routes())
|
|
@@ -66,6 +69,7 @@ def build_repo_router(static_dir: Path) -> APIRouter:
|
|
|
66
69
|
router.include_router(build_sessions_routes())
|
|
67
70
|
router.include_router(build_settings_routes())
|
|
68
71
|
router.include_router(build_system_routes())
|
|
72
|
+
router.include_router(build_templates_routes())
|
|
69
73
|
router.include_router(build_terminal_image_routes())
|
|
70
74
|
router.include_router(build_usage_routes())
|
|
71
75
|
router.include_router(build_voice_routes())
|
|
@@ -14,7 +14,7 @@ from typing import Any, Dict, Optional
|
|
|
14
14
|
|
|
15
15
|
from fastapi import APIRouter
|
|
16
16
|
|
|
17
|
-
from ....core.flows.models import FlowRunRecord, FlowRunStatus
|
|
17
|
+
from ....core.flows.models import FlowEventType, FlowRunRecord, FlowRunStatus
|
|
18
18
|
from ....core.flows.store import FlowStore
|
|
19
19
|
from ....core.utils import find_repo_root
|
|
20
20
|
from ....tickets.files import list_ticket_paths, read_ticket, ticket_is_done
|
|
@@ -26,18 +26,6 @@ def _flows_db_path(repo_root: Path) -> Path:
|
|
|
26
26
|
return repo_root / ".codex-autorunner" / "flows.db"
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def _load_flow_store(repo_root: Path) -> Optional[FlowStore]:
|
|
30
|
-
db_path = _flows_db_path(repo_root)
|
|
31
|
-
if not db_path.exists():
|
|
32
|
-
return None
|
|
33
|
-
store = FlowStore(db_path)
|
|
34
|
-
try:
|
|
35
|
-
store.initialize()
|
|
36
|
-
except Exception:
|
|
37
|
-
return None
|
|
38
|
-
return store
|
|
39
|
-
|
|
40
|
-
|
|
41
29
|
def _select_primary_run(records: list[FlowRunRecord]) -> Optional[FlowRunRecord]:
|
|
42
30
|
"""Select the primary run for analytics display.
|
|
43
31
|
|
|
@@ -154,19 +142,19 @@ def _aggregate_diff_stats(dispatch_history_dir: Path) -> Dict[str, int]:
|
|
|
154
142
|
|
|
155
143
|
|
|
156
144
|
def _build_summary(repo_root: Path) -> Dict[str, Any]:
|
|
145
|
+
from ....core.config import load_repo_config
|
|
146
|
+
|
|
157
147
|
ticket_dir = repo_root / ".codex-autorunner" / "tickets"
|
|
158
|
-
|
|
148
|
+
db_path = _flows_db_path(repo_root)
|
|
159
149
|
records: list[FlowRunRecord] = []
|
|
160
|
-
if
|
|
150
|
+
if db_path.exists():
|
|
161
151
|
try:
|
|
162
|
-
|
|
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")
|
|
163
156
|
except Exception:
|
|
164
157
|
records = []
|
|
165
|
-
finally:
|
|
166
|
-
try:
|
|
167
|
-
store.close()
|
|
168
|
-
except Exception:
|
|
169
|
-
pass
|
|
170
158
|
|
|
171
159
|
run_record = _select_primary_run(records)
|
|
172
160
|
|
|
@@ -226,7 +214,26 @@ def _build_summary(repo_root: Path) -> Dict[str, Any]:
|
|
|
226
214
|
)
|
|
227
215
|
turns["dispatches"] = _count_history_dirs(outbox_paths.dispatch_history_dir)
|
|
228
216
|
turns["replies"] = _count_history_dirs(reply_paths.reply_history_dir)
|
|
229
|
-
|
|
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
|
+
)
|
|
230
237
|
|
|
231
238
|
# If current ticket is known, read its frontmatter to pick agent id when available.
|
|
232
239
|
if current_ticket:
|