@deepseek-ai/dsh-api-session-controller 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 +34 -9
- package/README.zh.md +34 -9
- package/lib/client.js +774 -641
- package/lib/index.js +318 -143
- package/lib/typert.host.js +384 -186
- package/lib/typert.remote-client.d.ts +10 -5
- package/lib/typert.remote-client.js +225 -125
- package/lib/types/archived-session-gate.d.ts +30 -0
- package/lib/types/archived-session-gate.js +52 -0
- package/lib/types/client/contract/session.d.ts +11 -4
- package/lib/types/client/contract/sessions.d.ts +11 -16
- package/lib/types/client/contract/snapshot.d.ts +1 -1
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/sessions/lineage.d.ts +4 -1
- package/lib/types/client/sessions/lineage.js +2 -1
- package/lib/types/client/sessions/manager.d.ts +66 -59
- package/lib/types/client/sessions/manager.js +249 -277
- package/lib/types/client/sessions/projection-store.d.ts +38 -13
- package/lib/types/client/sessions/projection-store.js +54 -13
- package/lib/types/client/sessions/remotes.d.ts +1 -2
- package/lib/types/client/sessions/service.d.ts +23 -35
- package/lib/types/client/sessions/service.js +35 -47
- package/lib/types/client/sessions/session.d.ts +23 -19
- package/lib/types/client/sessions/session.js +158 -59
- package/lib/types/client/transport.js +5 -2
- package/lib/types/commands.d.ts +4 -2
- package/lib/types/commands.js +72 -36
- package/lib/types/control.d.ts +2 -4
- package/lib/types/control.js +2 -44
- package/lib/types/history.js +25 -10
- package/lib/types/index.d.ts +32 -5
- package/lib/types/index.js +117 -13
- package/lib/types/list.js +24 -15
- package/lib/types/skill-catalog.js +60 -50
- package/lib/types/types.d.ts +45 -25
- package/lib/types/types.js +0 -1
- package/package.json +71 -76
|
@@ -15,10 +15,15 @@ function projectionsBaseline(value) {
|
|
|
15
15
|
asOfSeq: value.asOfSeq === -1 ? -1 : SessionSeq(value.asOfSeq),
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
18
|
+
/** Minimum message count for ordinary history windows. */
|
|
19
19
|
export const PAGE_MESSAGES = 50;
|
|
20
|
-
|
|
20
|
+
const HISTORY_PAGE_OPTIONS = { maxMessages: 500, turnWindow: { minMessages: PAGE_MESSAGES, minTurns: 2 } };
|
|
21
|
+
/** Minimum messages per page while a turn jump loops backwards. */
|
|
21
22
|
export const JUMP_PAGE_MESSAGES = 200;
|
|
23
|
+
const JUMP_PAGE_OPTIONS = {
|
|
24
|
+
...HISTORY_PAGE_OPTIONS,
|
|
25
|
+
turnWindow: { ...HISTORY_PAGE_OPTIONS.turnWindow, minMessages: JUMP_PAGE_MESSAGES },
|
|
26
|
+
};
|
|
22
27
|
/**
|
|
23
28
|
* Owns a session's event window, lifecycle state, and observable
|
|
24
29
|
* snapshot. React bindings remain outside this data layer. Features see only
|
|
@@ -43,6 +48,7 @@ export class Session {
|
|
|
43
48
|
jumpTargetSeq = null;
|
|
44
49
|
/** The running jump loop's completion, shared by retargeting callers. */
|
|
45
50
|
jumpPromise = null;
|
|
51
|
+
pendingHistory = null;
|
|
46
52
|
stopObservingInbox;
|
|
47
53
|
assistantStream = new ClientAssistantStream();
|
|
48
54
|
running = false;
|
|
@@ -56,7 +62,7 @@ export class Session {
|
|
|
56
62
|
promptAttempted = false;
|
|
57
63
|
/** A first accepted prompt stays in the engaging phase until its turn is observable. */
|
|
58
64
|
firstPromptPendingTurn = false;
|
|
59
|
-
/**
|
|
65
|
+
/** New Session display state; unknown bare sessions begin conservatively blank. */
|
|
60
66
|
blankBit = true;
|
|
61
67
|
removed = false;
|
|
62
68
|
promptError = null;
|
|
@@ -72,8 +78,9 @@ export class Session {
|
|
|
72
78
|
* Per-session projection value store (push model; see the session-projection
|
|
73
79
|
* subsystem page, docs/subsystems/session-projection.md): finished whole
|
|
74
80
|
* values computed on the Host, seeded by the tail page's
|
|
75
|
-
* projections block and updated by Session Controller control frames
|
|
76
|
-
*
|
|
81
|
+
* projections block and updated by Session Controller control frames;
|
|
82
|
+
* Host-sequenced writes merge under higher-seq-wins and cached list blocks
|
|
83
|
+
* yield to them (projection-store.ts). Keys are read via `projections.faceOf(key)`
|
|
77
84
|
* (the useProjection resolution face); the conversation snapshot never
|
|
78
85
|
* carries projection values, and no client-side domain folding exists.
|
|
79
86
|
* Manager-owned when constructed through SessionManager (frames route and
|
|
@@ -140,16 +147,15 @@ export class Session {
|
|
|
140
147
|
*/
|
|
141
148
|
beginSubmission(input) {
|
|
142
149
|
const requestId = randomUUID();
|
|
150
|
+
const placement = this.running ? input.mode === 'steer' ? 'steering' : 'queued' : 'transcript';
|
|
143
151
|
this.pendingSubmissions = [...this.pendingSubmissions, {
|
|
144
152
|
requestId,
|
|
145
|
-
placement
|
|
146
|
-
? input.mode === 'steer' ? 'steering' : 'queued'
|
|
147
|
-
: 'transcript',
|
|
153
|
+
placement,
|
|
148
154
|
time: Date.now(),
|
|
149
155
|
text: input.text,
|
|
150
156
|
attachments: input.attachments,
|
|
151
157
|
}];
|
|
152
|
-
this.submissionSettlements.set(requestId, { onRetire: input.onRetire, retiring: false });
|
|
158
|
+
this.submissionSettlements.set(requestId, { placement, onRetire: input.onRetire, retiring: false });
|
|
153
159
|
// The blank → engaging edge flips here, ahead of prompt(): the composer
|
|
154
160
|
// docks and the echo renders on the click's own frame.
|
|
155
161
|
this.promptAttempted = true;
|
|
@@ -213,19 +219,12 @@ export class Session {
|
|
|
213
219
|
this.notifier.markDirty();
|
|
214
220
|
return result;
|
|
215
221
|
}
|
|
216
|
-
//
|
|
217
|
-
// conversation's first turn on the host (the host criterion — a logged
|
|
218
|
-
// turn/start — is fact, not optimism; standalone command and projection
|
|
219
|
-
// events never flip it), while a rejected first prompt must keep the
|
|
220
|
-
// session blank — the client-side blank mirror only ever lowers, so
|
|
221
|
-
// flipping early on a failure would surface the session forever and
|
|
222
|
-
// strip its connectWorkspace reuse eligibility against the host's
|
|
223
|
-
// authority.
|
|
222
|
+
// Rejection must leave a first prompt blank and eligible for workspace reuse.
|
|
224
223
|
if (this.blankBit) {
|
|
225
224
|
this.blankBit = false;
|
|
226
|
-
this.options.onEngaged?.(this);
|
|
227
225
|
this.notifier.markDirty();
|
|
228
226
|
}
|
|
227
|
+
this.options.onEngaged?.(this);
|
|
229
228
|
return result;
|
|
230
229
|
}
|
|
231
230
|
/**
|
|
@@ -310,7 +309,7 @@ export class Session {
|
|
|
310
309
|
this.openPromise = promise;
|
|
311
310
|
return promise;
|
|
312
311
|
}
|
|
313
|
-
/**
|
|
312
|
+
/** Prepend one Turn-aligned page: at least 50 messages and two Turn starts, capped at 500 messages. */
|
|
314
313
|
async loadOlder() {
|
|
315
314
|
if (this.openState !== 'open' || !this.hasMore || this.loadingOlder)
|
|
316
315
|
return;
|
|
@@ -320,7 +319,10 @@ export class Session {
|
|
|
320
319
|
this.loadingOlder = true;
|
|
321
320
|
this.notifier.markDirty();
|
|
322
321
|
try {
|
|
323
|
-
await events.prepend({
|
|
322
|
+
await events.prepend({
|
|
323
|
+
beforeSeq: this.baseSeq,
|
|
324
|
+
...HISTORY_PAGE_OPTIONS,
|
|
325
|
+
});
|
|
324
326
|
}
|
|
325
327
|
catch (error) {
|
|
326
328
|
if (!isRemoteFailure(error)) {
|
|
@@ -347,6 +349,15 @@ export class Session {
|
|
|
347
349
|
// loop starts here.
|
|
348
350
|
if (this.loadingOlder)
|
|
349
351
|
return Promise.resolve();
|
|
352
|
+
const events = this.events;
|
|
353
|
+
if (events === undefined)
|
|
354
|
+
return Promise.resolve();
|
|
355
|
+
const pending = {
|
|
356
|
+
beforeSeq: this.baseSeq,
|
|
357
|
+
hasMore: this.hasMore,
|
|
358
|
+
pages: [],
|
|
359
|
+
};
|
|
360
|
+
this.pendingHistory = pending;
|
|
350
361
|
this.jumpTargetSeq = seq;
|
|
351
362
|
this.loadingOlder = true;
|
|
352
363
|
this.notifier.markDirty();
|
|
@@ -356,17 +367,14 @@ export class Session {
|
|
|
356
367
|
const generation = this.openGeneration;
|
|
357
368
|
this.jumpPromise = (async () => {
|
|
358
369
|
try {
|
|
359
|
-
while (
|
|
370
|
+
while (pending.hasMore && this.jumpTargetSeq !== null && pending.beforeSeq > this.jumpTargetSeq) {
|
|
360
371
|
if (generation !== this.openGeneration)
|
|
361
372
|
return;
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
return;
|
|
365
|
-
const before = this.baseSeq;
|
|
366
|
-
await events.prepend({ beforeSeq: this.baseSeq, maxMessages: JUMP_PAGE_MESSAGES });
|
|
373
|
+
const before = pending.beforeSeq;
|
|
374
|
+
await events.prepend({ beforeSeq: before, ...JUMP_PAGE_OPTIONS });
|
|
367
375
|
// No-progress guard: an empty or dropped page that still claims more
|
|
368
376
|
// history must end the loop, not spin it.
|
|
369
|
-
if (
|
|
377
|
+
if (pending.beforeSeq >= before)
|
|
370
378
|
return;
|
|
371
379
|
}
|
|
372
380
|
}
|
|
@@ -378,7 +386,11 @@ export class Session {
|
|
|
378
386
|
finally {
|
|
379
387
|
this.jumpTargetSeq = null;
|
|
380
388
|
this.jumpPromise = null;
|
|
389
|
+
this.pendingHistory = null;
|
|
381
390
|
this.loadingOlder = false;
|
|
391
|
+
if (generation === this.openGeneration && pending.pages.length > 0) {
|
|
392
|
+
this.prependWindow(pending.pages.reverse().flat(), pending.hasMore);
|
|
393
|
+
}
|
|
382
394
|
this.notifier.markDirty();
|
|
383
395
|
}
|
|
384
396
|
})();
|
|
@@ -424,8 +436,7 @@ export class Session {
|
|
|
424
436
|
* @param running - the new running state.
|
|
425
437
|
*/
|
|
426
438
|
handleRunning(running) {
|
|
427
|
-
//
|
|
428
|
-
// running:true proves another side's first message landed.
|
|
439
|
+
// Running converts display state without establishing durable turn history.
|
|
429
440
|
if (running && this.blankBit) {
|
|
430
441
|
this.blankBit = false;
|
|
431
442
|
this.notifier.markDirty();
|
|
@@ -465,13 +476,15 @@ export class Session {
|
|
|
465
476
|
this.notifier.markDirty();
|
|
466
477
|
}
|
|
467
478
|
/**
|
|
468
|
-
*
|
|
469
|
-
* `
|
|
470
|
-
* running
|
|
471
|
-
*
|
|
472
|
-
*
|
|
479
|
+
* Apply the Manager's effective display blank, further reconciled with the
|
|
480
|
+
* current `sessionListMetadata` projection. Local send attempts and current
|
|
481
|
+
* running state prevent re-blanking; an earlier false summary alone does not.
|
|
482
|
+
* The Manager retains acceptance and earlier running observations across
|
|
483
|
+
* Session-object replacement.
|
|
484
|
+
* @param blank - New Session display state after Manager reconciliation.
|
|
473
485
|
*/
|
|
474
486
|
handleBlank(blank) {
|
|
487
|
+
blank = blank && this.projections.values().sessionListMetadata?.blank !== false;
|
|
475
488
|
if (blank === this.blankBit)
|
|
476
489
|
return;
|
|
477
490
|
if (blank && (this.promptAttempted || this.running))
|
|
@@ -499,10 +512,12 @@ export class Session {
|
|
|
499
512
|
async dispose() {
|
|
500
513
|
this.stopObservingInbox();
|
|
501
514
|
// Unsettled echoes retire as failed so their owners can restore or
|
|
502
|
-
// release browser resources; echoes
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
515
|
+
// release browser resources; admitted echoes keep their observed outcome.
|
|
516
|
+
for (const [requestId, settlement] of [...this.submissionSettlements]) {
|
|
517
|
+
if (settlement.admitted !== undefined)
|
|
518
|
+
this.scheduleObservedRetirement(requestId, settlement.admitted);
|
|
519
|
+
else
|
|
520
|
+
this.retireFailedSubmission(requestId);
|
|
506
521
|
}
|
|
507
522
|
this.openGeneration++;
|
|
508
523
|
const events = this.events;
|
|
@@ -527,7 +542,7 @@ export class Session {
|
|
|
527
542
|
});
|
|
528
543
|
this.events = events;
|
|
529
544
|
try {
|
|
530
|
-
await events.open(
|
|
545
|
+
await events.open(HISTORY_PAGE_OPTIONS);
|
|
531
546
|
if (generation !== this.openGeneration || this.events !== events)
|
|
532
547
|
return;
|
|
533
548
|
this.openState = 'open';
|
|
@@ -570,13 +585,33 @@ export class Session {
|
|
|
570
585
|
const visible = this.assistantStream.replace(entries, assistantStream);
|
|
571
586
|
this.baseSeq = SessionLogOffset(entries[0]?.event.seq ?? 0);
|
|
572
587
|
this.hasMore = hasMore;
|
|
588
|
+
if (this.pendingHistory !== null) {
|
|
589
|
+
this.pendingHistory.beforeSeq = this.baseSeq;
|
|
590
|
+
this.pendingHistory.hasMore = hasMore;
|
|
591
|
+
this.pendingHistory.pages.length = 0;
|
|
592
|
+
}
|
|
573
593
|
if (visible.some(entry => entry.event.type === 'turn/start'))
|
|
574
594
|
this.firstPromptPendingTurn = false;
|
|
575
595
|
if (projections !== undefined)
|
|
576
596
|
this.projections.seed(projections);
|
|
577
597
|
this.eventSource.replace(visible, hasMore);
|
|
598
|
+
// A new follow baseline replaces confirmed optimistic inputs with Host-owned rows.
|
|
599
|
+
// Receipt-backed inputs are accepted, not failed, even if their history is outside this window.
|
|
600
|
+
if (projections !== undefined) {
|
|
601
|
+
for (const [requestId, { receipt }] of this.submissionSettlements) {
|
|
602
|
+
if (receipt !== undefined && receipt.seq <= projections.asOfSeq) {
|
|
603
|
+
this.scheduleObservedRetirement(requestId, receipt.attachments);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
578
607
|
for (const entry of visible)
|
|
579
608
|
this.observeSubmissionEvent(entry.event);
|
|
609
|
+
if (projections !== undefined) {
|
|
610
|
+
const inbox = projections.values.inbox;
|
|
611
|
+
for (const target of ['next-turn', 'next-step']) {
|
|
612
|
+
this.observeSubmissionInsertions(target, inbox?.[target] ?? [], 0, projections.asOfSeq);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
580
615
|
this.notifier.markDirty();
|
|
581
616
|
}
|
|
582
617
|
publishAssistantEntry(result) {
|
|
@@ -609,6 +644,13 @@ export class Session {
|
|
|
609
644
|
}
|
|
610
645
|
/** Prepend one stream-validated history page. */
|
|
611
646
|
prependWindow(entries, hasMore) {
|
|
647
|
+
if (this.pendingHistory !== null) {
|
|
648
|
+
const pending = this.pendingHistory;
|
|
649
|
+
pending.beforeSeq = entries[0] === undefined ? pending.beforeSeq : SessionLogOffset(entries[0].event.seq);
|
|
650
|
+
pending.hasMore = hasMore;
|
|
651
|
+
pending.pages.push(entries);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
612
654
|
this.baseSeq = entries[0] === undefined ? this.baseSeq : SessionLogOffset(entries[0].event.seq);
|
|
613
655
|
this.hasMore = hasMore;
|
|
614
656
|
this.eventSource.prepend(entries, hasMore);
|
|
@@ -631,37 +673,91 @@ export class Session {
|
|
|
631
673
|
if (this.submissionSettlements.size === 0)
|
|
632
674
|
return;
|
|
633
675
|
if (event.type === 'agent/inbox/spliced') {
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
676
|
+
const { target, start, removedCount = 0, inserted, outcome } = event.data;
|
|
677
|
+
for (const [requestId, settlement] of this.submissionSettlements) {
|
|
678
|
+
const receipt = settlement.receipt;
|
|
679
|
+
if (receipt?.target !== target || receipt.index === null || receipt.seq >= event.seq)
|
|
680
|
+
continue;
|
|
681
|
+
const removed = receipt.index >= start && receipt.index < start + removedCount;
|
|
682
|
+
if (removed && outcome === 'canceled')
|
|
683
|
+
this.retireFailedSubmission(requestId);
|
|
684
|
+
else
|
|
685
|
+
settlement.receipt = {
|
|
686
|
+
...receipt,
|
|
687
|
+
seq: event.seq,
|
|
688
|
+
index: removed ? null : receipt.index < start ? receipt.index : receipt.index + inserted.length - removedCount,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
this.observeSubmissionInsertions(target, inserted, start, event.seq);
|
|
692
|
+
for (const message of inserted)
|
|
693
|
+
this.observeSubmissionMessage(message, false);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (event.type === 'request/context' || event.type === 'turn/end') {
|
|
697
|
+
for (const [requestId, settlement] of this.submissionSettlements) {
|
|
698
|
+
if (settlement.admitted === undefined && settlement.receipt?.index === null
|
|
699
|
+
&& settlement.receipt.seq < event.seq)
|
|
700
|
+
this.retireFailedSubmission(requestId);
|
|
638
701
|
}
|
|
639
702
|
return;
|
|
640
703
|
}
|
|
641
|
-
if (event.type
|
|
704
|
+
if (event.type === 'user/message')
|
|
705
|
+
this.observeSubmissionMessage(event.data, true);
|
|
706
|
+
}
|
|
707
|
+
observeSubmissionInsertions(target, messages, start, seq) {
|
|
708
|
+
for (const [index, message] of messages.entries()) {
|
|
709
|
+
const source = message.source;
|
|
710
|
+
if (source.kind !== 'user' || !('rpcId' in source))
|
|
711
|
+
continue;
|
|
712
|
+
const settlement = this.submissionSettlements.get(source.rpcId);
|
|
713
|
+
if (settlement === undefined || settlement.placement === 'queued'
|
|
714
|
+
|| settlement.retiring || (settlement.receipt?.seq ?? -1) > seq)
|
|
715
|
+
continue;
|
|
716
|
+
settlement.receipt = { target, seq, index: start + index, attachments: attachmentRefsIn(message.content) };
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
observeSubmissionMessage(message, admitted) {
|
|
720
|
+
const source = message.source;
|
|
721
|
+
if (source.kind !== 'user' || !('rpcId' in source))
|
|
722
|
+
return;
|
|
723
|
+
const settlement = this.submissionSettlements.get(source.rpcId);
|
|
724
|
+
if (settlement === undefined || settlement.retiring)
|
|
725
|
+
return;
|
|
726
|
+
if (!admitted) {
|
|
727
|
+
if (settlement.placement === 'queued')
|
|
728
|
+
this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(message.content));
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
settlement.admitted = attachmentRefsIn(message.content);
|
|
732
|
+
this.retireAdmittedSubmission(source.rpcId);
|
|
733
|
+
}
|
|
734
|
+
/** Retire admitted Chat identities only after stale Inbox rows can no longer reappear. */
|
|
735
|
+
retireAdmittedSubmission(requestId) {
|
|
736
|
+
const settlement = this.submissionSettlements.get(requestId);
|
|
737
|
+
if (settlement?.admitted === undefined)
|
|
642
738
|
return;
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
const data = event.data;
|
|
647
|
-
const source = data?.source;
|
|
648
|
-
if (source?.kind !== 'user' || typeof source.rpcId !== 'string')
|
|
739
|
+
const receipt = settlement.receipt;
|
|
740
|
+
if (receipt?.index === null
|
|
741
|
+
&& (this.projections.seqOf('inbox') ?? -1) < receipt.seq)
|
|
649
742
|
return;
|
|
650
|
-
this.scheduleObservedRetirement(
|
|
743
|
+
this.scheduleObservedRetirement(requestId, settlement.admitted);
|
|
651
744
|
}
|
|
652
|
-
/**
|
|
745
|
+
/** Inbox acceptance retires queued echoes; its watermark completes admitted Chat handoffs. */
|
|
653
746
|
observeSubmissionInbox() {
|
|
654
747
|
if (this.submissionSettlements.size === 0)
|
|
655
748
|
return;
|
|
656
749
|
const inbox = this.projections.get('inbox');
|
|
657
750
|
if (inbox === undefined)
|
|
658
751
|
return;
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
if (
|
|
662
|
-
this.
|
|
663
|
-
|
|
752
|
+
const seq = this.projections.seqOf('inbox');
|
|
753
|
+
for (const target of ['next-turn', 'next-step']) {
|
|
754
|
+
if (seq !== undefined)
|
|
755
|
+
this.observeSubmissionInsertions(target, inbox[target], 0, seq);
|
|
756
|
+
for (const message of inbox[target])
|
|
757
|
+
this.observeSubmissionMessage(message, false);
|
|
664
758
|
}
|
|
759
|
+
for (const requestId of this.submissionSettlements.keys())
|
|
760
|
+
this.retireAdmittedSubmission(requestId);
|
|
665
761
|
}
|
|
666
762
|
/**
|
|
667
763
|
* Latch one observed settlement and remove the echo an animation frame
|
|
@@ -679,7 +775,7 @@ export class Session {
|
|
|
679
775
|
/** Remove one unsettled echo immediately (prompt rejection, abort, or disposal). */
|
|
680
776
|
retireFailedSubmission(requestId) {
|
|
681
777
|
const settlement = this.submissionSettlements.get(requestId);
|
|
682
|
-
if (settlement === undefined || settlement.retiring)
|
|
778
|
+
if (settlement === undefined || settlement.retiring || settlement.admitted !== undefined)
|
|
683
779
|
return;
|
|
684
780
|
settlement.retiring = true;
|
|
685
781
|
this.finishSubmission(requestId, { reason: 'failed' });
|
|
@@ -710,6 +806,7 @@ export class Session {
|
|
|
710
806
|
this.notifier.markDirty();
|
|
711
807
|
}
|
|
712
808
|
buildSnapshot() {
|
|
809
|
+
const identity = this.projections.values().subagent;
|
|
713
810
|
return {
|
|
714
811
|
sessionId: this.sessionId,
|
|
715
812
|
pendingSubmissions: this.pendingSubmissions,
|
|
@@ -717,7 +814,9 @@ export class Session {
|
|
|
717
814
|
subagent: this.address === undefined
|
|
718
815
|
? null
|
|
719
816
|
: {
|
|
720
|
-
address: this.address
|
|
817
|
+
address: this.address.mode === 'unknown' && identity != null
|
|
818
|
+
? { ...this.address, mode: identity.mode }
|
|
819
|
+
: this.address,
|
|
721
820
|
...(this.parentAvailable === undefined ? {} : { parentAvailable: this.parentAvailable }),
|
|
722
821
|
},
|
|
723
822
|
removed: this.removed,
|
|
@@ -76,7 +76,7 @@ export class SessionEventStream extends RemoteJournalStream {
|
|
|
76
76
|
for await (const frame of this.remote.session.follow({
|
|
77
77
|
address: this.address,
|
|
78
78
|
assistantStream: true,
|
|
79
|
-
...(request
|
|
79
|
+
...this.repairRequest(request),
|
|
80
80
|
}, signal)) {
|
|
81
81
|
if (frame.type === 'snapshot') {
|
|
82
82
|
for (const record of frame.records)
|
|
@@ -121,7 +121,10 @@ export class SessionEventStream extends RemoteJournalStream {
|
|
|
121
121
|
}
|
|
122
122
|
/** @inheritdoc */
|
|
123
123
|
repairRequest(request) {
|
|
124
|
-
return
|
|
124
|
+
return {
|
|
125
|
+
...(request.maxMessages === undefined ? {} : { maxMessages: request.maxMessages }),
|
|
126
|
+
...(request.turnWindow === undefined ? {} : { turnWindow: request.turnWindow }),
|
|
127
|
+
};
|
|
125
128
|
}
|
|
126
129
|
}
|
|
127
130
|
//# sourceMappingURL=transport.js.map
|
package/lib/types/commands.d.ts
CHANGED
|
@@ -32,8 +32,10 @@ export declare class SessionCommandController {
|
|
|
32
32
|
*/
|
|
33
33
|
rename(request: SessionRenameRequest): Promise<SessionRenameValue>;
|
|
34
34
|
/**
|
|
35
|
-
* Create a new ordinary Session from
|
|
36
|
-
*
|
|
35
|
+
* Create a new ordinary Session from an exact event prefix. An explicit
|
|
36
|
+
* `atSeq` is the inclusive cut; an omitted value selects the latest
|
|
37
|
+
* completed-turn prefix. An open cut receives synthetic fork closers.
|
|
38
|
+
* @param request - source Session and optional exact event boundary.
|
|
37
39
|
* @returns the new Session identity.
|
|
38
40
|
*/
|
|
39
41
|
fork(request: SessionForkRequest): Promise<SessionForkValue>;
|
package/lib/types/commands.js
CHANGED
|
@@ -55,6 +55,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
55
55
|
import { brandString } from '@deepseek-ai/dsh-brand';
|
|
56
56
|
import { AttachmentError } from '@deepseek-ai/dsh-attachment';
|
|
57
57
|
import { ReasoningEffortId, assistantStreamChunks, createUserMessage, freezeMessage, } from '@deepseek-ai/dsh-llm';
|
|
58
|
+
import { buildForkSeed } from '@deepseek-ai/dsh-session/fork';
|
|
58
59
|
import { SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session';
|
|
59
60
|
import { SessionQueryError } from '@deepseek-ai/dsh-session-query';
|
|
60
61
|
import { SessionTitleInvalidError } from '@deepseek-ai/dsh-session-title';
|
|
@@ -65,6 +66,23 @@ import { ApiSessionCwdConflict, ApiSessionNotFound, ApiSessionPresetConflict, Ap
|
|
|
65
66
|
function hasPromptContent(content) {
|
|
66
67
|
return content.some(part => part.type !== 'text' || part.text.trim().length > 0);
|
|
67
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the omitted-`atSeq` default to the latest completed-turn prefix,
|
|
71
|
+
* including standalone events before the next turn begins.
|
|
72
|
+
*/
|
|
73
|
+
function latestCompletedPrefixBoundary(events) {
|
|
74
|
+
const lastTurnEnd = events.findLast(event => event.type === 'turn/end');
|
|
75
|
+
if (lastTurnEnd === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
let boundary = lastTurnEnd.seq;
|
|
78
|
+
for (const next of events.slice(boundary + 1)) {
|
|
79
|
+
if (next.type === 'turn/start' || (next.type === 'user/message' && next.surfaceOp === 'append')
|
|
80
|
+
|| next.type === 'agent/inbox/spliced')
|
|
81
|
+
break;
|
|
82
|
+
boundary = next.seq;
|
|
83
|
+
}
|
|
84
|
+
return boundary;
|
|
85
|
+
}
|
|
68
86
|
/** Implements Session business commands delegated by the Session Controller Remote service. */
|
|
69
87
|
export class SessionCommandController {
|
|
70
88
|
ctx;
|
|
@@ -180,8 +198,10 @@ export class SessionCommandController {
|
|
|
180
198
|
}
|
|
181
199
|
}
|
|
182
200
|
/**
|
|
183
|
-
* Create a new ordinary Session from
|
|
184
|
-
*
|
|
201
|
+
* Create a new ordinary Session from an exact event prefix. An explicit
|
|
202
|
+
* `atSeq` is the inclusive cut; an omitted value selects the latest
|
|
203
|
+
* completed-turn prefix. An open cut receives synthetic fork closers.
|
|
204
|
+
* @param request - source Session and optional exact event boundary.
|
|
185
205
|
* @returns the new Session identity.
|
|
186
206
|
*/
|
|
187
207
|
async fork(request) {
|
|
@@ -208,20 +228,13 @@ export class SessionCommandController {
|
|
|
208
228
|
throw new RemoteError('gateway/internal', `fork source unavailable for session "${request.sessionId}": ${String(error)}`, {});
|
|
209
229
|
}
|
|
210
230
|
const source = __addDisposableResource(env_1, observed, false);
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
?? (atSeq === undefined || atSeq > lastSeq
|
|
217
|
-
? source.events.findLast(event => event.type === 'turn/end')
|
|
218
|
-
: undefined);
|
|
219
|
-
if (boundary === undefined) {
|
|
220
|
-
throw new RemoteError('session/fork-unavailable', atSeq !== undefined && atSeq <= lastSeq
|
|
221
|
-
? `session "${request.sessionId}" has not completed the turn containing event ${String(atSeq)}`
|
|
222
|
-
: `session "${request.sessionId}" has no completed turn to fork from`, { sessionId: request.sessionId });
|
|
231
|
+
const boundary = atSeq ?? latestCompletedPrefixBoundary(source.events);
|
|
232
|
+
if (boundary === undefined || source.events[boundary]?.seq !== boundary) {
|
|
233
|
+
throw new RemoteError('session/fork-unavailable', request.atSeq === undefined
|
|
234
|
+
? `session "${request.sessionId}" has no completed turn to fork from`
|
|
235
|
+
: `event ${String(request.atSeq)} does not exist in session "${request.sessionId}" (last seq: ${String(source.events.at(-1)?.seq ?? 'none')})`, { sessionId: request.sessionId });
|
|
223
236
|
}
|
|
224
|
-
const
|
|
237
|
+
const seed = buildForkSeed(source.events, boundary);
|
|
225
238
|
let workspace;
|
|
226
239
|
try {
|
|
227
240
|
workspace = await this.forkWorkspace(source.header);
|
|
@@ -235,8 +248,8 @@ export class SessionCommandController {
|
|
|
235
248
|
const { provider, model } = this.ctx.agentDefaultModel.currentSelection();
|
|
236
249
|
await this.ctx.agents.create({
|
|
237
250
|
sessionId: childId,
|
|
238
|
-
seed
|
|
239
|
-
inheritedEventCount:
|
|
251
|
+
seed,
|
|
252
|
+
inheritedEventCount: SessionLogOffset(boundary + 1),
|
|
240
253
|
meta: {
|
|
241
254
|
...(source.header.cwd === undefined ? {} : { cwd: source.header.cwd }),
|
|
242
255
|
parentSession: source.header.id,
|
|
@@ -383,6 +396,7 @@ export class SessionCommandController {
|
|
|
383
396
|
*/
|
|
384
397
|
async updateQueue(request) {
|
|
385
398
|
if (request.action.kind === 'edit') {
|
|
399
|
+
// oxlint-disable-next-line typescript/no-unnecessary-condition -- Remote callers can submit untyped JSON.
|
|
386
400
|
if (request.action.content.some(block => block.type !== 'text')) {
|
|
387
401
|
throw new RemoteError('session/attachment-invalid', 'queue edits accept text content only', { reason: 'QUEUE_EDIT_NON_TEXT' });
|
|
388
402
|
}
|
|
@@ -471,6 +485,9 @@ export class SessionCommandController {
|
|
|
471
485
|
rejectCreation(sessionId, error) {
|
|
472
486
|
if (remoteErrorOf(error) !== undefined)
|
|
473
487
|
throw error;
|
|
488
|
+
if (error instanceof Error && error.name === 'SessionAlreadyOwnedError') {
|
|
489
|
+
throw new RemoteError('session/writer-held', error.message, { sessionId });
|
|
490
|
+
}
|
|
474
491
|
if (error instanceof ApiSessionPresetConflict) {
|
|
475
492
|
throw new RemoteError('agent-preset/conflict', error.message, {
|
|
476
493
|
sessionId: error.sessionId,
|
|
@@ -554,33 +571,52 @@ function imageBlockIn(content, match) {
|
|
|
554
571
|
if (match(ref))
|
|
555
572
|
return ref;
|
|
556
573
|
}
|
|
557
|
-
if (block.type === 'tool-result') {
|
|
558
|
-
const nested = imageBlockIn(block.content, match);
|
|
559
|
-
if (nested !== undefined)
|
|
560
|
-
return nested;
|
|
561
|
-
}
|
|
562
574
|
}
|
|
563
575
|
return undefined;
|
|
564
576
|
}
|
|
577
|
+
/** Read only first-party declared content fields; unknown event payloads stay opaque. */
|
|
565
578
|
function imageInEvent(event, match) {
|
|
566
579
|
const data = event.data;
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
580
|
+
// First-party event payloads can be present without their producer plugin mounted.
|
|
581
|
+
const type = event.type;
|
|
582
|
+
switch (type) {
|
|
583
|
+
case 'user/message':
|
|
584
|
+
case 'tool/ptc-dispatch':
|
|
585
|
+
return imageBlockIn(data.content, match);
|
|
586
|
+
case 'system/message':
|
|
587
|
+
case 'developer/message':
|
|
588
|
+
case 'tool/result':
|
|
589
|
+
case 'team/message/queued':
|
|
590
|
+
return imageBlockIn(data.message?.content, match);
|
|
591
|
+
case 'agent/inbox/spliced': {
|
|
592
|
+
const messages = data.inserted;
|
|
593
|
+
if (!Array.isArray(messages))
|
|
594
|
+
return undefined;
|
|
595
|
+
for (const message of messages) {
|
|
596
|
+
if (typeof message !== 'object' || message === null || Array.isArray(message))
|
|
597
|
+
continue;
|
|
598
|
+
const found = imageBlockIn(message.content, match);
|
|
599
|
+
if (found !== undefined)
|
|
600
|
+
return found;
|
|
601
|
+
}
|
|
602
|
+
return undefined;
|
|
603
|
+
}
|
|
604
|
+
case 'compaction/summary':
|
|
605
|
+
return imageBlockIn(data.summary, match) ?? imageBlockIn(data.rawOutput, match);
|
|
606
|
+
case 'assistant/message': {
|
|
607
|
+
const found = imageBlockIn(data.message?.content, match);
|
|
581
608
|
if (found !== undefined)
|
|
582
609
|
return found;
|
|
610
|
+
break;
|
|
583
611
|
}
|
|
612
|
+
case 'assistant/attempt': break;
|
|
613
|
+
default: return undefined;
|
|
614
|
+
}
|
|
615
|
+
const assistant = event;
|
|
616
|
+
for (const chunk of assistantStreamChunks(assistant.data.stream, 'block-end')) {
|
|
617
|
+
const found = imageBlockIn([chunk.block], match);
|
|
618
|
+
if (found !== undefined)
|
|
619
|
+
return found;
|
|
584
620
|
}
|
|
585
621
|
return undefined;
|
|
586
622
|
}
|
package/lib/types/control.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/** Live Session
|
|
1
|
+
/** Live Session projection state with reconnect baselines. */
|
|
2
2
|
import type { Context } from '@deepseek-ai/cordis';
|
|
3
3
|
import type { SessionControlFrame } from './types.ts';
|
|
4
4
|
/** Owns the Host-wide Session control stream. */
|
|
5
5
|
export declare class SessionControlController {
|
|
6
6
|
private readonly ctx;
|
|
7
7
|
private readonly streams;
|
|
8
|
-
/** @param ctx - Host context carrying live Agent
|
|
8
|
+
/** @param ctx - Host context carrying live Agent and projection services. */
|
|
9
9
|
constructor(ctx: Context);
|
|
10
10
|
/**
|
|
11
11
|
* Open one generation of Host-wide live control state.
|
|
@@ -15,8 +15,6 @@ export declare class SessionControlController {
|
|
|
15
15
|
control(signal: AbortSignal): AsyncIterable<SessionControlFrame>;
|
|
16
16
|
private baseline;
|
|
17
17
|
private projectionBaseline;
|
|
18
|
-
private onJobsChanged;
|
|
19
|
-
private jobsFor;
|
|
20
18
|
private broadcast;
|
|
21
19
|
}
|
|
22
20
|
//# sourceMappingURL=control.d.ts.map
|