@ouro.bot/cli 0.1.0-alpha.665 → 0.1.0-alpha.667

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 (35) hide show
  1. package/changelog.json +13 -0
  2. package/dist/arc/flight-recorder.js +324 -5
  3. package/dist/heart/core.js +167 -4
  4. package/dist/heart/cross-chat-delivery.js +3 -2
  5. package/dist/heart/daemon/cli-exec.js +139 -1
  6. package/dist/heart/daemon/cli-help.js +13 -2
  7. package/dist/heart/daemon/cli-parse.js +138 -2
  8. package/dist/heart/daemon/daemon-entry.js +24 -5
  9. package/dist/heart/daemon/daemon.js +10 -1
  10. package/dist/heart/habits/habit-parser.js +8 -0
  11. package/dist/heart/habits/habit-runtime-state.js +17 -3
  12. package/dist/heart/habits/habit-scheduler.js +24 -5
  13. package/dist/heart/habits/habit-session-summary.js +318 -0
  14. package/dist/heart/habits/habit-session.js +618 -0
  15. package/dist/heart/mailbox/mailbox-http-hooks.js +29 -1
  16. package/dist/heart/mailbox/mailbox-http-routes.js +122 -1
  17. package/dist/heart/mailbox/mailbox-read.js +5 -1
  18. package/dist/heart/mailbox/readers/runtime-readers.js +87 -0
  19. package/dist/mailbox-ui/assets/index-CaTIFDmv.js +1 -0
  20. package/dist/mailbox-ui/assets/index-Du_9G9WO.css +1 -0
  21. package/dist/mailbox-ui/assets/vendor-CcN1XpQ9.js +61 -0
  22. package/dist/mailbox-ui/index.html +3 -2
  23. package/dist/repertoire/tools-notes.js +50 -0
  24. package/dist/repertoire/tools-record.js +13 -0
  25. package/dist/repertoire/tools-session.js +140 -0
  26. package/dist/repertoire/tools-surface.js +11 -0
  27. package/dist/repertoire/tools.js +7 -0
  28. package/dist/senses/habit-turn-message.js +41 -3
  29. package/dist/senses/inner-dialog-worker.js +264 -68
  30. package/dist/senses/inner-dialog.js +29 -15
  31. package/dist/senses/pipeline.js +2 -11
  32. package/dist/senses/surface-tool.js +2 -1
  33. package/package.json +1 -1
  34. package/dist/mailbox-ui/assets/index-BZ60na8O.js +0 -61
  35. package/dist/mailbox-ui/assets/index-DG6Xf5uL.css +0 -1
@@ -36,13 +36,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.HEARTBEAT_OK_REST_SUPPRESSION_MS = exports.HABIT_RECURSION_BURST_THRESHOLD = exports.HABIT_RECURSION_BURST_WINDOW_MS = exports.HABIT_RECURSION_MIN_INTERVAL_MS = exports.MAX_CONSECUTIVE_INSTINCT_TURNS = void 0;
37
37
  exports.createInnerDialogWorker = createInnerDialogWorker;
38
38
  exports.startInnerDialogWorker = startInnerDialogWorker;
39
+ const fs = __importStar(require("fs"));
39
40
  const path = __importStar(require("path"));
40
41
  const inner_dialog_1 = require("./inner-dialog");
41
42
  const runtime_1 = require("../nerves/runtime");
42
43
  const identity_1 = require("../heart/identity");
43
44
  const pending_1 = require("../mind/pending");
45
+ const habit_parser_1 = require("../heart/habits/habit-parser");
44
46
  const habit_runtime_state_1 = require("../heart/habits/habit-runtime-state");
47
+ const habit_session_1 = require("../heart/habits/habit-session");
45
48
  const flight_recorder_1 = require("../arc/flight-recorder");
49
+ const habit_session_summary_1 = require("../heart/habits/habit-session-summary");
50
+ const store_file_1 = require("../mind/friends/store-file");
51
+ const tools_base_1 = require("../repertoire/tools-base");
52
+ const tools_surface_1 = require("../repertoire/tools-surface");
53
+ const tools_1 = require("../repertoire/tools");
46
54
  /**
47
55
  * Cap on consecutive `instinct` follow-on turns triggered by `hasPendingWork()`
48
56
  * with no externally-queued work in between. Without this cap, a turn that
@@ -83,62 +91,190 @@ function isHeartbeatOkRestResult(result) {
83
91
  const maybeResult = result;
84
92
  return maybeResult.turnOutcome === "rested" && maybeResult.restStatus === "HEARTBEAT_OK";
85
93
  }
86
- function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runInnerDialogTurn)(options), hasPendingWork = () => (0, pending_1.hasPendingMessages)((0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)())), nowSource = () => Date.now()) {
94
+ function isRecord(value) {
95
+ return !!value && typeof value === "object" && !Array.isArray(value);
96
+ }
97
+ function contentToText(content) {
98
+ if (typeof content === "string")
99
+ return content.trim();
100
+ if (!Array.isArray(content))
101
+ return "";
102
+ return content
103
+ .map((part) => isRecord(part) && typeof part.text === "string" ? part.text : "")
104
+ .filter((text) => text.trim().length > 0)
105
+ .join("\n")
106
+ .trim();
107
+ }
108
+ function resultMessages(result) {
109
+ if (Array.isArray(result))
110
+ return result.flatMap((entry) => resultMessages(entry));
111
+ return isRecord(result) && Array.isArray(result.messages) ? result.messages : [];
112
+ }
113
+ function latestAssistantText(results) {
114
+ for (let resultIndex = results.length - 1; resultIndex >= 0; resultIndex--) {
115
+ const messages = resultMessages(results[resultIndex]);
116
+ for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex--) {
117
+ const message = messages[messageIndex];
118
+ if (!isRecord(message) || message.role !== "assistant")
119
+ continue;
120
+ const text = contentToText(message.content);
121
+ if (text.length > 0)
122
+ return text.replace(/^checkpoint\s*:\s*/i, "").trim() || text;
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+ function deriveHabitSummarySnapshot(habitRun) {
128
+ const assistant = latestAssistantText(habitRun.results);
129
+ if (assistant)
130
+ return { summary: assistant, decisions: [], nextLikelyStep: null };
131
+ if (habitRun.errors.length > 0) {
132
+ return {
133
+ summary: `Habit ${habitRun.habit.name} finished with errors: ${habitRun.errors.join("; ")}`,
134
+ decisions: [],
135
+ nextLikelyStep: null,
136
+ };
137
+ }
138
+ const surfaced = habitRun.surfaceAttempts.find((attempt) => attempt.result !== "blocked" && attempt.result !== "failed" && attempt.result !== "unavailable");
139
+ if (surfaced) {
140
+ return {
141
+ summary: `Habit ${habitRun.habit.name} surfaced via ${surfaced.recipient}/${surfaced.channel}.`,
142
+ decisions: [],
143
+ nextLikelyStep: null,
144
+ };
145
+ }
146
+ const produced = habitRun.producedRefs.find((ref) => ref.kind !== "none");
147
+ if (produced) {
148
+ return {
149
+ summary: `Habit ${habitRun.habit.name} produced ${produced.kind}: ${produced.locator}.`,
150
+ decisions: [],
151
+ nextLikelyStep: null,
152
+ };
153
+ }
154
+ if (habitRun.results.some(isHeartbeatOkRestResult)) {
155
+ return {
156
+ summary: `Habit ${habitRun.habit.name} rested with HEARTBEAT_OK.`,
157
+ decisions: [],
158
+ nextLikelyStep: null,
159
+ };
160
+ }
161
+ return {
162
+ summary: `Habit ${habitRun.habit.name} completed without additional surfaced output.`,
163
+ decisions: [],
164
+ nextLikelyStep: null,
165
+ };
166
+ }
167
+ function fallbackHabitFile(habitName) {
168
+ return {
169
+ name: habitName,
170
+ title: habitName,
171
+ cadence: null,
172
+ status: "active",
173
+ lastRun: null,
174
+ created: null,
175
+ tools: [],
176
+ origin: null,
177
+ surface: { family: false, originator: false, extra: [] },
178
+ continuity: { mode: "fresh" },
179
+ body: "",
180
+ };
181
+ }
182
+ function readHabitForRun(agentRoot, habitName, errors) {
183
+ const habitPath = path.join(agentRoot, "habits", `${habitName}.md`);
184
+ try {
185
+ return (0, habit_parser_1.parseHabitFile)(fs.readFileSync(habitPath, "utf-8"), habitPath);
186
+ }
187
+ catch (error) {
188
+ const reason = error instanceof Error ? error.message : String(error);
189
+ errors.push(`habit file could not be read: ${reason}`);
190
+ (0, runtime_1.emitNervesEvent)({
191
+ level: "warn",
192
+ component: "senses",
193
+ event: "senses.habit_file_read_error",
194
+ message: "habit file could not be read for habit session",
195
+ meta: { habitName, habitPath, reason },
196
+ });
197
+ return fallbackHabitFile(habitName);
198
+ }
199
+ }
200
+ async function prepareHabitRun(habitName, trigger, startedAt) {
201
+ const agentRoot = (0, identity_1.getAgentRoot)();
202
+ const errors = [];
203
+ const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, readHabitForRun(agentRoot, habitName, errors));
204
+ const runId = (0, flight_recorder_1.createHabitRunId)(habitName, new Date(startedAt));
205
+ const operationId = habit.continuity.mode === "stateful" ? `habit:${habit.name}` : null;
206
+ const priorSessionSummary = readPriorSessionSummary(agentRoot, operationId);
207
+ const paths = (0, habit_session_1.createHabitSessionPaths)(agentRoot, runId, habit.name);
208
+ const friendStore = new store_file_1.FileFriendStore(path.join(agentRoot, "friends"));
209
+ const permissionEnvelope = await (0, habit_session_1.normalizeHabitPermissionEnvelope)(habit, { agentRoot, friendStore });
210
+ const toolPolicy = (0, habit_session_1.filterHabitToolsForEnvelope)([...tools_base_1.baseToolDefinitions, tools_surface_1.surfaceToolDefinition], habit.tools ?? null, permissionEnvelope, riskProfileForHabitPolicy);
211
+ return {
212
+ agentRoot,
213
+ habit,
214
+ runId,
215
+ operationId,
216
+ trigger,
217
+ startedAt,
218
+ priorSessionSummary,
219
+ paths,
220
+ permissionEnvelope,
221
+ toolPolicy,
222
+ friendStore,
223
+ results: [],
224
+ errors,
225
+ producedRefs: [],
226
+ surfaceAttempts: [],
227
+ };
228
+ }
229
+ function riskProfileForHabitPolicy(definition, name) {
230
+ const probeArgs = name === "shell" ? { command: "touch /tmp/habit-policy-probe" } : {};
231
+ return (0, tools_1.riskProfileForTool)(definition, name, probeArgs);
232
+ }
233
+ function readPriorSessionSummary(agentRoot, operationId) {
234
+ if (operationId === null)
235
+ return undefined;
236
+ try {
237
+ const summary = (0, habit_session_summary_1.readHabitSessionSummary)(agentRoot, { operationId, which: "latest" });
238
+ if (!summary)
239
+ return { mode: "stateful", summary: null, sources: {}, warnings: [] };
240
+ return {
241
+ mode: "stateful",
242
+ summary: summary.summary,
243
+ sources: summary.sources,
244
+ warnings: summary.warnings,
245
+ };
246
+ }
247
+ catch (error) {
248
+ return {
249
+ mode: "stateful",
250
+ summary: null,
251
+ sources: {},
252
+ warnings: [`prior summary read failed: ${String(error)}`],
253
+ };
254
+ }
255
+ }
256
+ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runInnerDialogTurn)(options), hasPendingWork = (pendingDir) => (0, pending_1.hasPendingMessages)(pendingDir ?? (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)())), nowSource = () => Date.now()) {
87
257
  let running = false;
88
258
  const queue = [];
89
259
  const lastFireByHabit = new Map();
90
260
  const recentHabitFires = [];
91
261
  let heartbeatOkRestedAt = null;
92
- function habitOutcomeForTurn(result, errors) {
93
- if (errors.length > 0)
94
- return { outcome: "error", producedRefs: [] };
95
- const toolNames = new Set();
96
- if (result && typeof result === "object" && Array.isArray(result.messages)) {
97
- for (const message of result.messages) {
98
- const toolCalls = message.tool_calls;
99
- if (!Array.isArray(toolCalls))
100
- continue;
101
- for (const call of toolCalls) {
102
- const functionName = call.function?.name;
103
- if (typeof functionName === "string")
104
- toolNames.add(functionName);
105
- }
106
- }
107
- }
108
- if (toolNames.has("send_message") || toolNames.has("surface")) {
109
- return { outcome: "surfaced", producedRefs: [{ kind: "surface", locator: "tool:send_message_or_surface" }] };
110
- }
111
- if (toolNames.has("diary_write") || toolNames.has("note")) {
112
- return { outcome: "wrote_record", producedRefs: [{ kind: "desk_record", locator: "desk/_record" }] };
113
- }
114
- if ([...toolNames].some((name) => name.startsWith("mcp__desk__"))) {
115
- return { outcome: "updated_desk", producedRefs: [{ kind: "desk_task", locator: "desk/" }] };
116
- }
117
- return { outcome: "no_change", producedRefs: [] };
118
- }
119
- function recordHabitCompletion(habitName, startedAt = new Date(nowSource()).toISOString(), endedAt = startedAt, trigger = "overdue", result, errors = []) {
120
- try {
121
- const agentRoot = (0, identity_1.getAgentRoot)();
122
- (0, habit_runtime_state_1.recordHabitRun)(agentRoot, habitName, endedAt, {
123
- definitionPath: path.join(agentRoot, "habits", `${habitName}.md`),
124
- });
125
- const { outcome, producedRefs } = habitOutcomeForTurn(result, errors);
126
- (0, flight_recorder_1.writeHabitRunReceipt)(agentRoot, {
127
- schemaVersion: 1,
128
- runId: (0, flight_recorder_1.createHabitRunId)(habitName, new Date(startedAt)),
129
- habitName,
130
- trigger,
131
- startedAt,
132
- endedAt,
133
- outcome,
134
- producedRefs,
135
- surfaceAttempts: [],
136
- errors,
137
- });
138
- }
139
- catch {
140
- // Habit file/state may be unavailable during the turn — skip gracefully
141
- }
262
+ function recordHabitCompletion(habitRun, endedAt = habitRun.startedAt) {
263
+ (0, habit_session_1.completeHabitRun)({
264
+ agentRoot: habitRun.agentRoot,
265
+ habit: habitRun.habit,
266
+ runId: habitRun.runId,
267
+ trigger: habitRun.trigger,
268
+ startedAt: habitRun.startedAt,
269
+ endedAt,
270
+ operationId: habitRun.operationId,
271
+ permissionEnvelope: habitRun.permissionEnvelope,
272
+ toolPolicy: habitRun.toolPolicy,
273
+ producedRefs: habitRun.producedRefs,
274
+ surfaceAttempts: habitRun.surfaceAttempts,
275
+ errors: habitRun.errors,
276
+ summarySnapshot: deriveHabitSummarySnapshot(habitRun),
277
+ });
142
278
  }
143
279
  function clearHeartbeatRestShield() {
144
280
  heartbeatOkRestedAt = null;
@@ -154,9 +290,11 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
154
290
  }
155
291
  return true;
156
292
  }
157
- function reuseHeartbeatOkRest(habitName) {
293
+ async function reuseHeartbeatOkRest(habitName) {
158
294
  const nowIso = new Date(nowSource()).toISOString();
159
- recordHabitCompletion(habitName, nowIso, nowIso, "overdue", { turnOutcome: "rested", restStatus: "HEARTBEAT_OK" });
295
+ const habitRun = await prepareHabitRun(habitName, "overdue", nowIso);
296
+ habitRun.results.push({ turnOutcome: "rested", restStatus: "HEARTBEAT_OK" });
297
+ recordHabitCompletion(habitRun, nowIso);
160
298
  (0, runtime_1.emitNervesEvent)({
161
299
  level: "info",
162
300
  component: "senses",
@@ -220,19 +358,61 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
220
358
  let nextHabitName = habitName;
221
359
  let nextAwaitName = awaitName;
222
360
  let nextTrigger = trigger;
361
+ let nextHabitRun = null;
223
362
  let consecutiveInstinctTurns = reason === "instinct" ? 1 : 0;
224
363
  runLoop: do {
225
364
  const currentReason = nextReason;
226
365
  const currentHabitName = nextHabitName;
227
366
  const currentTrigger = nextTrigger ?? "overdue";
228
- const habitStartedAt = currentReason === "habit" && currentHabitName ? new Date(nowSource()).toISOString() : null;
367
+ const currentHabitRun = currentReason === "habit" && currentHabitName
368
+ ? nextHabitRun && nextHabitRun.habit.name === currentHabitName
369
+ ? nextHabitRun
370
+ : await prepareHabitRun(currentHabitName, currentTrigger, new Date(nowSource()).toISOString())
371
+ : null;
372
+ nextHabitRun = null;
373
+ let currentHabitRunFinalized = false;
374
+ const finalizeCurrentHabitRun = () => {
375
+ if (!currentHabitRun || currentHabitRunFinalized)
376
+ return;
377
+ recordHabitCompletion(currentHabitRun, new Date(nowSource()).toISOString());
378
+ currentHabitRunFinalized = true;
379
+ };
229
380
  const turnErrors = [];
230
381
  if (!(currentReason === "habit" && currentHabitName === "heartbeat")) {
231
382
  clearHeartbeatRestShield();
232
383
  }
233
384
  let turnResult;
234
385
  try {
235
- turnResult = await runTurn({ reason: nextReason, taskId: nextTaskId, habitName: nextHabitName, awaitName: nextAwaitName });
386
+ const turnOptions = {
387
+ reason: nextReason,
388
+ taskId: nextTaskId,
389
+ habitName: nextHabitName,
390
+ awaitName: nextAwaitName,
391
+ ...(currentHabitRun
392
+ ? {
393
+ trigger: currentHabitRun.trigger,
394
+ preparedHabit: {
395
+ runId: currentHabitRun.runId,
396
+ trigger: currentHabitRun.trigger,
397
+ operationId: currentHabitRun.operationId,
398
+ habit: currentHabitRun.habit,
399
+ priorSessionSummary: currentHabitRun.priorSessionSummary,
400
+ },
401
+ habitSession: {
402
+ runId: currentHabitRun.runId,
403
+ sessionPath: currentHabitRun.paths.sessionPath,
404
+ pendingDir: currentHabitRun.paths.pendingDir,
405
+ permissionEnvelope: currentHabitRun.permissionEnvelope,
406
+ toolPolicy: currentHabitRun.toolPolicy,
407
+ friendStore: currentHabitRun.friendStore,
408
+ recordProducedRef: (ref) => { currentHabitRun.producedRefs.push(ref); },
409
+ recordSurfaceAttempt: (attempt) => { currentHabitRun.surfaceAttempts.push(attempt); },
410
+ recordError: (error) => { currentHabitRun.errors.push(error); },
411
+ },
412
+ }
413
+ : {}),
414
+ };
415
+ turnResult = await runTurn(turnOptions);
236
416
  }
237
417
  catch (error) {
238
418
  clearHeartbeatRestShield();
@@ -251,21 +431,23 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
251
431
  if (currentReason === "habit" && currentHabitName === "heartbeat") {
252
432
  heartbeatOkRestedAt = isHeartbeatOkRestResult(turnResult) ? nowSource() : null;
253
433
  }
254
- // Record lastRun after a habit turn without dirtying the tracked habit file.
255
- if (currentReason === "habit" && currentHabitName && habitStartedAt) {
256
- recordHabitCompletion(currentHabitName, habitStartedAt, new Date(nowSource()).toISOString(), currentTrigger, turnResult, turnErrors);
434
+ if (currentHabitRun) {
435
+ currentHabitRun.results.push(turnResult);
436
+ currentHabitRun.errors.push(...turnErrors);
257
437
  }
258
438
  // Drain queue first. Externally-queued work resets the instinct cap
259
439
  // because a real outside trigger arrived between turns.
260
440
  while (queue.length > 0) {
261
441
  const next = queue.shift();
262
442
  if (next.reason === "habit" && next.habitName === "heartbeat" && shouldReuseHeartbeatOkRest(next.habitName)) {
263
- reuseHeartbeatOkRest(next.habitName);
443
+ finalizeCurrentHabitRun();
444
+ await reuseHeartbeatOkRest(next.habitName);
264
445
  continue;
265
446
  }
266
447
  if (!(next.reason === "habit" && next.habitName === "heartbeat")) {
267
448
  clearHeartbeatRestShield();
268
449
  }
450
+ finalizeCurrentHabitRun();
269
451
  nextReason = next.reason;
270
452
  nextTaskId = next.taskId;
271
453
  nextHabitName = next.habitName;
@@ -278,7 +460,7 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
278
460
  // tool that writes to the inner-dialog pending dir during a turn
279
461
  // would cause hasPendingWork() to be true here, producing a
280
462
  // self-sustaining "instinct" loop with no external input. Cap it.
281
- if (hasPendingWork()) {
463
+ if (hasPendingWork(currentHabitRun?.paths.pendingDir)) {
282
464
  clearHeartbeatRestShield();
283
465
  if (consecutiveInstinctTurns >= exports.MAX_CONSECUTIVE_INSTINCT_TURNS) {
284
466
  (0, runtime_1.emitNervesEvent)({
@@ -292,16 +474,30 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
292
474
  lastReason: nextReason,
293
475
  },
294
476
  });
477
+ finalizeCurrentHabitRun();
295
478
  break;
296
479
  }
297
- consecutiveInstinctTurns += 1;
298
- nextReason = "instinct";
299
- nextTaskId = undefined;
300
- nextHabitName = undefined;
301
- nextAwaitName = undefined;
302
- nextTrigger = undefined;
480
+ if (currentReason === "habit" && currentHabitName && currentHabitRun) {
481
+ consecutiveInstinctTurns += 1;
482
+ nextReason = "habit";
483
+ nextTaskId = undefined;
484
+ nextHabitName = currentHabitName;
485
+ nextAwaitName = undefined;
486
+ nextTrigger = currentTrigger;
487
+ nextHabitRun = currentHabitRun;
488
+ }
489
+ else {
490
+ finalizeCurrentHabitRun();
491
+ consecutiveInstinctTurns += 1;
492
+ nextReason = "instinct";
493
+ nextTaskId = undefined;
494
+ nextHabitName = undefined;
495
+ nextAwaitName = undefined;
496
+ nextTrigger = undefined;
497
+ }
303
498
  continue;
304
499
  }
500
+ finalizeCurrentHabitRun();
305
501
  break;
306
502
  } while (true);
307
503
  }
@@ -317,7 +513,7 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
317
513
  /* v8 ignore next -- defensive fallback: live habit dispatch always sets habitName @preserve */
318
514
  const habitName = maybeMessage.habitName ?? "(unnamed)";
319
515
  if (shouldReuseHeartbeatOkRest(habitName)) {
320
- reuseHeartbeatOkRest(habitName);
516
+ await reuseHeartbeatOkRest(habitName);
321
517
  return;
322
518
  }
323
519
  recordHabitFireForRecursion(habitName);
@@ -335,7 +531,7 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
335
531
  if (maybeMessage.type === "heartbeat") {
336
532
  // Backward compatibility: heartbeat -> habit/heartbeat
337
533
  if (shouldReuseHeartbeatOkRest("heartbeat")) {
338
- reuseHeartbeatOkRest("heartbeat");
534
+ await reuseHeartbeatOkRest("heartbeat");
339
535
  return;
340
536
  }
341
537
  recordHabitFireForRecursion("heartbeat");
@@ -645,7 +645,7 @@ function buildHabitSurfacePolicy(origin, surface) {
645
645
  async function runInnerDialogTurn(options) {
646
646
  const now = options?.now ?? (() => new Date());
647
647
  const reason = options?.reason ?? "instinct";
648
- const sessionFilePath = innerDialogSessionPath();
648
+ const sessionFilePath = options?.habitSession?.sessionPath ?? innerDialogSessionPath();
649
649
  const agentName = (0, identity_1.getAgentName)();
650
650
  writeInnerDialogRuntimeState(sessionFilePath, {
651
651
  status: "running",
@@ -661,7 +661,7 @@ async function runInnerDialogTurn(options) {
661
661
  resting: false,
662
662
  lastHeartbeatAt: now().toISOString(),
663
663
  };
664
- const pendingDir = (0, pending_1.getInnerDialogPendingDir)(agentName);
664
+ const pendingDir = options?.habitSession?.pendingDir ?? (0, pending_1.getInnerDialogPendingDir)(agentName);
665
665
  const shouldUseHeldReturnWake = !options?.taskId && reason !== "habit" && reason !== "await"
666
666
  ? (0, obligations_1.listActiveReturnObligations)(agentName).length > 0
667
667
  : false;
@@ -669,7 +669,7 @@ async function runInnerDialogTurn(options) {
669
669
  let userContent;
670
670
  let habitTools;
671
671
  let habitParsedSuccessfully = false;
672
- if (existingMessages.length === 0) {
672
+ if (existingMessages.length === 0 && !(reason === "habit" && options?.habitName)) {
673
673
  // Fresh session: bootstrap message with non-canonical cleanup nudge
674
674
  const aspirations = readAspirations((0, identity_1.getAgentRoot)());
675
675
  const nonCanonical = (0, bundle_manifest_1.findNonCanonicalBundlePaths)((0, identity_1.getAgentRoot)());
@@ -692,24 +692,35 @@ async function runInnerDialogTurn(options) {
692
692
  const agentRoot = (0, identity_1.getAgentRoot)();
693
693
  const habitName = options.habitName;
694
694
  const habitFilePath = path.join(agentRoot, "habits", `${habitName}.md`);
695
+ const preparedHabit = options.preparedHabit?.habit.name === habitName ? options.preparedHabit.habit : null;
695
696
  // Read and parse the habit file
696
697
  let habitBody;
697
698
  let habitTitle = habitName;
698
699
  let habitLastRun = null;
699
700
  let habitOrigin = null;
700
701
  let habitSurface = { family: true, originator: true, extra: [] };
701
- try {
702
- const habitContent = fs.readFileSync(habitFilePath, "utf-8");
703
- const parsed = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(habitContent, habitFilePath));
704
- habitBody = parsed.body || undefined;
705
- habitTitle = parsed.title || habitName;
706
- habitLastRun = parsed.lastRun;
707
- habitTools = parsed.tools;
708
- habitOrigin = parsed.origin;
709
- habitSurface = parsed.surface;
702
+ if (preparedHabit) {
703
+ habitBody = preparedHabit.body || undefined;
704
+ habitTitle = preparedHabit.title || habitName;
705
+ habitLastRun = preparedHabit.lastRun;
706
+ habitTools = preparedHabit.tools;
707
+ habitOrigin = preparedHabit.origin;
708
+ habitSurface = preparedHabit.surface;
710
709
  }
711
- catch {
712
- // Habit file missing or unreadable
710
+ else {
711
+ try {
712
+ const habitContent = fs.readFileSync(habitFilePath, "utf-8");
713
+ const parsed = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(habitContent, habitFilePath));
714
+ habitBody = parsed.body || undefined;
715
+ habitTitle = parsed.title || habitName;
716
+ habitLastRun = parsed.lastRun;
717
+ habitTools = parsed.tools;
718
+ habitOrigin = parsed.origin;
719
+ habitSurface = parsed.surface;
720
+ }
721
+ catch {
722
+ // Habit file missing or unreadable
723
+ }
713
724
  }
714
725
  // If the habit file couldn't be read at all (no body, no title parsed), error message
715
726
  if (habitBody === undefined && habitTitle === habitName) {
@@ -753,6 +764,7 @@ async function runInnerDialogTurn(options) {
753
764
  arcResume,
754
765
  deskOrientation,
755
766
  surfacePolicy,
767
+ priorSessionSummary: options.preparedHabit?.habit.name === habitName ? options.preparedHabit.priorSessionSummary : undefined,
756
768
  now,
757
769
  });
758
770
  }
@@ -875,7 +887,7 @@ async function runInnerDialogTurn(options) {
875
887
  runAgent: core_1.runAgent,
876
888
  postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
877
889
  const prepared = (0, context_1.postTurnTrim)(turnMessages, usage, hooks);
878
- (0, context_1.deferPostTurnPersist)(sessionPathArg, prepared, usage, state);
890
+ return (0, context_1.deferPostTurnPersist)(sessionPathArg, prepared, usage, state);
879
891
  },
880
892
  accumulateFriendTokens: tokens_1.accumulateFriendTokens,
881
893
  signal: options?.signal,
@@ -917,7 +929,9 @@ async function runInnerDialogTurn(options) {
917
929
  toolContext: {
918
930
  signin: async () => undefined,
919
931
  delegatedOrigins: attentionQueue,
932
+ ...(options?.habitSession ? { habitSession: options.habitSession } : {}),
920
933
  },
934
+ ...(options?.habitSession ? { habitSession: options.habitSession } : {}),
921
935
  },
922
936
  });
923
937
  // Post-turn routeDelegatedCompletion removed: delivery is now inline via surface tool.
@@ -48,7 +48,6 @@ const continuity_1 = require("./continuity");
48
48
  const manager_1 = require("../heart/bridges/manager");
49
49
  const identity_1 = require("../heart/identity");
50
50
  const auth_flow_1 = require("../heart/auth/auth-flow");
51
- const socket_client_1 = require("../heart/daemon/socket-client");
52
51
  const active_work_1 = require("../heart/active-work");
53
52
  const delegation_1 = require("../heart/delegation");
54
53
  const obligations_1 = require("../arc/obligations");
@@ -828,7 +827,7 @@ async function handleInboundTurn(input) {
828
827
  /* v8 ignore next -- defensive: error always set when errored @preserve */
829
828
  result.error?.message ?? "unknown error", classification, currentProvider, currentBinding.model, agentName, inventory, {}, { currentLane });
830
829
  input.failoverState.pending = failoverContext;
831
- input.postTurn(sessionMessages, session.sessionPath, result.usage);
830
+ await input.postTurn(sessionMessages, session.sessionPath, result.usage);
832
831
  try {
833
832
  const agentRoot = (0, identity_1.getAgentRoot)();
834
833
  const postTurnArc = readPostTurnFlightRecorderArcSnapshot(agentRoot);
@@ -903,7 +902,7 @@ async function handleInboundTurn(input) {
903
902
  ? { lastFriendActivityAt }
904
903
  : undefined)
905
904
  : (Object.keys(continuingState).length > 0 ? continuingState : undefined);
906
- input.postTurn(sessionMessages, session.sessionPath, result.usage, undefined, nextState);
905
+ await input.postTurn(sessionMessages, session.sessionPath, result.usage, undefined, nextState);
907
906
  try {
908
907
  const agentRoot = (0, identity_1.getAgentRoot)();
909
908
  const postTurnArc = readPostTurnFlightRecorderArcSnapshot(agentRoot);
@@ -949,14 +948,6 @@ async function handleInboundTurn(input) {
949
948
  friendId: resolvedContext.friend.id,
950
949
  },
951
950
  });
952
- // DRY cross-session awareness: notify inner dialog that activity happened on another channel
953
- // Inner dialog's next checkpoint will include this session's state
954
- if (input.channel !== "inner") {
955
- try {
956
- (0, socket_client_1.requestInnerWake)((0, identity_1.getAgentName)(), existingToolContext?.daemonSocketPath).catch(/* v8 ignore next */ () => { });
957
- }
958
- catch { /* getAgentName may fail in test environments */ }
959
- }
960
951
  return {
961
952
  resolvedContext,
962
953
  gateResult,
@@ -4,7 +4,7 @@ exports.handleSurface = handleSurface;
4
4
  const attention_queue_1 = require("./attention-queue");
5
5
  const runtime_1 = require("../nerves/runtime");
6
6
  async function handleSurface(input) {
7
- const { content, delegationId, friendId, deliveryHint, queue, routeToFriend, advanceObligation, completePonderPacket, fulfillHeartObligation, } = input;
7
+ const { content, delegationId, friendId, deliveryHint, queue, routeToFriend, advanceObligation, completePonderPacket, fulfillHeartObligation, onRouteResult, } = input;
8
8
  // Resolve target friend
9
9
  let targetFriendId;
10
10
  let queueItem;
@@ -68,6 +68,7 @@ async function handleSurface(input) {
68
68
  ...(result.detail ? { detail: result.detail } : {}),
69
69
  },
70
70
  });
71
+ onRouteResult?.({ targetFriendId, queueItem, result });
71
72
  // On successful routing with delegationId:
72
73
  // 1. Advance obligation to "returned" (disk FIRST — crash safety)
73
74
  // 2. Dequeue from process-local queue (AFTER obligation advance)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.665",
3
+ "version": "0.1.0-alpha.667",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",