@itookit/dsht 0.3.4 → 0.3.7
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 +2 -2
- package/README.zh.md +2 -2
- package/dist/controller/controller.d.ts +107 -0
- package/dist/controller/controller.js +114 -1
- package/dist/cost/controller.d.ts +5 -0
- package/dist/cost/controller.js +2 -1
- package/dist/cost/scanner.d.ts +4 -2
- package/dist/cost/scanner.js +6 -3
- package/dist/session/controller.d.ts +141 -1
- package/dist/session/controller.js +362 -29
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +1 -0
- package/dist/session/info.d.ts +262 -0
- package/dist/session/info.js +326 -0
- package/dist/session/transcript.d.ts +37 -1
- package/dist/session/transcript.js +73 -0
- package/dist/state.d.ts +3 -2
- package/dist/state.js +2 -2
- package/dist/ui/app.js +183 -173
- package/dist/ui/chat/status.js +4 -4
- package/dist/ui/dialogs/index.d.ts +3 -6
- package/dsht-m.png +0 -0
- package/package.json +1 -1
- package/dist/ui/input/history.d.ts +0 -19
- package/dist/ui/input/history.js +0 -43
|
@@ -7,11 +7,14 @@ import { saveTranscriptHtml } from "./export-html.js";
|
|
|
7
7
|
import { releaseHistoryLayout } from "./history.js";
|
|
8
8
|
import { resolveTarget, sessionLabel } from "./navigation.js";
|
|
9
9
|
import { fileReferences } from "./references.js";
|
|
10
|
-
import {
|
|
10
|
+
import { PromptCache } from "./info.js";
|
|
11
|
+
import { recordPrompts, Transcript, toolLine } from "./transcript.js";
|
|
11
12
|
/** Built-in preset identifiers and the labels the web session header shows. */
|
|
12
13
|
const BUILT_IN_MODES = new Map([['standard', 'Standard mode'], ['ptc', 'PTC mode'], ['minimal', 'Minimal mode'], ['cordis', 'Creator mode']]);
|
|
13
14
|
/** Bounded match count for one history search. */
|
|
14
15
|
const SEARCH_MATCH_LIMIT = 200;
|
|
16
|
+
/** Bounded page count for the prompt backfill that runs once per opened session. */
|
|
17
|
+
const PROMPT_BACKFILL_PAGES = 200;
|
|
15
18
|
/** Owns the selected session: its follow stream, transcript, history window and interactions. */
|
|
16
19
|
export class SessionController {
|
|
17
20
|
store;
|
|
@@ -20,7 +23,13 @@ export class SessionController {
|
|
|
20
23
|
historyLimits;
|
|
21
24
|
follow;
|
|
22
25
|
interactions = new Map();
|
|
23
|
-
|
|
26
|
+
/** Cancels the background prompt backfill of the previous selection. */
|
|
27
|
+
promptBackfill;
|
|
28
|
+
/** Prompts of sessions this process has already read, so re-opening one costs no page request. */
|
|
29
|
+
promptCache = new PromptCache();
|
|
30
|
+
/** Prompt index, composer and reading view of the selected session; the instance `State.session` exposes. */
|
|
31
|
+
get info() { return this.store.state.session; }
|
|
32
|
+
get prompts() { return this.info.prompts; }
|
|
24
33
|
stoppingSession;
|
|
25
34
|
interruptTask;
|
|
26
35
|
admission;
|
|
@@ -61,7 +70,7 @@ export class SessionController {
|
|
|
61
70
|
get workingSince() {
|
|
62
71
|
if (!this.running || !this.store.state.sessionId)
|
|
63
72
|
return undefined;
|
|
64
|
-
return this.
|
|
73
|
+
return this.info.record.activeTurnStartedAt ?? this.connection.observedAt(this.store.state.sessionId);
|
|
65
74
|
}
|
|
66
75
|
/** Present only sessions explicitly accounted to the selected workspace. */
|
|
67
76
|
get visibleSessions() {
|
|
@@ -75,13 +84,13 @@ export class SessionController {
|
|
|
75
84
|
/** Whether a turn, cancellation or prompt admission is still in flight. */
|
|
76
85
|
get active() { return this.interruptTask !== undefined || this.running || this.admission !== undefined; }
|
|
77
86
|
/** Whether reading protects the loaded window, suspending history reclamation. */
|
|
78
|
-
get pinned() { return this.
|
|
87
|
+
get pinned() { return this.info.view.pinned; }
|
|
79
88
|
/** Drop generation-scoped state before a new connection generation begins. */
|
|
80
89
|
beginGeneration() { this.stoppingSession = undefined; }
|
|
81
90
|
/** Invalidate in-flight work and drop transient interactions when a generation ends. */
|
|
82
91
|
endGeneration() {
|
|
83
92
|
this.store.bumpSelection();
|
|
84
|
-
this.
|
|
93
|
+
this.info.record.ready = false;
|
|
85
94
|
this.interactions.clear();
|
|
86
95
|
}
|
|
87
96
|
/** Wait for an in-flight cancellation so shutdown leaves nothing running. */
|
|
@@ -181,7 +190,7 @@ export class SessionController {
|
|
|
181
190
|
* @param pinned - Whether the main transcript is actively being read away from its tail.
|
|
182
191
|
*/
|
|
183
192
|
pinHistory(pinned) {
|
|
184
|
-
this.
|
|
193
|
+
this.info.view.pinned = pinned;
|
|
185
194
|
if (!pinned && this.reclaimHistory())
|
|
186
195
|
this.store.update({});
|
|
187
196
|
}
|
|
@@ -254,7 +263,7 @@ export class SessionController {
|
|
|
254
263
|
this.follow?.cancel();
|
|
255
264
|
this.follow = undefined;
|
|
256
265
|
this.releaseTranscript();
|
|
257
|
-
this.store.update({ workspaceId, sessionId: undefined, showAllSessions: false,
|
|
266
|
+
this.store.update({ workspaceId, sessionId: undefined, showAllSessions: false, screen: 'sessions' });
|
|
258
267
|
}
|
|
259
268
|
/** Open a workspace picker, or resolve a workspace by ID, exact title/path, or unique ID prefix.
|
|
260
269
|
* @param query - Workspace target, if any.
|
|
@@ -313,13 +322,13 @@ export class SessionController {
|
|
|
313
322
|
const selection = this.store.selection();
|
|
314
323
|
this.follow?.cancel();
|
|
315
324
|
this.follow = undefined;
|
|
316
|
-
this.
|
|
317
|
-
const transcript =
|
|
325
|
+
this.info.reset(sessionId);
|
|
326
|
+
const transcript = this.info.record;
|
|
318
327
|
const workspace = this.store.state.workspaces.find(item => array(item.sessionIds).includes(sessionId));
|
|
319
328
|
const workspaceId = workspace ? string(workspace.workspaceId)
|
|
320
329
|
: this.store.state.sessions.some(item => item.sessionId === sessionId) ? undefined : this.store.state.workspaceId;
|
|
321
330
|
this.connection.observe(sessionId);
|
|
322
|
-
this.store.update({ sessionId, workspaceId, showAllSessions: false,
|
|
331
|
+
this.store.update({ sessionId, workspaceId, showAllSessions: false, screen: 'chat', status: 'Loading session…' });
|
|
323
332
|
this.follow = this.host.require().subscribe('session/follow', {
|
|
324
333
|
request: { address: { kind: 'session', sessionId }, maxMessages: 80, assistantStream: true },
|
|
325
334
|
}, {
|
|
@@ -328,11 +337,12 @@ export class SessionController {
|
|
|
328
337
|
return;
|
|
329
338
|
try {
|
|
330
339
|
transcript.accept(value);
|
|
340
|
+
this.prompts.fold(transcript.promptsSince(this.prompts.through));
|
|
331
341
|
this.reclaimHistory();
|
|
332
342
|
const frame = object(value);
|
|
333
343
|
if (frame.type === 'snapshot')
|
|
334
344
|
this.connection.telemetryView().snapshot(sessionId, frame.projections);
|
|
335
|
-
this.store.update({
|
|
345
|
+
this.store.update({ status: this.stoppingSession === sessionId ? this.store.state.status : transcript.hasLiveContent ? 'Responding…' : 'Connected' });
|
|
336
346
|
}
|
|
337
347
|
catch (error) {
|
|
338
348
|
this.connection.fail(new Error(errorText(error)));
|
|
@@ -345,16 +355,17 @@ export class SessionController {
|
|
|
345
355
|
this.store.update({ status: 'Session disconnected', error: errorText(error ?? 'Session stream ended') });
|
|
346
356
|
},
|
|
347
357
|
});
|
|
358
|
+
this.backfillPrompts(sessionId, selection);
|
|
348
359
|
}
|
|
349
360
|
/** Wait for the selected follow snapshot, failing on disconnect or cancellation.
|
|
350
361
|
* @param signal - Cancels waiting without closing the session.
|
|
351
362
|
*/
|
|
352
363
|
async waitForHistory(signal) {
|
|
353
|
-
const transcript = this.
|
|
364
|
+
const transcript = this.info.record;
|
|
354
365
|
const deadline = Date.now() + this.host.require().timeoutMs;
|
|
355
366
|
while (!transcript.ready) {
|
|
356
367
|
signal.throwIfAborted();
|
|
357
|
-
if (!this.store.state.online || this.
|
|
368
|
+
if (!this.store.state.online || this.info.record !== transcript)
|
|
358
369
|
throw new Error('Session changed while loading history');
|
|
359
370
|
if (Date.now() >= deadline)
|
|
360
371
|
throw new Error('Session snapshot timed out');
|
|
@@ -397,7 +408,7 @@ export class SessionController {
|
|
|
397
408
|
* @returns The host's successful command result text.
|
|
398
409
|
*/
|
|
399
410
|
async command(line, signal) {
|
|
400
|
-
if (!this.
|
|
411
|
+
if (!this.info.record.ready)
|
|
401
412
|
throw new Error('Wait for the session snapshot before running commands');
|
|
402
413
|
const execution = await this.host.require().call('commands/execute', {
|
|
403
414
|
agentId: this.sessionId, line, submittedAttachments: [],
|
|
@@ -410,6 +421,11 @@ export class SessionController {
|
|
|
410
421
|
}
|
|
411
422
|
if (result.kind === 'error')
|
|
412
423
|
throw new Error(string(result.text));
|
|
424
|
+
// Compaction rewrites the host log, so a cached prompt list for this session may describe
|
|
425
|
+
// records that no longer exist. The live index keeps what the reader already sees; the cache is
|
|
426
|
+
// dropped so the next open reads the rewritten history.
|
|
427
|
+
if (line.trim().split(/\s/, 1)[0] === '/compact')
|
|
428
|
+
this.promptCache.drop(this.sessionId);
|
|
413
429
|
return result.text === undefined ? 'Command completed.' : string(result.text);
|
|
414
430
|
}
|
|
415
431
|
/** Remove one host-owned pending input; an already claimed item reports a host error.
|
|
@@ -432,7 +448,7 @@ export class SessionController {
|
|
|
432
448
|
* @returns Absolute saved filename.
|
|
433
449
|
*/
|
|
434
450
|
async exportHtml(path, signal) {
|
|
435
|
-
return saveTranscriptHtml(this.
|
|
451
|
+
return saveTranscriptHtml(this.info.record, this.sessionId, path, signal);
|
|
436
452
|
}
|
|
437
453
|
/** Admit text once as steering while running, or a new turn while idle; a lost response can leave delivery uncertain.
|
|
438
454
|
* @param text - Composed prompt text.
|
|
@@ -441,7 +457,7 @@ export class SessionController {
|
|
|
441
457
|
if (this.store.state.pending.length)
|
|
442
458
|
throw new Error('Answer the pending question or approval first');
|
|
443
459
|
this.stoppingSession = undefined;
|
|
444
|
-
if (!this.
|
|
460
|
+
if (!this.info.record.ready)
|
|
445
461
|
throw new Error('Wait for the session snapshot before sending');
|
|
446
462
|
const admission = this.host.require().call('session/prompt', { request: {
|
|
447
463
|
sessionId: this.sessionId, requestId: randomUUID(), mode: this.running ? 'steer' : 'queue',
|
|
@@ -493,13 +509,17 @@ export class SessionController {
|
|
|
493
509
|
this.store.update({});
|
|
494
510
|
}
|
|
495
511
|
/** Add a page before the retained window using its fixed opening cut.
|
|
512
|
+
*
|
|
513
|
+
* Reclamation is deliberately not pinned here. Every caller that needs the fetched page to
|
|
514
|
+
* survive is already covered by the view: scrolling back sets a scroll position, `/think` opens a
|
|
515
|
+
* panel, and a recall page folds its prompts into the index before this resolves. A pin set here
|
|
516
|
+
* had no clearing edge, so after one recall page the history budget silently stopped applying for
|
|
517
|
+
* the rest of the session while every following page kept adding records.
|
|
496
518
|
* @param signal - Cancels local paging without interrupting the remote agent.
|
|
497
519
|
* @param transcript - Transcript to extend; defaults to the live one.
|
|
498
520
|
*/
|
|
499
|
-
async older(signal, transcript = this.
|
|
521
|
+
async older(signal, transcript = this.info.record) {
|
|
500
522
|
const selection = this.store.selection();
|
|
501
|
-
if (transcript === this.store.state.transcript)
|
|
502
|
-
this.historyPinned = true;
|
|
503
523
|
if (!transcript.ready || !transcript.hasMore || transcript.beforeSeq === undefined)
|
|
504
524
|
return;
|
|
505
525
|
const result = await this.host.require().call('session/page', { request: {
|
|
@@ -511,13 +531,326 @@ export class SessionController {
|
|
|
511
531
|
transcript.addPage(result);
|
|
512
532
|
this.store.update({});
|
|
513
533
|
}
|
|
534
|
+
/** Recall one step through the session's prompt index; navigation never touches the network.
|
|
535
|
+
* @param direction - Negative for older input, positive for newer input.
|
|
536
|
+
* @param current - Composer content before recall began, restored at the newest position.
|
|
537
|
+
* @returns The recalled prompt, or the unsent draft.
|
|
538
|
+
*/
|
|
539
|
+
recall(direction, current) { return this.prompts.move(direction, current); }
|
|
540
|
+
/** Composer draft, caret and parked draft, as the selected session holds them. */
|
|
541
|
+
get composer() { return this.info.composer; }
|
|
542
|
+
/** Replace the composer text and caret, publishing only when either actually changed. */
|
|
543
|
+
setComposer(draft, cursor = draft.length) {
|
|
544
|
+
const composer = this.info.composer;
|
|
545
|
+
if (composer.draft === draft && composer.cursor === cursor)
|
|
546
|
+
return;
|
|
547
|
+
composer.draft = draft;
|
|
548
|
+
composer.cursor = cursor;
|
|
549
|
+
this.store.update({});
|
|
550
|
+
}
|
|
551
|
+
/** Move the caret without changing the text. */
|
|
552
|
+
setComposerCursor(cursor) {
|
|
553
|
+
const composer = this.info.composer;
|
|
554
|
+
if (composer.cursor === cursor)
|
|
555
|
+
return;
|
|
556
|
+
composer.cursor = cursor;
|
|
557
|
+
this.store.update({});
|
|
558
|
+
}
|
|
559
|
+
/** Move a non-empty draft aside while a dialog owns the keyboard. */
|
|
560
|
+
parkComposer() {
|
|
561
|
+
const composer = this.info.composer;
|
|
562
|
+
if (composer.draft === '')
|
|
563
|
+
return;
|
|
564
|
+
composer.parked = composer.draft;
|
|
565
|
+
composer.draft = '';
|
|
566
|
+
composer.cursor = 0;
|
|
567
|
+
this.store.update({});
|
|
568
|
+
}
|
|
569
|
+
/** Give a parked draft back once no dialog needs the keyboard. */
|
|
570
|
+
restoreComposer() {
|
|
571
|
+
const composer = this.info.composer;
|
|
572
|
+
if (composer.parked === '')
|
|
573
|
+
return;
|
|
574
|
+
composer.draft = composer.parked;
|
|
575
|
+
composer.cursor = composer.parked.length;
|
|
576
|
+
composer.parked = '';
|
|
577
|
+
this.store.update({});
|
|
578
|
+
}
|
|
579
|
+
/** How the selected session's record is being read right now. */
|
|
580
|
+
get view() { return this.info.view; }
|
|
581
|
+
/** Show a detached history window, releasing the one it replaces.
|
|
582
|
+
* @param window - Record to display, or undefined to return to the live transcript.
|
|
583
|
+
*/
|
|
584
|
+
setViewWindow(window) {
|
|
585
|
+
if (this.info.view.window === window)
|
|
586
|
+
return;
|
|
587
|
+
this.info.closeWindow();
|
|
588
|
+
this.info.view.window = window;
|
|
589
|
+
this.store.update({});
|
|
590
|
+
}
|
|
591
|
+
/** Move the reader's position inside the displayed record.
|
|
592
|
+
* @param scroll - Rows scrolled back from the live end.
|
|
593
|
+
*/
|
|
594
|
+
setScroll(scroll) {
|
|
595
|
+
if (this.info.view.scroll === scroll)
|
|
596
|
+
return;
|
|
597
|
+
this.info.view.scroll = scroll;
|
|
598
|
+
this.store.update({});
|
|
599
|
+
}
|
|
600
|
+
/** Replace the set of expanded reasoning blocks, keyed by message sequence.
|
|
601
|
+
* @param folds - Sequences to expand beyond the default fold.
|
|
602
|
+
*/
|
|
603
|
+
setFolds(folds) {
|
|
604
|
+
if (this.info.view.folds === folds)
|
|
605
|
+
return;
|
|
606
|
+
this.info.view.folds = folds;
|
|
607
|
+
this.store.update({});
|
|
608
|
+
}
|
|
609
|
+
/** Set the fold mode of the live attempt's completed reasoning.
|
|
610
|
+
* @param reasoning - `row` to fold, `full` to keep the streamed text.
|
|
611
|
+
*/
|
|
612
|
+
setLiveReasoning(reasoning) {
|
|
613
|
+
if (this.info.view.liveReasoning === reasoning)
|
|
614
|
+
return;
|
|
615
|
+
this.info.view.liveReasoning = reasoning;
|
|
616
|
+
this.store.update({});
|
|
617
|
+
}
|
|
618
|
+
/** Local answer state for the selected session's pending waterfalls. */
|
|
619
|
+
get interaction() { return this.info.interaction; }
|
|
620
|
+
/** Replace the partly collected answers, keyed by the waterfall event id.
|
|
621
|
+
* @param answers - Answers collected so far, by event id.
|
|
622
|
+
*/
|
|
623
|
+
setAnswers(answers) {
|
|
624
|
+
this.info.interaction.answers = answers;
|
|
625
|
+
this.store.update({});
|
|
626
|
+
}
|
|
627
|
+
/** Replace the pending question's option keyboard state.
|
|
628
|
+
* @param option - Highlighted option, toggled labels and free-text mode; undefined clears it.
|
|
629
|
+
*/
|
|
630
|
+
setOption(option) {
|
|
631
|
+
if (this.info.interaction.option === option)
|
|
632
|
+
return;
|
|
633
|
+
this.info.interaction.option = option;
|
|
634
|
+
this.store.update({});
|
|
635
|
+
}
|
|
636
|
+
/** Replace the pending approval's selected row.
|
|
637
|
+
* @param approval - Selected approval row; undefined clears the highlight.
|
|
638
|
+
*/
|
|
639
|
+
setApproval(approval) {
|
|
640
|
+
if (this.info.interaction.approval === approval)
|
|
641
|
+
return;
|
|
642
|
+
this.info.interaction.approval = approval;
|
|
643
|
+
this.store.update({});
|
|
644
|
+
}
|
|
645
|
+
/** Composer-adjacent `@` reference menu state. */
|
|
646
|
+
get reference() { return this.info.reference; }
|
|
647
|
+
/** Highlight one row of the open reference menu.
|
|
648
|
+
* @param index - Row index into the current matches.
|
|
649
|
+
*/
|
|
650
|
+
setReferenceIndex(index) {
|
|
651
|
+
if (this.info.reference.index === index)
|
|
652
|
+
return;
|
|
653
|
+
this.info.reference.index = index;
|
|
654
|
+
this.store.update({});
|
|
655
|
+
}
|
|
656
|
+
/** Remember the draft that dismissed the reference menu, so it does not reopen while it stands.
|
|
657
|
+
* @param draft - Composer text at dismissal, or undefined to allow the menu again.
|
|
658
|
+
*/
|
|
659
|
+
setReferenceDismissed(draft) {
|
|
660
|
+
if (this.info.reference.dismissed === draft)
|
|
661
|
+
return;
|
|
662
|
+
this.info.reference.dismissed = draft;
|
|
663
|
+
this.store.update({});
|
|
664
|
+
}
|
|
665
|
+
/** Panels the selected session has open. */
|
|
666
|
+
get panels() { return this.info.panels; }
|
|
667
|
+
/** Show or hide the reasoning panel.
|
|
668
|
+
* @param open - Whether `/think` is open.
|
|
669
|
+
*/
|
|
670
|
+
openThoughts(open) {
|
|
671
|
+
if (this.info.panels.thoughts === open)
|
|
672
|
+
return;
|
|
673
|
+
this.info.panels.thoughts = open;
|
|
674
|
+
this.store.update({});
|
|
675
|
+
}
|
|
676
|
+
/** Show or hide the pending-input panel.
|
|
677
|
+
* @param open - Whether `/queue` is open.
|
|
678
|
+
*/
|
|
679
|
+
openQueue(open) {
|
|
680
|
+
if (this.info.panels.queue === open)
|
|
681
|
+
return;
|
|
682
|
+
this.info.panels.queue = open;
|
|
683
|
+
this.store.update({});
|
|
684
|
+
}
|
|
685
|
+
/** Show the model dialog at one step, or close it.
|
|
686
|
+
* @param model - Catalog plus the provider or model being inspected; undefined closes the dialog.
|
|
687
|
+
*/
|
|
688
|
+
setModelPanel(model) {
|
|
689
|
+
if (this.info.panels.model === model)
|
|
690
|
+
return;
|
|
691
|
+
this.info.panels.model = model;
|
|
692
|
+
this.store.update({});
|
|
693
|
+
}
|
|
694
|
+
/** Show the history or content-search dialog, or close it.
|
|
695
|
+
* @param history - Query, content-search mode and matches; undefined closes the dialog.
|
|
696
|
+
*/
|
|
697
|
+
setHistoryPanel(history) {
|
|
698
|
+
if (this.info.panels.history === history)
|
|
699
|
+
return;
|
|
700
|
+
this.info.panels.history = history;
|
|
701
|
+
this.store.update({});
|
|
702
|
+
}
|
|
703
|
+
/** Show the host session-search results, or close them.
|
|
704
|
+
* @param search - Query, results and truncation flag; undefined closes the dialog.
|
|
705
|
+
*/
|
|
706
|
+
setSearchPanel(search) {
|
|
707
|
+
if (this.info.panels.search === search)
|
|
708
|
+
return;
|
|
709
|
+
this.info.panels.search = search;
|
|
710
|
+
this.store.update({});
|
|
711
|
+
}
|
|
712
|
+
/** Remember a locally submitted command, which never becomes a durable session record. */
|
|
713
|
+
recordRecall(value) { this.prompts.record(value); }
|
|
714
|
+
/** Leave recall navigation because the composer was edited or replaced. */
|
|
715
|
+
resetRecall() { this.prompts.resetCursor(); }
|
|
716
|
+
/** Whether recall is parked on the oldest prompt it retains in memory. */
|
|
717
|
+
get recallAtOldest() { return this.prompts.atOldest; }
|
|
718
|
+
/** How many prompts the session retains for recall. */
|
|
719
|
+
get recallLength() { return this.prompts.length; }
|
|
720
|
+
/** Whether an older prompt is reachable at all: shed from the window, or still on the host. */
|
|
721
|
+
get recallHasOlder() {
|
|
722
|
+
const transcript = this.info.record;
|
|
723
|
+
const oldest = this.prompts.oldest;
|
|
724
|
+
const refillable = oldest !== undefined && transcript.beforeSeq !== undefined && oldest > transcript.beforeSeq;
|
|
725
|
+
return refillable || (!this.prompts.exhausted && transcript.hasMore);
|
|
726
|
+
}
|
|
727
|
+
/** Recover older prompts from the loaded window before spending a page request.
|
|
728
|
+
*
|
|
729
|
+
* The transcript window can still hold prompts the index's budgets evicted, so a backward step
|
|
730
|
+
* refills from memory first and only then asks the caller to page. That is what keeps recall
|
|
731
|
+
* complete across submissions, where the previous buffer lost them at the eviction boundary.
|
|
732
|
+
* @returns Whether any older prompt was recovered.
|
|
733
|
+
*/
|
|
734
|
+
refillRecall() {
|
|
735
|
+
const oldest = this.prompts.oldest;
|
|
736
|
+
if (oldest === undefined)
|
|
737
|
+
return false;
|
|
738
|
+
return this.prompts.prepend(this.info.record.promptsBefore(oldest)) > 0;
|
|
739
|
+
}
|
|
740
|
+
/** Seed recall from a complete cached entry, so an open that follows a scan costs no request.
|
|
741
|
+
* @param sessionId - Session being opened.
|
|
742
|
+
* @returns Whether the cache covered this session.
|
|
743
|
+
*/
|
|
744
|
+
adoptCachedPrompts(sessionId) {
|
|
745
|
+
const cached = this.promptCache.get(sessionId);
|
|
746
|
+
if (!cached?.complete)
|
|
747
|
+
return false;
|
|
748
|
+
const oldest = this.prompts.oldest;
|
|
749
|
+
const older = oldest === undefined ? cached.prompts : cached.prompts.filter(prompt => prompt.seq < oldest);
|
|
750
|
+
this.prompts.prepend(older);
|
|
751
|
+
// Same budget as the walk it replaces: `settle` may shed the oldest prefix, and `markComplete`
|
|
752
|
+
// then refuses, so the lazy backward step stays available for whatever was shed.
|
|
753
|
+
this.prompts.settle();
|
|
754
|
+
this.prompts.markComplete();
|
|
755
|
+
this.store.update({});
|
|
756
|
+
return true;
|
|
757
|
+
}
|
|
758
|
+
/** Fold one history page the cost scan already read into the prompt cache.
|
|
759
|
+
*
|
|
760
|
+
* The scan reads every session's whole history on connect, so this is where two readers stop
|
|
761
|
+
* paying twice: it hands over pages it already has, and an open that follows reads the cache.
|
|
762
|
+
* @param sessionId - Session the page belongs to.
|
|
763
|
+
* @param records - Raw records of one scanned page.
|
|
764
|
+
*/
|
|
765
|
+
rememberScanPage(sessionId, records) {
|
|
766
|
+
this.promptCache.observe(sessionId, recordPrompts(records), false);
|
|
767
|
+
}
|
|
768
|
+
/** Report that the scanned session's history was read to its beginning.
|
|
769
|
+
* @param sessionId - Session the scan finished.
|
|
770
|
+
*/
|
|
771
|
+
rememberScanDone(sessionId) {
|
|
772
|
+
this.promptCache.observe(sessionId, [], true);
|
|
773
|
+
}
|
|
774
|
+
/** Fold every prompt the host still holds into the recall index, in the background.
|
|
775
|
+
*
|
|
776
|
+
* Session start delivers only the newest window, so without this the arrows could reach older
|
|
777
|
+
* prompts but not show them without paging first. Each page is parsed into a temporary transcript
|
|
778
|
+
* and only its prompts are kept, so the live record, its memory window and the row cache never
|
|
779
|
+
* grow. The walk is bounded and the next selection cancels it; anything past the bound is still
|
|
780
|
+
* reachable through the lazy backward step.
|
|
781
|
+
* @param sessionId - Session being opened.
|
|
782
|
+
* @param selection - Selector generation that must still be current.
|
|
783
|
+
*/
|
|
784
|
+
backfillPrompts(sessionId, selection) {
|
|
785
|
+
this.promptBackfill?.abort();
|
|
786
|
+
const abort = new AbortController();
|
|
787
|
+
this.promptBackfill = abort;
|
|
788
|
+
void (async () => {
|
|
789
|
+
try {
|
|
790
|
+
while (!this.info.record.ready) {
|
|
791
|
+
abort.signal.throwIfAborted();
|
|
792
|
+
await delay(20, undefined, { signal: abort.signal });
|
|
793
|
+
}
|
|
794
|
+
// A cost scan or an earlier open may already have this session's prompts cached.
|
|
795
|
+
if (this.adoptCachedPrompts(sessionId))
|
|
796
|
+
return;
|
|
797
|
+
const throughSeq = this.info.record.readThrough;
|
|
798
|
+
let beforeSeq = this.info.record.beforeSeq;
|
|
799
|
+
let hasMore = this.info.record.hasMore;
|
|
800
|
+
for (let page = 0; hasMore && beforeSeq !== undefined && page < PROMPT_BACKFILL_PAGES; page++) {
|
|
801
|
+
abort.signal.throwIfAborted();
|
|
802
|
+
if (selection !== this.store.selection())
|
|
803
|
+
return;
|
|
804
|
+
const result = object(await this.host.require().call('session/page', { request: {
|
|
805
|
+
address: { kind: 'session', sessionId }, throughSeq, beforeSeq, maxMessages: 80,
|
|
806
|
+
} }, abort.signal));
|
|
807
|
+
abort.signal.throwIfAborted();
|
|
808
|
+
if (selection !== this.store.selection())
|
|
809
|
+
return;
|
|
810
|
+
const temporary = new Transcript();
|
|
811
|
+
try {
|
|
812
|
+
temporary.accept({ type: 'snapshot', cursor: throughSeq, assistantStream: { revision: 0 }, records: result.records, hasMore: result.hasMore });
|
|
813
|
+
const next = temporary.beforeSeq;
|
|
814
|
+
if (temporary.hasMore && (next === undefined || next >= beforeSeq))
|
|
815
|
+
throw new Error('Host history page did not advance');
|
|
816
|
+
this.prompts.prepend(temporary.promptsSince(-1).prompts);
|
|
817
|
+
beforeSeq = next;
|
|
818
|
+
hasMore = temporary.hasMore;
|
|
819
|
+
}
|
|
820
|
+
finally {
|
|
821
|
+
temporary.dispose();
|
|
822
|
+
}
|
|
823
|
+
// A scan that finished while this walk ran already established the same list.
|
|
824
|
+
if (this.adoptCachedPrompts(sessionId))
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
this.prompts.settle();
|
|
828
|
+
// Only a walk that ended because the host said "no more" makes the index exhaustive; one
|
|
829
|
+
// stopped by the page bound leaves the lazy backward step in charge of the rest.
|
|
830
|
+
if (!hasMore)
|
|
831
|
+
this.prompts.markComplete();
|
|
832
|
+
// Cache only what the walk established: advertising a capped or shed list would let a later
|
|
833
|
+
// open skip a fetch it still needs.
|
|
834
|
+
if (!hasMore)
|
|
835
|
+
this.promptCache.put(sessionId, { prompts: this.prompts.durableItems, complete: this.prompts.exhausted });
|
|
836
|
+
this.store.update({});
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
// A cancelled, disconnected or unavailable history leaves the lazy backward step in charge.
|
|
840
|
+
}
|
|
841
|
+
finally {
|
|
842
|
+
if (this.promptBackfill === abort)
|
|
843
|
+
this.promptBackfill = undefined;
|
|
844
|
+
}
|
|
845
|
+
})();
|
|
846
|
+
}
|
|
514
847
|
/** Search one page at a time, preserving only the first 200 matches and releasing temporary content.
|
|
515
848
|
* @param query - Literal, case-insensitive text including folded reasoning.
|
|
516
849
|
* @param signal - Cancels HTTP and processing without cancelling the agent.
|
|
517
850
|
* @returns Newest-first bounded summaries and an explicit truncation flag.
|
|
518
851
|
*/
|
|
519
852
|
async searchHistory(query, signal) {
|
|
520
|
-
const source = this.
|
|
853
|
+
const source = this.info.record;
|
|
521
854
|
if (!source.ready)
|
|
522
855
|
throw new Error('Wait for the session snapshot');
|
|
523
856
|
const sessionId = this.sessionId;
|
|
@@ -579,7 +912,7 @@ export class SessionController {
|
|
|
579
912
|
* @returns A caller-owned historical window that must be disposed when closed.
|
|
580
913
|
*/
|
|
581
914
|
async historyAt(target, signal) {
|
|
582
|
-
const source = this.
|
|
915
|
+
const source = this.info.record;
|
|
583
916
|
const selection = this.store.selection();
|
|
584
917
|
const page = object(await this.host.require().call('session/page', { request: {
|
|
585
918
|
address: { kind: 'session', sessionId: this.sessionId }, throughSeq: source.readThrough,
|
|
@@ -605,14 +938,14 @@ export class SessionController {
|
|
|
605
938
|
* @param signal - Cancels local paging without interrupting the remote agent.
|
|
606
939
|
*/
|
|
607
940
|
async historyThrough(target, signal) {
|
|
608
|
-
const transcript = this.
|
|
941
|
+
const transcript = this.info.record;
|
|
609
942
|
if (!transcript.ready)
|
|
610
943
|
throw new Error('Wait for the session snapshot');
|
|
611
944
|
while (transcript.hasMore && (target === 'first' || transcript.beforeSeq !== undefined && transcript.beforeSeq > target)) {
|
|
612
945
|
signal.throwIfAborted();
|
|
613
946
|
const before = transcript.beforeSeq;
|
|
614
947
|
await this.older(signal);
|
|
615
|
-
if (this.
|
|
948
|
+
if (this.info.record !== transcript)
|
|
616
949
|
throw new Error('Session changed while loading history');
|
|
617
950
|
if (transcript.hasMore && (before === undefined || transcript.beforeSeq === undefined || transcript.beforeSeq >= before)) {
|
|
618
951
|
throw new Error('Host history page did not advance');
|
|
@@ -623,18 +956,18 @@ export class SessionController {
|
|
|
623
956
|
* @returns Number of removed records.
|
|
624
957
|
*/
|
|
625
958
|
reclaimHistory() {
|
|
626
|
-
if (this.
|
|
959
|
+
if (this.info.view.pinned || !this.store.state.online)
|
|
627
960
|
return 0;
|
|
628
|
-
const removed = this.
|
|
961
|
+
const removed = this.info.record.trimHistory(this.historyLimits);
|
|
629
962
|
if (removed)
|
|
630
|
-
releaseHistoryLayout(this.
|
|
963
|
+
releaseHistoryLayout(this.info.record);
|
|
631
964
|
return removed;
|
|
632
965
|
}
|
|
633
966
|
/** Release the selected transcript and its layout caches. */
|
|
634
967
|
releaseTranscript() {
|
|
635
|
-
|
|
636
|
-
this.
|
|
637
|
-
this.
|
|
968
|
+
this.promptBackfill?.abort();
|
|
969
|
+
this.promptBackfill = undefined;
|
|
970
|
+
this.info.reset();
|
|
638
971
|
}
|
|
639
972
|
/** @returns The selected session identity, or a `Select a session first` failure. */
|
|
640
973
|
get sessionId() {
|
package/dist/session/index.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export { Telemetry } from './telemetry.ts';
|
|
|
9
9
|
export type { QueuedInput } from './telemetry.ts';
|
|
10
10
|
export { DEFAULT_HISTORY_LIMITS, historyLimits } from './memory.ts';
|
|
11
11
|
export type { HistoryLimits } from './memory.ts';
|
|
12
|
+
export { DEFAULT_PROMPT_LIMITS, promptText, PromptIndex, SessionInfo } from './info.ts';
|
|
13
|
+
export type { ComposerState, InteractionState, ModelState, OptionState, PanelState, PromptEntry, PromptLimits, PromptRecord, ReferenceState, ViewState } from './info.ts';
|
|
12
14
|
export { navigationCommand, resolveTarget, sessionLabel } from './navigation.ts';
|
|
13
15
|
export { activeReference, fileMention, fileReferences } from './references.ts';
|
|
14
16
|
export type { FileReference } from './references.ts';
|
package/dist/session/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { historyLayout, layoutStats, releaseHistoryLayout } from "./history.js";
|
|
|
5
5
|
export { markdownCacheStats } from "./markdown.js";
|
|
6
6
|
export { Telemetry } from "./telemetry.js";
|
|
7
7
|
export { DEFAULT_HISTORY_LIMITS, historyLimits } from "./memory.js";
|
|
8
|
+
export { DEFAULT_PROMPT_LIMITS, promptText, PromptIndex, SessionInfo } from "./info.js";
|
|
8
9
|
export { navigationCommand, resolveTarget, sessionLabel } from "./navigation.js";
|
|
9
10
|
export { activeReference, fileMention, fileReferences } from "./references.js";
|
|
10
11
|
export { saveSessionLog } from "./export.js";
|