codex-autorunner 1.1.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.
Files changed (127) hide show
  1. codex_autorunner/agents/opencode/client.py +113 -4
  2. codex_autorunner/agents/opencode/supervisor.py +4 -0
  3. codex_autorunner/agents/registry.py +17 -7
  4. codex_autorunner/bootstrap.py +219 -1
  5. codex_autorunner/core/__init__.py +17 -1
  6. codex_autorunner/core/about_car.py +114 -1
  7. codex_autorunner/core/app_server_threads.py +6 -0
  8. codex_autorunner/core/config.py +236 -1
  9. codex_autorunner/core/context_awareness.py +38 -0
  10. codex_autorunner/core/docs.py +0 -122
  11. codex_autorunner/core/filebox.py +265 -0
  12. codex_autorunner/core/flows/controller.py +71 -1
  13. codex_autorunner/core/flows/reconciler.py +4 -1
  14. codex_autorunner/core/flows/runtime.py +22 -0
  15. codex_autorunner/core/flows/store.py +61 -9
  16. codex_autorunner/core/flows/transition.py +23 -16
  17. codex_autorunner/core/flows/ux_helpers.py +18 -3
  18. codex_autorunner/core/flows/worker_process.py +32 -6
  19. codex_autorunner/core/hub.py +198 -41
  20. codex_autorunner/core/lifecycle_events.py +253 -0
  21. codex_autorunner/core/path_utils.py +2 -1
  22. codex_autorunner/core/pma_audit.py +224 -0
  23. codex_autorunner/core/pma_context.py +496 -0
  24. codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
  25. codex_autorunner/core/pma_lifecycle.py +527 -0
  26. codex_autorunner/core/pma_queue.py +367 -0
  27. codex_autorunner/core/pma_safety.py +221 -0
  28. codex_autorunner/core/pma_state.py +115 -0
  29. codex_autorunner/core/ports/agent_backend.py +2 -5
  30. codex_autorunner/core/ports/run_event.py +1 -4
  31. codex_autorunner/core/prompt.py +0 -80
  32. codex_autorunner/core/prompts.py +56 -172
  33. codex_autorunner/core/redaction.py +0 -4
  34. codex_autorunner/core/review_context.py +11 -9
  35. codex_autorunner/core/runner_controller.py +35 -33
  36. codex_autorunner/core/runner_state.py +147 -0
  37. codex_autorunner/core/runtime.py +829 -0
  38. codex_autorunner/core/sqlite_utils.py +13 -4
  39. codex_autorunner/core/state.py +7 -10
  40. codex_autorunner/core/state_roots.py +5 -0
  41. codex_autorunner/core/templates/__init__.py +39 -0
  42. codex_autorunner/core/templates/git_mirror.py +234 -0
  43. codex_autorunner/core/templates/provenance.py +56 -0
  44. codex_autorunner/core/templates/scan_cache.py +120 -0
  45. codex_autorunner/core/ticket_linter_cli.py +17 -0
  46. codex_autorunner/core/ticket_manager_cli.py +154 -92
  47. codex_autorunner/core/time_utils.py +11 -0
  48. codex_autorunner/core/types.py +18 -0
  49. codex_autorunner/core/utils.py +34 -6
  50. codex_autorunner/flows/review/service.py +23 -25
  51. codex_autorunner/flows/ticket_flow/definition.py +43 -1
  52. codex_autorunner/integrations/agents/__init__.py +2 -0
  53. codex_autorunner/integrations/agents/backend_orchestrator.py +18 -0
  54. codex_autorunner/integrations/agents/codex_backend.py +19 -8
  55. codex_autorunner/integrations/agents/runner.py +3 -8
  56. codex_autorunner/integrations/agents/wiring.py +8 -0
  57. codex_autorunner/integrations/telegram/doctor.py +228 -6
  58. codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
  59. codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
  60. codex_autorunner/integrations/telegram/handlers/commands/flows.py +346 -58
  61. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
  62. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +202 -45
  63. codex_autorunner/integrations/telegram/handlers/commands_spec.py +18 -7
  64. codex_autorunner/integrations/telegram/handlers/messages.py +26 -1
  65. codex_autorunner/integrations/telegram/helpers.py +1 -3
  66. codex_autorunner/integrations/telegram/runtime.py +9 -4
  67. codex_autorunner/integrations/telegram/service.py +30 -0
  68. codex_autorunner/integrations/telegram/state.py +38 -0
  69. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +10 -4
  70. codex_autorunner/integrations/telegram/transport.py +10 -3
  71. codex_autorunner/integrations/templates/__init__.py +27 -0
  72. codex_autorunner/integrations/templates/scan_agent.py +312 -0
  73. codex_autorunner/server.py +2 -2
  74. codex_autorunner/static/agentControls.js +21 -5
  75. codex_autorunner/static/app.js +115 -11
  76. codex_autorunner/static/chatUploads.js +137 -0
  77. codex_autorunner/static/docChatCore.js +185 -13
  78. codex_autorunner/static/fileChat.js +68 -40
  79. codex_autorunner/static/fileboxUi.js +159 -0
  80. codex_autorunner/static/hub.js +46 -81
  81. codex_autorunner/static/index.html +303 -24
  82. codex_autorunner/static/messages.js +82 -4
  83. codex_autorunner/static/notifications.js +255 -0
  84. codex_autorunner/static/pma.js +1167 -0
  85. codex_autorunner/static/settings.js +3 -0
  86. codex_autorunner/static/streamUtils.js +57 -0
  87. codex_autorunner/static/styles.css +9125 -6742
  88. codex_autorunner/static/templateReposSettings.js +225 -0
  89. codex_autorunner/static/ticketChatActions.js +165 -3
  90. codex_autorunner/static/ticketChatStream.js +17 -119
  91. codex_autorunner/static/ticketEditor.js +41 -13
  92. codex_autorunner/static/ticketTemplates.js +798 -0
  93. codex_autorunner/static/tickets.js +69 -19
  94. codex_autorunner/static/turnEvents.js +27 -0
  95. codex_autorunner/static/turnResume.js +33 -0
  96. codex_autorunner/static/utils.js +28 -0
  97. codex_autorunner/static/workspace.js +258 -44
  98. codex_autorunner/static/workspaceFileBrowser.js +6 -4
  99. codex_autorunner/surfaces/cli/cli.py +1465 -155
  100. codex_autorunner/surfaces/cli/pma_cli.py +817 -0
  101. codex_autorunner/surfaces/web/app.py +253 -49
  102. codex_autorunner/surfaces/web/routes/__init__.py +4 -0
  103. codex_autorunner/surfaces/web/routes/analytics.py +29 -22
  104. codex_autorunner/surfaces/web/routes/file_chat.py +317 -36
  105. codex_autorunner/surfaces/web/routes/filebox.py +227 -0
  106. codex_autorunner/surfaces/web/routes/flows.py +219 -29
  107. codex_autorunner/surfaces/web/routes/messages.py +70 -39
  108. codex_autorunner/surfaces/web/routes/pma.py +1652 -0
  109. codex_autorunner/surfaces/web/routes/repos.py +1 -1
  110. codex_autorunner/surfaces/web/routes/shared.py +0 -3
  111. codex_autorunner/surfaces/web/routes/templates.py +634 -0
  112. codex_autorunner/surfaces/web/runner_manager.py +2 -2
  113. codex_autorunner/surfaces/web/schemas.py +70 -18
  114. codex_autorunner/tickets/agent_pool.py +27 -0
  115. codex_autorunner/tickets/files.py +33 -16
  116. codex_autorunner/tickets/lint.py +50 -0
  117. codex_autorunner/tickets/models.py +3 -0
  118. codex_autorunner/tickets/outbox.py +41 -5
  119. codex_autorunner/tickets/runner.py +350 -69
  120. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/METADATA +15 -19
  121. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/RECORD +125 -94
  122. codex_autorunner/core/adapter_utils.py +0 -21
  123. codex_autorunner/core/engine.py +0 -3302
  124. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
  125. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
  126. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
  127. {codex_autorunner-1.1.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
+ ]