@itookit/dsht 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +4 -2
- package/README.zh.md +6 -4
- package/dist/catalog/controller.d.ts +26 -6
- package/dist/catalog/controller.js +73 -45
- package/dist/catalog/index.d.ts +1 -0
- package/dist/cli/dsht.js +4 -1
- package/dist/cli/startup.js +30 -11
- package/dist/cli/verifier.d.ts +4 -0
- package/dist/cli/verifier.js +28 -5
- package/dist/contracts.d.ts +20 -5
- package/dist/controller/connection-streams.d.ts +22 -0
- package/dist/controller/connection-streams.js +105 -0
- package/dist/controller/connection.d.ts +14 -3
- package/dist/controller/connection.js +40 -69
- package/dist/controller/controller.d.ts +17 -233
- package/dist/controller/controller.js +108 -811
- package/dist/controller/foreground.d.ts +44 -0
- package/dist/controller/foreground.js +79 -0
- package/dist/controller/loop-coordinator.d.ts +48 -0
- package/dist/controller/loop-coordinator.js +647 -0
- package/dist/controller/verifier.d.ts +4 -0
- package/dist/cost/controller.d.ts +1 -1
- package/dist/cost/controller.js +12 -5
- package/dist/cost/scanner.js +1 -0
- package/dist/session/controller.d.ts +23 -35
- package/dist/session/controller.js +113 -363
- package/dist/session/history-reader.d.ts +32 -0
- package/dist/session/history-reader.js +170 -0
- package/dist/session/index.d.ts +1 -1
- package/dist/session/info.d.ts +3 -38
- package/dist/session/info.js +14 -1
- package/dist/session/interactions.d.ts +26 -0
- package/dist/session/interactions.js +75 -0
- package/dist/session/navigator.d.ts +47 -0
- package/dist/session/navigator.js +158 -0
- package/dist/session/prompt-backfill.d.ts +23 -0
- package/dist/session/prompt-backfill.js +88 -0
- package/dist/session/state.d.ts +20 -0
- package/dist/session/state.js +1 -0
- package/dist/session/telemetry.d.ts +15 -6
- package/dist/session/telemetry.js +44 -7
- package/dist/session/transcript.d.ts +5 -1
- package/dist/slash/index.d.ts +1 -1
- package/dist/slash/parse.d.ts +2 -126
- package/dist/slash/registry.d.ts +1 -1
- package/dist/slash/types.d.ts +126 -0
- package/dist/slash/types.js +1 -0
- package/dist/state.d.ts +5 -17
- package/dist/state.js +1 -1
- package/dist/transport/client.d.ts +4 -3
- package/dist/transport/client.js +71 -25
- package/dist/ui/app.js +86 -301
- package/dist/ui/chat/shell-view.d.ts +2 -0
- package/dist/ui/chat/shell-view.js +8 -0
- package/dist/ui/chat/use-history-view.d.ts +69 -0
- package/dist/ui/chat/use-history-view.js +123 -0
- package/dist/ui/dialogs/use-panels.d.ts +53 -0
- package/dist/ui/dialogs/use-panels.js +51 -0
- package/dist/ui/input/use-composer.d.ts +35 -0
- package/dist/ui/input/use-composer.js +109 -0
- package/dist/ui/input/use-deferred-lines.d.ts +16 -0
- package/dist/ui/input/use-deferred-lines.js +54 -0
- package/dist/ui/input/use-history-recall.d.ts +20 -0
- package/dist/ui/input/use-history-recall.js +47 -0
- package/package.json +1 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** Event/control subscriptions and their startup deadlines belong to one connection generation. */
|
|
2
|
+
import { RemoteError } from "../transport/client.js";
|
|
3
|
+
import { controlFrame, hostEvent } from "../transport/events.js";
|
|
4
|
+
import { errorText, object, string } from "../transport/wire.js";
|
|
5
|
+
export class ConnectionStreams {
|
|
6
|
+
client;
|
|
7
|
+
host;
|
|
8
|
+
subscriptions = new Set();
|
|
9
|
+
abort = new AbortController();
|
|
10
|
+
signal;
|
|
11
|
+
constructor(client, host, signal) {
|
|
12
|
+
this.client = client;
|
|
13
|
+
this.host = host;
|
|
14
|
+
this.signal = AbortSignal.any([signal, this.abort.signal]);
|
|
15
|
+
}
|
|
16
|
+
async start() {
|
|
17
|
+
let clientId = '';
|
|
18
|
+
await this.follow('$events', 'Host ready timed out', value => {
|
|
19
|
+
const frame = object(value);
|
|
20
|
+
if (frame.type === 'ready') {
|
|
21
|
+
clientId = string(frame.clientId);
|
|
22
|
+
this.host.identified(clientId);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
const event = hostEvent(frame);
|
|
26
|
+
if (event && !this.host.event(event) && 'eventId' in event) {
|
|
27
|
+
// Unknown waterfalls still need a result so the host's event chain can continue.
|
|
28
|
+
void this.client.call('$events/result', { clientId, eventId: event.eventId, outcome: { kind: 'next' } })
|
|
29
|
+
.catch(error => this.host.fail(new Error(errorText(error))));
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
});
|
|
33
|
+
await this.follow('session/control', 'Session control baseline timed out', value => {
|
|
34
|
+
try {
|
|
35
|
+
this.host.event({ kind: 'control', frame: controlFrame(value) });
|
|
36
|
+
this.host.changed();
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
// Missing live metrics must not blind a running conversation.
|
|
40
|
+
this.host.degraded(`Live metrics degraded: ${errorText(error)}`);
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}, error => {
|
|
44
|
+
if (!(error instanceof RemoteError) || error.code !== 'gateway/method-unavailable')
|
|
45
|
+
return false;
|
|
46
|
+
this.host.degraded('Live metrics unavailable on this host');
|
|
47
|
+
return true;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
close() {
|
|
51
|
+
this.abort.abort();
|
|
52
|
+
for (const subscription of this.subscriptions)
|
|
53
|
+
subscription.cancel();
|
|
54
|
+
this.subscriptions.clear();
|
|
55
|
+
}
|
|
56
|
+
/** Resolve the first usable frame while retaining the stream until this generation ends. */
|
|
57
|
+
follow(endpoint, timeout, item, unavailable) {
|
|
58
|
+
this.signal.throwIfAborted();
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
let settled = false;
|
|
61
|
+
const finish = (error) => {
|
|
62
|
+
if (settled)
|
|
63
|
+
return;
|
|
64
|
+
settled = true;
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
this.signal.removeEventListener('abort', cancelled);
|
|
67
|
+
if (error === undefined)
|
|
68
|
+
resolve();
|
|
69
|
+
else
|
|
70
|
+
reject(error);
|
|
71
|
+
};
|
|
72
|
+
const cancelled = () => finish(this.signal.reason);
|
|
73
|
+
const timer = setTimeout(() => finish(new Error(timeout)), this.client.timeoutMs);
|
|
74
|
+
this.signal.addEventListener('abort', cancelled, { once: true });
|
|
75
|
+
try {
|
|
76
|
+
const subscription = this.client.subscribe(endpoint, {}, {
|
|
77
|
+
item: value => {
|
|
78
|
+
if (this.signal.aborted)
|
|
79
|
+
return;
|
|
80
|
+
if (item(value))
|
|
81
|
+
finish();
|
|
82
|
+
},
|
|
83
|
+
end: error => {
|
|
84
|
+
if (this.signal.aborted)
|
|
85
|
+
return;
|
|
86
|
+
if (unavailable?.(error)) {
|
|
87
|
+
finish();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const reason = error ?? new Error(`${endpoint} stream ended`);
|
|
91
|
+
finish(reason);
|
|
92
|
+
this.host.fail(reason);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
if (this.signal.aborted)
|
|
96
|
+
subscription.cancel();
|
|
97
|
+
else
|
|
98
|
+
this.subscriptions.add(subscription);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
finish(error);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { Client } from '../transport/client.ts';
|
|
2
2
|
import { type Json } from '../transport/wire.ts';
|
|
3
|
-
import {
|
|
3
|
+
import type { HostEvent } from '../transport/events.ts';
|
|
4
4
|
import type { HostAccess } from '../transport/host.ts';
|
|
5
5
|
import type { ConnectionView } from '../session/connection-view.ts';
|
|
6
|
-
|
|
6
|
+
/** Connection publication cannot replace session, navigation, catalog or shell state. */
|
|
7
|
+
export interface ConnectionStore {
|
|
8
|
+
readonly state: {
|
|
9
|
+
readonly online: boolean;
|
|
10
|
+
};
|
|
11
|
+
update(patch: Partial<{
|
|
12
|
+
online: boolean;
|
|
13
|
+
status: string;
|
|
14
|
+
lastFailure: string;
|
|
15
|
+
controlError: string | undefined;
|
|
16
|
+
}>): void;
|
|
17
|
+
}
|
|
7
18
|
/** Connection inputs resolved by the CLI or a library consumer. */
|
|
8
19
|
export interface ConnectionOptions {
|
|
9
20
|
base: string;
|
|
@@ -40,7 +51,7 @@ export declare class ConnectionController implements HostAccess, ConnectionView
|
|
|
40
51
|
private readonly abort;
|
|
41
52
|
private runTask;
|
|
42
53
|
private generationFailed;
|
|
43
|
-
constructor(store:
|
|
54
|
+
constructor(store: ConnectionStore, options: ConnectionOptions, listener: ConnectionListener);
|
|
44
55
|
/** Start one retry loop, with a fresh snapshot generation after every disconnect. */
|
|
45
56
|
start(): void;
|
|
46
57
|
/** Cancel retries and HTTP, close the socket, and wait for the loop to settle. */
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** Owns the physical connection, its generations, and the host event subscriptions. */
|
|
2
2
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
3
|
import { AuthenticationRequired } from "../transport/auth.js";
|
|
4
|
-
import { HttpError
|
|
5
|
-
import { errorText
|
|
6
|
-
import {
|
|
4
|
+
import { HttpError } from "../transport/client.js";
|
|
5
|
+
import { errorText } from "../transport/wire.js";
|
|
6
|
+
import { ConnectionStreams } from "./connection-streams.js";
|
|
7
7
|
/** Owns the physical connection: reconnects, subscriptions and generation lifecycle.
|
|
8
8
|
*
|
|
9
9
|
* Host running state, projections and interactions belong to the session domain; this class only
|
|
@@ -56,90 +56,61 @@ export class ConnectionController {
|
|
|
56
56
|
let attempt = 0;
|
|
57
57
|
while (!this.abort.signal.aborted) {
|
|
58
58
|
this.listener.begin();
|
|
59
|
+
if (this.abort.signal.aborted)
|
|
60
|
+
break;
|
|
59
61
|
const client = this.options.makeClient();
|
|
60
62
|
this.current = client;
|
|
63
|
+
const generation = new AbortController();
|
|
64
|
+
const signal = AbortSignal.any([this.abort.signal, generation.signal]);
|
|
65
|
+
let disconnect;
|
|
66
|
+
const disconnected = new Promise(resolve => { disconnect = resolve; });
|
|
67
|
+
const fail = (error) => {
|
|
68
|
+
if (generation.signal.aborted)
|
|
69
|
+
return;
|
|
70
|
+
generation.abort(error);
|
|
71
|
+
disconnect(error);
|
|
72
|
+
// End HTTP as well as streams, including requests the ready callback is awaiting.
|
|
73
|
+
void client.close();
|
|
74
|
+
};
|
|
75
|
+
this.generationFailed = fail;
|
|
76
|
+
const streams = new ConnectionStreams(client, {
|
|
77
|
+
identified: id => { this.clientId = id; }, event: event => this.listener.event(event),
|
|
78
|
+
changed: () => this.store.update({}), degraded: controlError => this.store.update({ controlError }), fail,
|
|
79
|
+
}, signal);
|
|
61
80
|
try {
|
|
81
|
+
signal.throwIfAborted();
|
|
62
82
|
await this.options.authenticate(client);
|
|
83
|
+
signal.throwIfAborted();
|
|
63
84
|
await client.connect();
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const ready = new Promise((resolve, reject) => {
|
|
68
|
-
const timer = setTimeout(() => reject(new Error('Host ready timed out')), client.timeoutMs);
|
|
69
|
-
client.subscribe('$events', {}, {
|
|
70
|
-
item: value => {
|
|
71
|
-
const frame = object(value);
|
|
72
|
-
if (frame.type === 'ready') {
|
|
73
|
-
this.clientId = string(frame.clientId);
|
|
74
|
-
clearTimeout(timer);
|
|
75
|
-
resolve();
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
const event = hostEvent(frame);
|
|
79
|
-
if (!event)
|
|
80
|
-
return;
|
|
81
|
-
if (this.listener.event(event))
|
|
82
|
-
return;
|
|
83
|
-
// An unanswered waterfall would block the host's event chain, so it is always settled.
|
|
84
|
-
if ('eventId' in event) {
|
|
85
|
-
void client.call('$events/result', { clientId: this.clientId,
|
|
86
|
-
eventId: event.eventId, outcome: { kind: 'next' } }).catch(error => fail(new Error(errorText(error))));
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
end: error => { clearTimeout(timer); const reason = error ?? new Error('Event stream ended'); reject(reason); fail(reason); },
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
await ready;
|
|
93
|
-
await new Promise((resolve, reject) => {
|
|
94
|
-
const timer = setTimeout(() => reject(new Error('Session control baseline timed out')), client.timeoutMs);
|
|
95
|
-
client.subscribe('session/control', {}, {
|
|
96
|
-
item: value => {
|
|
97
|
-
try {
|
|
98
|
-
this.listener.event({ kind: 'control', frame: controlFrame(value) });
|
|
99
|
-
this.store.update({});
|
|
100
|
-
clearTimeout(timer);
|
|
101
|
-
resolve();
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
// Live metrics are not worth the connection: a frame this client cannot decode is
|
|
105
|
-
// reported and skipped, because failing the generation would blind a running turn.
|
|
106
|
-
clearTimeout(timer);
|
|
107
|
-
this.store.update({ controlError: `Live metrics degraded: ${errorText(error)}` });
|
|
108
|
-
resolve();
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
end: error => {
|
|
112
|
-
clearTimeout(timer);
|
|
113
|
-
if (error instanceof RemoteError && error.code === 'gateway/method-unavailable') {
|
|
114
|
-
this.store.update({ controlError: 'Live metrics unavailable on this host' });
|
|
115
|
-
resolve();
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
const reason = error ?? new Error('Session control stream ended');
|
|
119
|
-
reject(reason);
|
|
120
|
-
fail(reason);
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
});
|
|
85
|
+
signal.throwIfAborted();
|
|
86
|
+
await streams.start();
|
|
87
|
+
signal.throwIfAborted();
|
|
125
88
|
await this.listener.ready();
|
|
89
|
+
signal.throwIfAborted();
|
|
126
90
|
attempt = 0;
|
|
127
91
|
const error = await disconnected;
|
|
128
92
|
if (!this.abort.signal.aborted)
|
|
129
93
|
throw error;
|
|
130
94
|
}
|
|
131
95
|
catch (error) {
|
|
132
|
-
if (
|
|
133
|
-
|
|
96
|
+
if (this.abort.signal.aborted)
|
|
97
|
+
break;
|
|
98
|
+
const reason = generation.signal.aborted ? generation.signal.reason : error;
|
|
99
|
+
if (reason instanceof AuthenticationRequired || reason instanceof HttpError && [401, 403].includes(reason.status)) {
|
|
100
|
+
this.store.update({ lastFailure: `${errorText(reason)}. Set DSH_TOKEN and restart to log in.`, status: 'Login required' });
|
|
134
101
|
return;
|
|
135
102
|
}
|
|
136
|
-
|
|
137
|
-
this.store.update({ lastFailure: errorText(error), status: 'Reconnecting…' });
|
|
103
|
+
this.store.update({ lastFailure: errorText(reason), status: 'Reconnecting…' });
|
|
138
104
|
}
|
|
139
105
|
finally {
|
|
140
106
|
this.generationFailed = undefined;
|
|
141
|
-
|
|
107
|
+
generation.abort();
|
|
108
|
+
streams.close();
|
|
109
|
+
this.clientId = '';
|
|
110
|
+
this.store.update({ online: false });
|
|
142
111
|
await client.close();
|
|
112
|
+
if (this.current === client)
|
|
113
|
+
this.current = undefined;
|
|
143
114
|
await this.listener.ended();
|
|
144
115
|
}
|
|
145
116
|
if (!this.abort.signal.aborted) {
|
|
@@ -5,7 +5,7 @@ import { type HistoryLimits } from '../session/memory.ts';
|
|
|
5
5
|
import { type SessionRender } from '../session/history.ts';
|
|
6
6
|
import { SessionController } from '../session/controller.ts';
|
|
7
7
|
import { CatalogController } from '../catalog/controller.ts';
|
|
8
|
-
import type {
|
|
8
|
+
import type { TelemetryReader } from '../session/telemetry.ts';
|
|
9
9
|
import type { Transcript } from '../session/transcript.ts';
|
|
10
10
|
import type { CostLedger } from '../cost/ledger.ts';
|
|
11
11
|
import { CostController } from '../cost/controller.ts';
|
|
@@ -14,7 +14,7 @@ import { MemoryLog } from './memory-log.ts';
|
|
|
14
14
|
import { TraceLog } from './trace-log.ts';
|
|
15
15
|
import { PromptStore } from './prompts.ts';
|
|
16
16
|
import { type LoopLimits, type LoopProtocol } from './loop.ts';
|
|
17
|
-
import {
|
|
17
|
+
import type { VerifierPort } from './verifier.ts';
|
|
18
18
|
import type { ClientActivity, ForegroundKind, ForegroundSnapshot, LoopProgress, LoopRecord, OutputSource, PeekSnapshot } from '../contracts.ts';
|
|
19
19
|
import { SessionPeek } from '../session/peek.ts';
|
|
20
20
|
import { type ControllerStore, type State } from '../state.ts';
|
|
@@ -24,13 +24,13 @@ import type { SavedPrompt } from '../contracts.ts';
|
|
|
24
24
|
import type { FileReference } from '../session/references.ts';
|
|
25
25
|
import type { InteractionState, OptionState } from '../session/info.ts';
|
|
26
26
|
import type { Reasoning } from '../session/history.ts';
|
|
27
|
-
/** Mutating operations
|
|
27
|
+
/** Mutating operations: foreground work, local edits, and control cancellation with its own admission. */
|
|
28
28
|
export interface Actions {
|
|
29
29
|
/** Claim the single foreground slot for work the front end orchestrates itself (paging, loading).
|
|
30
30
|
*
|
|
31
31
|
* The controller owns the slot, the abort controller and the identity; the front end only supplies
|
|
32
32
|
* the work and renders `queries.foreground`. Returns undefined when the client is busy with another
|
|
33
|
-
* operation or
|
|
33
|
+
* operation or the client has stopped. Admitted work keeps its result/error on cancellation.
|
|
34
34
|
*/
|
|
35
35
|
foreground<T>(kind: ForegroundKind, label: string, work: (signal: AbortSignal) => Promise<T>, wait?: boolean): Promise<T | undefined>;
|
|
36
36
|
/** Cancel the operation that owns the slot; false when none is running. */
|
|
@@ -80,6 +80,7 @@ export interface Actions {
|
|
|
80
80
|
/** Repeated keys share one cancellation; it reports exit eligibility itself. */
|
|
81
81
|
interrupt(force?: boolean): Promise<boolean>;
|
|
82
82
|
older(signal?: AbortSignal, transcript?: Transcript): Promise<boolean>;
|
|
83
|
+
openHistory(target: number, signal: AbortSignal): Promise<boolean>;
|
|
83
84
|
historyThrough(target: number | 'first', signal: AbortSignal): Promise<boolean>;
|
|
84
85
|
removeQueued(itemId: string): Promise<boolean>;
|
|
85
86
|
/** Local shortcut prompts: no connection and no busy envelope, since they never reach the host. */
|
|
@@ -100,7 +101,7 @@ export interface Actions {
|
|
|
100
101
|
pickWorkspace(workspaceId?: string): void;
|
|
101
102
|
/** Leave a picker and return to the selected conversation; false when none is selected. */
|
|
102
103
|
showChat(): boolean;
|
|
103
|
-
|
|
104
|
+
showLatest(): void;
|
|
104
105
|
pinHistory(pinned: boolean): void;
|
|
105
106
|
setAnswers(answers: Record<string, AnswerValue['answers']>): void;
|
|
106
107
|
setOption(option?: OptionState): void;
|
|
@@ -128,7 +129,7 @@ export interface Queries {
|
|
|
128
129
|
readonly record: Transcript;
|
|
129
130
|
readonly window: Transcript | undefined;
|
|
130
131
|
readonly interaction: InteractionState;
|
|
131
|
-
readonly telemetry:
|
|
132
|
+
readonly telemetry: TelemetryReader;
|
|
132
133
|
readonly recallAtOldest: boolean;
|
|
133
134
|
readonly recallLength: number;
|
|
134
135
|
readonly recallHasOlder: boolean;
|
|
@@ -151,7 +152,6 @@ export interface Queries {
|
|
|
151
152
|
pendingCounts(): ReadonlyMap<string, number>;
|
|
152
153
|
recall(direction: -1 | 1, current: string): string;
|
|
153
154
|
references(query: string, signal: AbortSignal): Promise<FileReference[]>;
|
|
154
|
-
historyAt(target: number, signal: AbortSignal): Promise<Transcript>;
|
|
155
155
|
/** Plain rows and offsets for one laid-out record. */
|
|
156
156
|
render(input: {
|
|
157
157
|
transcript: Transcript;
|
|
@@ -221,50 +221,12 @@ export declare class Controller implements ControllerStore, ConnectionListener {
|
|
|
221
221
|
readonly trace: TraceLog | undefined;
|
|
222
222
|
/** Shortcut prompts the operator saved; in memory for this run when no path was supplied. */
|
|
223
223
|
readonly promptStore: PromptStore;
|
|
224
|
-
/** Running agent loop, if any; the loop lives here, not in the UI. */
|
|
225
|
-
private loop?;
|
|
226
|
-
/** Prompt the loop still has to send, when it could not be sent immediately. */
|
|
227
|
-
private loopPrompt?;
|
|
228
224
|
/** Finished turns of the selected session, so a waiter never has to sample a cached flag. */
|
|
229
225
|
private completedTurns;
|
|
230
226
|
/** Sessions the host reported busy, so a lone idle frame cannot claim a finished turn. */
|
|
231
227
|
private readonly busySessions;
|
|
232
|
-
/**
|
|
233
|
-
private
|
|
234
|
-
/** Timer that settles an attempt whose reply never commits its result block. */
|
|
235
|
-
private loopSettleTimer?;
|
|
236
|
-
/** Whether an attempt is currently being judged by an independent verifier. */
|
|
237
|
-
private loopVerifying;
|
|
238
|
-
/** A verdict is being consumed right now.
|
|
239
|
-
*
|
|
240
|
-
* Consuming one reads the artifact, so it is asynchronous; without this gate a replayed idle edge
|
|
241
|
-
* could start a second verification of an attempt whose verdict is already being applied.
|
|
242
|
-
*/
|
|
243
|
-
private loopSettling;
|
|
244
|
-
/** Cancels that verification when the loop is stopped or replaced. */
|
|
245
|
-
private loopVerifyAbort?;
|
|
246
|
-
/** Verdict of the previous attempt on the current step, for the retry and the next verifier. */
|
|
247
|
-
private loopPrevious?;
|
|
248
|
-
/** Identity of the run in flight, so its verdicts are isolated from every other run's. */
|
|
249
|
-
private loopRunId?;
|
|
250
|
-
/** Whether the run in flight already wrote its `loop end`; one end per begin (I9). */
|
|
251
|
-
private loopEndTraced;
|
|
252
|
-
/** Sequence of the verification task in flight, so a retry never reuses the old task's identity. */
|
|
253
|
-
private loopVerifySeq;
|
|
254
|
-
/** Identity of the verification task whose result may still decide the attempt in flight. */
|
|
255
|
-
private loopVerifyIdentity?;
|
|
256
|
-
/** Timer that stops the whole run when its budget expires. */
|
|
257
|
-
private loopDeadlineTimer?;
|
|
258
|
-
/** Whole-run budget, when the operator set one. */
|
|
259
|
-
private readonly deadlineMs?;
|
|
260
|
-
/** Consecutive verifier outages in this attempt, so a broken verifier is retried then reported. */
|
|
261
|
-
private loopVerifierMisses;
|
|
262
|
-
/** What the operator answered when a verifier abstained; consumed by the next judgment. */
|
|
263
|
-
private loopAnswer?;
|
|
264
|
-
/** Whether a reply block may stand in for a missing verdict; off unless asked for. */
|
|
265
|
-
private readonly allowSelfFallback;
|
|
266
|
-
/** Client-side directory verdict files are written under. */
|
|
267
|
-
private readonly verdictRoot;
|
|
228
|
+
/** Loop execution owns all verifier, settling and deadline state. */
|
|
229
|
+
private readonly loops;
|
|
268
230
|
/** Local `!` commands, run on this machine and shown inline in the transcript. */
|
|
269
231
|
readonly shell: ShellController;
|
|
270
232
|
/** Mutating surface the UI drives. */
|
|
@@ -281,26 +243,17 @@ export declare class Controller implements ControllerStore, ConnectionListener {
|
|
|
281
243
|
readonly memoryLogPath: string | undefined;
|
|
282
244
|
/** Directory this client runs in. */
|
|
283
245
|
readonly localDirectory: string;
|
|
284
|
-
/** Independent verifier for scored rounds, when one was supplied. */
|
|
285
|
-
private readonly verifier;
|
|
286
246
|
/** Session opened at startup, when one was named. */
|
|
287
247
|
private readonly initialSession;
|
|
288
248
|
private readonly observers;
|
|
289
249
|
private selector;
|
|
290
250
|
private connectionSettled;
|
|
251
|
+
/** Startup defaults apply once; subsequent generations restore the operator's selection. */
|
|
252
|
+
private initialized;
|
|
291
253
|
/** Counter behind `commandId`, so every executed line has one identifier in begin and end. */
|
|
292
254
|
private commandSeq;
|
|
293
|
-
/**
|
|
294
|
-
private
|
|
295
|
-
/** Callers waiting for the slot, oldest first; woken one at a time so nobody barges in. */
|
|
296
|
-
private readonly foregroundWaiters;
|
|
297
|
-
/** True while a waiter has been promised the slot but has not taken it yet. */
|
|
298
|
-
private foregroundGranted;
|
|
299
|
-
/** Set when the client stops, so a waiter never starts work on a closed connection. */
|
|
300
|
-
private foregroundClosed;
|
|
301
|
-
private foregroundSeq;
|
|
302
|
-
/** Which operation the current async continuation belongs to, so a nested call never re-claims. */
|
|
303
|
-
private readonly foregroundOwner;
|
|
255
|
+
/** Scheduling and cancellation are owned independently of domain operations. */
|
|
256
|
+
private readonly foregroundSlot;
|
|
304
257
|
/** Read-only follower for the full-screen view; it borrows the connection and never selects. */
|
|
305
258
|
readonly peek: SessionPeek;
|
|
306
259
|
/** Sources this client created, keyed by session id; the host cannot record that lineage itself. */
|
|
@@ -384,39 +337,11 @@ export declare class Controller implements ControllerStore, ConnectionListener {
|
|
|
384
337
|
private runAction;
|
|
385
338
|
/** Same slot, for an operation that produces a value the caller needs. */
|
|
386
339
|
private runActionValue;
|
|
387
|
-
/**
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
* calling an action) belongs to that operation: refusing it for being busy would deadlock the very
|
|
391
|
-
* work that owns the slot. An async-local owner answers that exactly, where a plain flag could not
|
|
392
|
-
* tell a nested call from a second, unrelated one.
|
|
393
|
-
* @returns True when this call runs inside the operation that owns the slot.
|
|
394
|
-
*/
|
|
395
|
-
private ownsForeground;
|
|
396
|
-
/** Run one operation in the foreground slot, claiming it when the caller does not already own it.
|
|
397
|
-
*
|
|
398
|
-
* The slot serializes what the operator is doing — one thing at a time — which is a different
|
|
399
|
-
* question from the session write order (§6.3): a read takes this slot too. The controller owns the
|
|
400
|
-
* slot, the abort controller and the identity, so the front end only renders `queries.foreground`
|
|
401
|
-
* and cancels it in one call.
|
|
402
|
-
* @param kind - What the operation is.
|
|
403
|
-
* @param label - Human label shown while it runs.
|
|
404
|
-
* @param work - The work, handed the signal that `cancelForeground` aborts.
|
|
405
|
-
* @returns What the work returned, or undefined when the slot was taken or the work was cancelled.
|
|
340
|
+
/** Run outside the foreground slot, with the same application failure reporting.
|
|
341
|
+
* Local prompt edits need no connection; verifier cancellation uses the session control lane and
|
|
342
|
+
* must reach the host even while the foreground is busy or closed for shutdown.
|
|
406
343
|
*/
|
|
407
|
-
private
|
|
408
|
-
/** Cancel the operation that owns the foreground slot; false when none is running.
|
|
409
|
-
* @returns Whether an operation was there to cancel.
|
|
410
|
-
*/
|
|
411
|
-
private cancelForeground;
|
|
412
|
-
/** Run one local operation that needs no connection, reporting failure the way `runAction` does.
|
|
413
|
-
*
|
|
414
|
-
* Shortcut prompts are the operator's own file, so they must keep working while the host is
|
|
415
|
-
* disconnected; only the failure line is shared with the remote operations.
|
|
416
|
-
* @param operation - Operation to run against local storage.
|
|
417
|
-
* @returns Whether it ran to completion.
|
|
418
|
-
*/
|
|
419
|
-
private runLocalAction;
|
|
344
|
+
private runImmediateAction;
|
|
420
345
|
/** Read the counters one memory sample records; content never leaves as text.
|
|
421
346
|
*
|
|
422
347
|
* The retained transcript and the ledger are small in practice, so a sample also reads the two
|
|
@@ -494,14 +419,6 @@ export declare class Controller implements ControllerStore, ConnectionListener {
|
|
|
494
419
|
private pendingCounts;
|
|
495
420
|
/** Load the optional preset roster once per connection. */
|
|
496
421
|
private loadPresetNames;
|
|
497
|
-
/** @returns Host model routes and adapter-owned reasoning choices. */
|
|
498
|
-
private modelCatalog;
|
|
499
|
-
/** Select the next request's model.
|
|
500
|
-
* @param provider - Host provider route ID.
|
|
501
|
-
* @param model - Exact model ID.
|
|
502
|
-
* @param reasoningEffort - Optional adapter-owned effort ID.
|
|
503
|
-
*/
|
|
504
|
-
private selectModel;
|
|
505
422
|
/** Stop the selected turn, or allow exit only while idle.
|
|
506
423
|
* @param force - Send an explicit cancellation even when the cached running flag is idle.
|
|
507
424
|
* @returns True when the caller may exit.
|
|
@@ -517,7 +434,6 @@ export declare class Controller implements ControllerStore, ConnectionListener {
|
|
|
517
434
|
/** Show a detached history window, releasing the one it replaces.
|
|
518
435
|
* @param window - Record to display, or undefined to return to the live transcript.
|
|
519
436
|
*/
|
|
520
|
-
private setViewWindow;
|
|
521
437
|
/** Recall one step through the selected session's prompt index; never touches the network.
|
|
522
438
|
* @param direction - Negative for older input, positive for newer input.
|
|
523
439
|
* @param current - Composer content before recall began, restored at the newest position.
|
|
@@ -660,132 +576,6 @@ export declare class Controller implements ControllerStore, ConnectionListener {
|
|
|
660
576
|
* agent and starts an idle one, exactly like submitted text.
|
|
661
577
|
*/
|
|
662
578
|
private handoff;
|
|
663
|
-
/** Start a scored loop and send its opening step.
|
|
664
|
-
* @param protocol - Prompt text and step count the loop follows.
|
|
665
|
-
* @param limits - Resolved `--from/--to/--score/--tries`.
|
|
666
|
-
*/
|
|
667
|
-
private startLoop;
|
|
668
|
-
/** End a run whose next prompt could not be sent, keeping the reason visible.
|
|
669
|
-
*
|
|
670
|
-
* A rejected send is not a verdict and not an operator cancellation, so the phase is `needs-human`
|
|
671
|
-
* and `terminalReason` says why — this is what keeps `phase=cancelled` meaning "a person stopped it".
|
|
672
|
-
* @param loop - Run that could not send.
|
|
673
|
-
* @param error - What the host or the session layer rejected with.
|
|
674
|
-
*/
|
|
675
|
-
private rejectLoopSend;
|
|
676
|
-
/** Record the end of one run once, with the phase it stopped in and why.
|
|
677
|
-
*
|
|
678
|
-
* I9 needs every `begin` to be paired inside the trace window; this is the only writer of `loop end`.
|
|
679
|
-
* @param loop - Run whose terminal phase was just published.
|
|
680
|
-
*/
|
|
681
|
-
private traceLoopEnd;
|
|
682
|
-
/** Whether a step begins with verification rather than with a work prompt.
|
|
683
|
-
*
|
|
684
|
-
* Needs all three: the record asks for it, the record has a verifier prompt, and this client was
|
|
685
|
-
* given a verifier. Otherwise there is nobody to verify first, and the step asks for work.
|
|
686
|
-
* @param loop - Loop about to start a step.
|
|
687
|
-
* @returns True when the step starts by verifying.
|
|
688
|
-
*/
|
|
689
|
-
private startsByVerifying;
|
|
690
|
-
/** Verify the step in flight without a work turn before it.
|
|
691
|
-
*
|
|
692
|
-
* The activity is set inside `verifyRound`, so the first verification, a work-turn verdict and a
|
|
693
|
-
* retry all publish the same state without this caller having to remember it.
|
|
694
|
-
* @param loop - Loop whose step and attempt are already set.
|
|
695
|
-
*/
|
|
696
|
-
private verifyStep;
|
|
697
|
-
/** Check the round's own section before spending a verifier on it, then verify or ask for work.
|
|
698
|
-
*
|
|
699
|
-
* Verify-first exists so a round that already passes costs no work turn. When the section is not in
|
|
700
|
-
* the artifact at all the round *cannot* pass, and this client can read that itself — the same check
|
|
701
|
-
* it applies to a verdict before accepting one. Sending a verifier to discover "the file is missing"
|
|
702
|
-
* costs a session and, because a failed attempt consumes one, also the round's first attempt: a run
|
|
703
|
-
* over a document with no review yet would start working at attempt 2/10. An artifact this client
|
|
704
|
-
* cannot read stays a boundary: the verification runs and its verdict decides, as before.
|
|
705
|
-
* @param loop - Loop whose step and attempt are already set.
|
|
706
|
-
*/
|
|
707
|
-
private verifySection;
|
|
708
|
-
/** Whether the directory this client runs in is readable here at all.
|
|
709
|
-
*
|
|
710
|
-
* `readText` answers undefined for a missing file and for a file inside a directory this machine does
|
|
711
|
-
* not have, and the two mean opposite things: the first is a producer that wrote nothing, the second
|
|
712
|
-
* is a workspace whose verdict cannot be checked from here.
|
|
713
|
-
* @returns True when the directory can be listed.
|
|
714
|
-
*/
|
|
715
|
-
private workspaceVisible;
|
|
716
|
-
/** Arm the whole-run budget, when the operator set one. */
|
|
717
|
-
private armLoopDeadline;
|
|
718
|
-
/** Answer a paused run, so the current artifact is judged again with what the operator supplied.
|
|
719
|
-
*
|
|
720
|
-
* The answer is not a work order: it only adds a condition to the judgment, so nothing is sent to the
|
|
721
|
-
* agent and no attempt is consumed. The verification that follows is a new task with a new identity,
|
|
722
|
-
* which is what keeps a late verdict from the paused one from deciding the attempt.
|
|
723
|
-
* @param text - What the operator added; never written to the trace.
|
|
724
|
-
*/
|
|
725
|
-
private answerLoop;
|
|
726
|
-
/** Stop a running review; the terminal progress stays visible for the reader.
|
|
727
|
-
* @param reason - Why it stopped; the default is an operator action.
|
|
728
|
-
*/
|
|
729
|
-
private stopLoop;
|
|
730
|
-
/** Drop a finished run's progress line, keeping an active one.
|
|
731
|
-
*
|
|
732
|
-
* D3 keeps a terminal result on screen because it is what the reader most needs to see — but it is a
|
|
733
|
-
* result, not a status: the next line the operator runs means they have read it. An active run is
|
|
734
|
-
* left alone, so `/loop answer` and `/loop stop` keep the target they were typed for.
|
|
735
|
-
*/
|
|
736
|
-
private clearLoopResult;
|
|
737
|
-
/** Drop the loop entirely, without publishing a cancelled phase.
|
|
738
|
-
* @param reason - Why it was dropped, recorded when it never reached a terminal phase itself.
|
|
739
|
-
*/
|
|
740
|
-
private forgetLoop;
|
|
741
|
-
/** Note that an attempt's turn ended; its block may still be arriving.
|
|
742
|
-
* @param sessionId - Session the host reported idle.
|
|
743
|
-
*/
|
|
744
|
-
private settleLoop;
|
|
745
|
-
/** Consume the attempt once its result block is committed, or the grace period expires.
|
|
746
|
-
*
|
|
747
|
-
* The final assistant message can land a moment after the idle event, so an empty parse is not yet
|
|
748
|
-
* a failed attempt: the next publish retries, and one timer covers a transcript that never grows.
|
|
749
|
-
*/
|
|
750
|
-
private trySettleLoop;
|
|
751
|
-
/** Score one finished round out of band, in a process of its own.
|
|
752
|
-
*
|
|
753
|
-
* The child writes its verdict to a file, so a slow or failed verifier delays the attempt instead
|
|
754
|
-
* of corrupting it: with no verdict the reply block decides, and with neither the attempt fails.
|
|
755
|
-
* @param loop - The run whose attempt just finished.
|
|
756
|
-
*/
|
|
757
|
-
private verifyRound;
|
|
758
|
-
/** Apply a protocol's own artifact requirement, then settle the attempt.
|
|
759
|
-
*
|
|
760
|
-
* The score is the verifier's judgement; whether the round's conclusion actually reached the
|
|
761
|
-
* artifact is a fact this client checks itself. A hard condition may not be overridden by a score:
|
|
762
|
-
* a round whose section is missing fails even at 10/10, and the missing line is fed back to the
|
|
763
|
-
* next attempt like any other finding. An artifact this client cannot read cannot be checked, so
|
|
764
|
-
* the verdict stands rather than being failed on a boundary.
|
|
765
|
-
* @param loop - The run whose attempt just finished.
|
|
766
|
-
* @param result - Verdict to consume.
|
|
767
|
-
* @param note - Note to show instead, when the check accepted the verdict unchanged.
|
|
768
|
-
*/
|
|
769
|
-
private settleChecked;
|
|
770
|
-
/** Apply one attempt's verdict, and continue the run when it has a next step.
|
|
771
|
-
* @param loop - The run being settled.
|
|
772
|
-
* @param result - Verdict to consume, or undefined when the attempt produced none.
|
|
773
|
-
*/
|
|
774
|
-
private settleWith;
|
|
775
|
-
/** Stop an in-flight verification, if any; its result can no longer decide anything. */
|
|
776
|
-
private abortVerification;
|
|
777
|
-
/** Stop the run because the whole-run budget expired; a budget stop, not a verdict.
|
|
778
|
-
*
|
|
779
|
-
* Any verification in flight is cancelled (bounded, as everywhere else) and its late result can
|
|
780
|
-
* no longer decide anything, because the loop is no longer active.
|
|
781
|
-
*/
|
|
782
|
-
private expireLoopDeadline;
|
|
783
|
-
/** Drop the run deadline, if one is armed. */
|
|
784
|
-
private clearLoopDeadline;
|
|
785
|
-
/** Drop a pending settle timer, if any. */
|
|
786
|
-
private forgetSettleTimer;
|
|
787
|
-
/** Send the prompt the loop is holding, once the client can actually send it. */
|
|
788
|
-
private flushLoop;
|
|
789
579
|
/** Cancel the active turn; pending queue items remain host-owned. */
|
|
790
580
|
private cancelTurn;
|
|
791
581
|
/** Add a page before the retained window.
|
|
@@ -799,12 +589,6 @@ export declare class Controller implements ControllerStore, ConnectionListener {
|
|
|
799
589
|
* @returns Newest-first bounded summaries and an explicit truncation flag.
|
|
800
590
|
*/
|
|
801
591
|
private searchHistory;
|
|
802
|
-
/** Load a separate small window ending at a search target.
|
|
803
|
-
* @param target - Durable message sequence to display.
|
|
804
|
-
* @param signal - Cancels the target-page request.
|
|
805
|
-
* @returns A caller-owned historical window that must be disposed when closed.
|
|
806
|
-
*/
|
|
807
|
-
private historyAt;
|
|
808
592
|
/** Plain rows and offsets for one laid-out record; the projection stays in the session domain. */
|
|
809
593
|
private render;
|
|
810
594
|
/** Load the prefix required for an explicit history jump.
|