@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/gateway",
3
- "version": "0.6.81",
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.81';
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) { clearTimeout(ttl); endRelaySession(s.id); trace?.commit(); return sendJson(res, 502, { error: { message: `bridge: ${e.message}`, type: 'bridge_error' } }); }
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
- export function createRelaySession({ vault, redactOpts, bridgeUrl, token, harness = null }) {
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
  }