@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
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
import { openEncryptedDatabase, resolveDbKey, dbKeyPathFor, } from "./sqlcipher-db.js";
|
|
70
70
|
import { migrateToEncryptedIfNeeded } from "./identity-migration.js";
|
|
71
71
|
import { ensureIdentitySchema } from "./db-identity-store.js";
|
|
72
|
+
import { migrateSessionTablesToAgentId } from "./agent-id-migration.js";
|
|
72
73
|
import { randomUUID, createHash } from "node:crypto";
|
|
73
74
|
import * as lp from "it-length-prefixed";
|
|
74
75
|
import { decode, Encoder } from "cbor-x";
|
|
@@ -332,7 +333,7 @@ export class SessionNodeManager {
|
|
|
332
333
|
this.#db.exec(`
|
|
333
334
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
334
335
|
session_id TEXT NOT NULL,
|
|
335
|
-
|
|
336
|
+
agent_id TEXT NOT NULL,
|
|
336
337
|
counterparty_pubkey TEXT NOT NULL,
|
|
337
338
|
status TEXT NOT NULL,
|
|
338
339
|
created_at INTEGER NOT NULL,
|
|
@@ -340,7 +341,9 @@ export class SessionNodeManager {
|
|
|
340
341
|
-- DOD-LOOP-1: composite key so two of the operator's agents can hold both ends of the
|
|
341
342
|
-- SAME session_id on ONE daemon (the loopback case). A bare session_id PK would reject
|
|
342
343
|
-- the second end's row.
|
|
343
|
-
|
|
344
|
+
-- DOD-AGENT-ID-JOINKEY-1: keyed on the STABLE agent_id, never the mutable, reuse-freed
|
|
345
|
+
-- agent_name. The display name lives on the agents table and is joined in for reads.
|
|
346
|
+
PRIMARY KEY (agent_id, session_id)
|
|
344
347
|
)
|
|
345
348
|
`);
|
|
346
349
|
// M7-SESSION-001: idempotent schema extension — add message_count and interrupted_at
|
|
@@ -386,7 +389,7 @@ export class SessionNodeManager {
|
|
|
386
389
|
// Merkle root so the achieved commitment is never discarded.
|
|
387
390
|
this.#db.exec(`
|
|
388
391
|
CREATE TABLE IF NOT EXISTS seal_interrupted_artifacts (
|
|
389
|
-
|
|
392
|
+
agent_id TEXT NOT NULL,
|
|
390
393
|
session_id TEXT NOT NULL,
|
|
391
394
|
role TEXT NOT NULL,
|
|
392
395
|
own_leaf TEXT NOT NULL,
|
|
@@ -395,7 +398,7 @@ export class SessionNodeManager {
|
|
|
395
398
|
nonce TEXT NOT NULL,
|
|
396
399
|
created_at INTEGER NOT NULL,
|
|
397
400
|
-- DOD-LOOP-1: composite key (per-agent end of a loopback session).
|
|
398
|
-
PRIMARY KEY (
|
|
401
|
+
PRIMARY KEY (agent_id, session_id)
|
|
399
402
|
)
|
|
400
403
|
`);
|
|
401
404
|
// DAEMON-004 (AC-007 / SI-001): the daemon-owned per-session Merkle tree,
|
|
@@ -405,14 +408,14 @@ export class SessionNodeManager {
|
|
|
405
408
|
// by session_id ORDER BY leaf_index is the only read pattern.
|
|
406
409
|
this.#db.exec(`
|
|
407
410
|
CREATE TABLE IF NOT EXISTS session_tree_leaves (
|
|
408
|
-
|
|
411
|
+
agent_id TEXT NOT NULL,
|
|
409
412
|
session_id TEXT NOT NULL,
|
|
410
413
|
leaf_index INTEGER NOT NULL,
|
|
411
414
|
leaf_kind TEXT NOT NULL,
|
|
412
415
|
leaf_hash_hex TEXT NOT NULL,
|
|
413
416
|
created_at INTEGER NOT NULL,
|
|
414
417
|
-- DOD-LOOP-1: composite key so each agent's end has its own append-ordered tree.
|
|
415
|
-
PRIMARY KEY (
|
|
418
|
+
PRIMARY KEY (agent_id, session_id, leaf_index)
|
|
416
419
|
)
|
|
417
420
|
`);
|
|
418
421
|
// DOD-LOG-1 (PERSIST-LOG-001) / PERSIST-002 (AC-010): the durable, ENCRYPTED-at-rest readable
|
|
@@ -422,13 +425,13 @@ export class SessionNodeManager {
|
|
|
422
425
|
// provided by whole-DB SQLCipher, not a per-column cipher (relay/directory never see it — INV-3).
|
|
423
426
|
this.#db.exec(`
|
|
424
427
|
CREATE TABLE IF NOT EXISTS transcript (
|
|
425
|
-
|
|
428
|
+
agent_id TEXT NOT NULL,
|
|
426
429
|
session_id TEXT NOT NULL,
|
|
427
430
|
sequence INTEGER NOT NULL,
|
|
428
431
|
direction TEXT NOT NULL, -- 'sent' | 'received'
|
|
429
432
|
blob BLOB NOT NULL, -- readable plaintext bytes (whole-DB SQLCipher-encrypted at rest)
|
|
430
433
|
created_at INTEGER NOT NULL,
|
|
431
|
-
PRIMARY KEY (
|
|
434
|
+
PRIMARY KEY (agent_id, session_id, sequence, direction)
|
|
432
435
|
)
|
|
433
436
|
`);
|
|
434
437
|
// M8C-INBOX-1 (N2): per-agent, per-session read watermark. `last_delivered_seq` is the highest
|
|
@@ -438,10 +441,10 @@ export class SessionNodeManager {
|
|
|
438
441
|
// across daemon restarts, not just within one process (INV-PUSHPULL). Additive table.
|
|
439
442
|
this.#db.exec(`
|
|
440
443
|
CREATE TABLE IF NOT EXISTS message_watermarks (
|
|
441
|
-
|
|
444
|
+
agent_id TEXT NOT NULL,
|
|
442
445
|
session_id TEXT NOT NULL,
|
|
443
446
|
last_delivered_seq INTEGER NOT NULL,
|
|
444
|
-
PRIMARY KEY (
|
|
447
|
+
PRIMARY KEY (agent_id, session_id)
|
|
445
448
|
)
|
|
446
449
|
`);
|
|
447
450
|
// M8C-CONTACT-1: binary per-agent contact whitelist. This is an ACCESS-CONTROL LIST, not a
|
|
@@ -450,10 +453,10 @@ export class SessionNodeManager {
|
|
|
450
453
|
// re-resolved); known stays known until explicitly removed (no TTL/expiry on membership).
|
|
451
454
|
this.#db.exec(`
|
|
452
455
|
CREATE TABLE IF NOT EXISTS contacts (
|
|
453
|
-
|
|
456
|
+
agent_id TEXT NOT NULL,
|
|
454
457
|
pubkey TEXT NOT NULL,
|
|
455
458
|
added_at INTEGER NOT NULL,
|
|
456
|
-
PRIMARY KEY (
|
|
459
|
+
PRIMARY KEY (agent_id, pubkey)
|
|
457
460
|
)
|
|
458
461
|
`);
|
|
459
462
|
// MONIKER-3 AC1: the receiver's own pet name for a pubkey — the top tier of whoLabel.
|
|
@@ -463,6 +466,15 @@ export class SessionNodeManager {
|
|
|
463
466
|
if (!contactCols.some((c) => c.name === "moniker")) {
|
|
464
467
|
this.#db.exec("ALTER TABLE contacts ADD COLUMN moniker TEXT");
|
|
465
468
|
}
|
|
469
|
+
// DOD-AGENT-ID-JOINKEY-1: finish REMOVE-001. Re-key the seven child tables from the mutable,
|
|
470
|
+
// reuse-freed `agent_name` to the stable `agent_id`, in ONE transaction. Runs AFTER every
|
|
471
|
+
// CREATE/ALTER above, so an existing table has its full historical column set before it is
|
|
472
|
+
// rebuilt, and BEFORE any read below touches it. A no-op once the tables carry `agent_id`.
|
|
473
|
+
//
|
|
474
|
+
// `retry_queue` (the seventh) is created later, by RetryQueue's constructor. On an existing
|
|
475
|
+
// database it already exists here and is re-keyed in the same transaction; on a fresh one it is
|
|
476
|
+
// absent, is skipped, and RetryQueue then creates it directly in the re-keyed shape.
|
|
477
|
+
migrateSessionTablesToAgentId(this.#db, this.#logger);
|
|
466
478
|
// M8C-TGDOOR-1: daemon-wide Telegram settings (bot token + allowlisted operator chat). A
|
|
467
479
|
// NEW dedicated table — NOT folded into the parked M9-CFG-001 config store, because a bot
|
|
468
480
|
// token has no sensible default (a required credential, unlike AWAY/TTL/CONTACT's real
|
|
@@ -479,8 +491,15 @@ export class SessionNodeManager {
|
|
|
479
491
|
// Step 2: Detect interrupted sessions (SIGKILL detection — AC-010).
|
|
480
492
|
// Any 'active' row in a freshly-started daemon is a remnant of a prior
|
|
481
493
|
// killed process. Batch-update to 'interrupted' before IPC opens.
|
|
494
|
+
// DOD-AGENT-ID-JOINKEY-1: this sweep spans EVERY agent, so it cannot resolve one name up front.
|
|
495
|
+
// It scopes its UPDATE by the row's own agent_id and LEFT JOINs `agents` only to LOG a human
|
|
496
|
+
// name. LEFT, not INNER: an inner join would silently skip a session whose agent row is missing,
|
|
497
|
+
// leaving it 'active' forever — a stuck row hidden by the query that was meant to find it. An
|
|
498
|
+
// orphan is instead marked interrupted like any other AND reported loudly.
|
|
482
499
|
const activeRows = this.#db
|
|
483
|
-
.prepare(
|
|
500
|
+
.prepare(`SELECT s.session_id, s.agent_id, a.agent_name
|
|
501
|
+
FROM sessions s LEFT JOIN agents a ON a.agent_id = s.agent_id
|
|
502
|
+
WHERE s.status = 'active'`)
|
|
484
503
|
.all();
|
|
485
504
|
if (activeRows.length > 0) {
|
|
486
505
|
const now = Date.now();
|
|
@@ -488,8 +507,15 @@ export class SessionNodeManager {
|
|
|
488
507
|
for (const row of activeRows) {
|
|
489
508
|
try {
|
|
490
509
|
this.#db
|
|
491
|
-
.prepare("UPDATE sessions SET status = 'interrupted', updated_at = ?, interrupted_at = COALESCE(interrupted_at, ?) WHERE
|
|
492
|
-
.run(now, interruptedAt, row.
|
|
510
|
+
.prepare("UPDATE sessions SET status = 'interrupted', updated_at = ?, interrupted_at = COALESCE(interrupted_at, ?) WHERE agent_id = ? AND session_id = ?")
|
|
511
|
+
.run(now, interruptedAt, row.agent_id, row.session_id);
|
|
512
|
+
if (row.agent_name === null) {
|
|
513
|
+
this.#logger.error("session.agent.orphaned", {
|
|
514
|
+
sessionId: row.session_id,
|
|
515
|
+
agentId: row.agent_id,
|
|
516
|
+
impact: "session row references an agent_id with no agents row",
|
|
517
|
+
});
|
|
518
|
+
}
|
|
493
519
|
this.#logger.warn("session.interrupted.detected", {
|
|
494
520
|
sessionId: row.session_id,
|
|
495
521
|
agentName: row.agent_name,
|
|
@@ -557,11 +583,12 @@ export class SessionNodeManager {
|
|
|
557
583
|
if (!this.#db)
|
|
558
584
|
return;
|
|
559
585
|
try {
|
|
586
|
+
const agentId = this.#requireAgentId(agentName);
|
|
560
587
|
const blob = Buffer.from(plaintext);
|
|
561
588
|
this.#db
|
|
562
|
-
.prepare(`INSERT OR IGNORE INTO transcript (
|
|
589
|
+
.prepare(`INSERT OR IGNORE INTO transcript (agent_id, session_id, sequence, direction, blob, created_at)
|
|
563
590
|
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
564
|
-
.run(
|
|
591
|
+
.run(agentId, sessionId, sequence, direction, blob, Date.now());
|
|
565
592
|
this.#logger.info("transcript.message.recorded", { sessionId, agentName, sequence, direction, correlationId });
|
|
566
593
|
}
|
|
567
594
|
catch (err) {
|
|
@@ -590,8 +617,8 @@ export class SessionNodeManager {
|
|
|
590
617
|
return { messages: [], undecryptable: 0 };
|
|
591
618
|
const rows = this.#db
|
|
592
619
|
.prepare(`SELECT sequence, direction, blob, created_at FROM transcript
|
|
593
|
-
WHERE
|
|
594
|
-
.all(agentName, sessionId);
|
|
620
|
+
WHERE agent_id = ? AND session_id = ? ORDER BY sequence ASC, direction ASC`)
|
|
621
|
+
.all(this.#requireAgentId(agentName), sessionId);
|
|
595
622
|
const messages = [];
|
|
596
623
|
// PERSIST-002 (AC-010): the blob is plaintext (whole-DB SQLCipher at rest), so there is no
|
|
597
624
|
// per-row decrypt step that can fail — `undecryptable` stays 0 and is kept only for callers that
|
|
@@ -614,8 +641,8 @@ export class SessionNodeManager {
|
|
|
614
641
|
if (!this.#db)
|
|
615
642
|
return -1;
|
|
616
643
|
const row = this.#db
|
|
617
|
-
.prepare("SELECT last_delivered_seq FROM message_watermarks WHERE
|
|
618
|
-
.get(agentName, sessionId);
|
|
644
|
+
.prepare("SELECT last_delivered_seq FROM message_watermarks WHERE agent_id = ? AND session_id = ?")
|
|
645
|
+
.get(this.#requireAgentId(agentName), sessionId);
|
|
619
646
|
return row ? row.last_delivered_seq : -1;
|
|
620
647
|
}
|
|
621
648
|
/** Advance the read watermark (delivery marks read). MONOTONIC — never lowers, so a replayed or
|
|
@@ -624,11 +651,11 @@ export class SessionNodeManager {
|
|
|
624
651
|
if (!this.#db)
|
|
625
652
|
return;
|
|
626
653
|
this.#db
|
|
627
|
-
.prepare(`INSERT INTO message_watermarks (
|
|
654
|
+
.prepare(`INSERT INTO message_watermarks (agent_id, session_id, last_delivered_seq)
|
|
628
655
|
VALUES (?, ?, ?)
|
|
629
|
-
ON CONFLICT(
|
|
656
|
+
ON CONFLICT(agent_id, session_id)
|
|
630
657
|
DO UPDATE SET last_delivered_seq = MAX(last_delivered_seq, excluded.last_delivered_seq)`)
|
|
631
|
-
.run(agentName, sessionId, seq);
|
|
658
|
+
.run(this.#requireAgentId(agentName), sessionId, seq);
|
|
632
659
|
this.#logger.info("message.watermark.advanced", { agentName, sessionId, sequence: seq });
|
|
633
660
|
}
|
|
634
661
|
/** INBOX-1 (N2): per-session unread summary for an agent — sessions that have RECEIVED transcript
|
|
@@ -643,21 +670,21 @@ export class SessionNodeManager {
|
|
|
643
670
|
MAX(t.sequence) AS last_seq
|
|
644
671
|
FROM transcript t
|
|
645
672
|
LEFT JOIN message_watermarks w
|
|
646
|
-
ON w.
|
|
647
|
-
WHERE t.
|
|
673
|
+
ON w.agent_id = t.agent_id AND w.session_id = t.session_id
|
|
674
|
+
WHERE t.agent_id = ?
|
|
648
675
|
AND t.direction = 'received'
|
|
649
676
|
AND t.sequence > COALESCE(w.last_delivered_seq, -1)
|
|
650
677
|
GROUP BY t.session_id
|
|
651
678
|
HAVING unread_count > 0
|
|
652
679
|
ORDER BY t.session_id ASC`)
|
|
653
|
-
.all(agentName);
|
|
680
|
+
.all(this.#requireAgentId(agentName));
|
|
654
681
|
return rows;
|
|
655
682
|
}
|
|
656
683
|
/** M8C-CONTACT-1: is this pubkey a known contact of this agent? */
|
|
657
684
|
isContact(agentName, pubkey) {
|
|
658
685
|
if (!this.#db)
|
|
659
686
|
return false;
|
|
660
|
-
const row = this.#db.prepare("SELECT 1 FROM contacts WHERE
|
|
687
|
+
const row = this.#db.prepare("SELECT 1 FROM contacts WHERE agent_id = ? AND pubkey = ?").get(this.#requireAgentId(agentName), pubkey);
|
|
661
688
|
return row !== undefined;
|
|
662
689
|
}
|
|
663
690
|
/** M8C-CONTACT-1: pin a contact at add time — idempotent (re-adding an existing contact is a
|
|
@@ -675,13 +702,14 @@ export class SessionNodeManager {
|
|
|
675
702
|
if (moniker !== undefined && moniker !== null && validateMoniker(moniker) === null) {
|
|
676
703
|
throw new Error(`invalid contact moniker for agent '${agentName}': must match ${MONIKER_RE.source}`);
|
|
677
704
|
}
|
|
705
|
+
const agentId = this.#requireAgentId(agentName);
|
|
678
706
|
this.#db
|
|
679
|
-
.prepare("INSERT OR IGNORE INTO contacts (
|
|
680
|
-
.run(
|
|
707
|
+
.prepare("INSERT OR IGNORE INTO contacts (agent_id, pubkey, added_at) VALUES (?, ?, ?)")
|
|
708
|
+
.run(agentId, pubkey, Date.now());
|
|
681
709
|
if (moniker !== undefined && moniker !== null) {
|
|
682
710
|
this.#db
|
|
683
|
-
.prepare("UPDATE contacts SET moniker = ? WHERE
|
|
684
|
-
.run(moniker,
|
|
711
|
+
.prepare("UPDATE contacts SET moniker = ? WHERE agent_id = ? AND pubkey = ?")
|
|
712
|
+
.run(moniker, agentId, pubkey);
|
|
685
713
|
}
|
|
686
714
|
}
|
|
687
715
|
/** MONIKER-3 AC3: rename (string) or clear (null) an EXISTING contact's pet name. Returns false
|
|
@@ -696,15 +724,15 @@ export class SessionNodeManager {
|
|
|
696
724
|
throw new Error(`invalid contact moniker for agent '${agentName}': must match ${MONIKER_RE.source}`);
|
|
697
725
|
}
|
|
698
726
|
const res = this.#db
|
|
699
|
-
.prepare("UPDATE contacts SET moniker = ? WHERE
|
|
700
|
-
.run(moniker, agentName, pubkey);
|
|
727
|
+
.prepare("UPDATE contacts SET moniker = ? WHERE agent_id = ? AND pubkey = ?")
|
|
728
|
+
.run(moniker, this.#requireAgentId(agentName), pubkey);
|
|
701
729
|
return res.changes > 0;
|
|
702
730
|
}
|
|
703
731
|
/** M8C-CONTACT-1: known stays known until explicitly removed. */
|
|
704
732
|
removeContact(agentName, pubkey) {
|
|
705
733
|
if (!this.#db)
|
|
706
734
|
return false;
|
|
707
|
-
const res = this.#db.prepare("DELETE FROM contacts WHERE
|
|
735
|
+
const res = this.#db.prepare("DELETE FROM contacts WHERE agent_id = ? AND pubkey = ?").run(this.#requireAgentId(agentName), pubkey);
|
|
708
736
|
return res.changes > 0;
|
|
709
737
|
}
|
|
710
738
|
/** MONIKER-4: the operator's pet name for a pubkey (whoLabel's top tier), or null. Read-only
|
|
@@ -717,8 +745,8 @@ export class SessionNodeManager {
|
|
|
717
745
|
return null;
|
|
718
746
|
}
|
|
719
747
|
const row = this.#db
|
|
720
|
-
.prepare("SELECT moniker FROM contacts WHERE
|
|
721
|
-
.get(agentName, pubkey);
|
|
748
|
+
.prepare("SELECT moniker FROM contacts WHERE agent_id = ? AND pubkey = ?")
|
|
749
|
+
.get(this.#requireAgentId(agentName), pubkey);
|
|
722
750
|
return row?.moniker ?? null;
|
|
723
751
|
}
|
|
724
752
|
/** M8C-CONTACT-1: list an agent's known contacts, oldest-added first. MONIKER-3 AC3: includes
|
|
@@ -727,16 +755,16 @@ export class SessionNodeManager {
|
|
|
727
755
|
if (!this.#db)
|
|
728
756
|
return [];
|
|
729
757
|
return this.#db
|
|
730
|
-
.prepare("SELECT pubkey, added_at, moniker FROM contacts WHERE
|
|
731
|
-
.all(agentName);
|
|
758
|
+
.prepare("SELECT pubkey, added_at, moniker FROM contacts WHERE agent_id = ? ORDER BY added_at ASC")
|
|
759
|
+
.all(this.#requireAgentId(agentName));
|
|
732
760
|
}
|
|
733
761
|
/** M8C-ABUSE-1: cumulative RECEIVED byte total for a session (anti-drip-feed accounting). */
|
|
734
762
|
#getReceivedBytesTotal(agentName, sessionId) {
|
|
735
763
|
if (!this.#db)
|
|
736
764
|
return 0;
|
|
737
765
|
const row = this.#db
|
|
738
|
-
.prepare("SELECT COALESCE(SUM(LENGTH(blob)), 0) AS total FROM transcript WHERE
|
|
739
|
-
.get(agentName, sessionId);
|
|
766
|
+
.prepare("SELECT COALESCE(SUM(LENGTH(blob)), 0) AS total FROM transcript WHERE agent_id = ? AND session_id = ? AND direction = 'received'")
|
|
767
|
+
.get(this.#requireAgentId(agentName), sessionId);
|
|
740
768
|
return row.total;
|
|
741
769
|
}
|
|
742
770
|
/** M8C-ABUSE-1 (reviewer HIGH fix, D18): bytes currently sitting in the out-of-order hold
|
|
@@ -763,8 +791,8 @@ export class SessionNodeManager {
|
|
|
763
791
|
if (!this.#db)
|
|
764
792
|
return 0;
|
|
765
793
|
const row = this.#db
|
|
766
|
-
.prepare("SELECT COUNT(*) AS n FROM sessions WHERE
|
|
767
|
-
.get(agentName, counterpartyPubkey);
|
|
794
|
+
.prepare("SELECT COUNT(*) AS n FROM sessions WHERE agent_id = ? AND counterparty_pubkey = ? AND status IN ('active', 'interrupted')")
|
|
795
|
+
.get(this.#requireAgentId(agentName), counterpartyPubkey);
|
|
768
796
|
return row.n;
|
|
769
797
|
}
|
|
770
798
|
/** M8C-ABUSE-1 (anti-swarm): non-terminal sessions this agent holds with counterparties that are
|
|
@@ -775,9 +803,9 @@ export class SessionNodeManager {
|
|
|
775
803
|
return 0;
|
|
776
804
|
const row = this.#db
|
|
777
805
|
.prepare(`SELECT COUNT(*) AS n FROM sessions s
|
|
778
|
-
WHERE s.
|
|
779
|
-
AND NOT EXISTS (SELECT 1 FROM contacts c WHERE c.
|
|
780
|
-
.get(agentName);
|
|
806
|
+
WHERE s.agent_id = ? AND s.status IN ('active', 'interrupted')
|
|
807
|
+
AND NOT EXISTS (SELECT 1 FROM contacts c WHERE c.agent_id = s.agent_id AND c.pubkey = s.counterparty_pubkey)`)
|
|
808
|
+
.get(this.#requireAgentId(agentName));
|
|
781
809
|
return row.n;
|
|
782
810
|
}
|
|
783
811
|
/** M8C-ABUSE-1: is a NEW inbound session from this counterparty within the acceptance bounds?
|
|
@@ -906,6 +934,66 @@ export class SessionNodeManager {
|
|
|
906
934
|
#k(agentName, sessionId) {
|
|
907
935
|
return `${agentName}\x1f${sessionId}`;
|
|
908
936
|
}
|
|
937
|
+
/**
|
|
938
|
+
* DOD-AGENT-ID-JOINKEY-1: resolve an agent's NAME to its STABLE agent_id. This is the ONE place a
|
|
939
|
+
* name becomes a key, and it is the boundary between the two worlds:
|
|
940
|
+
*
|
|
941
|
+
* - ABOVE it, addressing by name is correct. The operator says `cello_use_agent { name }`, and
|
|
942
|
+
* the in-memory maps (#k, standing receivers, keyProviders) key by name safely, because
|
|
943
|
+
* name-addressing only ever resolves ACTIVE agents and the `agents_active_name` partial unique
|
|
944
|
+
* index makes active names unique.
|
|
945
|
+
* - BELOW it, only `agent_id` may touch SQL. `agent_name` is a mutable display attribute that a
|
|
946
|
+
* retire frees for reuse; a table joined on it hands a new keypair the dead identity's rows.
|
|
947
|
+
*
|
|
948
|
+
* It resolves ONLY non-retired agents — a retired identity is gone from the runtime (`list` omits
|
|
949
|
+
* it, `start` returns agent_not_found), so no live surface may act as one.
|
|
950
|
+
*
|
|
951
|
+
* It THROWS on an unresolvable name rather than returning null. A null would flow into a
|
|
952
|
+
* `WHERE agent_id IS NULL` that quietly matches nothing: reads would return empty and writes would
|
|
953
|
+
* vanish, and the daemon would look healthy while losing an agent's data. Every caller has already
|
|
954
|
+
* resolved the agent before it gets here, so an unresolvable name is a bug in the caller, not a
|
|
955
|
+
* condition to absorb.
|
|
956
|
+
*/
|
|
957
|
+
#requireAgentId(agentName) {
|
|
958
|
+
if (!this.#db)
|
|
959
|
+
throw new Error("agent_id_unresolved: database is not open");
|
|
960
|
+
const row = this.#db
|
|
961
|
+
.prepare("SELECT agent_id FROM agents WHERE agent_name = ? AND state != 'retired'")
|
|
962
|
+
.get(agentName);
|
|
963
|
+
if (!row) {
|
|
964
|
+
this.#logger.error("session.agent_id.unresolved", { agentName });
|
|
965
|
+
throw new Error(`agent_id_unresolved: no active agent named '${agentName}'. The session tables are keyed by the ` +
|
|
966
|
+
`stable agent_id (DOD-AGENT-ID-JOINKEY-1); scoping a query by an unresolvable name would ` +
|
|
967
|
+
`silently match nothing.`);
|
|
968
|
+
}
|
|
969
|
+
return row.agent_id;
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* DOD-AGENT-ID-JOINKEY-1: the public form of the name→stable-id resolver, for components that own
|
|
973
|
+
* agent-scoped tables of their own (RetryQueue) and must be handed the STABLE key, never a name.
|
|
974
|
+
* The daemon resolves ONCE, at its own boundary, exactly as this class does internally. Throws on
|
|
975
|
+
* an unresolvable name — see #requireAgentId for why null is not an option.
|
|
976
|
+
*/
|
|
977
|
+
resolveAgentId(agentName) {
|
|
978
|
+
return this.#requireAgentId(agentName);
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Reverse lookup: the display name of a stable agent_id, or null if no such agent.
|
|
982
|
+
*
|
|
983
|
+
* Deliberately INCLUDES retired agents. Its caller (the startup awaiting-content re-park) holds an
|
|
984
|
+
* agent_id read off a durable row and needs a name to find that agent's standing receiver. A
|
|
985
|
+
* retired agent resolves to its name and then has no standing receiver, so the park fails cleanly
|
|
986
|
+
* and loudly — which is correct. Filtering retired agents out here would instead make the row
|
|
987
|
+
* unattributable and the failure mute.
|
|
988
|
+
*/
|
|
989
|
+
agentNameForId(agentId) {
|
|
990
|
+
if (!this.#db)
|
|
991
|
+
return null;
|
|
992
|
+
const row = this.#db
|
|
993
|
+
.prepare("SELECT agent_name FROM agents WHERE agent_id = ?")
|
|
994
|
+
.get(agentId);
|
|
995
|
+
return row?.agent_name ?? null;
|
|
996
|
+
}
|
|
909
997
|
/**
|
|
910
998
|
* Create a new outbound session node.
|
|
911
999
|
* Called during cello_initiate_session.
|
|
@@ -1152,8 +1240,8 @@ export class SessionNodeManager {
|
|
|
1152
1240
|
// before the in-memory entry exists) can deposit un-acked content to the same relay.
|
|
1153
1241
|
try {
|
|
1154
1242
|
this.#db
|
|
1155
|
-
?.prepare("UPDATE sessions SET relay_peer_id = ?, relay_addrs = ?, updated_at = ? WHERE
|
|
1156
|
-
.run(relay.relayPeerId, JSON.stringify(relay.relayAddrs), Date.now(), agentName, sessionId);
|
|
1243
|
+
?.prepare("UPDATE sessions SET relay_peer_id = ?, relay_addrs = ?, updated_at = ? WHERE agent_id = ? AND session_id = ?")
|
|
1244
|
+
.run(relay.relayPeerId, JSON.stringify(relay.relayAddrs), Date.now(), this.#requireAgentId(agentName), sessionId);
|
|
1157
1245
|
}
|
|
1158
1246
|
catch (err) {
|
|
1159
1247
|
this.#logger.warn("session.relay.endpoint.persist.failed", {
|
|
@@ -1595,8 +1683,22 @@ export class SessionNodeManager {
|
|
|
1595
1683
|
getSessionsByStatus(status) {
|
|
1596
1684
|
if (!this.#db)
|
|
1597
1685
|
return [];
|
|
1686
|
+
// Spans EVERY agent (cello_status is daemon-wide), so no single name can be resolved up front —
|
|
1687
|
+
// the display name is joined in from `agents`, its one source of truth. `agent_name` is no
|
|
1688
|
+
// longer a `sessions` column, so without this join buildActiveSessions/buildInterruptedSessions
|
|
1689
|
+
// read `row.agent_name` as undefined.
|
|
1690
|
+
//
|
|
1691
|
+
// DOD-AGENT-ID-JOINKEY-1 (reviewer Finding 1): INNER JOIN with `state != 'retired'`, NOT a bare
|
|
1692
|
+
// LEFT JOIN. This is the LIVE-status + reaper surface, and a retired agent is gone from the
|
|
1693
|
+
// runtime — its leftover session rows (kept for accountability, never re-statused) are not
|
|
1694
|
+
// resumable and must not appear here. If they did, the half-open reaper would resolve their
|
|
1695
|
+
// RETIRED name via #requireAgentId, which throws, taking down cello_status for the whole daemon.
|
|
1696
|
+
// Excluding them also guarantees a non-null agent_name on every returned row. The full historical
|
|
1697
|
+
// archive (getAllSessions) keeps its LEFT JOIN and still shows retired/orphaned rows.
|
|
1598
1698
|
return this.#db
|
|
1599
|
-
.prepare(
|
|
1699
|
+
.prepare(`SELECT s.*, a.agent_name AS agent_name
|
|
1700
|
+
FROM sessions s JOIN agents a ON a.agent_id = s.agent_id
|
|
1701
|
+
WHERE s.status = ? AND a.state != 'retired'`)
|
|
1600
1702
|
.all(status);
|
|
1601
1703
|
}
|
|
1602
1704
|
/**
|
|
@@ -1610,9 +1712,13 @@ export class SessionNodeManager {
|
|
|
1610
1712
|
getSessionsForAgent(agentName) {
|
|
1611
1713
|
if (!this.#db)
|
|
1612
1714
|
return [];
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1715
|
+
// Scoped by the STABLE id. `agent_name` is not a column of `sessions` any more, so it is stamped
|
|
1716
|
+
// back on for display — and it is exactly the name we just resolved the id FROM, so no join is
|
|
1717
|
+
// needed and no stale copy can exist.
|
|
1718
|
+
const rows = this.#db
|
|
1719
|
+
.prepare("SELECT * FROM sessions WHERE agent_id = ? ORDER BY updated_at DESC")
|
|
1720
|
+
.all(this.#requireAgentId(agentName));
|
|
1721
|
+
return rows.map((r) => ({ ...r, agent_name: agentName }));
|
|
1616
1722
|
}
|
|
1617
1723
|
/**
|
|
1618
1724
|
* Every persisted session across ALL agents, most-recently-updated first. Backs the daemon-wide
|
|
@@ -1622,8 +1728,13 @@ export class SessionNodeManager {
|
|
|
1622
1728
|
getAllSessions() {
|
|
1623
1729
|
if (!this.#db)
|
|
1624
1730
|
return [];
|
|
1731
|
+
// Spans EVERY agent, so no single name can be resolved up front: the display name is joined in
|
|
1732
|
+
// from `agents`, its one source of truth. LEFT JOIN, not INNER — a session whose agent row is
|
|
1733
|
+
// missing must still be listed (an invisible session is worse than an unnamed one).
|
|
1625
1734
|
return this.#db
|
|
1626
|
-
.prepare(
|
|
1735
|
+
.prepare(`SELECT s.*, a.agent_name AS agent_name
|
|
1736
|
+
FROM sessions s LEFT JOIN agents a ON a.agent_id = s.agent_id
|
|
1737
|
+
ORDER BY s.updated_at DESC`)
|
|
1627
1738
|
.all();
|
|
1628
1739
|
}
|
|
1629
1740
|
/**
|
|
@@ -1639,8 +1750,8 @@ export class SessionNodeManager {
|
|
|
1639
1750
|
if (!this.#db)
|
|
1640
1751
|
return;
|
|
1641
1752
|
this.#db
|
|
1642
|
-
.prepare("UPDATE sessions SET seal_legibility = ?, sealed_root_hex = ?, updated_at = ? WHERE
|
|
1643
|
-
.run(legibilityJson, sealedRootHex, Date.now(), agentName, sessionId);
|
|
1753
|
+
.prepare("UPDATE sessions SET seal_legibility = ?, sealed_root_hex = ?, updated_at = ? WHERE agent_id = ? AND session_id = ?")
|
|
1754
|
+
.run(legibilityJson, sealedRootHex, Date.now(), this.#requireAgentId(agentName), sessionId);
|
|
1644
1755
|
}
|
|
1645
1756
|
/**
|
|
1646
1757
|
* M8B FINDING-6 (cascade-2): persist a seal certificate for a session that may have NO local
|
|
@@ -1658,9 +1769,9 @@ export class SessionNodeManager {
|
|
|
1658
1769
|
const now = Date.now();
|
|
1659
1770
|
this.#db
|
|
1660
1771
|
.prepare(`INSERT OR IGNORE INTO sessions
|
|
1661
|
-
(session_id,
|
|
1772
|
+
(session_id, agent_id, counterparty_pubkey, status, created_at, updated_at)
|
|
1662
1773
|
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
1663
|
-
.run(sessionId, agentName, counterpartyPubkeyHex, "sealed", now, now);
|
|
1774
|
+
.run(sessionId, this.#requireAgentId(agentName), counterpartyPubkeyHex, "sealed", now, now);
|
|
1664
1775
|
this.recordSealCertificate(agentName, sessionId, sealedRootHex, legibilityJson);
|
|
1665
1776
|
}
|
|
1666
1777
|
/**
|
|
@@ -1673,8 +1784,8 @@ export class SessionNodeManager {
|
|
|
1673
1784
|
if (!this.#db)
|
|
1674
1785
|
return;
|
|
1675
1786
|
this.#db
|
|
1676
|
-
.prepare("UPDATE sessions SET counterparty_primary_pubkey = ?, updated_at = ? WHERE
|
|
1677
|
-
.run(primaryPubkeyHex, Date.now(), agentName, sessionId);
|
|
1787
|
+
.prepare("UPDATE sessions SET counterparty_primary_pubkey = ?, updated_at = ? WHERE agent_id = ? AND session_id = ?")
|
|
1788
|
+
.run(primaryPubkeyHex, Date.now(), this.#requireAgentId(agentName), sessionId);
|
|
1678
1789
|
}
|
|
1679
1790
|
/**
|
|
1680
1791
|
* M7-SESSION-004 (AC-005/AC-006): read the persisted seal certificate for a session.
|
|
@@ -1688,8 +1799,8 @@ export class SessionNodeManager {
|
|
|
1688
1799
|
if (!this.#db)
|
|
1689
1800
|
return null;
|
|
1690
1801
|
const row = this.#db
|
|
1691
|
-
.prepare("SELECT sealed_root_hex, seal_legibility FROM sessions WHERE
|
|
1692
|
-
.get(agentName, sessionId);
|
|
1802
|
+
.prepare("SELECT sealed_root_hex, seal_legibility FROM sessions WHERE agent_id = ? AND session_id = ?")
|
|
1803
|
+
.get(this.#requireAgentId(agentName), sessionId);
|
|
1693
1804
|
if (!row || !row.seal_legibility || !row.sealed_root_hex)
|
|
1694
1805
|
return null;
|
|
1695
1806
|
let legibility;
|
|
@@ -1741,8 +1852,8 @@ export class SessionNodeManager {
|
|
|
1741
1852
|
// the pre-check above raced (it cannot — DatabaseSync is synchronous), the
|
|
1742
1853
|
// UPDATE only mutates a row that is still active.
|
|
1743
1854
|
this.#db
|
|
1744
|
-
.prepare("UPDATE sessions SET status = 'interrupted', updated_at = ?, message_count = ?, interrupted_at = ? WHERE
|
|
1745
|
-
.run(now, authoritativeCount, interruptedAt, agentName, sessionId);
|
|
1855
|
+
.prepare("UPDATE sessions SET status = 'interrupted', updated_at = ?, message_count = ?, interrupted_at = ? WHERE agent_id = ? AND session_id = ? AND status = 'active'")
|
|
1856
|
+
.run(now, authoritativeCount, interruptedAt, this.#requireAgentId(agentName), sessionId);
|
|
1746
1857
|
}
|
|
1747
1858
|
catch (err) {
|
|
1748
1859
|
this.#logger.error("session.interrupt.db.write.failed", {
|
|
@@ -1826,9 +1937,9 @@ export class SessionNodeManager {
|
|
|
1826
1937
|
try {
|
|
1827
1938
|
this.#db
|
|
1828
1939
|
.prepare(`INSERT OR REPLACE INTO seal_interrupted_artifacts
|
|
1829
|
-
(
|
|
1940
|
+
(agent_id, session_id, role, own_leaf, counterparty_leaf, merkle_root, nonce, created_at)
|
|
1830
1941
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1831
|
-
.run(opts.agentName, opts.sessionId, opts.role, JSON.stringify(opts.ownLeaf), JSON.stringify(opts.counterpartyLeaf), opts.merkleRoot, opts.nonce, now);
|
|
1942
|
+
.run(this.#requireAgentId(opts.agentName), opts.sessionId, opts.role, JSON.stringify(opts.ownLeaf), JSON.stringify(opts.counterpartyLeaf), opts.merkleRoot, opts.nonce, now);
|
|
1832
1943
|
}
|
|
1833
1944
|
catch (err) {
|
|
1834
1945
|
this.#logger.error("session.interrupted.db.write.failed", {
|
|
@@ -1842,8 +1953,8 @@ export class SessionNodeManager {
|
|
|
1842
1953
|
// active-session seal). The guard still refuses to overwrite a terminal
|
|
1843
1954
|
// 'sealed' row or an already-pending one.
|
|
1844
1955
|
const result = this.#db
|
|
1845
|
-
.prepare("UPDATE sessions SET status = 'seal_interrupted_pending', updated_at = ? WHERE
|
|
1846
|
-
.run(now, opts.agentName, opts.sessionId);
|
|
1956
|
+
.prepare("UPDATE sessions SET status = 'seal_interrupted_pending', updated_at = ? WHERE agent_id = ? AND session_id = ? AND status IN ('active', 'interrupted')")
|
|
1957
|
+
.run(now, this.#requireAgentId(opts.agentName), opts.sessionId);
|
|
1847
1958
|
return Number(result.changes) > 0;
|
|
1848
1959
|
}
|
|
1849
1960
|
/**
|
|
@@ -1854,8 +1965,8 @@ export class SessionNodeManager {
|
|
|
1854
1965
|
if (!this.#db)
|
|
1855
1966
|
return null;
|
|
1856
1967
|
const row = this.#db
|
|
1857
|
-
.prepare("SELECT * FROM seal_interrupted_artifacts WHERE
|
|
1858
|
-
.get(agentName, sessionId);
|
|
1968
|
+
.prepare("SELECT * FROM seal_interrupted_artifacts WHERE agent_id = ? AND session_id = ?")
|
|
1969
|
+
.get(this.#requireAgentId(agentName), sessionId);
|
|
1859
1970
|
if (!row)
|
|
1860
1971
|
return null;
|
|
1861
1972
|
return {
|
|
@@ -1874,9 +1985,11 @@ export class SessionNodeManager {
|
|
|
1874
1985
|
if (!this.#db)
|
|
1875
1986
|
return null;
|
|
1876
1987
|
const row = this.#db
|
|
1877
|
-
.prepare("SELECT * FROM sessions WHERE
|
|
1878
|
-
.get(agentName, sessionId);
|
|
1879
|
-
|
|
1988
|
+
.prepare("SELECT * FROM sessions WHERE agent_id = ? AND session_id = ?")
|
|
1989
|
+
.get(this.#requireAgentId(agentName), sessionId);
|
|
1990
|
+
// `agent_name` is display-only and no longer stored on the row; stamp back the name whose
|
|
1991
|
+
// agent_id scoped this lookup (~50 daemon call sites read `record.agent_name`).
|
|
1992
|
+
return row ? { ...row, agent_name: agentName } : null;
|
|
1880
1993
|
}
|
|
1881
1994
|
/**
|
|
1882
1995
|
* MSG-2 startup-flush: the persisted relay endpoint for a session, or null if none was
|
|
@@ -1887,8 +2000,8 @@ export class SessionNodeManager {
|
|
|
1887
2000
|
if (!this.#db)
|
|
1888
2001
|
return null;
|
|
1889
2002
|
const row = this.#db
|
|
1890
|
-
.prepare("SELECT relay_peer_id, relay_addrs FROM sessions WHERE
|
|
1891
|
-
.get(agentName, sessionId);
|
|
2003
|
+
.prepare("SELECT relay_peer_id, relay_addrs FROM sessions WHERE agent_id = ? AND session_id = ?")
|
|
2004
|
+
.get(this.#requireAgentId(agentName), sessionId);
|
|
1892
2005
|
if (!row?.relay_peer_id || !row?.relay_addrs)
|
|
1893
2006
|
return null;
|
|
1894
2007
|
try {
|
|
@@ -1911,8 +2024,8 @@ export class SessionNodeManager {
|
|
|
1911
2024
|
if (!this.#db)
|
|
1912
2025
|
return [];
|
|
1913
2026
|
const rows = this.#db
|
|
1914
|
-
.prepare("SELECT DISTINCT relay_peer_id, relay_addrs FROM sessions WHERE
|
|
1915
|
-
.all(agentName);
|
|
2027
|
+
.prepare("SELECT DISTINCT relay_peer_id, relay_addrs FROM sessions WHERE agent_id = ? AND relay_peer_id IS NOT NULL")
|
|
2028
|
+
.all(this.#requireAgentId(agentName));
|
|
1916
2029
|
const byPeer = new Map();
|
|
1917
2030
|
for (const row of rows) {
|
|
1918
2031
|
if (!row.relay_peer_id || !row.relay_addrs)
|
|
@@ -1962,9 +2075,9 @@ export class SessionNodeManager {
|
|
|
1962
2075
|
try {
|
|
1963
2076
|
this.#db
|
|
1964
2077
|
.prepare(`INSERT INTO session_tree_leaves
|
|
1965
|
-
(
|
|
2078
|
+
(agent_id, session_id, leaf_index, leaf_kind, leaf_hash_hex, created_at)
|
|
1966
2079
|
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
1967
|
-
.run(agentName, sessionId, leafIndex, kind, leafHashHex, Date.now());
|
|
2080
|
+
.run(this.#requireAgentId(agentName), sessionId, leafIndex, kind, leafHashHex, Date.now());
|
|
1968
2081
|
// DAEMON-004 (finding #2): keep sessions.message_count synced to the tree
|
|
1969
2082
|
// size. message_count is the bilateral leafCount the seal flow signs over
|
|
1970
2083
|
// (handleSealInterruptedFlow / the responder). If it diverged from the
|
|
@@ -1972,8 +2085,8 @@ export class SessionNodeManager {
|
|
|
1972
2085
|
// truncated transcript and the bilateral leafCount check would mismatch.
|
|
1973
2086
|
// The tree (leafIndex + 1 leaves) is authoritative; the column tracks it.
|
|
1974
2087
|
this.#db
|
|
1975
|
-
.prepare("UPDATE sessions SET message_count = ?, updated_at = ? WHERE
|
|
1976
|
-
.run(leafIndex + 1, Date.now(), agentName, sessionId);
|
|
2088
|
+
.prepare("UPDATE sessions SET message_count = ?, updated_at = ? WHERE agent_id = ? AND session_id = ?")
|
|
2089
|
+
.run(leafIndex + 1, Date.now(), this.#requireAgentId(agentName), sessionId);
|
|
1977
2090
|
}
|
|
1978
2091
|
catch (err) {
|
|
1979
2092
|
// A persist failure must be visible, not swallowed: the in-memory tree
|
|
@@ -2803,8 +2916,8 @@ export class SessionNodeManager {
|
|
|
2803
2916
|
if (!this.#db)
|
|
2804
2917
|
return null;
|
|
2805
2918
|
const row = this.#db
|
|
2806
|
-
.prepare("SELECT sealed_root_hex FROM sessions WHERE
|
|
2807
|
-
.get(agentName, sessionId);
|
|
2919
|
+
.prepare("SELECT sealed_root_hex FROM sessions WHERE agent_id = ? AND session_id = ?")
|
|
2920
|
+
.get(this.#requireAgentId(agentName), sessionId);
|
|
2808
2921
|
return row?.sealed_root_hex ?? null;
|
|
2809
2922
|
}
|
|
2810
2923
|
// ─── CELLO-M7-MSG-001: delivery ACK / TTF tracking (send side) ──────────────
|
|
@@ -2973,8 +3086,8 @@ export class SessionNodeManager {
|
|
|
2973
3086
|
if (!this.#db)
|
|
2974
3087
|
return SessionTree.empty();
|
|
2975
3088
|
const rows = this.#db
|
|
2976
|
-
.prepare("SELECT leaf_kind, leaf_hash_hex FROM session_tree_leaves WHERE
|
|
2977
|
-
.all(agentName, sessionId);
|
|
3089
|
+
.prepare("SELECT leaf_kind, leaf_hash_hex FROM session_tree_leaves WHERE agent_id = ? AND session_id = ? ORDER BY leaf_index ASC")
|
|
3090
|
+
.all(this.#requireAgentId(agentName), sessionId);
|
|
2978
3091
|
return SessionTree.fromLeaves(rows.map((r) => ({ kind: r.leaf_kind === "ctrl" ? "ctrl" : "msg", hashHex: r.leaf_hash_hex })));
|
|
2979
3092
|
}
|
|
2980
3093
|
/**
|
|
@@ -3424,9 +3537,9 @@ export class SessionNodeManager {
|
|
|
3424
3537
|
try {
|
|
3425
3538
|
this.#db
|
|
3426
3539
|
.prepare(`INSERT INTO sessions
|
|
3427
|
-
(session_id,
|
|
3540
|
+
(session_id, agent_id, counterparty_pubkey, status, created_at, updated_at)
|
|
3428
3541
|
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
3429
|
-
.run(sessionId, agentName, counterpartyPubkey, status, now, now);
|
|
3542
|
+
.run(sessionId, this.#requireAgentId(agentName), counterpartyPubkey, status, now, now);
|
|
3430
3543
|
return true;
|
|
3431
3544
|
}
|
|
3432
3545
|
catch (err) {
|
|
@@ -3448,8 +3561,8 @@ export class SessionNodeManager {
|
|
|
3448
3561
|
if (!this.#db)
|
|
3449
3562
|
return 0;
|
|
3450
3563
|
const row = this.#db
|
|
3451
|
-
.prepare("SELECT COUNT(*) AS n FROM transcript WHERE
|
|
3452
|
-
.get(agentName, sessionId);
|
|
3564
|
+
.prepare("SELECT COUNT(*) AS n FROM transcript WHERE agent_id = ? AND session_id = ? AND direction = 'received'")
|
|
3565
|
+
.get(this.#requireAgentId(agentName), sessionId);
|
|
3453
3566
|
return row.n;
|
|
3454
3567
|
}
|
|
3455
3568
|
/** CC-5/F21: unilaterally mark a session locally-terminal ("abandoned") — retire its live node and
|
|
@@ -3471,8 +3584,8 @@ export class SessionNodeManager {
|
|
|
3471
3584
|
const now = Date.now();
|
|
3472
3585
|
try {
|
|
3473
3586
|
this.#db
|
|
3474
|
-
.prepare("UPDATE sessions SET status = ?, updated_at = ? WHERE
|
|
3475
|
-
.run(status, now, agentName, sessionId);
|
|
3587
|
+
.prepare("UPDATE sessions SET status = ?, updated_at = ? WHERE agent_id = ? AND session_id = ?")
|
|
3588
|
+
.run(status, now, this.#requireAgentId(agentName), sessionId);
|
|
3476
3589
|
return true;
|
|
3477
3590
|
}
|
|
3478
3591
|
catch (err) {
|