@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.
Files changed (166) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +33 -12
  3. package/README.zh.md +35 -14
  4. package/dist/catalog/controller.d.ts +26 -6
  5. package/dist/catalog/controller.js +73 -45
  6. package/dist/catalog/index.d.ts +1 -0
  7. package/dist/cli/dsht.js +206 -18
  8. package/dist/cli/startup.d.ts +40 -0
  9. package/dist/cli/startup.js +314 -0
  10. package/dist/cli/trace-summary.d.ts +78 -0
  11. package/dist/cli/trace-summary.js +241 -0
  12. package/dist/cli/verifier.d.ts +64 -0
  13. package/dist/cli/verifier.js +265 -0
  14. package/dist/contracts.d.ts +359 -0
  15. package/dist/contracts.js +1 -0
  16. package/dist/controller/commands.d.ts +47 -0
  17. package/dist/controller/commands.js +322 -0
  18. package/dist/controller/connection-streams.d.ts +22 -0
  19. package/dist/controller/connection-streams.js +105 -0
  20. package/dist/controller/connection.d.ts +24 -31
  21. package/dist/controller/connection.js +48 -111
  22. package/dist/controller/controller.d.ts +412 -178
  23. package/dist/controller/controller.js +713 -167
  24. package/dist/controller/foreground.d.ts +44 -0
  25. package/dist/controller/foreground.js +79 -0
  26. package/dist/controller/index.d.ts +8 -1
  27. package/dist/controller/index.js +5 -0
  28. package/dist/controller/loop-contract.d.ts +136 -0
  29. package/dist/controller/loop-contract.js +308 -0
  30. package/dist/controller/loop-coordinator.d.ts +48 -0
  31. package/dist/controller/loop-coordinator.js +647 -0
  32. package/dist/controller/loop-prompts-schema.d.ts +56 -0
  33. package/dist/controller/loop-prompts-schema.js +144 -0
  34. package/dist/controller/loop-prompts.d.ts +55 -0
  35. package/dist/controller/loop-prompts.generated.d.ts +104 -0
  36. package/dist/controller/loop-prompts.generated.js +185 -0
  37. package/dist/controller/loop-prompts.js +104 -0
  38. package/dist/controller/loop-protocols.d.ts +39 -0
  39. package/dist/controller/loop-protocols.js +115 -0
  40. package/dist/controller/loop.d.ts +275 -0
  41. package/dist/controller/loop.js +378 -0
  42. package/dist/controller/prompts.d.ts +54 -0
  43. package/dist/controller/prompts.js +162 -0
  44. package/dist/controller/trace-log.d.ts +45 -0
  45. package/dist/controller/trace-log.js +144 -0
  46. package/dist/controller/verifier.d.ts +130 -0
  47. package/dist/controller/verifier.js +75 -0
  48. package/dist/cost/controller.d.ts +1 -1
  49. package/dist/cost/controller.js +12 -5
  50. package/dist/cost/index.d.ts +1 -1
  51. package/dist/cost/index.js +1 -1
  52. package/dist/cost/ledger.d.ts +0 -1
  53. package/dist/cost/ledger.js +0 -1
  54. package/dist/cost/scanner.js +1 -0
  55. package/dist/json.d.ts +18 -0
  56. package/dist/json.js +19 -0
  57. package/dist/references.d.ts +25 -0
  58. package/dist/references.js +26 -0
  59. package/dist/session/connection-view.d.ts +2 -11
  60. package/dist/session/controller.d.ts +94 -105
  61. package/dist/session/controller.js +262 -536
  62. package/dist/session/history-reader.d.ts +32 -0
  63. package/dist/session/history-reader.js +170 -0
  64. package/dist/session/history.d.ts +6 -18
  65. package/dist/session/history.js +1 -24
  66. package/dist/session/index.d.ts +9 -4
  67. package/dist/session/index.js +7 -3
  68. package/dist/session/info.d.ts +20 -82
  69. package/dist/session/info.js +52 -25
  70. package/dist/session/interactions.d.ts +26 -0
  71. package/dist/session/interactions.js +75 -0
  72. package/dist/session/markdown.js +1 -1
  73. package/dist/session/math.js +1 -1
  74. package/dist/session/mutation-gate.d.ts +51 -0
  75. package/dist/session/mutation-gate.js +73 -0
  76. package/dist/session/navigation.d.ts +2 -89
  77. package/dist/session/navigation.js +2 -129
  78. package/dist/session/navigator.d.ts +47 -0
  79. package/dist/session/navigator.js +158 -0
  80. package/dist/session/peek.d.ts +38 -0
  81. package/dist/session/peek.js +103 -0
  82. package/dist/session/prompt-backfill.d.ts +23 -0
  83. package/dist/session/prompt-backfill.js +88 -0
  84. package/dist/session/references.d.ts +2 -20
  85. package/dist/session/references.js +1 -26
  86. package/dist/session/runtime.d.ts +26 -0
  87. package/dist/session/runtime.js +28 -0
  88. package/dist/session/state.d.ts +20 -0
  89. package/dist/session/state.js +1 -0
  90. package/dist/session/telemetry.d.ts +25 -17
  91. package/dist/session/telemetry.js +66 -60
  92. package/dist/session/transcript.d.ts +5 -7
  93. package/dist/session/transcript.js +2 -15
  94. package/dist/session/types.d.ts +25 -0
  95. package/dist/session/types.js +0 -1
  96. package/dist/session-title.d.ts +9 -0
  97. package/dist/session-title.js +21 -0
  98. package/dist/shell/controller.d.ts +31 -1
  99. package/dist/shell/controller.js +34 -2
  100. package/dist/shell/index.d.ts +3 -3
  101. package/dist/shell/index.js +2 -2
  102. package/dist/shell/runner.d.ts +10 -0
  103. package/dist/shell/runner.js +48 -9
  104. package/dist/slash/index.d.ts +10 -0
  105. package/dist/slash/index.js +7 -0
  106. package/dist/slash/parse.d.ts +42 -0
  107. package/dist/slash/parse.js +259 -0
  108. package/dist/slash/pipeline.d.ts +140 -0
  109. package/dist/slash/pipeline.js +115 -0
  110. package/dist/slash/registry.d.ts +88 -0
  111. package/dist/slash/registry.js +177 -0
  112. package/dist/slash/types.d.ts +126 -0
  113. package/dist/slash/types.js +1 -0
  114. package/dist/state.d.ts +16 -18
  115. package/dist/state.js +4 -3
  116. package/dist/text.d.ts +28 -0
  117. package/dist/text.js +55 -0
  118. package/dist/transport/client.d.ts +4 -3
  119. package/dist/transport/client.js +71 -25
  120. package/dist/transport/events.d.ts +104 -0
  121. package/dist/transport/events.js +149 -0
  122. package/dist/transport/wire.d.ts +9 -17
  123. package/dist/transport/wire.js +2 -27
  124. package/dist/ui/app.js +750 -550
  125. package/dist/ui/chat/header.js +1 -1
  126. package/dist/ui/chat/history-view.d.ts +1 -1
  127. package/dist/ui/chat/loop-status.d.ts +11 -0
  128. package/dist/ui/chat/loop-status.js +28 -0
  129. package/dist/ui/chat/navigation-model.d.ts +86 -0
  130. package/dist/ui/chat/navigation-model.js +107 -0
  131. package/dist/ui/chat/shell-view.d.ts +17 -2
  132. package/dist/ui/chat/shell-view.js +45 -3
  133. package/dist/ui/chat/status.d.ts +47 -3
  134. package/dist/ui/chat/status.js +65 -50
  135. package/dist/ui/chat/use-history-view.d.ts +69 -0
  136. package/dist/ui/chat/use-history-view.js +123 -0
  137. package/dist/ui/chat/viewport.d.ts +1 -1
  138. package/dist/ui/dialogs/cost.d.ts +21 -4
  139. package/dist/ui/dialogs/cost.js +7 -12
  140. package/dist/ui/dialogs/index.d.ts +22 -5
  141. package/dist/ui/dialogs/index.js +19 -3
  142. package/dist/ui/dialogs/loop.d.ts +43 -0
  143. package/dist/ui/dialogs/loop.js +224 -0
  144. package/dist/ui/dialogs/peek.d.ts +25 -0
  145. package/dist/ui/dialogs/peek.js +35 -0
  146. package/dist/ui/dialogs/picker.d.ts +2 -0
  147. package/dist/ui/dialogs/picker.js +4 -2
  148. package/dist/ui/dialogs/use-panels.d.ts +53 -0
  149. package/dist/ui/dialogs/use-panels.js +51 -0
  150. package/dist/ui/input/mouse.d.ts +12 -2
  151. package/dist/ui/input/mouse.js +20 -7
  152. package/dist/ui/input/references.d.ts +1 -1
  153. package/dist/ui/input/use-composer.d.ts +35 -0
  154. package/dist/ui/input/use-composer.js +109 -0
  155. package/dist/ui/input/use-deferred-lines.d.ts +16 -0
  156. package/dist/ui/input/use-deferred-lines.js +54 -0
  157. package/dist/ui/input/use-history-recall.d.ts +20 -0
  158. package/dist/ui/input/use-history-recall.js +47 -0
  159. package/dist/ui/status/model.d.ts +7 -0
  160. package/dist/ui/status/model.js +5 -0
  161. package/dist/ui/theme/index.d.ts +1 -1
  162. package/package.json +6 -4
  163. package/dist/ui/commands/parse.d.ts +0 -104
  164. package/dist/ui/commands/parse.js +0 -135
  165. package/dist/ui/commands/registry.d.ts +0 -33
  166. 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
+ }