@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 +19 -3
- package/package.json +2 -2
- package/server/handler.d.ts +7 -2
- package/server/handler.js +6 -5
- package/server/mcp-handler.d.ts +1 -1
- package/server/page-dispatch.d.ts +2 -0
- package/server/page-dispatch.js +13 -8
- package/server/request-body.d.ts +3 -2
- package/server/request-body.js +7 -4
- package/server/request-check.d.ts +5 -4
- package/server/request-check.js +11 -9
- package/server/session-authority.d.ts +1 -1
- package/server/session-authority.js +6 -4
- package/server/session-boundary.d.ts +2 -0
- package/server/session-boundary.js +1 -1
- package/server/session-store.d.ts +3 -3
- package/server/session-store.js +2 -2
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
28
|
+
"@theoremjs/agents": "^0.3.4-next.608.gd007c52"
|
|
29
29
|
}
|
|
30
30
|
}
|
package/server/handler.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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.
|
|
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,
|
package/server/mcp-handler.d.ts
CHANGED
|
@@ -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>;
|
package/server/page-dispatch.js
CHANGED
|
@@ -54,16 +54,21 @@ export function createMemoryPageToolBroker(maxPending = 128) {
|
|
|
54
54
|
},
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
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 };
|
package/server/request-body.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function
|
|
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>;
|
package/server/request-body.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { labelText, TheoremError, throwIfAborted } from '@theoremjs/agents';
|
|
2
|
-
|
|
3
|
-
export function requestByteLimit(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 ||
|
|
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;
|
package/server/request-check.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
|
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;
|
package/server/session-store.js
CHANGED
|
@@ -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
|