@ouro.bot/cli 0.1.0-alpha.491 → 0.1.0-alpha.492

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/changelog.json CHANGED
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.492",
6
+ "changes": [
7
+ "Engine-level fix for the actual root cause of Slugger's empty-reply MCP bug (PR #611's strip+retry was the right shape, but missed the deepest layer). MiniMax-M2.7 occasionally emits an assistant message with BOTH inline `<think>...</think>` reasoning AND tool_calls. When that combination is replayed in a subsequent turn, MiniMax rejects with error 2013 ('tool result's tool id not found') and stalls the entire session — every subsequent turn fails the same way, the failover layer fires repeatedly suggesting a provider switch, and the agent's own answer never reaches the operator. Slugger's session was stuck for 11 unanswered user messages because of this exact loop.",
8
+ "The fix has two halves and an AX rule: (1) **Persist-time strip** — runAgent now strips `<think>` blocks from the assistant message's persisted `content` before saving, while preserving the original reasoning trace on `_inline_reasoning` for audit. New `engine.inline_reasoning_stripped` info-level nerve event fires when this happens. (2) **Load-time repair** — `sanitizeProviderMessages` self-heals existing sessions that were saved before (1) by stripping the same blocks at load time. (3) **AX rule: full agent awareness, no silent fixes**. When the load-time repair runs, the synthetic tool-result that fills in for the missing tool result is an **explanatory** one — it tells the agent specifically: \"your previous tool call's result was lost because the assistant message had inline reasoning blocks the provider rejected; the harness has stripped them; your reasoning trace is preserved out-of-band; if the work needs to be done, retry the tool call now.\" Tool calls whose parent didn't have stripped reasoning still get a generic-but-improved \"this tool call's result was lost — possible causes [...]; retry if needed\" message instead of the old vague \"interrupted (previous turn timed out)\" line. The agent always sees what happened and what to do next.",
9
+ "Also: the no-tool-call retry path (added in #611's last commit) now uses the same shared `stripThinkBlocksForViolationCheck` helper so the violation-detection logic is consistent. Three regression tests cover the full path: persist-time strip preserves `_inline_reasoning`, load-time repair produces the explanatory tool-result message, generic orphans get the generic message, and unclosed `<think>` tags drop everything from the open tag onward."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.491",
6
14
  "changes": [
@@ -403,10 +403,13 @@ function repairOrphanedToolCalls(messages) {
403
403
  }
404
404
  const missing = asst.tool_calls.filter((tc) => !resultIds.has(tc.id));
405
405
  if (missing.length > 0) {
406
+ // AX rule: the agent must see what happened. Don't say "interrupted"
407
+ // — that's vague. Tell them the result was lost, possible causes,
408
+ // and what to do next.
406
409
  const syntheticResults = missing.map((tc) => ({
407
410
  role: "tool",
408
411
  tool_call_id: tc.id,
409
- content: "error: tool call was interrupted (previous turn timed out or was aborted)",
412
+ content: "error: this tool call's result was lost — the previous turn ended before the tool finished (provider rejection, daemon interrupt, or the tool itself errored). if the work needs to be done, retry the tool call now.",
410
413
  }));
411
414
  let insertAt = i + 1;
412
415
  while (insertAt < messages.length && messages[insertAt].role === "tool")
@@ -764,8 +767,36 @@ async function runAgent(messages, callbacks, channel, signal, options) {
764
767
  const msg = {
765
768
  role: "assistant",
766
769
  };
767
- if (result.content)
768
- msg.content = result.content;
770
+ // Persist assistant content WITHOUT inline <think>...</think> blocks.
771
+ // Reasoning content already routed through onReasoningChunk for live
772
+ // surfacing and persisted separately as `_reasoning_items` for
773
+ // providers that support a reasoning channel; saving it inline AND
774
+ // alongside tool_calls causes MiniMax to reject the replayed turn
775
+ // with "tool result's tool id not found" (error code 2013) because
776
+ // it can't reconcile reasoning-with-tools in the same assistant
777
+ // message. Strip aggressively at persist so the next replay is
778
+ // clean; preserve the original reasoning trace on the message via
779
+ // `_inline_reasoning` so debug/audit paths can still see it.
780
+ if (result.content) {
781
+ const stripped = stripThinkBlocksForViolationCheck(result.content);
782
+ if (stripped.length > 0)
783
+ msg.content = stripped;
784
+ if (stripped.length !== result.content.length) {
785
+ msg._inline_reasoning = result.content;
786
+ (0, runtime_1.emitNervesEvent)({
787
+ level: "info",
788
+ component: "engine",
789
+ event: "engine.inline_reasoning_stripped",
790
+ message: "stripped inline <think> blocks from persisted assistant message; preserved on _inline_reasoning",
791
+ meta: {
792
+ provider: providerRuntime.id,
793
+ model: providerRuntime.model,
794
+ originalLength: result.content.length,
795
+ strippedLength: stripped.length,
796
+ },
797
+ });
798
+ }
799
+ }
769
800
  if (result.toolCalls.length)
770
801
  msg.tool_calls = result.toolCalls.map((tc) => ({
771
802
  id: tc.id,
@@ -334,7 +334,7 @@ function repairSessionMessages(messages) {
334
334
  });
335
335
  return result.map(toProviderMessage);
336
336
  }
337
- function repairToolCallSequences(messages) {
337
+ function repairToolCallSequences(messages, inlineReasoningStrippedCallIds = new Set()) {
338
338
  const normalized = messages.map(normalizeMessage);
339
339
  const validCallIds = new Set();
340
340
  for (const msg of normalized) {
@@ -381,7 +381,7 @@ function repairToolCallSequences(messages) {
381
381
  continue;
382
382
  const syntheticResults = missing.map((toolCall) => ({
383
383
  role: "tool",
384
- content: "error: tool call was interrupted (previous turn timed out or was aborted)",
384
+ content: buildSyntheticToolResultMessage(toolCall.id, inlineReasoningStrippedCallIds),
385
385
  name: null,
386
386
  toolCallId: toolCall.id,
387
387
  toolCalls: [],
@@ -460,6 +460,105 @@ function migrateToolNames(messages) {
460
460
  }
461
461
  return safeMessages.map(normalizeMessage).map(toProviderMessage);
462
462
  }
463
+ /**
464
+ * Strip inline `<think>...</think>` blocks from a string. Mirrors the
465
+ * helper at senses/shared-turn.ts (operator-facing) and core.ts
466
+ * (live-turn) — kept inline here because session-events.ts is the load-
467
+ * time repair path and needs its own copy to avoid sense/heart import
468
+ * cycles. If the close tag is missing, drops everything from the open
469
+ * tag onward.
470
+ */
471
+ function stripInlineThinkBlocks(input) {
472
+ let out = input;
473
+ for (;;) {
474
+ const open = out.indexOf("<think>");
475
+ if (open === -1)
476
+ break;
477
+ const close = out.indexOf("</think>", open + "<think>".length);
478
+ if (close === -1) {
479
+ out = out.slice(0, open);
480
+ break;
481
+ }
482
+ out = out.slice(0, open) + out.slice(close + "</think>".length);
483
+ }
484
+ return out.trim();
485
+ }
486
+ /**
487
+ * Strip inline `<think>` content from any assistant message that ALSO has
488
+ * tool_calls. MiniMax-style models persist think-content + tool_calls on
489
+ * the same assistant turn; replaying that combination triggers MiniMax
490
+ * error 2013 ("tool result's tool id not found") and stalls the session.
491
+ *
492
+ * AX requirement: the agent MUST see that this happened. We don't silently
493
+ * paper over their previous turn — we strip for replay correctness AND
494
+ * collect the affected tool_call_ids in `inlineReasoningStrippedCallIds`
495
+ * so the downstream synthetic-tool-result repair can produce an
496
+ * explanatory message addressed to those specific calls. The agent sees:
497
+ * "your previous tool call's result was lost because the assistant message
498
+ * had inline reasoning blocks the provider couldn't replay — here's what
499
+ * happened, retry if needed." Full awareness, no silent corrections.
500
+ *
501
+ * This load-time repair self-heals existing sessions that were saved
502
+ * before the persist-time strip in core.ts landed.
503
+ */
504
+ function repairInlineReasoningOnReplay(messages, inlineReasoningStrippedCallIds) {
505
+ let repaired = 0;
506
+ const result = messages.map((msg) => {
507
+ if (msg.role !== "assistant")
508
+ return msg;
509
+ const a = msg;
510
+ if (!a.tool_calls || a.tool_calls.length === 0)
511
+ return msg;
512
+ if (typeof a.content !== "string")
513
+ return msg;
514
+ if (!a.content.includes("<think>"))
515
+ return msg;
516
+ const stripped = stripInlineThinkBlocks(a.content);
517
+ repaired++;
518
+ for (const tc of a.tool_calls)
519
+ inlineReasoningStrippedCallIds.add(tc.id);
520
+ return { ...a, content: stripped.length > 0 ? stripped : null };
521
+ });
522
+ if (repaired > 0) {
523
+ (0, runtime_1.emitNervesEvent)({
524
+ level: "info",
525
+ event: "mind.session_inline_reasoning_repair",
526
+ component: "mind",
527
+ message: "stripped inline <think> blocks from assistant messages with tool_calls so replay is valid; agent will see explanatory tool-result messages",
528
+ meta: { repaired, affectedCallIds: inlineReasoningStrippedCallIds.size },
529
+ });
530
+ }
531
+ return result;
532
+ }
533
+ /**
534
+ * Compose the synthetic tool-result message the agent sees when their
535
+ * previous turn's tool call has no matching tool result. The default
536
+ * message tells the agent what happened (turn ended early, result lost)
537
+ * and what to do (retry if the work isn't done). When the parent
538
+ * assistant message had inline `<think>` reasoning that the provider
539
+ * rejected, the message is more specific so the agent can adjust.
540
+ *
541
+ * AX rule: every repair must produce a message the agent can read and
542
+ * act on. Silent strips are never OK.
543
+ */
544
+ function buildSyntheticToolResultMessage(toolCallId, inlineReasoningStrippedCallIds) {
545
+ if (inlineReasoningStrippedCallIds.has(toolCallId)) {
546
+ return [
547
+ "error: this tool call's result was lost.",
548
+ "your previous assistant turn included inline `<think>...</think>` reasoning alongside tool_calls,",
549
+ "and the provider (likely MiniMax) rejects that combination on replay (error 2013).",
550
+ "the harness has stripped the inline reasoning from the persisted content so the next replay is valid;",
551
+ "your reasoning trace itself is preserved out-of-band and not lost.",
552
+ "if the underlying work still needs to be done, retry the tool call now —",
553
+ "the call may not have run, or it ran but the result didn't reach you.",
554
+ ].join(" ");
555
+ }
556
+ return [
557
+ "error: this tool call's result was lost — the previous turn ended before the tool finished",
558
+ "(provider rejection, daemon interrupt, or the tool itself errored).",
559
+ "if the work needs to be done, retry the tool call now.",
560
+ ].join(" ");
561
+ }
463
562
  function sanitizeProviderMessages(messages) {
464
563
  const safeMessages = messages.filter((message) => Boolean(message) && typeof message === "object");
465
564
  const normalized = safeMessages.map(normalizeMessage);
@@ -473,7 +572,12 @@ function sanitizeProviderMessages(messages) {
473
572
  meta: { violations },
474
573
  });
475
574
  }
476
- return canonicalizeSystemMessageSequence(migrateToolNames(repairToolCallSequences(repairSessionMessages(normalized.map(toProviderMessage)))));
575
+ // Track which tool_call_ids belonged to assistant messages whose inline
576
+ // reasoning we just stripped. The synthetic-tool-result repair downstream
577
+ // uses this set to produce an explanatory message for those calls so the
578
+ // agent has full awareness of what happened.
579
+ const inlineReasoningStrippedCallIds = new Set();
580
+ return canonicalizeSystemMessageSequence(migrateToolNames(repairToolCallSequences(repairInlineReasoningOnReplay(repairSessionMessages(normalized.map(toProviderMessage)), inlineReasoningStrippedCallIds), inlineReasoningStrippedCallIds)));
477
581
  }
478
582
  function stampIngressTime(msg) {
479
583
  msg._ingressAt = new Date().toISOString();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.491",
3
+ "version": "0.1.0-alpha.492",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",