mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 14:09:42 +02:00
113 lines
5.7 KiB
TypeScript
113 lines
5.7 KiB
TypeScript
/**
|
|
* @fileoverview Bounds for the agent wait primitives.
|
|
*
|
|
* These back the blocking endpoints an agent uses to orchestrate other sessions
|
|
* (`GET /api/sessions/:id/wait`, `GET /api/sessions/:id/wait-output`, and the
|
|
* `wait` field on `POST /api/sessions/:id/input`). Plan: `docs/agent-control-plan.md`.
|
|
*
|
|
* Why every value is bounded:
|
|
* - An unbounded long-poll is a socket leak. A caller that asks for a 12-hour wait
|
|
* and walks away holds a connection (and a waiter, and a timer) until the process
|
|
* restarts, so `MAX_WAIT_MS` is a hard ceiling applied server-side.
|
|
* - `DEFAULT_WAIT_MS` is deliberately short (60s). Production is reached through
|
|
* `tailscale serve` and users also run cloudflared tunnels; both can cut an idle
|
|
* connection, so the documented pattern is a client-side loop over short waits
|
|
* rather than one very long call. Fastify itself is happy to hold the request
|
|
* (`requestTimeout` defaults to 0, and `keepAliveTimeout` applies between
|
|
* requests, not to an in-flight one), the intermediaries are the constraint.
|
|
* - The waiter caps mirror `MAX_SSE_CLIENTS` in `map-limits.ts`: each pending
|
|
* waiter costs an open HTTP response plus a timer, so the pool is capped rather
|
|
* than queued. Exceeding a cap is an explicit error, never a silent wait.
|
|
* - There are THREE caps, not two, because a process-wide pool with no per-user
|
|
* dimension lets one user deny the primitive to everyone else. `middleware/auth.ts`
|
|
* already treats that shape as a bug (its `userFailures` bucket exists so "one user
|
|
* behind a NAT can't lock out everyone else"); `MAX_WAITERS_PER_OWNER` is the same
|
|
* idea for waiters. It applies only when the caller has an owner, so single-user
|
|
* mode is byte-identical to having no owner cap at all.
|
|
*
|
|
* All values are env-overridable and clamped to sane hard bounds, so a typo in an
|
|
* env var degrades to the default instead of disabling the protection.
|
|
*
|
|
* @module config/agent-wait
|
|
*/
|
|
|
|
/** Absolute floor for any wait, in ms. Sub-second waits are polling, not waiting. */
|
|
export const MIN_WAIT_MS = 1_000;
|
|
|
|
/** Ceiling the operator-configurable maximum is itself clamped to. */
|
|
const HARD_MAX_WAIT_MS = 3_600_000;
|
|
|
|
function envInt(name: string, fallback: number, min: number, max: number): number {
|
|
const raw = parseInt(process.env[name] || '', 10);
|
|
if (!Number.isFinite(raw) || raw <= 0) return fallback;
|
|
return Math.max(min, Math.min(max, raw));
|
|
}
|
|
|
|
/** Longest a single wait may block. Requests above this are clamped down, not rejected. */
|
|
export const MAX_WAIT_MS = envInt('CODEMAN_WAIT_MAX_MS', 600_000, MIN_WAIT_MS, HARD_MAX_WAIT_MS);
|
|
|
|
/** Used when the caller omits `timeout`. Never exceeds MAX_WAIT_MS. */
|
|
export const DEFAULT_WAIT_MS = Math.min(
|
|
envInt('CODEMAN_WAIT_DEFAULT_MS', 60_000, MIN_WAIT_MS, HARD_MAX_WAIT_MS),
|
|
MAX_WAIT_MS
|
|
);
|
|
|
|
/** Concurrent waiters (signal + output) allowed against one session. */
|
|
export const MAX_WAITERS_PER_SESSION = envInt('CODEMAN_WAIT_MAX_PER_SESSION', 16, 1, 256);
|
|
|
|
/**
|
|
* Ceiling the operator-configurable total is itself clamped to.
|
|
*
|
|
* 512 rather than the 4096 this started at. Every other knob in this file degrades
|
|
* safely on a bad value; a 4096 ceiling instead lets a well-meaning operator turn the
|
|
* protection into the problem, since 4096 concurrent held responses (each an open
|
|
* socket, a timer and a pending promise) exceeds the 1024 soft `RLIMIT_NOFILE` that is
|
|
* still the default on most Linux distros, before counting PTYs, SSE clients and
|
|
* WebSockets. 512 is ~5x `MAX_SSE_CLIENTS` (100, the pool this one is modelled on), so
|
|
* the knob stays useful for a busy orchestration host while the whole server still fits
|
|
* inside a default fd budget with room to spare.
|
|
*/
|
|
const HARD_MAX_WAITERS_TOTAL = 512;
|
|
|
|
/** Concurrent waiters allowed across every session in the process. */
|
|
export const MAX_WAITERS_TOTAL = envInt('CODEMAN_WAIT_MAX_TOTAL', 128, 1, HARD_MAX_WAITERS_TOTAL);
|
|
|
|
/**
|
|
* Concurrent waiters allowed for one owner (multi-user mode's `Session.owner`).
|
|
*
|
|
* Sits between the per-session cap (16) and the process-wide one (128): high enough
|
|
* that one user orchestrating several workers at once never trips it, low enough that
|
|
* a single user cannot occupy the whole pool and deny the primitive to everyone else,
|
|
* admin included. Ignored entirely when the caller has no owner, which is every
|
|
* request in single-user mode.
|
|
*/
|
|
export const MAX_WAITERS_PER_OWNER = envInt('CODEMAN_WAIT_MAX_PER_OWNER', 48, 1, HARD_MAX_WAITERS_TOTAL);
|
|
|
|
/** Bounds on the literal `match` string accepted by wait-output. */
|
|
export const MIN_MATCH_LENGTH = 1;
|
|
export const MAX_MATCH_LENGTH = 200;
|
|
|
|
/**
|
|
* Tail of the terminal buffer scanned by `wait-output?from=buffer`.
|
|
*
|
|
* The buffer itself runs to 32MB. Scanning all of it would be an ANSI strip over
|
|
* 32MB (a full second copy) on a request an agent may issue in a loop, and the
|
|
* question `from=buffer` answers is "did this appear recently", not "ever". The
|
|
* tail is continuous with the live stream, since `_terminalBuffer.append(data)`
|
|
* and `emit('terminal', data)` receive the same bytes.
|
|
*/
|
|
export const MAX_BUFFER_SCAN_BYTES = envInt('CODEMAN_WAIT_BUFFER_SCAN_BYTES', 256 * 1024, 4 * 1024, 8 * 1024 * 1024);
|
|
|
|
/** Characters of surrounding output returned either side of a wait-output match. */
|
|
export const MAX_SNIPPET_CONTEXT = 80;
|
|
|
|
/**
|
|
* Clamp a caller-supplied timeout into [MIN_WAIT_MS, MAX_WAIT_MS].
|
|
* Absent / non-numeric / non-finite input falls back to DEFAULT_WAIT_MS.
|
|
*/
|
|
export function clampWaitMs(value: unknown): number {
|
|
const n = typeof value === 'string' ? Number(value) : value;
|
|
if (typeof n !== 'number' || !Number.isFinite(n)) return DEFAULT_WAIT_MS;
|
|
return Math.max(MIN_WAIT_MS, Math.min(MAX_WAIT_MS, Math.trunc(n)));
|
|
}
|