@securecoms/agent-bridge 0.26.1 → 0.26.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +342 -36
- package/dist/bridge-agent.d.ts +128 -3
- package/dist/index.js +342 -36
- package/dist/memory-taps.d.ts +3 -2
- package/dist/service-log.d.ts +14 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -7597,6 +7597,15 @@ function systemPrompt(p, settings, agentName) {
|
|
|
7597
7597
|
}
|
|
7598
7598
|
|
|
7599
7599
|
// src/service-log.ts
|
|
7600
|
+
var ENUM_VALUE = /^[a-z][a-z_]{0,31}$/;
|
|
7601
|
+
var ServiceLogEnum = class _ServiceLogEnum {
|
|
7602
|
+
constructor(value) {
|
|
7603
|
+
this.value = value;
|
|
7604
|
+
}
|
|
7605
|
+
static of(value, allowed) {
|
|
7606
|
+
return new _ServiceLogEnum(allowed.includes(value) && ENUM_VALUE.test(value) ? value : "invalid");
|
|
7607
|
+
}
|
|
7608
|
+
};
|
|
7600
7609
|
var TOKEN = /^[a-z][a-z0-9_.-]{0,63}$/;
|
|
7601
7610
|
var CLASS = /^[A-Za-z_$][A-Za-z0-9_$]{0,63}$/;
|
|
7602
7611
|
function errorClass(error) {
|
|
@@ -7626,6 +7635,7 @@ function formatServiceLog(level3, event, fields = {}, error) {
|
|
|
7626
7635
|
if (!TOKEN.test(key)) continue;
|
|
7627
7636
|
if (typeof value === "boolean") parts.push(`${key}=${value}`);
|
|
7628
7637
|
else if (typeof value === "number" && Number.isFinite(value)) parts.push(`${key}=${value}`);
|
|
7638
|
+
else if (value instanceof ServiceLogEnum && ENUM_VALUE.test(value.value)) parts.push(`${key}=${value.value}`);
|
|
7629
7639
|
}
|
|
7630
7640
|
if (error !== void 0) parts.push(`error_class=${errorClass(error)}`);
|
|
7631
7641
|
return parts.join(" ");
|
|
@@ -28383,9 +28393,9 @@ async function recordInboundMemory(sink, channelId, senderId, text, messageId, m
|
|
|
28383
28393
|
} catch {
|
|
28384
28394
|
}
|
|
28385
28395
|
}
|
|
28386
|
-
async function recordOutboundMemory(sink, channelId, text, messageId) {
|
|
28396
|
+
async function recordOutboundMemory(sink, channelId, text, messageId, modality = "text") {
|
|
28387
28397
|
try {
|
|
28388
|
-
await sink.record({ conversationId: channelId, role: "agent", text, modality
|
|
28398
|
+
await sink.record({ conversationId: channelId, role: "agent", text, modality, messageId });
|
|
28389
28399
|
} catch {
|
|
28390
28400
|
}
|
|
28391
28401
|
}
|
|
@@ -29110,6 +29120,15 @@ function logPendingCommitResolved(outcome) {
|
|
|
29110
29120
|
writeServiceLog("info", "mls.pending_commit.resolved.unknown");
|
|
29111
29121
|
}
|
|
29112
29122
|
}
|
|
29123
|
+
var OWNER_DM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
29124
|
+
Object.freeze({ tool: "current_time", mode: "read" }),
|
|
29125
|
+
Object.freeze({ tool: "web_search", mode: "read" })
|
|
29126
|
+
]);
|
|
29127
|
+
var NO_TOOL_GRANTS = Object.freeze([]);
|
|
29128
|
+
function ownerDmDefaultGrantsEnabled(env = process.env) {
|
|
29129
|
+
const v = env.SECURECOMS_OWNER_DM_DEFAULT_GRANTS;
|
|
29130
|
+
return v === void 0 || v === "on";
|
|
29131
|
+
}
|
|
29113
29132
|
var VAULT_REF_BARE_TEXT = "[Files link]";
|
|
29114
29133
|
var VOICE_MODE_GUIDANCE = "Reply immediately and conversationally, one short sentence per message; never bundle multiple points into one message.";
|
|
29115
29134
|
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.";
|
|
@@ -29121,6 +29140,20 @@ function selectVoiceGuidance(isDm, voiceDefaultResponder) {
|
|
|
29121
29140
|
}
|
|
29122
29141
|
return VOICE_MODE_GUIDANCE;
|
|
29123
29142
|
}
|
|
29143
|
+
var VOICE_COALESCE_WINDOW_MS = 0;
|
|
29144
|
+
var VOICE_COALESCE_WINDOW_MAX_MS = 5e3;
|
|
29145
|
+
function voiceCoalesceWindowFromEnv(env = process.env) {
|
|
29146
|
+
const raw = env.SECURECOMS_VOICE_COALESCE_MS;
|
|
29147
|
+
if (raw === void 0 || !/^\d{1,6}$/.test(raw.trim())) return VOICE_COALESCE_WINDOW_MS;
|
|
29148
|
+
return Math.min(Number(raw.trim()), VOICE_COALESCE_WINDOW_MAX_MS);
|
|
29149
|
+
}
|
|
29150
|
+
function waitMs(ms) {
|
|
29151
|
+
if (ms <= 0) return Promise.resolve();
|
|
29152
|
+
return new Promise((resolve3) => {
|
|
29153
|
+
const t = setTimeout(resolve3, ms);
|
|
29154
|
+
t.unref?.();
|
|
29155
|
+
});
|
|
29156
|
+
}
|
|
29124
29157
|
var DEFAULT_MAX_BUFFER = 1e3;
|
|
29125
29158
|
var DEFAULT_MAX_DELIVERY_ATTEMPTS = 5;
|
|
29126
29159
|
var REDELIVERY_BACKOFF_BASE_MS = 5e3;
|
|
@@ -29405,6 +29438,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
29405
29438
|
this.fastLlm = deps.fastLlm ?? (cfg.voice?.fastPath.enabled ? new FastLlmClient(cfg.voice.fastPath, this.fetchImpl) : void 0);
|
|
29406
29439
|
this.voiceSettingsSource = cfg.voice ? "file" : "none";
|
|
29407
29440
|
this.latency = deps.latency ?? new LatencyRecorder();
|
|
29441
|
+
this.voiceCoalesceWindowMs = Math.max(0, deps.voiceCoalesceWindowMs ?? voiceCoalesceWindowFromEnv());
|
|
29408
29442
|
this.maxBuffer = deps.maxBuffer ?? DEFAULT_MAX_BUFFER;
|
|
29409
29443
|
this.maxDeliveryAttempts = deps.maxDeliveryAttempts ?? DEFAULT_MAX_DELIVERY_ATTEMPTS;
|
|
29410
29444
|
this.maxAgentHops = deps.maxAgentHops ?? DEFAULT_MAX_AGENT_HOPS;
|
|
@@ -29653,6 +29687,19 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
29653
29687
|
/** Room send-floor tokens reserved by queued fast replies, per channel (F3). */
|
|
29654
29688
|
fastReplyReserved = /* @__PURE__ */ new Map();
|
|
29655
29689
|
static FAST_REPLY_MAX_JOBS = 32;
|
|
29690
|
+
/** Voice repeats (a): see {@link VOICE_COALESCE_WINDOW_MS}. */
|
|
29691
|
+
voiceCoalesceWindowMs;
|
|
29692
|
+
/** H1: fast-path reply ids being published right now (recorded as voice memory). */
|
|
29693
|
+
spokenReplyIds = /* @__PURE__ */ new Set();
|
|
29694
|
+
/** Open coalescing windows, keyed by channel + speaker. */
|
|
29695
|
+
voiceCoalescing = /* @__PURE__ */ new Map();
|
|
29696
|
+
/** Ids of every coalesced fragment (capped FIFO): a redelivered row is acked, never a second turn. */
|
|
29697
|
+
voiceCoalescedIds = /* @__PURE__ */ new Set();
|
|
29698
|
+
/** Merged turn id -> the absorbed fragments' held frames, retired with it. */
|
|
29699
|
+
voiceCoalescedTails = /* @__PURE__ */ new Map();
|
|
29700
|
+
/** (4) recv.lag: application queue ids already measured (capped FIFO), so a re-pulled row is not re-counted. */
|
|
29701
|
+
recvLagSeen = /* @__PURE__ */ new Set();
|
|
29702
|
+
static RECV_LAG_SEEN_CAP = 2e3;
|
|
29656
29703
|
/** Fast-path turns handed to the runtime behind a spoken ack (LLM [[DEFER]], pre-check, or ack_only). */
|
|
29657
29704
|
fastPathDeferred = 0;
|
|
29658
29705
|
/** Of those, `full`-mode turns the deterministic pre-check deferred without calling the fast model. */
|
|
@@ -29746,6 +29793,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
29746
29793
|
* back to the delivery-row-driven Task 11 behaviour only.
|
|
29747
29794
|
*/
|
|
29748
29795
|
selfHealSweepEnabled = process.env.SECURECOMS_SELF_HEAL_SWEEP !== "false";
|
|
29796
|
+
/** Read once at startup; see ownerDmDefaultGrantsEnabled. */
|
|
29797
|
+
ownerDmDefaultGrants = ownerDmDefaultGrantsEnabled();
|
|
29749
29798
|
/** Task 12: proactive sweep cadence (in addition to the once-at-boot fire). */
|
|
29750
29799
|
static SELF_HEAL_SWEEP_INTERVAL_MS = SELF_HEAL_SWEEP_INTERVAL_MS;
|
|
29751
29800
|
/**
|
|
@@ -31110,7 +31159,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31110
31159
|
/** Bookkeeping after a CONFIRMED publish of `body` under `id`. */
|
|
31111
31160
|
afterPublish(channelId, body, id, opts) {
|
|
31112
31161
|
if (opts.agentTurn !== false) this.agentHops.noteOwnSend(channelId);
|
|
31113
|
-
|
|
31162
|
+
const modality = this.spokenReplyIds.has(id) ? "voice" : "text";
|
|
31163
|
+
if (body.kind === "text") void recordOutboundMemory(this.memory, channelId, body.text, id, modality);
|
|
31114
31164
|
}
|
|
31115
31165
|
isIdempotentSend(messageId, opts) {
|
|
31116
31166
|
if (opts.inReplyTo === void 0 && opts.idempotencyKey === void 0) return false;
|
|
@@ -31288,26 +31338,26 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31288
31338
|
} catch (e) {
|
|
31289
31339
|
if (e instanceof ReplyInFlightError) {
|
|
31290
31340
|
if (inFlight >= REPLY_IN_FLIGHT_RETRIES) throw e;
|
|
31291
|
-
const
|
|
31341
|
+
const waitMs3 = Math.min(
|
|
31292
31342
|
REPLY_RATE_LIMIT_MAX_WAIT_MS,
|
|
31293
31343
|
Math.max(REPLY_IN_FLIGHT_BASE_WAIT_MS * 2 ** inFlight, e.retryAfterSec * 1e3)
|
|
31294
31344
|
);
|
|
31295
31345
|
inFlight++;
|
|
31296
|
-
writeServiceLog("info", "reply.in_flight_retry", { attempt: inFlight, wait_ms:
|
|
31297
|
-
await this.replySleep(
|
|
31346
|
+
writeServiceLog("info", "reply.in_flight_retry", { attempt: inFlight, wait_ms: waitMs3 });
|
|
31347
|
+
await this.replySleep(waitMs3);
|
|
31298
31348
|
continue;
|
|
31299
31349
|
}
|
|
31300
31350
|
const backendSec = backendRetryAfterSec(e);
|
|
31301
31351
|
const wait = e instanceof RateLimitedError ? e.retryAfterMs : backendSec !== void 0 ? backendSec * 1e3 : null;
|
|
31302
31352
|
if (wait === null) throw e;
|
|
31303
|
-
const
|
|
31353
|
+
const waitMs2 = Math.min(REPLY_RATE_LIMIT_MAX_WAIT_MS, Math.max(REPLY_RATE_LIMIT_MIN_WAIT_MS, wait));
|
|
31304
31354
|
if (rateLimited >= REPLY_RATE_LIMIT_RETRIES) {
|
|
31305
|
-
throw e instanceof RateLimitedError ? e : new RateLimitedError(channelId,
|
|
31355
|
+
throw e instanceof RateLimitedError ? e : new RateLimitedError(channelId, waitMs2);
|
|
31306
31356
|
}
|
|
31307
31357
|
rateLimited++;
|
|
31308
31358
|
this.replyRateLimitRetries++;
|
|
31309
|
-
writeServiceLog("info", "reply.rate_limited_retry", { attempt: rateLimited, wait_ms:
|
|
31310
|
-
await this.replySleep(
|
|
31359
|
+
writeServiceLog("info", "reply.rate_limited_retry", { attempt: rateLimited, wait_ms: waitMs2 });
|
|
31360
|
+
await this.replySleep(waitMs2);
|
|
31311
31361
|
}
|
|
31312
31362
|
}
|
|
31313
31363
|
}
|
|
@@ -31796,6 +31846,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31796
31846
|
if (needsRuntime(text)) return { kind: "ack", line: deferAck(messageId), intent: true };
|
|
31797
31847
|
return { kind: "fast" };
|
|
31798
31848
|
}
|
|
31849
|
+
noteVoiceCoalesced(messageId) {
|
|
31850
|
+
this.voiceCoalescedIds.add(messageId);
|
|
31851
|
+
if (this.voiceCoalescedIds.size > _BridgeAgent.FAST_PATH_SEEN_CAP) {
|
|
31852
|
+
const oldest = this.voiceCoalescedIds.values().next().value;
|
|
31853
|
+
if (oldest !== void 0) this.voiceCoalescedIds.delete(oldest);
|
|
31854
|
+
}
|
|
31855
|
+
}
|
|
31799
31856
|
markFastPathSeen(messageId) {
|
|
31800
31857
|
this.fastPathSeen.add(messageId);
|
|
31801
31858
|
if (this.fastPathSeen.size > _BridgeAgent.FAST_PATH_SEEN_CAP) {
|
|
@@ -31812,22 +31869,36 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31812
31869
|
* reserve a room send-floor token until they finish. A job never throws.
|
|
31813
31870
|
*/
|
|
31814
31871
|
enqueueFastReply(channelId, job, spends = true) {
|
|
31815
|
-
|
|
31816
|
-
this.fastReplyJobs++;
|
|
31817
|
-
this.fastReplyReserved.set(channelId, (this.fastReplyReserved.get(channelId) ?? 0) + 1);
|
|
31818
|
-
}
|
|
31872
|
+
const release = spends ? this.reserveFastReply(channelId) : void 0;
|
|
31819
31873
|
const prev = this.fastReplyChains.get(channelId) ?? Promise.resolve();
|
|
31820
31874
|
const next = prev.then(() => this.outsideIngestCycle(job)).catch((e) => writeServiceLog("warn", "voice.fast_reply_job_failed", {}, e)).finally(() => {
|
|
31821
|
-
|
|
31822
|
-
this.fastReplyJobs--;
|
|
31823
|
-
const left3 = (this.fastReplyReserved.get(channelId) ?? 1) - 1;
|
|
31824
|
-
if (left3 > 0) this.fastReplyReserved.set(channelId, left3);
|
|
31825
|
-
else this.fastReplyReserved.delete(channelId);
|
|
31826
|
-
}
|
|
31875
|
+
release?.();
|
|
31827
31876
|
if (this.fastReplyChains.get(channelId) === next) this.fastReplyChains.delete(channelId);
|
|
31828
31877
|
});
|
|
31829
31878
|
this.fastReplyChains.set(channelId, next);
|
|
31830
31879
|
}
|
|
31880
|
+
/** Count a fast reply against FAST_REPLY_MAX_JOBS and reserve its room send-floor token; returns the release. */
|
|
31881
|
+
reserveFastReply(channelId) {
|
|
31882
|
+
this.fastReplyJobs++;
|
|
31883
|
+
this.fastReplyReserved.set(channelId, (this.fastReplyReserved.get(channelId) ?? 0) + 1);
|
|
31884
|
+
return () => {
|
|
31885
|
+
this.fastReplyJobs--;
|
|
31886
|
+
const left3 = (this.fastReplyReserved.get(channelId) ?? 1) - 1;
|
|
31887
|
+
if (left3 > 0) this.fastReplyReserved.set(channelId, left3);
|
|
31888
|
+
else this.fastReplyReserved.delete(channelId);
|
|
31889
|
+
};
|
|
31890
|
+
}
|
|
31891
|
+
/**
|
|
31892
|
+
* Speak a planned fast turn ("ack" or "fast") and settle its held frame:
|
|
31893
|
+
* retired when the fast model answered alone, otherwise released to the
|
|
31894
|
+
* runtime (with the "ack already spoken" guidance only when an ack or a
|
|
31895
|
+
* deferring fast reply was actually published — F3).
|
|
31896
|
+
*/
|
|
31897
|
+
async speakAndSettle(turn, plan, held, heldAfterAck, fastText, guidance, fastSpeaker, greeting) {
|
|
31898
|
+
const outcome = plan.kind === "ack" ? await this.sendFastLine(turn.channelId, turn.messageId, plan.line) ? "deferred" : "failed" : await this.runFastReply(turn, fastText, guidance, fastSpeaker, greeting);
|
|
31899
|
+
if (outcome === "answered") await this.settleHeldRuntimeMessage(held, false);
|
|
31900
|
+
else await this.settleHeldRuntimeMessage(outcome === "deferred" ? heldAfterAck : held, true);
|
|
31901
|
+
}
|
|
31831
31902
|
/**
|
|
31832
31903
|
* Stage a runtime frame without overtaking a turn of the same channel that
|
|
31833
31904
|
* is still queued behind the fast path (F2): while the channel has a queue,
|
|
@@ -31843,10 +31914,19 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31843
31914
|
async settleFastReplies() {
|
|
31844
31915
|
while (this.fastReplyChains.size) await Promise.all([...this.fastReplyChains.values()]);
|
|
31845
31916
|
}
|
|
31917
|
+
/** Send a line the voice fast path speaks: its memory record is "voice" (H1). */
|
|
31918
|
+
async sendSpoken(channelId, text, messageId) {
|
|
31919
|
+
this.spokenReplyIds.add(messageId);
|
|
31920
|
+
try {
|
|
31921
|
+
await this.send(channelId, text, messageId);
|
|
31922
|
+
} finally {
|
|
31923
|
+
this.spokenReplyIds.delete(messageId);
|
|
31924
|
+
}
|
|
31925
|
+
}
|
|
31846
31926
|
/** Speak a fixed acknowledgement line; failures are counted, never thrown. */
|
|
31847
31927
|
async sendFastLine(channelId, messageId, line) {
|
|
31848
31928
|
try {
|
|
31849
|
-
await this.
|
|
31929
|
+
await this.sendSpoken(channelId, line, fastReplyId(this.replyIdKey, this.cfg.identityId, channelId, messageId));
|
|
31850
31930
|
this.fastPathSent++;
|
|
31851
31931
|
return true;
|
|
31852
31932
|
} catch (e) {
|
|
@@ -31903,7 +31983,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31903
31983
|
const raw = await this.latency.time("voice.fast_llm", () => fastLlm.complete(messages));
|
|
31904
31984
|
const { reply, defer } = splitFastReply(raw, 200, maxSentences);
|
|
31905
31985
|
if (!reply) return "failed";
|
|
31906
|
-
await this.
|
|
31986
|
+
await this.sendSpoken(m.channelId, reply, fastReplyId(this.replyIdKey, this.cfg.identityId, m.channelId, m.messageId));
|
|
31907
31987
|
this.fastPathSent++;
|
|
31908
31988
|
if (defer) this.fastPathDeferred++;
|
|
31909
31989
|
return defer ? "deferred" : "answered";
|
|
@@ -31936,15 +32016,48 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31936
32016
|
}
|
|
31937
32017
|
}
|
|
31938
32018
|
/** Settle a held frame: the fast path answered alone (retire it) or the runtime answers next (release it, in channel order). */
|
|
31939
|
-
settleHeldRuntimeMessage(message, release) {
|
|
32019
|
+
async settleHeldRuntimeMessage(message, release) {
|
|
31940
32020
|
if (!this.runtimePendingIds.has(message.message_id)) return;
|
|
31941
32021
|
if (!release) {
|
|
31942
32022
|
this.ackRuntimeMessages([message.message_id]);
|
|
31943
32023
|
return;
|
|
31944
32024
|
}
|
|
31945
|
-
this.
|
|
32025
|
+
const memory = await this.memoryAtRelease(message);
|
|
32026
|
+
if (this.closingDown || !this.runtimePendingIds.has(message.message_id)) return;
|
|
32027
|
+
this.buffer.push(memory === message.memory ? message : { ...message, memory });
|
|
31946
32028
|
this.wakeListeners();
|
|
31947
32029
|
}
|
|
32030
|
+
/**
|
|
32031
|
+
* Voice repeats (b): a held frame's memory was read when its turn ARRIVED,
|
|
32032
|
+
* before the replies queued ahead of it (an earlier turn's fast reply, this
|
|
32033
|
+
* turn's own ack) were spoken. The runtime replaces its history with the
|
|
32034
|
+
* frame's memory, so with the arrival snapshot it answered an
|
|
32035
|
+
* already-answered turn again. At release the context is re-read and the
|
|
32036
|
+
* AGENT turns it holds beyond the snapshot (afterPublish ->
|
|
32037
|
+
* recordOutboundMemory) are appended to the snapshot. Peer turns recorded
|
|
32038
|
+
* since (this turn itself, later turns) are not, so the frame stays
|
|
32039
|
+
* prior-turn-only for what the runtime is about to answer. New turns are
|
|
32040
|
+
* found by recorded time, never by `seq` (the OpenClaw provider renumbers
|
|
32041
|
+
* seq per read). A failed re-read keeps the arrival snapshot.
|
|
32042
|
+
*/
|
|
32043
|
+
async memoryAtRelease(message) {
|
|
32044
|
+
const prior = message.memory;
|
|
32045
|
+
if (!prior || !this.mayReadOpenClawMemoryForTurn(message.authorization)) return prior;
|
|
32046
|
+
let fresh;
|
|
32047
|
+
try {
|
|
32048
|
+
fresh = await this.memory.context(message.channel_id, _BridgeAgent.TURN_MEMORY_LIMIT);
|
|
32049
|
+
} catch {
|
|
32050
|
+
return prior;
|
|
32051
|
+
}
|
|
32052
|
+
const after = prior.recent.reduce((max, turn) => Math.max(max, turn.ts ?? 0), message.ts);
|
|
32053
|
+
const spoken = fresh.recent.filter((turn) => turn.role === "agent" && turn.ts !== void 0 && turn.ts > after);
|
|
32054
|
+
if (spoken.length === 0) return prior;
|
|
32055
|
+
return {
|
|
32056
|
+
recent: [...prior.recent, ...spoken].slice(-_BridgeAgent.TURN_MEMORY_LIMIT),
|
|
32057
|
+
facts: prior.facts
|
|
32058
|
+
};
|
|
32059
|
+
}
|
|
32060
|
+
static TURN_MEMORY_LIMIT = 50;
|
|
31948
32061
|
/**
|
|
31949
32062
|
* List channels in one enclave (default: the daemon's own home enclave).
|
|
31950
32063
|
* Task 3: called per-*known* enclave by refreshChannelMeta so a
|
|
@@ -32206,10 +32319,22 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
32206
32319
|
* Missing or stale metadata is UNKNOWN with zero grants, never a permissive
|
|
32207
32320
|
* guess. The returned object and grant array are frozen before they cross the
|
|
32208
32321
|
* daemon boundary so one consumer cannot widen a later execution check.
|
|
32322
|
+
*
|
|
32323
|
+
* Grants (architect ruling 2026-10-02): OWNER_DM_DEFAULT_READ_GRANTS only when
|
|
32324
|
+
* the kill switch is on, the channel is a DM, and EITHER
|
|
32325
|
+
* (a) audience is owner_dm AND the sender is the pinned structural owner
|
|
32326
|
+
* (`channelOwner`, never "anomalous") — a lying `is_agent_owner` flag
|
|
32327
|
+
* alone gets nothing; OR
|
|
32328
|
+
* (b) the sender is this channel's server-marked relay (`is_relay`) and the
|
|
32329
|
+
* relayed `speaker_identity_id` (passed by the caller only for relay
|
|
32330
|
+
* turns) is that same pinned owner, whose roster row is the agent's
|
|
32331
|
+
* owner. A speaker claim from any non-relay sender gets nothing.
|
|
32332
|
+
* Everything else gets the frozen empty list.
|
|
32209
32333
|
*/
|
|
32210
|
-
authorizationFor(channelId, senderId) {
|
|
32334
|
+
authorizationFor(channelId, senderId, relaySpeakerId) {
|
|
32211
32335
|
const meta = this.freshMeta(channelId);
|
|
32212
|
-
const
|
|
32336
|
+
const roster = this.freshRoster(channelId);
|
|
32337
|
+
const sender = roster?.get(senderId);
|
|
32213
32338
|
let audience = "unknown";
|
|
32214
32339
|
if (meta && sender) {
|
|
32215
32340
|
if (sender.is_agent) audience = "agent_to_agent";
|
|
@@ -32218,11 +32343,71 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
32218
32343
|
else if (sender.is_partner) audience = "partner_room";
|
|
32219
32344
|
else audience = "team_room";
|
|
32220
32345
|
}
|
|
32346
|
+
let tool_grants = NO_TOOL_GRANTS;
|
|
32347
|
+
if (this.ownerDmDefaultGrants && meta?.is_dm === true && sender) {
|
|
32348
|
+
const pinned = this.channelOwner.get(channelId);
|
|
32349
|
+
const ownerId = pinned && pinned !== "anomalous" ? pinned.identityId : void 0;
|
|
32350
|
+
if (ownerId !== void 0) {
|
|
32351
|
+
const ownerDirect = audience === "owner_dm" && senderId === ownerId;
|
|
32352
|
+
const owner = roster?.get(ownerId);
|
|
32353
|
+
const ownerRelayed = relaySpeakerId !== void 0 && sender.is_relay && relaySpeakerId === ownerId && owner !== void 0 && !owner.is_agent && owner.is_agent_owner;
|
|
32354
|
+
if (ownerDirect || ownerRelayed) tool_grants = OWNER_DM_DEFAULT_READ_GRANTS;
|
|
32355
|
+
}
|
|
32356
|
+
}
|
|
32357
|
+
this.logAuthzDecision(channelId, senderId, relaySpeakerId, audience, tool_grants.length);
|
|
32221
32358
|
return Object.freeze({
|
|
32222
32359
|
version: 1,
|
|
32223
32360
|
audience,
|
|
32224
32361
|
sender_membership: sender ? "active" : "unknown",
|
|
32225
|
-
tool_grants
|
|
32362
|
+
tool_grants
|
|
32363
|
+
});
|
|
32364
|
+
}
|
|
32365
|
+
/**
|
|
32366
|
+
* One content-free INFO line per authorization decision (every call site of
|
|
32367
|
+
* authorizationFor stages exactly one runtime turn). It says WHY a turn got
|
|
32368
|
+
* the grants it got, without any identifier, name or text: every field is a
|
|
32369
|
+
* boolean, a count, or a closed-set label.
|
|
32370
|
+
*
|
|
32371
|
+
* owner_pin pinned | anomalous | missing (channelOwner cache)
|
|
32372
|
+
* owner_row_is_agent_owner the pinned owner's roster row; `absent` when
|
|
32373
|
+
* there is no pin or the row is not in the roster
|
|
32374
|
+
* sender_is_owner sender identity == pinned owner identity
|
|
32375
|
+
* relay sender's roster row has is_relay
|
|
32376
|
+
* relay_speaker_is_owner `na` unless a relay speaker was passed
|
|
32377
|
+
* kill_switch SECURECOMS_OWNER_DM_DEFAULT_GRANTS as read at
|
|
32378
|
+
* startup: `on` = default grants enabled
|
|
32379
|
+
* meta_fresh / roster_fresh whether the channel meta / roster caches were
|
|
32380
|
+
* fresh (either stale forces audience=unknown)
|
|
32381
|
+
*/
|
|
32382
|
+
logAuthzDecision(channelId, senderId, relaySpeakerId, audience, grants) {
|
|
32383
|
+
const meta = this.freshMeta(channelId);
|
|
32384
|
+
const roster = this.freshRoster(channelId);
|
|
32385
|
+
const sender = roster?.get(senderId);
|
|
32386
|
+
const pinned = this.channelOwner.get(channelId);
|
|
32387
|
+
const ownerId = pinned && pinned !== "anomalous" ? pinned.identityId : void 0;
|
|
32388
|
+
const ownerRow = ownerId !== void 0 ? roster?.get(ownerId) : void 0;
|
|
32389
|
+
const ownerPin = pinned === void 0 ? "missing" : pinned === "anomalous" ? "anomalous" : "pinned";
|
|
32390
|
+
const ownerRowIsAgentOwner = ownerRow === void 0 ? "absent" : ownerRow.is_agent_owner ? "true" : "false";
|
|
32391
|
+
const speakerIsOwner = relaySpeakerId === void 0 ? "na" : ownerId !== void 0 && relaySpeakerId === ownerId ? "true" : "false";
|
|
32392
|
+
writeServiceLog("info", "authz.decision", {
|
|
32393
|
+
audience: ServiceLogEnum.of(audience, [
|
|
32394
|
+
"owner_dm",
|
|
32395
|
+
"other_human_dm",
|
|
32396
|
+
"team_room",
|
|
32397
|
+
"partner_room",
|
|
32398
|
+
"agent_to_agent",
|
|
32399
|
+
"unknown"
|
|
32400
|
+
]),
|
|
32401
|
+
grants,
|
|
32402
|
+
owner_pin: ServiceLogEnum.of(ownerPin, ["pinned", "anomalous", "missing"]),
|
|
32403
|
+
owner_row_is_agent_owner: ServiceLogEnum.of(ownerRowIsAgentOwner, ["true", "false", "absent"]),
|
|
32404
|
+
sender_is_owner: ownerId !== void 0 && senderId === ownerId,
|
|
32405
|
+
relay: sender?.is_relay === true,
|
|
32406
|
+
relay_speaker_is_owner: ServiceLogEnum.of(speakerIsOwner, ["true", "false", "na"]),
|
|
32407
|
+
is_dm: meta?.is_dm === true,
|
|
32408
|
+
kill_switch: ServiceLogEnum.of(this.ownerDmDefaultGrants ? "on" : "off", ["on", "off"]),
|
|
32409
|
+
meta_fresh: meta !== void 0,
|
|
32410
|
+
roster_fresh: roster !== void 0
|
|
32226
32411
|
});
|
|
32227
32412
|
}
|
|
32228
32413
|
/**
|
|
@@ -32730,11 +32915,36 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
32730
32915
|
this.ingestInFlight = entry;
|
|
32731
32916
|
return promise;
|
|
32732
32917
|
}
|
|
32918
|
+
/**
|
|
32919
|
+
* (4) recv.lag = server message created_at -> this pull's completion, per
|
|
32920
|
+
* application row, counted once per queue id. It makes the daemon-side
|
|
32921
|
+
* receive delay measurable (recv.pull alone times only the HTTP call).
|
|
32922
|
+
* Cross-clock: daemon-vs-server skew lands in the sample; a negative lag
|
|
32923
|
+
* is clamped to 0. The server serialises a naive UTC datetime (no offset),
|
|
32924
|
+
* which Date.parse would read as LOCAL time, so a bare timestamp is UTC.
|
|
32925
|
+
*/
|
|
32926
|
+
recordRecvLag(rows) {
|
|
32927
|
+
const now = Date.now();
|
|
32928
|
+
for (const row of rows) {
|
|
32929
|
+
if (row.message_type !== "application" || typeof row.queue_id !== "string") continue;
|
|
32930
|
+
if (typeof row.created_at !== "string" || this.recvLagSeen.has(row.queue_id)) continue;
|
|
32931
|
+
const iso = /(Z|[+-]\d{2}:?\d{2})$/i.test(row.created_at) ? row.created_at : `${row.created_at}Z`;
|
|
32932
|
+
const createdAt = Date.parse(iso);
|
|
32933
|
+
if (!Number.isFinite(createdAt)) continue;
|
|
32934
|
+
this.recvLagSeen.add(row.queue_id);
|
|
32935
|
+
if (this.recvLagSeen.size > _BridgeAgent.RECV_LAG_SEEN_CAP) {
|
|
32936
|
+
const oldest = this.recvLagSeen.values().next().value;
|
|
32937
|
+
if (oldest !== void 0) this.recvLagSeen.delete(oldest);
|
|
32938
|
+
}
|
|
32939
|
+
this.latency.record("recv.lag", Math.max(0, now - createdAt));
|
|
32940
|
+
}
|
|
32941
|
+
}
|
|
32733
32942
|
async ingestCycle() {
|
|
32734
32943
|
if (this.closingDown) return;
|
|
32735
32944
|
const cycleStart = performance.now();
|
|
32736
32945
|
try {
|
|
32737
32946
|
const rows = await this.latency.time("recv.pull", () => this.backend.pullDelivery(this.cfg.deviceId));
|
|
32947
|
+
this.recordRecvLag(rows);
|
|
32738
32948
|
const channelIds = /* @__PURE__ */ new Set();
|
|
32739
32949
|
for (const row of rows) {
|
|
32740
32950
|
if (row.connect_channel_id) channelIds.add(row.connect_channel_id);
|
|
@@ -32991,21 +33201,23 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
32991
33201
|
)?.name;
|
|
32992
33202
|
const turnAuthorization = this.authorizationFor(
|
|
32993
33203
|
m.channelId,
|
|
32994
|
-
m.envelope.sender_id
|
|
33204
|
+
m.envelope.sender_id,
|
|
33205
|
+
speaker?.identityId
|
|
32995
33206
|
);
|
|
32996
33207
|
const runtimeMemory = this.memoryForTurn(m.channelId, turnAuthorization);
|
|
32997
33208
|
const voiceGuidance = m.voiceMode ? selectVoiceGuidance(meta?.is_dm, m.voiceDefaultResponder) : void 0;
|
|
32998
33209
|
const humanTurn = senderIsAgent !== true || speaker !== void 0;
|
|
32999
33210
|
const fastPathEligible = Boolean(m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
|
|
33211
|
+
const turnText = parsed.text;
|
|
33212
|
+
const isCallStart = turnText.trim() === VOICE_CALL_START_MARKER;
|
|
33213
|
+
const coalescible = this.voiceCoalesceWindowMs > 0 && m.voiceMode === true && speaker !== void 0 && parsed.transcribed === true && voiceGuidance !== void 0 && !isCallStart;
|
|
33000
33214
|
let fastPathRoomBlocked = false;
|
|
33001
|
-
if (fastPathEligible && meta && meta.is_dm === false) {
|
|
33215
|
+
if (!coalescible && fastPathEligible && meta && meta.is_dm === false) {
|
|
33002
33216
|
const reserved = this.fastReplyReserved.get(m.channelId) ?? 0;
|
|
33003
33217
|
if (this.rateFloor.available(`room:${m.channelId}`) - reserved < 2) fastPathRoomBlocked = true;
|
|
33004
33218
|
}
|
|
33005
33219
|
if (fastPathRoomBlocked) this.fastPathSkipped++;
|
|
33006
33220
|
const fastPathFiring = fastPathEligible && !fastPathRoomBlocked && !this.pausedChannels.has(m.channelId);
|
|
33007
|
-
const turnText = parsed.text;
|
|
33008
|
-
const isCallStart = turnText.trim() === VOICE_CALL_START_MARKER;
|
|
33009
33221
|
const fastGreeting = isCallStart && fastPathFiring;
|
|
33010
33222
|
const runtimeVoice = (ackSent) => m.voiceMode ? {
|
|
33011
33223
|
voice: true,
|
|
@@ -33026,6 +33238,95 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
33026
33238
|
...speaker ? { speaker } : {},
|
|
33027
33239
|
...runtimeVoice(ackSent)
|
|
33028
33240
|
});
|
|
33241
|
+
if (coalescible) {
|
|
33242
|
+
if (this.voiceCoalescedIds.has(m.messageId) || this.runtimePendingIds.has(m.messageId)) {
|
|
33243
|
+
acked.push(m.queueId);
|
|
33244
|
+
continue;
|
|
33245
|
+
}
|
|
33246
|
+
const windowKey = `${m.channelId}\0${speaker.identityId}`;
|
|
33247
|
+
const open = this.voiceCoalescing.get(windowKey);
|
|
33248
|
+
if (open && !open.closed) {
|
|
33249
|
+
open.ids.push(m.messageId);
|
|
33250
|
+
open.parts.push(turnText);
|
|
33251
|
+
const staging = (async () => {
|
|
33252
|
+
const ok = await this.stageHeldRuntimeMessage(runtimeEntry(false, await runtimeMemory));
|
|
33253
|
+
if (!ok) {
|
|
33254
|
+
const i = open.ids.indexOf(m.messageId);
|
|
33255
|
+
if (i > 0) {
|
|
33256
|
+
open.ids.splice(i, 1);
|
|
33257
|
+
open.parts.splice(i, 1);
|
|
33258
|
+
}
|
|
33259
|
+
}
|
|
33260
|
+
return ok;
|
|
33261
|
+
})();
|
|
33262
|
+
open.pending = Promise.all([open.pending, staging]);
|
|
33263
|
+
if (!await staging) continue;
|
|
33264
|
+
this.noteVoiceCoalesced(m.messageId);
|
|
33265
|
+
acked.push(m.queueId);
|
|
33266
|
+
continue;
|
|
33267
|
+
}
|
|
33268
|
+
const held = runtimeEntry(false, await runtimeMemory);
|
|
33269
|
+
if (!await this.stageHeldRuntimeMessage(held)) continue;
|
|
33270
|
+
this.noteVoiceCoalesced(m.messageId);
|
|
33271
|
+
const win = {
|
|
33272
|
+
speakerId: speaker.identityId,
|
|
33273
|
+
ids: [m.messageId],
|
|
33274
|
+
parts: [turnText],
|
|
33275
|
+
closed: false,
|
|
33276
|
+
pending: Promise.resolve()
|
|
33277
|
+
};
|
|
33278
|
+
this.voiceCoalescing.set(windowKey, win);
|
|
33279
|
+
const openedAt = Date.now();
|
|
33280
|
+
const channelId = m.channelId;
|
|
33281
|
+
const senderId = m.envelope.sender_id;
|
|
33282
|
+
const isRoom = meta?.is_dm === false;
|
|
33283
|
+
this.enqueueFastReply(channelId, async () => {
|
|
33284
|
+
await waitMs(openedAt + this.voiceCoalesceWindowMs - Date.now());
|
|
33285
|
+
win.closed = true;
|
|
33286
|
+
if (this.voiceCoalescing.get(windowKey) === win) this.voiceCoalescing.delete(windowKey);
|
|
33287
|
+
await win.pending;
|
|
33288
|
+
if (this.closingDown) return;
|
|
33289
|
+
const [firstId, ...tailIds] = win.ids;
|
|
33290
|
+
if (tailIds.length) this.voiceCoalescedTails.set(firstId, tailIds);
|
|
33291
|
+
const text = win.parts.join(" ");
|
|
33292
|
+
const merged = { ...held, text };
|
|
33293
|
+
void recordInboundMemory(this.memory, channelId, senderId, text, firstId, "voice");
|
|
33294
|
+
let roomBlocked = false;
|
|
33295
|
+
if (fastPathEligible && isRoom) {
|
|
33296
|
+
const reserved = this.fastReplyReserved.get(channelId) ?? 0;
|
|
33297
|
+
if (this.rateFloor.available(`room:${channelId}`) - reserved < 2) roomBlocked = true;
|
|
33298
|
+
}
|
|
33299
|
+
if (roomBlocked) this.fastPathSkipped++;
|
|
33300
|
+
const plan = fastPathEligible && !roomBlocked && !this.pausedChannels.has(channelId) ? this.planFastPath(firstId, text, false) : void 0;
|
|
33301
|
+
if (!plan || plan.kind === "dup" || plan.kind === "skip") {
|
|
33302
|
+
if (plan?.kind === "skip" && plan.full) this.fastPathSkipped++;
|
|
33303
|
+
await this.settleHeldRuntimeMessage(merged, true);
|
|
33304
|
+
return;
|
|
33305
|
+
}
|
|
33306
|
+
this.markFastPathSeen(firstId);
|
|
33307
|
+
if (plan.kind === "ack") {
|
|
33308
|
+
this.fastPathDeferred++;
|
|
33309
|
+
if (plan.intent) this.fastPathIntentDeferred++;
|
|
33310
|
+
}
|
|
33311
|
+
const release = this.reserveFastReply(channelId);
|
|
33312
|
+
try {
|
|
33313
|
+
await this.speakAndSettle(
|
|
33314
|
+
{ channelId, messageId: firstId, senderId, authorization: turnAuthorization },
|
|
33315
|
+
plan,
|
|
33316
|
+
merged,
|
|
33317
|
+
{ ...merged, ...runtimeVoice(true) },
|
|
33318
|
+
text,
|
|
33319
|
+
voiceGuidance,
|
|
33320
|
+
speaker,
|
|
33321
|
+
false
|
|
33322
|
+
);
|
|
33323
|
+
} finally {
|
|
33324
|
+
release();
|
|
33325
|
+
}
|
|
33326
|
+
}, false);
|
|
33327
|
+
acked.push(m.queueId);
|
|
33328
|
+
continue;
|
|
33329
|
+
}
|
|
33029
33330
|
if (fastPathFiring) {
|
|
33030
33331
|
const fastSpeaker = speaker ?? (senderName ? { identityId: m.envelope.sender_id, displayName: senderName } : void 0);
|
|
33031
33332
|
const fastText = fastGreeting ? greetingTurnFor(this.voiceSettings ?? DEFAULT_VOICE_SETTINGS, fastSpeaker?.displayName) : parsed.text;
|
|
@@ -33051,11 +33352,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
33051
33352
|
this.fastPathDeferred++;
|
|
33052
33353
|
if (ack.intent) this.fastPathIntentDeferred++;
|
|
33053
33354
|
}
|
|
33054
|
-
|
|
33055
|
-
|
|
33056
|
-
|
|
33057
|
-
|
|
33058
|
-
|
|
33355
|
+
const heldAfterAck = { ...held, ...runtimeVoice(true) };
|
|
33356
|
+
this.enqueueFastReply(
|
|
33357
|
+
m.channelId,
|
|
33358
|
+
() => this.speakAndSettle(turn, ack ?? plan, held, heldAfterAck, fastText, guidanceForFast, fastSpeaker, greeting)
|
|
33359
|
+
);
|
|
33059
33360
|
}
|
|
33060
33361
|
} else {
|
|
33061
33362
|
if (!await this.stageRuntimeMessageInOrder(runtimeEntry(false, await runtimeMemory))) continue;
|
|
@@ -33476,6 +33777,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
33476
33777
|
this.deliveryAttempts.delete(messageId);
|
|
33477
33778
|
this.redeliverNotBefore.delete(messageId);
|
|
33478
33779
|
this.supersedeRefundable.delete(messageId);
|
|
33780
|
+
const tails = this.voiceCoalescedTails.get(messageId);
|
|
33781
|
+
if (tails) {
|
|
33782
|
+
this.voiceCoalescedTails.delete(messageId);
|
|
33783
|
+
this.ackRuntimeMessages(tails);
|
|
33784
|
+
}
|
|
33479
33785
|
}
|
|
33480
33786
|
}
|
|
33481
33787
|
async restoreRuntimeInbox() {
|