@deepseek-ai/dsh-api-session-controller 0.1.6-alpha.1 → 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 +42 -7
- package/README.zh.md +42 -7
- package/lib/client.js +1037 -992
- package/lib/index.js +314 -183
- package/lib/typert.host.js +539 -331
- package/lib/typert.remote-client.d.ts +10 -5
- package/lib/typert.remote-client.js +374 -264
- package/lib/types/agent.d.ts +1 -1
- package/lib/types/agent.js +8 -1
- 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 +65 -35
- package/lib/types/client/contract/snapshot.d.ts +0 -14
- package/lib/types/client/index.d.ts +12 -3
- package/lib/types/client/index.js +16 -5
- package/lib/types/client/scope.d.ts +13 -3
- package/lib/types/client/scope.js +15 -22
- package/lib/types/client/sessions/lineage.d.ts +5 -5
- package/lib/types/client/sessions/lineage.js +3 -4
- package/lib/types/client/sessions/manager.d.ts +91 -105
- package/lib/types/client/sessions/manager.js +307 -434
- package/lib/types/client/sessions/projection-store.d.ts +34 -21
- package/lib/types/client/sessions/projection-store.js +48 -24
- package/lib/types/client/sessions/remotes.d.ts +1 -2
- package/lib/types/client/sessions/service.d.ts +53 -150
- package/lib/types/client/sessions/service.js +267 -264
- package/lib/types/client/sessions/session.d.ts +24 -35
- package/lib/types/client/sessions/session.js +131 -73
- package/lib/types/commands.d.ts +6 -4
- package/lib/types/commands.js +82 -40
- package/lib/types/control.d.ts +2 -4
- package/lib/types/control.js +2 -84
- package/lib/types/history.js +1 -1
- package/lib/types/index.d.ts +34 -7
- package/lib/types/index.js +118 -14
- package/lib/types/list.js +24 -15
- package/lib/types/skill-catalog.js +60 -50
- package/lib/types/types.d.ts +40 -40
- package/lib/types/types.js +0 -1
- package/package.json +71 -76
- package/lib/types/client/sessions/queue-mirror.d.ts +0 -26
- package/lib/types/client/sessions/queue-mirror.js +0 -61
|
@@ -3,7 +3,7 @@ import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-atta
|
|
|
3
3
|
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client';
|
|
4
4
|
import type { MessageId } from '@deepseek-ai/dsh-llm/brand';
|
|
5
5
|
import { SessionSeq, type SessionId } from '@deepseek-ai/dsh-session/types';
|
|
6
|
-
import type { PromptContentPart, QueueAction,
|
|
6
|
+
import type { PromptContentPart, QueueAction, SessionRequestId } from '../../types.ts';
|
|
7
7
|
import type { BeginSubmissionInput, SessionFace, SubmissionHandle } from '../contract/session.ts';
|
|
8
8
|
import type { SessionSnapshot } from '../contract/snapshot.ts';
|
|
9
9
|
import { MutableSessionEventSource } from '../contract/events.ts';
|
|
@@ -18,15 +18,12 @@ export declare const JUMP_PAGE_MESSAGES = 200;
|
|
|
18
18
|
export interface SessionOptions {
|
|
19
19
|
/** Catalog-discovered address selecting non-activating subagent transport. */
|
|
20
20
|
address?: SubagentAddress;
|
|
21
|
-
/** Whether the exact direct parent Agent
|
|
21
|
+
/** Whether the exact direct parent Agent is available in Host summaries; absent until known. */
|
|
22
22
|
parentAvailable?: boolean;
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* frame. Acceptance is the flip point because it proves the user message
|
|
28
|
-
* is in the host log; a rejected first prompt keeps the session blank
|
|
29
|
-
* (hidden, still reusable by connectWorkspace).
|
|
24
|
+
* Publish each accepted prompt to the Manager, including after this Session
|
|
25
|
+
* object is replaced. Acceptance converts display state, but does not
|
|
26
|
+
* establish that a turn started or reached durable history.
|
|
30
27
|
*/
|
|
31
28
|
onEngaged?(session: Session): void;
|
|
32
29
|
/**
|
|
@@ -59,8 +56,8 @@ export declare class Session implements SessionFace {
|
|
|
59
56
|
private jumpTargetSeq;
|
|
60
57
|
/** The running jump loop's completion, shared by retargeting callers. */
|
|
61
58
|
private jumpPromise;
|
|
62
|
-
|
|
63
|
-
private readonly
|
|
59
|
+
private pendingHistory;
|
|
60
|
+
private readonly stopObservingInbox;
|
|
64
61
|
private readonly assistantStream;
|
|
65
62
|
private running;
|
|
66
63
|
private address;
|
|
@@ -73,7 +70,7 @@ export declare class Session implements SessionFace {
|
|
|
73
70
|
private promptAttempted;
|
|
74
71
|
/** A first accepted prompt stays in the engaging phase until its turn is observable. */
|
|
75
72
|
private firstPromptPendingTurn;
|
|
76
|
-
/**
|
|
73
|
+
/** New Session display state; unknown bare sessions begin conservatively blank. */
|
|
77
74
|
private blankBit;
|
|
78
75
|
private removed;
|
|
79
76
|
private promptError;
|
|
@@ -81,7 +78,7 @@ export declare class Session implements SessionFace {
|
|
|
81
78
|
/** Local submission echoes, insertion-ordered (see SessionSnapshot.pendingSubmissions). */
|
|
82
79
|
private pendingSubmissions;
|
|
83
80
|
/** Per-echo settlement state; `retiring` latches the first observation so a
|
|
84
|
-
*
|
|
81
|
+
* Inbox projection and its durable event cannot both retire one echo. */
|
|
85
82
|
private readonly submissionSettlements;
|
|
86
83
|
/** Owns the addressed page/follow lifecycle while this Session is open. */
|
|
87
84
|
private events;
|
|
@@ -89,8 +86,9 @@ export declare class Session implements SessionFace {
|
|
|
89
86
|
* Per-session projection value store (push model; see the session-projection
|
|
90
87
|
* subsystem page, docs/subsystems/session-projection.md): finished whole
|
|
91
88
|
* values computed on the Host, seeded by the tail page's
|
|
92
|
-
* projections block and updated by Session Controller control frames
|
|
93
|
-
*
|
|
89
|
+
* projections block and updated by Session Controller control frames;
|
|
90
|
+
* Host-sequenced writes merge under higher-seq-wins and cached list blocks
|
|
91
|
+
* yield to them (projection-store.ts). Keys are read via `projections.faceOf(key)`
|
|
94
92
|
* (the useProjection resolution face); the conversation snapshot never
|
|
95
93
|
* carries projection values, and no client-side domain folding exists.
|
|
96
94
|
* Manager-owned when constructed through SessionManager (frames route and
|
|
@@ -199,7 +197,7 @@ export declare class Session implements SessionFace {
|
|
|
199
197
|
/** Jump loader: page backwards until the window covers seq (see ISession.loadThrough). */
|
|
200
198
|
loadThrough(seq: SessionSeq): Promise<void>;
|
|
201
199
|
/** Rebuild an opened history source after address replacement.
|
|
202
|
-
* Invalidates any in-flight open first;
|
|
200
|
+
* Invalidates any in-flight open first; projection state belongs to the independently
|
|
203
201
|
* reconnecting control stream and remains untouched. */
|
|
204
202
|
resync(): Promise<void>;
|
|
205
203
|
/**
|
|
@@ -213,18 +211,6 @@ export declare class Session implements SessionFace {
|
|
|
213
211
|
* @returns the cached reference (stable until the next flush).
|
|
214
212
|
*/
|
|
215
213
|
getSnapshot(): SessionSnapshot;
|
|
216
|
-
/**
|
|
217
|
-
* Replace every transient control value for this Session from one stream baseline.
|
|
218
|
-
* @param queue - complete pending queue for this Session.
|
|
219
|
-
*/
|
|
220
|
-
replaceControl(queue: readonly SessionQueuedItem[]): void;
|
|
221
|
-
/**
|
|
222
|
-
* Apply one Session-addressed live control update.
|
|
223
|
-
* @param frame - queue replacement addressed to this Session.
|
|
224
|
-
*/
|
|
225
|
-
handleControlFrame(frame: Extract<SessionControlFrame, {
|
|
226
|
-
type: 'queue';
|
|
227
|
-
}>): void;
|
|
228
214
|
/**
|
|
229
215
|
* Running-bit relay from the host stream (list entry and snapshot stay consistent).
|
|
230
216
|
* @param running - the new running state.
|
|
@@ -243,11 +229,12 @@ export declare class Session implements SessionFace {
|
|
|
243
229
|
*/
|
|
244
230
|
handleSubagentParentAvailable(available: boolean): void;
|
|
245
231
|
/**
|
|
246
|
-
*
|
|
247
|
-
* `
|
|
248
|
-
* running
|
|
249
|
-
*
|
|
250
|
-
*
|
|
232
|
+
* Apply the Manager's effective display blank, further reconciled with the
|
|
233
|
+
* current `sessionListMetadata` projection. Local send attempts and current
|
|
234
|
+
* running state prevent re-blanking; an earlier false summary alone does not.
|
|
235
|
+
* The Manager retains acceptance and earlier running observations across
|
|
236
|
+
* Session-object replacement.
|
|
237
|
+
* @param blank - New Session display state after Manager reconciliation.
|
|
251
238
|
*/
|
|
252
239
|
handleBlank(blank: boolean): void;
|
|
253
240
|
/** `api-session/removed` relay: flag the snapshot while retaining the resident instance. */
|
|
@@ -273,10 +260,12 @@ export declare class Session implements SessionFace {
|
|
|
273
260
|
private prependWindow;
|
|
274
261
|
/** Append one stream-validated live event. */
|
|
275
262
|
private appendLive;
|
|
276
|
-
/**
|
|
263
|
+
/** Observe durable acceptance even when insertion and claim share one projection notification. */
|
|
277
264
|
private observeSubmissionEvent;
|
|
278
|
-
|
|
279
|
-
private
|
|
265
|
+
private observeSteeringInsertions;
|
|
266
|
+
private observeSubmissionMessage;
|
|
267
|
+
/** Retire non-steering echoes when the Inbox accepts their queue occurrences. */
|
|
268
|
+
private observeSubmissionInbox;
|
|
280
269
|
/**
|
|
281
270
|
* Latch one observed settlement and remove the echo an animation frame
|
|
282
271
|
* later. The delay keeps the echo in the snapshot until the frame in which
|
|
@@ -8,7 +8,6 @@ import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client';
|
|
|
8
8
|
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol';
|
|
9
9
|
import { ProjectionValueStore } from "./projection-store.js";
|
|
10
10
|
import { resolvedClientTimeZone } from "../time-zone.js";
|
|
11
|
-
import { SessionQueueMirror } from "./queue-mirror.js";
|
|
12
11
|
import { ClientAssistantStream, } from "./assistant-stream.js";
|
|
13
12
|
function projectionsBaseline(value) {
|
|
14
13
|
return {
|
|
@@ -44,8 +43,8 @@ export class Session {
|
|
|
44
43
|
jumpTargetSeq = null;
|
|
45
44
|
/** The running jump loop's completion, shared by retargeting callers. */
|
|
46
45
|
jumpPromise = null;
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
pendingHistory = null;
|
|
47
|
+
stopObservingInbox;
|
|
49
48
|
assistantStream = new ClientAssistantStream();
|
|
50
49
|
running = false;
|
|
51
50
|
address;
|
|
@@ -58,7 +57,7 @@ export class Session {
|
|
|
58
57
|
promptAttempted = false;
|
|
59
58
|
/** A first accepted prompt stays in the engaging phase until its turn is observable. */
|
|
60
59
|
firstPromptPendingTurn = false;
|
|
61
|
-
/**
|
|
60
|
+
/** New Session display state; unknown bare sessions begin conservatively blank. */
|
|
62
61
|
blankBit = true;
|
|
63
62
|
removed = false;
|
|
64
63
|
promptError = null;
|
|
@@ -66,7 +65,7 @@ export class Session {
|
|
|
66
65
|
/** Local submission echoes, insertion-ordered (see SessionSnapshot.pendingSubmissions). */
|
|
67
66
|
pendingSubmissions = [];
|
|
68
67
|
/** Per-echo settlement state; `retiring` latches the first observation so a
|
|
69
|
-
*
|
|
68
|
+
* Inbox projection and its durable event cannot both retire one echo. */
|
|
70
69
|
submissionSettlements = new Map();
|
|
71
70
|
/** Owns the addressed page/follow lifecycle while this Session is open. */
|
|
72
71
|
events;
|
|
@@ -74,8 +73,9 @@ export class Session {
|
|
|
74
73
|
* Per-session projection value store (push model; see the session-projection
|
|
75
74
|
* subsystem page, docs/subsystems/session-projection.md): finished whole
|
|
76
75
|
* values computed on the Host, seeded by the tail page's
|
|
77
|
-
* projections block and updated by Session Controller control frames
|
|
78
|
-
*
|
|
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)`
|
|
79
79
|
* (the useProjection resolution face); the conversation snapshot never
|
|
80
80
|
* carries projection values, and no client-side domain folding exists.
|
|
81
81
|
* Manager-owned when constructed through SessionManager (frames route and
|
|
@@ -111,6 +111,9 @@ export class Session {
|
|
|
111
111
|
this.snapshotCache = this.buildSnapshot();
|
|
112
112
|
});
|
|
113
113
|
this.snapshotCache = this.buildSnapshot();
|
|
114
|
+
this.stopObservingInbox = this.projections.faceOf('inbox').subscribe(() => {
|
|
115
|
+
this.observeSubmissionInbox();
|
|
116
|
+
});
|
|
114
117
|
}
|
|
115
118
|
/**
|
|
116
119
|
* Bind the Agent-scoped context minted by ClientSessions (single write;
|
|
@@ -139,16 +142,15 @@ export class Session {
|
|
|
139
142
|
*/
|
|
140
143
|
beginSubmission(input) {
|
|
141
144
|
const requestId = randomUUID();
|
|
145
|
+
const placement = this.running ? input.mode === 'steer' ? 'steering' : 'queued' : 'transcript';
|
|
142
146
|
this.pendingSubmissions = [...this.pendingSubmissions, {
|
|
143
147
|
requestId,
|
|
144
|
-
placement
|
|
145
|
-
? input.mode === 'steer' ? 'steering' : 'queued'
|
|
146
|
-
: 'transcript',
|
|
148
|
+
placement,
|
|
147
149
|
time: Date.now(),
|
|
148
150
|
text: input.text,
|
|
149
151
|
attachments: input.attachments,
|
|
150
152
|
}];
|
|
151
|
-
this.submissionSettlements.set(requestId, { onRetire: input.onRetire, retiring: false });
|
|
153
|
+
this.submissionSettlements.set(requestId, { placement, onRetire: input.onRetire, retiring: false });
|
|
152
154
|
// The blank → engaging edge flips here, ahead of prompt(): the composer
|
|
153
155
|
// docks and the echo renders on the click's own frame.
|
|
154
156
|
this.promptAttempted = true;
|
|
@@ -212,19 +214,12 @@ export class Session {
|
|
|
212
214
|
this.notifier.markDirty();
|
|
213
215
|
return result;
|
|
214
216
|
}
|
|
215
|
-
//
|
|
216
|
-
// conversation's first turn on the host (the host criterion — a logged
|
|
217
|
-
// turn/start — is fact, not optimism; standalone command and projection
|
|
218
|
-
// events never flip it), while a rejected first prompt must keep the
|
|
219
|
-
// session blank — the client-side blank mirror only ever lowers, so
|
|
220
|
-
// flipping early on a failure would surface the session forever and
|
|
221
|
-
// strip its connectWorkspace reuse eligibility against the host's
|
|
222
|
-
// authority.
|
|
217
|
+
// Rejection must leave a first prompt blank and eligible for workspace reuse.
|
|
223
218
|
if (this.blankBit) {
|
|
224
219
|
this.blankBit = false;
|
|
225
|
-
this.options.onEngaged?.(this);
|
|
226
220
|
this.notifier.markDirty();
|
|
227
221
|
}
|
|
222
|
+
this.options.onEngaged?.(this);
|
|
228
223
|
return result;
|
|
229
224
|
}
|
|
230
225
|
/**
|
|
@@ -346,6 +341,15 @@ export class Session {
|
|
|
346
341
|
// loop starts here.
|
|
347
342
|
if (this.loadingOlder)
|
|
348
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;
|
|
349
353
|
this.jumpTargetSeq = seq;
|
|
350
354
|
this.loadingOlder = true;
|
|
351
355
|
this.notifier.markDirty();
|
|
@@ -355,17 +359,14 @@ export class Session {
|
|
|
355
359
|
const generation = this.openGeneration;
|
|
356
360
|
this.jumpPromise = (async () => {
|
|
357
361
|
try {
|
|
358
|
-
while (
|
|
362
|
+
while (pending.hasMore && this.jumpTargetSeq !== null && pending.beforeSeq > this.jumpTargetSeq) {
|
|
359
363
|
if (generation !== this.openGeneration)
|
|
360
364
|
return;
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
return;
|
|
364
|
-
const before = this.baseSeq;
|
|
365
|
-
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 });
|
|
366
367
|
// No-progress guard: an empty or dropped page that still claims more
|
|
367
368
|
// history must end the loop, not spin it.
|
|
368
|
-
if (
|
|
369
|
+
if (pending.beforeSeq >= before)
|
|
369
370
|
return;
|
|
370
371
|
}
|
|
371
372
|
}
|
|
@@ -377,14 +378,18 @@ export class Session {
|
|
|
377
378
|
finally {
|
|
378
379
|
this.jumpTargetSeq = null;
|
|
379
380
|
this.jumpPromise = null;
|
|
381
|
+
this.pendingHistory = null;
|
|
380
382
|
this.loadingOlder = false;
|
|
383
|
+
if (generation === this.openGeneration && pending.pages.length > 0) {
|
|
384
|
+
this.prependWindow(pending.pages.reverse().flat(), pending.hasMore);
|
|
385
|
+
}
|
|
381
386
|
this.notifier.markDirty();
|
|
382
387
|
}
|
|
383
388
|
})();
|
|
384
389
|
return this.jumpPromise;
|
|
385
390
|
}
|
|
386
391
|
/** Rebuild an opened history source after address replacement.
|
|
387
|
-
* Invalidates any in-flight open first;
|
|
392
|
+
* Invalidates any in-flight open first; projection state belongs to the independently
|
|
388
393
|
* reconnecting control stream and remains untouched. */
|
|
389
394
|
async resync() {
|
|
390
395
|
if (this.openState === 'cold')
|
|
@@ -418,31 +423,12 @@ export class Session {
|
|
|
418
423
|
return this.snapshotCache;
|
|
419
424
|
}
|
|
420
425
|
// ---- Manager-only entry points (@internal; never called by the UI) ----
|
|
421
|
-
/**
|
|
422
|
-
* Replace every transient control value for this Session from one stream baseline.
|
|
423
|
-
* @param queue - complete pending queue for this Session.
|
|
424
|
-
*/
|
|
425
|
-
replaceControl(queue) {
|
|
426
|
-
this.queueMirror.replace(queue);
|
|
427
|
-
this.observeSubmissionQueue(queue);
|
|
428
|
-
this.notifier.markDirty();
|
|
429
|
-
}
|
|
430
|
-
/**
|
|
431
|
-
* Apply one Session-addressed live control update.
|
|
432
|
-
* @param frame - queue replacement addressed to this Session.
|
|
433
|
-
*/
|
|
434
|
-
handleControlFrame(frame) {
|
|
435
|
-
this.queueMirror.replace(frame.items);
|
|
436
|
-
this.observeSubmissionQueue(frame.items);
|
|
437
|
-
this.notifier.markDirty();
|
|
438
|
-
}
|
|
439
426
|
/**
|
|
440
427
|
* Running-bit relay from the host stream (list entry and snapshot stay consistent).
|
|
441
428
|
* @param running - the new running state.
|
|
442
429
|
*/
|
|
443
430
|
handleRunning(running) {
|
|
444
|
-
//
|
|
445
|
-
// running:true proves another side's first message landed.
|
|
431
|
+
// Running converts display state without establishing durable turn history.
|
|
446
432
|
if (running && this.blankBit) {
|
|
447
433
|
this.blankBit = false;
|
|
448
434
|
this.notifier.markDirty();
|
|
@@ -482,13 +468,15 @@ export class Session {
|
|
|
482
468
|
this.notifier.markDirty();
|
|
483
469
|
}
|
|
484
470
|
/**
|
|
485
|
-
*
|
|
486
|
-
* `
|
|
487
|
-
* running
|
|
488
|
-
*
|
|
489
|
-
*
|
|
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.
|
|
490
477
|
*/
|
|
491
478
|
handleBlank(blank) {
|
|
479
|
+
blank = blank && this.projections.values().sessionListMetadata?.blank !== false;
|
|
492
480
|
if (blank === this.blankBit)
|
|
493
481
|
return;
|
|
494
482
|
if (blank && (this.promptAttempted || this.running))
|
|
@@ -514,6 +502,7 @@ export class Session {
|
|
|
514
502
|
* @returns when the Remote iterator has completed teardown.
|
|
515
503
|
*/
|
|
516
504
|
async dispose() {
|
|
505
|
+
this.stopObservingInbox();
|
|
517
506
|
// Unsettled echoes retire as failed so their owners can restore or
|
|
518
507
|
// release browser resources; echoes already scheduled as observed keep
|
|
519
508
|
// that settlement.
|
|
@@ -586,13 +575,31 @@ export class Session {
|
|
|
586
575
|
const visible = this.assistantStream.replace(entries, assistantStream);
|
|
587
576
|
this.baseSeq = SessionLogOffset(entries[0]?.event.seq ?? 0);
|
|
588
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
|
+
}
|
|
589
583
|
if (visible.some(entry => entry.event.type === 'turn/start'))
|
|
590
584
|
this.firstPromptPendingTurn = false;
|
|
591
585
|
if (projections !== undefined)
|
|
592
586
|
this.projections.seed(projections);
|
|
593
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
|
+
}
|
|
594
597
|
for (const entry of visible)
|
|
595
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
|
+
}
|
|
596
603
|
this.notifier.markDirty();
|
|
597
604
|
}
|
|
598
605
|
publishAssistantEntry(result) {
|
|
@@ -625,6 +632,13 @@ export class Session {
|
|
|
625
632
|
}
|
|
626
633
|
/** Prepend one stream-validated history page. */
|
|
627
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
|
+
}
|
|
628
642
|
this.baseSeq = entries[0] === undefined ? this.baseSeq : SessionLogOffset(entries[0].event.seq);
|
|
629
643
|
this.hasMore = hasMore;
|
|
630
644
|
this.eventSource.prepend(entries, hasMore);
|
|
@@ -635,36 +649,78 @@ export class Session {
|
|
|
635
649
|
const awaitingFirstTurn = this.firstPromptPendingTurn;
|
|
636
650
|
if (event.type === 'turn/start')
|
|
637
651
|
this.firstPromptPendingTurn = false;
|
|
638
|
-
const queueChanged = this.queueMirror.acceptDurable(event);
|
|
639
652
|
this.eventSource.append(entry);
|
|
640
653
|
// After the feed append: the conversation assembly's animation frame is
|
|
641
654
|
// registered by the feed subscribers above, so the echo-retirement frame
|
|
642
655
|
// scheduled here always runs after the durable node became renderable.
|
|
643
656
|
this.observeSubmissionEvent(event);
|
|
644
|
-
return
|
|
657
|
+
return awaitingFirstTurn !== this.firstPromptPendingTurn;
|
|
645
658
|
}
|
|
646
|
-
/**
|
|
659
|
+
/** Observe durable acceptance even when insertion and claim share one projection notification. */
|
|
647
660
|
observeSubmissionEvent(event) {
|
|
648
|
-
if (this.submissionSettlements.size === 0
|
|
661
|
+
if (this.submissionSettlements.size === 0)
|
|
662
|
+
return;
|
|
663
|
+
if (event.type === 'agent/inbox/spliced') {
|
|
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);
|
|
690
|
+
}
|
|
649
691
|
return;
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
692
|
+
}
|
|
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))
|
|
656
710
|
return;
|
|
657
|
-
this.
|
|
711
|
+
if (!admitted && this.submissionSettlements.get(source.rpcId)?.placement === 'steering')
|
|
712
|
+
return;
|
|
713
|
+
this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(message.content));
|
|
658
714
|
}
|
|
659
|
-
/** Retire echoes
|
|
660
|
-
|
|
715
|
+
/** Retire non-steering echoes when the Inbox accepts their queue occurrences. */
|
|
716
|
+
observeSubmissionInbox() {
|
|
661
717
|
if (this.submissionSettlements.size === 0)
|
|
662
718
|
return;
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
719
|
+
const inbox = this.projections.get('inbox');
|
|
720
|
+
if (inbox === undefined)
|
|
721
|
+
return;
|
|
722
|
+
for (const message of [...inbox['next-turn'], ...inbox['next-step']])
|
|
723
|
+
this.observeSubmissionMessage(message, false);
|
|
668
724
|
}
|
|
669
725
|
/**
|
|
670
726
|
* Latch one observed settlement and remove the echo an animation frame
|
|
@@ -713,15 +769,17 @@ export class Session {
|
|
|
713
769
|
this.notifier.markDirty();
|
|
714
770
|
}
|
|
715
771
|
buildSnapshot() {
|
|
772
|
+
const identity = this.projections.values().subagent;
|
|
716
773
|
return {
|
|
717
774
|
sessionId: this.sessionId,
|
|
718
|
-
queue: this.queueMirror.snapshot(),
|
|
719
775
|
pendingSubmissions: this.pendingSubmissions,
|
|
720
776
|
running: this.running,
|
|
721
777
|
subagent: this.address === undefined
|
|
722
778
|
? null
|
|
723
779
|
: {
|
|
724
|
-
address: this.address
|
|
780
|
+
address: this.address.mode === 'unknown' && identity != null
|
|
781
|
+
? { ...this.address, mode: identity.mode }
|
|
782
|
+
: this.address,
|
|
725
783
|
...(this.parentAvailable === undefined ? {} : { parentAvailable: this.parentAvailable }),
|
|
726
784
|
},
|
|
727
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>;
|
|
@@ -50,11 +52,11 @@ export declare class SessionCommandController {
|
|
|
50
52
|
*/
|
|
51
53
|
attachment(request: SessionAttachmentRequest): Promise<SessionAttachmentValue>;
|
|
52
54
|
/**
|
|
53
|
-
* Mutate one
|
|
55
|
+
* Mutate one pending Inbox occurrence, restoring an ordinary cold Agent when needed.
|
|
54
56
|
* @param request - Session, queue item, and requested mutation.
|
|
55
57
|
* @returns acknowledgement that the queue mutation was applied.
|
|
56
58
|
*/
|
|
57
|
-
updateQueue(request: SessionUpdateQueueRequest): SessionUpdateQueueValue
|
|
59
|
+
updateQueue(request: SessionUpdateQueueRequest): Promise<SessionUpdateQueueValue>;
|
|
58
60
|
/**
|
|
59
61
|
* Cancel one live ordinary Agent while retaining pending inbox work.
|
|
60
62
|
* @param request - Session whose active Agent turn is cancelled.
|