@ouro.bot/cli 0.1.0-alpha.923 → 0.1.0-alpha.925

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.
@@ -0,0 +1,262 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.noticeRecordId = noticeRecordId;
37
+ exports.readNoticeOutbox = readNoticeOutbox;
38
+ exports.enqueueNotice = enqueueNotice;
39
+ exports.noticeDeliveryState = noticeDeliveryState;
40
+ exports.knownNotice = knownNotice;
41
+ exports.declareNoticeTargets = declareNoticeTargets;
42
+ exports.claimPendingNotices = claimPendingNotices;
43
+ exports.recordNoticeDelivery = recordNoticeDelivery;
44
+ const node_crypto_1 = require("node:crypto");
45
+ const fs = __importStar(require("node:fs"));
46
+ const path = __importStar(require("node:path"));
47
+ const node_util_1 = require("node:util");
48
+ const file_lock_1 = require("../../heart/file-lock");
49
+ const runtime_1 = require("../../nerves/runtime");
50
+ const redact_1 = require("./redact");
51
+ const returns_1 = require("./returns");
52
+ const MAX_BYTES = 2 * 1024 * 1024;
53
+ const RECENT_SENT = 256;
54
+ const STATES = new Set(["pending", "sent", "unknown"]);
55
+ const KINDS = new Set(["premature", "gate", "done", "working", "waiting", "unclear", "error", "loop_guard"]);
56
+ const fileFor = (root) => path.join((0, returns_1.shepherdStateDir)(root), "notice-outbox.json");
57
+ const object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
58
+ const nullableString = (value) => value === null || typeof value === "string";
59
+ function isReturn(value) {
60
+ return object(value)
61
+ && ["at", "host", "session", "transition", "kind", "action", "reason"].every(key => typeof value[key] === "string")
62
+ && KINDS.has(String(value.kind)) && ["respond", "let_through"].includes(String(value.action))
63
+ && ["agent", "cwd", "task", "reply"].every(key => nullableString(value[key]))
64
+ && ["latencyMs", "inputTokens"].every(key => value[key] === null || typeof value[key] === "number")
65
+ && (value.title === undefined || typeof value.title === "string")
66
+ && (value.workspaceTitle === undefined || nullableString(value.workspaceTitle));
67
+ }
68
+ function isDelivery(value) {
69
+ return object(value) && typeof value.state === "string" && STATES.has(value.state)
70
+ && typeof value.at === "string" && Number.isSafeInteger(value.attempts) && Number(value.attempts) >= 0
71
+ && (value.reason === undefined || typeof value.reason === "string")
72
+ && (value.messageId === undefined || typeof value.messageId === "string");
73
+ }
74
+ function noticeRecordId(record) {
75
+ return (0, node_crypto_1.createHash)("sha256").update(JSON.stringify([record.host, record.session, record.transition, record.at])).digest("hex");
76
+ }
77
+ function readNoticeOutbox(root) {
78
+ let raw;
79
+ try {
80
+ raw = fs.readFileSync(fileFor(root), "utf8");
81
+ }
82
+ catch (error) {
83
+ if (error.code === "ENOENT")
84
+ return [];
85
+ throw error;
86
+ }
87
+ if (Buffer.byteLength(raw) > MAX_BYTES)
88
+ throw new Error("Shepherd notice outbox exceeds its 2 MiB limit");
89
+ let value;
90
+ try {
91
+ value = JSON.parse(raw);
92
+ }
93
+ catch {
94
+ throw new Error("Shepherd notice outbox is malformed JSON");
95
+ }
96
+ if (!object(value) || value.schemaVersion !== 1 || !Array.isArray(value.entries))
97
+ throw new Error("invalid Shepherd notice outbox schema");
98
+ const entries = [];
99
+ const seen = new Set();
100
+ for (const entry of value.entries) {
101
+ if (!object(entry) || typeof entry.id !== "string" || !isReturn(entry.record)
102
+ || !object(entry.deliveries) || !Object.values(entry.deliveries).every(isDelivery)
103
+ || entry.id !== noticeRecordId(entry.record) || seen.has(entry.id))
104
+ throw new Error("invalid Shepherd notice outbox entry");
105
+ seen.add(entry.id);
106
+ entries.push({ id: entry.id, record: entry.record, deliveries: entry.deliveries });
107
+ }
108
+ return entries;
109
+ }
110
+ function atomicWrite(file, value) {
111
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
112
+ const encoded = JSON.stringify(value);
113
+ if (Buffer.byteLength(encoded) > MAX_BYTES)
114
+ throw new Error("Shepherd notice outbox exceeds its 2 MiB limit");
115
+ const temp = `${file}.${(0, node_crypto_1.randomUUID)()}.tmp`;
116
+ const fd = fs.openSync(temp, "wx", 0o600);
117
+ try {
118
+ fs.writeFileSync(fd, encoded);
119
+ fs.fsyncSync(fd);
120
+ }
121
+ finally {
122
+ fs.closeSync(fd);
123
+ }
124
+ fs.renameSync(temp, file);
125
+ }
126
+ function persist(root, entries) {
127
+ const sent = entries.filter(entry => Object.keys(entry.deliveries).length > 0 && Object.values(entry.deliveries).every(value => value.state === "sent"));
128
+ const older = sent.slice(0, Math.max(0, sent.length - RECENT_SENT));
129
+ const ids = new Set(older.map(entry => entry.id));
130
+ // Archive acceptances before removing their payloads from the active snapshot.
131
+ for (const entry of older)
132
+ atomicWrite(path.join((0, returns_1.shepherdStateDir)(root), "notice-receipts", `${entry.id}.json`), entry);
133
+ atomicWrite(fileFor(root), { schemaVersion: 1, entries: entries.filter(entry => !ids.has(entry.id)) });
134
+ }
135
+ function archived(root, id) {
136
+ const file = path.join((0, returns_1.shepherdStateDir)(root), "notice-receipts", `${id}.json`);
137
+ let raw;
138
+ try {
139
+ raw = fs.readFileSync(file, "utf8");
140
+ }
141
+ catch (error) {
142
+ if (error.code === "ENOENT")
143
+ return undefined;
144
+ throw error;
145
+ }
146
+ const value = JSON.parse(raw);
147
+ if (!object(value) || value.id !== id || !isReturn(value.record) || !object(value.deliveries)
148
+ || !Object.values(value.deliveries).every(isDelivery))
149
+ throw new Error("invalid archived Shepherd notice receipt");
150
+ return { id, record: value.record, deliveries: value.deliveries };
151
+ }
152
+ function mutate(root, change) {
153
+ const dir = (0, returns_1.shepherdStateDir)(root);
154
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
155
+ return (0, file_lock_1.withFileLock)(dir, "notice-outbox", () => {
156
+ const entries = readNoticeOutbox(root);
157
+ const result = change(entries);
158
+ persist(root, entries);
159
+ return result;
160
+ }, { waitMs: 1000 });
161
+ }
162
+ function enqueueNotice(root, record) {
163
+ if (!isReturn(record))
164
+ return Promise.reject(new Error("invalid Shepherd return for a notice"));
165
+ const redacted = {
166
+ ...record,
167
+ reason: (0, redact_1.redactSecrets)(record.reason),
168
+ reply: record.reply === null ? null : (0, redact_1.redactSecrets)(record.reply),
169
+ ...(record.title !== undefined ? { title: (0, redact_1.redactSecrets)(record.title) } : {}),
170
+ ...(record.workspaceTitle !== undefined && record.workspaceTitle !== null ? { workspaceTitle: (0, redact_1.redactSecrets)(record.workspaceTitle) } : {}),
171
+ };
172
+ const normalized = JSON.parse(JSON.stringify(redacted));
173
+ if (!isReturn(normalized))
174
+ return Promise.reject(new Error("invalid normalized Shepherd return for a notice"));
175
+ const safe = normalized;
176
+ const id = noticeRecordId(safe);
177
+ return mutate(root, entries => {
178
+ const existing = entries.find(entry => entry.id === id) ?? archived(root, id);
179
+ if (existing && !(0, node_util_1.isDeepStrictEqual)(existing.record, safe))
180
+ throw new Error("the same return has a different notice body");
181
+ if (!existing)
182
+ entries.push({ id, record: safe, deliveries: {} });
183
+ return id;
184
+ }).then(value => {
185
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_notice_queued", message: "persisted a Shepherd owner notice intent", meta: { id } });
186
+ return value;
187
+ });
188
+ }
189
+ function noticeDeliveryState(entry, friendId) {
190
+ return Object.hasOwn(entry.deliveries, friendId) ? entry.deliveries[friendId].state : "pending";
191
+ }
192
+ function knownNotice(root, id, entries) {
193
+ if (!/^[a-f0-9]{64}$/.test(id))
194
+ throw new Error(`unknown notice: ${id}`);
195
+ return entries.find(value => value.id === id) ?? archived(root, id);
196
+ }
197
+ function validTarget(friendId) {
198
+ return !!friendId && !["__proto__", "constructor", "prototype"].includes(friendId);
199
+ }
200
+ function selectedEntries(root, entries, ids) {
201
+ return ids.map(id => {
202
+ const entry = knownNotice(root, id, entries);
203
+ if (!entry)
204
+ throw new Error(`unknown notice: ${id}`);
205
+ return entry;
206
+ });
207
+ }
208
+ function declareNoticeTargets(root, ids, friendIds, at) {
209
+ if (!friendIds.every(validTarget))
210
+ return Promise.reject(new Error("invalid notice target"));
211
+ return mutate(root, entries => {
212
+ for (const entry of selectedEntries(root, entries, ids)) {
213
+ if (!entries.includes(entry) && friendIds.some(friendId => !Object.hasOwn(entry.deliveries, friendId)))
214
+ throw new Error("notice is already settled; it cannot acquire new recipients");
215
+ for (const friendId of friendIds) {
216
+ if (!Object.hasOwn(entry.deliveries, friendId))
217
+ entry.deliveries[friendId] = { state: "pending", at, attempts: 0 };
218
+ }
219
+ }
220
+ });
221
+ }
222
+ /** Claim under the same cross-process lock as receipts, before any transport side effect. */
223
+ function claimPendingNotices(root, ids, friendId, at) {
224
+ if (!validTarget(friendId))
225
+ return Promise.reject(new Error("invalid notice target"));
226
+ return mutate(root, entries => {
227
+ const claimed = selectedEntries(root, entries, ids).filter(entry => noticeDeliveryState(entry, friendId) === "pending");
228
+ for (const entry of claimed) {
229
+ if (!entries.includes(entry))
230
+ throw new Error("notice is already settled; it cannot acquire new recipients");
231
+ entry.deliveries[friendId] = {
232
+ state: "unknown", at, reason: "awaiting transport acceptance", attempts: (entry.deliveries[friendId]?.attempts ?? 0) + 1,
233
+ };
234
+ }
235
+ return claimed;
236
+ });
237
+ }
238
+ function recordNoticeDelivery(root, ids, friendId, receipt) {
239
+ if (!validTarget(friendId))
240
+ return Promise.reject(new Error("invalid notice target"));
241
+ return mutate(root, entries => {
242
+ let updated = 0;
243
+ for (const entry of selectedEntries(root, entries, ids)) {
244
+ if (!entries.includes(entry) && !Object.hasOwn(entry.deliveries, friendId))
245
+ throw new Error("notice is already settled; it cannot acquire new recipients");
246
+ const previous = Object.hasOwn(entry.deliveries, friendId) ? entry.deliveries[friendId] : undefined;
247
+ // A late failure cannot erase an acceptance another worker already recorded.
248
+ if (previous?.state === "sent")
249
+ continue;
250
+ entry.deliveries[friendId] = {
251
+ ...receipt,
252
+ ...(receipt.reason ? { reason: (0, redact_1.redactSecrets)(receipt.reason) } : {}),
253
+ attempts: previous?.state === "unknown" ? previous.attempts : (previous?.attempts ?? 0) + 1,
254
+ };
255
+ updated += 1;
256
+ }
257
+ return updated;
258
+ }).then(updated => {
259
+ if (updated > 0)
260
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_notice_receipt", message: "persisted a Shepherd owner notice delivery outcome", meta: { friendId, state: receipt.state, notices: updated } });
261
+ });
262
+ }
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.NOTICE_LIST_MAX = exports.NOTICE_COALESCE_MS = void 0;
36
+ exports.NOTICE_RETRY_MS = exports.NOTICE_LIST_MAX = exports.NOTICE_COALESCE_MS = void 0;
37
37
  exports.noticeWanted = noticeWanted;
38
38
  exports.plain = plain;
39
39
  exports.noticeLine = noticeLine;
@@ -47,6 +47,8 @@ const friends_1 = require("@ouro.bot/friends");
47
47
  const await_alert_1 = require("../../heart/awaiting/await-alert");
48
48
  const config_1 = require("../../heart/config");
49
49
  const runtime_1 = require("../../nerves/runtime");
50
+ const returns_1 = require("./returns");
51
+ const notice_outbox_1 = require("./notice-outbox");
50
52
  /**
51
53
  * The owner notice: after a judged return, the agent's owner hears what happened in one short plain message on their
52
54
  * own channel. It is deterministic, written here and sent with no model turn, so the owner hears about every return the
@@ -55,6 +57,7 @@ const runtime_1 = require("../../nerves/runtime");
55
57
  exports.NOTICE_COALESCE_MS = 5_000;
56
58
  /** Returns named one by one in a coalesced message; the rest are counted. */
57
59
  exports.NOTICE_LIST_MAX = 6;
60
+ exports.NOTICE_RETRY_MS = 30_000;
58
61
  const NAME_MAX = 60;
59
62
  const REASON_MAX = 300;
60
63
  const REPLY_MAX = 240;
@@ -105,6 +108,8 @@ function noticeLine(record, location) {
105
108
  return `${subject} needs you. ${why}`;
106
109
  case "done":
107
110
  return `${subject} says it is done. ${why}`;
111
+ case "waiting":
112
+ return `${subject} is waiting on an outside dependency; the work is not yet delivered. ${why}`;
108
113
  case "loop_guard":
109
114
  return `${subject}: I stopped answering it after too many replies, so it is waiting for you. ${why}`;
110
115
  case "working":
@@ -184,7 +189,7 @@ function createTeamsNoticeSender(agentRoot, deps = {}) {
184
189
  bot.catch(() => bots.delete(config.clientId));
185
190
  }
186
191
  const { sendProactiveTeamsMessageToSession } = await Promise.resolve().then(() => __importStar(require("../teams")));
187
- return sendProactiveTeamsMessageToSession({ friendId: target.friendId, sessionKey: target.key, text, textFormat: "plain", intent: "generic_outreach" }, { botApi: await bot, store: new friends_1.FileFriendStore(path.join(agentRoot, "friends")) });
192
+ return sendProactiveTeamsMessageToSession({ friendId: target.friendId, sessionKey: target.key, text, textFormat: "plain", intent: "generic_outreach", captureReceipt: true }, { botApi: await bot, store: new friends_1.FileFriendStore(path.join(agentRoot, "friends")) });
188
193
  };
189
194
  }
190
195
  /** Collects notices for NOTICE_COALESCE_MS after the first, then sends them as one message. It never throws. */
@@ -192,15 +197,39 @@ function createOwnerNotifier(options) {
192
197
  const { agent, policy } = options;
193
198
  const resolveTargets = options.resolveTargets ?? (() => resolveNoticeTargets(options.agentRoot));
194
199
  const send = options.send ?? createTeamsNoticeSender(options.agentRoot);
195
- const waiting = [];
200
+ const retryWrites = new Map();
201
+ const writes = new Set();
196
202
  let cancel = null;
197
- async function flush() {
203
+ let stopped = false;
204
+ let flushing = null;
205
+ function schedule(ms) {
206
+ if (!stopped && policy !== "off")
207
+ cancel ??= options.schedule(() => { void flush(); }, ms);
208
+ }
209
+ function persistIntent(record) {
210
+ const key = `${record.host}:${record.session}:${record.transition}:${record.at}`;
211
+ retryWrites.set(key, record);
212
+ const write = (0, notice_outbox_1.enqueueNotice)(options.agentRoot, record).then(() => {
213
+ retryWrites.delete(key);
214
+ }).catch(error => {
215
+ (0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.shepherd_notice_error", message: "could not persist a Shepherd owner notice intent; it remains waiting", meta: { agent, error: error.message } });
216
+ schedule(exports.NOTICE_RETRY_MS);
217
+ }).finally(() => { writes.delete(write); });
218
+ writes.add(write);
219
+ return write;
220
+ }
221
+ async function runFlush() {
198
222
  cancel?.();
199
223
  cancel = null;
200
- const records = waiting.splice(0);
201
- if (records.length === 0)
224
+ if (stopped || policy === "off")
202
225
  return;
226
+ await Promise.all([...writes]);
227
+ for (const record of [...retryWrites.values()])
228
+ await persistIntent(record);
203
229
  try {
230
+ const entries = (0, notice_outbox_1.readNoticeOutbox)(options.agentRoot).filter(entry => noticeWanted(policy, entry.record) && (Object.keys(entry.deliveries).length === 0 || Object.values(entry.deliveries).some(value => value.state !== "sent")));
231
+ if (entries.length === 0)
232
+ return;
204
233
  const refs = new Map();
205
234
  try {
206
235
  for (const session of await options.listSessions())
@@ -209,41 +238,86 @@ function createOwnerNotifier(options) {
209
238
  catch {
210
239
  // Without the host's handles, each notice names a short session id instead.
211
240
  }
212
- const text = noticeMessage(records, refs);
213
241
  const targets = await resolveTargets();
214
242
  if (targets.length === 0) {
215
- (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_notice_no_target", message: "no family friend is reachable on Teams, so the Shepherd owner notice was not sent", meta: { agent, returns: records.length } });
243
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_notice_no_target", message: "no family friend is reachable on Teams, so the Shepherd owner notice was not sent", meta: { agent, returns: entries.length } });
244
+ schedule(exports.NOTICE_RETRY_MS);
216
245
  return;
217
246
  }
247
+ await (0, notice_outbox_1.declareNoticeTargets)(options.agentRoot, entries.map(entry => entry.id), targets.map(target => target.friendId), new Date().toISOString());
248
+ const reachable = new Set(targets.map(target => target.friendId));
249
+ let retry = entries.some(entry => Object.entries(entry.deliveries).some(([friendId, delivery]) => delivery.state === "pending" && !reachable.has(friendId)));
218
250
  for (const target of targets) {
219
- try {
220
- const result = await send(target, text);
221
- (0, runtime_1.emitNervesEvent)({
222
- level: result.delivered ? "info" : "warn",
223
- component: "senses",
224
- event: result.delivered ? "senses.shepherd_notice_sent" : "senses.shepherd_notice_not_sent",
225
- message: result.delivered ? "sent the Shepherd owner notice" : "the Shepherd owner notice was not delivered",
226
- meta: { agent, friendId: target.friendId, returns: records.length, ...(result.delivered ? {} : { reason: result.reason ?? "unknown" }) },
227
- });
228
- }
229
- catch (error) {
230
- (0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.shepherd_notice_error", message: "sending the Shepherd owner notice failed", meta: { agent, friendId: target.friendId, error: error.message } });
251
+ const pending = entries.filter(entry => (0, notice_outbox_1.noticeDeliveryState)(entry, target.friendId) === "pending");
252
+ for (let start = 0; start < pending.length && !stopped; start += exports.NOTICE_LIST_MAX) {
253
+ // Only the worker that atomically claims a pending entry may initiate transport.
254
+ const batch = await (0, notice_outbox_1.claimPendingNotices)(options.agentRoot, pending.slice(start, start + exports.NOTICE_LIST_MAX).map(entry => entry.id), target.friendId, new Date().toISOString());
255
+ if (batch.length === 0)
256
+ continue;
257
+ const ids = batch.map(entry => entry.id);
258
+ try {
259
+ const result = await send(target, noticeMessage(batch.map(entry => entry.record), refs));
260
+ const state = result.delivered ? "sent"
261
+ : result.delivery ? result.delivery.state === "not_sent" ? "pending" : "unknown"
262
+ : result.reason && result.reason !== "send_error" ? "pending" : "unknown";
263
+ await (0, notice_outbox_1.recordNoticeDelivery)(options.agentRoot, ids, target.friendId, {
264
+ state, at: new Date().toISOString(), ...(result.reason ? { reason: result.reason } : {}),
265
+ ...(result.delivery?.messageId ? { messageId: result.delivery.messageId } : {}),
266
+ });
267
+ retry ||= state === "pending";
268
+ (0, runtime_1.emitNervesEvent)({
269
+ level: result.delivered ? "info" : "warn", component: "senses",
270
+ event: result.delivered ? "senses.shepherd_notice_sent" : "senses.shepherd_notice_not_sent",
271
+ message: result.delivered ? "sent the Shepherd owner notice" : "the Shepherd owner notice was not delivered",
272
+ meta: { agent, friendId: target.friendId, returns: batch.length, ...(result.delivered ? {} : { reason: result.reason ?? "unknown" }) },
273
+ });
274
+ }
275
+ catch (error) {
276
+ (0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.shepherd_notice_error", message: "sending the Shepherd owner notice failed; its outcome remains unknown", meta: { agent, friendId: target.friendId, error: error.message } });
277
+ }
231
278
  }
232
279
  }
280
+ if (retry)
281
+ schedule(exports.NOTICE_RETRY_MS);
233
282
  }
234
283
  catch (error) {
235
284
  (0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.shepherd_notice_error", message: "preparing the Shepherd owner notice failed", meta: { agent, error: error.message } });
285
+ schedule(exports.NOTICE_RETRY_MS);
286
+ }
287
+ }
288
+ function flush() {
289
+ if (flushing)
290
+ return flushing;
291
+ flushing = runFlush().finally(() => { flushing = null; });
292
+ return flushing;
293
+ }
294
+ try {
295
+ const existing = (0, notice_outbox_1.readNoticeOutbox)(options.agentRoot);
296
+ let recovered = false;
297
+ for (const record of (0, returns_1.readReturns)(options.agentRoot)) {
298
+ if (record.noticeEligible === true && noticeWanted(policy, record) && !(0, notice_outbox_1.knownNotice)(options.agentRoot, (0, notice_outbox_1.noticeRecordId)(record), existing)) {
299
+ void persistIntent(record);
300
+ recovered = true;
301
+ }
236
302
  }
303
+ if (recovered || existing.some(entry => Object.keys(entry.deliveries).length === 0 || Object.values(entry.deliveries).some(value => value.state === "pending")))
304
+ schedule(exports.NOTICE_RETRY_MS);
305
+ }
306
+ catch (error) {
307
+ (0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.shepherd_notice_error", message: "could not read waiting Shepherd owner notices", meta: { agent, error: error.message } });
308
+ schedule(exports.NOTICE_RETRY_MS);
237
309
  }
238
310
  return {
239
311
  notify(record) {
240
- if (!noticeWanted(policy, record))
241
- return;
242
- waiting.push(record);
243
- cancel ??= options.schedule(() => { void flush(); }, exports.NOTICE_COALESCE_MS);
312
+ if (stopped || !noticeWanted(policy, record))
313
+ return Promise.resolve();
314
+ const write = persistIntent(record);
315
+ schedule(exports.NOTICE_COALESCE_MS);
316
+ return write;
244
317
  },
245
318
  flush,
246
319
  stop() {
320
+ stopped = true;
247
321
  cancel?.();
248
322
  cancel = null;
249
323
  },
@@ -58,6 +58,7 @@ const judge_1 = require("./judge");
58
58
  const redact_1 = require("./redact");
59
59
  const owner_notice_1 = require("./owner-notice");
60
60
  const returns_1 = require("./returns");
61
+ const guidance_1 = require("./guidance");
61
62
  /**
62
63
  * Ouro Shepherd: every time a coding agent in a terminal hands control back, a cheap judge decides
63
64
  * whether it should have. A premature return is answered in the terminal, visibly, as
@@ -108,7 +109,7 @@ function escalationContent(record, human) {
108
109
  const who = `${record.agent ? field(record.agent) : "A coding agent"}${record.cwd ? ` in ${field(path.basename(record.cwd))}` : ""}`;
109
110
  const what = record.kind === "loop_guard"
110
111
  ? `Shepherd answered it ${exports.STREAK_MAX} times in a row (or ${exports.HOURLY_MAX} in an hour) and stopped`
111
- : record.kind === "gate" ? `it needs ${human}` : "it says the work is done";
112
+ : record.kind === "gate" ? `it needs ${human}` : record.kind === "waiting" ? "it is waiting on an outside dependency, not delivered done" : "it says the work is done";
112
113
  return [
113
114
  record.kind === "working" ? "[Shepherd update]" : "[Shepherd return]",
114
115
  record.kind === "working"
@@ -226,7 +227,10 @@ async function startShepherdSenseApp(options) {
226
227
  const text = (0, redact_1.redactSecrets)(await host.read(event.sessionId, exports.READ_LINES));
227
228
  record.task = [...text.matchAll(/Desk-Task:\s*(\S+)/g)].pop()?.[1] ?? null;
228
229
  const screen = (0, redact_1.screenTail)(text, exports.SCREEN_LINES);
229
- const verdict = await judge({ human, agent: event.agent, cwd: event.cwd, task: record.task, lastBody: event.lastBody ? (0, redact_1.redactSecrets)(event.lastBody) : null, backgroundWork: event.backgroundWork, screen });
230
+ const target = { host: host.name, session: event.sessionId, cwd: event.cwd, task: record.task };
231
+ const guidance = guidanceFor(target);
232
+ record.guidanceRevision = guidance.revision;
233
+ const verdict = await judge({ human, agent: event.agent, cwd: event.cwd, task: record.task, lastBody: event.lastBody ? (0, redact_1.redactSecrets)(event.lastBody) : null, backgroundWork: event.backgroundWork, screen, guidance });
230
234
  // The host's own word that background work is running outranks a reading of the screen: such a stop is never a return.
231
235
  const kind = event.backgroundWork ? "working" : verdict.kind;
232
236
  const news = verdict.news === true || (kind === "working" && verdict.kind === "gate");
@@ -243,7 +247,10 @@ async function startShepherdSenseApp(options) {
243
247
  await host.signal(event.sessionId, field(`${event.agent ?? "agent"} waiting on its background work: ${verdict.reason}`, 120));
244
248
  }
245
249
  if (kind === "premature" && verdict.reply) {
246
- if (state.streak >= exports.STREAK_MAX || state.answers.length >= exports.HOURLY_MAX) {
250
+ if (guidance.paused) {
251
+ record.reason = `Shepherd guidance has this session paused, so nothing was sent: ${verdict.reason}`;
252
+ }
253
+ else if (state.streak >= exports.STREAK_MAX || state.answers.length >= exports.HOURLY_MAX) {
247
254
  Object.assign(record, { kind: "loop_guard", reason: `loop guard: ${verdict.reason}` });
248
255
  state.tripped = true;
249
256
  }
@@ -255,29 +262,52 @@ async function startShepherdSenseApp(options) {
255
262
  record.reason = `the screen changed while judging, so nothing was sent: ${verdict.reason}`;
256
263
  }
257
264
  else {
258
- if ("key" in verdict.reply)
259
- await host.key(event.sessionId, verdict.reply.key);
260
- else
261
- await host.prompt(event.sessionId, `[Ouro for ${human}] ${verdict.reply.text}`);
262
- Object.assign(record, { action: "respond", reply: "key" in verdict.reply ? `key:${verdict.reply.key}` : verdict.reply.text });
263
- state.streak += 1;
264
- state.answers.push(now());
265
- await host.signal(event.sessionId, field(`answered for ${human}: ${verdict.reason}`, 120));
265
+ // No awaited host reads between this final check and initiating the send.
266
+ const latest = guidanceFor(target);
267
+ if (latest.paused || latest.revision !== guidance.revision) {
268
+ record.reason = `Shepherd guidance paused or changed while judging (revision ${guidance.revision} to ${latest.revision}), so nothing was sent: ${verdict.reason}`;
269
+ }
270
+ else {
271
+ if ("key" in verdict.reply)
272
+ await host.key(event.sessionId, verdict.reply.key);
273
+ else
274
+ await host.prompt(event.sessionId, `[Ouro for ${human}] ${verdict.reply.text}`);
275
+ Object.assign(record, { action: "respond", reply: "key" in verdict.reply ? `key:${verdict.reply.key}` : verdict.reply.text });
276
+ state.streak += 1;
277
+ state.answers.push(now());
278
+ await host.signal(event.sessionId, field(`answered for ${human}: ${verdict.reason}`, 120));
279
+ }
266
280
  }
267
281
  }
268
- if (record.kind === "gate" || record.kind === "done" || record.kind === "loop_guard") {
282
+ if (record.kind === "gate" || record.kind === "done" || record.kind === "waiting" || record.kind === "loop_guard") {
269
283
  escalate(escalationContent(record, human));
270
284
  requestWake(event.transitionId);
271
- await host.signal(event.sessionId, field(`${record.kind === "gate" ? `needs ${human}` : record.kind === "done" ? "done" : "stopped answering"}: ${verdict.reason}`, 120));
285
+ await host.signal(event.sessionId, field(`${record.kind === "gate" ? `needs ${human}` : record.kind === "done" ? "done" : record.kind === "waiting" ? "waiting on an outside dependency" : "stopped answering"}: ${verdict.reason}`, 120));
272
286
  }
273
287
  }
274
288
  catch (error) {
275
289
  Object.assign(record, { kind: record.reason ? record.kind : "error", reason: `${record.reason ? `${record.reason}; then ` : ""}${error.message}` });
276
290
  }
277
291
  finally {
292
+ record.noticeEligible = notice;
278
293
  log(record);
279
- if (notice)
280
- notifyOwner(record);
294
+ if (notice) {
295
+ try {
296
+ await notifyOwner(record);
297
+ }
298
+ catch (error) {
299
+ (0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.shepherd_notice_error", message: "owner notice custody failed; the eligible return remains logged for recovery", meta: { agent, error: error.message } });
300
+ }
301
+ }
302
+ }
303
+ }
304
+ function guidanceFor(target) {
305
+ try {
306
+ return (0, guidance_1.applicableGuidance)((0, guidance_1.readGuidance)(agentRoot), target);
307
+ }
308
+ catch (error) {
309
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_guidance_error", message: "could not load Shepherd guidance; automatic replies fail closed", meta: { agent, session: target.session, error: (0, redact_1.redactSecrets)(error.message) } });
310
+ throw error;
281
311
  }
282
312
  }
283
313
  function log(record) {