@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 +9 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/providers/error-classification.js +2 -0
- package/dist/senses/shepherd/owner-notice.js +2 -2
- package/dist/senses/shepherd/sense.js +101 -23
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
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,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.
|
|
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
|
|
130
|
+
return `Shepherd could not determine the current state of ${subject}. ${why}`;
|
|
131
131
|
default:
|
|
132
|
-
return
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
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 (
|
|
498
|
-
|
|
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 ||
|
|
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
|
-
|
|
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.
|
|
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
|
});
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
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.
|
|
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",
|