@deepseek-ai/dsh-session 0.1.6-alpha.2 → 0.1.7-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 +11 -9
- package/README.zh.md +12 -8
- package/lib/index.js +215 -125
- package/lib/invariant.js +4 -1
- package/lib/types/fork.d.ts +18 -0
- package/lib/types/fork.js +28 -0
- package/lib/types/index.d.ts +27 -29
- package/lib/types/index.js +56 -61
- package/lib/types/invariant.js +5 -1
- package/lib/types/known-event-types.js +1 -0
- package/lib/types/repair.d.ts +42 -7
- package/lib/types/repair.js +52 -23
- package/lib/types/surface.d.ts +4 -4
- package/lib/types/surface.js +73 -17
- package/lib/types/types.d.ts +38 -15
- package/lib/types/types.js +1 -1
- package/package.json +15 -11
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fork seed construction over an exact source-event prefix.
|
|
3
|
+
* @module @deepseek-ai/dsh-session/fork
|
|
4
|
+
*/
|
|
5
|
+
import { openTurnClosers } from "./repair.js";
|
|
6
|
+
import { SessionSeq } from "./types.js";
|
|
7
|
+
/**
|
|
8
|
+
* Copy an inclusive event prefix, mark its inherited cut, and close its open tail with forked results
|
|
9
|
+
* and step/turn endings. Closed steps and turns are preserved unchanged.
|
|
10
|
+
* The caller validates that the boundary is an existing contiguous event seq;
|
|
11
|
+
* Session construction snapshots the borrowed events before publication.
|
|
12
|
+
*
|
|
13
|
+
* @param events - source log with contiguous seqs from zero.
|
|
14
|
+
* @param boundary - inclusive source event seq the child inherits through.
|
|
15
|
+
* @returns a new array retaining the source event objects, followed by synthetic
|
|
16
|
+
* closers outside the inherited prefix counted by `inheritedEventCount`.
|
|
17
|
+
*/
|
|
18
|
+
export function buildForkSeed(events, boundary) {
|
|
19
|
+
const prefix = events.slice(0, boundary + 1);
|
|
20
|
+
prefix.push({
|
|
21
|
+
type: 'session/end-seed', seq: SessionSeq(boundary + 1),
|
|
22
|
+
// oxlint-disable-next-line typescript/no-non-null-assertion -- the caller validated this exact source event.
|
|
23
|
+
time: events[boundary].time,
|
|
24
|
+
data: { inherited: true },
|
|
25
|
+
});
|
|
26
|
+
return prefix.concat(openTurnClosers(prefix, { kind: 'forked' }));
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=fork.js.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -12,10 +12,11 @@ import { SessionLogOffset, SessionSeq } from './types.ts';
|
|
|
12
12
|
import type { TypertLookup } from '@deepseek-ai/dsh-typert-protocol';
|
|
13
13
|
import type { CreateSessionOptions, EpochHeader, PrepareSessionOptions, RequestContext, SessionEvent, SessionEventMap, SessionEventType, SessionHeader, SessionId, SessionSeedEventState, SurfaceIntent, SurfaceEventType } from './types.ts';
|
|
14
14
|
import type { SessionSurface, SessionMessageProjection } from './surface.ts';
|
|
15
|
+
export { buildForkSeed } from './fork.ts';
|
|
15
16
|
export * from './types.ts';
|
|
16
17
|
export { SessionPreparation } from './preparation.ts';
|
|
17
18
|
export type { SessionPreparationOptions } from './preparation.ts';
|
|
18
|
-
export type { AssistantMessage, SystemMessage, ToolResultMessage, UserMessage } from '@deepseek-ai/dsh-llm';
|
|
19
|
+
export type { AssistantMessage, DeveloperMessage, SystemMessage, ToolResultMessage, UserMessage } from '@deepseek-ai/dsh-llm';
|
|
19
20
|
export { interruptedTurnClosers, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from './repair.ts';
|
|
20
21
|
export type { SessionSurface, SurfaceFoldReplacement, SurfaceFoldResult, SessionMessageProjection, SessionMessageProjectionContext } from './surface.ts';
|
|
21
22
|
export { deriveEventMessage, foldSurface, isAppendSurfaceEvent, isReplacementSurfaceEvent, isSurfaceEvent, isSurfaceEligibleType } from './surface.ts';
|
|
@@ -120,29 +121,25 @@ export declare class Session {
|
|
|
120
121
|
/** The session identity, derived from its durable header's single copy. */
|
|
121
122
|
get id(): SessionId;
|
|
122
123
|
/**
|
|
123
|
-
* The
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
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
|
-
* fork-lineage cut: a resumed session's constructor seed is its full stored
|
|
131
|
-
* log, while the inherited count keeps the original fork value — this field is the
|
|
132
|
-
* in-process construction fact.
|
|
124
|
+
* The constructor seed length (0 without one), before any marker appended
|
|
125
|
+
* during construction. Seed events never publish on `session/event`. A
|
|
126
|
+
* marker appended before the store attaches occupies this seq without
|
|
127
|
+
* publishing either; otherwise this seq is available for the next append.
|
|
133
128
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* When this lifecycle appends the marker, it occupies this seq before the
|
|
142
|
-
* store attaches and therefore does not publish either. Otherwise this seq
|
|
143
|
-
* holds an ordinary published write.
|
|
129
|
+
* This in-process offset is not persisted. A fork seed can already contain
|
|
130
|
+
* the child's inherited marker and synthetic closers, so its child-owned
|
|
131
|
+
* history starts at {@link inheritedEventCount}, before this offset. A
|
|
132
|
+
* resumed Session's seed contains its full stored log, while its inherited
|
|
133
|
+
* count keeps the durable fork cut. Consumers needing complete canonical
|
|
134
|
+
* history start at seq 0.
|
|
144
135
|
*/
|
|
145
136
|
readonly firstLiveSeq: SessionLogOffset;
|
|
137
|
+
/**
|
|
138
|
+
* First event produced for this object lifecycle. A new fork includes its
|
|
139
|
+
* child-owned seed marker and closers; a restored Session starts after its
|
|
140
|
+
* complete stored prefix. This in-process capture offset is not persisted.
|
|
141
|
+
*/
|
|
142
|
+
readonly firstLifecycleSeq: SessionLogOffset;
|
|
146
143
|
/**
|
|
147
144
|
* Create a detached session by validating and snapshotting borrowed seed
|
|
148
145
|
* events and storage metadata.
|
|
@@ -307,11 +304,10 @@ export type SessionForkSource = Session | SessionId;
|
|
|
307
304
|
* Rejection codes for session forking: the fork source id is unknown to the
|
|
308
305
|
* live store (`SESSION_NOT_FOUND`) or names a session object that is not the
|
|
309
306
|
* store's live instance (`SESSION_NOT_LIVE`); the requested child id is
|
|
310
|
-
* already taken (`SESSION_ALREADY_EXISTS`); the boundary is not a contiguous
|
|
311
|
-
* existing seq (`INVALID_BOUNDARY`)
|
|
312
|
-
* open turn (`OPEN_TURN`).
|
|
307
|
+
* already taken (`SESSION_ALREADY_EXISTS`); or the boundary is not a contiguous
|
|
308
|
+
* existing seq (`INVALID_BOUNDARY`).
|
|
313
309
|
*/
|
|
314
|
-
export type SessionForkErrorCode = 'SESSION_NOT_FOUND' | 'SESSION_NOT_LIVE' | 'SESSION_ALREADY_EXISTS' | 'INVALID_BOUNDARY'
|
|
310
|
+
export type SessionForkErrorCode = 'SESSION_NOT_FOUND' | 'SESSION_NOT_LIVE' | 'SESSION_ALREADY_EXISTS' | 'INVALID_BOUNDARY';
|
|
315
311
|
/** Typed error for session fork rejections. */
|
|
316
312
|
export declare class SessionForkError extends Error {
|
|
317
313
|
readonly code: SessionForkErrorCode;
|
|
@@ -444,10 +440,12 @@ export declare class SessionStore extends Service {
|
|
|
444
440
|
*/
|
|
445
441
|
list(): Session[];
|
|
446
442
|
/**
|
|
447
|
-
* Create a live child session from
|
|
443
|
+
* Create a live child session from an exact prefix of a live source.
|
|
448
444
|
* `boundary` is an inclusive source event seq; omitted means the source's
|
|
449
|
-
* current last event.
|
|
450
|
-
*
|
|
445
|
+
* current last event. An open tail receives synthetic tool results and
|
|
446
|
+
* step/turn closers with the forked cause. Closed steps and turns remain
|
|
447
|
+
* unchanged, including any failed tool calls already missing results.
|
|
448
|
+
* `inheritedEventCount` counts only copied source events, excluding these closers.
|
|
451
449
|
*
|
|
452
450
|
* @param source - Live source session object or id.
|
|
453
451
|
* @param boundary - Inclusive source event seq to fork through; omitted means
|
|
@@ -458,7 +456,7 @@ export declare class SessionStore extends Service {
|
|
|
458
456
|
* @returns The created live child session.
|
|
459
457
|
*/
|
|
460
458
|
fork(source: SessionForkSource, boundary?: SessionSeq, childSessionId?: SessionId): Session;
|
|
461
|
-
private
|
|
459
|
+
private _forkBoundary;
|
|
462
460
|
private _resolveForkSource;
|
|
463
461
|
}
|
|
464
462
|
export { decodeSeqRanges, encodeSeqRanges } from './seq-ranges.ts';
|
package/lib/types/index.js
CHANGED
|
@@ -13,6 +13,8 @@ import { scopeOf, scopeTarget } from '@deepseek-ai/dsh-scope';
|
|
|
13
13
|
import { SESSION_FORMAT_VERSION, SessionLogOffset, SessionSeq } from "./types.js";
|
|
14
14
|
import { SurfaceManager, validateSessionEventData, validateSurfaceMetadata } from "./surface.js";
|
|
15
15
|
import { foldRequestHeader } from "./request-header.js";
|
|
16
|
+
import { buildForkSeed } from "./fork.js";
|
|
17
|
+
export { buildForkSeed } from "./fork.js";
|
|
16
18
|
export * from "./types.js";
|
|
17
19
|
export { SessionPreparation } from "./preparation.js";
|
|
18
20
|
export { interruptedTurnClosers, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from "./repair.js";
|
|
@@ -101,6 +103,7 @@ export function adoptSessionEvent(event) {
|
|
|
101
103
|
case 'user/message':
|
|
102
104
|
deepFreeze(event.data);
|
|
103
105
|
break;
|
|
106
|
+
case 'developer/message':
|
|
104
107
|
case 'system/message':
|
|
105
108
|
case 'assistant/message':
|
|
106
109
|
case 'tool/result':
|
|
@@ -153,6 +156,7 @@ function assertSessionEventEnvelope(value, index) {
|
|
|
153
156
|
validateSessionEventData(event, `seed ${type} at index ${index}`);
|
|
154
157
|
switch (type) {
|
|
155
158
|
case 'request/header':
|
|
159
|
+
case 'developer/message':
|
|
156
160
|
case 'system/message':
|
|
157
161
|
case 'user/message':
|
|
158
162
|
case 'assistant/attempt':
|
|
@@ -226,16 +230,17 @@ function assertAdapterDefaults(value, config, index) {
|
|
|
226
230
|
throw new Error(`seed request/header at index ${index} has invalid adapterDefaults`);
|
|
227
231
|
}
|
|
228
232
|
}
|
|
229
|
-
/** The
|
|
233
|
+
/** The surface event types whose payload carries an identified message. */
|
|
230
234
|
function isMessageEventType(type) {
|
|
231
|
-
return type === 'system/message' || type === 'user/message'
|
|
235
|
+
return type === 'developer/message' || type === 'system/message' || type === 'user/message'
|
|
232
236
|
|| type === 'assistant/message' || type === 'tool/result';
|
|
233
237
|
}
|
|
234
238
|
const MESSAGE_ROLE_BY_TYPE = {
|
|
235
239
|
'system/message': 'system',
|
|
240
|
+
'developer/message': 'developer',
|
|
236
241
|
'user/message': 'user',
|
|
237
242
|
'assistant/message': 'assistant',
|
|
238
|
-
'tool/result': '
|
|
243
|
+
'tool/result': 'tool',
|
|
239
244
|
};
|
|
240
245
|
/** Validate only the event-specific invariants needed to safely replay a message. */
|
|
241
246
|
function assertMessageEventShape(event, subject) {
|
|
@@ -268,9 +273,8 @@ function assertMessageEventShape(event, subject) {
|
|
|
268
273
|
}
|
|
269
274
|
const sourceRecord = source;
|
|
270
275
|
if (type === 'system/message') {
|
|
271
|
-
if (sourceRecord['kind'] !== '
|
|
272
|
-
|
|
273
|
-
throw new Error(`${subject} message must have plugin source`);
|
|
276
|
+
if (sourceRecord['kind'] !== 'system-prompt') {
|
|
277
|
+
throw new Error(`${subject} message must have system-prompt source`);
|
|
274
278
|
}
|
|
275
279
|
return;
|
|
276
280
|
}
|
|
@@ -287,14 +291,7 @@ function assertMessageEventShape(event, subject) {
|
|
|
287
291
|
|| sourceRecord['callId'] === '') {
|
|
288
292
|
throw new Error(`${subject} message must have tool source`);
|
|
289
293
|
}
|
|
290
|
-
|
|
291
|
-
const block = content[0];
|
|
292
|
-
if (content.length !== 1 || typeof block !== 'object' || block === null
|
|
293
|
-
|| block['type'] !== 'tool-result'
|
|
294
|
-
|| !Array.isArray(block['content'])) {
|
|
295
|
-
throw new Error(`${subject} message must contain one tool-result block`);
|
|
296
|
-
}
|
|
297
|
-
if (block['toolCallId'] !== sourceRecord['callId']) {
|
|
294
|
+
if (messageRecord['toolCallId'] !== sourceRecord['callId']) {
|
|
298
295
|
throw new Error(`${subject} message has mismatched tool call ids`);
|
|
299
296
|
}
|
|
300
297
|
}
|
|
@@ -358,29 +355,25 @@ export class Session {
|
|
|
358
355
|
return this.header.id;
|
|
359
356
|
}
|
|
360
357
|
/**
|
|
361
|
-
* The
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
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
|
|
368
|
-
* fork-lineage cut: a resumed session's constructor seed is its full stored
|
|
369
|
-
* log, while the inherited count keeps the original fork value — this field is the
|
|
370
|
-
* in-process construction fact.
|
|
371
|
-
*
|
|
372
|
-
* Not persisted itself: a seeded session projects it into the log as the
|
|
373
|
-
* `session/end-seed` event, which is what a consumer reading STORED history
|
|
374
|
-
* reads. Locate the LAST such event, not necessarily one at this seq — a
|
|
375
|
-
* seed already ending in one is not re-marked, so reopening an untouched
|
|
376
|
-
* session leaves that event at a smaller seq than `firstLiveSeq`. Prefer
|
|
377
|
-
* this field in-process: it is exact before the marker reaches storage.
|
|
358
|
+
* The constructor seed length (0 without one), before any marker appended
|
|
359
|
+
* during construction. Seed events never publish on `session/event`. A
|
|
360
|
+
* marker appended before the store attaches occupies this seq without
|
|
361
|
+
* publishing either; otherwise this seq is available for the next append.
|
|
378
362
|
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
363
|
+
* This in-process offset is not persisted. A fork seed can already contain
|
|
364
|
+
* the child's inherited marker and synthetic closers, so its child-owned
|
|
365
|
+
* history starts at {@link inheritedEventCount}, before this offset. A
|
|
366
|
+
* resumed Session's seed contains its full stored log, while its inherited
|
|
367
|
+
* count keeps the durable fork cut. Consumers needing complete canonical
|
|
368
|
+
* history start at seq 0.
|
|
382
369
|
*/
|
|
383
370
|
firstLiveSeq;
|
|
371
|
+
/**
|
|
372
|
+
* First event produced for this object lifecycle. A new fork includes its
|
|
373
|
+
* child-owned seed marker and closers; a restored Session starts after its
|
|
374
|
+
* complete stored prefix. This in-process capture offset is not persisted.
|
|
375
|
+
*/
|
|
376
|
+
firstLifecycleSeq;
|
|
384
377
|
/**
|
|
385
378
|
* Create a detached session by validating and snapshotting borrowed seed
|
|
386
379
|
* events and storage metadata.
|
|
@@ -462,17 +455,23 @@ export class Session {
|
|
|
462
455
|
if (inheritedEventCount > this.log.length) {
|
|
463
456
|
throw new Error('session inherited event count exceeds its event log');
|
|
464
457
|
}
|
|
465
|
-
|
|
466
|
-
|
|
458
|
+
const seedMarker = this.log[inheritedEventCount];
|
|
459
|
+
const markedSeed = seedMarker?.type === 'session/end-seed' && seedMarker.data.inherited === true;
|
|
460
|
+
if (mode === 'snapshot' && this.header.isSeeded && inheritedEventCount !== this.log.length && !markedSeed) {
|
|
461
|
+
throw new Error('seeded session constructor seed must equal its inherited prefix or mark its inherited cut');
|
|
462
|
+
}
|
|
463
|
+
if (markedSeed && this.log.slice(inheritedEventCount + 1).some(event => event.type === 'session/end-seed' && event.data.inherited === true)) {
|
|
464
|
+
throw new Error('session inherited event count must identify the final inherited marker');
|
|
467
465
|
}
|
|
468
466
|
this.inheritedEventCount = inheritedEventCount;
|
|
467
|
+
this.firstLifecycleSeq = mode === 'snapshot' && this.header.isSeeded ? inheritedEventCount : this.firstLiveSeq;
|
|
469
468
|
// A fresh seeded child always owns one tagged marker at its inherited cut,
|
|
470
469
|
// even when the copied prefix already ends in an ancestor marker. Restore
|
|
471
470
|
// retains that durable marker and appends only the ordinary resume marker.
|
|
472
|
-
if (seed !== undefined && mode === 'snapshot' && this.header.isSeeded) {
|
|
471
|
+
if (seed !== undefined && mode === 'snapshot' && this.header.isSeeded && !markedSeed) {
|
|
473
472
|
this.append('session/end-seed', { inherited: true });
|
|
474
473
|
}
|
|
475
|
-
else if (seed !== undefined && this.log.at(-1)?.type !== 'session/end-seed') {
|
|
474
|
+
else if (seed !== undefined && !(mode === 'snapshot' && this.header.isSeeded) && this.log.at(-1)?.type !== 'session/end-seed') {
|
|
476
475
|
this.append('session/end-seed', {});
|
|
477
476
|
}
|
|
478
477
|
}
|
|
@@ -1035,10 +1034,12 @@ export class SessionStore extends Service {
|
|
|
1035
1034
|
return [...this.store.values()].map(entry => entry.session);
|
|
1036
1035
|
}
|
|
1037
1036
|
/**
|
|
1038
|
-
* Create a live child session from
|
|
1037
|
+
* Create a live child session from an exact prefix of a live source.
|
|
1039
1038
|
* `boundary` is an inclusive source event seq; omitted means the source's
|
|
1040
|
-
* current last event.
|
|
1041
|
-
*
|
|
1039
|
+
* current last event. An open tail receives synthetic tool results and
|
|
1040
|
+
* step/turn closers with the forked cause. Closed steps and turns remain
|
|
1041
|
+
* unchanged, including any failed tool calls already missing results.
|
|
1042
|
+
* `inheritedEventCount` counts only copied source events, excluding these closers.
|
|
1042
1043
|
*
|
|
1043
1044
|
* @param source - Live source session object or id.
|
|
1044
1045
|
* @param boundary - Inclusive source event seq to fork through; omitted means
|
|
@@ -1053,10 +1054,13 @@ export class SessionStore extends Service {
|
|
|
1053
1054
|
throw new SessionForkError(`session "${childSessionId}" already exists`, 'SESSION_ALREADY_EXISTS');
|
|
1054
1055
|
}
|
|
1055
1056
|
const liveSource = this._resolveForkSource(source);
|
|
1056
|
-
|
|
1057
|
+
// oxlint-disable-next-line typescript/no-deprecated -- Existing fork snapshot read; migration deferred.
|
|
1058
|
+
const events = liveSource.snapshotEvents();
|
|
1059
|
+
const resolved = this._forkBoundary(liveSource.id, events, boundary);
|
|
1060
|
+
const seed = resolved === undefined ? [] : buildForkSeed(events, resolved);
|
|
1057
1061
|
return this.create(childSessionId, {
|
|
1058
1062
|
seed,
|
|
1059
|
-
inheritedEventCount: SessionLogOffset(
|
|
1063
|
+
inheritedEventCount: SessionLogOffset(resolved === undefined ? 0 : resolved + 1),
|
|
1060
1064
|
meta: {
|
|
1061
1065
|
...liveSource.header.cwd !== undefined ? { cwd: liveSource.header.cwd } : {},
|
|
1062
1066
|
parentSession: liveSource.id,
|
|
@@ -1064,38 +1068,29 @@ export class SessionStore extends Service {
|
|
|
1064
1068
|
},
|
|
1065
1069
|
});
|
|
1066
1070
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
const lastEvent = session.snapshotEvents().at(-1);
|
|
1071
|
+
_forkBoundary(sessionId, events, requestedBoundary) {
|
|
1072
|
+
const lastEvent = events.at(-1);
|
|
1070
1073
|
let boundary;
|
|
1071
1074
|
if (requestedBoundary !== undefined) {
|
|
1072
1075
|
boundary = requestedBoundary;
|
|
1073
1076
|
}
|
|
1074
1077
|
else {
|
|
1075
1078
|
if (lastEvent === undefined)
|
|
1076
|
-
return
|
|
1079
|
+
return undefined;
|
|
1077
1080
|
boundary = lastEvent.seq;
|
|
1078
1081
|
}
|
|
1079
1082
|
if (!Number.isSafeInteger(boundary) || boundary < 0) {
|
|
1080
|
-
throw new SessionForkError(`fork boundary for session "${
|
|
1083
|
+
throw new SessionForkError(`fork boundary for session "${sessionId}" must be a non-negative safe integer, got ${String(boundary)}`, 'INVALID_BOUNDARY');
|
|
1081
1084
|
}
|
|
1082
|
-
if (boundary >=
|
|
1085
|
+
if (boundary >= events.length) {
|
|
1083
1086
|
const lastSeq = lastEvent?.seq;
|
|
1084
|
-
throw new SessionForkError(`fork boundary ${boundary} does not exist in session "${
|
|
1087
|
+
throw new SessionForkError(`fork boundary ${boundary} does not exist in session "${sessionId}" (last seq: ${lastSeq ?? 'none'})`, 'INVALID_BOUNDARY');
|
|
1085
1088
|
}
|
|
1086
|
-
|
|
1087
|
-
const boundaryEvent = session.eventAt(boundary);
|
|
1089
|
+
const boundaryEvent = events[boundary];
|
|
1088
1090
|
if (boundaryEvent === undefined || boundaryEvent.seq !== boundary) {
|
|
1089
|
-
throw new SessionForkError(`fork boundary ${boundary} does not match a contiguous event seq in session "${
|
|
1090
|
-
}
|
|
1091
|
-
// oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.
|
|
1092
|
-
const events = session.snapshotEvents(SessionLogOffset(0), SessionLogOffset(boundary + 1));
|
|
1093
|
-
const lastTurnBoundary = events
|
|
1094
|
-
.findLast(event => event.type === 'turn/start' || event.type === 'turn/end');
|
|
1095
|
-
if (lastTurnBoundary?.type === 'turn/start') {
|
|
1096
|
-
throw new SessionForkError(`fork boundary ${boundary} in session "${session.id}" ends inside open turn ${lastTurnBoundary.data.turn}`, 'OPEN_TURN');
|
|
1091
|
+
throw new SessionForkError(`fork boundary ${boundary} does not match a contiguous event seq in session "${sessionId}"`, 'INVALID_BOUNDARY');
|
|
1097
1092
|
}
|
|
1098
|
-
return
|
|
1093
|
+
return boundary;
|
|
1099
1094
|
}
|
|
1100
1095
|
_resolveForkSource(source) {
|
|
1101
1096
|
if (typeof source === 'string') {
|
package/lib/types/invariant.js
CHANGED
|
@@ -65,6 +65,10 @@ function validateEvent(trace, event, fail) {
|
|
|
65
65
|
openStep = event.data.step;
|
|
66
66
|
break;
|
|
67
67
|
}
|
|
68
|
+
case 'developer/message': {
|
|
69
|
+
requireOpenStep(trace, 'developer/message', event.data.turn, event.data.step, fail);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
68
72
|
case 'step/end': {
|
|
69
73
|
requireOpenStep(trace, 'step/end', event.data.turn, event.data.step, fail);
|
|
70
74
|
pendingCalls = { kind: 'clear' };
|
|
@@ -96,7 +100,7 @@ function validateEvent(trace, event, fail) {
|
|
|
96
100
|
}
|
|
97
101
|
requireOpenStep(trace, 'tool/result', event.data.turn, event.data.step, fail);
|
|
98
102
|
const callId = event.data.message.source.callId;
|
|
99
|
-
const syntheticNotStarted = event.data.message.
|
|
103
|
+
const syntheticNotStarted = event.data.message.isError === true && event.data.error?.code === TOOL_NOT_STARTED;
|
|
100
104
|
if (!trace.pendingCalls.has(callId) && !syntheticNotStarted) {
|
|
101
105
|
fail(`tool/result for ${callId} with no prior tool/call in this step`);
|
|
102
106
|
}
|
package/lib/types/repair.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Synthetic closer events that balance a session log whose tail turn is open.
|
|
3
|
+
* Two producers share the mechanism: crash recovery closes an interrupted
|
|
4
|
+
* persisted log on reload, and fork-seed construction closes a prefix cut
|
|
5
|
+
* inside the source's open turn. Both preserve every fully written event and
|
|
6
|
+
* close the unfinished step and turn. Calls in already closed steps remain
|
|
7
|
+
* unchanged, including any missing results.
|
|
5
8
|
* @module @deepseek-ai/dsh-session/repair
|
|
6
9
|
*/
|
|
7
10
|
import type { SessionEvent } from './types.ts';
|
|
@@ -9,14 +12,46 @@ import type { SessionEvent } from './types.ts';
|
|
|
9
12
|
export declare const TOOL_NOT_STARTED = "TOOL_NOT_STARTED";
|
|
10
13
|
/** Recovery code for a recorded tool call whose completed outcome was not durably recorded. */
|
|
11
14
|
export declare const TOOL_OUTCOME_UNKNOWN = "TOOL_OUTCOME_UNKNOWN";
|
|
15
|
+
/**
|
|
16
|
+
* Why an open tail turn is closed with synthetic events: `interrupted` is
|
|
17
|
+
* crash recovery over a persisted log; `forked` is a fork seed cut inside the
|
|
18
|
+
* source's open turn. The cause selects the synthetic `turn/end` reason, the
|
|
19
|
+
* model-visible wording of synthetic error tool results, and the
|
|
20
|
+
* deterministic synthetic message-id prefix. The error codes
|
|
21
|
+
* ({@link TOOL_NOT_STARTED} / {@link TOOL_OUTCOME_UNKNOWN}) are shared: both
|
|
22
|
+
* causes state the same fact about the call's recorded lifecycle.
|
|
23
|
+
*/
|
|
24
|
+
export type OpenTurnCloseCause = {
|
|
25
|
+
readonly kind: 'interrupted';
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: 'forked';
|
|
28
|
+
};
|
|
12
29
|
/**
|
|
13
30
|
* Return deterministic synthetic events that close an open tail turn. Unmatched
|
|
14
|
-
* calls receive error results
|
|
15
|
-
*
|
|
16
|
-
* last real event. A balanced or empty log returns no
|
|
31
|
+
* calls in its open step receive error results, followed by `step/end` and a
|
|
32
|
+
* `turn/end` carrying the cause's reason. Calls in closed steps remain unchanged.
|
|
33
|
+
* Sequences continue the log and timestamps reuse the last real event. A balanced or empty log returns no
|
|
34
|
+
* events.
|
|
35
|
+
*
|
|
36
|
+
* Package-internal: each cause has exactly one owner, so external callers go
|
|
37
|
+
* through {@link interruptedTurnClosers} (persistence crash recovery) or
|
|
38
|
+
* `buildForkSeed` in `./fork.ts` (fork seeds) instead of selecting a cause.
|
|
17
39
|
*
|
|
18
|
-
* @param events - the
|
|
40
|
+
* @param events - the log to scan: a valid committed prefix, possibly ending
|
|
41
|
+
* inside an open turn (a crash tail or a mid-turn fork cut).
|
|
42
|
+
* @param cause - why the turn is being closed; selects the `turn/end` reason
|
|
43
|
+
* and the model-visible wording of synthetic error tool results.
|
|
19
44
|
* @returns the synthetic closer events to append after `events`, in order; empty when the log is already balanced.
|
|
20
45
|
*/
|
|
46
|
+
export declare function openTurnClosers(events: readonly SessionEvent[], cause: OpenTurnCloseCause): SessionEvent[];
|
|
47
|
+
/**
|
|
48
|
+
* Crash-recovery entry point: synthetic closers that balance a persisted log
|
|
49
|
+
* whose tail turn was interrupted. Used by crash-recovery callers; fork
|
|
50
|
+
* seeds receive their `forked`-cause closers through `buildForkSeed` in
|
|
51
|
+
* `./fork.ts`, and cause selection stays internal to those two owners.
|
|
52
|
+
*
|
|
53
|
+
* @param events - the persisted log to scan, possibly ending inside an open turn.
|
|
54
|
+
* @returns the synthetic `interrupted` closer events to append after `events`; empty when the log is already balanced.
|
|
55
|
+
*/
|
|
21
56
|
export declare function interruptedTurnClosers(events: readonly SessionEvent[]): SessionEvent[];
|
|
22
57
|
//# sourceMappingURL=repair.d.ts.map
|
package/lib/types/repair.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Synthetic closer events that balance a session log whose tail turn is open.
|
|
3
|
+
* Two producers share the mechanism: crash recovery closes an interrupted
|
|
4
|
+
* persisted log on reload, and fork-seed construction closes a prefix cut
|
|
5
|
+
* inside the source's open turn. Both preserve every fully written event and
|
|
6
|
+
* close the unfinished step and turn. Calls in already closed steps remain
|
|
7
|
+
* unchanged, including any missing results.
|
|
5
8
|
* @module @deepseek-ai/dsh-session/repair
|
|
6
9
|
*/
|
|
7
10
|
import { brandString } from '@deepseek-ai/dsh-brand';
|
|
@@ -11,16 +14,35 @@ import { SessionSeq } from "./types.js";
|
|
|
11
14
|
export const TOOL_NOT_STARTED = 'TOOL_NOT_STARTED';
|
|
12
15
|
/** Recovery code for a recorded tool call whose completed outcome was not durably recorded. */
|
|
13
16
|
export const TOOL_OUTCOME_UNKNOWN = 'TOOL_OUTCOME_UNKNOWN';
|
|
17
|
+
/** Model-visible wording of the synthetic error tool results, keyed by cause. */
|
|
18
|
+
const CLOSER_TEXT = {
|
|
19
|
+
interrupted: {
|
|
20
|
+
started: 'The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.',
|
|
21
|
+
notStarted: 'The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.',
|
|
22
|
+
},
|
|
23
|
+
forked: {
|
|
24
|
+
started: 'The history inherited by this branch records this tool call starting but does not include its result. The parent session may have completed it after the fork point. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.',
|
|
25
|
+
notStarted: 'The history inherited by this branch has no record of this tool call starting. The parent session may have executed it after the fork point. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
14
28
|
/**
|
|
15
29
|
* Return deterministic synthetic events that close an open tail turn. Unmatched
|
|
16
|
-
* calls receive error results
|
|
17
|
-
*
|
|
18
|
-
* last real event. A balanced or empty log returns no
|
|
30
|
+
* calls in its open step receive error results, followed by `step/end` and a
|
|
31
|
+
* `turn/end` carrying the cause's reason. Calls in closed steps remain unchanged.
|
|
32
|
+
* Sequences continue the log and timestamps reuse the last real event. A balanced or empty log returns no
|
|
33
|
+
* events.
|
|
19
34
|
*
|
|
20
|
-
*
|
|
35
|
+
* Package-internal: each cause has exactly one owner, so external callers go
|
|
36
|
+
* through {@link interruptedTurnClosers} (persistence crash recovery) or
|
|
37
|
+
* `buildForkSeed` in `./fork.ts` (fork seeds) instead of selecting a cause.
|
|
38
|
+
*
|
|
39
|
+
* @param events - the log to scan: a valid committed prefix, possibly ending
|
|
40
|
+
* inside an open turn (a crash tail or a mid-turn fork cut).
|
|
41
|
+
* @param cause - why the turn is being closed; selects the `turn/end` reason
|
|
42
|
+
* and the model-visible wording of synthetic error tool results.
|
|
21
43
|
* @returns the synthetic closer events to append after `events`, in order; empty when the log is already balanced.
|
|
22
44
|
*/
|
|
23
|
-
export function
|
|
45
|
+
export function openTurnClosers(events, cause) {
|
|
24
46
|
let openTurn = null;
|
|
25
47
|
let openStep = null;
|
|
26
48
|
// Reset at each turn boundary so earlier calls cannot leak into tail repair.
|
|
@@ -70,7 +92,7 @@ export function interruptedTurnClosers(events) {
|
|
|
70
92
|
break;
|
|
71
93
|
}
|
|
72
94
|
}
|
|
73
|
-
// Balanced log (no
|
|
95
|
+
// Balanced log (no open tail turn): nothing to close. An open turn implies
|
|
74
96
|
// `events` is non-empty (its turn/start was logged), so `last` exists.
|
|
75
97
|
const last = events.at(-1);
|
|
76
98
|
if (openTurn === null || last === undefined)
|
|
@@ -81,24 +103,19 @@ export function interruptedTurnClosers(events) {
|
|
|
81
103
|
let seq = last.seq + 1;
|
|
82
104
|
const time = last.time;
|
|
83
105
|
const closers = [];
|
|
84
|
-
// Close calls before their step
|
|
85
|
-
|
|
106
|
+
// Close calls before their step; Map insertion order preserves transcript order.
|
|
107
|
+
const text = CLOSER_TEXT[cause.kind];
|
|
86
108
|
for (const [callId, { step, callSeq }] of pendingCalls) {
|
|
87
109
|
const started = callSeq !== undefined;
|
|
88
110
|
const message = deepFreeze({
|
|
89
|
-
id: brandString(
|
|
90
|
-
role: '
|
|
111
|
+
id: brandString(`${cause.kind}-tool-result-${callId}-${seq}`),
|
|
112
|
+
role: 'tool',
|
|
113
|
+
toolCallId: callId,
|
|
114
|
+
isError: true,
|
|
91
115
|
source: { kind: 'tool', callId },
|
|
92
116
|
content: [{
|
|
93
|
-
type: '
|
|
94
|
-
|
|
95
|
-
isError: true,
|
|
96
|
-
content: [{
|
|
97
|
-
type: 'text',
|
|
98
|
-
text: started
|
|
99
|
-
? 'The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.'
|
|
100
|
-
: 'The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.',
|
|
101
|
-
}],
|
|
117
|
+
type: 'text',
|
|
118
|
+
text: started ? text.started : text.notStarted,
|
|
102
119
|
}],
|
|
103
120
|
});
|
|
104
121
|
closers.push({
|
|
@@ -122,7 +139,19 @@ export function interruptedTurnClosers(events) {
|
|
|
122
139
|
if (openStep !== null) {
|
|
123
140
|
closers.push({ type: 'step/end', seq: SessionSeq(seq++), time, data: { turn: openTurn, step: openStep } });
|
|
124
141
|
}
|
|
125
|
-
closers.push({ type: 'turn/end', seq: SessionSeq(seq++), time, data: { turn: openTurn, reason: { kind:
|
|
142
|
+
closers.push({ type: 'turn/end', seq: SessionSeq(seq++), time, data: { turn: openTurn, reason: { kind: cause.kind } } });
|
|
126
143
|
return closers;
|
|
127
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Crash-recovery entry point: synthetic closers that balance a persisted log
|
|
147
|
+
* whose tail turn was interrupted. Used by crash-recovery callers; fork
|
|
148
|
+
* seeds receive their `forked`-cause closers through `buildForkSeed` in
|
|
149
|
+
* `./fork.ts`, and cause selection stays internal to those two owners.
|
|
150
|
+
*
|
|
151
|
+
* @param events - the persisted log to scan, possibly ending inside an open turn.
|
|
152
|
+
* @returns the synthetic `interrupted` closer events to append after `events`; empty when the log is already balanced.
|
|
153
|
+
*/
|
|
154
|
+
export function interruptedTurnClosers(events) {
|
|
155
|
+
return openTurnClosers(events, { kind: 'interrupted' });
|
|
156
|
+
}
|
|
128
157
|
//# sourceMappingURL=repair.js.map
|
package/lib/types/surface.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface SessionMessageProjection<T extends SessionEventType = SessionEv
|
|
|
38
38
|
/**
|
|
39
39
|
* Whether an event type can join the model-visible surface.
|
|
40
40
|
* @param type - event type to test.
|
|
41
|
-
* @returns true for one of the
|
|
41
|
+
* @returns true for one of the message-producing event types.
|
|
42
42
|
*/
|
|
43
43
|
export declare function isSurfaceEligibleType(type: string): boolean;
|
|
44
44
|
/**
|
|
@@ -76,7 +76,7 @@ export declare function isReplacementSurfaceEvent(event: SessionEvent): event is
|
|
|
76
76
|
/**
|
|
77
77
|
* Project a single event into the LLM message it derives to, or null when it
|
|
78
78
|
* produces none — a non-surface event (attempt, boundary, log-only record) or an
|
|
79
|
-
* empty-content
|
|
79
|
+
* empty-content system, developer, or assistant message. A caller
|
|
80
80
|
* reconstructing model input supplies the same prefix's `projectedMessages`
|
|
81
81
|
* from {@link foldSurface}; without that map this function reads original
|
|
82
82
|
* event content. Session instance methods apply the live projection. Messages
|
|
@@ -87,11 +87,11 @@ export declare function isReplacementSurfaceEvent(event: SessionEvent): event is
|
|
|
87
87
|
*/
|
|
88
88
|
export declare function deriveEventMessage(event: SessionEvent, projectedMessages?: ReadonlyMap<SessionSeq, Message>): Message | null;
|
|
89
89
|
/**
|
|
90
|
-
* Reject noncanonical request-header fields and contradictory tool failure metadata.
|
|
90
|
+
* Reject noncanonical request-header fields, developer roles/content, and contradictory tool failure metadata.
|
|
91
91
|
* This does not validate complete event payloads or embedded provider streams.
|
|
92
92
|
* @param event - event whose locally related payload fields are inspected.
|
|
93
93
|
* @param subject - event location to include in validation errors.
|
|
94
|
-
* @throws when request
|
|
94
|
+
* @throws when request-header fields, developer roles/content, or tool failure metadata are invalid.
|
|
95
95
|
*/
|
|
96
96
|
export declare function validateSessionEventData(event: Pick<SessionEvent, 'type' | 'data'>, subject: string): void;
|
|
97
97
|
/** One replacement operation observed while folding a session surface. */
|