@ouro.bot/cli 0.1.0-alpha.666 → 0.1.0-alpha.668

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.
@@ -104,6 +104,11 @@ function parseSurface(raw) {
104
104
  extra: record ? parseStringArray(record.extra) : [],
105
105
  };
106
106
  }
107
+ function parseContinuity(raw) {
108
+ const record = objectRecord(raw);
109
+ const mode = record ? record.mode : null;
110
+ return { mode: mode === "stateful" ? "stateful" : "fresh" };
111
+ }
107
112
  function extractFrontmatterAndBody(content) {
108
113
  const lines = content.split(/\r?\n/);
109
114
  if (lines[0]?.trim() !== "---") {
@@ -137,6 +142,7 @@ function parseHabitFile(content, filePath) {
137
142
  tools: undefined,
138
143
  origin: null,
139
144
  surface: { family: true, originator: true, extra: [] },
145
+ continuity: { mode: "fresh" },
140
146
  body: content.trim(),
141
147
  };
142
148
  }
@@ -154,6 +160,7 @@ function parseHabitFile(content, filePath) {
154
160
  const tools = parseToolsField(frontmatter.tools);
155
161
  const origin = parseOrigin(frontmatter.origin);
156
162
  const surface = parseSurface(frontmatter.surface);
163
+ const continuity = parseContinuity(frontmatter.continuity);
157
164
  return {
158
165
  name: stem,
159
166
  title,
@@ -164,6 +171,7 @@ function parseHabitFile(content, filePath) {
164
171
  tools,
165
172
  origin,
166
173
  surface,
174
+ continuity,
167
175
  body,
168
176
  };
169
177
  }
@@ -47,6 +47,9 @@ function habitRuntimeStateDir(agentRoot) {
47
47
  function isNonEmptyString(value) {
48
48
  return typeof value === "string" && value.trim().length > 0;
49
49
  }
50
+ function nullableCursor(value) {
51
+ return isNonEmptyString(value) ? value.trim() : null;
52
+ }
50
53
  function stripLegacyLastRunFromDefinition(definitionPath) {
51
54
  const content = fs.readFileSync(definitionPath, "utf-8");
52
55
  const lines = content.split(/\r?\n/);
@@ -72,23 +75,34 @@ function applyHabitRuntimeState(agentRoot, habit) {
72
75
  return habit;
73
76
  return { ...habit, lastRun: runtimeLastRun };
74
77
  }
75
- function writeHabitLastRun(agentRoot, habitName, lastRun, updatedAt = lastRun) {
78
+ function writeHabitLastRun(agentRoot, habitName, lastRun, updatedAt = lastRun, options = {}) {
76
79
  const record = {
77
80
  schemaVersion: 1,
78
81
  name: habitName,
79
82
  lastRun,
80
83
  updatedAt,
84
+ activeOperationId: nullableCursor(options.activeOperationId),
85
+ latestRunId: nullableCursor(options.latestRunId),
86
+ latestReceiptLocator: nullableCursor(options.latestReceiptLocator),
81
87
  };
82
88
  (0, json_store_1.writeJsonFile)(habitRuntimeStateDir(agentRoot), habitName, record);
83
89
  (0, runtime_1.emitNervesEvent)({
84
90
  component: "daemon",
85
91
  event: "daemon.habit_runtime_state_write",
86
92
  message: "wrote habit runtime state",
87
- meta: { agentRoot, habitName, lastRun, updatedAt },
93
+ meta: {
94
+ agentRoot,
95
+ habitName,
96
+ lastRun,
97
+ updatedAt,
98
+ activeOperationId: record.activeOperationId,
99
+ latestRunId: record.latestRunId,
100
+ latestReceiptLocator: record.latestReceiptLocator,
101
+ },
88
102
  });
89
103
  }
90
104
  function recordHabitRun(agentRoot, habitName, lastRun, options = {}) {
91
- writeHabitLastRun(agentRoot, habitName, lastRun);
105
+ writeHabitLastRun(agentRoot, habitName, lastRun, lastRun, options);
92
106
  if (!options.definitionPath)
93
107
  return;
94
108
  try {
@@ -0,0 +1,318 @@
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.selectHabitRunReceipt = selectHabitRunReceipt;
37
+ exports.readHabitSessionSummary = readHabitSessionSummary;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const flight_recorder_1 = require("../../arc/flight-recorder");
41
+ const runtime_1 = require("../../nerves/runtime");
42
+ const session_events_1 = require("../session-events");
43
+ const VALID_WHICH = new Set(["latest", "previous", "latest-success", "latest-failure"]);
44
+ const SUCCESS_OUTCOMES = new Set([
45
+ "no_change",
46
+ "wrote_arc",
47
+ "updated_desk",
48
+ "wrote_record",
49
+ "surfaced",
50
+ ]);
51
+ const FAILURE_OUTCOMES = new Set(["blocked", "error"]);
52
+ const MAX_SUMMARY_CHARS = 1600;
53
+ const TRUNCATION_SUFFIX = "\n[truncated]";
54
+ function selectorError(code, message) {
55
+ return { ok: false, error: { code, message } };
56
+ }
57
+ function normalizeWhich(value) {
58
+ if (value === undefined)
59
+ return "latest";
60
+ return VALID_WHICH.has(value) ? value : null;
61
+ }
62
+ function sortNewestFirst(receipts) {
63
+ return [...receipts].sort((left, right) => right.endedAt.localeCompare(left.endedAt) || right.runId.localeCompare(left.runId));
64
+ }
65
+ function filterReceipts(receipts, selector) {
66
+ return receipts.filter((receipt) => {
67
+ if (selector.habitName !== undefined && receipt.habitName !== selector.habitName)
68
+ return false;
69
+ if (selector.operationId !== undefined && receipt.operationId !== selector.operationId)
70
+ return false;
71
+ return true;
72
+ });
73
+ }
74
+ function filterByWhich(receipts, which) {
75
+ if (which === "latest" || which === "previous")
76
+ return receipts;
77
+ const outcomes = which === "latest-success" ? SUCCESS_OUTCOMES : FAILURE_OUTCOMES;
78
+ return receipts.filter((receipt) => outcomes.has(receipt.outcome));
79
+ }
80
+ function selectHabitRunReceipt(receipts, selector) {
81
+ if (selector.runId !== undefined) {
82
+ if (selector.habitName !== undefined || selector.operationId !== undefined || selector.which !== undefined) {
83
+ return selectorError("run_id_exclusive", "runId cannot be combined with habitName, operationId, or which");
84
+ }
85
+ const receipt = receipts.find((entry) => entry.runId === selector.runId);
86
+ return receipt ? { ok: true, receipt } : selectorError("not_found", "no habit run matched selector");
87
+ }
88
+ if (selector.habitName === undefined && selector.operationId === undefined) {
89
+ return selectorError("selector_required", "provide runId, habitName, or operationId");
90
+ }
91
+ const which = normalizeWhich(selector.which);
92
+ if (which === null) {
93
+ return selectorError("invalid_which", "which must be latest, previous, latest-success, or latest-failure");
94
+ }
95
+ const matches = filterByWhich(sortNewestFirst(filterReceipts([...receipts], selector)), which);
96
+ const index = which === "previous" ? 1 : 0;
97
+ const receipt = matches[index];
98
+ return receipt ? { ok: true, receipt } : selectorError("not_found", "no habit run matched selector");
99
+ }
100
+ function isRecord(value) {
101
+ return value !== null && typeof value === "object" && !Array.isArray(value);
102
+ }
103
+ function stringValue(value) {
104
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
105
+ }
106
+ function summarySnapshot(receipt) {
107
+ const snapshot = receipt.summarySnapshot;
108
+ return {
109
+ summary: snapshot.summary,
110
+ decisions: snapshot.decisions,
111
+ nextLikelyStep: snapshot.nextLikelyStep,
112
+ };
113
+ }
114
+ function relativeSource(receipt, key) {
115
+ if (key === "receipt")
116
+ return receipt.receiptLocator;
117
+ if (key === "session")
118
+ return receipt.sessionLocator;
119
+ if (key === "pending")
120
+ return receipt.pendingLocator;
121
+ return receipt.runtimeStateLocator;
122
+ }
123
+ function safeSourcePath(agentRoot, locator, key, expectedPrefix) {
124
+ const normalizedInput = locator.replace(/\\/g, "/");
125
+ const normalized = path.posix.normalize(normalizedInput);
126
+ const unsafe = path.isAbsolute(locator)
127
+ || normalizedInput.startsWith("/")
128
+ || normalized === "."
129
+ || normalized === ".."
130
+ || normalized.startsWith("../")
131
+ || normalized !== normalizedInput
132
+ || !normalized.startsWith(expectedPrefix);
133
+ if (unsafe) {
134
+ return { ok: false, warning: `${key} locator unsafe: ${locator}` };
135
+ }
136
+ const root = path.resolve(agentRoot);
137
+ const filePath = path.resolve(agentRoot, normalized);
138
+ /* v8 ignore next -- defensive containment check after normalized bundle-relative locator validation @preserve */
139
+ if (filePath !== root && !filePath.startsWith(`${root}${path.sep}`)) {
140
+ return { ok: false, warning: `${key} locator escaped bundle: ${locator}` };
141
+ }
142
+ return { ok: true, filePath };
143
+ }
144
+ function safeExistingRealPath(agentRoot, filePath, key, locator) {
145
+ try {
146
+ const root = fs.realpathSync.native(agentRoot);
147
+ const realPath = fs.realpathSync.native(filePath);
148
+ if (realPath !== root && !realPath.startsWith(`${root}${path.sep}`)) {
149
+ return { ok: false, warning: `${key} locator escaped bundle via symlink: ${locator}` };
150
+ }
151
+ return { ok: true, filePath: realPath };
152
+ }
153
+ catch {
154
+ /* v8 ignore next -- defensive realpath race/permission guard; existence is checked before callers invoke this @preserve */
155
+ return { ok: false, warning: `${key} locator unreadable: ${locator}` };
156
+ }
157
+ }
158
+ function sessionSummaryFromMessages(messages) {
159
+ const toolsUsed = new Set();
160
+ for (const message of messages) {
161
+ /* v8 ignore next -- defensive: session projection returns message records; this protects malformed direct projection callers @preserve */
162
+ if (!isRecord(message))
163
+ continue;
164
+ const name = stringValue(message.name) ?? stringValue(message.toolName);
165
+ /* v8 ignore next -- defensive: canonical projections expose tool names from assistant tool_calls; legacy direct tool-name projections still count if present @preserve */
166
+ if (name && message.role === "tool")
167
+ toolsUsed.add(name);
168
+ if (Array.isArray(message.tool_calls)) {
169
+ /* v8 ignore start -- defensive: projected provider messages normally expose structured tool calls; malformed direct envelopes are kept inert @preserve */
170
+ for (const toolCall of message.tool_calls) {
171
+ if (!isRecord(toolCall))
172
+ continue;
173
+ const fn = isRecord(toolCall.function) ? toolCall.function : null;
174
+ const toolName = fn ? stringValue(fn.name) : null;
175
+ if (toolName)
176
+ toolsUsed.add(toolName);
177
+ }
178
+ /* v8 ignore stop @preserve */
179
+ }
180
+ }
181
+ const warnings = messages.length === 0 ? ["session file had no usable messages"] : [];
182
+ return {
183
+ decisions: [],
184
+ nextLikelyStep: null,
185
+ toolsUsed: [...toolsUsed].sort(),
186
+ warnings,
187
+ };
188
+ }
189
+ function readSessionEnrichment(agentRoot, receipt) {
190
+ const source = safeSourcePath(agentRoot, receipt.sessionLocator, "session", "state/habit-sessions/");
191
+ if (!source.ok) {
192
+ return { decisions: [], nextLikelyStep: null, toolsUsed: [], warnings: [source.warning] };
193
+ }
194
+ if (!fs.existsSync(source.filePath)) {
195
+ return { decisions: [], nextLikelyStep: null, toolsUsed: [], warnings: ["session file missing"] };
196
+ }
197
+ const realSource = safeExistingRealPath(agentRoot, source.filePath, "session", receipt.sessionLocator);
198
+ if (!realSource.ok) {
199
+ return { decisions: [], nextLikelyStep: null, toolsUsed: [], warnings: [realSource.warning] };
200
+ }
201
+ const envelope = (0, session_events_1.loadSessionEnvelopeFile)(realSource.filePath);
202
+ if (!envelope) {
203
+ return { decisions: [], nextLikelyStep: null, toolsUsed: [], warnings: ["session file malformed"] };
204
+ }
205
+ return sessionSummaryFromMessages((0, session_events_1.projectProviderMessages)(envelope));
206
+ }
207
+ function readPending(agentRoot, receipt) {
208
+ const source = safeSourcePath(agentRoot, receipt.pendingLocator, "pending", "state/habit-sessions/");
209
+ if (!source.ok)
210
+ return { pending: { count: 0, files: [] }, warnings: [source.warning] };
211
+ try {
212
+ if (!fs.existsSync(source.filePath))
213
+ return { pending: { count: 0, files: [] }, warnings: [] };
214
+ const realSource = safeExistingRealPath(agentRoot, source.filePath, "pending", receipt.pendingLocator);
215
+ if (!realSource.ok)
216
+ return { pending: { count: 0, files: [] }, warnings: [realSource.warning] };
217
+ const entries = fs.readdirSync(realSource.filePath, { withFileTypes: true })
218
+ .filter((entry) => entry.isFile())
219
+ .map((entry) => entry.name)
220
+ .sort();
221
+ return { pending: { count: entries.length, files: entries }, warnings: [] };
222
+ }
223
+ catch {
224
+ return { pending: { count: 0, files: [] }, warnings: [] };
225
+ }
226
+ }
227
+ function runtimeOperationId(agentRoot, receipt) {
228
+ const source = safeSourcePath(agentRoot, receipt.runtimeStateLocator, "runtimeState", "state/habits/");
229
+ if (!source.ok)
230
+ return { operationId: null, warnings: [source.warning] };
231
+ try {
232
+ if (!fs.existsSync(source.filePath))
233
+ return { operationId: null, warnings: [] };
234
+ const realSource = safeExistingRealPath(agentRoot, source.filePath, "runtimeState", receipt.runtimeStateLocator);
235
+ if (!realSource.ok)
236
+ return { operationId: null, warnings: [realSource.warning] };
237
+ const parsed = JSON.parse(fs.readFileSync(realSource.filePath, "utf-8"));
238
+ if (!isRecord(parsed))
239
+ return { operationId: null, warnings: [] };
240
+ if (parsed.schemaVersion !== 1)
241
+ return { operationId: null, warnings: [] };
242
+ if (stringValue(parsed.name) !== receipt.habitName)
243
+ return { operationId: null, warnings: [] };
244
+ if (stringValue(parsed.latestRunId) !== receipt.runId)
245
+ return { operationId: null, warnings: [] };
246
+ if (stringValue(parsed.latestReceiptLocator) !== receipt.receiptLocator)
247
+ return { operationId: null, warnings: [] };
248
+ return { operationId: stringValue(parsed.activeOperationId), warnings: [] };
249
+ }
250
+ catch {
251
+ return { operationId: null, warnings: [] };
252
+ }
253
+ }
254
+ function uniqueStrings(values) {
255
+ return [...new Set(values)];
256
+ }
257
+ function truncateSummary(value) {
258
+ if (value.length <= MAX_SUMMARY_CHARS)
259
+ return value;
260
+ return `${value.slice(0, MAX_SUMMARY_CHARS - TRUNCATION_SUFFIX.length)}${TRUNCATION_SUFFIX}`;
261
+ }
262
+ function legacySummaryWarnings(receipt) {
263
+ return receipt.permissionEnvelope.warnings.some((warning) => warning.includes("legacy receipt normalized"))
264
+ ? ["legacy receipt normalized"]
265
+ : [];
266
+ }
267
+ function readHabitSessionSummary(agentRoot, selector) {
268
+ const selection = selectHabitRunReceipt((0, flight_recorder_1.listHabitRunReceipts)(agentRoot), selector);
269
+ if (!selection.ok)
270
+ return null;
271
+ const receipt = selection.receipt;
272
+ const snapshot = summarySnapshot(receipt);
273
+ const session = readSessionEnrichment(agentRoot, receipt);
274
+ const pending = readPending(agentRoot, receipt);
275
+ const runtime = runtimeOperationId(agentRoot, receipt);
276
+ const operationId = receipt.operationId ?? runtime.operationId;
277
+ const decisions = uniqueStrings([
278
+ ...snapshot.decisions,
279
+ ...session.decisions,
280
+ ]);
281
+ const summary = truncateSummary(snapshot.summary);
282
+ const nextLikelyStep = snapshot.nextLikelyStep ?? session.nextLikelyStep;
283
+ const result = {
284
+ runId: receipt.runId,
285
+ habitName: receipt.habitName,
286
+ operationId: operationId ?? null,
287
+ status: receipt.outcome,
288
+ triggeredAt: receipt.startedAt,
289
+ completedAt: receipt.endedAt,
290
+ summary,
291
+ decisions,
292
+ pending: pending.pending,
293
+ messagesSent: receipt.surfaceAttempts,
294
+ toolsUsed: session.toolsUsed,
295
+ producedRefs: receipt.producedRefs,
296
+ errors: receipt.errors,
297
+ nextLikelyStep: nextLikelyStep ?? null,
298
+ sources: {
299
+ receipt: relativeSource(receipt, "receipt"),
300
+ session: relativeSource(receipt, "session"),
301
+ pending: relativeSource(receipt, "pending"),
302
+ runtimeState: relativeSource(receipt, "runtimeState"),
303
+ },
304
+ warnings: [
305
+ ...legacySummaryWarnings(receipt),
306
+ ...session.warnings,
307
+ ...pending.warnings,
308
+ ...runtime.warnings,
309
+ ],
310
+ };
311
+ (0, runtime_1.emitNervesEvent)({
312
+ component: "daemon",
313
+ event: "daemon.habit_session_summary_read",
314
+ message: "habit session summary read",
315
+ meta: { agentRoot, runId: receipt.runId, habitName: receipt.habitName, warningCount: result.warnings.length },
316
+ });
317
+ return result;
318
+ }
@@ -59,14 +59,29 @@ function isHabitRuntimeStateSnapshot(value, habitName) {
59
59
  return record.schemaVersion === 1
60
60
  && record.name === habitName
61
61
  && typeof record.lastRun === "string"
62
- && typeof record.updatedAt === "string";
62
+ && typeof record.updatedAt === "string"
63
+ && (record.activeOperationId === undefined || record.activeOperationId === null || typeof record.activeOperationId === "string")
64
+ && (record.latestRunId === undefined || record.latestRunId === null || typeof record.latestRunId === "string")
65
+ && (record.latestReceiptLocator === undefined || record.latestReceiptLocator === null || typeof record.latestReceiptLocator === "string");
66
+ }
67
+ function runtimeCursorValue(value) {
68
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
63
69
  }
64
70
  function readHabitRuntimeStateSnapshot(agentRoot, habitName) {
65
71
  const runtimeStatePath = path.join(agentRoot, "state", "habits", `${habitName}.json`);
66
72
  try {
67
73
  const parsed = JSON.parse(fs.readFileSync(runtimeStatePath, "utf-8"));
68
- if (isHabitRuntimeStateSnapshot(parsed, habitName))
69
- return parsed;
74
+ if (isHabitRuntimeStateSnapshot(parsed, habitName)) {
75
+ return {
76
+ schemaVersion: 1,
77
+ name: parsed.name,
78
+ lastRun: parsed.lastRun,
79
+ updatedAt: parsed.updatedAt,
80
+ activeOperationId: runtimeCursorValue(parsed.activeOperationId),
81
+ latestRunId: runtimeCursorValue(parsed.latestRunId),
82
+ latestReceiptLocator: runtimeCursorValue(parsed.latestReceiptLocator),
83
+ };
84
+ }
70
85
  (0, runtime_1.emitNervesEvent)({
71
86
  level: "warn",
72
87
  component: "daemon",
@@ -444,6 +459,37 @@ function computeNextRunAt(habit, endedAt) {
444
459
  return null;
445
460
  return new Date(endedMs + cadenceMs).toISOString();
446
461
  }
462
+ function defaultSummarySnapshot(input) {
463
+ const errors = input.errors ?? [];
464
+ if (errors.length > 0) {
465
+ return {
466
+ summary: `Habit ${input.habit.name} finished with errors: ${errors.join("; ")}`,
467
+ decisions: [],
468
+ nextLikelyStep: null,
469
+ };
470
+ }
471
+ const surfaced = (input.surfaceAttempts ?? []).find((attempt) => attempt.result !== "blocked" && attempt.result !== "failed" && attempt.result !== "unavailable");
472
+ if (surfaced) {
473
+ return {
474
+ summary: `Habit ${input.habit.name} surfaced via ${surfaced.recipient}/${surfaced.channel}.`,
475
+ decisions: [],
476
+ nextLikelyStep: null,
477
+ };
478
+ }
479
+ const produced = (input.producedRefs ?? []).find((ref) => ref.kind !== "none");
480
+ if (produced) {
481
+ return {
482
+ summary: `Habit ${input.habit.name} produced ${produced.kind}: ${produced.locator}.`,
483
+ decisions: [],
484
+ nextLikelyStep: null,
485
+ };
486
+ }
487
+ return {
488
+ summary: `Habit ${input.habit.name} finished with ${input.outcome}.`,
489
+ decisions: [],
490
+ nextLikelyStep: null,
491
+ };
492
+ }
447
493
  function buildHabitRunReceipt(input) {
448
494
  const paths = createHabitSessionPaths(input.agentRoot, input.runId, input.habit.name);
449
495
  const receipt = {
@@ -460,9 +506,11 @@ function buildHabitRunReceipt(input) {
460
506
  pendingLocator: paths.pendingLocator,
461
507
  runtimeStateLocator: paths.runtimeStateLocator,
462
508
  receiptLocator: paths.receiptLocator,
509
+ operationId: input.operationId ?? null,
463
510
  nextRunAt: input.nextRunAt ?? computeNextRunAt(input.habit, input.endedAt),
464
511
  permissionEnvelope: input.permissionEnvelope,
465
512
  toolPolicy: input.toolPolicy,
513
+ summarySnapshot: input.summarySnapshot ?? defaultSummarySnapshot(input),
466
514
  producedRefs: input.producedRefs ?? [],
467
515
  surfaceAttempts: input.surfaceAttempts ?? [],
468
516
  errors: input.errors ?? [],
@@ -510,8 +558,9 @@ function habitOutcomeForCompletion(input) {
510
558
  }
511
559
  function completeHabitRun(input) {
512
560
  const { outcome, producedRefs } = habitOutcomeForCompletion(input);
561
+ let receipt;
513
562
  try {
514
- (0, flight_recorder_1.writeHabitRunReceipt)(input.agentRoot, buildHabitRunReceipt({
563
+ receipt = buildHabitRunReceipt({
515
564
  agentRoot: input.agentRoot,
516
565
  habit: input.habit,
517
566
  runId: input.runId,
@@ -519,12 +568,15 @@ function completeHabitRun(input) {
519
568
  startedAt: input.startedAt,
520
569
  endedAt: input.endedAt,
521
570
  outcome,
571
+ operationId: input.operationId ?? null,
522
572
  permissionEnvelope: input.permissionEnvelope,
523
573
  toolPolicy: input.toolPolicy,
524
574
  producedRefs,
525
575
  surfaceAttempts: input.surfaceAttempts,
526
576
  errors: input.errors,
527
- }));
577
+ summarySnapshot: input.summarySnapshot,
578
+ });
579
+ (0, flight_recorder_1.writeHabitRunReceipt)(input.agentRoot, receipt);
528
580
  }
529
581
  catch (error) {
530
582
  (0, runtime_1.emitNervesEvent)({
@@ -535,7 +587,7 @@ function completeHabitRun(input) {
535
587
  meta: {
536
588
  habitName: input.habit.name,
537
589
  runId: input.runId,
538
- error: error instanceof Error ? error.message : String(error),
590
+ error: String(error),
539
591
  },
540
592
  });
541
593
  return { outcome, producedRefs, receiptWritten: false, runtimeStateRecorded: false };
@@ -543,6 +595,9 @@ function completeHabitRun(input) {
543
595
  try {
544
596
  (0, habit_runtime_state_1.recordHabitRun)(input.agentRoot, input.habit.name, input.endedAt, {
545
597
  definitionPath: path.join(input.agentRoot, "habits", `${input.habit.name}.md`),
598
+ activeOperationId: receipt.operationId ?? null,
599
+ latestRunId: receipt.runId,
600
+ latestReceiptLocator: receipt.receiptLocator,
546
601
  });
547
602
  return { outcome, producedRefs, receiptWritten: true, runtimeStateRecorded: true };
548
603
  }
@@ -555,7 +610,7 @@ function completeHabitRun(input) {
555
610
  meta: {
556
611
  habitName: input.habit.name,
557
612
  runId: input.runId,
558
- error: error instanceof Error ? error.message : String(error),
613
+ error: String(error),
559
614
  },
560
615
  });
561
616
  return { outcome, producedRefs, receiptWritten: true, runtimeStateRecorded: false };
@@ -33,13 +33,37 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.isSafeMailboxAgentName = isSafeMailboxAgentName;
36
37
  exports.createMailboxHttpReadHooks = createMailboxHttpReadHooks;
37
38
  const path = __importStar(require("path"));
38
39
  const mailbox_read_1 = require("./mailbox-read");
40
+ function isSafeMailboxAgentName(agentName) {
41
+ const trimmed = agentName.trim();
42
+ return trimmed === agentName
43
+ && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(agentName)
44
+ && agentName !== "."
45
+ && agentName !== ".."
46
+ && !agentName.includes("/")
47
+ && !agentName.includes("\\");
48
+ }
49
+ function resolveAgentRoot(bundlesRoot, agentName) {
50
+ if (!isSafeMailboxAgentName(agentName)) {
51
+ throw new Error("mailbox API requires a safe agent name");
52
+ }
53
+ if (!bundlesRoot)
54
+ return path.join(`${agentName}.ouro`);
55
+ const root = path.resolve(bundlesRoot);
56
+ const agentRoot = path.resolve(root, `${agentName}.ouro`);
57
+ /* v8 ignore next -- defensive containment guard after strict safe bundle-name validation @preserve */
58
+ if (agentRoot !== root && !agentRoot.startsWith(`${root}${path.sep}`)) {
59
+ throw new Error("mailbox API agent root escaped bundles root");
60
+ }
61
+ return agentRoot;
62
+ }
39
63
  function createMailboxHttpReadHooks(options) {
40
64
  const bundlesRoot = options.bundlesRoot;
41
65
  const readOptions = bundlesRoot ? { bundlesRoot } : undefined;
42
- const agentRoot = (agentName) => path.join(bundlesRoot ?? "", `${agentName}.ouro`);
66
+ const agentRoot = (agentName) => resolveAgentRoot(bundlesRoot, agentName);
43
67
  return {
44
68
  agentRoot,
45
69
  readAgentSessions: options.readAgentSessions ?? ((agentName) => (0, mailbox_read_1.readSessionInventory)(agentName, readOptions)),
@@ -60,6 +84,8 @@ function createMailboxHttpReadHooks(options) {
60
84
  readAgentHabits: options.readAgentHabits ?? ((agentName) => (0, mailbox_read_1.readHabitView)(agentRoot(agentName))),
61
85
  readAgentHabitRuns: options.readAgentHabitRuns ?? ((agentName, habitOptions) => (0, mailbox_read_1.readHabitRunView)(agentRoot(agentName), habitOptions)),
62
86
  readAgentHabitRun: options.readAgentHabitRun ?? ((agentName, runId) => (0, mailbox_read_1.readHabitRunReceiptView)(agentRoot(agentName), runId)),
87
+ readAgentHabitRunSummaries: options.readAgentHabitRunSummaries ?? ((agentName, habitOptions) => (0, mailbox_read_1.readHabitSessionSummaryListView)(agentRoot(agentName), habitOptions)),
88
+ readAgentHabitRunSummary: options.readAgentHabitRunSummary ?? ((agentName, selector) => (0, mailbox_read_1.readHabitSessionSummaryView)(agentRoot(agentName), selector)),
63
89
  readAgentMail: options.readAgentMail ?? ((agentName) => (0, mailbox_read_1.readMailView)(agentName)),
64
90
  readAgentMailMessage: options.readAgentMailMessage ?? ((agentName, messageId) => (0, mailbox_read_1.readMailMessageView)(agentName, messageId)),
65
91
  readDaemonHealth: options.readDaemonHealth ?? (() => (0, mailbox_read_1.readDaemonHealthDeep)(options.healthPath)),