@securecoms/agent-bridge 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -1
- package/connectors/hermes/adapter.py +5 -1
- package/connectors/hermes/tests/test_adapter_wiring.py +6 -1
- package/connectors/openclaw/openclaw-entry.js +8 -4
- package/connectors/openclaw/openclaw-entry.smoke.test.ts +23 -1
- package/dist/bin.js +502 -35
- package/dist/bridge-agent.d.ts +65 -0
- package/dist/index.js +497 -30
- package/dist/reply-receipts.d.ts +30 -0
- package/dist/service/runtime-config-dir.d.ts +34 -0
- package/dist/version.d.ts +1 -1
- package/dist/ws-server.d.ts +4 -2
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -5460,8 +5460,11 @@ var RestMlsBackend = class {
|
|
|
5460
5460
|
queue_ids: queueIds
|
|
5461
5461
|
});
|
|
5462
5462
|
}
|
|
5463
|
-
async nackDelivery(
|
|
5464
|
-
await this.post("/api/v1/mls/delivery/nack", {
|
|
5463
|
+
async nackDelivery(deviceId, queueIds) {
|
|
5464
|
+
await this.post("/api/v1/mls/delivery/nack", {
|
|
5465
|
+
device_id: deviceId,
|
|
5466
|
+
queue_ids: queueIds
|
|
5467
|
+
});
|
|
5465
5468
|
}
|
|
5466
5469
|
async listGroupMessages(deviceId, groupId, opts) {
|
|
5467
5470
|
const params = new URLSearchParams({ device_id: deviceId, limit: String(opts?.limit ?? 100) });
|
|
@@ -5969,7 +5972,7 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
5969
5972
|
import { MLSGroupManager as MLSGroupManager3, bytesToHex as bytesToHex4, hexToBytes as hexToBytes3 } from "@agentvault/crypto";
|
|
5970
5973
|
|
|
5971
5974
|
// ../connect-mls-store/dist/schema.js
|
|
5972
|
-
var SCHEMA_VERSION =
|
|
5975
|
+
var SCHEMA_VERSION = 7;
|
|
5973
5976
|
function openSchema(db) {
|
|
5974
5977
|
db.exec("PRAGMA journal_mode=WAL");
|
|
5975
5978
|
const row = db.prepare("PRAGMA user_version").get();
|
|
@@ -6061,6 +6064,17 @@ function openSchema(db) {
|
|
|
6061
6064
|
);
|
|
6062
6065
|
`);
|
|
6063
6066
|
}
|
|
6067
|
+
if (row.user_version < 7) {
|
|
6068
|
+
db.exec(`
|
|
6069
|
+
CREATE TABLE IF NOT EXISTS agent_memory_fact_assignment (
|
|
6070
|
+
identity_id TEXT NOT NULL,
|
|
6071
|
+
assignment_key TEXT NOT NULL,
|
|
6072
|
+
conversation_id TEXT NOT NULL,
|
|
6073
|
+
applied_at INTEGER NOT NULL,
|
|
6074
|
+
PRIMARY KEY (identity_id, assignment_key)
|
|
6075
|
+
);
|
|
6076
|
+
`);
|
|
6077
|
+
}
|
|
6064
6078
|
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
6065
6079
|
}
|
|
6066
6080
|
|
|
@@ -6100,6 +6114,10 @@ function decodeBundle(bytes) {
|
|
|
6100
6114
|
}
|
|
6101
6115
|
};
|
|
6102
6116
|
}
|
|
6117
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
6118
|
+
var DEFAULT_MEMORY_RETENTION_MS = 30 * DAY_MS;
|
|
6119
|
+
var DEFAULT_MEMORY_FACT_RETENTION_K = 200;
|
|
6120
|
+
var DEFAULT_MEMORY_FACT_RETENTION_MS = 90 * DAY_MS;
|
|
6103
6121
|
var SqliteMlsStateStore = class {
|
|
6104
6122
|
db;
|
|
6105
6123
|
id;
|
|
@@ -6113,6 +6131,10 @@ var SqliteMlsStateStore = class {
|
|
|
6113
6131
|
bundles = /* @__PURE__ */ new Map();
|
|
6114
6132
|
lastKpIds = /* @__PURE__ */ new Map();
|
|
6115
6133
|
memoryRetentionK;
|
|
6134
|
+
memoryRetentionMs;
|
|
6135
|
+
memoryFactRetentionK;
|
|
6136
|
+
memoryFactRetentionMs;
|
|
6137
|
+
memoryNow;
|
|
6116
6138
|
// M0(b)/F4-1 + C1 + H2: in-memory ONLY for now — the daemon side of FileSafe
|
|
6117
6139
|
// (this store) isn't wired into the multi-member vault yet (Task 5's capture
|
|
6118
6140
|
// sites are all connect-web/browser owner paths). Kept here purely so
|
|
@@ -6127,6 +6149,10 @@ var SqliteMlsStateStore = class {
|
|
|
6127
6149
|
this.id = opts.identityId;
|
|
6128
6150
|
this.cipher = opts.cipherFor(opts.identityId);
|
|
6129
6151
|
this.memoryRetentionK = opts.memoryRetentionK ?? 500;
|
|
6152
|
+
this.memoryRetentionMs = opts.memoryRetentionMs ?? DEFAULT_MEMORY_RETENTION_MS;
|
|
6153
|
+
this.memoryFactRetentionK = opts.memoryFactRetentionK ?? DEFAULT_MEMORY_FACT_RETENTION_K;
|
|
6154
|
+
this.memoryFactRetentionMs = opts.memoryFactRetentionMs ?? DEFAULT_MEMORY_FACT_RETENTION_MS;
|
|
6155
|
+
this.memoryNow = opts.memoryNow ?? Date.now;
|
|
6130
6156
|
try {
|
|
6131
6157
|
this.db = new DatabaseSync(opts.dbPath);
|
|
6132
6158
|
} catch (e) {
|
|
@@ -6344,23 +6370,24 @@ var SqliteMlsStateStore = class {
|
|
|
6344
6370
|
const dedupKey = input.messageId ?? input.idempotencyKey ?? null;
|
|
6345
6371
|
if (!dedupKey)
|
|
6346
6372
|
throw new Error("recordMemory requires message_id or idempotency_key");
|
|
6373
|
+
const now = this.memoryNow();
|
|
6374
|
+
this.pruneMemory(input.conversationId, now);
|
|
6347
6375
|
const sealed = await this.cipher.seal(new TextEncoder().encode(input.text));
|
|
6348
6376
|
const info = this.db.prepare(`INSERT OR IGNORE INTO agent_memory
|
|
6349
6377
|
(identity_id, conversation_id, seq, role, peer_id, sealed_text, modality, message_id, idempotency_key, dedup_key, recorded_at)
|
|
6350
6378
|
VALUES (?, ?,
|
|
6351
6379
|
(SELECT COALESCE(MAX(seq),0)+1 FROM agent_memory WHERE identity_id=? AND conversation_id=?),
|
|
6352
|
-
?, ?, ?, ?, ?, ?, ?, ?)`).run(this.id, input.conversationId, this.id, input.conversationId, input.role, input.peerId ?? null, sealed, input.modality, input.messageId ?? null, input.idempotencyKey ?? null, dedupKey,
|
|
6380
|
+
?, ?, ?, ?, ?, ?, ?, ?)`).run(this.id, input.conversationId, this.id, input.conversationId, input.role, input.peerId ?? null, sealed, input.modality, input.messageId ?? null, input.idempotencyKey ?? null, dedupKey, now);
|
|
6353
6381
|
if (info.changes === 0) {
|
|
6354
6382
|
const row2 = this.db.prepare("SELECT seq FROM agent_memory WHERE identity_id=? AND conversation_id=? AND dedup_key=?").get(this.id, input.conversationId, dedupKey);
|
|
6355
6383
|
return { seq: row2.seq, deduped: true };
|
|
6356
6384
|
}
|
|
6357
6385
|
const row = this.db.prepare("SELECT MAX(seq) AS seq FROM agent_memory WHERE identity_id=? AND conversation_id=?").get(this.id, input.conversationId);
|
|
6358
|
-
|
|
6359
|
-
this.db.prepare("DELETE FROM agent_memory WHERE identity_id=? AND conversation_id=? AND seq <= ?").run(this.id, input.conversationId, row.seq - this.memoryRetentionK);
|
|
6360
|
-
}
|
|
6386
|
+
this.pruneMemoryTurns(input.conversationId, now);
|
|
6361
6387
|
return { seq: row.seq, deduped: false };
|
|
6362
6388
|
}
|
|
6363
6389
|
async memoryContext(conversationId, limit = 50) {
|
|
6390
|
+
this.pruneMemory(conversationId, this.memoryNow());
|
|
6364
6391
|
const rows = this.db.prepare(`SELECT seq, role, peer_id, sealed_text, recorded_at FROM agent_memory
|
|
6365
6392
|
WHERE identity_id=? AND conversation_id=? ORDER BY seq DESC LIMIT ?`).all(this.id, conversationId, limit);
|
|
6366
6393
|
const recent = [];
|
|
@@ -6375,9 +6402,94 @@ var SqliteMlsStateStore = class {
|
|
|
6375
6402
|
return { recent, facts };
|
|
6376
6403
|
}
|
|
6377
6404
|
async recordMemoryFact(conversationId, fact) {
|
|
6405
|
+
const now = this.memoryNow();
|
|
6378
6406
|
const sealed = await this.cipher.seal(new TextEncoder().encode(fact));
|
|
6379
6407
|
this.db.prepare(`INSERT INTO agent_memory_fact (identity_id, conversation_id, fact_id, sealed_fact, added_at)
|
|
6380
|
-
VALUES (?, ?, (SELECT COALESCE(MAX(fact_id),0)+1 FROM agent_memory_fact WHERE identity_id=? AND conversation_id=?), ?, ?)`).run(this.id, conversationId, this.id, conversationId, sealed,
|
|
6408
|
+
VALUES (?, ?, (SELECT COALESCE(MAX(fact_id),0)+1 FROM agent_memory_fact WHERE identity_id=? AND conversation_id=?), ?, ?)`).run(this.id, conversationId, this.id, conversationId, sealed, now);
|
|
6409
|
+
this.pruneMemoryFacts(conversationId, now);
|
|
6410
|
+
}
|
|
6411
|
+
/**
|
|
6412
|
+
* Import one owner-approved legacy fact exactly once. `assignmentKey` is
|
|
6413
|
+
* content-free (`source-id:line-hash`) and intentionally excludes the target
|
|
6414
|
+
* channel: the same legacy fact can therefore never be replayed into a
|
|
6415
|
+
* second audience. The ledger and sealed fact commit atomically.
|
|
6416
|
+
*/
|
|
6417
|
+
async recordImportedMemoryFact(conversationId, fact, assignmentKey) {
|
|
6418
|
+
if (!/^[0-9a-f]{64}:[0-9a-f]{64}$/.test(assignmentKey)) {
|
|
6419
|
+
throw new Error("invalid legacy memory assignment key");
|
|
6420
|
+
}
|
|
6421
|
+
const existing = this.db.prepare(`SELECT conversation_id FROM agent_memory_fact_assignment
|
|
6422
|
+
WHERE identity_id=? AND assignment_key=?`).get(this.id, assignmentKey);
|
|
6423
|
+
if (existing) {
|
|
6424
|
+
if (existing.conversation_id !== conversationId) {
|
|
6425
|
+
throw new Error("legacy memory fact is already assigned to another channel");
|
|
6426
|
+
}
|
|
6427
|
+
return { factId: 0, deduped: true };
|
|
6428
|
+
}
|
|
6429
|
+
const now = this.memoryNow();
|
|
6430
|
+
const sealed = await this.cipher.seal(new TextEncoder().encode(fact));
|
|
6431
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
6432
|
+
try {
|
|
6433
|
+
const raced = this.db.prepare(`SELECT conversation_id FROM agent_memory_fact_assignment
|
|
6434
|
+
WHERE identity_id=? AND assignment_key=?`).get(this.id, assignmentKey);
|
|
6435
|
+
if (raced) {
|
|
6436
|
+
if (raced.conversation_id !== conversationId) {
|
|
6437
|
+
throw new Error("legacy memory fact is already assigned to another channel");
|
|
6438
|
+
}
|
|
6439
|
+
this.db.exec("COMMIT");
|
|
6440
|
+
return { factId: 0, deduped: true };
|
|
6441
|
+
}
|
|
6442
|
+
const row = this.db.prepare("SELECT COALESCE(MAX(fact_id),0)+1 AS fact_id FROM agent_memory_fact WHERE identity_id=? AND conversation_id=?").get(this.id, conversationId);
|
|
6443
|
+
this.db.prepare(`INSERT INTO agent_memory_fact (identity_id, conversation_id, fact_id, sealed_fact, added_at)
|
|
6444
|
+
VALUES (?, ?, ?, ?, ?)`).run(this.id, conversationId, row.fact_id, sealed, now);
|
|
6445
|
+
this.db.prepare(`INSERT INTO agent_memory_fact_assignment (identity_id, assignment_key, conversation_id, applied_at)
|
|
6446
|
+
VALUES (?, ?, ?, ?)`).run(this.id, assignmentKey, conversationId, now);
|
|
6447
|
+
this.pruneMemoryFacts(conversationId, now);
|
|
6448
|
+
this.db.exec("COMMIT");
|
|
6449
|
+
return { factId: row.fact_id, deduped: false };
|
|
6450
|
+
} catch (error) {
|
|
6451
|
+
this.db.exec("ROLLBACK");
|
|
6452
|
+
throw error;
|
|
6453
|
+
}
|
|
6454
|
+
}
|
|
6455
|
+
/** Remove every durable turn and fact in one channel-scoped namespace. */
|
|
6456
|
+
purgeMemory(conversationId) {
|
|
6457
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
6458
|
+
try {
|
|
6459
|
+
this.db.prepare("DELETE FROM agent_memory WHERE identity_id=? AND conversation_id=?").run(this.id, conversationId);
|
|
6460
|
+
this.db.prepare("DELETE FROM agent_memory_fact WHERE identity_id=? AND conversation_id=?").run(this.id, conversationId);
|
|
6461
|
+
this.db.prepare("DELETE FROM agent_memory_fact_assignment WHERE identity_id=? AND conversation_id=?").run(this.id, conversationId);
|
|
6462
|
+
this.db.exec("COMMIT");
|
|
6463
|
+
} catch (error) {
|
|
6464
|
+
this.db.exec("ROLLBACK");
|
|
6465
|
+
throw error;
|
|
6466
|
+
}
|
|
6467
|
+
}
|
|
6468
|
+
pruneMemory(conversationId, now) {
|
|
6469
|
+
this.pruneMemoryTurns(conversationId, now);
|
|
6470
|
+
this.pruneMemoryFacts(conversationId, now);
|
|
6471
|
+
}
|
|
6472
|
+
pruneMemoryTurns(conversationId, now) {
|
|
6473
|
+
if (this.memoryRetentionMs > 0) {
|
|
6474
|
+
this.db.prepare("DELETE FROM agent_memory WHERE identity_id=? AND conversation_id=? AND recorded_at < ?").run(this.id, conversationId, now - this.memoryRetentionMs);
|
|
6475
|
+
}
|
|
6476
|
+
if (this.memoryRetentionK > 0) {
|
|
6477
|
+
const row = this.db.prepare("SELECT MAX(seq) AS seq FROM agent_memory WHERE identity_id=? AND conversation_id=?").get(this.id, conversationId);
|
|
6478
|
+
if (row.seq != null) {
|
|
6479
|
+
this.db.prepare("DELETE FROM agent_memory WHERE identity_id=? AND conversation_id=? AND seq <= ?").run(this.id, conversationId, row.seq - this.memoryRetentionK);
|
|
6480
|
+
}
|
|
6481
|
+
}
|
|
6482
|
+
}
|
|
6483
|
+
pruneMemoryFacts(conversationId, now) {
|
|
6484
|
+
if (this.memoryFactRetentionMs > 0) {
|
|
6485
|
+
this.db.prepare("DELETE FROM agent_memory_fact WHERE identity_id=? AND conversation_id=? AND added_at < ?").run(this.id, conversationId, now - this.memoryFactRetentionMs);
|
|
6486
|
+
}
|
|
6487
|
+
if (this.memoryFactRetentionK > 0) {
|
|
6488
|
+
const row = this.db.prepare("SELECT MAX(fact_id) AS fact_id FROM agent_memory_fact WHERE identity_id=? AND conversation_id=?").get(this.id, conversationId);
|
|
6489
|
+
if (row.fact_id != null) {
|
|
6490
|
+
this.db.prepare("DELETE FROM agent_memory_fact WHERE identity_id=? AND conversation_id=? AND fact_id <= ?").run(this.id, conversationId, row.fact_id - this.memoryFactRetentionK);
|
|
6491
|
+
}
|
|
6492
|
+
}
|
|
6381
6493
|
}
|
|
6382
6494
|
/** @internal test-only */
|
|
6383
6495
|
_rawMainForTest(identityId, channelId) {
|
|
@@ -7469,6 +7581,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
7469
7581
|
import {
|
|
7470
7582
|
chmodSync,
|
|
7471
7583
|
existsSync,
|
|
7584
|
+
lstatSync,
|
|
7472
7585
|
mkdirSync,
|
|
7473
7586
|
readFileSync as readFileSync6,
|
|
7474
7587
|
renameSync,
|
|
@@ -7476,7 +7589,7 @@ import {
|
|
|
7476
7589
|
unlinkSync,
|
|
7477
7590
|
writeFileSync
|
|
7478
7591
|
} from "node:fs";
|
|
7479
|
-
import { basename as basename2, join as join8 } from "node:path";
|
|
7592
|
+
import { basename as basename2, isAbsolute, join as join8 } from "node:path";
|
|
7480
7593
|
var MEMORY_FILE = "MEMORY.md";
|
|
7481
7594
|
var REAL_FS = {
|
|
7482
7595
|
mkdir: (p, mode) => mkdirSync(p, { recursive: true, mode }),
|
|
@@ -7551,6 +7664,54 @@ function lineHash(line) {
|
|
|
7551
7664
|
return createHash("sha256").update(line.trim(), "utf8").digest("hex");
|
|
7552
7665
|
}
|
|
7553
7666
|
var LOCAL_REPORT_FILE = "local-config.json";
|
|
7667
|
+
var LEGACY_MEMORY_SOURCE_MAX_BYTES = 4 * 1024 * 1024;
|
|
7668
|
+
var LEGACY_MEMORY_REPORT_MAX_BYTES = 4 * 1024 * 1024;
|
|
7669
|
+
var LEGACY_MEMORY_REPORT_MAX_SOURCES = 100;
|
|
7670
|
+
var LEGACY_MEMORY_FACT_MAX_BYTES = 1024;
|
|
7671
|
+
var LEGACY_MEMORY_FACT_DECISION_MAX = 500;
|
|
7672
|
+
function legacySourceId(source, bytes) {
|
|
7673
|
+
return createHash("sha256").update(source.kind).update("\0").update(source.channel_id ?? "").update("\0").update(source.path).update("\0").update(bytes).digest("hex");
|
|
7674
|
+
}
|
|
7675
|
+
function localLegacySources(doc) {
|
|
7676
|
+
if (!Array.isArray(doc.legacy_memory)) return [];
|
|
7677
|
+
const out = [];
|
|
7678
|
+
let contentBudget = LEGACY_MEMORY_REPORT_MAX_BYTES;
|
|
7679
|
+
for (const raw of doc.legacy_memory) {
|
|
7680
|
+
if (out.length >= LEGACY_MEMORY_REPORT_MAX_SOURCES) break;
|
|
7681
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
7682
|
+
const source = raw;
|
|
7683
|
+
const kind = source.kind;
|
|
7684
|
+
const path = source.path;
|
|
7685
|
+
const channelId = source.channelId;
|
|
7686
|
+
if (kind !== "global-memory" && kind !== "conversation-history" && kind !== "captured-memory" || typeof path !== "string" || !isAbsolute(path) || channelId !== void 0 && typeof channelId !== "string") continue;
|
|
7687
|
+
try {
|
|
7688
|
+
const st = lstatSync(path);
|
|
7689
|
+
if (!st.isFile() || st.isSymbolicLink()) continue;
|
|
7690
|
+
if (st.size <= LEGACY_MEMORY_SOURCE_MAX_BYTES && st.size > contentBudget) continue;
|
|
7691
|
+
const oversize = st.size > LEGACY_MEMORY_SOURCE_MAX_BYTES;
|
|
7692
|
+
const bytes = oversize ? Buffer.from(`oversize:${st.size}:${st.mtimeMs}:${st.ctimeMs}`, "utf8") : Buffer.from(readFileSync6(path));
|
|
7693
|
+
if (!oversize && (bytes.byteLength > LEGACY_MEMORY_SOURCE_MAX_BYTES || bytes.byteLength > contentBudget)) {
|
|
7694
|
+
continue;
|
|
7695
|
+
}
|
|
7696
|
+
const exportable = !oversize;
|
|
7697
|
+
const candidate = {
|
|
7698
|
+
id: "",
|
|
7699
|
+
kind,
|
|
7700
|
+
...typeof channelId === "string" && channelId ? { channel_id: channelId } : {},
|
|
7701
|
+
path,
|
|
7702
|
+
content: exportable ? bytes.toString("utf8") : null,
|
|
7703
|
+
bytes: exportable ? bytes.byteLength : st.size,
|
|
7704
|
+
exportable
|
|
7705
|
+
};
|
|
7706
|
+
candidate.id = legacySourceId(candidate, bytes);
|
|
7707
|
+
if (source.id !== candidate.id) continue;
|
|
7708
|
+
out.push(candidate);
|
|
7709
|
+
if (exportable) contentBudget -= bytes.byteLength;
|
|
7710
|
+
} catch {
|
|
7711
|
+
}
|
|
7712
|
+
}
|
|
7713
|
+
return out;
|
|
7714
|
+
}
|
|
7554
7715
|
function readLocalReport(home, identityId) {
|
|
7555
7716
|
const p = join8(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
|
|
7556
7717
|
if (!existsSync(p)) return null;
|
|
@@ -7567,7 +7728,93 @@ function readLocalReport(home, identityId) {
|
|
|
7567
7728
|
const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.fromEntries(
|
|
7568
7729
|
Object.entries(raw).filter(([, v]) => typeof v === "string")
|
|
7569
7730
|
) : {};
|
|
7570
|
-
return {
|
|
7731
|
+
return {
|
|
7732
|
+
persona,
|
|
7733
|
+
has_search_key: hasSearchKey,
|
|
7734
|
+
has_llm_key: hasLlmKey,
|
|
7735
|
+
legacy_memory: localLegacySources(doc).map(({ path: _path, ...source }) => source)
|
|
7736
|
+
};
|
|
7737
|
+
}
|
|
7738
|
+
function deleteLegacyMemorySources(home, identityId, ids) {
|
|
7739
|
+
if (ids.length === 0) return 0;
|
|
7740
|
+
const p = join8(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
|
|
7741
|
+
if (!existsSync(p)) return 0;
|
|
7742
|
+
let doc;
|
|
7743
|
+
try {
|
|
7744
|
+
doc = JSON.parse(readFileSync6(p, "utf8"));
|
|
7745
|
+
} catch {
|
|
7746
|
+
return 0;
|
|
7747
|
+
}
|
|
7748
|
+
const wanted = new Set(ids);
|
|
7749
|
+
let deleted = 0;
|
|
7750
|
+
for (const source of localLegacySources(doc)) {
|
|
7751
|
+
if (!wanted.has(source.id)) continue;
|
|
7752
|
+
try {
|
|
7753
|
+
unlinkSync(source.path);
|
|
7754
|
+
deleted += 1;
|
|
7755
|
+
} catch {
|
|
7756
|
+
}
|
|
7757
|
+
}
|
|
7758
|
+
return deleted;
|
|
7759
|
+
}
|
|
7760
|
+
async function applyLegacyMemoryFactDecisions(home, identityId, rawDecisions, recordFact) {
|
|
7761
|
+
if (!Array.isArray(rawDecisions) || rawDecisions.length > LEGACY_MEMORY_FACT_DECISION_MAX) {
|
|
7762
|
+
return { sourcesDeleted: 0, factsAssigned: 0 };
|
|
7763
|
+
}
|
|
7764
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
7765
|
+
for (const raw of rawDecisions) {
|
|
7766
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
7767
|
+
return { sourcesDeleted: 0, factsAssigned: 0 };
|
|
7768
|
+
}
|
|
7769
|
+
const d = raw;
|
|
7770
|
+
if (typeof d.source_id !== "string" || !/^[0-9a-f]{64}$/.test(d.source_id) || typeof d.fact_hash !== "string" || !/^[0-9a-f]{64}$/.test(d.fact_hash) || d.action !== "assign" && d.action !== "delete" || d.action === "assign" && (typeof d.channel_id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(d.channel_id)) || d.action === "delete" && d.channel_id !== void 0) return { sourcesDeleted: 0, factsAssigned: 0 };
|
|
7771
|
+
const decision = d;
|
|
7772
|
+
const current = bySource.get(decision.source_id);
|
|
7773
|
+
if (current === null) continue;
|
|
7774
|
+
const group = current ?? /* @__PURE__ */ new Map();
|
|
7775
|
+
const prior = group.get(decision.fact_hash);
|
|
7776
|
+
if (prior && (prior.action !== decision.action || prior.channel_id !== decision.channel_id)) {
|
|
7777
|
+
bySource.set(decision.source_id, null);
|
|
7778
|
+
continue;
|
|
7779
|
+
}
|
|
7780
|
+
group.set(decision.fact_hash, decision);
|
|
7781
|
+
bySource.set(decision.source_id, group);
|
|
7782
|
+
}
|
|
7783
|
+
const reportPath = join8(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
|
|
7784
|
+
if (!existsSync(reportPath)) return { sourcesDeleted: 0, factsAssigned: 0 };
|
|
7785
|
+
let doc;
|
|
7786
|
+
try {
|
|
7787
|
+
doc = JSON.parse(readFileSync6(reportPath, "utf8"));
|
|
7788
|
+
} catch {
|
|
7789
|
+
return { sourcesDeleted: 0, factsAssigned: 0 };
|
|
7790
|
+
}
|
|
7791
|
+
let sourcesDeleted = 0;
|
|
7792
|
+
let factsAssigned = 0;
|
|
7793
|
+
for (const source of localLegacySources(doc)) {
|
|
7794
|
+
const decisions = bySource.get(source.id);
|
|
7795
|
+
if (!decisions || source.kind !== "global-memory" || source.content === null) continue;
|
|
7796
|
+
const facts = source.content.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
7797
|
+
const currentHashes = new Set(facts.map(lineHash));
|
|
7798
|
+
if (currentHashes.size === 0 || decisions.size !== currentHashes.size || [...currentHashes].some((hash) => !decisions.has(hash)) || [...decisions].some(([hash]) => !currentHashes.has(hash))) continue;
|
|
7799
|
+
const uniqueFacts = /* @__PURE__ */ new Map();
|
|
7800
|
+
for (const fact of facts) uniqueFacts.set(lineHash(fact), fact);
|
|
7801
|
+
const assignments = [...decisions].filter(([, decision]) => decision.action === "assign");
|
|
7802
|
+
if (assignments.some(([hash]) => Buffer.byteLength(uniqueFacts.get(hash) ?? "", "utf8") > LEGACY_MEMORY_FACT_MAX_BYTES)) {
|
|
7803
|
+
continue;
|
|
7804
|
+
}
|
|
7805
|
+
try {
|
|
7806
|
+
for (const [hash, decision] of assignments) {
|
|
7807
|
+
await recordFact(decision.channel_id, uniqueFacts.get(hash), `${source.id}:${hash}`);
|
|
7808
|
+
factsAssigned += 1;
|
|
7809
|
+
}
|
|
7810
|
+
const stillCurrent = localLegacySources(doc).some((candidate) => candidate.id === source.id);
|
|
7811
|
+
if (!stillCurrent) continue;
|
|
7812
|
+
unlinkSync(source.path);
|
|
7813
|
+
sourcesDeleted += 1;
|
|
7814
|
+
} catch {
|
|
7815
|
+
}
|
|
7816
|
+
}
|
|
7817
|
+
return { sourcesDeleted, factsAssigned };
|
|
7571
7818
|
}
|
|
7572
7819
|
function readManagedMemory(home, identityId) {
|
|
7573
7820
|
const p = join8(runtimeConfigDir(home, identityId), MEMORY_FILE);
|
|
@@ -7596,6 +7843,51 @@ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
|
|
|
7596
7843
|
|
|
7597
7844
|
// src/bridge-agent.ts
|
|
7598
7845
|
import { homedir as homedir2 } from "node:os";
|
|
7846
|
+
|
|
7847
|
+
// src/reply-receipts.ts
|
|
7848
|
+
var DEFAULT_TTL_MS = 15 * 60 * 1e3;
|
|
7849
|
+
var DEFAULT_CAPACITY2 = 256;
|
|
7850
|
+
var RuntimeReplyReceiptLedger = class {
|
|
7851
|
+
receipts = /* @__PURE__ */ new Map();
|
|
7852
|
+
ttlMs;
|
|
7853
|
+
capacity;
|
|
7854
|
+
now;
|
|
7855
|
+
constructor(opts = {}) {
|
|
7856
|
+
this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
7857
|
+
this.capacity = opts.capacity ?? DEFAULT_CAPACITY2;
|
|
7858
|
+
this.now = opts.now ?? Date.now;
|
|
7859
|
+
if (!Number.isFinite(this.ttlMs) || this.ttlMs <= 0) throw new Error("reply receipt ttlMs must be positive");
|
|
7860
|
+
if (!Number.isInteger(this.capacity) || this.capacity <= 0) throw new Error("reply receipt capacity must be positive");
|
|
7861
|
+
}
|
|
7862
|
+
record(inReplyTo, channelId, replyMessageId) {
|
|
7863
|
+
const receipt = Object.freeze({
|
|
7864
|
+
in_reply_to: inReplyTo,
|
|
7865
|
+
reply_message_id: replyMessageId,
|
|
7866
|
+
channel_id: channelId,
|
|
7867
|
+
replied_at: this.now()
|
|
7868
|
+
});
|
|
7869
|
+
this.prune(receipt.replied_at);
|
|
7870
|
+
this.receipts.delete(inReplyTo);
|
|
7871
|
+
this.receipts.set(inReplyTo, receipt);
|
|
7872
|
+
while (this.receipts.size > this.capacity) {
|
|
7873
|
+
const oldest = this.receipts.keys().next().value;
|
|
7874
|
+
if (oldest === void 0) break;
|
|
7875
|
+
this.receipts.delete(oldest);
|
|
7876
|
+
}
|
|
7877
|
+
return receipt;
|
|
7878
|
+
}
|
|
7879
|
+
get(inReplyTo) {
|
|
7880
|
+
this.prune(this.now());
|
|
7881
|
+
return this.receipts.get(inReplyTo) ?? null;
|
|
7882
|
+
}
|
|
7883
|
+
prune(now) {
|
|
7884
|
+
for (const [key, receipt] of this.receipts) {
|
|
7885
|
+
if (now - receipt.replied_at >= this.ttlMs) this.receipts.delete(key);
|
|
7886
|
+
}
|
|
7887
|
+
}
|
|
7888
|
+
};
|
|
7889
|
+
|
|
7890
|
+
// src/bridge-agent.ts
|
|
7599
7891
|
var VOICE_MODE_GUIDANCE = "Reply immediately and conversationally, one short sentence per message; never bundle multiple points into one message.";
|
|
7600
7892
|
var FAST_PATH_SPOKEN_GUIDANCE = "A one-sentence spoken acknowledgement has already been sent for this turn. Reply only with information it did not contain, in one or two short sentences; if there is nothing to add, send nothing.";
|
|
7601
7893
|
var ROOM_VOICE_GUIDANCE_MENTION_ONLY = "You are in a group voice room. Reply ONLY when a message @mentions your display name. Keep replies to one short sentence. When you should not reply, send nothing.";
|
|
@@ -7865,6 +8157,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
7865
8157
|
channelOwner = /* @__PURE__ */ new Map();
|
|
7866
8158
|
buffer = [];
|
|
7867
8159
|
maxBuffer;
|
|
8160
|
+
/**
|
|
8161
|
+
* Short-lived, content-free runtime reply correlations. The attached runtime
|
|
8162
|
+
* supplies the inbound message id only when an outbound send is its reply;
|
|
8163
|
+
* ws-server/http-server record the receipt after send() publishes.
|
|
8164
|
+
*/
|
|
8165
|
+
runtimeReplyReceipts = new RuntimeReplyReceiptLedger();
|
|
7868
8166
|
/** Cumulative count of messages shed by the buffer cap (surfaced on health()/metrics()). */
|
|
7869
8167
|
droppedMessages = 0;
|
|
7870
8168
|
/**
|
|
@@ -8090,6 +8388,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8090
8388
|
waiters = [];
|
|
8091
8389
|
pingHandle;
|
|
8092
8390
|
safetyTimer;
|
|
8391
|
+
runtimeConfigTimer;
|
|
8093
8392
|
listeners = /* @__PURE__ */ new Set();
|
|
8094
8393
|
startedAt = 0;
|
|
8095
8394
|
store;
|
|
@@ -8199,25 +8498,65 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8199
8498
|
});
|
|
8200
8499
|
if (!res.ok) throw new Error(`runtime-config ${res.status}`);
|
|
8201
8500
|
const payload = await res.json();
|
|
8202
|
-
if (!payload
|
|
8501
|
+
if (!payload) {
|
|
8502
|
+
process.stderr.write("[bridge] runtime config not applied (empty response)\n");
|
|
8503
|
+
return;
|
|
8504
|
+
}
|
|
8505
|
+
if (typeof payload.version !== "number") {
|
|
8203
8506
|
process.stderr.write(
|
|
8204
|
-
`[bridge] runtime config not applied (version ${JSON.stringify(payload
|
|
8507
|
+
`[bridge] runtime config not applied (version ${JSON.stringify(payload.version ?? null)} \u2014 invalid response)
|
|
8205
8508
|
`
|
|
8206
8509
|
);
|
|
8207
8510
|
return;
|
|
8208
8511
|
}
|
|
8512
|
+
const version = payload.version;
|
|
8513
|
+
if (!version) {
|
|
8514
|
+
writeManagedFiles(this.home, this.cfg.identityId, {});
|
|
8515
|
+
process.stderr.write(
|
|
8516
|
+
`[bridge] runtime config not applied (version ${JSON.stringify(payload?.version ?? null)} \u2014 no saved config)
|
|
8517
|
+
`
|
|
8518
|
+
);
|
|
8519
|
+
}
|
|
8209
8520
|
const skillsJson = payload.skills_json ?? {};
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8521
|
+
if (version) {
|
|
8522
|
+
writeManagedFiles(this.home, this.cfg.identityId, {
|
|
8523
|
+
...payload.files ?? {},
|
|
8524
|
+
"skills.json": `${JSON.stringify(skillsJson, null, 2)}
|
|
8213
8525
|
`
|
|
8214
|
-
|
|
8526
|
+
});
|
|
8527
|
+
}
|
|
8215
8528
|
if (payload.memory_prunes?.length) {
|
|
8216
8529
|
pruneManagedMemory(this.home, this.cfg.identityId, payload.memory_prunes);
|
|
8217
8530
|
}
|
|
8531
|
+
if (payload.legacy_memory_deletes?.length) {
|
|
8532
|
+
deleteLegacyMemorySources(this.home, this.cfg.identityId, payload.legacy_memory_deletes);
|
|
8533
|
+
}
|
|
8534
|
+
if (payload.legacy_memory_fact_decisions?.length) {
|
|
8535
|
+
const provider = this.cfg.memoryProvider ?? "securecoms";
|
|
8536
|
+
const result = await applyLegacyMemoryFactDecisions(
|
|
8537
|
+
this.home,
|
|
8538
|
+
this.cfg.identityId,
|
|
8539
|
+
payload.legacy_memory_fact_decisions,
|
|
8540
|
+
async (channelId, fact, assignmentKey) => {
|
|
8541
|
+
if (provider === "none" || provider === "hermes") {
|
|
8542
|
+
throw new Error("configured memory provider has no protected channel-fact import path");
|
|
8543
|
+
}
|
|
8544
|
+
if (provider === "openclaw" && !await this.mayReadOpenClawMemory(channelId)) {
|
|
8545
|
+
throw new Error("OpenClaw legacy fact target is not the active owner DM");
|
|
8546
|
+
}
|
|
8547
|
+
await this.store.recordImportedMemoryFact(channelId, fact, assignmentKey);
|
|
8548
|
+
}
|
|
8549
|
+
);
|
|
8550
|
+
if (result.sourcesDeleted > 0) {
|
|
8551
|
+
process.stderr.write(
|
|
8552
|
+
`[bridge] legacy-memory disposition applied (sources=${result.sourcesDeleted}, assigned=${result.factsAssigned})
|
|
8553
|
+
`
|
|
8554
|
+
);
|
|
8555
|
+
}
|
|
8556
|
+
}
|
|
8218
8557
|
const local = readLocalReport(this.home, this.cfg.identityId);
|
|
8219
8558
|
const report = {
|
|
8220
|
-
applied_version:
|
|
8559
|
+
applied_version: version,
|
|
8221
8560
|
paths: checkConfiguredPaths(skillsJson),
|
|
8222
8561
|
// Read AFTER the prunes above, so the owner's next view of memory is
|
|
8223
8562
|
// the file as it now stands, not the one they just pruned from.
|
|
@@ -8232,7 +8571,15 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8232
8571
|
has_search_key: local?.has_search_key ?? false,
|
|
8233
8572
|
// Same idea, for the BYOK LLM key file: only the runtime can see
|
|
8234
8573
|
// whether one is configured in its own environment.
|
|
8235
|
-
has_llm_key: local?.has_llm_key ?? false
|
|
8574
|
+
has_llm_key: local?.has_llm_key ?? false,
|
|
8575
|
+
// The backend uses this only to expose target channels compatible with
|
|
8576
|
+
// the actual protected-memory adapter. It is an enum, never a path or
|
|
8577
|
+
// provider credential.
|
|
8578
|
+
memory_provider: this.cfg.memoryProvider ?? "securecoms",
|
|
8579
|
+
// Inert legacy files only. `readLocalReport` revalidates the current
|
|
8580
|
+
// regular-file bytes and strips every local path before this leaves the
|
|
8581
|
+
// machine.
|
|
8582
|
+
legacy_memory: local?.legacy_memory ?? []
|
|
8236
8583
|
};
|
|
8237
8584
|
const posted = await this.fetchImpl(`${base}/report`, {
|
|
8238
8585
|
method: "POST",
|
|
@@ -8244,13 +8591,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8244
8591
|
});
|
|
8245
8592
|
if (!posted.ok) {
|
|
8246
8593
|
process.stderr.write(
|
|
8247
|
-
`[bridge] runtime config v${
|
|
8594
|
+
version ? `[bridge] runtime config v${version} written, but reporting it failed (${posted.status}) \u2014 the console will keep showing it as not applied until the next cycle
|
|
8595
|
+
` : `[bridge] legacy-memory inventory report failed (${posted.status}); no saved runtime config was applied
|
|
8248
8596
|
`
|
|
8249
8597
|
);
|
|
8250
8598
|
return;
|
|
8251
8599
|
}
|
|
8252
|
-
process.stderr.write(
|
|
8253
|
-
`
|
|
8600
|
+
process.stderr.write(
|
|
8601
|
+
version ? `[bridge] runtime config v${version} applied
|
|
8602
|
+
` : "[bridge] legacy-memory inventory reported; no saved runtime config was applied\n"
|
|
8603
|
+
);
|
|
8254
8604
|
} catch (e) {
|
|
8255
8605
|
process.stderr.write(
|
|
8256
8606
|
`[bridge] runtime config refetch failed: ${e?.message ?? e}
|
|
@@ -8286,6 +8636,10 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8286
8636
|
await this.provider.publishKeyPackage();
|
|
8287
8637
|
await this.refetchVoiceSettings();
|
|
8288
8638
|
await this.refetchRuntimeConfig();
|
|
8639
|
+
this.runtimeConfigTimer = setInterval(() => {
|
|
8640
|
+
void this.refetchRuntimeConfig();
|
|
8641
|
+
}, 6 * 60 * 60 * 1e3);
|
|
8642
|
+
this.runtimeConfigTimer.unref?.();
|
|
8289
8643
|
this.startReceivePump();
|
|
8290
8644
|
if (this.selfHealSweepEnabled) {
|
|
8291
8645
|
this.bootSweepTimer = setTimeout(
|
|
@@ -9088,7 +9442,47 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9088
9442
|
async recordMemory(input) {
|
|
9089
9443
|
return this.memory.record(input);
|
|
9090
9444
|
}
|
|
9445
|
+
/**
|
|
9446
|
+
* OpenClaw collapses text history and workspace facts into the owner's
|
|
9447
|
+
* session. Resolve the channel from fresh, authenticated server metadata
|
|
9448
|
+
* before exposing that provider through the local bridge API. A cold or
|
|
9449
|
+
* stale cache is refreshed once; any failure remains a fail-closed denial.
|
|
9450
|
+
*/
|
|
9451
|
+
async mayReadOpenClawMemory(channelId) {
|
|
9452
|
+
if (this.cfg.memoryProvider !== "openclaw") return true;
|
|
9453
|
+
if (!this.freshMeta(channelId)) await this.refreshChannelMeta();
|
|
9454
|
+
if (!this.freshRoster(channelId)) await this.refreshChannelRoster(channelId);
|
|
9455
|
+
const meta = this.freshMeta(channelId);
|
|
9456
|
+
const roster = this.freshRoster(channelId);
|
|
9457
|
+
if (meta?.is_dm !== true || !roster) return false;
|
|
9458
|
+
const humanCounterparts = [...roster.entries()].filter(
|
|
9459
|
+
([identityId, member]) => identityId !== this.cfg.identityId && !member.is_agent
|
|
9460
|
+
);
|
|
9461
|
+
return humanCounterparts.length === 1 && humanCounterparts[0][1].is_agent_owner;
|
|
9462
|
+
}
|
|
9463
|
+
/** An inbound turn may receive OpenClaw memory only from an active owner DM. */
|
|
9464
|
+
mayReadOpenClawMemoryForTurn(authorization) {
|
|
9465
|
+
return this.cfg.memoryProvider !== "openclaw" || authorization.sender_membership === "active" && authorization.audience === "owner_dm";
|
|
9466
|
+
}
|
|
9467
|
+
/**
|
|
9468
|
+
* Resolve the prior-turn snapshot carried to the native runtime. OpenClaw
|
|
9469
|
+
* stays owner-DM-only; every provider failure degrades to empty context and
|
|
9470
|
+
* never falls back to the runtime's retired plaintext JSON files.
|
|
9471
|
+
*/
|
|
9472
|
+
async memoryForTurn(channelId, authorization, limit = 50) {
|
|
9473
|
+
if (!this.mayReadOpenClawMemoryForTurn(authorization)) {
|
|
9474
|
+
return { recent: [], facts: [] };
|
|
9475
|
+
}
|
|
9476
|
+
try {
|
|
9477
|
+
return await this.memory.context(channelId, limit);
|
|
9478
|
+
} catch {
|
|
9479
|
+
return { recent: [], facts: [] };
|
|
9480
|
+
}
|
|
9481
|
+
}
|
|
9091
9482
|
async memoryContext(conversationId, limit) {
|
|
9483
|
+
if (!await this.mayReadOpenClawMemory(conversationId)) {
|
|
9484
|
+
return { recent: [], facts: [] };
|
|
9485
|
+
}
|
|
9092
9486
|
return this.memory.context(conversationId, limit);
|
|
9093
9487
|
}
|
|
9094
9488
|
/**
|
|
@@ -9132,10 +9526,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9132
9526
|
}
|
|
9133
9527
|
try {
|
|
9134
9528
|
let memory;
|
|
9135
|
-
|
|
9136
|
-
memory = await this.memory.context(m.channelId, 30);
|
|
9137
|
-
} catch {
|
|
9529
|
+
if (!this.mayReadOpenClawMemoryForTurn(m.authorization)) {
|
|
9138
9530
|
memory = { recent: [], facts: [] };
|
|
9531
|
+
} else {
|
|
9532
|
+
try {
|
|
9533
|
+
memory = await this.memory.context(m.channelId, 30);
|
|
9534
|
+
} catch {
|
|
9535
|
+
memory = { recent: [], facts: [] };
|
|
9536
|
+
}
|
|
9139
9537
|
}
|
|
9140
9538
|
const messages = buildFastMessages({
|
|
9141
9539
|
persona: this.cfg.voice.persona,
|
|
@@ -9388,13 +9786,42 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9388
9786
|
const rows = await res.json();
|
|
9389
9787
|
const members = /* @__PURE__ */ new Map();
|
|
9390
9788
|
for (const r of rows) {
|
|
9391
|
-
members.set(r.identity_id, {
|
|
9789
|
+
members.set(r.identity_id, {
|
|
9790
|
+
name: r.display_name,
|
|
9791
|
+
is_agent: r.is_agent,
|
|
9792
|
+
is_agent_owner: r.is_agent_owner === true,
|
|
9793
|
+
is_partner: r.is_partner === true
|
|
9794
|
+
});
|
|
9392
9795
|
}
|
|
9393
9796
|
this.channelRoster.set(channelId, { members, at: Date.now() });
|
|
9394
9797
|
await this.resolveAndPinOwner(channelId, rows);
|
|
9395
9798
|
} catch {
|
|
9396
9799
|
}
|
|
9397
9800
|
}
|
|
9801
|
+
/**
|
|
9802
|
+
* Classify the sender only from server-authenticated channel/roster data.
|
|
9803
|
+
* Missing or stale metadata is UNKNOWN with zero grants, never a permissive
|
|
9804
|
+
* guess. The returned object and grant array are frozen before they cross the
|
|
9805
|
+
* daemon boundary so one consumer cannot widen a later execution check.
|
|
9806
|
+
*/
|
|
9807
|
+
authorizationFor(channelId, senderId) {
|
|
9808
|
+
const meta = this.freshMeta(channelId);
|
|
9809
|
+
const sender = this.freshRoster(channelId)?.get(senderId);
|
|
9810
|
+
let audience = "unknown";
|
|
9811
|
+
if (meta && sender) {
|
|
9812
|
+
if (sender.is_agent) audience = "agent_to_agent";
|
|
9813
|
+
else if (meta.is_dm && sender.is_agent_owner) audience = "owner_dm";
|
|
9814
|
+
else if (meta.is_dm) audience = "other_human_dm";
|
|
9815
|
+
else if (sender.is_partner) audience = "partner_room";
|
|
9816
|
+
else audience = "team_room";
|
|
9817
|
+
}
|
|
9818
|
+
return Object.freeze({
|
|
9819
|
+
version: 1,
|
|
9820
|
+
audience,
|
|
9821
|
+
sender_membership: sender ? "active" : "unknown",
|
|
9822
|
+
tool_grants: Object.freeze([])
|
|
9823
|
+
});
|
|
9824
|
+
}
|
|
9398
9825
|
/**
|
|
9399
9826
|
* Task 8 (owner-attested Reset Secure Session): STRUCTURAL owner selection —
|
|
9400
9827
|
* NEVER read the server's `role` field to pick the owner. In a DM the owner
|
|
@@ -9908,6 +10335,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9908
10335
|
sender_id: m.envelope.sender_id,
|
|
9909
10336
|
text: typeof parsed.text === "string" ? parsed.text : "",
|
|
9910
10337
|
ts: m.receivedAt,
|
|
10338
|
+
authorization: this.authorizationFor(m.channelId, m.envelope.sender_id),
|
|
9911
10339
|
sender_name: senderName,
|
|
9912
10340
|
sender_is_agent: senderIsAgent,
|
|
9913
10341
|
attachments: [attachment],
|
|
@@ -9946,6 +10374,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9946
10374
|
const senderName = this.freshRoster(m.channelId)?.get(
|
|
9947
10375
|
m.envelope.sender_id
|
|
9948
10376
|
)?.name;
|
|
10377
|
+
const turnAuthorization = this.authorizationFor(
|
|
10378
|
+
m.channelId,
|
|
10379
|
+
m.envelope.sender_id
|
|
10380
|
+
);
|
|
10381
|
+
const runtimeMemory = this.memoryForTurn(m.channelId, turnAuthorization);
|
|
9949
10382
|
const speaker = parsed.relay === true && typeof parsed.speaker_identity_id === "string" && typeof parsed.speaker_display_name === "string" ? {
|
|
9950
10383
|
identityId: parsed.speaker_identity_id,
|
|
9951
10384
|
displayName: parsed.speaker_display_name
|
|
@@ -9962,12 +10395,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9962
10395
|
const turnText = parsed.text;
|
|
9963
10396
|
const isCallStart = turnText.trim() === VOICE_CALL_START_MARKER;
|
|
9964
10397
|
const fastGreeting = isCallStart && fastPathFiring;
|
|
9965
|
-
const runtimeEntry = (ackSent) => ({
|
|
10398
|
+
const runtimeEntry = (ackSent, memory) => ({
|
|
9966
10399
|
channel_id: m.channelId,
|
|
9967
10400
|
message_id: m.messageId,
|
|
9968
10401
|
sender_id: m.envelope.sender_id,
|
|
9969
10402
|
text: turnText,
|
|
9970
10403
|
ts: m.receivedAt,
|
|
10404
|
+
authorization: turnAuthorization,
|
|
10405
|
+
memory,
|
|
9971
10406
|
// A relay-attributed turn is the SPEAKER's message to the runtime:
|
|
9972
10407
|
// name it after them and never flag it as agent-authored.
|
|
9973
10408
|
sender_name: speaker?.displayName ?? senderName,
|
|
@@ -9981,18 +10416,23 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9981
10416
|
if (fastPathFiring) {
|
|
9982
10417
|
const fastSpeaker = speaker ?? (senderName ? { identityId: m.envelope.sender_id, displayName: senderName } : void 0);
|
|
9983
10418
|
void this.runFastPath(
|
|
9984
|
-
{
|
|
10419
|
+
{
|
|
10420
|
+
channelId: m.channelId,
|
|
10421
|
+
messageId: m.messageId,
|
|
10422
|
+
senderId: m.envelope.sender_id,
|
|
10423
|
+
authorization: turnAuthorization
|
|
10424
|
+
},
|
|
9985
10425
|
fastGreeting ? greetingTurnFor(this.voiceSettings ?? DEFAULT_VOICE_SETTINGS, fastSpeaker?.displayName) : parsed.text,
|
|
9986
10426
|
voiceGuidance,
|
|
9987
10427
|
fastSpeaker
|
|
9988
|
-
).then((outcome) => {
|
|
10428
|
+
).then(async (outcome) => {
|
|
9989
10429
|
if (outcome === "answered") return;
|
|
9990
|
-
this.buffer.push(runtimeEntry(outcome === "deferred"));
|
|
10430
|
+
this.buffer.push(runtimeEntry(outcome === "deferred", await runtimeMemory));
|
|
9991
10431
|
this.capBuffer();
|
|
9992
10432
|
this.wakeListeners();
|
|
9993
10433
|
});
|
|
9994
10434
|
} else {
|
|
9995
|
-
this.buffer.push(runtimeEntry(false));
|
|
10435
|
+
this.buffer.push(runtimeEntry(false, await runtimeMemory));
|
|
9996
10436
|
}
|
|
9997
10437
|
this.capBuffer();
|
|
9998
10438
|
void recordInboundMemory(
|
|
@@ -10058,6 +10498,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
10058
10498
|
this.buffer.push(...keep);
|
|
10059
10499
|
return out;
|
|
10060
10500
|
}
|
|
10501
|
+
recordRuntimeReply(inReplyTo, channelId, replyMessageId) {
|
|
10502
|
+
return this.runtimeReplyReceipts.record(inReplyTo, channelId, replyMessageId);
|
|
10503
|
+
}
|
|
10504
|
+
runtimeReplyReceipt(inReplyTo) {
|
|
10505
|
+
return this.runtimeReplyReceipts.get(inReplyTo);
|
|
10506
|
+
}
|
|
10061
10507
|
/**
|
|
10062
10508
|
* Put messages back at the front (SSE/disconnect write-failure recovery).
|
|
10063
10509
|
*
|
|
@@ -10209,6 +10655,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
10209
10655
|
this.closingDown = true;
|
|
10210
10656
|
this.pingHandle?.close();
|
|
10211
10657
|
if (this.safetyTimer) clearInterval(this.safetyTimer);
|
|
10658
|
+
if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
|
|
10212
10659
|
if (this.bootSweepTimer) clearTimeout(this.bootSweepTimer);
|
|
10213
10660
|
if (this.selfHealTimer) clearTimeout(this.selfHealTimer);
|
|
10214
10661
|
await this.provider.drainInFlight(_BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
@@ -10334,6 +10781,7 @@ var sendSchema = z3.object({
|
|
|
10334
10781
|
channel_id: z3.string().min(1),
|
|
10335
10782
|
text: z3.string().optional(),
|
|
10336
10783
|
idempotency_key: z3.string().optional(),
|
|
10784
|
+
in_reply_to: z3.string().min(1).max(200).optional(),
|
|
10337
10785
|
a2a: a2aSchema.optional()
|
|
10338
10786
|
}).refine((d) => typeof d.text === "string" || d.a2a !== void 0, {
|
|
10339
10787
|
message: "send requires either text or a2a"
|
|
@@ -10450,10 +10898,19 @@ function createHttpServer(agent, opts) {
|
|
|
10450
10898
|
if (method === "GET" && path === "/v1/metrics") {
|
|
10451
10899
|
return json(res, 200, agent.metrics());
|
|
10452
10900
|
}
|
|
10901
|
+
if (method === "GET" && path === "/v1/reply-receipt") {
|
|
10902
|
+
const inReplyTo = url.searchParams.get("in_reply_to");
|
|
10903
|
+
if (!inReplyTo || inReplyTo.length > 200) return jsonError(res, "invalid_request");
|
|
10904
|
+
const receipt = agent.runtimeReplyReceipt(inReplyTo);
|
|
10905
|
+
return json(res, 200, receipt ? { found: true, receipt } : { found: false });
|
|
10906
|
+
}
|
|
10453
10907
|
if (method === "POST" && path === "/v1/send") {
|
|
10454
10908
|
const parsed = sendSchema.safeParse(JSON.parse(await readBody(req, SEND_CAP)));
|
|
10455
10909
|
if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
|
|
10456
10910
|
const r = parsed.data.a2a ? await agent.sendA2A(parsed.data.channel_id, parsed.data.a2a) : await agent.send(parsed.data.channel_id, parsed.data.text);
|
|
10911
|
+
if (parsed.data.in_reply_to) {
|
|
10912
|
+
agent.recordRuntimeReply(parsed.data.in_reply_to, parsed.data.channel_id, r.message_id);
|
|
10913
|
+
}
|
|
10457
10914
|
return json(res, 200, r);
|
|
10458
10915
|
}
|
|
10459
10916
|
if (method === "POST" && path === "/v1/send-attachment") {
|
|
@@ -10709,6 +11166,10 @@ async function handleInbound(sock, agent, data, inflight) {
|
|
|
10709
11166
|
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
10710
11167
|
return;
|
|
10711
11168
|
}
|
|
11169
|
+
if (m.in_reply_to !== void 0 && (typeof m.in_reply_to !== "string" || m.in_reply_to.length === 0 || m.in_reply_to.length > 200)) {
|
|
11170
|
+
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
11171
|
+
return;
|
|
11172
|
+
}
|
|
10712
11173
|
if (m.a2a && typeof m.a2a === "object") {
|
|
10713
11174
|
const a = m.a2a;
|
|
10714
11175
|
if (typeof a.a2a_version !== "string" || !a.a2a_version || a.kind !== "message" && a.kind !== "task" && a.kind !== "artifact" || typeof a.object !== "object" || a.object === null) {
|
|
@@ -10717,6 +11178,9 @@ async function handleInbound(sock, agent, data, inflight) {
|
|
|
10717
11178
|
}
|
|
10718
11179
|
try {
|
|
10719
11180
|
const r = await agent.sendA2A(m.channel_id, { a2a_version: a.a2a_version, kind: a.kind, object: a.object });
|
|
11181
|
+
if (typeof m.in_reply_to === "string") {
|
|
11182
|
+
agent.recordRuntimeReply(m.in_reply_to, m.channel_id, r.message_id);
|
|
11183
|
+
}
|
|
10720
11184
|
sendJson(sock, { type: "sent", message_id: r.message_id });
|
|
10721
11185
|
} catch (e) {
|
|
10722
11186
|
if (e instanceof ChannelPendingError) {
|
|
@@ -10735,6 +11199,9 @@ async function handleInbound(sock, agent, data, inflight) {
|
|
|
10735
11199
|
}
|
|
10736
11200
|
try {
|
|
10737
11201
|
const r = await agent.send(m.channel_id, m.text);
|
|
11202
|
+
if (typeof m.in_reply_to === "string") {
|
|
11203
|
+
agent.recordRuntimeReply(m.in_reply_to, m.channel_id, r.message_id);
|
|
11204
|
+
}
|
|
10738
11205
|
sendJson(sock, { type: "sent", message_id: r.message_id });
|
|
10739
11206
|
} catch (e) {
|
|
10740
11207
|
if (e instanceof ChannelPendingError) {
|
|
@@ -10846,7 +11313,7 @@ async function handleInbound(sock, agent, data, inflight) {
|
|
|
10846
11313
|
|
|
10847
11314
|
// src/bin.ts
|
|
10848
11315
|
import { readFileSync as readFileSync9, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, rmSync, existsSync as existsSync3, realpathSync, readdirSync as readdirSync2, statSync as statSync4 } from "node:fs";
|
|
10849
|
-
import { join as join13, dirname as dirname7, isAbsolute as
|
|
11316
|
+
import { join as join13, dirname as dirname7, isAbsolute as isAbsolute3, resolve as resolve2 } from "node:path";
|
|
10850
11317
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10851
11318
|
import { homedir as homedir3, userInfo } from "node:os";
|
|
10852
11319
|
import { spawnSync } from "node:child_process";
|
|
@@ -10855,7 +11322,7 @@ import { spawnSync } from "node:child_process";
|
|
|
10855
11322
|
import { dirname as dirname4 } from "node:path";
|
|
10856
11323
|
|
|
10857
11324
|
// src/service/config.ts
|
|
10858
|
-
import { basename as basename3, extname, isAbsolute, resolve } from "node:path";
|
|
11325
|
+
import { basename as basename3, extname, isAbsolute as isAbsolute2, resolve } from "node:path";
|
|
10859
11326
|
|
|
10860
11327
|
// src/service/resolve-node.ts
|
|
10861
11328
|
var REQUIRED_NODE_MAJOR = 24;
|
|
@@ -10935,9 +11402,9 @@ function deriveServiceConfig(input) {
|
|
|
10935
11402
|
}
|
|
10936
11403
|
const stem = input.label ?? basename3(input.credentialPath, extname(input.credentialPath));
|
|
10937
11404
|
const slug = slugify(stem);
|
|
10938
|
-
const cred =
|
|
11405
|
+
const cred = isAbsolute2(input.credentialPath) ? input.credentialPath : resolve(input.credentialPath);
|
|
10939
11406
|
const args2 = ["--credential", cred];
|
|
10940
|
-
if (input.storePath) args2.push("--store",
|
|
11407
|
+
if (input.storePath) args2.push("--store", isAbsolute2(input.storePath) ? input.storePath : resolve(input.storePath));
|
|
10941
11408
|
if (input.port != null) args2.push("--port", String(input.port));
|
|
10942
11409
|
return {
|
|
10943
11410
|
slug,
|
|
@@ -12048,7 +12515,7 @@ async function runServiceVerb(verb) {
|
|
|
12048
12515
|
}
|
|
12049
12516
|
try {
|
|
12050
12517
|
if (verb === "install") {
|
|
12051
|
-
const credAbs =
|
|
12518
|
+
const credAbs = isAbsolute3(credentialPath) ? credentialPath : resolve2(credentialPath);
|
|
12052
12519
|
const installExecPath = chooseInstallExecPath(
|
|
12053
12520
|
process.execPath,
|
|
12054
12521
|
argOrEnv("--exec-path", "SECURECOMS_EXEC_PATH"),
|