1140 lines
41 KiB
Python
1140 lines
41 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Transactional storage for inter-session coordination.
|
||
|
|
|
||
|
|
This module is deliberately independent of the ``coord`` command so it can be
|
||
|
|
integrated incrementally by the CLI, lifecycle hook, and write-veto hook.
|
||
|
|
|
||
|
|
The important identity distinction is:
|
||
|
|
|
||
|
|
``conversation_id``
|
||
|
|
The durable Claude/Codex conversation or thread identifier. A resumed
|
||
|
|
conversation retains this identifier.
|
||
|
|
|
||
|
|
``runtime_id``
|
||
|
|
A UUID for one live process attachment to that conversation. Every resume
|
||
|
|
registers a fresh runtime, so a stale process cannot impersonate the
|
||
|
|
resumed one or accidentally renew its leases.
|
||
|
|
|
||
|
|
Lease keys are generic resource names, not necessarily filesystem paths. For
|
||
|
|
example, callers may claim ``integration/master``, ``push/origin/master``,
|
||
|
|
``activate/home/tpp15s``, or ``deploy/system/nix-control`` with the same atomic
|
||
|
|
semantics as ``tests/checks.nix``.
|
||
|
|
|
||
|
|
All read/check/write decisions that must be atomic use ``BEGIN IMMEDIATE``.
|
||
|
|
SQLite serializes those transactions even when callers are separate processes.
|
||
|
|
WAL mode lets readers continue while a writer commits. Runtime state is local:
|
||
|
|
the containing directory is forced to 0700 and database files to 0600.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import contextlib
|
||
|
|
import dataclasses
|
||
|
|
import hashlib
|
||
|
|
import hmac
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import pathlib
|
||
|
|
import sqlite3
|
||
|
|
import time
|
||
|
|
import uuid
|
||
|
|
from collections.abc import Callable, Iterable, Iterator, Sequence
|
||
|
|
from typing import Any, TextIO
|
||
|
|
|
||
|
|
|
||
|
|
SCHEMA_VERSION = 1
|
||
|
|
DEFAULT_INSTANCE_TTL = 900.0
|
||
|
|
DEFAULT_LEASE_TTL = 3600.0
|
||
|
|
VALID_LEASE_MODES = frozenset(("exclusive", "append"))
|
||
|
|
|
||
|
|
# Human-facing handles deliberately have much less entropy than the UUIDs they
|
||
|
|
# represent: they are exact, collision-checked aliases, not security tokens.
|
||
|
|
# Five 7-bit words provide a 35-bit display namespace while normally costing
|
||
|
|
# far fewer model tokens than a 36-character UUID. Full IDs remain canonical
|
||
|
|
# in SQLite, tmux metadata, hook environment variables, and audit exports.
|
||
|
|
HANDLE_WORDS = (
|
||
|
|
"acorn", "alpine", "amber", "apple", "arrow", "ash", "atlas", "aurora",
|
||
|
|
"autumn", "badger", "bamboo", "bay", "beacon", "bear", "birch", "blue",
|
||
|
|
"breeze", "brook", "cedar", "cloud", "coast", "coral", "crane", "creek",
|
||
|
|
"dawn", "delta", "dune", "eagle", "earth", "ember", "fern", "field",
|
||
|
|
"finch", "flame", "flora", "fog", "forest", "fox", "frost", "garden",
|
||
|
|
"glade", "gold", "grove", "gull", "harbor", "hare", "hawk", "hazel",
|
||
|
|
"hill", "ice", "iris", "island", "ivy", "jade", "jay", "lake",
|
||
|
|
"lark", "leaf", "lemon", "light", "lily", "lion", "lotus", "maple",
|
||
|
|
"marsh", "meadow", "mint", "moon", "moss", "north", "oak", "ocean",
|
||
|
|
"olive", "orchid", "otter", "owl", "panda", "path", "peach", "pear",
|
||
|
|
"pine", "plum", "pond", "poppy", "rain", "raven", "reed", "ridge",
|
||
|
|
"river", "robin", "rock", "rose", "ruby", "sage", "sand", "seal",
|
||
|
|
"shadow", "shore", "silver", "sky", "snow", "south", "sparrow", "spring",
|
||
|
|
"star", "stone", "storm", "sun", "swift", "tide", "tiger", "trail",
|
||
|
|
"tree", "tulip", "valley", "violet", "wave", "west", "whale", "willow",
|
||
|
|
"wind", "wing", "winter", "wolf", "wood", "wren", "yarrow", "zenith",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def friendly_handle(value: str, namespace: str) -> str:
|
||
|
|
"""Return a stable five-word exact alias for an opaque internal ID."""
|
||
|
|
if not value:
|
||
|
|
raise ValueError("cannot derive a handle from an empty identity")
|
||
|
|
if len(HANDLE_WORDS) != 128 or len(set(HANDLE_WORDS)) != 128:
|
||
|
|
raise RuntimeError("friendly handle vocabulary must contain 128 unique words")
|
||
|
|
digest = hashlib.sha256(f"{namespace}\0{value}".encode("utf-8")).digest()
|
||
|
|
bits = int.from_bytes(digest[:5], "big") >> 5
|
||
|
|
indices = [((bits >> shift) & 0x7F) for shift in (28, 21, 14, 7, 0)]
|
||
|
|
return "-".join(HANDLE_WORDS[index] for index in indices)
|
||
|
|
|
||
|
|
|
||
|
|
@dataclasses.dataclass(frozen=True)
|
||
|
|
class LeaseConflict:
|
||
|
|
path: str
|
||
|
|
runtime_id: str
|
||
|
|
mode: str
|
||
|
|
purpose: str
|
||
|
|
expires_at: float
|
||
|
|
|
||
|
|
|
||
|
|
class ClaimRefused(RuntimeError):
|
||
|
|
"""An atomic lease claim conflicted; no path in the request was claimed."""
|
||
|
|
|
||
|
|
def __init__(self, conflicts: Sequence[LeaseConflict]):
|
||
|
|
self.conflicts = tuple(conflicts)
|
||
|
|
detail = ", ".join(
|
||
|
|
f"{item.path} ({item.mode} by {item.runtime_id})" for item in conflicts
|
||
|
|
)
|
||
|
|
super().__init__(f"lease claim refused: {detail}")
|
||
|
|
|
||
|
|
|
||
|
|
@dataclasses.dataclass(frozen=True)
|
||
|
|
class Delivery:
|
||
|
|
message_id: int
|
||
|
|
sender_runtime_id: str
|
||
|
|
kind: str
|
||
|
|
body: str
|
||
|
|
refs: tuple[str, ...]
|
||
|
|
created_at: float
|
||
|
|
state: str
|
||
|
|
|
||
|
|
|
||
|
|
class CoordStore:
|
||
|
|
"""Small process-safe coordination database.
|
||
|
|
|
||
|
|
A store object holds no long-lived connection and is safe to share between
|
||
|
|
threads. Each operation opens its own connection. Independent processes may
|
||
|
|
construct their own objects pointed at the same database.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
db_path: str | os.PathLike[str],
|
||
|
|
*,
|
||
|
|
clock: Callable[[], float] = time.time,
|
||
|
|
timeout: float = 30.0,
|
||
|
|
) -> None:
|
||
|
|
self.path = pathlib.Path(db_path)
|
||
|
|
self.clock = clock
|
||
|
|
self.timeout = timeout
|
||
|
|
self._secure_parent()
|
||
|
|
self._initialize()
|
||
|
|
|
||
|
|
def _secure_parent(self) -> None:
|
||
|
|
self.path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||
|
|
os.chmod(self.path.parent, 0o700)
|
||
|
|
|
||
|
|
def _secure_database_files(self) -> None:
|
||
|
|
for suffix in ("", "-wal", "-shm"):
|
||
|
|
candidate = pathlib.Path(f"{self.path}{suffix}")
|
||
|
|
with contextlib.suppress(FileNotFoundError):
|
||
|
|
os.chmod(candidate, 0o600)
|
||
|
|
|
||
|
|
def _connect(self) -> sqlite3.Connection:
|
||
|
|
conn = sqlite3.connect(
|
||
|
|
self.path,
|
||
|
|
timeout=self.timeout,
|
||
|
|
isolation_level=None,
|
||
|
|
)
|
||
|
|
conn.row_factory = sqlite3.Row
|
||
|
|
conn.execute("PRAGMA foreign_keys = ON")
|
||
|
|
conn.execute("PRAGMA busy_timeout = %d" % int(self.timeout * 1000))
|
||
|
|
self._secure_database_files()
|
||
|
|
return conn
|
||
|
|
|
||
|
|
@contextlib.contextmanager
|
||
|
|
def _transaction(self) -> Iterator[sqlite3.Connection]:
|
||
|
|
conn = self._connect()
|
||
|
|
try:
|
||
|
|
conn.execute("BEGIN IMMEDIATE")
|
||
|
|
yield conn
|
||
|
|
conn.commit()
|
||
|
|
except BaseException:
|
||
|
|
conn.rollback()
|
||
|
|
raise
|
||
|
|
finally:
|
||
|
|
conn.close()
|
||
|
|
self._secure_database_files()
|
||
|
|
|
||
|
|
def _initialize(self) -> None:
|
||
|
|
# journal_mode cannot be changed while a transaction is active. Set it
|
||
|
|
# once before creating the schema; the mode persists in the database.
|
||
|
|
conn = self._connect()
|
||
|
|
try:
|
||
|
|
mode = conn.execute("PRAGMA journal_mode = WAL").fetchone()[0]
|
||
|
|
if str(mode).lower() != "wal":
|
||
|
|
raise RuntimeError(f"could not enable SQLite WAL mode (got {mode!r})")
|
||
|
|
conn.execute("PRAGMA synchronous = FULL")
|
||
|
|
finally:
|
||
|
|
conn.close()
|
||
|
|
self._secure_database_files()
|
||
|
|
with self._transaction() as conn:
|
||
|
|
conn.executescript(
|
||
|
|
"""
|
||
|
|
CREATE TABLE IF NOT EXISTS metadata (
|
||
|
|
key TEXT PRIMARY KEY,
|
||
|
|
value TEXT NOT NULL
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS instances (
|
||
|
|
runtime_id TEXT PRIMARY KEY,
|
||
|
|
conversation_id TEXT NOT NULL,
|
||
|
|
harness TEXT NOT NULL,
|
||
|
|
host TEXT NOT NULL,
|
||
|
|
cwd TEXT NOT NULL,
|
||
|
|
label TEXT NOT NULL,
|
||
|
|
registered_at REAL NOT NULL,
|
||
|
|
heartbeat_at REAL NOT NULL,
|
||
|
|
heartbeat_expires_at REAL NOT NULL,
|
||
|
|
ended_at REAL
|
||
|
|
);
|
||
|
|
CREATE INDEX IF NOT EXISTS instances_conversation
|
||
|
|
ON instances(conversation_id);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS workspace_records (
|
||
|
|
instance_id TEXT PRIMARY KEY,
|
||
|
|
record_json TEXT NOT NULL,
|
||
|
|
updated_at REAL NOT NULL
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS leases (
|
||
|
|
path TEXT NOT NULL,
|
||
|
|
runtime_id TEXT NOT NULL REFERENCES instances(runtime_id),
|
||
|
|
mode TEXT NOT NULL CHECK(mode IN ('exclusive', 'append')),
|
||
|
|
purpose TEXT NOT NULL,
|
||
|
|
claimed_at REAL NOT NULL,
|
||
|
|
renewed_at REAL NOT NULL,
|
||
|
|
expires_at REAL NOT NULL,
|
||
|
|
PRIMARY KEY(path, runtime_id)
|
||
|
|
);
|
||
|
|
CREATE INDEX IF NOT EXISTS leases_expiry ON leases(expires_at);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS messages (
|
||
|
|
message_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
sender_runtime_id TEXT NOT NULL REFERENCES instances(runtime_id),
|
||
|
|
requested_recipient TEXT NOT NULL,
|
||
|
|
kind TEXT NOT NULL,
|
||
|
|
body TEXT NOT NULL,
|
||
|
|
refs_json TEXT NOT NULL,
|
||
|
|
created_at REAL NOT NULL
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS message_deliveries (
|
||
|
|
message_id INTEGER NOT NULL REFERENCES messages(message_id),
|
||
|
|
recipient_runtime_id TEXT NOT NULL REFERENCES instances(runtime_id),
|
||
|
|
state TEXT NOT NULL CHECK(state IN ('queued', 'delivered', 'acked')),
|
||
|
|
delivered_at REAL,
|
||
|
|
acked_at REAL,
|
||
|
|
PRIMARY KEY(message_id, recipient_runtime_id)
|
||
|
|
);
|
||
|
|
CREATE INDEX IF NOT EXISTS deliveries_inbox
|
||
|
|
ON message_deliveries(recipient_runtime_id, state, message_id);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS audit_events (
|
||
|
|
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
occurred_at REAL NOT NULL,
|
||
|
|
event TEXT NOT NULL,
|
||
|
|
runtime_id TEXT,
|
||
|
|
details_json TEXT NOT NULL
|
||
|
|
);
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
row = conn.execute(
|
||
|
|
"SELECT value FROM metadata WHERE key = 'schema_version'"
|
||
|
|
).fetchone()
|
||
|
|
if row is None:
|
||
|
|
conn.execute(
|
||
|
|
"INSERT INTO metadata(key, value) VALUES('schema_version', ?)",
|
||
|
|
(str(SCHEMA_VERSION),),
|
||
|
|
)
|
||
|
|
elif int(row["value"]) != SCHEMA_VERSION:
|
||
|
|
raise RuntimeError(
|
||
|
|
f"unsupported coordination schema {row['value']}; "
|
||
|
|
f"expected {SCHEMA_VERSION}"
|
||
|
|
)
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _normalize_paths(paths: Iterable[str]) -> tuple[str, ...]:
|
||
|
|
normalized = tuple(
|
||
|
|
dict.fromkeys(os.path.normpath(path.strip()) for path in paths if path.strip())
|
||
|
|
)
|
||
|
|
if not normalized:
|
||
|
|
raise ValueError("at least one non-empty path is required")
|
||
|
|
if any(path == "." or os.path.isabs(path) or path.startswith("../") for path in normalized):
|
||
|
|
raise ValueError("lease paths must be repository-relative and may not escape it")
|
||
|
|
return normalized
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def resources_overlap(left: str, right: str) -> bool:
|
||
|
|
"""Whether two exact resource names are equal or one contains the other."""
|
||
|
|
return (
|
||
|
|
left == right
|
||
|
|
or left.startswith(right.rstrip("/") + "/")
|
||
|
|
or right.startswith(left.rstrip("/") + "/")
|
||
|
|
)
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _audit(
|
||
|
|
conn: sqlite3.Connection,
|
||
|
|
now: float,
|
||
|
|
event: str,
|
||
|
|
runtime_id: str | None,
|
||
|
|
details: dict[str, Any],
|
||
|
|
) -> None:
|
||
|
|
conn.execute(
|
||
|
|
"""
|
||
|
|
INSERT INTO audit_events(occurred_at, event, runtime_id, details_json)
|
||
|
|
VALUES(?, ?, ?, ?)
|
||
|
|
""",
|
||
|
|
(now, event, runtime_id, json.dumps(details, sort_keys=True, separators=(",", ":"))),
|
||
|
|
)
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _require_live(
|
||
|
|
conn: sqlite3.Connection, runtime_id: str, now: float
|
||
|
|
) -> sqlite3.Row:
|
||
|
|
row = conn.execute(
|
||
|
|
"""
|
||
|
|
SELECT * FROM instances
|
||
|
|
WHERE runtime_id = ? AND ended_at IS NULL AND heartbeat_expires_at > ?
|
||
|
|
""",
|
||
|
|
(runtime_id, now),
|
||
|
|
).fetchone()
|
||
|
|
if row is None:
|
||
|
|
raise KeyError(f"runtime instance is not registered and live: {runtime_id}")
|
||
|
|
return row
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _reap(conn: sqlite3.Connection, now: float) -> int:
|
||
|
|
cursor = conn.execute(
|
||
|
|
"""
|
||
|
|
DELETE FROM leases
|
||
|
|
WHERE expires_at <= ?
|
||
|
|
OR runtime_id IN (
|
||
|
|
SELECT runtime_id FROM instances
|
||
|
|
WHERE ended_at IS NOT NULL OR heartbeat_expires_at <= ?
|
||
|
|
)
|
||
|
|
""",
|
||
|
|
(now, now),
|
||
|
|
)
|
||
|
|
return cursor.rowcount
|
||
|
|
|
||
|
|
def register_instance(
|
||
|
|
self,
|
||
|
|
conversation_id: str,
|
||
|
|
*,
|
||
|
|
runtime_id: str | None = None,
|
||
|
|
harness: str = "",
|
||
|
|
host: str = "",
|
||
|
|
cwd: str = "",
|
||
|
|
label: str = "",
|
||
|
|
ttl_seconds: float = DEFAULT_INSTANCE_TTL,
|
||
|
|
) -> str:
|
||
|
|
if not conversation_id:
|
||
|
|
raise ValueError("conversation_id is required")
|
||
|
|
if ttl_seconds <= 0:
|
||
|
|
raise ValueError("ttl_seconds must be positive")
|
||
|
|
runtime_id = runtime_id or str(uuid.uuid4())
|
||
|
|
if runtime_id == conversation_id:
|
||
|
|
raise ValueError("runtime_id must be distinct from conversation_id")
|
||
|
|
now = self.clock()
|
||
|
|
with self._transaction() as conn:
|
||
|
|
conn.execute(
|
||
|
|
"""
|
||
|
|
INSERT INTO instances(
|
||
|
|
runtime_id, conversation_id, harness, host, cwd, label,
|
||
|
|
registered_at, heartbeat_at, heartbeat_expires_at, ended_at
|
||
|
|
) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
||
|
|
""",
|
||
|
|
(
|
||
|
|
runtime_id,
|
||
|
|
conversation_id,
|
||
|
|
harness,
|
||
|
|
host,
|
||
|
|
cwd,
|
||
|
|
label,
|
||
|
|
now,
|
||
|
|
now,
|
||
|
|
now + ttl_seconds,
|
||
|
|
),
|
||
|
|
)
|
||
|
|
# Carry undelivered work across a genuine resume. Only dead/expired
|
||
|
|
# attachments of the same conversation are migrated; concurrently
|
||
|
|
# live forks retain independent inboxes.
|
||
|
|
stale_deliveries = conn.execute(
|
||
|
|
"""
|
||
|
|
SELECT d.message_id, d.recipient_runtime_id
|
||
|
|
FROM message_deliveries AS d
|
||
|
|
JOIN instances AS old ON old.runtime_id = d.recipient_runtime_id
|
||
|
|
WHERE old.conversation_id = ? AND old.runtime_id != ?
|
||
|
|
AND (old.ended_at IS NOT NULL OR old.heartbeat_expires_at <= ?)
|
||
|
|
AND d.state != 'acked'
|
||
|
|
ORDER BY d.message_id, d.recipient_runtime_id
|
||
|
|
""",
|
||
|
|
(conversation_id, runtime_id, now),
|
||
|
|
).fetchall()
|
||
|
|
for delivery in stale_deliveries:
|
||
|
|
conn.execute(
|
||
|
|
"""
|
||
|
|
INSERT INTO message_deliveries(
|
||
|
|
message_id, recipient_runtime_id, state
|
||
|
|
) VALUES(?, ?, 'queued')
|
||
|
|
ON CONFLICT(message_id, recipient_runtime_id) DO NOTHING
|
||
|
|
""",
|
||
|
|
(delivery["message_id"], runtime_id),
|
||
|
|
)
|
||
|
|
conn.execute(
|
||
|
|
"""
|
||
|
|
DELETE FROM message_deliveries
|
||
|
|
WHERE message_id = ? AND recipient_runtime_id = ?
|
||
|
|
""",
|
||
|
|
(delivery["message_id"], delivery["recipient_runtime_id"]),
|
||
|
|
)
|
||
|
|
self._audit(
|
||
|
|
conn,
|
||
|
|
now,
|
||
|
|
"instance.registered",
|
||
|
|
runtime_id,
|
||
|
|
{
|
||
|
|
"conversation_id": conversation_id,
|
||
|
|
"ttl_seconds": ttl_seconds,
|
||
|
|
"deliveries_migrated": len(stale_deliveries),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
return runtime_id
|
||
|
|
|
||
|
|
def heartbeat(
|
||
|
|
self,
|
||
|
|
runtime_id: str,
|
||
|
|
*,
|
||
|
|
ttl_seconds: float = DEFAULT_INSTANCE_TTL,
|
||
|
|
renew_lease_ttl_seconds: float | None = None,
|
||
|
|
) -> None:
|
||
|
|
if ttl_seconds <= 0:
|
||
|
|
raise ValueError("ttl_seconds must be positive")
|
||
|
|
if renew_lease_ttl_seconds is not None and renew_lease_ttl_seconds <= 0:
|
||
|
|
raise ValueError("renew_lease_ttl_seconds must be positive")
|
||
|
|
now = self.clock()
|
||
|
|
with self._transaction() as conn:
|
||
|
|
row = conn.execute(
|
||
|
|
"""
|
||
|
|
SELECT ended_at, heartbeat_expires_at
|
||
|
|
FROM instances WHERE runtime_id = ?
|
||
|
|
""",
|
||
|
|
(runtime_id,),
|
||
|
|
).fetchone()
|
||
|
|
if (
|
||
|
|
row is None
|
||
|
|
or row["ended_at"] is not None
|
||
|
|
or row["heartbeat_expires_at"] <= now
|
||
|
|
):
|
||
|
|
raise KeyError(f"runtime instance is not active: {runtime_id}")
|
||
|
|
conn.execute(
|
||
|
|
"""
|
||
|
|
UPDATE instances
|
||
|
|
SET heartbeat_at = ?, heartbeat_expires_at = ?
|
||
|
|
WHERE runtime_id = ?
|
||
|
|
""",
|
||
|
|
(now, now + ttl_seconds, runtime_id),
|
||
|
|
)
|
||
|
|
renewed = 0
|
||
|
|
if renew_lease_ttl_seconds is not None:
|
||
|
|
renewed = conn.execute(
|
||
|
|
"""
|
||
|
|
UPDATE leases SET renewed_at = ?, expires_at = ?
|
||
|
|
WHERE runtime_id = ? AND expires_at > ?
|
||
|
|
""",
|
||
|
|
(now, now + renew_lease_ttl_seconds, runtime_id, now),
|
||
|
|
).rowcount
|
||
|
|
self._audit(
|
||
|
|
conn,
|
||
|
|
now,
|
||
|
|
"instance.heartbeat",
|
||
|
|
runtime_id,
|
||
|
|
{"ttl_seconds": ttl_seconds, "leases_renewed": renewed},
|
||
|
|
)
|
||
|
|
|
||
|
|
def end_instance(self, runtime_id: str, *, reason: str = "clean") -> None:
|
||
|
|
now = self.clock()
|
||
|
|
with self._transaction() as conn:
|
||
|
|
cursor = conn.execute(
|
||
|
|
"""
|
||
|
|
UPDATE instances SET ended_at = ?
|
||
|
|
WHERE runtime_id = ? AND ended_at IS NULL
|
||
|
|
""",
|
||
|
|
(now, runtime_id),
|
||
|
|
)
|
||
|
|
if cursor.rowcount != 1:
|
||
|
|
raise KeyError(f"runtime instance is not active: {runtime_id}")
|
||
|
|
released = conn.execute(
|
||
|
|
"DELETE FROM leases WHERE runtime_id = ?", (runtime_id,)
|
||
|
|
).rowcount
|
||
|
|
self._audit(
|
||
|
|
conn,
|
||
|
|
now,
|
||
|
|
"instance.ended",
|
||
|
|
runtime_id,
|
||
|
|
{"reason": reason, "leases_released": released},
|
||
|
|
)
|
||
|
|
|
||
|
|
def claim(
|
||
|
|
self,
|
||
|
|
runtime_id: str,
|
||
|
|
paths: Iterable[str],
|
||
|
|
*,
|
||
|
|
mode: str,
|
||
|
|
purpose: str = "",
|
||
|
|
ttl_seconds: float = DEFAULT_LEASE_TTL,
|
||
|
|
) -> None:
|
||
|
|
paths = self._normalize_paths(paths)
|
||
|
|
if mode not in VALID_LEASE_MODES:
|
||
|
|
raise ValueError(f"invalid lease mode: {mode}")
|
||
|
|
if ttl_seconds <= 0:
|
||
|
|
raise ValueError("ttl_seconds must be positive")
|
||
|
|
now = self.clock()
|
||
|
|
with self._transaction() as conn:
|
||
|
|
self._require_live(conn, runtime_id, now)
|
||
|
|
self._reap(conn, now)
|
||
|
|
rows = conn.execute(
|
||
|
|
"""
|
||
|
|
SELECT path, runtime_id, mode, purpose, expires_at
|
||
|
|
FROM leases
|
||
|
|
WHERE runtime_id != ?
|
||
|
|
AND (? = 'exclusive' OR mode = 'exclusive')
|
||
|
|
ORDER BY path, runtime_id
|
||
|
|
""",
|
||
|
|
(runtime_id, mode),
|
||
|
|
).fetchall()
|
||
|
|
conflicts = [
|
||
|
|
row
|
||
|
|
for row in rows
|
||
|
|
if any(self.resources_overlap(path, row["path"]) for path in paths)
|
||
|
|
]
|
||
|
|
if conflicts:
|
||
|
|
raise ClaimRefused(
|
||
|
|
[
|
||
|
|
LeaseConflict(
|
||
|
|
row["path"],
|
||
|
|
row["runtime_id"],
|
||
|
|
row["mode"],
|
||
|
|
row["purpose"],
|
||
|
|
row["expires_at"],
|
||
|
|
)
|
||
|
|
for row in conflicts
|
||
|
|
]
|
||
|
|
)
|
||
|
|
for path in paths:
|
||
|
|
conn.execute(
|
||
|
|
"""
|
||
|
|
INSERT INTO leases(
|
||
|
|
path, runtime_id, mode, purpose,
|
||
|
|
claimed_at, renewed_at, expires_at
|
||
|
|
) VALUES(?, ?, ?, ?, ?, ?, ?)
|
||
|
|
ON CONFLICT(path, runtime_id) DO UPDATE SET
|
||
|
|
mode = excluded.mode,
|
||
|
|
purpose = excluded.purpose,
|
||
|
|
renewed_at = excluded.renewed_at,
|
||
|
|
expires_at = excluded.expires_at
|
||
|
|
""",
|
||
|
|
(path, runtime_id, mode, purpose, now, now, now + ttl_seconds),
|
||
|
|
)
|
||
|
|
self._audit(
|
||
|
|
conn,
|
||
|
|
now,
|
||
|
|
"lease.claimed",
|
||
|
|
runtime_id,
|
||
|
|
{
|
||
|
|
"paths": paths,
|
||
|
|
"mode": mode,
|
||
|
|
"purpose": purpose,
|
||
|
|
"ttl_seconds": ttl_seconds,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
def release(self, runtime_id: str, paths: Iterable[str]) -> tuple[str, ...]:
|
||
|
|
paths = self._normalize_paths(paths)
|
||
|
|
now = self.clock()
|
||
|
|
with self._transaction() as conn:
|
||
|
|
self._require_live(conn, runtime_id, now)
|
||
|
|
placeholders = ",".join("?" for _ in paths)
|
||
|
|
rows = conn.execute(
|
||
|
|
f"""
|
||
|
|
SELECT path FROM leases
|
||
|
|
WHERE runtime_id = ? AND path IN ({placeholders})
|
||
|
|
ORDER BY path
|
||
|
|
""",
|
||
|
|
(runtime_id, *paths),
|
||
|
|
).fetchall()
|
||
|
|
released = tuple(row["path"] for row in rows)
|
||
|
|
conn.execute(
|
||
|
|
f"""
|
||
|
|
DELETE FROM leases
|
||
|
|
WHERE runtime_id = ? AND path IN ({placeholders})
|
||
|
|
""",
|
||
|
|
(runtime_id, *paths),
|
||
|
|
)
|
||
|
|
self._audit(
|
||
|
|
conn, now, "lease.released", runtime_id, {"paths": released}
|
||
|
|
)
|
||
|
|
return released
|
||
|
|
|
||
|
|
def renew_leases(
|
||
|
|
self,
|
||
|
|
runtime_id: str,
|
||
|
|
*,
|
||
|
|
paths: Iterable[str] | None = None,
|
||
|
|
ttl_seconds: float = DEFAULT_LEASE_TTL,
|
||
|
|
) -> tuple[str, ...]:
|
||
|
|
if ttl_seconds <= 0:
|
||
|
|
raise ValueError("ttl_seconds must be positive")
|
||
|
|
normalized = self._normalize_paths(paths) if paths is not None else None
|
||
|
|
now = self.clock()
|
||
|
|
with self._transaction() as conn:
|
||
|
|
self._require_live(conn, runtime_id, now)
|
||
|
|
self._reap(conn, now)
|
||
|
|
parameters: list[Any] = [now, now + ttl_seconds, runtime_id]
|
||
|
|
suffix = ""
|
||
|
|
if normalized is not None:
|
||
|
|
suffix = " AND path IN (%s)" % ",".join("?" for _ in normalized)
|
||
|
|
parameters.extend(normalized)
|
||
|
|
conn.execute(
|
||
|
|
f"""
|
||
|
|
UPDATE leases SET renewed_at = ?, expires_at = ?
|
||
|
|
WHERE runtime_id = ?{suffix}
|
||
|
|
""",
|
||
|
|
parameters,
|
||
|
|
)
|
||
|
|
rows = conn.execute(
|
||
|
|
f"""
|
||
|
|
SELECT path FROM leases
|
||
|
|
WHERE runtime_id = ?{suffix}
|
||
|
|
ORDER BY path
|
||
|
|
""",
|
||
|
|
(runtime_id, *(normalized or ())),
|
||
|
|
).fetchall()
|
||
|
|
renewed = tuple(row["path"] for row in rows)
|
||
|
|
self._audit(
|
||
|
|
conn,
|
||
|
|
now,
|
||
|
|
"lease.renewed",
|
||
|
|
runtime_id,
|
||
|
|
{"paths": renewed, "ttl_seconds": ttl_seconds},
|
||
|
|
)
|
||
|
|
return renewed
|
||
|
|
|
||
|
|
# Resource-named aliases make it explicit that leases cover global operations
|
||
|
|
# and external systems as well as repository files.
|
||
|
|
def claim_resources(
|
||
|
|
self,
|
||
|
|
runtime_id: str,
|
||
|
|
resources: Iterable[str],
|
||
|
|
*,
|
||
|
|
mode: str,
|
||
|
|
purpose: str = "",
|
||
|
|
ttl_seconds: float = DEFAULT_LEASE_TTL,
|
||
|
|
) -> None:
|
||
|
|
self.claim(
|
||
|
|
runtime_id,
|
||
|
|
resources,
|
||
|
|
mode=mode,
|
||
|
|
purpose=purpose,
|
||
|
|
ttl_seconds=ttl_seconds,
|
||
|
|
)
|
||
|
|
|
||
|
|
def release_resources(
|
||
|
|
self, runtime_id: str, resources: Iterable[str]
|
||
|
|
) -> tuple[str, ...]:
|
||
|
|
return self.release(runtime_id, resources)
|
||
|
|
|
||
|
|
def renew_resources(
|
||
|
|
self,
|
||
|
|
runtime_id: str,
|
||
|
|
*,
|
||
|
|
resources: Iterable[str] | None = None,
|
||
|
|
ttl_seconds: float = DEFAULT_LEASE_TTL,
|
||
|
|
) -> tuple[str, ...]:
|
||
|
|
return self.renew_leases(
|
||
|
|
runtime_id, paths=resources, ttl_seconds=ttl_seconds
|
||
|
|
)
|
||
|
|
|
||
|
|
def active_leases(self) -> list[dict[str, Any]]:
|
||
|
|
now = self.clock()
|
||
|
|
with self._transaction() as conn:
|
||
|
|
self._reap(conn, now)
|
||
|
|
return [
|
||
|
|
dict(row)
|
||
|
|
for row in conn.execute(
|
||
|
|
"""
|
||
|
|
SELECT l.* FROM leases AS l
|
||
|
|
JOIN instances AS i USING(runtime_id)
|
||
|
|
WHERE l.expires_at > ? AND i.ended_at IS NULL
|
||
|
|
AND i.heartbeat_expires_at > ?
|
||
|
|
ORDER BY l.path, l.runtime_id
|
||
|
|
""",
|
||
|
|
(now, now),
|
||
|
|
)
|
||
|
|
]
|
||
|
|
|
||
|
|
def live_instances(self) -> list[dict[str, Any]]:
|
||
|
|
"""Return live runtime attachments with their full conversation IDs."""
|
||
|
|
now = self.clock()
|
||
|
|
conn = self._connect()
|
||
|
|
try:
|
||
|
|
return [
|
||
|
|
dict(row)
|
||
|
|
for row in conn.execute(
|
||
|
|
"""
|
||
|
|
SELECT * FROM instances
|
||
|
|
WHERE ended_at IS NULL AND heartbeat_expires_at > ?
|
||
|
|
ORDER BY registered_at, runtime_id
|
||
|
|
""",
|
||
|
|
(now,),
|
||
|
|
)
|
||
|
|
]
|
||
|
|
finally:
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
def resolve_runtime(self, identity: str) -> str:
|
||
|
|
"""Resolve an exact runtime, friendly handle, or live conversation.
|
||
|
|
|
||
|
|
Prefix matching is intentionally forbidden. Friendly handles are
|
||
|
|
derived from the full runtime ID and must match exactly; the extremely
|
||
|
|
unlikely ambiguous handle is refused rather than guessed.
|
||
|
|
"""
|
||
|
|
now = self.clock()
|
||
|
|
conn = self._connect()
|
||
|
|
try:
|
||
|
|
exact = conn.execute(
|
||
|
|
"""
|
||
|
|
SELECT runtime_id FROM instances
|
||
|
|
WHERE runtime_id = ? AND ended_at IS NULL
|
||
|
|
AND heartbeat_expires_at > ?
|
||
|
|
""",
|
||
|
|
(identity, now),
|
||
|
|
).fetchone()
|
||
|
|
if exact is not None:
|
||
|
|
return str(exact["runtime_id"])
|
||
|
|
handle_matches = [
|
||
|
|
str(row["runtime_id"])
|
||
|
|
for row in conn.execute(
|
||
|
|
"""
|
||
|
|
SELECT runtime_id FROM instances
|
||
|
|
WHERE ended_at IS NULL AND heartbeat_expires_at > ?
|
||
|
|
ORDER BY runtime_id
|
||
|
|
""",
|
||
|
|
(now,),
|
||
|
|
)
|
||
|
|
if hmac.compare_digest(
|
||
|
|
friendly_handle(str(row["runtime_id"]), "runtime"), identity
|
||
|
|
)
|
||
|
|
]
|
||
|
|
if len(handle_matches) == 1:
|
||
|
|
return handle_matches[0]
|
||
|
|
if len(handle_matches) > 1:
|
||
|
|
raise KeyError(
|
||
|
|
f"ambiguous runtime handle: {identity}; use a full runtime ID"
|
||
|
|
)
|
||
|
|
latest = conn.execute(
|
||
|
|
"""
|
||
|
|
SELECT runtime_id FROM instances
|
||
|
|
WHERE conversation_id = ? AND ended_at IS NULL
|
||
|
|
AND heartbeat_expires_at > ?
|
||
|
|
ORDER BY registered_at DESC, runtime_id DESC
|
||
|
|
LIMIT 1
|
||
|
|
""",
|
||
|
|
(identity, now),
|
||
|
|
).fetchone()
|
||
|
|
if latest is None:
|
||
|
|
raise KeyError(f"no live runtime for exact identity: {identity}")
|
||
|
|
return str(latest["runtime_id"])
|
||
|
|
finally:
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
def save_workspace_record(
|
||
|
|
self,
|
||
|
|
event: str,
|
||
|
|
record: dict[str, Any],
|
||
|
|
) -> None:
|
||
|
|
"""Transactionally retain the latest full lifecycle record."""
|
||
|
|
instance_id = str(record.get("instance_id", ""))
|
||
|
|
if not instance_id:
|
||
|
|
raise ValueError("workspace record requires instance_id")
|
||
|
|
now = self.clock()
|
||
|
|
payload = json.dumps(record, sort_keys=True, separators=(",", ":"))
|
||
|
|
with self._transaction() as conn:
|
||
|
|
conn.execute(
|
||
|
|
"""
|
||
|
|
INSERT INTO workspace_records(instance_id, record_json, updated_at)
|
||
|
|
VALUES(?, ?, ?)
|
||
|
|
ON CONFLICT(instance_id) DO UPDATE SET
|
||
|
|
record_json = excluded.record_json,
|
||
|
|
updated_at = excluded.updated_at
|
||
|
|
""",
|
||
|
|
(instance_id, payload, now),
|
||
|
|
)
|
||
|
|
self._audit(
|
||
|
|
conn,
|
||
|
|
now,
|
||
|
|
event,
|
||
|
|
str(record.get("runtime_id") or "") or None,
|
||
|
|
{"instance_id": instance_id, "record": record},
|
||
|
|
)
|
||
|
|
|
||
|
|
def workspace_record(self, instance_id: str) -> dict[str, Any]:
|
||
|
|
conn = self._connect()
|
||
|
|
try:
|
||
|
|
row = conn.execute(
|
||
|
|
"SELECT record_json FROM workspace_records WHERE instance_id = ?",
|
||
|
|
(instance_id,),
|
||
|
|
).fetchone()
|
||
|
|
if row is not None:
|
||
|
|
return dict(json.loads(row["record_json"]))
|
||
|
|
matches = []
|
||
|
|
for candidate in conn.execute(
|
||
|
|
"SELECT instance_id, record_json FROM workspace_records ORDER BY instance_id"
|
||
|
|
):
|
||
|
|
if hmac.compare_digest(
|
||
|
|
friendly_handle(str(candidate["instance_id"]), "work"),
|
||
|
|
instance_id,
|
||
|
|
):
|
||
|
|
matches.append(dict(json.loads(candidate["record_json"])))
|
||
|
|
if len(matches) == 1:
|
||
|
|
return matches[0]
|
||
|
|
if len(matches) > 1:
|
||
|
|
raise KeyError(
|
||
|
|
f"ambiguous workspace handle: {instance_id}; use a full instance ID"
|
||
|
|
)
|
||
|
|
raise KeyError(f"unknown workspace instance: {instance_id}")
|
||
|
|
finally:
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
def workspace_records(self) -> list[dict[str, Any]]:
|
||
|
|
conn = self._connect()
|
||
|
|
try:
|
||
|
|
return [
|
||
|
|
dict(json.loads(row["record_json"]))
|
||
|
|
for row in conn.execute(
|
||
|
|
"SELECT record_json FROM workspace_records ORDER BY updated_at, instance_id"
|
||
|
|
)
|
||
|
|
]
|
||
|
|
finally:
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
def is_managed_workspace(self, path: str | os.PathLike[str]) -> bool:
|
||
|
|
candidate = pathlib.Path(path).resolve()
|
||
|
|
for record in self.workspace_records():
|
||
|
|
actual = record.get("actual_path")
|
||
|
|
if (
|
||
|
|
not actual
|
||
|
|
or record.get("lifecycle_state") != "active"
|
||
|
|
or pathlib.Path(str(actual)).resolve() != candidate
|
||
|
|
):
|
||
|
|
continue
|
||
|
|
try:
|
||
|
|
marker = json.loads(
|
||
|
|
(candidate / ".jj" / "fleet-coord-owner.json").read_text(
|
||
|
|
encoding="utf-8"
|
||
|
|
)
|
||
|
|
)
|
||
|
|
except (FileNotFoundError, OSError, json.JSONDecodeError):
|
||
|
|
return False
|
||
|
|
return (
|
||
|
|
marker.get("instance_id") == record.get("instance_id")
|
||
|
|
and hmac.compare_digest(
|
||
|
|
str(marker.get("ownership_token", "")),
|
||
|
|
str(record.get("ownership_token", "")),
|
||
|
|
)
|
||
|
|
)
|
||
|
|
return False
|
||
|
|
|
||
|
|
def record_event(
|
||
|
|
self,
|
||
|
|
event: str,
|
||
|
|
*,
|
||
|
|
runtime_id: str | None = None,
|
||
|
|
details: dict[str, Any] | None = None,
|
||
|
|
) -> int:
|
||
|
|
"""Append a generic structured event for integrations layered above this API."""
|
||
|
|
if not event:
|
||
|
|
raise ValueError("event is required")
|
||
|
|
now = self.clock()
|
||
|
|
with self._transaction() as conn:
|
||
|
|
if runtime_id is not None:
|
||
|
|
self._require_live(conn, runtime_id, now)
|
||
|
|
self._audit(conn, now, event, runtime_id, details or {})
|
||
|
|
return int(conn.execute("SELECT last_insert_rowid()").fetchone()[0])
|
||
|
|
|
||
|
|
def send_message(
|
||
|
|
self,
|
||
|
|
sender_runtime_id: str,
|
||
|
|
*,
|
||
|
|
recipient: str | Iterable[str] = "*",
|
||
|
|
kind: str = "fyi",
|
||
|
|
body: str,
|
||
|
|
refs: Iterable[str] = (),
|
||
|
|
) -> int:
|
||
|
|
if not body:
|
||
|
|
raise ValueError("message body is required")
|
||
|
|
now = self.clock()
|
||
|
|
with self._transaction() as conn:
|
||
|
|
self._require_live(conn, sender_runtime_id, now)
|
||
|
|
if recipient == "*":
|
||
|
|
requested = "*"
|
||
|
|
recipients = [
|
||
|
|
row["runtime_id"]
|
||
|
|
for row in conn.execute(
|
||
|
|
"""
|
||
|
|
SELECT runtime_id FROM instances
|
||
|
|
WHERE runtime_id != ? AND ended_at IS NULL
|
||
|
|
AND heartbeat_expires_at > ?
|
||
|
|
ORDER BY runtime_id
|
||
|
|
""",
|
||
|
|
(sender_runtime_id, now),
|
||
|
|
)
|
||
|
|
]
|
||
|
|
else:
|
||
|
|
recipients = (
|
||
|
|
(recipient,)
|
||
|
|
if isinstance(recipient, str)
|
||
|
|
else tuple(dict.fromkeys(recipient))
|
||
|
|
)
|
||
|
|
requested = ",".join(recipients)
|
||
|
|
if not recipients:
|
||
|
|
raise ValueError("at least one recipient is required")
|
||
|
|
placeholders = ",".join("?" for _ in recipients)
|
||
|
|
found = {
|
||
|
|
row["runtime_id"]
|
||
|
|
for row in conn.execute(
|
||
|
|
f"""
|
||
|
|
SELECT runtime_id FROM instances
|
||
|
|
WHERE runtime_id IN ({placeholders})
|
||
|
|
AND ended_at IS NULL AND heartbeat_expires_at > ?
|
||
|
|
""",
|
||
|
|
(*recipients, now),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
missing = set(recipients) - found
|
||
|
|
if missing:
|
||
|
|
raise KeyError(f"recipient runtime is not live: {sorted(missing)}")
|
||
|
|
cursor = conn.execute(
|
||
|
|
"""
|
||
|
|
INSERT INTO messages(
|
||
|
|
sender_runtime_id, requested_recipient, kind,
|
||
|
|
body, refs_json, created_at
|
||
|
|
) VALUES(?, ?, ?, ?, ?, ?)
|
||
|
|
""",
|
||
|
|
(
|
||
|
|
sender_runtime_id,
|
||
|
|
requested,
|
||
|
|
kind,
|
||
|
|
body,
|
||
|
|
json.dumps(tuple(refs), separators=(",", ":")),
|
||
|
|
now,
|
||
|
|
),
|
||
|
|
)
|
||
|
|
message_id = int(cursor.lastrowid)
|
||
|
|
conn.executemany(
|
||
|
|
"""
|
||
|
|
INSERT INTO message_deliveries(
|
||
|
|
message_id, recipient_runtime_id, state
|
||
|
|
) VALUES(?, ?, 'queued')
|
||
|
|
""",
|
||
|
|
((message_id, target) for target in recipients),
|
||
|
|
)
|
||
|
|
self._audit(
|
||
|
|
conn,
|
||
|
|
now,
|
||
|
|
"message.queued",
|
||
|
|
sender_runtime_id,
|
||
|
|
{
|
||
|
|
"message_id": message_id,
|
||
|
|
"requested_recipient": requested,
|
||
|
|
"recipient_snapshot": recipients,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
return message_id
|
||
|
|
|
||
|
|
def inbox(
|
||
|
|
self,
|
||
|
|
runtime_id: str,
|
||
|
|
*,
|
||
|
|
limit: int = 100,
|
||
|
|
mark_delivered: bool = True,
|
||
|
|
) -> list[Delivery]:
|
||
|
|
if limit <= 0:
|
||
|
|
return []
|
||
|
|
now = self.clock()
|
||
|
|
with self._transaction() as conn:
|
||
|
|
self._require_live(conn, runtime_id, now)
|
||
|
|
rows = conn.execute(
|
||
|
|
"""
|
||
|
|
SELECT m.*, d.state
|
||
|
|
FROM message_deliveries AS d
|
||
|
|
JOIN messages AS m USING(message_id)
|
||
|
|
WHERE d.recipient_runtime_id = ? AND d.state != 'acked'
|
||
|
|
ORDER BY m.message_id
|
||
|
|
LIMIT ?
|
||
|
|
""",
|
||
|
|
(runtime_id, limit),
|
||
|
|
).fetchall()
|
||
|
|
ids = [row["message_id"] for row in rows]
|
||
|
|
if mark_delivered and ids:
|
||
|
|
placeholders = ",".join("?" for _ in ids)
|
||
|
|
conn.execute(
|
||
|
|
f"""
|
||
|
|
UPDATE message_deliveries
|
||
|
|
SET state = 'delivered', delivered_at = COALESCE(delivered_at, ?)
|
||
|
|
WHERE recipient_runtime_id = ?
|
||
|
|
AND message_id IN ({placeholders}) AND state = 'queued'
|
||
|
|
""",
|
||
|
|
(now, runtime_id, *ids),
|
||
|
|
)
|
||
|
|
self._audit(
|
||
|
|
conn,
|
||
|
|
now,
|
||
|
|
"message.delivered",
|
||
|
|
runtime_id,
|
||
|
|
{"message_ids": ids},
|
||
|
|
)
|
||
|
|
return [
|
||
|
|
Delivery(
|
||
|
|
message_id=row["message_id"],
|
||
|
|
sender_runtime_id=row["sender_runtime_id"],
|
||
|
|
kind=row["kind"],
|
||
|
|
body=row["body"],
|
||
|
|
refs=tuple(json.loads(row["refs_json"])),
|
||
|
|
created_at=row["created_at"],
|
||
|
|
# Report the state observed by this read. This transaction
|
||
|
|
# updates queued rows after reading them, while the hook
|
||
|
|
# needs to distinguish first delivery from a reminder.
|
||
|
|
state=row["state"],
|
||
|
|
)
|
||
|
|
for row in rows
|
||
|
|
]
|
||
|
|
|
||
|
|
def ack(self, runtime_id: str, message_ids: Iterable[int]) -> tuple[int, ...]:
|
||
|
|
ids = tuple(dict.fromkeys(int(item) for item in message_ids))
|
||
|
|
if not ids:
|
||
|
|
return ()
|
||
|
|
now = self.clock()
|
||
|
|
with self._transaction() as conn:
|
||
|
|
self._require_live(conn, runtime_id, now)
|
||
|
|
placeholders = ",".join("?" for _ in ids)
|
||
|
|
rows = conn.execute(
|
||
|
|
f"""
|
||
|
|
SELECT message_id FROM message_deliveries
|
||
|
|
WHERE recipient_runtime_id = ?
|
||
|
|
AND message_id IN ({placeholders}) AND state != 'acked'
|
||
|
|
ORDER BY message_id
|
||
|
|
""",
|
||
|
|
(runtime_id, *ids),
|
||
|
|
).fetchall()
|
||
|
|
acked = tuple(row["message_id"] for row in rows)
|
||
|
|
conn.execute(
|
||
|
|
f"""
|
||
|
|
UPDATE message_deliveries
|
||
|
|
SET state = 'acked', acked_at = ?
|
||
|
|
WHERE recipient_runtime_id = ?
|
||
|
|
AND message_id IN ({placeholders}) AND state != 'acked'
|
||
|
|
""",
|
||
|
|
(now, runtime_id, *ids),
|
||
|
|
)
|
||
|
|
self._audit(
|
||
|
|
conn, now, "message.acked", runtime_id, {"message_ids": acked}
|
||
|
|
)
|
||
|
|
return acked
|
||
|
|
|
||
|
|
def delivery_state(self, message_id: int, runtime_id: str) -> str | None:
|
||
|
|
conn = self._connect()
|
||
|
|
try:
|
||
|
|
row = conn.execute(
|
||
|
|
"""
|
||
|
|
SELECT state FROM message_deliveries
|
||
|
|
WHERE message_id = ? AND recipient_runtime_id = ?
|
||
|
|
""",
|
||
|
|
(message_id, runtime_id),
|
||
|
|
).fetchone()
|
||
|
|
return None if row is None else str(row["state"])
|
||
|
|
finally:
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
def audit_events(self, *, after_id: int = 0) -> list[dict[str, Any]]:
|
||
|
|
conn = self._connect()
|
||
|
|
try:
|
||
|
|
rows = conn.execute(
|
||
|
|
"""
|
||
|
|
SELECT * FROM audit_events
|
||
|
|
WHERE event_id > ? ORDER BY event_id
|
||
|
|
""",
|
||
|
|
(after_id,),
|
||
|
|
).fetchall()
|
||
|
|
return [
|
||
|
|
{
|
||
|
|
"event_id": row["event_id"],
|
||
|
|
"occurred_at": row["occurred_at"],
|
||
|
|
"event": row["event"],
|
||
|
|
"runtime_id": row["runtime_id"],
|
||
|
|
"details": json.loads(row["details_json"]),
|
||
|
|
}
|
||
|
|
for row in rows
|
||
|
|
]
|
||
|
|
finally:
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
def export_audit_jsonl(
|
||
|
|
self,
|
||
|
|
destination: str | os.PathLike[str] | TextIO,
|
||
|
|
*,
|
||
|
|
after_id: int = 0,
|
||
|
|
include_secrets: bool = False,
|
||
|
|
) -> int:
|
||
|
|
def without_secrets(value: Any) -> Any:
|
||
|
|
if isinstance(value, dict):
|
||
|
|
return {
|
||
|
|
key: without_secrets(item)
|
||
|
|
for key, item in value.items()
|
||
|
|
if key != "ownership_token"
|
||
|
|
}
|
||
|
|
if isinstance(value, list):
|
||
|
|
return [without_secrets(item) for item in value]
|
||
|
|
return value
|
||
|
|
|
||
|
|
events = self.audit_events(after_id=after_id)
|
||
|
|
close = False
|
||
|
|
if hasattr(destination, "write"):
|
||
|
|
stream = destination
|
||
|
|
else:
|
||
|
|
target = pathlib.Path(destination)
|
||
|
|
target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||
|
|
fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||
|
|
os.fchmod(fd, 0o600)
|
||
|
|
stream = os.fdopen(fd, "w", encoding="utf-8")
|
||
|
|
close = True
|
||
|
|
try:
|
||
|
|
for event in events:
|
||
|
|
exported = event if include_secrets else without_secrets(event)
|
||
|
|
stream.write(
|
||
|
|
json.dumps(exported, sort_keys=True, separators=(",", ":"))
|
||
|
|
+ "\n"
|
||
|
|
)
|
||
|
|
finally:
|
||
|
|
if close:
|
||
|
|
stream.close()
|
||
|
|
return len(events)
|