@ouro.bot/cli 0.1.0-alpha.720 → 0.1.0-alpha.722

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,302 @@
1
+ "use strict";
2
+ /**
3
+ * Pipeline freshness — "is this pipe still moving?"
4
+ *
5
+ * Every outage this module exists to prevent had the same shape: a health
6
+ * check verified that something was *configured* or *connected*, and reported
7
+ * a green tick next to a pipe that had been dead for weeks.
8
+ *
9
+ * - mail: `mail enabled` / `mail config ok` / `45479 messages` all passed
10
+ * while zero mail had been ingested for 77 days after a vault key rotation
11
+ * changed the mailbox keyIds. The message count is cumulative, so it can
12
+ * only ever go up — it is not a liveness signal.
13
+ * - bluebubbles: the upstream HTTP probe stayed `ok` for days while inbound
14
+ * delivery was dead, because the server was POSTing its webhook to a
15
+ * stale port. A reachable upstream does not prove inbound delivery.
16
+ * - launchd: checks asserted a plist *file* existed rather than that the
17
+ * job was loaded.
18
+ *
19
+ * The fix is to assert on *observed data flow*, with three rules baked into
20
+ * this primitive:
21
+ *
22
+ * 1. Age is always stated in the message, in human units, with the
23
+ * timestamp it was derived from.
24
+ * 2. "Never observed any activity" and "could not observe" are distinct
25
+ * states from "activity, but stale" — and none of them can read as
26
+ * `pass`. An unobservable pipe is unverified, not healthy.
27
+ * 3. Every non-`pass` result carries a concrete remediation hint.
28
+ *
29
+ * Thresholds are always supplied by the caller so freshness policy lives with
30
+ * the pipe it describes, not in this file.
31
+ */
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.DEFAULT_CLOCK_SKEW_TOLERANCE_MS = exports.FRESHNESS_DAY_MS = exports.FRESHNESS_HOUR_MS = exports.FRESHNESS_MINUTE_MS = void 0;
34
+ exports.formatFreshnessAge = formatFreshnessAge;
35
+ exports.formatFreshnessTimestamp = formatFreshnessTimestamp;
36
+ exports.evaluateFreshness = evaluateFreshness;
37
+ exports.observeCreatePerEventStore = observeCreatePerEventStore;
38
+ exports.observeAppendPerEventStore = observeAppendPerEventStore;
39
+ exports.resolveFreshnessThresholds = resolveFreshnessThresholds;
40
+ const runtime_1 = require("../../nerves/runtime");
41
+ exports.FRESHNESS_MINUTE_MS = 60 * 1000;
42
+ exports.FRESHNESS_HOUR_MS = 60 * exports.FRESHNESS_MINUTE_MS;
43
+ exports.FRESHNESS_DAY_MS = 24 * exports.FRESHNESS_HOUR_MS;
44
+ /** Small tolerance before a future timestamp is treated as clock skew. */
45
+ exports.DEFAULT_CLOCK_SKEW_TOLERANCE_MS = exports.FRESHNESS_MINUTE_MS;
46
+ /**
47
+ * Human-readable duration. Always rounds down so the age is never overstated:
48
+ * "77 days" means at least 77 days have elapsed.
49
+ */
50
+ function formatFreshnessAge(ms) {
51
+ const clamped = Math.max(0, ms);
52
+ const seconds = Math.floor(clamped / 1000);
53
+ if (seconds < 60)
54
+ return `${seconds} second${seconds === 1 ? "" : "s"}`;
55
+ const minutes = Math.floor(clamped / exports.FRESHNESS_MINUTE_MS);
56
+ if (minutes < 60)
57
+ return `${minutes} minute${minutes === 1 ? "" : "s"}`;
58
+ const hours = Math.floor(clamped / exports.FRESHNESS_HOUR_MS);
59
+ if (hours < 48)
60
+ return `${hours} hour${hours === 1 ? "" : "s"}`;
61
+ // The day bucket only starts at 48h, so it is always plural.
62
+ return `${Math.floor(clamped / exports.FRESHNESS_DAY_MS)} days`;
63
+ }
64
+ /** Minute-precision ISO instant — keeps the incident date visible and unambiguous. */
65
+ function formatFreshnessTimestamp(ms) {
66
+ return new Date(ms).toISOString().replace(/:\d{2}\.\d{3}Z$/, "Z");
67
+ }
68
+ function joinDetail(parts) {
69
+ return parts.filter((part) => !!part && part.trim().length > 0).join("; ");
70
+ }
71
+ function statusForAge(ageMs, thresholds) {
72
+ if (ageMs >= thresholds.failAfterMs)
73
+ return "fail";
74
+ if (ageMs >= thresholds.warnAfterMs)
75
+ return "warn";
76
+ return "pass";
77
+ }
78
+ /**
79
+ * Severity for "readable store, nothing ever recorded".
80
+ *
81
+ * Floors at `warn` on purpose: a configured-but-never-delivered pipe is never
82
+ * healthy, but a pipe wired up minutes ago does not deserve a hard failure.
83
+ */
84
+ function statusForNever(input) {
85
+ const since = input.configuredSinceMs;
86
+ if (typeof since !== "number" || !Number.isFinite(since)) {
87
+ return { status: "fail", configuredForMs: null };
88
+ }
89
+ const configuredForMs = Math.max(0, input.nowMs - since);
90
+ const escalated = statusForAge(configuredForMs, input.thresholds);
91
+ return { status: escalated === "pass" ? "warn" : escalated, configuredForMs };
92
+ }
93
+ function evaluateActivity(input, atMs) {
94
+ const tolerance = input.clockSkewToleranceMs ?? exports.DEFAULT_CLOCK_SKEW_TOLERANCE_MS;
95
+ const skewMs = atMs - input.nowMs;
96
+ const timestamp = formatFreshnessTimestamp(atMs);
97
+ if (skewMs > tolerance) {
98
+ return {
99
+ status: "warn",
100
+ state: "future",
101
+ ageMs: 0,
102
+ detail: joinDetail([
103
+ input.context,
104
+ `last ${input.unit} is timestamped ${timestamp}, ${formatFreshnessAge(skewMs)} in the future — clock skew means freshness cannot be trusted`,
105
+ input.provenance,
106
+ `fix: ${input.remediation}`,
107
+ ]),
108
+ };
109
+ }
110
+ const ageMs = Math.max(0, -skewMs);
111
+ const status = statusForAge(ageMs, input.thresholds);
112
+ if (status === "pass") {
113
+ return {
114
+ status,
115
+ state: "fresh",
116
+ ageMs,
117
+ detail: joinDetail([
118
+ input.context,
119
+ `${input.activity} ${formatFreshnessAge(ageMs)} ago (last ${input.unit} ${timestamp})`,
120
+ input.provenance,
121
+ ]),
122
+ };
123
+ }
124
+ return {
125
+ status,
126
+ state: "stale",
127
+ ageMs,
128
+ detail: joinDetail([
129
+ input.context,
130
+ `no ${input.activity} in ${formatFreshnessAge(ageMs)} — last ${input.unit} ${timestamp}`,
131
+ input.provenance,
132
+ `fix: ${input.remediation}`,
133
+ ]),
134
+ };
135
+ }
136
+ /**
137
+ * Turn a single observation into a status + actionable detail.
138
+ *
139
+ * The only path that can return `pass` is an observed activity timestamp
140
+ * inside the warn threshold. Everything else — no activity, unreadable store,
141
+ * skewed clock — is surfaced, never swallowed.
142
+ */
143
+ function evaluateFreshness(input) {
144
+ const observation = input.observation;
145
+ if (observation.kind === "unknown") {
146
+ return {
147
+ status: "fail",
148
+ state: "unknown",
149
+ ageMs: null,
150
+ detail: joinDetail([
151
+ input.context,
152
+ `${input.activity}: last-activity time could not be determined — ${observation.reason}; unverified, not healthy`,
153
+ input.provenance,
154
+ `fix: ${input.remediation}`,
155
+ ]),
156
+ };
157
+ }
158
+ if (observation.kind === "none") {
159
+ const { status, configuredForMs } = statusForNever(input);
160
+ return {
161
+ status,
162
+ state: "never",
163
+ ageMs: null,
164
+ detail: joinDetail([
165
+ input.context,
166
+ configuredForMs === null
167
+ ? `no ${input.activity} ever — the store is readable and empty`
168
+ : `no ${input.activity} ever — the store is readable and empty, and this pipe has been configured for ${formatFreshnessAge(configuredForMs)}`,
169
+ input.provenance,
170
+ `fix: ${input.remediation}`,
171
+ ]),
172
+ };
173
+ }
174
+ if (!Number.isFinite(observation.atMs)) {
175
+ return evaluateFreshness({
176
+ ...input,
177
+ observation: { kind: "unknown", reason: `observed timestamp is not a finite epoch-ms value (${String(observation.atMs)})` },
178
+ });
179
+ }
180
+ return evaluateActivity(input, observation.atMs);
181
+ }
182
+ /**
183
+ * Last-write probe for **create-per-event** stores — one new file per event,
184
+ * such as the mailroom's `messages/<id>.json`.
185
+ *
186
+ * A directory's mtime advances whenever an entry is created or removed, so it
187
+ * is an exact answer to "when was a file last added here" for O(1) cost, with
188
+ * no per-entry stat. That matters: the production mailroom holds ~45k message
189
+ * files and must never be stat-walked by a health check.
190
+ *
191
+ * It is also the semantically *correct* signal here. A message's `receivedAt`
192
+ * is when the mail was sent, not when this machine ingested it — an mbox
193
+ * backfill writes month-old `receivedAt` values today — so parsing message
194
+ * bodies would answer a different question than "is the pipe moving?".
195
+ *
196
+ * `hasEntries` is supplied by the caller (which typically already lists the
197
+ * directory for a count) so an empty directory reports `none` rather than
198
+ * reporting its own creation time as activity.
199
+ */
200
+ function observeCreatePerEventStore(dir, deps, options) {
201
+ const provenance = `derived from ${dir} directory mtime (O(1), no per-entry scan)`;
202
+ if (!deps.existsSync(dir)) {
203
+ return { observation: { kind: "none" }, provenance: `${dir} does not exist` };
204
+ }
205
+ if (!options.hasEntries) {
206
+ return { observation: { kind: "none" }, provenance: `${dir} is empty` };
207
+ }
208
+ try {
209
+ return { observation: { kind: "activity", atMs: deps.statSync(dir).mtimeMs }, provenance };
210
+ }
211
+ catch (error) {
212
+ return {
213
+ observation: { kind: "unknown", reason: `${dir} could not be stat'd: ${error instanceof Error ? error.message : String(error)}` },
214
+ provenance,
215
+ };
216
+ }
217
+ }
218
+ /**
219
+ * Last-write probe for **append-per-event** stores — a small set of long-lived
220
+ * logs appended in place, such as the BlueBubbles inbound ndjson (one file per
221
+ * chat).
222
+ *
223
+ * Directory mtime is the wrong signal here: appending to an existing file does
224
+ * not touch its parent directory, so a busy-but-not-growing log set would look
225
+ * frozen. This stats each matching entry and takes the newest.
226
+ *
227
+ * The scan is bounded by `maxEntries`. When the bound is hit the provenance
228
+ * says so explicitly — a bounded scan must never masquerade as exhaustive.
229
+ */
230
+ function observeAppendPerEventStore(dir, deps, options) {
231
+ if (!deps.existsSync(dir)) {
232
+ return { observation: { kind: "none" }, provenance: `${dir} does not exist` };
233
+ }
234
+ let entries;
235
+ try {
236
+ entries = deps.readdirSync(dir).filter((name) => name.endsWith(options.suffix));
237
+ }
238
+ catch (error) {
239
+ return {
240
+ observation: { kind: "unknown", reason: `${dir} could not be listed: ${error instanceof Error ? error.message : String(error)}` },
241
+ provenance: `attempted directory listing of ${dir}`,
242
+ };
243
+ }
244
+ if (entries.length === 0) {
245
+ return { observation: { kind: "none" }, provenance: `${dir} holds no ${options.suffix} logs` };
246
+ }
247
+ const scanned = entries.slice(0, options.maxEntries);
248
+ const truncated = entries.length > scanned.length;
249
+ const provenance = truncated
250
+ ? `newest mtime across the first ${scanned.length} of ${entries.length} ${options.suffix} logs in ${dir} — scan bounded, newer activity may exist in the unscanned ${entries.length - scanned.length}`
251
+ : `newest mtime across ${scanned.length} ${options.suffix} log${scanned.length === 1 ? "" : "s"} in ${dir}`;
252
+ let newest = Number.NEGATIVE_INFINITY;
253
+ const failures = [];
254
+ for (const name of scanned) {
255
+ try {
256
+ const mtimeMs = deps.statSync(`${dir}/${name}`).mtimeMs;
257
+ if (mtimeMs > newest)
258
+ newest = mtimeMs;
259
+ }
260
+ catch (error) {
261
+ failures.push(`${name}: ${error instanceof Error ? error.message : String(error)}`);
262
+ }
263
+ }
264
+ if (newest === Number.NEGATIVE_INFINITY) {
265
+ return {
266
+ observation: { kind: "unknown", reason: `none of the ${scanned.length} log(s) in ${dir} could be stat'd: ${failures.slice(0, 3).join("; ")}` },
267
+ provenance,
268
+ };
269
+ }
270
+ return {
271
+ observation: { kind: "activity", atMs: newest },
272
+ provenance: failures.length > 0 ? `${provenance} (${failures.length} unreadable and skipped)` : provenance,
273
+ };
274
+ }
275
+ /**
276
+ * Merge an `agent.json` override onto in-repo defaults.
277
+ *
278
+ * Configuration lives in a committed config file rather than an environment
279
+ * variable, per the repo configuration policy. Non-positive and non-finite
280
+ * values are ignored so a malformed override can never disable the check.
281
+ */
282
+ function resolveFreshnessThresholds(defaults, override) {
283
+ const record = override && typeof override === "object" && !Array.isArray(override)
284
+ ? override
285
+ : null;
286
+ const hours = (key) => {
287
+ const value = record?.[key];
288
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value * exports.FRESHNESS_HOUR_MS : null;
289
+ };
290
+ return {
291
+ warnAfterMs: hours("warnAfterHours") ?? defaults.warnAfterMs,
292
+ failAfterMs: hours("failAfterHours") ?? defaults.failAfterMs,
293
+ };
294
+ }
295
+ /* v8 ignore start -- module load observability event */
296
+ (0, runtime_1.emitNervesEvent)({
297
+ component: "daemon",
298
+ event: "daemon.pipeline_freshness_loaded",
299
+ message: "pipeline freshness primitive loaded",
300
+ meta: {},
301
+ });
302
+ /* v8 ignore stop */
@@ -460,10 +460,11 @@ function computeNextRunAt(habit, endedAt) {
460
460
  }
461
461
  function defaultSummarySnapshot(input) {
462
462
  const errors = input.errors ?? [];
463
+ const decisions = (0, flight_recorder_1.decisionsFromHabitRunTraceSteps)(input.traceSteps);
463
464
  if (errors.length > 0) {
464
465
  return {
465
466
  summary: `Habit ${input.habit.name} finished with errors: ${errors.join("; ")}`,
466
- decisions: [],
467
+ decisions,
467
468
  nextLikelyStep: null,
468
469
  };
469
470
  }
@@ -471,7 +472,7 @@ function defaultSummarySnapshot(input) {
471
472
  if (surfaced) {
472
473
  return {
473
474
  summary: `Habit ${input.habit.name} surfaced via ${surfaced.recipient}/${surfaced.channel}.`,
474
- decisions: [],
475
+ decisions,
475
476
  nextLikelyStep: null,
476
477
  };
477
478
  }
@@ -479,13 +480,13 @@ function defaultSummarySnapshot(input) {
479
480
  if (produced) {
480
481
  return {
481
482
  summary: `Habit ${input.habit.name} produced ${produced.kind}: ${produced.locator}.`,
482
- decisions: [],
483
+ decisions,
483
484
  nextLikelyStep: null,
484
485
  };
485
486
  }
486
487
  return {
487
488
  summary: `Habit ${input.habit.name} finished with ${input.outcome}.`,
488
- decisions: [],
489
+ decisions,
489
490
  nextLikelyStep: null,
490
491
  };
491
492
  }
@@ -512,6 +513,7 @@ function buildHabitRunReceipt(input) {
512
513
  summarySnapshot: input.summarySnapshot ?? defaultSummarySnapshot(input),
513
514
  producedRefs: input.producedRefs ?? [],
514
515
  surfaceAttempts: input.surfaceAttempts ?? [],
516
+ traceSteps: input.traceSteps ?? [],
515
517
  errors: input.errors ?? [],
516
518
  };
517
519
  (0, runtime_1.emitNervesEvent)({
@@ -572,6 +574,7 @@ function completeHabitRun(input) {
572
574
  toolPolicy: input.toolPolicy,
573
575
  producedRefs,
574
576
  surfaceAttempts: input.surfaceAttempts,
577
+ traceSteps: input.traceSteps,
575
578
  errors: input.errors,
576
579
  summarySnapshot: input.summarySnapshot,
577
580
  });
@@ -86,6 +86,8 @@ function createMailboxHttpReadHooks(options) {
86
86
  readAgentHabitRun: options.readAgentHabitRun ?? ((agentName, runId) => (0, mailbox_read_1.readHabitRunReceiptView)(agentRoot(agentName), runId)),
87
87
  readAgentHabitRunSummaries: options.readAgentHabitRunSummaries ?? ((agentName, habitOptions) => (0, mailbox_read_1.readHabitSessionSummaryListView)(agentRoot(agentName), habitOptions)),
88
88
  readAgentHabitRunSummary: options.readAgentHabitRunSummary ?? ((agentName, selector) => (0, mailbox_read_1.readHabitSessionSummaryView)(agentRoot(agentName), selector)),
89
+ readAgentContextPackets: options.readAgentContextPackets ?? ((agentName, packetOptions) => (0, mailbox_read_1.readContextPacketListView)(agentRoot(agentName), packetOptions)),
90
+ readAgentContextPacket: options.readAgentContextPacket ?? ((agentName, packetId) => (0, mailbox_read_1.readContextPacketView)(agentRoot(agentName), packetId)),
89
91
  readAgentMail: options.readAgentMail ?? ((agentName) => (0, mailbox_read_1.readMailView)(agentName)),
90
92
  readAgentMailMessage: options.readAgentMailMessage ?? ((agentName, messageId) => (0, mailbox_read_1.readMailMessageView)(agentName, messageId)),
91
93
  readDaemonHealth: options.readDaemonHealth ?? (() => (0, mailbox_read_1.readDaemonHealthDeep)(options.healthPath)),
@@ -321,6 +321,29 @@ async function handleAgentRoute(request, response, context) {
321
321
  (0, mailbox_http_response_1.writeJson)(response, 200, view);
322
322
  return;
323
323
  }
324
+ if (surface === "context-packets") {
325
+ const limit = parseHabitRunLimit(request.url);
326
+ if (limit === null) {
327
+ (0, mailbox_http_response_1.writeJson)(response, 400, { ok: false, error: "limit must be an integer between 1 and 100" });
328
+ return;
329
+ }
330
+ const view = limit === undefined
331
+ ? options.hooks.readAgentContextPackets(agent)
332
+ : options.hooks.readAgentContextPackets(agent, { limit });
333
+ (0, mailbox_http_response_1.writeJson)(response, 200, view);
334
+ return;
335
+ }
336
+ if (surface.startsWith("context-packets/")) {
337
+ const rawPacketId = surface.slice("context-packets/".length);
338
+ const packetId = decodePathSegment(rawPacketId);
339
+ const view = packetId ? options.hooks.readAgentContextPacket(agent, packetId) : null;
340
+ if (!view) {
341
+ (0, mailbox_http_response_1.writeJson)(response, 404, { ok: false, error: `context packet '${rawPacketId}' not found` });
342
+ return;
343
+ }
344
+ (0, mailbox_http_response_1.writeJson)(response, 200, view);
345
+ return;
346
+ }
324
347
  if (surface === "mail") {
325
348
  (0, mailbox_http_response_1.writeJson)(response, 200, await options.hooks.readAgentMail(agent));
326
349
  return;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.readSelfFixView = exports.readMailboxContinuity = exports.readOrientationView = exports.readObligationDetailView = exports.readNoteDecisionView = exports.readSentinelView = exports.readContextLossGauntletView = exports.readChangesView = exports.readNeedsMeView = exports.readNotesView = exports.readLogView = exports.readHabitView = exports.readHabitRunView = exports.readHabitRunReceiptView = exports.readHabitSessionSummaryView = exports.readHabitSessionSummaryListView = exports.readFriendView = exports.readDeskPrefs = exports.readDaemonHealthDeep = exports.readCodingDeep = exports.readBridgeInventory = exports.readAttentionView = exports.readMailView = exports.readMailMessageView = exports.readSessionTranscript = exports.readSessionInventory = exports.readMailboxMachineState = exports.readMailboxAgentState = exports.readObligationSummary = void 0;
3
+ exports.readSelfFixView = exports.readMailboxContinuity = exports.readOrientationView = exports.readObligationDetailView = exports.readNoteDecisionView = exports.readSentinelView = exports.readContextLossGauntletView = exports.readChangesView = exports.readNeedsMeView = exports.readNotesView = exports.readLogView = exports.readContextPacketView = exports.readContextPacketListView = exports.readHabitView = exports.readHabitRunView = exports.readHabitRunReceiptView = exports.readHabitSessionSummaryView = exports.readHabitSessionSummaryListView = exports.readFriendView = exports.readDeskPrefs = exports.readDaemonHealthDeep = exports.readCodingDeep = exports.readBridgeInventory = exports.readAttentionView = exports.readMailView = exports.readMailMessageView = exports.readSessionTranscript = exports.readSessionInventory = exports.readMailboxMachineState = exports.readMailboxAgentState = exports.readObligationSummary = void 0;
4
4
  var agent_machine_1 = require("./readers/agent-machine");
5
5
  Object.defineProperty(exports, "readObligationSummary", { enumerable: true, get: function () { return agent_machine_1.readObligationSummary; } });
6
6
  Object.defineProperty(exports, "readMailboxAgentState", { enumerable: true, get: function () { return agent_machine_1.readMailboxAgentState; } });
@@ -23,6 +23,8 @@ Object.defineProperty(exports, "readHabitSessionSummaryView", { enumerable: true
23
23
  Object.defineProperty(exports, "readHabitRunReceiptView", { enumerable: true, get: function () { return runtime_readers_1.readHabitRunReceiptView; } });
24
24
  Object.defineProperty(exports, "readHabitRunView", { enumerable: true, get: function () { return runtime_readers_1.readHabitRunView; } });
25
25
  Object.defineProperty(exports, "readHabitView", { enumerable: true, get: function () { return runtime_readers_1.readHabitView; } });
26
+ Object.defineProperty(exports, "readContextPacketListView", { enumerable: true, get: function () { return runtime_readers_1.readContextPacketListView; } });
27
+ Object.defineProperty(exports, "readContextPacketView", { enumerable: true, get: function () { return runtime_readers_1.readContextPacketView; } });
26
28
  Object.defineProperty(exports, "readLogView", { enumerable: true, get: function () { return runtime_readers_1.readLogView; } });
27
29
  Object.defineProperty(exports, "readNotesView", { enumerable: true, get: function () { return runtime_readers_1.readNotesView; } });
28
30
  Object.defineProperty(exports, "readNeedsMeView", { enumerable: true, get: function () { return runtime_readers_1.readNeedsMeView; } });
@@ -35,6 +35,8 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.readCodingDeep = readCodingDeep;
37
37
  exports.readAttentionView = readAttentionView;
38
+ exports.readContextPacketListView = readContextPacketListView;
39
+ exports.readContextPacketView = readContextPacketView;
38
40
  exports.readBridgeInventory = readBridgeInventory;
39
41
  exports.readDaemonHealthDeep = readDaemonHealthDeep;
40
42
  exports.readNotesView = readNotesView;
@@ -56,6 +58,7 @@ const habit_session_summary_1 = require("../../habits/habit-session-summary");
56
58
  const identity_1 = require("../../identity");
57
59
  const daemon_health_1 = require("../../daemon/daemon-health");
58
60
  const flight_recorder_1 = require("../../../arc/flight-recorder");
61
+ const context_packet_ledger_1 = require("../../../senses/context-packet-ledger");
59
62
  const shared_1 = require("./shared");
60
63
  const agent_machine_1 = require("./agent-machine");
61
64
  const sessions_1 = require("./sessions");
@@ -217,6 +220,12 @@ function readAttentionView(agentName, options = {}) {
217
220
  returnObligations,
218
221
  };
219
222
  }
223
+ function readContextPacketListView(agentRoot, options = {}) {
224
+ return (0, context_packet_ledger_1.listSenseContextPacketSummaries)(agentRoot, options);
225
+ }
226
+ function readContextPacketView(agentRoot, packetId) {
227
+ return (0, context_packet_ledger_1.readSenseContextPacketView)(agentRoot, packetId);
228
+ }
220
229
  /* v8 ignore start — defensive parsing */
221
230
  function readBridgeInventory(agentRoot) {
222
231
  const bridgesDir = path.join(agentRoot, "state", "bridges");
@@ -44,6 +44,7 @@ const run_ledger_1 = require("../heart/run-ledger");
44
44
  const spend_ledger_1 = require("./spend-ledger");
45
45
  const habit_policy_1 = require("./habit-policy");
46
46
  const runtime_1 = require("../nerves/runtime");
47
+ const flight_recorder_1 = require("../arc/flight-recorder");
47
48
  function sha256Hex(input) {
48
49
  return (0, node_crypto_1.createHash)("sha256").update(input).digest("hex");
49
50
  }
@@ -127,13 +128,33 @@ function producedRefsFor(payload) {
127
128
  const snapshotId = stringField(refreshRecord(payload) ?? {}, "snapshotId");
128
129
  return snapshotId ? [{ kind: "none", locator: `state/rsvp/snapshots/${snapshotId}.json` }] : [];
129
130
  }
131
+ function outboundAction(payload) {
132
+ const refresh = refreshRecord(payload);
133
+ const decision = refresh?.outboundDecision;
134
+ if (!decision || typeof decision !== "object" || Array.isArray(decision))
135
+ return "unknown";
136
+ return stringField(decision, "action") ?? "unknown";
137
+ }
130
138
  function surfaceAttemptsFor(payload, requestedSend, errorMessage) {
131
139
  if (payload.sendAllowed === true) {
140
+ const delivery = (refreshRecord(payload)?.delivery ?? {});
141
+ const rawStatus = stringField(delivery, "status") ?? stringField(delivery, "result");
142
+ if (rawStatus === "failed" || rawStatus === "error") {
143
+ return [{
144
+ recipient: "rsvp",
145
+ channel: "bluebubbles",
146
+ reason: "status",
147
+ result: "failed",
148
+ rawStatus,
149
+ error: stringField(delivery, "error") ?? stringField(delivery, "message") ?? rawStatus,
150
+ }];
151
+ }
132
152
  return [{
133
153
  recipient: "rsvp",
134
154
  channel: "bluebubbles",
135
155
  reason: "status",
136
156
  result: "sent",
157
+ ...(rawStatus ? { rawStatus } : {}),
137
158
  }];
138
159
  }
139
160
  if (requestedSend && errorMessage) {
@@ -153,6 +174,140 @@ function payloadError(payload) {
153
174
  }
154
175
  return null;
155
176
  }
177
+ function traceStatusForSurfaceAttempt(attempt) {
178
+ if (!attempt)
179
+ return "skipped";
180
+ if (attempt.result === "failed")
181
+ return "failed";
182
+ if (attempt.result === "blocked" || attempt.result === "unavailable")
183
+ return "blocked";
184
+ return "succeeded";
185
+ }
186
+ function nativeRsvpTraceSteps(input) {
187
+ const refresh = refreshRecord(input.payload);
188
+ const snapshotId = stringField(refresh ?? {}, "snapshotId");
189
+ const reportText = stringField(refresh ?? {}, "reportText");
190
+ const action = outboundAction(input.payload);
191
+ const firstAttempt = input.surfaceAttempts[0];
192
+ const snapshotLocator = snapshotId ? `state/rsvp/snapshots/${snapshotId}.json` : null;
193
+ const decisions = [
194
+ `mode=${input.mode}`,
195
+ `sendAllowed=${String(input.sendAllowed)}`,
196
+ `outboundAction=${action}`,
197
+ ];
198
+ return [
199
+ {
200
+ schemaVersion: 1,
201
+ stepId: "trigger",
202
+ kind: "trigger",
203
+ status: "succeeded",
204
+ at: input.startedAt,
205
+ summary: `${input.trigger} triggered ${input.habitName}.`,
206
+ },
207
+ {
208
+ schemaVersion: 1,
209
+ stepId: "habit-definition",
210
+ kind: "habit_definition",
211
+ status: "succeeded",
212
+ at: input.startedAt,
213
+ summary: `Loaded habit definition for ${input.habitName}.`,
214
+ refs: [{ kind: "habit_definition", locator: `habits/${input.habitName}.md`, label: input.habitName }],
215
+ },
216
+ {
217
+ schemaVersion: 1,
218
+ stepId: "fetch-refresh",
219
+ kind: "fetch",
220
+ status: input.errors.length > 0 ? "failed" : "succeeded",
221
+ at: input.endedAt,
222
+ summary: `Ran RSVP refresh in ${input.mode} mode.`,
223
+ refs: [{ kind: "source", locator: `rsvp/${input.source}` }],
224
+ },
225
+ {
226
+ schemaVersion: 1,
227
+ stepId: "snapshot",
228
+ kind: "snapshot",
229
+ status: snapshotLocator ? "succeeded" : "skipped",
230
+ at: input.endedAt,
231
+ summary: snapshotLocator ? "Recorded the RSVP snapshot reference." : "No RSVP snapshot reference was returned.",
232
+ ...(snapshotLocator ? { refs: [{ kind: "snapshot", locator: snapshotLocator }] } : {}),
233
+ },
234
+ {
235
+ schemaVersion: 1,
236
+ stepId: "render",
237
+ kind: "render",
238
+ status: reportText ? "succeeded" : "skipped",
239
+ at: input.endedAt,
240
+ summary: reportText ? "Rendered RSVP update text." : "No rendered RSVP update text was returned.",
241
+ },
242
+ {
243
+ schemaVersion: 1,
244
+ stepId: "decision",
245
+ kind: "decision",
246
+ status: input.errors.length > 0 ? "blocked" : "succeeded",
247
+ at: input.endedAt,
248
+ summary: `RSVP outbound decision: ${action}.`,
249
+ decisions,
250
+ },
251
+ {
252
+ schemaVersion: 1,
253
+ stepId: "produced-ref",
254
+ kind: "produced_ref",
255
+ status: input.producedRefs.length > 0 ? "succeeded" : "skipped",
256
+ at: input.endedAt,
257
+ summary: input.producedRefs.length > 0 ? "Recorded produced RSVP refs." : "No separate produced refs were recorded.",
258
+ ...(input.producedRefs.length > 0 ? { producedRefs: input.producedRefs } : {}),
259
+ },
260
+ {
261
+ schemaVersion: 1,
262
+ stepId: "surface-attempt",
263
+ kind: "surface_attempt",
264
+ status: traceStatusForSurfaceAttempt(firstAttempt),
265
+ at: input.endedAt,
266
+ summary: firstAttempt ? `Surface attempt ${firstAttempt.result} via ${firstAttempt.channel}.` : "No surface attempt was needed.",
267
+ ...(firstAttempt ? { surfaceAttempt: firstAttempt } : {}),
268
+ },
269
+ {
270
+ schemaVersion: 1,
271
+ stepId: "send",
272
+ kind: "send",
273
+ status: input.sendAllowed
274
+ ? traceStatusForSurfaceAttempt(firstAttempt)
275
+ : "skipped",
276
+ at: input.endedAt,
277
+ summary: input.sendAllowed ? "Live send was allowed for this run." : "Live send was not allowed for this run.",
278
+ ...(firstAttempt ? { surfaceAttempt: firstAttempt } : {}),
279
+ },
280
+ {
281
+ schemaVersion: 1,
282
+ stepId: "ledger",
283
+ kind: "ledger",
284
+ status: "succeeded",
285
+ at: input.endedAt,
286
+ summary: "Recorded native RSVP run ledger evidence.",
287
+ refs: [
288
+ { kind: "ledger", locator: "state/run-ledger/runs.jsonl" },
289
+ { kind: "ledger", locator: "state/rsvp/spend-ledger.json" },
290
+ ],
291
+ },
292
+ {
293
+ schemaVersion: 1,
294
+ stepId: "error",
295
+ kind: "error",
296
+ status: input.errors.length > 0 ? "failed" : "skipped",
297
+ at: input.endedAt,
298
+ summary: input.errors.length > 0 ? "RSVP refresh returned an error." : "No RSVP error happened.",
299
+ ...(input.errors[0] ? { error: input.errors[0] } : {}),
300
+ },
301
+ {
302
+ schemaVersion: 1,
303
+ stepId: "complete",
304
+ kind: "complete",
305
+ status: input.lifecycle === "completed" ? "succeeded" : "failed",
306
+ at: input.endedAt,
307
+ summary: `Native RSVP habit ${input.lifecycle}.`,
308
+ },
309
+ ];
310
+ }
156
311
  function recordNativeRunLedger(input) {
157
312
  const target = {
158
313
  habitName: input.habitName,
@@ -247,6 +402,7 @@ async function runNativeRsvpHabit(input) {
247
402
  const lifecycle = errorMessage ? "error" : "completed";
248
403
  const surfaceAttempts = surfaceAttemptsFor(payload, policy.sendAllowed, errorMessage);
249
404
  const errors = errorMessage ? [errorMessage] : [];
405
+ const producedRefs = producedRefsFor(payload);
250
406
  recordNativeRunLedger({
251
407
  agentRoot,
252
408
  agent: input.agent,
@@ -258,6 +414,20 @@ async function runNativeRsvpHabit(input) {
258
414
  endedAt,
259
415
  lifecycle,
260
416
  });
417
+ const traceSteps = nativeRsvpTraceSteps({
418
+ habitName: input.habitName,
419
+ trigger: input.trigger,
420
+ mode: habit.rsvp.mode,
421
+ source: habit.rsvp.source,
422
+ sendAllowed: policy.sendAllowed,
423
+ startedAt,
424
+ endedAt,
425
+ payload,
426
+ producedRefs,
427
+ surfaceAttempts,
428
+ errors,
429
+ lifecycle,
430
+ });
261
431
  const completion = (0, habit_session_1.completeHabitRun)({
262
432
  agentRoot,
263
433
  habit,
@@ -268,17 +438,15 @@ async function runNativeRsvpHabit(input) {
268
438
  operationId: `rsvp-native:${input.trigger}:${input.occurrenceId ?? runId}`,
269
439
  permissionEnvelope: permissionEnvelope(policy.sendAllowed),
270
440
  toolPolicy: toolPolicy(policy.sendAllowed),
271
- producedRefs: producedRefsFor(payload),
441
+ producedRefs,
272
442
  surfaceAttempts,
443
+ traceSteps,
273
444
  errors,
274
445
  summarySnapshot: {
275
446
  summary: errorMessage
276
447
  ? `Native RSVP habit failed: ${errorMessage}`
277
448
  : "Native RSVP habit completed.",
278
- decisions: [
279
- `mode=${habit.rsvp.mode}`,
280
- `sendAllowed=${String(policy.sendAllowed)}`,
281
- ],
449
+ decisions: (0, flight_recorder_1.decisionsFromHabitRunTraceSteps)(traceSteps),
282
450
  nextLikelyStep: null,
283
451
  },
284
452
  });