@mjasnikovs/pi-task 0.38.25 → 0.38.26

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.
@@ -17,12 +17,25 @@ export interface ChildOutcome {
17
17
  }
18
18
  /**
19
19
  * The one place worker child-failure is turned into a user-facing message.
20
- * Returns `null` when the child succeeded (caller proceeds to format output),
21
- * otherwise the standard abort/exit message. Concentrating this here keeps the
22
- * stderr-tail rule identical across every worker it had already drifted
23
- * (`pi-worker` skipped the `.trim()` the others applied).
20
+ * Returns `null` when the child succeeded (caller proceeds to format output).
21
+ *
22
+ * It ASKS THE LADDER now (`classifyWorkerFailure` -> `describeWorkerFailure`)
23
+ * rather than re-deriving the answer from two fields. It used to be
24
+ * `if (aborted) return abortedMessage`, and every kill path also sets `aborted`,
25
+ * so a wall-clock kill, a hung command, a dead backend, a loop kill and a user
26
+ * ESC all produced the same four words — while `childFailureReason`, one line
27
+ * later at the only caller that had the richer result, computed the real cause
28
+ * and sent it to a debug trail nobody reads. That is the same shape as the bug
29
+ * `worker-failure.ts`'s own header records, one layer out: a second author of a
30
+ * taxonomy the module exists to own.
31
+ *
32
+ * A caller with only a `ChildOutcome` is unaffected. It carries no kill flags,
33
+ * so the ladder falls through to `aborted`/`exit` and returns exactly what this
34
+ * function returned before — which `shared.test.ts` pins.
24
35
  */
25
- export declare function formatChildFailure(child: ChildOutcome, abortedMessage: string): string | null;
36
+ export declare function formatChildFailure(child: ChildOutcome | (WorkerFailureInput & {
37
+ stderr?: string;
38
+ }), abortedMessage: string): string | null;
26
39
  /**
27
40
  * What a worker tool PRODUCED — an answer, or a statement that it has none.
28
41
  *
Binary file
@@ -122,4 +122,30 @@ export declare const FAILURE_RULES: ReadonlyArray<{
122
122
  * which is not the same as "it answered": the text may still be empty, and that
123
123
  * judgement belongs to the caller.
124
124
  */
125
+ /**
126
+ * What a worker failure SAYS to the caller that asked for the work.
127
+ *
128
+ * WHY IT EXISTS. The ladder above already names the cause exactly, with its
129
+ * detail — which tool hung, how long the stream was idle, which exit code. None
130
+ * of that reached a human. `formatChildFailure` was handed a `ChildOutcome`
131
+ * (`{aborted, exitCode, stderr}`) and answered `if (aborted) return
132
+ * abortedMessage`, so a 240s wall-clock kill, a hung `bash`, a dead model
133
+ * backend, a loop kill and a user pressing ESC all printed the SAME four words.
134
+ * The discriminating value was computed a line later by `childFailureReason` and
135
+ * put in the debug trail, which a user reading a tool result never sees.
136
+ *
137
+ * That is not only unhelpful; it is why the `pi-worker` tool's 240s cap has no
138
+ * base rate. 53 recorded invocations across eight repos carry 14 failures, and
139
+ * NOTHING in the transcript says which of them ran out of time — the honest
140
+ * bound recoverable from timestamps alone is "somewhere between 0 and 8".
141
+ *
142
+ * A switch, not a table: `WorkerFailure` is a discriminated union carrying a
143
+ * different payload per arm, so the exhaustiveness check is the compiler's and a
144
+ * ninth arm cannot be added without a message.
145
+ */
146
+ export declare function describeWorkerFailure(f: WorkerFailure,
147
+ /** What a genuine user cancel says. The caller's wording — only this arm is theirs. */
148
+ abortedMessage: string,
149
+ /** stderr for the `exit` arm; ignored by every other. */
150
+ stderr?: string): string;
125
151
  export declare function classifyWorkerFailure(r: WorkerFailureInput): WorkerFailure | undefined;
@@ -83,6 +83,60 @@ export const FAILURE_RULES = [
83
83
  * which is not the same as "it answered": the text may still be empty, and that
84
84
  * judgement belongs to the caller.
85
85
  */
86
+ /**
87
+ * What a worker failure SAYS to the caller that asked for the work.
88
+ *
89
+ * WHY IT EXISTS. The ladder above already names the cause exactly, with its
90
+ * detail — which tool hung, how long the stream was idle, which exit code. None
91
+ * of that reached a human. `formatChildFailure` was handed a `ChildOutcome`
92
+ * (`{aborted, exitCode, stderr}`) and answered `if (aborted) return
93
+ * abortedMessage`, so a 240s wall-clock kill, a hung `bash`, a dead model
94
+ * backend, a loop kill and a user pressing ESC all printed the SAME four words.
95
+ * The discriminating value was computed a line later by `childFailureReason` and
96
+ * put in the debug trail, which a user reading a tool result never sees.
97
+ *
98
+ * That is not only unhelpful; it is why the `pi-worker` tool's 240s cap has no
99
+ * base rate. 53 recorded invocations across eight repos carry 14 failures, and
100
+ * NOTHING in the transcript says which of them ran out of time — the honest
101
+ * bound recoverable from timestamps alone is "somewhere between 0 and 8".
102
+ *
103
+ * A switch, not a table: `WorkerFailure` is a discriminated union carrying a
104
+ * different payload per arm, so the exhaustiveness check is the compiler's and a
105
+ * ninth arm cannot be added without a message.
106
+ */
107
+ export function describeWorkerFailure(f,
108
+ /** What a genuine user cancel says. The caller's wording — only this arm is theirs. */
109
+ abortedMessage,
110
+ /** stderr for the `exit` arm; ignored by every other. */
111
+ stderr = '') {
112
+ switch (f.kind) {
113
+ case 'stalled':
114
+ return ('Worker stopped: it produced no output and the model backend was '
115
+ + 'unreachable. This is an infrastructure failure, not a bad question — '
116
+ + 'retrying the same request once the backend is back is reasonable.');
117
+ case 'command-timeout':
118
+ return (`Worker killed: \`${f.toolName}\` ran past its `
119
+ + `${Math.round(f.timeoutMs / 1000)}s per-command limit and was still `
120
+ + 'running. A command that does not terminate on its own (a dev server, a '
121
+ + 'watcher) has to be bounded by the command itself.');
122
+ case 'stream-stall':
123
+ return (`Worker killed: no output for ${Math.round(f.idleMs / 1000)}s while the `
124
+ + 'model backend was still reachable — a hung stream, not a slow one.');
125
+ case 'worker-timeout':
126
+ return ('Worker ran out of time before answering, on every attempt, and returned '
127
+ + 'nothing. The question was too broad for one worker: narrow it to one '
128
+ + 'directory or one question, or split it across several workers.');
129
+ case 'loop':
130
+ return ('Worker killed: it repeated the same tool call without making progress. '
131
+ + 'Nothing it had already read was answering the question as asked.');
132
+ case 'leaked-tool-call':
133
+ return 'Worker produced a malformed tool call instead of an answer.';
134
+ case 'aborted':
135
+ return abortedMessage;
136
+ case 'exit':
137
+ return `Worker exited ${f.code}.\n${stderr.trim().slice(-500) || '(no stderr)'}`;
138
+ }
139
+ }
86
140
  export function classifyWorkerFailure(r) {
87
141
  for (const rule of FAILURE_RULES) {
88
142
  const hit = rule.match(r);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.38.25",
3
+ "version": "0.38.26",
4
4
  "description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",