@deepseek-ai/dsh-session 0.0.1-rc.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/LICENSE +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +143 -0
- package/README.zh.md +143 -0
- package/lib/index.js +1841 -0
- package/lib/invariant.js +168 -0
- package/lib/types/chunk-rows.d.ts +92 -0
- package/lib/types/chunk-rows.js +301 -0
- package/lib/types/index.d.ts +424 -0
- package/lib/types/index.js +1015 -0
- package/lib/types/invariant.d.ts +18 -0
- package/lib/types/invariant.js +199 -0
- package/lib/types/json.d.ts +36 -0
- package/lib/types/json.js +174 -0
- package/lib/types/preparation.d.ts +33 -0
- package/lib/types/preparation.js +37 -0
- package/lib/types/repair.d.ts +38 -0
- package/lib/types/repair.js +144 -0
- package/lib/types/request-header.d.ts +35 -0
- package/lib/types/request-header.js +65 -0
- package/lib/types/surface.d.ts +123 -0
- package/lib/types/surface.js +377 -0
- package/lib/types/types.d.ts +426 -0
- package/lib/types/types.js +18 -0
- package/package.json +60 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned relational invariants for the session event log. Load this
|
|
3
|
+
* companion beside `@deepseek-ai/dsh-invariants` to enable the checks.
|
|
4
|
+
*
|
|
5
|
+
* @module @deepseek-ai/dsh-session/invariant
|
|
6
|
+
*/
|
|
7
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
8
|
+
/** Cordis companion plugin name. */
|
|
9
|
+
export declare const name = "session-invariant";
|
|
10
|
+
/** Service required before the companion can reserve package ownership. */
|
|
11
|
+
export declare const inject: string[];
|
|
12
|
+
/**
|
|
13
|
+
* Register the session invariant companion.
|
|
14
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
15
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
16
|
+
*/
|
|
17
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
18
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned relational invariants for the session event log. Load this
|
|
3
|
+
* companion beside `@deepseek-ai/dsh-invariants` to enable the checks.
|
|
4
|
+
*
|
|
5
|
+
* @module @deepseek-ai/dsh-session/invariant
|
|
6
|
+
*/
|
|
7
|
+
import { assertNever } from '@deepseek-ai/dsh-llm';
|
|
8
|
+
import { TOOL_NOT_STARTED } from "./repair.js";
|
|
9
|
+
const PACKAGE_NAME = '@deepseek-ai/dsh-session';
|
|
10
|
+
/** Cordis companion plugin name. */
|
|
11
|
+
export const name = 'session-invariant';
|
|
12
|
+
/** Service required before the companion can reserve package ownership. */
|
|
13
|
+
export const inject = ['invariants'];
|
|
14
|
+
/** Assert that a step-scoped event names the currently open turn and step. */
|
|
15
|
+
function requireOpenStep(trace, kind, turn, step, fail) {
|
|
16
|
+
if (trace.openTurn !== turn || trace.openStep !== step) {
|
|
17
|
+
fail(`${kind} names turn ${turn}/step ${step} but open is turn ${trace.openTurn}/step ${trace.openStep}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Validate one candidate event without mutating the committed trace. */
|
|
21
|
+
function validateEvent(trace, event, fail) {
|
|
22
|
+
if (event.seq <= trace.lastSeq) {
|
|
23
|
+
fail(`seq must strictly increase: saw ${event.seq} after ${trace.lastSeq}`);
|
|
24
|
+
}
|
|
25
|
+
let openTurn = trace.openTurn;
|
|
26
|
+
let openStep = trace.openStep;
|
|
27
|
+
let nextTurn = trace.nextTurn;
|
|
28
|
+
let nextStep = trace.nextStep;
|
|
29
|
+
let pendingCalls = { kind: 'none' };
|
|
30
|
+
// Context and plugin-owned log-only events may be appended between model
|
|
31
|
+
// executions. Core execution events retain their explicit turn relations.
|
|
32
|
+
switch (event.type) {
|
|
33
|
+
case 'turn/start': {
|
|
34
|
+
if (trace.openTurn !== null) {
|
|
35
|
+
fail(`turn/start ${event.data.turn} while turn ${trace.openTurn} is still open`);
|
|
36
|
+
}
|
|
37
|
+
if (event.data.turn !== trace.nextTurn) {
|
|
38
|
+
fail(`turn/start expected turn ${trace.nextTurn}, got ${event.data.turn}`);
|
|
39
|
+
}
|
|
40
|
+
openTurn = event.data.turn;
|
|
41
|
+
nextStep = 1;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case 'turn/end': {
|
|
45
|
+
if (trace.openTurn !== event.data.turn) {
|
|
46
|
+
fail(`turn/end ${event.data.turn} does not match open turn ${trace.openTurn}`);
|
|
47
|
+
}
|
|
48
|
+
if (trace.openStep !== null) {
|
|
49
|
+
fail(`turn/end ${event.data.turn} while step ${trace.openStep} is still open`);
|
|
50
|
+
}
|
|
51
|
+
openTurn = null;
|
|
52
|
+
nextTurn += 1;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
case 'step/start': {
|
|
56
|
+
if (trace.openTurn !== event.data.turn) {
|
|
57
|
+
fail(`step/start in turn ${event.data.turn} but open turn is ${trace.openTurn}`);
|
|
58
|
+
}
|
|
59
|
+
if (trace.openStep !== null) {
|
|
60
|
+
fail(`step/start ${event.data.step} while step ${trace.openStep} is still open`);
|
|
61
|
+
}
|
|
62
|
+
if (event.data.step !== trace.nextStep) {
|
|
63
|
+
fail(`step/start expected step ${trace.nextStep} in turn ${event.data.turn}, got ${event.data.step}`);
|
|
64
|
+
}
|
|
65
|
+
openStep = event.data.step;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case 'step/end': {
|
|
69
|
+
requireOpenStep(trace, 'step/end', event.data.turn, event.data.step, fail);
|
|
70
|
+
pendingCalls = { kind: 'clear' };
|
|
71
|
+
openStep = null;
|
|
72
|
+
nextStep += 1;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case 'assistant/chunk': {
|
|
76
|
+
requireOpenStep(trace, 'assistant/chunk', event.data.turn, event.data.step, fail);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case 'assistant/message': {
|
|
80
|
+
requireOpenStep(trace, 'assistant/message', event.data.turn, event.data.step, fail);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case 'tool/call': {
|
|
84
|
+
requireOpenStep(trace, 'tool/call', event.data.turn, event.data.step, fail);
|
|
85
|
+
pendingCalls = { kind: 'add', callId: event.data.callId };
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case 'tool/result': {
|
|
89
|
+
// Session has already validated a content rewrite that cites its replaced event.
|
|
90
|
+
// It is durable turn work, not a second execution of the original call.
|
|
91
|
+
if (event.surfaceOp !== 'append') {
|
|
92
|
+
if (trace.openTurn === null) {
|
|
93
|
+
fail('tool/result surface replacement appended outside any open turn');
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
requireOpenStep(trace, 'tool/result', event.data.turn, event.data.step, fail);
|
|
98
|
+
const callId = event.data.message.source.callId;
|
|
99
|
+
const syntheticNotStarted = event.data.message.content[0].isError === true && event.data.error?.code === TOOL_NOT_STARTED;
|
|
100
|
+
if (!trace.pendingCalls.has(callId) && !syntheticNotStarted) {
|
|
101
|
+
fail(`tool/result for ${callId} with no prior tool/call in this step`);
|
|
102
|
+
}
|
|
103
|
+
pendingCalls = { kind: 'delete', callId };
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case 'user/message':
|
|
107
|
+
break;
|
|
108
|
+
case 'session/end-seed':
|
|
109
|
+
// Unconstrained: an unbalanced seed legally puts it inside an open turn.
|
|
110
|
+
break;
|
|
111
|
+
case 'todo/write':
|
|
112
|
+
case 'request/header':
|
|
113
|
+
case 'request/context': {
|
|
114
|
+
if (trace.openTurn === null) {
|
|
115
|
+
fail(`${event.type} appended outside any open turn (core execution events must be turn-enclosed)`);
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
default:
|
|
120
|
+
// Merge-extensible event relations belong to their owning plugin.
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
scalars: { lastSeq: event.seq, openTurn, openStep, nextTurn, nextStep },
|
|
125
|
+
pendingCalls,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** Apply one already-validated transition after its event commits. */
|
|
129
|
+
function applyTransition(trace, transition) {
|
|
130
|
+
Object.assign(trace, transition.scalars);
|
|
131
|
+
switch (transition.pendingCalls.kind) {
|
|
132
|
+
case 'none':
|
|
133
|
+
break;
|
|
134
|
+
case 'add':
|
|
135
|
+
trace.pendingCalls.add(transition.pendingCalls.callId);
|
|
136
|
+
break;
|
|
137
|
+
case 'delete':
|
|
138
|
+
trace.pendingCalls.delete(transition.pendingCalls.callId);
|
|
139
|
+
break;
|
|
140
|
+
case 'clear':
|
|
141
|
+
trace.pendingCalls.clear();
|
|
142
|
+
break;
|
|
143
|
+
/* v8 ignore next -- validateEvent produces this closed transition union */
|
|
144
|
+
default:
|
|
145
|
+
assertNever(transition.pendingCalls, 'session trace pending-call transition');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Install the session contribution into its child registration fiber. */
|
|
149
|
+
const install = Object.assign((ctx, fail) => {
|
|
150
|
+
const traces = new WeakMap();
|
|
151
|
+
const stagedTransitions = new WeakMap();
|
|
152
|
+
const freshTrace = () => ({
|
|
153
|
+
lastSeq: -1,
|
|
154
|
+
openTurn: null,
|
|
155
|
+
openStep: null,
|
|
156
|
+
nextTurn: 1,
|
|
157
|
+
nextStep: 1,
|
|
158
|
+
pendingCalls: new Set(),
|
|
159
|
+
});
|
|
160
|
+
const seedSession = (session) => {
|
|
161
|
+
const trace = freshTrace();
|
|
162
|
+
traces.set(session, trace);
|
|
163
|
+
for (const event of session.events) {
|
|
164
|
+
applyTransition(trace, validateEvent(trace, event, fail));
|
|
165
|
+
}
|
|
166
|
+
return trace;
|
|
167
|
+
};
|
|
168
|
+
/* v8 ignore next -- session/event always follows list() or session/created seeding */
|
|
169
|
+
const traceFor = (session) => traces.get(session) ?? seedSession(session);
|
|
170
|
+
for (const session of ctx.sessions.list())
|
|
171
|
+
seedSession(session);
|
|
172
|
+
ctx.on('session/created', (session) => { seedSession(session); }, { global: true });
|
|
173
|
+
ctx.on('session/event', (session, event) => {
|
|
174
|
+
const staged = stagedTransitions.get(event);
|
|
175
|
+
/* v8 ignore next 2 -- internal/dispatch stages the exact callback arguments */
|
|
176
|
+
if (staged === undefined || staged.session !== session) {
|
|
177
|
+
return fail('session/event reached publication without matching pre-commit validation');
|
|
178
|
+
}
|
|
179
|
+
stagedTransitions.delete(event);
|
|
180
|
+
applyTransition(staged.trace, staged.transition);
|
|
181
|
+
}, { global: true });
|
|
182
|
+
ctx.on('internal/dispatch', (_mode, eventName, args) => {
|
|
183
|
+
if (eventName !== 'session/event')
|
|
184
|
+
return;
|
|
185
|
+
const [session, event] = args;
|
|
186
|
+
const trace = traceFor(session);
|
|
187
|
+
const transition = validateEvent(trace, event, fail);
|
|
188
|
+
// A later dispatch listener may veto. Validation is pure, so abandoning
|
|
189
|
+
// this weakly keyed transition does not advance or retain the session.
|
|
190
|
+
stagedTransitions.set(event, { session, trace, transition });
|
|
191
|
+
}, { global: true });
|
|
192
|
+
}, { inject: ['sessions'] });
|
|
193
|
+
/**
|
|
194
|
+
* Register the session invariant companion.
|
|
195
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
196
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
197
|
+
*/
|
|
198
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
199
|
+
//# sourceMappingURL=invariant.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Lossless-JSON validation and detached snapshots for durable session data. @module @deepseek-ai/dsh-session/json */
|
|
2
|
+
/**
|
|
3
|
+
* A value that round-trips losslessly through JSON: `null`, a boolean, a finite
|
|
4
|
+
* number other than negative zero, a string, an array of such values, or a
|
|
5
|
+
* plain object whose values are such values. Arrays may carry only their dense
|
|
6
|
+
* indexed elements; extra own properties would be discarded by JSON. TypeScript
|
|
7
|
+
* cannot distinguish `-0` from `number`, so {@link isJsonValue} and
|
|
8
|
+
* {@link snapshotJsonValue} enforce these details at runtime. Use this type for
|
|
9
|
+
* a payload that must survive session-log persistence and replay byte-identically
|
|
10
|
+
* — e.g. a tool's private presentation `meta`.
|
|
11
|
+
*/
|
|
12
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | {
|
|
13
|
+
[key: string]: JsonValue;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Validate and detach lossless JSON in one read per property, so a stateful
|
|
17
|
+
* getter cannot change between validation and copying. Traversal is iterative,
|
|
18
|
+
* so valid nesting is bounded by available memory rather than the JavaScript
|
|
19
|
+
* call stack. Accepts ordinary arrays, plain or null-prototype objects, and JSON
|
|
20
|
+
* scalars; rejects sparse, cyclic, exotic, negative-zero, and non-finite values.
|
|
21
|
+
* Getter throws propagate.
|
|
22
|
+
*
|
|
23
|
+
* @param value - the candidate value to validate and detach.
|
|
24
|
+
* @returns the detached snapshot, or `undefined` when the value is not
|
|
25
|
+
* losslessly JSON-serializable.
|
|
26
|
+
*/
|
|
27
|
+
export declare function snapshotJsonValue<T>(value: T): T | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Test the same lossless JSON boundary as {@link snapshotJsonValue} without
|
|
30
|
+
* detaching it. Only own enumerable string properties participate; `toJSON`
|
|
31
|
+
* is ignored and getters run, so persistence boundaries use the snapshotter.
|
|
32
|
+
* @param value - the candidate event data to test.
|
|
33
|
+
* @returns whether `value` survives JSON round-trip losslessly.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isJsonValue(value: unknown): boolean;
|
|
36
|
+
//# sourceMappingURL=json.d.ts.map
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/** Lossless-JSON validation and detached snapshots for durable session data. @module @deepseek-ai/dsh-session/json */
|
|
2
|
+
/** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
|
|
3
|
+
function hasIntrinsicConstructor(prototype, name) {
|
|
4
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'constructor');
|
|
5
|
+
const constructor = descriptor?.value;
|
|
6
|
+
if (typeof constructor !== 'function')
|
|
7
|
+
return false;
|
|
8
|
+
try {
|
|
9
|
+
return constructor.name === name
|
|
10
|
+
&& constructor.prototype === prototype
|
|
11
|
+
&& Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Whether a candidate is one realm's intrinsic `Object.prototype`. */
|
|
18
|
+
function isIntrinsicObjectPrototype(value) {
|
|
19
|
+
return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, 'Object');
|
|
20
|
+
}
|
|
21
|
+
/** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
|
|
22
|
+
function hasPlainArrayPrototype(value) {
|
|
23
|
+
const prototype = Object.getPrototypeOf(value);
|
|
24
|
+
if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, 'Array'))
|
|
25
|
+
return false;
|
|
26
|
+
const objectPrototype = Object.getPrototypeOf(prototype);
|
|
27
|
+
return typeof objectPrototype === 'object'
|
|
28
|
+
&& objectPrototype !== null
|
|
29
|
+
&& isIntrinsicObjectPrototype(objectPrototype);
|
|
30
|
+
}
|
|
31
|
+
/** Whether an object is a plain or null-prototype record from any JavaScript realm. */
|
|
32
|
+
function hasPlainObjectPrototype(value) {
|
|
33
|
+
const prototype = Object.getPrototypeOf(value);
|
|
34
|
+
return prototype === null
|
|
35
|
+
|| typeof prototype === 'object' && isIntrinsicObjectPrototype(prototype);
|
|
36
|
+
}
|
|
37
|
+
/** Return every JSON-visible object key, or reject own data JSON would discard. */
|
|
38
|
+
function enumerableStringKeys(value) {
|
|
39
|
+
const keys = Reflect.ownKeys(value);
|
|
40
|
+
if (keys.some(key => typeof key !== 'string' || !Object.prototype.propertyIsEnumerable.call(value, key)))
|
|
41
|
+
return undefined;
|
|
42
|
+
return keys;
|
|
43
|
+
}
|
|
44
|
+
/** Validate lossless JSON iteratively, optionally materializing a detached snapshot. */
|
|
45
|
+
function walkJsonValue(value, detach) {
|
|
46
|
+
const ancestors = new Set();
|
|
47
|
+
let root;
|
|
48
|
+
const assign = (destination, item) => {
|
|
49
|
+
if (destination === undefined)
|
|
50
|
+
return;
|
|
51
|
+
if (destination.kind === 'root') {
|
|
52
|
+
root = item;
|
|
53
|
+
}
|
|
54
|
+
else if (destination.kind === 'array') {
|
|
55
|
+
destination.target[destination.index] = item;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
Object.defineProperty(destination.target, destination.key, {
|
|
59
|
+
value: item,
|
|
60
|
+
enumerable: true,
|
|
61
|
+
configurable: true,
|
|
62
|
+
writable: true,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const tasks = [{
|
|
67
|
+
kind: 'visit',
|
|
68
|
+
value,
|
|
69
|
+
...(detach ? { destination: { kind: 'root' } } : {}),
|
|
70
|
+
}];
|
|
71
|
+
for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
|
|
72
|
+
if (task.kind === 'leave') {
|
|
73
|
+
ancestors.delete(task.source);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (task.kind === 'array-item') {
|
|
77
|
+
if (!Object.prototype.hasOwnProperty.call(task.source, task.index))
|
|
78
|
+
return undefined;
|
|
79
|
+
tasks.push({
|
|
80
|
+
kind: 'visit',
|
|
81
|
+
value: task.source[task.index],
|
|
82
|
+
...(task.target === undefined ? {} : { destination: { kind: 'array', target: task.target, index: task.index } }),
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (task.kind === 'object-property') {
|
|
87
|
+
tasks.push({
|
|
88
|
+
kind: 'visit',
|
|
89
|
+
value: task.source[task.key],
|
|
90
|
+
...(task.target === undefined ? {} : { destination: { kind: 'object', target: task.target, key: task.key } }),
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const current = task.value;
|
|
95
|
+
if (current === null) {
|
|
96
|
+
assign(task.destination, null);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (typeof current === 'boolean' || typeof current === 'string') {
|
|
100
|
+
assign(task.destination, current);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (typeof current === 'number') {
|
|
104
|
+
if (!Number.isFinite(current) || Object.is(current, -0))
|
|
105
|
+
return undefined;
|
|
106
|
+
assign(task.destination, current);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (typeof current !== 'object')
|
|
110
|
+
return undefined;
|
|
111
|
+
if (ancestors.has(current))
|
|
112
|
+
return undefined;
|
|
113
|
+
if (Array.isArray(current)) {
|
|
114
|
+
if (!hasPlainArrayPrototype(current))
|
|
115
|
+
return undefined;
|
|
116
|
+
const length = current.length;
|
|
117
|
+
if (Reflect.ownKeys(current).length !== length + 1)
|
|
118
|
+
return undefined;
|
|
119
|
+
const target = detach ? [] : undefined;
|
|
120
|
+
if (target !== undefined)
|
|
121
|
+
assign(task.destination, target);
|
|
122
|
+
ancestors.add(current);
|
|
123
|
+
tasks.push({ kind: 'leave', source: current });
|
|
124
|
+
for (let index = length - 1; index >= 0; index--) {
|
|
125
|
+
tasks.push({ kind: 'array-item', source: current, index, ...(target === undefined ? {} : { target }) });
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!hasPlainObjectPrototype(current))
|
|
130
|
+
return undefined;
|
|
131
|
+
const keys = enumerableStringKeys(current);
|
|
132
|
+
if (keys === undefined)
|
|
133
|
+
return undefined;
|
|
134
|
+
const target = detach ? {} : undefined;
|
|
135
|
+
if (target !== undefined)
|
|
136
|
+
assign(task.destination, target);
|
|
137
|
+
ancestors.add(current);
|
|
138
|
+
tasks.push({ kind: 'leave', source: current });
|
|
139
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
140
|
+
const key = keys[index];
|
|
141
|
+
/* v8 ignore next -- the loop is bounded by the captured key count. */
|
|
142
|
+
if (key === undefined)
|
|
143
|
+
return undefined;
|
|
144
|
+
tasks.push({ kind: 'object-property', source: current, key, ...(target === undefined ? {} : { target }) });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return detach ? root : true;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Validate and detach lossless JSON in one read per property, so a stateful
|
|
151
|
+
* getter cannot change between validation and copying. Traversal is iterative,
|
|
152
|
+
* so valid nesting is bounded by available memory rather than the JavaScript
|
|
153
|
+
* call stack. Accepts ordinary arrays, plain or null-prototype objects, and JSON
|
|
154
|
+
* scalars; rejects sparse, cyclic, exotic, negative-zero, and non-finite values.
|
|
155
|
+
* Getter throws propagate.
|
|
156
|
+
*
|
|
157
|
+
* @param value - the candidate value to validate and detach.
|
|
158
|
+
* @returns the detached snapshot, or `undefined` when the value is not
|
|
159
|
+
* losslessly JSON-serializable.
|
|
160
|
+
*/
|
|
161
|
+
export function snapshotJsonValue(value) {
|
|
162
|
+
return walkJsonValue(value, true);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Test the same lossless JSON boundary as {@link snapshotJsonValue} without
|
|
166
|
+
* detaching it. Only own enumerable string properties participate; `toJSON`
|
|
167
|
+
* is ignored and getters run, so persistence boundaries use the snapshotter.
|
|
168
|
+
* @param value - the candidate event data to test.
|
|
169
|
+
* @returns whether `value` survives JSON round-trip losslessly.
|
|
170
|
+
*/
|
|
171
|
+
export function isJsonValue(value) {
|
|
172
|
+
return walkJsonValue(value, false) === true;
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=json.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership of one unpublished Session before registry publication.
|
|
3
|
+
* @module @deepseek-ai/dsh-session/preparation
|
|
4
|
+
*/
|
|
5
|
+
import type { Session } from './index.ts';
|
|
6
|
+
/** Options for a preparation whose provider retains unpublished state. */
|
|
7
|
+
export interface SessionPreparationOptions {
|
|
8
|
+
/** Release provider-owned state when the Session was not published. */
|
|
9
|
+
readonly release?: () => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* One exact unpublished Session and the provider state that keeps it usable.
|
|
13
|
+
* Disposal is synchronous and idempotent. Providers decide whether release
|
|
14
|
+
* returns the Session to a cache or discards it; publication may consume that
|
|
15
|
+
* state before disposal, making the callback a no-op.
|
|
16
|
+
*/
|
|
17
|
+
export declare class SessionPreparation implements Disposable {
|
|
18
|
+
private readonly options;
|
|
19
|
+
private released;
|
|
20
|
+
/** The exact Session to use for setup and publication. */
|
|
21
|
+
readonly session: Session;
|
|
22
|
+
private constructor();
|
|
23
|
+
/**
|
|
24
|
+
* Wrap an unpublished Session in one preparation lifetime.
|
|
25
|
+
* @param session - exact unpublished Session.
|
|
26
|
+
* @param options - optional provider release behavior.
|
|
27
|
+
* @returns a preparation disposed after publication or rollback.
|
|
28
|
+
*/
|
|
29
|
+
static create(session: Session, options?: SessionPreparationOptions): SessionPreparation;
|
|
30
|
+
/** Release provider state once when this preparation leaves its caller. */
|
|
31
|
+
[Symbol.dispose](): void;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=preparation.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership of one unpublished Session before registry publication.
|
|
3
|
+
* @module @deepseek-ai/dsh-session/preparation
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* One exact unpublished Session and the provider state that keeps it usable.
|
|
7
|
+
* Disposal is synchronous and idempotent. Providers decide whether release
|
|
8
|
+
* returns the Session to a cache or discards it; publication may consume that
|
|
9
|
+
* state before disposal, making the callback a no-op.
|
|
10
|
+
*/
|
|
11
|
+
export class SessionPreparation {
|
|
12
|
+
options;
|
|
13
|
+
released = false;
|
|
14
|
+
/** The exact Session to use for setup and publication. */
|
|
15
|
+
session;
|
|
16
|
+
constructor(session, options) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
this.session = session;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Wrap an unpublished Session in one preparation lifetime.
|
|
22
|
+
* @param session - exact unpublished Session.
|
|
23
|
+
* @param options - optional provider release behavior.
|
|
24
|
+
* @returns a preparation disposed after publication or rollback.
|
|
25
|
+
*/
|
|
26
|
+
static create(session, options) {
|
|
27
|
+
return new SessionPreparation(session, options ?? {});
|
|
28
|
+
}
|
|
29
|
+
/** Release provider state once when this preparation leaves its caller. */
|
|
30
|
+
[Symbol.dispose]() {
|
|
31
|
+
if (this.released)
|
|
32
|
+
return;
|
|
33
|
+
this.released = true;
|
|
34
|
+
this.options.release?.();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=preparation.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crash-recovery repair for an interrupted session log. It preserves a fully
|
|
3
|
+
* written final turn and supplies the missing tool, step, and turn boundaries
|
|
4
|
+
* needed to resume with a provider-valid transcript, plus the activity-time
|
|
5
|
+
* read that must skip the end-seed boundary — which this module does
|
|
6
|
+
* not write (`Session`'s constructor does) but whose synthetic closers can
|
|
7
|
+
* inherit that boundary's timestamp, the one real coupling between the two.
|
|
8
|
+
* @module @deepseek-ai/dsh-session/repair
|
|
9
|
+
*/
|
|
10
|
+
import type { SessionEvent } from './types.ts';
|
|
11
|
+
/**
|
|
12
|
+
* The `time` of the log's last event representing actual work, skipping the
|
|
13
|
+
* `session/end-seed` boundary — picking a session up is not activity, so
|
|
14
|
+
* activity ordering must exclude it.
|
|
15
|
+
*
|
|
16
|
+
* Excluded by type, so a pickup time still leaks when a boundary is the last
|
|
17
|
+
* event of an open turn: {@link interruptedTurnClosers} copies it onto the
|
|
18
|
+
* synthetic `turn/end`, which this counts as work. Reachable only by seeding an
|
|
19
|
+
* unbalanced log directly — `load()` balances first.
|
|
20
|
+
* @param events - the log to scan, in seq order.
|
|
21
|
+
* @returns the latest non-boundary event's `time`, or undefined when there is none.
|
|
22
|
+
*/
|
|
23
|
+
export declare function lastActivityTime(events: readonly SessionEvent[]): number | undefined;
|
|
24
|
+
/** Recovery code for an assistant tool request that never reached a recorded call start. */
|
|
25
|
+
export declare const TOOL_NOT_STARTED = "TOOL_NOT_STARTED";
|
|
26
|
+
/** Recovery code for a recorded tool call whose completed outcome was not durably recorded. */
|
|
27
|
+
export declare const TOOL_OUTCOME_UNKNOWN = "TOOL_OUTCOME_UNKNOWN";
|
|
28
|
+
/**
|
|
29
|
+
* Return deterministic synthetic events that close an open tail turn. Unmatched
|
|
30
|
+
* calls receive error results first, followed by an open `step/end` and an
|
|
31
|
+
* interrupted `turn/end`; sequences continue the log and timestamps reuse the
|
|
32
|
+
* last real event. A balanced or empty log returns no events.
|
|
33
|
+
*
|
|
34
|
+
* @param events - the loaded durable log to scan (a valid committed prefix, possibly with a crash tail).
|
|
35
|
+
* @returns the synthetic closer events to append after `events`, in order; empty when the log is already balanced.
|
|
36
|
+
*/
|
|
37
|
+
export declare function interruptedTurnClosers(events: readonly SessionEvent[]): SessionEvent[];
|
|
38
|
+
//# sourceMappingURL=repair.d.ts.map
|