@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/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/chunk":
54
- requireOpenStep(trace, "assistant/chunk", event.data.turn, event.data.step, fail);
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":
@@ -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), so consumers
128
- * that replay the log as a publication substitute (telemetry adoption)
129
- * start here. Distinct from {@link inheritedEventCount}, the DURABLE
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 taking ownership of fresh persistence values.
158
- * The storage format, event envelopes, sequence continuity, surface transitions,
159
- * and header fields are validated before the restored objects are frozen.
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 - fresh detached events whose ownership is transferred.
162
- * @param header - fresh detached metadata whose ownership is transferred.
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/chunk`.
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 — persistence plugins
309
- * subscribe to `session/event` and flush on `session/flush` / dispose.
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
- * `seedSource: 'persistence'`, metadata and events must be fresh detached
349
- * graphs whose ownership transfers to this call: they are validated and
350
- * frozen in place through {@link Session.fromRestore}, so the caller must
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
@@ -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
- 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`);
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 header = record?.['header'];
183
- const headerRecord = typeof header === 'object' && header !== null && !Array.isArray(header)
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?.['adapterDefaults'], configRecord, index);
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 !== 'user/message' && type !== 'assistant/message'
199
- && type !== 'tool/result')
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 !== 'user/message' && type !== 'assistant/message'
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 === 'assistant/message' ? 'assistant' : 'user';
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), so consumers
349
- * that replay the log as a publication substitute (telemetry adoption)
350
- * start here. Distinct from {@link inheritedEventCount}, the DURABLE
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 taking ownership of fresh persistence values.
381
- * The storage format, event envelopes, sequence continuity, surface transitions,
382
- * and header fields are validated before the restored objects are frozen.
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 - fresh detached events whose ownership is transferred.
385
- * @param header - fresh detached metadata whose ownership is transferred.
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, 'restore', inheritedEventCount);
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 === 'restore'
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 === 'restore' ? source : snapshotJsonValue(source);
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 === 'restore' ? freezeRestoredObject(snapshot) : deepFreeze(snapshot));
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
- // Appended here so the marker is already in `events` when a backend
445
- // captures the creation seed: no load-time write. Re-marking is skipped
446
- // because a cold session is resumed on first touch, so repeatedly opening
447
- // one must not grow its log per open.
448
- if (seed !== undefined && this.log.at(-1)?.type !== 'session/end-seed') {
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/chunk`.
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 — persistence plugins
692
- * subscribe to `session/event` and flush on `session/flush` / dispose.
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
- * `seedSource: 'persistence'`, metadata and events must be fresh detached
754
- * graphs whose ownership transfers to this call: they are validated and
755
- * frozen in place through {@link Session.fromRestore}, so the caller must
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?.seedSource === 'persistence') {
775
- return Session.fromRestore(sessionId, options.seed, options.meta, options.inheritedEventCount);
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;
@@ -72,8 +72,8 @@ function validateEvent(trace, event, fail) {
72
72
  nextStep += 1;
73
73
  break;
74
74
  }
75
- case 'assistant/chunk': {
76
- requireOpenStep(trace, 'assistant/chunk', event.data.turn, event.data.step, fail);
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/chunk',
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/code-dispatch',
67
- 'tool/code-dispatch-start',
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 system prompt and empty tool
12
- * list become absent fields, matching how requests are built. Logging, folding,
13
- * and comparison use this one representation.
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, system, and tools all match.
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 system prompt and empty tool
12
- * list become absent fields, matching how requests are built. Logging, folding,
13
- * and comparison use this one representation.
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, system, and tools all match.
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 ?? [];