nexura-postgres 0.4.0__tar.gz

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.
@@ -0,0 +1,79 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ .venv/
25
+ venv/
26
+ ENV/
27
+
28
+ # Testing and linting
29
+ .pytest_cache/
30
+ .coverage
31
+ htmlcov/
32
+ .ruff_cache/
33
+ .mypy_cache/
34
+
35
+ # IDE
36
+ .idea/
37
+ .vscode/
38
+ *.swp
39
+ *.swo
40
+
41
+ # Local databases and run state (never commit)
42
+ *.sqlite3
43
+ *.sqlite3-shm
44
+ *.sqlite3-wal
45
+ /:memory:
46
+ .agy/
47
+
48
+ # Node / Studio build output
49
+ node_modules/
50
+ studio-web/dist/
51
+ clients/typescript/dist/
52
+ studio-web/test-results/
53
+ studio-web/playwright-report/
54
+ *.tgz
55
+
56
+ # Never commit (added 2026-10-18 before the first local git commit)
57
+ # Local databases, trigger encryption keys and other key material
58
+ *.db
59
+ *.key
60
+ *.sqlite3.trigger.key
61
+ .env
62
+ .env.*
63
+ !.env.example
64
+ *.pem
65
+ # Local run logs at the project root (gate, suite, browser)
66
+ *.log
67
+ .*.log
68
+ # Autopilot scratch (backups, repros, agent prompts); history lives in oss-completion-blueprint/
69
+ /.autopilot/
70
+ # Tool caches and local agent state
71
+ .mypy_cache/
72
+ .claude/
73
+ .playwright-mcp/
74
+ # Closed edition: its own nested local git repository, never part of this one
75
+ /enterprise/
76
+ # Internal review archive from the founder; not part of the public repository
77
+ /codex reviews/
78
+ # Demo-video recorder (local only; videos go to .autopilot)
79
+ studio-web/demos/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Liceron
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.5
2
+ Name: nexura-postgres
3
+ Version: 0.4.0
4
+ Summary: PostgreSQL backends for nexura-runtime: durable run and session storage, and a pgvector vector store.
5
+ Project-URL: Homepage, https://github.com/Liceron/nexura
6
+ Project-URL: Documentation, https://github.com/Liceron/nexura/blob/main/docs/README.md
7
+ Project-URL: Source, https://github.com/Liceron/nexura
8
+ Project-URL: Issues, https://github.com/Liceron/nexura/issues
9
+ Project-URL: Changelog, https://github.com/Liceron/nexura/blob/main/CHANGELOG.md
10
+ Author-email: Liceron <info@liceron.in>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: agents,asyncpg,pgvector,postgres,postgresql,workflows
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Database
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: <3.13,>=3.12
21
+ Requires-Dist: asyncpg<1.0.0,>=0.30.0
22
+ Requires-Dist: nexura-agents==0.4.0
23
+ Requires-Dist: nexura-runtime==0.4.0
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Nexura Postgres
27
+
28
+ PostgreSQL run/session storage and a pgvector vector store for nexura-runtime.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install nexura-postgres
34
+ ```
35
+
36
+ Alpha (0.4): `pip install nexura-postgres`. See [package installation](https://github.com/Liceron/nexura/blob/main/docs/packages.md).
37
+
38
+ ## Minimal example
39
+
40
+ This example constructs a repository without connecting. Call `await repository.init_db()` against a running PostgreSQL server to create the run/session/event tables. pgvector needs the `vector` extension. SQLite needs no add-on; without this package the runtime factory raises `MissingDependencyError` naming `nexura-postgres`.
41
+
42
+ ```python
43
+ from nexura_runtime.storage import StorageRepository, get_storage_repository
44
+
45
+ from nexura_postgres import PostgresRepository
46
+
47
+ repository = get_storage_repository("postgresql://nexura:secret@localhost:5432/nexura")
48
+ assert isinstance(repository, PostgresRepository)
49
+ assert isinstance(repository, StorageRepository) # same protocol as SQLiteRepository
50
+ print(type(repository).__name__, repository.dsn)
51
+ ```
52
+
53
+ ## Durable workflows and agents
54
+
55
+ `PostgresWorkflowStore(dsn)` is the workflow checkpoint store for `WorkflowEngine` on several
56
+ hosts (revisioned checkpoints, history, single-use approvals and the execution journal on the
57
+ same pool); `nexura-runtime serve --workflow-state-url postgresql://...` uses it for the
58
+ service, together with `PostgresTriggerStore(dsn)`, the trigger job queue (webhooks, receipts,
59
+ schedules and job leases on the database clock). `PostgresCheckpointStore(dsn)` does the same
60
+ for SDK agent runs. See
61
+ [Durable execution](https://github.com/Liceron/nexura/blob/main/docs/durable-execution.md).
62
+
63
+ ## Status and limits
64
+
65
+ > Status: **alpha** (0.4), Python 3.12 (the same as `nexura-runtime`). Tested against a local
66
+ > disposable PostgreSQL service with pgvector; never against a managed cloud database.
67
+
68
+ ## Documentation and licence
69
+
70
+ [Main documentation](https://github.com/Liceron/nexura/blob/main/docs/README.md) · [Full guide](https://github.com/Liceron/nexura/blob/main/docs/knowledge.md). MIT, Copyright (c) 2026 Liceron. See [LICENSE](https://github.com/Liceron/nexura/blob/main/packages/nexura-postgres/LICENSE).
@@ -0,0 +1,45 @@
1
+ # Nexura Postgres
2
+
3
+ PostgreSQL run/session storage and a pgvector vector store for nexura-runtime.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install nexura-postgres
9
+ ```
10
+
11
+ Alpha (0.4): `pip install nexura-postgres`. See [package installation](https://github.com/Liceron/nexura/blob/main/docs/packages.md).
12
+
13
+ ## Minimal example
14
+
15
+ This example constructs a repository without connecting. Call `await repository.init_db()` against a running PostgreSQL server to create the run/session/event tables. pgvector needs the `vector` extension. SQLite needs no add-on; without this package the runtime factory raises `MissingDependencyError` naming `nexura-postgres`.
16
+
17
+ ```python
18
+ from nexura_runtime.storage import StorageRepository, get_storage_repository
19
+
20
+ from nexura_postgres import PostgresRepository
21
+
22
+ repository = get_storage_repository("postgresql://nexura:secret@localhost:5432/nexura")
23
+ assert isinstance(repository, PostgresRepository)
24
+ assert isinstance(repository, StorageRepository) # same protocol as SQLiteRepository
25
+ print(type(repository).__name__, repository.dsn)
26
+ ```
27
+
28
+ ## Durable workflows and agents
29
+
30
+ `PostgresWorkflowStore(dsn)` is the workflow checkpoint store for `WorkflowEngine` on several
31
+ hosts (revisioned checkpoints, history, single-use approvals and the execution journal on the
32
+ same pool); `nexura-runtime serve --workflow-state-url postgresql://...` uses it for the
33
+ service, together with `PostgresTriggerStore(dsn)`, the trigger job queue (webhooks, receipts,
34
+ schedules and job leases on the database clock). `PostgresCheckpointStore(dsn)` does the same
35
+ for SDK agent runs. See
36
+ [Durable execution](https://github.com/Liceron/nexura/blob/main/docs/durable-execution.md).
37
+
38
+ ## Status and limits
39
+
40
+ > Status: **alpha** (0.4), Python 3.12 (the same as `nexura-runtime`). Tested against a local
41
+ > disposable PostgreSQL service with pgvector; never against a managed cloud database.
42
+
43
+ ## Documentation and licence
44
+
45
+ [Main documentation](https://github.com/Liceron/nexura/blob/main/docs/README.md) · [Full guide](https://github.com/Liceron/nexura/blob/main/docs/knowledge.md). MIT, Copyright (c) 2026 Liceron. See [LICENSE](https://github.com/Liceron/nexura/blob/main/packages/nexura-postgres/LICENSE).
@@ -0,0 +1,52 @@
1
+ [project]
2
+ name = "nexura-postgres"
3
+ version = "0.4.0"
4
+ description = "PostgreSQL backends for nexura-runtime: durable run and session storage, and a pgvector vector store."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12,<3.13"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "Liceron", email = "info@liceron.in" }]
10
+ keywords = ["agents", "workflows", "postgres", "postgresql", "pgvector", "asyncpg"]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Intended Audience :: Developers",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Topic :: Database",
16
+ "Topic :: Software Development :: Libraries :: Python Modules",
17
+ "Typing :: Typed",
18
+ ]
19
+ # Same version as the runtime it plugs into: get_storage_repository("postgresql://...") in the
20
+ # runtime returns this package's PostgresRepository when it is installed.
21
+ # The old `nexura-runtime[postgres]` extra also listed sqlalchemy, which no code imported.
22
+ dependencies = [
23
+ "nexura-runtime==0.4.0",
24
+ "nexura-agents==0.4.0",
25
+ "asyncpg>=0.30.0,<1.0.0",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/Liceron/nexura"
30
+ Documentation = "https://github.com/Liceron/nexura/blob/main/docs/README.md"
31
+ Source = "https://github.com/Liceron/nexura"
32
+ Issues = "https://github.com/Liceron/nexura/issues"
33
+ Changelog = "https://github.com/Liceron/nexura/blob/main/CHANGELOG.md"
34
+
35
+ [tool.uv.sources]
36
+ # Development inside the monorepo uses the workspace copies.
37
+ nexura-runtime = { workspace = true }
38
+ nexura-agents = { workspace = true }
39
+
40
+ [build-system]
41
+ requires = ["hatchling"]
42
+ build-backend = "hatchling.build"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/nexura_postgres"]
46
+
47
+ [tool.hatch.build.targets.sdist]
48
+ include = ["/src", "/README.md", "/LICENSE", "/pyproject.toml"]
49
+ exclude = [
50
+ "**/__pycache__", "**/*.pyc",
51
+ "**/*blueprint*/**", "**/internal/**", "**/product/**", "**/.autopilot/**",
52
+ ]
@@ -0,0 +1,30 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 Liceron
3
+ """PostgreSQL backends: run and session storage, the execution journal, durable SDK agent
4
+ checkpoints (``PostgresCheckpointStore``), durable workflow state
5
+ (``PostgresWorkflowStore``), the trigger job queue (``PostgresTriggerStore``) and the pgvector
6
+ store.
7
+
8
+ Distributed as ``nexura-postgres`` (it depends on ``nexura-runtime`` and asyncpg). The runtime's
9
+ ``get_storage_repository("postgresql://...")`` returns ``PostgresRepository`` when this package
10
+ is installed (``nexura_runtime.modules.optional_packages``); without it, that call raises
11
+ ``MissingDependencyError`` naming ``pip install nexura-postgres``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from nexura_postgres.checkpoint_store import PostgresCheckpointStore
17
+ from nexura_postgres.execution_store import PostgresExecutionStore
18
+ from nexura_postgres.pgvector import PgVectorStoreAdapter
19
+ from nexura_postgres.repository import PostgresRepository
20
+ from nexura_postgres.trigger_store import PostgresTriggerStore
21
+ from nexura_postgres.workflow_store import PostgresWorkflowStore
22
+
23
+ __all__ = [
24
+ "PgVectorStoreAdapter",
25
+ "PostgresCheckpointStore",
26
+ "PostgresRepository",
27
+ "PostgresExecutionStore",
28
+ "PostgresTriggerStore",
29
+ "PostgresWorkflowStore",
30
+ ]
@@ -0,0 +1,296 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 Liceron
3
+ """Postgres checkpoint store for durable SDK agent runs (A6 S5).
4
+
5
+ ``Runner(checkpoint_store=PostgresCheckpointStore(dsn))`` keeps the same
6
+ ``RunCheckpointStore`` protocol as the SQLite store (checkpoints, the recovery claim with
7
+ fenced saves and deletes, and single-use approvals), on Postgres, plus the execution
8
+ journal (``PostgresExecutionStore``) in the same database: the run's fenced lease from its
9
+ first write, per-call intents and effect records. Lease expiry uses the database clock, so
10
+ several hosts can share one database. Every operation on a run is one transaction under a
11
+ per-run advisory lock.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ from typing import Any
18
+
19
+ from nexura_agents.durable import DEFAULT_CLAIM_LEASE_SECONDS, DEFAULT_WORKSPACE, check_fence
20
+ from nexura_agents.errors import ApprovalDecisionError
21
+
22
+ from nexura_postgres.execution_store import PostgresExecutionStore
23
+
24
+ _NOW = "SELECT EXTRACT(EPOCH FROM clock_timestamp())::double precision"
25
+ _SCHEMA = (
26
+ """CREATE TABLE IF NOT EXISTS nexura_sdk_checkpoints (
27
+ workspace_id TEXT NOT NULL,
28
+ run_id TEXT NOT NULL,
29
+ data TEXT NOT NULL,
30
+ updated_at DOUBLE PRECISION NOT NULL,
31
+ PRIMARY KEY (workspace_id, run_id))""",
32
+ """CREATE INDEX IF NOT EXISTS nexura_sdk_checkpoints_updated
33
+ ON nexura_sdk_checkpoints (workspace_id, updated_at, run_id)""",
34
+ """CREATE TABLE IF NOT EXISTS nexura_sdk_run_claims (
35
+ workspace_id TEXT NOT NULL,
36
+ run_id TEXT NOT NULL,
37
+ owner TEXT NOT NULL,
38
+ expires_at DOUBLE PRECISION NOT NULL,
39
+ lease_seconds DOUBLE PRECISION NOT NULL,
40
+ PRIMARY KEY (workspace_id, run_id))""",
41
+ """CREATE TABLE IF NOT EXISTS nexura_sdk_approvals (
42
+ workspace_id TEXT NOT NULL,
43
+ approval_id TEXT NOT NULL,
44
+ payload TEXT NOT NULL,
45
+ expires_at DOUBLE PRECISION NOT NULL,
46
+ consumed_at DOUBLE PRECISION,
47
+ PRIMARY KEY (workspace_id, approval_id))""",
48
+ )
49
+
50
+
51
+ class PostgresCheckpointStore:
52
+ """Durable, multi-host checkpoint store for ``Runner(checkpoint_store=...)``.
53
+
54
+ Give a DSN (a pool is created on first use and closed by ``close()``) or an asyncpg
55
+ pool you own. Tables are created on first use (or call ``init_db()``). Rows are scoped
56
+ by ``workspace_id``. Checkpoints hold the full transcript: protect the database like
57
+ session data. An asyncpg pool belongs to one event loop; use the store from one loop.
58
+ """
59
+
60
+ def __init__(
61
+ self, dsn: str | None = None, *, pool: Any = None, workspace_id: str = DEFAULT_WORKSPACE
62
+ ) -> None:
63
+ if dsn is None and pool is None:
64
+ raise ValueError("Provide a Postgres DSN or pool")
65
+ if not isinstance(workspace_id, str) or not workspace_id:
66
+ raise ValueError("workspace_id must be a nonempty string")
67
+ self.workspace_id = workspace_id
68
+ self._dsn = dsn
69
+ self._pool = pool
70
+ self._owns_pool = pool is None
71
+ self._lock = asyncio.Lock()
72
+ self._ready = False
73
+ self._journal: PostgresExecutionStore | None = None
74
+ self._journal_ready = False
75
+
76
+ async def _get_pool(self) -> Any:
77
+ async with self._lock:
78
+ if self._pool is None:
79
+ import asyncpg # type: ignore[import-untyped]
80
+
81
+ self._pool = await asyncpg.create_pool(self._dsn, min_size=1, max_size=5)
82
+ if not self._ready:
83
+ async with self._pool.acquire() as conn, conn.transaction():
84
+ # Concurrent first use from several processes: create tables once.
85
+ await conn.execute("SELECT pg_advisory_xact_lock(734658102)")
86
+ for statement in _SCHEMA:
87
+ await conn.execute(statement)
88
+ self._ready = True
89
+ return self._pool
90
+
91
+ async def init_db(self) -> None:
92
+ """Create the checkpoint tables and apply the execution journal's migrations."""
93
+ await self._get_pool()
94
+ await self.open_execution_store()
95
+
96
+ async def open_execution_store(self) -> PostgresExecutionStore:
97
+ """The execution journal on the same pool (migrated on first use)."""
98
+ pool = await self._get_pool()
99
+ if self._journal is None:
100
+ self._journal = PostgresExecutionStore(pool=pool)
101
+ if not self._journal_ready:
102
+ await self._journal.init_db()
103
+ self._journal_ready = True
104
+ return self._journal
105
+
106
+ async def _run_lock(self, conn: Any, run_id: str) -> float:
107
+ await conn.execute(
108
+ "SELECT pg_advisory_xact_lock(hashtextextended($1::text || '/' || $2::text, 0))",
109
+ self.workspace_id,
110
+ run_id,
111
+ )
112
+ now: float = await conn.fetchval(_NOW)
113
+ return now
114
+
115
+ async def _held(self, conn: Any, run_id: str) -> Any:
116
+ return await conn.fetchrow(
117
+ "SELECT owner, expires_at, lease_seconds FROM nexura_sdk_run_claims "
118
+ "WHERE workspace_id = $1 AND run_id = $2",
119
+ self.workspace_id,
120
+ run_id,
121
+ )
122
+
123
+ # ------------------------------------------------------------------ checkpoints
124
+
125
+ async def save(self, run_id: str, checkpoint_json: str, owner: str | None = None) -> None:
126
+ """Store the checkpoint of ``run_id``; fenced by the recovery claim in one transaction."""
127
+ pool = await self._get_pool()
128
+ async with pool.acquire() as conn, conn.transaction():
129
+ now = await self._run_lock(conn, run_id)
130
+ row = await self._held(conn, run_id)
131
+ check_fence(
132
+ run_id,
133
+ owner,
134
+ None if row is None else (str(row["owner"]), float(row["expires_at"])),
135
+ now=now,
136
+ )
137
+ await conn.execute(
138
+ "INSERT INTO nexura_sdk_checkpoints (workspace_id, run_id, data, updated_at) "
139
+ "VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id, run_id) DO UPDATE "
140
+ "SET data = EXCLUDED.data, updated_at = EXCLUDED.updated_at",
141
+ self.workspace_id,
142
+ run_id,
143
+ checkpoint_json,
144
+ now,
145
+ )
146
+ if row is not None and owner is not None:
147
+ await conn.execute(
148
+ "UPDATE nexura_sdk_run_claims SET expires_at = $3 "
149
+ "WHERE workspace_id = $1 AND run_id = $2 AND owner = $4",
150
+ self.workspace_id,
151
+ run_id,
152
+ now + float(row["lease_seconds"]),
153
+ owner,
154
+ )
155
+
156
+ async def load(self, run_id: str) -> str | None:
157
+ """Return the checkpoint of ``run_id`` or None."""
158
+ pool = await self._get_pool()
159
+ async with pool.acquire() as conn:
160
+ value = await conn.fetchval(
161
+ "SELECT data FROM nexura_sdk_checkpoints WHERE workspace_id = $1 AND run_id = $2",
162
+ self.workspace_id,
163
+ run_id,
164
+ )
165
+ return None if value is None else str(value)
166
+
167
+ async def delete(self, run_id: str, owner: str | None = None, *, fenced: bool = False) -> None:
168
+ """Remove the checkpoint and claim of ``run_id`` (``fenced``: only if not stale)."""
169
+ pool = await self._get_pool()
170
+ async with pool.acquire() as conn, conn.transaction():
171
+ now = await self._run_lock(conn, run_id)
172
+ if fenced:
173
+ row = await self._held(conn, run_id)
174
+ check_fence(
175
+ run_id,
176
+ owner,
177
+ None if row is None else (str(row["owner"]), float(row["expires_at"])),
178
+ now=now,
179
+ )
180
+ for table in ("nexura_sdk_checkpoints", "nexura_sdk_run_claims"):
181
+ await conn.execute(
182
+ f"DELETE FROM {table} WHERE workspace_id = $1 AND run_id = $2", # noqa: S608
183
+ self.workspace_id,
184
+ run_id,
185
+ )
186
+
187
+ async def list_incomplete(self, limit: int = 100) -> list[str]:
188
+ """Run ids with a checkpoint, oldest update first."""
189
+ pool = await self._get_pool()
190
+ async with pool.acquire() as conn:
191
+ rows = await conn.fetch(
192
+ "SELECT run_id FROM nexura_sdk_checkpoints WHERE workspace_id = $1 "
193
+ "ORDER BY updated_at, run_id LIMIT $2",
194
+ self.workspace_id,
195
+ max(int(limit), 0),
196
+ )
197
+ return [str(row["run_id"]) for row in rows]
198
+
199
+ async def claim(
200
+ self, run_id: str, owner: str, lease_seconds: float = DEFAULT_CLAIM_LEASE_SECONDS
201
+ ) -> bool:
202
+ """Take the exclusive recovery claim of ``run_id`` (database clock); False when held."""
203
+ pool = await self._get_pool()
204
+ async with pool.acquire() as conn, conn.transaction():
205
+ now = await self._run_lock(conn, run_id)
206
+ row = await self._held(conn, run_id)
207
+ if row is not None and float(row["expires_at"]) > now and row["owner"] != owner:
208
+ return False
209
+ await conn.execute(
210
+ "INSERT INTO nexura_sdk_run_claims "
211
+ "(workspace_id, run_id, owner, expires_at, lease_seconds) "
212
+ "VALUES ($1, $2, $3, $4, $5) ON CONFLICT (workspace_id, run_id) DO UPDATE "
213
+ "SET owner = EXCLUDED.owner, expires_at = EXCLUDED.expires_at, "
214
+ "lease_seconds = EXCLUDED.lease_seconds",
215
+ self.workspace_id,
216
+ run_id,
217
+ owner,
218
+ now + float(lease_seconds),
219
+ float(lease_seconds),
220
+ )
221
+ return True
222
+
223
+ async def release(self, run_id: str, owner: str) -> None:
224
+ """Give the claim back (no-op when ``owner`` does not hold it)."""
225
+ pool = await self._get_pool()
226
+ async with pool.acquire() as conn:
227
+ await conn.execute(
228
+ "DELETE FROM nexura_sdk_run_claims "
229
+ "WHERE workspace_id = $1 AND run_id = $2 AND owner = $3",
230
+ self.workspace_id,
231
+ run_id,
232
+ owner,
233
+ )
234
+
235
+ # ------------------------------------------------------------------ approvals
236
+
237
+ async def save_approval(self, approval_id: str, payload: str, expires_at: float) -> None:
238
+ """Persist an immutable approval independently of run checkpoint cleanup."""
239
+ pool = await self._get_pool()
240
+ async with pool.acquire() as conn, conn.transaction():
241
+ await conn.execute(
242
+ "INSERT INTO nexura_sdk_approvals (workspace_id, approval_id, payload, expires_at) "
243
+ "VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id, approval_id) DO NOTHING",
244
+ self.workspace_id,
245
+ approval_id,
246
+ payload,
247
+ float(expires_at),
248
+ )
249
+ row = await conn.fetchrow(
250
+ "SELECT payload, expires_at FROM nexura_sdk_approvals "
251
+ "WHERE workspace_id = $1 AND approval_id = $2",
252
+ self.workspace_id,
253
+ approval_id,
254
+ )
255
+ if row is not None and (row["payload"], float(row["expires_at"])) != (
256
+ payload,
257
+ float(expires_at),
258
+ ):
259
+ raise ApprovalDecisionError("Stored interruption differs from approval envelope.")
260
+
261
+ async def load_approval(self, approval_id: str) -> str | None:
262
+ """Load the original approval payload, including after consumption."""
263
+ pool = await self._get_pool()
264
+ async with pool.acquire() as conn:
265
+ value = await conn.fetchval(
266
+ "SELECT payload FROM nexura_sdk_approvals "
267
+ "WHERE workspace_id = $1 AND approval_id = $2",
268
+ self.workspace_id,
269
+ approval_id,
270
+ )
271
+ return None if value is None else str(value)
272
+
273
+ async def consume_approval(self, approval_id: str, payload: str) -> bool:
274
+ """Consume a matching unexpired approval once, atomically across hosts."""
275
+ pool = await self._get_pool()
276
+ async with pool.acquire() as conn:
277
+ result: str = await conn.execute(
278
+ "UPDATE nexura_sdk_approvals "
279
+ "SET consumed_at = EXTRACT(EPOCH FROM clock_timestamp()) "
280
+ "WHERE workspace_id = $1 AND approval_id = $2 AND payload = $3 "
281
+ "AND consumed_at IS NULL "
282
+ "AND expires_at > EXTRACT(EPOCH FROM clock_timestamp())",
283
+ self.workspace_id,
284
+ approval_id,
285
+ payload,
286
+ )
287
+ return result.rsplit(" ", 1)[-1] == "1"
288
+
289
+ async def close(self) -> None:
290
+ """Close a pool this store created; a supplied pool stays open for its owner."""
291
+ if self._pool is not None and self._owns_pool:
292
+ await self._pool.close()
293
+ self._pool = None
294
+ self._ready = False
295
+ self._journal = None
296
+ self._journal_ready = False