@deepseek-ai/dsh-session 0.1.2-alpha.5 → 0.1.3-alpha.2
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 +6 -7
- package/README.zh.md +6 -7
- package/lib/index.js +73 -376
- package/lib/invariant.js +2 -2
- package/lib/types/index.d.ts +23 -20
- package/lib/types/index.js +73 -63
- package/lib/types/invariant.js +2 -2
- package/lib/types/known-event-types.js +3 -1
- package/lib/types/surface.d.ts +1 -1
- package/lib/types/surface.js +8 -5
- package/lib/types/types.d.ts +67 -56
- 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/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
18
|
export type { AssistantMessage, 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';
|
|
@@ -124,9 +122,10 @@ export declare class Session {
|
|
|
124
122
|
* The first seq appended IN THIS PROCESS: the length of the constructor
|
|
125
123
|
* seed (0 without one). Events with smaller seq values entered through
|
|
126
124
|
* construction — replay, fork, or resume — and were never published on the
|
|
127
|
-
* `session/event` firehose (constructor seeds do not emit)
|
|
128
|
-
*
|
|
129
|
-
*
|
|
125
|
+
* `session/event` firehose (constructor seeds do not emit). This offset marks
|
|
126
|
+
* the constructor-input boundary for lifecycle ownership and persistence
|
|
127
|
+
* adoption; consumers that need complete canonical history still start at
|
|
128
|
+
* seq 0. Distinct from {@link inheritedEventCount}, the DURABLE
|
|
130
129
|
* fork-lineage cut: a resumed session's constructor seed is its full stored
|
|
131
130
|
* log, while the inherited count keeps the original fork value — this field is the
|
|
132
131
|
* in-process construction fact.
|
|
@@ -154,16 +153,19 @@ export declare class Session {
|
|
|
154
153
|
*/
|
|
155
154
|
static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader, inheritedEventCount?: SessionLogOffset): Session;
|
|
156
155
|
/**
|
|
157
|
-
* Restore a detached session by
|
|
158
|
-
*
|
|
159
|
-
* and header fields are validated
|
|
156
|
+
* Restore a detached session by adopting an independently owned or deeply frozen seed.
|
|
157
|
+
* Runtime-required event fields, event envelopes, sequence continuity, surface
|
|
158
|
+
* transitions, and header fields are validated without copying or freezing events.
|
|
159
|
+
* Embedded Assistant streams remain opaque until a stream consumer or storage
|
|
160
|
+
* verifier reads them.
|
|
160
161
|
* @param id - restored session identity.
|
|
161
|
-
* @param seed -
|
|
162
|
-
* @param header -
|
|
162
|
+
* @param seed - independently owned or deeply frozen events.
|
|
163
|
+
* @param header - independently owned storage metadata.
|
|
163
164
|
* @param inheritedEventCount - exact fork-inherited prefix length decoded from storage.
|
|
165
|
+
* @param eventState - aliasing state carried from the operation that produced the seed.
|
|
164
166
|
* @returns a restored detached session.
|
|
165
167
|
*/
|
|
166
|
-
static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader, inheritedEventCount: SessionLogOffset): Session;
|
|
168
|
+
static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader, inheritedEventCount: SessionLogOffset, eventState: SessionSeedEventState): Session;
|
|
167
169
|
private constructor();
|
|
168
170
|
/** Cached immutable full snapshot of the private append-only log. */
|
|
169
171
|
private eventsSnapshot;
|
|
@@ -212,7 +214,8 @@ export declare class Session {
|
|
|
212
214
|
* declare how it joins the surface, the sole source of derived model
|
|
213
215
|
* history) and
|
|
214
216
|
* rejected by the compiler for non-surface types like `turn/start` or
|
|
215
|
-
* `assistant/
|
|
217
|
+
* `assistant/attempt`. Assistant messages embed their exact provider
|
|
218
|
+
* stream and cannot cite top-level source events.
|
|
216
219
|
* @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
|
|
217
220
|
* `data` that entered the log, so reading `event.data` back sees the logged
|
|
218
221
|
* value, never the caller's still-mutable input.
|
|
@@ -230,7 +233,7 @@ export declare class Session {
|
|
|
230
233
|
* append reentered while this acceptance/publication boundary is open also
|
|
231
234
|
* rejects before the log changes.
|
|
232
235
|
*/
|
|
233
|
-
append<T extends SessionEventType>(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [opts: SurfaceIntent] : []): SessionEvent<T>;
|
|
236
|
+
append<T extends SessionEventType>(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [opts: SurfaceIntent<T>] : []): SessionEvent<T>;
|
|
234
237
|
/** Cached fold of the request-header events — see {@link requestHeader}. */
|
|
235
238
|
private headerFold;
|
|
236
239
|
/** Log position (events consumed) the header fold has reached. */
|
|
@@ -305,8 +308,9 @@ export declare class SessionForkError extends Error {
|
|
|
305
308
|
/**
|
|
306
309
|
* In-memory session store (`ctx.sessions`).
|
|
307
310
|
*
|
|
308
|
-
* Persistence is intentionally not implemented here —
|
|
309
|
-
*
|
|
311
|
+
* Persistence is intentionally not implemented here — the agent lifecycle
|
|
312
|
+
* attaches a session-log writer to each published session's write handle;
|
|
313
|
+
* a session published outside that lifecycle persists nothing.
|
|
310
314
|
*/
|
|
311
315
|
export declare class SessionStore extends Service {
|
|
312
316
|
private store;
|
|
@@ -345,10 +349,9 @@ export declare class SessionStore extends Service {
|
|
|
345
349
|
*
|
|
346
350
|
* @param id - the session id; omitted, the store mints `session-<n>`.
|
|
347
351
|
* @param options - seed events and/or creation metadata for the header. With
|
|
348
|
-
* `
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
* retain no mutable aliases.
|
|
352
|
+
* `eventState`, every seed event is either independently owned or any
|
|
353
|
+
* shared value is deeply frozen; {@link Session.fromRestore} validates and
|
|
354
|
+
* adopts those values without copying or freezing them.
|
|
352
355
|
* @returns the constructed session, NOT yet in the store.
|
|
353
356
|
* @throws if a session with `id` already exists, metadata is not a plain
|
|
354
357
|
* lossless-JSON record with valid scalar fields, or `meta.cwd` is a
|
package/lib/types/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
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
14
|
import { deriveEventMessage, SurfaceManager } from "./surface.js";
|
|
@@ -16,7 +16,6 @@ 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";
|
|
@@ -117,28 +116,9 @@ export function adoptSessionEvent(event) {
|
|
|
117
116
|
export function snapshotSessionEvent(event) {
|
|
118
117
|
return adoptSessionEvent(structuredClone(event));
|
|
119
118
|
}
|
|
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
119
|
/** Validate the fixed event envelope after one-pass JSON materialization. */
|
|
137
120
|
function assertSessionEventEnvelope(value, index) {
|
|
138
121
|
const event = value;
|
|
139
|
-
if (event['type'] === 'request/header-delta') {
|
|
140
|
-
throw new Error(`seed event at index ${index} uses unsupported legacy request/header-delta format`);
|
|
141
|
-
}
|
|
142
122
|
for (const key in event) {
|
|
143
123
|
switch (key) {
|
|
144
124
|
case 'type':
|
|
@@ -166,6 +146,7 @@ function assertSessionEventEnvelope(value, index) {
|
|
|
166
146
|
switch (type) {
|
|
167
147
|
case 'request/header':
|
|
168
148
|
case 'user/message':
|
|
149
|
+
case 'assistant/attempt':
|
|
169
150
|
case 'assistant/message':
|
|
170
151
|
case 'tool/result':
|
|
171
152
|
assertCurrentLlmShape(event, index);
|
|
@@ -193,12 +174,36 @@ function assertCurrentLlmShape(event, index) {
|
|
|
193
174
|
throw new Error(`seed request/header at index ${index} has an invalid reasoningEffort`);
|
|
194
175
|
}
|
|
195
176
|
assertAdapterDefaults(headerRecord?.['adapterDefaults'], configRecord, index);
|
|
177
|
+
const reason = record?.['reason'];
|
|
178
|
+
if (reason !== 'initial' && reason !== 'resume' && reason !== 'change' && reason !== 'series') {
|
|
179
|
+
throw new Error(`seed request/header at index ${index} has an invalid reason`);
|
|
180
|
+
}
|
|
181
|
+
if (record?.['startsSeries'] !== undefined && record['startsSeries'] !== true) {
|
|
182
|
+
throw new Error(`seed request/header at index ${index} has an invalid startsSeries marker`);
|
|
183
|
+
}
|
|
196
184
|
}
|
|
197
185
|
const type = event['type'];
|
|
186
|
+
if (type === 'assistant/attempt') {
|
|
187
|
+
assertAssistantSettlementShape(record, type, index);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
198
190
|
if (type !== 'user/message' && type !== 'assistant/message'
|
|
199
191
|
&& type !== 'tool/result')
|
|
200
192
|
return;
|
|
201
193
|
assertMessageEventShape(event, `seed ${type} at index ${index}`);
|
|
194
|
+
if (type === 'assistant/message') {
|
|
195
|
+
assertAssistantSettlementShape(record, type, index);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** Validate fields used directly by restored Session lifecycle logic without replaying the embedded stream. */
|
|
199
|
+
function assertAssistantSettlementShape(data, type, index) {
|
|
200
|
+
const turn = data?.['turn'];
|
|
201
|
+
const step = data?.['step'];
|
|
202
|
+
if (typeof turn !== 'number' || !Number.isSafeInteger(turn) || turn < 0 || Object.is(turn, -0)
|
|
203
|
+
|| typeof step !== 'number' || !Number.isSafeInteger(step) || step < 0 || Object.is(step, -0)
|
|
204
|
+
|| !Array.isArray(data?.['stream'])) {
|
|
205
|
+
throw new Error(`seed ${type} at index ${index} has invalid settlement fields`);
|
|
206
|
+
}
|
|
202
207
|
}
|
|
203
208
|
const allowedAdapterKeys = new Set(['reasoningEffort', 'maxTokens']);
|
|
204
209
|
/** Validate adapter-default markers imported from a durable request header. */
|
|
@@ -279,17 +284,6 @@ function hasProviderModel(value) {
|
|
|
279
284
|
return typeof pair['provider'] === 'string' && pair['provider'].length > 0
|
|
280
285
|
&& typeof pair['model'] === 'string' && pair['model'].length > 0;
|
|
281
286
|
}
|
|
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
287
|
/** Resolve one listener snapshot, including Cordis's internal dispatch checks. */
|
|
294
288
|
function collectSessionCallbacks(ctx, args) {
|
|
295
289
|
return [...ctx.events.dispatch('emit', args)];
|
|
@@ -345,9 +339,10 @@ export class Session {
|
|
|
345
339
|
* The first seq appended IN THIS PROCESS: the length of the constructor
|
|
346
340
|
* seed (0 without one). Events with smaller seq values entered through
|
|
347
341
|
* construction — replay, fork, or resume — and were never published on the
|
|
348
|
-
* `session/event` firehose (constructor seeds do not emit)
|
|
349
|
-
*
|
|
350
|
-
*
|
|
342
|
+
* `session/event` firehose (constructor seeds do not emit). This offset marks
|
|
343
|
+
* the constructor-input boundary for lifecycle ownership and persistence
|
|
344
|
+
* adoption; consumers that need complete canonical history still start at
|
|
345
|
+
* seq 0. Distinct from {@link inheritedEventCount}, the DURABLE
|
|
351
346
|
* fork-lineage cut: a resumed session's constructor seed is its full stored
|
|
352
347
|
* log, while the inherited count keeps the original fork value — this field is the
|
|
353
348
|
* in-process construction fact.
|
|
@@ -377,22 +372,23 @@ export class Session {
|
|
|
377
372
|
return new Session(id, seed, header, 'snapshot', inheritedEventCount);
|
|
378
373
|
}
|
|
379
374
|
/**
|
|
380
|
-
* Restore a detached session by
|
|
381
|
-
*
|
|
382
|
-
* and header fields are validated
|
|
375
|
+
* Restore a detached session by adopting an independently owned or deeply frozen seed.
|
|
376
|
+
* Runtime-required event fields, event envelopes, sequence continuity, surface
|
|
377
|
+
* transitions, and header fields are validated without copying or freezing events.
|
|
378
|
+
* Embedded Assistant streams remain opaque until a stream consumer or storage
|
|
379
|
+
* verifier reads them.
|
|
383
380
|
* @param id - restored session identity.
|
|
384
|
-
* @param seed -
|
|
385
|
-
* @param header -
|
|
381
|
+
* @param seed - independently owned or deeply frozen events.
|
|
382
|
+
* @param header - independently owned storage metadata.
|
|
386
383
|
* @param inheritedEventCount - exact fork-inherited prefix length decoded from storage.
|
|
384
|
+
* @param eventState - aliasing state carried from the operation that produced the seed.
|
|
387
385
|
* @returns a restored detached session.
|
|
388
386
|
*/
|
|
389
|
-
static fromRestore(id, seed, header, inheritedEventCount) {
|
|
390
|
-
return new Session(id, seed, header,
|
|
387
|
+
static fromRestore(id, seed, header, inheritedEventCount, eventState) {
|
|
388
|
+
return new Session(id, seed, header, eventState, inheritedEventCount);
|
|
391
389
|
}
|
|
392
390
|
constructor(id, seed, header, mode = 'snapshot', suppliedInheritedEventCount) {
|
|
393
|
-
const restoredHeader = mode === '
|
|
394
|
-
? validateRestoredSessionHeader(id, header)
|
|
395
|
-
: undefined;
|
|
391
|
+
const restoredHeader = mode === 'snapshot' ? undefined : validateRestoredSessionHeader(id, header);
|
|
396
392
|
if (seed !== undefined) {
|
|
397
393
|
// Validate the seed to the SAME invariants `append` enforces, so a
|
|
398
394
|
// replay/fork (`ctx.sessions.create(id, { seed })`) cannot construct a
|
|
@@ -404,12 +400,11 @@ export class Session {
|
|
|
404
400
|
for (const [index, source] of seed.entries()) {
|
|
405
401
|
// The seed is a persistence/replay boundary: validate and detach the
|
|
406
402
|
// complete event in one lossless-JSON pass.
|
|
407
|
-
const snapshot = mode === '
|
|
403
|
+
const snapshot = mode === 'snapshot' ? snapshotJsonValue(source) : source;
|
|
408
404
|
if (snapshot === undefined) {
|
|
409
405
|
throw new Error(`seed event at index ${index} is not losslessly JSON-serializable`);
|
|
410
406
|
}
|
|
411
407
|
assertSessionEventEnvelope(snapshot, index);
|
|
412
|
-
assertSupportedRequestHeader(snapshot.type, snapshot.data, `seed event at index ${index}`);
|
|
413
408
|
if (snapshot.seq !== index) {
|
|
414
409
|
throw new Error(`seed event at index ${index} has seq ${snapshot.seq} (expected ${index}); seed must be contiguous from 0`);
|
|
415
410
|
}
|
|
@@ -422,7 +417,7 @@ export class Session {
|
|
|
422
417
|
catch (error) {
|
|
423
418
|
throw new Error(`invalid seed event at index ${index}: ${error instanceof Error ? error.message : 'invalid surface metadata'}`);
|
|
424
419
|
}
|
|
425
|
-
this.log.push(mode === '
|
|
420
|
+
this.log.push(mode === 'snapshot' ? deepFreeze(snapshot) : snapshot);
|
|
426
421
|
}
|
|
427
422
|
}
|
|
428
423
|
this.firstLiveSeq = SessionLogOffset(this.log.length);
|
|
@@ -440,12 +435,17 @@ export class Session {
|
|
|
440
435
|
if (inheritedEventCount > this.log.length) {
|
|
441
436
|
throw new Error('session inherited event count exceeds its event log');
|
|
442
437
|
}
|
|
438
|
+
if (mode === 'snapshot' && this.header.isSeeded && inheritedEventCount !== this.log.length) {
|
|
439
|
+
throw new Error('seeded session constructor seed must equal its inherited prefix');
|
|
440
|
+
}
|
|
443
441
|
this.inheritedEventCount = inheritedEventCount;
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
442
|
+
// A fresh seeded child always owns one tagged marker at its inherited cut,
|
|
443
|
+
// even when the copied prefix already ends in an ancestor marker. Restore
|
|
444
|
+
// retains that durable marker and appends only the ordinary resume marker.
|
|
445
|
+
if (seed !== undefined && mode === 'snapshot' && this.header.isSeeded) {
|
|
446
|
+
this.append('session/end-seed', { inherited: true });
|
|
447
|
+
}
|
|
448
|
+
else if (seed !== undefined && this.log.at(-1)?.type !== 'session/end-seed') {
|
|
449
449
|
this.append('session/end-seed', {});
|
|
450
450
|
}
|
|
451
451
|
}
|
|
@@ -510,7 +510,8 @@ export class Session {
|
|
|
510
510
|
* declare how it joins the surface, the sole source of derived model
|
|
511
511
|
* history) and
|
|
512
512
|
* rejected by the compiler for non-surface types like `turn/start` or
|
|
513
|
-
* `assistant/
|
|
513
|
+
* `assistant/attempt`. Assistant messages embed their exact provider
|
|
514
|
+
* stream and cannot cite top-level source events.
|
|
514
515
|
* @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
|
|
515
516
|
* `data` that entered the log, so reading `event.data` back sees the logged
|
|
516
517
|
* value, never the caller's still-mutable input.
|
|
@@ -538,7 +539,6 @@ export class Session {
|
|
|
538
539
|
if (dataSnapshot === undefined) {
|
|
539
540
|
throw new Error(`session event "${type}" carries non-JSON-serializable data`);
|
|
540
541
|
}
|
|
541
|
-
assertSupportedRequestHeader(type, dataSnapshot, `session event "${type}"`);
|
|
542
542
|
const surfaceMetadataSnapshot = snapshotJsonValue(surfaceMetadata);
|
|
543
543
|
if (surfaceMetadataSnapshot === undefined) {
|
|
544
544
|
throw new Error(`session event "${type}" carries non-JSON-serializable surface metadata`);
|
|
@@ -688,8 +688,9 @@ export class SessionForkError extends Error {
|
|
|
688
688
|
/**
|
|
689
689
|
* In-memory session store (`ctx.sessions`).
|
|
690
690
|
*
|
|
691
|
-
* Persistence is intentionally not implemented here —
|
|
692
|
-
*
|
|
691
|
+
* Persistence is intentionally not implemented here — the agent lifecycle
|
|
692
|
+
* attaches a session-log writer to each published session's write handle;
|
|
693
|
+
* a session published outside that lifecycle persists nothing.
|
|
693
694
|
*/
|
|
694
695
|
export class SessionStore extends Service {
|
|
695
696
|
store = new Map();
|
|
@@ -750,10 +751,9 @@ export class SessionStore extends Service {
|
|
|
750
751
|
*
|
|
751
752
|
* @param id - the session id; omitted, the store mints `session-<n>`.
|
|
752
753
|
* @param options - seed events and/or creation metadata for the header. With
|
|
753
|
-
* `
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
* retain no mutable aliases.
|
|
754
|
+
* `eventState`, every seed event is either independently owned or any
|
|
755
|
+
* shared value is deeply frozen; {@link Session.fromRestore} validates and
|
|
756
|
+
* adopts those values without copying or freezing them.
|
|
757
757
|
* @returns the constructed session, NOT yet in the store.
|
|
758
758
|
* @throws if a session with `id` already exists, metadata is not a plain
|
|
759
759
|
* lossless-JSON record with valid scalar fields, or `meta.cwd` is a
|
|
@@ -771,8 +771,18 @@ export class SessionStore extends Service {
|
|
|
771
771
|
}
|
|
772
772
|
if (this.store.has(sessionId))
|
|
773
773
|
throw new Error(`session "${sessionId}" already exists`);
|
|
774
|
-
if (options
|
|
775
|
-
|
|
774
|
+
if (options !== undefined) {
|
|
775
|
+
const { eventState } = options;
|
|
776
|
+
switch (eventState) {
|
|
777
|
+
case 'detached':
|
|
778
|
+
case 'shared-frozen':
|
|
779
|
+
return Session.fromRestore(sessionId, options.seed, options.meta, options.inheritedEventCount, eventState);
|
|
780
|
+
case undefined:
|
|
781
|
+
break;
|
|
782
|
+
/* v8 ignore next -- closed-union exhaustiveness guard */
|
|
783
|
+
default:
|
|
784
|
+
assertNever(eventState, 'SessionStore.prepare event state');
|
|
785
|
+
}
|
|
776
786
|
}
|
|
777
787
|
const seed = options?.seed;
|
|
778
788
|
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': {
|
|
@@ -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',
|
package/lib/types/surface.d.ts
CHANGED
|
@@ -50,7 +50,7 @@ export declare function isReplacementSurfaceEvent(event: SessionEvent): event is
|
|
|
50
50
|
};
|
|
51
51
|
/**
|
|
52
52
|
* Project a single event into the LLM message it derives to, or null when it
|
|
53
|
-
* produces none — a non-surface event (
|
|
53
|
+
* produces none — a non-surface event (attempt, boundary, log-only record) or an
|
|
54
54
|
* empty-content assistant/message (which exists only to host usage). This is
|
|
55
55
|
* THE per-node projection rule: `Session.deriveMessages` folds it over the
|
|
56
56
|
* live surface, external reconstructors and pure projections fold the same
|
package/lib/types/surface.js
CHANGED
|
@@ -58,7 +58,7 @@ export function isReplacementSurfaceEvent(event) {
|
|
|
58
58
|
}
|
|
59
59
|
/**
|
|
60
60
|
* Project a single event into the LLM message it derives to, or null when it
|
|
61
|
-
* produces none — a non-surface event (
|
|
61
|
+
* produces none — a non-surface event (attempt, boundary, log-only record) or an
|
|
62
62
|
* empty-content assistant/message (which exists only to host usage). This is
|
|
63
63
|
* THE per-node projection rule: `Session.deriveMessages` folds it over the
|
|
64
64
|
* live surface, external reconstructors and pure projections fold the same
|
|
@@ -71,7 +71,7 @@ export function isReplacementSurfaceEvent(event) {
|
|
|
71
71
|
*/
|
|
72
72
|
export function deriveEventMessage(event) {
|
|
73
73
|
// Intentionally non-exhaustive: only message-producing events derive
|
|
74
|
-
// history; turn/step boundaries,
|
|
74
|
+
// history; turn/step boundaries, failed attempts, and errors are trace/replay
|
|
75
75
|
// data.
|
|
76
76
|
switch (event.type) {
|
|
77
77
|
// Ordinary prompts and injected context project in user role: the event's
|
|
@@ -97,7 +97,7 @@ export function deriveEventMessage(event) {
|
|
|
97
97
|
return event.data.message;
|
|
98
98
|
}
|
|
99
99
|
default:
|
|
100
|
-
// A non-surface event (boundary,
|
|
100
|
+
// A non-surface event (boundary, attempt, log-only record) projects to
|
|
101
101
|
// no message. Merge-extensible union: no assertNever here.
|
|
102
102
|
return null;
|
|
103
103
|
}
|
|
@@ -153,13 +153,16 @@ function surfaceOpOf(event) {
|
|
|
153
153
|
/** Validate cited source-event seqs against prior log entries and the replacement range. */
|
|
154
154
|
function assertProvenance(event, shadowedSeqs) {
|
|
155
155
|
const raw = event.sourceEventSeqs;
|
|
156
|
+
if (event.type === 'assistant/message' && raw !== undefined) {
|
|
157
|
+
throw new Error('assistant/message embeds its source stream and cannot carry sourceEventSeqs');
|
|
158
|
+
}
|
|
156
159
|
const sources = new Set();
|
|
157
160
|
if (raw !== undefined) {
|
|
158
161
|
if (!Array.isArray(raw)) {
|
|
159
162
|
throw new Error(`sourceEventSeqs on event at seq ${event.seq} must be an array when present`);
|
|
160
163
|
}
|
|
161
|
-
if (raw.length === 0
|
|
162
|
-
throw new Error('sourceEventSeqs must not be empty
|
|
164
|
+
if (raw.length === 0) {
|
|
165
|
+
throw new Error('sourceEventSeqs must not be empty');
|
|
163
166
|
}
|
|
164
167
|
let nonEarlierSource;
|
|
165
168
|
for (const source of raw) {
|