synapse-cli-agent 0.1.13__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 (131) hide show
  1. synapse/__init__.py +13 -0
  2. synapse/__main__.py +6 -0
  3. synapse/app/__init__.py +1 -0
  4. synapse/app/agent.py +492 -0
  5. synapse/app/agent_md.py +107 -0
  6. synapse/cli.py +750 -0
  7. synapse/commands/__init__.py +1 -0
  8. synapse/commands/compression.py +573 -0
  9. synapse/commands/helpers.py +22 -0
  10. synapse/commands/mcp.py +406 -0
  11. synapse/commands/model.py +173 -0
  12. synapse/commands/result.py +34 -0
  13. synapse/commands/sessions.py +443 -0
  14. synapse/commands/slash_cmds.py +521 -0
  15. synapse/commands/slash_complete.py +816 -0
  16. synapse/commands/theme.py +99 -0
  17. synapse/config.py +27 -0
  18. synapse/content/__init__.py +1 -0
  19. synapse/content/input_history.py +122 -0
  20. synapse/content/multimodal.py +733 -0
  21. synapse/content/prompts.py +249 -0
  22. synapse/content/skills_catalog.py +128 -0
  23. synapse/integrations/__init__.py +1 -0
  24. synapse/integrations/checkpoint_seed.py +281 -0
  25. synapse/integrations/codex_history.py +375 -0
  26. synapse/integrations/codex_import.py +393 -0
  27. synapse/integrations/codex_sessions.py +629 -0
  28. synapse/integrations/describe_image.py +370 -0
  29. synapse/integrations/http_clients.py +199 -0
  30. synapse/integrations/llm_openai_compat.py +90 -0
  31. synapse/integrations/llm_openai_websocket.py +187 -0
  32. synapse/integrations/mcp_client.py +646 -0
  33. synapse/integrations/vision_middleware.py +62 -0
  34. synapse/models/__init__.py +5 -0
  35. synapse/models/config.py +240 -0
  36. synapse/models/helpers.py +206 -0
  37. synapse/models/profile.py +59 -0
  38. synapse/models/registry.py +722 -0
  39. synapse/models_registry.py +7 -0
  40. synapse/observability/__init__.py +1 -0
  41. synapse/observability/startup_trace.py +127 -0
  42. synapse/runtime/__init__.py +1 -0
  43. synapse/runtime/async_runtime.py +176 -0
  44. synapse/runtime/backends.py +458 -0
  45. synapse/runtime/context_compact.py +249 -0
  46. synapse/runtime/execute_capture.py +48 -0
  47. synapse/runtime/fs_permissions.py +79 -0
  48. synapse/runtime/harness.py +57 -0
  49. synapse/runtime/hitl.py +197 -0
  50. synapse/runtime/interaction_ledger.py +82 -0
  51. synapse/runtime/middleware.py +802 -0
  52. synapse/runtime/model_request_compression_middleware.py +745 -0
  53. synapse/runtime/pathing.py +146 -0
  54. synapse/runtime/safety.py +184 -0
  55. synapse/runtime/steer.py +240 -0
  56. synapse/runtime/subagents.py +207 -0
  57. synapse/runtime/tool_ignore.py +221 -0
  58. synapse/runtime/tool_output_eval.py +118 -0
  59. synapse/runtime/tool_output_middleware.py +585 -0
  60. synapse/runtime/tool_output_usage_middleware.py +60 -0
  61. synapse/sessions/__init__.py +31 -0
  62. synapse/sessions/cancel_repair.py +208 -0
  63. synapse/sessions/session_recap.py +174 -0
  64. synapse/sessions/store.py +695 -0
  65. synapse/sessions/transcript.py +754 -0
  66. synapse/settings/__init__.py +5 -0
  67. synapse/settings/config_paths.py +184 -0
  68. synapse/settings/schema.py +464 -0
  69. synapse/tool_output/__init__.py +59 -0
  70. synapse/tool_output/detection.py +170 -0
  71. synapse/tool_output/metrics.py +32 -0
  72. synapse/tool_output/models.py +173 -0
  73. synapse/tool_output/pipeline.py +330 -0
  74. synapse/tool_output/repository.py +721 -0
  75. synapse/tool_output/transformers.py +648 -0
  76. synapse/tools/__init__.py +5 -0
  77. synapse/tools/session_tools.py +204 -0
  78. synapse/ui/__init__.py +10 -0
  79. synapse/ui/bottombar/__init__.py +73 -0
  80. synapse/ui/bottombar/components/__init__.py +143 -0
  81. synapse/ui/bottombar/components/key_hints.py +30 -0
  82. synapse/ui/bottombar/components/mcp.py +64 -0
  83. synapse/ui/bottombar/components/mode.py +24 -0
  84. synapse/ui/bottombar/components/model.py +28 -0
  85. synapse/ui/bottombar/components/thread.py +29 -0
  86. synapse/ui/bottombar/context.py +36 -0
  87. synapse/ui/bottombar/core.py +74 -0
  88. synapse/ui/dialogs/__init__.py +25 -0
  89. synapse/ui/dialogs/base.py +362 -0
  90. synapse/ui/dialogs/codex_session_list.py +84 -0
  91. synapse/ui/dialogs/compression_diagnostics.py +210 -0
  92. synapse/ui/dialogs/git_explore.py +702 -0
  93. synapse/ui/dialogs/mcp_panel.py +407 -0
  94. synapse/ui/dialogs/model_picker.py +128 -0
  95. synapse/ui/dialogs/safety_panel.py +63 -0
  96. synapse/ui/dialogs/session_list.py +98 -0
  97. synapse/ui/dialogs/theme_designer.py +863 -0
  98. synapse/ui/dialogs/theme_picker.py +113 -0
  99. synapse/ui/git_explore/__init__.py +31 -0
  100. synapse/ui/git_explore/engine.py +82 -0
  101. synapse/ui/git_explore/provider.py +242 -0
  102. synapse/ui/git_explore/unified.py +85 -0
  103. synapse/ui/rendering.py +350 -0
  104. synapse/ui/sink.py +70 -0
  105. synapse/ui/steer_widget.py +367 -0
  106. synapse/ui/stream.py +1207 -0
  107. synapse/ui/stream_events.py +421 -0
  108. synapse/ui/stream_runtime.py +252 -0
  109. synapse/ui/theme.py +1154 -0
  110. synapse/ui/timeline.py +621 -0
  111. synapse/ui/topbar/__init__.py +97 -0
  112. synapse/ui/topbar/components/__init__.py +150 -0
  113. synapse/ui/topbar/components/branch.py +41 -0
  114. synapse/ui/topbar/components/title.py +24 -0
  115. synapse/ui/topbar/components/tool_output.py +24 -0
  116. synapse/ui/topbar/components/usage.py +24 -0
  117. synapse/ui/topbar/components/workspace.py +32 -0
  118. synapse/ui/topbar/context.py +32 -0
  119. synapse/ui/topbar/core.py +979 -0
  120. synapse/ui/topbar/git_changes_popover.py +178 -0
  121. synapse/ui/topbar/git_chrome.py +475 -0
  122. synapse/ui/topbar/tool_output_popover.py +84 -0
  123. synapse/ui/topbar/widget.py +474 -0
  124. synapse/ui/tui.py +5717 -0
  125. synapse/ui/turn_rail.py +71 -0
  126. synapse/ui/user_turn.py +83 -0
  127. synapse/ui/welcome.py +261 -0
  128. synapse_cli_agent-0.1.13.dist-info/METADATA +412 -0
  129. synapse_cli_agent-0.1.13.dist-info/RECORD +131 -0
  130. synapse_cli_agent-0.1.13.dist-info/WHEEL +4 -0
  131. synapse_cli_agent-0.1.13.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,393 @@
1
+ """Idempotent, compensating import of safe Codex text snapshots.
2
+
3
+ The ledger intentionally lives beside Synapse session metadata rather than in
4
+ Codex state. It coordinates two separate SQLite databases: LangGraph
5
+ checkpoints and Synapse session metadata. It cannot make them atomic, so each
6
+ pending import carries a short lease and is reconciled by verifying the seeded
7
+ terminal checkpoint before marking the import complete.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ import sqlite3
15
+ import uuid
16
+ from dataclasses import dataclass
17
+ from datetime import UTC, datetime, timedelta
18
+ from pathlib import Path
19
+ from typing import TYPE_CHECKING, Any, Literal
20
+
21
+ from synapse.integrations.checkpoint_seed import CheckpointSeeder, CheckpointSeedError
22
+ from synapse.integrations.codex_history import PARSER_VERSION, PROJECTION_KIND
23
+ from synapse.sessions.store import SessionInfo, SessionStore, title_from_user_message
24
+
25
+ if TYPE_CHECKING:
26
+ from synapse.integrations.codex_history import CodexTextSnapshot
27
+
28
+ _LEASE_SECONDS = 120
29
+
30
+
31
+ class CodexImportError(RuntimeError):
32
+ """A Codex snapshot cannot be imported or recovered safely."""
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class CodexImportResult:
37
+ """One completed or idempotently reused imported Synapse session."""
38
+
39
+ thread_id: str
40
+ snapshot_digest: str
41
+ reused: bool
42
+ recovered: bool
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class _LedgerEntry:
47
+ source_id: str
48
+ snapshot_digest: str
49
+ thread_id: str
50
+ status: str
51
+ lease_until: str | None
52
+
53
+
54
+ class CodexImportLedger:
55
+ """Durable source-to-thread mapping with short leases for crash recovery."""
56
+
57
+ def __init__(self, path: Path | str) -> None:
58
+ self.path = Path(path).expanduser()
59
+ self.path.parent.mkdir(parents=True, exist_ok=True)
60
+ self._conn = sqlite3.connect(self.path, check_same_thread=False)
61
+ self._conn.row_factory = sqlite3.Row
62
+ self._init_db()
63
+
64
+ def close(self) -> None:
65
+ self._conn.close()
66
+
67
+ def claim(self, source_id: str, snapshot_digest: str, thread_id: str) -> Literal[
68
+ "new", "completed", "recover"
69
+ ]:
70
+ """Claim a source, reuse completion, or take over an expired lease."""
71
+ now = _utcnow()
72
+ lease_until = _lease_expiry()
73
+ self._conn.execute("BEGIN IMMEDIATE")
74
+ try:
75
+ row = self._conn.execute(
76
+ "SELECT * FROM codex_imports WHERE source_id = ?", (source_id,)
77
+ ).fetchone()
78
+ if row is None:
79
+ self._conn.execute(
80
+ """
81
+ INSERT INTO codex_imports(
82
+ source_id, snapshot_digest, thread_id, status, lease_until,
83
+ created_at, updated_at
84
+ ) VALUES (?, ?, ?, 'pending', ?, ?, ?)
85
+ """,
86
+ (source_id, snapshot_digest, thread_id, lease_until, now, now),
87
+ )
88
+ self._conn.commit()
89
+ return "new"
90
+
91
+ entry = _entry_from_row(row)
92
+ if entry.snapshot_digest != snapshot_digest:
93
+ raise CodexImportError("Codex source changed after its first import attempt")
94
+ if entry.status == "completed":
95
+ self._conn.commit()
96
+ return "completed"
97
+ if entry.status != "pending":
98
+ raise CodexImportError("Codex import ledger has an invalid state")
99
+ if not _lease_is_expired(entry.lease_until):
100
+ raise CodexImportError("Codex import is already in progress")
101
+ self._conn.execute(
102
+ "UPDATE codex_imports SET lease_until = ?, updated_at = ? WHERE source_id = ?",
103
+ (lease_until, now, source_id),
104
+ )
105
+ self._conn.commit()
106
+ return "recover"
107
+ except Exception:
108
+ self._conn.rollback()
109
+ raise
110
+
111
+ def entry(self, source_id: str) -> _LedgerEntry | None:
112
+ row = self._conn.execute(
113
+ "SELECT * FROM codex_imports WHERE source_id = ?", (source_id,)
114
+ ).fetchone()
115
+ return _entry_from_row(row) if row is not None else None
116
+
117
+ def complete(self, source_id: str, thread_id: str) -> None:
118
+ now = _utcnow()
119
+ updated = self._conn.execute(
120
+ """
121
+ UPDATE codex_imports
122
+ SET status = 'completed', lease_until = NULL, updated_at = ?
123
+ WHERE source_id = ? AND thread_id = ? AND status = 'pending'
124
+ """,
125
+ (now, source_id, thread_id),
126
+ )
127
+ self._conn.commit()
128
+ if updated.rowcount != 1:
129
+ raise CodexImportError("Codex import ledger completion was lost")
130
+
131
+ def abandon(self, source_id: str, thread_id: str) -> None:
132
+ """Remove an uncompleted ledger row after confirmed compensation."""
133
+ self._conn.execute(
134
+ """
135
+ DELETE FROM codex_imports
136
+ WHERE source_id = ? AND thread_id = ? AND status = 'pending'
137
+ """,
138
+ (source_id, thread_id),
139
+ )
140
+ self._conn.commit()
141
+
142
+ def _init_db(self) -> None:
143
+ self._conn.execute(
144
+ """
145
+ CREATE TABLE IF NOT EXISTS codex_imports (
146
+ source_id TEXT PRIMARY KEY,
147
+ snapshot_digest TEXT NOT NULL,
148
+ thread_id TEXT NOT NULL UNIQUE,
149
+ status TEXT NOT NULL CHECK(status IN ('pending', 'completed')),
150
+ lease_until TEXT,
151
+ created_at TEXT NOT NULL,
152
+ updated_at TEXT NOT NULL
153
+ )
154
+ """
155
+ )
156
+ self._conn.commit()
157
+
158
+
159
+ def import_codex_session(
160
+ *,
161
+ native_id: str,
162
+ settings: Any,
163
+ agent: Any,
164
+ workspace: Path | None = None,
165
+ codex_home: Path | None = None,
166
+ ) -> CodexImportResult:
167
+ """Discover, project, and import one Codex session through one agent graph."""
168
+ from synapse.integrations.codex_history import CodexHistoryProjector
169
+ from synapse.integrations.codex_sessions import CodexSessionScanner
170
+
171
+ target_workspace = workspace or Path(getattr(settings, "workspace", Path.cwd()))
172
+ session = CodexSessionScanner(codex_home).inspect(
173
+ native_id,
174
+ workspace=target_workspace,
175
+ include_rollout_fallback=True,
176
+ )
177
+ if session is None:
178
+ raise CodexImportError(f"Codex session not found: {native_id}")
179
+ snapshot = CodexHistoryProjector().project_path(session.rollout_path)
180
+ if not snapshot.importable:
181
+ warning_codes = ",".join(warning.code for warning in snapshot.warnings)
182
+ raise CodexImportError(
183
+ f"Codex session cannot be imported safely: {warning_codes or 'unknown'}"
184
+ )
185
+ if not snapshot.messages:
186
+ raise CodexImportError("Codex session cannot be imported safely: no_visible_messages")
187
+
188
+ title = next(
189
+ (message.text for message in snapshot.messages if message.role == "user"),
190
+ session.title,
191
+ )
192
+
193
+ sessions_path = settings.resolved_sessions_path()
194
+ store = SessionStore(sessions_path)
195
+ ledger = CodexImportLedger(default_codex_import_ledger_path(sessions_path))
196
+ try:
197
+ service = CodexImportService(
198
+ seeder=CheckpointSeeder(agent),
199
+ sessions=store,
200
+ ledger=ledger,
201
+ )
202
+ return service.import_snapshot(
203
+ native_id=session.native_id,
204
+ snapshot=snapshot,
205
+ title=title,
206
+ )
207
+ finally:
208
+ ledger.close()
209
+ store.close()
210
+
211
+
212
+ class CodexImportService:
213
+ """Coordinate immutable snapshot import across checkpoint and metadata stores."""
214
+
215
+ def __init__(
216
+ self,
217
+ *,
218
+ seeder: CheckpointSeeder,
219
+ sessions: SessionStore,
220
+ ledger: CodexImportLedger,
221
+ ) -> None:
222
+ self._seeder = seeder
223
+ self._sessions = sessions
224
+ self._ledger = ledger
225
+
226
+ def import_snapshot(
227
+ self,
228
+ *,
229
+ native_id: str,
230
+ snapshot: CodexTextSnapshot,
231
+ title: str,
232
+ ) -> CodexImportResult:
233
+ """Create or recover exactly one Synapse session for one Codex source."""
234
+ if not snapshot.messages:
235
+ raise CodexImportError("Codex session cannot be imported safely: no_visible_messages")
236
+ digest = snapshot_digest(snapshot)
237
+ source_id = _source_id(native_id)
238
+ proposed_thread_id = f"codex-{uuid.uuid4().hex[:20]}"
239
+ claim = self._ledger.claim(source_id, digest, proposed_thread_id)
240
+ entry = self._ledger.entry(source_id)
241
+ if entry is None:
242
+ raise CodexImportError("Codex import ledger claim disappeared")
243
+ if claim == "completed":
244
+ self._verify_completed(entry, snapshot)
245
+ return CodexImportResult(entry.thread_id, digest, reused=True, recovered=False)
246
+ if claim == "recover":
247
+ return self._recover(entry, snapshot, title, digest)
248
+ return self._seed_new(entry, snapshot, title, digest)
249
+
250
+ def _seed_new(
251
+ self,
252
+ entry: _LedgerEntry,
253
+ snapshot: CodexTextSnapshot,
254
+ title: str,
255
+ digest: str,
256
+ ) -> CodexImportResult:
257
+ try:
258
+ self._seeder.seed_snapshot(entry.thread_id, snapshot)
259
+ self._ensure_session(entry.thread_id, title)
260
+ self._ledger.complete(entry.source_id, entry.thread_id)
261
+ except Exception as exc:
262
+ self._compensate_new(entry, exc)
263
+ if isinstance(exc, CodexImportError):
264
+ raise
265
+ raise CodexImportError("Codex snapshot import failed") from exc
266
+ return CodexImportResult(entry.thread_id, digest, reused=False, recovered=False)
267
+
268
+ def _recover(
269
+ self,
270
+ entry: _LedgerEntry,
271
+ snapshot: CodexTextSnapshot,
272
+ title: str,
273
+ digest: str,
274
+ ) -> CodexImportResult:
275
+ try:
276
+ self._seeder.verify_snapshot(entry.thread_id, snapshot)
277
+ except CheckpointSeedError as exc:
278
+ if self._thread_exists(entry.thread_id):
279
+ raise CodexImportError(
280
+ "pending import checkpoint does not match its immutable snapshot"
281
+ ) from exc
282
+ self._sessions.delete(entry.thread_id)
283
+ try:
284
+ self._seeder.seed_snapshot(entry.thread_id, snapshot)
285
+ except Exception as seed_error:
286
+ self._ledger.abandon(entry.source_id, entry.thread_id)
287
+ raise CodexImportError("pending import could not be reseeded") from seed_error
288
+ try:
289
+ self._ensure_session(entry.thread_id, title)
290
+ self._ledger.complete(entry.source_id, entry.thread_id)
291
+ except Exception as exc:
292
+ raise CodexImportError("pending import could not be reconciled") from exc
293
+ return CodexImportResult(entry.thread_id, digest, reused=False, recovered=True)
294
+
295
+ def _verify_completed(self, entry: _LedgerEntry, snapshot: CodexTextSnapshot) -> None:
296
+ try:
297
+ self._seeder.verify_snapshot(entry.thread_id, snapshot)
298
+ except CheckpointSeedError as exc:
299
+ raise CodexImportError(
300
+ "completed import checkpoint no longer matches its snapshot"
301
+ ) from exc
302
+ if self._sessions.get(entry.thread_id) is None:
303
+ raise CodexImportError("completed import session metadata is missing")
304
+
305
+ def _ensure_session(self, thread_id: str, title: str) -> SessionInfo:
306
+ safe_title = title_from_user_message(title) or "Imported Codex session"
307
+ return self._sessions.ensure(thread_id, title=safe_title)
308
+
309
+ def _thread_exists(self, thread_id: str) -> bool:
310
+ return self._seeder.has_thread(thread_id)
311
+
312
+ def _compensate_new(self, entry: _LedgerEntry, cause: Exception) -> None:
313
+ cleanup_error: Exception | None = None
314
+ try:
315
+ self._sessions.delete(entry.thread_id)
316
+ self._seeder.delete_thread(entry.thread_id)
317
+ self._ledger.abandon(entry.source_id, entry.thread_id)
318
+ except Exception as exc: # noqa: BLE001
319
+ cleanup_error = exc
320
+ if cleanup_error is not None:
321
+ raise CodexImportError(
322
+ "Codex import failed and compensation was incomplete"
323
+ ) from cleanup_error
324
+
325
+
326
+ def default_codex_import_ledger_path(sessions_path: Path | str) -> Path:
327
+ """Place the ledger next to project-local Synapse session metadata."""
328
+ return Path(sessions_path).expanduser().resolve().parent / "codex-imports.sqlite"
329
+
330
+
331
+ def snapshot_digest(snapshot: CodexTextSnapshot) -> str:
332
+ """Hash the immutable supported visible-text projection, never raw rollout bytes."""
333
+ if (
334
+ not snapshot.importable
335
+ or snapshot.projection_kind != PROJECTION_KIND
336
+ or snapshot.parser_version != PARSER_VERSION
337
+ ):
338
+ raise CodexImportError("Codex snapshot is not importable under the current contract")
339
+ payload = {
340
+ "projection_kind": snapshot.projection_kind,
341
+ "parser_version": snapshot.parser_version,
342
+ "messages": [
343
+ {
344
+ "source_id": message.source_id,
345
+ "turn_id": message.turn_id,
346
+ "role": message.role,
347
+ "text": message.text,
348
+ }
349
+ for message in snapshot.messages
350
+ ],
351
+ }
352
+ encoded = json.dumps(
353
+ payload,
354
+ ensure_ascii=True,
355
+ separators=(",", ":"),
356
+ sort_keys=True,
357
+ ).encode()
358
+ return hashlib.sha256(encoded).hexdigest()
359
+
360
+
361
+ def _source_id(native_id: str) -> str:
362
+ value = native_id.strip()
363
+ if not value or len(value) > 200 or "\x00" in value:
364
+ raise CodexImportError("Codex native session id is invalid")
365
+ return f"codex:{value.casefold()}"
366
+
367
+
368
+ def _entry_from_row(row: sqlite3.Row) -> _LedgerEntry:
369
+ return _LedgerEntry(
370
+ source_id=str(row["source_id"]),
371
+ snapshot_digest=str(row["snapshot_digest"]),
372
+ thread_id=str(row["thread_id"]),
373
+ status=str(row["status"]),
374
+ lease_until=str(row["lease_until"]) if row["lease_until"] is not None else None,
375
+ )
376
+
377
+
378
+ def _utcnow() -> str:
379
+ return datetime.now(UTC).replace(microsecond=0).isoformat()
380
+
381
+
382
+ def _lease_expiry() -> str:
383
+ expiry = datetime.now(UTC) + timedelta(seconds=_LEASE_SECONDS)
384
+ return expiry.replace(microsecond=0).isoformat()
385
+
386
+
387
+ def _lease_is_expired(value: str | None) -> bool:
388
+ if value is None:
389
+ return True
390
+ try:
391
+ return datetime.fromisoformat(value) <= datetime.now(UTC)
392
+ except ValueError:
393
+ return False