@chatpanel/gateway 0.6.81 → 0.6.83
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/package.json +1 -1
- package/src/server.js +8 -6
- package/src/team-store.js +3 -0
- package/src/toolrelay.js +19 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/gateway",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.83",
|
|
4
4
|
"description": "Local privacy gateway \u2014 redacts PII out of OpenAI/Anthropic API traffic before it reaches a model, then restores it in the reply. Point opencode, codex, aider, Claude Code, etc. at it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/server.js
CHANGED
|
@@ -27,7 +27,7 @@ import { ensureGatewayToken, isAdminAuthorized } from './gateway-token.js';
|
|
|
27
27
|
import { resolveBridgeUrl } from './bridge.js';
|
|
28
28
|
import { secureFetch } from './secure-fetch.js';
|
|
29
29
|
import { streamBridgeChat, readBridgeToken, openBridgeChat } from './bridge.js';
|
|
30
|
-
import { createRelaySession, getRelaySession, endRelaySession, pumpBridgeStream, deliverToolResult, toolsToSpecs, parseToolCallId } from './toolrelay.js';
|
|
30
|
+
import { createRelaySession, getRelaySession, endRelaySession, pumpBridgeStream, deliverToolResult, toolsToSpecs, parseToolCallId, touchRelaySession } from './toolrelay.js';
|
|
31
31
|
import { shaperFor } from './shape.js';
|
|
32
32
|
import { startNer, ensureNer } from './ner.js';
|
|
33
33
|
import { installTimestampedConsole } from './log.js';
|
|
@@ -58,7 +58,7 @@ import * as openai from './openai.js';
|
|
|
58
58
|
import * as responses from './responses.js';
|
|
59
59
|
import * as anthropic from './anthropic.js';
|
|
60
60
|
|
|
61
|
-
export const VERSION = '0.6.
|
|
61
|
+
export const VERSION = '0.6.83';
|
|
62
62
|
|
|
63
63
|
// WARM search tier — SQLite + FTS5 record store (falls back to an encrypted-JSON
|
|
64
64
|
// store if SQLite can't load), fed by the extension's ingest sync + backup-ingest.
|
|
@@ -380,9 +380,9 @@ async function pumpRelay(res, s, shaper, trace) {
|
|
|
380
380
|
let first = true;
|
|
381
381
|
const tick = () => { if (trace && first) { trace.lap('upstream', t0); sStart = trace.clock(); first = false; } };
|
|
382
382
|
await pumpBridgeStream(s, {
|
|
383
|
-
onText: (text) => { tick(); const r = restorer.push(text); if (r) res.write(shaper.sseDelta(r)); },
|
|
383
|
+
onText: (text) => { tick(); touchRelaySession(s.id); const r = restorer.push(text); if (r) res.write(shaper.sseDelta(r)); },
|
|
384
384
|
onToolRequest: ({ name, restoredArgs, toolId }) => {
|
|
385
|
-
tick();
|
|
385
|
+
tick(); touchRelaySession(s.id);
|
|
386
386
|
const tail = restorer.flush(); if (tail) res.write(shaper.sseDelta(tail));
|
|
387
387
|
res.write(shaper.sseToolCalls([{ id: toolId, name, arguments: JSON.stringify(restoredArgs) }]));
|
|
388
388
|
res.write(shaper.sseToolFinish());
|
|
@@ -403,14 +403,15 @@ async function startRelay(req, res, { kind, adapter, agent }, body, vault, cfg,
|
|
|
403
403
|
// handler), but the custom dictionary stays capped for free.
|
|
404
404
|
const redactOpts = { tier: cfg.redaction.tier === 'full' ? 'full' : 'basic', dictionary: gatedDictionary(cfg.redaction, isPro), entities: [] };
|
|
405
405
|
const bridgeUrl = await resolveBridgeUrl(cfg);
|
|
406
|
+
// The session's life is idle time, re-armed on every round (toolrelay.js) — not a flat
|
|
407
|
+
// clock from here, which ended every long tool-using turn at 135 s.
|
|
406
408
|
const s = createRelaySession({ vault, redactOpts, bridgeUrl, token, harness });
|
|
407
|
-
const ttl = setTimeout(() => endRelaySession(s.id), 135_000); // bridge tool-call timeout is 120s
|
|
408
409
|
// The placeholder note is already in `system` (injected into the body after
|
|
409
410
|
// redaction in the main handler), so toTurn() carried it here — nothing to add.
|
|
410
411
|
let resp;
|
|
411
412
|
try {
|
|
412
413
|
resp = await openBridgeChat({ bridgeUrl, agent, token, messages, system, specs: toolsToSpecs(tools), options: bridgeAgentOptions(cfg), signal: undefined });
|
|
413
|
-
} catch (e) {
|
|
414
|
+
} catch (e) { endRelaySession(s.id); trace?.commit(); return sendJson(res, 502, { error: { message: `bridge: ${e.message}`, type: 'bridge_error' } }); }
|
|
414
415
|
s.reader = resp.body.getReader();
|
|
415
416
|
res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' });
|
|
416
417
|
res.write(shaper.sseHead());
|
|
@@ -421,6 +422,7 @@ async function startRelay(req, res, { kind, adapter, agent }, body, vault, cfg,
|
|
|
421
422
|
// The relay redacts the tool result with ITS vault (the main handler skips
|
|
422
423
|
// redaction for a relay-resume), so time that here as the 'redact' leg.
|
|
423
424
|
async function resumeRelay(res, s, toolContent, model, trace = null) {
|
|
425
|
+
touchRelaySession(s.id); s.rounds = (s.rounds || 0) + 1;
|
|
424
426
|
try {
|
|
425
427
|
const rd0 = trace ? trace.clock() : 0;
|
|
426
428
|
await deliverToolResult(s, toolContent);
|
package/src/team-store.js
CHANGED
|
@@ -87,6 +87,9 @@ export function applyEvent(run, ev) {
|
|
|
87
87
|
if (type === 'board.thread-status' && p.status !== 'waiting' && run.status === 'waiting' && !(run.threads.threads || []).some((t) => t.kind === 'ask' && t.status === 'waiting')) run.status = 'answered';
|
|
88
88
|
break;
|
|
89
89
|
case 'task.waiting': { const t = run.tasks.find((x) => x.id === p.taskId); if (t) { t.status = 'waiting'; t.waitingOn = p.threadId; } break; }
|
|
90
|
+
case 'task.model': { const t = run.tasks.find((x) => x.id === p.taskId); if (t) t.model = p.model; break; }
|
|
91
|
+
case 'task.tool': { const t = run.tasks.find((x) => x.id === p.taskId); if (t) t.tools = (t.tools || 0) + 1; break; }
|
|
92
|
+
case 'run.usage': run.usage = p.usage || run.usage; break;
|
|
90
93
|
case 'run.waiting': run.status = 'running'; break;
|
|
91
94
|
case 'run.resumed': run.status = 'running'; run.endedAt = null; run.checkpoint = null; break;
|
|
92
95
|
default: break;
|
package/src/toolrelay.js
CHANGED
|
@@ -35,15 +35,32 @@ export function toolsToSpecs(tools) {
|
|
|
35
35
|
.map((t) => ({ name: t.function.name, description: t.function.description || '', parameters: t.function.parameters || { type: 'object', properties: {} } }));
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// A parked session lives while something is happening on it. The limit is IDLE time —
|
|
39
|
+
// re-armed on every tool result the client brings back and every chunk the agent sends —
|
|
40
|
+
// not time since the turn began: a flat 135 s from the start killed every relayed turn
|
|
41
|
+
// with more than a handful of tool rounds, at 135.1 s exactly, and the client read the
|
|
42
|
+
// empty resume as "the model returned no answer". A team member's turn is many rounds.
|
|
43
|
+
export const RELAY_IDLE_MS = 135_000; // the bridge's own tool-call timeout is 120 s
|
|
44
|
+
|
|
45
|
+
export function createRelaySession({ vault, redactOpts, bridgeUrl, token, harness = null, idleMs = RELAY_IDLE_MS }) {
|
|
39
46
|
const id = randomUUID().slice(0, 8);
|
|
40
|
-
const s = { id, reader: null, decoder: new TextDecoder(), buf: '', bridgeSessionId: null, toolId: null, vault: vault || createVault(), redactOpts: redactOpts || { tier: 'basic' }, bridgeUrl, token, harness, done: false };
|
|
47
|
+
const s = { id, reader: null, decoder: new TextDecoder(), buf: '', bridgeSessionId: null, toolId: null, vault: vault || createVault(), redactOpts: redactOpts || { tier: 'basic' }, bridgeUrl, token, harness, done: false, idleMs, ttl: null, startedAt: Date.now(), rounds: 0 };
|
|
41
48
|
sessions.set(id, s);
|
|
49
|
+
touchRelaySession(id);
|
|
42
50
|
return s;
|
|
43
51
|
}
|
|
44
52
|
export const getRelaySession = (id) => sessions.get(id);
|
|
53
|
+
/** Something happened on the session: the idle clock starts over. */
|
|
54
|
+
export function touchRelaySession(id) {
|
|
55
|
+
const s = sessions.get(id);
|
|
56
|
+
if (!s) return;
|
|
57
|
+
if (s.ttl) clearTimeout(s.ttl);
|
|
58
|
+
s.ttl = setTimeout(() => endRelaySession(id), s.idleMs);
|
|
59
|
+
if (typeof s.ttl.unref === 'function') s.ttl.unref();
|
|
60
|
+
}
|
|
45
61
|
export function endRelaySession(id) {
|
|
46
62
|
const s = sessions.get(id);
|
|
63
|
+
if (s?.ttl) clearTimeout(s.ttl);
|
|
47
64
|
if (s?.reader) { try { s.reader.cancel(); } catch { /* ignore */ } }
|
|
48
65
|
sessions.delete(id);
|
|
49
66
|
}
|