omp-onlyne 0.2.0 → 0.3.0

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/src/store.ts DELETED
@@ -1,189 +0,0 @@
1
- import { Database } from "bun:sqlite";
2
- import { chmodSync, existsSync, mkdirSync } from "node:fs";
3
- import { dirname } from "node:path";
4
- import type { Entry, Storage, ThreadRef, Transaction } from "@onlyne/lib";
5
- import type { ContextRecord } from "./types.ts";
6
- import { bridgeText as t } from "./bridge-text.ts";
7
-
8
- /** Key/value rows under one SQLite file; the library's durable state and the plugin's registry share it. */
9
- export class SqliteStorage implements Storage {
10
- readonly guarantee = "durable" as const;
11
- readonly #db: Database;
12
- #tail: Promise<unknown> = Promise.resolve();
13
- #closed = false;
14
-
15
- constructor(path: string) {
16
- // The store carries trust rows and, while a rotation is unacknowledged, the old private
17
- // key that proves it. It is secret material: 0700 for the directory, 0600 for the files,
18
- // including the WAL and shared-memory sidecars SQLite creates beside them.
19
- const dir = dirname(path);
20
- mkdirSync(dir, { recursive: true, mode: 0o700 });
21
- chmodSync(dir, 0o700);
22
- this.#db = new Database(path, { create: true });
23
- this.#db.exec("PRAGMA journal_mode = WAL");
24
- this.#db.exec("PRAGMA synchronous = FULL");
25
- this.#db.exec("PRAGMA busy_timeout = 5000");
26
- this.#db.exec("CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
27
- for (const file of [path, `${path}-wal`, `${path}-shm`]) {
28
- if (existsSync(file)) chmodSync(file, 0o600);
29
- }
30
- }
31
-
32
- /** Serialize every transaction; SQLite's own BEGIN IMMEDIATE would otherwise race across awaits. */
33
- transaction<T>(work: (tx: Transaction) => Promise<T>): Promise<T> {
34
- const run = () => this.#execute(work);
35
- const result = this.#tail.then(run, run);
36
- this.#tail = result.then(
37
- () => undefined,
38
- () => undefined,
39
- );
40
- return result;
41
- }
42
-
43
- async #execute<T>(work: (tx: Transaction) => Promise<T>): Promise<T> {
44
- if (this.#closed) throw new Error(t("storeClosed"));
45
- this.#db.exec("BEGIN IMMEDIATE");
46
- const tx: Transaction = {
47
- get: async <V>(key: string) => this.#get<V>(key),
48
- put: async <V>(key: string, value: V) => this.#put(key, value),
49
- delete: async (key: string) => {
50
- this.#db.query("DELETE FROM kv WHERE key = ?").run(key);
51
- },
52
- scan: async <V>(prefix: string) => this.#scan<V>(prefix),
53
- };
54
- try {
55
- const value = await work(tx);
56
- this.#db.exec("COMMIT");
57
- return value;
58
- } catch (error) {
59
- try {
60
- this.#db.exec("ROLLBACK");
61
- } catch {
62
- // The transaction may already be gone; preserve the original failure.
63
- }
64
- throw error;
65
- }
66
- }
67
-
68
- #get<T>(key: string): T | undefined {
69
- const row = this.#db.query<{ value: string }, [string]>("SELECT value FROM kv WHERE key = ?").get(key);
70
- return row === null ? undefined : (JSON.parse(row.value) as T);
71
- }
72
-
73
- #put(key: string, value: unknown): void {
74
- this.#db
75
- .query("INSERT INTO kv (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value")
76
- .run(key, JSON.stringify(value));
77
- }
78
-
79
- #scan<T>(prefix: string): Array<{ key: string; value: T }> {
80
- const rows = this.#db
81
- .query<{ key: string; value: string }, [number, string]>(
82
- "SELECT key, value FROM kv WHERE substr(key, 1, ?) = ? ORDER BY key",
83
- )
84
- .all(prefix.length, prefix);
85
- return rows.map(row => ({ key: row.key, value: JSON.parse(row.value) as T }));
86
- }
87
-
88
- close(): void {
89
- if (this.#closed) return;
90
- this.#closed = true;
91
- this.#db.close();
92
- }
93
- }
94
-
95
- const COUNTER_KEY = "meta/context-counter";
96
- const CONTEXT_PREFIX = "context/";
97
-
98
- /**
99
- * Whether a registered context can answer a delivery. Only the session holding this node's
100
- * own send right can: a subagent context reads and drafts, never sends. Advertising one as a
101
- * continuation target hands the sender a message nobody there can reply to, and the sender
102
- * has no way to see that from the alias (caerulea report 4, problem 7).
103
- */
104
- export function canAnswer(record: ContextRecord): boolean {
105
- return record.agent_id !== undefined && record.agent_id === record.main_agent_id;
106
- }
107
-
108
- /** Alias allocation and binding for execution contexts; the store is the authority across restarts. */
109
- export class ContextRegistry {
110
- constructor(private readonly storage: Storage, private readonly nodeId: string, private readonly mainAgentId: string) {}
111
-
112
- /** The main OMP session is addressable like a registered execution context. */
113
- async ensureMain(): Promise<ContextRecord> {
114
- return this.storage.transaction(async tx => {
115
- const key = `${CONTEXT_PREFIX}main`;
116
- const current = await tx.get<ContextRecord>(key);
117
- if (current !== undefined) return current;
118
- const record: ContextRecord = {
119
- alias: "main", node_id: this.nodeId, main_agent_id: this.mainAgentId, agent_id: this.mainAgentId,
120
- summary: t("mainSummary"), state: "idle", created_at: new Date().toISOString(),
121
- };
122
- await tx.put(key, record);
123
- return record;
124
- });
125
- }
126
-
127
- async allocate(thread_ref: ThreadRef | undefined, summary: string, source?: Entry["message"]): Promise<ContextRecord> {
128
- return this.storage.transaction(async tx => {
129
- const counter = (await tx.get<number>(COUNTER_KEY)) ?? 0;
130
- const next = counter + 1;
131
- await tx.put(COUNTER_KEY, next);
132
- const alias = `c-${String(next)}`;
133
- const record: ContextRecord = {
134
- alias,
135
- node_id: this.nodeId,
136
- main_agent_id: this.mainAgentId,
137
- ...(source === undefined ? {} : { source: { sender: source.sender, id: source.id } }),
138
- ...(thread_ref === undefined ? {} : { thread_ref }),
139
- summary,
140
- state: "unbound",
141
- created_at: new Date().toISOString(),
142
- };
143
- await tx.put(`${CONTEXT_PREFIX}${alias}`, record);
144
- return record;
145
- });
146
- }
147
-
148
- async bind(
149
- alias: string,
150
- binding: { agent_id: string; session_file?: string; parent_id?: string },
151
- ): Promise<ContextRecord> {
152
- return this.storage.transaction(async tx => {
153
- const key = `${CONTEXT_PREFIX}${alias}`;
154
- const current = await tx.get<ContextRecord>(key);
155
- if (current === undefined) throw new Error(t("contextNotRegistered", { alias }));
156
- const next: ContextRecord = {
157
- ...current,
158
- agent_id: binding.agent_id,
159
- ...(binding.session_file === undefined ? {} : { session_file: binding.session_file }),
160
- ...(binding.parent_id === undefined ? {} : { parent_id: binding.parent_id }),
161
- state: current.agent_id === binding.agent_id ? current.state : "idle",
162
- bound_at: new Date().toISOString(),
163
- };
164
- await tx.put(key, next);
165
- return next;
166
- });
167
- }
168
-
169
- async find(alias: string): Promise<ContextRecord | undefined> {
170
- return this.storage.transaction(tx => tx.get<ContextRecord>(`${CONTEXT_PREFIX}${alias}`));
171
- }
172
-
173
- async list(): Promise<ContextRecord[]> {
174
- return this.storage.transaction(async tx =>
175
- (await tx.scan<ContextRecord>(CONTEXT_PREFIX)).map(row => row.value),
176
- );
177
- }
178
-
179
- async updateState(alias: string, state: ContextRecord["state"]): Promise<ContextRecord | undefined> {
180
- return this.storage.transaction(async tx => {
181
- const key = `${CONTEXT_PREFIX}${alias}`;
182
- const current = await tx.get<ContextRecord>(key);
183
- if (current === undefined) return undefined;
184
- const next = { ...current, state };
185
- await tx.put(key, next);
186
- return next;
187
- });
188
- }
189
- }