sightmesh 0.13.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 (52) hide show
  1. sightmesh/__init__.py +48 -0
  2. sightmesh/approvals.py +249 -0
  3. sightmesh/bridge.py +368 -0
  4. sightmesh/cdesktop.py +1020 -0
  5. sightmesh/cli/__init__.py +128 -0
  6. sightmesh/cli/approvals_commands.py +522 -0
  7. sightmesh/cli/bridge_commands.py +79 -0
  8. sightmesh/cli/common.py +243 -0
  9. sightmesh/cli/diagnostics.py +656 -0
  10. sightmesh/cli/fleet.py +284 -0
  11. sightmesh/cli/messaging.py +393 -0
  12. sightmesh/cli/pool_commands.py +521 -0
  13. sightmesh/cli/routing_commands.py +315 -0
  14. sightmesh/cli/service_updates.py +140 -0
  15. sightmesh/cli/spawn.py +617 -0
  16. sightmesh/cli/tasks.py +248 -0
  17. sightmesh/cli/workspaces.py +467 -0
  18. sightmesh/conductor_migrate.py +802 -0
  19. sightmesh/durable.py +1017 -0
  20. sightmesh/effects.py +558 -0
  21. sightmesh/escalation.py +1189 -0
  22. sightmesh/execution_routing.py +744 -0
  23. sightmesh/external_runs.py +447 -0
  24. sightmesh/fence.py +26 -0
  25. sightmesh/fleet.py +626 -0
  26. sightmesh/leases.py +511 -0
  27. sightmesh/liveness.py +835 -0
  28. sightmesh/migration.py +220 -0
  29. sightmesh/observability.py +288 -0
  30. sightmesh/pool/__init__.py +79 -0
  31. sightmesh/pool/core.py +673 -0
  32. sightmesh/pool/server.py +355 -0
  33. sightmesh/pool/ui.html +414 -0
  34. sightmesh/profiles.py +160 -0
  35. sightmesh/repowire.py +52 -0
  36. sightmesh/routing.py +82 -0
  37. sightmesh/runtime-lock.json +16 -0
  38. sightmesh/runtime_lock.py +171 -0
  39. sightmesh/sdk.py +1336 -0
  40. sightmesh/service.py +461 -0
  41. sightmesh/service_process.py +146 -0
  42. sightmesh/stall_settings.py +30 -0
  43. sightmesh/stalls.py +44 -0
  44. sightmesh/succession.py +610 -0
  45. sightmesh/task_store.py +1159 -0
  46. sightmesh/updates.py +716 -0
  47. sightmesh/wakes.py +604 -0
  48. sightmesh-0.13.0.dist-info/METADATA +165 -0
  49. sightmesh-0.13.0.dist-info/RECORD +52 -0
  50. sightmesh-0.13.0.dist-info/WHEEL +4 -0
  51. sightmesh-0.13.0.dist-info/entry_points.txt +2 -0
  52. sightmesh-0.13.0.dist-info/licenses/LICENSE +201 -0
sightmesh/__init__.py ADDED
@@ -0,0 +1,48 @@
1
+ """Local orchestration for visible Claude and Codex workers."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ _SDK_EXPORTS = frozenset(
7
+ {
8
+ "BatchResult",
9
+ "Command",
10
+ "SightMesh",
11
+ "SightMeshError",
12
+ "Worker",
13
+ "WorkerSpec",
14
+ }
15
+ )
16
+
17
+ if TYPE_CHECKING:
18
+ from .sdk import BatchResult, Command, SightMesh, SightMeshError, Worker, WorkerSpec
19
+
20
+ try:
21
+ __version__ = version("sightmesh")
22
+ except PackageNotFoundError:
23
+ __version__ = "0+unknown"
24
+
25
+ __all__ = [
26
+ "BatchResult",
27
+ "Command",
28
+ "SightMesh",
29
+ "SightMeshError",
30
+ "Worker",
31
+ "WorkerSpec",
32
+ "__version__",
33
+ ]
34
+
35
+
36
+ def __getattr__(name: str) -> Any:
37
+ """Load the SDK only for the package-root convenience exports."""
38
+ if name not in _SDK_EXPORTS:
39
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
40
+ from . import sdk
41
+
42
+ value = getattr(sdk, name)
43
+ globals()[name] = value
44
+ return value
45
+
46
+
47
+ def __dir__() -> list[str]:
48
+ return sorted(set(globals()) | _SDK_EXPORTS)
sightmesh/approvals.py ADDED
@@ -0,0 +1,249 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import os
5
+ import sqlite3
6
+ import time
7
+ import uuid
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from .service import state_dir
13
+
14
+
15
+ class ApprovalAuditError(RuntimeError):
16
+ pass
17
+
18
+
19
+ def approval_db_path() -> Path:
20
+ return state_dir() / "approvals.sqlite3"
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class ApprovalDecision:
25
+ decision_id: str
26
+ approval_id: str
27
+ execution_process_id: str
28
+ session_id: str | None
29
+ workspace_id: str | None
30
+ tool_name: str
31
+ decision: str
32
+ reviewer_kind: str
33
+ reviewer_id: str
34
+ reason_sha256: str | None
35
+ status: str
36
+ error: str | None
37
+ created_at: float
38
+ completed_at: float | None
39
+
40
+ def to_dict(self) -> dict[str, Any]:
41
+ return {
42
+ "decision_id": self.decision_id,
43
+ "approval_id": self.approval_id,
44
+ "execution_process_id": self.execution_process_id,
45
+ "session_id": self.session_id,
46
+ "workspace_id": self.workspace_id,
47
+ "tool_name": self.tool_name,
48
+ "decision": self.decision,
49
+ "reviewer_kind": self.reviewer_kind,
50
+ "reviewer_id": self.reviewer_id,
51
+ "reason_sha256": self.reason_sha256,
52
+ "status": self.status,
53
+ "error": self.error,
54
+ "created_at": self.created_at,
55
+ "completed_at": self.completed_at,
56
+ }
57
+
58
+
59
+ class ApprovalAuditStore:
60
+ def __init__(self, path: Path | None = None) -> None:
61
+ self.path = path or approval_db_path()
62
+ self.path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
63
+ self.path.parent.chmod(0o700)
64
+ self._initialize()
65
+
66
+ def _connect(self) -> sqlite3.Connection:
67
+ try:
68
+ conn = sqlite3.connect(self.path, timeout=30, isolation_level=None)
69
+ conn.row_factory = sqlite3.Row
70
+ conn.execute("PRAGMA busy_timeout = 30000")
71
+ conn.execute("PRAGMA journal_mode = WAL")
72
+ for path in (
73
+ self.path,
74
+ self.path.with_name(f"{self.path.name}-wal"),
75
+ self.path.with_name(f"{self.path.name}-shm"),
76
+ ):
77
+ if path.exists():
78
+ os.chmod(path, 0o600)
79
+ return conn
80
+ except sqlite3.Error as exc:
81
+ raise ApprovalAuditError(
82
+ f"Cannot open approval audit store {self.path}: {exc}"
83
+ ) from exc
84
+
85
+ def _initialize(self) -> None:
86
+ try:
87
+ with self._connect() as conn:
88
+ conn.execute(
89
+ """
90
+ CREATE TABLE IF NOT EXISTS approval_decisions (
91
+ decision_id TEXT PRIMARY KEY,
92
+ approval_id TEXT NOT NULL,
93
+ execution_process_id TEXT NOT NULL,
94
+ session_id TEXT,
95
+ workspace_id TEXT,
96
+ tool_name TEXT NOT NULL,
97
+ decision TEXT NOT NULL
98
+ CHECK (decision IN ('approved', 'denied')),
99
+ reviewer_kind TEXT NOT NULL
100
+ CHECK (reviewer_kind IN ('human', 'session')),
101
+ reviewer_id TEXT NOT NULL,
102
+ reason_sha256 TEXT,
103
+ status TEXT NOT NULL
104
+ CHECK (status IN ('submitting', 'responded', 'failed')),
105
+ error TEXT,
106
+ created_at REAL NOT NULL,
107
+ completed_at REAL
108
+ )
109
+ """
110
+ )
111
+ conn.execute(
112
+ "CREATE INDEX IF NOT EXISTS idx_approval_decisions_created "
113
+ "ON approval_decisions(created_at DESC)"
114
+ )
115
+ conn.execute(
116
+ "CREATE INDEX IF NOT EXISTS idx_approval_decisions_approval "
117
+ "ON approval_decisions(approval_id, created_at DESC)"
118
+ )
119
+ except sqlite3.DatabaseError as exc:
120
+ raise ApprovalAuditError(
121
+ f"Cannot initialize approval audit store {self.path}: {exc}"
122
+ ) from exc
123
+
124
+ def begin(
125
+ self,
126
+ *,
127
+ approval: dict[str, Any],
128
+ decision: str,
129
+ reviewer_kind: str,
130
+ reviewer_id: str,
131
+ reason: str | None,
132
+ ) -> ApprovalDecision:
133
+ now = time.time()
134
+ record = ApprovalDecision(
135
+ decision_id=str(uuid.uuid4()),
136
+ approval_id=str(approval["approval_id"]),
137
+ execution_process_id=str(approval["execution_process_id"]),
138
+ session_id=_optional_string(approval.get("session_id")),
139
+ workspace_id=_optional_string(approval.get("workspace_id")),
140
+ tool_name=str(approval["tool_name"]),
141
+ decision=decision,
142
+ reviewer_kind=reviewer_kind,
143
+ reviewer_id=reviewer_id,
144
+ reason_sha256=(
145
+ hashlib.sha256(reason.encode("utf-8")).hexdigest() if reason else None
146
+ ),
147
+ status="submitting",
148
+ error=None,
149
+ created_at=now,
150
+ completed_at=None,
151
+ )
152
+ try:
153
+ with self._connect() as conn:
154
+ conn.execute(
155
+ """
156
+ INSERT INTO approval_decisions (
157
+ decision_id, approval_id, execution_process_id, session_id,
158
+ workspace_id, tool_name, decision, reviewer_kind,
159
+ reviewer_id, reason_sha256, status, error, created_at,
160
+ completed_at
161
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
162
+ """,
163
+ (
164
+ record.decision_id,
165
+ record.approval_id,
166
+ record.execution_process_id,
167
+ record.session_id,
168
+ record.workspace_id,
169
+ record.tool_name,
170
+ record.decision,
171
+ record.reviewer_kind,
172
+ record.reviewer_id,
173
+ record.reason_sha256,
174
+ record.status,
175
+ record.error,
176
+ record.created_at,
177
+ record.completed_at,
178
+ ),
179
+ )
180
+ except sqlite3.DatabaseError as exc:
181
+ raise ApprovalAuditError(f"Cannot record approval attempt: {exc}") from exc
182
+ return record
183
+
184
+ def finish(
185
+ self, decision_id: str, *, succeeded: bool, error: str | None = None
186
+ ) -> ApprovalDecision:
187
+ status = "responded" if succeeded else "failed"
188
+ completed_at = time.time()
189
+ try:
190
+ with self._connect() as conn:
191
+ cursor = conn.execute(
192
+ """
193
+ UPDATE approval_decisions
194
+ SET status = ?, error = ?, completed_at = ?
195
+ WHERE decision_id = ? AND status = 'submitting'
196
+ """,
197
+ (status, error, completed_at, decision_id),
198
+ )
199
+ if cursor.rowcount != 1:
200
+ raise ApprovalAuditError(
201
+ f"Approval audit attempt is missing or already finished: {decision_id}"
202
+ )
203
+ row = conn.execute(
204
+ "SELECT * FROM approval_decisions WHERE decision_id = ?",
205
+ (decision_id,),
206
+ ).fetchone()
207
+ except sqlite3.DatabaseError as exc:
208
+ raise ApprovalAuditError(f"Cannot finish approval audit: {exc}") from exc
209
+ if row is None:
210
+ raise ApprovalAuditError(
211
+ f"Approval audit attempt is missing: {decision_id}"
212
+ )
213
+ return _from_row(row)
214
+
215
+ def history(self, *, limit: int = 50) -> list[ApprovalDecision]:
216
+ if limit < 1 or limit > 500:
217
+ raise ValueError("Approval history limit must be between 1 and 500")
218
+ try:
219
+ with self._connect() as conn:
220
+ rows = conn.execute(
221
+ "SELECT * FROM approval_decisions ORDER BY created_at DESC LIMIT ?",
222
+ (limit,),
223
+ ).fetchall()
224
+ except sqlite3.DatabaseError as exc:
225
+ raise ApprovalAuditError(f"Cannot read approval history: {exc}") from exc
226
+ return [_from_row(row) for row in rows]
227
+
228
+
229
+ def _optional_string(value: Any) -> str | None:
230
+ return str(value) if value is not None else None
231
+
232
+
233
+ def _from_row(row: sqlite3.Row) -> ApprovalDecision:
234
+ return ApprovalDecision(
235
+ decision_id=row["decision_id"],
236
+ approval_id=row["approval_id"],
237
+ execution_process_id=row["execution_process_id"],
238
+ session_id=row["session_id"],
239
+ workspace_id=row["workspace_id"],
240
+ tool_name=row["tool_name"],
241
+ decision=row["decision"],
242
+ reviewer_kind=row["reviewer_kind"],
243
+ reviewer_id=row["reviewer_id"],
244
+ reason_sha256=row["reason_sha256"],
245
+ status=row["status"],
246
+ error=row["error"],
247
+ created_at=row["created_at"],
248
+ completed_at=row["completed_at"],
249
+ )
sightmesh/bridge.py ADDED
@@ -0,0 +1,368 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import hashlib
5
+ import json
6
+ import logging
7
+ import os
8
+ import re
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import websockets
14
+
15
+ from . import leases
16
+ from .cdesktop import CdesktopClient, CdesktopError
17
+ from .durable import DurableExecutionReconciler
18
+ from .execution_routing import ExecutionRoutingError
19
+ from .fence import open_transport
20
+ from .pool.core import PoolError
21
+ from .routing import (
22
+ clear_peer_identity,
23
+ enabled_workspaces,
24
+ peer_identity,
25
+ set_peer_identity,
26
+ )
27
+ from .sdk import SightMesh, SightMeshError
28
+ from .succession import SuccessionError
29
+ from .task_store import TaskStoreError
30
+
31
+ LOGGER = logging.getLogger("sightmesh.bridge")
32
+
33
+
34
+ def _peer_name(workspace: dict[str, Any], session: dict[str, Any]) -> str:
35
+ raw = f"cd-{workspace.get('name') or 'workspace'}-{session['id'][:6]}"
36
+ normalized = re.sub(r"[^A-Za-z0-9_.-]+", "-", raw).strip("-.")
37
+ normalized = re.sub(r"-+", "-", normalized)
38
+ return normalized[:48] or f"cd-session-{session['id'][:6]}"
39
+
40
+
41
+ _BACKENDS = {"CLAUDE_CODE": "claude-code", "CODEX": "codex", "OPENCODE": "opencode"}
42
+
43
+
44
+ def _backend(executor: str | None) -> str:
45
+ return _BACKENDS.get(executor or "", "codex")
46
+
47
+
48
+ def _repo_path(client: CdesktopClient, workspace: dict[str, Any]) -> str:
49
+ repos = client.workspace_repos(workspace["id"])
50
+ if not repos:
51
+ raise CdesktopError(f"Workspace {workspace['id']} has no repository")
52
+ repo = repos[0]
53
+ if workspace.get("use_worktree"):
54
+ container = workspace.get("container_ref")
55
+ if not container:
56
+ raise CdesktopError(f"Workspace {workspace['id']} has no container path")
57
+ return str(Path(container) / repo["name"])
58
+ return str(Path(repo["path"]).expanduser().resolve())
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class BridgedSession:
63
+ workspace: dict[str, Any]
64
+ session: dict[str, Any]
65
+ path: str
66
+
67
+
68
+ class RepowireSessionBridge:
69
+ def __init__(
70
+ self,
71
+ client: CdesktopClient,
72
+ bridged: BridgedSession,
73
+ repowire_url: str,
74
+ ) -> None:
75
+ self.client = client
76
+ self.bridged = bridged
77
+ self.repowire_url = repowire_url
78
+ self.assigned_name = _peer_name(bridged.workspace, bridged.session)
79
+
80
+ async def run(self) -> None:
81
+ delay = 1.0
82
+ while True:
83
+ try:
84
+ await self._connection()
85
+ delay = 1.0
86
+ except asyncio.CancelledError:
87
+ raise
88
+ except Exception as exc: # noqa: BLE001 - isolate one bridge connection
89
+ LOGGER.warning("Bridge %s disconnected: %s", self.assigned_name, exc)
90
+ await asyncio.sleep(delay)
91
+ delay = min(delay * 2, 15.0)
92
+
93
+ async def _connection(self) -> None:
94
+ async with open_transport(
95
+ websockets.connect, self.repowire_url, ping_interval=20, ping_timeout=20
96
+ ) as ws:
97
+ connect: dict[str, Any] = {
98
+ "type": "connect",
99
+ "display_name": self.assigned_name,
100
+ "circle": "default",
101
+ "backend": _backend(self.bridged.session.get("executor")),
102
+ "path": self.bridged.path,
103
+ "role": "agent",
104
+ "hook_version": 1,
105
+ "capabilities": ["cdesktop_followup_bridge"],
106
+ }
107
+ existing_peer_id = peer_identity(self.bridged.session["id"])
108
+ if existing_peer_id:
109
+ connect["peer_id"] = existing_peer_id
110
+ auth_token = os.environ.get("REPOWIRE_AUTH_TOKEN")
111
+ if auth_token:
112
+ connect["auth_token"] = auth_token
113
+ await ws.send(json.dumps(connect))
114
+ connected = json.loads(await ws.recv())
115
+ if connected.get("code") == "peer_retired" and existing_peer_id:
116
+ clear_peer_identity(self.bridged.session["id"])
117
+ if connected.get("type") != "connected":
118
+ raise RuntimeError(str(connected))
119
+ set_peer_identity(self.bridged.session["id"], connected["session_id"])
120
+ self.assigned_name = connected.get("display_name") or self.assigned_name
121
+ await ws.send(
122
+ json.dumps({"type": "status", "status": "online", "turn_state": "idle"})
123
+ )
124
+ LOGGER.info(
125
+ "Bridged %s to cdesktop session %s",
126
+ self.assigned_name,
127
+ self.bridged.session["id"],
128
+ )
129
+ async for raw in ws:
130
+ message = json.loads(raw)
131
+ await self._handle(ws, message)
132
+
133
+ async def _handle(self, ws: Any, message: dict[str, Any]) -> None:
134
+ message_type = message.get("type")
135
+ if message_type == "ping":
136
+ await ws.send(json.dumps({"type": "pong"}))
137
+ return
138
+ if message_type not in {"ask", "notify", "broadcast"}:
139
+ if message_type == "query":
140
+ await ws.send(
141
+ json.dumps(
142
+ {
143
+ "type": "error",
144
+ "correlation_id": message.get("correlation_id"),
145
+ "error": "Legacy query is unsupported by the cdesktop bridge; use ask",
146
+ }
147
+ )
148
+ )
149
+ return
150
+
151
+ from_peer = message.get("from_peer", "unknown")
152
+ text = message.get("text", "")
153
+ correlation_id = message.get("correlation_id")
154
+ if message_type == "ask":
155
+ question_flag = " --question" if message.get("question") else ""
156
+ prompt = (
157
+ f"## Request from @{from_peer}\n\n{text}\n\n"
158
+ "When complete, send a concise result back with:\n\n"
159
+ f"sightmesh bridge-reply {correlation_id} --from-peer {self.assigned_name}"
160
+ f"{question_flag} --message 'RESULT'\n\n"
161
+ "Do not delegate to hidden or native subagents."
162
+ )
163
+ else:
164
+ prompt = (
165
+ f"## {message_type.title()} from @{from_peer}\n\n{text}\n\n"
166
+ "Do not delegate to hidden or native subagents."
167
+ )
168
+
169
+ try:
170
+ await asyncio.to_thread(
171
+ self.client.send,
172
+ self.bridged.session["id"],
173
+ prompt,
174
+ None,
175
+ dedupe_key=_dedupe_key(
176
+ self.bridged.session["id"], message_type, message
177
+ ),
178
+ )
179
+ except (CdesktopError, OSError, RuntimeError) as exc:
180
+ await self._send_delivery_failure(ws, message, str(exc))
181
+ return
182
+ await self._send_delivery_ack(ws, message)
183
+
184
+ async def _send_delivery_ack(self, ws: Any, message: dict[str, Any]) -> None:
185
+ delivery_id = message.get("delivery_id")
186
+ if not delivery_id:
187
+ return
188
+ await ws.send(
189
+ json.dumps(
190
+ {
191
+ "type": "delivery_ack",
192
+ "delivery_id": delivery_id,
193
+ "message_type": message.get("type"),
194
+ "status": "injected",
195
+ }
196
+ )
197
+ )
198
+
199
+ async def _send_delivery_failure(
200
+ self, ws: Any, message: dict[str, Any], detail: str
201
+ ) -> None:
202
+ delivery_id = message.get("delivery_id")
203
+ if delivery_id:
204
+ await ws.send(
205
+ json.dumps(
206
+ {
207
+ "type": "delivery_ack",
208
+ "delivery_id": delivery_id,
209
+ "message_type": message.get("type"),
210
+ "status": "failed",
211
+ "detail": detail,
212
+ }
213
+ )
214
+ )
215
+ correlation_id = message.get("correlation_id")
216
+ if correlation_id:
217
+ await self._send_error(ws, correlation_id, detail)
218
+
219
+ async def _send_error(self, ws: Any, correlation_id: str, error: str) -> None:
220
+ await ws.send(
221
+ json.dumps(
222
+ {"type": "error", "correlation_id": correlation_id, "error": error}
223
+ )
224
+ )
225
+
226
+
227
+ class BridgeSupervisor:
228
+ def __init__(
229
+ self,
230
+ client: CdesktopClient,
231
+ repowire_url: str,
232
+ ) -> None:
233
+ self.client = client
234
+ self.repowire_url = repowire_url
235
+ self.tasks: dict[str, asyncio.Task[None]] = {}
236
+ self.reconciler = DurableExecutionReconciler(client)
237
+ self.managed_tasks = SightMesh(
238
+ client=client,
239
+ store=self.reconciler.task_store,
240
+ ownership=self.reconciler.ownership,
241
+ )
242
+ async def run(self) -> None:
243
+ while True:
244
+ try:
245
+ await self.reconcile()
246
+ except asyncio.CancelledError:
247
+ raise
248
+ except Exception as exc: # noqa: BLE001 - one tick never ends the loop
249
+ LOGGER.warning("Bridge reconcile tick failed: %s", exc)
250
+ await asyncio.sleep(2)
251
+
252
+ async def reconcile(self) -> None:
253
+ enabled = enabled_workspaces()
254
+ desired: dict[str, BridgedSession] = {}
255
+ # Wake delivery reads task rows, not workspaces, so it runs once per
256
+ # tick and cannot be starved behind a slow or failing workspace scan.
257
+ try:
258
+ await asyncio.to_thread(self.reconciler.reconcile_kernel)
259
+ except Exception as exc: # noqa: BLE001 - a kernel pass never ends a tick
260
+ LOGGER.warning("Cannot reconcile the task kernel: %s", exc)
261
+ # A launch rejected before its task ever activated holds no session, so
262
+ # the per-session pass below can never reach it. This journal-keyed
263
+ # sweep is what advances it past its typed provider outcome.
264
+ try:
265
+ await asyncio.to_thread(self.managed_tasks.reconcile_provider_outcomes)
266
+ except (
267
+ CdesktopError,
268
+ ExecutionRoutingError,
269
+ PoolError,
270
+ SightMeshError,
271
+ SuccessionError,
272
+ TaskStoreError,
273
+ ) as exc:
274
+ LOGGER.warning("Cannot reconcile typed provider outcomes: %s", exc)
275
+ try:
276
+ await asyncio.to_thread(
277
+ leases.sync_active_workspaces,
278
+ self.client,
279
+ on_error=lambda detail: LOGGER.warning(
280
+ "Cannot reconcile one cdesktop workspace: %s", detail
281
+ ),
282
+ )
283
+ workspaces = await asyncio.to_thread(self.client.workspaces)
284
+ for workspace in workspaces:
285
+ if workspace.get("archived"):
286
+ continue
287
+ try:
288
+ sessions = await asyncio.to_thread(
289
+ self.client.sessions, workspace["id"]
290
+ )
291
+ except CdesktopError as exc:
292
+ LOGGER.warning(
293
+ "Cannot inspect workspace %s sessions: %s", workspace["id"], exc
294
+ )
295
+ continue
296
+ await asyncio.to_thread(self.reconciler.reconcile_sessions, sessions)
297
+ for session in sessions:
298
+ try:
299
+ await asyncio.to_thread(
300
+ self.managed_tasks.reconcile_provider_outcome,
301
+ str(session["id"]),
302
+ )
303
+ except (
304
+ CdesktopError,
305
+ ExecutionRoutingError,
306
+ PoolError,
307
+ SightMeshError,
308
+ SuccessionError,
309
+ TaskStoreError,
310
+ ) as exc:
311
+ LOGGER.warning(
312
+ "Cannot reconcile managed task %s: %s",
313
+ session.get("id"),
314
+ exc,
315
+ )
316
+ if workspace["id"] not in enabled:
317
+ continue
318
+ try:
319
+ path = await asyncio.to_thread(_repo_path, self.client, workspace)
320
+ except CdesktopError as exc:
321
+ LOGGER.warning(
322
+ "Cannot bridge workspace %s: %s", workspace["id"], exc
323
+ )
324
+ continue
325
+ for session in sessions:
326
+ # A quarantined (retired/superseded) session never gets a
327
+ # peer bridge; injected follow-ups would auto-resume it
328
+ # into a worktree its successor now owns.
329
+ if self.reconciler.ownership.is_quarantined(session["id"]):
330
+ continue
331
+ desired[session["id"]] = BridgedSession(workspace, session, path)
332
+ except (CdesktopError, leases.LeaseError) as exc:
333
+ LOGGER.warning("Cannot reconcile cdesktop ownership: %s", exc)
334
+ return
335
+
336
+ for session_id in set(self.tasks) - set(desired):
337
+ self.tasks.pop(session_id).cancel()
338
+ for session_id, bridged in desired.items():
339
+ task = self.tasks.get(session_id)
340
+ if task is None or task.done():
341
+ self.tasks[session_id] = asyncio.create_task(
342
+ RepowireSessionBridge(
343
+ self.client,
344
+ bridged,
345
+ self.repowire_url,
346
+ ).run(),
347
+ name=f"bridge-{session_id}",
348
+ )
349
+
350
+
351
+ async def run_bridge(cdesktop_url: str | None, repowire_url: str) -> None:
352
+ client = CdesktopClient(cdesktop_url)
353
+ await BridgeSupervisor(client, repowire_url).run()
354
+
355
+
356
+ def _dedupe_key(session_id: str, message_type: str, message: dict[str, Any]) -> str:
357
+ source = message.get("delivery_id") or message.get("correlation_id")
358
+ if not source:
359
+ source = hashlib.sha256(
360
+ json.dumps(
361
+ {
362
+ "from_peer": message.get("from_peer"),
363
+ "text": message.get("text"),
364
+ },
365
+ sort_keys=True,
366
+ ).encode("utf-8")
367
+ ).hexdigest()
368
+ return f"repowire:{session_id}:{message_type}:{source}"