@theoremjs/web 0.3.4-next.604.ge4d5866 → 0.3.4-next.608.gd007c52

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
@@ -243,10 +243,26 @@ const transport = withPageToolDispatch(
243
243
  | `sessionStore`, `steerInbox` | Where open gates are kept, and where steers and stops reach a running turn. Default: process memory, which reaches only turns this process runs. On more than one instance, give a shared `SteerInbox`. |
244
244
  | `credentialStore` | Where tool credentials are kept, by session. Default: process memory. |
245
245
  | `authorizationUrl` | Returns the sign-in URL for an OAuth gate. Without it, an OAuth gate has no URL. |
246
- | `gateTtlMs` | How long a gate waits for an answer. Default: 30 minutes. A later answer fails with `session.gate_expired`. |
246
+ | `gateTtlMs` | How long a gate waits for an answer. A later answer fails with `session.gate_expired`. Unset, a gate waits until it is answered. |
247
+ | `maxRequestBytes` | The largest browser request body, in bytes. Unset, a body of any size is read. |
248
+ | `pageToolTimeoutMs` | How long the page has to answer a tool call it runs. A call it has not answered by then settles as timed out. Unset, the call waits until the page answers or the turn stops. |
249
+ | `operationRetentionMs` | How long a settled tool result is kept. Default: 24 hours. |
247
250
  | `clientEvents` | Options for `forClient` and `clientError`, which prepare each event and each caught failure for the browser. `reader: 'builder'` sends the raw reason in place of the wording of the profile. Use it only in a development tool on your own machine. |
248
251
  | `onError` | Receives each error that the handler catches. A visitor sees the wording of the profile for that kind of error, never the error. |
249
252
 
253
+ ### Defaults
254
+
255
+ A limit that stops a turn or refuses a request is unset unless you set it: `gateTtlMs`, `maxRequestBytes`, `pageToolTimeoutMs`, and a session store's `capacity`. Only what keeps stored data from growing without end has a default:
256
+
257
+ | What | Default | Set it with |
258
+ | --- | --- | --- |
259
+ | A settled tool result | Kept 24 hours | `operationRetentionMs` |
260
+ | A session in `createMemorySessionStore` | Dropped after 24 idle hours; at most 10,000 kept | `ttlMs`, `maxSessions` |
261
+ | A conversation in `createMemoryConversationStore` | Dropped after 24 hours unsaved; at most 10,000 kept | `ttlMs`, `maxSessions` |
262
+ | The session cookie | Lasts 30 days | `session` |
263
+
264
+ A model profile that leaves out `maxSteps` makes at most 20 model calls a turn.
265
+
250
266
  ### Start a conversation with content
251
267
 
252
268
  `startConversation` stores a conversation that opens on blocks you wrote, and returns its ref and its view. The model reads the blocks as history, so the user's next message is a follow-up.
@@ -388,7 +404,7 @@ A session store with a real limit declares its `capacity`: what it holds for one
388
404
 
389
405
  A store that declares no `capacity` holds any size. `createMemorySessionStore` declares none unless you pass `capacity`, with both fields. A store you write declares the capacity of its own storage, if it has one.
390
406
 
391
- Set `maxRequestBytes` to bound browser JSON before parsing. Its default is 16 MiB.
407
+ Set `maxRequestBytes` to bound browser JSON before parsing. Unset, a body of any size is read. A relay passes the same number to `parseLiveOpenMessage` and `parseLiveClientMessage`.
392
408
  The handler rejects oversized requests before provider access or tool preparation.
393
409
  Provider host options also accept `limits` for deadlines and upstream output.
394
410
  When the store declares a capacity, the handler keeps room for one `maxOutcomeBytes` result for each gate and each call that has not settled.
@@ -413,7 +429,7 @@ Forward live page results to `answerPageResult`. Preserve checkpoint ownership t
413
429
  The default `PageToolBroker` keeps active result waiters in process memory. Several workers need a shared broker or request affinity.
414
430
  Use `pageToolBroker` to supply that broker. It carries results; the atomic session store retains execution authority.
415
431
  Lost result delivery does not permit another dispatch. The operation retains its claim or an uncertain outcome.
416
- The default result deadline is 20 seconds. An expired or interrupted result cannot reopen approval.
432
+ Set `pageToolTimeoutMs` to give the page a deadline for a result. Unset, a dispatch waits until the page answers or the turn stops. An expired or interrupted result cannot reopen approval.
417
433
 
418
434
  ## Build your own server
419
435
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theoremjs/web",
3
- "version": "0.3.4-next.604.ge4d5866",
3
+ "version": "0.3.4-next.608.gd007c52",
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.604.ge4d5866"
28
+ "@theoremjs/agents": "^0.3.4-next.608.gd007c52"
29
29
  }
30
30
  }
@@ -97,14 +97,19 @@ export type TheoremHandlerOptions = {
97
97
  /**
98
98
  * How long a gate (permission, confirmation, sign-in) waits for its answer, in
99
99
  * milliseconds. An answer after that is refused with `session.gate_expired`.
100
- * Default: 30 minutes.
100
+ * Unset, a gate waits until it is answered.
101
101
  */
102
102
  gateTtlMs?: number;
103
103
  /** Retain settled tool results in milliseconds. Default: 24 hours. */
104
104
  operationRetentionMs?: number;
105
- /** Maximum UTF-8 request body bytes before JSON parsing. Default: 16 MiB. */
105
+ /** Maximum UTF-8 request body bytes before JSON parsing. Unset, a body of any size is read. */
106
106
  maxRequestBytes?: number;
107
107
  pageToolBroker?: PageToolBroker;
108
+ /**
109
+ * How long the page has to answer a tool call it runs, in milliseconds. A call it has not
110
+ * answered by then settles as timed out. Unset, it waits until the page answers or the turn stops.
111
+ */
112
+ pageToolTimeoutMs?: number;
108
113
  /** Check current resource access before disclosing a saved tool result. This callback must be read-only. */
109
114
  authorizeToolReplay?: (context: {
110
115
  request: Request;
package/server/handler.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { withPublicLabels } from '@theoremjs/agents/guardrails';
2
- import { createMemoryPageToolBroker, pageToolResultSchema, sessionPageDispatch, } from "./page-dispatch.js";
2
+ import { checkPageToolTimeout, createMemoryPageToolBroker, pageToolResultSchema, sessionPageDispatch, } from "./page-dispatch.js";
3
3
  /**
4
4
  * One Web-standard handler that serves a Theorem chat UI for a single profile.
5
5
  *
@@ -368,7 +368,7 @@ async function* invokeAnswered(ctx, request, sessionId, { callId, pending, answe
368
368
  yield* invokeTool({
369
369
  profile: ctx.profile.id,
370
370
  execution: sessionToolCoordinator(ctx.sessions, sessionId),
371
- pageExecution: sessionPageDispatch(ctx.sessions, sessionId, ctx.pageToolBroker).execution,
371
+ pageExecution: sessionPageDispatch(ctx.sessions, sessionId, ctx.pageToolBroker, ctx.options.pageToolTimeoutMs).execution,
372
372
  name: pending.name,
373
373
  callId,
374
374
  operationId: pending.operationId,
@@ -407,7 +407,7 @@ async function* chatTurn(ctx, request, session, turn, key, stop) {
407
407
  const prepared = await preparedTurn(ctx.options.prepare, {
408
408
  profile: ctx.profile.id,
409
409
  execution: sessionToolCoordinator(ctx.sessions, session.id),
410
- pageExecution: sessionPageDispatch(ctx.sessions, session.id, ctx.pageToolBroker).execution,
410
+ pageExecution: sessionPageDispatch(ctx.sessions, session.id, ctx.pageToolBroker, ctx.options.pageToolTimeoutMs).execution,
411
411
  input: turn.input,
412
412
  ...(turn.providerState ? { providerState: turn.providerState } : {}),
413
413
  sessionPermissions: state.permissions,
@@ -574,7 +574,7 @@ async function* callEvents(ctx, request, session, body) {
574
574
  const events = invokeTool({
575
575
  profile: ctx.profile.id,
576
576
  execution: sessionToolCoordinator(ctx.sessions, session.id),
577
- pageExecution: sessionPageDispatch(ctx.sessions, session.id, ctx.pageToolBroker).execution,
577
+ pageExecution: sessionPageDispatch(ctx.sessions, session.id, ctx.pageToolBroker, ctx.options.pageToolTimeoutMs).execution,
578
578
  name: body.name,
579
579
  input: body.input,
580
580
  sessionPermissions: state.permissions,
@@ -691,7 +691,7 @@ async function hostRoute(ctx, request, session) {
691
691
  async function route(ctx, request, session) {
692
692
  if (request.method === 'POST' && routeOf(request) === 'page-result') {
693
693
  const body = await readBody(request, pageToolResultSchema, ctx.options.maxRequestBytes);
694
- await sessionPageDispatch(ctx.sessions, session.id, ctx.pageToolBroker).answer(body);
694
+ await sessionPageDispatch(ctx.sessions, session.id, ctx.pageToolBroker, ctx.options.pageToolTimeoutMs).answer(body);
695
695
  return jsonResponse(200, { accepted: true });
696
696
  }
697
697
  if (ctx.profile.type === 'host')
@@ -779,6 +779,7 @@ function serve(name, profile, options, chatOptions = {}) {
779
779
  });
780
780
  }
781
781
  requestByteLimit(options.maxRequestBytes);
782
+ checkPageToolTimeout(options.pageToolTimeoutMs);
782
783
  registerProfile(profile);
783
784
  const ctx = {
784
785
  options,
@@ -30,7 +30,7 @@ export type TheoremMcpHandlerOptions = {
30
30
  origins?: readonly string[];
31
31
  /** Handed to each tool as `ctx.host`. */
32
32
  host?: (request: Request) => unknown;
33
- /** Largest request body read, in bytes. */
33
+ /** Largest request body read, in bytes. Unset, a body of any size is read. */
34
34
  maxRequestBytes?: number;
35
35
  /** Who reads this host's replies: forwarded to `clientError` for a failure the handler catches. */
36
36
  clientEvents?: ClientTurnOptions;
@@ -16,6 +16,8 @@ export interface PageToolBroker {
16
16
  deliver(sessionId: string, grantId: string, page: PageAnswer): Promise<void>;
17
17
  }
18
18
  export declare function createMemoryPageToolBroker(maxPending?: number): PageToolBroker;
19
+ /** Refuses a page tool timeout that is not a whole number of milliseconds a timer can hold. */
20
+ export declare function checkPageToolTimeout(timeoutMs: number | undefined): void;
19
21
  export declare function sessionPageDispatch(store: MutableStore<TheoremSessionState>, sessionId: string, broker: PageToolBroker, timeoutMs?: number): {
20
22
  execution: PageToolExecution;
21
23
  answer(result: PageToolResult): Promise<boolean>;
@@ -54,16 +54,21 @@ export function createMemoryPageToolBroker(maxPending = 128) {
54
54
  },
55
55
  };
56
56
  }
57
- export function sessionPageDispatch(store, sessionId, broker, timeoutMs = 20_000) {
58
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647)
57
+ /** Refuses a page tool timeout that is not a whole number of milliseconds a timer can hold. */
58
+ export function checkPageToolTimeout(timeoutMs) {
59
+ if (timeoutMs !== undefined &&
60
+ (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647))
59
61
  throw new TheoremError('config', labelText('error.config'));
62
+ }
63
+ export function sessionPageDispatch(store, sessionId, broker, timeoutMs) {
64
+ checkPageToolTimeout(timeoutMs);
60
65
  return {
61
66
  execution: {
62
67
  async dispatch(claim, signal) {
63
68
  if (!claim.invocation)
64
69
  refused('internal', 'The page dispatch got a claim with no invocation'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
65
70
  const grantId = crypto.randomUUID();
66
- const expiresAt = Date.now() + timeoutMs;
71
+ const expiresAt = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
67
72
  const pending = broker.reserve(sessionId, grantId, signal);
68
73
  try {
69
74
  await store.mutate(sessionId, (state) => {
@@ -73,15 +78,15 @@ export function sessionPageDispatch(store, sessionId, broker, timeoutMs = 20_000
73
78
  record.invocationHash !== claim.invocationHash ||
74
79
  record.pageGrant)
75
80
  refused('internal', 'The session store holds no running claim for this page dispatch');
76
- record.pageGrant = { grantId, expiresAt };
81
+ record.pageGrant = { grantId, ...(expiresAt === undefined ? {} : { expiresAt }) };
77
82
  });
78
83
  }
79
84
  catch (error) {
80
85
  pending.cancel();
81
86
  throw error;
82
87
  }
83
- const remaining = expiresAt - Date.now();
84
- if (remaining <= 0 || signal?.aborted) {
88
+ const remaining = expiresAt === undefined ? undefined : expiresAt - Date.now();
89
+ if ((remaining !== undefined && remaining <= 0) || signal?.aborted) {
85
90
  pending.cancel();
86
91
  if (signal?.aborted)
87
92
  refused('cancelled', 'The page dispatch was stopped before it was sent'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
@@ -91,7 +96,7 @@ export function sessionPageDispatch(store, sessionId, broker, timeoutMs = 20_000
91
96
  const expired = new Promise((resolve) => {
92
97
  expire = () => resolve({ timedOut: true });
93
98
  });
94
- const timer = setTimeout(expire, remaining);
99
+ const timer = remaining === undefined ? undefined : setTimeout(expire, remaining);
95
100
  signal?.addEventListener('abort', expire, { once: true });
96
101
  return {
97
102
  grantId,
@@ -122,7 +127,7 @@ export function sessionPageDispatch(store, sessionId, broker, timeoutMs = 20_000
122
127
  }
123
128
  if (record.status !== 'running')
124
129
  refused('request', `The page answered a call that is ${record.status}, not running`); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
125
- if (Date.now() >= record.pageGrant.expiresAt)
130
+ if (record.pageGrant.expiresAt !== undefined && Date.now() >= record.pageGrant.expiresAt)
126
131
  refused('timeout', 'The page answered after its grant expired'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
127
132
  record.pageGrant.resultHash = hash;
128
133
  return { duplicate: false, settled: false };
@@ -1,2 +1,3 @@
1
- export declare function requestByteLimit(bytes?: number): number;
2
- export declare function boundedRequestJson(request: Request, maxBytes: number): Promise<unknown>;
1
+ /** The host's request byte limit, checked. Unset, a request of any size is read. */
2
+ export declare function requestByteLimit(bytes: number | undefined): number | undefined;
3
+ export declare function boundedRequestJson(request: Request, maxBytes: number | undefined): Promise<unknown>;
@@ -1,13 +1,16 @@
1
1
  import { labelText, TheoremError, throwIfAborted } from '@theoremjs/agents';
2
- const DEFAULT_REQUEST_BYTES = 16 * 1024 * 1024;
3
- export function requestByteLimit(bytes = DEFAULT_REQUEST_BYTES) {
2
+ /** The host's request byte limit, checked. Unset, a request of any size is read. */
3
+ export function requestByteLimit(bytes) {
4
+ if (bytes === undefined)
5
+ return undefined;
4
6
  if (!Number.isSafeInteger(bytes) || bytes <= 0) {
5
7
  throw new TheoremError('config', labelText('error.config'));
6
8
  }
7
9
  return bytes;
8
10
  }
9
11
  export async function boundedRequestJson(request, maxBytes) {
10
- if (Number(request.headers.get('content-length')) > maxBytes || !request.body) {
12
+ if ((maxBytes !== undefined && Number(request.headers.get('content-length')) > maxBytes) ||
13
+ !request.body) {
11
14
  throw new TheoremError('request', labelText('error.request'));
12
15
  }
13
16
  const reader = request.body.getReader();
@@ -26,7 +29,7 @@ export async function boundedRequestJson(request, maxBytes) {
26
29
  if (next.done)
27
30
  break;
28
31
  bytes += next.value.byteLength;
29
- if (bytes > maxBytes)
32
+ if (maxBytes !== undefined && bytes > maxBytes)
30
33
  throw new TheoremError('request', labelText('error.request'));
31
34
  if (next.value.byteLength)
32
35
  text.push(decoder.decode(next.value, { stream: true }));
@@ -11,11 +11,12 @@ import { type LiveClientMessage, type LiveOpenMessage } from '../client/live-mes
11
11
  export declare function checkRequest<T>(schema: z.ZodType<T>, raw: unknown, what: string): T;
12
12
  /**
13
13
  * One text frame from the live client, as a relay reads it: JSON that passes
14
- * `liveClientMessageSchema`, else a `request` error.
14
+ * `liveClientMessageSchema`, else a `request` error. A frame over `maxBytes`, when set, is refused.
15
15
  */
16
- export declare function parseLiveClientMessage(text: string): LiveClientMessage;
16
+ export declare function parseLiveClientMessage(text: string, maxBytes?: number): LiveClientMessage;
17
17
  /**
18
18
  * The live client's first frame, as a relay reads it before it opens the
19
- * session: JSON that passes `liveOpenMessageSchema`, else a `request` error.
19
+ * session: JSON that passes `liveOpenMessageSchema`, else a `request` error. A frame over
20
+ * `maxBytes`, when set, is refused.
20
21
  */
21
- export declare function parseLiveOpenMessage(text: string): LiveOpenMessage;
22
+ export declare function parseLiveOpenMessage(text: string, maxBytes?: number): LiveOpenMessage;
@@ -18,9 +18,10 @@ export function checkRequest(schema, raw, what) {
18
18
  }
19
19
  return parsed.data;
20
20
  }
21
- function liveFrame(text) {
22
- const limit = requestByteLimit();
23
- if (text.length > limit || new TextEncoder().encode(text).byteLength > limit)
21
+ function liveFrame(text, maxBytes) {
22
+ const limit = requestByteLimit(maxBytes);
23
+ if (limit !== undefined &&
24
+ (text.length > limit || new TextEncoder().encode(text).byteLength > limit))
24
25
  throw new TheoremError('request', labelText('error.request'));
25
26
  try {
26
27
  return JSON.parse(text);
@@ -34,16 +35,17 @@ function liveFrame(text) {
34
35
  }
35
36
  /**
36
37
  * One text frame from the live client, as a relay reads it: JSON that passes
37
- * `liveClientMessageSchema`, else a `request` error.
38
+ * `liveClientMessageSchema`, else a `request` error. A frame over `maxBytes`, when set, is refused.
38
39
  */
39
- export function parseLiveClientMessage(text) {
40
- return checkRequest(liveClientMessageSchema, liveFrame(text), 'live message');
40
+ export function parseLiveClientMessage(text, maxBytes) {
41
+ return checkRequest(liveClientMessageSchema, liveFrame(text, maxBytes), 'live message');
41
42
  }
42
43
  /**
43
44
  * The live client's first frame, as a relay reads it before it opens the
44
- * session: JSON that passes `liveOpenMessageSchema`, else a `request` error.
45
+ * session: JSON that passes `liveOpenMessageSchema`, else a `request` error. A frame over
46
+ * `maxBytes`, when set, is refused.
45
47
  */
46
- export function parseLiveOpenMessage(text) {
48
+ export function parseLiveOpenMessage(text, maxBytes) {
47
49
  // labels-exempt: internal diagnostic; the user reads the error kind's wording
48
- return checkRequest(liveOpenMessageSchema, liveFrame(text), 'live open message');
50
+ return checkRequest(liveOpenMessageSchema, liveFrame(text, maxBytes), 'live open message');
49
51
  }
@@ -9,4 +9,4 @@ export type SessionStateStore = MutableStore<TheoremSessionState> & {
9
9
  readonly capacity?: TheoremSessionCapacity;
10
10
  };
11
11
  /** Session state, with expired gates dropped as it loads so they can't be answered. */
12
- export declare function sessionStateStore(store: TheoremSessionStore, gateTtlMs: number, operationRetentionMs: number): SessionStateStore;
12
+ export declare function sessionStateStore(store: TheoremSessionStore, gateTtlMs: number | undefined, operationRetentionMs: number): SessionStateStore;
@@ -23,8 +23,9 @@ export function sessionStateStore(store, gateTtlMs, operationRetentionMs) {
23
23
  });
24
24
  }
25
25
  const state = structuredClone(parsed.data);
26
- for (const gate of Object.values(state.gates))
27
- gate.expiresAt ??= gate.createdAt + gateTtlMs;
26
+ if (gateTtlMs !== undefined)
27
+ for (const gate of Object.values(state.gates))
28
+ gate.expiresAt ??= gate.createdAt + gateTtlMs;
28
29
  state.gates = pruneGates(state.gates, now, gateTtlMs);
29
30
  for (const operation of Object.values(state.operations)) {
30
31
  if (operation.status === 'ready' && operation.answer.resume.granted === true)
@@ -44,8 +45,9 @@ export function sessionStateStore(store, gateTtlMs, operationRetentionMs) {
44
45
  for (let conflict = 0; conflict < 8; conflict++) {
45
46
  const { revision, state } = await load(sessionId, now);
46
47
  const result = await change(state);
47
- for (const gate of Object.values(state.gates))
48
- gate.expiresAt ??= gate.createdAt + gateTtlMs;
48
+ if (gateTtlMs !== undefined)
49
+ for (const gate of Object.values(state.gates))
50
+ gate.expiresAt ??= gate.createdAt + gateTtlMs;
49
51
  for (const operation of Object.values(state.operations)) {
50
52
  if (operation.status === 'settled')
51
53
  operation.retainUntil ??= operation.settledAt + operationRetentionMs;
@@ -9,6 +9,8 @@ export interface SessionBoundaryOptions {
9
9
  gateTtlMs?: number;
10
10
  operationRetentionMs?: number;
11
11
  pageToolBroker?: PageToolBroker;
12
+ /** How long the page has to answer a tool call it runs, in milliseconds. Unset, it waits. */
13
+ pageToolTimeoutMs?: number;
12
14
  }
13
15
  export type PreparedLiveOpen = Pick<SessionRequest, 'history' | 'slots' | 'context' | 'providerState' | 'awayMs'>;
14
16
  export interface TheoremSessionBoundary {
@@ -22,7 +22,7 @@ export async function createTheoremSessionBoundary(options) {
22
22
  const store = sessionStateStore(options.sessionStore, resolveGateTtlMs('session boundary', options.gateTtlMs), retention);
23
23
  // invariant: Refuse unavailable or malformed authority before an upstream connection opens.
24
24
  await store.read(id);
25
- const page = sessionPageDispatch(store, id, options.pageToolBroker ?? createMemoryPageToolBroker());
25
+ const page = sessionPageDispatch(store, id, options.pageToolBroker ?? createMemoryPageToolBroker(), options.pageToolTimeoutMs);
26
26
  return {
27
27
  pageExecution: page.execution,
28
28
  answerPageResult: page.answer,
@@ -33,7 +33,7 @@ export type PendingToolGate = {
33
33
  };
34
34
  export type SessionPageGrant = {
35
35
  grantId: string;
36
- expiresAt: number;
36
+ expiresAt?: number;
37
37
  resultHash?: string;
38
38
  };
39
39
  export type SessionToolOperation = {
@@ -98,11 +98,11 @@ export interface TheoremSessionStore {
98
98
  compareAndSwap(sessionId: string, expectedRevision: string | null, next: TheoremSessionState): Promise<SessionWriteResult>;
99
99
  }
100
100
  export declare function emptySessionState(): TheoremSessionState;
101
- /** Drop gates (waiting or settled) older than `ttlMs`. */
101
+ /** Drop gates (waiting or settled) older than `ttlMs`, or past their own `expiresAt`. */
102
102
  export declare function pruneGates<Gate extends {
103
103
  createdAt: number;
104
104
  expiresAt?: number;
105
- }>(gates: Record<string, Gate>, now: number, ttlMs: number): Record<string, Gate>;
105
+ }>(gates: Record<string, Gate>, now: number, ttlMs: number | undefined): Record<string, Gate>;
106
106
  export type MemorySessionStoreOptions = {
107
107
  /** What it holds for one session. Unset, it holds any size. */
108
108
  capacity?: TheoremSessionCapacity;
@@ -100,7 +100,7 @@ const identityFields = {
100
100
  };
101
101
  const pageGrant = z.strictObject({
102
102
  grantId: z.string().min(1),
103
- expiresAt: z.number().finite().positive(),
103
+ expiresAt: z.number().finite().positive().optional(),
104
104
  resultHash: z.string().optional(),
105
105
  });
106
106
  const operationSchema = z.discriminatedUnion('status', [
@@ -171,7 +171,7 @@ export function emptySessionState() {
171
171
  providerCheckpoints: [],
172
172
  };
173
173
  }
174
- /** Drop gates (waiting or settled) older than `ttlMs`. */
174
+ /** Drop gates (waiting or settled) older than `ttlMs`, or past their own `expiresAt`. */
175
175
  export function pruneGates(gates, now, ttlMs) {
176
176
  return Object.fromEntries(Object.entries(gates).filter(([, gate]) => gate.expiresAt !== undefined
177
177
  ? now < gate.expiresAt