@deepseek-ai/dsh-session 0.1.2-rc.1 → 0.1.5-alpha.1
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.i18n.yaml +2 -2
- package/README.md +15 -12
- package/README.zh.md +15 -12
- package/lib/index.js +254 -480
- package/lib/invariant.js +5 -2
- package/lib/types/index.d.ts +26 -21
- package/lib/types/index.js +109 -75
- package/lib/types/invariant.js +6 -2
- package/lib/types/known-event-types.js +6 -3
- package/lib/types/request-header.d.ts +4 -4
- package/lib/types/request-header.js +5 -7
- package/lib/types/surface.d.ts +18 -2
- package/lib/types/surface.js +110 -23
- package/lib/types/types.d.ts +101 -85
- package/lib/types/types.js +9 -10
- package/package.json +9 -13
- package/lib/types/chunk-rows.d.ts +0 -106
- package/lib/types/chunk-rows.js +0 -328
package/lib/invariant.js
CHANGED
|
@@ -50,8 +50,8 @@ function validateEvent(trace, event, fail) {
|
|
|
50
50
|
openStep = null;
|
|
51
51
|
nextStep += 1;
|
|
52
52
|
break;
|
|
53
|
-
case "assistant/
|
|
54
|
-
requireOpenStep(trace, "assistant/
|
|
53
|
+
case "assistant/attempt":
|
|
54
|
+
requireOpenStep(trace, "assistant/attempt", event.data.turn, event.data.step, fail);
|
|
55
55
|
break;
|
|
56
56
|
case "assistant/message":
|
|
57
57
|
requireOpenStep(trace, "assistant/message", event.data.turn, event.data.step, fail);
|
|
@@ -78,6 +78,9 @@ function validateEvent(trace, event, fail) {
|
|
|
78
78
|
};
|
|
79
79
|
break;
|
|
80
80
|
}
|
|
81
|
+
case "system/message":
|
|
82
|
+
requireOpenStep(trace, "system/message", event.data.turn, event.data.step, fail);
|
|
83
|
+
break;
|
|
81
84
|
case "user/message": break;
|
|
82
85
|
case "session/end-seed": break;
|
|
83
86
|
case "request/header":
|
package/lib/types/index.d.ts
CHANGED
|
@@ -10,15 +10,13 @@ import type { Scoped } from '@deepseek-ai/dsh-scope';
|
|
|
10
10
|
import type { Message } from '@deepseek-ai/dsh-llm';
|
|
11
11
|
import { SessionLogOffset, SessionSeq } from './types.ts';
|
|
12
12
|
import type { TypertLookup } from '@deepseek-ai/dsh-typert-protocol';
|
|
13
|
-
import type { CreateSessionOptions, EpochHeader, PrepareSessionOptions, RequestContext, SessionEvent, SessionEventMap, SessionEventType, SessionHeader, SessionId, SurfaceIntent, SurfaceEventType } from './types.ts';
|
|
13
|
+
import type { CreateSessionOptions, EpochHeader, PrepareSessionOptions, RequestContext, SessionEvent, SessionEventMap, SessionEventType, SessionHeader, SessionId, SessionSeedEventState, SurfaceIntent, SurfaceEventType } from './types.ts';
|
|
14
14
|
import type { SessionSurface } from './surface.ts';
|
|
15
15
|
export * from './types.ts';
|
|
16
16
|
export { SessionPreparation } from './preparation.ts';
|
|
17
17
|
export type { SessionPreparationOptions } from './preparation.ts';
|
|
18
|
-
export type { AssistantMessage, ToolResultMessage, UserMessage } from '@deepseek-ai/dsh-llm';
|
|
18
|
+
export type { AssistantMessage, SystemMessage, ToolResultMessage, UserMessage } from '@deepseek-ai/dsh-llm';
|
|
19
19
|
export { interruptedTurnClosers, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from './repair.ts';
|
|
20
|
-
export { decodeStorageRecord, packChunkRuns } from './chunk-rows.ts';
|
|
21
|
-
export type { ChunkRow, StorageRecord } from './chunk-rows.ts';
|
|
22
20
|
export type { SessionSurface, SurfaceFoldReplacement, SurfaceFoldResult } from './surface.ts';
|
|
23
21
|
export { deriveEventMessage, foldSurface, isAppendSurfaceEvent, isReplacementSurfaceEvent, isSurfaceEvent, isSurfaceEligibleType } from './surface.ts';
|
|
24
22
|
export { canonicalHeader, foldRequestHeader, headerEquals } from './request-header.ts';
|
|
@@ -85,6 +83,7 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
|
|
|
85
83
|
* Use {@link snapshotSessionEvent} when exclusive ownership is not guaranteed.
|
|
86
84
|
* @param event - exclusively owned event imported across a trusted boundary.
|
|
87
85
|
* @returns the same event object with a validated, deeply frozen message.
|
|
86
|
+
* @throws when event-local surface metadata, request-header fields, or message invariants are invalid; history relations are not checked.
|
|
88
87
|
*/
|
|
89
88
|
export declare function adoptSessionEvent<T extends SessionEvent>(event: T): T;
|
|
90
89
|
/**
|
|
@@ -124,9 +123,10 @@ export declare class Session {
|
|
|
124
123
|
* The first seq appended IN THIS PROCESS: the length of the constructor
|
|
125
124
|
* seed (0 without one). Events with smaller seq values entered through
|
|
126
125
|
* construction — replay, fork, or resume — and were never published on the
|
|
127
|
-
* `session/event` firehose (constructor seeds do not emit)
|
|
128
|
-
*
|
|
129
|
-
*
|
|
126
|
+
* `session/event` firehose (constructor seeds do not emit). This offset marks
|
|
127
|
+
* the constructor-input boundary for lifecycle ownership and persistence
|
|
128
|
+
* adoption; consumers that need complete canonical history still start at
|
|
129
|
+
* seq 0. Distinct from {@link inheritedEventCount}, the DURABLE
|
|
130
130
|
* fork-lineage cut: a resumed session's constructor seed is its full stored
|
|
131
131
|
* log, while the inherited count keeps the original fork value — this field is the
|
|
132
132
|
* in-process construction fact.
|
|
@@ -154,16 +154,19 @@ export declare class Session {
|
|
|
154
154
|
*/
|
|
155
155
|
static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader, inheritedEventCount?: SessionLogOffset): Session;
|
|
156
156
|
/**
|
|
157
|
-
* Restore a detached session by
|
|
158
|
-
*
|
|
159
|
-
* and header fields are validated
|
|
157
|
+
* Restore a detached session by adopting an independently owned or deeply frozen seed.
|
|
158
|
+
* Runtime-required event fields, event envelopes, sequence continuity, surface
|
|
159
|
+
* transitions, and header fields are validated without copying or freezing events.
|
|
160
|
+
* Embedded Assistant streams remain opaque until a stream consumer or storage
|
|
161
|
+
* verifier reads them.
|
|
160
162
|
* @param id - restored session identity.
|
|
161
|
-
* @param seed -
|
|
162
|
-
* @param header -
|
|
163
|
+
* @param seed - independently owned or deeply frozen events.
|
|
164
|
+
* @param header - independently owned storage metadata.
|
|
163
165
|
* @param inheritedEventCount - exact fork-inherited prefix length decoded from storage.
|
|
166
|
+
* @param eventState - aliasing state carried from the operation that produced the seed.
|
|
164
167
|
* @returns a restored detached session.
|
|
165
168
|
*/
|
|
166
|
-
static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader, inheritedEventCount: SessionLogOffset): Session;
|
|
169
|
+
static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader, inheritedEventCount: SessionLogOffset, eventState: SessionSeedEventState): Session;
|
|
167
170
|
private constructor();
|
|
168
171
|
/** Cached immutable full snapshot of the private append-only log. */
|
|
169
172
|
private eventsSnapshot;
|
|
@@ -212,7 +215,8 @@ export declare class Session {
|
|
|
212
215
|
* declare how it joins the surface, the sole source of derived model
|
|
213
216
|
* history) and
|
|
214
217
|
* rejected by the compiler for non-surface types like `turn/start` or
|
|
215
|
-
* `assistant/
|
|
218
|
+
* `assistant/attempt`. Assistant messages embed their exact provider
|
|
219
|
+
* stream and cannot cite top-level source events.
|
|
216
220
|
* @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
|
|
217
221
|
* `data` that entered the log, so reading `event.data` back sees the logged
|
|
218
222
|
* value, never the caller's still-mutable input.
|
|
@@ -220,6 +224,7 @@ export declare class Session {
|
|
|
220
224
|
* (BigInt, function, symbol, undefined, negative zero, non-finite number,
|
|
221
225
|
* circular reference, sparse array, or an exotic object such as
|
|
222
226
|
* Map/Set/Date/class instance), or when the candidate violates the
|
|
227
|
+
* request-header empty-field or tool-error consistency rules, or the
|
|
223
228
|
* canonical surface contract (marker shape and eligibility, unique
|
|
224
229
|
* earlier source-event references, positional replacement validity, and complete
|
|
225
230
|
* shadowed-node coverage). One iterative pass reads, validates, and
|
|
@@ -230,7 +235,7 @@ export declare class Session {
|
|
|
230
235
|
* append reentered while this acceptance/publication boundary is open also
|
|
231
236
|
* rejects before the log changes.
|
|
232
237
|
*/
|
|
233
|
-
append<T extends SessionEventType>(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [opts: SurfaceIntent] : []): SessionEvent<T>;
|
|
238
|
+
append<T extends SessionEventType>(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [opts: SurfaceIntent<T>] : []): SessionEvent<T>;
|
|
234
239
|
/** Cached fold of the request-header events — see {@link requestHeader}. */
|
|
235
240
|
private headerFold;
|
|
236
241
|
/** Log position (events consumed) the header fold has reached. */
|
|
@@ -305,8 +310,9 @@ export declare class SessionForkError extends Error {
|
|
|
305
310
|
/**
|
|
306
311
|
* In-memory session store (`ctx.sessions`).
|
|
307
312
|
*
|
|
308
|
-
* Persistence is intentionally not implemented here —
|
|
309
|
-
*
|
|
313
|
+
* Persistence is intentionally not implemented here — the agent lifecycle
|
|
314
|
+
* attaches a session-log writer to each published session's write handle;
|
|
315
|
+
* a session published outside that lifecycle persists nothing.
|
|
310
316
|
*/
|
|
311
317
|
export declare class SessionStore extends Service {
|
|
312
318
|
private store;
|
|
@@ -345,10 +351,9 @@ export declare class SessionStore extends Service {
|
|
|
345
351
|
*
|
|
346
352
|
* @param id - the session id; omitted, the store mints `session-<n>`.
|
|
347
353
|
* @param options - seed events and/or creation metadata for the header. With
|
|
348
|
-
* `
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
* retain no mutable aliases.
|
|
354
|
+
* `eventState`, every seed event is either independently owned or any
|
|
355
|
+
* shared value is deeply frozen; {@link Session.fromRestore} validates and
|
|
356
|
+
* adopts those values without copying or freezing them.
|
|
352
357
|
* @returns the constructed session, NOT yet in the store.
|
|
353
358
|
* @throws if a session with `id` already exists, metadata is not a plain
|
|
354
359
|
* lossless-JSON record with valid scalar fields, or `meta.cwd` is a
|
package/lib/types/index.js
CHANGED
|
@@ -8,15 +8,14 @@
|
|
|
8
8
|
import { Service } from '@deepseek-ai/cordis';
|
|
9
9
|
import { isAbsolute } from 'node:path';
|
|
10
10
|
import { brandString } from '@deepseek-ai/dsh-brand';
|
|
11
|
-
import { deepFreeze, snapshotJsonValue } from '@deepseek-ai/dsh-util-values';
|
|
11
|
+
import { assertNever, deepFreeze, snapshotJsonValue } from '@deepseek-ai/dsh-util-values';
|
|
12
12
|
import { scopeOf, scopeTarget } from '@deepseek-ai/dsh-scope';
|
|
13
13
|
import { SESSION_FORMAT_VERSION, SessionLogOffset, SessionSeq } from "./types.js";
|
|
14
|
-
import { deriveEventMessage, SurfaceManager } from "./surface.js";
|
|
14
|
+
import { deriveEventMessage, SurfaceManager, validateSessionEventData, validateSurfaceMetadata } from "./surface.js";
|
|
15
15
|
import { foldRequestHeader } from "./request-header.js";
|
|
16
16
|
export * from "./types.js";
|
|
17
17
|
export { SessionPreparation } from "./preparation.js";
|
|
18
18
|
export { interruptedTurnClosers, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from "./repair.js";
|
|
19
|
-
export { decodeStorageRecord, packChunkRuns } from "./chunk-rows.js";
|
|
20
19
|
export { deriveEventMessage, foldSurface, isAppendSurfaceEvent, isReplacementSurfaceEvent, isSurfaceEvent, isSurfaceEligibleType } from "./surface.js";
|
|
21
20
|
export { canonicalHeader, foldRequestHeader, headerEquals } from "./request-header.js";
|
|
22
21
|
export { KNOWN_SESSION_EVENT_TYPES } from "./known-event-types.js";
|
|
@@ -92,13 +91,17 @@ function snapshotSessionHeader(id, source) {
|
|
|
92
91
|
* Use {@link snapshotSessionEvent} when exclusive ownership is not guaranteed.
|
|
93
92
|
* @param event - exclusively owned event imported across a trusted boundary.
|
|
94
93
|
* @returns the same event object with a validated, deeply frozen message.
|
|
94
|
+
* @throws when event-local surface metadata, request-header fields, or message invariants are invalid; history relations are not checked.
|
|
95
95
|
*/
|
|
96
96
|
export function adoptSessionEvent(event) {
|
|
97
|
+
validateSessionEventData(event, `session event at seq ${event.seq}`);
|
|
98
|
+
validateSurfaceMetadata(event);
|
|
97
99
|
assertMessageEventShape(event, `session event at seq ${event.seq}`);
|
|
98
100
|
switch (event.type) {
|
|
99
101
|
case 'user/message':
|
|
100
102
|
deepFreeze(event.data);
|
|
101
103
|
break;
|
|
104
|
+
case 'system/message':
|
|
102
105
|
case 'assistant/message':
|
|
103
106
|
case 'tool/result':
|
|
104
107
|
deepFreeze(event.data.message);
|
|
@@ -117,28 +120,12 @@ export function adoptSessionEvent(event) {
|
|
|
117
120
|
export function snapshotSessionEvent(event) {
|
|
118
121
|
return adoptSessionEvent(structuredClone(event));
|
|
119
122
|
}
|
|
120
|
-
/** Deep-freeze one acyclic JSON tree without consuming the JavaScript call stack. */
|
|
121
|
-
function freezeRestoredObject(value) {
|
|
122
|
-
const pending = [value];
|
|
123
|
-
while (pending.length > 0) {
|
|
124
|
-
// The non-empty check proves an object remains to visit.
|
|
125
|
-
// oxlint-disable-next-line typescript/no-non-null-assertion
|
|
126
|
-
const current = pending.pop();
|
|
127
|
-
Object.freeze(current);
|
|
128
|
-
for (const key in current) {
|
|
129
|
-
const child = current[key];
|
|
130
|
-
if (child !== null && typeof child === 'object')
|
|
131
|
-
pending.push(child);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return value;
|
|
135
|
-
}
|
|
136
123
|
/** Validate the fixed event envelope after one-pass JSON materialization. */
|
|
137
124
|
function assertSessionEventEnvelope(value, index) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
throw new Error(`seed event at index ${index} uses unsupported legacy request/header-delta format`);
|
|
125
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
126
|
+
throw new Error(`seed event at index ${index} has an invalid event envelope`);
|
|
141
127
|
}
|
|
128
|
+
const event = value;
|
|
142
129
|
for (const key in event) {
|
|
143
130
|
switch (key) {
|
|
144
131
|
case 'type':
|
|
@@ -163,9 +150,12 @@ function assertSessionEventEnvelope(value, index) {
|
|
|
163
150
|
|| (event['ignorable'] !== undefined && event['ignorable'] !== true)) {
|
|
164
151
|
throw new Error(`seed event at index ${index} has an invalid event envelope`);
|
|
165
152
|
}
|
|
153
|
+
validateSessionEventData(event, `seed ${type} at index ${index}`);
|
|
166
154
|
switch (type) {
|
|
167
155
|
case 'request/header':
|
|
156
|
+
case 'system/message':
|
|
168
157
|
case 'user/message':
|
|
158
|
+
case 'assistant/attempt':
|
|
169
159
|
case 'assistant/message':
|
|
170
160
|
case 'tool/result':
|
|
171
161
|
assertCurrentLlmShape(event, index);
|
|
@@ -179,11 +169,8 @@ function assertCurrentLlmShape(event, index) {
|
|
|
179
169
|
? data
|
|
180
170
|
: undefined;
|
|
181
171
|
if (event['type'] === 'request/header') {
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
? header
|
|
185
|
-
: undefined;
|
|
186
|
-
const config = headerRecord?.['config'];
|
|
172
|
+
const headerRecord = record?.['header'];
|
|
173
|
+
const config = headerRecord['config'];
|
|
187
174
|
if (!hasProviderModel(config))
|
|
188
175
|
throw new Error(`seed request/header at index ${index} lacks provider/model`);
|
|
189
176
|
const configRecord = config;
|
|
@@ -192,13 +179,36 @@ function assertCurrentLlmShape(event, index) {
|
|
|
192
179
|
&& (typeof reasoningEffort !== 'string' || reasoningEffort.length === 0)) {
|
|
193
180
|
throw new Error(`seed request/header at index ${index} has an invalid reasoningEffort`);
|
|
194
181
|
}
|
|
195
|
-
assertAdapterDefaults(headerRecord
|
|
182
|
+
assertAdapterDefaults(headerRecord['adapterDefaults'], configRecord, index);
|
|
183
|
+
const reason = record?.['reason'];
|
|
184
|
+
if (reason !== 'initial' && reason !== 'resume' && reason !== 'change' && reason !== 'series') {
|
|
185
|
+
throw new Error(`seed request/header at index ${index} has an invalid reason`);
|
|
186
|
+
}
|
|
187
|
+
if (record?.['startsSeries'] !== undefined && record['startsSeries'] !== true) {
|
|
188
|
+
throw new Error(`seed request/header at index ${index} has an invalid startsSeries marker`);
|
|
189
|
+
}
|
|
196
190
|
}
|
|
197
191
|
const type = event['type'];
|
|
198
|
-
if (type
|
|
199
|
-
|
|
192
|
+
if (type === 'assistant/attempt') {
|
|
193
|
+
assertAssistantSettlementShape(record, type, index);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (!isMessageEventType(type))
|
|
200
197
|
return;
|
|
201
198
|
assertMessageEventShape(event, `seed ${type} at index ${index}`);
|
|
199
|
+
if (type === 'assistant/message') {
|
|
200
|
+
assertAssistantSettlementShape(record, type, index);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Validate fields used directly by restored Session lifecycle logic without replaying the embedded stream. */
|
|
204
|
+
function assertAssistantSettlementShape(data, type, index) {
|
|
205
|
+
const turn = data?.['turn'];
|
|
206
|
+
const step = data?.['step'];
|
|
207
|
+
if (typeof turn !== 'number' || !Number.isSafeInteger(turn) || turn < 0 || Object.is(turn, -0)
|
|
208
|
+
|| typeof step !== 'number' || !Number.isSafeInteger(step) || step < 0 || Object.is(step, -0)
|
|
209
|
+
|| !Array.isArray(data?.['stream'])) {
|
|
210
|
+
throw new Error(`seed ${type} at index ${index} has invalid settlement fields`);
|
|
211
|
+
}
|
|
202
212
|
}
|
|
203
213
|
const allowedAdapterKeys = new Set(['reasoningEffort', 'maxTokens']);
|
|
204
214
|
/** Validate adapter-default markers imported from a durable request header. */
|
|
@@ -216,11 +226,21 @@ function assertAdapterDefaults(value, config, index) {
|
|
|
216
226
|
throw new Error(`seed request/header at index ${index} has invalid adapterDefaults`);
|
|
217
227
|
}
|
|
218
228
|
}
|
|
229
|
+
/** The four surface event types whose payload carries an identified message. */
|
|
230
|
+
function isMessageEventType(type) {
|
|
231
|
+
return type === 'system/message' || type === 'user/message'
|
|
232
|
+
|| type === 'assistant/message' || type === 'tool/result';
|
|
233
|
+
}
|
|
234
|
+
const MESSAGE_ROLE_BY_TYPE = {
|
|
235
|
+
'system/message': 'system',
|
|
236
|
+
'user/message': 'user',
|
|
237
|
+
'assistant/message': 'assistant',
|
|
238
|
+
'tool/result': 'user',
|
|
239
|
+
};
|
|
219
240
|
/** Validate only the event-specific invariants needed to safely replay a message. */
|
|
220
241
|
function assertMessageEventShape(event, subject) {
|
|
221
242
|
const type = event['type'];
|
|
222
|
-
if (type
|
|
223
|
-
&& type !== 'tool/result')
|
|
243
|
+
if (!isMessageEventType(type))
|
|
224
244
|
return;
|
|
225
245
|
const data = event['data'];
|
|
226
246
|
const record = typeof data === 'object' && data !== null
|
|
@@ -233,7 +253,7 @@ function assertMessageEventShape(event, subject) {
|
|
|
233
253
|
throw new Error(`${subject} lacks an identified message`);
|
|
234
254
|
}
|
|
235
255
|
const messageRecord = message;
|
|
236
|
-
const expectedRole = type
|
|
256
|
+
const expectedRole = MESSAGE_ROLE_BY_TYPE[type];
|
|
237
257
|
if (messageRecord['role'] !== expectedRole) {
|
|
238
258
|
throw new Error(`${subject} message must have role "${expectedRole}"`);
|
|
239
259
|
}
|
|
@@ -247,6 +267,13 @@ function assertMessageEventShape(event, subject) {
|
|
|
247
267
|
throw new Error(`${subject} message has invalid content`);
|
|
248
268
|
}
|
|
249
269
|
const sourceRecord = source;
|
|
270
|
+
if (type === 'system/message') {
|
|
271
|
+
if (sourceRecord['kind'] !== 'plugin' || typeof sourceRecord['plugin'] !== 'string'
|
|
272
|
+
|| sourceRecord['plugin'] === '') {
|
|
273
|
+
throw new Error(`${subject} message must have plugin source`);
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
250
277
|
if (type === 'assistant/message') {
|
|
251
278
|
if (sourceRecord['kind'] !== 'model' || !hasProviderModel(sourceRecord)) {
|
|
252
279
|
throw new Error(`${subject} message must have model source`);
|
|
@@ -279,17 +306,6 @@ function hasProviderModel(value) {
|
|
|
279
306
|
return typeof pair['provider'] === 'string' && pair['provider'].length > 0
|
|
280
307
|
&& typeof pair['model'] === 'string' && pair['model'].length > 0;
|
|
281
308
|
}
|
|
282
|
-
/** Reject request-header vocabulary removed with the legacy delta codec. */
|
|
283
|
-
function assertSupportedRequestHeader(type, data, location) {
|
|
284
|
-
if (type === 'request/header-delta') {
|
|
285
|
-
throw new Error(`${location} uses unsupported legacy request/header-delta format`);
|
|
286
|
-
}
|
|
287
|
-
if (type === 'request/header'
|
|
288
|
-
&& data !== null && typeof data === 'object' && !Array.isArray(data)
|
|
289
|
-
&& data['reason'] === 'fallback') {
|
|
290
|
-
throw new Error(`${location} uses unsupported legacy request/header reason "fallback"`);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
309
|
/** Resolve one listener snapshot, including Cordis's internal dispatch checks. */
|
|
294
310
|
function collectSessionCallbacks(ctx, args) {
|
|
295
311
|
return [...ctx.events.dispatch('emit', args)];
|
|
@@ -345,9 +361,10 @@ export class Session {
|
|
|
345
361
|
* The first seq appended IN THIS PROCESS: the length of the constructor
|
|
346
362
|
* seed (0 without one). Events with smaller seq values entered through
|
|
347
363
|
* construction — replay, fork, or resume — and were never published on the
|
|
348
|
-
* `session/event` firehose (constructor seeds do not emit)
|
|
349
|
-
*
|
|
350
|
-
*
|
|
364
|
+
* `session/event` firehose (constructor seeds do not emit). This offset marks
|
|
365
|
+
* the constructor-input boundary for lifecycle ownership and persistence
|
|
366
|
+
* adoption; consumers that need complete canonical history still start at
|
|
367
|
+
* seq 0. Distinct from {@link inheritedEventCount}, the DURABLE
|
|
351
368
|
* fork-lineage cut: a resumed session's constructor seed is its full stored
|
|
352
369
|
* log, while the inherited count keeps the original fork value — this field is the
|
|
353
370
|
* in-process construction fact.
|
|
@@ -377,22 +394,23 @@ export class Session {
|
|
|
377
394
|
return new Session(id, seed, header, 'snapshot', inheritedEventCount);
|
|
378
395
|
}
|
|
379
396
|
/**
|
|
380
|
-
* Restore a detached session by
|
|
381
|
-
*
|
|
382
|
-
* and header fields are validated
|
|
397
|
+
* Restore a detached session by adopting an independently owned or deeply frozen seed.
|
|
398
|
+
* Runtime-required event fields, event envelopes, sequence continuity, surface
|
|
399
|
+
* transitions, and header fields are validated without copying or freezing events.
|
|
400
|
+
* Embedded Assistant streams remain opaque until a stream consumer or storage
|
|
401
|
+
* verifier reads them.
|
|
383
402
|
* @param id - restored session identity.
|
|
384
|
-
* @param seed -
|
|
385
|
-
* @param header -
|
|
403
|
+
* @param seed - independently owned or deeply frozen events.
|
|
404
|
+
* @param header - independently owned storage metadata.
|
|
386
405
|
* @param inheritedEventCount - exact fork-inherited prefix length decoded from storage.
|
|
406
|
+
* @param eventState - aliasing state carried from the operation that produced the seed.
|
|
387
407
|
* @returns a restored detached session.
|
|
388
408
|
*/
|
|
389
|
-
static fromRestore(id, seed, header, inheritedEventCount) {
|
|
390
|
-
return new Session(id, seed, header,
|
|
409
|
+
static fromRestore(id, seed, header, inheritedEventCount, eventState) {
|
|
410
|
+
return new Session(id, seed, header, eventState, inheritedEventCount);
|
|
391
411
|
}
|
|
392
412
|
constructor(id, seed, header, mode = 'snapshot', suppliedInheritedEventCount) {
|
|
393
|
-
const restoredHeader = mode === '
|
|
394
|
-
? validateRestoredSessionHeader(id, header)
|
|
395
|
-
: undefined;
|
|
413
|
+
const restoredHeader = mode === 'snapshot' ? undefined : validateRestoredSessionHeader(id, header);
|
|
396
414
|
if (seed !== undefined) {
|
|
397
415
|
// Validate the seed to the SAME invariants `append` enforces, so a
|
|
398
416
|
// replay/fork (`ctx.sessions.create(id, { seed })`) cannot construct a
|
|
@@ -404,12 +422,11 @@ export class Session {
|
|
|
404
422
|
for (const [index, source] of seed.entries()) {
|
|
405
423
|
// The seed is a persistence/replay boundary: validate and detach the
|
|
406
424
|
// complete event in one lossless-JSON pass.
|
|
407
|
-
const snapshot = mode === '
|
|
425
|
+
const snapshot = mode === 'snapshot' ? snapshotJsonValue(source) : source;
|
|
408
426
|
if (snapshot === undefined) {
|
|
409
427
|
throw new Error(`seed event at index ${index} is not losslessly JSON-serializable`);
|
|
410
428
|
}
|
|
411
429
|
assertSessionEventEnvelope(snapshot, index);
|
|
412
|
-
assertSupportedRequestHeader(snapshot.type, snapshot.data, `seed event at index ${index}`);
|
|
413
430
|
if (snapshot.seq !== index) {
|
|
414
431
|
throw new Error(`seed event at index ${index} has seq ${snapshot.seq} (expected ${index}); seed must be contiguous from 0`);
|
|
415
432
|
}
|
|
@@ -422,7 +439,7 @@ export class Session {
|
|
|
422
439
|
catch (error) {
|
|
423
440
|
throw new Error(`invalid seed event at index ${index}: ${error instanceof Error ? error.message : 'invalid surface metadata'}`);
|
|
424
441
|
}
|
|
425
|
-
this.log.push(mode === '
|
|
442
|
+
this.log.push(mode === 'snapshot' ? deepFreeze(snapshot) : snapshot);
|
|
426
443
|
}
|
|
427
444
|
}
|
|
428
445
|
this.firstLiveSeq = SessionLogOffset(this.log.length);
|
|
@@ -440,12 +457,17 @@ export class Session {
|
|
|
440
457
|
if (inheritedEventCount > this.log.length) {
|
|
441
458
|
throw new Error('session inherited event count exceeds its event log');
|
|
442
459
|
}
|
|
460
|
+
if (mode === 'snapshot' && this.header.isSeeded && inheritedEventCount !== this.log.length) {
|
|
461
|
+
throw new Error('seeded session constructor seed must equal its inherited prefix');
|
|
462
|
+
}
|
|
443
463
|
this.inheritedEventCount = inheritedEventCount;
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
464
|
+
// A fresh seeded child always owns one tagged marker at its inherited cut,
|
|
465
|
+
// even when the copied prefix already ends in an ancestor marker. Restore
|
|
466
|
+
// retains that durable marker and appends only the ordinary resume marker.
|
|
467
|
+
if (seed !== undefined && mode === 'snapshot' && this.header.isSeeded) {
|
|
468
|
+
this.append('session/end-seed', { inherited: true });
|
|
469
|
+
}
|
|
470
|
+
else if (seed !== undefined && this.log.at(-1)?.type !== 'session/end-seed') {
|
|
449
471
|
this.append('session/end-seed', {});
|
|
450
472
|
}
|
|
451
473
|
}
|
|
@@ -510,7 +532,8 @@ export class Session {
|
|
|
510
532
|
* declare how it joins the surface, the sole source of derived model
|
|
511
533
|
* history) and
|
|
512
534
|
* rejected by the compiler for non-surface types like `turn/start` or
|
|
513
|
-
* `assistant/
|
|
535
|
+
* `assistant/attempt`. Assistant messages embed their exact provider
|
|
536
|
+
* stream and cannot cite top-level source events.
|
|
514
537
|
* @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
|
|
515
538
|
* `data` that entered the log, so reading `event.data` back sees the logged
|
|
516
539
|
* value, never the caller's still-mutable input.
|
|
@@ -518,6 +541,7 @@ export class Session {
|
|
|
518
541
|
* (BigInt, function, symbol, undefined, negative zero, non-finite number,
|
|
519
542
|
* circular reference, sparse array, or an exotic object such as
|
|
520
543
|
* Map/Set/Date/class instance), or when the candidate violates the
|
|
544
|
+
* request-header empty-field or tool-error consistency rules, or the
|
|
521
545
|
* canonical surface contract (marker shape and eligibility, unique
|
|
522
546
|
* earlier source-event references, positional replacement validity, and complete
|
|
523
547
|
* shadowed-node coverage). One iterative pass reads, validates, and
|
|
@@ -538,7 +562,6 @@ export class Session {
|
|
|
538
562
|
if (dataSnapshot === undefined) {
|
|
539
563
|
throw new Error(`session event "${type}" carries non-JSON-serializable data`);
|
|
540
564
|
}
|
|
541
|
-
assertSupportedRequestHeader(type, dataSnapshot, `session event "${type}"`);
|
|
542
565
|
const surfaceMetadataSnapshot = snapshotJsonValue(surfaceMetadata);
|
|
543
566
|
if (surfaceMetadataSnapshot === undefined) {
|
|
544
567
|
throw new Error(`session event "${type}" carries non-JSON-serializable surface metadata`);
|
|
@@ -554,6 +577,7 @@ export class Session {
|
|
|
554
577
|
data: dataSnapshot,
|
|
555
578
|
...surfaceMetadataSnapshot,
|
|
556
579
|
});
|
|
580
|
+
validateSessionEventData(event, `session event "${type}" at seq ${event.seq}`);
|
|
557
581
|
this.surfaceManager.validateNext(event);
|
|
558
582
|
if (entry !== undefined)
|
|
559
583
|
entry.appending = true;
|
|
@@ -688,8 +712,9 @@ export class SessionForkError extends Error {
|
|
|
688
712
|
/**
|
|
689
713
|
* In-memory session store (`ctx.sessions`).
|
|
690
714
|
*
|
|
691
|
-
* Persistence is intentionally not implemented here —
|
|
692
|
-
*
|
|
715
|
+
* Persistence is intentionally not implemented here — the agent lifecycle
|
|
716
|
+
* attaches a session-log writer to each published session's write handle;
|
|
717
|
+
* a session published outside that lifecycle persists nothing.
|
|
693
718
|
*/
|
|
694
719
|
export class SessionStore extends Service {
|
|
695
720
|
store = new Map();
|
|
@@ -750,10 +775,9 @@ export class SessionStore extends Service {
|
|
|
750
775
|
*
|
|
751
776
|
* @param id - the session id; omitted, the store mints `session-<n>`.
|
|
752
777
|
* @param options - seed events and/or creation metadata for the header. With
|
|
753
|
-
* `
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
* retain no mutable aliases.
|
|
778
|
+
* `eventState`, every seed event is either independently owned or any
|
|
779
|
+
* shared value is deeply frozen; {@link Session.fromRestore} validates and
|
|
780
|
+
* adopts those values without copying or freezing them.
|
|
757
781
|
* @returns the constructed session, NOT yet in the store.
|
|
758
782
|
* @throws if a session with `id` already exists, metadata is not a plain
|
|
759
783
|
* lossless-JSON record with valid scalar fields, or `meta.cwd` is a
|
|
@@ -771,8 +795,18 @@ export class SessionStore extends Service {
|
|
|
771
795
|
}
|
|
772
796
|
if (this.store.has(sessionId))
|
|
773
797
|
throw new Error(`session "${sessionId}" already exists`);
|
|
774
|
-
if (options
|
|
775
|
-
|
|
798
|
+
if (options !== undefined) {
|
|
799
|
+
const { eventState } = options;
|
|
800
|
+
switch (eventState) {
|
|
801
|
+
case 'detached':
|
|
802
|
+
case 'shared-frozen':
|
|
803
|
+
return Session.fromRestore(sessionId, options.seed, options.meta, options.inheritedEventCount, eventState);
|
|
804
|
+
case undefined:
|
|
805
|
+
break;
|
|
806
|
+
/* v8 ignore next -- closed-union exhaustiveness guard */
|
|
807
|
+
default:
|
|
808
|
+
assertNever(eventState, 'SessionStore.prepare event state');
|
|
809
|
+
}
|
|
776
810
|
}
|
|
777
811
|
const seed = options?.seed;
|
|
778
812
|
const meta = options?.meta;
|
package/lib/types/invariant.js
CHANGED
|
@@ -72,8 +72,8 @@ function validateEvent(trace, event, fail) {
|
|
|
72
72
|
nextStep += 1;
|
|
73
73
|
break;
|
|
74
74
|
}
|
|
75
|
-
case 'assistant/
|
|
76
|
-
requireOpenStep(trace, 'assistant/
|
|
75
|
+
case 'assistant/attempt': {
|
|
76
|
+
requireOpenStep(trace, 'assistant/attempt', event.data.turn, event.data.step, fail);
|
|
77
77
|
break;
|
|
78
78
|
}
|
|
79
79
|
case 'assistant/message': {
|
|
@@ -103,6 +103,10 @@ function validateEvent(trace, event, fail) {
|
|
|
103
103
|
pendingCalls = { kind: 'delete', callId };
|
|
104
104
|
break;
|
|
105
105
|
}
|
|
106
|
+
case 'system/message': {
|
|
107
|
+
requireOpenStep(trace, 'system/message', event.data.turn, event.data.step, fail);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
106
110
|
case 'user/message':
|
|
107
111
|
break;
|
|
108
112
|
case 'session/end-seed':
|
|
@@ -24,7 +24,7 @@ export const KNOWN_SESSION_EVENT_TYPES = new Set([
|
|
|
24
24
|
'approval/asked',
|
|
25
25
|
'approval/decided',
|
|
26
26
|
'approval/policy',
|
|
27
|
-
'assistant/
|
|
27
|
+
'assistant/attempt',
|
|
28
28
|
'assistant/message',
|
|
29
29
|
'command/done',
|
|
30
30
|
'command/run',
|
|
@@ -32,6 +32,8 @@ export const KNOWN_SESSION_EVENT_TYPES = new Set([
|
|
|
32
32
|
'compaction/prune',
|
|
33
33
|
'compaction/start',
|
|
34
34
|
'compaction/summary',
|
|
35
|
+
'feedback/message-delete',
|
|
36
|
+
'feedback/message-put',
|
|
35
37
|
'feedback/record',
|
|
36
38
|
'goal/change',
|
|
37
39
|
'hook/invoked',
|
|
@@ -53,6 +55,7 @@ export const KNOWN_SESSION_EVENT_TYPES = new Set([
|
|
|
53
55
|
'step/start',
|
|
54
56
|
'subagent/descriptor',
|
|
55
57
|
'subagent/model-selection-policy',
|
|
58
|
+
'system/message',
|
|
56
59
|
'team/member',
|
|
57
60
|
'team/message/delivered',
|
|
58
61
|
'team/message/queued',
|
|
@@ -63,8 +66,8 @@ export const KNOWN_SESSION_EVENT_TYPES = new Set([
|
|
|
63
66
|
'tool-workflow/run-end',
|
|
64
67
|
'tool-workflow/run-start',
|
|
65
68
|
'tool/call',
|
|
66
|
-
'tool/
|
|
67
|
-
'tool/
|
|
69
|
+
'tool/ptc-dispatch',
|
|
70
|
+
'tool/ptc-dispatch-start',
|
|
68
71
|
'tool/result',
|
|
69
72
|
'turn/end',
|
|
70
73
|
'turn/start',
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { EpochHeader, SessionEvent } from './types.ts';
|
|
10
10
|
/**
|
|
11
|
-
* Normalize a header to canonical form: an empty
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Normalize a header to canonical form: an empty tool list becomes an absent
|
|
12
|
+
* field, matching how requests are built. Logging, folding, and comparison use
|
|
13
|
+
* this one representation.
|
|
14
14
|
* @param header - the header to normalize (not mutated).
|
|
15
15
|
* @returns the canonical header.
|
|
16
16
|
*/
|
|
@@ -19,7 +19,7 @@ export declare function canonicalHeader(header: EpochHeader): EpochHeader;
|
|
|
19
19
|
* Field-wise equality over canonical headers. Tool schemas compare in order.
|
|
20
20
|
* @param a - one canonical header.
|
|
21
21
|
* @param b - the other.
|
|
22
|
-
* @returns whether config,
|
|
22
|
+
* @returns whether config, adapter defaults, and tools all match.
|
|
23
23
|
*/
|
|
24
24
|
export declare function headerEquals(a: EpochHeader, b: EpochHeader): boolean;
|
|
25
25
|
/**
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { callConfigEquals } from '@deepseek-ai/dsh-llm';
|
|
10
10
|
/**
|
|
11
|
-
* Normalize a header to canonical form: an empty
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Normalize a header to canonical form: an empty tool list becomes an absent
|
|
12
|
+
* field, matching how requests are built. Logging, folding, and comparison use
|
|
13
|
+
* this one representation.
|
|
14
14
|
* @param header - the header to normalize (not mutated).
|
|
15
15
|
* @returns the canonical header.
|
|
16
16
|
*/
|
|
@@ -21,7 +21,6 @@ export function canonicalHeader(header) {
|
|
|
21
21
|
...adapterDefaults?.reasoningEffort === true || adapterDefaults?.maxTokens === true
|
|
22
22
|
? { adapterDefaults }
|
|
23
23
|
: {},
|
|
24
|
-
...header.system !== undefined && header.system.length > 0 ? { system: header.system } : {},
|
|
25
24
|
...header.tools !== undefined && header.tools.length > 0 ? { tools: header.tools } : {},
|
|
26
25
|
};
|
|
27
26
|
}
|
|
@@ -33,13 +32,12 @@ function sameSchema(a, b) {
|
|
|
33
32
|
* Field-wise equality over canonical headers. Tool schemas compare in order.
|
|
34
33
|
* @param a - one canonical header.
|
|
35
34
|
* @param b - the other.
|
|
36
|
-
* @returns whether config,
|
|
35
|
+
* @returns whether config, adapter defaults, and tools all match.
|
|
37
36
|
*/
|
|
38
37
|
export function headerEquals(a, b) {
|
|
39
38
|
if (!callConfigEquals(a.config, b.config)
|
|
40
39
|
|| a.adapterDefaults?.reasoningEffort !== b.adapterDefaults?.reasoningEffort
|
|
41
|
-
|| a.adapterDefaults?.maxTokens !== b.adapterDefaults?.maxTokens
|
|
42
|
-
|| a.system !== b.system)
|
|
40
|
+
|| a.adapterDefaults?.maxTokens !== b.adapterDefaults?.maxTokens)
|
|
43
41
|
return false;
|
|
44
42
|
const at = a.tools ?? [];
|
|
45
43
|
const bt = b.tools ?? [];
|