[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

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View 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,
};