@cello-protocol/daemon 0.0.44 → 0.0.45

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,61 @@
1
+ /**
2
+ * DOD-AGENT-ID-JOINKEY-1 — finish the migration REMOVE-001 started.
3
+ *
4
+ * `agent_name` was the original PRIMARY KEY of `agents`. REMOVE-001 added the stable surrogate
5
+ * `agent_id` **specifically to make names reusable** (retire keeps the rows and FREES the name), and
6
+ * migrated only the parent table. SEVEN child tables were left joining on the column that had just
7
+ * become a mutable, reusable attribute:
8
+ *
9
+ * sessions · seal_interrupted_artifacts · session_tree_leaves · transcript · message_watermarks ·
10
+ * contacts · retry_queue
11
+ *
12
+ * (`retry_queue` is the seventh — it predates REMOVE-001, so REMOVE-001 skipped seven, not six.)
13
+ *
14
+ * The hazard is not stale data. Retire an agent, create a new one with the freed name — a different
15
+ * agent_id and a different K_local keypair — and every `WHERE agent_name = ?` hands the new identity
16
+ * the dead one's transcript, its contact whitelist (an ACCESS-CONTROL list), and its interrupted
17
+ * sessions. Resuming one would seal with the wrong keypair.
18
+ *
19
+ * This module carries `agent_id` into all seven and demotes `agent_name` to what it always was after
20
+ * REMOVE-001: a display label on the parent table, with exactly one source of truth.
21
+ *
22
+ * ── Why this is safe, and why it deletes nothing ────────────────────────────────────────────────
23
+ * Purely client-side. The directory has no `agent_name` column and never sees one (AC1's wire proof:
24
+ * parties are identified by pubkey + session_id; a name reaches the wire only as the self-declared
25
+ * `offered_moniker` display label, which this migration does not touch). No row content is destroyed:
26
+ * every row is COPIED into the new table before the old one is dropped. Retire-reuse orphans need no
27
+ * purge either — once joined on `agent_id`, a new same-named agent simply never matches them.
28
+ *
29
+ * ── Why one transaction ─────────────────────────────────────────────────────────────────────────
30
+ * SQLite cannot alter a PRIMARY KEY, so each table must be REBUILT (create, copy, drop, rename).
31
+ * SQLite DDL is transactional, so all seven rebuilds run inside ONE `BEGIN…COMMIT`: a crash rolls the
32
+ * entire migration back atomically and the daemon reopens on the untouched original schema. There is
33
+ * no half-migrated state to recover from, and therefore no recovery code to get wrong.
34
+ *
35
+ * ── Why the target schema is pinned literally here ──────────────────────────────────────────────
36
+ * The DDL below duplicates what `session-node-manager` / `retry-queue` create on a fresh database.
37
+ * That duplication is DELIBERATE. A migration must migrate to a FIXED target: if it read its target
38
+ * from the live schema, a future column added upstream would silently change what this migration
39
+ * produces on an old database, and the two shapes would drift apart unnoticed. The drift is instead
40
+ * caught by a test that asserts a MIGRATED database's schema is byte-identical to a FRESH one.
41
+ */
42
+ import type { Logger } from "./types.js";
43
+ import type { DaemonDatabase } from "./sqlcipher-db.js";
44
+ export interface AgentIdMigrationResult {
45
+ readonly migrated: boolean;
46
+ /** Rows copied, per rebuilt table. Absent tables are omitted. */
47
+ readonly rowCounts: Readonly<Record<string, number>>;
48
+ }
49
+ /**
50
+ * Re-key the seven child tables from `agent_name` to `agent_id`. Idempotent: a table that already has
51
+ * an `agent_id` column is skipped, so a second call is a no-op. All rebuilds share ONE transaction.
52
+ *
53
+ * Throws (leaving the database untouched) on:
54
+ * - an ambiguous `agent_name → agent_id` map (a reused retired name),
55
+ * - any row whose non-null `agent_name` does not resolve to an agent,
56
+ * - any row count that does not match pre-migration.
57
+ *
58
+ * Never half-commits, never silently drops a row, never invents an agent_id.
59
+ */
60
+ export declare function migrateSessionTablesToAgentId(db: DaemonDatabase, logger: Logger): AgentIdMigrationResult;
61
+ //# sourceMappingURL=agent-id-migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-id-migration.d.ts","sourceRoot":"","sources":["../src/agent-id-migration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAoJxD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACtD;AAyDD;;;;;;;;;;GAUG;AACH,wBAAgB,6BAA6B,CAAC,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,sBAAsB,CA0HxG"}
@@ -0,0 +1,331 @@
1
+ /**
2
+ * DOD-AGENT-ID-JOINKEY-1 — finish the migration REMOVE-001 started.
3
+ *
4
+ * `agent_name` was the original PRIMARY KEY of `agents`. REMOVE-001 added the stable surrogate
5
+ * `agent_id` **specifically to make names reusable** (retire keeps the rows and FREES the name), and
6
+ * migrated only the parent table. SEVEN child tables were left joining on the column that had just
7
+ * become a mutable, reusable attribute:
8
+ *
9
+ * sessions · seal_interrupted_artifacts · session_tree_leaves · transcript · message_watermarks ·
10
+ * contacts · retry_queue
11
+ *
12
+ * (`retry_queue` is the seventh — it predates REMOVE-001, so REMOVE-001 skipped seven, not six.)
13
+ *
14
+ * The hazard is not stale data. Retire an agent, create a new one with the freed name — a different
15
+ * agent_id and a different K_local keypair — and every `WHERE agent_name = ?` hands the new identity
16
+ * the dead one's transcript, its contact whitelist (an ACCESS-CONTROL list), and its interrupted
17
+ * sessions. Resuming one would seal with the wrong keypair.
18
+ *
19
+ * This module carries `agent_id` into all seven and demotes `agent_name` to what it always was after
20
+ * REMOVE-001: a display label on the parent table, with exactly one source of truth.
21
+ *
22
+ * ── Why this is safe, and why it deletes nothing ────────────────────────────────────────────────
23
+ * Purely client-side. The directory has no `agent_name` column and never sees one (AC1's wire proof:
24
+ * parties are identified by pubkey + session_id; a name reaches the wire only as the self-declared
25
+ * `offered_moniker` display label, which this migration does not touch). No row content is destroyed:
26
+ * every row is COPIED into the new table before the old one is dropped. Retire-reuse orphans need no
27
+ * purge either — once joined on `agent_id`, a new same-named agent simply never matches them.
28
+ *
29
+ * ── Why one transaction ─────────────────────────────────────────────────────────────────────────
30
+ * SQLite cannot alter a PRIMARY KEY, so each table must be REBUILT (create, copy, drop, rename).
31
+ * SQLite DDL is transactional, so all seven rebuilds run inside ONE `BEGIN…COMMIT`: a crash rolls the
32
+ * entire migration back atomically and the daemon reopens on the untouched original schema. There is
33
+ * no half-migrated state to recover from, and therefore no recovery code to get wrong.
34
+ *
35
+ * ── Why the target schema is pinned literally here ──────────────────────────────────────────────
36
+ * The DDL below duplicates what `session-node-manager` / `retry-queue` create on a fresh database.
37
+ * That duplication is DELIBERATE. A migration must migrate to a FIXED target: if it read its target
38
+ * from the live schema, a future column added upstream would silently change what this migration
39
+ * produces on an old database, and the two shapes would drift apart unnoticed. The drift is instead
40
+ * caught by a test that asserts a MIGRATED database's schema is byte-identical to a FRESH one.
41
+ */
42
+ const REKEY_TARGETS = [
43
+ {
44
+ table: "sessions",
45
+ backfill: "strict",
46
+ createSql: (t) => `
47
+ CREATE TABLE ${t} (
48
+ session_id TEXT NOT NULL,
49
+ agent_id TEXT NOT NULL,
50
+ counterparty_pubkey TEXT NOT NULL,
51
+ status TEXT NOT NULL,
52
+ created_at INTEGER NOT NULL,
53
+ updated_at INTEGER NOT NULL,
54
+ message_count INTEGER NOT NULL DEFAULT 0,
55
+ interrupted_at TEXT,
56
+ relay_peer_id TEXT,
57
+ relay_addrs TEXT,
58
+ seal_legibility TEXT,
59
+ sealed_root_hex TEXT,
60
+ counterparty_primary_pubkey TEXT,
61
+ PRIMARY KEY (agent_id, session_id)
62
+ )`,
63
+ indexSql: () => [],
64
+ },
65
+ {
66
+ table: "seal_interrupted_artifacts",
67
+ backfill: "strict",
68
+ createSql: (t) => `
69
+ CREATE TABLE ${t} (
70
+ agent_id TEXT NOT NULL,
71
+ session_id TEXT NOT NULL,
72
+ role TEXT NOT NULL,
73
+ own_leaf TEXT NOT NULL,
74
+ counterparty_leaf TEXT NOT NULL,
75
+ merkle_root TEXT NOT NULL,
76
+ nonce TEXT NOT NULL,
77
+ created_at INTEGER NOT NULL,
78
+ PRIMARY KEY (agent_id, session_id)
79
+ )`,
80
+ indexSql: () => [],
81
+ },
82
+ {
83
+ table: "session_tree_leaves",
84
+ backfill: "strict",
85
+ createSql: (t) => `
86
+ CREATE TABLE ${t} (
87
+ agent_id TEXT NOT NULL,
88
+ session_id TEXT NOT NULL,
89
+ leaf_index INTEGER NOT NULL,
90
+ leaf_kind TEXT NOT NULL,
91
+ leaf_hash_hex TEXT NOT NULL,
92
+ created_at INTEGER NOT NULL,
93
+ PRIMARY KEY (agent_id, session_id, leaf_index)
94
+ )`,
95
+ indexSql: () => [],
96
+ },
97
+ {
98
+ table: "transcript",
99
+ backfill: "strict",
100
+ createSql: (t) => `
101
+ CREATE TABLE ${t} (
102
+ agent_id TEXT NOT NULL,
103
+ session_id TEXT NOT NULL,
104
+ sequence INTEGER NOT NULL,
105
+ direction TEXT NOT NULL,
106
+ blob BLOB NOT NULL,
107
+ created_at INTEGER NOT NULL,
108
+ PRIMARY KEY (agent_id, session_id, sequence, direction)
109
+ )`,
110
+ indexSql: () => [],
111
+ },
112
+ {
113
+ table: "message_watermarks",
114
+ backfill: "strict",
115
+ createSql: (t) => `
116
+ CREATE TABLE ${t} (
117
+ agent_id TEXT NOT NULL,
118
+ session_id TEXT NOT NULL,
119
+ last_delivered_seq INTEGER NOT NULL,
120
+ PRIMARY KEY (agent_id, session_id)
121
+ )`,
122
+ indexSql: () => [],
123
+ },
124
+ {
125
+ table: "contacts",
126
+ backfill: "strict",
127
+ createSql: (t) => `
128
+ CREATE TABLE ${t} (
129
+ agent_id TEXT NOT NULL,
130
+ pubkey TEXT NOT NULL,
131
+ added_at INTEGER NOT NULL,
132
+ moniker TEXT,
133
+ PRIMARY KEY (agent_id, pubkey)
134
+ )`,
135
+ indexSql: () => [],
136
+ },
137
+ {
138
+ // The SEVENTH. DOD-LOOP-1 added `agent_name` here so two of the operator's agents could hold
139
+ // awaiting content for the SAME session_id without colliding — then left the agent OUT of the
140
+ // uniqueness constraint, which stayed `UNIQUE(session_id, nonce_hex)`. For an awaiting row,
141
+ // nonce_hex IS the content hash, so two local agents parking identical content in one session
142
+ // collided, and the collision was swallowed into memory and lost on the next restart.
143
+ //
144
+ // The correct constraint falls out of the re-key. It is expressed as a UNIQUE INDEX over
145
+ // COALESCE(agent_id, '') rather than a table constraint, because SQLite treats NULLs as DISTINCT
146
+ // in a UNIQUE constraint: with a bare UNIQUE(agent_id, session_id, nonce_hex), the legacy
147
+ // agent-less direct-retry rows (agent_id NULL) would lose the (session_id, nonce_hex) dedup they
148
+ // have always had. COALESCE folds every agent-less row into one bucket, preserving exactly the
149
+ // old semantics for them while giving each agent its own namespace.
150
+ table: "retry_queue",
151
+ backfill: "nullable",
152
+ createSql: (t) => `
153
+ CREATE TABLE ${t} (
154
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
155
+ session_id TEXT NOT NULL,
156
+ agent_id TEXT,
157
+ nonce_hex TEXT NOT NULL,
158
+ content_blob BLOB NOT NULL,
159
+ queued_at INTEGER NOT NULL,
160
+ attempts INTEGER NOT NULL DEFAULT 1,
161
+ position INTEGER NOT NULL,
162
+ awaiting_ack INTEGER NOT NULL DEFAULT 0,
163
+ content_hash_hex TEXT
164
+ )`,
165
+ indexSql: (t) => [
166
+ `CREATE INDEX IF NOT EXISTS retry_queue_by_session_position ON ${t}(session_id, position ASC)`,
167
+ `CREATE UNIQUE INDEX IF NOT EXISTS retry_queue_agent_session_nonce ON ${t}(COALESCE(agent_id, ''), session_id, nonce_hex)`,
168
+ ],
169
+ },
170
+ ];
171
+ function columnsOf(db, table) {
172
+ return db.prepare(`PRAGMA table_info(${table})`).all().map((c) => c.name);
173
+ }
174
+ function tableExists(db, table) {
175
+ return (db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) !== undefined);
176
+ }
177
+ /**
178
+ * The `agent_name → agent_id` map, over ALL agent rows INCLUDING retired ones (a retired agent's rows
179
+ * must backfill to its OWN id and then sit inert — never be re-attributed to the live agent that
180
+ * reused its name).
181
+ *
182
+ * If any name resolves to more than one agent_id the map is AMBIGUOUS and the migration ABORTS. It
183
+ * does not guess, does not prefer the newest, does not prefer the oldest. A wrong attribution here
184
+ * would silently bind one identity's transcript, contacts and interrupted sessions to a DIFFERENT
185
+ * keypair — which is precisely the defect this migration exists to remove, except made permanent,
186
+ * unauditable, and blessed by a migration. A loud abort costs a wipe on a single-operator machine; a
187
+ * silent guess costs correctness forever.
188
+ *
189
+ * (A timestamp-based disambiguation is available in principle — rows written after the new agent's
190
+ * creation must belong to it, since a retired agent cannot write. It is deliberately NOT used: it
191
+ * depends on all seven tables carrying a trustworthy timestamp, and cleverness buys nothing on a
192
+ * database we would happily wipe.)
193
+ */
194
+ function buildNameToAgentId(db) {
195
+ const rows = db
196
+ .prepare("SELECT agent_name, agent_id FROM agents")
197
+ .all();
198
+ const map = new Map();
199
+ const ambiguous = new Set();
200
+ for (const r of rows) {
201
+ if (map.has(r.agent_name))
202
+ ambiguous.add(r.agent_name);
203
+ map.set(r.agent_name, r.agent_id);
204
+ }
205
+ if (ambiguous.size > 0) {
206
+ throw new Error(`agent_id_backfill_ambiguous: agent name(s) [${[...ambiguous].sort().join(", ")}] resolve to more ` +
207
+ `than one agent_id (a retired agent's name was reused). The migration cannot know which identity ` +
208
+ `owns the existing session rows and refuses to guess — attributing them to the wrong keypair is ` +
209
+ `the very defect it exists to remove. Resolve by hand or start from a fresh database.`);
210
+ }
211
+ return map;
212
+ }
213
+ /** Does this table still need re-keying? True iff it exists and has no `agent_id` column. */
214
+ function needsRekey(db, table) {
215
+ if (!tableExists(db, table))
216
+ return false;
217
+ return !columnsOf(db, table).includes("agent_id");
218
+ }
219
+ /**
220
+ * Re-key the seven child tables from `agent_name` to `agent_id`. Idempotent: a table that already has
221
+ * an `agent_id` column is skipped, so a second call is a no-op. All rebuilds share ONE transaction.
222
+ *
223
+ * Throws (leaving the database untouched) on:
224
+ * - an ambiguous `agent_name → agent_id` map (a reused retired name),
225
+ * - any row whose non-null `agent_name` does not resolve to an agent,
226
+ * - any row count that does not match pre-migration.
227
+ *
228
+ * Never half-commits, never silently drops a row, never invents an agent_id.
229
+ */
230
+ export function migrateSessionTablesToAgentId(db, logger) {
231
+ const pending = REKEY_TARGETS.filter((t) => needsRekey(db, t.table));
232
+ if (pending.length === 0)
233
+ return { migrated: false, rowCounts: {} };
234
+ // Ambiguity is checked BEFORE the transaction opens: nothing has been touched when it throws. It
235
+ // is logged with the same failure event as an in-transaction abort — a refusal to migrate is a
236
+ // loud event regardless of WHERE in the process it was detected.
237
+ let nameToId;
238
+ try {
239
+ nameToId = buildNameToAgentId(db);
240
+ }
241
+ catch (err) {
242
+ logger.error("daemon.migration.agent_id_backfill.failed", {
243
+ tables: pending.map((t) => t.table),
244
+ reason: err instanceof Error ? err.message : String(err),
245
+ });
246
+ throw err;
247
+ }
248
+ const before = new Map();
249
+ for (const t of pending) {
250
+ before.set(t.table, db.prepare(`SELECT COUNT(*) AS n FROM ${t.table}`).get().n);
251
+ }
252
+ const rowCounts = {};
253
+ db.exec("BEGIN");
254
+ try {
255
+ for (const target of pending) {
256
+ const oldCols = columnsOf(db, target.table);
257
+ const tmp = `${target.table}_rekey_new`;
258
+ db.exec(target.createSql(tmp));
259
+ // Copy every column the OLD table has that the NEW table also has (minus agent_name, which is
260
+ // replaced by agent_id). A pre-DOD-LOOP-1 retry_queue lacks agent_name/awaiting_ack entirely;
261
+ // taking the intersection handles every historical shape without a per-version special case.
262
+ const newCols = columnsOf(db, tmp);
263
+ const carried = oldCols.filter((c) => c !== "agent_name" && c !== "id" && newCols.includes(c));
264
+ const hasName = oldCols.includes("agent_name");
265
+ if (hasName) {
266
+ // Resolve through a JOIN on the parent so the mapping is the database's, not this process's.
267
+ // An unresolvable row is DROPPED by the inner join and caught by the count check below —
268
+ // for `strict` tables. For `nullable` (retry_queue), a NULL name must survive, so LEFT JOIN.
269
+ const join = target.backfill === "strict" ? "JOIN" : "LEFT JOIN";
270
+ db.exec(`INSERT INTO ${tmp} (agent_id${carried.length ? ", " + carried.join(", ") : ""})
271
+ SELECT a.agent_id${carried.length ? ", " + carried.map((c) => `t.${c}`).join(", ") : ""}
272
+ FROM ${target.table} t ${join} agents a ON a.agent_name = t.agent_name`);
273
+ }
274
+ else {
275
+ // No name to resolve (pre-DOD-LOOP-1 retry_queue): agent_id stays NULL — "not agent-scoped".
276
+ if (target.backfill === "strict") {
277
+ throw new Error(`agent_id_backfill_impossible: table '${target.table}' has neither agent_name nor agent_id`);
278
+ }
279
+ db.exec(`INSERT INTO ${tmp} (${carried.join(", ")}) SELECT ${carried.map((c) => `t.${c}`).join(", ")} FROM ${target.table} t`);
280
+ }
281
+ const after = db.prepare(`SELECT COUNT(*) AS n FROM ${tmp}`).get().n;
282
+ const expected = before.get(target.table);
283
+ if (after !== expected) {
284
+ throw new Error(`agent_id_backfill_row_count_mismatch: table '${target.table}' had ${expected} row(s) before and ` +
285
+ `${after} after the copy. A row failed to resolve to an agent_id (orphaned ${target.table}.agent_name ` +
286
+ `with no matching agents row), or a name matched more than one agent. Refusing to commit.`);
287
+ }
288
+ // Completeness. `strict` → every row must carry an agent_id. `nullable` → only rows that HAD a
289
+ // name must; a row that was always agent-less legitimately stays NULL.
290
+ const unresolved = target.backfill === "strict"
291
+ ? db.prepare(`SELECT COUNT(*) AS n FROM ${tmp} WHERE agent_id IS NULL`).get().n
292
+ : hasName
293
+ ? db
294
+ .prepare(`SELECT COUNT(*) AS n FROM ${target.table} t WHERE t.agent_name IS NOT NULL
295
+ AND NOT EXISTS (SELECT 1 FROM agents a WHERE a.agent_name = t.agent_name)`)
296
+ .get().n
297
+ : 0;
298
+ if (unresolved > 0) {
299
+ throw new Error(`agent_id_backfill_incomplete: table '${target.table}' has ${unresolved} row(s) whose agent could not ` +
300
+ `be resolved to a stable agent_id. Refusing to commit a table with an unattributable row.`);
301
+ }
302
+ db.exec(`DROP TABLE ${target.table}`);
303
+ db.exec(`ALTER TABLE ${tmp} RENAME TO ${target.table}`);
304
+ for (const sql of target.indexSql(target.table))
305
+ db.exec(sql);
306
+ rowCounts[target.table] = after;
307
+ }
308
+ db.exec("COMMIT");
309
+ }
310
+ catch (err) {
311
+ // SQLite may have already aborted the transaction; a failing ROLLBACK must not mask the real error.
312
+ try {
313
+ db.exec("ROLLBACK");
314
+ }
315
+ catch {
316
+ /* the failing statement may have already aborted the txn */
317
+ }
318
+ logger.error("daemon.migration.agent_id_backfill.failed", {
319
+ tables: pending.map((t) => t.table),
320
+ reason: err instanceof Error ? err.message : String(err),
321
+ });
322
+ throw err;
323
+ }
324
+ logger.info("daemon.migration.agent_id_backfill", {
325
+ tables: Object.keys(rowCounts),
326
+ rowCounts,
327
+ agentsMapped: nameToId.size,
328
+ });
329
+ return { migrated: true, rowCounts };
330
+ }
331
+ //# sourceMappingURL=agent-id-migration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-id-migration.js","sourceRoot":"","sources":["../src/agent-id-migration.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EAQrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAQ/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAsB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAQ/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA0InF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AA2CD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAE1D,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA6tL7E"}
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EAQrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAQ/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAsB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAQ/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA0InF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AA2CD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAE1D,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAmvL7E"}
package/dist/daemon.js CHANGED
@@ -1450,12 +1450,14 @@ export async function startDaemon(config) {
1450
1450
  // entry; a TTF expiry records the un-acked content for the crash backstop (the relay
1451
1451
  // park deposit itself is added in 3b). Both side effects are best-effort and never
1452
1452
  // throw into the content stream handler.
1453
+ // DOD-AGENT-ID-JOINKEY-1: RetryQueue owns an agent-scoped table, so it is handed the STABLE
1454
+ // agent_id. The daemon resolves the operator-facing name ONCE, here at its own boundary.
1453
1455
  sessionNodeManager.setAwaitingAckHooks({
1454
1456
  onPersisted: (agentName, sessionId, contentHashHex) => {
1455
- retryQueue.markContentAcked(agentName, sessionId, Buffer.from(contentHashHex, "hex"));
1457
+ retryQueue.markContentAcked(sessionNodeManager.resolveAgentId(agentName), sessionId, Buffer.from(contentHashHex, "hex"));
1456
1458
  },
1457
1459
  onTtf: (agentName, sessionId, contentHashHex, content) => {
1458
- retryQueue.enqueueAwaitingContent(agentName, sessionId, Buffer.from(contentHashHex, "hex"), content);
1460
+ retryQueue.enqueueAwaitingContent(sessionNodeManager.resolveAgentId(agentName), sessionId, Buffer.from(contentHashHex, "hex"), content);
1459
1461
  },
1460
1462
  });
1461
1463
  // MSG-001-3b (2b): the LIVE content-park deposit. On a not-confirmed send (direct delivery
@@ -1508,8 +1510,14 @@ export async function startDaemon(config) {
1508
1510
  // PERSISTED session state (the in-memory entry is gone after a restart). Same seal + deposit
1509
1511
  // as the live hook above; the endpoint + recipient come from the sessions row.
1510
1512
  const startupParkFn = async (entry) => {
1511
- const ep = sessionNodeManager.getPersistedRelayEndpoint(entry.agentName, entry.sessionId);
1512
- const record = sessionNodeManager.getSessionRecord(entry.agentName, entry.sessionId);
1513
+ // The durable row carries the OWNING agent's stable id. Resolve it back to a name for the
1514
+ // name-addressed session/standing-receiver lookups. A retired owner resolves fine and then has no
1515
+ // standing receiver, so the park fails loudly below rather than silently re-parking as someone else.
1516
+ const ownerName = sessionNodeManager.agentNameForId(entry.agentId);
1517
+ if (ownerName === null)
1518
+ return { parked: false, error: "owning_agent_not_found" };
1519
+ const ep = sessionNodeManager.getPersistedRelayEndpoint(ownerName, entry.sessionId);
1520
+ const record = sessionNodeManager.getSessionRecord(ownerName, entry.sessionId);
1513
1521
  if (!ep)
1514
1522
  return { parked: false, error: "no_persisted_relay_endpoint" };
1515
1523
  if (!record?.counterparty_pubkey)
@@ -1543,18 +1551,23 @@ export async function startDaemon(config) {
1543
1551
  // Re-park un-acked awaiting content to the relay store-and-forward queue. Runs once pre-IPC
1544
1552
  // (the crash backstop) and again per-agent when an agent comes online — because post-DOD-LOOP-1
1545
1553
  // the native `startupParkFn` needs the OWNING agent's standing receiver, which exists only once
1546
- // that agent is online. `filterAgent` scopes the drain to one agent's sessions on the agent-
1554
+ // that agent is online. `filterAgentName` scopes the drain to one agent's sessions on the agent-
1547
1555
  // online re-run; with no filter it attempts all (the pre-IPC pass / injected-target test path).
1548
- async function flushAwaitingContent(filterAgent) {
1556
+ async function flushAwaitingContent(filterAgentName) {
1557
+ // DOD-AGENT-ID-JOINKEY-1: the queue is keyed by the STABLE agent_id, but the caller (and the
1558
+ // human-readable log) speak the NAME. Resolve once here; log the name, filter by the id.
1559
+ const filterAgentId = filterAgentName !== undefined
1560
+ ? sessionNodeManager.resolveAgentId(filterAgentName)
1561
+ : undefined;
1549
1562
  const all = retryQueue.getAwaitingSessions();
1550
- const sessions = filterAgent === undefined
1563
+ const sessions = filterAgentId === undefined
1551
1564
  ? all
1552
- : all.filter((s) => s.agentName === filterAgent);
1565
+ : all.filter((s) => s.agentId === filterAgentId);
1553
1566
  if (sessions.length === 0)
1554
1567
  return;
1555
1568
  const parkFn = config.contentParkFn ?? startupParkFn;
1556
1569
  if (!parkFn) {
1557
- const pendingCount = sessions.reduce((n, s) => n + retryQueue.getAwaitingDepth(s.agentName, s.sessionId), 0);
1570
+ const pendingCount = sessions.reduce((n, s) => n + retryQueue.getAwaitingDepth(s.agentId, s.sessionId), 0);
1558
1571
  logger.warn("content.park.flush.deferred", {
1559
1572
  sessionCount: sessions.length,
1560
1573
  pendingCount,
@@ -1565,7 +1578,7 @@ export async function startDaemon(config) {
1565
1578
  let parkedTotal = 0;
1566
1579
  for (const s of sessions) {
1567
1580
  try {
1568
- parkedTotal += await retryQueue.drainAwaitingToPark(s.agentName, s.sessionId, parkFn);
1581
+ parkedTotal += await retryQueue.drainAwaitingToPark(s.agentId, s.sessionId, parkFn);
1569
1582
  }
1570
1583
  catch (err) {
1571
1584
  logger.error("content.park.flush.failed", {
@@ -1577,7 +1590,7 @@ export async function startDaemon(config) {
1577
1590
  logger.info("content.park.flush.completed", {
1578
1591
  sessionCount: sessions.length,
1579
1592
  parkedCount: parkedTotal,
1580
- ...(filterAgent !== undefined ? { agentName: filterAgent } : {}),
1593
+ ...(filterAgentName !== undefined ? { agentName: filterAgentName } : {}),
1581
1594
  });
1582
1595
  }
1583
1596
  await flushAwaitingContent();
@@ -3507,10 +3520,16 @@ export async function startDaemon(config) {
3507
3520
  }
3508
3521
  // DOD-LOOP-1: awaiting content is keyed by the OWNING agent. Prefer an explicit agentName param;
3509
3522
  // fall back to the connection's current agent.
3523
+ // DOD-AGENT-ID-JOINKEY-1: the old `?? ""` fell back to an EMPTY-STRING agent, silently merging
3524
+ // every unaddressed caller's awaiting content into one nameless queue. There is no such agent.
3510
3525
  const agentName = params?.agentName
3511
- ?? perConnectionState.get(connectionId)?.currentAgent ?? "";
3512
- retryQueue.enqueueAwaitingContent(agentName, sessionId, Buffer.from(contentHashHex, "hex"), Buffer.from(contentHex, "hex"));
3513
- return { queued: true, awaitingDepth: retryQueue.getAwaitingDepth(agentName, sessionId) };
3526
+ ?? perConnectionState.get(connectionId)?.currentAgent;
3527
+ if (!agentName) {
3528
+ return { error: "no_current_agent", guidance: "Select an agent with cello_use_agent, or pass agentName." };
3529
+ }
3530
+ const agentId = sessionNodeManager.resolveAgentId(agentName);
3531
+ retryQueue.enqueueAwaitingContent(agentId, sessionId, Buffer.from(contentHashHex, "hex"), Buffer.from(contentHex, "hex"));
3532
+ return { queued: true, awaitingDepth: retryQueue.getAwaitingDepth(agentId, sessionId) };
3514
3533
  });
3515
3534
  // CELLO-M7-MSG-001: a `persisted` delivery ACK (or a confirmed park) clears the durable
3516
3535
  // awaiting-ACK entry so the startup flush does not re-park already-delivered content.
@@ -3521,9 +3540,13 @@ export async function startDaemon(config) {
3521
3540
  return { error: "missing_params", guidance: "Provide sessionId and contentHash (hex)." };
3522
3541
  }
3523
3542
  const agentName = params?.agentName
3524
- ?? perConnectionState.get(connectionId)?.currentAgent ?? "";
3525
- retryQueue.markContentAcked(agentName, sessionId, Buffer.from(contentHashHex, "hex"));
3526
- return { acked: true, awaitingDepth: retryQueue.getAwaitingDepth(agentName, sessionId) };
3543
+ ?? perConnectionState.get(connectionId)?.currentAgent;
3544
+ if (!agentName) {
3545
+ return { error: "no_current_agent", guidance: "Select an agent with cello_use_agent, or pass agentName." };
3546
+ }
3547
+ const agentId = sessionNodeManager.resolveAgentId(agentName);
3548
+ retryQueue.markContentAcked(agentId, sessionId, Buffer.from(contentHashHex, "hex"));
3549
+ return { acked: true, awaitingDepth: retryQueue.getAwaitingDepth(agentId, sessionId) };
3527
3550
  });
3528
3551
  handlers.set("check_nonce", async (params, _connectionId) => {
3529
3552
  const sessionId = params?.sessionId;