@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/index.js
CHANGED
|
@@ -27308,6 +27308,15 @@ var TokenBucketFloor = class {
|
|
|
27308
27308
|
};
|
|
27309
27309
|
|
|
27310
27310
|
// src/service-log.ts
|
|
27311
|
+
var ENUM_VALUE = /^[a-z][a-z_]{0,31}$/;
|
|
27312
|
+
var ServiceLogEnum = class _ServiceLogEnum {
|
|
27313
|
+
constructor(value) {
|
|
27314
|
+
this.value = value;
|
|
27315
|
+
}
|
|
27316
|
+
static of(value, allowed) {
|
|
27317
|
+
return new _ServiceLogEnum(allowed.includes(value) && ENUM_VALUE.test(value) ? value : "invalid");
|
|
27318
|
+
}
|
|
27319
|
+
};
|
|
27311
27320
|
var TOKEN2 = /^[a-z][a-z0-9_.-]{0,63}$/;
|
|
27312
27321
|
var CLASS2 = /^[A-Za-z_$][A-Za-z0-9_$]{0,63}$/;
|
|
27313
27322
|
function errorClass2(error) {
|
|
@@ -27337,6 +27346,7 @@ function formatServiceLog2(level3, event, fields = {}, error) {
|
|
|
27337
27346
|
if (!TOKEN2.test(key)) continue;
|
|
27338
27347
|
if (typeof value === "boolean") parts.push(`${key}=${value}`);
|
|
27339
27348
|
else if (typeof value === "number" && Number.isFinite(value)) parts.push(`${key}=${value}`);
|
|
27349
|
+
else if (value instanceof ServiceLogEnum && ENUM_VALUE.test(value.value)) parts.push(`${key}=${value.value}`);
|
|
27340
27350
|
}
|
|
27341
27351
|
if (error !== void 0) parts.push(`error_class=${errorClass2(error)}`);
|
|
27342
27352
|
return parts.join(" ");
|
|
@@ -27971,9 +27981,9 @@ async function recordInboundMemory(sink, channelId, senderId, text, messageId, m
|
|
|
27971
27981
|
} catch {
|
|
27972
27982
|
}
|
|
27973
27983
|
}
|
|
27974
|
-
async function recordOutboundMemory(sink, channelId, text, messageId) {
|
|
27984
|
+
async function recordOutboundMemory(sink, channelId, text, messageId, modality = "text") {
|
|
27975
27985
|
try {
|
|
27976
|
-
await sink.record({ conversationId: channelId, role: "agent", text, modality
|
|
27986
|
+
await sink.record({ conversationId: channelId, role: "agent", text, modality, messageId });
|
|
27977
27987
|
} catch {
|
|
27978
27988
|
}
|
|
27979
27989
|
}
|
|
@@ -28958,6 +28968,15 @@ function logPendingCommitResolved(outcome) {
|
|
|
28958
28968
|
writeServiceLog2("info", "mls.pending_commit.resolved.unknown");
|
|
28959
28969
|
}
|
|
28960
28970
|
}
|
|
28971
|
+
var OWNER_DM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
28972
|
+
Object.freeze({ tool: "current_time", mode: "read" }),
|
|
28973
|
+
Object.freeze({ tool: "web_search", mode: "read" })
|
|
28974
|
+
]);
|
|
28975
|
+
var NO_TOOL_GRANTS = Object.freeze([]);
|
|
28976
|
+
function ownerDmDefaultGrantsEnabled(env = process.env) {
|
|
28977
|
+
const v = env.SECURECOMS_OWNER_DM_DEFAULT_GRANTS;
|
|
28978
|
+
return v === void 0 || v === "on";
|
|
28979
|
+
}
|
|
28961
28980
|
var VAULT_REF_BARE_TEXT = "[Files link]";
|
|
28962
28981
|
var VOICE_MODE_GUIDANCE = "Reply immediately and conversationally, one short sentence per message; never bundle multiple points into one message.";
|
|
28963
28982
|
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.";
|
|
@@ -28969,6 +28988,20 @@ function selectVoiceGuidance(isDm, voiceDefaultResponder) {
|
|
|
28969
28988
|
}
|
|
28970
28989
|
return VOICE_MODE_GUIDANCE;
|
|
28971
28990
|
}
|
|
28991
|
+
var VOICE_COALESCE_WINDOW_MS = 0;
|
|
28992
|
+
var VOICE_COALESCE_WINDOW_MAX_MS = 5e3;
|
|
28993
|
+
function voiceCoalesceWindowFromEnv(env = process.env) {
|
|
28994
|
+
const raw = env.SECURECOMS_VOICE_COALESCE_MS;
|
|
28995
|
+
if (raw === void 0 || !/^\d{1,6}$/.test(raw.trim())) return VOICE_COALESCE_WINDOW_MS;
|
|
28996
|
+
return Math.min(Number(raw.trim()), VOICE_COALESCE_WINDOW_MAX_MS);
|
|
28997
|
+
}
|
|
28998
|
+
function waitMs(ms) {
|
|
28999
|
+
if (ms <= 0) return Promise.resolve();
|
|
29000
|
+
return new Promise((resolve) => {
|
|
29001
|
+
const t = setTimeout(resolve, ms);
|
|
29002
|
+
t.unref?.();
|
|
29003
|
+
});
|
|
29004
|
+
}
|
|
28972
29005
|
var DEFAULT_MAX_BUFFER = 1e3;
|
|
28973
29006
|
var DEFAULT_MAX_DELIVERY_ATTEMPTS = 5;
|
|
28974
29007
|
var REDELIVERY_BACKOFF_BASE_MS = 5e3;
|
|
@@ -29253,6 +29286,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
29253
29286
|
this.fastLlm = deps.fastLlm ?? (cfg.voice?.fastPath.enabled ? new FastLlmClient(cfg.voice.fastPath, this.fetchImpl) : void 0);
|
|
29254
29287
|
this.voiceSettingsSource = cfg.voice ? "file" : "none";
|
|
29255
29288
|
this.latency = deps.latency ?? new LatencyRecorder();
|
|
29289
|
+
this.voiceCoalesceWindowMs = Math.max(0, deps.voiceCoalesceWindowMs ?? voiceCoalesceWindowFromEnv());
|
|
29256
29290
|
this.maxBuffer = deps.maxBuffer ?? DEFAULT_MAX_BUFFER;
|
|
29257
29291
|
this.maxDeliveryAttempts = deps.maxDeliveryAttempts ?? DEFAULT_MAX_DELIVERY_ATTEMPTS;
|
|
29258
29292
|
this.maxAgentHops = deps.maxAgentHops ?? DEFAULT_MAX_AGENT_HOPS;
|
|
@@ -29501,6 +29535,19 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
29501
29535
|
/** Room send-floor tokens reserved by queued fast replies, per channel (F3). */
|
|
29502
29536
|
fastReplyReserved = /* @__PURE__ */ new Map();
|
|
29503
29537
|
static FAST_REPLY_MAX_JOBS = 32;
|
|
29538
|
+
/** Voice repeats (a): see {@link VOICE_COALESCE_WINDOW_MS}. */
|
|
29539
|
+
voiceCoalesceWindowMs;
|
|
29540
|
+
/** H1: fast-path reply ids being published right now (recorded as voice memory). */
|
|
29541
|
+
spokenReplyIds = /* @__PURE__ */ new Set();
|
|
29542
|
+
/** Open coalescing windows, keyed by channel + speaker. */
|
|
29543
|
+
voiceCoalescing = /* @__PURE__ */ new Map();
|
|
29544
|
+
/** Ids of every coalesced fragment (capped FIFO): a redelivered row is acked, never a second turn. */
|
|
29545
|
+
voiceCoalescedIds = /* @__PURE__ */ new Set();
|
|
29546
|
+
/** Merged turn id -> the absorbed fragments' held frames, retired with it. */
|
|
29547
|
+
voiceCoalescedTails = /* @__PURE__ */ new Map();
|
|
29548
|
+
/** (4) recv.lag: application queue ids already measured (capped FIFO), so a re-pulled row is not re-counted. */
|
|
29549
|
+
recvLagSeen = /* @__PURE__ */ new Set();
|
|
29550
|
+
static RECV_LAG_SEEN_CAP = 2e3;
|
|
29504
29551
|
/** Fast-path turns handed to the runtime behind a spoken ack (LLM [[DEFER]], pre-check, or ack_only). */
|
|
29505
29552
|
fastPathDeferred = 0;
|
|
29506
29553
|
/** Of those, `full`-mode turns the deterministic pre-check deferred without calling the fast model. */
|
|
@@ -29594,6 +29641,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
29594
29641
|
* back to the delivery-row-driven Task 11 behaviour only.
|
|
29595
29642
|
*/
|
|
29596
29643
|
selfHealSweepEnabled = process.env.SECURECOMS_SELF_HEAL_SWEEP !== "false";
|
|
29644
|
+
/** Read once at startup; see ownerDmDefaultGrantsEnabled. */
|
|
29645
|
+
ownerDmDefaultGrants = ownerDmDefaultGrantsEnabled();
|
|
29597
29646
|
/** Task 12: proactive sweep cadence (in addition to the once-at-boot fire). */
|
|
29598
29647
|
static SELF_HEAL_SWEEP_INTERVAL_MS = SELF_HEAL_SWEEP_INTERVAL_MS;
|
|
29599
29648
|
/**
|
|
@@ -30958,7 +31007,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
30958
31007
|
/** Bookkeeping after a CONFIRMED publish of `body` under `id`. */
|
|
30959
31008
|
afterPublish(channelId, body, id, opts) {
|
|
30960
31009
|
if (opts.agentTurn !== false) this.agentHops.noteOwnSend(channelId);
|
|
30961
|
-
|
|
31010
|
+
const modality = this.spokenReplyIds.has(id) ? "voice" : "text";
|
|
31011
|
+
if (body.kind === "text") void recordOutboundMemory(this.memory, channelId, body.text, id, modality);
|
|
30962
31012
|
}
|
|
30963
31013
|
isIdempotentSend(messageId, opts) {
|
|
30964
31014
|
if (opts.inReplyTo === void 0 && opts.idempotencyKey === void 0) return false;
|
|
@@ -31136,26 +31186,26 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31136
31186
|
} catch (e) {
|
|
31137
31187
|
if (e instanceof ReplyInFlightError) {
|
|
31138
31188
|
if (inFlight >= REPLY_IN_FLIGHT_RETRIES) throw e;
|
|
31139
|
-
const
|
|
31189
|
+
const waitMs3 = Math.min(
|
|
31140
31190
|
REPLY_RATE_LIMIT_MAX_WAIT_MS,
|
|
31141
31191
|
Math.max(REPLY_IN_FLIGHT_BASE_WAIT_MS * 2 ** inFlight, e.retryAfterSec * 1e3)
|
|
31142
31192
|
);
|
|
31143
31193
|
inFlight++;
|
|
31144
|
-
writeServiceLog2("info", "reply.in_flight_retry", { attempt: inFlight, wait_ms:
|
|
31145
|
-
await this.replySleep(
|
|
31194
|
+
writeServiceLog2("info", "reply.in_flight_retry", { attempt: inFlight, wait_ms: waitMs3 });
|
|
31195
|
+
await this.replySleep(waitMs3);
|
|
31146
31196
|
continue;
|
|
31147
31197
|
}
|
|
31148
31198
|
const backendSec = backendRetryAfterSec(e);
|
|
31149
31199
|
const wait = e instanceof RateLimitedError ? e.retryAfterMs : backendSec !== void 0 ? backendSec * 1e3 : null;
|
|
31150
31200
|
if (wait === null) throw e;
|
|
31151
|
-
const
|
|
31201
|
+
const waitMs2 = Math.min(REPLY_RATE_LIMIT_MAX_WAIT_MS, Math.max(REPLY_RATE_LIMIT_MIN_WAIT_MS, wait));
|
|
31152
31202
|
if (rateLimited >= REPLY_RATE_LIMIT_RETRIES) {
|
|
31153
|
-
throw e instanceof RateLimitedError ? e : new RateLimitedError(channelId,
|
|
31203
|
+
throw e instanceof RateLimitedError ? e : new RateLimitedError(channelId, waitMs2);
|
|
31154
31204
|
}
|
|
31155
31205
|
rateLimited++;
|
|
31156
31206
|
this.replyRateLimitRetries++;
|
|
31157
|
-
writeServiceLog2("info", "reply.rate_limited_retry", { attempt: rateLimited, wait_ms:
|
|
31158
|
-
await this.replySleep(
|
|
31207
|
+
writeServiceLog2("info", "reply.rate_limited_retry", { attempt: rateLimited, wait_ms: waitMs2 });
|
|
31208
|
+
await this.replySleep(waitMs2);
|
|
31159
31209
|
}
|
|
31160
31210
|
}
|
|
31161
31211
|
}
|
|
@@ -31644,6 +31694,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31644
31694
|
if (needsRuntime(text)) return { kind: "ack", line: deferAck(messageId), intent: true };
|
|
31645
31695
|
return { kind: "fast" };
|
|
31646
31696
|
}
|
|
31697
|
+
noteVoiceCoalesced(messageId) {
|
|
31698
|
+
this.voiceCoalescedIds.add(messageId);
|
|
31699
|
+
if (this.voiceCoalescedIds.size > _BridgeAgent.FAST_PATH_SEEN_CAP) {
|
|
31700
|
+
const oldest = this.voiceCoalescedIds.values().next().value;
|
|
31701
|
+
if (oldest !== void 0) this.voiceCoalescedIds.delete(oldest);
|
|
31702
|
+
}
|
|
31703
|
+
}
|
|
31647
31704
|
markFastPathSeen(messageId) {
|
|
31648
31705
|
this.fastPathSeen.add(messageId);
|
|
31649
31706
|
if (this.fastPathSeen.size > _BridgeAgent.FAST_PATH_SEEN_CAP) {
|
|
@@ -31660,22 +31717,36 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31660
31717
|
* reserve a room send-floor token until they finish. A job never throws.
|
|
31661
31718
|
*/
|
|
31662
31719
|
enqueueFastReply(channelId, job, spends = true) {
|
|
31663
|
-
|
|
31664
|
-
this.fastReplyJobs++;
|
|
31665
|
-
this.fastReplyReserved.set(channelId, (this.fastReplyReserved.get(channelId) ?? 0) + 1);
|
|
31666
|
-
}
|
|
31720
|
+
const release = spends ? this.reserveFastReply(channelId) : void 0;
|
|
31667
31721
|
const prev = this.fastReplyChains.get(channelId) ?? Promise.resolve();
|
|
31668
31722
|
const next = prev.then(() => this.outsideIngestCycle(job)).catch((e) => writeServiceLog2("warn", "voice.fast_reply_job_failed", {}, e)).finally(() => {
|
|
31669
|
-
|
|
31670
|
-
this.fastReplyJobs--;
|
|
31671
|
-
const left3 = (this.fastReplyReserved.get(channelId) ?? 1) - 1;
|
|
31672
|
-
if (left3 > 0) this.fastReplyReserved.set(channelId, left3);
|
|
31673
|
-
else this.fastReplyReserved.delete(channelId);
|
|
31674
|
-
}
|
|
31723
|
+
release?.();
|
|
31675
31724
|
if (this.fastReplyChains.get(channelId) === next) this.fastReplyChains.delete(channelId);
|
|
31676
31725
|
});
|
|
31677
31726
|
this.fastReplyChains.set(channelId, next);
|
|
31678
31727
|
}
|
|
31728
|
+
/** Count a fast reply against FAST_REPLY_MAX_JOBS and reserve its room send-floor token; returns the release. */
|
|
31729
|
+
reserveFastReply(channelId) {
|
|
31730
|
+
this.fastReplyJobs++;
|
|
31731
|
+
this.fastReplyReserved.set(channelId, (this.fastReplyReserved.get(channelId) ?? 0) + 1);
|
|
31732
|
+
return () => {
|
|
31733
|
+
this.fastReplyJobs--;
|
|
31734
|
+
const left3 = (this.fastReplyReserved.get(channelId) ?? 1) - 1;
|
|
31735
|
+
if (left3 > 0) this.fastReplyReserved.set(channelId, left3);
|
|
31736
|
+
else this.fastReplyReserved.delete(channelId);
|
|
31737
|
+
};
|
|
31738
|
+
}
|
|
31739
|
+
/**
|
|
31740
|
+
* Speak a planned fast turn ("ack" or "fast") and settle its held frame:
|
|
31741
|
+
* retired when the fast model answered alone, otherwise released to the
|
|
31742
|
+
* runtime (with the "ack already spoken" guidance only when an ack or a
|
|
31743
|
+
* deferring fast reply was actually published — F3).
|
|
31744
|
+
*/
|
|
31745
|
+
async speakAndSettle(turn, plan, held, heldAfterAck, fastText, guidance, fastSpeaker, greeting) {
|
|
31746
|
+
const outcome = plan.kind === "ack" ? await this.sendFastLine(turn.channelId, turn.messageId, plan.line) ? "deferred" : "failed" : await this.runFastReply(turn, fastText, guidance, fastSpeaker, greeting);
|
|
31747
|
+
if (outcome === "answered") await this.settleHeldRuntimeMessage(held, false);
|
|
31748
|
+
else await this.settleHeldRuntimeMessage(outcome === "deferred" ? heldAfterAck : held, true);
|
|
31749
|
+
}
|
|
31679
31750
|
/**
|
|
31680
31751
|
* Stage a runtime frame without overtaking a turn of the same channel that
|
|
31681
31752
|
* is still queued behind the fast path (F2): while the channel has a queue,
|
|
@@ -31691,10 +31762,19 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31691
31762
|
async settleFastReplies() {
|
|
31692
31763
|
while (this.fastReplyChains.size) await Promise.all([...this.fastReplyChains.values()]);
|
|
31693
31764
|
}
|
|
31765
|
+
/** Send a line the voice fast path speaks: its memory record is "voice" (H1). */
|
|
31766
|
+
async sendSpoken(channelId, text, messageId) {
|
|
31767
|
+
this.spokenReplyIds.add(messageId);
|
|
31768
|
+
try {
|
|
31769
|
+
await this.send(channelId, text, messageId);
|
|
31770
|
+
} finally {
|
|
31771
|
+
this.spokenReplyIds.delete(messageId);
|
|
31772
|
+
}
|
|
31773
|
+
}
|
|
31694
31774
|
/** Speak a fixed acknowledgement line; failures are counted, never thrown. */
|
|
31695
31775
|
async sendFastLine(channelId, messageId, line) {
|
|
31696
31776
|
try {
|
|
31697
|
-
await this.
|
|
31777
|
+
await this.sendSpoken(channelId, line, fastReplyId(this.replyIdKey, this.cfg.identityId, channelId, messageId));
|
|
31698
31778
|
this.fastPathSent++;
|
|
31699
31779
|
return true;
|
|
31700
31780
|
} catch (e) {
|
|
@@ -31751,7 +31831,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31751
31831
|
const raw = await this.latency.time("voice.fast_llm", () => fastLlm.complete(messages));
|
|
31752
31832
|
const { reply, defer } = splitFastReply(raw, 200, maxSentences);
|
|
31753
31833
|
if (!reply) return "failed";
|
|
31754
|
-
await this.
|
|
31834
|
+
await this.sendSpoken(m.channelId, reply, fastReplyId(this.replyIdKey, this.cfg.identityId, m.channelId, m.messageId));
|
|
31755
31835
|
this.fastPathSent++;
|
|
31756
31836
|
if (defer) this.fastPathDeferred++;
|
|
31757
31837
|
return defer ? "deferred" : "answered";
|
|
@@ -31784,15 +31864,48 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31784
31864
|
}
|
|
31785
31865
|
}
|
|
31786
31866
|
/** Settle a held frame: the fast path answered alone (retire it) or the runtime answers next (release it, in channel order). */
|
|
31787
|
-
settleHeldRuntimeMessage(message, release) {
|
|
31867
|
+
async settleHeldRuntimeMessage(message, release) {
|
|
31788
31868
|
if (!this.runtimePendingIds.has(message.message_id)) return;
|
|
31789
31869
|
if (!release) {
|
|
31790
31870
|
this.ackRuntimeMessages([message.message_id]);
|
|
31791
31871
|
return;
|
|
31792
31872
|
}
|
|
31793
|
-
this.
|
|
31873
|
+
const memory = await this.memoryAtRelease(message);
|
|
31874
|
+
if (this.closingDown || !this.runtimePendingIds.has(message.message_id)) return;
|
|
31875
|
+
this.buffer.push(memory === message.memory ? message : { ...message, memory });
|
|
31794
31876
|
this.wakeListeners();
|
|
31795
31877
|
}
|
|
31878
|
+
/**
|
|
31879
|
+
* Voice repeats (b): a held frame's memory was read when its turn ARRIVED,
|
|
31880
|
+
* before the replies queued ahead of it (an earlier turn's fast reply, this
|
|
31881
|
+
* turn's own ack) were spoken. The runtime replaces its history with the
|
|
31882
|
+
* frame's memory, so with the arrival snapshot it answered an
|
|
31883
|
+
* already-answered turn again. At release the context is re-read and the
|
|
31884
|
+
* AGENT turns it holds beyond the snapshot (afterPublish ->
|
|
31885
|
+
* recordOutboundMemory) are appended to the snapshot. Peer turns recorded
|
|
31886
|
+
* since (this turn itself, later turns) are not, so the frame stays
|
|
31887
|
+
* prior-turn-only for what the runtime is about to answer. New turns are
|
|
31888
|
+
* found by recorded time, never by `seq` (the OpenClaw provider renumbers
|
|
31889
|
+
* seq per read). A failed re-read keeps the arrival snapshot.
|
|
31890
|
+
*/
|
|
31891
|
+
async memoryAtRelease(message) {
|
|
31892
|
+
const prior = message.memory;
|
|
31893
|
+
if (!prior || !this.mayReadOpenClawMemoryForTurn(message.authorization)) return prior;
|
|
31894
|
+
let fresh;
|
|
31895
|
+
try {
|
|
31896
|
+
fresh = await this.memory.context(message.channel_id, _BridgeAgent.TURN_MEMORY_LIMIT);
|
|
31897
|
+
} catch {
|
|
31898
|
+
return prior;
|
|
31899
|
+
}
|
|
31900
|
+
const after = prior.recent.reduce((max, turn) => Math.max(max, turn.ts ?? 0), message.ts);
|
|
31901
|
+
const spoken = fresh.recent.filter((turn) => turn.role === "agent" && turn.ts !== void 0 && turn.ts > after);
|
|
31902
|
+
if (spoken.length === 0) return prior;
|
|
31903
|
+
return {
|
|
31904
|
+
recent: [...prior.recent, ...spoken].slice(-_BridgeAgent.TURN_MEMORY_LIMIT),
|
|
31905
|
+
facts: prior.facts
|
|
31906
|
+
};
|
|
31907
|
+
}
|
|
31908
|
+
static TURN_MEMORY_LIMIT = 50;
|
|
31796
31909
|
/**
|
|
31797
31910
|
* List channels in one enclave (default: the daemon's own home enclave).
|
|
31798
31911
|
* Task 3: called per-*known* enclave by refreshChannelMeta so a
|
|
@@ -32054,10 +32167,22 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
32054
32167
|
* Missing or stale metadata is UNKNOWN with zero grants, never a permissive
|
|
32055
32168
|
* guess. The returned object and grant array are frozen before they cross the
|
|
32056
32169
|
* daemon boundary so one consumer cannot widen a later execution check.
|
|
32170
|
+
*
|
|
32171
|
+
* Grants (architect ruling 2026-10-02): OWNER_DM_DEFAULT_READ_GRANTS only when
|
|
32172
|
+
* the kill switch is on, the channel is a DM, and EITHER
|
|
32173
|
+
* (a) audience is owner_dm AND the sender is the pinned structural owner
|
|
32174
|
+
* (`channelOwner`, never "anomalous") — a lying `is_agent_owner` flag
|
|
32175
|
+
* alone gets nothing; OR
|
|
32176
|
+
* (b) the sender is this channel's server-marked relay (`is_relay`) and the
|
|
32177
|
+
* relayed `speaker_identity_id` (passed by the caller only for relay
|
|
32178
|
+
* turns) is that same pinned owner, whose roster row is the agent's
|
|
32179
|
+
* owner. A speaker claim from any non-relay sender gets nothing.
|
|
32180
|
+
* Everything else gets the frozen empty list.
|
|
32057
32181
|
*/
|
|
32058
|
-
authorizationFor(channelId, senderId) {
|
|
32182
|
+
authorizationFor(channelId, senderId, relaySpeakerId) {
|
|
32059
32183
|
const meta = this.freshMeta(channelId);
|
|
32060
|
-
const
|
|
32184
|
+
const roster = this.freshRoster(channelId);
|
|
32185
|
+
const sender = roster?.get(senderId);
|
|
32061
32186
|
let audience = "unknown";
|
|
32062
32187
|
if (meta && sender) {
|
|
32063
32188
|
if (sender.is_agent) audience = "agent_to_agent";
|
|
@@ -32066,11 +32191,71 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
32066
32191
|
else if (sender.is_partner) audience = "partner_room";
|
|
32067
32192
|
else audience = "team_room";
|
|
32068
32193
|
}
|
|
32194
|
+
let tool_grants = NO_TOOL_GRANTS;
|
|
32195
|
+
if (this.ownerDmDefaultGrants && meta?.is_dm === true && sender) {
|
|
32196
|
+
const pinned = this.channelOwner.get(channelId);
|
|
32197
|
+
const ownerId = pinned && pinned !== "anomalous" ? pinned.identityId : void 0;
|
|
32198
|
+
if (ownerId !== void 0) {
|
|
32199
|
+
const ownerDirect = audience === "owner_dm" && senderId === ownerId;
|
|
32200
|
+
const owner = roster?.get(ownerId);
|
|
32201
|
+
const ownerRelayed = relaySpeakerId !== void 0 && sender.is_relay && relaySpeakerId === ownerId && owner !== void 0 && !owner.is_agent && owner.is_agent_owner;
|
|
32202
|
+
if (ownerDirect || ownerRelayed) tool_grants = OWNER_DM_DEFAULT_READ_GRANTS;
|
|
32203
|
+
}
|
|
32204
|
+
}
|
|
32205
|
+
this.logAuthzDecision(channelId, senderId, relaySpeakerId, audience, tool_grants.length);
|
|
32069
32206
|
return Object.freeze({
|
|
32070
32207
|
version: 1,
|
|
32071
32208
|
audience,
|
|
32072
32209
|
sender_membership: sender ? "active" : "unknown",
|
|
32073
|
-
tool_grants
|
|
32210
|
+
tool_grants
|
|
32211
|
+
});
|
|
32212
|
+
}
|
|
32213
|
+
/**
|
|
32214
|
+
* One content-free INFO line per authorization decision (every call site of
|
|
32215
|
+
* authorizationFor stages exactly one runtime turn). It says WHY a turn got
|
|
32216
|
+
* the grants it got, without any identifier, name or text: every field is a
|
|
32217
|
+
* boolean, a count, or a closed-set label.
|
|
32218
|
+
*
|
|
32219
|
+
* owner_pin pinned | anomalous | missing (channelOwner cache)
|
|
32220
|
+
* owner_row_is_agent_owner the pinned owner's roster row; `absent` when
|
|
32221
|
+
* there is no pin or the row is not in the roster
|
|
32222
|
+
* sender_is_owner sender identity == pinned owner identity
|
|
32223
|
+
* relay sender's roster row has is_relay
|
|
32224
|
+
* relay_speaker_is_owner `na` unless a relay speaker was passed
|
|
32225
|
+
* kill_switch SECURECOMS_OWNER_DM_DEFAULT_GRANTS as read at
|
|
32226
|
+
* startup: `on` = default grants enabled
|
|
32227
|
+
* meta_fresh / roster_fresh whether the channel meta / roster caches were
|
|
32228
|
+
* fresh (either stale forces audience=unknown)
|
|
32229
|
+
*/
|
|
32230
|
+
logAuthzDecision(channelId, senderId, relaySpeakerId, audience, grants) {
|
|
32231
|
+
const meta = this.freshMeta(channelId);
|
|
32232
|
+
const roster = this.freshRoster(channelId);
|
|
32233
|
+
const sender = roster?.get(senderId);
|
|
32234
|
+
const pinned = this.channelOwner.get(channelId);
|
|
32235
|
+
const ownerId = pinned && pinned !== "anomalous" ? pinned.identityId : void 0;
|
|
32236
|
+
const ownerRow = ownerId !== void 0 ? roster?.get(ownerId) : void 0;
|
|
32237
|
+
const ownerPin = pinned === void 0 ? "missing" : pinned === "anomalous" ? "anomalous" : "pinned";
|
|
32238
|
+
const ownerRowIsAgentOwner = ownerRow === void 0 ? "absent" : ownerRow.is_agent_owner ? "true" : "false";
|
|
32239
|
+
const speakerIsOwner = relaySpeakerId === void 0 ? "na" : ownerId !== void 0 && relaySpeakerId === ownerId ? "true" : "false";
|
|
32240
|
+
writeServiceLog2("info", "authz.decision", {
|
|
32241
|
+
audience: ServiceLogEnum.of(audience, [
|
|
32242
|
+
"owner_dm",
|
|
32243
|
+
"other_human_dm",
|
|
32244
|
+
"team_room",
|
|
32245
|
+
"partner_room",
|
|
32246
|
+
"agent_to_agent",
|
|
32247
|
+
"unknown"
|
|
32248
|
+
]),
|
|
32249
|
+
grants,
|
|
32250
|
+
owner_pin: ServiceLogEnum.of(ownerPin, ["pinned", "anomalous", "missing"]),
|
|
32251
|
+
owner_row_is_agent_owner: ServiceLogEnum.of(ownerRowIsAgentOwner, ["true", "false", "absent"]),
|
|
32252
|
+
sender_is_owner: ownerId !== void 0 && senderId === ownerId,
|
|
32253
|
+
relay: sender?.is_relay === true,
|
|
32254
|
+
relay_speaker_is_owner: ServiceLogEnum.of(speakerIsOwner, ["true", "false", "na"]),
|
|
32255
|
+
is_dm: meta?.is_dm === true,
|
|
32256
|
+
kill_switch: ServiceLogEnum.of(this.ownerDmDefaultGrants ? "on" : "off", ["on", "off"]),
|
|
32257
|
+
meta_fresh: meta !== void 0,
|
|
32258
|
+
roster_fresh: roster !== void 0
|
|
32074
32259
|
});
|
|
32075
32260
|
}
|
|
32076
32261
|
/**
|
|
@@ -32578,11 +32763,36 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
32578
32763
|
this.ingestInFlight = entry;
|
|
32579
32764
|
return promise;
|
|
32580
32765
|
}
|
|
32766
|
+
/**
|
|
32767
|
+
* (4) recv.lag = server message created_at -> this pull's completion, per
|
|
32768
|
+
* application row, counted once per queue id. It makes the daemon-side
|
|
32769
|
+
* receive delay measurable (recv.pull alone times only the HTTP call).
|
|
32770
|
+
* Cross-clock: daemon-vs-server skew lands in the sample; a negative lag
|
|
32771
|
+
* is clamped to 0. The server serialises a naive UTC datetime (no offset),
|
|
32772
|
+
* which Date.parse would read as LOCAL time, so a bare timestamp is UTC.
|
|
32773
|
+
*/
|
|
32774
|
+
recordRecvLag(rows) {
|
|
32775
|
+
const now = Date.now();
|
|
32776
|
+
for (const row of rows) {
|
|
32777
|
+
if (row.message_type !== "application" || typeof row.queue_id !== "string") continue;
|
|
32778
|
+
if (typeof row.created_at !== "string" || this.recvLagSeen.has(row.queue_id)) continue;
|
|
32779
|
+
const iso = /(Z|[+-]\d{2}:?\d{2})$/i.test(row.created_at) ? row.created_at : `${row.created_at}Z`;
|
|
32780
|
+
const createdAt = Date.parse(iso);
|
|
32781
|
+
if (!Number.isFinite(createdAt)) continue;
|
|
32782
|
+
this.recvLagSeen.add(row.queue_id);
|
|
32783
|
+
if (this.recvLagSeen.size > _BridgeAgent.RECV_LAG_SEEN_CAP) {
|
|
32784
|
+
const oldest = this.recvLagSeen.values().next().value;
|
|
32785
|
+
if (oldest !== void 0) this.recvLagSeen.delete(oldest);
|
|
32786
|
+
}
|
|
32787
|
+
this.latency.record("recv.lag", Math.max(0, now - createdAt));
|
|
32788
|
+
}
|
|
32789
|
+
}
|
|
32581
32790
|
async ingestCycle() {
|
|
32582
32791
|
if (this.closingDown) return;
|
|
32583
32792
|
const cycleStart = performance.now();
|
|
32584
32793
|
try {
|
|
32585
32794
|
const rows = await this.latency.time("recv.pull", () => this.backend.pullDelivery(this.cfg.deviceId));
|
|
32795
|
+
this.recordRecvLag(rows);
|
|
32586
32796
|
const channelIds = /* @__PURE__ */ new Set();
|
|
32587
32797
|
for (const row of rows) {
|
|
32588
32798
|
if (row.connect_channel_id) channelIds.add(row.connect_channel_id);
|
|
@@ -32839,21 +33049,23 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
32839
33049
|
)?.name;
|
|
32840
33050
|
const turnAuthorization = this.authorizationFor(
|
|
32841
33051
|
m.channelId,
|
|
32842
|
-
m.envelope.sender_id
|
|
33052
|
+
m.envelope.sender_id,
|
|
33053
|
+
speaker?.identityId
|
|
32843
33054
|
);
|
|
32844
33055
|
const runtimeMemory = this.memoryForTurn(m.channelId, turnAuthorization);
|
|
32845
33056
|
const voiceGuidance = m.voiceMode ? selectVoiceGuidance(meta?.is_dm, m.voiceDefaultResponder) : void 0;
|
|
32846
33057
|
const humanTurn = senderIsAgent !== true || speaker !== void 0;
|
|
32847
33058
|
const fastPathEligible = Boolean(m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
|
|
33059
|
+
const turnText = parsed.text;
|
|
33060
|
+
const isCallStart = turnText.trim() === VOICE_CALL_START_MARKER;
|
|
33061
|
+
const coalescible = this.voiceCoalesceWindowMs > 0 && m.voiceMode === true && speaker !== void 0 && parsed.transcribed === true && voiceGuidance !== void 0 && !isCallStart;
|
|
32848
33062
|
let fastPathRoomBlocked = false;
|
|
32849
|
-
if (fastPathEligible && meta && meta.is_dm === false) {
|
|
33063
|
+
if (!coalescible && fastPathEligible && meta && meta.is_dm === false) {
|
|
32850
33064
|
const reserved = this.fastReplyReserved.get(m.channelId) ?? 0;
|
|
32851
33065
|
if (this.rateFloor.available(`room:${m.channelId}`) - reserved < 2) fastPathRoomBlocked = true;
|
|
32852
33066
|
}
|
|
32853
33067
|
if (fastPathRoomBlocked) this.fastPathSkipped++;
|
|
32854
33068
|
const fastPathFiring = fastPathEligible && !fastPathRoomBlocked && !this.pausedChannels.has(m.channelId);
|
|
32855
|
-
const turnText = parsed.text;
|
|
32856
|
-
const isCallStart = turnText.trim() === VOICE_CALL_START_MARKER;
|
|
32857
33069
|
const fastGreeting = isCallStart && fastPathFiring;
|
|
32858
33070
|
const runtimeVoice = (ackSent) => m.voiceMode ? {
|
|
32859
33071
|
voice: true,
|
|
@@ -32874,6 +33086,95 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
32874
33086
|
...speaker ? { speaker } : {},
|
|
32875
33087
|
...runtimeVoice(ackSent)
|
|
32876
33088
|
});
|
|
33089
|
+
if (coalescible) {
|
|
33090
|
+
if (this.voiceCoalescedIds.has(m.messageId) || this.runtimePendingIds.has(m.messageId)) {
|
|
33091
|
+
acked.push(m.queueId);
|
|
33092
|
+
continue;
|
|
33093
|
+
}
|
|
33094
|
+
const windowKey = `${m.channelId}\0${speaker.identityId}`;
|
|
33095
|
+
const open = this.voiceCoalescing.get(windowKey);
|
|
33096
|
+
if (open && !open.closed) {
|
|
33097
|
+
open.ids.push(m.messageId);
|
|
33098
|
+
open.parts.push(turnText);
|
|
33099
|
+
const staging = (async () => {
|
|
33100
|
+
const ok = await this.stageHeldRuntimeMessage(runtimeEntry(false, await runtimeMemory));
|
|
33101
|
+
if (!ok) {
|
|
33102
|
+
const i = open.ids.indexOf(m.messageId);
|
|
33103
|
+
if (i > 0) {
|
|
33104
|
+
open.ids.splice(i, 1);
|
|
33105
|
+
open.parts.splice(i, 1);
|
|
33106
|
+
}
|
|
33107
|
+
}
|
|
33108
|
+
return ok;
|
|
33109
|
+
})();
|
|
33110
|
+
open.pending = Promise.all([open.pending, staging]);
|
|
33111
|
+
if (!await staging) continue;
|
|
33112
|
+
this.noteVoiceCoalesced(m.messageId);
|
|
33113
|
+
acked.push(m.queueId);
|
|
33114
|
+
continue;
|
|
33115
|
+
}
|
|
33116
|
+
const held = runtimeEntry(false, await runtimeMemory);
|
|
33117
|
+
if (!await this.stageHeldRuntimeMessage(held)) continue;
|
|
33118
|
+
this.noteVoiceCoalesced(m.messageId);
|
|
33119
|
+
const win = {
|
|
33120
|
+
speakerId: speaker.identityId,
|
|
33121
|
+
ids: [m.messageId],
|
|
33122
|
+
parts: [turnText],
|
|
33123
|
+
closed: false,
|
|
33124
|
+
pending: Promise.resolve()
|
|
33125
|
+
};
|
|
33126
|
+
this.voiceCoalescing.set(windowKey, win);
|
|
33127
|
+
const openedAt = Date.now();
|
|
33128
|
+
const channelId = m.channelId;
|
|
33129
|
+
const senderId = m.envelope.sender_id;
|
|
33130
|
+
const isRoom = meta?.is_dm === false;
|
|
33131
|
+
this.enqueueFastReply(channelId, async () => {
|
|
33132
|
+
await waitMs(openedAt + this.voiceCoalesceWindowMs - Date.now());
|
|
33133
|
+
win.closed = true;
|
|
33134
|
+
if (this.voiceCoalescing.get(windowKey) === win) this.voiceCoalescing.delete(windowKey);
|
|
33135
|
+
await win.pending;
|
|
33136
|
+
if (this.closingDown) return;
|
|
33137
|
+
const [firstId, ...tailIds] = win.ids;
|
|
33138
|
+
if (tailIds.length) this.voiceCoalescedTails.set(firstId, tailIds);
|
|
33139
|
+
const text = win.parts.join(" ");
|
|
33140
|
+
const merged = { ...held, text };
|
|
33141
|
+
void recordInboundMemory(this.memory, channelId, senderId, text, firstId, "voice");
|
|
33142
|
+
let roomBlocked = false;
|
|
33143
|
+
if (fastPathEligible && isRoom) {
|
|
33144
|
+
const reserved = this.fastReplyReserved.get(channelId) ?? 0;
|
|
33145
|
+
if (this.rateFloor.available(`room:${channelId}`) - reserved < 2) roomBlocked = true;
|
|
33146
|
+
}
|
|
33147
|
+
if (roomBlocked) this.fastPathSkipped++;
|
|
33148
|
+
const plan = fastPathEligible && !roomBlocked && !this.pausedChannels.has(channelId) ? this.planFastPath(firstId, text, false) : void 0;
|
|
33149
|
+
if (!plan || plan.kind === "dup" || plan.kind === "skip") {
|
|
33150
|
+
if (plan?.kind === "skip" && plan.full) this.fastPathSkipped++;
|
|
33151
|
+
await this.settleHeldRuntimeMessage(merged, true);
|
|
33152
|
+
return;
|
|
33153
|
+
}
|
|
33154
|
+
this.markFastPathSeen(firstId);
|
|
33155
|
+
if (plan.kind === "ack") {
|
|
33156
|
+
this.fastPathDeferred++;
|
|
33157
|
+
if (plan.intent) this.fastPathIntentDeferred++;
|
|
33158
|
+
}
|
|
33159
|
+
const release = this.reserveFastReply(channelId);
|
|
33160
|
+
try {
|
|
33161
|
+
await this.speakAndSettle(
|
|
33162
|
+
{ channelId, messageId: firstId, senderId, authorization: turnAuthorization },
|
|
33163
|
+
plan,
|
|
33164
|
+
merged,
|
|
33165
|
+
{ ...merged, ...runtimeVoice(true) },
|
|
33166
|
+
text,
|
|
33167
|
+
voiceGuidance,
|
|
33168
|
+
speaker,
|
|
33169
|
+
false
|
|
33170
|
+
);
|
|
33171
|
+
} finally {
|
|
33172
|
+
release();
|
|
33173
|
+
}
|
|
33174
|
+
}, false);
|
|
33175
|
+
acked.push(m.queueId);
|
|
33176
|
+
continue;
|
|
33177
|
+
}
|
|
32877
33178
|
if (fastPathFiring) {
|
|
32878
33179
|
const fastSpeaker = speaker ?? (senderName ? { identityId: m.envelope.sender_id, displayName: senderName } : void 0);
|
|
32879
33180
|
const fastText = fastGreeting ? greetingTurnFor(this.voiceSettings ?? DEFAULT_VOICE_SETTINGS, fastSpeaker?.displayName) : parsed.text;
|
|
@@ -32899,11 +33200,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
32899
33200
|
this.fastPathDeferred++;
|
|
32900
33201
|
if (ack.intent) this.fastPathIntentDeferred++;
|
|
32901
33202
|
}
|
|
32902
|
-
|
|
32903
|
-
|
|
32904
|
-
|
|
32905
|
-
|
|
32906
|
-
|
|
33203
|
+
const heldAfterAck = { ...held, ...runtimeVoice(true) };
|
|
33204
|
+
this.enqueueFastReply(
|
|
33205
|
+
m.channelId,
|
|
33206
|
+
() => this.speakAndSettle(turn, ack ?? plan, held, heldAfterAck, fastText, guidanceForFast, fastSpeaker, greeting)
|
|
33207
|
+
);
|
|
32907
33208
|
}
|
|
32908
33209
|
} else {
|
|
32909
33210
|
if (!await this.stageRuntimeMessageInOrder(runtimeEntry(false, await runtimeMemory))) continue;
|
|
@@ -33324,6 +33625,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
33324
33625
|
this.deliveryAttempts.delete(messageId);
|
|
33325
33626
|
this.redeliverNotBefore.delete(messageId);
|
|
33326
33627
|
this.supersedeRefundable.delete(messageId);
|
|
33628
|
+
const tails = this.voiceCoalescedTails.get(messageId);
|
|
33629
|
+
if (tails) {
|
|
33630
|
+
this.voiceCoalescedTails.delete(messageId);
|
|
33631
|
+
this.ackRuntimeMessages(tails);
|
|
33632
|
+
}
|
|
33327
33633
|
}
|
|
33328
33634
|
}
|
|
33329
33635
|
async restoreRuntimeInbox() {
|
package/dist/memory-taps.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ interface MemorySink {
|
|
|
12
12
|
* for a turn whose InboxEntry had the server-set `voiceMode` flag, so memory
|
|
13
13
|
* continuity reflects how the turn actually arrived. */
|
|
14
14
|
export declare function recordInboundMemory(sink: MemorySink, channelId: string, senderId: string, text: string, messageId: string, modality?: "text" | "voice"): Promise<void>;
|
|
15
|
-
/** Record the agent's own outbound
|
|
16
|
-
|
|
15
|
+
/** Record the agent's own outbound reply. Best-effort. `modality` is "voice"
|
|
16
|
+
* for a line the voice fast path spoke (H1), else "text". */
|
|
17
|
+
export declare function recordOutboundMemory(sink: MemorySink, channelId: string, text: string, messageId: string, modality?: "text" | "voice"): Promise<void>;
|
|
17
18
|
export {};
|
package/dist/service-log.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
export type ServiceLogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* A closed-set label for a service-log field. The only way to build one is
|
|
4
|
+
* `ServiceLogEnum.of(value, allowed)`: a value outside `allowed`, or one that
|
|
5
|
+
* is not a short lowercase word, renders as `invalid`. Raw strings are still
|
|
6
|
+
* dropped by `formatServiceLog`, so an identifier, name or message text can
|
|
7
|
+
* never reach the log through a field. The privacy test requires `allowed` to
|
|
8
|
+
* be an inline array of string literals at every call site.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ServiceLogEnum {
|
|
11
|
+
readonly value: string;
|
|
12
|
+
private constructor();
|
|
13
|
+
static of<T extends string>(value: T, allowed: readonly T[]): ServiceLogEnum;
|
|
14
|
+
}
|
|
15
|
+
export type ServiceLogFields = Readonly<Record<string, number | boolean | ServiceLogEnum>>;
|
|
3
16
|
export type ServiceLog = (level: ServiceLogLevel, event: string, fields?: ServiceLogFields, error?: unknown) => void;
|
|
4
17
|
/** Reduce a thrown value without reading its message, stack, or string form. */
|
|
5
18
|
export declare function errorClass(error: unknown): string;
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const BRIDGE_VERSION = "0.26.
|
|
1
|
+
export declare const BRIDGE_VERSION = "0.26.3";
|