@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.
- package/changelog.json +20 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/daemon/cli-exec.js +23 -0
- package/dist/heart/daemon/cli-help.js +7 -0
- package/dist/heart/daemon/cli-parse.js +19 -0
- package/dist/heart/frontend-session-service.js +45 -1
- package/dist/heart/frontend-socket.js +26 -1
- package/dist/repertoire/tools-shepherd-guidance.js +166 -0
- package/dist/repertoire/tools-shepherd.js +15 -3
- package/dist/senses/shared-turn.js +11 -0
- package/dist/senses/shepherd/composer.js +317 -0
- package/dist/senses/shepherd/guidance.js +270 -0
- package/dist/senses/shepherd/judge.js +8 -2
- package/dist/senses/shepherd/notice-outbox.js +262 -0
- package/dist/senses/shepherd/owner-notice.js +98 -24
- package/dist/senses/shepherd/sense.js +45 -15
- package/dist/senses/shepherd/surface.js +119 -0
- package/dist/senses/shepherd/target-binding.js +13 -0
- package/dist/senses/shepherd/terminal-process.js +22 -1
- package/dist/senses/teams.js +23 -3
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -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
|
|
200
|
+
const retryWrites = new Map();
|
|
201
|
+
const writes = new Set();
|
|
196
202
|
let cancel = null;
|
|
197
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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) {
|