@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 +12 -15
- package/package.json +2 -2
- package/server/checkpoint-ownership.d.ts +1 -1
- package/server/checkpoint-ownership.js +2 -2
- package/server/handler.d.ts +0 -3
- package/server/handler.js +4 -5
- package/server/mod.d.ts +2 -2
- package/server/mod.js +1 -1
- package/server/session-authority.d.ts +6 -2
- package/server/session-authority.js +7 -6
- package/server/session-boundary.d.ts +0 -2
- package/server/session-boundary.js +4 -4
- package/server/session-limits.d.ts +15 -8
- package/server/session-limits.js +46 -48
- package/server/session-store.d.ts +5 -3
- package/server/session-store.js +5 -6
- package/server/tool-operations.d.ts +3 -0
- package/server/tool-operations.js +20 -9
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
|
-
|
|
381
|
+
A session store declares its `capacity`: what it holds for one session, in bytes of UTF-8 JSON.
|
|
382
382
|
|
|
383
|
-
|
|
|
384
|
-
| --- | --- |
|
|
385
|
-
| `
|
|
386
|
-
| `
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
The
|
|
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.
|
|
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.
|
|
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[]
|
|
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
|
|
41
|
-
return [...new Set([...existing, ...added].reverse())].
|
|
40
|
+
export function retainedCheckpointIdentities(existing, added) {
|
|
41
|
+
return [...new Set([...existing, ...added].reverse())].reverse();
|
|
42
42
|
}
|
package/server/handler.d.ts
CHANGED
|
@@ -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',
|
|
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(
|
|
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 {
|
|
23
|
-
export {
|
|
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 {
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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',
|
|
74
|
-
|
|
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
|
|
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, [
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
export declare
|
|
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;
|
package/server/session-limits.js
CHANGED
|
@@ -1,59 +1,57 @@
|
|
|
1
|
-
import { labelText, TheoremError } from '@theoremjs/agents';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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: '
|
|
15
|
+
cause: { code: 'session_store_capacity_declared' },
|
|
17
16
|
});
|
|
18
17
|
}
|
|
19
|
-
return
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
for (const operation of
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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
|
-
/**
|
|
103
|
-
|
|
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. */
|
package/server/session-store.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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',
|
|
250
|
-
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
},
|