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,527 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PMA lifecycle command router.
|
|
3
|
+
|
|
4
|
+
Provides unified lifecycle commands for PMA across Web and Telegram surfaces:
|
|
5
|
+
- /new - new PMA session/thread
|
|
6
|
+
- /reset - clear volatile state; keep stable defaults
|
|
7
|
+
- /stop - interrupt current work and clear queue for current lane
|
|
8
|
+
- /compact - summarize/compact history into durable artifacts
|
|
9
|
+
|
|
10
|
+
All commands create durable artifacts and emit event records for observability.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Optional
|
|
22
|
+
|
|
23
|
+
from .app_server_threads import (
|
|
24
|
+
PMA_KEY,
|
|
25
|
+
PMA_OPENCODE_KEY,
|
|
26
|
+
AppServerThreadRegistry,
|
|
27
|
+
)
|
|
28
|
+
from .logging_utils import log_event
|
|
29
|
+
from .pma_audit import PmaActionType
|
|
30
|
+
from .pma_queue import PmaQueue
|
|
31
|
+
from .pma_safety import PmaSafetyChecker, PmaSafetyConfig
|
|
32
|
+
from .time_utils import now_iso
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LifecycleCommand(Enum):
|
|
38
|
+
"""PMA lifecycle command types."""
|
|
39
|
+
|
|
40
|
+
NEW = "new"
|
|
41
|
+
RESET = "reset"
|
|
42
|
+
STOP = "stop"
|
|
43
|
+
COMPACT = "compact"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class LifecycleCommandResult:
|
|
48
|
+
"""Result of executing a lifecycle command."""
|
|
49
|
+
|
|
50
|
+
status: str
|
|
51
|
+
command: LifecycleCommand
|
|
52
|
+
message: str
|
|
53
|
+
artifact_path: Optional[Path] = None
|
|
54
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
55
|
+
error: Optional[str] = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PmaLifecycleRouter:
|
|
59
|
+
"""
|
|
60
|
+
Unified router for PMA lifecycle commands.
|
|
61
|
+
|
|
62
|
+
Provides a single adapter-level implementation that can be called from:
|
|
63
|
+
- Web UI API endpoints
|
|
64
|
+
- Telegram slash commands
|
|
65
|
+
- CLI commands
|
|
66
|
+
|
|
67
|
+
All commands are idempotent and create durable artifacts.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, hub_root: Path) -> None:
|
|
71
|
+
self._hub_root = hub_root
|
|
72
|
+
self._artifacts_dir = hub_root / ".codex-autorunner" / "pma" / "lifecycle"
|
|
73
|
+
self._artifacts_dir.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
self._events_log = (
|
|
75
|
+
hub_root / ".codex-autorunner" / "pma" / "lifecycle_events.jsonl"
|
|
76
|
+
)
|
|
77
|
+
safety_config = PmaSafetyConfig()
|
|
78
|
+
self._safety_checker = PmaSafetyChecker(hub_root, config=safety_config)
|
|
79
|
+
|
|
80
|
+
async def new(
|
|
81
|
+
self,
|
|
82
|
+
*,
|
|
83
|
+
agent: Optional[str] = None,
|
|
84
|
+
lane_id: str = "pma:default",
|
|
85
|
+
metadata: Optional[dict[str, Any]] = None,
|
|
86
|
+
) -> LifecycleCommandResult:
|
|
87
|
+
"""
|
|
88
|
+
Start a new PMA session/thread.
|
|
89
|
+
|
|
90
|
+
- If agent is opencode: creates a new OpenCode session
|
|
91
|
+
- If agent is codex or not specified: creates a new app-server thread
|
|
92
|
+
- In PMA mode: resets the PMA thread state
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
agent: The agent to use (codex|opencode)
|
|
96
|
+
lane_id: The PMA queue lane ID
|
|
97
|
+
metadata: Additional metadata to include in the artifact
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
LifecycleCommandResult with artifact path and details
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
event_id = self._generate_event_id()
|
|
104
|
+
timestamp = now_iso()
|
|
105
|
+
|
|
106
|
+
# Reset thread state
|
|
107
|
+
registry = AppServerThreadRegistry(
|
|
108
|
+
self._hub_root / ".codex-autorunner" / "app_server_threads.json"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
cleared_keys = []
|
|
112
|
+
if agent == "opencode" or not agent:
|
|
113
|
+
if registry.reset_thread(PMA_OPENCODE_KEY):
|
|
114
|
+
cleared_keys.append(PMA_OPENCODE_KEY)
|
|
115
|
+
|
|
116
|
+
if agent != "opencode" or not agent:
|
|
117
|
+
if registry.reset_thread(PMA_KEY):
|
|
118
|
+
cleared_keys.append(PMA_KEY)
|
|
119
|
+
|
|
120
|
+
# Create artifact
|
|
121
|
+
artifact = {
|
|
122
|
+
"event_id": event_id,
|
|
123
|
+
"command": LifecycleCommand.NEW.value,
|
|
124
|
+
"timestamp": timestamp,
|
|
125
|
+
"agent": agent,
|
|
126
|
+
"lane_id": lane_id,
|
|
127
|
+
"cleared_threads": cleared_keys,
|
|
128
|
+
"metadata": metadata or {},
|
|
129
|
+
}
|
|
130
|
+
artifact_path = self._write_artifact(event_id, artifact)
|
|
131
|
+
|
|
132
|
+
# Record action in safety checker
|
|
133
|
+
self._safety_checker.record_action(
|
|
134
|
+
action_type=PmaActionType.SESSION_NEW,
|
|
135
|
+
agent=agent,
|
|
136
|
+
thread_id=None,
|
|
137
|
+
turn_id=None,
|
|
138
|
+
client_turn_id=None,
|
|
139
|
+
details={
|
|
140
|
+
"command": "new",
|
|
141
|
+
"cleared_threads": cleared_keys,
|
|
142
|
+
"lane_id": lane_id,
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Emit event record
|
|
147
|
+
self._emit_event(
|
|
148
|
+
{
|
|
149
|
+
"event_id": event_id,
|
|
150
|
+
"event_type": "pma_lifecycle_new",
|
|
151
|
+
"timestamp": timestamp,
|
|
152
|
+
"agent": agent,
|
|
153
|
+
"lane_id": lane_id,
|
|
154
|
+
"cleared_threads": cleared_keys,
|
|
155
|
+
"artifact_path": str(artifact_path),
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
log_event(
|
|
160
|
+
logger,
|
|
161
|
+
logging.INFO,
|
|
162
|
+
"pma.lifecycle.new",
|
|
163
|
+
event_id=event_id,
|
|
164
|
+
agent=agent,
|
|
165
|
+
lane_id=lane_id,
|
|
166
|
+
cleared_threads=cleared_keys,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return LifecycleCommandResult(
|
|
170
|
+
status="ok",
|
|
171
|
+
command=LifecycleCommand.NEW,
|
|
172
|
+
message=f"New PMA session started (agent={agent or 'default'})",
|
|
173
|
+
artifact_path=artifact_path,
|
|
174
|
+
details={
|
|
175
|
+
"cleared_threads": cleared_keys,
|
|
176
|
+
"agent": agent,
|
|
177
|
+
"lane_id": lane_id,
|
|
178
|
+
},
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
except Exception as exc:
|
|
182
|
+
log_event(
|
|
183
|
+
logger,
|
|
184
|
+
logging.ERROR,
|
|
185
|
+
"pma.lifecycle.new.failed",
|
|
186
|
+
exc=exc,
|
|
187
|
+
agent=agent,
|
|
188
|
+
lane_id=lane_id,
|
|
189
|
+
)
|
|
190
|
+
return LifecycleCommandResult(
|
|
191
|
+
status="error",
|
|
192
|
+
command=LifecycleCommand.NEW,
|
|
193
|
+
message=f"Failed to start new PMA session: {exc}",
|
|
194
|
+
error=str(exc),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
async def reset(
|
|
198
|
+
self,
|
|
199
|
+
*,
|
|
200
|
+
agent: Optional[str] = None,
|
|
201
|
+
metadata: Optional[dict[str, Any]] = None,
|
|
202
|
+
) -> LifecycleCommandResult:
|
|
203
|
+
"""
|
|
204
|
+
Reset PMA thread state (clear volatile state; keep stable defaults).
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
agent: The agent thread to reset (opencode|codex|all)
|
|
208
|
+
metadata: Additional metadata to include in the artifact
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
LifecycleCommandResult with artifact path and details
|
|
212
|
+
"""
|
|
213
|
+
try:
|
|
214
|
+
event_id = self._generate_event_id()
|
|
215
|
+
timestamp = now_iso()
|
|
216
|
+
|
|
217
|
+
# Reset thread state
|
|
218
|
+
registry = AppServerThreadRegistry(
|
|
219
|
+
self._hub_root / ".codex-autorunner" / "app_server_threads.json"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
cleared_keys = []
|
|
223
|
+
if agent in ("", "all", None):
|
|
224
|
+
if registry.reset_thread(PMA_KEY):
|
|
225
|
+
cleared_keys.append(PMA_KEY)
|
|
226
|
+
if registry.reset_thread(PMA_OPENCODE_KEY):
|
|
227
|
+
cleared_keys.append(PMA_OPENCODE_KEY)
|
|
228
|
+
elif agent == "opencode":
|
|
229
|
+
if registry.reset_thread(PMA_OPENCODE_KEY):
|
|
230
|
+
cleared_keys.append(PMA_OPENCODE_KEY)
|
|
231
|
+
else:
|
|
232
|
+
if registry.reset_thread(PMA_KEY):
|
|
233
|
+
cleared_keys.append(PMA_KEY)
|
|
234
|
+
|
|
235
|
+
# Create artifact
|
|
236
|
+
artifact = {
|
|
237
|
+
"event_id": event_id,
|
|
238
|
+
"command": LifecycleCommand.RESET.value,
|
|
239
|
+
"timestamp": timestamp,
|
|
240
|
+
"agent": agent,
|
|
241
|
+
"cleared_threads": cleared_keys,
|
|
242
|
+
"metadata": metadata or {},
|
|
243
|
+
}
|
|
244
|
+
artifact_path = self._write_artifact(event_id, artifact)
|
|
245
|
+
|
|
246
|
+
# Record action in safety checker
|
|
247
|
+
self._safety_checker.record_action(
|
|
248
|
+
action_type=PmaActionType.SESSION_RESET,
|
|
249
|
+
agent=agent,
|
|
250
|
+
thread_id=None,
|
|
251
|
+
turn_id=None,
|
|
252
|
+
client_turn_id=None,
|
|
253
|
+
details={
|
|
254
|
+
"command": "reset",
|
|
255
|
+
"cleared_threads": cleared_keys,
|
|
256
|
+
},
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Emit event record
|
|
260
|
+
self._emit_event(
|
|
261
|
+
{
|
|
262
|
+
"event_id": event_id,
|
|
263
|
+
"event_type": "pma_lifecycle_reset",
|
|
264
|
+
"timestamp": timestamp,
|
|
265
|
+
"agent": agent,
|
|
266
|
+
"cleared_threads": cleared_keys,
|
|
267
|
+
"artifact_path": str(artifact_path),
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
log_event(
|
|
272
|
+
logger,
|
|
273
|
+
logging.INFO,
|
|
274
|
+
"pma.lifecycle.reset",
|
|
275
|
+
event_id=event_id,
|
|
276
|
+
agent=agent,
|
|
277
|
+
cleared_threads=cleared_keys,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
return LifecycleCommandResult(
|
|
281
|
+
status="ok",
|
|
282
|
+
command=LifecycleCommand.RESET,
|
|
283
|
+
message=f"PMA thread reset. Cleared: {', '.join(cleared_keys)}",
|
|
284
|
+
artifact_path=artifact_path,
|
|
285
|
+
details={
|
|
286
|
+
"cleared_threads": cleared_keys,
|
|
287
|
+
"agent": agent,
|
|
288
|
+
},
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
except Exception as exc:
|
|
292
|
+
log_event(
|
|
293
|
+
logger,
|
|
294
|
+
logging.ERROR,
|
|
295
|
+
"pma.lifecycle.reset.failed",
|
|
296
|
+
exc=exc,
|
|
297
|
+
agent=agent,
|
|
298
|
+
)
|
|
299
|
+
return LifecycleCommandResult(
|
|
300
|
+
status="error",
|
|
301
|
+
command=LifecycleCommand.RESET,
|
|
302
|
+
message=f"Failed to reset PMA thread: {exc}",
|
|
303
|
+
error=str(exc),
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
async def stop(
|
|
307
|
+
self,
|
|
308
|
+
*,
|
|
309
|
+
lane_id: str = "pma:default",
|
|
310
|
+
metadata: Optional[dict[str, Any]] = None,
|
|
311
|
+
) -> LifecycleCommandResult:
|
|
312
|
+
"""
|
|
313
|
+
Stop PMA lane: interrupt current work and clear queue for current lane.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
lane_id: The PMA queue lane ID
|
|
317
|
+
metadata: Additional metadata to include in the artifact
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
LifecycleCommandResult with artifact path and details
|
|
321
|
+
"""
|
|
322
|
+
try:
|
|
323
|
+
event_id = self._generate_event_id()
|
|
324
|
+
timestamp = now_iso()
|
|
325
|
+
|
|
326
|
+
# Cancel queued items
|
|
327
|
+
queue = PmaQueue(self._hub_root)
|
|
328
|
+
cancelled = await queue.cancel_lane(lane_id)
|
|
329
|
+
|
|
330
|
+
# Create artifact
|
|
331
|
+
artifact = {
|
|
332
|
+
"event_id": event_id,
|
|
333
|
+
"command": LifecycleCommand.STOP.value,
|
|
334
|
+
"timestamp": timestamp,
|
|
335
|
+
"lane_id": lane_id,
|
|
336
|
+
"cancelled_items": cancelled,
|
|
337
|
+
"metadata": metadata or {},
|
|
338
|
+
}
|
|
339
|
+
artifact_path = self._write_artifact(event_id, artifact)
|
|
340
|
+
|
|
341
|
+
# Record action in safety checker
|
|
342
|
+
self._safety_checker.record_action(
|
|
343
|
+
action_type=PmaActionType.SESSION_STOP,
|
|
344
|
+
agent=None,
|
|
345
|
+
thread_id=None,
|
|
346
|
+
turn_id=None,
|
|
347
|
+
client_turn_id=None,
|
|
348
|
+
details={
|
|
349
|
+
"command": "stop",
|
|
350
|
+
"lane_id": lane_id,
|
|
351
|
+
"cancelled_items": cancelled,
|
|
352
|
+
},
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# Emit event record
|
|
356
|
+
self._emit_event(
|
|
357
|
+
{
|
|
358
|
+
"event_id": event_id,
|
|
359
|
+
"event_type": "pma_lifecycle_stop",
|
|
360
|
+
"timestamp": timestamp,
|
|
361
|
+
"lane_id": lane_id,
|
|
362
|
+
"cancelled_items": cancelled,
|
|
363
|
+
"artifact_path": str(artifact_path),
|
|
364
|
+
}
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
log_event(
|
|
368
|
+
logger,
|
|
369
|
+
logging.INFO,
|
|
370
|
+
"pma.lifecycle.stop",
|
|
371
|
+
event_id=event_id,
|
|
372
|
+
lane_id=lane_id,
|
|
373
|
+
cancelled=cancelled,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
return LifecycleCommandResult(
|
|
377
|
+
status="ok",
|
|
378
|
+
command=LifecycleCommand.STOP,
|
|
379
|
+
message=f"PMA lane stopped. Cancelled {cancelled} queued items",
|
|
380
|
+
artifact_path=artifact_path,
|
|
381
|
+
details={
|
|
382
|
+
"lane_id": lane_id,
|
|
383
|
+
"cancelled_items": cancelled,
|
|
384
|
+
},
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
except Exception as exc:
|
|
388
|
+
log_event(
|
|
389
|
+
logger,
|
|
390
|
+
logging.ERROR,
|
|
391
|
+
"pma.lifecycle.stop.failed",
|
|
392
|
+
exc=exc,
|
|
393
|
+
lane_id=lane_id,
|
|
394
|
+
)
|
|
395
|
+
return LifecycleCommandResult(
|
|
396
|
+
status="error",
|
|
397
|
+
command=LifecycleCommand.STOP,
|
|
398
|
+
message=f"Failed to stop PMA lane: {exc}",
|
|
399
|
+
error=str(exc),
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
async def compact(
|
|
403
|
+
self,
|
|
404
|
+
*,
|
|
405
|
+
summary: str,
|
|
406
|
+
agent: Optional[str] = None,
|
|
407
|
+
thread_id: Optional[str] = None,
|
|
408
|
+
metadata: Optional[dict[str, Any]] = None,
|
|
409
|
+
) -> LifecycleCommandResult:
|
|
410
|
+
"""
|
|
411
|
+
Compact PMA history (summarize/compact into durable artifacts).
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
summary: The compact summary text
|
|
415
|
+
agent: The agent used for compacting
|
|
416
|
+
thread_id: The thread ID being compacted
|
|
417
|
+
metadata: Additional metadata to include in the artifact
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
LifecycleCommandResult with artifact path and details
|
|
421
|
+
"""
|
|
422
|
+
try:
|
|
423
|
+
event_id = self._generate_event_id()
|
|
424
|
+
timestamp = now_iso()
|
|
425
|
+
|
|
426
|
+
# Create artifact
|
|
427
|
+
artifact = {
|
|
428
|
+
"event_id": event_id,
|
|
429
|
+
"command": LifecycleCommand.COMPACT.value,
|
|
430
|
+
"timestamp": timestamp,
|
|
431
|
+
"agent": agent,
|
|
432
|
+
"thread_id": thread_id,
|
|
433
|
+
"summary": summary,
|
|
434
|
+
"metadata": metadata or {},
|
|
435
|
+
}
|
|
436
|
+
artifact_path = self._write_artifact(event_id, artifact)
|
|
437
|
+
|
|
438
|
+
# Record action in safety checker
|
|
439
|
+
self._safety_checker.record_action(
|
|
440
|
+
action_type=PmaActionType.SESSION_COMPACT,
|
|
441
|
+
agent=agent,
|
|
442
|
+
thread_id=thread_id,
|
|
443
|
+
turn_id=None,
|
|
444
|
+
client_turn_id=None,
|
|
445
|
+
details={
|
|
446
|
+
"command": "compact",
|
|
447
|
+
"summary_length": len(summary),
|
|
448
|
+
},
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Emit event record
|
|
452
|
+
self._emit_event(
|
|
453
|
+
{
|
|
454
|
+
"event_id": event_id,
|
|
455
|
+
"event_type": "pma_lifecycle_compact",
|
|
456
|
+
"timestamp": timestamp,
|
|
457
|
+
"agent": agent,
|
|
458
|
+
"thread_id": thread_id,
|
|
459
|
+
"summary_length": len(summary),
|
|
460
|
+
"artifact_path": str(artifact_path),
|
|
461
|
+
}
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
log_event(
|
|
465
|
+
logger,
|
|
466
|
+
logging.INFO,
|
|
467
|
+
"pma.lifecycle.compact",
|
|
468
|
+
event_id=event_id,
|
|
469
|
+
agent=agent,
|
|
470
|
+
thread_id=thread_id,
|
|
471
|
+
summary_length=len(summary),
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
return LifecycleCommandResult(
|
|
475
|
+
status="ok",
|
|
476
|
+
command=LifecycleCommand.COMPACT,
|
|
477
|
+
message="PMA history compacted",
|
|
478
|
+
artifact_path=artifact_path,
|
|
479
|
+
details={
|
|
480
|
+
"agent": agent,
|
|
481
|
+
"thread_id": thread_id,
|
|
482
|
+
"summary_length": len(summary),
|
|
483
|
+
},
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
except Exception as exc:
|
|
487
|
+
log_event(
|
|
488
|
+
logger,
|
|
489
|
+
logging.ERROR,
|
|
490
|
+
"pma.lifecycle.compact.failed",
|
|
491
|
+
exc=exc,
|
|
492
|
+
agent=agent,
|
|
493
|
+
thread_id=thread_id,
|
|
494
|
+
)
|
|
495
|
+
return LifecycleCommandResult(
|
|
496
|
+
status="error",
|
|
497
|
+
command=LifecycleCommand.COMPACT,
|
|
498
|
+
message=f"Failed to compact PMA history: {exc}",
|
|
499
|
+
error=str(exc),
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
def _generate_event_id(self) -> str:
|
|
503
|
+
"""Generate a unique event ID."""
|
|
504
|
+
import uuid
|
|
505
|
+
|
|
506
|
+
return f"{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S')}-{uuid.uuid4().hex[:8]}"
|
|
507
|
+
|
|
508
|
+
def _write_artifact(self, event_id: str, artifact: dict[str, Any]) -> Path:
|
|
509
|
+
"""Write a durable artifact to disk."""
|
|
510
|
+
artifact_path = self._artifacts_dir / f"{event_id}.json"
|
|
511
|
+
artifact_path.write_text(json.dumps(artifact, indent=2), encoding="utf-8")
|
|
512
|
+
return artifact_path
|
|
513
|
+
|
|
514
|
+
def _emit_event(self, event: dict[str, Any]) -> None:
|
|
515
|
+
"""Emit an event record to the lifecycle events log."""
|
|
516
|
+
try:
|
|
517
|
+
with open(self._events_log, "a", encoding="utf-8") as f:
|
|
518
|
+
f.write(json.dumps(event) + "\n")
|
|
519
|
+
except Exception:
|
|
520
|
+
logger.exception("Failed to emit lifecycle event")
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
__all__ = [
|
|
524
|
+
"LifecycleCommand",
|
|
525
|
+
"LifecycleCommandResult",
|
|
526
|
+
"PmaLifecycleRouter",
|
|
527
|
+
]
|