@deepseek-ai/dsh-session 0.1.3-alpha.2 → 0.1.5-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.
@@ -11,7 +11,7 @@ import { brandString } from '@deepseek-ai/dsh-brand';
11
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";
@@ -91,13 +91,17 @@ function snapshotSessionHeader(id, source) {
91
91
  * Use {@link snapshotSessionEvent} when exclusive ownership is not guaranteed.
92
92
  * @param event - exclusively owned event imported across a trusted boundary.
93
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.
94
95
  */
95
96
  export function adoptSessionEvent(event) {
97
+ validateSessionEventData(event, `session event at seq ${event.seq}`);
98
+ validateSurfaceMetadata(event);
96
99
  assertMessageEventShape(event, `session event at seq ${event.seq}`);
97
100
  switch (event.type) {
98
101
  case 'user/message':
99
102
  deepFreeze(event.data);
100
103
  break;
104
+ case 'system/message':
101
105
  case 'assistant/message':
102
106
  case 'tool/result':
103
107
  deepFreeze(event.data.message);
@@ -118,6 +122,9 @@ export function snapshotSessionEvent(event) {
118
122
  }
119
123
  /** Validate the fixed event envelope after one-pass JSON materialization. */
120
124
  function assertSessionEventEnvelope(value, index) {
125
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
126
+ throw new Error(`seed event at index ${index} has an invalid event envelope`);
127
+ }
121
128
  const event = value;
122
129
  for (const key in event) {
123
130
  switch (key) {
@@ -143,8 +150,10 @@ function assertSessionEventEnvelope(value, index) {
143
150
  || (event['ignorable'] !== undefined && event['ignorable'] !== true)) {
144
151
  throw new Error(`seed event at index ${index} has an invalid event envelope`);
145
152
  }
153
+ validateSessionEventData(event, `seed ${type} at index ${index}`);
146
154
  switch (type) {
147
155
  case 'request/header':
156
+ case 'system/message':
148
157
  case 'user/message':
149
158
  case 'assistant/attempt':
150
159
  case 'assistant/message':
@@ -160,11 +169,8 @@ function assertCurrentLlmShape(event, index) {
160
169
  ? data
161
170
  : undefined;
162
171
  if (event['type'] === 'request/header') {
163
- const header = record?.['header'];
164
- const headerRecord = typeof header === 'object' && header !== null && !Array.isArray(header)
165
- ? header
166
- : undefined;
167
- const config = headerRecord?.['config'];
172
+ const headerRecord = record?.['header'];
173
+ const config = headerRecord['config'];
168
174
  if (!hasProviderModel(config))
169
175
  throw new Error(`seed request/header at index ${index} lacks provider/model`);
170
176
  const configRecord = config;
@@ -173,7 +179,7 @@ function assertCurrentLlmShape(event, index) {
173
179
  && (typeof reasoningEffort !== 'string' || reasoningEffort.length === 0)) {
174
180
  throw new Error(`seed request/header at index ${index} has an invalid reasoningEffort`);
175
181
  }
176
- assertAdapterDefaults(headerRecord?.['adapterDefaults'], configRecord, index);
182
+ assertAdapterDefaults(headerRecord['adapterDefaults'], configRecord, index);
177
183
  const reason = record?.['reason'];
178
184
  if (reason !== 'initial' && reason !== 'resume' && reason !== 'change' && reason !== 'series') {
179
185
  throw new Error(`seed request/header at index ${index} has an invalid reason`);
@@ -187,8 +193,7 @@ function assertCurrentLlmShape(event, index) {
187
193
  assertAssistantSettlementShape(record, type, index);
188
194
  return;
189
195
  }
190
- if (type !== 'user/message' && type !== 'assistant/message'
191
- && type !== 'tool/result')
196
+ if (!isMessageEventType(type))
192
197
  return;
193
198
  assertMessageEventShape(event, `seed ${type} at index ${index}`);
194
199
  if (type === 'assistant/message') {
@@ -221,11 +226,21 @@ function assertAdapterDefaults(value, config, index) {
221
226
  throw new Error(`seed request/header at index ${index} has invalid adapterDefaults`);
222
227
  }
223
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
+ };
224
240
  /** Validate only the event-specific invariants needed to safely replay a message. */
225
241
  function assertMessageEventShape(event, subject) {
226
242
  const type = event['type'];
227
- if (type !== 'user/message' && type !== 'assistant/message'
228
- && type !== 'tool/result')
243
+ if (!isMessageEventType(type))
229
244
  return;
230
245
  const data = event['data'];
231
246
  const record = typeof data === 'object' && data !== null
@@ -238,7 +253,7 @@ function assertMessageEventShape(event, subject) {
238
253
  throw new Error(`${subject} lacks an identified message`);
239
254
  }
240
255
  const messageRecord = message;
241
- const expectedRole = type === 'assistant/message' ? 'assistant' : 'user';
256
+ const expectedRole = MESSAGE_ROLE_BY_TYPE[type];
242
257
  if (messageRecord['role'] !== expectedRole) {
243
258
  throw new Error(`${subject} message must have role "${expectedRole}"`);
244
259
  }
@@ -252,6 +267,13 @@ function assertMessageEventShape(event, subject) {
252
267
  throw new Error(`${subject} message has invalid content`);
253
268
  }
254
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
+ }
255
277
  if (type === 'assistant/message') {
256
278
  if (sourceRecord['kind'] !== 'model' || !hasProviderModel(sourceRecord)) {
257
279
  throw new Error(`${subject} message must have model source`);
@@ -519,6 +541,7 @@ export class Session {
519
541
  * (BigInt, function, symbol, undefined, negative zero, non-finite number,
520
542
  * circular reference, sparse array, or an exotic object such as
521
543
  * Map/Set/Date/class instance), or when the candidate violates the
544
+ * request-header empty-field or tool-error consistency rules, or the
522
545
  * canonical surface contract (marker shape and eligibility, unique
523
546
  * earlier source-event references, positional replacement validity, and complete
524
547
  * shadowed-node coverage). One iterative pass reads, validates, and
@@ -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;
@@ -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':
@@ -32,6 +32,7 @@ export const KNOWN_SESSION_EVENT_TYPES = new Set([
32
32
  'compaction/prune',
33
33
  'compaction/start',
34
34
  'compaction/summary',
35
+ 'deliverables/presented',
35
36
  'feedback/message-delete',
36
37
  'feedback/message-put',
37
38
  'feedback/record',
@@ -53,8 +54,10 @@ export const KNOWN_SESSION_EVENT_TYPES = new Set([
53
54
  'session/title-llm-request',
54
55
  'step/end',
55
56
  'step/start',
57
+ 'subagent/catalog',
56
58
  'subagent/descriptor',
57
59
  'subagent/model-selection-policy',
60
+ 'system/message',
58
61
  'team/member',
59
62
  'team/message/delivered',
60
63
  'team/message/queued',
@@ -65,8 +68,8 @@ export const KNOWN_SESSION_EVENT_TYPES = new Set([
65
68
  'tool-workflow/run-end',
66
69
  'tool-workflow/run-start',
67
70
  'tool/call',
68
- 'tool/code-dispatch',
69
- 'tool/code-dispatch-start',
71
+ 'tool/ptc-dispatch',
72
+ 'tool/ptc-dispatch-start',
70
73
  'tool/result',
71
74
  'turn/end',
72
75
  '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 ?? [];
@@ -13,7 +13,7 @@ import type { SessionEvent, SurfaceEvent, SurfaceOp } from './types.ts';
13
13
  /**
14
14
  * Whether an event type can join the model-visible surface.
15
15
  * @param type - event type to test.
16
- * @returns true for one of the three message-producing event types.
16
+ * @returns true for one of the four message-producing event types.
17
17
  */
18
18
  export declare function isSurfaceEligibleType(type: string): boolean;
19
19
  /**
@@ -62,6 +62,14 @@ export declare function isReplacementSurfaceEvent(event: SessionEvent): event is
62
62
  * @returns the derived message, or null when the event produces none.
63
63
  */
64
64
  export declare function deriveEventMessage(event: SessionEvent): Message | null;
65
+ /**
66
+ * Reject noncanonical request-header fields and contradictory tool failure metadata.
67
+ * This does not validate complete event payloads or embedded provider streams.
68
+ * @param event - event whose locally related payload fields are inspected.
69
+ * @param subject - event location to include in validation errors.
70
+ * @throws when request data/header is not an object, optional header fields are empty, or tool failure metadata contradicts its message.
71
+ */
72
+ export declare function validateSessionEventData(event: Pick<SessionEvent, 'type' | 'data'>, subject: string): void;
65
73
  /** One replacement operation observed while folding a session surface. */
66
74
  export interface SurfaceFoldReplacement {
67
75
  /** Seq of the event that replaced the prior surface range. */
@@ -87,6 +95,14 @@ export interface SessionSurface {
87
95
  /** Monotonic count of committed positional replacements. */
88
96
  readonly replaceGeneration: number;
89
97
  }
98
+ /**
99
+ * Validate one event's surface metadata without checking membership in a log or surface.
100
+ * @param event - event whose marker and source sequence values are inspected.
101
+ * Unknown ignorable records retain opaque metadata and never change the surface.
102
+ * @returns the validated operation, or undefined for a log-only or unknown ignorable event.
103
+ * @throws when metadata violates event-local eligibility, marker, or source-sequence rules.
104
+ */
105
+ export declare function validateSurfaceMetadata(event: SessionEvent): SurfaceOp | undefined;
90
106
  /**
91
107
  * Replay a complete session log through the canonical surface fold.
92
108
  * @param events - session events in contiguous seq order.
@@ -8,8 +8,10 @@
8
8
  * @module @deepseek-ai/dsh-session/surface
9
9
  */
10
10
  import { SessionLogOffset, SessionSeq } from "./types.js";
11
+ import { KNOWN_SESSION_EVENT_TYPES } from "./known-event-types.js";
11
12
  /** Runtime counterpart of the message-producing event union. */
12
13
  const SURFACE_EVENT_TYPES = new Set([
14
+ 'system/message',
13
15
  'user/message',
14
16
  'assistant/message',
15
17
  'tool/result',
@@ -17,7 +19,7 @@ const SURFACE_EVENT_TYPES = new Set([
17
19
  /**
18
20
  * Whether an event type can join the model-visible surface.
19
21
  * @param type - event type to test.
20
- * @returns true for one of the three message-producing event types.
22
+ * @returns true for one of the four message-producing event types.
21
23
  */
22
24
  export function isSurfaceEligibleType(type) {
23
25
  return SURFACE_EVENT_TYPES.has(type);
@@ -30,7 +32,8 @@ export function isSurfaceEligibleType(type) {
30
32
  export function isSurfaceEvent(event) {
31
33
  if (!SURFACE_EVENT_TYPES.has(event.type))
32
34
  return false;
33
- return event.surfaceOp !== undefined;
35
+ const candidate = event;
36
+ return candidate.surfaceOp !== undefined;
34
37
  }
35
38
  /**
36
39
  * Narrow an event to an append-origin surface event: one that entered the
@@ -85,10 +88,13 @@ export function deriveEventMessage(event) {
85
88
  case 'user/message': {
86
89
  return event.data;
87
90
  }
91
+ // An empty-content message projects to no wire message. For
92
+ // system/message the node records "no system prompt" while keeping its
93
+ // surface position; for assistant/message the event exists only to host a
94
+ // max-tokens step's usage and must not inject a content-less assistant
95
+ // turn into the provider transcript.
96
+ case 'system/message':
88
97
  case 'assistant/message': {
89
- // Skip an empty-content assistant/message: it exists only to host a
90
- // max-tokens step's usage and must not inject a content-less assistant
91
- // turn into the provider transcript.
92
98
  if (event.data.message.content.length === 0)
93
99
  return null;
94
100
  return event.data.message;
@@ -102,6 +108,48 @@ export function deriveEventMessage(event) {
102
108
  return null;
103
109
  }
104
110
  }
111
+ /** Whether a payload field is a JSON object rather than an array or scalar. */
112
+ function isRecord(value) {
113
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
114
+ }
115
+ /**
116
+ * Reject noncanonical request-header fields and contradictory tool failure metadata.
117
+ * This does not validate complete event payloads or embedded provider streams.
118
+ * @param event - event whose locally related payload fields are inspected.
119
+ * @param subject - event location to include in validation errors.
120
+ * @throws when request data/header is not an object, optional header fields are empty, or tool failure metadata contradicts its message.
121
+ */
122
+ export function validateSessionEventData(event, subject) {
123
+ const data = event.data;
124
+ if (event.type === 'request/header') {
125
+ if (!isRecord(data))
126
+ throw new Error(`${subject} data must be an object`);
127
+ const header = data['header'];
128
+ if (!isRecord(header))
129
+ throw new Error(`${subject} header must be an object`);
130
+ if (Object.hasOwn(header, 'system'))
131
+ throw new Error(`${subject} must omit header.system; use system/message`);
132
+ if (Array.isArray(header['tools']) && header['tools'].length === 0) {
133
+ throw new Error(`${subject} must omit empty tools`);
134
+ }
135
+ const defaults = header['adapterDefaults'];
136
+ if (isRecord(defaults) && Object.keys(defaults).length === 0) {
137
+ throw new Error(`${subject} must omit empty adapterDefaults`);
138
+ }
139
+ }
140
+ else if (event.type === 'tool/result') {
141
+ if (!isRecord(data))
142
+ throw new Error(`${subject} data must be an object`);
143
+ if (data['error'] === undefined)
144
+ return;
145
+ const message = data['message'];
146
+ const content = isRecord(message) ? message['content'] : undefined;
147
+ const block = Array.isArray(content) ? content[0] : undefined;
148
+ if (!isRecord(block) || block['isError'] !== true) {
149
+ throw new Error(`${subject} error requires message content[0].isError === true`);
150
+ }
151
+ }
152
+ }
105
153
  /** Create an empty surface fold state. */
106
154
  function createFoldState() {
107
155
  return { nodes: [], replaceGeneration: 0 };
@@ -118,16 +166,19 @@ function isReplaceOp(value) {
118
166
  const op = value;
119
167
  return Object.keys(op).length === 3
120
168
  && Object.hasOwn(op, 'op')
121
- && Object.hasOwn(op, 'start')
122
- && Object.hasOwn(op, 'end')
169
+ && Object.hasOwn(op, 'startSeq')
170
+ && Object.hasOwn(op, 'endSeq')
123
171
  && op['op'] === 'replace'
124
- && isEventSeq(op['start'])
125
- && isEventSeq(op['end']);
172
+ && isEventSeq(op['startSeq'])
173
+ && isEventSeq(op['endSeq']);
126
174
  }
127
175
  /** Validate event-local surface eligibility and return its operation. */
128
176
  function surfaceOpOf(event) {
129
177
  const raw = event;
130
178
  if (!isSurfaceEligibleType(event.type)) {
179
+ // Unknown ignorable records retain opaque metadata without affecting history.
180
+ if (!KNOWN_SESSION_EVENT_TYPES.has(event.type) && event.ignorable === true)
181
+ return;
131
182
  if (raw.surfaceOp !== undefined) {
132
183
  throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry surfaceOp`);
133
184
  }
@@ -185,18 +236,35 @@ function assertProvenance(event, shadowedSeqs) {
185
236
  throw new Error(`surface replace: sourceEventSeqs must include every shadowed surface node; missing ${missing.join(', ')}`);
186
237
  }
187
238
  }
239
+ /**
240
+ * Validate one event's surface metadata without checking membership in a log or surface.
241
+ * @param event - event whose marker and source sequence values are inspected.
242
+ * Unknown ignorable records retain opaque metadata and never change the surface.
243
+ * @returns the validated operation, or undefined for a log-only or unknown ignorable event.
244
+ * @throws when metadata violates event-local eligibility, marker, or source-sequence rules.
245
+ */
246
+ export function validateSurfaceMetadata(event) {
247
+ const op = surfaceOpOf(event);
248
+ if (op !== undefined && op !== 'append'
249
+ && (op.startSeq >= event.seq || op.endSeq >= event.seq)) {
250
+ throw new Error(`surface replace at seq ${event.seq}: startSeq and endSeq must reference earlier events`);
251
+ }
252
+ if (op !== undefined)
253
+ assertProvenance(event, []);
254
+ return op;
255
+ }
188
256
  /** Locate one replacement range without mutating the current fold state. */
189
257
  function replacementRange(state, op) {
190
- const startIdx = state.nodes.indexOf(op.start);
258
+ const startIdx = state.nodes.indexOf(op.startSeq);
191
259
  if (startIdx === -1) {
192
- throw new Error(`surface replace: start seq ${op.start} not found in surface`);
260
+ throw new Error(`surface replace: start seq ${op.startSeq} not found in surface`);
193
261
  }
194
- const endIdx = state.nodes.indexOf(op.end);
262
+ const endIdx = state.nodes.indexOf(op.endSeq);
195
263
  if (endIdx === -1) {
196
- throw new Error(`surface replace: end seq ${op.end} not found in surface`);
264
+ throw new Error(`surface replace: end seq ${op.endSeq} not found in surface`);
197
265
  }
198
266
  if (startIdx > endIdx) {
199
- throw new Error(`surface replace: start seq ${op.start} (index ${startIdx}) is after end seq ${op.end} (index ${endIdx})`);
267
+ throw new Error(`surface replace: start seq ${op.startSeq} (index ${startIdx}) is after end seq ${op.endSeq} (index ${endIdx})`);
200
268
  }
201
269
  return {
202
270
  startIdx,
@@ -254,26 +322,42 @@ function assertToolResultRewrite(event, shadowedSeqs, events, baseSeq) {
254
322
  }
255
323
  }
256
324
  }
325
+ /**
326
+ * Protect the system prompt at surface node 0. A replacement covering node 0
327
+ * while that node is a `system/message` must itself be a `system/message` over
328
+ * exactly that node; later system nodes carry no protection and a compaction
329
+ * range may shadow them.
330
+ */
331
+ function assertSystemHeadRewrite(event, state, startIdx, shadowedSeqs, events, baseSeq) {
332
+ if (startIdx !== 0)
333
+ return;
334
+ const head = events[state.nodes[0] - baseSeq];
335
+ if (head?.type !== 'system/message')
336
+ return;
337
+ if (event.type !== 'system/message' || shadowedSeqs.length !== 1) {
338
+ throw new Error('surface replace: node 0 holds the system prompt and may be rewritten only by a system/message over exactly that node');
339
+ }
340
+ }
257
341
  /** Validate one event at its replay boundary and prepare its atomic fold transition. */
258
342
  function planSurfaceEvent(state, event, expectedSeq, events, baseSeq) {
259
343
  if (event.seq !== expectedSeq) {
260
344
  throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`);
261
345
  }
262
- const surfaceOp = surfaceOpOf(event);
346
+ const surfaceOp = validateSurfaceMetadata(event);
263
347
  if (surfaceOp === undefined)
264
348
  return;
265
349
  if (surfaceOp === 'append') {
266
- assertProvenance(event, []);
267
350
  return { kind: 'append', seq: event.seq };
268
351
  }
269
352
  const range = replacementRange(state, surfaceOp);
270
353
  assertProvenance(event, range.shadowedSeqs);
271
354
  assertToolResultRewrite(event, range.shadowedSeqs, events, baseSeq);
355
+ assertSystemHeadRewrite(event, state, range.startIdx, range.shadowedSeqs, events, baseSeq);
272
356
  return {
273
357
  kind: 'replace',
274
358
  seq: event.seq,
275
- start: surfaceOp.start,
276
- end: surfaceOp.end,
359
+ start: surfaceOp.startSeq,
360
+ end: surfaceOp.endSeq,
277
361
  ...range,
278
362
  };
279
363
  }