@securecoms/agent-bridge 0.25.2 → 0.25.4
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 +121 -9
- package/dist/bridge-agent.d.ts +26 -0
- package/dist/index.js +121 -9
- package/dist/status-gate.d.ts +45 -0
- package/dist/version.d.ts +1 -1
- package/dist/voice/fast-prompt.d.ts +3 -0
- package/dist/voice/persona.d.ts +7 -1
- package/dist/voice/prompt-compiler.d.ts +8 -1
- package/dist/voice/settings.d.ts +6 -0
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -7436,10 +7436,10 @@ var HANDOFF = {
|
|
|
7436
7436
|
balanced: "If the message needs files, tools, memory lookups, current data, or any work you cannot do instantly, say in one sentence that you are checking and end your reply with the exact token [[DEFER]] so the full agent answers next. For greetings, small talk, and anything you can answer completely yourself, answer completely and do NOT add the token.",
|
|
7437
7437
|
self: "Answer yourself whenever you reasonably can. Only when the message clearly needs files, tools, or current data you cannot see, say in one sentence that you are checking and end your reply with the exact token [[DEFER]]."
|
|
7438
7438
|
};
|
|
7439
|
-
function compileVoicePrompt(s) {
|
|
7439
|
+
function compileVoicePrompt(s, agentName) {
|
|
7440
7440
|
const parts = [];
|
|
7441
7441
|
const persona = s.persona.trim();
|
|
7442
|
-
const displayName =
|
|
7442
|
+
const displayName = (agentName ?? "").trim();
|
|
7443
7443
|
if (displayName && !persona.toLowerCase().includes(displayName.toLowerCase())) {
|
|
7444
7444
|
parts.push(`You are ${displayName}.`);
|
|
7445
7445
|
}
|
|
@@ -7491,6 +7491,9 @@ var Row = z.object({
|
|
|
7491
7491
|
voice_id: z.string().nullable().optional(),
|
|
7492
7492
|
pace: z.enum(PACES).default("normal"),
|
|
7493
7493
|
display_name: z.string().max(40).default(""),
|
|
7494
|
+
// Owner decision 2026-10-01 ("one name"). Lenient: a bad value is dropped
|
|
7495
|
+
// (see identityName below), never a reason to reject the whole row.
|
|
7496
|
+
identity_display_name: z.unknown().optional(),
|
|
7494
7497
|
persona: z.string().max(600).default(""),
|
|
7495
7498
|
warmth: z.enum(WARMTHS).default("friendly"),
|
|
7496
7499
|
formality: z.enum(FORMALITIES).default("neutral"),
|
|
@@ -7501,8 +7504,14 @@ var Row = z.object({
|
|
|
7501
7504
|
avoid_topics: z.array(z.string().max(80)).max(10).default([]),
|
|
7502
7505
|
handoff: z.enum(HANDOFFS).default("balanced")
|
|
7503
7506
|
}).passthrough();
|
|
7507
|
+
function identityName(v) {
|
|
7508
|
+
if (typeof v !== "string") return void 0;
|
|
7509
|
+
const name = v.trim();
|
|
7510
|
+
return name && name.length <= 200 ? name : void 0;
|
|
7511
|
+
}
|
|
7504
7512
|
function parseVoiceSettings(raw) {
|
|
7505
7513
|
const r = Row.parse(raw);
|
|
7514
|
+
const identityDisplayName = identityName(r.identity_display_name);
|
|
7506
7515
|
return {
|
|
7507
7516
|
version: r.version,
|
|
7508
7517
|
voiceId: r.voice_id ?? null,
|
|
@@ -7516,7 +7525,8 @@ function parseVoiceSettings(raw) {
|
|
|
7516
7525
|
quirks: r.quirks,
|
|
7517
7526
|
greetingStyle: r.greeting_style,
|
|
7518
7527
|
avoidTopics: r.avoid_topics,
|
|
7519
|
-
handoff: r.handoff
|
|
7528
|
+
handoff: r.handoff,
|
|
7529
|
+
...identityDisplayName ? { identityDisplayName } : {}
|
|
7520
7530
|
};
|
|
7521
7531
|
}
|
|
7522
7532
|
function settingsToVoiceConfig(s, proxy) {
|
|
@@ -7577,10 +7587,11 @@ function parseVoiceConfig(raw, env) {
|
|
|
7577
7587
|
}
|
|
7578
7588
|
};
|
|
7579
7589
|
}
|
|
7580
|
-
function systemPrompt(p, settings) {
|
|
7590
|
+
function systemPrompt(p, settings, agentName) {
|
|
7581
7591
|
const core = p.persona.trim().endsWith(CONTINUITY_CLAUSE) ? p.persona.trim().slice(0, -CONTINUITY_CLAUSE.length).trim() : p.persona.trim();
|
|
7582
7592
|
return compileVoicePrompt(
|
|
7583
|
-
settings ?? { ...DEFAULT_VOICE_SETTINGS,
|
|
7593
|
+
settings ?? { ...DEFAULT_VOICE_SETTINGS, persona: core, quirks: p.quirks },
|
|
7594
|
+
agentName
|
|
7584
7595
|
);
|
|
7585
7596
|
}
|
|
7586
7597
|
|
|
@@ -28350,7 +28361,7 @@ function clampContent(content, max = MAX_MESSAGE_CHARS) {
|
|
|
28350
28361
|
}
|
|
28351
28362
|
function buildFastMessages(args2) {
|
|
28352
28363
|
const cap = args2.charCap ?? 4e3;
|
|
28353
|
-
const sp = systemPrompt(args2.persona, args2.settings);
|
|
28364
|
+
const sp = systemPrompt(args2.persona, args2.settings, args2.agentName);
|
|
28354
28365
|
const head = sp.slice(0, sp.length - CONTINUITY_CLAUSE.length).trimEnd();
|
|
28355
28366
|
const system = `${head} ${args2.guidance.trim()} ${CONTINUITY_CLAUSE}`;
|
|
28356
28367
|
const recent = args2.memory.recent.map((t) => ({
|
|
@@ -28827,6 +28838,70 @@ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
|
|
|
28827
28838
|
|
|
28828
28839
|
// src/bridge-agent.ts
|
|
28829
28840
|
import { homedir as homedir2 } from "node:os";
|
|
28841
|
+
|
|
28842
|
+
// src/status-gate.ts
|
|
28843
|
+
var STATUS_WORKING_DELAY_MS = 3e3;
|
|
28844
|
+
var StatusGate = class {
|
|
28845
|
+
constructor(publish, opts = {}) {
|
|
28846
|
+
this.publish = publish;
|
|
28847
|
+
this.opts = opts;
|
|
28848
|
+
}
|
|
28849
|
+
entries = /* @__PURE__ */ new Map();
|
|
28850
|
+
chains = /* @__PURE__ */ new Map();
|
|
28851
|
+
/** The agent reported a status. Resolves once any publish it starts has settled. */
|
|
28852
|
+
async report(channelId, state, detail) {
|
|
28853
|
+
if (state === "working") {
|
|
28854
|
+
this.holdWorking(channelId, detail);
|
|
28855
|
+
return;
|
|
28856
|
+
}
|
|
28857
|
+
const entry = this.entries.get(channelId);
|
|
28858
|
+
this.entries.delete(channelId);
|
|
28859
|
+
if (entry?.timer) clearTimeout(entry.timer);
|
|
28860
|
+
if (state === "idle" && entry && !entry.published) return;
|
|
28861
|
+
await this.enqueue(channelId, state, detail);
|
|
28862
|
+
}
|
|
28863
|
+
/** A reply body is about to publish on the channel. */
|
|
28864
|
+
replyStarted(channelId) {
|
|
28865
|
+
const entry = this.entries.get(channelId);
|
|
28866
|
+
if (!entry?.timer) return;
|
|
28867
|
+
clearTimeout(entry.timer);
|
|
28868
|
+
entry.timer = null;
|
|
28869
|
+
}
|
|
28870
|
+
/** Drop every pending "working" (bridge shutdown). */
|
|
28871
|
+
close() {
|
|
28872
|
+
for (const entry of this.entries.values()) if (entry.timer) clearTimeout(entry.timer);
|
|
28873
|
+
this.entries.clear();
|
|
28874
|
+
}
|
|
28875
|
+
holdWorking(channelId, detail) {
|
|
28876
|
+
const existing = this.entries.get(channelId);
|
|
28877
|
+
if (existing && (existing.timer || existing.published)) return;
|
|
28878
|
+
const entry = { timer: null, published: false };
|
|
28879
|
+
this.entries.set(channelId, entry);
|
|
28880
|
+
entry.timer = setTimeout(() => {
|
|
28881
|
+
entry.timer = null;
|
|
28882
|
+
if (this.entries.get(channelId) !== entry) return;
|
|
28883
|
+
entry.published = true;
|
|
28884
|
+
this.enqueue(channelId, "working", detail).catch(() => {
|
|
28885
|
+
});
|
|
28886
|
+
}, this.opts.delayMs ?? STATUS_WORKING_DELAY_MS);
|
|
28887
|
+
entry.timer.unref?.();
|
|
28888
|
+
}
|
|
28889
|
+
/** Publishes in order per channel; rejects with the publish error. */
|
|
28890
|
+
enqueue(channelId, state, detail) {
|
|
28891
|
+
const prev = this.chains.get(channelId) ?? Promise.resolve();
|
|
28892
|
+
const run = prev.then(() => this.publish(channelId, state, detail));
|
|
28893
|
+
const settled = run.catch((err) => {
|
|
28894
|
+
this.opts.onError?.(state, err);
|
|
28895
|
+
});
|
|
28896
|
+
this.chains.set(channelId, settled);
|
|
28897
|
+
void settled.then(() => {
|
|
28898
|
+
if (this.chains.get(channelId) === settled) this.chains.delete(channelId);
|
|
28899
|
+
});
|
|
28900
|
+
return run;
|
|
28901
|
+
}
|
|
28902
|
+
};
|
|
28903
|
+
|
|
28904
|
+
// src/bridge-agent.ts
|
|
28830
28905
|
function logSendSupersededAdoption(kind) {
|
|
28831
28906
|
switch (kind) {
|
|
28832
28907
|
case "adopted":
|
|
@@ -29250,8 +29325,23 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
29250
29325
|
* `roomDisplayName`. Kept across a later failed refresh, so a transient
|
|
29251
29326
|
* members-fetch error does not make the agent deaf to its own name; a
|
|
29252
29327
|
* successful refresh after a rename replaces it.
|
|
29328
|
+
*
|
|
29329
|
+
* 2026-10-01 ("one name"): also the name the voice fast path speaks as
|
|
29330
|
+
* ("You are <name>."). A voice settings fetch that carries
|
|
29331
|
+
* `identity_display_name` writes it too (the backend pings
|
|
29332
|
+
* `voice_settings_changed` on a rename), so the latest observation from
|
|
29333
|
+
* either source wins; both read the same identity column.
|
|
29253
29334
|
*/
|
|
29254
29335
|
lastOwnDisplayName;
|
|
29336
|
+
/**
|
|
29337
|
+
* Thinking-indicator gate (owner rule, 2026-10-01): the agent-facing status
|
|
29338
|
+
* routes go through it, so a reply inside 3 s raises no "thinking..." strip.
|
|
29339
|
+
* See status-gate.ts.
|
|
29340
|
+
*/
|
|
29341
|
+
statusGate = new StatusGate(
|
|
29342
|
+
(channelId, state, detail) => this.reportStatus(channelId, state, detail),
|
|
29343
|
+
{ onError: (state) => writeServiceLog("warn", "status.publish_failed", { working: state === "working", idle: state === "idle" }) }
|
|
29344
|
+
);
|
|
29255
29345
|
/**
|
|
29256
29346
|
* Task 8 (owner-attested Reset Secure Session): per-channel STRUCTURAL owner
|
|
29257
29347
|
* cache, fed by resolveAndPinOwner() on every roster refresh and consumed by
|
|
@@ -29653,6 +29743,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
29653
29743
|
const res = await this.fetchBounded("voice_settings", "read", url, { headers: { authorization: `Bearer ${this.cfg.apiKey}` } });
|
|
29654
29744
|
if (!res.ok) throw new Error(`voice-settings ${res.status}`);
|
|
29655
29745
|
const s = parseVoiceSettings(await res.json());
|
|
29746
|
+
if (s.identityDisplayName) this.lastOwnDisplayName = s.identityDisplayName;
|
|
29656
29747
|
const proxy = settingsToVoiceConfig(s, { baseUrl: this.cfg.apiBase, apiKey: this.cfg.apiKey });
|
|
29657
29748
|
if (s.version === 0 && this.fileVoiceConfig) {
|
|
29658
29749
|
const merged = {
|
|
@@ -30782,6 +30873,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
30782
30873
|
*/
|
|
30783
30874
|
async publishBody(channelId, body, id, onEnvelope) {
|
|
30784
30875
|
this.enforceBodyRateFloor(channelId, body);
|
|
30876
|
+
this.statusGate.replyStarted(channelId);
|
|
30785
30877
|
const joined = await this.reconcileJoin(channelId);
|
|
30786
30878
|
if (!joined) throw new ChannelPendingError(channelId);
|
|
30787
30879
|
const sendStart = performance.now();
|
|
@@ -31550,7 +31642,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31550
31642
|
memory,
|
|
31551
31643
|
speaker,
|
|
31552
31644
|
text,
|
|
31553
|
-
settings: this.voiceSettings ?? void 0
|
|
31645
|
+
settings: this.voiceSettings ?? void 0,
|
|
31646
|
+
agentName: this.voiceAgentName()
|
|
31554
31647
|
});
|
|
31555
31648
|
const maxSentences = this.voiceSettings?.brevity === "one" ? 1 : 2;
|
|
31556
31649
|
const { reply, defer } = splitFastReply(await this.fastLlm.complete(messages), 200, maxSentences);
|
|
@@ -31894,6 +31987,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31894
31987
|
if (TRACE) writeServiceLog("debug", "identity.owner_pin_resolved");
|
|
31895
31988
|
return { identityId: cached.identityId, publicKeyHex: pin.publicKeyHex };
|
|
31896
31989
|
}
|
|
31990
|
+
/**
|
|
31991
|
+
* The agent-facing status entry (HTTP /v1/status, WS "status"): "working"
|
|
31992
|
+
* is held 3 s and dropped if the turn ends or a reply starts first. See
|
|
31993
|
+
* status-gate.ts. Resolves at once for "working".
|
|
31994
|
+
*/
|
|
31995
|
+
async reportStatusGated(channelId, state, detail) {
|
|
31996
|
+
await this.statusGate.report(channelId, state, detail);
|
|
31997
|
+
}
|
|
31897
31998
|
async reportStatus(channelId, state, detail) {
|
|
31898
31999
|
const joined = await this.reconcileJoin(channelId);
|
|
31899
32000
|
if (!joined) throw new ChannelPendingError(channelId);
|
|
@@ -32857,6 +32958,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
32857
32958
|
ownRoomDisplayName() {
|
|
32858
32959
|
return this.lastOwnDisplayName;
|
|
32859
32960
|
}
|
|
32961
|
+
/**
|
|
32962
|
+
* 2026-10-01 (owner decision, "one name"): the name the voice prompt uses.
|
|
32963
|
+
* The agent's CURRENT identity display name (roster or voice settings
|
|
32964
|
+
* fetch, whichever saw it last); before either has, the credential's
|
|
32965
|
+
* agent_name (the same identity name as of credential issue). Never the
|
|
32966
|
+
* legacy voice settings `display_name` or the file `voice.persona.display_name`.
|
|
32967
|
+
*/
|
|
32968
|
+
voiceAgentName() {
|
|
32969
|
+
return this.lastOwnDisplayName ?? this.cfg.agentName;
|
|
32970
|
+
}
|
|
32860
32971
|
/** Phase 3 F3: the loop guard's view of the sender — server roster first. */
|
|
32861
32972
|
loopSenderIsAgent(channelId, senderId, envelopeSaysAgent, relayedHumanTurn) {
|
|
32862
32973
|
if (relayedHumanTurn) return false;
|
|
@@ -33188,6 +33299,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
33188
33299
|
}
|
|
33189
33300
|
async close() {
|
|
33190
33301
|
this.closingDown = true;
|
|
33302
|
+
this.statusGate.close();
|
|
33191
33303
|
for (const t of this.pauseRecheckTimers.values()) clearTimeout(t);
|
|
33192
33304
|
this.pauseRecheckTimers.clear();
|
|
33193
33305
|
for (const t of this.supersedeRecheckTimers.values()) clearTimeout(t);
|
|
@@ -33534,7 +33646,7 @@ function createHttpServer(agent, opts) {
|
|
|
33534
33646
|
if (method === "POST" && path === "/v1/status") {
|
|
33535
33647
|
const parsed = statusSchema.safeParse(JSON.parse(await readBody(req, OTHER_CAP)));
|
|
33536
33648
|
if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
|
|
33537
|
-
await agent.
|
|
33649
|
+
await agent.reportStatusGated(parsed.data.channel_id, parsed.data.state, parsed.data.detail);
|
|
33538
33650
|
return json(res, 200, { ok: true });
|
|
33539
33651
|
}
|
|
33540
33652
|
if (method === "POST" && path === "/v1/memory/record") {
|
|
@@ -33904,7 +34016,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
33904
34016
|
}
|
|
33905
34017
|
const detail = typeof m.detail === "string" ? m.detail : void 0;
|
|
33906
34018
|
try {
|
|
33907
|
-
await agent.
|
|
34019
|
+
await agent.reportStatusGated(m.channel_id, m.state, detail);
|
|
33908
34020
|
sendJson(sock, { type: "ok" });
|
|
33909
34021
|
} catch (e) {
|
|
33910
34022
|
if (e instanceof ChannelPendingError) {
|
package/dist/bridge-agent.d.ts
CHANGED
|
@@ -567,8 +567,20 @@ export declare class BridgeAgent {
|
|
|
567
567
|
* `roomDisplayName`. Kept across a later failed refresh, so a transient
|
|
568
568
|
* members-fetch error does not make the agent deaf to its own name; a
|
|
569
569
|
* successful refresh after a rename replaces it.
|
|
570
|
+
*
|
|
571
|
+
* 2026-10-01 ("one name"): also the name the voice fast path speaks as
|
|
572
|
+
* ("You are <name>."). A voice settings fetch that carries
|
|
573
|
+
* `identity_display_name` writes it too (the backend pings
|
|
574
|
+
* `voice_settings_changed` on a rename), so the latest observation from
|
|
575
|
+
* either source wins; both read the same identity column.
|
|
570
576
|
*/
|
|
571
577
|
private lastOwnDisplayName;
|
|
578
|
+
/**
|
|
579
|
+
* Thinking-indicator gate (owner rule, 2026-10-01): the agent-facing status
|
|
580
|
+
* routes go through it, so a reply inside 3 s raises no "thinking..." strip.
|
|
581
|
+
* See status-gate.ts.
|
|
582
|
+
*/
|
|
583
|
+
private readonly statusGate;
|
|
572
584
|
/**
|
|
573
585
|
* Task 8 (owner-attested Reset Secure Session): per-channel STRUCTURAL owner
|
|
574
586
|
* cache, fed by resolveAndPinOwner() on every roster refresh and consumed by
|
|
@@ -1813,6 +1825,12 @@ export declare class BridgeAgent {
|
|
|
1813
1825
|
* never trusted on its own, it is always reconfirmed against the store.
|
|
1814
1826
|
*/
|
|
1815
1827
|
private resolvePinnedOwnerHook;
|
|
1828
|
+
/**
|
|
1829
|
+
* The agent-facing status entry (HTTP /v1/status, WS "status"): "working"
|
|
1830
|
+
* is held 3 s and dropped if the turn ends or a reply starts first. See
|
|
1831
|
+
* status-gate.ts. Resolves at once for "working".
|
|
1832
|
+
*/
|
|
1833
|
+
reportStatusGated(channelId: string, state: "idle" | "working" | "stalled" | "error", detail?: string): Promise<void>;
|
|
1816
1834
|
reportStatus(channelId: string, state: "idle" | "working" | "stalled" | "error", detail?: string): Promise<void>;
|
|
1817
1835
|
/** `runId` must exist and not have settled yet — settle() is terminal. */
|
|
1818
1836
|
private requireActionRun;
|
|
@@ -2001,6 +2019,14 @@ export declare class BridgeAgent {
|
|
|
2001
2019
|
* the identity id.
|
|
2002
2020
|
*/
|
|
2003
2021
|
private ownRoomDisplayName;
|
|
2022
|
+
/**
|
|
2023
|
+
* 2026-10-01 (owner decision, "one name"): the name the voice prompt uses.
|
|
2024
|
+
* The agent's CURRENT identity display name (roster or voice settings
|
|
2025
|
+
* fetch, whichever saw it last); before either has, the credential's
|
|
2026
|
+
* agent_name (the same identity name as of credential issue). Never the
|
|
2027
|
+
* legacy voice settings `display_name` or the file `voice.persona.display_name`.
|
|
2028
|
+
*/
|
|
2029
|
+
private voiceAgentName;
|
|
2004
2030
|
/** Phase 3 F3: the loop guard's view of the sender — server roster first. */
|
|
2005
2031
|
private loopSenderIsAgent;
|
|
2006
2032
|
private noteAgentTurnSuppressed;
|
package/dist/index.js
CHANGED
|
@@ -28041,10 +28041,10 @@ var HANDOFF = {
|
|
|
28041
28041
|
balanced: "If the message needs files, tools, memory lookups, current data, or any work you cannot do instantly, say in one sentence that you are checking and end your reply with the exact token [[DEFER]] so the full agent answers next. For greetings, small talk, and anything you can answer completely yourself, answer completely and do NOT add the token.",
|
|
28042
28042
|
self: "Answer yourself whenever you reasonably can. Only when the message clearly needs files, tools, or current data you cannot see, say in one sentence that you are checking and end your reply with the exact token [[DEFER]]."
|
|
28043
28043
|
};
|
|
28044
|
-
function compileVoicePrompt(s) {
|
|
28044
|
+
function compileVoicePrompt(s, agentName) {
|
|
28045
28045
|
const parts = [];
|
|
28046
28046
|
const persona = s.persona.trim();
|
|
28047
|
-
const displayName =
|
|
28047
|
+
const displayName = (agentName ?? "").trim();
|
|
28048
28048
|
if (displayName && !persona.toLowerCase().includes(displayName.toLowerCase())) {
|
|
28049
28049
|
parts.push(`You are ${displayName}.`);
|
|
28050
28050
|
}
|
|
@@ -28096,6 +28096,9 @@ var Row = z.object({
|
|
|
28096
28096
|
voice_id: z.string().nullable().optional(),
|
|
28097
28097
|
pace: z.enum(PACES).default("normal"),
|
|
28098
28098
|
display_name: z.string().max(40).default(""),
|
|
28099
|
+
// Owner decision 2026-10-01 ("one name"). Lenient: a bad value is dropped
|
|
28100
|
+
// (see identityName below), never a reason to reject the whole row.
|
|
28101
|
+
identity_display_name: z.unknown().optional(),
|
|
28099
28102
|
persona: z.string().max(600).default(""),
|
|
28100
28103
|
warmth: z.enum(WARMTHS).default("friendly"),
|
|
28101
28104
|
formality: z.enum(FORMALITIES).default("neutral"),
|
|
@@ -28106,8 +28109,14 @@ var Row = z.object({
|
|
|
28106
28109
|
avoid_topics: z.array(z.string().max(80)).max(10).default([]),
|
|
28107
28110
|
handoff: z.enum(HANDOFFS).default("balanced")
|
|
28108
28111
|
}).passthrough();
|
|
28112
|
+
function identityName(v) {
|
|
28113
|
+
if (typeof v !== "string") return void 0;
|
|
28114
|
+
const name = v.trim();
|
|
28115
|
+
return name && name.length <= 200 ? name : void 0;
|
|
28116
|
+
}
|
|
28109
28117
|
function parseVoiceSettings(raw) {
|
|
28110
28118
|
const r = Row.parse(raw);
|
|
28119
|
+
const identityDisplayName = identityName(r.identity_display_name);
|
|
28111
28120
|
return {
|
|
28112
28121
|
version: r.version,
|
|
28113
28122
|
voiceId: r.voice_id ?? null,
|
|
@@ -28121,7 +28130,8 @@ function parseVoiceSettings(raw) {
|
|
|
28121
28130
|
quirks: r.quirks,
|
|
28122
28131
|
greetingStyle: r.greeting_style,
|
|
28123
28132
|
avoidTopics: r.avoid_topics,
|
|
28124
|
-
handoff: r.handoff
|
|
28133
|
+
handoff: r.handoff,
|
|
28134
|
+
...identityDisplayName ? { identityDisplayName } : {}
|
|
28125
28135
|
};
|
|
28126
28136
|
}
|
|
28127
28137
|
function settingsToVoiceConfig(s, proxy) {
|
|
@@ -28182,10 +28192,11 @@ function parseVoiceConfig(raw, env) {
|
|
|
28182
28192
|
}
|
|
28183
28193
|
};
|
|
28184
28194
|
}
|
|
28185
|
-
function systemPrompt(p, settings) {
|
|
28195
|
+
function systemPrompt(p, settings, agentName) {
|
|
28186
28196
|
const core = p.persona.trim().endsWith(CONTINUITY_CLAUSE) ? p.persona.trim().slice(0, -CONTINUITY_CLAUSE.length).trim() : p.persona.trim();
|
|
28187
28197
|
return compileVoicePrompt(
|
|
28188
|
-
settings ?? { ...DEFAULT_VOICE_SETTINGS,
|
|
28198
|
+
settings ?? { ...DEFAULT_VOICE_SETTINGS, persona: core, quirks: p.quirks },
|
|
28199
|
+
agentName
|
|
28189
28200
|
);
|
|
28190
28201
|
}
|
|
28191
28202
|
|
|
@@ -28199,7 +28210,7 @@ function clampContent(content, max = MAX_MESSAGE_CHARS) {
|
|
|
28199
28210
|
}
|
|
28200
28211
|
function buildFastMessages(args2) {
|
|
28201
28212
|
const cap = args2.charCap ?? 4e3;
|
|
28202
|
-
const sp = systemPrompt(args2.persona, args2.settings);
|
|
28213
|
+
const sp = systemPrompt(args2.persona, args2.settings, args2.agentName);
|
|
28203
28214
|
const head = sp.slice(0, sp.length - CONTINUITY_CLAUSE.length).trimEnd();
|
|
28204
28215
|
const system = `${head} ${args2.guidance.trim()} ${CONTINUITY_CLAUSE}`;
|
|
28205
28216
|
const recent = args2.memory.recent.map((t) => ({
|
|
@@ -28676,6 +28687,70 @@ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
|
|
|
28676
28687
|
|
|
28677
28688
|
// src/bridge-agent.ts
|
|
28678
28689
|
import { homedir as homedir2 } from "node:os";
|
|
28690
|
+
|
|
28691
|
+
// src/status-gate.ts
|
|
28692
|
+
var STATUS_WORKING_DELAY_MS = 3e3;
|
|
28693
|
+
var StatusGate = class {
|
|
28694
|
+
constructor(publish, opts = {}) {
|
|
28695
|
+
this.publish = publish;
|
|
28696
|
+
this.opts = opts;
|
|
28697
|
+
}
|
|
28698
|
+
entries = /* @__PURE__ */ new Map();
|
|
28699
|
+
chains = /* @__PURE__ */ new Map();
|
|
28700
|
+
/** The agent reported a status. Resolves once any publish it starts has settled. */
|
|
28701
|
+
async report(channelId, state, detail) {
|
|
28702
|
+
if (state === "working") {
|
|
28703
|
+
this.holdWorking(channelId, detail);
|
|
28704
|
+
return;
|
|
28705
|
+
}
|
|
28706
|
+
const entry = this.entries.get(channelId);
|
|
28707
|
+
this.entries.delete(channelId);
|
|
28708
|
+
if (entry?.timer) clearTimeout(entry.timer);
|
|
28709
|
+
if (state === "idle" && entry && !entry.published) return;
|
|
28710
|
+
await this.enqueue(channelId, state, detail);
|
|
28711
|
+
}
|
|
28712
|
+
/** A reply body is about to publish on the channel. */
|
|
28713
|
+
replyStarted(channelId) {
|
|
28714
|
+
const entry = this.entries.get(channelId);
|
|
28715
|
+
if (!entry?.timer) return;
|
|
28716
|
+
clearTimeout(entry.timer);
|
|
28717
|
+
entry.timer = null;
|
|
28718
|
+
}
|
|
28719
|
+
/** Drop every pending "working" (bridge shutdown). */
|
|
28720
|
+
close() {
|
|
28721
|
+
for (const entry of this.entries.values()) if (entry.timer) clearTimeout(entry.timer);
|
|
28722
|
+
this.entries.clear();
|
|
28723
|
+
}
|
|
28724
|
+
holdWorking(channelId, detail) {
|
|
28725
|
+
const existing = this.entries.get(channelId);
|
|
28726
|
+
if (existing && (existing.timer || existing.published)) return;
|
|
28727
|
+
const entry = { timer: null, published: false };
|
|
28728
|
+
this.entries.set(channelId, entry);
|
|
28729
|
+
entry.timer = setTimeout(() => {
|
|
28730
|
+
entry.timer = null;
|
|
28731
|
+
if (this.entries.get(channelId) !== entry) return;
|
|
28732
|
+
entry.published = true;
|
|
28733
|
+
this.enqueue(channelId, "working", detail).catch(() => {
|
|
28734
|
+
});
|
|
28735
|
+
}, this.opts.delayMs ?? STATUS_WORKING_DELAY_MS);
|
|
28736
|
+
entry.timer.unref?.();
|
|
28737
|
+
}
|
|
28738
|
+
/** Publishes in order per channel; rejects with the publish error. */
|
|
28739
|
+
enqueue(channelId, state, detail) {
|
|
28740
|
+
const prev = this.chains.get(channelId) ?? Promise.resolve();
|
|
28741
|
+
const run = prev.then(() => this.publish(channelId, state, detail));
|
|
28742
|
+
const settled = run.catch((err) => {
|
|
28743
|
+
this.opts.onError?.(state, err);
|
|
28744
|
+
});
|
|
28745
|
+
this.chains.set(channelId, settled);
|
|
28746
|
+
void settled.then(() => {
|
|
28747
|
+
if (this.chains.get(channelId) === settled) this.chains.delete(channelId);
|
|
28748
|
+
});
|
|
28749
|
+
return run;
|
|
28750
|
+
}
|
|
28751
|
+
};
|
|
28752
|
+
|
|
28753
|
+
// src/bridge-agent.ts
|
|
28679
28754
|
function logSendSupersededAdoption(kind) {
|
|
28680
28755
|
switch (kind) {
|
|
28681
28756
|
case "adopted":
|
|
@@ -29099,8 +29174,23 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
29099
29174
|
* `roomDisplayName`. Kept across a later failed refresh, so a transient
|
|
29100
29175
|
* members-fetch error does not make the agent deaf to its own name; a
|
|
29101
29176
|
* successful refresh after a rename replaces it.
|
|
29177
|
+
*
|
|
29178
|
+
* 2026-10-01 ("one name"): also the name the voice fast path speaks as
|
|
29179
|
+
* ("You are <name>."). A voice settings fetch that carries
|
|
29180
|
+
* `identity_display_name` writes it too (the backend pings
|
|
29181
|
+
* `voice_settings_changed` on a rename), so the latest observation from
|
|
29182
|
+
* either source wins; both read the same identity column.
|
|
29102
29183
|
*/
|
|
29103
29184
|
lastOwnDisplayName;
|
|
29185
|
+
/**
|
|
29186
|
+
* Thinking-indicator gate (owner rule, 2026-10-01): the agent-facing status
|
|
29187
|
+
* routes go through it, so a reply inside 3 s raises no "thinking..." strip.
|
|
29188
|
+
* See status-gate.ts.
|
|
29189
|
+
*/
|
|
29190
|
+
statusGate = new StatusGate(
|
|
29191
|
+
(channelId, state, detail) => this.reportStatus(channelId, state, detail),
|
|
29192
|
+
{ onError: (state) => writeServiceLog2("warn", "status.publish_failed", { working: state === "working", idle: state === "idle" }) }
|
|
29193
|
+
);
|
|
29104
29194
|
/**
|
|
29105
29195
|
* Task 8 (owner-attested Reset Secure Session): per-channel STRUCTURAL owner
|
|
29106
29196
|
* cache, fed by resolveAndPinOwner() on every roster refresh and consumed by
|
|
@@ -29502,6 +29592,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
29502
29592
|
const res = await this.fetchBounded("voice_settings", "read", url, { headers: { authorization: `Bearer ${this.cfg.apiKey}` } });
|
|
29503
29593
|
if (!res.ok) throw new Error(`voice-settings ${res.status}`);
|
|
29504
29594
|
const s = parseVoiceSettings(await res.json());
|
|
29595
|
+
if (s.identityDisplayName) this.lastOwnDisplayName = s.identityDisplayName;
|
|
29505
29596
|
const proxy = settingsToVoiceConfig(s, { baseUrl: this.cfg.apiBase, apiKey: this.cfg.apiKey });
|
|
29506
29597
|
if (s.version === 0 && this.fileVoiceConfig) {
|
|
29507
29598
|
const merged = {
|
|
@@ -30631,6 +30722,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
30631
30722
|
*/
|
|
30632
30723
|
async publishBody(channelId, body, id, onEnvelope) {
|
|
30633
30724
|
this.enforceBodyRateFloor(channelId, body);
|
|
30725
|
+
this.statusGate.replyStarted(channelId);
|
|
30634
30726
|
const joined = await this.reconcileJoin(channelId);
|
|
30635
30727
|
if (!joined) throw new ChannelPendingError(channelId);
|
|
30636
30728
|
const sendStart = performance.now();
|
|
@@ -31399,7 +31491,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31399
31491
|
memory,
|
|
31400
31492
|
speaker,
|
|
31401
31493
|
text,
|
|
31402
|
-
settings: this.voiceSettings ?? void 0
|
|
31494
|
+
settings: this.voiceSettings ?? void 0,
|
|
31495
|
+
agentName: this.voiceAgentName()
|
|
31403
31496
|
});
|
|
31404
31497
|
const maxSentences = this.voiceSettings?.brevity === "one" ? 1 : 2;
|
|
31405
31498
|
const { reply, defer } = splitFastReply(await this.fastLlm.complete(messages), 200, maxSentences);
|
|
@@ -31743,6 +31836,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31743
31836
|
if (TRACE) writeServiceLog2("debug", "identity.owner_pin_resolved");
|
|
31744
31837
|
return { identityId: cached.identityId, publicKeyHex: pin.publicKeyHex };
|
|
31745
31838
|
}
|
|
31839
|
+
/**
|
|
31840
|
+
* The agent-facing status entry (HTTP /v1/status, WS "status"): "working"
|
|
31841
|
+
* is held 3 s and dropped if the turn ends or a reply starts first. See
|
|
31842
|
+
* status-gate.ts. Resolves at once for "working".
|
|
31843
|
+
*/
|
|
31844
|
+
async reportStatusGated(channelId, state, detail) {
|
|
31845
|
+
await this.statusGate.report(channelId, state, detail);
|
|
31846
|
+
}
|
|
31746
31847
|
async reportStatus(channelId, state, detail) {
|
|
31747
31848
|
const joined = await this.reconcileJoin(channelId);
|
|
31748
31849
|
if (!joined) throw new ChannelPendingError(channelId);
|
|
@@ -32706,6 +32807,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
32706
32807
|
ownRoomDisplayName() {
|
|
32707
32808
|
return this.lastOwnDisplayName;
|
|
32708
32809
|
}
|
|
32810
|
+
/**
|
|
32811
|
+
* 2026-10-01 (owner decision, "one name"): the name the voice prompt uses.
|
|
32812
|
+
* The agent's CURRENT identity display name (roster or voice settings
|
|
32813
|
+
* fetch, whichever saw it last); before either has, the credential's
|
|
32814
|
+
* agent_name (the same identity name as of credential issue). Never the
|
|
32815
|
+
* legacy voice settings `display_name` or the file `voice.persona.display_name`.
|
|
32816
|
+
*/
|
|
32817
|
+
voiceAgentName() {
|
|
32818
|
+
return this.lastOwnDisplayName ?? this.cfg.agentName;
|
|
32819
|
+
}
|
|
32709
32820
|
/** Phase 3 F3: the loop guard's view of the sender — server roster first. */
|
|
32710
32821
|
loopSenderIsAgent(channelId, senderId, envelopeSaysAgent, relayedHumanTurn) {
|
|
32711
32822
|
if (relayedHumanTurn) return false;
|
|
@@ -33037,6 +33148,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
33037
33148
|
}
|
|
33038
33149
|
async close() {
|
|
33039
33150
|
this.closingDown = true;
|
|
33151
|
+
this.statusGate.close();
|
|
33040
33152
|
for (const t of this.pauseRecheckTimers.values()) clearTimeout(t);
|
|
33041
33153
|
this.pauseRecheckTimers.clear();
|
|
33042
33154
|
for (const t of this.supersedeRecheckTimers.values()) clearTimeout(t);
|
|
@@ -33490,7 +33602,7 @@ function createHttpServer(agent, opts) {
|
|
|
33490
33602
|
if (method === "POST" && path === "/v1/status") {
|
|
33491
33603
|
const parsed = statusSchema.safeParse(JSON.parse(await readBody(req, OTHER_CAP)));
|
|
33492
33604
|
if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
|
|
33493
|
-
await agent.
|
|
33605
|
+
await agent.reportStatusGated(parsed.data.channel_id, parsed.data.state, parsed.data.detail);
|
|
33494
33606
|
return json(res, 200, { ok: true });
|
|
33495
33607
|
}
|
|
33496
33608
|
if (method === "POST" && path === "/v1/memory/record") {
|
|
@@ -33860,7 +33972,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
33860
33972
|
}
|
|
33861
33973
|
const detail = typeof m.detail === "string" ? m.detail : void 0;
|
|
33862
33974
|
try {
|
|
33863
|
-
await agent.
|
|
33975
|
+
await agent.reportStatusGated(m.channel_id, m.state, detail);
|
|
33864
33976
|
sendJson(sock, { type: "ok" });
|
|
33865
33977
|
} catch (e) {
|
|
33866
33978
|
if (e instanceof ChannelPendingError) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thinking-indicator gate (owner rule, 2026-10-01): a reply that comes back
|
|
3
|
+
* within {@link STATUS_WORKING_DELAY_MS} shows no "thinking..." strip at all;
|
|
4
|
+
* a longer turn shows it.
|
|
5
|
+
*
|
|
6
|
+
* Every status is an MLS publish (about 4.4 s in prod) on the same per-tenant
|
|
7
|
+
* send path as the reply, so an eager "working" made quick replies wait behind
|
|
8
|
+
* it. The gate holds "working" for the delay and publishes it only if the turn
|
|
9
|
+
* is still running then:
|
|
10
|
+
*
|
|
11
|
+
* - "idle" before the delay cancels the pending "working" and publishes
|
|
12
|
+
* nothing (the strip was never shown, so there is nothing to clear);
|
|
13
|
+
* - a reply starting to publish on the channel also cancels it, because
|
|
14
|
+
* some runtimes (OpenClaw) send "idle" only after the reply publish has
|
|
15
|
+
* settled, which would otherwise raise the strip under a visible answer;
|
|
16
|
+
* - once "working" is published, "idle" is published after it, in order;
|
|
17
|
+
* - "stalled" / "error" publish at once (after anything queued) and drop a
|
|
18
|
+
* pending "working".
|
|
19
|
+
*
|
|
20
|
+
* Only the agent-facing status routes (HTTP /v1/status, WS "status") go
|
|
21
|
+
* through the gate. BridgeAgent.reportStatus itself still publishes at once.
|
|
22
|
+
*/
|
|
23
|
+
export declare const STATUS_WORKING_DELAY_MS = 3000;
|
|
24
|
+
export type AgentStatusState = "idle" | "working" | "stalled" | "error";
|
|
25
|
+
type Publish = (channelId: string, state: AgentStatusState, detail?: string) => Promise<void>;
|
|
26
|
+
export declare class StatusGate {
|
|
27
|
+
private readonly publish;
|
|
28
|
+
private readonly opts;
|
|
29
|
+
private readonly entries;
|
|
30
|
+
private readonly chains;
|
|
31
|
+
constructor(publish: Publish, opts?: {
|
|
32
|
+
delayMs?: number;
|
|
33
|
+
onError?: (state: AgentStatusState, err: unknown) => void;
|
|
34
|
+
});
|
|
35
|
+
/** The agent reported a status. Resolves once any publish it starts has settled. */
|
|
36
|
+
report(channelId: string, state: AgentStatusState, detail?: string): Promise<void>;
|
|
37
|
+
/** A reply body is about to publish on the channel. */
|
|
38
|
+
replyStarted(channelId: string): void;
|
|
39
|
+
/** Drop every pending "working" (bridge shutdown). */
|
|
40
|
+
close(): void;
|
|
41
|
+
private holdWorking;
|
|
42
|
+
/** Publishes in order per channel; rejects with the publish error. */
|
|
43
|
+
private enqueue;
|
|
44
|
+
}
|
|
45
|
+
export {};
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const BRIDGE_VERSION = "0.25.
|
|
1
|
+
export declare const BRIDGE_VERSION = "0.25.4";
|
|
@@ -35,6 +35,9 @@ export declare function buildFastMessages(args: {
|
|
|
35
35
|
* from these backend-loaded dials (compileVoicePrompt) instead of the
|
|
36
36
|
* credential file's persona block. */
|
|
37
37
|
settings?: VoiceSettings;
|
|
38
|
+
/** The agent's identity display name ("one name", 2026-10-01): the only
|
|
39
|
+
* source of the prompt's "You are <name>." line. */
|
|
40
|
+
agentName?: string;
|
|
38
41
|
}): ChatMessage[];
|
|
39
42
|
/** The fast model appends this when the full agent must answer next. */
|
|
40
43
|
export declare const DEFER_TOKEN = "[[DEFER]]";
|
package/dist/voice/persona.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { type VoiceSettings } from "./settings.js";
|
|
2
2
|
export declare const CONTINUITY_CLAUSE: string;
|
|
3
|
+
/**
|
|
4
|
+
* `displayName` is legacy (the credential file's `voice.persona.display_name`,
|
|
5
|
+
* or the backend's voice settings `display_name`). It is still parsed so old
|
|
6
|
+
* credentials and payloads load, but the prompt never reads it: the agent's
|
|
7
|
+
* name comes from its identity display name (owner decision 2026-10-01, "one name").
|
|
8
|
+
*/
|
|
3
9
|
export interface VoicePersona {
|
|
4
10
|
displayName?: string;
|
|
5
11
|
persona: string;
|
|
@@ -20,4 +26,4 @@ export interface VoiceConfig {
|
|
|
20
26
|
settings?: VoiceSettings;
|
|
21
27
|
}
|
|
22
28
|
export declare function parseVoiceConfig(raw: unknown, env: Record<string, string | undefined>): VoiceConfig | undefined;
|
|
23
|
-
export declare function systemPrompt(p: VoicePersona, settings?: VoiceSettings): string;
|
|
29
|
+
export declare function systemPrompt(p: VoicePersona, settings?: VoiceSettings, agentName?: string): string;
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { VoiceSettings } from "./settings.js";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* `agentName` is the agent's identity display name (`ConnectIdentity.display_name`,
|
|
4
|
+
* the one name shown in rosters and the Directory). Owner decision 2026-10-01
|
|
5
|
+
* ("one name"): the voice settings `display_name` (`s.displayName`) is a legacy
|
|
6
|
+
* field that is still parsed for compatibility but never read here, so a
|
|
7
|
+
* rename reaches calls too.
|
|
8
|
+
*/
|
|
9
|
+
export declare function compileVoicePrompt(s: VoiceSettings, agentName?: string): string;
|
|
3
10
|
export declare const VOICE_CALL_START_MARKER = "[voice call started]";
|
|
4
11
|
export declare function greetingTurnFor(s: VoiceSettings, callerName?: string): string;
|
package/dist/voice/settings.d.ts
CHANGED
|
@@ -10,7 +10,13 @@ export interface VoiceSettings {
|
|
|
10
10
|
version: number;
|
|
11
11
|
voiceId: string | null;
|
|
12
12
|
pace: (typeof PACES)[number];
|
|
13
|
+
/** Legacy voice-only name (backend `display_name`). Parsed for compatibility
|
|
14
|
+
* with older backends/web clients, never used for the prompt ("one name",
|
|
15
|
+
* 2026-10-01). */
|
|
13
16
|
displayName: string;
|
|
17
|
+
/** The agent's identity display name at fetch time (backend
|
|
18
|
+
* `identity_display_name`), when the backend sends it. */
|
|
19
|
+
identityDisplayName?: string;
|
|
14
20
|
persona: string;
|
|
15
21
|
warmth: (typeof WARMTHS)[number];
|
|
16
22
|
formality: (typeof FORMALITIES)[number];
|