@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 +16 -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/heart/streaming.js +23 -1
- package/dist/senses/shepherd/judge-evaluation-command.js +5 -2
- package/dist/senses/shepherd/judge-evaluation.js +9 -4
- package/dist/senses/shepherd/judge.js +34 -1
- 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,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,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;
|
package/dist/heart/streaming.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
86
|
-
|
|
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:
|
|
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
|
|
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",
|