[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
195
electron/bridges/mcpServerBridge/sessionIdleManager.cjs
Normal file
195
electron/bridges/mcpServerBridge/sessionIdleManager.cjs
Normal file
@@ -0,0 +1,195 @@
|
||||
"use strict";
|
||||
|
||||
const DEFAULT_SESSION_IDLE_TIMEOUT_MINUTES = 30;
|
||||
const MIN_SESSION_IDLE_TIMEOUT_MINUTES = 1;
|
||||
const MAX_SESSION_IDLE_TIMEOUT_MINUTES = 24 * 60;
|
||||
|
||||
function normalizeSessionIdleTimeoutMinutes(value) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return DEFAULT_SESSION_IDLE_TIMEOUT_MINUTES;
|
||||
return Math.min(
|
||||
MAX_SESSION_IDLE_TIMEOUT_MINUTES,
|
||||
Math.max(MIN_SESSION_IDLE_TIMEOUT_MINUTES, Math.round(parsed)),
|
||||
);
|
||||
}
|
||||
|
||||
function createSessionIdleManager(options = {}) {
|
||||
const DateImpl = options.Date || Date;
|
||||
const setTimeoutImpl = options.setTimeout || setTimeout;
|
||||
const clearTimeoutImpl = options.clearTimeout || clearTimeout;
|
||||
const onIdle = typeof options.onIdle === "function" ? options.onIdle : async () => {};
|
||||
const entries = new Map();
|
||||
let timeoutMinutes = normalizeSessionIdleTimeoutMinutes(options.timeoutMinutes);
|
||||
|
||||
function clearTimer(entry) {
|
||||
if (entry.timer == null) return;
|
||||
clearTimeoutImpl(entry.timer);
|
||||
entry.timer = null;
|
||||
}
|
||||
|
||||
function schedule(entry) {
|
||||
clearTimer(entry);
|
||||
if (entry.activeCount > 0 || entry.checking || entry.closing) return;
|
||||
const expiresAt = entry.lastActivityAt + timeoutMinutes * 60 * 1000;
|
||||
const delay = Math.max(0, expiresAt - DateImpl.now());
|
||||
entry.timer = setTimeoutImpl(() => {
|
||||
entry.timer = null;
|
||||
const current = entries.get(entry.sessionId);
|
||||
if (current !== entry || entry.activeCount > 0 || entry.checking || entry.closing) return;
|
||||
const latestExpiresAt = entry.lastActivityAt + timeoutMinutes * 60 * 1000;
|
||||
if (DateImpl.now() < latestExpiresAt) {
|
||||
schedule(entry);
|
||||
return;
|
||||
}
|
||||
entry.checking = true;
|
||||
const activityVersion = entry.activityVersion;
|
||||
Promise.resolve(onIdle({
|
||||
chatSessionId: entry.chatSessionId,
|
||||
sessionId: entry.sessionId,
|
||||
}, activityVersion)).catch(() => {
|
||||
resume(entry.sessionId);
|
||||
});
|
||||
}, delay);
|
||||
entry.timer?.unref?.();
|
||||
}
|
||||
|
||||
function track(chatSessionId, sessionId) {
|
||||
if (!chatSessionId || !sessionId) return false;
|
||||
const existing = entries.get(sessionId);
|
||||
if (existing) clearTimer(existing);
|
||||
const entry = {
|
||||
chatSessionId,
|
||||
sessionId,
|
||||
lastActivityAt: DateImpl.now(),
|
||||
activityVersion: 0,
|
||||
activeCount: 0,
|
||||
checking: false,
|
||||
closing: false,
|
||||
timer: null,
|
||||
};
|
||||
entries.set(sessionId, entry);
|
||||
schedule(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
function touch(chatSessionId, sessionId) {
|
||||
const entry = entries.get(sessionId);
|
||||
if (!entry || entry.closing) return false;
|
||||
entry.checking = false;
|
||||
entry.activityVersion += 1;
|
||||
entry.lastActivityAt = DateImpl.now();
|
||||
// Keep an existing timer instead of recreating it for every output chunk.
|
||||
// Its callback rechecks lastActivityAt and extends the deadline as needed.
|
||||
if (entry.timer == null) schedule(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
function beginActivity(chatSessionId, sessionId) {
|
||||
const entry = entries.get(sessionId);
|
||||
if (!entry || entry.closing) return false;
|
||||
entry.checking = false;
|
||||
entry.activityVersion += 1;
|
||||
entry.activeCount += 1;
|
||||
entry.lastActivityAt = DateImpl.now();
|
||||
clearTimer(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
function endActivity(chatSessionId, sessionId) {
|
||||
const entry = entries.get(sessionId);
|
||||
if (!entry || entry.closing) return false;
|
||||
entry.activityVersion += 1;
|
||||
entry.activeCount = Math.max(0, entry.activeCount - 1);
|
||||
entry.lastActivityAt = DateImpl.now();
|
||||
schedule(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
function beginClose(sessionId) {
|
||||
const entry = entries.get(sessionId);
|
||||
if (!entry || entry.closing) return false;
|
||||
entry.checking = false;
|
||||
entry.closing = true;
|
||||
clearTimer(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
function beginIdleClose(sessionId, activityVersion) {
|
||||
if (!isIdleCheckCurrent(sessionId, activityVersion)) return false;
|
||||
return beginClose(sessionId);
|
||||
}
|
||||
|
||||
function isIdleCheckCurrent(sessionId, activityVersion) {
|
||||
const entry = entries.get(sessionId);
|
||||
return Boolean(
|
||||
entry
|
||||
&& entry.checking
|
||||
&& !entry.closing
|
||||
&& entry.activeCount === 0
|
||||
&& entry.activityVersion === activityVersion
|
||||
);
|
||||
}
|
||||
|
||||
function resume(sessionId) {
|
||||
const entry = entries.get(sessionId);
|
||||
if (!entry) return false;
|
||||
entry.closing = false;
|
||||
entry.checking = false;
|
||||
entry.activityVersion += 1;
|
||||
entry.activeCount = 0;
|
||||
entry.lastActivityAt = DateImpl.now();
|
||||
schedule(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
function forgetSession(sessionId) {
|
||||
const entry = entries.get(sessionId);
|
||||
if (!entry) return false;
|
||||
clearTimer(entry);
|
||||
entries.delete(sessionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
function setTimeoutMinutes(value) {
|
||||
timeoutMinutes = normalizeSessionIdleTimeoutMinutes(value);
|
||||
for (const entry of entries.values()) schedule(entry);
|
||||
return timeoutMinutes;
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
for (const entry of entries.values()) clearTimer(entry);
|
||||
entries.clear();
|
||||
}
|
||||
|
||||
function scopeCleared() {
|
||||
// Intentionally keep timers alive. A deleted/interrupted AI scope is one of
|
||||
// the cases where the idle fallback is needed most.
|
||||
}
|
||||
|
||||
return {
|
||||
track,
|
||||
touch,
|
||||
beginActivity,
|
||||
endActivity,
|
||||
beginClose,
|
||||
beginIdleClose,
|
||||
resume,
|
||||
forgetSession,
|
||||
isTracked: (sessionId) => entries.has(sessionId),
|
||||
hasActivity: (sessionId) => Boolean(entries.get(sessionId)?.activeCount),
|
||||
isIdleCheckCurrent,
|
||||
isClosing: (sessionId) => Boolean(entries.get(sessionId)?.closing),
|
||||
setTimeoutMinutes,
|
||||
getTimeoutMinutes: () => timeoutMinutes,
|
||||
clearAll,
|
||||
scopeCleared,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_SESSION_IDLE_TIMEOUT_MINUTES,
|
||||
MIN_SESSION_IDLE_TIMEOUT_MINUTES,
|
||||
MAX_SESSION_IDLE_TIMEOUT_MINUTES,
|
||||
normalizeSessionIdleTimeoutMinutes,
|
||||
createSessionIdleManager,
|
||||
};
|
||||
Reference in New Issue
Block a user