@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.
- package/README.i18n.yaml +2 -2
- package/README.md +13 -9
- package/README.zh.md +13 -9
- package/lib/index.js +190 -111
- package/lib/invariant.js +3 -0
- package/lib/types/index.d.ts +3 -1
- package/lib/types/index.js +36 -12
- package/lib/types/invariant.js +4 -0
- package/lib/types/known-event-types.js +5 -2
- package/lib/types/request-header.d.ts +4 -4
- package/lib/types/request-header.js +5 -7
- package/lib/types/surface.d.ts +17 -1
- package/lib/types/surface.js +102 -18
- package/lib/types/types.d.ts +45 -40
- package/lib/types/types.js +1 -1
- package/package.json +9 -9
package/lib/types/index.js
CHANGED
|
@@ -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
|
|
164
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|
package/lib/types/invariant.js
CHANGED
|
@@ -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/
|
|
69
|
-
'tool/
|
|
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
|
|
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 ?? [];
|
package/lib/types/surface.d.ts
CHANGED
|
@@ -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
|
|
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.
|
package/lib/types/surface.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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, '
|
|
122
|
-
&& Object.hasOwn(op, '
|
|
169
|
+
&& Object.hasOwn(op, 'startSeq')
|
|
170
|
+
&& Object.hasOwn(op, 'endSeq')
|
|
123
171
|
&& op['op'] === 'replace'
|
|
124
|
-
&& isEventSeq(op['
|
|
125
|
-
&& isEventSeq(op['
|
|
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.
|
|
258
|
+
const startIdx = state.nodes.indexOf(op.startSeq);
|
|
191
259
|
if (startIdx === -1) {
|
|
192
|
-
throw new Error(`surface replace: start seq ${op.
|
|
260
|
+
throw new Error(`surface replace: start seq ${op.startSeq} not found in surface`);
|
|
193
261
|
}
|
|
194
|
-
const endIdx = state.nodes.indexOf(op.
|
|
262
|
+
const endIdx = state.nodes.indexOf(op.endSeq);
|
|
195
263
|
if (endIdx === -1) {
|
|
196
|
-
throw new Error(`surface replace: end seq ${op.
|
|
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.
|
|
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 =
|
|
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.
|
|
276
|
-
end: surfaceOp.
|
|
359
|
+
start: surfaceOp.startSeq,
|
|
360
|
+
end: surfaceOp.endSeq,
|
|
277
361
|
...range,
|
|
278
362
|
};
|
|
279
363
|
}
|