@agentvault/claude-bridge 0.6.3 → 0.6.5
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/index.js +127 -12
- package/dist/session.d.ts +15 -0
- package/dist/worker-queue.d.ts +54 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -446,6 +446,10 @@ var init_subcommand = __esm({
|
|
|
446
446
|
}
|
|
447
447
|
});
|
|
448
448
|
|
|
449
|
+
// src/index.ts
|
|
450
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "node:fs";
|
|
451
|
+
import { join as join10 } from "node:path";
|
|
452
|
+
|
|
449
453
|
// ../plugin/dist/index.js
|
|
450
454
|
import * as nc from "node:crypto";
|
|
451
455
|
import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
|
|
@@ -65555,7 +65559,7 @@ var init_channel = __esm2({
|
|
|
65555
65559
|
*/
|
|
65556
65560
|
sendActivitySpan(spanData) {
|
|
65557
65561
|
if (!this._ws || this._ws.readyState !== WebSocket2.OPEN) return;
|
|
65558
|
-
const pluginVersion = true ? "0.23.
|
|
65562
|
+
const pluginVersion = true ? "0.23.10" : "0.0.0-dev";
|
|
65559
65563
|
const agentName = this.config.agentName ?? "Agent";
|
|
65560
65564
|
const resource = {
|
|
65561
65565
|
"service.name": "agentvault-agent",
|
|
@@ -65866,6 +65870,50 @@ var init_channel = __esm2({
|
|
|
65866
65870
|
);
|
|
65867
65871
|
this.emit("rooms_reconciled", { pruned: stale });
|
|
65868
65872
|
}
|
|
65873
|
+
/**
|
|
65874
|
+
* Handle an in-band `{event:"error"}` frame from the server.
|
|
65875
|
+
*
|
|
65876
|
+
* This is an APPLICATION-level error about one resource — it is NOT a
|
|
65877
|
+
* transport fault, and must never be treated as one. When the error names a
|
|
65878
|
+
* dead resource, the correct response is to PRUNE that resource; recycling
|
|
65879
|
+
* the connection would just replay the same doomed write.
|
|
65880
|
+
*
|
|
65881
|
+
* `conversation deleted` (#460/#481) is the server refusing a write to a
|
|
65882
|
+
* shared 1:1 MLS group whose conversations are all closed/deleted. It carries
|
|
65883
|
+
* `group_id` for exactly this purpose. Without the prune, `_persisted.mlsGroups`
|
|
65884
|
+
* is append-only and the 1:1 send path fans out to every entry, so the dead
|
|
65885
|
+
* group takes a rejected write on every message for the life of the agent
|
|
65886
|
+
* (loopita: group c1b1e11a, closed 2026-07-22, still bouncing 3 days later).
|
|
65887
|
+
*
|
|
65888
|
+
* Deliberately NARROW — it prunes only on this exact detail AND only when
|
|
65889
|
+
* `group_id` matches a persisted shared 1:1 group. Unknown ids and other
|
|
65890
|
+
* details prune nothing, so a server-side wording change or a room/A2A group
|
|
65891
|
+
* can never cost us live MLS state. Safe because `closed`/`deleted` are
|
|
65892
|
+
* terminal server-side, and a genuinely new group re-arrives via Welcome.
|
|
65893
|
+
*
|
|
65894
|
+
* Mirrors the #629 room prune: MLS state file, in-memory group, persisted
|
|
65895
|
+
* entry, then persist.
|
|
65896
|
+
*/
|
|
65897
|
+
async _handleServerError(payload) {
|
|
65898
|
+
if (payload?.detail !== "conversation deleted") return;
|
|
65899
|
+
const mlsGroupId = payload.group_id;
|
|
65900
|
+
if (!mlsGroupId) return;
|
|
65901
|
+
const groups = this._persisted?.mlsGroups;
|
|
65902
|
+
if (!groups) return;
|
|
65903
|
+
const gid = Object.keys(groups).find((k2) => groups[k2]?.mlsGroupId === mlsGroupId);
|
|
65904
|
+
if (!gid) return;
|
|
65905
|
+
try {
|
|
65906
|
+
await deleteMlsState(this.config.dataDir, mlsGroupId);
|
|
65907
|
+
} catch {
|
|
65908
|
+
}
|
|
65909
|
+
this._mlsGroups.delete(`1to1-group:${gid}`);
|
|
65910
|
+
delete groups[gid];
|
|
65911
|
+
await this._persistState();
|
|
65912
|
+
console.log(
|
|
65913
|
+
`[SecureChannel] Pruned dead shared 1:1 group ${gid.slice(0, 8)} (${mlsGroupId.slice(0, 8)}) \u2014 server reports its conversation deleted`
|
|
65914
|
+
);
|
|
65915
|
+
this.emit("dm_group_pruned", { conversationGroupId: gid, mlsGroupId });
|
|
65916
|
+
}
|
|
65869
65917
|
/**
|
|
65870
65918
|
* Return info for all joined rooms.
|
|
65871
65919
|
*/
|
|
@@ -67241,7 +67289,7 @@ var init_channel = __esm2({
|
|
|
67241
67289
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
67242
67290
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
67243
67291
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
67244
|
-
pluginVersion: true ? "0.23.
|
|
67292
|
+
pluginVersion: true ? "0.23.10" : "0.0.0-dev"
|
|
67245
67293
|
});
|
|
67246
67294
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
67247
67295
|
}
|
|
@@ -67559,7 +67607,7 @@ var init_channel = __esm2({
|
|
|
67559
67607
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
67560
67608
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
67561
67609
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
67562
|
-
pluginVersion: true ? "0.23.
|
|
67610
|
+
pluginVersion: true ? "0.23.10" : "0.0.0-dev"
|
|
67563
67611
|
});
|
|
67564
67612
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
67565
67613
|
}
|
|
@@ -67869,6 +67917,7 @@ var init_channel = __esm2({
|
|
|
67869
67917
|
if (data.event === "error") {
|
|
67870
67918
|
const detail = data.data?.detail || data.detail || "Unknown server error";
|
|
67871
67919
|
console.error(`[SecureChannel] Server error: ${detail}`);
|
|
67920
|
+
await this._handleServerError(data.data || data);
|
|
67872
67921
|
this.emit("error", new Error(`Server: ${detail}`));
|
|
67873
67922
|
}
|
|
67874
67923
|
if (data.event === "a2a_message_mls") {
|
|
@@ -97337,7 +97386,7 @@ var init_index = __esm2({
|
|
|
97337
97386
|
init_skill_invoker();
|
|
97338
97387
|
await init_skill_telemetry();
|
|
97339
97388
|
await init_policy_enforcer();
|
|
97340
|
-
VERSION = true ? "0.23.
|
|
97389
|
+
VERSION = true ? "0.23.10" : "0.0.0-dev";
|
|
97341
97390
|
}
|
|
97342
97391
|
});
|
|
97343
97392
|
await init_index();
|
|
@@ -132670,6 +132719,23 @@ var PersistentClaudeSession = class {
|
|
|
132670
132719
|
abort() {
|
|
132671
132720
|
this._abort?.abort();
|
|
132672
132721
|
}
|
|
132722
|
+
/**
|
|
132723
|
+
* Forensic snapshot of the CURRENT turn, for the worker-queue trap.
|
|
132724
|
+
*
|
|
132725
|
+
* Read by `WorkerQueue` when a task times out or errors, BEFORE `abort()` tears
|
|
132726
|
+
* the worker down. It is the difference between two hangs that look identical
|
|
132727
|
+
* from outside (a five-minute silence) but have opposite causes:
|
|
132728
|
+
* composedChars > 0, sawResult false -> the model streamed then stalled
|
|
132729
|
+
* composedChars = 0 -> the turn never produced anything
|
|
132730
|
+
* Read-only; it must never mutate turn state.
|
|
132731
|
+
*/
|
|
132732
|
+
snapshot() {
|
|
132733
|
+
return {
|
|
132734
|
+
composedChars: this.turnText.length,
|
|
132735
|
+
sawResult: this.sawResultThisTurn,
|
|
132736
|
+
said: this.saidThisTurn
|
|
132737
|
+
};
|
|
132738
|
+
}
|
|
132673
132739
|
async *input() {
|
|
132674
132740
|
while (true) {
|
|
132675
132741
|
let item;
|
|
@@ -132852,7 +132918,11 @@ var WorkerQueue = class {
|
|
|
132852
132918
|
loop = Promise.resolve();
|
|
132853
132919
|
running = false;
|
|
132854
132920
|
enqueue(task) {
|
|
132855
|
-
this.q.push(
|
|
132921
|
+
this.q.push({
|
|
132922
|
+
task,
|
|
132923
|
+
enqueuedAt: Date.now(),
|
|
132924
|
+
queueDepthAtEnqueue: this.q.length + (this.running ? 1 : 0)
|
|
132925
|
+
});
|
|
132856
132926
|
if (!this.running) {
|
|
132857
132927
|
this.running = true;
|
|
132858
132928
|
this.loop = this.drain();
|
|
@@ -132865,16 +132935,28 @@ var WorkerQueue = class {
|
|
|
132865
132935
|
async drain() {
|
|
132866
132936
|
try {
|
|
132867
132937
|
while (this.q.length > 0) {
|
|
132868
|
-
const
|
|
132869
|
-
await this.runOne(
|
|
132938
|
+
const entry = this.q.shift();
|
|
132939
|
+
await this.runOne(entry);
|
|
132870
132940
|
}
|
|
132871
132941
|
} finally {
|
|
132872
132942
|
this.running = false;
|
|
132873
132943
|
}
|
|
132874
132944
|
}
|
|
132875
|
-
|
|
132945
|
+
/** Best-effort trap emit. A broken sink must never break the queue. */
|
|
132946
|
+
emitIncident(rec) {
|
|
132947
|
+
try {
|
|
132948
|
+
this.deps.onIncident?.(rec);
|
|
132949
|
+
} catch {
|
|
132950
|
+
}
|
|
132951
|
+
}
|
|
132952
|
+
async runOne(entry) {
|
|
132953
|
+
const { task } = entry;
|
|
132954
|
+
const startedAt = Date.now();
|
|
132955
|
+
const waitedMs = startedAt - entry.enqueuedAt;
|
|
132956
|
+
const queueDepthAtStart = this.q.length;
|
|
132876
132957
|
let session;
|
|
132877
132958
|
let timer;
|
|
132959
|
+
let timedOut = false;
|
|
132878
132960
|
try {
|
|
132879
132961
|
session = this.deps.makeSession(task);
|
|
132880
132962
|
session.push(task.instruction, task.reply, {
|
|
@@ -132885,13 +132967,28 @@ var WorkerQueue = class {
|
|
|
132885
132967
|
const currentSession = session;
|
|
132886
132968
|
const timeout = new Promise((_resolve, reject) => {
|
|
132887
132969
|
timer = setTimeout(() => {
|
|
132970
|
+
timedOut = true;
|
|
132888
132971
|
currentSession.abort();
|
|
132889
132972
|
reject(new Error("worker task timeout"));
|
|
132890
132973
|
}, this.deps.timeoutMs);
|
|
132891
132974
|
});
|
|
132892
132975
|
await Promise.race([session.start(), timeout]);
|
|
132893
132976
|
} catch (e7) {
|
|
132894
|
-
|
|
132977
|
+
const err = e7;
|
|
132978
|
+
this.deps.log(`[worker-queue] task failed: ${err.message}`);
|
|
132979
|
+
const snap = session?.snapshot?.();
|
|
132980
|
+
this.emitIncident({
|
|
132981
|
+
at: new Date(startedAt).toISOString(),
|
|
132982
|
+
outcome: timedOut ? "timeout" : "error",
|
|
132983
|
+
error: err.message,
|
|
132984
|
+
waitedMs,
|
|
132985
|
+
ranMs: Date.now() - startedAt,
|
|
132986
|
+
timeoutMs: this.deps.timeoutMs,
|
|
132987
|
+
queueDepthAtEnqueue: entry.queueDepthAtEnqueue,
|
|
132988
|
+
queueDepthAtStart,
|
|
132989
|
+
instructionChars: task.instruction.length,
|
|
132990
|
+
...snap ? { session: snap } : {}
|
|
132991
|
+
});
|
|
132895
132992
|
session?.abort();
|
|
132896
132993
|
try {
|
|
132897
132994
|
await task.reply(GENERIC_ERROR);
|
|
@@ -133230,7 +133327,7 @@ function wireBridge(channel, session, target, opts = {}) {
|
|
|
133230
133327
|
}
|
|
133231
133328
|
channel.on("error", (err) => {
|
|
133232
133329
|
const msg = err instanceof Error ? err.message : String(err);
|
|
133233
|
-
log(`channel error (
|
|
133330
|
+
log(`channel error (non-fatal, not a reconnect trigger): ${msg}`);
|
|
133234
133331
|
});
|
|
133235
133332
|
const hush = new RoomHushState();
|
|
133236
133333
|
channel.on("room_hushed", (e7) => {
|
|
@@ -133372,7 +133469,7 @@ async function main() {
|
|
|
133372
133469
|
"[bridge] warning: passing the invite token on the command line is visible to other local users via 'ps'. Prefer: AV_INVITE_TOKEN=\u2026 npx @agentvault/claude-bridge"
|
|
133373
133470
|
);
|
|
133374
133471
|
}
|
|
133375
|
-
console.error(`[bridge] version: ${true ? "0.6.
|
|
133472
|
+
console.error(`[bridge] version: ${true ? "0.6.5" : "dev"}`);
|
|
133376
133473
|
console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
|
|
133377
133474
|
console.error(`[bridge] workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
|
|
133378
133475
|
if (cfg.armRoom) {
|
|
@@ -133446,7 +133543,25 @@ async function main() {
|
|
|
133446
133543
|
deviceJwt
|
|
133447
133544
|
}),
|
|
133448
133545
|
timeoutMs: WORKER_TIMEOUT_MS,
|
|
133449
|
-
log: (m6) => console.error(m6)
|
|
133546
|
+
log: (m6) => console.error(m6),
|
|
133547
|
+
// TRAP (2026-07-25). The 07-24 five-minute silence was diagnosable only by
|
|
133548
|
+
// luck — the drop-trap happened to record a reply landing 300.016s after the
|
|
133549
|
+
// ack, and 300s is WORKER_TIMEOUT_MS exactly. Nothing recorded WHY the worker
|
|
133550
|
+
// hung, and these logs carry no timestamps, so the evidence was already gone.
|
|
133551
|
+
// Append-only JSONL beside the logs, plus one stamped console line so the
|
|
133552
|
+
// incident is visible in bridge.error.log at the moment it happens.
|
|
133553
|
+
onIncident: (rec) => {
|
|
133554
|
+
console.error(
|
|
133555
|
+
`[worker-trap] ${rec.at} ${rec.outcome} after ${rec.ranMs}ms (waited ${rec.waitedMs}ms behind ${rec.queueDepthAtEnqueue}, ${rec.queueDepthAtStart} still queued)` + (rec.session ? ` composed=${rec.session.composedChars} result=${rec.session.sawResult} said=${rec.session.said}` : "")
|
|
133556
|
+
);
|
|
133557
|
+
try {
|
|
133558
|
+
const dir = join10(cfg.dataDir, "logs");
|
|
133559
|
+
mkdirSync5(dir, { recursive: true });
|
|
133560
|
+
appendFileSync2(join10(dir, "worker-incidents.jsonl"), JSON.stringify(rec) + "\n");
|
|
133561
|
+
} catch (err) {
|
|
133562
|
+
console.error(`[worker-trap] could not persist incident: ${err.message}`);
|
|
133563
|
+
}
|
|
133564
|
+
}
|
|
133450
133565
|
});
|
|
133451
133566
|
const router = makeRouter({ workAllowed, listener, queue: workerQueue });
|
|
133452
133567
|
wireBridge(
|
package/dist/session.d.ts
CHANGED
|
@@ -171,6 +171,21 @@ export declare class PersistentClaudeSession {
|
|
|
171
171
|
deliver(text: string): Promise<void>;
|
|
172
172
|
/** Abort the in-flight query() — used by the worker queue on a per-task timeout. */
|
|
173
173
|
abort(): void;
|
|
174
|
+
/**
|
|
175
|
+
* Forensic snapshot of the CURRENT turn, for the worker-queue trap.
|
|
176
|
+
*
|
|
177
|
+
* Read by `WorkerQueue` when a task times out or errors, BEFORE `abort()` tears
|
|
178
|
+
* the worker down. It is the difference between two hangs that look identical
|
|
179
|
+
* from outside (a five-minute silence) but have opposite causes:
|
|
180
|
+
* composedChars > 0, sawResult false -> the model streamed then stalled
|
|
181
|
+
* composedChars = 0 -> the turn never produced anything
|
|
182
|
+
* Read-only; it must never mutate turn state.
|
|
183
|
+
*/
|
|
184
|
+
snapshot(): {
|
|
185
|
+
composedChars: number;
|
|
186
|
+
sawResult: boolean;
|
|
187
|
+
said: boolean;
|
|
188
|
+
};
|
|
174
189
|
private input;
|
|
175
190
|
/**
|
|
176
191
|
* Build the SDK options for this session. Locked mode (default) is safe for
|
package/dist/worker-queue.d.ts
CHANGED
|
@@ -16,20 +16,70 @@ export type WorkerTask = {
|
|
|
16
16
|
* disarm denies the next call). Absent for owner DMs. */
|
|
17
17
|
armed?: () => boolean;
|
|
18
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* Forensic record of a task that did NOT complete cleanly.
|
|
21
|
+
*
|
|
22
|
+
* Exists because the 2026-07-24 "loopita went quiet for 5 minutes" incident was
|
|
23
|
+
* diagnosable only by coincidence: the drop-trap happened to capture that the
|
|
24
|
+
* reply landed 300.016s after the ack, and 300s is `WORKER_TIMEOUT_MS` exactly.
|
|
25
|
+
* Nothing in the bridge recorded WHY the worker hung, and the logs carry no
|
|
26
|
+
* timestamps at all, so the evidence was gone by the time we looked.
|
|
27
|
+
*/
|
|
28
|
+
export type WorkerIncident = {
|
|
29
|
+
/** ISO timestamp — the bridge's own logs are unstamped, so this is the anchor. */
|
|
30
|
+
at: string;
|
|
31
|
+
outcome: "timeout" | "error";
|
|
32
|
+
error: string;
|
|
33
|
+
/**
|
|
34
|
+
* enqueue -> start. THE load-bearing field: it separates "this task hung" from
|
|
35
|
+
* "this task was stuck behind one that hung". Both look identical to the owner
|
|
36
|
+
* (a long silence) and have completely different fixes.
|
|
37
|
+
*/
|
|
38
|
+
waitedMs: number;
|
|
39
|
+
/** start -> failure. Approaches timeoutMs for a true hang. */
|
|
40
|
+
ranMs: number;
|
|
41
|
+
timeoutMs: number;
|
|
42
|
+
/** Depth when this task was enqueued — >0 means it was already behind someone. */
|
|
43
|
+
queueDepthAtEnqueue: number;
|
|
44
|
+
/** Depth still waiting when this task began — the blast radius of this hang. */
|
|
45
|
+
queueDepthAtStart: number;
|
|
46
|
+
instructionChars: number;
|
|
47
|
+
/** What the worker had produced when it died, when the session can report it. */
|
|
48
|
+
session?: {
|
|
49
|
+
composedChars: number;
|
|
50
|
+
sawResult: boolean;
|
|
51
|
+
said: boolean;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
19
54
|
export interface WorkerQueueDeps {
|
|
20
55
|
/** Build a fresh ephemeral worker session for this task (not yet started). */
|
|
21
56
|
makeSession: (task: WorkerTask) => PersistentClaudeSession;
|
|
22
57
|
/** Per-task wall-clock cap; on breach the worker is aborted and the queue advances. */
|
|
23
58
|
timeoutMs: number;
|
|
24
59
|
log: (m: string) => void;
|
|
60
|
+
/**
|
|
61
|
+
* Trap sink for non-clean outcomes. Optional, best-effort, and never allowed to
|
|
62
|
+
* affect the queue: a trap that can break the thing it observes is worse than
|
|
63
|
+
* no trap.
|
|
64
|
+
*/
|
|
65
|
+
onIncident?: (rec: WorkerIncident) => void;
|
|
25
66
|
}
|
|
26
67
|
/**
|
|
27
68
|
* Serial FIFO of tool-eligible turns. One worker runs at a time (concurrent tool
|
|
28
69
|
* calls against one device/workspace can interleave destructively; the owner is a
|
|
29
70
|
* single actor). Each task runs in a fresh ephemeral worker seeded with only its
|
|
30
71
|
* instruction; on error or timeout the worker is torn down, a generic message is
|
|
31
|
-
* sent to the task's reply, and the queue advances
|
|
32
|
-
*
|
|
72
|
+
* sent to the task's reply, and the queue advances.
|
|
73
|
+
*
|
|
74
|
+
* ⚠️ A task cannot wedge the queue PERMANENTLY, but it absolutely can wedge it for
|
|
75
|
+
* up to `timeoutMs`. Serial + a 5-minute cap means ONE hung worker delays every
|
|
76
|
+
* message behind it by up to five minutes, and the owner gets the generic error
|
|
77
|
+
* for the FIRST message while a LATER one answers normally. That is not
|
|
78
|
+
* hypothetical: it is the 2026-07-24 loopita incident, where the reply landed
|
|
79
|
+
* 300.016s after the ack. The timeout is the amplifier, not the trigger — see
|
|
80
|
+
* `WorkerIncident` and the `waitedMs` field, which is what tells a hung task
|
|
81
|
+
* apart from its victims. (This comment previously claimed a task "can never
|
|
82
|
+
* wedge the queue"; it could, and did.)
|
|
33
83
|
*/
|
|
34
84
|
export declare class WorkerQueue {
|
|
35
85
|
private deps;
|
|
@@ -41,6 +91,8 @@ export declare class WorkerQueue {
|
|
|
41
91
|
/** Resolves when the queue has processed everything enqueued so far. */
|
|
42
92
|
whenDrained(): Promise<void>;
|
|
43
93
|
private drain;
|
|
94
|
+
/** Best-effort trap emit. A broken sink must never break the queue. */
|
|
95
|
+
private emitIncident;
|
|
44
96
|
private runOne;
|
|
45
97
|
}
|
|
46
98
|
//# sourceMappingURL=worker-queue.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentvault/claude-bridge",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgentVault Claude Bridge — daemon for bridging a Claude agent into secure E2E-encrypted AgentVault 1:1 direct messages and rooms.",
|
|
6
6
|
"main": "dist/index.js",
|