@proagentstore/cli 0.4.50 → 0.4.51
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.
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the LAST completed engine turn ended (#545).
|
|
3
|
+
*
|
|
4
|
+
* The exit code was already known on this side and spent entirely on prose: `runOneShot`'s close
|
|
5
|
+
* handler pushed `[codex exited with code 1]` into the transcript and set no field, so a production
|
|
6
|
+
* Codex session whose every turn exited 1 — three times, the reason printed each time — reported
|
|
7
|
+
* `alive: true, ready: true, runState: "idle"`, and the Pilot spent fifteen minutes and three BYOK
|
|
8
|
+
* decisions rediscovering it from the pane.
|
|
9
|
+
*
|
|
10
|
+
* OUTCOME AND LIVENESS ARE TWO FACTS, and this module owns only the first. Nothing here touches
|
|
11
|
+
* `alive`: a one-shot session has no process between turns, a failing turn does not make the
|
|
12
|
+
* session unable to take another, and conflating the two once killed every delegated goal on
|
|
13
|
+
* codex/grok/gemini at iteration 0 (see `HeadlessSession.alive`). The failure is REPORTED here;
|
|
14
|
+
* what to make of one is the cloud's judgement, in `workers/api/src/lib/coding-turn-outcome.ts`.
|
|
15
|
+
*
|
|
16
|
+
* Its own module, beside `engine-usage.ts` / `engine-acts.ts` / `engine-auth.ts`, for the reason
|
|
17
|
+
* they are: the RULE that turns a process exit into a verdict is worth testing without spawning a
|
|
18
|
+
* process, and it is the part someone will later be tempted to change.
|
|
19
|
+
*/
|
|
20
|
+
/** Longest engine line carried as {@link EngineTurnReport.detail}. The pane holds the rest. */
|
|
21
|
+
export const MAX_TURN_DETAIL = 240;
|
|
22
|
+
/**
|
|
23
|
+
* A one-shot turn's process has exited — say what that means, and nothing more.
|
|
24
|
+
*
|
|
25
|
+
* A SIGNAL outranks the code because a signalled process's code is null and the kill is ours: the
|
|
26
|
+
* 15-minute wedge ceiling and `interrupt()` both land here, and counting either as an engine
|
|
27
|
+
* failure would let three slow builds read as a broken CLI.
|
|
28
|
+
*/
|
|
29
|
+
export function turnReportFromExit(code, signal, lastLine = "", now = Date.now()) {
|
|
30
|
+
const detail = lastLine.trim().slice(0, MAX_TURN_DETAIL);
|
|
31
|
+
return {
|
|
32
|
+
verdict: signal !== null ? "killed" : code === 0 ? "ok" : "failed",
|
|
33
|
+
exitCode: code,
|
|
34
|
+
signal,
|
|
35
|
+
at: now,
|
|
36
|
+
...(detail ? { detail } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A stream-json turn ended with a `result` event — the structured path's analogue of an exit code.
|
|
41
|
+
*
|
|
42
|
+
* Without it the field would exist for three engines and silently not for the flagship, which is
|
|
43
|
+
* the shape of gap that makes a platform-wide claim ("we notice a failed turn") false in the one
|
|
44
|
+
* case that runs most.
|
|
45
|
+
*/
|
|
46
|
+
export function turnReportFromResult(isError, detail = "", now = Date.now()) {
|
|
47
|
+
const text = detail.trim().slice(0, MAX_TURN_DETAIL);
|
|
48
|
+
return {
|
|
49
|
+
verdict: isError ? "failed" : "ok",
|
|
50
|
+
exitCode: null,
|
|
51
|
+
signal: null,
|
|
52
|
+
at: now,
|
|
53
|
+
...(text ? { detail: text } : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { classifyCommand, commandFromToolInput, fillTargetFromResult } from "./engine-acts.js";
|
|
3
3
|
import { parseEngineUsage } from "./engine-usage.js";
|
|
4
|
+
import { turnReportFromExit, turnReportFromResult } from "./engine-turn.js";
|
|
4
5
|
/**
|
|
5
6
|
* Merge the platform's resolved engine env over the machine's, where an EMPTY value means
|
|
6
7
|
* REMOVE rather than "set to empty".
|
|
@@ -88,6 +89,16 @@ export class HeadlessSession {
|
|
|
88
89
|
turnStartedAt = 0;
|
|
89
90
|
/** Set by stop() — the only thing that ends a one-shot session (see `alive`). */
|
|
90
91
|
stopped = false;
|
|
92
|
+
/** How the last COMPLETED turn ended (#545). Null until one has. See {@link EngineTurnReport}. */
|
|
93
|
+
turnReport = null;
|
|
94
|
+
/**
|
|
95
|
+
* The engine's own last output line of the turn in flight (#545).
|
|
96
|
+
*
|
|
97
|
+
* Recorded as it is written, so the failure detail is the engine's sentence rather than
|
|
98
|
+
* something parsed back out of a rendered pane. Reset when a turn starts, so a report can never
|
|
99
|
+
* carry a previous turn's line.
|
|
100
|
+
*/
|
|
101
|
+
turnLastLine = "";
|
|
91
102
|
/** Measured engine spend not yet handed to the cloud (#267). Drained by {@link takeUsage}. */
|
|
92
103
|
pendingUsage = [];
|
|
93
104
|
/**
|
|
@@ -216,6 +227,15 @@ export class HeadlessSession {
|
|
|
216
227
|
return !this.stopped && !this.spawnFailed;
|
|
217
228
|
return this.procAlive;
|
|
218
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* How the last completed turn ended (#545) — NOT whether the session can take another.
|
|
232
|
+
*
|
|
233
|
+
* Read by the snapshot and carried to the cloud. Null means no turn has completed on this
|
|
234
|
+
* session; it is never a claim that a turn went well.
|
|
235
|
+
*/
|
|
236
|
+
get lastTurn() {
|
|
237
|
+
return this.turnReport;
|
|
238
|
+
}
|
|
219
239
|
/** Is a process running THIS instant? The persistent engine's liveness, and the spawn guard. */
|
|
220
240
|
get procAlive() {
|
|
221
241
|
return this.proc !== null && this.proc.exitCode === null && this.proc.signalCode === null;
|
|
@@ -397,6 +417,9 @@ export class HeadlessSession {
|
|
|
397
417
|
* preset decides what runs and what it costs; the platform does not need to know the engine.
|
|
398
418
|
*/
|
|
399
419
|
runOneShot(text) {
|
|
420
|
+
// Arm the per-turn line capture BEFORE the spawn, so a report can only ever carry a line
|
|
421
|
+
// this turn produced (#545).
|
|
422
|
+
this.turnLastLine = "";
|
|
400
423
|
const proc = spawn(this.cmdBin, [...this.cmdArgs, text], {
|
|
401
424
|
cwd: this.config.workDir,
|
|
402
425
|
env: mergeEnv(process.env, this.config.env),
|
|
@@ -450,7 +473,7 @@ export class HeadlessSession {
|
|
|
450
473
|
}
|
|
451
474
|
}, maxTurnMs);
|
|
452
475
|
ceiling.unref();
|
|
453
|
-
proc.on("close", (code) => {
|
|
476
|
+
proc.on("close", (code, signal) => {
|
|
454
477
|
clearTimeout(ceiling); // cleared before the staleness guard: the timer belongs to THIS process
|
|
455
478
|
// A non-zero exit is the engine's own failure (bad flags, not signed in) and the
|
|
456
479
|
// operator needs to see it — silently going idle is how "stdin is not a terminal"
|
|
@@ -465,6 +488,14 @@ export class HeadlessSession {
|
|
|
465
488
|
// kill-tmux.
|
|
466
489
|
if (this.proc !== proc)
|
|
467
490
|
return;
|
|
491
|
+
// THE EXIT CODE STOPS BEING ONLY PROSE HERE (#545). Recorded after the staleness guard
|
|
492
|
+
// on purpose: a turn aborted by its successor (see the kill above) must not overwrite
|
|
493
|
+
// the report of the turn that replaced it — the loser's outcome is about a turn nobody
|
|
494
|
+
// is waiting on any more.
|
|
495
|
+
//
|
|
496
|
+
// A signal means WE ended it (the wedge ceiling, an interrupt), which is the `killed`
|
|
497
|
+
// verdict: evidence about this platform's timers, not about the engine's health.
|
|
498
|
+
this.turnReport = turnReportFromExit(code, signal, this.turnLastLine);
|
|
468
499
|
this.run = "idle";
|
|
469
500
|
this.proc = null;
|
|
470
501
|
});
|
|
@@ -536,8 +567,12 @@ export class HeadlessSession {
|
|
|
536
567
|
/** Raw-engine stdout: strip ANSI control codes and append to the transcript. */
|
|
537
568
|
pushRaw(line) {
|
|
538
569
|
const clean = stripAnsi(line);
|
|
539
|
-
if (clean.trim())
|
|
570
|
+
if (clean.trim()) {
|
|
540
571
|
this.push(clean);
|
|
572
|
+
// The engine's own words, kept for the turn's report (#545) — captured on the way in,
|
|
573
|
+
// never scraped back out of the rendered pane.
|
|
574
|
+
this.turnLastLine = clean.trim();
|
|
575
|
+
}
|
|
541
576
|
if (this.transcript.length > 4000)
|
|
542
577
|
this.transcript = this.transcript.slice(-3000);
|
|
543
578
|
}
|
|
@@ -576,8 +611,14 @@ export class HeadlessSession {
|
|
|
576
611
|
}
|
|
577
612
|
break;
|
|
578
613
|
case "result": {
|
|
579
|
-
|
|
580
|
-
|
|
614
|
+
const failure = ev.is_error ? String(ev.result ?? ev.subtype ?? "failed") : "";
|
|
615
|
+
if (failure)
|
|
616
|
+
this.push(`[error] ${failure}`);
|
|
617
|
+
// The structured path's ANALOGUE of a non-zero exit (#545). Claude has no process
|
|
618
|
+
// per turn, so `exitCode` is honestly null and the verdict comes from the protocol's
|
|
619
|
+
// own `is_error` — the same claim, in the words the engine states it in. Without
|
|
620
|
+
// this the field would exist for three engines and silently not for the flagship.
|
|
621
|
+
this.turnReport = turnReportFromResult(ev.is_error === true, failure);
|
|
581
622
|
// The same event that ends the turn also reports what the turn COST (#267). It was
|
|
582
623
|
// parsed and thrown away, which is why Engine spend was absent from the ledger.
|
|
583
624
|
// An errored turn still burned tokens, so this is recorded regardless of is_error.
|
|
@@ -151,6 +151,10 @@ export class CodingRuntime {
|
|
|
151
151
|
// exactly the question asked about a session that just stopped.
|
|
152
152
|
authResolved: session.authResolved,
|
|
153
153
|
engineRuntime: session.engineRuntime,
|
|
154
|
+
// Reported on EVERY capture, including one where the session is not alive: "how did the
|
|
155
|
+
// last turn end" is exactly the question asked about a session that just stopped, and
|
|
156
|
+
// the omitted-when-null shape keeps "not measured" distinguishable from a verdict.
|
|
157
|
+
...(session.lastTurn ? { lastTurn: session.lastTurn } : {}),
|
|
154
158
|
...(opts.drainUsage ? { usage: session.takeUsage(), acts: session.takeActs() } : {}),
|
|
155
159
|
};
|
|
156
160
|
}
|