@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/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;
|
|
@@ -7409,11 +7570,98 @@ function readLocalReport(home, identityId) {
|
|
|
7409
7570
|
}
|
|
7410
7571
|
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
|
|
7411
7572
|
const hasSearchKey = doc.has_search_key === true;
|
|
7573
|
+
const hasLlmKey = doc.has_llm_key === true;
|
|
7412
7574
|
const raw = doc.persona;
|
|
7413
7575
|
const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.fromEntries(
|
|
7414
7576
|
Object.entries(raw).filter(([, v]) => typeof v === "string")
|
|
7415
7577
|
) : {};
|
|
7416
|
-
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 };
|
|
7417
7665
|
}
|
|
7418
7666
|
function readManagedMemory(home, identityId) {
|
|
7419
7667
|
const p = join6(runtimeConfigDir(home, identityId), MEMORY_FILE);
|
|
@@ -7442,6 +7690,51 @@ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
|
|
|
7442
7690
|
|
|
7443
7691
|
// src/bridge-agent.ts
|
|
7444
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
|
|
7445
7738
|
var VOICE_MODE_GUIDANCE = "Reply immediately and conversationally, one short sentence per message; never bundle multiple points into one message.";
|
|
7446
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.";
|
|
7447
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.";
|
|
@@ -7711,6 +8004,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
7711
8004
|
channelOwner = /* @__PURE__ */ new Map();
|
|
7712
8005
|
buffer = [];
|
|
7713
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();
|
|
7714
8013
|
/** Cumulative count of messages shed by the buffer cap (surfaced on health()/metrics()). */
|
|
7715
8014
|
droppedMessages = 0;
|
|
7716
8015
|
/**
|
|
@@ -7936,6 +8235,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
7936
8235
|
waiters = [];
|
|
7937
8236
|
pingHandle;
|
|
7938
8237
|
safetyTimer;
|
|
8238
|
+
runtimeConfigTimer;
|
|
7939
8239
|
listeners = /* @__PURE__ */ new Set();
|
|
7940
8240
|
startedAt = 0;
|
|
7941
8241
|
store;
|
|
@@ -8045,25 +8345,65 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8045
8345
|
});
|
|
8046
8346
|
if (!res.ok) throw new Error(`runtime-config ${res.status}`);
|
|
8047
8347
|
const payload = await res.json();
|
|
8048
|
-
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") {
|
|
8049
8353
|
process.stderr.write(
|
|
8050
|
-
`[bridge] runtime config not applied (version ${JSON.stringify(payload
|
|
8354
|
+
`[bridge] runtime config not applied (version ${JSON.stringify(payload.version ?? null)} \u2014 invalid response)
|
|
8051
8355
|
`
|
|
8052
8356
|
);
|
|
8053
8357
|
return;
|
|
8054
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
|
+
}
|
|
8055
8367
|
const skillsJson = payload.skills_json ?? {};
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8368
|
+
if (version) {
|
|
8369
|
+
writeManagedFiles(this.home, this.cfg.identityId, {
|
|
8370
|
+
...payload.files ?? {},
|
|
8371
|
+
"skills.json": `${JSON.stringify(skillsJson, null, 2)}
|
|
8059
8372
|
`
|
|
8060
|
-
|
|
8373
|
+
});
|
|
8374
|
+
}
|
|
8061
8375
|
if (payload.memory_prunes?.length) {
|
|
8062
8376
|
pruneManagedMemory(this.home, this.cfg.identityId, payload.memory_prunes);
|
|
8063
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
|
+
}
|
|
8064
8404
|
const local = readLocalReport(this.home, this.cfg.identityId);
|
|
8065
8405
|
const report = {
|
|
8066
|
-
applied_version:
|
|
8406
|
+
applied_version: version,
|
|
8067
8407
|
paths: checkConfiguredPaths(skillsJson),
|
|
8068
8408
|
// Read AFTER the prunes above, so the owner's next view of memory is
|
|
8069
8409
|
// the file as it now stands, not the one they just pruned from.
|
|
@@ -8075,7 +8415,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8075
8415
|
// is an environment value on that process, invisible to the daemon and
|
|
8076
8416
|
// to the backend. The backend needs it to generate a truthful tool
|
|
8077
8417
|
// paragraph, so it rides along here.
|
|
8078
|
-
has_search_key: local?.has_search_key ?? false
|
|
8418
|
+
has_search_key: local?.has_search_key ?? false,
|
|
8419
|
+
// Same idea, for the BYOK LLM key file: only the runtime can see
|
|
8420
|
+
// whether one is configured in its own environment.
|
|
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 ?? []
|
|
8079
8430
|
};
|
|
8080
8431
|
const posted = await this.fetchImpl(`${base}/report`, {
|
|
8081
8432
|
method: "POST",
|
|
@@ -8087,13 +8438,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8087
8438
|
});
|
|
8088
8439
|
if (!posted.ok) {
|
|
8089
8440
|
process.stderr.write(
|
|
8090
|
-
`[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
|
|
8091
8443
|
`
|
|
8092
8444
|
);
|
|
8093
8445
|
return;
|
|
8094
8446
|
}
|
|
8095
|
-
process.stderr.write(
|
|
8096
|
-
`
|
|
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
|
+
);
|
|
8097
8451
|
} catch (e) {
|
|
8098
8452
|
process.stderr.write(
|
|
8099
8453
|
`[bridge] runtime config refetch failed: ${e?.message ?? e}
|
|
@@ -8129,6 +8483,10 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8129
8483
|
await this.provider.publishKeyPackage();
|
|
8130
8484
|
await this.refetchVoiceSettings();
|
|
8131
8485
|
await this.refetchRuntimeConfig();
|
|
8486
|
+
this.runtimeConfigTimer = setInterval(() => {
|
|
8487
|
+
void this.refetchRuntimeConfig();
|
|
8488
|
+
}, 6 * 60 * 60 * 1e3);
|
|
8489
|
+
this.runtimeConfigTimer.unref?.();
|
|
8132
8490
|
this.startReceivePump();
|
|
8133
8491
|
if (this.selfHealSweepEnabled) {
|
|
8134
8492
|
this.bootSweepTimer = setTimeout(
|
|
@@ -8931,7 +9289,47 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8931
9289
|
async recordMemory(input) {
|
|
8932
9290
|
return this.memory.record(input);
|
|
8933
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
|
+
}
|
|
8934
9329
|
async memoryContext(conversationId, limit) {
|
|
9330
|
+
if (!await this.mayReadOpenClawMemory(conversationId)) {
|
|
9331
|
+
return { recent: [], facts: [] };
|
|
9332
|
+
}
|
|
8935
9333
|
return this.memory.context(conversationId, limit);
|
|
8936
9334
|
}
|
|
8937
9335
|
/**
|
|
@@ -8975,10 +9373,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8975
9373
|
}
|
|
8976
9374
|
try {
|
|
8977
9375
|
let memory;
|
|
8978
|
-
|
|
8979
|
-
memory = await this.memory.context(m.channelId, 30);
|
|
8980
|
-
} catch {
|
|
9376
|
+
if (!this.mayReadOpenClawMemoryForTurn(m.authorization)) {
|
|
8981
9377
|
memory = { recent: [], facts: [] };
|
|
9378
|
+
} else {
|
|
9379
|
+
try {
|
|
9380
|
+
memory = await this.memory.context(m.channelId, 30);
|
|
9381
|
+
} catch {
|
|
9382
|
+
memory = { recent: [], facts: [] };
|
|
9383
|
+
}
|
|
8982
9384
|
}
|
|
8983
9385
|
const messages = buildFastMessages({
|
|
8984
9386
|
persona: this.cfg.voice.persona,
|
|
@@ -9231,13 +9633,42 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9231
9633
|
const rows = await res.json();
|
|
9232
9634
|
const members = /* @__PURE__ */ new Map();
|
|
9233
9635
|
for (const r of rows) {
|
|
9234
|
-
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
|
+
});
|
|
9235
9642
|
}
|
|
9236
9643
|
this.channelRoster.set(channelId, { members, at: Date.now() });
|
|
9237
9644
|
await this.resolveAndPinOwner(channelId, rows);
|
|
9238
9645
|
} catch {
|
|
9239
9646
|
}
|
|
9240
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
|
+
}
|
|
9241
9672
|
/**
|
|
9242
9673
|
* Task 8 (owner-attested Reset Secure Session): STRUCTURAL owner selection —
|
|
9243
9674
|
* NEVER read the server's `role` field to pick the owner. In a DM the owner
|
|
@@ -9751,6 +10182,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9751
10182
|
sender_id: m.envelope.sender_id,
|
|
9752
10183
|
text: typeof parsed.text === "string" ? parsed.text : "",
|
|
9753
10184
|
ts: m.receivedAt,
|
|
10185
|
+
authorization: this.authorizationFor(m.channelId, m.envelope.sender_id),
|
|
9754
10186
|
sender_name: senderName,
|
|
9755
10187
|
sender_is_agent: senderIsAgent,
|
|
9756
10188
|
attachments: [attachment],
|
|
@@ -9789,6 +10221,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9789
10221
|
const senderName = this.freshRoster(m.channelId)?.get(
|
|
9790
10222
|
m.envelope.sender_id
|
|
9791
10223
|
)?.name;
|
|
10224
|
+
const turnAuthorization = this.authorizationFor(
|
|
10225
|
+
m.channelId,
|
|
10226
|
+
m.envelope.sender_id
|
|
10227
|
+
);
|
|
10228
|
+
const runtimeMemory = this.memoryForTurn(m.channelId, turnAuthorization);
|
|
9792
10229
|
const speaker = parsed.relay === true && typeof parsed.speaker_identity_id === "string" && typeof parsed.speaker_display_name === "string" ? {
|
|
9793
10230
|
identityId: parsed.speaker_identity_id,
|
|
9794
10231
|
displayName: parsed.speaker_display_name
|
|
@@ -9805,12 +10242,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9805
10242
|
const turnText = parsed.text;
|
|
9806
10243
|
const isCallStart = turnText.trim() === VOICE_CALL_START_MARKER;
|
|
9807
10244
|
const fastGreeting = isCallStart && fastPathFiring;
|
|
9808
|
-
const runtimeEntry = (ackSent) => ({
|
|
10245
|
+
const runtimeEntry = (ackSent, memory) => ({
|
|
9809
10246
|
channel_id: m.channelId,
|
|
9810
10247
|
message_id: m.messageId,
|
|
9811
10248
|
sender_id: m.envelope.sender_id,
|
|
9812
10249
|
text: turnText,
|
|
9813
10250
|
ts: m.receivedAt,
|
|
10251
|
+
authorization: turnAuthorization,
|
|
10252
|
+
memory,
|
|
9814
10253
|
// A relay-attributed turn is the SPEAKER's message to the runtime:
|
|
9815
10254
|
// name it after them and never flag it as agent-authored.
|
|
9816
10255
|
sender_name: speaker?.displayName ?? senderName,
|
|
@@ -9824,18 +10263,23 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9824
10263
|
if (fastPathFiring) {
|
|
9825
10264
|
const fastSpeaker = speaker ?? (senderName ? { identityId: m.envelope.sender_id, displayName: senderName } : void 0);
|
|
9826
10265
|
void this.runFastPath(
|
|
9827
|
-
{
|
|
10266
|
+
{
|
|
10267
|
+
channelId: m.channelId,
|
|
10268
|
+
messageId: m.messageId,
|
|
10269
|
+
senderId: m.envelope.sender_id,
|
|
10270
|
+
authorization: turnAuthorization
|
|
10271
|
+
},
|
|
9828
10272
|
fastGreeting ? greetingTurnFor(this.voiceSettings ?? DEFAULT_VOICE_SETTINGS, fastSpeaker?.displayName) : parsed.text,
|
|
9829
10273
|
voiceGuidance,
|
|
9830
10274
|
fastSpeaker
|
|
9831
|
-
).then((outcome) => {
|
|
10275
|
+
).then(async (outcome) => {
|
|
9832
10276
|
if (outcome === "answered") return;
|
|
9833
|
-
this.buffer.push(runtimeEntry(outcome === "deferred"));
|
|
10277
|
+
this.buffer.push(runtimeEntry(outcome === "deferred", await runtimeMemory));
|
|
9834
10278
|
this.capBuffer();
|
|
9835
10279
|
this.wakeListeners();
|
|
9836
10280
|
});
|
|
9837
10281
|
} else {
|
|
9838
|
-
this.buffer.push(runtimeEntry(false));
|
|
10282
|
+
this.buffer.push(runtimeEntry(false, await runtimeMemory));
|
|
9839
10283
|
}
|
|
9840
10284
|
this.capBuffer();
|
|
9841
10285
|
void recordInboundMemory(
|
|
@@ -9901,6 +10345,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9901
10345
|
this.buffer.push(...keep);
|
|
9902
10346
|
return out;
|
|
9903
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
|
+
}
|
|
9904
10354
|
/**
|
|
9905
10355
|
* Put messages back at the front (SSE/disconnect write-failure recovery).
|
|
9906
10356
|
*
|
|
@@ -10052,6 +10502,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
10052
10502
|
this.closingDown = true;
|
|
10053
10503
|
this.pingHandle?.close();
|
|
10054
10504
|
if (this.safetyTimer) clearInterval(this.safetyTimer);
|
|
10505
|
+
if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
|
|
10055
10506
|
if (this.bootSweepTimer) clearTimeout(this.bootSweepTimer);
|
|
10056
10507
|
if (this.selfHealTimer) clearTimeout(this.selfHealTimer);
|
|
10057
10508
|
await this.provider.drainInFlight(_BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
@@ -10285,6 +10736,7 @@ var sendSchema = z3.object({
|
|
|
10285
10736
|
channel_id: z3.string().min(1),
|
|
10286
10737
|
text: z3.string().optional(),
|
|
10287
10738
|
idempotency_key: z3.string().optional(),
|
|
10739
|
+
in_reply_to: z3.string().min(1).max(200).optional(),
|
|
10288
10740
|
a2a: a2aSchema.optional()
|
|
10289
10741
|
}).refine((d) => typeof d.text === "string" || d.a2a !== void 0, {
|
|
10290
10742
|
message: "send requires either text or a2a"
|
|
@@ -10401,10 +10853,19 @@ function createHttpServer(agent, opts) {
|
|
|
10401
10853
|
if (method === "GET" && path === "/v1/metrics") {
|
|
10402
10854
|
return json(res, 200, agent.metrics());
|
|
10403
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
|
+
}
|
|
10404
10862
|
if (method === "POST" && path === "/v1/send") {
|
|
10405
10863
|
const parsed = sendSchema.safeParse(JSON.parse(await readBody(req, SEND_CAP)));
|
|
10406
10864
|
if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
|
|
10407
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
|
+
}
|
|
10408
10869
|
return json(res, 200, r);
|
|
10409
10870
|
}
|
|
10410
10871
|
if (method === "POST" && path === "/v1/send-attachment") {
|
|
@@ -10660,6 +11121,10 @@ async function handleInbound(sock, agent, data, inflight) {
|
|
|
10660
11121
|
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
10661
11122
|
return;
|
|
10662
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
|
+
}
|
|
10663
11128
|
if (m.a2a && typeof m.a2a === "object") {
|
|
10664
11129
|
const a = m.a2a;
|
|
10665
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) {
|
|
@@ -10668,6 +11133,9 @@ async function handleInbound(sock, agent, data, inflight) {
|
|
|
10668
11133
|
}
|
|
10669
11134
|
try {
|
|
10670
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
|
+
}
|
|
10671
11139
|
sendJson(sock, { type: "sent", message_id: r.message_id });
|
|
10672
11140
|
} catch (e) {
|
|
10673
11141
|
if (e instanceof ChannelPendingError) {
|
|
@@ -10686,6 +11154,9 @@ async function handleInbound(sock, agent, data, inflight) {
|
|
|
10686
11154
|
}
|
|
10687
11155
|
try {
|
|
10688
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
|
+
}
|
|
10689
11160
|
sendJson(sock, { type: "sent", message_id: r.message_id });
|
|
10690
11161
|
} catch (e) {
|
|
10691
11162
|
if (e instanceof ChannelPendingError) {
|