@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/index.js
CHANGED
|
@@ -5172,8 +5172,11 @@ var RestMlsBackend = class {
|
|
|
5172
5172
|
queue_ids: queueIds
|
|
5173
5173
|
});
|
|
5174
5174
|
}
|
|
5175
|
-
async nackDelivery(
|
|
5176
|
-
await this.post("/api/v1/mls/delivery/nack", {
|
|
5175
|
+
async nackDelivery(deviceId, queueIds) {
|
|
5176
|
+
await this.post("/api/v1/mls/delivery/nack", {
|
|
5177
|
+
device_id: deviceId,
|
|
5178
|
+
queue_ids: queueIds
|
|
5179
|
+
});
|
|
5177
5180
|
}
|
|
5178
5181
|
async listGroupMessages(deviceId, groupId, opts) {
|
|
5179
5182
|
const params = new URLSearchParams({ device_id: deviceId, limit: String(opts?.limit ?? 100) });
|
|
@@ -5681,7 +5684,7 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
5681
5684
|
import { MLSGroupManager as MLSGroupManager3, bytesToHex as bytesToHex4, hexToBytes as hexToBytes3 } from "@agentvault/crypto";
|
|
5682
5685
|
|
|
5683
5686
|
// ../connect-mls-store/dist/schema.js
|
|
5684
|
-
var SCHEMA_VERSION =
|
|
5687
|
+
var SCHEMA_VERSION = 7;
|
|
5685
5688
|
function openSchema(db) {
|
|
5686
5689
|
db.exec("PRAGMA journal_mode=WAL");
|
|
5687
5690
|
const row = db.prepare("PRAGMA user_version").get();
|
|
@@ -5773,6 +5776,17 @@ function openSchema(db) {
|
|
|
5773
5776
|
);
|
|
5774
5777
|
`);
|
|
5775
5778
|
}
|
|
5779
|
+
if (row.user_version < 7) {
|
|
5780
|
+
db.exec(`
|
|
5781
|
+
CREATE TABLE IF NOT EXISTS agent_memory_fact_assignment (
|
|
5782
|
+
identity_id TEXT NOT NULL,
|
|
5783
|
+
assignment_key TEXT NOT NULL,
|
|
5784
|
+
conversation_id TEXT NOT NULL,
|
|
5785
|
+
applied_at INTEGER NOT NULL,
|
|
5786
|
+
PRIMARY KEY (identity_id, assignment_key)
|
|
5787
|
+
);
|
|
5788
|
+
`);
|
|
5789
|
+
}
|
|
5776
5790
|
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
5777
5791
|
}
|
|
5778
5792
|
|
|
@@ -5812,6 +5826,10 @@ function decodeBundle(bytes) {
|
|
|
5812
5826
|
}
|
|
5813
5827
|
};
|
|
5814
5828
|
}
|
|
5829
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
5830
|
+
var DEFAULT_MEMORY_RETENTION_MS = 30 * DAY_MS;
|
|
5831
|
+
var DEFAULT_MEMORY_FACT_RETENTION_K = 200;
|
|
5832
|
+
var DEFAULT_MEMORY_FACT_RETENTION_MS = 90 * DAY_MS;
|
|
5815
5833
|
var SqliteMlsStateStore = class {
|
|
5816
5834
|
db;
|
|
5817
5835
|
id;
|
|
@@ -5825,6 +5843,10 @@ var SqliteMlsStateStore = class {
|
|
|
5825
5843
|
bundles = /* @__PURE__ */ new Map();
|
|
5826
5844
|
lastKpIds = /* @__PURE__ */ new Map();
|
|
5827
5845
|
memoryRetentionK;
|
|
5846
|
+
memoryRetentionMs;
|
|
5847
|
+
memoryFactRetentionK;
|
|
5848
|
+
memoryFactRetentionMs;
|
|
5849
|
+
memoryNow;
|
|
5828
5850
|
// M0(b)/F4-1 + C1 + H2: in-memory ONLY for now — the daemon side of FileSafe
|
|
5829
5851
|
// (this store) isn't wired into the multi-member vault yet (Task 5's capture
|
|
5830
5852
|
// sites are all connect-web/browser owner paths). Kept here purely so
|
|
@@ -5839,6 +5861,10 @@ var SqliteMlsStateStore = class {
|
|
|
5839
5861
|
this.id = opts.identityId;
|
|
5840
5862
|
this.cipher = opts.cipherFor(opts.identityId);
|
|
5841
5863
|
this.memoryRetentionK = opts.memoryRetentionK ?? 500;
|
|
5864
|
+
this.memoryRetentionMs = opts.memoryRetentionMs ?? DEFAULT_MEMORY_RETENTION_MS;
|
|
5865
|
+
this.memoryFactRetentionK = opts.memoryFactRetentionK ?? DEFAULT_MEMORY_FACT_RETENTION_K;
|
|
5866
|
+
this.memoryFactRetentionMs = opts.memoryFactRetentionMs ?? DEFAULT_MEMORY_FACT_RETENTION_MS;
|
|
5867
|
+
this.memoryNow = opts.memoryNow ?? Date.now;
|
|
5842
5868
|
try {
|
|
5843
5869
|
this.db = new DatabaseSync(opts.dbPath);
|
|
5844
5870
|
} catch (e) {
|
|
@@ -6056,23 +6082,24 @@ var SqliteMlsStateStore = class {
|
|
|
6056
6082
|
const dedupKey = input.messageId ?? input.idempotencyKey ?? null;
|
|
6057
6083
|
if (!dedupKey)
|
|
6058
6084
|
throw new Error("recordMemory requires message_id or idempotency_key");
|
|
6085
|
+
const now = this.memoryNow();
|
|
6086
|
+
this.pruneMemory(input.conversationId, now);
|
|
6059
6087
|
const sealed = await this.cipher.seal(new TextEncoder().encode(input.text));
|
|
6060
6088
|
const info = this.db.prepare(`INSERT OR IGNORE INTO agent_memory
|
|
6061
6089
|
(identity_id, conversation_id, seq, role, peer_id, sealed_text, modality, message_id, idempotency_key, dedup_key, recorded_at)
|
|
6062
6090
|
VALUES (?, ?,
|
|
6063
6091
|
(SELECT COALESCE(MAX(seq),0)+1 FROM agent_memory WHERE identity_id=? AND conversation_id=?),
|
|
6064
|
-
?, ?, ?, ?, ?, ?, ?, ?)`).run(this.id, input.conversationId, this.id, input.conversationId, input.role, input.peerId ?? null, sealed, input.modality, input.messageId ?? null, input.idempotencyKey ?? null, dedupKey,
|
|
6092
|
+
?, ?, ?, ?, ?, ?, ?, ?)`).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);
|
|
6065
6093
|
if (info.changes === 0) {
|
|
6066
6094
|
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);
|
|
6067
6095
|
return { seq: row2.seq, deduped: true };
|
|
6068
6096
|
}
|
|
6069
6097
|
const row = this.db.prepare("SELECT MAX(seq) AS seq FROM agent_memory WHERE identity_id=? AND conversation_id=?").get(this.id, input.conversationId);
|
|
6070
|
-
|
|
6071
|
-
this.db.prepare("DELETE FROM agent_memory WHERE identity_id=? AND conversation_id=? AND seq <= ?").run(this.id, input.conversationId, row.seq - this.memoryRetentionK);
|
|
6072
|
-
}
|
|
6098
|
+
this.pruneMemoryTurns(input.conversationId, now);
|
|
6073
6099
|
return { seq: row.seq, deduped: false };
|
|
6074
6100
|
}
|
|
6075
6101
|
async memoryContext(conversationId, limit = 50) {
|
|
6102
|
+
this.pruneMemory(conversationId, this.memoryNow());
|
|
6076
6103
|
const rows = this.db.prepare(`SELECT seq, role, peer_id, sealed_text, recorded_at FROM agent_memory
|
|
6077
6104
|
WHERE identity_id=? AND conversation_id=? ORDER BY seq DESC LIMIT ?`).all(this.id, conversationId, limit);
|
|
6078
6105
|
const recent = [];
|
|
@@ -6087,9 +6114,94 @@ var SqliteMlsStateStore = class {
|
|
|
6087
6114
|
return { recent, facts };
|
|
6088
6115
|
}
|
|
6089
6116
|
async recordMemoryFact(conversationId, fact) {
|
|
6117
|
+
const now = this.memoryNow();
|
|
6090
6118
|
const sealed = await this.cipher.seal(new TextEncoder().encode(fact));
|
|
6091
6119
|
this.db.prepare(`INSERT INTO agent_memory_fact (identity_id, conversation_id, fact_id, sealed_fact, added_at)
|
|
6092
|
-
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,
|
|
6120
|
+
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);
|
|
6121
|
+
this.pruneMemoryFacts(conversationId, now);
|
|
6122
|
+
}
|
|
6123
|
+
/**
|
|
6124
|
+
* Import one owner-approved legacy fact exactly once. `assignmentKey` is
|
|
6125
|
+
* content-free (`source-id:line-hash`) and intentionally excludes the target
|
|
6126
|
+
* channel: the same legacy fact can therefore never be replayed into a
|
|
6127
|
+
* second audience. The ledger and sealed fact commit atomically.
|
|
6128
|
+
*/
|
|
6129
|
+
async recordImportedMemoryFact(conversationId, fact, assignmentKey) {
|
|
6130
|
+
if (!/^[0-9a-f]{64}:[0-9a-f]{64}$/.test(assignmentKey)) {
|
|
6131
|
+
throw new Error("invalid legacy memory assignment key");
|
|
6132
|
+
}
|
|
6133
|
+
const existing = this.db.prepare(`SELECT conversation_id FROM agent_memory_fact_assignment
|
|
6134
|
+
WHERE identity_id=? AND assignment_key=?`).get(this.id, assignmentKey);
|
|
6135
|
+
if (existing) {
|
|
6136
|
+
if (existing.conversation_id !== conversationId) {
|
|
6137
|
+
throw new Error("legacy memory fact is already assigned to another channel");
|
|
6138
|
+
}
|
|
6139
|
+
return { factId: 0, deduped: true };
|
|
6140
|
+
}
|
|
6141
|
+
const now = this.memoryNow();
|
|
6142
|
+
const sealed = await this.cipher.seal(new TextEncoder().encode(fact));
|
|
6143
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
6144
|
+
try {
|
|
6145
|
+
const raced = this.db.prepare(`SELECT conversation_id FROM agent_memory_fact_assignment
|
|
6146
|
+
WHERE identity_id=? AND assignment_key=?`).get(this.id, assignmentKey);
|
|
6147
|
+
if (raced) {
|
|
6148
|
+
if (raced.conversation_id !== conversationId) {
|
|
6149
|
+
throw new Error("legacy memory fact is already assigned to another channel");
|
|
6150
|
+
}
|
|
6151
|
+
this.db.exec("COMMIT");
|
|
6152
|
+
return { factId: 0, deduped: true };
|
|
6153
|
+
}
|
|
6154
|
+
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);
|
|
6155
|
+
this.db.prepare(`INSERT INTO agent_memory_fact (identity_id, conversation_id, fact_id, sealed_fact, added_at)
|
|
6156
|
+
VALUES (?, ?, ?, ?, ?)`).run(this.id, conversationId, row.fact_id, sealed, now);
|
|
6157
|
+
this.db.prepare(`INSERT INTO agent_memory_fact_assignment (identity_id, assignment_key, conversation_id, applied_at)
|
|
6158
|
+
VALUES (?, ?, ?, ?)`).run(this.id, assignmentKey, conversationId, now);
|
|
6159
|
+
this.pruneMemoryFacts(conversationId, now);
|
|
6160
|
+
this.db.exec("COMMIT");
|
|
6161
|
+
return { factId: row.fact_id, deduped: false };
|
|
6162
|
+
} catch (error) {
|
|
6163
|
+
this.db.exec("ROLLBACK");
|
|
6164
|
+
throw error;
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
6167
|
+
/** Remove every durable turn and fact in one channel-scoped namespace. */
|
|
6168
|
+
purgeMemory(conversationId) {
|
|
6169
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
6170
|
+
try {
|
|
6171
|
+
this.db.prepare("DELETE FROM agent_memory WHERE identity_id=? AND conversation_id=?").run(this.id, conversationId);
|
|
6172
|
+
this.db.prepare("DELETE FROM agent_memory_fact WHERE identity_id=? AND conversation_id=?").run(this.id, conversationId);
|
|
6173
|
+
this.db.prepare("DELETE FROM agent_memory_fact_assignment WHERE identity_id=? AND conversation_id=?").run(this.id, conversationId);
|
|
6174
|
+
this.db.exec("COMMIT");
|
|
6175
|
+
} catch (error) {
|
|
6176
|
+
this.db.exec("ROLLBACK");
|
|
6177
|
+
throw error;
|
|
6178
|
+
}
|
|
6179
|
+
}
|
|
6180
|
+
pruneMemory(conversationId, now) {
|
|
6181
|
+
this.pruneMemoryTurns(conversationId, now);
|
|
6182
|
+
this.pruneMemoryFacts(conversationId, now);
|
|
6183
|
+
}
|
|
6184
|
+
pruneMemoryTurns(conversationId, now) {
|
|
6185
|
+
if (this.memoryRetentionMs > 0) {
|
|
6186
|
+
this.db.prepare("DELETE FROM agent_memory WHERE identity_id=? AND conversation_id=? AND recorded_at < ?").run(this.id, conversationId, now - this.memoryRetentionMs);
|
|
6187
|
+
}
|
|
6188
|
+
if (this.memoryRetentionK > 0) {
|
|
6189
|
+
const row = this.db.prepare("SELECT MAX(seq) AS seq FROM agent_memory WHERE identity_id=? AND conversation_id=?").get(this.id, conversationId);
|
|
6190
|
+
if (row.seq != null) {
|
|
6191
|
+
this.db.prepare("DELETE FROM agent_memory WHERE identity_id=? AND conversation_id=? AND seq <= ?").run(this.id, conversationId, row.seq - this.memoryRetentionK);
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
}
|
|
6195
|
+
pruneMemoryFacts(conversationId, now) {
|
|
6196
|
+
if (this.memoryFactRetentionMs > 0) {
|
|
6197
|
+
this.db.prepare("DELETE FROM agent_memory_fact WHERE identity_id=? AND conversation_id=? AND added_at < ?").run(this.id, conversationId, now - this.memoryFactRetentionMs);
|
|
6198
|
+
}
|
|
6199
|
+
if (this.memoryFactRetentionK > 0) {
|
|
6200
|
+
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);
|
|
6201
|
+
if (row.fact_id != null) {
|
|
6202
|
+
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);
|
|
6203
|
+
}
|
|
6204
|
+
}
|
|
6093
6205
|
}
|
|
6094
6206
|
/** @internal test-only */
|
|
6095
6207
|
_rawMainForTest(identityId, channelId) {
|
|
@@ -7316,6 +7428,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
7316
7428
|
import {
|
|
7317
7429
|
chmodSync,
|
|
7318
7430
|
existsSync,
|
|
7431
|
+
lstatSync,
|
|
7319
7432
|
mkdirSync,
|
|
7320
7433
|
readFileSync as readFileSync5,
|
|
7321
7434
|
renameSync,
|
|
@@ -7323,7 +7436,7 @@ import {
|
|
|
7323
7436
|
unlinkSync,
|
|
7324
7437
|
writeFileSync
|
|
7325
7438
|
} from "node:fs";
|
|
7326
|
-
import { basename as basename2, join as join6 } from "node:path";
|
|
7439
|
+
import { basename as basename2, isAbsolute, join as join6 } from "node:path";
|
|
7327
7440
|
var MEMORY_FILE = "MEMORY.md";
|
|
7328
7441
|
var REAL_FS = {
|
|
7329
7442
|
mkdir: (p, mode) => mkdirSync(p, { recursive: true, mode }),
|
|
@@ -7398,6 +7511,54 @@ function lineHash(line) {
|
|
|
7398
7511
|
return createHash("sha256").update(line.trim(), "utf8").digest("hex");
|
|
7399
7512
|
}
|
|
7400
7513
|
var LOCAL_REPORT_FILE = "local-config.json";
|
|
7514
|
+
var LEGACY_MEMORY_SOURCE_MAX_BYTES = 4 * 1024 * 1024;
|
|
7515
|
+
var LEGACY_MEMORY_REPORT_MAX_BYTES = 4 * 1024 * 1024;
|
|
7516
|
+
var LEGACY_MEMORY_REPORT_MAX_SOURCES = 100;
|
|
7517
|
+
var LEGACY_MEMORY_FACT_MAX_BYTES = 1024;
|
|
7518
|
+
var LEGACY_MEMORY_FACT_DECISION_MAX = 500;
|
|
7519
|
+
function legacySourceId(source, bytes) {
|
|
7520
|
+
return createHash("sha256").update(source.kind).update("\0").update(source.channel_id ?? "").update("\0").update(source.path).update("\0").update(bytes).digest("hex");
|
|
7521
|
+
}
|
|
7522
|
+
function localLegacySources(doc) {
|
|
7523
|
+
if (!Array.isArray(doc.legacy_memory)) return [];
|
|
7524
|
+
const out = [];
|
|
7525
|
+
let contentBudget = LEGACY_MEMORY_REPORT_MAX_BYTES;
|
|
7526
|
+
for (const raw of doc.legacy_memory) {
|
|
7527
|
+
if (out.length >= LEGACY_MEMORY_REPORT_MAX_SOURCES) break;
|
|
7528
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
7529
|
+
const source = raw;
|
|
7530
|
+
const kind = source.kind;
|
|
7531
|
+
const path = source.path;
|
|
7532
|
+
const channelId = source.channelId;
|
|
7533
|
+
if (kind !== "global-memory" && kind !== "conversation-history" && kind !== "captured-memory" || typeof path !== "string" || !isAbsolute(path) || channelId !== void 0 && typeof channelId !== "string") continue;
|
|
7534
|
+
try {
|
|
7535
|
+
const st = lstatSync(path);
|
|
7536
|
+
if (!st.isFile() || st.isSymbolicLink()) continue;
|
|
7537
|
+
if (st.size <= LEGACY_MEMORY_SOURCE_MAX_BYTES && st.size > contentBudget) continue;
|
|
7538
|
+
const oversize = st.size > LEGACY_MEMORY_SOURCE_MAX_BYTES;
|
|
7539
|
+
const bytes = oversize ? Buffer.from(`oversize:${st.size}:${st.mtimeMs}:${st.ctimeMs}`, "utf8") : Buffer.from(readFileSync5(path));
|
|
7540
|
+
if (!oversize && (bytes.byteLength > LEGACY_MEMORY_SOURCE_MAX_BYTES || bytes.byteLength > contentBudget)) {
|
|
7541
|
+
continue;
|
|
7542
|
+
}
|
|
7543
|
+
const exportable = !oversize;
|
|
7544
|
+
const candidate = {
|
|
7545
|
+
id: "",
|
|
7546
|
+
kind,
|
|
7547
|
+
...typeof channelId === "string" && channelId ? { channel_id: channelId } : {},
|
|
7548
|
+
path,
|
|
7549
|
+
content: exportable ? bytes.toString("utf8") : null,
|
|
7550
|
+
bytes: exportable ? bytes.byteLength : st.size,
|
|
7551
|
+
exportable
|
|
7552
|
+
};
|
|
7553
|
+
candidate.id = legacySourceId(candidate, bytes);
|
|
7554
|
+
if (source.id !== candidate.id) continue;
|
|
7555
|
+
out.push(candidate);
|
|
7556
|
+
if (exportable) contentBudget -= bytes.byteLength;
|
|
7557
|
+
} catch {
|
|
7558
|
+
}
|
|
7559
|
+
}
|
|
7560
|
+
return out;
|
|
7561
|
+
}
|
|
7401
7562
|
function readLocalReport(home, identityId) {
|
|
7402
7563
|
const p = join6(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
|
|
7403
7564
|
if (!existsSync(p)) return null;
|
|
@@ -7414,7 +7575,93 @@ function readLocalReport(home, identityId) {
|
|
|
7414
7575
|
const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.fromEntries(
|
|
7415
7576
|
Object.entries(raw).filter(([, v]) => typeof v === "string")
|
|
7416
7577
|
) : {};
|
|
7417
|
-
return {
|
|
7578
|
+
return {
|
|
7579
|
+
persona,
|
|
7580
|
+
has_search_key: hasSearchKey,
|
|
7581
|
+
has_llm_key: hasLlmKey,
|
|
7582
|
+
legacy_memory: localLegacySources(doc).map(({ path: _path, ...source }) => source)
|
|
7583
|
+
};
|
|
7584
|
+
}
|
|
7585
|
+
function deleteLegacyMemorySources(home, identityId, ids) {
|
|
7586
|
+
if (ids.length === 0) return 0;
|
|
7587
|
+
const p = join6(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
|
|
7588
|
+
if (!existsSync(p)) return 0;
|
|
7589
|
+
let doc;
|
|
7590
|
+
try {
|
|
7591
|
+
doc = JSON.parse(readFileSync5(p, "utf8"));
|
|
7592
|
+
} catch {
|
|
7593
|
+
return 0;
|
|
7594
|
+
}
|
|
7595
|
+
const wanted = new Set(ids);
|
|
7596
|
+
let deleted = 0;
|
|
7597
|
+
for (const source of localLegacySources(doc)) {
|
|
7598
|
+
if (!wanted.has(source.id)) continue;
|
|
7599
|
+
try {
|
|
7600
|
+
unlinkSync(source.path);
|
|
7601
|
+
deleted += 1;
|
|
7602
|
+
} catch {
|
|
7603
|
+
}
|
|
7604
|
+
}
|
|
7605
|
+
return deleted;
|
|
7606
|
+
}
|
|
7607
|
+
async function applyLegacyMemoryFactDecisions(home, identityId, rawDecisions, recordFact) {
|
|
7608
|
+
if (!Array.isArray(rawDecisions) || rawDecisions.length > LEGACY_MEMORY_FACT_DECISION_MAX) {
|
|
7609
|
+
return { sourcesDeleted: 0, factsAssigned: 0 };
|
|
7610
|
+
}
|
|
7611
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
7612
|
+
for (const raw of rawDecisions) {
|
|
7613
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
7614
|
+
return { sourcesDeleted: 0, factsAssigned: 0 };
|
|
7615
|
+
}
|
|
7616
|
+
const d = raw;
|
|
7617
|
+
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 };
|
|
7618
|
+
const decision = d;
|
|
7619
|
+
const current = bySource.get(decision.source_id);
|
|
7620
|
+
if (current === null) continue;
|
|
7621
|
+
const group = current ?? /* @__PURE__ */ new Map();
|
|
7622
|
+
const prior = group.get(decision.fact_hash);
|
|
7623
|
+
if (prior && (prior.action !== decision.action || prior.channel_id !== decision.channel_id)) {
|
|
7624
|
+
bySource.set(decision.source_id, null);
|
|
7625
|
+
continue;
|
|
7626
|
+
}
|
|
7627
|
+
group.set(decision.fact_hash, decision);
|
|
7628
|
+
bySource.set(decision.source_id, group);
|
|
7629
|
+
}
|
|
7630
|
+
const reportPath = join6(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
|
|
7631
|
+
if (!existsSync(reportPath)) return { sourcesDeleted: 0, factsAssigned: 0 };
|
|
7632
|
+
let doc;
|
|
7633
|
+
try {
|
|
7634
|
+
doc = JSON.parse(readFileSync5(reportPath, "utf8"));
|
|
7635
|
+
} catch {
|
|
7636
|
+
return { sourcesDeleted: 0, factsAssigned: 0 };
|
|
7637
|
+
}
|
|
7638
|
+
let sourcesDeleted = 0;
|
|
7639
|
+
let factsAssigned = 0;
|
|
7640
|
+
for (const source of localLegacySources(doc)) {
|
|
7641
|
+
const decisions = bySource.get(source.id);
|
|
7642
|
+
if (!decisions || source.kind !== "global-memory" || source.content === null) continue;
|
|
7643
|
+
const facts = source.content.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
7644
|
+
const currentHashes = new Set(facts.map(lineHash));
|
|
7645
|
+
if (currentHashes.size === 0 || decisions.size !== currentHashes.size || [...currentHashes].some((hash) => !decisions.has(hash)) || [...decisions].some(([hash]) => !currentHashes.has(hash))) continue;
|
|
7646
|
+
const uniqueFacts = /* @__PURE__ */ new Map();
|
|
7647
|
+
for (const fact of facts) uniqueFacts.set(lineHash(fact), fact);
|
|
7648
|
+
const assignments = [...decisions].filter(([, decision]) => decision.action === "assign");
|
|
7649
|
+
if (assignments.some(([hash]) => Buffer.byteLength(uniqueFacts.get(hash) ?? "", "utf8") > LEGACY_MEMORY_FACT_MAX_BYTES)) {
|
|
7650
|
+
continue;
|
|
7651
|
+
}
|
|
7652
|
+
try {
|
|
7653
|
+
for (const [hash, decision] of assignments) {
|
|
7654
|
+
await recordFact(decision.channel_id, uniqueFacts.get(hash), `${source.id}:${hash}`);
|
|
7655
|
+
factsAssigned += 1;
|
|
7656
|
+
}
|
|
7657
|
+
const stillCurrent = localLegacySources(doc).some((candidate) => candidate.id === source.id);
|
|
7658
|
+
if (!stillCurrent) continue;
|
|
7659
|
+
unlinkSync(source.path);
|
|
7660
|
+
sourcesDeleted += 1;
|
|
7661
|
+
} catch {
|
|
7662
|
+
}
|
|
7663
|
+
}
|
|
7664
|
+
return { sourcesDeleted, factsAssigned };
|
|
7418
7665
|
}
|
|
7419
7666
|
function readManagedMemory(home, identityId) {
|
|
7420
7667
|
const p = join6(runtimeConfigDir(home, identityId), MEMORY_FILE);
|
|
@@ -7443,6 +7690,51 @@ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
|
|
|
7443
7690
|
|
|
7444
7691
|
// src/bridge-agent.ts
|
|
7445
7692
|
import { homedir as homedir2 } from "node:os";
|
|
7693
|
+
|
|
7694
|
+
// src/reply-receipts.ts
|
|
7695
|
+
var DEFAULT_TTL_MS = 15 * 60 * 1e3;
|
|
7696
|
+
var DEFAULT_CAPACITY2 = 256;
|
|
7697
|
+
var RuntimeReplyReceiptLedger = class {
|
|
7698
|
+
receipts = /* @__PURE__ */ new Map();
|
|
7699
|
+
ttlMs;
|
|
7700
|
+
capacity;
|
|
7701
|
+
now;
|
|
7702
|
+
constructor(opts = {}) {
|
|
7703
|
+
this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
7704
|
+
this.capacity = opts.capacity ?? DEFAULT_CAPACITY2;
|
|
7705
|
+
this.now = opts.now ?? Date.now;
|
|
7706
|
+
if (!Number.isFinite(this.ttlMs) || this.ttlMs <= 0) throw new Error("reply receipt ttlMs must be positive");
|
|
7707
|
+
if (!Number.isInteger(this.capacity) || this.capacity <= 0) throw new Error("reply receipt capacity must be positive");
|
|
7708
|
+
}
|
|
7709
|
+
record(inReplyTo, channelId, replyMessageId) {
|
|
7710
|
+
const receipt = Object.freeze({
|
|
7711
|
+
in_reply_to: inReplyTo,
|
|
7712
|
+
reply_message_id: replyMessageId,
|
|
7713
|
+
channel_id: channelId,
|
|
7714
|
+
replied_at: this.now()
|
|
7715
|
+
});
|
|
7716
|
+
this.prune(receipt.replied_at);
|
|
7717
|
+
this.receipts.delete(inReplyTo);
|
|
7718
|
+
this.receipts.set(inReplyTo, receipt);
|
|
7719
|
+
while (this.receipts.size > this.capacity) {
|
|
7720
|
+
const oldest = this.receipts.keys().next().value;
|
|
7721
|
+
if (oldest === void 0) break;
|
|
7722
|
+
this.receipts.delete(oldest);
|
|
7723
|
+
}
|
|
7724
|
+
return receipt;
|
|
7725
|
+
}
|
|
7726
|
+
get(inReplyTo) {
|
|
7727
|
+
this.prune(this.now());
|
|
7728
|
+
return this.receipts.get(inReplyTo) ?? null;
|
|
7729
|
+
}
|
|
7730
|
+
prune(now) {
|
|
7731
|
+
for (const [key, receipt] of this.receipts) {
|
|
7732
|
+
if (now - receipt.replied_at >= this.ttlMs) this.receipts.delete(key);
|
|
7733
|
+
}
|
|
7734
|
+
}
|
|
7735
|
+
};
|
|
7736
|
+
|
|
7737
|
+
// src/bridge-agent.ts
|
|
7446
7738
|
var VOICE_MODE_GUIDANCE = "Reply immediately and conversationally, one short sentence per message; never bundle multiple points into one message.";
|
|
7447
7739
|
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.";
|
|
7448
7740
|
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.";
|
|
@@ -7712,6 +8004,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
7712
8004
|
channelOwner = /* @__PURE__ */ new Map();
|
|
7713
8005
|
buffer = [];
|
|
7714
8006
|
maxBuffer;
|
|
8007
|
+
/**
|
|
8008
|
+
* Short-lived, content-free runtime reply correlations. The attached runtime
|
|
8009
|
+
* supplies the inbound message id only when an outbound send is its reply;
|
|
8010
|
+
* ws-server/http-server record the receipt after send() publishes.
|
|
8011
|
+
*/
|
|
8012
|
+
runtimeReplyReceipts = new RuntimeReplyReceiptLedger();
|
|
7715
8013
|
/** Cumulative count of messages shed by the buffer cap (surfaced on health()/metrics()). */
|
|
7716
8014
|
droppedMessages = 0;
|
|
7717
8015
|
/**
|
|
@@ -7937,6 +8235,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
7937
8235
|
waiters = [];
|
|
7938
8236
|
pingHandle;
|
|
7939
8237
|
safetyTimer;
|
|
8238
|
+
runtimeConfigTimer;
|
|
7940
8239
|
listeners = /* @__PURE__ */ new Set();
|
|
7941
8240
|
startedAt = 0;
|
|
7942
8241
|
store;
|
|
@@ -8046,25 +8345,65 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8046
8345
|
});
|
|
8047
8346
|
if (!res.ok) throw new Error(`runtime-config ${res.status}`);
|
|
8048
8347
|
const payload = await res.json();
|
|
8049
|
-
if (!payload
|
|
8348
|
+
if (!payload) {
|
|
8349
|
+
process.stderr.write("[bridge] runtime config not applied (empty response)\n");
|
|
8350
|
+
return;
|
|
8351
|
+
}
|
|
8352
|
+
if (typeof payload.version !== "number") {
|
|
8050
8353
|
process.stderr.write(
|
|
8051
|
-
`[bridge] runtime config not applied (version ${JSON.stringify(payload
|
|
8354
|
+
`[bridge] runtime config not applied (version ${JSON.stringify(payload.version ?? null)} \u2014 invalid response)
|
|
8052
8355
|
`
|
|
8053
8356
|
);
|
|
8054
8357
|
return;
|
|
8055
8358
|
}
|
|
8359
|
+
const version = payload.version;
|
|
8360
|
+
if (!version) {
|
|
8361
|
+
writeManagedFiles(this.home, this.cfg.identityId, {});
|
|
8362
|
+
process.stderr.write(
|
|
8363
|
+
`[bridge] runtime config not applied (version ${JSON.stringify(payload?.version ?? null)} \u2014 no saved config)
|
|
8364
|
+
`
|
|
8365
|
+
);
|
|
8366
|
+
}
|
|
8056
8367
|
const skillsJson = payload.skills_json ?? {};
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8368
|
+
if (version) {
|
|
8369
|
+
writeManagedFiles(this.home, this.cfg.identityId, {
|
|
8370
|
+
...payload.files ?? {},
|
|
8371
|
+
"skills.json": `${JSON.stringify(skillsJson, null, 2)}
|
|
8060
8372
|
`
|
|
8061
|
-
|
|
8373
|
+
});
|
|
8374
|
+
}
|
|
8062
8375
|
if (payload.memory_prunes?.length) {
|
|
8063
8376
|
pruneManagedMemory(this.home, this.cfg.identityId, payload.memory_prunes);
|
|
8064
8377
|
}
|
|
8378
|
+
if (payload.legacy_memory_deletes?.length) {
|
|
8379
|
+
deleteLegacyMemorySources(this.home, this.cfg.identityId, payload.legacy_memory_deletes);
|
|
8380
|
+
}
|
|
8381
|
+
if (payload.legacy_memory_fact_decisions?.length) {
|
|
8382
|
+
const provider = this.cfg.memoryProvider ?? "securecoms";
|
|
8383
|
+
const result = await applyLegacyMemoryFactDecisions(
|
|
8384
|
+
this.home,
|
|
8385
|
+
this.cfg.identityId,
|
|
8386
|
+
payload.legacy_memory_fact_decisions,
|
|
8387
|
+
async (channelId, fact, assignmentKey) => {
|
|
8388
|
+
if (provider === "none" || provider === "hermes") {
|
|
8389
|
+
throw new Error("configured memory provider has no protected channel-fact import path");
|
|
8390
|
+
}
|
|
8391
|
+
if (provider === "openclaw" && !await this.mayReadOpenClawMemory(channelId)) {
|
|
8392
|
+
throw new Error("OpenClaw legacy fact target is not the active owner DM");
|
|
8393
|
+
}
|
|
8394
|
+
await this.store.recordImportedMemoryFact(channelId, fact, assignmentKey);
|
|
8395
|
+
}
|
|
8396
|
+
);
|
|
8397
|
+
if (result.sourcesDeleted > 0) {
|
|
8398
|
+
process.stderr.write(
|
|
8399
|
+
`[bridge] legacy-memory disposition applied (sources=${result.sourcesDeleted}, assigned=${result.factsAssigned})
|
|
8400
|
+
`
|
|
8401
|
+
);
|
|
8402
|
+
}
|
|
8403
|
+
}
|
|
8065
8404
|
const local = readLocalReport(this.home, this.cfg.identityId);
|
|
8066
8405
|
const report = {
|
|
8067
|
-
applied_version:
|
|
8406
|
+
applied_version: version,
|
|
8068
8407
|
paths: checkConfiguredPaths(skillsJson),
|
|
8069
8408
|
// Read AFTER the prunes above, so the owner's next view of memory is
|
|
8070
8409
|
// the file as it now stands, not the one they just pruned from.
|
|
@@ -8079,7 +8418,15 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8079
8418
|
has_search_key: local?.has_search_key ?? false,
|
|
8080
8419
|
// Same idea, for the BYOK LLM key file: only the runtime can see
|
|
8081
8420
|
// whether one is configured in its own environment.
|
|
8082
|
-
has_llm_key: local?.has_llm_key ?? false
|
|
8421
|
+
has_llm_key: local?.has_llm_key ?? false,
|
|
8422
|
+
// The backend uses this only to expose target channels compatible with
|
|
8423
|
+
// the actual protected-memory adapter. It is an enum, never a path or
|
|
8424
|
+
// provider credential.
|
|
8425
|
+
memory_provider: this.cfg.memoryProvider ?? "securecoms",
|
|
8426
|
+
// Inert legacy files only. `readLocalReport` revalidates the current
|
|
8427
|
+
// regular-file bytes and strips every local path before this leaves the
|
|
8428
|
+
// machine.
|
|
8429
|
+
legacy_memory: local?.legacy_memory ?? []
|
|
8083
8430
|
};
|
|
8084
8431
|
const posted = await this.fetchImpl(`${base}/report`, {
|
|
8085
8432
|
method: "POST",
|
|
@@ -8091,13 +8438,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8091
8438
|
});
|
|
8092
8439
|
if (!posted.ok) {
|
|
8093
8440
|
process.stderr.write(
|
|
8094
|
-
`[bridge] runtime config v${
|
|
8441
|
+
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
|
|
8442
|
+
` : `[bridge] legacy-memory inventory report failed (${posted.status}); no saved runtime config was applied
|
|
8095
8443
|
`
|
|
8096
8444
|
);
|
|
8097
8445
|
return;
|
|
8098
8446
|
}
|
|
8099
|
-
process.stderr.write(
|
|
8100
|
-
`
|
|
8447
|
+
process.stderr.write(
|
|
8448
|
+
version ? `[bridge] runtime config v${version} applied
|
|
8449
|
+
` : "[bridge] legacy-memory inventory reported; no saved runtime config was applied\n"
|
|
8450
|
+
);
|
|
8101
8451
|
} catch (e) {
|
|
8102
8452
|
process.stderr.write(
|
|
8103
8453
|
`[bridge] runtime config refetch failed: ${e?.message ?? e}
|
|
@@ -8133,6 +8483,10 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8133
8483
|
await this.provider.publishKeyPackage();
|
|
8134
8484
|
await this.refetchVoiceSettings();
|
|
8135
8485
|
await this.refetchRuntimeConfig();
|
|
8486
|
+
this.runtimeConfigTimer = setInterval(() => {
|
|
8487
|
+
void this.refetchRuntimeConfig();
|
|
8488
|
+
}, 6 * 60 * 60 * 1e3);
|
|
8489
|
+
this.runtimeConfigTimer.unref?.();
|
|
8136
8490
|
this.startReceivePump();
|
|
8137
8491
|
if (this.selfHealSweepEnabled) {
|
|
8138
8492
|
this.bootSweepTimer = setTimeout(
|
|
@@ -8935,7 +9289,47 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8935
9289
|
async recordMemory(input) {
|
|
8936
9290
|
return this.memory.record(input);
|
|
8937
9291
|
}
|
|
9292
|
+
/**
|
|
9293
|
+
* OpenClaw collapses text history and workspace facts into the owner's
|
|
9294
|
+
* session. Resolve the channel from fresh, authenticated server metadata
|
|
9295
|
+
* before exposing that provider through the local bridge API. A cold or
|
|
9296
|
+
* stale cache is refreshed once; any failure remains a fail-closed denial.
|
|
9297
|
+
*/
|
|
9298
|
+
async mayReadOpenClawMemory(channelId) {
|
|
9299
|
+
if (this.cfg.memoryProvider !== "openclaw") return true;
|
|
9300
|
+
if (!this.freshMeta(channelId)) await this.refreshChannelMeta();
|
|
9301
|
+
if (!this.freshRoster(channelId)) await this.refreshChannelRoster(channelId);
|
|
9302
|
+
const meta = this.freshMeta(channelId);
|
|
9303
|
+
const roster = this.freshRoster(channelId);
|
|
9304
|
+
if (meta?.is_dm !== true || !roster) return false;
|
|
9305
|
+
const humanCounterparts = [...roster.entries()].filter(
|
|
9306
|
+
([identityId, member]) => identityId !== this.cfg.identityId && !member.is_agent
|
|
9307
|
+
);
|
|
9308
|
+
return humanCounterparts.length === 1 && humanCounterparts[0][1].is_agent_owner;
|
|
9309
|
+
}
|
|
9310
|
+
/** An inbound turn may receive OpenClaw memory only from an active owner DM. */
|
|
9311
|
+
mayReadOpenClawMemoryForTurn(authorization) {
|
|
9312
|
+
return this.cfg.memoryProvider !== "openclaw" || authorization.sender_membership === "active" && authorization.audience === "owner_dm";
|
|
9313
|
+
}
|
|
9314
|
+
/**
|
|
9315
|
+
* Resolve the prior-turn snapshot carried to the native runtime. OpenClaw
|
|
9316
|
+
* stays owner-DM-only; every provider failure degrades to empty context and
|
|
9317
|
+
* never falls back to the runtime's retired plaintext JSON files.
|
|
9318
|
+
*/
|
|
9319
|
+
async memoryForTurn(channelId, authorization, limit = 50) {
|
|
9320
|
+
if (!this.mayReadOpenClawMemoryForTurn(authorization)) {
|
|
9321
|
+
return { recent: [], facts: [] };
|
|
9322
|
+
}
|
|
9323
|
+
try {
|
|
9324
|
+
return await this.memory.context(channelId, limit);
|
|
9325
|
+
} catch {
|
|
9326
|
+
return { recent: [], facts: [] };
|
|
9327
|
+
}
|
|
9328
|
+
}
|
|
8938
9329
|
async memoryContext(conversationId, limit) {
|
|
9330
|
+
if (!await this.mayReadOpenClawMemory(conversationId)) {
|
|
9331
|
+
return { recent: [], facts: [] };
|
|
9332
|
+
}
|
|
8939
9333
|
return this.memory.context(conversationId, limit);
|
|
8940
9334
|
}
|
|
8941
9335
|
/**
|
|
@@ -8979,10 +9373,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8979
9373
|
}
|
|
8980
9374
|
try {
|
|
8981
9375
|
let memory;
|
|
8982
|
-
|
|
8983
|
-
memory = await this.memory.context(m.channelId, 30);
|
|
8984
|
-
} catch {
|
|
9376
|
+
if (!this.mayReadOpenClawMemoryForTurn(m.authorization)) {
|
|
8985
9377
|
memory = { recent: [], facts: [] };
|
|
9378
|
+
} else {
|
|
9379
|
+
try {
|
|
9380
|
+
memory = await this.memory.context(m.channelId, 30);
|
|
9381
|
+
} catch {
|
|
9382
|
+
memory = { recent: [], facts: [] };
|
|
9383
|
+
}
|
|
8986
9384
|
}
|
|
8987
9385
|
const messages = buildFastMessages({
|
|
8988
9386
|
persona: this.cfg.voice.persona,
|
|
@@ -9235,13 +9633,42 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9235
9633
|
const rows = await res.json();
|
|
9236
9634
|
const members = /* @__PURE__ */ new Map();
|
|
9237
9635
|
for (const r of rows) {
|
|
9238
|
-
members.set(r.identity_id, {
|
|
9636
|
+
members.set(r.identity_id, {
|
|
9637
|
+
name: r.display_name,
|
|
9638
|
+
is_agent: r.is_agent,
|
|
9639
|
+
is_agent_owner: r.is_agent_owner === true,
|
|
9640
|
+
is_partner: r.is_partner === true
|
|
9641
|
+
});
|
|
9239
9642
|
}
|
|
9240
9643
|
this.channelRoster.set(channelId, { members, at: Date.now() });
|
|
9241
9644
|
await this.resolveAndPinOwner(channelId, rows);
|
|
9242
9645
|
} catch {
|
|
9243
9646
|
}
|
|
9244
9647
|
}
|
|
9648
|
+
/**
|
|
9649
|
+
* Classify the sender only from server-authenticated channel/roster data.
|
|
9650
|
+
* Missing or stale metadata is UNKNOWN with zero grants, never a permissive
|
|
9651
|
+
* guess. The returned object and grant array are frozen before they cross the
|
|
9652
|
+
* daemon boundary so one consumer cannot widen a later execution check.
|
|
9653
|
+
*/
|
|
9654
|
+
authorizationFor(channelId, senderId) {
|
|
9655
|
+
const meta = this.freshMeta(channelId);
|
|
9656
|
+
const sender = this.freshRoster(channelId)?.get(senderId);
|
|
9657
|
+
let audience = "unknown";
|
|
9658
|
+
if (meta && sender) {
|
|
9659
|
+
if (sender.is_agent) audience = "agent_to_agent";
|
|
9660
|
+
else if (meta.is_dm && sender.is_agent_owner) audience = "owner_dm";
|
|
9661
|
+
else if (meta.is_dm) audience = "other_human_dm";
|
|
9662
|
+
else if (sender.is_partner) audience = "partner_room";
|
|
9663
|
+
else audience = "team_room";
|
|
9664
|
+
}
|
|
9665
|
+
return Object.freeze({
|
|
9666
|
+
version: 1,
|
|
9667
|
+
audience,
|
|
9668
|
+
sender_membership: sender ? "active" : "unknown",
|
|
9669
|
+
tool_grants: Object.freeze([])
|
|
9670
|
+
});
|
|
9671
|
+
}
|
|
9245
9672
|
/**
|
|
9246
9673
|
* Task 8 (owner-attested Reset Secure Session): STRUCTURAL owner selection —
|
|
9247
9674
|
* NEVER read the server's `role` field to pick the owner. In a DM the owner
|
|
@@ -9755,6 +10182,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9755
10182
|
sender_id: m.envelope.sender_id,
|
|
9756
10183
|
text: typeof parsed.text === "string" ? parsed.text : "",
|
|
9757
10184
|
ts: m.receivedAt,
|
|
10185
|
+
authorization: this.authorizationFor(m.channelId, m.envelope.sender_id),
|
|
9758
10186
|
sender_name: senderName,
|
|
9759
10187
|
sender_is_agent: senderIsAgent,
|
|
9760
10188
|
attachments: [attachment],
|
|
@@ -9793,6 +10221,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9793
10221
|
const senderName = this.freshRoster(m.channelId)?.get(
|
|
9794
10222
|
m.envelope.sender_id
|
|
9795
10223
|
)?.name;
|
|
10224
|
+
const turnAuthorization = this.authorizationFor(
|
|
10225
|
+
m.channelId,
|
|
10226
|
+
m.envelope.sender_id
|
|
10227
|
+
);
|
|
10228
|
+
const runtimeMemory = this.memoryForTurn(m.channelId, turnAuthorization);
|
|
9796
10229
|
const speaker = parsed.relay === true && typeof parsed.speaker_identity_id === "string" && typeof parsed.speaker_display_name === "string" ? {
|
|
9797
10230
|
identityId: parsed.speaker_identity_id,
|
|
9798
10231
|
displayName: parsed.speaker_display_name
|
|
@@ -9809,12 +10242,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9809
10242
|
const turnText = parsed.text;
|
|
9810
10243
|
const isCallStart = turnText.trim() === VOICE_CALL_START_MARKER;
|
|
9811
10244
|
const fastGreeting = isCallStart && fastPathFiring;
|
|
9812
|
-
const runtimeEntry = (ackSent) => ({
|
|
10245
|
+
const runtimeEntry = (ackSent, memory) => ({
|
|
9813
10246
|
channel_id: m.channelId,
|
|
9814
10247
|
message_id: m.messageId,
|
|
9815
10248
|
sender_id: m.envelope.sender_id,
|
|
9816
10249
|
text: turnText,
|
|
9817
10250
|
ts: m.receivedAt,
|
|
10251
|
+
authorization: turnAuthorization,
|
|
10252
|
+
memory,
|
|
9818
10253
|
// A relay-attributed turn is the SPEAKER's message to the runtime:
|
|
9819
10254
|
// name it after them and never flag it as agent-authored.
|
|
9820
10255
|
sender_name: speaker?.displayName ?? senderName,
|
|
@@ -9828,18 +10263,23 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9828
10263
|
if (fastPathFiring) {
|
|
9829
10264
|
const fastSpeaker = speaker ?? (senderName ? { identityId: m.envelope.sender_id, displayName: senderName } : void 0);
|
|
9830
10265
|
void this.runFastPath(
|
|
9831
|
-
{
|
|
10266
|
+
{
|
|
10267
|
+
channelId: m.channelId,
|
|
10268
|
+
messageId: m.messageId,
|
|
10269
|
+
senderId: m.envelope.sender_id,
|
|
10270
|
+
authorization: turnAuthorization
|
|
10271
|
+
},
|
|
9832
10272
|
fastGreeting ? greetingTurnFor(this.voiceSettings ?? DEFAULT_VOICE_SETTINGS, fastSpeaker?.displayName) : parsed.text,
|
|
9833
10273
|
voiceGuidance,
|
|
9834
10274
|
fastSpeaker
|
|
9835
|
-
).then((outcome) => {
|
|
10275
|
+
).then(async (outcome) => {
|
|
9836
10276
|
if (outcome === "answered") return;
|
|
9837
|
-
this.buffer.push(runtimeEntry(outcome === "deferred"));
|
|
10277
|
+
this.buffer.push(runtimeEntry(outcome === "deferred", await runtimeMemory));
|
|
9838
10278
|
this.capBuffer();
|
|
9839
10279
|
this.wakeListeners();
|
|
9840
10280
|
});
|
|
9841
10281
|
} else {
|
|
9842
|
-
this.buffer.push(runtimeEntry(false));
|
|
10282
|
+
this.buffer.push(runtimeEntry(false, await runtimeMemory));
|
|
9843
10283
|
}
|
|
9844
10284
|
this.capBuffer();
|
|
9845
10285
|
void recordInboundMemory(
|
|
@@ -9905,6 +10345,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9905
10345
|
this.buffer.push(...keep);
|
|
9906
10346
|
return out;
|
|
9907
10347
|
}
|
|
10348
|
+
recordRuntimeReply(inReplyTo, channelId, replyMessageId) {
|
|
10349
|
+
return this.runtimeReplyReceipts.record(inReplyTo, channelId, replyMessageId);
|
|
10350
|
+
}
|
|
10351
|
+
runtimeReplyReceipt(inReplyTo) {
|
|
10352
|
+
return this.runtimeReplyReceipts.get(inReplyTo);
|
|
10353
|
+
}
|
|
9908
10354
|
/**
|
|
9909
10355
|
* Put messages back at the front (SSE/disconnect write-failure recovery).
|
|
9910
10356
|
*
|
|
@@ -10056,6 +10502,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
10056
10502
|
this.closingDown = true;
|
|
10057
10503
|
this.pingHandle?.close();
|
|
10058
10504
|
if (this.safetyTimer) clearInterval(this.safetyTimer);
|
|
10505
|
+
if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
|
|
10059
10506
|
if (this.bootSweepTimer) clearTimeout(this.bootSweepTimer);
|
|
10060
10507
|
if (this.selfHealTimer) clearTimeout(this.selfHealTimer);
|
|
10061
10508
|
await this.provider.drainInFlight(_BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
@@ -10289,6 +10736,7 @@ var sendSchema = z3.object({
|
|
|
10289
10736
|
channel_id: z3.string().min(1),
|
|
10290
10737
|
text: z3.string().optional(),
|
|
10291
10738
|
idempotency_key: z3.string().optional(),
|
|
10739
|
+
in_reply_to: z3.string().min(1).max(200).optional(),
|
|
10292
10740
|
a2a: a2aSchema.optional()
|
|
10293
10741
|
}).refine((d) => typeof d.text === "string" || d.a2a !== void 0, {
|
|
10294
10742
|
message: "send requires either text or a2a"
|
|
@@ -10405,10 +10853,19 @@ function createHttpServer(agent, opts) {
|
|
|
10405
10853
|
if (method === "GET" && path === "/v1/metrics") {
|
|
10406
10854
|
return json(res, 200, agent.metrics());
|
|
10407
10855
|
}
|
|
10856
|
+
if (method === "GET" && path === "/v1/reply-receipt") {
|
|
10857
|
+
const inReplyTo = url.searchParams.get("in_reply_to");
|
|
10858
|
+
if (!inReplyTo || inReplyTo.length > 200) return jsonError(res, "invalid_request");
|
|
10859
|
+
const receipt = agent.runtimeReplyReceipt(inReplyTo);
|
|
10860
|
+
return json(res, 200, receipt ? { found: true, receipt } : { found: false });
|
|
10861
|
+
}
|
|
10408
10862
|
if (method === "POST" && path === "/v1/send") {
|
|
10409
10863
|
const parsed = sendSchema.safeParse(JSON.parse(await readBody(req, SEND_CAP)));
|
|
10410
10864
|
if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
|
|
10411
10865
|
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);
|
|
10866
|
+
if (parsed.data.in_reply_to) {
|
|
10867
|
+
agent.recordRuntimeReply(parsed.data.in_reply_to, parsed.data.channel_id, r.message_id);
|
|
10868
|
+
}
|
|
10412
10869
|
return json(res, 200, r);
|
|
10413
10870
|
}
|
|
10414
10871
|
if (method === "POST" && path === "/v1/send-attachment") {
|
|
@@ -10664,6 +11121,10 @@ async function handleInbound(sock, agent, data, inflight) {
|
|
|
10664
11121
|
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
10665
11122
|
return;
|
|
10666
11123
|
}
|
|
11124
|
+
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)) {
|
|
11125
|
+
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
11126
|
+
return;
|
|
11127
|
+
}
|
|
10667
11128
|
if (m.a2a && typeof m.a2a === "object") {
|
|
10668
11129
|
const a = m.a2a;
|
|
10669
11130
|
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) {
|
|
@@ -10672,6 +11133,9 @@ async function handleInbound(sock, agent, data, inflight) {
|
|
|
10672
11133
|
}
|
|
10673
11134
|
try {
|
|
10674
11135
|
const r = await agent.sendA2A(m.channel_id, { a2a_version: a.a2a_version, kind: a.kind, object: a.object });
|
|
11136
|
+
if (typeof m.in_reply_to === "string") {
|
|
11137
|
+
agent.recordRuntimeReply(m.in_reply_to, m.channel_id, r.message_id);
|
|
11138
|
+
}
|
|
10675
11139
|
sendJson(sock, { type: "sent", message_id: r.message_id });
|
|
10676
11140
|
} catch (e) {
|
|
10677
11141
|
if (e instanceof ChannelPendingError) {
|
|
@@ -10690,6 +11154,9 @@ async function handleInbound(sock, agent, data, inflight) {
|
|
|
10690
11154
|
}
|
|
10691
11155
|
try {
|
|
10692
11156
|
const r = await agent.send(m.channel_id, m.text);
|
|
11157
|
+
if (typeof m.in_reply_to === "string") {
|
|
11158
|
+
agent.recordRuntimeReply(m.in_reply_to, m.channel_id, r.message_id);
|
|
11159
|
+
}
|
|
10693
11160
|
sendJson(sock, { type: "sent", message_id: r.message_id });
|
|
10694
11161
|
} catch (e) {
|
|
10695
11162
|
if (e instanceof ChannelPendingError) {
|