@theoremjs/web 0.3.4-next.574.g44edd0f → 0.3.4-next.590.g0246066

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -378,26 +378,23 @@ Each saved outcome records its absolute retention deadline. Changing a handler's
378
378
  Each issued gate records its absolute expiry. Changing `gateTtlMs` does not extend an issued gate.
379
379
  Ready, running, and uncertain operations do not expire.
380
380
 
381
- Use `sessionLimits` when your storage budget requires different finite limits. The defaults are:
381
+ A session store declares its `capacity`: what it holds for one session, in bytes of UTF-8 JSON.
382
382
 
383
- | Limit | Default |
384
- | --- | --- |
385
- | `maxGates` | 32 |
386
- | `maxOperations` | 128 |
387
- | `maxCheckpoints` | 256 |
388
- | `maxPermissions` | 2,048 |
389
- | `maxStateBytes` | 32 MiB |
390
- | `maxInvocationBytes` | 256 KiB |
391
- | `maxOutcomeBytes` | 256 KiB |
383
+ | Field | What it bounds | Memory store default |
384
+ | --- | --- | --- |
385
+ | `maxStateBytes` | One session's saved state | 16 MiB |
386
+ | `maxOutcomeBytes` | One tool result | 1 MiB (`DEFAULT_MAX_OUTCOME_BYTES`) |
387
+
388
+ Pass `capacity` to `createMemorySessionStore` to change them. A store you write declares the capacity of its own storage.
392
389
 
393
390
  Set `maxRequestBytes` to bound browser JSON before parsing. Its default is 16 MiB.
394
391
  The handler rejects oversized requests before provider access or tool preparation.
395
392
  Provider host options also accept `limits` for deadlines and upstream output.
396
- Byte limits measure UTF-8 JSON. The handler reserves one maximum outcome for each gate and unresolved operation.
397
- A new claim fails before dispatch when it cannot reserve that space.
398
- An oversized result after dispatch leaves an uncertain claim. It does not restore an approval.
399
- Limits never remove unresolved authority or shorten retained settlements. The host must reconcile and archive unresolved actions.
400
- The memory store accepts the same `sessionLimits` options. Shared stores receive checked states through the handler.
393
+ The handler keeps room for one `maxOutcomeBytes` result for each gate and each call that has not settled.
394
+ When a session is full, the handler drops its oldest settled results, then its oldest provider checkpoints.
395
+ It never drops gates, permissions, or calls that have not settled. When those alone do not fit, the new call is refused before it runs, and the user reads `session.full`.
396
+ A result larger than `maxOutcomeBytes` settles as the call's `result_too_large` failure. The call is never left unresolved because of its size.
397
+ The handler fits each state to the store's capacity before `compareAndSwap`.
401
398
 
402
399
  The default memory store coordinates handlers that share the same store object.
403
400
  It retains unexpired gates, unresolved operations, and unexpired outcomes during idle and capacity eviction.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theoremjs/web",
3
- "version": "0.3.4-next.574.g44edd0f",
3
+ "version": "0.3.4-next.590.g0246066",
4
4
  "description": "Theorem on the web platform, with no UI framework: Web-standard server handlers that serve a profile, and the browser client that talks to them.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,6 +25,6 @@
25
25
  },
26
26
  "peerDependencies": {
27
27
  "zod": "^4.6.5",
28
- "@theoremjs/agents": "^0.3.4-next.574.g44edd0f"
28
+ "@theoremjs/agents": "^0.3.4-next.590.g0246066"
29
29
  }
30
30
  }
@@ -3,4 +3,4 @@ import type { TheoremSessionState } from './session-store.ts';
3
3
  export declare function checkpointIdentity(checkpoint: ProviderCheckpoint): Promise<string>;
4
4
  export declare function checkpointOwnershipKey(identity: string, operations: TheoremSessionState['operations']): Promise<string>;
5
5
  export declare function requireOwnedCheckpoint(identities: readonly string[], checkpoint: ProviderCheckpoint | undefined, operations?: TheoremSessionState['operations'], portableHistory?: readonly TurnHistoryMessage[]): Promise<ProviderCheckpoint | undefined>;
6
- export declare function retainedCheckpointIdentities(existing: readonly string[], added: readonly string[], limit: number): string[];
6
+ export declare function retainedCheckpointIdentities(existing: readonly string[], added: readonly string[]): string[];
@@ -37,6 +37,6 @@ export async function requireOwnedCheckpoint(identities, checkpoint, operations
37
37
  }
38
38
  return parsed;
39
39
  }
40
- export function retainedCheckpointIdentities(existing, added, limit) {
41
- return [...new Set([...existing, ...added].reverse())].slice(0, limit).reverse();
40
+ export function retainedCheckpointIdentities(existing, added) {
41
+ return [...new Set([...existing, ...added].reverse())].reverse();
42
42
  }
@@ -37,7 +37,6 @@ import { type ClientTurnOptions } from '@theoremjs/agents/host';
37
37
  import { type TheoremConversationStore } from './conversation-store.ts';
38
38
  import { type TheoremCredentialStore } from './credential-store.ts';
39
39
  export { theoremSession, theoremSessionId } from './session-identity.ts';
40
- import { type TheoremSessionLimits } from './session-limits.ts';
41
40
  import { type SessionToolOperation, type TheoremSessionStore } from './session-store.ts';
42
41
  import { type SteerInbox } from './steer-inbox.ts';
43
42
  export type TheoremRequestContext = {
@@ -97,8 +96,6 @@ export type TheoremHandlerOptions = {
97
96
  gateTtlMs?: number;
98
97
  /** Retain settled tool results in milliseconds. Default: 24 hours. */
99
98
  operationRetentionMs?: number;
100
- /** Finite per-session bounds. Unresolved authority is never evicted to satisfy them. */
101
- sessionLimits?: Partial<TheoremSessionLimits>;
102
99
  /** Maximum UTF-8 request body bytes before JSON parsing. Default: 16 MiB. */
103
100
  maxRequestBytes?: number;
104
101
  pageToolBroker?: PageToolBroker;
package/server/handler.js CHANGED
@@ -42,13 +42,12 @@ import { theoremConversationRequestSchema, theoremInvokeRequestSchema, theoremRe
42
42
  import { answerReply, conversationView, messageReply, openConversation, postedMessage, readerConversationHost, requiredOperationId, rewindConversation, statelessRefusal, waitingCalls, } from "./conversation.js";
43
43
  import { unknownConversation } from "./conversation-store.js";
44
44
  import { createMemoryCredentialStore, } from "./credential-store.js";
45
- import { sessionStateStore } from "./session-authority.js";
45
+ import { sessionStateStore, } from "./session-authority.js";
46
46
  import { theoremSession } from "./session-identity.js";
47
47
  export { theoremSession, theoremSessionId } from "./session-identity.js";
48
48
  import { jsonResponse } from "./json-response.js";
49
49
  import { boundedRequestJson, requestByteLimit } from "./request-body.js";
50
50
  import { checkRequest } from "./request-check.js";
51
- import { resolveSessionLimits } from "./session-limits.js";
52
51
  import { createMemorySessionStore, DEFAULT_OPERATION_RETENTION_MS, } from "./session-store.js";
53
52
  import { createMemorySteerInbox, steerStage, steerUnitOf, stopOnRequest, } from "./steer-inbox.js";
54
53
  import { sameAcceptedAnswer, sessionToolCoordinator } from "./tool-operations.js";
@@ -216,7 +215,8 @@ async function recordOutcome(sessions, sessionId, events, context) {
216
215
  for (const { gate } of pending) {
217
216
  const operation = state.operations[gate.operationId];
218
217
  if (operation && operation.status !== 'ready')
219
- throw new TheoremError('unavailable', labelText('error.unavailable'));
218
+ throw new TheoremError('unavailable', `Tool operation ${gate.operationId} is ${operation.status}, not ready, so its gate was not saved`, // labels-exempt: internal diagnostic; the user reads session.busy
219
+ { copy: { key: 'session.busy' } });
220
220
  delete state.operations[gate.operationId];
221
221
  Object.defineProperty(state.gates, gate.operationId, {
222
222
  value: gate,
@@ -773,7 +773,6 @@ function serve(name, profile, options, chatOptions = {}) {
773
773
  cause: { code: 'operation_retention' },
774
774
  });
775
775
  }
776
- const limits = resolveSessionLimits(options.sessionLimits);
777
776
  requestByteLimit(options.maxRequestBytes);
778
777
  registerProfile(profile);
779
778
  const ctx = {
@@ -782,7 +781,7 @@ function serve(name, profile, options, chatOptions = {}) {
782
781
  profile,
783
782
  ...(chatOptions.chat ? { chat: chatOptions.chat } : {}),
784
783
  inbox: chatOptions.steerInbox ?? createMemorySteerInbox(),
785
- sessions: sessionStateStore(options.sessionStore ?? createMemorySessionStore({ sessionLimits: limits }), gateTtlMs, operationRetentionMs, limits),
784
+ sessions: sessionStateStore(options.sessionStore ?? createMemorySessionStore(), gateTtlMs, operationRetentionMs),
786
785
  credentials: lockedStore(options.credentialStore ?? createMemoryCredentialStore(), (saved = {}) => saved),
787
786
  };
788
787
  return async (request) => {
package/server/mod.d.ts CHANGED
@@ -19,8 +19,8 @@ export { createMemoryPageToolBroker, type PageToolBroker, type PageToolResult, p
19
19
  export { checkRequest, parseLiveClientMessage, parseLiveOpenMessage } from './request-check.ts';
20
20
  export { sessionStateStore } from './session-authority.ts';
21
21
  export { createTheoremSessionBoundary, type PreparedLiveOpen, type SessionBoundaryOptions, type TheoremSessionBoundary, } from './session-boundary.ts';
22
- export type { TheoremSessionLimits } from './session-limits.ts';
23
- export { resolveSessionLimits } from './session-limits.ts';
22
+ export type { TheoremSessionCapacity } from './session-limits.ts';
23
+ export { DEFAULT_SESSION_CAPACITY } from './session-limits.ts';
24
24
  export { createMemorySessionStore, DEFAULT_OPERATION_RETENTION_MS, type MemorySessionStoreOptions, type PendingToolGate, type SessionSnapshot, type SessionToolOperation, type SessionWriteResult, type TheoremSessionState, type TheoremSessionStore, theoremSessionStateSchema, } from './session-store.ts';
25
25
  export { createMemorySteerInbox, type SteerInbox, type SteerUnit, steerStage, steerUnitOf, stopOnRequest, } from './steer-inbox.ts';
26
26
  export { sessionToolCoordinator } from './tool-operations.ts';
package/server/mod.js CHANGED
@@ -18,7 +18,7 @@ export { createMemoryPageToolBroker, pageToolResultSchema, sessionPageDispatch,
18
18
  export { checkRequest, parseLiveClientMessage, parseLiveOpenMessage } from "./request-check.js";
19
19
  export { sessionStateStore } from "./session-authority.js";
20
20
  export { createTheoremSessionBoundary, } from "./session-boundary.js";
21
- export { resolveSessionLimits } from "./session-limits.js";
21
+ export { DEFAULT_SESSION_CAPACITY } from "./session-limits.js";
22
22
  export { createMemorySessionStore, DEFAULT_OPERATION_RETENTION_MS, theoremSessionStateSchema, } from "./session-store.js";
23
23
  export { createMemorySteerInbox, steerStage, steerUnitOf, stopOnRequest, } from "./steer-inbox.js";
24
24
  export { sessionToolCoordinator } from "./tool-operations.js";
@@ -1,8 +1,12 @@
1
- import { type TheoremSessionLimits } from './session-limits.ts';
1
+ import { type TheoremSessionCapacity } from './session-limits.ts';
2
2
  import { type TheoremSessionState, type TheoremSessionStore } from './session-store.ts';
3
3
  export type MutableStore<T> = {
4
4
  read(sessionId: string): Promise<T>;
5
5
  mutate<R>(sessionId: string, change: (value: T) => R | Promise<R>): Promise<R>;
6
6
  };
7
+ /** Session state with the capacity its store declared. */
8
+ export type SessionStateStore = MutableStore<TheoremSessionState> & {
9
+ readonly capacity: TheoremSessionCapacity;
10
+ };
7
11
  /** Session state, with expired gates dropped as it loads so they can't be answered. */
8
- export declare function sessionStateStore(store: TheoremSessionStore, gateTtlMs: number, operationRetentionMs: number, limits: TheoremSessionLimits): MutableStore<TheoremSessionState>;
12
+ export declare function sessionStateStore(store: TheoremSessionStore, gateTtlMs: number, operationRetentionMs: number): SessionStateStore;
@@ -1,13 +1,14 @@
1
1
  import { labelText, TheoremError, z } from '@theoremjs/agents';
2
- import { checkSessionLimits } from "./session-limits.js";
2
+ import { checkedCapacity, fitSession } from "./session-limits.js";
3
3
  import { emptySessionState, pruneGates, theoremSessionStateSchema, } from "./session-store.js";
4
4
  /** Session state, with expired gates dropped as it loads so they can't be answered. */
5
- export function sessionStateStore(store, gateTtlMs, operationRetentionMs, limits) {
5
+ export function sessionStateStore(store, gateTtlMs, operationRetentionMs) {
6
6
  if (typeof store.compareAndSwap !== 'function') {
7
7
  throw new TheoremError('config', labelText('error.config'), {
8
8
  cause: { code: 'session_store_contract', required: 'compareAndSwap' },
9
9
  });
10
10
  }
11
+ const capacity = checkedCapacity(store.capacity);
11
12
  const writeResult = z.discriminatedUnion('written', [
12
13
  z.strictObject({ written: z.literal(false) }),
13
14
  z.strictObject({ written: z.literal(true), revision: z.string().min(1) }),
@@ -34,6 +35,7 @@ export function sessionStateStore(store, gateTtlMs, operationRetentionMs, limits
34
35
  return { revision: snapshot?.revision ?? null, state };
35
36
  };
36
37
  return {
38
+ capacity,
37
39
  async read(sessionId) {
38
40
  return (await load(sessionId, Date.now())).state;
39
41
  },
@@ -48,7 +50,7 @@ export function sessionStateStore(store, gateTtlMs, operationRetentionMs, limits
48
50
  if (operation.status === 'settled')
49
51
  operation.retainUntil ??= operation.settledAt + operationRetentionMs;
50
52
  }
51
- checkSessionLimits(state, limits);
53
+ fitSession(state, capacity);
52
54
  const next = theoremSessionStateSchema.safeParse(state);
53
55
  if (!next.success) {
54
56
  throw new TheoremError('config', labelText('error.config'), {
@@ -70,9 +72,8 @@ export function sessionStateStore(store, gateTtlMs, operationRetentionMs, limits
70
72
  return result;
71
73
  }
72
74
  }
73
- throw new TheoremError('unavailable', labelText('error.unavailable'), {
74
- cause: { code: 'session_store_contention' },
75
- });
75
+ throw new TheoremError('unavailable', 'Other writes changed the session on each of 8 tries, so this change was not saved', // labels-exempt: internal diagnostic; the user reads session.busy
76
+ { copy: { key: 'session.busy' }, cause: { code: 'session_store_contention' } });
76
77
  },
77
78
  };
78
79
  }
@@ -1,7 +1,6 @@
1
1
  import { type PageToolExecution, type ProviderCheckpoint, type SessionRequest, type ToolExecutionCoordinator, type TurnHistoryMessage } from '@theoremjs/agents';
2
2
  import { type LiveOpenMessage } from '../client/live-messages.ts';
3
3
  import { type PageToolBroker, type PageToolResult } from './page-dispatch.ts';
4
- import { type TheoremSessionLimits } from './session-limits.ts';
5
4
  import { type TheoremSessionStore } from './session-store.ts';
6
5
  export interface SessionBoundaryOptions {
7
6
  request: Request;
@@ -9,7 +8,6 @@ export interface SessionBoundaryOptions {
9
8
  sessionStore: TheoremSessionStore;
10
9
  gateTtlMs?: number;
11
10
  operationRetentionMs?: number;
12
- sessionLimits?: Partial<TheoremSessionLimits>;
13
11
  pageToolBroker?: PageToolBroker;
14
12
  }
15
13
  export type PreparedLiveOpen = Pick<SessionRequest, 'history' | 'slots' | 'context' | 'providerState' | 'awayMs'>;
@@ -6,7 +6,6 @@ import { createMemoryPageToolBroker, sessionPageDispatch, } from "./page-dispatc
6
6
  import { checkRequest } from "./request-check.js";
7
7
  import { sessionStateStore } from "./session-authority.js";
8
8
  import { theoremSession } from "./session-identity.js";
9
- import { resolveSessionLimits } from "./session-limits.js";
10
9
  import { DEFAULT_OPERATION_RETENTION_MS } from "./session-store.js";
11
10
  import { sessionToolCoordinator } from "./tool-operations.js";
12
11
  import { liveSessionOpen } from "./turn-input.js";
@@ -20,8 +19,7 @@ export async function createTheoremSessionBoundary(options) {
20
19
  const retention = options.operationRetentionMs ?? DEFAULT_OPERATION_RETENTION_MS;
21
20
  if (!Number.isSafeInteger(retention) || retention <= 0)
22
21
  throw new TheoremError('config', labelText('error.config'));
23
- const limits = resolveSessionLimits(options.sessionLimits);
24
- const store = sessionStateStore(options.sessionStore, resolveGateTtlMs('session boundary', options.gateTtlMs), retention, limits);
22
+ const store = sessionStateStore(options.sessionStore, resolveGateTtlMs('session boundary', options.gateTtlMs), retention);
25
23
  // invariant: Refuse unavailable or malformed authority before an upstream connection opens.
26
24
  await store.read(id);
27
25
  const page = sessionPageDispatch(store, id, options.pageToolBroker ?? createMemoryPageToolBroker());
@@ -56,7 +54,9 @@ export async function createTheoremSessionBoundary(options) {
56
54
  async recordCheckpoint(checkpoint) {
57
55
  const identity = await checkpointIdentity(checkpoint);
58
56
  await store.mutate(id, async (state) => {
59
- state.providerCheckpoints = retainedCheckpointIdentities(state.providerCheckpoints, [await checkpointOwnershipKey(identity, state.operations)], limits.maxCheckpoints);
57
+ state.providerCheckpoints = retainedCheckpointIdentities(state.providerCheckpoints, [
58
+ await checkpointOwnershipKey(identity, state.operations),
59
+ ]);
60
60
  });
61
61
  },
62
62
  };
@@ -1,12 +1,19 @@
1
1
  import type { TheoremSessionState } from './session-store.ts';
2
- export interface TheoremSessionLimits {
3
- maxGates: number;
4
- maxOperations: number;
5
- maxCheckpoints: number;
6
- maxPermissions: number;
2
+ /** What a session store holds for one session, in bytes of UTF-8 JSON. The store declares it. */
3
+ export interface TheoremSessionCapacity {
4
+ /** The most one session's saved state may take. */
7
5
  maxStateBytes: number;
8
- maxInvocationBytes: number;
6
+ /** The most one tool result may take. Room for one is kept for each call that has not settled. */
9
7
  maxOutcomeBytes: number;
10
8
  }
11
- export declare function resolveSessionLimits(options?: Partial<TheoremSessionLimits>): TheoremSessionLimits;
12
- export declare function checkSessionLimits(state: TheoremSessionState, limits: TheoremSessionLimits): void;
9
+ /** The memory store's default: room for sixteen full-size tool results in each session. */
10
+ export declare const DEFAULT_SESSION_CAPACITY: TheoremSessionCapacity;
11
+ /** The store's declared capacity, refused as `config` unless both are positive and one result fits. */
12
+ export declare function checkedCapacity(capacity: unknown): TheoremSessionCapacity;
13
+ /**
14
+ * Fits the state in the store's capacity: drops the oldest settled results, then the oldest
15
+ * provider checkpoints, and refuses with `session.full` when open work alone does not fit.
16
+ *
17
+ * invariant: Gates, permissions and calls that have not settled are never dropped to make room.
18
+ */
19
+ export declare function fitSession(state: TheoremSessionState, capacity: TheoremSessionCapacity): void;
@@ -1,59 +1,57 @@
1
- import { labelText, TheoremError } from '@theoremjs/agents';
2
- const DEFAULTS = {
3
- maxGates: 32,
4
- maxOperations: 128,
5
- maxCheckpoints: 256,
6
- maxPermissions: 2048,
7
- maxStateBytes: 32 * 1024 * 1024,
8
- maxInvocationBytes: 256 * 1024,
9
- maxOutcomeBytes: 256 * 1024,
1
+ import { DEFAULT_MAX_OUTCOME_BYTES, labelText, TheoremError } from '@theoremjs/agents';
2
+ /** The memory store's default: room for sixteen full-size tool results in each session. */
3
+ export const DEFAULT_SESSION_CAPACITY = {
4
+ maxStateBytes: 16 * DEFAULT_MAX_OUTCOME_BYTES,
5
+ maxOutcomeBytes: DEFAULT_MAX_OUTCOME_BYTES,
10
6
  };
11
- export function resolveSessionLimits(options = {}) {
12
- const limits = { ...DEFAULTS, ...options };
13
- if (Object.keys(options).some((key) => !Object.hasOwn(DEFAULTS, key)) ||
14
- Object.values(limits).some((value) => !Number.isSafeInteger(value) || value <= 0)) {
7
+ /** The store's declared capacity, refused as `config` unless both are positive and one result fits. */
8
+ export function checkedCapacity(capacity) {
9
+ const { maxStateBytes, maxOutcomeBytes } = (capacity ?? {});
10
+ if (!Number.isSafeInteger(maxStateBytes) ||
11
+ !Number.isSafeInteger(maxOutcomeBytes) ||
12
+ maxOutcomeBytes <= 0 ||
13
+ maxStateBytes < maxOutcomeBytes) {
15
14
  throw new TheoremError('config', labelText('error.config'), {
16
- cause: { code: 'session_limits' },
15
+ cause: { code: 'session_store_capacity_declared' },
17
16
  });
18
17
  }
19
- return limits;
18
+ return { maxStateBytes: maxStateBytes, maxOutcomeBytes: maxOutcomeBytes };
20
19
  }
21
20
  function bytes(value) {
22
21
  return new TextEncoder().encode(JSON.stringify(value)).byteLength;
23
22
  }
24
- function capacity() {
25
- throw new TheoremError('unavailable', labelText('error.unavailable'), {
26
- cause: { code: 'session_state_capacity' },
27
- });
23
+ /** The bytes the state takes with room kept for the result of each gate and each call that has not settled. */
24
+ function needed(state, capacity) {
25
+ const open = Object.keys(state.gates).length +
26
+ Object.values(state.operations).filter((operation) => operation.status !== 'settled').length;
27
+ return bytes(state) + open * capacity.maxOutcomeBytes;
28
28
  }
29
- export function checkSessionLimits(state, limits) {
30
- const gates = Object.values(state.gates);
31
- const operations = Object.values(state.operations);
32
- if (gates.length > limits.maxGates ||
33
- operations.length > limits.maxOperations ||
34
- state.providerCheckpoints.length > limits.maxCheckpoints ||
35
- state.permissions.length > limits.maxPermissions)
36
- capacity();
37
- for (const gate of gates)
38
- if (bytes(gate) > limits.maxInvocationBytes)
39
- capacity();
40
- let reserved = gates.length * limits.maxOutcomeBytes;
41
- for (const operation of operations) {
42
- if (operation.status !== 'ready' &&
43
- operation.invocation &&
44
- bytes(operation.invocation) > limits.maxInvocationBytes)
45
- capacity();
46
- if (operation.status === 'settled') {
47
- if (bytes(operation.outcome) > limits.maxOutcomeBytes)
48
- capacity();
49
- }
50
- else {
51
- reserved += limits.maxOutcomeBytes;
52
- if (operation.status === 'ready' && bytes(operation) > limits.maxInvocationBytes)
53
- capacity();
54
- }
29
+ /**
30
+ * Fits the state in the store's capacity: drops the oldest settled results, then the oldest
31
+ * provider checkpoints, and refuses with `session.full` when open work alone does not fit.
32
+ *
33
+ * invariant: Gates, permissions and calls that have not settled are never dropped to make room.
34
+ */
35
+ export function fitSession(state, capacity) {
36
+ if (needed(state, capacity) <= capacity.maxStateBytes)
37
+ return;
38
+ const settled = Object.values(state.operations)
39
+ .flatMap((operation) => (operation.status === 'settled' ? [operation] : []))
40
+ .sort((a, b) => a.settledAt - b.settledAt);
41
+ for (const operation of settled) {
42
+ delete state.operations[operation.operationId];
43
+ if (needed(state, capacity) <= capacity.maxStateBytes)
44
+ return;
45
+ }
46
+ while (state.providerCheckpoints.length) {
47
+ state.providerCheckpoints.shift();
48
+ if (needed(state, capacity) <= capacity.maxStateBytes)
49
+ return;
55
50
  }
56
- // invariant: Reserve terminal evidence before dispatch; unresolved records are never evicted to make room.
57
- if (bytes(state) + reserved > limits.maxStateBytes)
58
- capacity();
51
+ const need = needed(state, capacity);
52
+ throw new TheoremError('unavailable', `The session's open work needs ${need} bytes with room for its results, more than the store's maxStateBytes of ${capacity.maxStateBytes}`, // labels-exempt: internal diagnostic; the user reads session.full
53
+ {
54
+ copy: { key: 'session.full' },
55
+ cause: { code: 'session_state_capacity', need, maxStateBytes: capacity.maxStateBytes },
56
+ });
59
57
  }
@@ -11,7 +11,7 @@
11
11
  import { z } from '@theoremjs/agents';
12
12
  import type { ToolGateAuth } from '@theoremjs/agents/interface';
13
13
  import type { AnsweredGate, ToolGate, ToolId, ToolOperationIdentity, ToolOperationOutcome, ToolOperationUncertainty, TurnInput, TurnToolSnapshot } from '@theoremjs/agents/kernel';
14
- import { type TheoremSessionLimits } from './session-limits.ts';
14
+ import { type TheoremSessionCapacity } from './session-limits.ts';
15
15
  /** A tool call the kernel paused on a gate, as the server saw it. */
16
16
  export type PendingToolGate = {
17
17
  profileId: string;
@@ -88,6 +88,8 @@ export type SessionWriteResult = {
88
88
  * store (KV, Redis, a database row) when requests can reach different instances.
89
89
  */
90
90
  export interface TheoremSessionStore {
91
+ /** What it holds for one session. The handler fits each write to it before `compareAndSwap`. */
92
+ readonly capacity: TheoremSessionCapacity;
91
93
  load(sessionId: string): Promise<SessionSnapshot | undefined>;
92
94
  /** Compare and replace atomically; null creates only an absent session. */
93
95
  compareAndSwap(sessionId: string, expectedRevision: string | null, next: TheoremSessionState): Promise<SessionWriteResult>;
@@ -99,8 +101,8 @@ export declare function pruneGates<Gate extends {
99
101
  expiresAt?: number;
100
102
  }>(gates: Record<string, Gate>, now: number, ttlMs: number): Record<string, Gate>;
101
103
  export type MemorySessionStoreOptions = {
102
- /** Finite authority storage bounds. */
103
- sessionLimits?: Partial<TheoremSessionLimits>;
104
+ /** What it holds for one session. Default {@linkcode DEFAULT_SESSION_CAPACITY}. */
105
+ capacity?: Partial<TheoremSessionCapacity>;
104
106
  /** invariant: Idle retention is in milliseconds. Protected authority remains. Default 24h. */
105
107
  ttlMs?: number;
106
108
  /** Oldest sessions are evicted past this count. Default 10 000. */
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { jsonObjectSchema, jsonValueSchema, labelText, savedToolInvocationSchema, TheoremError, TOOL_AUTH_TYPES, TOOL_GATE_KINDS, TOOL_PERMISSION, toolOperationOutcomeSchema, turnHistoryMessageSchema, turnToolSnapshotSchema, z, } from '@theoremjs/agents';
12
12
  import { gateExpired } from '@theoremjs/agents/kernel';
13
- import { checkSessionLimits, resolveSessionLimits, } from "./session-limits.js";
13
+ import { checkedCapacity, DEFAULT_SESSION_CAPACITY, } from "./session-limits.js";
14
14
  export const DEFAULT_OPERATION_RETENTION_MS = 24 * 60 * 60 * 1000;
15
15
  const blobSchema = z.strictObject({
16
16
  data: z.string(),
@@ -219,7 +219,7 @@ export function createMemorySessionMap(options = {}) {
219
219
  }
220
220
  export function createMemorySessionStore(options = {}) {
221
221
  const { ttlMs, maxSessions } = memoryStoreLimits(options);
222
- const limits = resolveSessionLimits(options.sessionLimits);
222
+ const capacity = checkedCapacity({ ...DEFAULT_SESSION_CAPACITY, ...options.capacity });
223
223
  const sessions = new Map();
224
224
  const held = (entry) => Object.values(entry.snapshot.state.gates).some((gate) => gate.expiresAt === undefined || Date.now() < gate.expiresAt) ||
225
225
  Object.values(entry.snapshot.state.operations).some((operation) => operation.status !== 'settled' ||
@@ -234,6 +234,7 @@ export function createMemorySessionStore(options = {}) {
234
234
  return entry?.snapshot;
235
235
  };
236
236
  return {
237
+ capacity,
237
238
  load(sessionId) {
238
239
  return Promise.resolve(structuredClone(current(sessionId)));
239
240
  },
@@ -242,13 +243,11 @@ export function createMemorySessionStore(options = {}) {
242
243
  if ((snapshot?.revision ?? null) !== expectedRevision)
243
244
  return Promise.resolve({ written: false });
244
245
  const state = theoremSessionStateSchema.parse(next);
245
- checkSessionLimits(state, limits);
246
246
  if (!snapshot && sessions.size >= maxSessions) {
247
247
  const removable = [...sessions].find(([, entry]) => !held(entry));
248
248
  if (!removable) {
249
- throw new TheoremError('unavailable', labelText('error.unavailable'), {
250
- cause: { code: 'session_store_capacity' },
251
- });
249
+ throw new TheoremError('unavailable', `The memory session store holds ${maxSessions} sessions and each has work in progress, so a new one was not saved`, // labels-exempt: internal diagnostic; the user reads session.busy
250
+ { copy: { key: 'session.busy' }, cause: { code: 'session_store_capacity' } });
252
251
  }
253
252
  sessions.delete(removable[0]);
254
253
  }
@@ -1,6 +1,9 @@
1
1
  import { type ToolExecutionCoordinator } from '@theoremjs/agents';
2
2
  import type { TheoremSessionState } from './session-store.ts';
3
3
  export declare function sessionToolCoordinator(store: {
4
+ readonly capacity: {
5
+ maxOutcomeBytes: number;
6
+ };
4
7
  mutate<T>(id: string, change: (state: TheoremSessionState) => T): Promise<T>;
5
8
  }, sessionId: string): ToolExecutionCoordinator;
6
9
  export declare function sameAcceptedAnswer(left: unknown, right: unknown): boolean;
@@ -1,6 +1,7 @@
1
- import { labelText, TheoremError, } from '@theoremjs/agents';
2
- function unavailable() {
3
- throw new TheoremError('unavailable', labelText('error.unavailable'));
1
+ import { TheoremError, } from '@theoremjs/agents';
2
+ /** The session's record of a tool operation does not match the call: the user reads `tool.execution_uncertain`. */
3
+ function unavailable(reason) {
4
+ throw new TheoremError('unavailable', reason, { copy: { key: 'tool.execution_uncertain' } });
4
5
  }
5
6
  function sameOperation(record, identity) {
6
7
  return (record.operationId === identity.operationId &&
@@ -10,8 +11,9 @@ function sameOperation(record, identity) {
10
11
  record.invocationHash === identity.invocationHash);
11
12
  }
12
13
  function checkedRecord(state, identity) {
14
+ // labels-exempt: internal diagnostic; the user reads tool.execution_uncertain
13
15
  if (identity.operationId === '__proto__')
14
- unavailable();
16
+ unavailable('The tool operation id is __proto__');
15
17
  const record = Object.hasOwn(state.operations, identity.operationId)
16
18
  ? state.operations[identity.operationId]
17
19
  : undefined;
@@ -20,10 +22,12 @@ function checkedRecord(state, identity) {
20
22
  record.pending.profileId !== identity.profileId ||
21
23
  record.pending.name !== identity.toolId ||
22
24
  record.profileId !== identity.profileId)
23
- unavailable();
25
+ // labels-exempt: internal diagnostic; the user reads tool.execution_uncertain
26
+ unavailable(`The ready tool operation ${identity.operationId} is for another call`);
24
27
  }
25
28
  else if (record && !sameOperation(record, identity))
26
- unavailable();
29
+ // labels-exempt: internal diagnostic; the user reads tool.execution_uncertain
30
+ unavailable(`The session holds tool operation ${identity.operationId} for another call`);
27
31
  return record;
28
32
  }
29
33
  function checkedClaim(state, claim) {
@@ -31,7 +35,9 @@ function checkedClaim(state, claim) {
31
35
  if (!record ||
32
36
  (record.status !== 'running' && record.status !== 'uncertain') ||
33
37
  record.claimToken !== claim.claimToken)
34
- unavailable();
38
+ unavailable(
39
+ // labels-exempt: internal diagnostic; the user reads tool.execution_uncertain
40
+ `The claim on tool operation ${claim.operationId} is not the session's running claim`);
35
41
  return record;
36
42
  }
37
43
  function existingResult(record) {
@@ -63,6 +69,7 @@ function saveOutcome(state, identity, outcome) {
63
69
  }
64
70
  export function sessionToolCoordinator(store, sessionId) {
65
71
  return {
72
+ maxOutcomeBytes: store.capacity.maxOutcomeBytes,
66
73
  lookup(identity) {
67
74
  return store.mutate(sessionId, (state) => {
68
75
  const record = checkedRecord(state, identity);
@@ -96,9 +103,13 @@ export function sessionToolCoordinator(store, sessionId) {
96
103
  return store.mutate(sessionId, (state) => {
97
104
  const record = checkedRecord(state, identity);
98
105
  if (record && record.status !== 'ready')
99
- unavailable();
106
+ unavailable(
107
+ // labels-exempt: internal diagnostic; the user reads tool.execution_uncertain
108
+ `Tool operation ${identity.operationId} is ${record.status}, so it cannot settle as not started`);
100
109
  if (outcome.effects !== 'not_started')
101
- unavailable();
110
+ unavailable(
111
+ // labels-exempt: internal diagnostic; the user reads tool.execution_uncertain
112
+ `A not-started settlement of ${identity.operationId} has effects ${outcome.effects}`);
102
113
  saveOutcome(state, identity, outcome);
103
114
  });
104
115
  },