@ouro.bot/cli 0.1.0-alpha.934 → 0.1.0-alpha.936

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.
Files changed (40) hide show
  1. package/changelog.json +18 -0
  2. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
  3. package/deploy/unraid/sanctuary.xml +1 -1
  4. package/dist/heart/daemon/cli-defaults.js +5 -0
  5. package/dist/heart/daemon/cli-exec.js +9 -0
  6. package/dist/heart/daemon/cli-help.js +2 -2
  7. package/dist/heart/daemon/cli-parse.js +2 -1
  8. package/dist/heart/daemon/daemon.js +14 -0
  9. package/dist/heart/daemon/ouro-entry.js +4 -1
  10. package/dist/heart/daemon/runtime-logging.js +1 -1
  11. package/dist/heart/daemon/sense-manager.js +53 -0
  12. package/dist/heart/mailbox/mailbox-http-hooks.js +1 -0
  13. package/dist/heart/mailbox/mailbox-http-routes.js +8 -0
  14. package/dist/heart/mailbox/mailbox-read.js +3 -1
  15. package/dist/heart/mailbox/mailbox-supervision.js +131 -0
  16. package/dist/heart/shepherd/custody-types.js +2 -0
  17. package/dist/heart/shepherd/mandates.js +352 -0
  18. package/dist/heart/shepherd/operation-commands.js +19 -0
  19. package/dist/heart/shepherd/operations.js +135 -0
  20. package/dist/heart/shepherd/supervision.js +114 -0
  21. package/dist/heart/shepherd/types.js +2 -0
  22. package/dist/mailbox-ui/assets/index-DsiKaPyH.css +1 -0
  23. package/dist/mailbox-ui/assets/index-xU5krmwW.js +1 -0
  24. package/dist/mailbox-ui/index.html +2 -2
  25. package/dist/nerves/coverage/file-completeness.js +4 -0
  26. package/dist/repertoire/tools-shell.js +9 -4
  27. package/dist/senses/shepherd/checkin-store.js +209 -41
  28. package/dist/senses/shepherd/cmux.js +69 -23
  29. package/dist/senses/shepherd/guidance.js +5 -2
  30. package/dist/senses/shepherd/herdr.js +84 -2
  31. package/dist/senses/shepherd/observation-publisher.js +320 -0
  32. package/dist/senses/shepherd/operations-defaults.js +86 -0
  33. package/dist/senses/shepherd/operations.js +159 -0
  34. package/dist/senses/shepherd/recovery.js +543 -0
  35. package/dist/senses/shepherd/sense.js +8 -0
  36. package/dist/senses/teams.js +13 -1
  37. package/npm-shrinkwrap.json +2 -2
  38. package/package.json +1 -1
  39. package/dist/mailbox-ui/assets/index-Cyv4mw3Y.js +0 -1
  40. package/dist/mailbox-ui/assets/index-Du_9G9WO.css +0 -1
@@ -114,12 +114,26 @@ function herdrRequest(socketPath, method, params, timeoutMs = TIMEOUT_MS) {
114
114
  function createHerdrHost(connection, options = {}) {
115
115
  const schedule = options.schedule ?? cmux_1.realSchedule;
116
116
  const call = (method, params) => herdrRequest(connection.socketPath, method, params);
117
- async function panes() {
117
+ async function panes(strict = false) {
118
118
  const result = await call("pane.list", {});
119
+ if (strict) {
120
+ if (!Array.isArray(result.panes))
121
+ throw new Error("invalid Herdr terminal inventory");
122
+ const seen = new Set();
123
+ for (const pane of result.panes) {
124
+ if (!isRecord(pane) || typeof pane.pane_id !== "string" || !pane.pane_id.trim() || seen.has(pane.pane_id)) {
125
+ throw new Error("invalid Herdr terminal inventory identity");
126
+ }
127
+ seen.add(pane.pane_id);
128
+ }
129
+ }
119
130
  return Array.isArray(result.panes) ? result.panes.filter(isRecord) : [];
120
131
  }
121
132
  return {
122
133
  name: "herdr",
134
+ async retirementInventory() {
135
+ return (await panes(true)).map(pane => pane.pane_id);
136
+ },
123
137
  async list() {
124
138
  return (await panes()).map((pane) => ({
125
139
  id: str(pane.pane_id) ?? "",
@@ -187,6 +201,50 @@ function createHerdrHost(connection, options = {}) {
187
201
  let refresh = false;
188
202
  const status = new Map();
189
203
  const pending = new Map();
204
+ const inventory = new Map();
205
+ let cancelObservation = null;
206
+ let inventoryGeneration = 0;
207
+ const publishObservation = (error) => {
208
+ handlers.observed?.({ at: Date.now(), sessions: [...inventory.values()], ...(error ? { error } : {}) });
209
+ };
210
+ const inventoryRow = (pane) => {
211
+ const id = str(pane.pane_id);
212
+ if (!id)
213
+ return;
214
+ const state = str(pane.agent_status);
215
+ inventory.set(id, {
216
+ session: { id, ref: id, workspace: str(pane.workspace_id),
217
+ title: str(pane.title) ?? str(pane.terminal_title_stripped) ?? str(pane.label) ?? "",
218
+ agent: str(pane.agent), cwd: str(pane.foreground_cwd) ?? str(pane.cwd), focused: pane.focused === true },
219
+ activity: state === "working" ? "working" : RETURN_STATES.has(state ?? "") ? "idle" : "unknown",
220
+ observedAt: Date.now(),
221
+ });
222
+ };
223
+ const replaceInventory = (current) => {
224
+ const ids = new Set(current.flatMap(pane => str(pane.pane_id) ?? []));
225
+ for (const id of inventory.keys())
226
+ if (!ids.has(id))
227
+ inventory.delete(id);
228
+ for (const pane of current)
229
+ inventoryRow(pane);
230
+ publishObservation();
231
+ };
232
+ // Read-only refresh belongs to this host watcher, never to the judge or the browser.
233
+ const refreshObservation = async () => {
234
+ cancelObservation = null;
235
+ const generation = ++inventoryGeneration;
236
+ try {
237
+ const current = await panes(true);
238
+ if (!closed && generation === inventoryGeneration)
239
+ replaceInventory(current);
240
+ }
241
+ catch {
242
+ if (!closed && generation === inventoryGeneration)
243
+ publishObservation("host_unavailable");
244
+ }
245
+ if (!closed && generation === inventoryGeneration && !cancelObservation)
246
+ cancelObservation = schedule(() => void refreshObservation(), 5_000);
247
+ };
190
248
  const cancelPending = (paneId) => {
191
249
  pending.get(paneId)?.();
192
250
  pending.delete(paneId);
@@ -263,12 +321,22 @@ function createHerdrHost(connection, options = {}) {
263
321
  if (name === "pane_closed") {
264
322
  cancelPending(paneId);
265
323
  status.delete(paneId);
324
+ inventory.delete(paneId);
266
325
  refresh = true;
267
326
  stream?.close();
268
327
  return;
269
328
  }
270
329
  if (name !== "pane_agent_status_changed")
271
330
  return;
331
+ const row = inventory.get(paneId);
332
+ if (row) {
333
+ const next = str(data.agent_status);
334
+ row.activity = next === "working" ? "working" : RETURN_STATES.has(next ?? "") ? "idle" : "unknown";
335
+ row.observedAt = Date.now();
336
+ publishObservation();
337
+ if (closed)
338
+ return;
339
+ }
272
340
  observed(paneId, str(data.agent_status) ?? "unknown");
273
341
  };
274
342
  const reconnect = (error) => {
@@ -282,13 +350,17 @@ function createHerdrHost(connection, options = {}) {
282
350
  const delay = RECONNECT_DELAYS_MS[Math.min(attempt, RECONNECT_DELAYS_MS.length - 1)];
283
351
  attempt += 1;
284
352
  (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_herdr_closed", message: "Herdr event subscription closed; resubscribing", meta: { delay, error: error?.message ?? null } });
353
+ publishObservation("host_unavailable");
354
+ if (closed)
355
+ return;
285
356
  cancelReconnect = schedule(() => void subscribe(), delay);
286
357
  };
287
358
  const subscribe = async () => {
288
359
  cancelReconnect = null;
289
360
  let current;
361
+ const generation = ++inventoryGeneration;
290
362
  try {
291
- current = await panes();
363
+ current = await panes(!!handlers.observed);
292
364
  }
293
365
  catch (error) {
294
366
  return reconnect(error);
@@ -314,11 +386,19 @@ function createHerdrHost(connection, options = {}) {
314
386
  else if (next !== "unknown")
315
387
  status.set(id, next);
316
388
  }
389
+ if (generation === inventoryGeneration)
390
+ replaceInventory(current);
391
+ if (closed)
392
+ return;
393
+ if (handlers.observed && !cancelObservation)
394
+ cancelObservation = schedule(() => void refreshObservation(), 5_000);
317
395
  const subscriptions = [
318
396
  { type: "pane.created" }, { type: "pane.closed" }, { type: "pane.focused" }, { type: "pane.moved" },
319
397
  ...current.flatMap((pane) => str(pane.pane_id) ? [{ type: "pane.agent_status_changed", pane_id: pane.pane_id }] : []),
320
398
  ];
321
399
  stream = connectLines(connection.socketPath, (line) => {
400
+ if (closed)
401
+ return;
322
402
  if (isRecord(line.result) && line.result.type === "subscription_started") {
323
403
  attempt = 0;
324
404
  (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_herdr_connected", message: "Herdr event subscription started", meta: { panes: subscriptions.length - 4 } });
@@ -334,6 +414,8 @@ function createHerdrHost(connection, options = {}) {
334
414
  return {
335
415
  close() {
336
416
  closed = true;
417
+ inventoryGeneration += 1;
418
+ cancelObservation?.();
337
419
  cancelReconnect?.();
338
420
  for (const cancel of pending.values())
339
421
  cancel();
@@ -0,0 +1,320 @@
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.createShepherdObservationPublisher = createShepherdObservationPublisher;
37
+ const node_crypto_1 = require("node:crypto");
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ const mandates_1 = require("../../heart/shepherd/mandates");
41
+ const supervision_1 = require("../../heart/shepherd/supervision");
42
+ const runtime_1 = require("../../nerves/runtime");
43
+ const guidance_1 = require("./guidance");
44
+ const notice_outbox_1 = require("./notice-outbox");
45
+ const recovery_1 = require("./recovery");
46
+ const checkin_store_1 = require("./checkin-store");
47
+ const returns_1 = require("./returns");
48
+ const redact_1 = require("./redact");
49
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
50
+ const kinds = new Set(["premature", "gate", "done", "working", "waiting", "unclear", "error", "loop_guard"]);
51
+ const safe = (value) => (0, redact_1.redactSecrets)(value).replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029]+/g, " ").trim().slice(0, 2000) || "(unnamed)";
52
+ const idFor = (value) => (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(value)).digest("hex");
53
+ const iso = (at) => {
54
+ if (!Number.isFinite(Date.parse(at)))
55
+ throw new Error("Invalid observation source timestamp");
56
+ return new Date(at).toISOString();
57
+ };
58
+ const emptyPage = () => ({ totalCount: 0, truncated: false, items: [] });
59
+ const unknownCustody = () => ({
60
+ revision: null, bytes: null, capacity: "unknown", claims: { totalCount: null, truncated: true, items: [] },
61
+ });
62
+ function page(events, totalCount) {
63
+ const unique = [...new Map(events.map(event => [event.id, event])).values()];
64
+ const count = totalCount === undefined ? unique.length : totalCount;
65
+ const items = unique.sort((a, b) => b.at.localeCompare(a.at) || a.id.localeCompare(b.id)).slice(0, supervision_1.SHEPHERD_SUPERVISION_LIMIT);
66
+ return { items, totalCount: count, truncated: count === null || count > items.length };
67
+ }
68
+ /** Unlike the legacy convenience reader, a malformed row never becomes a healthy empty history. */
69
+ function readReturnSummaries(root) {
70
+ const file = (0, returns_1.returnsLogPath)(root);
71
+ const metadata = fs.lstatSync(file, { throwIfNoEntry: false });
72
+ if (!metadata)
73
+ return null;
74
+ if (!metadata.isFile() || metadata.nlink !== 1 || metadata.size > supervision_1.SHEPHERD_OBSERVATION_MAX_BYTES)
75
+ throw new Error("Invalid returns source");
76
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
77
+ let raw;
78
+ try {
79
+ const opened = fs.fstatSync(fd);
80
+ if (opened.ino !== metadata.ino || opened.dev !== metadata.dev)
81
+ throw new Error("Returns source changed");
82
+ const bytes = Buffer.alloc(supervision_1.SHEPHERD_OBSERVATION_MAX_BYTES + 1);
83
+ let size = 0;
84
+ while (size < bytes.length) {
85
+ const received = fs.readSync(fd, bytes, size, bytes.length - size, null);
86
+ if (!received)
87
+ break;
88
+ size += received;
89
+ }
90
+ if (size > supervision_1.SHEPHERD_OBSERVATION_MAX_BYTES)
91
+ throw new Error("Returns source exceeds limit");
92
+ raw = bytes.toString("utf8", 0, size);
93
+ }
94
+ finally {
95
+ fs.closeSync(fd);
96
+ }
97
+ return raw.split("\n").filter(line => line.trim()).map(line => {
98
+ const value = JSON.parse(line);
99
+ if (!object(value) || typeof value.at !== "string" || typeof value.host !== "string" || typeof value.session !== "string"
100
+ || typeof value.transition !== "string" || typeof value.kind !== "string" || !kinds.has(value.kind)
101
+ || !value.host || !value.session || !value.transition || (value.action !== "respond" && value.action !== "let_through")
102
+ || typeof value.reason !== "string")
103
+ throw new Error("Invalid returns source schema");
104
+ return { at: iso(value.at), host: safe(value.host), session: safe(value.session), transition: value.transition, kind: value.kind,
105
+ action: value.action, reason: safe(value.reason) };
106
+ });
107
+ }
108
+ function createShepherdObservationPublisher(options) {
109
+ const now = options.now ?? Date.now;
110
+ let stopped = false;
111
+ let observed = null;
112
+ let returns = [];
113
+ let custody = unknownCustody();
114
+ const events = { returns: emptyPage(), custody: emptyPage(), notices: emptyPage() };
115
+ let observations = emptyPage();
116
+ function projectAndPublish() {
117
+ const at = now();
118
+ const health = {
119
+ host: observed && !observed.error ? "ok" : "missing", guidance: "ok", mandates: "ok", returns: "ok", custody: "ok", notices: "ok",
120
+ };
121
+ const errors = [];
122
+ const fail = (source) => {
123
+ health[source] = "error";
124
+ errors.push({ source, code: "read_failed", detail: `The ${source} source could not be read. Any retained history is not current.` });
125
+ };
126
+ const read = (source, fn) => {
127
+ try {
128
+ return fn();
129
+ }
130
+ catch {
131
+ fail(source);
132
+ return null;
133
+ }
134
+ };
135
+ const guidance = read("guidance", () => (0, guidance_1.readGuidance)(options.agentRoot));
136
+ const mandates = read("mandates", () => (0, mandates_1.readShepherdMandates)(options.agentRoot));
137
+ try {
138
+ const current = readReturnSummaries(options.agentRoot);
139
+ if (current === null)
140
+ health.returns = "missing";
141
+ else {
142
+ returns = current;
143
+ events.returns = page(current.map(record => ({
144
+ id: `return:${idFor([record.host, record.session, record.transition, record.at])}`,
145
+ host: record.host, session: record.session, instance: null, at: record.at, kind: "judgment", summary: record.kind,
146
+ })));
147
+ }
148
+ }
149
+ catch {
150
+ fail("returns");
151
+ }
152
+ try {
153
+ const current = (0, recovery_1.readCheckInSnapshot)((0, checkin_store_1.checkInStorePath)(options.agentRoot), { limit: supervision_1.SHEPHERD_SUPERVISION_LIMIT });
154
+ if (current.state === "missing")
155
+ health.custody = "missing";
156
+ else {
157
+ custody = {
158
+ revision: current.revision, bytes: current.bytes, capacity: current.capacity,
159
+ claims: { totalCount: current.totalCount, truncated: current.nextCursor !== null, items: current.entries.map(entry => ({
160
+ id: entry.id, host: safe(entry.host), session: safe(entry.session), instance: entry.instance === null ? null : safe(entry.instance),
161
+ state: entry.state, claimedAt: iso(entry.claimedAt), reason: entry.reason === null ? null : safe(entry.reason), retired: entry.retired,
162
+ })) },
163
+ };
164
+ events.custody = page(current.entries.flatMap(entry => {
165
+ const base = { host: safe(entry.host), session: safe(entry.session), instance: entry.instance === null ? null : safe(entry.instance) };
166
+ const boundAdmission = entry.receipt?.requestId === entry.id && entry.receipt.surfaceId === entry.session
167
+ && entry.receipt.processInstance === entry.instance && entry.receipt.pasteAdmitted && entry.receipt.submitAdmitted;
168
+ return [{
169
+ ...base, id: `input:${entry.id}`, at: iso(entry.claimedAt),
170
+ kind: entry.state === "sent" ? boundAdmission ? "input_admitted" : "legacy_input_reported"
171
+ : entry.state === "not_sent" ? "observation" : "input_unknown",
172
+ summary: entry.state === "sent" ? boundAdmission ? "Bound host receipt reports paste and submit admission; not proof of task completion"
173
+ : "Legacy dispatcher reported sent; no qualified bound paste-and-submit receipt. Not proof of input admission or task completion."
174
+ : entry.state === "not_sent" ? "Input refused; no terminal input"
175
+ : "Claim held; input outcome is unknown. Reconcile evidence, do not retry blindly.",
176
+ }, ...entry.reconciliations.map(receipt => ({
177
+ ...base, id: `recovery:${entry.id}:${receipt.revision}`, at: iso(receipt.at),
178
+ kind: "recovery", summary: `Owner acknowledged recovery: ${receipt.resolution}; original state ${receipt.originalState}`,
179
+ }))];
180
+ }), current.nextCursor === null ? undefined : null);
181
+ }
182
+ }
183
+ catch {
184
+ fail("custody");
185
+ }
186
+ try {
187
+ events.notices = page((0, notice_outbox_1.readNoticeOutbox)(options.agentRoot).flatMap(entry => {
188
+ const base = { host: safe(entry.record.host), session: safe(entry.record.session), instance: null };
189
+ return [{
190
+ ...base, id: `notice:${entry.id}`, at: iso(entry.record.at), kind: "notification_queued", summary: "Owner notification queued",
191
+ }, ...Object.entries(entry.deliveries).map(([recipient, delivery]) => ({
192
+ ...base, id: `notice:${entry.id}:${idFor(recipient)}`, at: iso(delivery.at),
193
+ kind: delivery.state === "sent" ? "notification_delivered"
194
+ : delivery.state === "unknown" ? "notification_unknown" : "notification_queued",
195
+ summary: delivery.state === "sent" ? "Owner notification accepted by transport; not proof the owner read it"
196
+ : delivery.state === "unknown" ? "Owner notification delivery is unknown" : "Owner notification pending transport",
197
+ }))];
198
+ }));
199
+ }
200
+ catch {
201
+ fail("notices");
202
+ }
203
+ if (observed?.error)
204
+ errors.push({ source: "host", code: observed.error, detail: observed.error === "sense_stopped"
205
+ ? "The Shepherd sense is stopped. Retained observations are historical."
206
+ : "The terminal host is unavailable. Retained observations are historical." });
207
+ const sessions = (observed?.sessions ?? []).slice(0, supervision_1.SHEPHERD_SUPERVISION_LIMIT).map(row => {
208
+ const session = row.session;
209
+ let authority = "unknown";
210
+ let withheldReason = row.error ? "Terminal could not be read; no current policy assessment" : "Guarded automatic input is not qualified for this host";
211
+ if (row.error && errors.length < 50)
212
+ errors.push({ source: "host", code: "row_read_failed", detail: `Terminal ${safe(session.id)} could not be read` });
213
+ if (guidance && mandates) {
214
+ if ((0, guidance_1.applicableGuidance)(guidance, { host: options.host, session: session.id, cwd: session.cwd ?? null, task: null }).paused)
215
+ authority = "paused";
216
+ else if (session.cwd) {
217
+ const resolution = (0, mandates_1.resolveShepherdMandate)(mandates, {
218
+ project: session.cwd, host: options.host, session: session.id, ...(session.instance ? { instance: session.instance } : {}),
219
+ }, at);
220
+ authority = resolution.state === "authorized" ? "active" : resolution.reason === "ambiguous_mandates" ? "ambiguous" : "absent";
221
+ }
222
+ else
223
+ authority = "absent";
224
+ if (authority === "paused")
225
+ withheldReason = "Stored terminal pause is set";
226
+ else if (authority === "absent")
227
+ withheldReason = "No applicable stored mandate; legacy automatic paths are not uniformly mandate-enforced";
228
+ else if (authority === "ambiguous")
229
+ withheldReason = "Multiple stored mandates apply; policy scope is ambiguous";
230
+ }
231
+ const last = returns.filter(record => record.host === options.host && record.session === safe(session.id)).sort((a, b) => b.at.localeCompare(a.at))[0];
232
+ return {
233
+ host: safe(options.host), session: safe(session.id), instance: session.instance ? safe(session.instance) : null,
234
+ label: safe(session.title || session.ref || session.id), harness: session.agent ? safe(session.agent) : null,
235
+ project: session.cwd ? safe(session.cwd) : null, activity: row.activity,
236
+ observedAt: row.observedAt === null ? null : new Date(row.observedAt).toISOString(),
237
+ lastDecision: last?.kind ?? null, lastAction: last?.action ?? null, lastReason: last?.reason ?? null,
238
+ lastActionAt: last?.at ?? null, withheldReason, authority, capability: "unsupported",
239
+ };
240
+ });
241
+ const allEvents = [...Object.values(events), observations];
242
+ const total = allEvents.some(value => value.totalCount === null) ? null : allEvents.reduce((sum, value) => sum + value.totalCount, 0);
243
+ const snapshot = {
244
+ schemaVersion: 1, agentName: options.agentName, observedAt: new Date(observed?.at ?? at).toISOString(),
245
+ sourceHealth: health, sourceErrors: errors,
246
+ sessions: { totalCount: observed ? observed.sessions.length : null,
247
+ truncated: !observed || observed.sessions.length > sessions.length, items: sessions },
248
+ timeline: page(allEvents.flatMap(value => value.items), total), custody,
249
+ };
250
+ try {
251
+ if (!(0, supervision_1.isShepherdSupervisionSnapshot)(snapshot))
252
+ throw new Error("Invalid bounded observation projection");
253
+ const encoded = JSON.stringify(snapshot);
254
+ if (Buffer.byteLength(encoded) > supervision_1.SHEPHERD_OBSERVATION_MAX_BYTES)
255
+ throw new Error("Observation exceeds publication limit");
256
+ const dir = (0, returns_1.shepherdStateDir)(options.agentRoot);
257
+ (0, checkin_store_1.validateCustodyDirectory)(dir, true);
258
+ const temporary = path.join(dir, `${supervision_1.SHEPHERD_OBSERVATION_FILE}.${(0, node_crypto_1.randomUUID)()}.tmp`);
259
+ const fd = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
260
+ try {
261
+ fs.writeFileSync(fd, encoded);
262
+ fs.fsyncSync(fd);
263
+ }
264
+ finally {
265
+ fs.closeSync(fd);
266
+ }
267
+ fs.renameSync(temporary, path.join(dir, supervision_1.SHEPHERD_OBSERVATION_FILE));
268
+ const directory = fs.openSync(dir, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
269
+ try {
270
+ fs.fsyncSync(directory);
271
+ }
272
+ finally {
273
+ fs.closeSync(directory);
274
+ }
275
+ return true;
276
+ }
277
+ catch {
278
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_observation_failed",
279
+ message: "Could not publish a safe Shepherd observation; the previous replica must age out", meta: { agent: options.agentName } });
280
+ return false;
281
+ }
282
+ }
283
+ function publish() {
284
+ try {
285
+ return projectAndPublish();
286
+ }
287
+ catch {
288
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_observation_failed",
289
+ message: "Could not assemble a Shepherd observation; the previous replica must age out", meta: { agent: options.agentName } });
290
+ return false;
291
+ }
292
+ }
293
+ return {
294
+ observe(observation) {
295
+ if (stopped)
296
+ return false;
297
+ observed = structuredClone(observation);
298
+ if (!observation.error) {
299
+ const current = observation.sessions.slice(0, supervision_1.SHEPHERD_SUPERVISION_LIMIT).map(row => ({
300
+ id: `observation:${idFor([options.host, row.session.id, row.session.instance ?? null, observation.at])}`,
301
+ host: safe(options.host), session: safe(row.session.id), instance: row.session.instance ? safe(row.session.instance) : null,
302
+ at: new Date(observation.at).toISOString(), kind: "observation",
303
+ summary: row.error ? "Terminal observation failed" : `Observed terminal activity: ${row.activity}`,
304
+ }));
305
+ const unique = new Map([...current, ...observations.items].map(event => [event.id, event]));
306
+ observations = page([...unique.values()], observations.totalCount
307
+ + current.filter(event => !observations.items.some(previous => previous.id === event.id)).length);
308
+ }
309
+ return publish();
310
+ },
311
+ refresh() { return !stopped && publish(); },
312
+ stop() {
313
+ if (stopped)
314
+ return;
315
+ observed = { ...(observed ?? { at: 0, sessions: [] }), error: "sense_stopped" };
316
+ publish();
317
+ stopped = true;
318
+ },
319
+ };
320
+ }
@@ -0,0 +1,86 @@
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.createDefaultShepherdOperationDeps = createDefaultShepherdOperationDeps;
37
+ const path = __importStar(require("node:path"));
38
+ const fs = __importStar(require("node:fs"));
39
+ const identity_1 = require("../../heart/identity");
40
+ const runtime_credentials_1 = require("../../heart/runtime-credentials");
41
+ const shepherd_config_1 = require("../../heart/shepherd-config");
42
+ const host_1 = require("./host");
43
+ const surface_1 = require("./surface");
44
+ const runtime_1 = require("../../nerves/runtime");
45
+ function createDefaultShepherdOperationDeps(lifecycle) {
46
+ return {
47
+ agentRoot: agent => path.join((0, identity_1.getAgentBundlesRoot)(), `${agent}.ouro`),
48
+ resolveOwner: async (agent) => {
49
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(agent))
50
+ throw new Error("existing local owner requires a safe agent name");
51
+ const root = path.join((0, identity_1.getAgentBundlesRoot)(), `${agent}.ouro`);
52
+ if (!fs.lstatSync(root, { throwIfNoEntry: false })?.isDirectory()
53
+ || !fs.lstatSync(path.join(root, "friends"), { throwIfNoEntry: false })?.isDirectory()) {
54
+ throw new Error("existing local owner linking is required; no bundle or friends directory was created");
55
+ }
56
+ return { actorId: await (0, surface_1.resolveExistingLocalOwner)((0, surface_1.localShepherdFriendStore)(agent)) };
57
+ },
58
+ host: async (agent) => {
59
+ const machine = (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent);
60
+ if (!machine.ok)
61
+ throw new Error("machine Shepherd connection configuration is unavailable");
62
+ const connection = (0, shepherd_config_1.resolveShepherdConnection)(agent, machine.config);
63
+ if (!connection.ok)
64
+ throw new Error(connection.error);
65
+ return (0, host_1.createShepherdHost)(connection.connection, { agentName: agent });
66
+ },
67
+ senseStopped: async (agent) => {
68
+ const response = await lifecycle(agent);
69
+ if (!response.ok)
70
+ throw new Error(response.error ?? "daemon lifecycle unavailable");
71
+ const value = response.data;
72
+ if (value === null || typeof value !== "object" || !("kind" in value) || value.kind !== "daemon_lifecycle"
73
+ || !("enabled" in value) || value.enabled !== false || !("processState" in value) || value.processState !== "stopped"
74
+ || !("checkedAt" in value) || typeof value.checkedAt !== "string" || !Number.isFinite(Date.parse(value.checkedAt))
75
+ || Date.parse(value.checkedAt) > Date.now() || Date.now() - Date.parse(value.checkedAt) > 30_000) {
76
+ throw new Error("fresh disabled and stopped daemon lifecycle proof is unavailable");
77
+ }
78
+ (0, runtime_1.emitNervesEvent)({
79
+ component: "senses", event: "senses.shepherd_maintenance_quiescence_confirmed",
80
+ message: "validated fresh daemon proof that Shepherd is disabled and stopped",
81
+ meta: { agent, processState: "stopped" },
82
+ });
83
+ return { kind: "daemon_lifecycle", enabled: false, processState: "stopped", checkedAt: value.checkedAt };
84
+ },
85
+ };
86
+ }