@deepseek-ai/dsh-api-session-controller 0.1.6-alpha.2 → 0.1.7-alpha.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/README.i18n.yaml +2 -2
- package/README.md +31 -8
- package/README.zh.md +31 -8
- package/lib/client.js +603 -515
- package/lib/index.js +299 -137
- package/lib/typert.host.js +373 -183
- package/lib/typert.remote-client.d.ts +10 -5
- package/lib/typert.remote-client.js +216 -124
- 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 +2 -0
- package/lib/types/client/contract/sessions.d.ts +11 -16
- 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 +32 -13
- package/lib/types/client/sessions/projection-store.js +45 -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 +18 -16
- package/lib/types/client/sessions/session.js +112 -50
- 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 +1 -1
- 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 +37 -23
- package/lib/types/types.js +0 -1
- package/package.json +71 -76
|
@@ -43,6 +43,7 @@ export class Session {
|
|
|
43
43
|
jumpTargetSeq = null;
|
|
44
44
|
/** The running jump loop's completion, shared by retargeting callers. */
|
|
45
45
|
jumpPromise = null;
|
|
46
|
+
pendingHistory = null;
|
|
46
47
|
stopObservingInbox;
|
|
47
48
|
assistantStream = new ClientAssistantStream();
|
|
48
49
|
running = false;
|
|
@@ -56,7 +57,7 @@ export class Session {
|
|
|
56
57
|
promptAttempted = false;
|
|
57
58
|
/** A first accepted prompt stays in the engaging phase until its turn is observable. */
|
|
58
59
|
firstPromptPendingTurn = false;
|
|
59
|
-
/**
|
|
60
|
+
/** New Session display state; unknown bare sessions begin conservatively blank. */
|
|
60
61
|
blankBit = true;
|
|
61
62
|
removed = false;
|
|
62
63
|
promptError = null;
|
|
@@ -72,8 +73,9 @@ export class Session {
|
|
|
72
73
|
* Per-session projection value store (push model; see the session-projection
|
|
73
74
|
* subsystem page, docs/subsystems/session-projection.md): finished whole
|
|
74
75
|
* values computed on the Host, seeded by the tail page's
|
|
75
|
-
* projections block and updated by Session Controller control frames
|
|
76
|
-
*
|
|
76
|
+
* projections block and updated by Session Controller control frames;
|
|
77
|
+
* Host-sequenced writes merge under higher-seq-wins and cached list blocks
|
|
78
|
+
* yield to them (projection-store.ts). Keys are read via `projections.faceOf(key)`
|
|
77
79
|
* (the useProjection resolution face); the conversation snapshot never
|
|
78
80
|
* carries projection values, and no client-side domain folding exists.
|
|
79
81
|
* Manager-owned when constructed through SessionManager (frames route and
|
|
@@ -140,16 +142,15 @@ export class Session {
|
|
|
140
142
|
*/
|
|
141
143
|
beginSubmission(input) {
|
|
142
144
|
const requestId = randomUUID();
|
|
145
|
+
const placement = this.running ? input.mode === 'steer' ? 'steering' : 'queued' : 'transcript';
|
|
143
146
|
this.pendingSubmissions = [...this.pendingSubmissions, {
|
|
144
147
|
requestId,
|
|
145
|
-
placement
|
|
146
|
-
? input.mode === 'steer' ? 'steering' : 'queued'
|
|
147
|
-
: 'transcript',
|
|
148
|
+
placement,
|
|
148
149
|
time: Date.now(),
|
|
149
150
|
text: input.text,
|
|
150
151
|
attachments: input.attachments,
|
|
151
152
|
}];
|
|
152
|
-
this.submissionSettlements.set(requestId, { onRetire: input.onRetire, retiring: false });
|
|
153
|
+
this.submissionSettlements.set(requestId, { placement, onRetire: input.onRetire, retiring: false });
|
|
153
154
|
// The blank → engaging edge flips here, ahead of prompt(): the composer
|
|
154
155
|
// docks and the echo renders on the click's own frame.
|
|
155
156
|
this.promptAttempted = true;
|
|
@@ -213,19 +214,12 @@ export class Session {
|
|
|
213
214
|
this.notifier.markDirty();
|
|
214
215
|
return result;
|
|
215
216
|
}
|
|
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.
|
|
217
|
+
// Rejection must leave a first prompt blank and eligible for workspace reuse.
|
|
224
218
|
if (this.blankBit) {
|
|
225
219
|
this.blankBit = false;
|
|
226
|
-
this.options.onEngaged?.(this);
|
|
227
220
|
this.notifier.markDirty();
|
|
228
221
|
}
|
|
222
|
+
this.options.onEngaged?.(this);
|
|
229
223
|
return result;
|
|
230
224
|
}
|
|
231
225
|
/**
|
|
@@ -347,6 +341,15 @@ export class Session {
|
|
|
347
341
|
// loop starts here.
|
|
348
342
|
if (this.loadingOlder)
|
|
349
343
|
return Promise.resolve();
|
|
344
|
+
const events = this.events;
|
|
345
|
+
if (events === undefined)
|
|
346
|
+
return Promise.resolve();
|
|
347
|
+
const pending = {
|
|
348
|
+
beforeSeq: this.baseSeq,
|
|
349
|
+
hasMore: this.hasMore,
|
|
350
|
+
pages: [],
|
|
351
|
+
};
|
|
352
|
+
this.pendingHistory = pending;
|
|
350
353
|
this.jumpTargetSeq = seq;
|
|
351
354
|
this.loadingOlder = true;
|
|
352
355
|
this.notifier.markDirty();
|
|
@@ -356,17 +359,14 @@ export class Session {
|
|
|
356
359
|
const generation = this.openGeneration;
|
|
357
360
|
this.jumpPromise = (async () => {
|
|
358
361
|
try {
|
|
359
|
-
while (
|
|
362
|
+
while (pending.hasMore && this.jumpTargetSeq !== null && pending.beforeSeq > this.jumpTargetSeq) {
|
|
360
363
|
if (generation !== this.openGeneration)
|
|
361
364
|
return;
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
return;
|
|
365
|
-
const before = this.baseSeq;
|
|
366
|
-
await events.prepend({ beforeSeq: this.baseSeq, maxMessages: JUMP_PAGE_MESSAGES });
|
|
365
|
+
const before = pending.beforeSeq;
|
|
366
|
+
await events.prepend({ beforeSeq: before, maxMessages: JUMP_PAGE_MESSAGES });
|
|
367
367
|
// No-progress guard: an empty or dropped page that still claims more
|
|
368
368
|
// history must end the loop, not spin it.
|
|
369
|
-
if (
|
|
369
|
+
if (pending.beforeSeq >= before)
|
|
370
370
|
return;
|
|
371
371
|
}
|
|
372
372
|
}
|
|
@@ -378,7 +378,11 @@ export class Session {
|
|
|
378
378
|
finally {
|
|
379
379
|
this.jumpTargetSeq = null;
|
|
380
380
|
this.jumpPromise = null;
|
|
381
|
+
this.pendingHistory = null;
|
|
381
382
|
this.loadingOlder = false;
|
|
383
|
+
if (generation === this.openGeneration && pending.pages.length > 0) {
|
|
384
|
+
this.prependWindow(pending.pages.reverse().flat(), pending.hasMore);
|
|
385
|
+
}
|
|
382
386
|
this.notifier.markDirty();
|
|
383
387
|
}
|
|
384
388
|
})();
|
|
@@ -424,8 +428,7 @@ export class Session {
|
|
|
424
428
|
* @param running - the new running state.
|
|
425
429
|
*/
|
|
426
430
|
handleRunning(running) {
|
|
427
|
-
//
|
|
428
|
-
// running:true proves another side's first message landed.
|
|
431
|
+
// Running converts display state without establishing durable turn history.
|
|
429
432
|
if (running && this.blankBit) {
|
|
430
433
|
this.blankBit = false;
|
|
431
434
|
this.notifier.markDirty();
|
|
@@ -465,13 +468,15 @@ export class Session {
|
|
|
465
468
|
this.notifier.markDirty();
|
|
466
469
|
}
|
|
467
470
|
/**
|
|
468
|
-
*
|
|
469
|
-
* `
|
|
470
|
-
* running
|
|
471
|
-
*
|
|
472
|
-
*
|
|
471
|
+
* Apply the Manager's effective display blank, further reconciled with the
|
|
472
|
+
* current `sessionListMetadata` projection. Local send attempts and current
|
|
473
|
+
* running state prevent re-blanking; an earlier false summary alone does not.
|
|
474
|
+
* The Manager retains acceptance and earlier running observations across
|
|
475
|
+
* Session-object replacement.
|
|
476
|
+
* @param blank - New Session display state after Manager reconciliation.
|
|
473
477
|
*/
|
|
474
478
|
handleBlank(blank) {
|
|
479
|
+
blank = blank && this.projections.values().sessionListMetadata?.blank !== false;
|
|
475
480
|
if (blank === this.blankBit)
|
|
476
481
|
return;
|
|
477
482
|
if (blank && (this.promptAttempted || this.running))
|
|
@@ -570,13 +575,31 @@ export class Session {
|
|
|
570
575
|
const visible = this.assistantStream.replace(entries, assistantStream);
|
|
571
576
|
this.baseSeq = SessionLogOffset(entries[0]?.event.seq ?? 0);
|
|
572
577
|
this.hasMore = hasMore;
|
|
578
|
+
if (this.pendingHistory !== null) {
|
|
579
|
+
this.pendingHistory.beforeSeq = this.baseSeq;
|
|
580
|
+
this.pendingHistory.hasMore = hasMore;
|
|
581
|
+
this.pendingHistory.pages.length = 0;
|
|
582
|
+
}
|
|
573
583
|
if (visible.some(entry => entry.event.type === 'turn/start'))
|
|
574
584
|
this.firstPromptPendingTurn = false;
|
|
575
585
|
if (projections !== undefined)
|
|
576
586
|
this.projections.seed(projections);
|
|
577
587
|
this.eventSource.replace(visible, hasMore);
|
|
588
|
+
// A new follow baseline replaces optimistic steering with Host-owned rows.
|
|
589
|
+
// Receipt-backed inputs are accepted, not failed, even if their history is outside this window.
|
|
590
|
+
if (projections !== undefined) {
|
|
591
|
+
for (const [requestId, { receipt }] of this.submissionSettlements) {
|
|
592
|
+
if (receipt !== undefined && receipt.seq <= projections.asOfSeq) {
|
|
593
|
+
this.scheduleObservedRetirement(requestId, receipt.attachments);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
578
597
|
for (const entry of visible)
|
|
579
598
|
this.observeSubmissionEvent(entry.event);
|
|
599
|
+
if (projections !== undefined) {
|
|
600
|
+
const inbox = projections.values.inbox;
|
|
601
|
+
this.observeSteeringInsertions(inbox?.['next-step'] ?? [], 0, projections.asOfSeq);
|
|
602
|
+
}
|
|
580
603
|
this.notifier.markDirty();
|
|
581
604
|
}
|
|
582
605
|
publishAssistantEntry(result) {
|
|
@@ -609,6 +632,13 @@ export class Session {
|
|
|
609
632
|
}
|
|
610
633
|
/** Prepend one stream-validated history page. */
|
|
611
634
|
prependWindow(entries, hasMore) {
|
|
635
|
+
if (this.pendingHistory !== null) {
|
|
636
|
+
const pending = this.pendingHistory;
|
|
637
|
+
pending.beforeSeq = entries[0] === undefined ? pending.beforeSeq : SessionLogOffset(entries[0].event.seq);
|
|
638
|
+
pending.hasMore = hasMore;
|
|
639
|
+
pending.pages.push(entries);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
612
642
|
this.baseSeq = entries[0] === undefined ? this.baseSeq : SessionLogOffset(entries[0].event.seq);
|
|
613
643
|
this.hasMore = hasMore;
|
|
614
644
|
this.eventSource.prepend(entries, hasMore);
|
|
@@ -631,37 +661,66 @@ export class Session {
|
|
|
631
661
|
if (this.submissionSettlements.size === 0)
|
|
632
662
|
return;
|
|
633
663
|
if (event.type === 'agent/inbox/spliced') {
|
|
634
|
-
const
|
|
635
|
-
if (
|
|
636
|
-
for (const
|
|
637
|
-
|
|
664
|
+
const { target, start, removedCount = 0, inserted, outcome } = event.data;
|
|
665
|
+
if (target === 'next-step') {
|
|
666
|
+
for (const [requestId, settlement] of this.submissionSettlements) {
|
|
667
|
+
const receipt = settlement.receipt;
|
|
668
|
+
if (receipt === undefined || receipt.index === null || receipt.seq >= event.seq)
|
|
669
|
+
continue;
|
|
670
|
+
const removed = receipt.index >= start && receipt.index < start + removedCount;
|
|
671
|
+
if (removed && outcome === 'canceled')
|
|
672
|
+
this.retireFailedSubmission(requestId);
|
|
673
|
+
else
|
|
674
|
+
settlement.receipt = {
|
|
675
|
+
...receipt,
|
|
676
|
+
seq: event.seq,
|
|
677
|
+
index: removed ? null : receipt.index < start ? receipt.index : receipt.index + inserted.length - removedCount,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
this.observeSteeringInsertions(inserted, start, event.seq);
|
|
681
|
+
}
|
|
682
|
+
for (const message of inserted)
|
|
683
|
+
this.observeSubmissionMessage(message, false);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
if (event.type === 'request/context' || event.type === 'turn/end') {
|
|
687
|
+
for (const [requestId, settlement] of this.submissionSettlements) {
|
|
688
|
+
if (settlement.receipt?.index === null && settlement.receipt.seq < event.seq)
|
|
689
|
+
this.retireFailedSubmission(requestId);
|
|
638
690
|
}
|
|
639
691
|
return;
|
|
640
692
|
}
|
|
641
|
-
if (event.type
|
|
693
|
+
if (event.type === 'user/message')
|
|
694
|
+
this.observeSubmissionMessage(event.data, true);
|
|
695
|
+
}
|
|
696
|
+
observeSteeringInsertions(messages, start, seq) {
|
|
697
|
+
for (const [index, message] of messages.entries()) {
|
|
698
|
+
const source = message.source;
|
|
699
|
+
if (source.kind !== 'user' || !('rpcId' in source))
|
|
700
|
+
continue;
|
|
701
|
+
const settlement = this.submissionSettlements.get(source.rpcId);
|
|
702
|
+
if (settlement?.placement !== 'steering' || settlement.retiring || (settlement.receipt?.seq ?? -1) > seq)
|
|
703
|
+
continue;
|
|
704
|
+
settlement.receipt = { seq, index: start + index, attachments: attachmentRefsIn(message.content) };
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
observeSubmissionMessage(message, admitted) {
|
|
708
|
+
const source = message.source;
|
|
709
|
+
if (source.kind !== 'user' || !('rpcId' in source))
|
|
642
710
|
return;
|
|
643
|
-
|
|
644
|
-
// fields are narrowed rather than trusted (same posture as Conversation
|
|
645
|
-
// assembly matchers).
|
|
646
|
-
const data = event.data;
|
|
647
|
-
const source = data?.source;
|
|
648
|
-
if (source?.kind !== 'user' || typeof source.rpcId !== 'string')
|
|
711
|
+
if (!admitted && this.submissionSettlements.get(source.rpcId)?.placement === 'steering')
|
|
649
712
|
return;
|
|
650
|
-
this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(
|
|
713
|
+
this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(message.content));
|
|
651
714
|
}
|
|
652
|
-
/** Retire
|
|
715
|
+
/** Retire non-steering echoes when the Inbox accepts their queue occurrences. */
|
|
653
716
|
observeSubmissionInbox() {
|
|
654
717
|
if (this.submissionSettlements.size === 0)
|
|
655
718
|
return;
|
|
656
719
|
const inbox = this.projections.get('inbox');
|
|
657
720
|
if (inbox === undefined)
|
|
658
721
|
return;
|
|
659
|
-
for (const message of [...inbox['next-turn'], ...inbox['next-step']])
|
|
660
|
-
|
|
661
|
-
if (source.kind === 'user' && 'rpcId' in source) {
|
|
662
|
-
this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(message.content));
|
|
663
|
-
}
|
|
664
|
-
}
|
|
722
|
+
for (const message of [...inbox['next-turn'], ...inbox['next-step']])
|
|
723
|
+
this.observeSubmissionMessage(message, false);
|
|
665
724
|
}
|
|
666
725
|
/**
|
|
667
726
|
* Latch one observed settlement and remove the echo an animation frame
|
|
@@ -710,6 +769,7 @@ export class Session {
|
|
|
710
769
|
this.notifier.markDirty();
|
|
711
770
|
}
|
|
712
771
|
buildSnapshot() {
|
|
772
|
+
const identity = this.projections.values().subagent;
|
|
713
773
|
return {
|
|
714
774
|
sessionId: this.sessionId,
|
|
715
775
|
pendingSubmissions: this.pendingSubmissions,
|
|
@@ -717,7 +777,9 @@ export class Session {
|
|
|
717
777
|
subagent: this.address === undefined
|
|
718
778
|
? null
|
|
719
779
|
: {
|
|
720
|
-
address: this.address
|
|
780
|
+
address: this.address.mode === 'unknown' && identity != null
|
|
781
|
+
? { ...this.address, mode: identity.mode }
|
|
782
|
+
: this.address,
|
|
721
783
|
...(this.parentAvailable === undefined ? {} : { parentAvailable: this.parentAvailable }),
|
|
722
784
|
},
|
|
723
785
|
removed: this.removed,
|
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
|
package/lib/types/control.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/** Live Session
|
|
1
|
+
/** Live Session projection state with reconnect baselines. */
|
|
2
2
|
import { Deque } from '@deepseek-ai/dsh-deque';
|
|
3
3
|
/** Owns the Host-wide Session control stream. */
|
|
4
4
|
export class SessionControlController {
|
|
5
5
|
ctx;
|
|
6
6
|
streams = new Set();
|
|
7
|
-
/** @param ctx - Host context carrying live Agent
|
|
7
|
+
/** @param ctx - Host context carrying live Agent and projection services. */
|
|
8
8
|
constructor(ctx) {
|
|
9
9
|
this.ctx = ctx;
|
|
10
10
|
ctx.sessionProjections.onChanged((session, key, value, seq) => {
|
|
@@ -16,14 +16,6 @@ export class SessionControlController {
|
|
|
16
16
|
seq,
|
|
17
17
|
});
|
|
18
18
|
});
|
|
19
|
-
ctx.inject(['jobs'], (jobsCtx) => {
|
|
20
|
-
jobsCtx.jobs.onJobsChanged((owner) => { this.onJobsChanged(owner); });
|
|
21
|
-
});
|
|
22
|
-
ctx.on('session/created', (session) => {
|
|
23
|
-
const jobs = this.jobsFor(this.ctx.agents.get(session.id));
|
|
24
|
-
if (jobs.length > 0)
|
|
25
|
-
this.broadcast({ type: 'jobs', sessionId: session.id, jobs });
|
|
26
|
-
});
|
|
27
19
|
ctx.effect(() => () => {
|
|
28
20
|
for (const stream of this.streams)
|
|
29
21
|
stream.end();
|
|
@@ -50,13 +42,7 @@ export class SessionControlController {
|
|
|
50
42
|
}
|
|
51
43
|
baseline() {
|
|
52
44
|
const sessions = this.ctx.sessions.list();
|
|
53
|
-
const jobs = Object.create(null);
|
|
54
|
-
for (const session of sessions) {
|
|
55
|
-
const agent = this.ctx.agents.get(session.id);
|
|
56
|
-
jobs[session.id] = this.jobsFor(agent);
|
|
57
|
-
}
|
|
58
45
|
return {
|
|
59
|
-
jobs,
|
|
60
46
|
projections: this.projectionBaseline(sessions),
|
|
61
47
|
};
|
|
62
48
|
}
|
|
@@ -72,23 +58,6 @@ export class SessionControlController {
|
|
|
72
58
|
}
|
|
73
59
|
return blocks;
|
|
74
60
|
}
|
|
75
|
-
onJobsChanged(owner) {
|
|
76
|
-
if (owner !== undefined) {
|
|
77
|
-
this.broadcast({ type: 'jobs', sessionId: owner.id, jobs: this.jobsFor(owner) });
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
for (const session of this.ctx.sessions.list()) {
|
|
81
|
-
this.broadcast({
|
|
82
|
-
type: 'jobs',
|
|
83
|
-
sessionId: session.id,
|
|
84
|
-
jobs: this.jobsFor(this.ctx.agents.get(session.id)),
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
jobsFor(agent) {
|
|
89
|
-
const jobs = this.ctx.get('jobs');
|
|
90
|
-
return jobs === undefined ? [] : jobs.list(agent).map(jobView);
|
|
91
|
-
}
|
|
92
61
|
broadcast(frame) {
|
|
93
62
|
for (const stream of this.streams)
|
|
94
63
|
stream.push(frame);
|
|
@@ -135,15 +104,4 @@ class ControlQueue {
|
|
|
135
104
|
}
|
|
136
105
|
}
|
|
137
106
|
}
|
|
138
|
-
function jobView(job) {
|
|
139
|
-
return {
|
|
140
|
-
id: job.id,
|
|
141
|
-
kind: job.kind,
|
|
142
|
-
label: job.label,
|
|
143
|
-
status: job.status,
|
|
144
|
-
...(job.detail === undefined ? {} : { detail: job.detail }),
|
|
145
|
-
startedAt: job.startedAt,
|
|
146
|
-
...(job.finishedAt === undefined ? {} : { finishedAt: job.finishedAt }),
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
107
|
//# sourceMappingURL=control.js.map
|
package/lib/types/history.js
CHANGED
|
@@ -370,7 +370,7 @@ function validateAddress(address, header, inheritedEventCount, projections) {
|
|
|
370
370
|
reason: 'unsupported',
|
|
371
371
|
});
|
|
372
372
|
}
|
|
373
|
-
if (identity.mode !== address.mode) {
|
|
373
|
+
if (address.mode !== 'unknown' && identity.mode !== address.mode) {
|
|
374
374
|
throw new RemoteError('subagent/unauthorized', 'subagent mode does not match the supplied address', {
|
|
375
375
|
childSessionId: address.childSessionId,
|
|
376
376
|
});
|