fix(limits): clamp CODEMAN_MAX_SESSIONS to 500 and document it

Review follow-up. Other bounds assume a small session count: an owner's
tab layout holds at most MAX_TAB_REFS (512) refs and PUT
/api/session-order accepts at most 500 ids, so with
CODEMAN_MAX_SESSIONS=600 the 513th session creation answered HTTP 500
instead of the capacity message. The value is now clamped to a named
MAX_CONCURRENT_SESSIONS_CEILING of 500; any digit string above it
(including one past the safe-integer range) resolves to 500 instead of
falling back to 50. The resolver reads process.env directly, the unused
env parameter is gone.

Docs: a CODEMAN_MAX_SESSIONS row in the Settings Reference environment
table, the env name next to the cap in the cron guide and the
architecture invariants, and both copies of the skill's endpoints
reference now give the per-user cap as half the global cap (25 by
default). The changeset says "1 to 500". The test drops the
`as unknown as string` casts and adds '1000', '501' and an
out-of-range value, each refusing at 500.

Co-Authored-By: Christian Haberl <116439789+christianhaberl@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
committed 2026-10-21 08:01:24 +02:00
1 parent 04eb1ed158
commit b073035bdc
9 files changed
+49 -33

No files matched your search

+16 -12
View File
@@ -18,10 +18,10 @@ function sessionsOf(n: number, owner?: string): ReadonlyMap<string, never> {
/** Fresh module graph, so map-limits re-reads the environment at import. */
async function loadWithEnv(env: Record<string, string | undefined>): Promise<Helpers> {
vi.resetModules();
vi.stubEnv('CODEMAN_MAX_SESSIONS', undefined as unknown as string);
vi.stubEnv('CODEMAN_MAX_SESSIONS_PER_USER', undefined as unknown as string);
vi.stubEnv('CODEMAN_MULTIUSER', undefined as unknown as string);
for (const [key, value] of Object.entries(env)) vi.stubEnv(key, value as string);
vi.stubEnv('CODEMAN_MAX_SESSIONS', undefined);
vi.stubEnv('CODEMAN_MAX_SESSIONS_PER_USER', undefined);
vi.stubEnv('CODEMAN_MULTIUSER', undefined);
for (const [key, value] of Object.entries(env)) vi.stubEnv(key, value);
return import('../../src/web/route-helpers.js');
}
@@ -52,14 +52,18 @@ describe('global session cap (CODEMAN_MAX_SESSIONS)', () => {
expect(sessionCapacityState(sessionsOf(3)).atGlobalCap).toBe(true);
});
it.each(['', '0', '-5', '2.5', 'abc', '1e3', '99999999999999999999'])(
'falls back to 50 for an unusable value %j',
async (value) => {
const { sessionCapacityState } = await loadWithEnv({ CODEMAN_MAX_SESSIONS: value });
expect(sessionCapacityState(sessionsOf(49)).atGlobalCap).toBe(false);
expect(sessionCapacityState(sessionsOf(50)).atGlobalCap).toBe(true);
}
);
it.each(['1000', '501', '99999999999999999999'])('clamps %j to the ceiling of 500', async (value) => {
const { sessionCapacityState, sessionCapacityMessage } = await loadWithEnv({ CODEMAN_MAX_SESSIONS: value });
expect(sessionCapacityState(sessionsOf(499)).atGlobalCap).toBe(false);
expect(sessionCapacityState(sessionsOf(500)).atGlobalCap).toBe(true);
expect(sessionCapacityMessage(sessionsOf(500))).toContain('(500)');
});
it.each(['', '0', '-5', '2.5', 'abc', '1e3'])('falls back to 50 for an unusable value %j', async (value) => {
const { sessionCapacityState } = await loadWithEnv({ CODEMAN_MAX_SESSIONS: value });
expect(sessionCapacityState(sessionsOf(49)).atGlobalCap).toBe(false);
expect(sessionCapacityState(sessionsOf(50)).atGlobalCap).toBe(true);
});
it('derives the multi-user per-user cap (half the global cap) from it', async () => {
const { sessionCapacityState, sessionCapacityMessage } = await loadWithEnv({