@anvia/durable 0.0.0-bootstrap.0 → 0.1.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anvia
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.
package/README.md CHANGED
@@ -1,5 +1,163 @@
1
- # @anvia/durable — bootstrap placeholder
1
+ # @anvia/durable
2
2
 
3
- This version reserves the package for npm Trusted Publishing setup. It contains no runtime implementation and is not intended for application use.
3
+ Run Anvia agents with persisted model and tool checkpoints. Submit work, observe its
4
+ progress, and recover unfinished runs after a process restart.
4
5
 
5
- Future releases will be published from https://github.com/anvia-hq/anvia through its OIDC release workflow.
6
+ Experimental: this first version supports one runtime owner per local SQLite database,
7
+ static local tools, and committed progress events. It does not stream individual tokens.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ pnpm add @anvia/durable @anvia/core @anvia/openai zod
13
+ ```
14
+
15
+ SQLite requires Node.js 22.16 or newer. Use the accompanying core release with the
16
+ durable execution protocol; older core versions are rejected when importing this package.
17
+
18
+ ## Quickstart
19
+
20
+ ```ts
21
+ import { Agent } from "@anvia/core/agent";
22
+ import { DurableRuntime } from "@anvia/durable";
23
+ import { SqliteDurableStore } from "@anvia/durable/sqlite";
24
+ import { OpenAIClient } from "@anvia/openai";
25
+
26
+ const client = new OpenAIClient({ apiKey: process.env.OPENAI_API_KEY! });
27
+ const researcher = new Agent({
28
+ id: "researcher",
29
+ model: client.completionModel({ modelId: "gpt-5.6", api: "responses" }),
30
+ instructions: "Produce a concise research brief from the supplied material.",
31
+ });
32
+
33
+ const runtime = await DurableRuntime.open({
34
+ store: new SqliteDurableStore("./anvia-runs.sqlite"),
35
+ agents: [{ agent: researcher, version: "1" }],
36
+ maxConcurrentRuns: 4,
37
+ });
38
+
39
+ try {
40
+ await runtime.resume(); // discover unfinished work from the previous process
41
+ const run = await runtime.submit({
42
+ agentId: researcher.id,
43
+ sessionId: "research-session",
44
+ requestId: "research-job-42",
45
+ prompt: "Summarize these research notes: ...",
46
+ });
47
+
48
+ for await (const event of run.stream()) {
49
+ console.log(event.type, event.data);
50
+ }
51
+ const outcome = await run.result();
52
+ if (outcome.type === "response") console.log(outcome.output);
53
+ } finally {
54
+ await runtime.close();
55
+ }
56
+ ```
57
+
58
+ Keep the runtime alive at application scope in a server. A client disconnect should close
59
+ only that client's subscription. Call `run.cancel()` to explicitly cancel the work.
60
+
61
+ ## What survives a restart
62
+
63
+ - Submissions, deduplicated by `(sessionId, requestId)`; changed content with the same ID is rejected.
64
+ - Completed normalized model responses and tool results, reused during recovery.
65
+ - Pending approvals and questions, with responses accepted once per interaction ID.
66
+ - Session history, operation IDs, model-turn counts, usage, and progress-event cursors.
67
+ - Explicit cancellations, which are never restarted automatically.
68
+
69
+ Interrupted model requests may be sent again. A provider may bill both attempts; usage
70
+ records only completed responses received and committed by this runtime.
71
+
72
+ Tools default to `manual` recovery. Mark read-only tools `safe`, or use `idempotent` only
73
+ when the external service deduplicates requests using `context.operationId`:
74
+
75
+ ```ts
76
+ const runtime = await DurableRuntime.open({
77
+ store: new SqliteDurableStore("./anvia-runs.sqlite"),
78
+ agents: [
79
+ {
80
+ agent: researcher,
81
+ version: "1",
82
+ toolRecovery: { search: "safe", createTicket: "idempotent" },
83
+ },
84
+ ],
85
+ });
86
+ ```
87
+
88
+ The tools in `toolRecovery` must be registered on that agent. A committed tool result is
89
+ never executed again, regardless of policy. An interrupted `manual` tool becomes
90
+ `needs_attention`; inspect the external system and supply its actual result with
91
+ `run.resolveTool(operationId, output)`. Merely selecting `idempotent` does not make an API safe.
92
+
93
+ ## Queues, inspection, and HTTP
94
+
95
+ Use `runtime.listRuns({ sessionId, status: "waiting", limit: 25 })` to discover pending
96
+ approvals. Listings also support agent/status filters and insertion cursors. Inspect the
97
+ full record with `runtime.getRun(id)` and `run.snapshot()`.
98
+
99
+ `runtime.submit(input, { enqueue: true })` persists a successor behind the current session
100
+ run. Different sessions execute up to `maxConcurrentRuns` (default 4). Approvals and recovery
101
+ blocks pause their session queue while freeing capacity for other sessions.
102
+
103
+ Opt into persisted model backoff on a registration:
104
+
105
+ ```ts
106
+ { agent: researcher, version: "1", modelRetry: {
107
+ maxAttempts: 3, initialDelayMs: 1000, maxDelayMs: 30_000,
108
+ } }
109
+ ```
110
+
111
+ The policy applies to core completion-attempt errors, including provider errors, completion
112
+ validation, and completion observers. Attempt counts include interrupted
113
+ requests; retry deadlines survive restart. Tool recovery policies remain separate.
114
+
115
+ `@anvia/server/durable` provides an authorized Fetch handler, and `@anvia/client/durable`
116
+ provides a browser-safe HTTP/SSE client. Reconnect using an atomic snapshot and its event
117
+ cursor. See the [HTTP guide](../../docs/packages/durable.md#http-server-and-client).
118
+
119
+ ## Task graphs
120
+
121
+ `runtime.submitGraph({ sessionId, requestId, tasks })` creates a persisted static DAG.
122
+ Each task specifies `id`, `agentId`, `prompt`, and optional `dependsOn` task IDs. Independent
123
+ nodes run concurrently; a dependent starts only after all prerequisites return successfully.
124
+ Their committed outputs are supplied as JSON data in the dependent's input.
125
+
126
+ `graph.snapshot()` exposes nodes, edges, outputs, interactions, waiting reasons, and an event
127
+ cursor. `graph.stream()` emits committed task events; refresh the snapshot for live graph
128
+ state. Use each node's `runId` for existing approval, retry, and reconciliation methods.
129
+ `runtime.resume()` recovers eligible graph nodes after restart. Approvals and uncertain
130
+ external effects still require explicit decisions.
131
+
132
+ The HTTP client exposes `submitGraph`, `graphSnapshot`, `listGraphs`, `streamGraph`, and
133
+ `cancelGraph`. See the [task graph guide](../../docs/packages/durable.md#task-dependencies-and-exposed-graphs).
134
+ This version exposes graph data and execution;
135
+ Studio visualization and durable core Pipeline execution remain future work.
136
+
137
+ ## Custom tasks and dynamic ownership
138
+
139
+ Use `defineTask()` for schema-validated input, checkpoints, and output. Register definitions
140
+ in `DurableRuntime.open({ store, tasks, agents })`, then call `runtime.submitTask()`.
141
+ A phase can spawn custom or agent children, persist a child join, wait for a timer or named
142
+ signal, and journal external effects with explicit recovery policies. Waiting phases release
143
+ capacity; cancelling a tree fences new work and waits for owned callbacks to settle.
144
+
145
+ `task.graph()` exposes the complete bounded ownership tree and current waits. `task.stream()`
146
+ emits committed tree changes. The HTTP/client integration also exposes custom-task submission,
147
+ signals, reconciliation, cancellation, snapshots, and graph/event reads. See the
148
+ [custom task guide](../../docs/packages/durable-tasks.md) for contracts, examples, and limits.
149
+
150
+ ## Operational controls
151
+
152
+ Use `runtime.health()` for readiness, `runtime.metrics()` for aggregate status counts, and
153
+ `onFatalError` to notify your supervisor. Storage failures stop both schedulers and reject
154
+ new work. Configurable pending-work, payload, and operation limits bound admission.
155
+
156
+ `@anvia/durable/maintenance` provides offline `backupSqlite` and `restoreSqlite`; both write
157
+ new files only. See the [operations guide](../../docs/packages/durable-operations.md) for
158
+ restore, upgrade, rollback, and rollout procedures. The package remains experimental.
159
+
160
+ ## Learn more
161
+
162
+ - [Execution, recovery, and API guide](../../docs/packages/durable.md)
163
+ - [Anvia overview](../../README.md)
@@ -0,0 +1,499 @@
1
+ import {
2
+ eventTypeSchema,
3
+ graphListSchema,
4
+ json,
5
+ listOptionsSchema,
6
+ nonblank,
7
+ parseGraphRecord,
8
+ parseOperation,
9
+ parseRun,
10
+ parseTask,
11
+ taskListSchema
12
+ } from "./chunk-KIMHK6CE.js";
13
+
14
+ // src/metrics.ts
15
+ function sqliteMetrics(db) {
16
+ const statuses = (table) => Object.fromEntries(
17
+ db.prepare(`SELECT status, COUNT(*) AS n FROM ${table} GROUP BY status`).all().map((row) => [String(row.status), Number(row.n)])
18
+ );
19
+ const oldest = db.prepare(`SELECT MIN(created) AS oldest FROM (
20
+ SELECT MIN(json_extract(record, '$.createdAt')) AS created FROM anvia_durable_runs WHERE status NOT IN ('completed', 'failed', 'cancelled')
21
+ UNION ALL SELECT MIN(json_extract(record, '$.createdAt')) FROM anvia_durable_tasks WHERE status NOT IN ('completed', 'failed', 'cancelled')
22
+ )`).get().oldest;
23
+ const count = (table) => Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get().n);
24
+ return {
25
+ observedAt: (/* @__PURE__ */ new Date()).toISOString(),
26
+ runs: statuses("anvia_durable_runs"),
27
+ tasks: statuses("anvia_durable_tasks"),
28
+ oldestPendingAgeMs: typeof oldest === "string" ? Math.max(0, Date.now() - Date.parse(oldest)) : 0,
29
+ operations: count("anvia_durable_operations"),
30
+ events: count("anvia_durable_events"),
31
+ databaseBytes: Number(db.prepare("PRAGMA page_count").get().page_count) * Number(db.prepare("PRAGMA page_size").get().page_size)
32
+ };
33
+ }
34
+
35
+ // src/tasks/sqlite.ts
36
+ var taskTables = `
37
+ CREATE TABLE IF NOT EXISTS anvia_durable_tasks (
38
+ id TEXT PRIMARY KEY, root_id TEXT NOT NULL, parent_id TEXT NOT NULL,
39
+ session_id TEXT NOT NULL, key TEXT NOT NULL, status TEXT NOT NULL, ready_order INTEGER NOT NULL, record TEXT NOT NULL,
40
+ UNIQUE(session_id, parent_id, key)
41
+ );
42
+ CREATE INDEX IF NOT EXISTS anvia_durable_task_root ON anvia_durable_tasks(root_id);
43
+ CREATE INDEX IF NOT EXISTS anvia_durable_task_parent ON anvia_durable_tasks(parent_id);
44
+ CREATE INDEX IF NOT EXISTS anvia_durable_task_status ON anvia_durable_tasks(status);
45
+ `;
46
+ function taskTransaction(db, assertActive) {
47
+ const read = (sql, ...parameters) => {
48
+ assertActive();
49
+ return db.prepare(sql).all(...parameters).map((row) => parseTask(JSON.parse(String(row.record))));
50
+ };
51
+ return {
52
+ getTask: (id) => read("SELECT record FROM anvia_durable_tasks WHERE id = ?", id)[0],
53
+ findTask: (session, parent, key) => read(
54
+ "SELECT record FROM anvia_durable_tasks WHERE session_id = ? AND parent_id = ? AND key = ?",
55
+ session,
56
+ parent ?? "",
57
+ key
58
+ )[0],
59
+ taskChildren: (id) => read("SELECT record FROM anvia_durable_tasks WHERE parent_id = ? ORDER BY rowid", id),
60
+ taskTree: (root) => read("SELECT record FROM anvia_durable_tasks WHERE root_id = ? ORDER BY rowid", root),
61
+ putTask: (value) => {
62
+ assertActive();
63
+ const task = parseTask(value);
64
+ db.prepare(`INSERT INTO anvia_durable_tasks(id, root_id, parent_id, session_id, key, status, record, ready_order)
65
+ VALUES (?, ?, ?, ?, ?, ?, ?, (SELECT COALESCE(MAX(sequence), 0) + 1 FROM anvia_durable_events))
66
+ ON CONFLICT(id) DO UPDATE SET status = excluded.status, record = excluded.record, ready_order = excluded.ready_order`).run(
67
+ task.id,
68
+ task.rootId,
69
+ task.parentId ?? "",
70
+ task.sessionId,
71
+ task.key,
72
+ task.status,
73
+ JSON.stringify(task)
74
+ );
75
+ }
76
+ };
77
+ }
78
+ function listTasks(db, input) {
79
+ const options = taskListSchema.parse(input);
80
+ const limit = options.limit ?? 50;
81
+ const rows = db.prepare(`SELECT rowid AS cursor, record FROM anvia_durable_tasks WHERE parent_id = ''
82
+ AND rowid > ? ${options.sessionId === void 0 ? "" : "AND session_id = ?"} ORDER BY rowid LIMIT ?`).all(
83
+ options.after ?? 0,
84
+ ...options.sessionId === void 0 ? [] : [options.sessionId],
85
+ limit + 1
86
+ );
87
+ const page = rows.slice(0, limit);
88
+ return {
89
+ tasks: page.map((row) => parseTask(JSON.parse(String(row.record)))),
90
+ ...rows.length > limit ? { nextCursor: Number(page.at(-1).cursor) } : {}
91
+ };
92
+ }
93
+ function scheduleTasks(db, excluded, limit) {
94
+ const filter = excluded.length === 0 ? "" : `AND id NOT IN (${excluded.map(() => "?").join(",")})`;
95
+ return db.prepare(
96
+ `SELECT id FROM anvia_durable_tasks WHERE status IN ('pending', 'running') ${filter} ORDER BY ready_order, rowid LIMIT ?`
97
+ ).all(...excluded, limit).map((row) => String(row.id));
98
+ }
99
+ function unsettledTaskRoots(db, after) {
100
+ const rows = db.prepare(`SELECT rowid AS cursor, id FROM anvia_durable_tasks
101
+ WHERE parent_id = '' AND status NOT IN ('completed', 'failed', 'cancelled') AND rowid > ? ORDER BY rowid LIMIT 100`).all(after);
102
+ return {
103
+ ids: rows.map((row) => String(row.id)),
104
+ ...rows.length === 100 ? { cursor: Number(rows.at(-1).cursor) } : {}
105
+ };
106
+ }
107
+
108
+ // src/sqlite.ts
109
+ import { hostname } from "node:os";
110
+ import { DatabaseSync } from "node:sqlite";
111
+ var SqliteDurableStore = class {
112
+ database;
113
+ token = globalThis.crypto.randomUUID();
114
+ acquired = false;
115
+ closed = false;
116
+ inTransaction = false;
117
+ constructor(path) {
118
+ nonblank(path, "SQLite path");
119
+ this.database = new DatabaseSync(path);
120
+ try {
121
+ if (this.database.prepare("SELECT 1 FROM sqlite_master WHERE name = 'anvia_durable_backup'").get() !== void 0)
122
+ throw new Error("Sealed durable backup; restore it into a new database before opening.");
123
+ this.database.exec(`
124
+ PRAGMA busy_timeout = 5000;
125
+ PRAGMA journal_mode = WAL;
126
+ PRAGMA synchronous = FULL;
127
+ CREATE TABLE IF NOT EXISTS anvia_durable_owner (
128
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
129
+ version INTEGER NOT NULL, token TEXT, pid INTEGER, host TEXT
130
+ );
131
+ INSERT OR IGNORE INTO anvia_durable_owner(singleton, version) VALUES (1, 3);
132
+ CREATE TABLE IF NOT EXISTS anvia_durable_runs (
133
+ id TEXT PRIMARY KEY, session_id TEXT NOT NULL, request_id TEXT NOT NULL,
134
+ status TEXT NOT NULL, record TEXT NOT NULL,
135
+ UNIQUE(session_id, request_id)
136
+ );
137
+ CREATE INDEX IF NOT EXISTS anvia_durable_run_session ON anvia_durable_runs(session_id, status);
138
+ CREATE TABLE IF NOT EXISTS anvia_durable_graphs (
139
+ id TEXT PRIMARY KEY, session_id TEXT NOT NULL, request_id TEXT NOT NULL, record TEXT NOT NULL,
140
+ UNIQUE(session_id, request_id)
141
+ );
142
+ CREATE INDEX IF NOT EXISTS anvia_durable_run_graph ON anvia_durable_runs(json_extract(record, '$.graphId'));
143
+ CREATE TABLE IF NOT EXISTS anvia_durable_operations (
144
+ run_id TEXT NOT NULL, key TEXT NOT NULL, record TEXT NOT NULL,
145
+ PRIMARY KEY(run_id, key)
146
+ );
147
+ CREATE TABLE IF NOT EXISTS anvia_durable_events (
148
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
149
+ run_id TEXT NOT NULL, created_at TEXT NOT NULL, type TEXT NOT NULL, data TEXT NOT NULL
150
+ );
151
+ CREATE INDEX IF NOT EXISTS anvia_durable_event_run ON anvia_durable_events(run_id, sequence);
152
+ ${taskTables}
153
+ `);
154
+ const version = this.database.prepare("SELECT version FROM anvia_durable_owner WHERE singleton = 1").get();
155
+ if (version?.version !== 1 && version?.version !== 2 && version?.version !== 3)
156
+ throw new Error("Unsupported durable database schema version.");
157
+ } catch (error) {
158
+ this.database.close();
159
+ throw error;
160
+ }
161
+ }
162
+ acquire() {
163
+ if (this.closed || this.acquired)
164
+ throw new Error("Durable store is closed or already acquired.");
165
+ this.atomic(() => {
166
+ const owner = this.database.prepare("SELECT token, pid, host FROM anvia_durable_owner WHERE singleton = 1").get();
167
+ if (owner.token !== null && (owner.host !== hostname() || processExists(Number(owner.pid)))) {
168
+ throw new Error(
169
+ "Durable store already has a live owner. Only one runtime per database is supported."
170
+ );
171
+ }
172
+ this.database.prepare(
173
+ "UPDATE anvia_durable_owner SET token = ?, pid = ?, host = ?, version = 3 WHERE singleton = 1"
174
+ ).run(this.token, process.pid, hostname());
175
+ });
176
+ this.acquired = true;
177
+ }
178
+ close() {
179
+ if (this.closed) return;
180
+ try {
181
+ if (this.acquired) {
182
+ this.atomic(() => {
183
+ this.database.prepare(
184
+ "UPDATE anvia_durable_owner SET token = NULL, pid = NULL, host = NULL WHERE singleton = 1 AND token = ?"
185
+ ).run(this.token);
186
+ });
187
+ }
188
+ } finally {
189
+ this.database.close();
190
+ this.closed = true;
191
+ this.acquired = false;
192
+ }
193
+ }
194
+ transaction(callback) {
195
+ this.assertOpen();
196
+ return this.atomic(() => {
197
+ this.assertOwner();
198
+ let active = true;
199
+ try {
200
+ return callback(
201
+ this.transactionApi(() => {
202
+ if (!active) throw new Error("Durable transaction has finished.");
203
+ })
204
+ );
205
+ } finally {
206
+ active = false;
207
+ }
208
+ });
209
+ }
210
+ metrics() {
211
+ this.assertOpen();
212
+ return sqliteMetrics(this.database);
213
+ }
214
+ listGraphs(input) {
215
+ this.assertOpen();
216
+ const options = graphListSchema.parse(input);
217
+ const limit = options.limit ?? 50;
218
+ const filter = options.sessionId === void 0 ? "" : "AND session_id = ?";
219
+ const rows = this.database.prepare(`SELECT rowid AS cursor, id, session_id, request_id, json_extract(record, '$.createdAt') AS created_at
220
+ FROM anvia_durable_graphs WHERE rowid > ? ${filter} ORDER BY rowid LIMIT ?`).all(
221
+ options.after ?? 0,
222
+ ...options.sessionId === void 0 ? [] : [options.sessionId],
223
+ limit + 1
224
+ );
225
+ const page = rows.slice(0, limit);
226
+ return {
227
+ graphs: page.map((row) => ({
228
+ id: String(row.id),
229
+ sessionId: String(row.session_id),
230
+ requestId: String(row.request_id),
231
+ createdAt: String(row.created_at)
232
+ })),
233
+ ...rows.length > limit ? { nextCursor: Number(page.at(-1).cursor) } : {}
234
+ };
235
+ }
236
+ listTasks(options) {
237
+ this.assertOpen();
238
+ return listTasks(this.database, options);
239
+ }
240
+ scheduleTasks(excluded, limit) {
241
+ this.assertOpen();
242
+ return scheduleTasks(this.database, excluded, limit);
243
+ }
244
+ unsettledTaskRoots(after) {
245
+ this.assertOpen();
246
+ return unsettledTaskRoots(this.database, after);
247
+ }
248
+ taskEvents(rootId, after, limit) {
249
+ this.assertOpen();
250
+ if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 1e3)
251
+ throw new TypeError("Invalid task event cursor or page size.");
252
+ return this.database.prepare(`SELECT e.* FROM anvia_durable_events e JOIN anvia_durable_tasks t ON t.id = e.run_id
253
+ WHERE t.root_id = ? AND e.sequence > ? ORDER BY e.sequence LIMIT ?`).all(rootId, after, limit).map((row) => ({
254
+ sequence: Number(row.sequence),
255
+ runId: String(row.run_id),
256
+ createdAt: String(row.created_at),
257
+ type: eventTypeSchema.parse(row.type),
258
+ data: json(JSON.parse(String(row.data)))
259
+ }));
260
+ }
261
+ graphEvents(graphId, after, limit) {
262
+ this.assertOpen();
263
+ if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 1e3)
264
+ throw new TypeError("Invalid graph event cursor or page size.");
265
+ return this.database.prepare(`SELECT e.* FROM anvia_durable_events e JOIN anvia_durable_runs r ON r.id = e.run_id
266
+ WHERE json_extract(r.record, '$.graphId') = ? AND e.sequence > ? ORDER BY e.sequence LIMIT ?`).all(graphId, after, limit).map((row) => ({
267
+ sequence: Number(row.sequence),
268
+ runId: String(row.run_id),
269
+ createdAt: String(row.created_at),
270
+ type: eventTypeSchema.parse(row.type),
271
+ data: json(JSON.parse(String(row.data)))
272
+ }));
273
+ }
274
+ list(input) {
275
+ this.assertOpen();
276
+ const options = listOptionsSchema.parse(input);
277
+ const conditions = ["rowid > ?"];
278
+ const parameters = [options.after ?? 0];
279
+ for (const [column, value] of [
280
+ ["session_id", options.sessionId],
281
+ ["json_extract(record, '$.agentId')", options.agentId],
282
+ ["status", options.status]
283
+ ]) {
284
+ if (value !== void 0) {
285
+ conditions.push(`${column} = ?`);
286
+ parameters.push(value);
287
+ }
288
+ }
289
+ const limit = options.limit ?? 50;
290
+ const rows = this.database.prepare(
291
+ `SELECT rowid AS cursor, record FROM anvia_durable_runs WHERE ${conditions.join(" AND ")} ORDER BY rowid LIMIT ?`
292
+ ).all(...parameters, limit + 1);
293
+ const page = rows.slice(0, limit);
294
+ return {
295
+ runs: page.map((row) => {
296
+ const {
297
+ id,
298
+ agentId,
299
+ sessionId,
300
+ requestId,
301
+ status,
302
+ createdAt,
303
+ updatedAt,
304
+ error,
305
+ blockedOperation,
306
+ nextAttemptAt
307
+ } = parseRun(JSON.parse(String(row.record)));
308
+ return {
309
+ id,
310
+ agentId,
311
+ sessionId,
312
+ requestId,
313
+ status,
314
+ createdAt,
315
+ updatedAt,
316
+ ...error === void 0 ? {} : { error },
317
+ ...blockedOperation === void 0 ? {} : { blockedOperation },
318
+ ...nextAttemptAt === void 0 ? {} : { nextAttemptAt }
319
+ };
320
+ }),
321
+ ...rows.length > limit ? { nextCursor: Number(page.at(-1).cursor) } : {}
322
+ };
323
+ }
324
+ schedule(now, limit, excludedSessions) {
325
+ this.assertOpen();
326
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new TypeError("Invalid scheduler limit.");
327
+ const exclude = excludedSessions.length === 0 ? "" : `AND r.session_id NOT IN (${excludedSessions.map(() => "?").join(",")})`;
328
+ const eligible = `FROM anvia_durable_runs r
329
+ WHERE r.status IN ('queued', 'pending', 'running', 'retry_wait') ${exclude}
330
+ AND NOT EXISTS (SELECT 1 FROM anvia_durable_runs previous
331
+ WHERE previous.session_id = r.session_id AND previous.rowid < r.rowid
332
+ AND previous.status NOT IN ('completed', 'failed', 'cancelled'))
333
+ AND NOT EXISTS (SELECT 1 FROM json_each(json_extract(r.record, '$.dependencies')) dependency
334
+ LEFT JOIN anvia_durable_runs prerequisite ON prerequisite.id = dependency.value
335
+ WHERE prerequisite.id IS NULL OR prerequisite.status != 'completed'
336
+ OR json_extract(prerequisite.record, '$.outcome.type') IS NOT 'response')`;
337
+ const ids = this.database.prepare(`SELECT r.id ${eligible}
338
+ AND (r.status != 'retry_wait' OR json_extract(r.record, '$.nextAttemptAt') <= ?)
339
+ ORDER BY r.rowid LIMIT ?`).all(...excludedSessions, now, limit).map((row) => String(row.id));
340
+ const wake = this.database.prepare(`SELECT MIN(json_extract(r.record, '$.nextAttemptAt')) AS deadline
341
+ ${eligible} AND r.status = 'retry_wait' AND json_extract(r.record, '$.nextAttemptAt') > ?`).get(...excludedSessions, now);
342
+ return { ids, ...typeof wake?.deadline === "string" ? { nextAttemptAt: wake.deadline } : {} };
343
+ }
344
+ events(runId, after, limit) {
345
+ this.assertOpen();
346
+ if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) {
347
+ throw new TypeError("Invalid durable event cursor or page size.");
348
+ }
349
+ return this.database.prepare(
350
+ "SELECT * FROM anvia_durable_events WHERE run_id = ? AND sequence > ? ORDER BY sequence LIMIT ?"
351
+ ).all(runId, after, limit).map((row) => ({
352
+ sequence: Number(row.sequence),
353
+ runId: String(row.run_id),
354
+ createdAt: String(row.created_at),
355
+ type: eventTypeSchema.parse(row.type),
356
+ data: json(JSON.parse(String(row.data)))
357
+ }));
358
+ }
359
+ transactionApi(assertActive) {
360
+ const readRun = (sql, ...parameters) => {
361
+ assertActive();
362
+ const row = this.database.prepare(sql).get(...parameters);
363
+ return row === void 0 ? void 0 : parseRun(JSON.parse(String(row.record)));
364
+ };
365
+ return {
366
+ ...taskTransaction(this.database, assertActive),
367
+ pendingCounts: () => {
368
+ assertActive();
369
+ const count = (table) => Number(
370
+ this.database.prepare(
371
+ `SELECT COUNT(*) AS n FROM ${table} WHERE status NOT IN ('completed', 'failed', 'cancelled')`
372
+ ).get().n
373
+ );
374
+ return { runs: count("anvia_durable_runs"), tasks: count("anvia_durable_tasks") };
375
+ },
376
+ operationCount: (id) => {
377
+ assertActive();
378
+ return Number(
379
+ this.database.prepare("SELECT COUNT(*) AS n FROM anvia_durable_operations WHERE run_id = ?").get(id).n
380
+ );
381
+ },
382
+ getGraph: (id) => {
383
+ assertActive();
384
+ const row = this.database.prepare("SELECT record FROM anvia_durable_graphs WHERE id = ?").get(id);
385
+ return row === void 0 ? void 0 : parseGraphRecord(JSON.parse(String(row.record)));
386
+ },
387
+ findGraphRequest: (session, request) => {
388
+ assertActive();
389
+ const row = this.database.prepare(
390
+ "SELECT record FROM anvia_durable_graphs WHERE session_id = ? AND request_id = ?"
391
+ ).get(session, request);
392
+ return row === void 0 ? void 0 : parseGraphRecord(JSON.parse(String(row.record)));
393
+ },
394
+ putGraph: (graph) => {
395
+ assertActive();
396
+ this.database.prepare(`INSERT INTO anvia_durable_graphs(id, session_id, request_id, record) VALUES (?, ?, ?, ?)
397
+ ON CONFLICT(id) DO UPDATE SET record = excluded.record`).run(
398
+ graph.id,
399
+ graph.submission.sessionId,
400
+ graph.submission.requestId,
401
+ JSON.stringify(parseGraphRecord(json(graph)))
402
+ );
403
+ },
404
+ getRun: (id) => readRun("SELECT record FROM anvia_durable_runs WHERE id = ?", id),
405
+ findRequest: (session, request) => readRun(
406
+ "SELECT record FROM anvia_durable_runs WHERE session_id = ? AND request_id = ?",
407
+ session,
408
+ request
409
+ ),
410
+ activeRun: (session) => readRun(
411
+ "SELECT record FROM anvia_durable_runs WHERE session_id = ? AND status NOT IN ('completed', 'failed', 'cancelled') LIMIT 1",
412
+ session
413
+ ),
414
+ latestCompleted: (session) => readRun(
415
+ "SELECT record FROM anvia_durable_runs WHERE session_id = ? AND status = 'completed' ORDER BY rowid DESC LIMIT 1",
416
+ session
417
+ ),
418
+ hasLaterStartedRun: (id) => {
419
+ assertActive();
420
+ return this.database.prepare(`SELECT 1 FROM anvia_durable_runs later
421
+ JOIN anvia_durable_runs current ON later.session_id = current.session_id
422
+ WHERE current.id = ? AND later.rowid > current.rowid AND (later.status NOT IN ('queued', 'cancelled') OR json_extract(later.record, '$.startedAt') IS NOT NULL) LIMIT 1`).get(id) !== void 0;
423
+ },
424
+ putRun: (run) => {
425
+ assertActive();
426
+ const serialized = JSON.stringify(parseRun(json(run)));
427
+ this.database.prepare(`INSERT INTO anvia_durable_runs(id, session_id, request_id, status, record)
428
+ VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET status = excluded.status, record = excluded.record`).run(run.id, run.sessionId, run.requestId, run.status, serialized);
429
+ },
430
+ getOperation: (runId, key) => {
431
+ assertActive();
432
+ const row = this.database.prepare("SELECT record FROM anvia_durable_operations WHERE run_id = ? AND key = ?").get(runId, key);
433
+ return row === void 0 ? void 0 : parseOperation(JSON.parse(String(row.record)));
434
+ },
435
+ operations: (runId) => {
436
+ assertActive();
437
+ return this.database.prepare("SELECT record FROM anvia_durable_operations WHERE run_id = ? ORDER BY rowid").all(runId).map((row) => parseOperation(JSON.parse(String(row.record))));
438
+ },
439
+ putOperation: (runId, operation) => {
440
+ assertActive();
441
+ this.database.prepare(`INSERT INTO anvia_durable_operations(run_id, key, record) VALUES (?, ?, ?)
442
+ ON CONFLICT(run_id, key) DO UPDATE SET record = excluded.record`).run(runId, operation.key, JSON.stringify(parseOperation(json(operation))));
443
+ },
444
+ appendEvent: (runId, type, data) => {
445
+ assertActive();
446
+ this.database.prepare(
447
+ "INSERT INTO anvia_durable_events(run_id, created_at, type, data) VALUES (?, ?, ?, ?)"
448
+ ).run(runId, (/* @__PURE__ */ new Date()).toISOString(), type, JSON.stringify(json(data)));
449
+ },
450
+ cursor: () => {
451
+ assertActive();
452
+ return Number(
453
+ this.database.prepare("SELECT COALESCE(MAX(sequence), 0) AS cursor FROM anvia_durable_events").get().cursor
454
+ );
455
+ }
456
+ };
457
+ }
458
+ atomic(callback) {
459
+ if (this.inTransaction) throw new Error("Nested durable transactions are not supported.");
460
+ this.database.exec("BEGIN IMMEDIATE");
461
+ this.inTransaction = true;
462
+ try {
463
+ const result = callback();
464
+ if (result !== null && typeof result === "object" && "then" in result) {
465
+ void Promise.resolve(result).catch(() => {
466
+ });
467
+ throw new TypeError("Durable transactions must be synchronous.");
468
+ }
469
+ this.database.exec("COMMIT");
470
+ return result;
471
+ } catch (error) {
472
+ this.database.exec("ROLLBACK");
473
+ throw error;
474
+ } finally {
475
+ this.inTransaction = false;
476
+ }
477
+ }
478
+ assertOpen() {
479
+ if (this.closed || !this.acquired) throw new Error("Durable store is not acquired.");
480
+ }
481
+ assertOwner() {
482
+ const owner = this.database.prepare("SELECT token FROM anvia_durable_owner WHERE singleton = 1").get();
483
+ if (owner?.token !== this.token) throw new Error("Durable store ownership was lost.");
484
+ }
485
+ };
486
+ function processExists(pid) {
487
+ if (!Number.isSafeInteger(pid) || pid <= 0) return true;
488
+ try {
489
+ process.kill(pid, 0);
490
+ return true;
491
+ } catch (error) {
492
+ return error.code !== "ESRCH";
493
+ }
494
+ }
495
+
496
+ export {
497
+ SqliteDurableStore
498
+ };
499
+ //# sourceMappingURL=chunk-7WOOJGHA.js.map