@basein/runner 0.2.4 → 0.2.5

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.
@@ -715,6 +715,15 @@ export class ControlServer {
715
715
  logDetail("tool.pre.skipped", { run: run.runId, tool: toolName, why: "host housekeeping" });
716
716
  return {};
717
717
  }
718
+ // `run_scenario` with no live plan does nothing but answer "no scenario is
719
+ // armed". Recorded, that answer is a failed tool call, and a recording with
720
+ // one is never picked for calculation — observed in production on every
721
+ // first run in a project with the scenario server installed. With a live
722
+ // plan it is still recorded below, tagged `pinnedBy`.
723
+ if (this.replay.isDirectTool(toolName) && (!run.replay?.plan || run.replay.retired)) {
724
+ logDetail("tool.pre.skipped", { run: run.runId, tool: toolName, why: "no scenario armed" });
725
+ return {};
726
+ }
718
727
  const agentId = payload.agent_id ?? session.agentId;
719
728
  // The real intent, read from every line sharing this message's id
720
729
  // (segmented.md R-INTENT-3). `context` stays the *written* text only, with
@@ -1236,6 +1245,10 @@ export class ControlServer {
1236
1245
  // `tool_response` with no `tool_selected` before it.
1237
1246
  if (isHousekeeping(toolName))
1238
1247
  return {};
1248
+ // Nor for an unarmed `run_scenario`. The plan may have retired since the
1249
+ // pre hook, so the test is whether that hook opened a step, not the plan.
1250
+ if (this.replay.isDirectTool(toolName) && !run.builtIns.has(toolUseId))
1251
+ return {};
1239
1252
  const failed = payload.hook_event_name === "PostToolUseFailure" || payload.error !== undefined;
1240
1253
  // ── replay threading (docs/calculatedReplay.md §7.2) ─────────────────────
1241
1254
  // `toolOutputLogic` was authored against the bytes that were *recorded* for
@@ -3,16 +3,17 @@
3
3
  *
4
4
  * A step's tool call resolving is not the same as the step's work happening.
5
5
  * `executeStep` rejects only when a tool could not be run here; a tool that ran
6
- * and answered `Error: relation does not exist` resolves normally, with that
7
- * error as its response — which is right for threading (the output logic may
8
- * want to see it) and wrong for the verdict. Eight such steps once logged
9
- * `ok=true` each, the plan reported `steered_full`, and the ledger booked its
10
- * largest saving of the day on a replay that created nothing (docs/mcpmark.md §13).
6
+ * and reported a failure resolves normally, with that failure as its response —
7
+ * which is right for threading (the output logic may want to see it) and wrong
8
+ * for the verdict (docs/mcpmark.md §13).
11
9
  *
12
- * The signal is the MCP result itself: `isError: true` per the spec, or — for
13
- * servers that report failures as ordinary text, `postgres-mcp` among them — a
14
- * first text block that begins with the word "Error". Nothing else is read, and
15
- * a built-in's hook-shaped response is never judged.
10
+ * The signal is `isError: true` on the MCP result, per the spec, and nothing
11
+ * else — the same rule the recorder uses (`proxy/intercept.ts`), so a step is a
12
+ * success or an error in the recording and in its replay alike. A server that
13
+ * reports failures as ordinary text with `isError: false`, `postgres-mcp` among
14
+ * them, is taken at its word on both sides: its `Error: relation does not exist`
15
+ * is a successful call here, as it was when it was recorded. A built-in's
16
+ * hook-shaped response is never judged.
16
17
  */
17
18
  /**
18
19
  * The error a serialized `CallToolResult` reports, or undefined when it does
@@ -3,16 +3,17 @@
3
3
  *
4
4
  * A step's tool call resolving is not the same as the step's work happening.
5
5
  * `executeStep` rejects only when a tool could not be run here; a tool that ran
6
- * and answered `Error: relation does not exist` resolves normally, with that
7
- * error as its response — which is right for threading (the output logic may
8
- * want to see it) and wrong for the verdict. Eight such steps once logged
9
- * `ok=true` each, the plan reported `steered_full`, and the ledger booked its
10
- * largest saving of the day on a replay that created nothing (docs/mcpmark.md §13).
6
+ * and reported a failure resolves normally, with that failure as its response —
7
+ * which is right for threading (the output logic may want to see it) and wrong
8
+ * for the verdict (docs/mcpmark.md §13).
11
9
  *
12
- * The signal is the MCP result itself: `isError: true` per the spec, or — for
13
- * servers that report failures as ordinary text, `postgres-mcp` among them — a
14
- * first text block that begins with the word "Error". Nothing else is read, and
15
- * a built-in's hook-shaped response is never judged.
10
+ * The signal is `isError: true` on the MCP result, per the spec, and nothing
11
+ * else — the same rule the recorder uses (`proxy/intercept.ts`), so a step is a
12
+ * success or an error in the recording and in its replay alike. A server that
13
+ * reports failures as ordinary text with `isError: false`, `postgres-mcp` among
14
+ * them, is taken at its word on both sides: its `Error: relation does not exist`
15
+ * is a successful call here, as it was when it was recorded. A built-in's
16
+ * hook-shaped response is never judged.
16
17
  */
17
18
  const MAX_ERROR_CHARS = 500;
18
19
  /**
@@ -27,17 +28,12 @@ export function toolResultError(serialized) {
27
28
  catch {
28
29
  return undefined;
29
30
  }
30
- if (typeof parsed === "string")
31
- return looksLikeError(parsed) ? clip(parsed) : undefined;
32
31
  if (!parsed || typeof parsed !== "object")
33
32
  return undefined;
34
33
  const result = parsed;
35
- const text = firstText(result.content);
36
- if (result.isError === true)
37
- return clip(text ?? "the tool reported an error");
38
- if (text !== undefined && looksLikeError(text))
39
- return clip(text);
40
- return undefined;
34
+ if (result.isError !== true)
35
+ return undefined;
36
+ return clip(firstText(result.content) ?? "the tool reported an error");
41
37
  }
42
38
  function firstText(content) {
43
39
  if (!Array.isArray(content))
@@ -49,10 +45,6 @@ function firstText(content) {
49
45
  }
50
46
  return undefined;
51
47
  }
52
- /** "Error: …" / "ERROR: …" / "error occurred" — not "Errors were fixed". */
53
- function looksLikeError(text) {
54
- return /^\s*error\b/i.test(text);
55
- }
56
48
  function clip(text) {
57
49
  const trimmed = text.trim();
58
50
  return trimmed.length > MAX_ERROR_CHARS ? `${trimmed.slice(0, MAX_ERROR_CHARS)}…` : trimmed;