@ouro.bot/cli 0.1.0-alpha.347 → 0.1.0-alpha.349

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,19 @@
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.349",
6
+ "changes": [
7
+ "`ouro use`, `ouro check`, provider-scoped `ouro status --agent`, and machine-pool `ouro auth` now operate on machine-local provider state and credentials, with safe provenance, explicit lane/provider/model repair guidance, and legacy `auth switch`/`config model` compatibility routed through `state/providers.json` instead of mutating synced `agent.json`."
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.1.0-alpha.348",
12
+ "changes": [
13
+ "Provider attempts now share one bounded retry runner across real runtime turns, provider pings, health inventory, working-provider discovery, and GitHub Copilot model validation, retrying every provider failure class before terminal handling while preserving attempt metadata and nerves events.",
14
+ "Provider checks can now ping the selected lane model instead of drifting to provider defaults, and readiness/model pings use zero-delay retries so health checks stay fast."
15
+ ]
16
+ },
4
17
  {
5
18
  "version": "0.1.0-alpha.347",
6
19
  "changes": [
@@ -10,7 +10,6 @@ exports.isExternalStateQuery = isExternalStateQuery;
10
10
  exports.getSettleRetryError = getSettleRetryError;
11
11
  exports.stripLastToolCalls = stripLastToolCalls;
12
12
  exports.repairOrphanedToolCalls = repairOrphanedToolCalls;
13
- exports.isRetryBlocked = isRetryBlocked;
14
13
  exports.runAgent = runAgent;
15
14
  const config_1 = require("./config");
16
15
  const identity_1 = require("./identity");
@@ -33,6 +32,7 @@ const tool_loop_1 = require("./tool-loop");
33
32
  const packets_1 = require("../arc/packets");
34
33
  const tool_friction_1 = require("./tool-friction");
35
34
  const provider_models_1 = require("./provider-models");
35
+ const provider_attempt_1 = require("./provider-attempt");
36
36
  const _providerRuntimes = {
37
37
  human: null,
38
38
  agent: null,
@@ -366,38 +366,6 @@ function isContextOverflow(err) {
366
366
  return true;
367
367
  return false;
368
368
  }
369
- // HTTP statuses that will never become retryable on their own — the request is
370
- // semantically wrong (malformed, unauthorized, missing route, etc.) and the
371
- // caller has to do something different before it can succeed.
372
- const NON_RETRYABLE_HTTP_STATUSES = new Set([
373
- 400, // Bad Request — malformed payload
374
- 401, // Unauthorized — credentials invalid/expired
375
- 403, // Forbidden — credentials lack permission
376
- 404, // Not Found — model/route doesn't exist
377
- 422, // Unprocessable Entity — semantic validation failure
378
- ]);
379
- // Provider-classified error categories that we never retry. usage-limit is
380
- // distinct from rate-limit: rate limits clear in seconds (retryable), usage
381
- // limits are billing quotas that take hours/days to reset.
382
- const NON_RETRYABLE_CLASSIFICATIONS = new Set([
383
- "auth-failure",
384
- "usage-limit",
385
- ]);
386
- // Default policy: retry every error from the provider, EXCEPT the small set
387
- // above. The user explicitly requested this — past behavior was to retry only
388
- // on a known-transient list, which silently dropped real harness/SDK timeouts
389
- // (e.g. OpenAI SDK's "Request timed out." has no err.code and no status, so
390
- // the substring matchers missed it).
391
- function isRetryBlocked(error, classification) {
392
- const status = error.status;
393
- if (status !== undefined && NON_RETRYABLE_HTTP_STATUSES.has(status))
394
- return true;
395
- if (NON_RETRYABLE_CLASSIFICATIONS.has(classification))
396
- return true;
397
- return false;
398
- }
399
- const MAX_RETRIES = 3;
400
- const RETRY_BASE_MS = 2000;
401
369
  const RETRY_LABELS = {
402
370
  "auth-failure": "auth error",
403
371
  "usage-limit": "usage limit",
@@ -406,6 +374,29 @@ const RETRY_LABELS = {
406
374
  "network-error": "network error",
407
375
  "unknown": "error",
408
376
  };
377
+ function waitForProviderRetry(delayMs, signal) {
378
+ if (!signal) {
379
+ return new Promise((resolve) => {
380
+ setTimeout(resolve, delayMs);
381
+ });
382
+ }
383
+ return new Promise((resolve, reject) => {
384
+ let timer;
385
+ const onAbort = () => {
386
+ clearTimeout(timer);
387
+ reject(new provider_attempt_1.ProviderAttemptAbortError());
388
+ };
389
+ timer = setTimeout(() => {
390
+ signal.removeEventListener("abort", onAbort);
391
+ resolve();
392
+ }, delayMs);
393
+ if (signal.aborted) {
394
+ onAbort();
395
+ return;
396
+ }
397
+ signal.addEventListener("abort", onAbort, { once: true });
398
+ });
399
+ }
409
400
  function buildAuthFailureGuidance(provider, model, agentName, detail) {
410
401
  const mismatch = (0, provider_models_1.getProviderModelMismatchMessage)(provider, model);
411
402
  const modelLabel = model
@@ -500,7 +491,6 @@ async function runAgent(messages, callbacks, channel, signal, options) {
500
491
  let done = false;
501
492
  let lastUsage;
502
493
  let overflowRetried = false;
503
- let retryCount = 0;
504
494
  let outcome = "settled";
505
495
  let completion;
506
496
  let terminalError;
@@ -515,6 +505,35 @@ async function runAgent(messages, callbacks, channel, signal, options) {
515
505
  let sawExternalStateQuery = false;
516
506
  const toolLoopState = (0, tool_loop_1.createToolLoopState)();
517
507
  const toolFrictionLedger = (0, tool_friction_1.createToolFrictionLedger)();
508
+ const finishTerminalProviderError = (error, classification) => {
509
+ terminalError = error;
510
+ terminalErrorClassification = classification;
511
+ /* v8 ignore start — auth-failure guidance: tested via provider error classification tests @preserve */
512
+ if (terminalErrorClassification === "auth-failure") {
513
+ const agentName = (0, identity_2.getAgentName)();
514
+ const currentProvider = providerRuntime.id;
515
+ callbacks.onError(new Error(buildAuthFailureGuidance(currentProvider, providerRuntime.model, agentName, terminalError.message)), "terminal");
516
+ }
517
+ else {
518
+ callbacks.onError(terminalError, "terminal");
519
+ }
520
+ /* v8 ignore stop */
521
+ (0, runtime_1.emitNervesEvent)({
522
+ level: "error",
523
+ event: "engine.error",
524
+ trace_id: traceId,
525
+ component: "engine",
526
+ message: terminalError.message,
527
+ meta: {
528
+ provider: providerRuntime.id,
529
+ model: providerRuntime.model,
530
+ errorClassification: terminalErrorClassification,
531
+ },
532
+ });
533
+ stripLastToolCalls(messages);
534
+ outcome = "errored";
535
+ done = true;
536
+ };
518
537
  // Prevent MaxListenersExceeded warning — each iteration adds a listener
519
538
  try {
520
539
  require("events").setMaxListeners(50, signal);
@@ -581,21 +600,71 @@ async function runAgent(messages, callbacks, channel, signal, options) {
581
600
  break;
582
601
  }
583
602
  try {
584
- callbacks.onModelStart();
585
- const result = await providerRuntime.streamTurn({
586
- messages,
587
- activeTools,
588
- callbacks,
589
- signal,
590
- traceId,
591
- toolChoiceRequired,
592
- reasoningEffort: currentReasoningEffort,
593
- eagerSettleStreaming: true,
603
+ const callProviderTurn = async () => {
604
+ callbacks.onModelStart();
605
+ try {
606
+ return await providerRuntime.streamTurn({
607
+ messages,
608
+ activeTools,
609
+ callbacks,
610
+ signal,
611
+ traceId,
612
+ toolChoiceRequired,
613
+ reasoningEffort: currentReasoningEffort,
614
+ eagerSettleStreaming: true,
615
+ });
616
+ }
617
+ catch (error) {
618
+ if (signal?.aborted)
619
+ throw new provider_attempt_1.ProviderAttemptAbortError();
620
+ throw error;
621
+ }
622
+ };
623
+ const callProviderTurnWithOverflowRecovery = async () => {
624
+ try {
625
+ return await callProviderTurn();
626
+ }
627
+ catch (error) {
628
+ if (error instanceof provider_attempt_1.ProviderAttemptAbortError)
629
+ throw error;
630
+ if (isContextOverflow(error) && !overflowRetried) {
631
+ overflowRetried = true;
632
+ stripLastToolCalls(messages);
633
+ const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
634
+ const trimmed = (0, context_1.trimMessages)(messages, maxTokens, contextMargin, maxTokens * 2);
635
+ messages.splice(0, messages.length, ...trimmed);
636
+ providerRuntime.resetTurnState(messages);
637
+ callbacks.onError(new Error("context trimmed, retrying..."), "transient");
638
+ return callProviderTurn();
639
+ }
640
+ throw error;
641
+ }
642
+ };
643
+ const attempt = await (0, provider_attempt_1.runProviderAttempt)({
644
+ operation: "turn",
645
+ provider: providerRuntime.id,
646
+ model: providerRuntime.model,
647
+ run: callProviderTurnWithOverflowRecovery,
648
+ classifyError: (error) => providerRuntime.classifyError(error),
649
+ onRetry: (record, maxAttempts) => {
650
+ const delayMs = record.delayMs;
651
+ const seconds = delayMs / 1000;
652
+ const cause = RETRY_LABELS[record.classification];
653
+ callbacks.onError(new Error(`${cause}, retrying in ${seconds}s (${record.attempt}/${maxAttempts})...`), "transient");
654
+ },
655
+ sleep: async (delayMs) => {
656
+ await waitForProviderRetry(delayMs, signal);
657
+ providerRuntime.resetTurnState(messages);
658
+ },
594
659
  });
660
+ if (!attempt.ok) {
661
+ finishTerminalProviderError(attempt.error, attempt.classification);
662
+ continue;
663
+ }
664
+ const result = attempt.value;
595
665
  // Track usage from the latest API call
596
666
  if (result.usage)
597
667
  lastUsage = result.usage;
598
- retryCount = 0; // reset on success
599
668
  // SHARED: build CC-format assistant message from TurnResult
600
669
  const msg = {
601
670
  role: "assistant",
@@ -978,26 +1047,11 @@ async function runAgent(messages, callbacks, channel, signal, options) {
978
1047
  }
979
1048
  catch (e) {
980
1049
  // Abort is not an error — just stop cleanly
981
- if (signal?.aborted) {
1050
+ if (e instanceof provider_attempt_1.ProviderAttemptAbortError || signal?.aborted) {
982
1051
  stripLastToolCalls(messages);
983
1052
  outcome = "aborted";
984
1053
  break;
985
1054
  }
986
- // Context overflow: trim aggressively and retry once
987
- if (isContextOverflow(e) && !overflowRetried) {
988
- overflowRetried = true;
989
- stripLastToolCalls(messages);
990
- const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
991
- const trimmed = (0, context_1.trimMessages)(messages, maxTokens, contextMargin, maxTokens * 2);
992
- messages.splice(0, messages.length, ...trimmed);
993
- providerRuntime.resetTurnState(messages);
994
- callbacks.onError(new Error("context trimmed, retrying..."), "transient");
995
- continue;
996
- }
997
- // Retry policy: retry every error EXCEPT those on the blocklist
998
- // (NON_RETRYABLE_HTTP_STATUSES / NON_RETRYABLE_CLASSIFICATIONS).
999
- // The classification still drives the user-facing label and the
1000
- // auth-failure guidance message below — it just no longer gates retries.
1001
1055
  const errorForClassification = e instanceof Error ? e : /* v8 ignore next -- defensive @preserve */ new Error(String(e));
1002
1056
  let providerClassification;
1003
1057
  try {
@@ -1007,77 +1061,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1007
1061
  /* v8 ignore next -- defensive: classifyError should not throw @preserve */
1008
1062
  providerClassification = "unknown";
1009
1063
  }
1010
- const blocked = isRetryBlocked(errorForClassification, providerClassification);
1011
- const shouldRetry = !blocked && retryCount < MAX_RETRIES;
1012
- (0, runtime_1.emitNervesEvent)({
1013
- level: shouldRetry ? "info" : "warn",
1014
- event: shouldRetry ? "engine.provider_retry" : "engine.provider_retry_skip",
1015
- component: "engine",
1016
- message: shouldRetry
1017
- ? `provider error is retryable (attempt ${retryCount + 1}/${MAX_RETRIES})`
1018
- : blocked
1019
- ? `provider error is on retry blocklist`
1020
- : `provider error retries exhausted`,
1021
- meta: {
1022
- provider: providerRuntime.id,
1023
- model: providerRuntime.model,
1024
- retryCount,
1025
- maxRetries: MAX_RETRIES,
1026
- blocked,
1027
- providerClassification,
1028
- errorMessage: errorForClassification.message.slice(0, 200),
1029
- httpStatus: e.status ?? null,
1030
- },
1031
- });
1032
- if (shouldRetry) {
1033
- retryCount++;
1034
- const delay = RETRY_BASE_MS * Math.pow(2, retryCount - 1);
1035
- const cause = RETRY_LABELS[providerClassification];
1036
- callbacks.onError(new Error(`${cause}, retrying in ${delay / 1000}s (${retryCount}/${MAX_RETRIES})...`), "transient");
1037
- // Wait with abort support
1038
- const aborted = await new Promise((resolve) => {
1039
- const timer = setTimeout(() => resolve(false), delay);
1040
- if (signal) {
1041
- const onAbort = () => { clearTimeout(timer); resolve(true); };
1042
- if (signal.aborted) {
1043
- clearTimeout(timer);
1044
- resolve(true);
1045
- return;
1046
- }
1047
- signal.addEventListener("abort", onAbort, { once: true });
1048
- }
1049
- });
1050
- if (aborted) {
1051
- stripLastToolCalls(messages);
1052
- outcome = "aborted";
1053
- break;
1054
- }
1055
- providerRuntime.resetTurnState(messages);
1056
- continue;
1057
- }
1058
- terminalError = errorForClassification;
1059
- terminalErrorClassification = providerClassification;
1060
- /* v8 ignore start — auth-failure guidance: tested via provider error classification tests @preserve */
1061
- if (terminalErrorClassification === "auth-failure") {
1062
- const agentName = (0, identity_2.getAgentName)();
1063
- const currentProvider = providerRuntime.id;
1064
- callbacks.onError(new Error(buildAuthFailureGuidance(currentProvider, providerRuntime.model, agentName, terminalError.message)), "terminal");
1065
- }
1066
- else {
1067
- callbacks.onError(terminalError, "terminal");
1068
- }
1069
- /* v8 ignore stop */
1070
- (0, runtime_1.emitNervesEvent)({
1071
- level: "error",
1072
- event: "engine.error",
1073
- trace_id: traceId,
1074
- component: "engine",
1075
- message: terminalError.message,
1076
- meta: { errorClassification: terminalErrorClassification },
1077
- });
1078
- stripLastToolCalls(messages);
1079
- outcome = "errored";
1080
- done = true;
1064
+ finishTerminalProviderError(errorForClassification, providerClassification);
1081
1065
  }
1082
1066
  }
1083
1067
  (0, runtime_1.emitNervesEvent)({