@deepseek-ai/dsh-api-session-controller 0.1.2-alpha.2 → 0.1.2-alpha.4
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 +4 -2
- package/README.zh.md +4 -2
- package/lib/client.js +108 -14
- package/lib/index.js +49 -44
- package/lib/typert.host.js +113 -73
- package/lib/typert.remote-client.js +31 -31
- package/lib/types/agent.d.ts +3 -5
- package/lib/types/agent.js +5 -1
- package/lib/types/client/contract/session.d.ts +13 -2
- package/lib/types/client/contract/snapshot.d.ts +4 -0
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/sessions/manager.d.ts +2 -2
- package/lib/types/client/sessions/manager.js +10 -5
- package/lib/types/client/sessions/projection-store.d.ts +4 -3
- package/lib/types/client/sessions/queue-mirror.js +3 -0
- package/lib/types/client/sessions/service.d.ts +1 -1
- package/lib/types/client/sessions/service.js +2 -1
- package/lib/types/client/sessions/session.d.ts +10 -2
- package/lib/types/client/sessions/session.js +78 -8
- package/lib/types/commands.js +16 -22
- package/lib/types/history.js +45 -24
- package/lib/types/index.d.ts +3 -5
- package/lib/types/index.js +5 -1
- package/lib/types/list.js +4 -1
- package/lib/types/types.d.ts +22 -3
- package/package.json +57 -64
- package/lib/invariant.js +0 -13
- package/lib/types/invariant.d.ts +0 -9
- package/lib/types/invariant.js +0 -12
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Sessions remain resident after creation so their open Remote sources keep running off-screen.
|
|
2
2
|
import { randomUUID } from '@deepseek-ai/dsh-util-crypto';
|
|
3
|
+
import { SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session/types';
|
|
3
4
|
import { SessionEventStream } from "../transport.js";
|
|
4
5
|
import { MutableSessionEventSource } from "../contract/events.js";
|
|
5
6
|
import { Notifier } from "./notifier.js";
|
|
@@ -7,8 +8,16 @@ import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client';
|
|
|
7
8
|
import { ProjectionValueStore } from "./projection-store.js";
|
|
8
9
|
import { resolvedClientTimeZone } from "../time-zone.js";
|
|
9
10
|
import { SessionQueueMirror } from "./queue-mirror.js";
|
|
11
|
+
function projectionsBaseline(value) {
|
|
12
|
+
return {
|
|
13
|
+
...value,
|
|
14
|
+
asOfSeq: value.asOfSeq === -1 ? -1 : SessionSeq(value.asOfSeq),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
10
17
|
/** Messages requested per history page. */
|
|
11
18
|
export const PAGE_MESSAGES = 50;
|
|
19
|
+
/** Messages requested per page while a turn jump loops backwards (fewer, larger round trips). */
|
|
20
|
+
export const JUMP_PAGE_MESSAGES = 200;
|
|
12
21
|
/**
|
|
13
22
|
* Owns a session's event window, lifecycle state, and observable
|
|
14
23
|
* snapshot. React bindings remain outside this data layer. Features see only
|
|
@@ -20,7 +29,7 @@ export class Session {
|
|
|
20
29
|
remote;
|
|
21
30
|
options;
|
|
22
31
|
// ---- Window and derived state (all private; the snapshot is the only read API) ----
|
|
23
|
-
baseSeq = 0;
|
|
32
|
+
baseSeq = SessionLogOffset(0);
|
|
24
33
|
hasMore = false;
|
|
25
34
|
openState = 'cold';
|
|
26
35
|
openError = null;
|
|
@@ -29,6 +38,10 @@ export class Session {
|
|
|
29
38
|
* passes drop all writes once the generation moves on. */
|
|
30
39
|
openGeneration = 0;
|
|
31
40
|
loadingOlder = false;
|
|
41
|
+
/** Shared low-water target of the running jump loop; null when no jump is paging. */
|
|
42
|
+
jumpTargetSeq = null;
|
|
43
|
+
/** The running jump loop's completion, shared by retargeting callers. */
|
|
44
|
+
jumpPromise = null;
|
|
32
45
|
/** Authoritative stream-only inbox snapshot; pending work never hits history. */
|
|
33
46
|
queueMirror = new SessionQueueMirror();
|
|
34
47
|
running = false;
|
|
@@ -125,6 +138,9 @@ export class Session {
|
|
|
125
138
|
const requestId = randomUUID();
|
|
126
139
|
this.pendingSubmissions = [...this.pendingSubmissions, {
|
|
127
140
|
requestId,
|
|
141
|
+
placement: this.running
|
|
142
|
+
? input.mode === 'steer' ? 'steering' : 'queued'
|
|
143
|
+
: 'transcript',
|
|
128
144
|
time: Date.now(),
|
|
129
145
|
text: input.text,
|
|
130
146
|
images: input.images,
|
|
@@ -247,9 +263,11 @@ export class Session {
|
|
|
247
263
|
*/
|
|
248
264
|
async rename(title) {
|
|
249
265
|
const result = await this.remote.session.rename({ sessionId: this.sessionId, title });
|
|
250
|
-
if (result.ok)
|
|
251
|
-
|
|
252
|
-
|
|
266
|
+
if (!result.ok)
|
|
267
|
+
return result;
|
|
268
|
+
const seq = SessionSeq(result.value.seq);
|
|
269
|
+
this.projections.apply('title', result.value.title, seq);
|
|
270
|
+
return { ok: true, value: { title: result.value.title, seq } };
|
|
253
271
|
}
|
|
254
272
|
/**
|
|
255
273
|
* Execute one slash-command line against this session's agent — pure
|
|
@@ -300,6 +318,58 @@ export class Session {
|
|
|
300
318
|
this.notifier.markDirty();
|
|
301
319
|
}
|
|
302
320
|
}
|
|
321
|
+
/** Jump loader: page backwards until the window covers seq (see ISession.loadThrough). */
|
|
322
|
+
loadThrough(seq) {
|
|
323
|
+
if (this.openState !== 'open' || !this.hasMore || this.baseSeq <= seq)
|
|
324
|
+
return Promise.resolve();
|
|
325
|
+
if (this.jumpPromise !== null) {
|
|
326
|
+
// Retarget the running loop to the lowest requested seq.
|
|
327
|
+
this.jumpTargetSeq = SessionSeq(Math.min(this.jumpTargetSeq ?? seq, seq));
|
|
328
|
+
return this.jumpPromise;
|
|
329
|
+
}
|
|
330
|
+
// A plain single-page pull owns the busy flag; the jump does not queue
|
|
331
|
+
// behind it (the caller retries once it settles) and must leave no
|
|
332
|
+
// target behind — only the loop's finally clears that field, and no
|
|
333
|
+
// loop starts here.
|
|
334
|
+
if (this.loadingOlder)
|
|
335
|
+
return Promise.resolve();
|
|
336
|
+
this.jumpTargetSeq = seq;
|
|
337
|
+
this.loadingOlder = true;
|
|
338
|
+
this.notifier.markDirty();
|
|
339
|
+
// Stale-pass guard (the doOpen pattern): a resync mid-loop replaces the
|
|
340
|
+
// stream generation; this pass then stops instead of paging the new
|
|
341
|
+
// generation toward its old target.
|
|
342
|
+
const generation = this.openGeneration;
|
|
343
|
+
this.jumpPromise = (async () => {
|
|
344
|
+
try {
|
|
345
|
+
while (this.hasMore && this.jumpTargetSeq !== null && this.baseSeq > this.jumpTargetSeq) {
|
|
346
|
+
if (generation !== this.openGeneration)
|
|
347
|
+
return;
|
|
348
|
+
const events = this.events;
|
|
349
|
+
if (events === undefined)
|
|
350
|
+
return;
|
|
351
|
+
const before = this.baseSeq;
|
|
352
|
+
await events.prepend({ beforeSeq: this.baseSeq, maxMessages: JUMP_PAGE_MESSAGES });
|
|
353
|
+
// No-progress guard: an empty or dropped page that still claims more
|
|
354
|
+
// history must end the loop, not spin it.
|
|
355
|
+
if (this.baseSeq >= before)
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
if (!isRemoteFailure(error)) {
|
|
361
|
+
console.error('[session-controller] loadThrough failed:', error);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
this.jumpTargetSeq = null;
|
|
366
|
+
this.jumpPromise = null;
|
|
367
|
+
this.loadingOlder = false;
|
|
368
|
+
this.notifier.markDirty();
|
|
369
|
+
}
|
|
370
|
+
})();
|
|
371
|
+
return this.jumpPromise;
|
|
372
|
+
}
|
|
303
373
|
/** Rebuild an opened history source after address replacement.
|
|
304
374
|
* Invalidates any in-flight open first; queue state belongs to the independently
|
|
305
375
|
* reconnecting control stream and remains untouched. */
|
|
@@ -313,7 +383,7 @@ export class Session {
|
|
|
313
383
|
this.openPromise = null;
|
|
314
384
|
this.openState = 'cold';
|
|
315
385
|
this.openError = null;
|
|
316
|
-
this.baseSeq = 0;
|
|
386
|
+
this.baseSeq = SessionLogOffset(0);
|
|
317
387
|
this.notifier.markDirty();
|
|
318
388
|
await this.open();
|
|
319
389
|
}
|
|
@@ -483,7 +553,7 @@ export class Session {
|
|
|
483
553
|
acceptEventChange(change) {
|
|
484
554
|
switch (change.type) {
|
|
485
555
|
case 'replace':
|
|
486
|
-
this.installWindow(change.entries, change.hasMore, change.page.projections);
|
|
556
|
+
this.installWindow(change.entries, change.hasMore, change.page.projections === undefined ? undefined : projectionsBaseline(change.page.projections));
|
|
487
557
|
return;
|
|
488
558
|
case 'prepend':
|
|
489
559
|
this.prependWindow(change.entries, change.hasMore);
|
|
@@ -495,7 +565,7 @@ export class Session {
|
|
|
495
565
|
}
|
|
496
566
|
/** Replace the complete contiguous window and apply page-owned projection metadata. */
|
|
497
567
|
installWindow(entries, hasMore, projections) {
|
|
498
|
-
this.baseSeq = entries[0]?.event.seq ?? 0;
|
|
568
|
+
this.baseSeq = SessionLogOffset(entries[0]?.event.seq ?? 0);
|
|
499
569
|
this.hasMore = hasMore;
|
|
500
570
|
if (entries.some(entry => entry.event.type === 'turn/start'))
|
|
501
571
|
this.firstPromptPendingTurn = false;
|
|
@@ -508,7 +578,7 @@ export class Session {
|
|
|
508
578
|
}
|
|
509
579
|
/** Prepend one stream-validated history page. */
|
|
510
580
|
prependWindow(entries, hasMore) {
|
|
511
|
-
this.baseSeq = entries[0]
|
|
581
|
+
this.baseSeq = entries[0] === undefined ? this.baseSeq : SessionLogOffset(entries[0].event.seq);
|
|
512
582
|
this.hasMore = hasMore;
|
|
513
583
|
this.eventSource.prepend(entries, hasMore);
|
|
514
584
|
}
|
package/lib/types/commands.js
CHANGED
|
@@ -53,8 +53,9 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
53
53
|
});
|
|
54
54
|
import { randomUUID } from 'node:crypto';
|
|
55
55
|
import { brandString } from '@deepseek-ai/dsh-brand';
|
|
56
|
-
import { AttachmentError,
|
|
56
|
+
import { AttachmentError, admitPromptContent } from '@deepseek-ai/dsh-attachment';
|
|
57
57
|
import { ReasoningEffortId, createUserMessage, freezeMessage, } from '@deepseek-ai/dsh-llm';
|
|
58
|
+
import { SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session';
|
|
58
59
|
import { SessionQueryError } from '@deepseek-ai/dsh-session-query';
|
|
59
60
|
import { SessionTitleInvalidError } from '@deepseek-ai/dsh-session-title';
|
|
60
61
|
import { canonicalClientTimeZone } from '@deepseek-ai/dsh-util-time';
|
|
@@ -182,9 +183,12 @@ export class SessionCommandController {
|
|
|
182
183
|
async fork(request) {
|
|
183
184
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
184
185
|
try {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
let atSeq;
|
|
187
|
+
try {
|
|
188
|
+
atSeq = request.atSeq === undefined ? undefined : SessionSeq(request.atSeq);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
throw new RemoteError('gateway/bad-request', 'atSeq must be a non-negative safe integer', {});
|
|
188
192
|
}
|
|
189
193
|
let observed;
|
|
190
194
|
try {
|
|
@@ -201,7 +205,6 @@ export class SessionCommandController {
|
|
|
201
205
|
}
|
|
202
206
|
const source = __addDisposableResource(env_1, observed, false);
|
|
203
207
|
const lastSeq = source.events.at(-1)?.seq ?? -1;
|
|
204
|
-
const atSeq = request.atSeq;
|
|
205
208
|
const anchoredBoundary = atSeq === undefined
|
|
206
209
|
? undefined
|
|
207
210
|
: source.events.find(event => event.type === 'turn/end' && event.seq >= atSeq);
|
|
@@ -214,9 +217,10 @@ export class SessionCommandController {
|
|
|
214
217
|
? `session "${request.sessionId}" has not completed the turn containing event ${String(atSeq)}`
|
|
215
218
|
: `session "${request.sessionId}" has no completed turn to fork from`, { sessionId: request.sessionId });
|
|
216
219
|
}
|
|
217
|
-
let cut = boundary.seq + 1;
|
|
218
|
-
while (cut < source.events.length && source.events[cut]?.type !== 'turn/start')
|
|
219
|
-
cut
|
|
220
|
+
let cut = SessionLogOffset(boundary.seq + 1);
|
|
221
|
+
while (cut < source.events.length && source.events[cut]?.type !== 'turn/start') {
|
|
222
|
+
cut = SessionLogOffset(cut + 1);
|
|
223
|
+
}
|
|
220
224
|
let workspace;
|
|
221
225
|
try {
|
|
222
226
|
workspace = await this.forkWorkspace(source.header);
|
|
@@ -231,10 +235,11 @@ export class SessionCommandController {
|
|
|
231
235
|
await this.ctx.agents.create({
|
|
232
236
|
sessionId: childId,
|
|
233
237
|
seed: source.events.slice(0, cut),
|
|
238
|
+
inheritedEventCount: cut,
|
|
234
239
|
meta: {
|
|
235
240
|
...(source.header.cwd === undefined ? {} : { cwd: source.header.cwd }),
|
|
236
241
|
parentSession: source.header.id,
|
|
237
|
-
|
|
242
|
+
isSeeded: true,
|
|
238
243
|
...(composition.agentPreset === undefined
|
|
239
244
|
? {}
|
|
240
245
|
: { agentPreset: composition.agentPreset }),
|
|
@@ -296,7 +301,7 @@ export class SessionCommandController {
|
|
|
296
301
|
throw new RemoteError('session/attachment-invalid', `Model "${current.model}" does not support image input.`, { reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES' });
|
|
297
302
|
}
|
|
298
303
|
}
|
|
299
|
-
const content = await
|
|
304
|
+
const content = await admitPromptContent(this.ctx.attachments, request.content);
|
|
300
305
|
const message = createUserMessage({ content, source });
|
|
301
306
|
if (request.mode === 'steer')
|
|
302
307
|
agent.steer(message);
|
|
@@ -438,7 +443,7 @@ export class SessionCommandController {
|
|
|
438
443
|
async readSessionState(sessionId) {
|
|
439
444
|
const attached = this.ctx.sessions.get(sessionId);
|
|
440
445
|
if (attached !== undefined) {
|
|
441
|
-
return { id: attached.id, header: attached.header, events:
|
|
446
|
+
return { id: attached.id, header: attached.header, events: attached.snapshotEvents() };
|
|
442
447
|
}
|
|
443
448
|
const inspected = await inspectApiSession(this.ctx, sessionId);
|
|
444
449
|
return { id: inspected.meta.id, header: inspected.meta, events: inspected.events };
|
|
@@ -457,17 +462,6 @@ export class SessionCommandController {
|
|
|
457
462
|
return undefined;
|
|
458
463
|
}
|
|
459
464
|
}
|
|
460
|
-
async function durablePromptContent(ctx, content) {
|
|
461
|
-
if (content.every(part => part.type === 'text')) {
|
|
462
|
-
return content.map(part => ({ type: 'text', text: part.text }));
|
|
463
|
-
}
|
|
464
|
-
const refs = await admitEncodedImages(ctx.attachments, content.filter(part => part.type === 'image'));
|
|
465
|
-
let next = 0;
|
|
466
|
-
return content.map(part => part.type === 'text'
|
|
467
|
-
? { type: 'text', text: part.text }
|
|
468
|
-
// admitEncodedImages returns one reference per image part in order.
|
|
469
|
-
: { type: 'image', attachment: refs[next++] });
|
|
470
|
-
}
|
|
471
465
|
function imageBlockIn(content, match) {
|
|
472
466
|
if (!Array.isArray(content))
|
|
473
467
|
return undefined;
|
package/lib/types/history.js
CHANGED
|
@@ -52,7 +52,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
52
52
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
53
53
|
});
|
|
54
54
|
import { Deque } from '@deepseek-ai/dsh-deque';
|
|
55
|
-
import { isAppendSurfaceEvent } from '@deepseek-ai/dsh-session';
|
|
55
|
+
import { isAppendSurfaceEvent, SessionLogOffset, SessionSeq, } from '@deepseek-ai/dsh-session';
|
|
56
56
|
import { isChunkRow, packChunkRuns } from '@deepseek-ai/dsh-session/chunk-rows';
|
|
57
57
|
import { SessionQueryError } from '@deepseek-ai/dsh-session-query';
|
|
58
58
|
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol';
|
|
@@ -86,18 +86,24 @@ export class SessionHistoryController {
|
|
|
86
86
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
87
87
|
try {
|
|
88
88
|
validatePageRequest(request);
|
|
89
|
+
const throughSeq = request.throughSeq === -1
|
|
90
|
+
? -1
|
|
91
|
+
: SessionSeq(request.throughSeq);
|
|
92
|
+
const beforeSeq = request.beforeSeq === undefined
|
|
93
|
+
? undefined
|
|
94
|
+
: SessionLogOffset(request.beforeSeq);
|
|
89
95
|
const source = __addDisposableResource(env_1, await this.sourceFor(request.address, signal, false), false);
|
|
90
96
|
signal.throwIfAborted();
|
|
91
97
|
const sourceLog = source.events;
|
|
92
98
|
const sourceCursor = sourceLog.at(-1)?.seq ?? -1;
|
|
93
|
-
if (
|
|
94
|
-
throw new RemoteError('gateway/bad-request', `session page through seq ${String(
|
|
99
|
+
if (throughSeq > sourceCursor) {
|
|
100
|
+
throw new RemoteError('gateway/bad-request', `session page through seq ${String(throughSeq)} is past cursor ${String(sourceCursor)}`, {});
|
|
95
101
|
}
|
|
96
102
|
/* v8 ignore next -- Session and persistence validation guarantee a dense zero-based event prefix. */
|
|
97
|
-
if (
|
|
98
|
-
throw new RemoteError('gateway/internal', `session log does not contain through seq ${String(
|
|
103
|
+
if (throughSeq >= 0 && sourceLog[throughSeq]?.seq !== throughSeq) {
|
|
104
|
+
throw new RemoteError('gateway/internal', `session log does not contain through seq ${String(throughSeq)}`, {});
|
|
99
105
|
}
|
|
100
|
-
const page = paginate(sourceLog,
|
|
106
|
+
const page = paginate(sourceLog, beforeSeq, request.maxMessages ?? DEFAULT_MAX_MESSAGES, throughSeq);
|
|
101
107
|
const records = pageRecords(page.events);
|
|
102
108
|
return {
|
|
103
109
|
records,
|
|
@@ -148,9 +154,9 @@ export class SessionHistoryController {
|
|
|
148
154
|
// Constructor seed events have no session/event notification. Normally
|
|
149
155
|
// only the end-seed suffix is new; if persistence advanced after the
|
|
150
156
|
// opening observation, replay everything beyond that snapshot cursor.
|
|
151
|
-
const suffix = session.
|
|
157
|
+
const suffix = session.snapshotEvents(snapshotCursor === undefined
|
|
152
158
|
? session.firstLiveSeq
|
|
153
|
-
: snapshotCursor + 1);
|
|
159
|
+
: SessionLogOffset(snapshotCursor + 1));
|
|
154
160
|
for (let index = suffix.length - 1; index >= 0; index -= 1) {
|
|
155
161
|
buffered.pushFront(suffix[index]);
|
|
156
162
|
}
|
|
@@ -169,7 +175,7 @@ export class SessionHistoryController {
|
|
|
169
175
|
const page = paginate(events, undefined, request.maxMessages ?? DEFAULT_MAX_MESSAGES);
|
|
170
176
|
yield {
|
|
171
177
|
type: 'snapshot',
|
|
172
|
-
header: source.header,
|
|
178
|
+
header: wireHeader(source.header, source.inheritedEventCount),
|
|
173
179
|
cursor,
|
|
174
180
|
records: pageRecords(page.events),
|
|
175
181
|
hasMore: page.hasMore,
|
|
@@ -187,19 +193,20 @@ export class SessionHistoryController {
|
|
|
187
193
|
throw error;
|
|
188
194
|
}
|
|
189
195
|
}
|
|
190
|
-
let
|
|
196
|
+
let nextOffset = SessionLogOffset(cursor + 1);
|
|
191
197
|
while (!follower.closed && !signal.aborted) {
|
|
192
198
|
const item = buffered.popFront();
|
|
193
199
|
if (item === undefined) {
|
|
194
200
|
await new Promise((resolve) => { wake = resolve; });
|
|
195
201
|
continue;
|
|
196
202
|
}
|
|
197
|
-
|
|
203
|
+
const expectedSeq = SessionSeq(nextOffset);
|
|
204
|
+
if (item.seq < expectedSeq)
|
|
198
205
|
continue;
|
|
199
|
-
if (item.seq !==
|
|
200
|
-
throw new RemoteError('gateway/internal', `session event stream skipped seq ${String(
|
|
206
|
+
if (item.seq !== expectedSeq) {
|
|
207
|
+
throw new RemoteError('gateway/internal', `session event stream skipped seq ${String(expectedSeq)}`, {});
|
|
201
208
|
}
|
|
202
|
-
|
|
209
|
+
nextOffset = SessionLogOffset(nextOffset + 1);
|
|
203
210
|
yield entryFor(item);
|
|
204
211
|
}
|
|
205
212
|
}
|
|
@@ -230,7 +237,7 @@ export class SessionHistoryController {
|
|
|
230
237
|
rejectNotFound(address);
|
|
231
238
|
}
|
|
232
239
|
try {
|
|
233
|
-
validateAddress(address, observation.header, observation.projections);
|
|
240
|
+
validateAddress(address, observation.header, observation.inheritedEventCount, observation.projections);
|
|
234
241
|
}
|
|
235
242
|
catch (error) {
|
|
236
243
|
observation[Symbol.dispose]();
|
|
@@ -254,11 +261,15 @@ function projectionBlock(snapshot) {
|
|
|
254
261
|
};
|
|
255
262
|
}
|
|
256
263
|
function validatePageRequest(request) {
|
|
257
|
-
if (!Number.isSafeInteger(request.throughSeq)
|
|
264
|
+
if (!Number.isSafeInteger(request.throughSeq)
|
|
265
|
+
|| request.throughSeq < -1
|
|
266
|
+
|| Object.is(request.throughSeq, -0)) {
|
|
258
267
|
throw new RemoteError('gateway/bad-request', 'throughSeq must be an integer greater than or equal to -1', {});
|
|
259
268
|
}
|
|
260
269
|
if (request.beforeSeq !== undefined
|
|
261
|
-
&& (!Number.isSafeInteger(request.beforeSeq)
|
|
270
|
+
&& (!Number.isSafeInteger(request.beforeSeq)
|
|
271
|
+
|| request.beforeSeq < 0
|
|
272
|
+
|| Object.is(request.beforeSeq, -0))) {
|
|
262
273
|
throw new RemoteError('gateway/bad-request', 'beforeSeq must be a non-negative safe integer', {});
|
|
263
274
|
}
|
|
264
275
|
if (request.maxMessages !== undefined
|
|
@@ -275,7 +286,7 @@ function validateFollowRequest(request) {
|
|
|
275
286
|
function addressId(address) {
|
|
276
287
|
return address.kind === 'session' ? address.sessionId : address.childSessionId;
|
|
277
288
|
}
|
|
278
|
-
function validateAddress(address, header, projections) {
|
|
289
|
+
function validateAddress(address, header, inheritedEventCount, projections) {
|
|
279
290
|
if (address.kind === 'session') {
|
|
280
291
|
if (header.origin === 'subagent') {
|
|
281
292
|
throw new RemoteError('session/agent-busy', 'subagent Sessions require their durable parent address', {
|
|
@@ -297,7 +308,7 @@ function validateAddress(address, header, projections) {
|
|
|
297
308
|
reason: 'corrupt',
|
|
298
309
|
});
|
|
299
310
|
}
|
|
300
|
-
if (identity === undefined || identity.seq <
|
|
311
|
+
if (identity === undefined || identity.seq < inheritedEventCount) {
|
|
301
312
|
throw new RemoteError('subagent/catalog-diagnostic', 'subagent descriptor is unavailable', {
|
|
302
313
|
parentSessionId: address.parentSessionId,
|
|
303
314
|
childSessionId: address.childSessionId,
|
|
@@ -320,9 +331,9 @@ function rejectNotFound(address) {
|
|
|
320
331
|
});
|
|
321
332
|
}
|
|
322
333
|
function paginate(events, beforeSeq, maxMessages, throughSeq = events.at(-1)?.seq ?? -1) {
|
|
323
|
-
const end = Math.min(throughSeq + 1, beforeSeq ?? throughSeq + 1);
|
|
334
|
+
const end = SessionLogOffset(Math.min(throughSeq + 1, beforeSeq ?? throughSeq + 1));
|
|
324
335
|
let count = 0;
|
|
325
|
-
let cut = 0;
|
|
336
|
+
let cut = SessionLogOffset(0);
|
|
326
337
|
for (let index = end - 1; index >= 0; index--) {
|
|
327
338
|
const event = events[index];
|
|
328
339
|
if (!MESSAGE_TYPES.has(event.type) || !isAppendSurfaceEvent(event))
|
|
@@ -331,16 +342,26 @@ function paginate(events, beforeSeq, maxMessages, throughSeq = events.at(-1)?.se
|
|
|
331
342
|
const sources = event.sourceEventSeqs;
|
|
332
343
|
let groupStart = event.seq;
|
|
333
344
|
if (sources !== undefined) {
|
|
334
|
-
for (const source of sources)
|
|
335
|
-
|
|
345
|
+
for (const source of sources) {
|
|
346
|
+
if (source < groupStart)
|
|
347
|
+
groupStart = source;
|
|
348
|
+
}
|
|
336
349
|
}
|
|
337
350
|
if (count >= maxMessages) {
|
|
338
|
-
cut = groupStart;
|
|
351
|
+
cut = SessionLogOffset(groupStart);
|
|
339
352
|
break;
|
|
340
353
|
}
|
|
341
354
|
}
|
|
342
355
|
return { events: events.slice(cut, end), hasMore: cut > 0 };
|
|
343
356
|
}
|
|
357
|
+
/** Translate logical Session metadata to the unchanged v0 browser wire. */
|
|
358
|
+
function wireHeader(header, inheritedEventCount) {
|
|
359
|
+
const { isSeeded, ...wire } = header;
|
|
360
|
+
return {
|
|
361
|
+
...wire,
|
|
362
|
+
...isSeeded ? { seedLength: inheritedEventCount } : {},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
344
365
|
function entryFor(event) {
|
|
345
366
|
return {
|
|
346
367
|
type: 'event',
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Session Remote owner: cold reads, explicit Agent commands, and live control state. */
|
|
2
2
|
import { Context } from '@deepseek-ai/cordis';
|
|
3
3
|
import z from '@deepseek-ai/schemastery';
|
|
4
|
-
import type {
|
|
4
|
+
import type { SessionId } from '@deepseek-ai/dsh-session';
|
|
5
|
+
import type { SessionInspection } from '@deepseek-ai/dsh-session-persistence';
|
|
5
6
|
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
6
7
|
import { type ApiSessionAgentResult } from './agent.ts';
|
|
7
8
|
import { buildModelCatalog } from './catalog.ts';
|
|
@@ -60,10 +61,7 @@ export declare class SessionController extends TypertRemoteService {
|
|
|
60
61
|
* @param signal - optional caller cancellation for persistence reads.
|
|
61
62
|
* @returns the current attached state or persisted header and event prefix.
|
|
62
63
|
*/
|
|
63
|
-
inspect(sessionId: SessionId, signal?: AbortSignal): Promise<
|
|
64
|
-
meta: SessionHeader;
|
|
65
|
-
events: SessionEvent[];
|
|
66
|
-
}>;
|
|
64
|
+
inspect(sessionId: SessionId, signal?: AbortSignal): Promise<SessionInspection>;
|
|
67
65
|
/**
|
|
68
66
|
* Read all visible Session rows without resuming an Agent.
|
|
69
67
|
* @param _request - reserved empty list request.
|
package/lib/types/index.js
CHANGED
|
@@ -266,7 +266,11 @@ let SessionController = (() => {
|
|
|
266
266
|
inspect(sessionId, signal) {
|
|
267
267
|
const attached = this.ctx.sessions.get(sessionId);
|
|
268
268
|
if (attached !== undefined) {
|
|
269
|
-
return Promise.resolve({
|
|
269
|
+
return Promise.resolve({
|
|
270
|
+
meta: attached.header,
|
|
271
|
+
inheritedEventCount: attached.inheritedEventCount,
|
|
272
|
+
events: attached.snapshotEvents(),
|
|
273
|
+
});
|
|
270
274
|
}
|
|
271
275
|
return inspectApiSession(this.ctx, sessionId, signal);
|
|
272
276
|
}
|
package/lib/types/list.js
CHANGED
|
@@ -52,6 +52,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
52
52
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
53
53
|
});
|
|
54
54
|
import { stat } from 'node:fs/promises';
|
|
55
|
+
import { SessionLogOffset } from '@deepseek-ai/dsh-session';
|
|
55
56
|
import { SessionQueryError } from '@deepseek-ai/dsh-session-query';
|
|
56
57
|
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol';
|
|
57
58
|
import { z } from 'zod';
|
|
@@ -362,7 +363,9 @@ export class ApiSessionList {
|
|
|
362
363
|
projectionsFor(header, session) {
|
|
363
364
|
try {
|
|
364
365
|
const block = session === undefined
|
|
365
|
-
?
|
|
366
|
+
? header.isSeeded
|
|
367
|
+
? undefined
|
|
368
|
+
: this.ctx.get('sessionProjectionCache')?.cachedSnapshot(header, SessionLogOffset(0))
|
|
366
369
|
: this.ctx.sessionProjections.cachedSnapshot(session);
|
|
367
370
|
return block !== undefined && Object.keys(block.values).length > 0
|
|
368
371
|
? {
|
package/lib/types/types.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { Branded } from '@deepseek-ai/dsh-brand';
|
|
|
4
4
|
import type { MessageId } from '@deepseek-ai/dsh-llm/brand';
|
|
5
5
|
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types';
|
|
6
6
|
import type { ChunkRow } from '@deepseek-ai/dsh-session/chunk-rows';
|
|
7
|
-
import type {
|
|
7
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
8
8
|
import type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/types';
|
|
9
9
|
import type { JobId } from '@deepseek-ai/dsh-jobs/brand';
|
|
10
10
|
import type { JsonValue } from '@deepseek-ai/dsh-util-values';
|
|
@@ -358,6 +358,25 @@ export interface SessionEventEntry {
|
|
|
358
358
|
readonly type: 'event';
|
|
359
359
|
readonly event: SessionWireEvent;
|
|
360
360
|
}
|
|
361
|
+
/** v0-compatible Session metadata carried on the browser wire. */
|
|
362
|
+
export interface SessionWireHeader {
|
|
363
|
+
readonly version: number;
|
|
364
|
+
readonly id: SessionId;
|
|
365
|
+
readonly createdAt: number;
|
|
366
|
+
readonly cwd?: string;
|
|
367
|
+
readonly parentSession?: SessionId;
|
|
368
|
+
/** Exact inherited prefix length; absent for an unseeded Session. */
|
|
369
|
+
readonly seedLength?: number;
|
|
370
|
+
readonly origin?: 'subagent';
|
|
371
|
+
readonly delegationDepth?: number;
|
|
372
|
+
readonly agentPreset?: string;
|
|
373
|
+
}
|
|
374
|
+
/** Browser wire form of one Session surface operation. */
|
|
375
|
+
export type SessionWireSurfaceOp = 'append' | {
|
|
376
|
+
readonly op: 'replace';
|
|
377
|
+
readonly start: number;
|
|
378
|
+
readonly end: number;
|
|
379
|
+
};
|
|
361
380
|
/** Event-shaped wire representation of one packed chunk row. */
|
|
362
381
|
export type ChunkRowEvent = {
|
|
363
382
|
[Kind in ChunkRow['type']]: {
|
|
@@ -384,7 +403,7 @@ export interface SessionWireEvent {
|
|
|
384
403
|
readonly data: JsonValue;
|
|
385
404
|
readonly ignorable?: true;
|
|
386
405
|
readonly sourceEventSeqs?: number[];
|
|
387
|
-
readonly surfaceOp?:
|
|
406
|
+
readonly surfaceOp?: SessionWireSurfaceOp;
|
|
388
407
|
}
|
|
389
408
|
/** One message-aligned backwards-history request. */
|
|
390
409
|
export interface SessionPageRequest {
|
|
@@ -407,7 +426,7 @@ export interface SessionPage {
|
|
|
407
426
|
/** Complete opening window followed by ordered events appended after its cursor. */
|
|
408
427
|
export type SessionFollowFrame = {
|
|
409
428
|
readonly type: 'snapshot';
|
|
410
|
-
readonly header:
|
|
429
|
+
readonly header: SessionWireHeader;
|
|
411
430
|
readonly cursor: number;
|
|
412
431
|
readonly records: readonly SessionHistoryRecord[];
|
|
413
432
|
readonly hasMore: boolean;
|