@securecoms/agent-bridge 0.25.3 → 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 +85 -2
- package/dist/bridge-agent.d.ts +12 -0
- package/dist/index.js +85 -2
- package/dist/status-gate.d.ts +45 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -28838,6 +28838,70 @@ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
|
|
|
28838
28838
|
|
|
28839
28839
|
// src/bridge-agent.ts
|
|
28840
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
|
|
28841
28905
|
function logSendSupersededAdoption(kind) {
|
|
28842
28906
|
switch (kind) {
|
|
28843
28907
|
case "adopted":
|
|
@@ -29269,6 +29333,15 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
29269
29333
|
* either source wins; both read the same identity column.
|
|
29270
29334
|
*/
|
|
29271
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
|
+
);
|
|
29272
29345
|
/**
|
|
29273
29346
|
* Task 8 (owner-attested Reset Secure Session): per-channel STRUCTURAL owner
|
|
29274
29347
|
* cache, fed by resolveAndPinOwner() on every roster refresh and consumed by
|
|
@@ -30800,6 +30873,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
30800
30873
|
*/
|
|
30801
30874
|
async publishBody(channelId, body, id, onEnvelope) {
|
|
30802
30875
|
this.enforceBodyRateFloor(channelId, body);
|
|
30876
|
+
this.statusGate.replyStarted(channelId);
|
|
30803
30877
|
const joined = await this.reconcileJoin(channelId);
|
|
30804
30878
|
if (!joined) throw new ChannelPendingError(channelId);
|
|
30805
30879
|
const sendStart = performance.now();
|
|
@@ -31913,6 +31987,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31913
31987
|
if (TRACE) writeServiceLog("debug", "identity.owner_pin_resolved");
|
|
31914
31988
|
return { identityId: cached.identityId, publicKeyHex: pin.publicKeyHex };
|
|
31915
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
|
+
}
|
|
31916
31998
|
async reportStatus(channelId, state, detail) {
|
|
31917
31999
|
const joined = await this.reconcileJoin(channelId);
|
|
31918
32000
|
if (!joined) throw new ChannelPendingError(channelId);
|
|
@@ -33217,6 +33299,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
33217
33299
|
}
|
|
33218
33300
|
async close() {
|
|
33219
33301
|
this.closingDown = true;
|
|
33302
|
+
this.statusGate.close();
|
|
33220
33303
|
for (const t of this.pauseRecheckTimers.values()) clearTimeout(t);
|
|
33221
33304
|
this.pauseRecheckTimers.clear();
|
|
33222
33305
|
for (const t of this.supersedeRecheckTimers.values()) clearTimeout(t);
|
|
@@ -33563,7 +33646,7 @@ function createHttpServer(agent, opts) {
|
|
|
33563
33646
|
if (method === "POST" && path === "/v1/status") {
|
|
33564
33647
|
const parsed = statusSchema.safeParse(JSON.parse(await readBody(req, OTHER_CAP)));
|
|
33565
33648
|
if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
|
|
33566
|
-
await agent.
|
|
33649
|
+
await agent.reportStatusGated(parsed.data.channel_id, parsed.data.state, parsed.data.detail);
|
|
33567
33650
|
return json(res, 200, { ok: true });
|
|
33568
33651
|
}
|
|
33569
33652
|
if (method === "POST" && path === "/v1/memory/record") {
|
|
@@ -33933,7 +34016,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
33933
34016
|
}
|
|
33934
34017
|
const detail = typeof m.detail === "string" ? m.detail : void 0;
|
|
33935
34018
|
try {
|
|
33936
|
-
await agent.
|
|
34019
|
+
await agent.reportStatusGated(m.channel_id, m.state, detail);
|
|
33937
34020
|
sendJson(sock, { type: "ok" });
|
|
33938
34021
|
} catch (e) {
|
|
33939
34022
|
if (e instanceof ChannelPendingError) {
|
package/dist/bridge-agent.d.ts
CHANGED
|
@@ -575,6 +575,12 @@ export declare class BridgeAgent {
|
|
|
575
575
|
* either source wins; both read the same identity column.
|
|
576
576
|
*/
|
|
577
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;
|
|
578
584
|
/**
|
|
579
585
|
* Task 8 (owner-attested Reset Secure Session): per-channel STRUCTURAL owner
|
|
580
586
|
* cache, fed by resolveAndPinOwner() on every roster refresh and consumed by
|
|
@@ -1819,6 +1825,12 @@ export declare class BridgeAgent {
|
|
|
1819
1825
|
* never trusted on its own, it is always reconfirmed against the store.
|
|
1820
1826
|
*/
|
|
1821
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>;
|
|
1822
1834
|
reportStatus(channelId: string, state: "idle" | "working" | "stalled" | "error", detail?: string): Promise<void>;
|
|
1823
1835
|
/** `runId` must exist and not have settled yet — settle() is terminal. */
|
|
1824
1836
|
private requireActionRun;
|
package/dist/index.js
CHANGED
|
@@ -28687,6 +28687,70 @@ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
|
|
|
28687
28687
|
|
|
28688
28688
|
// src/bridge-agent.ts
|
|
28689
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
|
|
28690
28754
|
function logSendSupersededAdoption(kind) {
|
|
28691
28755
|
switch (kind) {
|
|
28692
28756
|
case "adopted":
|
|
@@ -29118,6 +29182,15 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
29118
29182
|
* either source wins; both read the same identity column.
|
|
29119
29183
|
*/
|
|
29120
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
|
+
);
|
|
29121
29194
|
/**
|
|
29122
29195
|
* Task 8 (owner-attested Reset Secure Session): per-channel STRUCTURAL owner
|
|
29123
29196
|
* cache, fed by resolveAndPinOwner() on every roster refresh and consumed by
|
|
@@ -30649,6 +30722,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
30649
30722
|
*/
|
|
30650
30723
|
async publishBody(channelId, body, id, onEnvelope) {
|
|
30651
30724
|
this.enforceBodyRateFloor(channelId, body);
|
|
30725
|
+
this.statusGate.replyStarted(channelId);
|
|
30652
30726
|
const joined = await this.reconcileJoin(channelId);
|
|
30653
30727
|
if (!joined) throw new ChannelPendingError(channelId);
|
|
30654
30728
|
const sendStart = performance.now();
|
|
@@ -31762,6 +31836,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31762
31836
|
if (TRACE) writeServiceLog2("debug", "identity.owner_pin_resolved");
|
|
31763
31837
|
return { identityId: cached.identityId, publicKeyHex: pin.publicKeyHex };
|
|
31764
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
|
+
}
|
|
31765
31847
|
async reportStatus(channelId, state, detail) {
|
|
31766
31848
|
const joined = await this.reconcileJoin(channelId);
|
|
31767
31849
|
if (!joined) throw new ChannelPendingError(channelId);
|
|
@@ -33066,6 +33148,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
33066
33148
|
}
|
|
33067
33149
|
async close() {
|
|
33068
33150
|
this.closingDown = true;
|
|
33151
|
+
this.statusGate.close();
|
|
33069
33152
|
for (const t of this.pauseRecheckTimers.values()) clearTimeout(t);
|
|
33070
33153
|
this.pauseRecheckTimers.clear();
|
|
33071
33154
|
for (const t of this.supersedeRecheckTimers.values()) clearTimeout(t);
|
|
@@ -33519,7 +33602,7 @@ function createHttpServer(agent, opts) {
|
|
|
33519
33602
|
if (method === "POST" && path === "/v1/status") {
|
|
33520
33603
|
const parsed = statusSchema.safeParse(JSON.parse(await readBody(req, OTHER_CAP)));
|
|
33521
33604
|
if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
|
|
33522
|
-
await agent.
|
|
33605
|
+
await agent.reportStatusGated(parsed.data.channel_id, parsed.data.state, parsed.data.detail);
|
|
33523
33606
|
return json(res, 200, { ok: true });
|
|
33524
33607
|
}
|
|
33525
33608
|
if (method === "POST" && path === "/v1/memory/record") {
|
|
@@ -33889,7 +33972,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
33889
33972
|
}
|
|
33890
33973
|
const detail = typeof m.detail === "string" ? m.detail : void 0;
|
|
33891
33974
|
try {
|
|
33892
|
-
await agent.
|
|
33975
|
+
await agent.reportStatusGated(m.channel_id, m.state, detail);
|
|
33893
33976
|
sendJson(sock, { type: "ok" });
|
|
33894
33977
|
} catch (e) {
|
|
33895
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";
|