@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,64 @@
|
|
|
1
|
+
import { runProcess } from '../shell/index.ts';
|
|
2
|
+
import type { VerifierOutcome, VerifierPort, VerifierRequest } from '../controller/verifier.ts';
|
|
3
|
+
/** What a child's error output says about why it failed, without carrying the output itself.
|
|
4
|
+
*
|
|
5
|
+
* A reason travels into the progress line, the verdict summary and the trace, and that log may be
|
|
6
|
+
* pasted into a report; the child's own words can name paths, projects or credentials. The class is
|
|
7
|
+
* the actionable half: `auth` and `host` are worth retrying differently, `config` never is.
|
|
8
|
+
*/
|
|
9
|
+
export type VerifierStderrClass = 'auth' | 'host' | 'config' | 'unknown';
|
|
10
|
+
/** Classify a child's captured error output.
|
|
11
|
+
* @param text - Everything the child wrote to stderr, bounded by the caller.
|
|
12
|
+
* @returns The most actionable class the output matches.
|
|
13
|
+
*/
|
|
14
|
+
export declare function classifyVerifierOutput(text: string): VerifierStderrClass;
|
|
15
|
+
/** Name the fault the child reported about itself, when this client wrote that line.
|
|
16
|
+
* @param text - Everything the child wrote to stderr.
|
|
17
|
+
* @returns The fault as one clause, or undefined when nothing recognized is there.
|
|
18
|
+
*/
|
|
19
|
+
export declare function childFault(text: string): string | undefined;
|
|
20
|
+
/** Everything a forked verification needs from the client that owns it. */
|
|
21
|
+
export interface ProcessVerifierOptions {
|
|
22
|
+
/** Program and arguments that start this client again: exec path, exec arguments, entry script. */
|
|
23
|
+
command: readonly string[];
|
|
24
|
+
/** Host URL as the operator gave it, so the child authenticates the same way. */
|
|
25
|
+
url: string;
|
|
26
|
+
/** Cookie directory the parent used, when it was explicit. */
|
|
27
|
+
authDir?: string;
|
|
28
|
+
/** Working directory for the child. */
|
|
29
|
+
cwd: string;
|
|
30
|
+
/** Environment for the child; it inherits the parent's, including DSH_TOKEN. */
|
|
31
|
+
env: NodeJS.ProcessEnv;
|
|
32
|
+
/** How long one verification may run. */
|
|
33
|
+
timeoutMs: number;
|
|
34
|
+
/** Creates the verifier's own session and names it; the controller owns the connection. */
|
|
35
|
+
createSession(title: string): Promise<string | undefined>;
|
|
36
|
+
/** Stops that session's turn on the host; without it a killed child leaves the agent running. */
|
|
37
|
+
cancelSession?(sessionId: string): Promise<void>;
|
|
38
|
+
/** Receives the child's output lines, so the parent can log why a verdict is missing. */
|
|
39
|
+
onLine(line: string, stream: 'stdout' | 'stderr'): void;
|
|
40
|
+
/** Quote the child's own (sanitized) words in a reason; off by default because that text leaves. */
|
|
41
|
+
verbose?: boolean;
|
|
42
|
+
/** Process runner, injectable so the logic can be tested without spawning a client. */
|
|
43
|
+
run?: typeof runProcess;
|
|
44
|
+
}
|
|
45
|
+
/** Run one verifier as a child client and read the file it leaves behind.
|
|
46
|
+
*
|
|
47
|
+
* The child is the same program as the parent but a different session, so it starts from an empty
|
|
48
|
+
* context: the prompt, the standard and the artifact on disk are all it knows.
|
|
49
|
+
*/
|
|
50
|
+
export declare class ProcessVerifier implements VerifierPort {
|
|
51
|
+
private readonly options;
|
|
52
|
+
/** This client forks itself, so the harness that judges is this one. */
|
|
53
|
+
readonly name = "dsht";
|
|
54
|
+
private readonly tasks;
|
|
55
|
+
constructor(options: ProcessVerifierOptions);
|
|
56
|
+
/** @param request - Round identity, prompt and verdict path.
|
|
57
|
+
* @param signal - Cancels the child; the run also stops at its own deadline.
|
|
58
|
+
* @returns The verdict read back from the file, or a note explaining its absence.
|
|
59
|
+
*/
|
|
60
|
+
verify(request: VerifierRequest, signal: AbortSignal): Promise<VerifierOutcome>;
|
|
61
|
+
/** Keep host cancellation available until every child has completed its bounded cleanup. */
|
|
62
|
+
settle(): Promise<void>;
|
|
63
|
+
private runVerification;
|
|
64
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/** Verification by a child `dsht` process: its own session, its own context, a file back.
|
|
2
|
+
*
|
|
3
|
+
* The reviewed session cannot judge itself fairly, and an in-host subagent still shares the process
|
|
4
|
+
* that produced the work. This implementation forks the client itself, points the child at a fresh
|
|
5
|
+
* named session, and reads the verdict file that session writes — so the verdict arrives through a
|
|
6
|
+
* file written on disk rather than through the reviewer's own reply.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { runProcess } from "../shell/index.js";
|
|
11
|
+
import { ensureDirectory, readText, removeFile } from "../storage/index.js";
|
|
12
|
+
import { parseVerdict } from "../controller/loop-contract.js";
|
|
13
|
+
import { sanitizeTraceText } from "../text.js";
|
|
14
|
+
import { parseNeedsHumanLine } from "../controller/verifier.js";
|
|
15
|
+
/** How long a cancellation request may wait for the host's answer before local cleanup proceeds. */
|
|
16
|
+
const CANCEL_CONFIRM_MS = 1_000;
|
|
17
|
+
/** How much of the child's error output is kept to classify it; the text itself is never reported. */
|
|
18
|
+
const VERIFIER_STDERR_LIMIT = 4_096;
|
|
19
|
+
/** Classify a child's captured error output.
|
|
20
|
+
* @param text - Everything the child wrote to stderr, bounded by the caller.
|
|
21
|
+
* @returns The most actionable class the output matches.
|
|
22
|
+
*/
|
|
23
|
+
export function classifyVerifierOutput(text) {
|
|
24
|
+
if (/unauthor|forbidden|401|403|credential|log ?in|token/i.test(text))
|
|
25
|
+
return 'auth';
|
|
26
|
+
if (/econnrefused|econnreset|enotfound|fetch failed|timed? ?out|socket|50[234]|unreachable/i.test(text))
|
|
27
|
+
return 'host';
|
|
28
|
+
if (/config|yaml|invalid|missing|enoent|no such file|usage|unknown option/i.test(text))
|
|
29
|
+
return 'config';
|
|
30
|
+
return 'unknown';
|
|
31
|
+
}
|
|
32
|
+
/** The faults this client's own child reports on stderr, named so a missing verdict explains itself.
|
|
33
|
+
*
|
|
34
|
+
* These sentences are written by `runStartup` in this same program, so naming them leaks nothing and
|
|
35
|
+
* needs no model text: the alternative is the keyword class below, which reads our own diagnosis as
|
|
36
|
+
* `unknown`. Anything else the child writes still goes through that class.
|
|
37
|
+
*/
|
|
38
|
+
const CHILD_FAULTS = [
|
|
39
|
+
{ pattern: /no parsable verdict in the reply/i, note: 'no JSON verdict in the reply' },
|
|
40
|
+
{ pattern: /no reply was committed after the prompt/i, note: 'no reply committed after the turn' },
|
|
41
|
+
];
|
|
42
|
+
/** Name the fault the child reported about itself, when this client wrote that line.
|
|
43
|
+
* @param text - Everything the child wrote to stderr.
|
|
44
|
+
* @returns The fault as one clause, or undefined when nothing recognized is there.
|
|
45
|
+
*/
|
|
46
|
+
export function childFault(text) {
|
|
47
|
+
return CHILD_FAULTS.find(fault => fault.pattern.test(text))?.note;
|
|
48
|
+
}
|
|
49
|
+
/** Content fingerprint of a workspace file, or undefined when it cannot be read from here.
|
|
50
|
+
*
|
|
51
|
+
* A remote host's workspace is not on this filesystem, so an unreadable artifact means the check
|
|
52
|
+
* cannot be made — which is reported as a boundary rather than treated as tampering.
|
|
53
|
+
* @param path - Absolute path of the file.
|
|
54
|
+
* @returns SHA-256 of its text, or undefined when it is not readable.
|
|
55
|
+
*/
|
|
56
|
+
async function fingerprint(path) {
|
|
57
|
+
const text = await readText(path);
|
|
58
|
+
return text === undefined ? undefined : createHash('sha256').update(text).digest('hex');
|
|
59
|
+
}
|
|
60
|
+
/** Run one verifier as a child client and read the file it leaves behind.
|
|
61
|
+
*
|
|
62
|
+
* The child is the same program as the parent but a different session, so it starts from an empty
|
|
63
|
+
* context: the prompt, the standard and the artifact on disk are all it knows.
|
|
64
|
+
*/
|
|
65
|
+
export class ProcessVerifier {
|
|
66
|
+
options;
|
|
67
|
+
/** This client forks itself, so the harness that judges is this one. */
|
|
68
|
+
name = 'dsht';
|
|
69
|
+
tasks = new Set();
|
|
70
|
+
constructor(options) {
|
|
71
|
+
this.options = options;
|
|
72
|
+
}
|
|
73
|
+
/** @param request - Round identity, prompt and verdict path.
|
|
74
|
+
* @param signal - Cancels the child; the run also stops at its own deadline.
|
|
75
|
+
* @returns The verdict read back from the file, or a note explaining its absence.
|
|
76
|
+
*/
|
|
77
|
+
async verify(request, signal) {
|
|
78
|
+
const task = this.runVerification(request, signal);
|
|
79
|
+
this.tasks.add(task);
|
|
80
|
+
try {
|
|
81
|
+
return await task;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
this.tasks.delete(task);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Keep host cancellation available until every child has completed its bounded cleanup. */
|
|
88
|
+
async settle() { await Promise.allSettled([...this.tasks]); }
|
|
89
|
+
async runVerification(request, signal) {
|
|
90
|
+
const run = this.options.run ?? runProcess;
|
|
91
|
+
const file = request.file;
|
|
92
|
+
// Cancelled before anything happened: no session, no child, no verdict file.
|
|
93
|
+
if (signal.aborted)
|
|
94
|
+
return { type: 'cancelled' };
|
|
95
|
+
await ensureDirectory(dirname(file));
|
|
96
|
+
// A verdict left by an earlier run must never be read as this one's.
|
|
97
|
+
await removeFile(file);
|
|
98
|
+
// "Do not modify the artifact" is an instruction until it can be checked: fingerprint it when
|
|
99
|
+
// the workspace this request declares is readable from here, and let a changed artifact void the
|
|
100
|
+
// verdict. Which file that is comes from the request, never from this process's own directory.
|
|
101
|
+
const artifactPath = request.artifact === undefined ? undefined : join(request.workspace, request.artifact);
|
|
102
|
+
const before = artifactPath === undefined ? undefined : await fingerprint(artifactPath);
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
let sessionId;
|
|
105
|
+
let cancelled = false;
|
|
106
|
+
let timedOut = false;
|
|
107
|
+
/** Settles when the host answered the cancel request, so the outcome can say if it was confirmed. */
|
|
108
|
+
let confirmation;
|
|
109
|
+
let confirmed;
|
|
110
|
+
// The host owns the turn, so a killed child proves nothing about it. Cancellation is therefore a
|
|
111
|
+
// request with a short, bounded wait: local cleanup never blocks on a host that never answers.
|
|
112
|
+
const requestCancel = (id) => {
|
|
113
|
+
const cancel = this.options.cancelSession;
|
|
114
|
+
if (cancel === undefined || confirmation !== undefined)
|
|
115
|
+
return;
|
|
116
|
+
confirmation = Promise.resolve(cancel(id)).then(() => { confirmed = true; }, () => { confirmed = false; });
|
|
117
|
+
};
|
|
118
|
+
const stop = () => {
|
|
119
|
+
if (sessionId !== undefined)
|
|
120
|
+
requestCancel(sessionId);
|
|
121
|
+
controller.abort();
|
|
122
|
+
};
|
|
123
|
+
const onAbort = () => { cancelled = true; stop(); };
|
|
124
|
+
const onTimeout = () => { timedOut = true; stop(); };
|
|
125
|
+
const timer = setTimeout(onTimeout, this.options.timeoutMs);
|
|
126
|
+
// Registered before the session exists, so an abort during creation cannot slip past the listener.
|
|
127
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
128
|
+
if (signal.aborted)
|
|
129
|
+
onAbort();
|
|
130
|
+
/** How the remote cancellation ended, and whether the remote turn is known to be stopped.
|
|
131
|
+
*
|
|
132
|
+
* `stopped: false` is what forbids a retry: starting a second remote task while the first may
|
|
133
|
+
* still be running doubles the cost and the side effects.
|
|
134
|
+
*/
|
|
135
|
+
const cancelNote = async () => {
|
|
136
|
+
if (confirmation === undefined) {
|
|
137
|
+
return this.options.cancelSession === undefined
|
|
138
|
+
? { note: 'host cannot cancel', stopped: false }
|
|
139
|
+
: { note: 'no verifier session to cancel', stopped: true };
|
|
140
|
+
}
|
|
141
|
+
let timer;
|
|
142
|
+
let settled;
|
|
143
|
+
try {
|
|
144
|
+
settled = await Promise.race([
|
|
145
|
+
confirmation.then(() => true),
|
|
146
|
+
// This bounded wait is part of verify()/settle(), so it must keep the process alive.
|
|
147
|
+
new Promise(resolve => { timer = setTimeout(() => resolve(false), CANCEL_CONFIRM_MS); }),
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
}
|
|
153
|
+
if (!settled)
|
|
154
|
+
return { note: `remote cancel unconfirmed after ${CANCEL_CONFIRM_MS} ms`, stopped: false };
|
|
155
|
+
return confirmed === true
|
|
156
|
+
? { note: 'remote cancel confirmed', stopped: true }
|
|
157
|
+
: { note: 'remote cancel rejected', stopped: false };
|
|
158
|
+
};
|
|
159
|
+
try {
|
|
160
|
+
const created = await this.options.createSession(request.title);
|
|
161
|
+
if (created === undefined)
|
|
162
|
+
return { type: 'unavailable', reason: 'could not create a verifier session' };
|
|
163
|
+
sessionId = created;
|
|
164
|
+
if (cancelled || timedOut) {
|
|
165
|
+
// The abort or the deadline landed while the session was being created: cancel what was just
|
|
166
|
+
// made and never spawn a child that would send a prompt nobody is waiting for.
|
|
167
|
+
requestCancel(created);
|
|
168
|
+
const { note, stopped } = await cancelNote();
|
|
169
|
+
return cancelled
|
|
170
|
+
? { type: 'cancelled' }
|
|
171
|
+
: { type: 'unavailable', reason: `verifier timed out after ${this.options.timeoutMs} ms · ${note}`, sessionId,
|
|
172
|
+
...(stopped ? {} : { retryable: false }) };
|
|
173
|
+
}
|
|
174
|
+
const [program, ...leading] = this.options.command;
|
|
175
|
+
// A missing entry is a configuration error, not an outage: retrying cannot fix it.
|
|
176
|
+
if (program === undefined)
|
|
177
|
+
return { type: 'unavailable', reason: 'no client entry to fork', retryable: false };
|
|
178
|
+
const args = [...leading,
|
|
179
|
+
'--url', this.options.url,
|
|
180
|
+
...(this.options.authDir === undefined ? [] : ['--auth-dir', this.options.authDir]),
|
|
181
|
+
'--session', created,
|
|
182
|
+
'--prompt', request.prompt,
|
|
183
|
+
// The child judges and this client owns the protocol file, so the verdict is written by the
|
|
184
|
+
// child from its own reply rather than by the model choosing a path and opening a file.
|
|
185
|
+
'--verdict', file, '--verdict-identity', request.verificationId,
|
|
186
|
+
'--wait', '--headless', '--no-memory-log'];
|
|
187
|
+
// The child reports an interaction it cannot answer on the line below; the same parser keeps
|
|
188
|
+
// both sides of the contract together, and every other line is passed through untouched.
|
|
189
|
+
let human;
|
|
190
|
+
// The child's output is kept only to classify why it failed: `stderrText` for the class, and the
|
|
191
|
+
// last line for the operator who explicitly asked for text (`--trace-verbose`). Neither is
|
|
192
|
+
// reported by default, because a reason reaches the progress line and the trace, and that log
|
|
193
|
+
// may be pasted into a report.
|
|
194
|
+
let stderrText = '';
|
|
195
|
+
let lastLine = '';
|
|
196
|
+
const onLine = (line, stream) => {
|
|
197
|
+
human ??= parseNeedsHumanLine(line);
|
|
198
|
+
const trimmed = line.trim();
|
|
199
|
+
if (trimmed !== '') {
|
|
200
|
+
lastLine = trimmed;
|
|
201
|
+
if (stream === 'stderr')
|
|
202
|
+
stderrText = `${stderrText}${trimmed}\n`.slice(-VERIFIER_STDERR_LIMIT);
|
|
203
|
+
}
|
|
204
|
+
this.options.onLine(line, stream);
|
|
205
|
+
};
|
|
206
|
+
/** A missing or unusable verdict stated as facts, with the child's words only on request. */
|
|
207
|
+
const reasonFor = (lead) => {
|
|
208
|
+
// What this client's own child said about the failure beats the keyword class: "no JSON in the
|
|
209
|
+
// reply" and "no reply committed" are different faults with different next steps, and both used
|
|
210
|
+
// to be reported as `stderrClass unknown`. A child that said nothing gets no class at all,
|
|
211
|
+
// because "unknown" would be noise on the ordinary "exited without writing a file" case.
|
|
212
|
+
const fault = childFault(stderrText);
|
|
213
|
+
const base = fault !== undefined ? `${lead} · ${fault}`
|
|
214
|
+
: stderrText === '' ? lead : `${lead} · stderrClass ${classifyVerifierOutput(stderrText)}`;
|
|
215
|
+
return this.options.verbose === true && lastLine !== '' ? `${base} · ${sanitizeTraceText(lastLine)}` : base;
|
|
216
|
+
};
|
|
217
|
+
const exit = await run(program, args, {
|
|
218
|
+
cwd: this.options.cwd, env: this.options.env, signal: controller.signal, onLine,
|
|
219
|
+
});
|
|
220
|
+
// A cancelled review decides nothing, and is not a verifier outage either.
|
|
221
|
+
if (cancelled) {
|
|
222
|
+
await cancelNote();
|
|
223
|
+
return { type: 'cancelled' };
|
|
224
|
+
}
|
|
225
|
+
// The child stopped because the host wants a human: cancel the turn it left waiting (bounded,
|
|
226
|
+
// reported), and hand the request to the caller instead of retrying into the same block.
|
|
227
|
+
if (human !== undefined) {
|
|
228
|
+
requestCancel(created);
|
|
229
|
+
await cancelNote();
|
|
230
|
+
return { type: 'needs-human', request: human, sessionId };
|
|
231
|
+
}
|
|
232
|
+
// A task that ran out of time is abandoned: a file that appeared as it died must not decide
|
|
233
|
+
// the attempt, or a killed verifier could still pass the round. When the host never confirmed
|
|
234
|
+
// the cancel, a retry could overlap a turn that is still running, so it is not offered one.
|
|
235
|
+
if (timedOut) {
|
|
236
|
+
const { note, stopped } = await cancelNote();
|
|
237
|
+
return { type: 'unavailable', reason: `verifier timed out after ${this.options.timeoutMs} ms · ${note}`, sessionId,
|
|
238
|
+
...(stopped ? {} : { retryable: false }) };
|
|
239
|
+
}
|
|
240
|
+
const text = await readText(file);
|
|
241
|
+
if (text === undefined) {
|
|
242
|
+
const why = exit.code === null ? `signal ${exit.signal ?? 'unknown'}` : `exit ${exit.code}`;
|
|
243
|
+
return { type: 'unavailable', reason: reasonFor(`verifier wrote no verdict (${why})`), sessionId };
|
|
244
|
+
}
|
|
245
|
+
if (before !== undefined) {
|
|
246
|
+
const after = artifactPath === undefined ? undefined : await fingerprint(artifactPath);
|
|
247
|
+
// Which process wrote it is not knowable here, so the reason states the fact rather than an
|
|
248
|
+
// attribution: the file this verdict is about is not the file that was judged.
|
|
249
|
+
if (after !== before) {
|
|
250
|
+
return { type: 'unavailable', sessionId,
|
|
251
|
+
reason: `reviewed artifact changed during verification (${request.artifact} · ${before.slice(0, 8)} → ${after?.slice(0, 8) ?? 'unreadable'})` };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const result = parseVerdict(text, { verificationId: request.verificationId,
|
|
255
|
+
kind: request.kind, step: request.step, attempt: request.attempt });
|
|
256
|
+
if (result === undefined)
|
|
257
|
+
return { type: 'unavailable', reason: reasonFor('verifier verdict was unusable'), sessionId };
|
|
258
|
+
return { type: 'verified', result, sessionId };
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
clearTimeout(timer);
|
|
262
|
+
signal.removeEventListener('abort', onAbort);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|