@securecoms/agent-bridge 0.19.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 +506 -35
- package/dist/bridge-agent.d.ts +65 -0
- package/dist/index.js +501 -30
- package/dist/reply-receipts.d.ts +30 -0
- package/dist/service/runtime-config-dir.d.ts +35 -0
- package/dist/version.d.ts +1 -0
- 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;
|
|
@@ -7562,11 +7723,98 @@ function readLocalReport(home, identityId) {
|
|
|
7562
7723
|
}
|
|
7563
7724
|
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
|
|
7564
7725
|
const hasSearchKey = doc.has_search_key === true;
|
|
7726
|
+
const hasLlmKey = doc.has_llm_key === true;
|
|
7565
7727
|
const raw = doc.persona;
|
|
7566
7728
|
const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.fromEntries(
|
|
7567
7729
|
Object.entries(raw).filter(([, v]) => typeof v === "string")
|
|
7568
7730
|
) : {};
|
|
7569
|
-
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 };
|
|
7570
7818
|
}
|
|
7571
7819
|
function readManagedMemory(home, identityId) {
|
|
7572
7820
|
const p = join8(runtimeConfigDir(home, identityId), MEMORY_FILE);
|
|
@@ -7595,6 +7843,51 @@ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
|
|
|
7595
7843
|
|
|
7596
7844
|
// src/bridge-agent.ts
|
|
7597
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
|
|
7598
7891
|
var VOICE_MODE_GUIDANCE = "Reply immediately and conversationally, one short sentence per message; never bundle multiple points into one message.";
|
|
7599
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.";
|
|
7600
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.";
|
|
@@ -7864,6 +8157,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
7864
8157
|
channelOwner = /* @__PURE__ */ new Map();
|
|
7865
8158
|
buffer = [];
|
|
7866
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();
|
|
7867
8166
|
/** Cumulative count of messages shed by the buffer cap (surfaced on health()/metrics()). */
|
|
7868
8167
|
droppedMessages = 0;
|
|
7869
8168
|
/**
|
|
@@ -8089,6 +8388,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8089
8388
|
waiters = [];
|
|
8090
8389
|
pingHandle;
|
|
8091
8390
|
safetyTimer;
|
|
8391
|
+
runtimeConfigTimer;
|
|
8092
8392
|
listeners = /* @__PURE__ */ new Set();
|
|
8093
8393
|
startedAt = 0;
|
|
8094
8394
|
store;
|
|
@@ -8198,25 +8498,65 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8198
8498
|
});
|
|
8199
8499
|
if (!res.ok) throw new Error(`runtime-config ${res.status}`);
|
|
8200
8500
|
const payload = await res.json();
|
|
8201
|
-
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") {
|
|
8202
8506
|
process.stderr.write(
|
|
8203
|
-
`[bridge] runtime config not applied (version ${JSON.stringify(payload
|
|
8507
|
+
`[bridge] runtime config not applied (version ${JSON.stringify(payload.version ?? null)} \u2014 invalid response)
|
|
8204
8508
|
`
|
|
8205
8509
|
);
|
|
8206
8510
|
return;
|
|
8207
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
|
+
}
|
|
8208
8520
|
const skillsJson = payload.skills_json ?? {};
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8521
|
+
if (version) {
|
|
8522
|
+
writeManagedFiles(this.home, this.cfg.identityId, {
|
|
8523
|
+
...payload.files ?? {},
|
|
8524
|
+
"skills.json": `${JSON.stringify(skillsJson, null, 2)}
|
|
8212
8525
|
`
|
|
8213
|
-
|
|
8526
|
+
});
|
|
8527
|
+
}
|
|
8214
8528
|
if (payload.memory_prunes?.length) {
|
|
8215
8529
|
pruneManagedMemory(this.home, this.cfg.identityId, payload.memory_prunes);
|
|
8216
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
|
+
}
|
|
8217
8557
|
const local = readLocalReport(this.home, this.cfg.identityId);
|
|
8218
8558
|
const report = {
|
|
8219
|
-
applied_version:
|
|
8559
|
+
applied_version: version,
|
|
8220
8560
|
paths: checkConfiguredPaths(skillsJson),
|
|
8221
8561
|
// Read AFTER the prunes above, so the owner's next view of memory is
|
|
8222
8562
|
// the file as it now stands, not the one they just pruned from.
|
|
@@ -8228,7 +8568,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8228
8568
|
// is an environment value on that process, invisible to the daemon and
|
|
8229
8569
|
// to the backend. The backend needs it to generate a truthful tool
|
|
8230
8570
|
// paragraph, so it rides along here.
|
|
8231
|
-
has_search_key: local?.has_search_key ?? false
|
|
8571
|
+
has_search_key: local?.has_search_key ?? false,
|
|
8572
|
+
// Same idea, for the BYOK LLM key file: only the runtime can see
|
|
8573
|
+
// whether one is configured in its own environment.
|
|
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 ?? []
|
|
8232
8583
|
};
|
|
8233
8584
|
const posted = await this.fetchImpl(`${base}/report`, {
|
|
8234
8585
|
method: "POST",
|
|
@@ -8240,13 +8591,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8240
8591
|
});
|
|
8241
8592
|
if (!posted.ok) {
|
|
8242
8593
|
process.stderr.write(
|
|
8243
|
-
`[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
|
|
8244
8596
|
`
|
|
8245
8597
|
);
|
|
8246
8598
|
return;
|
|
8247
8599
|
}
|
|
8248
|
-
process.stderr.write(
|
|
8249
|
-
`
|
|
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
|
+
);
|
|
8250
8604
|
} catch (e) {
|
|
8251
8605
|
process.stderr.write(
|
|
8252
8606
|
`[bridge] runtime config refetch failed: ${e?.message ?? e}
|
|
@@ -8282,6 +8636,10 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8282
8636
|
await this.provider.publishKeyPackage();
|
|
8283
8637
|
await this.refetchVoiceSettings();
|
|
8284
8638
|
await this.refetchRuntimeConfig();
|
|
8639
|
+
this.runtimeConfigTimer = setInterval(() => {
|
|
8640
|
+
void this.refetchRuntimeConfig();
|
|
8641
|
+
}, 6 * 60 * 60 * 1e3);
|
|
8642
|
+
this.runtimeConfigTimer.unref?.();
|
|
8285
8643
|
this.startReceivePump();
|
|
8286
8644
|
if (this.selfHealSweepEnabled) {
|
|
8287
8645
|
this.bootSweepTimer = setTimeout(
|
|
@@ -9084,7 +9442,47 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9084
9442
|
async recordMemory(input) {
|
|
9085
9443
|
return this.memory.record(input);
|
|
9086
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
|
+
}
|
|
9087
9482
|
async memoryContext(conversationId, limit) {
|
|
9483
|
+
if (!await this.mayReadOpenClawMemory(conversationId)) {
|
|
9484
|
+
return { recent: [], facts: [] };
|
|
9485
|
+
}
|
|
9088
9486
|
return this.memory.context(conversationId, limit);
|
|
9089
9487
|
}
|
|
9090
9488
|
/**
|
|
@@ -9128,10 +9526,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9128
9526
|
}
|
|
9129
9527
|
try {
|
|
9130
9528
|
let memory;
|
|
9131
|
-
|
|
9132
|
-
memory = await this.memory.context(m.channelId, 30);
|
|
9133
|
-
} catch {
|
|
9529
|
+
if (!this.mayReadOpenClawMemoryForTurn(m.authorization)) {
|
|
9134
9530
|
memory = { recent: [], facts: [] };
|
|
9531
|
+
} else {
|
|
9532
|
+
try {
|
|
9533
|
+
memory = await this.memory.context(m.channelId, 30);
|
|
9534
|
+
} catch {
|
|
9535
|
+
memory = { recent: [], facts: [] };
|
|
9536
|
+
}
|
|
9135
9537
|
}
|
|
9136
9538
|
const messages = buildFastMessages({
|
|
9137
9539
|
persona: this.cfg.voice.persona,
|
|
@@ -9384,13 +9786,42 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9384
9786
|
const rows = await res.json();
|
|
9385
9787
|
const members = /* @__PURE__ */ new Map();
|
|
9386
9788
|
for (const r of rows) {
|
|
9387
|
-
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
|
+
});
|
|
9388
9795
|
}
|
|
9389
9796
|
this.channelRoster.set(channelId, { members, at: Date.now() });
|
|
9390
9797
|
await this.resolveAndPinOwner(channelId, rows);
|
|
9391
9798
|
} catch {
|
|
9392
9799
|
}
|
|
9393
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
|
+
}
|
|
9394
9825
|
/**
|
|
9395
9826
|
* Task 8 (owner-attested Reset Secure Session): STRUCTURAL owner selection —
|
|
9396
9827
|
* NEVER read the server's `role` field to pick the owner. In a DM the owner
|
|
@@ -9904,6 +10335,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9904
10335
|
sender_id: m.envelope.sender_id,
|
|
9905
10336
|
text: typeof parsed.text === "string" ? parsed.text : "",
|
|
9906
10337
|
ts: m.receivedAt,
|
|
10338
|
+
authorization: this.authorizationFor(m.channelId, m.envelope.sender_id),
|
|
9907
10339
|
sender_name: senderName,
|
|
9908
10340
|
sender_is_agent: senderIsAgent,
|
|
9909
10341
|
attachments: [attachment],
|
|
@@ -9942,6 +10374,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9942
10374
|
const senderName = this.freshRoster(m.channelId)?.get(
|
|
9943
10375
|
m.envelope.sender_id
|
|
9944
10376
|
)?.name;
|
|
10377
|
+
const turnAuthorization = this.authorizationFor(
|
|
10378
|
+
m.channelId,
|
|
10379
|
+
m.envelope.sender_id
|
|
10380
|
+
);
|
|
10381
|
+
const runtimeMemory = this.memoryForTurn(m.channelId, turnAuthorization);
|
|
9945
10382
|
const speaker = parsed.relay === true && typeof parsed.speaker_identity_id === "string" && typeof parsed.speaker_display_name === "string" ? {
|
|
9946
10383
|
identityId: parsed.speaker_identity_id,
|
|
9947
10384
|
displayName: parsed.speaker_display_name
|
|
@@ -9958,12 +10395,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9958
10395
|
const turnText = parsed.text;
|
|
9959
10396
|
const isCallStart = turnText.trim() === VOICE_CALL_START_MARKER;
|
|
9960
10397
|
const fastGreeting = isCallStart && fastPathFiring;
|
|
9961
|
-
const runtimeEntry = (ackSent) => ({
|
|
10398
|
+
const runtimeEntry = (ackSent, memory) => ({
|
|
9962
10399
|
channel_id: m.channelId,
|
|
9963
10400
|
message_id: m.messageId,
|
|
9964
10401
|
sender_id: m.envelope.sender_id,
|
|
9965
10402
|
text: turnText,
|
|
9966
10403
|
ts: m.receivedAt,
|
|
10404
|
+
authorization: turnAuthorization,
|
|
10405
|
+
memory,
|
|
9967
10406
|
// A relay-attributed turn is the SPEAKER's message to the runtime:
|
|
9968
10407
|
// name it after them and never flag it as agent-authored.
|
|
9969
10408
|
sender_name: speaker?.displayName ?? senderName,
|
|
@@ -9977,18 +10416,23 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9977
10416
|
if (fastPathFiring) {
|
|
9978
10417
|
const fastSpeaker = speaker ?? (senderName ? { identityId: m.envelope.sender_id, displayName: senderName } : void 0);
|
|
9979
10418
|
void this.runFastPath(
|
|
9980
|
-
{
|
|
10419
|
+
{
|
|
10420
|
+
channelId: m.channelId,
|
|
10421
|
+
messageId: m.messageId,
|
|
10422
|
+
senderId: m.envelope.sender_id,
|
|
10423
|
+
authorization: turnAuthorization
|
|
10424
|
+
},
|
|
9981
10425
|
fastGreeting ? greetingTurnFor(this.voiceSettings ?? DEFAULT_VOICE_SETTINGS, fastSpeaker?.displayName) : parsed.text,
|
|
9982
10426
|
voiceGuidance,
|
|
9983
10427
|
fastSpeaker
|
|
9984
|
-
).then((outcome) => {
|
|
10428
|
+
).then(async (outcome) => {
|
|
9985
10429
|
if (outcome === "answered") return;
|
|
9986
|
-
this.buffer.push(runtimeEntry(outcome === "deferred"));
|
|
10430
|
+
this.buffer.push(runtimeEntry(outcome === "deferred", await runtimeMemory));
|
|
9987
10431
|
this.capBuffer();
|
|
9988
10432
|
this.wakeListeners();
|
|
9989
10433
|
});
|
|
9990
10434
|
} else {
|
|
9991
|
-
this.buffer.push(runtimeEntry(false));
|
|
10435
|
+
this.buffer.push(runtimeEntry(false, await runtimeMemory));
|
|
9992
10436
|
}
|
|
9993
10437
|
this.capBuffer();
|
|
9994
10438
|
void recordInboundMemory(
|
|
@@ -10054,6 +10498,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
10054
10498
|
this.buffer.push(...keep);
|
|
10055
10499
|
return out;
|
|
10056
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
|
+
}
|
|
10057
10507
|
/**
|
|
10058
10508
|
* Put messages back at the front (SSE/disconnect write-failure recovery).
|
|
10059
10509
|
*
|
|
@@ -10205,6 +10655,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
10205
10655
|
this.closingDown = true;
|
|
10206
10656
|
this.pingHandle?.close();
|
|
10207
10657
|
if (this.safetyTimer) clearInterval(this.safetyTimer);
|
|
10658
|
+
if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
|
|
10208
10659
|
if (this.bootSweepTimer) clearTimeout(this.bootSweepTimer);
|
|
10209
10660
|
if (this.selfHealTimer) clearTimeout(this.selfHealTimer);
|
|
10210
10661
|
await this.provider.drainInFlight(_BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
@@ -10330,6 +10781,7 @@ var sendSchema = z3.object({
|
|
|
10330
10781
|
channel_id: z3.string().min(1),
|
|
10331
10782
|
text: z3.string().optional(),
|
|
10332
10783
|
idempotency_key: z3.string().optional(),
|
|
10784
|
+
in_reply_to: z3.string().min(1).max(200).optional(),
|
|
10333
10785
|
a2a: a2aSchema.optional()
|
|
10334
10786
|
}).refine((d) => typeof d.text === "string" || d.a2a !== void 0, {
|
|
10335
10787
|
message: "send requires either text or a2a"
|
|
@@ -10446,10 +10898,19 @@ function createHttpServer(agent, opts) {
|
|
|
10446
10898
|
if (method === "GET" && path === "/v1/metrics") {
|
|
10447
10899
|
return json(res, 200, agent.metrics());
|
|
10448
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
|
+
}
|
|
10449
10907
|
if (method === "POST" && path === "/v1/send") {
|
|
10450
10908
|
const parsed = sendSchema.safeParse(JSON.parse(await readBody(req, SEND_CAP)));
|
|
10451
10909
|
if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
|
|
10452
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
|
+
}
|
|
10453
10914
|
return json(res, 200, r);
|
|
10454
10915
|
}
|
|
10455
10916
|
if (method === "POST" && path === "/v1/send-attachment") {
|
|
@@ -10705,6 +11166,10 @@ async function handleInbound(sock, agent, data, inflight) {
|
|
|
10705
11166
|
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
10706
11167
|
return;
|
|
10707
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
|
+
}
|
|
10708
11173
|
if (m.a2a && typeof m.a2a === "object") {
|
|
10709
11174
|
const a = m.a2a;
|
|
10710
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) {
|
|
@@ -10713,6 +11178,9 @@ async function handleInbound(sock, agent, data, inflight) {
|
|
|
10713
11178
|
}
|
|
10714
11179
|
try {
|
|
10715
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
|
+
}
|
|
10716
11184
|
sendJson(sock, { type: "sent", message_id: r.message_id });
|
|
10717
11185
|
} catch (e) {
|
|
10718
11186
|
if (e instanceof ChannelPendingError) {
|
|
@@ -10731,6 +11199,9 @@ async function handleInbound(sock, agent, data, inflight) {
|
|
|
10731
11199
|
}
|
|
10732
11200
|
try {
|
|
10733
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
|
+
}
|
|
10734
11205
|
sendJson(sock, { type: "sent", message_id: r.message_id });
|
|
10735
11206
|
} catch (e) {
|
|
10736
11207
|
if (e instanceof ChannelPendingError) {
|
|
@@ -10842,7 +11313,7 @@ async function handleInbound(sock, agent, data, inflight) {
|
|
|
10842
11313
|
|
|
10843
11314
|
// src/bin.ts
|
|
10844
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";
|
|
10845
|
-
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";
|
|
10846
11317
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10847
11318
|
import { homedir as homedir3, userInfo } from "node:os";
|
|
10848
11319
|
import { spawnSync } from "node:child_process";
|
|
@@ -10851,7 +11322,7 @@ import { spawnSync } from "node:child_process";
|
|
|
10851
11322
|
import { dirname as dirname4 } from "node:path";
|
|
10852
11323
|
|
|
10853
11324
|
// src/service/config.ts
|
|
10854
|
-
import { basename as basename3, extname, isAbsolute, resolve } from "node:path";
|
|
11325
|
+
import { basename as basename3, extname, isAbsolute as isAbsolute2, resolve } from "node:path";
|
|
10855
11326
|
|
|
10856
11327
|
// src/service/resolve-node.ts
|
|
10857
11328
|
var REQUIRED_NODE_MAJOR = 24;
|
|
@@ -10931,9 +11402,9 @@ function deriveServiceConfig(input) {
|
|
|
10931
11402
|
}
|
|
10932
11403
|
const stem = input.label ?? basename3(input.credentialPath, extname(input.credentialPath));
|
|
10933
11404
|
const slug = slugify(stem);
|
|
10934
|
-
const cred =
|
|
11405
|
+
const cred = isAbsolute2(input.credentialPath) ? input.credentialPath : resolve(input.credentialPath);
|
|
10935
11406
|
const args2 = ["--credential", cred];
|
|
10936
|
-
if (input.storePath) args2.push("--store",
|
|
11407
|
+
if (input.storePath) args2.push("--store", isAbsolute2(input.storePath) ? input.storePath : resolve(input.storePath));
|
|
10937
11408
|
if (input.port != null) args2.push("--port", String(input.port));
|
|
10938
11409
|
return {
|
|
10939
11410
|
slug,
|
|
@@ -12044,7 +12515,7 @@ async function runServiceVerb(verb) {
|
|
|
12044
12515
|
}
|
|
12045
12516
|
try {
|
|
12046
12517
|
if (verb === "install") {
|
|
12047
|
-
const credAbs =
|
|
12518
|
+
const credAbs = isAbsolute3(credentialPath) ? credentialPath : resolve2(credentialPath);
|
|
12048
12519
|
const installExecPath = chooseInstallExecPath(
|
|
12049
12520
|
process.execPath,
|
|
12050
12521
|
argOrEnv("--exec-path", "SECURECOMS_EXEC_PATH"),
|