mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 14:46:55 +01:00
Merge pull request #46 from Curabis/fix/bc-mcp-bridge-proactive-session-refresh
[bc-mcp-bridge] Reactive + proactive session self-heal
This commit is contained in:
commit
73e5a01a36
1 changed files with 50 additions and 5 deletions
|
|
@ -50,7 +50,18 @@ function loadConfig() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
let token = null, tokenExp = 0, sessionId = null;
|
let token = null, tokenExp = 0, sessionId = null, lastInitializeMsg = null, lastActivityAt = 0;
|
||||||
|
// 2026-08-07: the reactive re-initialize below only fires after BC has already
|
||||||
|
// rejected a request. It self-heals invisibly to the caller, but still burns
|
||||||
|
// two round trips (reject, initialize, retry) on the first call after any idle
|
||||||
|
// gap. A client that opens a fresh session per call (e.g. a Power Automate
|
||||||
|
// flow) never hits this at all, which is why it reads as more stable than a
|
||||||
|
// long-lived interactive session that caches sessionId for its whole
|
||||||
|
// lifetime. Proactively refreshing before a call that has been idle a while
|
||||||
|
// gets the same effective robustness without waiting for BC to reject first.
|
||||||
|
// No API exposes the session's actual TTL, so this threshold is a heuristic,
|
||||||
|
// not a documented guarantee.
|
||||||
|
const SESSION_IDLE_THRESHOLD_MS = 3 * 60 * 1000; // 3 min
|
||||||
|
|
||||||
async function getToken() {
|
async function getToken() {
|
||||||
if (token && Date.now() < tokenExp - 60000) return token; // forny 1 min foer udloeb
|
if (token && Date.now() < tokenExp - 60000) return token; // forny 1 min foer udloeb
|
||||||
|
|
@ -118,8 +129,23 @@ async function forwardOnce(msg) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function forward(msg) {
|
async function forward(msg) {
|
||||||
|
if (msg.method === "initialize") lastInitializeMsg = msg;
|
||||||
|
if (sessionId && msg.method !== "initialize" && lastActivityAt &&
|
||||||
|
Date.now() - lastActivityAt > SESSION_IDLE_THRESHOLD_MS) {
|
||||||
|
process.stderr.write(`[bc-mcp-bridge] session idle ${Math.round((Date.now() - lastActivityAt) / 1000)}s, refreshing proactively\n`);
|
||||||
|
sessionId = null;
|
||||||
|
if (lastInitializeMsg) {
|
||||||
try {
|
try {
|
||||||
return await forwardOnce(msg);
|
await forwardOnce(lastInitializeMsg);
|
||||||
|
} catch (initErr) {
|
||||||
|
process.stderr.write(`[bc-mcp-bridge] proactive re-initialize failed: ${initErr.message || initErr}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await forwardOnce(msg);
|
||||||
|
lastActivityAt = Date.now();
|
||||||
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 2026-08-04: one retry with a forced-fresh BC session (sessionId cleared).
|
// 2026-08-04: one retry with a forced-fresh BC session (sessionId cleared).
|
||||||
// A long-idle Claude Code session can leave the cached Mcp-Session-Id or
|
// A long-idle Claude Code session can leave the cached Mcp-Session-Id or
|
||||||
|
|
@ -127,11 +153,30 @@ async function forward(msg) {
|
||||||
// until REQUEST_TIMEOUT_MS, or as an "Internal_CompanyNotFound" that was
|
// until REQUEST_TIMEOUT_MS, or as an "Internal_CompanyNotFound" that was
|
||||||
// actually a dead session, not a real company problem. Quick, frequent
|
// actually a dead session, not a real company problem. Quick, frequent
|
||||||
// calls never idle long enough to hit this; a long session with gaps
|
// calls never idle long enough to hit this; a long session with gaps
|
||||||
// between calls does. One retry self-heals without the developer noticing;
|
// between calls does.
|
||||||
// if the retry also fails, this is a real error and should surface as one.
|
|
||||||
process.stderr.write(`[bc-mcp-bridge] retrying after: ${e.message || e}\n`);
|
process.stderr.write(`[bc-mcp-bridge] retrying after: ${e.message || e}\n`);
|
||||||
sessionId = null;
|
sessionId = null;
|
||||||
return await forwardOnce(msg);
|
// 2026-08-05: clearing sessionId is not enough on its own. BC's own error
|
||||||
|
// is explicit -- "A new session can only be created by an initialize
|
||||||
|
// request" -- so simply resending the original non-initialize message
|
||||||
|
// with no session header reproduces the exact same error, not a fresh
|
||||||
|
// session. Replay the client's actual initialize message first (cached
|
||||||
|
// above, the only thing BC will accept to hand out a new session), THEN
|
||||||
|
// retry the original call with the session that establishes. If there's
|
||||||
|
// no cached initialize yet (this failure IS the very first call, or is
|
||||||
|
// itself the initialize), skip straight to the plain retry below.
|
||||||
|
if (msg.method !== "initialize" && lastInitializeMsg) {
|
||||||
|
try {
|
||||||
|
await forwardOnce(lastInitializeMsg);
|
||||||
|
} catch (initErr) {
|
||||||
|
process.stderr.write(`[bc-mcp-bridge] re-initialize failed: ${initErr.message || initErr}\n`);
|
||||||
|
// Fall through anyway -- retrying the original message will still
|
||||||
|
// surface a clear, real error if the session truly can't be restored.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const result = await forwardOnce(msg);
|
||||||
|
lastActivityAt = Date.now();
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue