@agentvault/claude-bridge 0.6.2 → 0.6.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/index.js +195 -11
- 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",
|
|
@@ -65796,10 +65800,120 @@ var init_channel = __esm2({
|
|
|
65796
65800
|
this._sessions.delete(convId);
|
|
65797
65801
|
delete this._persisted.sessions[convId];
|
|
65798
65802
|
}
|
|
65803
|
+
if (room.mlsGroupId) {
|
|
65804
|
+
try {
|
|
65805
|
+
await deleteMlsState(this.config.dataDir, room.mlsGroupId);
|
|
65806
|
+
} catch {
|
|
65807
|
+
}
|
|
65808
|
+
}
|
|
65809
|
+
this._mlsGroups.delete(roomId);
|
|
65799
65810
|
delete this._persisted.rooms[roomId];
|
|
65800
65811
|
await this._persistState();
|
|
65801
65812
|
this.emit("room_left", { roomId });
|
|
65802
65813
|
}
|
|
65814
|
+
/**
|
|
65815
|
+
* #629 recurrence fix — reconcile local room state against the server on
|
|
65816
|
+
* connect.
|
|
65817
|
+
*
|
|
65818
|
+
* On disband the server marks each member ``"left"`` and sets
|
|
65819
|
+
* ``room.status="disbanded"`` but sends NO WS event to the agent's bridge, so a
|
|
65820
|
+
* disbanded/left room lingers in local state and replays dead MLS commit
|
|
65821
|
+
* history on every reconnect ("Desired gen in the past" / "invalid ghash tag").
|
|
65822
|
+
* ``GET /rooms`` returns ONLY active rooms the caller is a member of, so any
|
|
65823
|
+
* locally-persisted room ABSENT from it is terminal — prune its MLS state
|
|
65824
|
+
* (file + in-memory group, keyed by roomId), its sessions, and the room entry.
|
|
65825
|
+
*
|
|
65826
|
+
* Fail-safe: a failed fetch / non-ok / non-array response prunes NOTHING, so a
|
|
65827
|
+
* transient error can never over-prune a live room.
|
|
65828
|
+
*/
|
|
65829
|
+
async _reconcileRoomsWithServer() {
|
|
65830
|
+
const local = this._persisted?.rooms;
|
|
65831
|
+
if (!local || Object.keys(local).length === 0) return;
|
|
65832
|
+
if (!this._deviceJwt) return;
|
|
65833
|
+
let activeIds;
|
|
65834
|
+
try {
|
|
65835
|
+
const res = await fetch(`${this.config.apiUrl}/api/v1/rooms`, {
|
|
65836
|
+
headers: { Authorization: `Bearer ${this._deviceJwt}` }
|
|
65837
|
+
});
|
|
65838
|
+
if (!res.ok) return;
|
|
65839
|
+
const list = await res.json();
|
|
65840
|
+
if (!Array.isArray(list)) return;
|
|
65841
|
+
activeIds = new Set(
|
|
65842
|
+
list.map((r22) => r22?.id).filter((id) => !!id)
|
|
65843
|
+
);
|
|
65844
|
+
} catch (err) {
|
|
65845
|
+
console.warn(
|
|
65846
|
+
`[SecureChannel] room reconcile skipped (fetch failed, pruning nothing): ${err instanceof Error ? err.message : String(err)}`
|
|
65847
|
+
);
|
|
65848
|
+
return;
|
|
65849
|
+
}
|
|
65850
|
+
const stale = Object.keys(local).filter((rid) => !activeIds.has(rid));
|
|
65851
|
+
if (stale.length === 0) return;
|
|
65852
|
+
for (const rid of stale) {
|
|
65853
|
+
const room = local[rid];
|
|
65854
|
+
if (room?.mlsGroupId) {
|
|
65855
|
+
try {
|
|
65856
|
+
await deleteMlsState(this.config.dataDir, room.mlsGroupId);
|
|
65857
|
+
} catch {
|
|
65858
|
+
}
|
|
65859
|
+
}
|
|
65860
|
+
this._mlsGroups.delete(rid);
|
|
65861
|
+
for (const convId of room?.conversationIds ?? []) {
|
|
65862
|
+
this._sessions.delete(convId);
|
|
65863
|
+
if (this._persisted?.sessions) delete this._persisted.sessions[convId];
|
|
65864
|
+
}
|
|
65865
|
+
delete local[rid];
|
|
65866
|
+
}
|
|
65867
|
+
await this._persistState();
|
|
65868
|
+
console.log(
|
|
65869
|
+
`[SecureChannel] Room reconcile: pruned ${stale.length} disbanded/left room(s) from local state (${stale.map((r22) => r22.slice(0, 8)).join(", ")})`
|
|
65870
|
+
);
|
|
65871
|
+
this.emit("rooms_reconciled", { pruned: stale });
|
|
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
|
+
}
|
|
65803
65917
|
/**
|
|
65804
65918
|
* Return info for all joined rooms.
|
|
65805
65919
|
*/
|
|
@@ -67150,6 +67264,9 @@ var init_channel = __esm2({
|
|
|
67150
67264
|
await this._pullDrDeliveryQueue();
|
|
67151
67265
|
await this._flushOutboundQueue();
|
|
67152
67266
|
this._setState("ready");
|
|
67267
|
+
void this._reconcileRoomsWithServer().catch(
|
|
67268
|
+
(err) => console.warn(`[SecureChannel] room reconcile failed (ignored): ${err instanceof Error ? err.message : String(err)}`)
|
|
67269
|
+
);
|
|
67153
67270
|
if (this.config.enableScanning) {
|
|
67154
67271
|
this._scanEngine = new ScanEngine();
|
|
67155
67272
|
await this._fetchScanRules();
|
|
@@ -67172,7 +67289,7 @@ var init_channel = __esm2({
|
|
|
67172
67289
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
67173
67290
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
67174
67291
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
67175
|
-
pluginVersion: true ? "0.23.
|
|
67292
|
+
pluginVersion: true ? "0.23.10" : "0.0.0-dev"
|
|
67176
67293
|
});
|
|
67177
67294
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
67178
67295
|
}
|
|
@@ -67490,7 +67607,7 @@ var init_channel = __esm2({
|
|
|
67490
67607
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
67491
67608
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
67492
67609
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
67493
|
-
pluginVersion: true ? "0.23.
|
|
67610
|
+
pluginVersion: true ? "0.23.10" : "0.0.0-dev"
|
|
67494
67611
|
});
|
|
67495
67612
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
67496
67613
|
}
|
|
@@ -67800,6 +67917,7 @@ var init_channel = __esm2({
|
|
|
67800
67917
|
if (data.event === "error") {
|
|
67801
67918
|
const detail = data.data?.detail || data.detail || "Unknown server error";
|
|
67802
67919
|
console.error(`[SecureChannel] Server error: ${detail}`);
|
|
67920
|
+
await this._handleServerError(data.data || data);
|
|
67803
67921
|
this.emit("error", new Error(`Server: ${detail}`));
|
|
67804
67922
|
}
|
|
67805
67923
|
if (data.event === "a2a_message_mls") {
|
|
@@ -97268,7 +97386,7 @@ var init_index = __esm2({
|
|
|
97268
97386
|
init_skill_invoker();
|
|
97269
97387
|
await init_skill_telemetry();
|
|
97270
97388
|
await init_policy_enforcer();
|
|
97271
|
-
VERSION = true ? "0.23.
|
|
97389
|
+
VERSION = true ? "0.23.10" : "0.0.0-dev";
|
|
97272
97390
|
}
|
|
97273
97391
|
});
|
|
97274
97392
|
await init_index();
|
|
@@ -132601,6 +132719,23 @@ var PersistentClaudeSession = class {
|
|
|
132601
132719
|
abort() {
|
|
132602
132720
|
this._abort?.abort();
|
|
132603
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
|
+
}
|
|
132604
132739
|
async *input() {
|
|
132605
132740
|
while (true) {
|
|
132606
132741
|
let item;
|
|
@@ -132783,7 +132918,11 @@ var WorkerQueue = class {
|
|
|
132783
132918
|
loop = Promise.resolve();
|
|
132784
132919
|
running = false;
|
|
132785
132920
|
enqueue(task) {
|
|
132786
|
-
this.q.push(
|
|
132921
|
+
this.q.push({
|
|
132922
|
+
task,
|
|
132923
|
+
enqueuedAt: Date.now(),
|
|
132924
|
+
queueDepthAtEnqueue: this.q.length + (this.running ? 1 : 0)
|
|
132925
|
+
});
|
|
132787
132926
|
if (!this.running) {
|
|
132788
132927
|
this.running = true;
|
|
132789
132928
|
this.loop = this.drain();
|
|
@@ -132796,16 +132935,28 @@ var WorkerQueue = class {
|
|
|
132796
132935
|
async drain() {
|
|
132797
132936
|
try {
|
|
132798
132937
|
while (this.q.length > 0) {
|
|
132799
|
-
const
|
|
132800
|
-
await this.runOne(
|
|
132938
|
+
const entry = this.q.shift();
|
|
132939
|
+
await this.runOne(entry);
|
|
132801
132940
|
}
|
|
132802
132941
|
} finally {
|
|
132803
132942
|
this.running = false;
|
|
132804
132943
|
}
|
|
132805
132944
|
}
|
|
132806
|
-
|
|
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;
|
|
132807
132957
|
let session;
|
|
132808
132958
|
let timer;
|
|
132959
|
+
let timedOut = false;
|
|
132809
132960
|
try {
|
|
132810
132961
|
session = this.deps.makeSession(task);
|
|
132811
132962
|
session.push(task.instruction, task.reply, {
|
|
@@ -132816,13 +132967,28 @@ var WorkerQueue = class {
|
|
|
132816
132967
|
const currentSession = session;
|
|
132817
132968
|
const timeout = new Promise((_resolve, reject) => {
|
|
132818
132969
|
timer = setTimeout(() => {
|
|
132970
|
+
timedOut = true;
|
|
132819
132971
|
currentSession.abort();
|
|
132820
132972
|
reject(new Error("worker task timeout"));
|
|
132821
132973
|
}, this.deps.timeoutMs);
|
|
132822
132974
|
});
|
|
132823
132975
|
await Promise.race([session.start(), timeout]);
|
|
132824
132976
|
} catch (e7) {
|
|
132825
|
-
|
|
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
|
+
});
|
|
132826
132992
|
session?.abort();
|
|
132827
132993
|
try {
|
|
132828
132994
|
await task.reply(GENERIC_ERROR);
|
|
@@ -133303,7 +133469,7 @@ async function main() {
|
|
|
133303
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"
|
|
133304
133470
|
);
|
|
133305
133471
|
}
|
|
133306
|
-
console.error(`[bridge] version: ${true ? "0.6.
|
|
133472
|
+
console.error(`[bridge] version: ${true ? "0.6.4" : "dev"}`);
|
|
133307
133473
|
console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
|
|
133308
133474
|
console.error(`[bridge] workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
|
|
133309
133475
|
if (cfg.armRoom) {
|
|
@@ -133377,7 +133543,25 @@ async function main() {
|
|
|
133377
133543
|
deviceJwt
|
|
133378
133544
|
}),
|
|
133379
133545
|
timeoutMs: WORKER_TIMEOUT_MS,
|
|
133380
|
-
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
|
+
}
|
|
133381
133565
|
});
|
|
133382
133566
|
const router = makeRouter({ workAllowed, listener, queue: workerQueue });
|
|
133383
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.4",
|
|
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",
|