@itookit/dsht 0.3.0 → 0.3.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 +30 -15
- package/README.zh.md +30 -15
- package/dist/catalog/controller.d.ts +32 -0
- package/dist/catalog/controller.js +88 -0
- package/dist/catalog/index.d.ts +2 -0
- package/dist/catalog/index.js +2 -0
- package/dist/{cli.js → cli/index.js} +40 -24
- package/dist/controller/connection.d.ts +80 -0
- package/dist/controller/connection.js +190 -0
- package/dist/controller/controller.d.ts +260 -0
- package/dist/controller/controller.js +362 -0
- package/dist/controller/index.d.ts +5 -0
- package/dist/controller/index.js +3 -0
- package/dist/controller/memory-log.d.ts +35 -0
- package/dist/controller/memory-log.js +95 -0
- package/dist/cost/controller.d.ts +34 -0
- package/dist/cost/controller.js +107 -0
- package/dist/cost/index.d.ts +9 -0
- package/dist/cost/index.js +7 -0
- package/dist/cost/ledger-files.d.ts +20 -0
- package/dist/cost/ledger-files.js +128 -0
- package/dist/cost/ledger.d.ts +65 -0
- package/dist/cost/ledger.js +136 -0
- package/dist/cost/pricing.d.ts +48 -0
- package/dist/cost/pricing.js +141 -0
- package/dist/cost/records.d.ts +17 -0
- package/dist/cost/records.js +66 -0
- package/dist/cost/scanner.d.ts +22 -0
- package/dist/cost/scanner.js +95 -0
- package/dist/cost/types.d.ts +69 -0
- package/dist/cost/types.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/session/connection-view.d.ts +18 -0
- package/dist/session/connection-view.js +1 -0
- package/dist/{controller.d.ts → session/controller.d.ts} +102 -136
- package/dist/session/controller.js +616 -0
- package/dist/session/export-html.d.ts +9 -0
- package/dist/session/export-html.js +39 -0
- package/dist/{export.d.ts → session/export.d.ts} +1 -1
- package/dist/{export.js → session/export.js} +6 -20
- package/dist/{history.d.ts → session/history.d.ts} +17 -0
- package/dist/{history.js → session/history.js} +163 -2
- package/dist/session/index.d.ts +17 -0
- package/dist/session/index.js +10 -0
- package/dist/session/markdown.d.ts +39 -0
- package/dist/session/markdown.js +255 -0
- package/dist/session/math.d.ts +11 -0
- package/dist/session/math.js +82 -0
- package/dist/{navigation.d.ts → session/navigation.d.ts} +1 -1
- package/dist/{navigation.js → session/navigation.js} +1 -1
- package/dist/{references.js → session/references.js} +1 -1
- package/dist/{telemetry.d.ts → session/telemetry.d.ts} +1 -1
- package/dist/{telemetry.js → session/telemetry.js} +1 -1
- package/dist/{transcript.d.ts → session/transcript.d.ts} +23 -1
- package/dist/{transcript.js → session/transcript.js} +36 -18
- package/dist/session/types.d.ts +18 -0
- package/dist/session/types.js +2 -0
- package/dist/state.d.ts +41 -0
- package/dist/state.js +9 -0
- package/dist/storage/directories.d.ts +14 -0
- package/dist/storage/directories.js +24 -0
- package/dist/storage/files.d.ts +43 -0
- package/dist/storage/files.js +135 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.js +3 -0
- package/dist/transport/auth.js +65 -0
- package/dist/transport/host.d.ts +13 -0
- package/dist/transport/host.js +1 -0
- package/dist/ui/app.d.ts +11 -0
- package/dist/ui/app.js +726 -0
- package/dist/ui/chat/header.d.ts +11 -0
- package/dist/ui/chat/header.js +14 -0
- package/dist/{history-view.d.ts → ui/chat/history-view.d.ts} +1 -1
- package/dist/{history-view.js → ui/chat/history-view.js} +3 -3
- package/dist/ui/chat/status.d.ts +79 -0
- package/dist/ui/chat/status.js +311 -0
- package/dist/ui/chat/viewport.d.ts +17 -0
- package/dist/ui/chat/viewport.js +14 -0
- package/dist/ui/commands/parse.d.ts +96 -0
- package/dist/ui/commands/parse.js +121 -0
- package/dist/ui/commands/registry.d.ts +33 -0
- package/dist/ui/commands/registry.js +72 -0
- package/dist/ui/copy-mode.d.ts +4 -0
- package/dist/ui/copy-mode.js +6 -0
- package/dist/{cost-view.d.ts → ui/dialogs/cost.d.ts} +1 -1
- package/dist/{cost-view.js → ui/dialogs/cost.js} +3 -3
- package/dist/ui/dialogs/index.d.ts +120 -0
- package/dist/ui/dialogs/index.js +113 -0
- package/dist/ui/dialogs/picker.d.ts +18 -0
- package/dist/ui/dialogs/picker.js +38 -0
- package/dist/ui/frozen.d.ts +8 -0
- package/dist/ui/frozen.js +7 -0
- package/dist/ui/input/references.d.ts +12 -0
- package/dist/ui/input/references.js +15 -0
- package/dist/ui/mount.d.ts +6 -0
- package/dist/ui/mount.js +11 -0
- package/dist/{theme.d.ts → ui/theme/index.d.ts} +1 -1
- package/package.json +19 -13
- package/dist/app.d.ts +0 -21
- package/dist/app.js +0 -805
- package/dist/auth.js +0 -108
- package/dist/controller.js +0 -961
- package/dist/cost.d.ts +0 -119
- package/dist/cost.js +0 -313
- package/dist/status.d.ts +0 -28
- package/dist/status.js +0 -157
- /package/dist/{cli.d.ts → cli/index.d.ts} +0 -0
- /package/dist/{memory.d.ts → session/memory.d.ts} +0 -0
- /package/dist/{memory.js → session/memory.js} +0 -0
- /package/dist/{references.d.ts → session/references.d.ts} +0 -0
- /package/dist/{auth.d.ts → transport/auth.d.ts} +0 -0
- /package/dist/{client.d.ts → transport/client.d.ts} +0 -0
- /package/dist/{client.js → transport/client.js} +0 -0
- /package/dist/{endpoint.d.ts → transport/endpoint.d.ts} +0 -0
- /package/dist/{endpoint.js → transport/endpoint.js} +0 -0
- /package/dist/{wire.d.ts → transport/wire.d.ts} +0 -0
- /package/dist/{wire.js → transport/wire.js} +0 -0
- /package/dist/{input-history.d.ts → ui/input/history.d.ts} +0 -0
- /package/dist/{input-history.js → ui/input/history.js} +0 -0
- /package/dist/{input.d.ts → ui/input/input.d.ts} +0 -0
- /package/dist/{input.js → ui/input/input.js} +0 -0
- /package/dist/{mouse.d.ts → ui/input/mouse.d.ts} +0 -0
- /package/dist/{mouse.js → ui/input/mouse.js} +0 -0
- /package/dist/{theme.js → ui/theme/index.js} +0 -0
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
2
|
/** Standalone executable entry; connects to an existing host and never launches Harness. */
|
|
4
3
|
import { createHash } from 'node:crypto';
|
|
5
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
6
4
|
import { homedir } from 'node:os';
|
|
7
5
|
import { join } from 'node:path';
|
|
8
|
-
import { CostLedger, DEFAULT_PRICES, pricesFrom } from "
|
|
6
|
+
import { CostLedger, DEFAULT_PRICES, pricesFrom } from "../cost/index.js";
|
|
9
7
|
import { parseArgs } from 'node:util';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { sessionLabel } from "
|
|
13
|
-
import { CookieStore, login } from "
|
|
14
|
-
import { Client } from "
|
|
15
|
-
import { historyLimits } from "
|
|
16
|
-
import { Controller } from "
|
|
17
|
-
import { endpoint } from "
|
|
18
|
-
import { errorText, safeText, string } from "
|
|
8
|
+
import { mount } from "../ui/mount.js";
|
|
9
|
+
import { createPrivateFile, ensureDirectory, readText } from "../storage/index.js";
|
|
10
|
+
import { sessionLabel } from "../session/navigation.js";
|
|
11
|
+
import { CookieStore, login } from "../transport/auth.js";
|
|
12
|
+
import { Client } from "../transport/client.js";
|
|
13
|
+
import { historyLimits } from "../session/memory.js";
|
|
14
|
+
import { Controller } from "../controller/controller.js";
|
|
15
|
+
import { endpoint } from "../transport/endpoint.js";
|
|
16
|
+
import { errorText, safeText, string } from "../transport/wire.js";
|
|
19
17
|
const HELP = `Usage: dsht [options] [list workspaces|list sessions]
|
|
20
18
|
|
|
21
19
|
With no command, choose a workspace and session interactively.
|
|
@@ -26,6 +24,8 @@ With no command, choose a workspace and session interactively.
|
|
|
26
24
|
--auth-dir <path> Private cookie directory (or DSHT_AUTH_DIR)
|
|
27
25
|
--history-records <n> Soft history record limit (default 2000)
|
|
28
26
|
--history-mb <n> Soft history payload budget in MiB (default 16)
|
|
27
|
+
--memory-log <path> Append runtime memory samples; a failing log stops itself
|
|
28
|
+
--no-memory-log Disable the runtime memory log (default: enabled)
|
|
29
29
|
--json Print machine-readable list output
|
|
30
30
|
--help Show this help
|
|
31
31
|
|
|
@@ -34,6 +34,7 @@ First login: export DSH_TOKEN, or export DSH_URL as the URL printed by dsh web.
|
|
|
34
34
|
Cookies are saved per server origin and reused on later starts. Tokens are never saved.
|
|
35
35
|
/cost shows session, today and three-day CNY estimates.
|
|
36
36
|
DSHT_CONFIG_DIR overrides the prices.json directory; DSHT_STATE_DIR overrides usage storage.
|
|
37
|
+
The memory log defaults to <state>/memory.log; DSHT_MEMORY_LOG sets another path or 'off'.
|
|
37
38
|
Examples:
|
|
38
39
|
npx @itookit/dsht
|
|
39
40
|
dsht list workspaces --json
|
|
@@ -44,6 +45,7 @@ async function main() {
|
|
|
44
45
|
url: { type: 'string', default: process.env.DSH_URL ?? 'http://127.0.0.1:3080' },
|
|
45
46
|
'history-records': { type: 'string' }, 'history-mb': { type: 'string' },
|
|
46
47
|
workspace: { type: 'string' }, session: { type: 'string' }, 'auth-dir': { type: 'string' }, json: { type: 'boolean' }, help: { type: 'boolean' },
|
|
48
|
+
'memory-log': { type: 'string' }, 'no-memory-log': { type: 'boolean' },
|
|
47
49
|
} });
|
|
48
50
|
if (values.help) {
|
|
49
51
|
process.stdout.write(HELP);
|
|
@@ -83,21 +85,19 @@ async function main() {
|
|
|
83
85
|
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
84
86
|
throw new Error('Interactive mode requires a terminal. Use list workspaces or list sessions for scripts.');
|
|
85
87
|
const config = process.env.DSHT_CONFIG_DIR ?? join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'dsht');
|
|
86
|
-
await
|
|
88
|
+
await ensureDirectory(config);
|
|
87
89
|
const pricePath = join(config, 'prices.json');
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const prices = pricesFrom(JSON.parse(await readFile(pricePath, 'utf8')));
|
|
96
|
-
const costDirectory = join(process.env.DSHT_STATE_DIR ?? join(process.env.XDG_STATE_HOME ?? join(homedir(), '.local', 'state'), 'dsht'), 'cost', createHash('sha256').update(new URL(url).origin).digest('hex'));
|
|
90
|
+
await createPrivateFile(pricePath, JSON.stringify(DEFAULT_PRICES, null, 2) + '\n');
|
|
91
|
+
const raw = await readText(pricePath);
|
|
92
|
+
if (raw === undefined)
|
|
93
|
+
throw new Error(`Price configuration disappeared: ${pricePath}`);
|
|
94
|
+
const prices = pricesFrom(JSON.parse(raw));
|
|
95
|
+
const stateRoot = process.env.DSHT_STATE_DIR ?? join(process.env.XDG_STATE_HOME ?? join(homedir(), '.local', 'state'), 'dsht');
|
|
96
|
+
const costDirectory = join(stateRoot, 'cost', createHash('sha256').update(new URL(url).origin).digest('hex'));
|
|
97
97
|
const costs = new CostLedger(prices, costDirectory);
|
|
98
98
|
await costs.load();
|
|
99
|
-
const controller = new Controller(url, token, values.session, undefined, client => login(client, token, store), costs, limits);
|
|
100
|
-
const app =
|
|
99
|
+
const controller = new Controller(url, token, values.session, undefined, client => login(client, token, store), costs, limits, memoryLogPath(stateRoot, values['memory-log'], values['no-memory-log']));
|
|
100
|
+
const app = mount(controller);
|
|
101
101
|
const terminate = () => app.unmount();
|
|
102
102
|
process.once('SIGTERM', terminate);
|
|
103
103
|
controller.start();
|
|
@@ -109,4 +109,20 @@ async function main() {
|
|
|
109
109
|
await controller.shutdown();
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
+
/** Resolve the runtime memory log path: an explicit flag wins, then the environment, then the default.
|
|
113
|
+
* @param stateRoot - Application state root used for the default path.
|
|
114
|
+
* @param requested - `--memory-log` value, when given.
|
|
115
|
+
* @param disabled - `--no-memory-log` flag.
|
|
116
|
+
* @returns Absolute log path, or undefined when the log is disabled.
|
|
117
|
+
*/
|
|
118
|
+
function memoryLogPath(stateRoot, requested, disabled) {
|
|
119
|
+
if (requested !== undefined && requested.trim() === '')
|
|
120
|
+
throw new Error('--memory-log requires a path');
|
|
121
|
+
if (disabled)
|
|
122
|
+
return undefined;
|
|
123
|
+
const chosen = (requested ?? process.env.DSHT_MEMORY_LOG)?.trim();
|
|
124
|
+
if (chosen === undefined || chosen === '')
|
|
125
|
+
return join(stateRoot, 'memory.log');
|
|
126
|
+
return chosen === 'off' ? undefined : chosen;
|
|
127
|
+
}
|
|
112
128
|
main().catch(error => { process.stderr.write(`${errorText(error)}\n`); process.exitCode = 1; });
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Client } from '../transport/client.ts';
|
|
2
|
+
import { type ObjectValue } from '../transport/wire.ts';
|
|
3
|
+
import type { HostAccess } from '../transport/host.ts';
|
|
4
|
+
import { Telemetry } from '../session/telemetry.ts';
|
|
5
|
+
import type { ConnectionView } from '../session/connection-view.ts';
|
|
6
|
+
import type { ControllerStore } from '../state.ts';
|
|
7
|
+
/** Connection inputs resolved by the CLI or a library consumer. */
|
|
8
|
+
export interface ConnectionOptions {
|
|
9
|
+
base: string;
|
|
10
|
+
token: string | undefined;
|
|
11
|
+
initialSession: string | undefined;
|
|
12
|
+
/** Factory for one generation's client; replaced in tests. */
|
|
13
|
+
makeClient: () => Client;
|
|
14
|
+
/** Authentication used by every generation, including reconnects. */
|
|
15
|
+
authenticate: (client: Client) => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/** Reactions the controller facade owes to connection lifecycle and host events. */
|
|
18
|
+
export interface ConnectionListener {
|
|
19
|
+
/** A new generation starts; domains drop generation-scoped state. */
|
|
20
|
+
begin(): void;
|
|
21
|
+
/** The event stream is ready and the control baseline is applied. */
|
|
22
|
+
ready(): Promise<void>;
|
|
23
|
+
/** The generation ended and its socket is closed. */
|
|
24
|
+
ended(): Promise<void>;
|
|
25
|
+
/** Deliver a host waterfall; return true when a domain retained it for an answer. */
|
|
26
|
+
waterfall(frame: ObjectValue): boolean;
|
|
27
|
+
/** The host cancelled a waterfall a domain retained. */
|
|
28
|
+
cancelled(eventId: string): void;
|
|
29
|
+
/** The host reported one session's running state. */
|
|
30
|
+
status(sessionId: string, running: boolean): void;
|
|
31
|
+
/** The host reported one session's error. */
|
|
32
|
+
error(sessionId: unknown, error: unknown): void;
|
|
33
|
+
/** Model catalogs may have changed. */
|
|
34
|
+
invalidated(): void;
|
|
35
|
+
/** A turn finished, so billing may refresh. */
|
|
36
|
+
idle(): void;
|
|
37
|
+
}
|
|
38
|
+
/** Owns reconnects, subscriptions and the telemetry generation. User commands remain single-attempt operations. */
|
|
39
|
+
export declare class ConnectionController implements HostAccess, ConnectionView {
|
|
40
|
+
private readonly store;
|
|
41
|
+
private readonly options;
|
|
42
|
+
private readonly listener;
|
|
43
|
+
telemetry: Telemetry;
|
|
44
|
+
clientId: string;
|
|
45
|
+
private readonly runningUpdates;
|
|
46
|
+
private readonly observedRunningAt;
|
|
47
|
+
private current;
|
|
48
|
+
private readonly abort;
|
|
49
|
+
private runTask;
|
|
50
|
+
private generationFailed;
|
|
51
|
+
constructor(store: ControllerStore, options: ConnectionOptions, listener: ConnectionListener);
|
|
52
|
+
/** Start one retry loop, with a fresh snapshot generation after every disconnect. */
|
|
53
|
+
start(): void;
|
|
54
|
+
/** Cancel retries and HTTP, close the socket, and wait for the loop to settle. */
|
|
55
|
+
stop(): Promise<void>;
|
|
56
|
+
/** @returns The connected client, or undefined while offline. */
|
|
57
|
+
client(): Client | undefined;
|
|
58
|
+
/** @returns The connected client, or throws while offline. */
|
|
59
|
+
require(): Client;
|
|
60
|
+
/** @returns Whether the current generation applied its host baseline. */
|
|
61
|
+
online(): boolean;
|
|
62
|
+
/** @returns The client lifetime signal. */
|
|
63
|
+
signal(): AbortSignal;
|
|
64
|
+
/** @returns The projection store of the current generation. */
|
|
65
|
+
telemetryView(): Telemetry;
|
|
66
|
+
/** @returns Cached host running state, or undefined when never reported. */
|
|
67
|
+
runningFor(sessionId: string): boolean | undefined;
|
|
68
|
+
/** @returns When this client first observed the session, for the elapsed-time fallback. */
|
|
69
|
+
observedAt(sessionId: string): number | undefined;
|
|
70
|
+
/** Record an observation start for a session this client just opened. */
|
|
71
|
+
observe(sessionId: string): void;
|
|
72
|
+
/** Fail the current generation, so the controller reopens a snapshot. */
|
|
73
|
+
fail(error: Error): void;
|
|
74
|
+
/** Answer one retained host waterfall through the event-result endpoint. */
|
|
75
|
+
reply(frame: ObjectValue, outcome: ObjectValue): Promise<void>;
|
|
76
|
+
/** Run one generation, then retry with bounded jittered backoff until stopped. */
|
|
77
|
+
private run;
|
|
78
|
+
/** Apply one `api-session/status` notification to the running maps and the session view. */
|
|
79
|
+
private acceptStatus;
|
|
80
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/** Owns the physical connection, its generations, and the host event subscriptions. */
|
|
2
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
|
+
import { AuthenticationRequired } from "../transport/auth.js";
|
|
4
|
+
import { HttpError, RemoteError } from "../transport/client.js";
|
|
5
|
+
import { array, errorText, object, string } from "../transport/wire.js";
|
|
6
|
+
import { Telemetry } from "../session/telemetry.js";
|
|
7
|
+
/** Projection keys this client consumes; the host retains every other capability. */
|
|
8
|
+
const RETAINED_PROJECTIONS = new Set(['title', 'modelSelection', 'contextPressure', 'tokenUsage', 'sessionStats', 'agentPreset']);
|
|
9
|
+
/** Host events that invalidate the model catalog. */
|
|
10
|
+
const CATALOG_EVENTS = ['llm/adapters-updated', 'settings/document-updated', 'credentials/reference-updated'];
|
|
11
|
+
/** Owns reconnects, subscriptions and the telemetry generation. User commands remain single-attempt operations. */
|
|
12
|
+
export class ConnectionController {
|
|
13
|
+
store;
|
|
14
|
+
options;
|
|
15
|
+
listener;
|
|
16
|
+
telemetry = new Telemetry(RETAINED_PROJECTIONS);
|
|
17
|
+
clientId = '';
|
|
18
|
+
runningUpdates = new Map();
|
|
19
|
+
observedRunningAt = new Map();
|
|
20
|
+
current;
|
|
21
|
+
abort = new AbortController();
|
|
22
|
+
runTask;
|
|
23
|
+
generationFailed;
|
|
24
|
+
constructor(store, options, listener) {
|
|
25
|
+
this.store = store;
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.listener = listener;
|
|
28
|
+
}
|
|
29
|
+
/** Start one retry loop, with a fresh snapshot generation after every disconnect. */
|
|
30
|
+
start() { this.runTask ??= this.run(); }
|
|
31
|
+
/** Cancel retries and HTTP, close the socket, and wait for the loop to settle. */
|
|
32
|
+
async stop() {
|
|
33
|
+
this.abort.abort();
|
|
34
|
+
this.generationFailed?.(new Error('Client stopped'));
|
|
35
|
+
await this.current?.close();
|
|
36
|
+
await this.runTask;
|
|
37
|
+
}
|
|
38
|
+
/** @returns The connected client, or undefined while offline. */
|
|
39
|
+
client() { return this.current; }
|
|
40
|
+
/** @returns The connected client, or throws while offline. */
|
|
41
|
+
require() {
|
|
42
|
+
if (!this.current || !this.store.state.online)
|
|
43
|
+
throw new Error('Not connected');
|
|
44
|
+
return this.current;
|
|
45
|
+
}
|
|
46
|
+
/** @returns Whether the current generation applied its host baseline. */
|
|
47
|
+
online() { return this.store.state.online; }
|
|
48
|
+
/** @returns The client lifetime signal. */
|
|
49
|
+
signal() { return this.abort.signal; }
|
|
50
|
+
/** @returns The projection store of the current generation. */
|
|
51
|
+
telemetryView() { return this.telemetry; }
|
|
52
|
+
/** @returns Cached host running state, or undefined when never reported. */
|
|
53
|
+
runningFor(sessionId) { return this.runningUpdates.get(sessionId); }
|
|
54
|
+
/** @returns When this client first observed the session, for the elapsed-time fallback. */
|
|
55
|
+
observedAt(sessionId) { return this.observedRunningAt.get(sessionId); }
|
|
56
|
+
/** Record an observation start for a session this client just opened. */
|
|
57
|
+
observe(sessionId) { if (!this.observedRunningAt.has(sessionId))
|
|
58
|
+
this.observedRunningAt.set(sessionId, Date.now()); }
|
|
59
|
+
/** Fail the current generation, so the controller reopens a snapshot. */
|
|
60
|
+
fail(error) { this.generationFailed?.(error); }
|
|
61
|
+
/** Answer one retained host waterfall through the event-result endpoint. */
|
|
62
|
+
async reply(frame, outcome) {
|
|
63
|
+
await this.require().call('$events/result', { clientId: this.clientId, eventId: string(frame.eventId), outcome });
|
|
64
|
+
}
|
|
65
|
+
/** Run one generation, then retry with bounded jittered backoff until stopped. */
|
|
66
|
+
async run() {
|
|
67
|
+
let attempt = 0;
|
|
68
|
+
while (!this.abort.signal.aborted) {
|
|
69
|
+
this.runningUpdates.clear();
|
|
70
|
+
this.observedRunningAt.clear();
|
|
71
|
+
this.telemetry = new Telemetry(RETAINED_PROJECTIONS);
|
|
72
|
+
this.listener.begin();
|
|
73
|
+
const client = this.options.makeClient();
|
|
74
|
+
this.current = client;
|
|
75
|
+
try {
|
|
76
|
+
await this.options.authenticate(client);
|
|
77
|
+
await client.connect();
|
|
78
|
+
let fail;
|
|
79
|
+
const disconnected = new Promise(resolve => { fail = resolve; });
|
|
80
|
+
this.generationFailed = fail;
|
|
81
|
+
const ready = new Promise((resolve, reject) => {
|
|
82
|
+
const timer = setTimeout(() => reject(new Error('Host ready timed out')), client.timeoutMs);
|
|
83
|
+
client.subscribe('$events', {}, {
|
|
84
|
+
item: value => {
|
|
85
|
+
const frame = object(value);
|
|
86
|
+
if (frame.type === 'ready') {
|
|
87
|
+
this.clientId = string(frame.clientId);
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
resolve();
|
|
90
|
+
}
|
|
91
|
+
else if (frame.type === 'waterfall') {
|
|
92
|
+
if (this.listener.waterfall(frame))
|
|
93
|
+
return;
|
|
94
|
+
void client.call('$events/result', { clientId: this.clientId,
|
|
95
|
+
eventId: string(frame.eventId), outcome: { kind: 'next' } }).catch(error => fail(new Error(errorText(error))));
|
|
96
|
+
}
|
|
97
|
+
else if (frame.type === 'cancel') {
|
|
98
|
+
this.listener.cancelled(string(frame.eventId));
|
|
99
|
+
}
|
|
100
|
+
else if (frame.type === 'emit' && frame.event === 'api-session/status') {
|
|
101
|
+
this.acceptStatus(array(frame.args));
|
|
102
|
+
}
|
|
103
|
+
else if (frame.type === 'emit' && CATALOG_EVENTS.includes(String(frame.event))) {
|
|
104
|
+
this.listener.invalidated();
|
|
105
|
+
}
|
|
106
|
+
else if (frame.type === 'emit' && frame.event === 'api-session/error') {
|
|
107
|
+
const args = array(frame.args);
|
|
108
|
+
this.listener.error(args[0], args[1]);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
end: error => { clearTimeout(timer); const reason = error ?? new Error('Event stream ended'); reject(reason); fail(reason); },
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
await ready;
|
|
115
|
+
await new Promise((resolve, reject) => {
|
|
116
|
+
const timer = setTimeout(() => reject(new Error('Session control baseline timed out')), client.timeoutMs);
|
|
117
|
+
client.subscribe('session/control', {}, {
|
|
118
|
+
item: value => {
|
|
119
|
+
try {
|
|
120
|
+
this.telemetry.accept(value);
|
|
121
|
+
this.store.update({});
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
resolve();
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
reject(error);
|
|
128
|
+
fail(new Error(errorText(error)));
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
end: error => {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
if (error instanceof RemoteError && error.code === 'gateway/method-unavailable') {
|
|
134
|
+
this.store.update({ controlError: 'Live metrics unavailable on this host' });
|
|
135
|
+
resolve();
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
const reason = error ?? new Error('Session control stream ended');
|
|
139
|
+
reject(reason);
|
|
140
|
+
fail(reason);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
await this.listener.ready();
|
|
146
|
+
attempt = 0;
|
|
147
|
+
const error = await disconnected;
|
|
148
|
+
if (!this.abort.signal.aborted)
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (error instanceof AuthenticationRequired || error instanceof HttpError && [401, 403].includes(error.status)) {
|
|
153
|
+
this.store.update({ error: `${errorText(error)}. Set DSH_TOKEN and restart to log in.`, status: 'Login required' });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!this.abort.signal.aborted)
|
|
157
|
+
this.store.update({ error: errorText(error), status: 'Reconnecting…' });
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
this.generationFailed = undefined;
|
|
161
|
+
this.store.update({ online: false, pending: [] });
|
|
162
|
+
await client.close();
|
|
163
|
+
await this.listener.ended();
|
|
164
|
+
}
|
|
165
|
+
if (!this.abort.signal.aborted) {
|
|
166
|
+
try {
|
|
167
|
+
await delay(Math.min(500 * 2 ** attempt++, 10_000) * (0.8 + Math.random() * 0.4), undefined, { signal: this.abort.signal });
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (!this.abort.signal.aborted)
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** Apply one `api-session/status` notification to the running maps and the session view. */
|
|
177
|
+
acceptStatus(args) {
|
|
178
|
+
const sessionId = string(args[0]);
|
|
179
|
+
if (typeof args[1] !== 'boolean')
|
|
180
|
+
throw new Error('Invalid session running state');
|
|
181
|
+
if (args[1] && !this.runningUpdates.get(sessionId))
|
|
182
|
+
this.observedRunningAt.set(sessionId, Date.now());
|
|
183
|
+
if (!args[1])
|
|
184
|
+
this.observedRunningAt.delete(sessionId);
|
|
185
|
+
this.runningUpdates.set(sessionId, args[1]);
|
|
186
|
+
this.listener.status(sessionId, args[1]);
|
|
187
|
+
if (!args[1])
|
|
188
|
+
this.listener.idle();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/** Application facade: composes the connection, session, catalog and cost domains. */
|
|
2
|
+
import { Client } from '../transport/client.ts';
|
|
3
|
+
import { type Json, type ObjectValue } from '../transport/wire.ts';
|
|
4
|
+
import { type HistoryLimits } from '../session/memory.ts';
|
|
5
|
+
import { SessionController } from '../session/controller.ts';
|
|
6
|
+
import { CatalogController } from '../catalog/controller.ts';
|
|
7
|
+
import type { Telemetry } from '../session/telemetry.ts';
|
|
8
|
+
import type { Transcript } from '../session/transcript.ts';
|
|
9
|
+
import type { CostLedger } from '../cost/ledger.ts';
|
|
10
|
+
import { CostController } from '../cost/controller.ts';
|
|
11
|
+
import { ConnectionController, type ConnectionListener } from './connection.ts';
|
|
12
|
+
import { MemoryLog } from './memory-log.ts';
|
|
13
|
+
import { type ControllerStore, type State } from '../state.ts';
|
|
14
|
+
import type { HistorySearch, RemovalTarget } from '../session/types.ts';
|
|
15
|
+
/** Application facade over the domain controllers; the UI owns only this object.
|
|
16
|
+
*
|
|
17
|
+
* State lives here, connection generations live in `connection`, the selected session and its
|
|
18
|
+
* history live in `session`, model metadata lives in `catalog`, and billing lives in `cost`.
|
|
19
|
+
*/
|
|
20
|
+
export declare class Controller implements ControllerStore, ConnectionListener {
|
|
21
|
+
readonly base: string;
|
|
22
|
+
private readonly initialSession?;
|
|
23
|
+
readonly costs?: CostLedger | undefined;
|
|
24
|
+
readonly historyLimits: HistoryLimits;
|
|
25
|
+
readonly memoryLogPath?: string | undefined;
|
|
26
|
+
state: State;
|
|
27
|
+
/** Physical connection, retry loop and projection store. */
|
|
28
|
+
readonly connection: ConnectionController;
|
|
29
|
+
/** Selected session, follow stream, history and interactions. */
|
|
30
|
+
readonly session: SessionController;
|
|
31
|
+
/** Model routes and agent-preset metadata. */
|
|
32
|
+
readonly catalog: CatalogController;
|
|
33
|
+
/** Background billing scan; present only when a ledger was supplied. */
|
|
34
|
+
readonly cost: CostController | undefined;
|
|
35
|
+
/** Bounded runtime memory samples; present only when a log path was supplied. */
|
|
36
|
+
readonly memoryLog: MemoryLog | undefined;
|
|
37
|
+
private readonly observers;
|
|
38
|
+
private selector;
|
|
39
|
+
constructor(base: string, token: string | undefined, initialSession?: string | undefined, makeClient?: () => Client, authenticate?: (client: Client) => Promise<void>, costs?: CostLedger | undefined, historyLimits?: HistoryLimits, memoryLogPath?: string | undefined);
|
|
40
|
+
/** React-compatible state subscription. */
|
|
41
|
+
subscribe: (listener: () => void) => (() => void);
|
|
42
|
+
/** Snapshot identity changes only when the controller publishes. */
|
|
43
|
+
snapshot: () => State;
|
|
44
|
+
/** Current projection store; replaced at each connection generation. */
|
|
45
|
+
get telemetry(): Telemetry;
|
|
46
|
+
/** @returns The current selector generation. */
|
|
47
|
+
selection(): number;
|
|
48
|
+
/** Advance the selector generation when the selected workspace or session changes. */
|
|
49
|
+
bumpSelection(): void;
|
|
50
|
+
/** Publish a state patch, re-deriving the visible pending interactions.
|
|
51
|
+
* @param patch - Fields to replace on the current state.
|
|
52
|
+
*/
|
|
53
|
+
update(patch: Partial<State>): void;
|
|
54
|
+
/** Start one retry loop, with a fresh snapshot generation after every disconnect. */
|
|
55
|
+
start(): void;
|
|
56
|
+
/** Cancel retries and HTTP, close the socket, and release session and catalog work. */
|
|
57
|
+
stop(): Promise<void>;
|
|
58
|
+
/** Stop the selected turn and then close, so quitting does not leave host work running.
|
|
59
|
+
* An idle session stays untouched, and an in-flight cancellation is awaited rather than repeated.
|
|
60
|
+
*/
|
|
61
|
+
shutdown(): Promise<void>;
|
|
62
|
+
/** Run a UI operation and expose errors without destroying the current input.
|
|
63
|
+
* @param operation - Operation to run while the client is busy.
|
|
64
|
+
* @returns Whether the operation completed.
|
|
65
|
+
*/
|
|
66
|
+
perform(operation: () => Promise<void>): Promise<boolean>;
|
|
67
|
+
/** Read the counters one memory sample records; content never leaves as text.
|
|
68
|
+
*
|
|
69
|
+
* The retained transcript and the ledger are small in practice, so a sample also reads the two
|
|
70
|
+
* structures that grow with rendered content — the layout row cache and the bounded math and
|
|
71
|
+
* diagram cache — and the work the last cost scan re-read, which is the only timer here whose
|
|
72
|
+
* per-pass work scales with history. With a runtime that exposes `gc`, the sample also reports
|
|
73
|
+
* the heap after a forced collection, so retained state and uncollected garbage stay distinct.
|
|
74
|
+
*/
|
|
75
|
+
private memorySample;
|
|
76
|
+
/** Collect before reading the heap when the runtime exposes a collection.
|
|
77
|
+
* @returns Heap in use after the collection and how long it took, or undefined without `global.gc`.
|
|
78
|
+
*/
|
|
79
|
+
private forcedGc;
|
|
80
|
+
/** A new generation starts; drop generation-scoped domain state. */
|
|
81
|
+
begin(): void;
|
|
82
|
+
/** The event stream is ready and the control baseline is applied. */
|
|
83
|
+
ready(): Promise<void>;
|
|
84
|
+
/** The generation ended; invalidate session work and stop the scan. */
|
|
85
|
+
ended(): Promise<void>;
|
|
86
|
+
/** Deliver a host waterfall to the session domain.
|
|
87
|
+
* @param frame - One decoded waterfall frame.
|
|
88
|
+
* @returns Whether the session domain retained it.
|
|
89
|
+
*/
|
|
90
|
+
waterfall(frame: ObjectValue): boolean;
|
|
91
|
+
/** Drop a waterfall the host cancelled.
|
|
92
|
+
* @param eventId - Correlation id previously retained.
|
|
93
|
+
*/
|
|
94
|
+
cancelled(eventId: string): void;
|
|
95
|
+
/** Apply one host running-state notification.
|
|
96
|
+
* @param sessionId - Session whose state changed.
|
|
97
|
+
* @param running - Whether the host still runs that session.
|
|
98
|
+
*/
|
|
99
|
+
status(sessionId: string, running: boolean): void;
|
|
100
|
+
/** Surface a host-reported session error.
|
|
101
|
+
* @param sessionId - Session the host reported on.
|
|
102
|
+
* @param error - Error payload as delivered by the host.
|
|
103
|
+
*/
|
|
104
|
+
error(sessionId: unknown, error: unknown): void;
|
|
105
|
+
/** Reload the model catalog after a host settings, credential or adapter change. */
|
|
106
|
+
invalidated(): void;
|
|
107
|
+
/** Refresh billing after a turn finished. */
|
|
108
|
+
idle(): void;
|
|
109
|
+
/** @returns Host running state of the selected session. */
|
|
110
|
+
get running(): boolean;
|
|
111
|
+
/** @returns Current session title, falling back to the list title and then the ID. */
|
|
112
|
+
get sessionName(): string | undefined;
|
|
113
|
+
/** @returns Current agent-preset label. */
|
|
114
|
+
get sessionMode(): string | undefined;
|
|
115
|
+
/** @returns Epoch start of the active turn, when known. */
|
|
116
|
+
get workingSince(): number | undefined;
|
|
117
|
+
/** @returns Sessions accounted to the selected workspace, minus archived identities. */
|
|
118
|
+
get visibleSessions(): ObjectValue[];
|
|
119
|
+
/** Load the optional preset roster once per connection. */
|
|
120
|
+
loadPresetNames(): void;
|
|
121
|
+
/** @returns Host model routes and adapter-owned reasoning choices. */
|
|
122
|
+
modelCatalog(): Promise<ObjectValue>;
|
|
123
|
+
/** Select the next request's model.
|
|
124
|
+
* @param provider - Host provider route ID.
|
|
125
|
+
* @param model - Exact model ID.
|
|
126
|
+
* @param reasoningEffort - Optional adapter-owned effort ID.
|
|
127
|
+
*/
|
|
128
|
+
selectModel(provider: string, model: string, reasoningEffort?: string): Promise<void>;
|
|
129
|
+
/** Stop the selected turn, or allow exit only while idle.
|
|
130
|
+
* @param force - Send an explicit cancellation even when the cached running flag is idle.
|
|
131
|
+
* @returns True when the caller may exit.
|
|
132
|
+
*/
|
|
133
|
+
interrupt(force?: boolean): Promise<boolean>;
|
|
134
|
+
/** Keep history stable while the user reads, searches, or expands it.
|
|
135
|
+
* @param pinned - Whether the main transcript is being read away from its tail.
|
|
136
|
+
*/
|
|
137
|
+
pinHistory(pinned: boolean): void;
|
|
138
|
+
/** Refresh all HTTP-visible sessions without changing the selected conversation.
|
|
139
|
+
* @param signal - Optional cancellation for an explicit /cost refresh.
|
|
140
|
+
*/
|
|
141
|
+
refreshCosts(signal?: AbortSignal): Promise<void>;
|
|
142
|
+
/** Refresh both lists from the host, then show the requested picker.
|
|
143
|
+
* @param screen - Picker to display after the refresh.
|
|
144
|
+
*/
|
|
145
|
+
showPicker(screen: 'workspaces' | 'sessions'): Promise<void>;
|
|
146
|
+
/** Resolve a removal command to one reviewable object.
|
|
147
|
+
* @param kind - Workspace registration removal or session archival.
|
|
148
|
+
* @param query - Exact name, ID, or unambiguous ID prefix.
|
|
149
|
+
* @returns The fixed identity and display details for confirmation.
|
|
150
|
+
*/
|
|
151
|
+
removalTarget(kind: 'workspace' | 'session', query: string): Promise<RemovalTarget>;
|
|
152
|
+
/** Apply a confirmed removal or verified empty-session archival.
|
|
153
|
+
* @param target - Exact workspace or session identity reviewed by the user.
|
|
154
|
+
*/
|
|
155
|
+
removeTarget(target: RemovalTarget): Promise<void>;
|
|
156
|
+
/** Pick a workspace, or use all sessions when the identity is omitted.
|
|
157
|
+
* @param workspaceId - Workspace to select, if any.
|
|
158
|
+
*/
|
|
159
|
+
pickWorkspace(workspaceId?: string): void;
|
|
160
|
+
/** Open a workspace picker, or resolve a workspace target.
|
|
161
|
+
* @param query - Workspace target, if any.
|
|
162
|
+
*/
|
|
163
|
+
switchWorkspace(query?: string): Promise<void>;
|
|
164
|
+
/** Guide session selection, list all sessions with `all`, or resolve a target.
|
|
165
|
+
* @param query - Session target, `all`, or nothing for the guided picker.
|
|
166
|
+
*/
|
|
167
|
+
switchSession(query?: string): Promise<void>;
|
|
168
|
+
/** Prompt for a host path without starting a local agent. */
|
|
169
|
+
enterPath(): void;
|
|
170
|
+
/** Register a host directory and move to its session picker.
|
|
171
|
+
* @param path - Absolute directory path on the host.
|
|
172
|
+
*/
|
|
173
|
+
createWorkspace(path: string): Promise<void>;
|
|
174
|
+
/** Create a session in the selected workspace. */
|
|
175
|
+
createSession(): Promise<void>;
|
|
176
|
+
/** Replace the selected transcript and follow the session.
|
|
177
|
+
* @param sessionId - Session to follow.
|
|
178
|
+
*/
|
|
179
|
+
selectSession(sessionId: string): Promise<void>;
|
|
180
|
+
/** Wait for the selected follow snapshot.
|
|
181
|
+
* @param signal - Cancels waiting without closing the session.
|
|
182
|
+
*/
|
|
183
|
+
waitForHistory(signal: AbortSignal): Promise<void>;
|
|
184
|
+
/** Search host session results.
|
|
185
|
+
* @param query - Literal message text.
|
|
186
|
+
* @param workspaceOnly - Restrict hits to the selected workspace.
|
|
187
|
+
* @param signal - Cancels the HTTP search.
|
|
188
|
+
* @returns Session snippets and the global truncation flag.
|
|
189
|
+
*/
|
|
190
|
+
searchSessions(query: string, workspaceOnly: boolean, signal: AbortSignal): Promise<{
|
|
191
|
+
items: ObjectValue[];
|
|
192
|
+
hasMore: boolean;
|
|
193
|
+
}>;
|
|
194
|
+
/** Search host paths for the composer's `@` completion.
|
|
195
|
+
* @param query - Path text after @.
|
|
196
|
+
* @param signal - Cancels an obsolete lookup.
|
|
197
|
+
* @returns Validated candidates in host order.
|
|
198
|
+
*/
|
|
199
|
+
references(query: string, signal: AbortSignal): Promise<import("../session/references.ts").FileReference[]>;
|
|
200
|
+
/** Execute a human command directly, outside the model prompt queue.
|
|
201
|
+
* @param line - Complete slash command, including arguments.
|
|
202
|
+
* @param signal - Cancels the request while the host performs compaction.
|
|
203
|
+
* @returns The host's successful command result text.
|
|
204
|
+
*/
|
|
205
|
+
command(line: string, signal: AbortSignal): Promise<string>;
|
|
206
|
+
/** Remove one host-owned pending input.
|
|
207
|
+
* @param itemId - Queue occurrence identity from session/control.
|
|
208
|
+
*/
|
|
209
|
+
removeQueued(itemId: string): Promise<void>;
|
|
210
|
+
/** Export the selected host log to a new local ZIP file.
|
|
211
|
+
* @param path - Optional local destination; existing files are never overwritten.
|
|
212
|
+
* @param signal - Cancels the download and removes an incomplete file.
|
|
213
|
+
* @returns Absolute saved filename.
|
|
214
|
+
*/
|
|
215
|
+
exportLog(path: string | undefined, signal: AbortSignal): Promise<string>;
|
|
216
|
+
/** Save loaded Markdown, diagrams and math as offline HTML.
|
|
217
|
+
* @param path - Optional filename; existing files are not replaced.
|
|
218
|
+
* @param signal - Cancels the write.
|
|
219
|
+
* @returns Absolute saved filename.
|
|
220
|
+
*/
|
|
221
|
+
exportHtml(path: string | undefined, signal: AbortSignal): Promise<string>;
|
|
222
|
+
/** Admit text once as steering while running, or a new turn while idle.
|
|
223
|
+
* @param text - Composed prompt text.
|
|
224
|
+
*/
|
|
225
|
+
prompt(text: string): Promise<void>;
|
|
226
|
+
/** Cancel the active turn; pending queue items remain host-owned. */
|
|
227
|
+
cancelTurn(): Promise<void>;
|
|
228
|
+
/** Add a page before the retained window.
|
|
229
|
+
* @param signal - Cancels local paging without interrupting the remote agent.
|
|
230
|
+
* @param transcript - Transcript to extend; defaults to the live one.
|
|
231
|
+
*/
|
|
232
|
+
older(signal?: AbortSignal, transcript?: Transcript): Promise<void>;
|
|
233
|
+
/** Search the loaded history page by page.
|
|
234
|
+
* @param query - Literal, case-insensitive text including folded reasoning.
|
|
235
|
+
* @param signal - Cancels HTTP and processing without cancelling the agent.
|
|
236
|
+
* @returns Newest-first bounded summaries and an explicit truncation flag.
|
|
237
|
+
*/
|
|
238
|
+
searchHistory(query: string, signal: AbortSignal): Promise<HistorySearch>;
|
|
239
|
+
/** Load a separate small window ending at a search target.
|
|
240
|
+
* @param target - Durable message sequence to display.
|
|
241
|
+
* @param signal - Cancels the target-page request.
|
|
242
|
+
* @returns A caller-owned historical window that must be disposed when closed.
|
|
243
|
+
*/
|
|
244
|
+
historyAt(target: number, signal: AbortSignal): Promise<Transcript>;
|
|
245
|
+
/** Load the prefix required for an explicit history jump.
|
|
246
|
+
* @param target - Visible record sequence, or first for the oldest available history.
|
|
247
|
+
* @param signal - Cancels local paging without interrupting the remote agent.
|
|
248
|
+
*/
|
|
249
|
+
historyThrough(target: number | 'first', signal: AbortSignal): Promise<void>;
|
|
250
|
+
/** Answer the oldest selected-session interaction, after explicit user action.
|
|
251
|
+
* @param value - Structured answer value or approval outcome.
|
|
252
|
+
*/
|
|
253
|
+
answer(value: Json): Promise<void>;
|
|
254
|
+
/** Approve or reject the pending approval request.
|
|
255
|
+
* @param allowed - Whether the request is approved once.
|
|
256
|
+
*/
|
|
257
|
+
approve(allowed: boolean): Promise<void>;
|
|
258
|
+
}
|
|
259
|
+
export type { HistorySearch, RemovalTarget } from '../session/types.ts';
|
|
260
|
+
export type { State } from '../state.ts';
|