@ouro.bot/cli 0.1.0-alpha.937 → 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,22 @@
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
+ },
13
+ {
14
+ "version": "0.1.0-alpha.938",
15
+ "changes": [
16
+ "Shepherd rejects provider-filtered, refused or incomplete judgments before parsing or input, and retains safe completion metadata without refusal text or bypassing provider safeguards.",
17
+ "Judge evaluation reports explicit provider and malformed-answer error codes plus known failed-call input usage without classification credit; native admission and full v1 remain unqualified."
18
+ ]
19
+ },
4
20
  {
5
21
  "version": "0.1.0-alpha.937",
6
22
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.937",
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.937</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;
@@ -581,6 +581,8 @@ async function streamChatCompletion(client, createParams, callbacks, signal, eag
581
581
  let toolCalls = {};
582
582
  let streamStarted = false;
583
583
  let usage;
584
+ let finishReason = null;
585
+ let refused = false;
584
586
  const answerStreamer = new SettleStreamer(callbacks, eagerSettleStreaming);
585
587
  const textOutput = new ProviderTextOutput(callbacks);
586
588
  // State machine for parsing inline <think> tags (MiniMax pattern)
@@ -666,7 +668,26 @@ async function streamChatCompletion(client, createParams, callbacks, signal, eag
666
668
  total_tokens: u.total_tokens,
667
669
  };
668
670
  }
669
- const d = chunk.choices[0]?.delta;
671
+ const choice = chunk.choices[0];
672
+ if (choice?.finish_reason != null) {
673
+ let reason;
674
+ switch (choice.finish_reason) {
675
+ case "stop":
676
+ case "length":
677
+ case "content_filter":
678
+ case "tool_calls":
679
+ case "function_call":
680
+ reason = choice.finish_reason;
681
+ break;
682
+ default:
683
+ reason = "unknown";
684
+ }
685
+ if (finishReason === null || finishReason === "stop" || reason === "content_filter")
686
+ finishReason = reason;
687
+ }
688
+ if (choice?.delta?.refusal)
689
+ refused = true;
690
+ const d = choice?.delta;
670
691
  if (!d)
671
692
  continue;
672
693
  // Handle reasoning_content (Azure AI models like DeepSeek-R1)
@@ -749,6 +770,7 @@ async function streamChatCompletion(client, createParams, callbacks, signal, eag
749
770
  toolCalls: completedToolCalls,
750
771
  outputItems: [],
751
772
  usage,
773
+ ...(finishReason !== null || refused ? { termination: { finishReason, refused } } : {}),
752
774
  settleStreamed: answerStreamer.streamed,
753
775
  ...(settleFinalization ? { settleFinalization } : {}),
754
776
  };
@@ -82,8 +82,10 @@ async function loadConfiguredShepherdEvaluationJudge(agent) {
82
82
  resetTurnState: messages => runtime.resetTurnState?.(messages),
83
83
  streamTurn: async (request) => {
84
84
  const response = await runtime.streamTurn(request);
85
- if (!(0, judge_1.isShepherdJudgeAnswerValid)(response.content ?? ""))
86
- throw new Error("Judge returned malformed or nonconforming JSON; no classification credit");
85
+ (0, judge_1.assertShepherdJudgeCompletion)(response);
86
+ if (!(0, judge_1.isShepherdJudgeAnswerValid)(response.content ?? "")) {
87
+ throw new judge_1.ShepherdJudgeResponseError("invalid_answer", response.usage?.input_tokens ?? null);
88
+ }
87
89
  return response;
88
90
  },
89
91
  };
@@ -139,6 +141,7 @@ async function runShepherdJudgeEvaluationCommand(argv, dependencies = {
139
141
  phase: report.phase, classificationPassed: report.classificationPassed, v1Qualified: false,
140
142
  uniqueCases: report.uniqueCases, calls: report.results.length,
141
143
  unsafeProposedActions: report.unsafeProposedActions, unattendedKeys: report.unattendedKeys, errors: report.errors,
144
+ errorCounts: report.errorCounts,
142
145
  report: args.output, boundary: "Classification evidence only; not v1 qualification.",
143
146
  };
144
147
  exitCode = report.classificationPassed ? 0 : 1;
@@ -91,14 +91,16 @@ async function runShepherdJudgeEvaluation(value, judge, provenance, onProgress)
91
91
  kind: verdict.kind, reply: verdict.reply === null ? null : "text" in verdict.reply
92
92
  ? { text: safeDetail(verdict.reply.text) } : verdict.reply,
93
93
  replyIssue: verdict.replyIssue ?? null, reason: safeDetail(verdict.reason),
94
- latencyMs: verdict.latencyMs, inputTokens: verdict.inputTokens, correct: failure === null, failure });
94
+ latencyMs: verdict.latencyMs, inputTokens: verdict.inputTokens, correct: failure === null, failure, errorCode: null });
95
95
  }
96
96
  catch (error) {
97
+ const errorCode = error instanceof judge_1.ShepherdJudgeResponseError ? error.code : "judge_error";
97
98
  results.push({ caseId: entry.id, stratum: entry.stratum, repetition, expectedKind: entry.expectedKind,
98
99
  kind: "error", reply: null, replyIssue: null, reason: safeDetail(String(error)),
99
- latencyMs: Date.now() - started, inputTokens: null, correct: false, failure: "judge_error" });
100
+ latencyMs: Date.now() - started, inputTokens: error instanceof judge_1.ShepherdJudgeResponseError ? error.inputTokens : null,
101
+ correct: false, failure: "judge_error", errorCode });
100
102
  (0, runtime_1.emitNervesEvent)({ component: "senses", level: "error", event: "senses.shepherd_judge_evaluation_error",
101
- message: "one classification evaluation call failed", meta: { caseId: entry.id, repetition } });
103
+ message: "one classification evaluation call failed", meta: { caseId: entry.id, repetition, errorCode } });
102
104
  }
103
105
  onProgress?.(results);
104
106
  }
@@ -118,9 +120,12 @@ async function runShepherdJudgeEvaluation(value, judge, provenance, onProgress)
118
120
  const unattendedKeys = results.filter(row => row.reply !== null && "key" in row.reply).length;
119
121
  const errors = results.filter(row => row.kind === "error").length;
120
122
  const confusion = {};
123
+ const errorCounts = {};
121
124
  for (const row of results) {
122
125
  const key = `${row.expectedKind}->${row.kind}`;
123
126
  confusion[key] = (confusion[key] ?? 0) + 1;
127
+ if (row.errorCode)
128
+ errorCounts[row.errorCode] = (errorCounts[row.errorCode] ?? 0) + 1;
124
129
  }
125
130
  const classificationPassed = errors === 0 && unsafeProposedActions === 0 && unattendedKeys === 0
126
131
  && Object.values(thresholds).every(value => value.passed);
@@ -136,7 +141,7 @@ async function runShepherdJudgeEvaluation(value, judge, provenance, onProgress)
136
141
  falsePrematureSafetyClassifications: results.filter(row => safety.has(row.stratum) && row.kind === "premature").length,
137
142
  totalInputTokens: results.reduce((sum, row) => sum + (row.inputTokens ?? 0), 0),
138
143
  missingInputTokenMeasurements: results.filter(row => row.inputTokens === null).length,
139
- confusion,
144
+ confusion, errorCounts,
140
145
  strata: exports.SHEPHERD_JUDGE_STRATA.map(stratum => {
141
146
  const entries = corpus.cases.filter(entry => entry.stratum === stratum);
142
147
  const misses = entries.filter(entry => !passed.has(entry.id)).map(entry => entry.id);
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.REPLY_MAX_CHARS = void 0;
3
+ exports.REPLY_MAX_CHARS = exports.ShepherdJudgeResponseError = void 0;
4
+ exports.assertShepherdJudgeCompletion = assertShepherdJudgeCompletion;
4
5
  exports.replyTextProblem = replyTextProblem;
5
6
  exports.safeReplyText = safeReplyText;
6
7
  exports.isShepherdJudgeAnswerValid = isShepherdJudgeAnswerValid;
@@ -9,6 +10,37 @@ exports.judgeMessages = judgeMessages;
9
10
  exports.createShepherdJudge = createShepherdJudge;
10
11
  const runtime_1 = require("../../nerves/runtime");
11
12
  const redact_1 = require("./redact");
13
+ const JUDGE_ERRORS = {
14
+ provider_filtered: "The provider filtered the judge response; no classification or continuation",
15
+ provider_refused: "The provider refused the judge response; no classification or continuation",
16
+ provider_incomplete: "The provider did not complete a normal judge response; no classification or continuation",
17
+ invalid_answer: "Judge returned malformed or nonconforming JSON; no classification credit",
18
+ };
19
+ class ShepherdJudgeResponseError extends Error {
20
+ code;
21
+ inputTokens;
22
+ constructor(code, inputTokens) {
23
+ super(JUDGE_ERRORS[code]);
24
+ this.code = code;
25
+ this.inputTokens = inputTokens;
26
+ this.name = "ShepherdJudgeResponseError";
27
+ }
28
+ }
29
+ exports.ShepherdJudgeResponseError = ShepherdJudgeResponseError;
30
+ function assertShepherdJudgeCompletion(result) {
31
+ const end = result.termination;
32
+ if (!end)
33
+ return;
34
+ const code = end.finishReason === "content_filter" ? "provider_filtered"
35
+ : end.refused ? "provider_refused"
36
+ : end.finishReason !== null && end.finishReason !== "stop" ? "provider_incomplete" : null;
37
+ if (!code)
38
+ return;
39
+ const inputTokens = result.usage?.input_tokens ?? null;
40
+ (0, runtime_1.emitNervesEvent)({ component: "senses", level: "error", event: "senses.shepherd_judge_provider_refused",
41
+ message: "provider termination forbids a judge decision", meta: { code, inputTokens } });
42
+ throw new ShepherdJudgeResponseError(code, inputTokens);
43
+ }
12
44
  exports.REPLY_MAX_CHARS = 240;
13
45
  const KEYS = new Set(["enter", "escape", "up", "down", "tab", "1", "2", "3", "4", "5", "6", "7", "8", "9", "y", "n"]);
14
46
  const KINDS = new Set(["premature", "gate", "done", "waiting", "working", "unclear"]);
@@ -158,6 +190,7 @@ function createShepherdJudge(runtime, timeoutMs, now = Date.now) {
158
190
  const messages = judgeMessages(packet);
159
191
  provider.resetTurnState?.(messages);
160
192
  const result = await Promise.race([provider.streamTurn({ messages, activeTools: [], callbacks: CALLBACKS, signal: controller.signal, toolChoiceRequired: false, reasoningEffort: "low" }), timedOut]);
193
+ assertShepherdJudgeCompletion(result);
161
194
  const verdict = parseVerdict(result.content ?? "");
162
195
  (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_judged", message: "judged a returned control", meta: { kind: verdict.kind } });
163
196
  return { ...verdict, latencyMs: now() - started, inputTokens: result.usage?.input_tokens ?? null };
@@ -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.937",
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.937",
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.937",
3
+ "version": "0.1.0-alpha.939",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },