@ouro.bot/cli 0.1.0-alpha.938 → 0.1.0-alpha.939

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,15 @@
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.939",
6
+ "changes": [
7
+ "Shepherd attributes failed observations to itself without inventing an agent stop or a human decision, and keeps explicit HTTP responses separate from pure transport failures.",
8
+ "Retain bounded pre-dispatch judgment retries and real offline-to-online evidence, advance new outage probes, and prevent a stalled judge from blocking unrelated sessions with bounded per-session scheduling.",
9
+ "Capture queued generations, coalesce stale per-session work, preserve uncertain-send replay barriers and existing focus/pause/menu guards, and wait for active observations before closing custody.",
10
+ "This P0-only release preserves existing legacy automation warnings; native guarded admission and full Shepherd v1 remain unqualified."
11
+ ]
12
+ },
4
13
  {
5
14
  "version": "0.1.0-alpha.938",
6
15
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.938",
2
+ "runtimeVersion": "0.1.0-alpha.939",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-09-03T00:00:00.000Z"
5
5
  }
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0"?>
2
2
  <Container version="2">
3
3
  <Name>ouro-butler</Name>
4
- <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.938</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.939</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -30,6 +30,8 @@ const NETWORK_ERROR_MESSAGE_PATTERNS = [
30
30
  // True if the error looks like a transient network issue (no HTTP status, just
31
31
  // a socket/DNS/timeout failure from the underlying transport).
32
32
  function isNetworkError(error) {
33
+ if (error.status !== undefined)
34
+ return false;
33
35
  const code = error.code || "";
34
36
  if (NETWORK_ERROR_CODES.has(code))
35
37
  return true;
@@ -127,9 +127,9 @@ function noticeLine(record, location) {
127
127
  case "working":
128
128
  return `${subject} is still working in the background and has news. ${why}`;
129
129
  case "unclear":
130
- return `${subject} stopped and I could not tell what it needs, so I left it. ${why}`;
130
+ return `Shepherd could not determine the current state of ${subject}. ${why}`;
131
131
  default:
132
- return `${subject} stopped but I could not judge it, so I left it. ${why}`;
132
+ return `Shepherd could not complete its observation of ${subject}; the agent's state was not determined. ${why}`;
133
133
  }
134
134
  }
135
135
  /** A bounded display of latest observations; all input records retain delivery custody. */
@@ -87,6 +87,8 @@ const ESCALATION_TTL_MS = 30 * 60_000;
87
87
  const WAKE_DELAY_MS = 2_000;
88
88
  const FIELD_MAX = 200;
89
89
  const CONNECTIVITY_POLL_MS = 15_000;
90
+ const RECOVERY_JUDGE_ATTEMPTS = 3;
91
+ const CONCURRENT_OBSERVATIONS = 4;
90
92
  const NETWORK_RESUME = "Connectivity has recovered. Continue the existing task only if safe. Check uncertain writes before retrying. Do not bypass approvals or deliberate pauses.";
91
93
  exports.PERIODIC_RESUME = "Reassess your existing task and continue only already-authorized remaining work. Respect human decisions and pauses. Reconcile uncertain effects before retrying; do not wait for a scheduled tick when you can work now.";
92
94
  const fingerprint = (value) => (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(value)).digest("hex");
@@ -225,10 +227,14 @@ async function startShepherdSenseApp(options) {
225
227
  const wakeIds = [];
226
228
  let cancelWake = null;
227
229
  let chain = Promise.resolve();
230
+ const pending = [];
231
+ const active = new Set();
232
+ const activeSessions = new Set();
228
233
  const networkWaiting = new Map();
229
234
  const latestReturns = new Map();
230
235
  const checkConnectivity = options.checkConnectivity ?? checkShepherdConnectivity;
231
236
  let cancelConnectivity = null;
237
+ let connectivityImmediate = false;
232
238
  let recoveryGeneration = 0;
233
239
  let stopped = false;
234
240
  let cancelCheckIn = null;
@@ -238,6 +244,37 @@ async function startShepherdSenseApp(options) {
238
244
  const history = new Map((0, returns_1.readReturns)(agentRoot).filter(record => record.host === host.name).map(record => [record.session, record]));
239
245
  const generationFor = (session) => generations.get(session) ?? 0;
240
246
  const invalidate = (session) => generations.set(session, generationFor(session) + 1);
247
+ function enqueue(event, trigger = { kind: "immediate" }, generation = generationFor(event.sessionId)) {
248
+ const index = pending.findIndex(job => job.event.sessionId === event.sessionId);
249
+ const previous = pending[index];
250
+ if (previous && (previous.generation > generation ||
251
+ (previous.generation === generation && previous.trigger.kind !== "periodic" && trigger.kind === "periodic")))
252
+ return;
253
+ const job = { event, trigger, generation };
254
+ if (previous)
255
+ pending[index] = job;
256
+ else
257
+ pending.push(job);
258
+ drain();
259
+ }
260
+ function drain() {
261
+ while (!stopped && active.size < CONCURRENT_OBSERVATIONS) {
262
+ const index = pending.findIndex(job => !activeSessions.has(job.event.sessionId));
263
+ if (index === -1)
264
+ return;
265
+ const job = pending.splice(index, 1)[0];
266
+ activeSessions.add(job.event.sessionId);
267
+ const task = Promise.resolve().then(() => handle(job.event, job.trigger, job.generation)).catch(error => {
268
+ (0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.shepherd_chain_error",
269
+ message: "Shepherd observation failed; other sessions remain scheduled", meta: { agent, session: job.event.sessionId, error: (0, redact_1.redactSecrets)(String(error)) } });
270
+ }).finally(() => {
271
+ active.delete(task);
272
+ activeSessions.delete(job.event.sessionId);
273
+ drain();
274
+ });
275
+ active.add(task);
276
+ }
277
+ }
241
278
  function scheduleCheckIn() {
242
279
  if (stopped || checkInMs === 0 || !host.inspect || cancelCheckIn)
243
280
  return;
@@ -281,14 +318,14 @@ async function startShepherdSenseApp(options) {
281
318
  return;
282
319
  cancelQuietBatch = schedule(() => {
283
320
  cancelQuietBatch = null;
284
- chain = chain.then(async () => {
321
+ chain = chain.then(() => {
285
322
  for (const candidate of candidates) {
286
323
  if (stopped)
287
324
  break;
288
325
  const current = candidate.trigger.first;
289
- await handle({ sessionId: candidate.sessionId, transitionId: `periodic:${candidate.trigger.pass}:${candidate.sessionId}`,
326
+ enqueue({ sessionId: candidate.sessionId, transitionId: `periodic:${candidate.trigger.pass}:${candidate.sessionId}`,
290
327
  agent: current.session.agent, cwd: current.session.cwd ?? null, lastBody: null, backgroundWork: current.backgroundWork,
291
- title: current.session.title, workspaceTitle: current.session.workspaceTitle }, candidate.trigger);
328
+ title: current.session.title, workspaceTitle: current.session.workspaceTitle }, candidate.trigger, candidate.trigger.generation);
292
329
  }
293
330
  });
294
331
  }, cmux_1.QUIET_MS);
@@ -301,13 +338,20 @@ async function startShepherdSenseApp(options) {
301
338
  scheduleCheckIn();
302
339
  }
303
340
  }
304
- function scheduleConnectivity() {
305
- if (stopped || networkWaiting.size === 0 || cancelConnectivity)
341
+ function scheduleConnectivity(delay = CONNECTIVITY_POLL_MS) {
342
+ if (stopped || networkWaiting.size === 0)
306
343
  return;
344
+ if (cancelConnectivity) {
345
+ if (delay !== 0 || connectivityImmediate)
346
+ return;
347
+ cancelConnectivity();
348
+ }
349
+ connectivityImmediate = delay === 0;
307
350
  cancelConnectivity = schedule(() => {
308
351
  cancelConnectivity = null;
352
+ connectivityImmediate = false;
309
353
  chain = chain.then(checkRecovery);
310
- }, CONNECTIVITY_POLL_MS);
354
+ }, delay);
311
355
  }
312
356
  async function checkRecovery() {
313
357
  if (stopped || networkWaiting.size === 0)
@@ -322,7 +366,7 @@ async function startShepherdSenseApp(options) {
322
366
  candidate.offlineConfirmed = true;
323
367
  }
324
368
  else if (connectivity === "online") {
325
- const candidates = [...networkWaiting.values()].filter(candidate => candidate.offlineConfirmed);
369
+ const candidates = [...networkWaiting.values()].filter(candidate => candidate.offlineConfirmed || candidate.recheck);
326
370
  if (candidates.length === 0)
327
371
  return;
328
372
  recoveryGeneration += 1;
@@ -333,10 +377,14 @@ async function startShepherdSenseApp(options) {
333
377
  for (const candidate of candidates) {
334
378
  if (stopped)
335
379
  break;
336
- if (latestReturns.get(candidate.event.sessionId) !== candidate.event ||
380
+ if ((!candidate.recheck || candidate.recheck.kind !== "periodic") && latestReturns.get(candidate.event.sessionId) !== candidate.event ||
381
+ candidate.eventGeneration !== generationFor(candidate.event.sessionId) ||
337
382
  !sessions.some(session => session.id === candidate.event.sessionId))
338
383
  continue;
339
- await handle(candidate.event, { kind: "network", screen: candidate.screen, generation: recoveryGeneration });
384
+ const recheck = candidate.recheck?.kind === "immediate" && candidate.offlineConfirmed ? undefined : candidate.recheck;
385
+ enqueue(candidate.event, recheck ?? {
386
+ kind: "network", screen: candidate.screen, generation: recoveryGeneration, attempt: candidate.attempts + 1,
387
+ }, candidate.eventGeneration);
340
388
  }
341
389
  }
342
390
  }
@@ -347,11 +395,15 @@ async function startShepherdSenseApp(options) {
347
395
  scheduleConnectivity();
348
396
  }
349
397
  }
350
- function rememberNetworkWait(event, screen) {
351
- if (stopped || event.backgroundWork || latestReturns.get(event.sessionId) !== event)
352
- return;
353
- networkWaiting.set(event.sessionId, { event, screen, offlineConfirmed: false });
354
- scheduleConnectivity();
398
+ function rememberNetworkWait(event, screen, eventGeneration, recoveryAttempt = 0, recheck) {
399
+ if (stopped || event.backgroundWork || (recheck?.kind !== "periodic" && latestReturns.get(event.sessionId) !== event) ||
400
+ eventGeneration !== generationFor(event.sessionId))
401
+ return false;
402
+ networkWaiting.set(event.sessionId, {
403
+ event, screen, offlineConfirmed: !recheck && recoveryAttempt > 0, eventGeneration, attempts: recoveryAttempt, recheck,
404
+ });
405
+ scheduleConnectivity(recoveryAttempt === 0 || (recheck && recoveryAttempt === 1) ? 0 : CONNECTIVITY_POLL_MS);
406
+ return true;
355
407
  }
356
408
  const guardFor = (session) => {
357
409
  let entry = guard.get(session);
@@ -408,11 +460,12 @@ async function startShepherdSenseApp(options) {
408
460
  return true;
409
461
  }
410
462
  /** Judges one return and acts on it. Every outcome, including a failure, is one log line. */
411
- async function handle(event, trigger = { kind: "immediate" }) {
463
+ async function handle(event, trigger, eventGeneration) {
412
464
  if (stopped)
413
465
  return;
414
466
  const recovery = trigger.kind === "network" ? trigger : null;
415
467
  const periodic = trigger.kind === "periodic" ? trigger : null;
468
+ const observationRetry = trigger.kind === "immediate" ? trigger.retry : undefined;
416
469
  const record = {
417
470
  at: new Date(now()).toISOString(), host: host.name, session: event.sessionId, transition: recovery ? `network:${recovery.generation}:${event.transitionId}` : event.transitionId,
418
471
  agent: event.agent, cwd: event.cwd, task: null, kind: "unclear", action: "let_through", reason: "", reply: null, latencyMs: null, inputTokens: null,
@@ -467,7 +520,7 @@ async function startShepherdSenseApp(options) {
467
520
  const text = (0, redact_1.redactSecrets)(rawScreen);
468
521
  record.task = [...text.matchAll(/Desk-Task:\s*(\S+)/g)].pop()?.[1] ?? null;
469
522
  const screen = (0, redact_1.screenTail)(text, exports.SCREEN_LINES);
470
- if (recovery && screen !== recovery.screen) {
523
+ if ((recovery || observationRetry) && screen !== (recovery?.screen ?? observationRetry.screen)) {
471
524
  record.actionBlockReason = "state_changed";
472
525
  record.reason = "the screen changed since the network stop, so recovery was not sent";
473
526
  return;
@@ -484,6 +537,19 @@ async function startShepherdSenseApp(options) {
484
537
  };
485
538
  const guidance = checkedGuidance();
486
539
  record.guidanceRevision = guidance.revision;
540
+ if (stopped) {
541
+ notice = false;
542
+ record.actionBlockReason = "stopped";
543
+ record.reason = "Shepherd stopped before judgment; no input or notice sent";
544
+ return;
545
+ }
546
+ if ((observationRetry || periodic?.attempt) &&
547
+ (guidance.paused || current?.session.focused || current?.backgroundWork || now() - state.focusedAt < exports.FOCUS_HOLD_MS)) {
548
+ notice = false;
549
+ record.actionBlockReason = guidance.paused ? "guidance" : "state_changed";
550
+ record.reason = "the failed observation retry is now protected; no judgment or input sent";
551
+ return;
552
+ }
487
553
  let verdict;
488
554
  try {
489
555
  const prior = history.get(event.sessionId);
@@ -494,8 +560,19 @@ async function startShepherdSenseApp(options) {
494
560
  ...(periodic ? { periodicCheckIn: { priorJudgment: prior ? { kind: prior.kind, reason: prior.reason, at: prior.at } : null } } : {}) });
495
561
  }
496
562
  catch (error) {
497
- if (!recovery && !periodic && error instanceof Error && (0, error_classification_1.isNetworkError)(error))
498
- rememberNetworkWait(event, screen);
563
+ if (periodic)
564
+ notice = !stopped && eventGeneration === generationFor(event.sessionId) && !guidance.paused &&
565
+ current?.session.focused !== true && current?.backgroundWork !== true && now() - state.focusedAt >= exports.FOCUS_HOLD_MS;
566
+ if (error instanceof Error && (0, error_classification_1.isNetworkError)(error)) {
567
+ const attempt = recovery?.attempt ?? periodic?.attempt ?? observationRetry?.attempt ?? 1;
568
+ if (attempt < RECOVERY_JUDGE_ATTEMPTS && !guidance.paused &&
569
+ current?.session.focused !== true && current?.backgroundWork !== true &&
570
+ now() - state.focusedAt >= exports.FOCUS_HOLD_MS &&
571
+ rememberNetworkWait(event, screen, eventGeneration, attempt, recovery ? undefined :
572
+ periodic ? { ...periodic, attempt: attempt + 1 } : { kind: "immediate", retry: { screen, attempt: attempt + 1 } })) {
573
+ notice = false;
574
+ }
575
+ }
499
576
  throw error;
500
577
  }
501
578
  judged = true;
@@ -510,7 +587,7 @@ async function startShepherdSenseApp(options) {
510
587
  Object.assign(record, { kind, judgeKind: verdict.kind, reason: verdict.reason, latencyMs: verdict.latencyMs, inputTokens: verdict.inputTokens,
511
588
  ...(verdict.replyIssue ? { replyIssue: verdict.replyIssue } : {}) });
512
589
  if (!recovery && !periodic && kind === "waiting" && verdict.waitingOnNetwork)
513
- rememberNetworkWait(event, screen);
590
+ rememberNetworkWait(event, screen, eventGeneration);
514
591
  const noticeScreen = screen.replace(/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})\b/g, "<observation-time>");
515
592
  noticeBasis = [host.name, event.sessionId, record.agent, record.cwd, record.task, noticeScreen, guidance.revision];
516
593
  notice = true;
@@ -575,7 +652,8 @@ async function startShepherdSenseApp(options) {
575
652
  }
576
653
  // No awaited host reads between this final check and initiating the send.
577
654
  const latest = checkedGuidance();
578
- if (stopped || (recovery && (latestReturns.get(event.sessionId) !== event || now() - state.focusedAt < exports.FOCUS_HOLD_MS)) ||
655
+ if (stopped || eventGeneration !== generationFor(event.sessionId) ||
656
+ (recovery && (latestReturns.get(event.sessionId) !== event || now() - state.focusedAt < exports.FOCUS_HOLD_MS)) ||
579
657
  periodic && (periodic.generation !== generationFor(event.sessionId) || now() - state.focusedAt < exports.FOCUS_HOLD_MS)) {
580
658
  record.actionBlockReason = stopped ? "stopped" : now() - state.focusedAt < exports.FOCUS_HOLD_MS ? "focus" : "state_changed";
581
659
  record.reason = `Shepherd stopped, the human focused this session, or it returned again while judging, so nothing was sent: ${verdict.reason}`;
@@ -697,7 +775,7 @@ async function startShepherdSenseApp(options) {
697
775
  invalidate(event.sessionId);
698
776
  networkWaiting.delete(event.sessionId);
699
777
  latestReturns.set(event.sessionId, event);
700
- chain = chain.then(() => handle(event));
778
+ enqueue(event);
701
779
  },
702
780
  focused: (session) => {
703
781
  if (stopped)
@@ -718,13 +796,13 @@ async function startShepherdSenseApp(options) {
718
796
  stopped = true;
719
797
  observations.stop();
720
798
  networkWaiting.clear();
799
+ pending.length = 0;
721
800
  watcher.close();
722
801
  cancelWake?.();
723
802
  cancelConnectivity?.();
724
803
  cancelCheckIn?.();
725
804
  cancelQuietBatch?.();
726
- void chain.then(() => custody.close(), error => {
727
- custody.close();
805
+ void chain.finally(() => Promise.all(active)).finally(() => custody.close()).catch(error => {
728
806
  (0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.shepherd_chain_error",
729
807
  message: "Shepherd chain failed during shutdown", meta: { agent, error: (0, redact_1.redactSecrets)(String(error)) } });
730
808
  });
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.938",
3
+ "version": "0.1.0-alpha.939",
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.938",
9
+ "version": "0.1.0-alpha.939",
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.938",
3
+ "version": "0.1.0-alpha.939",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },