@cello-protocol/daemon 0.0.43 → 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.
- package/dist/agent-id-migration.d.ts +61 -0
- package/dist/agent-id-migration.d.ts.map +1 -0
- package/dist/agent-id-migration.js +331 -0
- package/dist/agent-id-migration.js.map +1 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +40 -17
- package/dist/daemon.js.map +1 -1
- package/dist/retry-queue.d.ts +9 -7
- package/dist/retry-queue.d.ts.map +1 -1
- package/dist/retry-queue.js +81 -48
- package/dist/retry-queue.js.map +1 -1
- package/dist/session-assignment-parser.d.ts.map +1 -1
- package/dist/session-assignment-parser.js +7 -0
- package/dist/session-assignment-parser.js.map +1 -1
- package/dist/session-node-manager.d.ts +17 -0
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +203 -90
- package/dist/session-node-manager.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -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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"}
|
package/dist/daemon.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
1512
|
-
|
|
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. `
|
|
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(
|
|
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 =
|
|
1563
|
+
const sessions = filterAgentId === undefined
|
|
1551
1564
|
? all
|
|
1552
|
-
: all.filter((s) => s.
|
|
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.
|
|
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.
|
|
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
|
-
...(
|
|
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
|
-
|
|
3513
|
-
|
|
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
|
-
|
|
3526
|
-
|
|
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;
|