@itookit/dsht 0.3.8 → 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 +33 -12
- package/README.zh.md +35 -14
- 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 +206 -18
- package/dist/cli/startup.d.ts +40 -0
- package/dist/cli/startup.js +314 -0
- package/dist/cli/trace-summary.d.ts +78 -0
- package/dist/cli/trace-summary.js +241 -0
- package/dist/cli/verifier.d.ts +64 -0
- package/dist/cli/verifier.js +265 -0
- package/dist/contracts.d.ts +359 -0
- package/dist/contracts.js +1 -0
- package/dist/controller/commands.d.ts +47 -0
- package/dist/controller/commands.js +322 -0
- package/dist/controller/connection-streams.d.ts +22 -0
- package/dist/controller/connection-streams.js +105 -0
- package/dist/controller/connection.d.ts +24 -31
- package/dist/controller/connection.js +48 -111
- package/dist/controller/controller.d.ts +412 -178
- package/dist/controller/controller.js +713 -167
- package/dist/controller/foreground.d.ts +44 -0
- package/dist/controller/foreground.js +79 -0
- package/dist/controller/index.d.ts +8 -1
- package/dist/controller/index.js +5 -0
- package/dist/controller/loop-contract.d.ts +136 -0
- package/dist/controller/loop-contract.js +308 -0
- package/dist/controller/loop-coordinator.d.ts +48 -0
- package/dist/controller/loop-coordinator.js +647 -0
- package/dist/controller/loop-prompts-schema.d.ts +56 -0
- package/dist/controller/loop-prompts-schema.js +144 -0
- package/dist/controller/loop-prompts.d.ts +55 -0
- package/dist/controller/loop-prompts.generated.d.ts +104 -0
- package/dist/controller/loop-prompts.generated.js +185 -0
- package/dist/controller/loop-prompts.js +104 -0
- package/dist/controller/loop-protocols.d.ts +39 -0
- package/dist/controller/loop-protocols.js +115 -0
- package/dist/controller/loop.d.ts +275 -0
- package/dist/controller/loop.js +378 -0
- package/dist/controller/prompts.d.ts +54 -0
- package/dist/controller/prompts.js +162 -0
- package/dist/controller/trace-log.d.ts +45 -0
- package/dist/controller/trace-log.js +144 -0
- package/dist/controller/verifier.d.ts +130 -0
- package/dist/controller/verifier.js +75 -0
- package/dist/cost/controller.d.ts +1 -1
- package/dist/cost/controller.js +12 -5
- package/dist/cost/index.d.ts +1 -1
- package/dist/cost/index.js +1 -1
- package/dist/cost/ledger.d.ts +0 -1
- package/dist/cost/ledger.js +0 -1
- package/dist/cost/scanner.js +1 -0
- package/dist/json.d.ts +18 -0
- package/dist/json.js +19 -0
- package/dist/references.d.ts +25 -0
- package/dist/references.js +26 -0
- package/dist/session/connection-view.d.ts +2 -11
- package/dist/session/controller.d.ts +94 -105
- package/dist/session/controller.js +262 -536
- package/dist/session/history-reader.d.ts +32 -0
- package/dist/session/history-reader.js +170 -0
- package/dist/session/history.d.ts +6 -18
- package/dist/session/history.js +1 -24
- package/dist/session/index.d.ts +9 -4
- package/dist/session/index.js +7 -3
- package/dist/session/info.d.ts +20 -82
- package/dist/session/info.js +52 -25
- package/dist/session/interactions.d.ts +26 -0
- package/dist/session/interactions.js +75 -0
- package/dist/session/markdown.js +1 -1
- package/dist/session/math.js +1 -1
- package/dist/session/mutation-gate.d.ts +51 -0
- package/dist/session/mutation-gate.js +73 -0
- package/dist/session/navigation.d.ts +2 -89
- package/dist/session/navigation.js +2 -129
- package/dist/session/navigator.d.ts +47 -0
- package/dist/session/navigator.js +158 -0
- package/dist/session/peek.d.ts +38 -0
- package/dist/session/peek.js +103 -0
- package/dist/session/prompt-backfill.d.ts +23 -0
- package/dist/session/prompt-backfill.js +88 -0
- package/dist/session/references.d.ts +2 -20
- package/dist/session/references.js +1 -26
- package/dist/session/runtime.d.ts +26 -0
- package/dist/session/runtime.js +28 -0
- package/dist/session/state.d.ts +20 -0
- package/dist/session/state.js +1 -0
- package/dist/session/telemetry.d.ts +25 -17
- package/dist/session/telemetry.js +66 -60
- package/dist/session/transcript.d.ts +5 -7
- package/dist/session/transcript.js +2 -15
- package/dist/session/types.d.ts +25 -0
- package/dist/session/types.js +0 -1
- package/dist/session-title.d.ts +9 -0
- package/dist/session-title.js +21 -0
- package/dist/shell/controller.d.ts +31 -1
- package/dist/shell/controller.js +34 -2
- package/dist/shell/index.d.ts +3 -3
- package/dist/shell/index.js +2 -2
- package/dist/shell/runner.d.ts +10 -0
- package/dist/shell/runner.js +48 -9
- package/dist/slash/index.d.ts +10 -0
- package/dist/slash/index.js +7 -0
- package/dist/slash/parse.d.ts +42 -0
- package/dist/slash/parse.js +259 -0
- package/dist/slash/pipeline.d.ts +140 -0
- package/dist/slash/pipeline.js +115 -0
- package/dist/slash/registry.d.ts +88 -0
- package/dist/slash/registry.js +177 -0
- package/dist/slash/types.d.ts +126 -0
- package/dist/slash/types.js +1 -0
- package/dist/state.d.ts +16 -18
- package/dist/state.js +4 -3
- package/dist/text.d.ts +28 -0
- package/dist/text.js +55 -0
- package/dist/transport/client.d.ts +4 -3
- package/dist/transport/client.js +71 -25
- package/dist/transport/events.d.ts +104 -0
- package/dist/transport/events.js +149 -0
- package/dist/transport/wire.d.ts +9 -17
- package/dist/transport/wire.js +2 -27
- package/dist/ui/app.js +750 -550
- package/dist/ui/chat/header.js +1 -1
- package/dist/ui/chat/history-view.d.ts +1 -1
- package/dist/ui/chat/loop-status.d.ts +11 -0
- package/dist/ui/chat/loop-status.js +28 -0
- package/dist/ui/chat/navigation-model.d.ts +86 -0
- package/dist/ui/chat/navigation-model.js +107 -0
- package/dist/ui/chat/shell-view.d.ts +17 -2
- package/dist/ui/chat/shell-view.js +45 -3
- package/dist/ui/chat/status.d.ts +47 -3
- package/dist/ui/chat/status.js +65 -50
- package/dist/ui/chat/use-history-view.d.ts +69 -0
- package/dist/ui/chat/use-history-view.js +123 -0
- package/dist/ui/chat/viewport.d.ts +1 -1
- package/dist/ui/dialogs/cost.d.ts +21 -4
- package/dist/ui/dialogs/cost.js +7 -12
- package/dist/ui/dialogs/index.d.ts +22 -5
- package/dist/ui/dialogs/index.js +19 -3
- package/dist/ui/dialogs/loop.d.ts +43 -0
- package/dist/ui/dialogs/loop.js +224 -0
- package/dist/ui/dialogs/peek.d.ts +25 -0
- package/dist/ui/dialogs/peek.js +35 -0
- package/dist/ui/dialogs/picker.d.ts +2 -0
- package/dist/ui/dialogs/picker.js +4 -2
- package/dist/ui/dialogs/use-panels.d.ts +53 -0
- package/dist/ui/dialogs/use-panels.js +51 -0
- package/dist/ui/input/mouse.d.ts +12 -2
- package/dist/ui/input/mouse.js +20 -7
- package/dist/ui/input/references.d.ts +1 -1
- 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/dist/ui/status/model.d.ts +7 -0
- package/dist/ui/status/model.js +5 -0
- package/dist/ui/theme/index.d.ts +1 -1
- package/package.json +6 -4
- package/dist/ui/commands/parse.d.ts +0 -104
- package/dist/ui/commands/parse.js +0 -135
- package/dist/ui/commands/registry.d.ts +0 -33
- package/dist/ui/commands/registry.js +0 -73
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { LOOP_USAGE } from "../slash/index.js";
|
|
2
|
+
import { errorText } from "../transport/wire.js";
|
|
3
|
+
import { loopProtocolFor, loopProtocolNames, loopRecordVars } from "./loop-protocols.js";
|
|
4
|
+
import { resolveLoop } from "./loop.js";
|
|
5
|
+
/** An accepted line: apply these effects in order, then clear the draft. */
|
|
6
|
+
function ok(effects) {
|
|
7
|
+
return { disposition: 'consume', outcome: 'ok', effects };
|
|
8
|
+
}
|
|
9
|
+
/** A refusal the operator can read; the draft — and any form on screen — stays where it is.
|
|
10
|
+
*
|
|
11
|
+
* A line that never ran does not rearrange the screen: closing panels is a view transition belonging
|
|
12
|
+
* to a command that executed, and doing it here would throw away a form the reader is still editing
|
|
13
|
+
* (the `/loop` parameter form is the case that found this).
|
|
14
|
+
* @param text - Failure line, applied last so the other effects cannot replace it.
|
|
15
|
+
* @param effects - What else to do first, when a caller really does want a surface closed.
|
|
16
|
+
*/
|
|
17
|
+
function refused(text, effects = []) {
|
|
18
|
+
return { disposition: 'retain', outcome: 'rejected', effects: [...effects, { kind: 'error', text }] };
|
|
19
|
+
}
|
|
20
|
+
/** An operation the caller aborted (Esc): nothing to show, and nothing to clear. */
|
|
21
|
+
function cancelled() {
|
|
22
|
+
return { disposition: 'retain', outcome: 'cancelled', effects: [] };
|
|
23
|
+
}
|
|
24
|
+
/** One cancellable port call, reporting whether the caller aborted it.
|
|
25
|
+
*
|
|
26
|
+
* `undefined` from the port means "no value" for two different reasons, and the result type must not
|
|
27
|
+
* confuse them: a refusal to start is not an interruption the operator caused.
|
|
28
|
+
*/
|
|
29
|
+
async function runCancellable(port, label, operation) {
|
|
30
|
+
let signal;
|
|
31
|
+
const value = await port.run(label, inner => { signal = inner; return operation(inner); });
|
|
32
|
+
return { value, cancelled: value === undefined && signal?.aborted === true };
|
|
33
|
+
}
|
|
34
|
+
/** Resolve a removal into either a finished removal or the confirmation the UI must show.
|
|
35
|
+
*
|
|
36
|
+
* Shared by the `/ws --delete`/`/resume --delete` commands and the pickers' `d` key, so the
|
|
37
|
+
* empty-session rule has one implementation.
|
|
38
|
+
* @param controller - Application facade the removal acts on.
|
|
39
|
+
* @param kind - Workspace registration or session archival.
|
|
40
|
+
* @param query - Exact name, ID, or unambiguous ID prefix.
|
|
41
|
+
* @returns The effects to apply, or undefined when the target could not be resolved.
|
|
42
|
+
*/
|
|
43
|
+
export async function removalIntent(controller, kind, query) {
|
|
44
|
+
const target = await controller.actions.removalTarget(kind, query);
|
|
45
|
+
if (target === undefined)
|
|
46
|
+
return undefined;
|
|
47
|
+
// An empty session carries no history, so it is archived without a review step.
|
|
48
|
+
if (target.kind === 'session' && target.empty) {
|
|
49
|
+
if (!await controller.actions.removeTarget(target))
|
|
50
|
+
return undefined;
|
|
51
|
+
return [{ kind: 'closePanels' }];
|
|
52
|
+
}
|
|
53
|
+
return [{ kind: 'closePanels' }, { kind: 'open', panel: 'removal' }, { kind: 'removal', removal: target }];
|
|
54
|
+
}
|
|
55
|
+
/** Run one submitted line and describe what it did and what the front end must show.
|
|
56
|
+
*
|
|
57
|
+
* This is the single entry point every front end uses — the composer and the scripted startup runner
|
|
58
|
+
* alike — so one line has one effect and one trace span wherever it came from.
|
|
59
|
+
* @param controller - Application facade the command acts on.
|
|
60
|
+
* @param command - Executable line; the front-end modes (`ignore`, `reference`) never reach here.
|
|
61
|
+
* @param port - Cancellable-operation port supplied by the caller.
|
|
62
|
+
* @returns The result, or undefined when the application did not accept the line.
|
|
63
|
+
*/
|
|
64
|
+
export async function runCommand(controller, command, port) {
|
|
65
|
+
// A finished run's progress line is a result the reader may still be reading; running this line means
|
|
66
|
+
// they are done with it. A run that is still active is never dropped here, so `/loop answer` and
|
|
67
|
+
// `/loop stop` keep their target — and this runs before the line's own effects, so the line that
|
|
68
|
+
// ends a run still leaves its terminal progress on screen to be read.
|
|
69
|
+
controller.actions.clearLoopResult();
|
|
70
|
+
// One span per executed line, with the id every other event of this line can name. The begin is
|
|
71
|
+
// written before any effect, so a crash mid-command still shows that the executor was entered.
|
|
72
|
+
const commandId = controller.nextCommandId();
|
|
73
|
+
controller.traceNote('command', { phase: 'begin', commandId, kind: command.kind });
|
|
74
|
+
// What the action envelope said before this line ran: a failure it records during the line is this
|
|
75
|
+
// line's own, and the result below is where the operator reads it.
|
|
76
|
+
const envelopeBefore = controller.state.lastFailure;
|
|
77
|
+
let result;
|
|
78
|
+
try {
|
|
79
|
+
result = await execute(controller, command, port);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
// The end must be written whatever happened, or one begin would never have its end.
|
|
83
|
+
controller.traceNote('command', { phase: 'end', commandId, kind: command.kind, outcome: 'failed',
|
|
84
|
+
disposition: 'retain', error: errorText(error).slice(0, 200) });
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
// The end is the one place that answers "how did this line end", whatever the command, so no command
|
|
88
|
+
// has to invent its own outcome name — and the outcome here is the same fact the reader is shown.
|
|
89
|
+
const failure = result?.effects.find(effect => effect.kind === 'error');
|
|
90
|
+
controller.traceNote('command', { phase: 'end', commandId, kind: command.kind,
|
|
91
|
+
outcome: result?.outcome ?? 'refused', disposition: result?.disposition ?? 'retain',
|
|
92
|
+
...(failure?.kind === 'error' ? { error: failure.text.slice(0, 200) } : {}) });
|
|
93
|
+
// One fact, one channel (13.2-D2): a failure this line already reported in its result must not stay
|
|
94
|
+
// in the action envelope and be shown a second time in the status bar. An unaccepted line keeps it —
|
|
95
|
+
// there the envelope is the only explanation the operator has.
|
|
96
|
+
if (result !== undefined && result.outcome !== 'ok' && controller.state.lastFailure !== envelopeBefore) {
|
|
97
|
+
controller.actions.clearFailure();
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
/** Dispatch one executable line to its application policy.
|
|
102
|
+
*
|
|
103
|
+
* Every branch is application policy: which action to call, which notice to show, and which panel the
|
|
104
|
+
* result belongs to. The `ui` layer only applies the returned effects to component state.
|
|
105
|
+
* @param controller - Application facade the command acts on.
|
|
106
|
+
* @param command - Executable line.
|
|
107
|
+
* @param port - Cancellable-operation port supplied by the caller.
|
|
108
|
+
* @returns The result, or undefined when the application did not accept the line.
|
|
109
|
+
*/
|
|
110
|
+
async function execute(controller, command, port) {
|
|
111
|
+
switch (command.kind) {
|
|
112
|
+
case 'quit': return ok([{ kind: 'quit' }]);
|
|
113
|
+
// Copy mode freezes the display, so panels are deliberately left as they are.
|
|
114
|
+
case 'copy': return ok([{ kind: 'copy' }]);
|
|
115
|
+
case 'panel': {
|
|
116
|
+
// The cost panel is the one panel whose rows are a host request. Opening it starts that request
|
|
117
|
+
// here, where the effect lives, rather than in the front end: the UI only shows its loading label.
|
|
118
|
+
if (command.panel === 'cost') {
|
|
119
|
+
void port.run('Refreshing costs…', signal => controller.actions.refreshCosts(signal)).catch(() => undefined);
|
|
120
|
+
}
|
|
121
|
+
return ok([{ kind: 'closePanels' }, { kind: 'toggle', panel: command.panel }]);
|
|
122
|
+
}
|
|
123
|
+
case 'remove': {
|
|
124
|
+
const effects = await removalIntent(controller, command.target, command.query);
|
|
125
|
+
return effects === undefined ? undefined : ok(effects);
|
|
126
|
+
}
|
|
127
|
+
case 'navigate': {
|
|
128
|
+
const accepted = command.target === 'workspace'
|
|
129
|
+
? await controller.actions.switchWorkspace(command.query)
|
|
130
|
+
: await controller.actions.switchSession(command.query);
|
|
131
|
+
return accepted ? ok([{ kind: 'closePanels' }, { kind: 'scroll', position: 0 }]) : undefined;
|
|
132
|
+
}
|
|
133
|
+
case 'path':
|
|
134
|
+
return await controller.actions.createWorkspace(command.value) ? ok([{ kind: 'closePanels' }]) : undefined;
|
|
135
|
+
case 'latest':
|
|
136
|
+
return ok([{ kind: 'closePanels' }, { kind: 'live' }, { kind: 'pinLive' }, { kind: 'resetFolds' }, { kind: 'scroll', position: 0 }]);
|
|
137
|
+
case 'models': {
|
|
138
|
+
if (!command.args.length) {
|
|
139
|
+
const catalog = await controller.actions.modelCatalog();
|
|
140
|
+
return catalog === undefined ? undefined : ok([{ kind: 'closePanels' }, { kind: 'model', model: { catalog } }]);
|
|
141
|
+
}
|
|
142
|
+
const accepted = await controller.actions.selectModel(command.args[0], command.args[1], command.args[2]);
|
|
143
|
+
return accepted ? ok([{ kind: 'closePanels' }, { kind: 'close', panel: 'model' }]) : undefined;
|
|
144
|
+
}
|
|
145
|
+
case 'queue': return ok([{ kind: 'closePanels' }, { kind: 'open', panel: 'queue' }]);
|
|
146
|
+
case 'prompts': return ok([{ kind: 'closePanels' }, { kind: 'open', panel: 'prompts' }]);
|
|
147
|
+
case 'savePrompt':
|
|
148
|
+
return await controller.actions.savePrompt(command.text)
|
|
149
|
+
? ok([{ kind: 'closePanels' }, { kind: 'notice', text: 'Saved prompt' }]) : undefined;
|
|
150
|
+
case 'shell':
|
|
151
|
+
controller.shell.start(command.command);
|
|
152
|
+
return ok([{ kind: 'closePanels' }, { kind: 'scroll', position: 0 }]);
|
|
153
|
+
case 'newSession':
|
|
154
|
+
return await controller.actions.createSession() ? ok([{ kind: 'closePanels' }]) : undefined;
|
|
155
|
+
case 'history':
|
|
156
|
+
return ok([{ kind: 'closePanels' }, { kind: 'history', history: { query: command.query, contentSearch: false } }]);
|
|
157
|
+
case 'sessionSearch': {
|
|
158
|
+
const { value: result, cancelled: aborted } = await runCancellable(port, 'Searching sessions…', signal => controller.actions.searchSessions(command.query, command.command === '/ssearch', signal));
|
|
159
|
+
if (result === undefined)
|
|
160
|
+
return aborted ? cancelled() : undefined;
|
|
161
|
+
return ok([{ kind: 'closePanels' }, { kind: 'search', search: { query: command.query, ...result } }]);
|
|
162
|
+
}
|
|
163
|
+
case 'historySearch': {
|
|
164
|
+
const { value: matches, cancelled: aborted } = await runCancellable(port, 'Searching history…', signal => controller.actions.searchHistory(command.query, signal));
|
|
165
|
+
if (matches === undefined)
|
|
166
|
+
return aborted ? cancelled() : undefined;
|
|
167
|
+
return ok([{ kind: 'closePanels' }, { kind: 'history', history: { query: command.query, contentSearch: true, matches } }]);
|
|
168
|
+
}
|
|
169
|
+
case 'think': {
|
|
170
|
+
if (command.target === 'live') {
|
|
171
|
+
return ok([{ kind: 'closePanels' }, { kind: 'toggleLiveReasoning' }, { kind: 'scroll', position: 0 }]);
|
|
172
|
+
}
|
|
173
|
+
if (!command.target)
|
|
174
|
+
return ok([{ kind: 'closePanels' }, { kind: 'open', panel: 'thoughts' }]);
|
|
175
|
+
const seq = Number(command.target);
|
|
176
|
+
const transcript = controller.queries.window ?? controller.queries.record;
|
|
177
|
+
if (!Number.isSafeInteger(seq) || !transcript.thoughts.some(entry => entry.seq === seq)) {
|
|
178
|
+
throw new Error('Use /think <message sequence> for a loaded reasoning block');
|
|
179
|
+
}
|
|
180
|
+
return ok([{ kind: 'closePanels' }, { kind: 'toggleFold', seq }]);
|
|
181
|
+
}
|
|
182
|
+
case 'older': {
|
|
183
|
+
const accepted = await controller.actions.older(undefined, controller.queries.window ?? controller.queries.record);
|
|
184
|
+
return accepted ? ok([{ kind: 'closePanels' }, { kind: 'scrollBy', delta: 10 }]) : undefined;
|
|
185
|
+
}
|
|
186
|
+
case 'compact': {
|
|
187
|
+
const { value: text, cancelled: aborted } = await runCancellable(port, 'Compacting history…', signal => controller.actions.command('/compact', signal));
|
|
188
|
+
if (text === undefined)
|
|
189
|
+
return aborted ? cancelled() : undefined;
|
|
190
|
+
return ok([{ kind: 'closePanels' }, { kind: 'notice', text }]);
|
|
191
|
+
}
|
|
192
|
+
case 'cancel':
|
|
193
|
+
return await controller.actions.cancelTurn() ? ok([{ kind: 'closePanels' }]) : undefined;
|
|
194
|
+
case 'approval':
|
|
195
|
+
return await controller.actions.approve(command.allowed) ? ok([{ kind: 'closePanels' }]) : undefined;
|
|
196
|
+
case 'hostCommand': {
|
|
197
|
+
const { value: text, cancelled: aborted } = await runCancellable(port, 'Running command…', signal => controller.actions.command(command.line, signal));
|
|
198
|
+
if (text === undefined)
|
|
199
|
+
return aborted ? cancelled() : undefined;
|
|
200
|
+
return ok([{ kind: 'closePanels' }, { kind: 'notice', text }]);
|
|
201
|
+
}
|
|
202
|
+
case 'export': {
|
|
203
|
+
const { value: saved, cancelled: aborted } = await runCancellable(port, 'Exporting session log…', signal => controller.actions.exportLog(command.destination, signal));
|
|
204
|
+
if (saved === undefined)
|
|
205
|
+
return aborted ? cancelled() : undefined;
|
|
206
|
+
return ok([{ kind: 'closePanels' }, { kind: 'notice', text: `Saved session log: ${saved}` }]);
|
|
207
|
+
}
|
|
208
|
+
case 'exportHtml': {
|
|
209
|
+
const { value: saved, cancelled: aborted } = await runCancellable(port, 'Exporting loaded conversation…', signal => controller.actions.exportHtml(command.destination, signal));
|
|
210
|
+
if (saved === undefined)
|
|
211
|
+
return aborted ? cancelled() : undefined;
|
|
212
|
+
return ok([{ kind: 'closePanels' }, { kind: 'notice', text: `Saved loaded conversation: ${saved}` }]);
|
|
213
|
+
}
|
|
214
|
+
case 'coredump': {
|
|
215
|
+
// V8 serializes the heap synchronously, so the client stalls until the file is written.
|
|
216
|
+
let saved = '';
|
|
217
|
+
await port.run('Writing heap snapshot…', async () => { saved = controller.actions.heapSnapshot(command.tag); });
|
|
218
|
+
return ok([{ kind: 'closePanels' }, { kind: 'notice', text: `Heap snapshot saved: ${saved}` }]);
|
|
219
|
+
}
|
|
220
|
+
case 'answer': {
|
|
221
|
+
// The application completes the question waterfall itself, so a line that answers one never
|
|
222
|
+
// leaves the front end to call `answer` on its own.
|
|
223
|
+
const accepted = await controller.actions.answerQuestion({ custom: command.text });
|
|
224
|
+
return accepted ? ok([{ kind: 'closePanels' }]) : undefined;
|
|
225
|
+
}
|
|
226
|
+
case 'error': return refused(command.message);
|
|
227
|
+
case 'prompt': {
|
|
228
|
+
const accepted = await controller.actions.prompt(command.text);
|
|
229
|
+
return accepted ? ok([{ kind: 'closePanels' }, { kind: 'live' }, { kind: 'scroll', position: 0 }]) : undefined;
|
|
230
|
+
}
|
|
231
|
+
case 'handoff': {
|
|
232
|
+
if (!await controller.actions.handoff())
|
|
233
|
+
return undefined;
|
|
234
|
+
return ok([{ kind: 'closePanels' }, { kind: 'live' }, { kind: 'scroll', position: 0 },
|
|
235
|
+
{ kind: 'notice', text: 'Handoff requested · local HANDOFF.md cleared' }]);
|
|
236
|
+
}
|
|
237
|
+
case 'loops':
|
|
238
|
+
// The record list is a composer surface the UI offers while the name is typed; a line that
|
|
239
|
+
// still reaches here has nobody to choose for it, so it is told to name one.
|
|
240
|
+
return refused(`Use /loop <name> · available: ${loopProtocolNames().join(', ')}`);
|
|
241
|
+
case 'loopAnswer': {
|
|
242
|
+
const progress = controller.queries.loop;
|
|
243
|
+
const interaction = progress?.interaction;
|
|
244
|
+
// The answer only means something to a judgment that asked for one; a host question is answered
|
|
245
|
+
// in its own dialog, and a rejected send retries on its own. Saying which is which beats a
|
|
246
|
+
// command that appears to do nothing.
|
|
247
|
+
if (progress === undefined || !progress.active || progress.phase !== 'needs-human' || interaction?.kind !== 'verdict') {
|
|
248
|
+
return refused(interaction === undefined
|
|
249
|
+
? 'No loop is waiting for an answer'
|
|
250
|
+
: `This run is waiting for a ${interaction.kind}: ${interaction.text} · answer it, or /loop abort`);
|
|
251
|
+
}
|
|
252
|
+
const accepted = await controller.actions.answerLoop(command.text);
|
|
253
|
+
return accepted
|
|
254
|
+
? ok([{ kind: 'closePanels' }, { kind: 'live' }, { kind: 'scroll', position: 0 },
|
|
255
|
+
{ kind: 'notice', text: `Answer added · re-judging step ${progress.step}` }])
|
|
256
|
+
: undefined;
|
|
257
|
+
}
|
|
258
|
+
case 'loopStop': {
|
|
259
|
+
// Stopping is a control command, so it is answered from the application's own view of the run
|
|
260
|
+
// rather than refused when nothing runs; a no-op that says so beats silence.
|
|
261
|
+
const progress = controller.queries.loop;
|
|
262
|
+
if (progress === undefined || !progress.active)
|
|
263
|
+
return ok([{ kind: 'closePanels' }, { kind: 'notice', text: 'No loop is running' }]);
|
|
264
|
+
controller.actions.stopLoop();
|
|
265
|
+
return ok([{ kind: 'closePanels' }, { kind: 'live' }, { kind: 'scroll', position: 0 },
|
|
266
|
+
{ kind: 'notice', text: `Loop stopped · ${progress.title}` }]);
|
|
267
|
+
}
|
|
268
|
+
case 'loop': {
|
|
269
|
+
// One command for every record: the name is looked up here, where the records are known, so
|
|
270
|
+
// the syntax layer never needs a table of protocols and a new record needs no code at all.
|
|
271
|
+
controller.traceNote('loop', { phase: 'command', name: command.name, interactive: port.interactive === true,
|
|
272
|
+
flags: Object.keys(command.options).filter(key => key !== 'vars'), vars: Object.keys(command.options.vars ?? {}) });
|
|
273
|
+
const declared = loopRecordVars(command.name);
|
|
274
|
+
if (declared === undefined) {
|
|
275
|
+
controller.traceNote('loop', { phase: 'rejected', name: command.name, reason: 'unknown-record' });
|
|
276
|
+
return refused(`Unknown loop record: ${command.name} · available: ${loopProtocolNames().join(', ')}`);
|
|
277
|
+
}
|
|
278
|
+
// A record variable only exists if the record declares it; retargeting is not a place to guess.
|
|
279
|
+
const vars = command.options.vars ?? {};
|
|
280
|
+
const unknown = Object.keys(vars).filter(name => !(name in declared));
|
|
281
|
+
if (unknown.length) {
|
|
282
|
+
const names = Object.keys(declared);
|
|
283
|
+
controller.traceNote('loop', { phase: 'rejected', name: command.name, reason: 'unknown-vars', vars: unknown });
|
|
284
|
+
return refused(`Unknown loop variable: ${unknown.join(', ')}`
|
|
285
|
+
+ ` · ${command.name} accepts: ${names.length ? names.join(', ') : 'none'}`);
|
|
286
|
+
}
|
|
287
|
+
// Any value the operator already chose — a flag, or a variable the form confirmed — means the
|
|
288
|
+
// decision is made; only a bare known record leaves every default open for the form.
|
|
289
|
+
const decided = command.options.from !== undefined || command.options.to !== undefined
|
|
290
|
+
|| command.options.score !== undefined || command.options.tries !== undefined
|
|
291
|
+
|| Object.keys(vars).length > 0;
|
|
292
|
+
if (port.interactive === true && !decided) {
|
|
293
|
+
controller.traceNote('loop', { phase: 'form', name: command.name });
|
|
294
|
+
return ok([{ kind: 'closePanels' }, { kind: 'loop', loop: { name: command.name } }]);
|
|
295
|
+
}
|
|
296
|
+
const protocol = loopProtocolFor(command.name, controller.queries.forkedVerification, vars, controller.queries.selfScoring);
|
|
297
|
+
if (protocol === undefined)
|
|
298
|
+
return refused(LOOP_USAGE);
|
|
299
|
+
const limits = resolveLoop(protocol, command.options);
|
|
300
|
+
if (limits === undefined)
|
|
301
|
+
return refused(LOOP_USAGE);
|
|
302
|
+
if (!await controller.actions.startLoop(protocol, limits)) {
|
|
303
|
+
// `startLoop` already traced its own refusal; this turns the same fact into something the
|
|
304
|
+
// operator can read, instead of a form that appears to ignore Start. The form stays on screen
|
|
305
|
+
// so the values can be retried.
|
|
306
|
+
const why = controller.state.lastFailure
|
|
307
|
+
|| (controller.state.online ? 'the host did not accept the request' : 'the client is offline');
|
|
308
|
+
controller.traceNote('loop', { phase: 'not-started', name: command.name, why: why.slice(0, 200) });
|
|
309
|
+
return refused(`Loop did not start: ${why}`);
|
|
310
|
+
}
|
|
311
|
+
// The run leaves a bar in the transcript where it started, so the loop is readable in place and
|
|
312
|
+
// the bar can open the verification session this run is using (`Controller.createVerifierSession`).
|
|
313
|
+
// A record that starts by verifying (`starts: verify`) already created that session inside
|
|
314
|
+
// `startLoop`, so the bar takes the newest one now and later checks re-point it through `link`.
|
|
315
|
+
const checking = controller.queries.sources.find(source => source.createdBy === 'verifier' && source.state === 'running');
|
|
316
|
+
controller.shell.note(`/loop ${command.name} ${limits.from}–${limits.to} · pass ${limits.score} · ≤${limits.tries} tries`, checking?.id);
|
|
317
|
+
return ok([{ kind: 'closePanels' }, { kind: 'live' }, { kind: 'scroll', position: 0 },
|
|
318
|
+
{ kind: 'notice', text: `${protocol.title} started · steps ${limits.from}–${limits.to} · pass ${limits.score} · ≤${limits.tries} tries` }]);
|
|
319
|
+
}
|
|
320
|
+
default: return undefined;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Event/control subscriptions and their startup deadlines belong to one connection generation. */
|
|
2
|
+
import { Client } from '../transport/client.ts';
|
|
3
|
+
import { type HostEvent } from '../transport/events.ts';
|
|
4
|
+
export interface StreamHost {
|
|
5
|
+
identified(clientId: string): void;
|
|
6
|
+
event(event: HostEvent): boolean;
|
|
7
|
+
changed(): void;
|
|
8
|
+
degraded(message: string): void;
|
|
9
|
+
fail(error: Error): void;
|
|
10
|
+
}
|
|
11
|
+
export declare class ConnectionStreams {
|
|
12
|
+
private readonly client;
|
|
13
|
+
private readonly host;
|
|
14
|
+
private readonly subscriptions;
|
|
15
|
+
private readonly abort;
|
|
16
|
+
private readonly signal;
|
|
17
|
+
constructor(client: Client, host: StreamHost, signal: AbortSignal);
|
|
18
|
+
start(): Promise<void>;
|
|
19
|
+
close(): void;
|
|
20
|
+
/** Resolve the first usable frame while retaining the stream until this generation ends. */
|
|
21
|
+
private follow;
|
|
22
|
+
}
|
|
@@ -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
|
-
import { type
|
|
2
|
+
import { type Json } from '../transport/wire.ts';
|
|
3
|
+
import type { HostEvent } from '../transport/events.ts';
|
|
3
4
|
import type { HostAccess } from '../transport/host.ts';
|
|
4
|
-
import { Telemetry } from '../session/telemetry.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;
|
|
@@ -22,33 +33,25 @@ export interface ConnectionListener {
|
|
|
22
33
|
ready(): Promise<void>;
|
|
23
34
|
/** The generation ended and its socket is closed. */
|
|
24
35
|
ended(): Promise<void>;
|
|
25
|
-
/**
|
|
26
|
-
|
|
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;
|
|
36
|
+
/** Route one normalized host event, so a feature never calls another feature directly. */
|
|
37
|
+
event(event: HostEvent): boolean;
|
|
37
38
|
}
|
|
38
|
-
/** Owns reconnects, subscriptions and
|
|
39
|
+
/** Owns the physical connection: reconnects, subscriptions and generation lifecycle.
|
|
40
|
+
*
|
|
41
|
+
* Host running state, projections and interactions belong to the session domain; this class only
|
|
42
|
+
* decodes wire frames into `HostEvent` and hands them to the application, which routes them on.
|
|
43
|
+
* User commands remain single-attempt operations.
|
|
44
|
+
*/
|
|
39
45
|
export declare class ConnectionController implements HostAccess, ConnectionView {
|
|
40
46
|
private readonly store;
|
|
41
47
|
private readonly options;
|
|
42
48
|
private readonly listener;
|
|
43
|
-
telemetry: Telemetry;
|
|
44
49
|
clientId: string;
|
|
45
|
-
private readonly runningUpdates;
|
|
46
|
-
private readonly observedRunningAt;
|
|
47
50
|
private current;
|
|
48
51
|
private readonly abort;
|
|
49
52
|
private runTask;
|
|
50
53
|
private generationFailed;
|
|
51
|
-
constructor(store:
|
|
54
|
+
constructor(store: ConnectionStore, options: ConnectionOptions, listener: ConnectionListener);
|
|
52
55
|
/** Start one retry loop, with a fresh snapshot generation after every disconnect. */
|
|
53
56
|
start(): void;
|
|
54
57
|
/** Cancel retries and HTTP, close the socket, and wait for the loop to settle. */
|
|
@@ -61,20 +64,10 @@ export declare class ConnectionController implements HostAccess, ConnectionView
|
|
|
61
64
|
online(): boolean;
|
|
62
65
|
/** @returns The client lifetime signal. */
|
|
63
66
|
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
67
|
/** Fail the current generation, so the controller reopens a snapshot. */
|
|
73
68
|
fail(error: Error): void;
|
|
74
69
|
/** Answer one retained host waterfall through the event-result endpoint. */
|
|
75
|
-
reply(
|
|
70
|
+
reply(eventId: string, outcome: Json): Promise<void>;
|
|
76
71
|
/** Run one generation, then retry with bounded jittered backoff until stopped. */
|
|
77
72
|
private run;
|
|
78
|
-
/** Apply one `api-session/status` notification to the running maps and the session view. */
|
|
79
|
-
private acceptStatus;
|
|
80
73
|
}
|