@ouro.bot/cli 0.1.0-alpha.759 → 0.1.0-alpha.760

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,12 @@
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.760",
6
+ "changes": [
7
+ "Force exact repeated requests to retry unresolved matching effects and fail closed instead of accepting stale text."
8
+ ]
9
+ },
4
10
  {
5
11
  "version": "0.1.0-alpha.759",
6
12
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.759",
2
+ "runtimeVersion": "0.1.0-alpha.760",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-08-30T00:00:00.000Z"
5
5
  }
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0"?>
2
2
  <Container version="2">
3
3
  <Name>Mendelow Cloud Butler</Name>
4
- <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.759</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.760</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -558,14 +558,35 @@ function isHarnessCorrectiveUserText(text) {
558
558
  || text.startsWith("private-return acknowledgement claimed work was queued, but no ponder packet was created this turn.")
559
559
  || text.startsWith("this exact request previously reached a tool that is advertised again now.");
560
560
  }
561
- function claimsCurrentToolFailure(text) {
562
- const normalized = text.replace(/\s+/g, " ").trim().toLowerCase();
563
- return /\b(?:still|currently|now)\s+(?:down|offline|unavailable|broken|failing|unreachable)\b/.test(normalized)
564
- || /\b(?:runtime|tool|service|server|system|lane)\b.{0,40}\b(?:is|remains?|seems?)\s+(?:still\s+)?(?:down|offline|unavailable|broken|failing|unreachable)\b/.test(normalized)
565
- || /\b(?:i|we)\s+(?:can(?:not|'t)|could(?:not|n't)|am unable to|are unable to)\s+(?:call|use|access|reach|run|change)\b/.test(normalized)
566
- || /\b(?:already\s+)?tried\b.{0,80}\b(?:failed|failure|error|down|offline|unavailable)\b/.test(normalized);
561
+ function toolResultIndicatesFailure(content) {
562
+ const normalized = content.trim().toLowerCase();
563
+ return normalized.startsWith("error:")
564
+ || normalized.startsWith("invalid tool arguments:")
565
+ || normalized.startsWith("unknown:")
566
+ || normalized.startsWith("relationship authorization required:")
567
+ || normalized.startsWith("orientation hold:")
568
+ || normalized.startsWith("commerce authority required:")
569
+ || normalized.startsWith("blocked:")
570
+ || normalized.startsWith("rejected:");
567
571
  }
568
- function historicalFailedToolsForExactRepeatedRequest(messages) {
572
+ function effectFingerprint(name, rawArguments) {
573
+ let args;
574
+ try {
575
+ const parsed = JSON.parse(rawArguments);
576
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
577
+ return null;
578
+ args = { ...parsed };
579
+ }
580
+ catch {
581
+ return null;
582
+ }
583
+ const profile = (0, tools_1.riskProfileForToolName)(name, args);
584
+ if (!profile || profile.mutates === "none")
585
+ return null;
586
+ delete args.expectedVersion;
587
+ return (0, tool_arguments_1.digestJson)({ name, args });
588
+ }
589
+ function historicalFailedEffectsForExactRepeatedRequest(messages) {
569
590
  const userRequests = [];
570
591
  for (let index = 0; index < messages.length; index += 1) {
571
592
  const message = messages[index];
@@ -582,32 +603,34 @@ function historicalFailedToolsForExactRepeatedRequest(messages) {
582
603
  const prior = userRequests.slice(0, -1).find((request) => request.text === current.text);
583
604
  if (!prior)
584
605
  return [];
585
- const namesByCallId = new Map();
586
- const failedNames = new Set();
606
+ const effectsByCallId = new Map();
607
+ const failedEffects = new Map();
587
608
  for (let index = prior.index + 1; index < current.index; index += 1) {
588
609
  const message = messages[index];
589
610
  if (message?.role === "assistant" && Array.isArray(message.tool_calls)) {
590
611
  for (const call of message.tool_calls) {
591
- if (call.type === "function" && call.id && call.function.name) {
592
- namesByCallId.set(call.id, call.function.name);
593
- }
612
+ if (call.type !== "function" || !call.id || !call.function.name)
613
+ continue;
614
+ const fingerprint = effectFingerprint(call.function.name, call.function.arguments);
615
+ if (fingerprint)
616
+ effectsByCallId.set(call.id, { name: call.function.name, fingerprint });
594
617
  }
595
618
  continue;
596
619
  }
597
620
  if (message?.role !== "tool")
598
621
  continue;
599
- const content = messageContentText(message.content).trim().toLowerCase();
600
- const name = namesByCallId.get(message.tool_call_id);
601
- if (!name)
622
+ const content = messageContentText(message.content);
623
+ const effect = effectsByCallId.get(message.tool_call_id);
624
+ if (!effect)
602
625
  continue;
603
- if (content.startsWith("error:") || content.startsWith("invalid tool arguments:")) {
604
- failedNames.add(name);
626
+ if (toolResultIndicatesFailure(content)) {
627
+ failedEffects.set(effect.fingerprint, effect);
605
628
  }
606
629
  else {
607
- failedNames.delete(name);
630
+ failedEffects.delete(effect.fingerprint);
608
631
  }
609
632
  }
610
- return [...failedNames];
633
+ return [...failedEffects.values()];
611
634
  }
612
635
  function latestUserMessageText(messages) {
613
636
  for (let i = messages.length - 1; i >= 0; i -= 1) {
@@ -1094,8 +1117,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1094
1117
  // a ponder packet created the return obligation in this turn.
1095
1118
  let noToolCallRetries = 0;
1096
1119
  const NO_TOOL_CALL_MAX_RETRIES = 2;
1097
- const historicalFailedToolNames = historicalFailedToolsForExactRepeatedRequest(messages);
1098
- let historicalToolFailureRetryIssued = false;
1120
+ let unresolvedHistoricalEffects = historicalFailedEffectsForExactRepeatedRequest(messages);
1121
+ let historicalToolFailureRetries = 0;
1099
1122
  let providerIterations = 0;
1100
1123
  const toolLoopState = (0, tool_loop_1.createToolLoopState)();
1101
1124
  const toolFrictionLedger = (0, tool_friction_1.createToolFrictionLedger)();
@@ -1200,7 +1223,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1200
1223
  const ordinaryActiveTools = relationshipToolNames
1201
1224
  ? unscopedOrdinaryActiveTools.filter((tool) => relationshipToolNames.includes(tool.function.name))
1202
1225
  : unscopedOrdinaryActiveTools;
1203
- const activeTools = options?.toolProfile === "sanctuary-health-private"
1226
+ const candidateActiveTools = options?.toolProfile === "sanctuary-health-private"
1204
1227
  ? (() => {
1205
1228
  const sendTools = baseTools.filter((tool) => tool.function.name === "send_message");
1206
1229
  if (channel !== "inner" || sendTools.length !== 1 || baseTools.length !== 1) {
@@ -1209,6 +1232,14 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1209
1232
  return [sendTools[0], tools_1.restTool];
1210
1233
  })()
1211
1234
  : ordinaryActiveTools;
1235
+ const candidateToolNames = new Set(candidateActiveTools.map((tool) => tool.function.name));
1236
+ const forcedHistoricalToolNames = new Set(unresolvedHistoricalEffects
1237
+ .map((effect) => effect.name)
1238
+ .filter((name) => candidateToolNames.has(name)));
1239
+ const forcingHistoricalEffect = forcedHistoricalToolNames.size > 0;
1240
+ const activeTools = forcingHistoricalEffect
1241
+ ? candidateActiveTools.filter((tool) => forcedHistoricalToolNames.has(tool.function.name))
1242
+ : candidateActiveTools;
1212
1243
  const activeToolNames = new Set(activeTools.map((tool) => tool.function.name));
1213
1244
  const steeringFollowUps = options?.drainSteeringFollowUps?.() ?? [];
1214
1245
  if (steeringFollowUps.length > 0) {
@@ -1262,7 +1293,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1262
1293
  callbacks: turnCallbackBufferRef.current?.callbacks ?? callbacks,
1263
1294
  signal,
1264
1295
  traceId,
1265
- toolChoiceRequired,
1296
+ toolChoiceRequired: forcingHistoricalEffect || toolChoiceRequired,
1266
1297
  reasoningEffort: currentReasoningEffort,
1267
1298
  eagerSettleStreaming: true,
1268
1299
  systemPrompt: structuredSystemPrompt,
@@ -1445,29 +1476,45 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1445
1476
  })
1446
1477
  : null;
1447
1478
  if (!result.toolCalls.length) {
1448
- const retryableHistoricalTools = historicalToolFailureRetryIssued
1449
- ? []
1450
- : historicalFailedToolNames.filter((name) => activeToolNames.has(name));
1451
- const responseClaimsCurrentToolFailure = claimsCurrentToolFailure(messageContentText(msg.content));
1452
- if (retryableHistoricalTools.length > 0 && responseClaimsCurrentToolFailure) {
1453
- historicalToolFailureRetryIssued = true;
1479
+ if (forcingHistoricalEffect) {
1454
1480
  streamCallbackBuffer?.discard();
1455
1481
  callbacks.onClearText?.();
1482
+ const retryableHistoricalTools = [...forcedHistoricalToolNames];
1483
+ if (historicalToolFailureRetries >= NO_TOOL_CALL_MAX_RETRIES) {
1484
+ const blockedAnswer = `I could not complete the unresolved ${retryableHistoricalTools.join(", ")} effect because the provider repeatedly returned no tool call. I did not treat the requested change as complete.`;
1485
+ (0, runtime_1.emitNervesEvent)({
1486
+ level: "warn",
1487
+ component: "engine",
1488
+ event: "engine.historical_tool_failure_retry",
1489
+ message: "unresolved historical effect exhausted deterministic tool-call retries; failing closed",
1490
+ meta: { provider: providerRuntime.id, model: providerRuntime.model, toolNames: retryableHistoricalTools, cap: NO_TOOL_CALL_MAX_RETRIES },
1491
+ });
1492
+ msg.content = blockedAnswer;
1493
+ pushGenerated(msg);
1494
+ callbacks.onTextChunk(blockedAnswer);
1495
+ completion = { answer: blockedAnswer, intent: "blocked" };
1496
+ outcome = "blocked";
1497
+ done = true;
1498
+ continue;
1499
+ }
1500
+ historicalToolFailureRetries += 1;
1456
1501
  (0, runtime_1.emitNervesEvent)({
1457
1502
  level: "warn",
1458
1503
  component: "engine",
1459
1504
  event: "engine.historical_tool_failure_retry",
1460
- message: "text-only response borrowed a historical failure for a currently advertised tool; retrying the exact repeated request",
1505
+ message: "unresolved historical effect returned no tool call; forcing another exact-effect attempt",
1461
1506
  meta: {
1462
1507
  provider: providerRuntime.id,
1463
1508
  model: providerRuntime.model,
1464
1509
  toolNames: retryableHistoricalTools,
1510
+ attempt: historicalToolFailureRetries,
1511
+ cap: NO_TOOL_CALL_MAX_RETRIES,
1465
1512
  },
1466
1513
  });
1467
1514
  pushGenerated(msg);
1468
1515
  messages.push({
1469
1516
  role: "user",
1470
- content: `this exact request previously reached a tool that is advertised again now. The latest recorded result for ${retryableHistoricalTools.join(", ")} was a historical failure; that does not prove current unavailability. Re-evaluate whether the requested effect is still unsatisfied. If it is, call the currently advertised tool before reporting a current failure. Do not repeat an effect that already succeeded.`,
1517
+ content: `this exact request previously reached a tool that is advertised again now. Its unresolved historical effect is through ${retryableHistoricalTools.join(", ")}. Only that tool is available now. Read current state if needed, then retry the exact failed effect. Do not report completion until the effect succeeds.`,
1471
1518
  });
1472
1519
  continue;
1473
1520
  }
@@ -1939,6 +1986,17 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1939
1986
  continue;
1940
1987
  }
1941
1988
  const args = validatedCallArguments.get(tc);
1989
+ const currentEffectFingerprint = effectFingerprint(tc.name, tc.arguments);
1990
+ if (forcingHistoricalEffect
1991
+ && currentEffectFingerprint
1992
+ && !unresolvedHistoricalEffects.some((effect) => effect.fingerprint === currentEffectFingerprint)) {
1993
+ const rejection = "rejected: this turn is retrying an unresolved historical effect, and these mutation arguments do not match it. Read current state or retry the exact failed effect.";
1994
+ callbacks.onToolStart(tc.name, args);
1995
+ callbacks.onToolEnd(tc.name, (0, tools_1.summarizeArgs)(tc.name, args), false);
1996
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: rejection });
1997
+ providerRuntime.appendToolOutput(tc.id, rejection);
1998
+ continue;
1999
+ }
1942
2000
  if (tc.name === "send_message" && args.friendId === "self") {
1943
2001
  const latestUserText = latestUserMessageText(messages);
1944
2002
  if (!isPrivateRuntimeChannel && looksLikePrivateReturnRequest(latestUserText)) {
@@ -2196,6 +2254,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
2196
2254
  success = false;
2197
2255
  augmentedToolContext?.habitSession?.recordError?.(toolResult);
2198
2256
  }
2257
+ if (success && currentEffectFingerprint && !toolResultIndicatesFailure(toolResult)) {
2258
+ unresolvedHistoricalEffects = unresolvedHistoricalEffects.filter((effect) => effect.fingerprint !== currentEffectFingerprint);
2259
+ }
2199
2260
  const resolvedRiskProfile = (0, tools_1.resolveToolDefinition)(tc.name)?.riskProfile;
2200
2261
  const toolRiskProfile = typeof resolvedRiskProfile === "function" ? resolvedRiskProfile(args) : resolvedRiskProfile;
2201
2262
  options?.toolBoundaryObserver?.({
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.759",
3
+ "version": "0.1.0-alpha.760",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@ouro.bot/cli",
9
- "version": "0.1.0-alpha.759",
9
+ "version": "0.1.0-alpha.760",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.78.0",
12
12
  "@azure/identity": "^4.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.759",
3
+ "version": "0.1.0-alpha.760",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },