codex-autorunner 1.0.0__py3-none-any.whl → 1.1.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/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/runtime.py +59 -18
- codex_autorunner/agents/registry.py +22 -3
- codex_autorunner/bootstrap.py +7 -3
- codex_autorunner/cli.py +5 -1174
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +4 -0
- codex_autorunner/core/about_car.py +6 -1
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_threads.py +11 -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 +197 -3
- codex_autorunner/core/drafts.py +58 -4
- codex_autorunner/core/engine.py +1329 -680
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/flows/controller.py +25 -1
- codex_autorunner/core/flows/models.py +13 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +131 -0
- codex_autorunner/core/flows/runtime.py +35 -4
- codex_autorunner/core/flows/store.py +83 -0
- codex_autorunner/core/flows/transition.py +5 -0
- codex_autorunner/core/flows/ux_helpers.py +257 -0
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +121 -7
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +11 -3
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/{integrations/agents → core/ports}/run_event.py +22 -2
- codex_autorunner/core/state_roots.py +57 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +201 -0
- codex_autorunner/core/ticket_manager_cli.py +432 -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 +91 -9
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
- codex_autorunner/flows/ticket_flow/definition.py +9 -2
- codex_autorunner/integrations/agents/__init__.py +9 -19
- codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +158 -17
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +305 -32
- codex_autorunner/integrations/agents/runner.py +91 -0
- codex_autorunner/integrations/agents/wiring.py +271 -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/handlers/callbacks.py +7 -0
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1203 -66
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +4 -3
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +8 -2
- codex_autorunner/integrations/telegram/handlers/messages.py +1 -0
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +24 -1
- codex_autorunner/integrations/telegram/service.py +15 -10
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +329 -40
- codex_autorunner/integrations/telegram/transport.py +3 -1
- 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 +2 -2
- codex_autorunner/static/agentControls.js +40 -11
- codex_autorunner/static/app.js +11 -3
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +7 -7
- codex_autorunner/static/dashboard.js +224 -171
- codex_autorunner/static/hub.js +112 -94
- codex_autorunner/static/index.html +80 -33
- codex_autorunner/static/messages.js +486 -83
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +125 -6
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/styles.css +1373 -101
- codex_autorunner/static/tabs.js +152 -11
- codex_autorunner/static/terminal.js +18 -0
- codex_autorunner/static/ticketEditor.js +99 -5
- codex_autorunner/static/tickets.js +760 -87
- codex_autorunner/static/utils.js +11 -0
- codex_autorunner/static/workspace.js +133 -40
- codex_autorunner/static/workspaceFileBrowser.js +9 -9
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +1224 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2019 -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 +78 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +277 -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 +836 -0
- codex_autorunner/surfaces/web/routes/flows.py +1164 -0
- codex_autorunner/surfaces/web/routes/messages.py +459 -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 +280 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -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 +417 -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 +26 -4
- codex_autorunner/tickets/files.py +6 -2
- codex_autorunner/tickets/models.py +3 -1
- codex_autorunner/tickets/outbox.py +12 -0
- codex_autorunner/tickets/runner.py +63 -5
- 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.1.0.dist-info/METADATA +154 -0
- codex_autorunner-1.1.0.dist-info/RECORD +308 -0
- 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.1.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
codex_autorunner/core/config.py
CHANGED
|
@@ -95,6 +95,9 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
95
95
|
"prev_run_max_chars": 6000,
|
|
96
96
|
"template": ".codex-autorunner/prompt.txt",
|
|
97
97
|
},
|
|
98
|
+
"ui": {
|
|
99
|
+
"editor": "vi",
|
|
100
|
+
},
|
|
98
101
|
"security": {
|
|
99
102
|
"redact_run_logs": True,
|
|
100
103
|
},
|
|
@@ -128,6 +131,9 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
128
131
|
},
|
|
129
132
|
},
|
|
130
133
|
},
|
|
134
|
+
"autorunner": {
|
|
135
|
+
"reuse_session": False,
|
|
136
|
+
},
|
|
131
137
|
"ticket_flow": {
|
|
132
138
|
"approval_mode": "yolo",
|
|
133
139
|
# Keep ticket_flow deterministic by default; surfaces can tighten this.
|
|
@@ -192,6 +198,11 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
192
198
|
"opencode": {
|
|
193
199
|
"session_stall_timeout_seconds": 60,
|
|
194
200
|
},
|
|
201
|
+
"usage": {
|
|
202
|
+
"cache_scope": "global",
|
|
203
|
+
"global_cache_root": None,
|
|
204
|
+
"repo_cache_path": ".codex-autorunner/usage/usage_series_cache.json",
|
|
205
|
+
},
|
|
195
206
|
"server": {
|
|
196
207
|
"host": "127.0.0.1",
|
|
197
208
|
"port": 4173,
|
|
@@ -205,6 +216,7 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
205
216
|
"enabled": "auto",
|
|
206
217
|
"events": ["run_finished", "run_error", "tui_idle"],
|
|
207
218
|
"tui_idle_seconds": 60,
|
|
219
|
+
"timeout_seconds": 5.0,
|
|
208
220
|
"discord": {
|
|
209
221
|
"webhook_url_env": "CAR_DISCORD_WEBHOOK_URL",
|
|
210
222
|
},
|
|
@@ -419,7 +431,9 @@ REPO_DEFAULT_KEYS = {
|
|
|
419
431
|
"docs",
|
|
420
432
|
"codex",
|
|
421
433
|
"prompt",
|
|
434
|
+
"ui",
|
|
422
435
|
"runner",
|
|
436
|
+
"autorunner",
|
|
423
437
|
"ticket_flow",
|
|
424
438
|
"git",
|
|
425
439
|
"github",
|
|
@@ -430,6 +444,7 @@ REPO_DEFAULT_KEYS = {
|
|
|
430
444
|
"server_log",
|
|
431
445
|
"review",
|
|
432
446
|
"opencode",
|
|
447
|
+
"usage",
|
|
433
448
|
}
|
|
434
449
|
DEFAULT_REPO_DEFAULTS = {
|
|
435
450
|
key: json.loads(json.dumps(DEFAULT_REPO_CONFIG[key])) for key in REPO_DEFAULT_KEYS
|
|
@@ -444,6 +459,7 @@ REPO_SHARED_KEYS = {
|
|
|
444
459
|
"static_assets",
|
|
445
460
|
"housekeeping",
|
|
446
461
|
"update",
|
|
462
|
+
"usage",
|
|
447
463
|
}
|
|
448
464
|
|
|
449
465
|
DEFAULT_HUB_CONFIG: Dict[str, Any] = {
|
|
@@ -603,6 +619,11 @@ DEFAULT_HUB_CONFIG: Dict[str, Any] = {
|
|
|
603
619
|
"opencode": {
|
|
604
620
|
"session_stall_timeout_seconds": 60,
|
|
605
621
|
},
|
|
622
|
+
"usage": {
|
|
623
|
+
"cache_scope": "global",
|
|
624
|
+
"global_cache_root": None,
|
|
625
|
+
"repo_cache_path": ".codex-autorunner/usage/usage_series_cache.json",
|
|
626
|
+
},
|
|
606
627
|
"server": {
|
|
607
628
|
"host": "127.0.0.1",
|
|
608
629
|
"port": 4173,
|
|
@@ -791,6 +812,13 @@ class OpenCodeConfig:
|
|
|
791
812
|
session_stall_timeout_seconds: Optional[float]
|
|
792
813
|
|
|
793
814
|
|
|
815
|
+
@dataclasses.dataclass
|
|
816
|
+
class UsageConfig:
|
|
817
|
+
cache_scope: str
|
|
818
|
+
global_cache_root: Path
|
|
819
|
+
repo_cache_path: Path
|
|
820
|
+
|
|
821
|
+
|
|
794
822
|
@dataclasses.dataclass(frozen=True)
|
|
795
823
|
class AgentConfig:
|
|
796
824
|
binary: str
|
|
@@ -819,12 +847,14 @@ class RepoConfig:
|
|
|
819
847
|
runner_stop_after_runs: Optional[int]
|
|
820
848
|
runner_max_wallclock_seconds: Optional[int]
|
|
821
849
|
runner_no_progress_threshold: int
|
|
850
|
+
autorunner_reuse_session: bool
|
|
822
851
|
ticket_flow: Dict[str, Any]
|
|
823
852
|
git_auto_commit: bool
|
|
824
853
|
git_commit_message_template: str
|
|
825
854
|
update_skip_checks: bool
|
|
826
855
|
app_server: AppServerConfig
|
|
827
856
|
opencode: OpenCodeConfig
|
|
857
|
+
usage: UsageConfig
|
|
828
858
|
server_host: str
|
|
829
859
|
server_port: int
|
|
830
860
|
server_base_path: str
|
|
@@ -875,6 +905,7 @@ class HubConfig:
|
|
|
875
905
|
update_skip_checks: bool
|
|
876
906
|
app_server: AppServerConfig
|
|
877
907
|
opencode: OpenCodeConfig
|
|
908
|
+
usage: UsageConfig
|
|
878
909
|
server_host: str
|
|
879
910
|
server_port: int
|
|
880
911
|
server_base_path: str
|
|
@@ -1243,6 +1274,40 @@ def _parse_opencode_config(
|
|
|
1243
1274
|
return OpenCodeConfig(session_stall_timeout_seconds=stall_timeout_seconds)
|
|
1244
1275
|
|
|
1245
1276
|
|
|
1277
|
+
def _parse_usage_config(
|
|
1278
|
+
cfg: Optional[Dict[str, Any]],
|
|
1279
|
+
root: Path,
|
|
1280
|
+
defaults: Optional[Dict[str, Any]],
|
|
1281
|
+
) -> UsageConfig:
|
|
1282
|
+
cfg = cfg if isinstance(cfg, dict) else {}
|
|
1283
|
+
defaults = defaults if isinstance(defaults, dict) else {}
|
|
1284
|
+
cache_scope = str(cfg.get("cache_scope", defaults.get("cache_scope", "global")))
|
|
1285
|
+
cache_scope = cache_scope.lower().strip() or "global"
|
|
1286
|
+
global_cache_raw = cfg.get("global_cache_root", defaults.get("global_cache_root"))
|
|
1287
|
+
if global_cache_raw is None:
|
|
1288
|
+
global_cache_raw = os.environ.get("CODEX_HOME", "~/.codex")
|
|
1289
|
+
global_cache_root = resolve_config_path(
|
|
1290
|
+
global_cache_raw,
|
|
1291
|
+
root,
|
|
1292
|
+
allow_absolute=True,
|
|
1293
|
+
allow_home=True,
|
|
1294
|
+
scope="usage.global_cache_root",
|
|
1295
|
+
)
|
|
1296
|
+
repo_cache_raw = cfg.get("repo_cache_path", defaults.get("repo_cache_path"))
|
|
1297
|
+
if repo_cache_raw is None:
|
|
1298
|
+
repo_cache_raw = ".codex-autorunner/usage/usage_series_cache.json"
|
|
1299
|
+
repo_cache_path = resolve_config_path(
|
|
1300
|
+
repo_cache_raw,
|
|
1301
|
+
root,
|
|
1302
|
+
scope="usage.repo_cache_path",
|
|
1303
|
+
)
|
|
1304
|
+
return UsageConfig(
|
|
1305
|
+
cache_scope=cache_scope,
|
|
1306
|
+
global_cache_root=global_cache_root,
|
|
1307
|
+
repo_cache_path=repo_cache_path,
|
|
1308
|
+
)
|
|
1309
|
+
|
|
1310
|
+
|
|
1246
1311
|
def _parse_agents_config(
|
|
1247
1312
|
cfg: Optional[Dict[str, Any]], defaults: Dict[str, Any]
|
|
1248
1313
|
) -> Dict[str, AgentConfig]:
|
|
@@ -1378,6 +1443,54 @@ def resolve_env_for_root(
|
|
|
1378
1443
|
return env
|
|
1379
1444
|
|
|
1380
1445
|
|
|
1446
|
+
VOICE_ENV_OVERRIDES = (
|
|
1447
|
+
"CODEX_AUTORUNNER_VOICE_ENABLED",
|
|
1448
|
+
"CODEX_AUTORUNNER_VOICE_PROVIDER",
|
|
1449
|
+
"CODEX_AUTORUNNER_VOICE_LATENCY",
|
|
1450
|
+
"CODEX_AUTORUNNER_VOICE_CHUNK_MS",
|
|
1451
|
+
"CODEX_AUTORUNNER_VOICE_SAMPLE_RATE",
|
|
1452
|
+
"CODEX_AUTORUNNER_VOICE_WARN_REMOTE",
|
|
1453
|
+
"CODEX_AUTORUNNER_VOICE_MAX_MS",
|
|
1454
|
+
"CODEX_AUTORUNNER_VOICE_SILENCE_MS",
|
|
1455
|
+
"CODEX_AUTORUNNER_VOICE_MIN_HOLD_MS",
|
|
1456
|
+
)
|
|
1457
|
+
|
|
1458
|
+
TELEGRAM_ENV_OVERRIDES = (
|
|
1459
|
+
"CAR_OPENCODE_COMMAND",
|
|
1460
|
+
"CAR_TELEGRAM_APP_SERVER_COMMAND",
|
|
1461
|
+
)
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
def collect_env_overrides(
|
|
1465
|
+
*,
|
|
1466
|
+
env: Optional[Mapping[str, str]] = None,
|
|
1467
|
+
include_telegram: bool = False,
|
|
1468
|
+
) -> list[str]:
|
|
1469
|
+
source = env if env is not None else os.environ
|
|
1470
|
+
overrides: list[str] = []
|
|
1471
|
+
|
|
1472
|
+
def _has_value(key: str) -> bool:
|
|
1473
|
+
value = source.get(key)
|
|
1474
|
+
if value is None:
|
|
1475
|
+
return False
|
|
1476
|
+
return str(value).strip() != ""
|
|
1477
|
+
|
|
1478
|
+
if source.get("CODEX_AUTORUNNER_SKIP_UPDATE_CHECKS") == "1":
|
|
1479
|
+
overrides.append("CODEX_AUTORUNNER_SKIP_UPDATE_CHECKS")
|
|
1480
|
+
if _has_value("CODEX_DISABLE_APP_SERVER_AUTORESTART_FOR_TESTS"):
|
|
1481
|
+
overrides.append("CODEX_DISABLE_APP_SERVER_AUTORESTART_FOR_TESTS")
|
|
1482
|
+
if _has_value("CAR_GLOBAL_STATE_ROOT"):
|
|
1483
|
+
overrides.append("CAR_GLOBAL_STATE_ROOT")
|
|
1484
|
+
for key in VOICE_ENV_OVERRIDES:
|
|
1485
|
+
if _has_value(key):
|
|
1486
|
+
overrides.append(key)
|
|
1487
|
+
if include_telegram:
|
|
1488
|
+
for key in TELEGRAM_ENV_OVERRIDES:
|
|
1489
|
+
if _has_value(key):
|
|
1490
|
+
overrides.append(key)
|
|
1491
|
+
return overrides
|
|
1492
|
+
|
|
1493
|
+
|
|
1381
1494
|
def load_hub_config_data(config_path: Path) -> Dict[str, Any]:
|
|
1382
1495
|
"""Load, merge, and return a raw hub config dict for the given config path."""
|
|
1383
1496
|
load_dotenv_for_root(config_path.parent.parent.resolve())
|
|
@@ -1417,7 +1530,7 @@ def load_hub_config(start: Path) -> HubConfig:
|
|
|
1417
1530
|
"""Load the nearest hub config walking upward from the provided path."""
|
|
1418
1531
|
config_path = _resolve_hub_config_path(start)
|
|
1419
1532
|
merged = load_hub_config_data(config_path)
|
|
1420
|
-
_validate_hub_config(merged)
|
|
1533
|
+
_validate_hub_config(merged, root=config_path.parent.parent.resolve())
|
|
1421
1534
|
return _build_hub_config(config_path, merged)
|
|
1422
1535
|
|
|
1423
1536
|
|
|
@@ -1463,7 +1576,7 @@ def load_repo_config(start: Path, hub_path: Optional[Path] = None) -> RepoConfig
|
|
|
1463
1576
|
repo_root = _resolve_repo_root(start)
|
|
1464
1577
|
hub_config_path = _resolve_hub_path_for_repo(repo_root, hub_path)
|
|
1465
1578
|
hub_config = load_hub_config_data(hub_config_path)
|
|
1466
|
-
_validate_hub_config(hub_config)
|
|
1579
|
+
_validate_hub_config(hub_config, root=hub_config_path.parent.parent.resolve())
|
|
1467
1580
|
hub = _build_hub_config(hub_config_path, hub_config)
|
|
1468
1581
|
return derive_repo_config(hub, repo_root)
|
|
1469
1582
|
|
|
@@ -1507,6 +1620,14 @@ def _build_repo_config(config_path: Path, cfg: Dict[str, Any]) -> RepoConfig:
|
|
|
1507
1620
|
Dict[str, Any], update_cfg if isinstance(update_cfg, dict) else {}
|
|
1508
1621
|
)
|
|
1509
1622
|
update_skip_checks = bool(update_cfg.get("skip_checks", False))
|
|
1623
|
+
autorunner_cfg = cfg.get("autorunner")
|
|
1624
|
+
autorunner_cfg = cast(
|
|
1625
|
+
Dict[str, Any], autorunner_cfg if isinstance(autorunner_cfg, dict) else {}
|
|
1626
|
+
)
|
|
1627
|
+
reuse_session_value = autorunner_cfg.get("reuse_session")
|
|
1628
|
+
autorunner_reuse_session = (
|
|
1629
|
+
bool(reuse_session_value) if reuse_session_value is not None else False
|
|
1630
|
+
)
|
|
1510
1631
|
return RepoConfig(
|
|
1511
1632
|
raw=cfg,
|
|
1512
1633
|
root=root,
|
|
@@ -1525,6 +1646,7 @@ def _build_repo_config(config_path: Path, cfg: Dict[str, Any]) -> RepoConfig:
|
|
|
1525
1646
|
runner_stop_after_runs=cfg["runner"].get("stop_after_runs"),
|
|
1526
1647
|
runner_max_wallclock_seconds=cfg["runner"].get("max_wallclock_seconds"),
|
|
1527
1648
|
runner_no_progress_threshold=int(cfg["runner"].get("no_progress_threshold", 3)),
|
|
1649
|
+
autorunner_reuse_session=autorunner_reuse_session,
|
|
1528
1650
|
git_auto_commit=bool(cfg["git"].get("auto_commit", False)),
|
|
1529
1651
|
git_commit_message_template=str(cfg["git"].get("commit_message_template")),
|
|
1530
1652
|
update_skip_checks=update_skip_checks,
|
|
@@ -1537,6 +1659,9 @@ def _build_repo_config(config_path: Path, cfg: Dict[str, Any]) -> RepoConfig:
|
|
|
1537
1659
|
opencode=_parse_opencode_config(
|
|
1538
1660
|
cfg.get("opencode"), root, DEFAULT_REPO_CONFIG.get("opencode")
|
|
1539
1661
|
),
|
|
1662
|
+
usage=_parse_usage_config(
|
|
1663
|
+
cfg.get("usage"), root, DEFAULT_REPO_CONFIG.get("usage")
|
|
1664
|
+
),
|
|
1540
1665
|
security=security_cfg,
|
|
1541
1666
|
server_host=str(cfg["server"].get("host")),
|
|
1542
1667
|
server_port=int(cfg["server"].get("port")),
|
|
@@ -1638,6 +1763,9 @@ def _build_hub_config(config_path: Path, cfg: Dict[str, Any]) -> HubConfig:
|
|
|
1638
1763
|
opencode=_parse_opencode_config(
|
|
1639
1764
|
cfg.get("opencode"), root, DEFAULT_HUB_CONFIG.get("opencode")
|
|
1640
1765
|
),
|
|
1766
|
+
usage=_parse_usage_config(
|
|
1767
|
+
cfg.get("usage"), root, DEFAULT_HUB_CONFIG.get("usage")
|
|
1768
|
+
),
|
|
1641
1769
|
server_host=str(cfg["server"]["host"]),
|
|
1642
1770
|
server_port=int(cfg["server"]["port"]),
|
|
1643
1771
|
server_base_path=_normalize_base_path(cfg["server"].get("base_path", "")),
|
|
@@ -1854,6 +1982,47 @@ def _validate_update_config(cfg: Dict[str, Any]) -> None:
|
|
|
1854
1982
|
raise ConfigError("update.skip_checks must be boolean or null")
|
|
1855
1983
|
|
|
1856
1984
|
|
|
1985
|
+
def _validate_usage_config(cfg: Dict[str, Any], *, root: Path) -> None:
|
|
1986
|
+
usage_cfg = cfg.get("usage")
|
|
1987
|
+
if usage_cfg is None:
|
|
1988
|
+
return
|
|
1989
|
+
if not isinstance(usage_cfg, dict):
|
|
1990
|
+
raise ConfigError("usage section must be a mapping if provided")
|
|
1991
|
+
cache_scope = usage_cfg.get("cache_scope")
|
|
1992
|
+
if cache_scope is not None and not isinstance(cache_scope, str):
|
|
1993
|
+
raise ConfigError("usage.cache_scope must be a string if provided")
|
|
1994
|
+
if isinstance(cache_scope, str):
|
|
1995
|
+
scope_val = cache_scope.strip().lower()
|
|
1996
|
+
if scope_val and scope_val not in {"global", "repo"}:
|
|
1997
|
+
raise ConfigError("usage.cache_scope must be 'global' or 'repo'")
|
|
1998
|
+
global_cache_root = usage_cfg.get("global_cache_root")
|
|
1999
|
+
if global_cache_root is not None:
|
|
2000
|
+
if not isinstance(global_cache_root, str):
|
|
2001
|
+
raise ConfigError("usage.global_cache_root must be a string or null")
|
|
2002
|
+
try:
|
|
2003
|
+
resolve_config_path(
|
|
2004
|
+
global_cache_root,
|
|
2005
|
+
root,
|
|
2006
|
+
allow_absolute=True,
|
|
2007
|
+
allow_home=True,
|
|
2008
|
+
scope="usage.global_cache_root",
|
|
2009
|
+
)
|
|
2010
|
+
except ConfigPathError as exc:
|
|
2011
|
+
raise ConfigError(str(exc)) from exc
|
|
2012
|
+
repo_cache_path = usage_cfg.get("repo_cache_path")
|
|
2013
|
+
if repo_cache_path is not None:
|
|
2014
|
+
if not isinstance(repo_cache_path, str):
|
|
2015
|
+
raise ConfigError("usage.repo_cache_path must be a string or null")
|
|
2016
|
+
try:
|
|
2017
|
+
resolve_config_path(
|
|
2018
|
+
repo_cache_path,
|
|
2019
|
+
root,
|
|
2020
|
+
scope="usage.repo_cache_path",
|
|
2021
|
+
)
|
|
2022
|
+
except ConfigPathError as exc:
|
|
2023
|
+
raise ConfigError(str(exc)) from exc
|
|
2024
|
+
|
|
2025
|
+
|
|
1857
2026
|
def _validate_agents_config(cfg: Dict[str, Any]) -> None:
|
|
1858
2027
|
agents_cfg = cfg.get("agents")
|
|
1859
2028
|
if agents_cfg is None:
|
|
@@ -1943,6 +2112,13 @@ def _validate_repo_config(cfg: Dict[str, Any], *, root: Path) -> None:
|
|
|
1943
2112
|
val = runner.get(k)
|
|
1944
2113
|
if val is not None and not isinstance(val, int):
|
|
1945
2114
|
raise ConfigError(f"runner.{k} must be an integer or null")
|
|
2115
|
+
autorunner_cfg = cfg.get("autorunner")
|
|
2116
|
+
if autorunner_cfg is not None and not isinstance(autorunner_cfg, dict):
|
|
2117
|
+
raise ConfigError("autorunner section must be a mapping if provided")
|
|
2118
|
+
if isinstance(autorunner_cfg, dict):
|
|
2119
|
+
reuse_session = autorunner_cfg.get("reuse_session")
|
|
2120
|
+
if reuse_session is not None and not isinstance(reuse_session, bool):
|
|
2121
|
+
raise ConfigError("autorunner.reuse_session must be boolean or null")
|
|
1946
2122
|
ticket_flow_cfg = cfg.get("ticket_flow")
|
|
1947
2123
|
if ticket_flow_cfg is not None and not isinstance(ticket_flow_cfg, dict):
|
|
1948
2124
|
raise ConfigError("ticket_flow section must be a mapping if provided")
|
|
@@ -1955,6 +2131,12 @@ def _validate_repo_config(cfg: Dict[str, Any], *, root: Path) -> None:
|
|
|
1955
2131
|
ticket_flow_cfg.get("default_approval_decision"), str
|
|
1956
2132
|
):
|
|
1957
2133
|
raise ConfigError("ticket_flow.default_approval_decision must be a string")
|
|
2134
|
+
ui_cfg = cfg.get("ui")
|
|
2135
|
+
if ui_cfg is not None and not isinstance(ui_cfg, dict):
|
|
2136
|
+
raise ConfigError("ui section must be a mapping if provided")
|
|
2137
|
+
if isinstance(ui_cfg, dict):
|
|
2138
|
+
if "editor" in ui_cfg and not isinstance(ui_cfg.get("editor"), str):
|
|
2139
|
+
raise ConfigError("ui.editor must be a string if provided")
|
|
1958
2140
|
git = cfg.get("git")
|
|
1959
2141
|
if not isinstance(git, dict):
|
|
1960
2142
|
raise ConfigError("git section must be a mapping")
|
|
@@ -1998,6 +2180,7 @@ def _validate_repo_config(cfg: Dict[str, Any], *, root: Path) -> None:
|
|
|
1998
2180
|
_validate_app_server_config(cfg)
|
|
1999
2181
|
_validate_opencode_config(cfg)
|
|
2000
2182
|
_validate_update_config(cfg)
|
|
2183
|
+
_validate_usage_config(cfg, root=root)
|
|
2001
2184
|
notifications_cfg = cfg.get("notifications")
|
|
2002
2185
|
if notifications_cfg is not None:
|
|
2003
2186
|
if not isinstance(notifications_cfg, dict):
|
|
@@ -2029,6 +2212,16 @@ def _validate_repo_config(cfg: Dict[str, Any], *, root: Path) -> None:
|
|
|
2029
2212
|
raise ConfigError(
|
|
2030
2213
|
"notifications.tui_idle_seconds must be >= 0 if provided"
|
|
2031
2214
|
)
|
|
2215
|
+
timeout_seconds = notifications_cfg.get("timeout_seconds")
|
|
2216
|
+
if timeout_seconds is not None:
|
|
2217
|
+
if not isinstance(timeout_seconds, (int, float)):
|
|
2218
|
+
raise ConfigError(
|
|
2219
|
+
"notifications.timeout_seconds must be a number if provided"
|
|
2220
|
+
)
|
|
2221
|
+
if timeout_seconds <= 0:
|
|
2222
|
+
raise ConfigError(
|
|
2223
|
+
"notifications.timeout_seconds must be > 0 if provided"
|
|
2224
|
+
)
|
|
2032
2225
|
discord_cfg = notifications_cfg.get("discord")
|
|
2033
2226
|
if discord_cfg is not None and not isinstance(discord_cfg, dict):
|
|
2034
2227
|
raise ConfigError("notifications.discord must be a mapping if provided")
|
|
@@ -2134,7 +2327,7 @@ def _validate_repo_config(cfg: Dict[str, Any], *, root: Path) -> None:
|
|
|
2134
2327
|
_validate_telegram_bot_config(cfg)
|
|
2135
2328
|
|
|
2136
2329
|
|
|
2137
|
-
def _validate_hub_config(cfg: Dict[str, Any]) -> None:
|
|
2330
|
+
def _validate_hub_config(cfg: Dict[str, Any], *, root: Path) -> None:
|
|
2138
2331
|
_validate_version(cfg)
|
|
2139
2332
|
if cfg.get("mode") != "hub":
|
|
2140
2333
|
raise ConfigError("Hub config must set mode: hub")
|
|
@@ -2143,6 +2336,7 @@ def _validate_hub_config(cfg: Dict[str, Any]) -> None:
|
|
|
2143
2336
|
_validate_agents_config(cfg)
|
|
2144
2337
|
_validate_opencode_config(cfg)
|
|
2145
2338
|
_validate_update_config(cfg)
|
|
2339
|
+
_validate_usage_config(cfg, root=root)
|
|
2146
2340
|
repo_defaults = cfg.get("repo_defaults")
|
|
2147
2341
|
if repo_defaults is not None:
|
|
2148
2342
|
if not isinstance(repo_defaults, dict):
|
codex_autorunner/core/drafts.py
CHANGED
|
@@ -2,12 +2,18 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import json
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime, timezone
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
from typing import Any, Dict, Optional
|
|
7
9
|
|
|
8
10
|
from .utils import atomic_write
|
|
9
11
|
|
|
10
12
|
FILE_CHAT_STATE_NAME = "file_chat_state.json"
|
|
13
|
+
FILE_CHAT_STATE_CORRUPT_SUFFIX = ".corrupt"
|
|
14
|
+
FILE_CHAT_STATE_NOTICE_SUFFIX = ".corrupt.json"
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
11
17
|
|
|
12
18
|
|
|
13
19
|
def state_path(repo_root: Path) -> Path:
|
|
@@ -23,12 +29,19 @@ def load_state(repo_root: Path) -> Dict[str, Any]:
|
|
|
23
29
|
if not path.exists():
|
|
24
30
|
return {"drafts": {}}
|
|
25
31
|
try:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
raw = path.read_text(encoding="utf-8")
|
|
33
|
+
except OSError as exc:
|
|
34
|
+
logger.warning("Failed to read file chat state at %s: %s", path, exc)
|
|
29
35
|
return {"drafts": {}}
|
|
30
|
-
|
|
36
|
+
try:
|
|
37
|
+
data = json.loads(raw)
|
|
38
|
+
except json.JSONDecodeError as exc:
|
|
39
|
+
_handle_corrupt_state(path, str(exc))
|
|
40
|
+
return {"drafts": {}}
|
|
41
|
+
if not isinstance(data, dict):
|
|
42
|
+
_handle_corrupt_state(path, f"Expected JSON object, got {type(data).__name__}")
|
|
31
43
|
return {"drafts": {}}
|
|
44
|
+
return data
|
|
32
45
|
|
|
33
46
|
|
|
34
47
|
def save_state(repo_root: Path, state: Dict[str, Any]) -> None:
|
|
@@ -80,3 +93,44 @@ def invalidate_drafts_for_path(repo_root: Path, rel_path: str) -> list[str]:
|
|
|
80
93
|
if removed_keys:
|
|
81
94
|
save_drafts(repo_root, drafts)
|
|
82
95
|
return removed_keys
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _stamp() -> str:
|
|
99
|
+
return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _notice_path(path: Path) -> Path:
|
|
103
|
+
return path.with_name(f"{path.name}{FILE_CHAT_STATE_NOTICE_SUFFIX}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _handle_corrupt_state(path: Path, detail: str) -> None:
|
|
107
|
+
stamp = _stamp()
|
|
108
|
+
backup_path = path.with_name(f"{path.name}{FILE_CHAT_STATE_CORRUPT_SUFFIX}.{stamp}")
|
|
109
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
try:
|
|
111
|
+
path.replace(backup_path)
|
|
112
|
+
backup_value = str(backup_path)
|
|
113
|
+
except OSError:
|
|
114
|
+
backup_value = ""
|
|
115
|
+
notice = {
|
|
116
|
+
"status": "corrupt",
|
|
117
|
+
"message": "Draft state reset due to corrupted file_chat_state.json.",
|
|
118
|
+
"detail": detail,
|
|
119
|
+
"detected_at": stamp,
|
|
120
|
+
"backup_path": backup_value,
|
|
121
|
+
}
|
|
122
|
+
notice_path = _notice_path(path)
|
|
123
|
+
try:
|
|
124
|
+
atomic_write(notice_path, json.dumps(notice, indent=2) + "\n")
|
|
125
|
+
except Exception:
|
|
126
|
+
logger.warning("Failed to write draft corruption notice at %s", notice_path)
|
|
127
|
+
try:
|
|
128
|
+
atomic_write(path, json.dumps({"drafts": {}}, indent=2) + "\n")
|
|
129
|
+
except Exception:
|
|
130
|
+
logger.warning("Failed to reset draft state at %s", path)
|
|
131
|
+
logger.warning(
|
|
132
|
+
"Corrupted file chat state detected; backup=%s notice=%s detail=%s",
|
|
133
|
+
backup_value or "unavailable",
|
|
134
|
+
notice_path,
|
|
135
|
+
detail,
|
|
136
|
+
)
|