@ouro.bot/cli 0.1.0-alpha.724 → 0.1.0-alpha.725

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 (54) hide show
  1. package/changelog.json +12 -0
  2. package/dist/heart/active-work.js +10 -3
  3. package/dist/heart/core.js +343 -77
  4. package/dist/heart/daemon/cli-exec.js +72 -14
  5. package/dist/heart/daemon/cli-help.js +13 -2
  6. package/dist/heart/daemon/cli-parse.js +82 -1
  7. package/dist/heart/daemon/daemon.js +286 -33
  8. package/dist/heart/daemon/doctor.js +21 -6
  9. package/dist/heart/daemon/habit-private-wake.js +6 -1
  10. package/dist/heart/habits/habit-cancel.js +810 -0
  11. package/dist/heart/habits/habit-lifecycle.js +1327 -0
  12. package/dist/heart/habits/habit-migration.js +28 -23
  13. package/dist/heart/habits/habit-parser.js +72 -8
  14. package/dist/heart/habits/habit-runtime-state.js +3 -24
  15. package/dist/heart/habits/habit-scheduler.js +268 -50
  16. package/dist/heart/habits/habit-session.js +16 -0
  17. package/dist/heart/hatch/hatch-flow.js +6 -4
  18. package/dist/heart/mailbox/readers/runtime-readers.js +26 -27
  19. package/dist/heart/orientation-frame.js +58 -1
  20. package/dist/heart/provider-attempt.js +4 -1
  21. package/dist/heart/providers/anthropic.js +23 -2
  22. package/dist/heart/start-of-turn-packet.js +14 -11
  23. package/dist/heart/streaming.js +565 -185
  24. package/dist/mind/obligation-steering.js +15 -9
  25. package/dist/mind/prompt.js +58 -26
  26. package/dist/nerves/coverage/run-artifacts.js +55 -35
  27. package/dist/nerves/review/cli.js +2 -2
  28. package/dist/nerves/review/core.js +14 -7
  29. package/dist/repertoire/tools-base.js +2 -0
  30. package/dist/repertoire/tools-habits.js +103 -0
  31. package/dist/repertoire/tools.js +18 -3
  32. package/dist/rsvp/cli.js +126 -20
  33. package/dist/rsvp/diagnostics.js +12 -2
  34. package/dist/rsvp/habit-policy.js +7 -0
  35. package/dist/rsvp/habit-stage.js +19 -4
  36. package/dist/rsvp/native-habit-runner.js +151 -11
  37. package/dist/rsvp/outbound-state.js +360 -0
  38. package/dist/senses/bluebubbles/client.js +90 -17
  39. package/dist/senses/bluebubbles/inbound-log.js +1 -0
  40. package/dist/senses/bluebubbles/index.js +662 -425
  41. package/dist/senses/bluebubbles/model.js +57 -13
  42. package/dist/senses/bluebubbles/mutation-log.js +21 -10
  43. package/dist/senses/bluebubbles/reaction-policy.js +55 -0
  44. package/dist/senses/bluebubbles/semantic-receipts.js +1818 -0
  45. package/dist/senses/cli/tui-store.js +1 -0
  46. package/dist/senses/cli.js +1 -0
  47. package/dist/senses/habit-lifecycle-guard.js +24 -0
  48. package/dist/senses/ingress-evidence.js +2 -0
  49. package/dist/senses/pipeline.js +90 -24
  50. package/dist/senses/private-runtime-worker.js +21 -20
  51. package/dist/senses/private-runtime.js +79 -29
  52. package/dist/senses/shared-turn.js +1 -0
  53. package/dist/senses/teams.js +1 -0
  54. package/package.json +6 -1
@@ -224,6 +224,7 @@ function createTuiCallbacks(store) {
224
224
  meta: {},
225
225
  });
226
226
  return {
227
+ settleOutputMode: "retractable_buffer",
227
228
  onModelStart: () => { store.modelStart(); },
228
229
  onModelStreamStart: () => { },
229
230
  onClearText: () => { store.clearText(); },
@@ -311,6 +311,7 @@ function createCliCallbacks() {
311
311
  const streamer = new MarkdownStreamer();
312
312
  const wrapper = new cli_layout_1.StreamingWordWrapper();
313
313
  return {
314
+ settleOutputMode: "final_only",
314
315
  onModelStart: () => {
315
316
  currentSpinner?.stop();
316
317
  setSpinner(null);
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.privateRuntimeHabitRejectionReason = privateRuntimeHabitRejectionReason;
4
+ const runtime_1 = require("../nerves/runtime");
5
+ function privateRuntimeHabitRejectionReason(habit, boundary) {
6
+ if (habit.status === "active")
7
+ return null;
8
+ const reason = `habit status ${habit.status} is non-executable before private runtime execution`;
9
+ (0, runtime_1.emitNervesEvent)({
10
+ level: "warn",
11
+ component: "senses",
12
+ event: "senses.habit_lifecycle_rejected",
13
+ message: "habit lifecycle state rejected before private runtime execution",
14
+ meta: {
15
+ boundary,
16
+ habitName: habit.name,
17
+ status: habit.status,
18
+ degradedReason: habit.status === "degraded" ? habit.degradedReason : null,
19
+ detail: habit.status === "degraded" ? habit.degradedDetail : null,
20
+ reason,
21
+ },
22
+ });
23
+ return reason;
24
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -39,6 +39,7 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  })();
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
41
  exports.emitObligationTransitionEpisodes = emitObligationTransitionEpisodes;
42
+ exports.selectCheckpointCurrentAsk = selectCheckpointCurrentAsk;
42
43
  exports.handleInboundTurn = handleInboundTurn;
43
44
  const fs = __importStar(require("fs"));
44
45
  const path = __importStar(require("path"));
@@ -203,6 +204,16 @@ function latestUserAuthoredText(messages, continuityIngressTexts) {
203
204
  .filter(Boolean);
204
205
  return userMessages[userMessages.length - 1];
205
206
  }
207
+ function selectCheckpointCurrentAsk(input) {
208
+ const frameSpeech = input.orientationFrame?.currentUserSpeech
209
+ .map((speech) => speech.trim())
210
+ .filter(Boolean)
211
+ .at(-1) ?? null;
212
+ if (input.orientationFrame?.speechKind === "reaction")
213
+ return frameSpeech;
214
+ const currentUtterance = input.currentUserMessage?.trim();
215
+ return currentUtterance || frameSpeech;
216
+ }
206
217
  function sessionRefForFlightRecorder(session) {
207
218
  return `${session.friendId}/${session.channel}/${session.key}`;
208
219
  }
@@ -212,6 +223,22 @@ function isTerminalWithoutContinuation(outcome, hasActiveContinuation) {
212
223
  || outcome === "rested"
213
224
  || outcome === "superseded");
214
225
  }
226
+ function selectPostTurnContinuityState(input) {
227
+ const continuingState = {
228
+ ...(input.mustResolveBeforeHandoff ? { mustResolveBeforeHandoff: true } : {}),
229
+ ...(typeof input.lastFriendActivityAt === "string" ? { lastFriendActivityAt: input.lastFriendActivityAt } : {}),
230
+ };
231
+ const clearsMustResolveBeforeHandoff = input.outcome === "settled"
232
+ || input.outcome === "blocked"
233
+ || input.outcome === "superseded"
234
+ || input.outcome === "observed";
235
+ if (!clearsMustResolveBeforeHandoff) {
236
+ return Object.keys(continuingState).length > 0 ? continuingState : undefined;
237
+ }
238
+ return typeof input.lastFriendActivityAt === "string"
239
+ ? { lastFriendActivityAt: input.lastFriendActivityAt }
240
+ : undefined;
241
+ }
215
242
  function readPostTurnFlightRecorderArcSnapshot(agentRoot) {
216
243
  const latest = (0, flight_recorder_1.readFlightRecorderResume)(agentRoot);
217
244
  return {
@@ -223,7 +250,7 @@ function readPostTurnFlightRecorderArcSnapshot(agentRoot) {
223
250
  unverifiedClaimIds: latest.unverifiedClaimIds,
224
251
  };
225
252
  }
226
- function recordPostTurnFlightRecorderCheckpoint(input) {
253
+ function recordTurnFlightRecorderCheckpoint(input) {
227
254
  const hasActiveContinuation = input.mustResolveBeforeHandoff
228
255
  || input.activeObligationIds.length > 0
229
256
  || input.activeReturnObligationIds.length > 0
@@ -240,9 +267,11 @@ function recordPostTurnFlightRecorderCheckpoint(input) {
240
267
  ? "inspect the latest session and wait for new input before acting"
241
268
  : "continue the current held work and update Arc/Desk with the next checkpoint");
242
269
  (0, flight_recorder_1.recordFlightRecorderEvent)(input.agentRoot, {
243
- kind: "post_turn_persisted",
270
+ kind: input.sessionPersisted ? "post_turn_persisted" : "blocker_detected",
244
271
  sessionRef: sessionRefForFlightRecorder(input.currentSession),
245
- summary: `persisted ${input.currentSession.channel}/${input.currentSession.key} turn with outcome ${input.outcome}`,
272
+ summary: input.sessionPersisted
273
+ ? `persisted ${input.currentSession.channel}/${input.currentSession.key} turn with outcome ${input.outcome}`
274
+ : `runAgent threw before session persistence for ${input.currentSession.channel}/${input.currentSession.key}`,
246
275
  currentAsk: input.currentAsk,
247
276
  nextSafeAction,
248
277
  blockedBecause,
@@ -255,6 +284,7 @@ function recordPostTurnFlightRecorderCheckpoint(input) {
255
284
  unverifiedClaimIds: input.unverifiedClaimIds,
256
285
  meta: {
257
286
  outcome: input.outcome,
287
+ sessionPersisted: input.sessionPersisted,
258
288
  mustResolveBeforeHandoff: input.mustResolveBeforeHandoff,
259
289
  sessionPath: input.currentSession.sessionPath,
260
290
  },
@@ -447,6 +477,9 @@ function prepareInboundRunLedger(input) {
447
477
  }
448
478
  }
449
479
  async function handleInboundTurn(input) {
480
+ // Continuity resumption is structured caller intent. Never infer it from the
481
+ // current message or from similarity to prior obligations.
482
+ const resumePriorWork = input.runAgentOptions?.resumePriorWork === true;
450
483
  // Reset session-scoped state when the session changes
451
484
  const sessionKey = `${input.channel}/${input.sessionKey ?? "session"}`;
452
485
  if (sessionKey !== _lastSessionKey) {
@@ -826,7 +859,7 @@ async function handleInboundTurn(input) {
826
859
  if (capabilities) {
827
860
  startOfTurnPacket.capabilities = capabilities;
828
861
  }
829
- renderedStartOfTurnPacket = (0, start_of_turn_packet_1.renderStartOfTurnPacket)(startOfTurnPacket);
862
+ renderedStartOfTurnPacket = (0, start_of_turn_packet_1.renderStartOfTurnPacket)(startOfTurnPacket, { resumePriorWork });
830
863
  if (!renderedStartOfTurnPacket)
831
864
  renderedStartOfTurnPacket = undefined;
832
865
  // Update self-presence
@@ -854,6 +887,7 @@ async function handleInboundTurn(input) {
854
887
  ?? latestUserAuthoredText(input.messages, input.continuityIngressTexts);
855
888
  let runAgentOptions = {
856
889
  ...input.runAgentOptions,
890
+ resumePriorWork,
857
891
  ...(orientationFrame ? { orientationFrame } : {}),
858
892
  ...(liveLatencyMode ? { skipKeptNotes: true } : {}),
859
893
  bridgeContext,
@@ -892,6 +926,10 @@ async function handleInboundTurn(input) {
892
926
  resolvedContext,
893
927
  runAgentOptions,
894
928
  }));
929
+ const checkpointCurrentAsk = selectCheckpointCurrentAsk({
930
+ currentUserMessage: runAgentOptions.toolContext?.currentUserMessage ?? currentUserMessage,
931
+ orientationFrame: runAgentOptions.orientationFrame,
932
+ });
895
933
  const runLedger = prepareInboundRunLedger({
896
934
  channel: input.channel,
897
935
  senseType: input.capabilities.senseType,
@@ -920,6 +958,37 @@ async function handleInboundTurn(input) {
920
958
  usage: (0, run_ledger_1.usageMetadataFromUsageData)(undefined, "reported-unavailable"),
921
959
  errorName: error instanceof Error ? error.name : "NonErrorThrown",
922
960
  }));
961
+ try {
962
+ const agentRoot = (0, identity_1.getAgentRoot)();
963
+ const postTurnArc = readPostTurnFlightRecorderArcSnapshot(agentRoot);
964
+ recordTurnFlightRecorderCheckpoint({
965
+ agentRoot,
966
+ currentSession,
967
+ currentAsk: checkpointCurrentAsk,
968
+ nextSafeAction: activeWorkFrame.resumeHandle?.nextAction
969
+ ?? activeWorkFrame.primaryObligation?.nextAction?.trim()
970
+ ?? null,
971
+ outcome: "errored",
972
+ sessionPersisted: false,
973
+ mustResolveBeforeHandoff,
974
+ activeObligationIds: postTurnArc.activeObligationIds,
975
+ activeReturnObligationIds: postTurnArc.activeReturnObligationIds,
976
+ activePacketIds: postTurnArc.activePacketIds,
977
+ openEvolutionCaseIds: postTurnArc.openEvolutionCaseIds,
978
+ recentClaimIds: postTurnArc.recentClaimIds,
979
+ unverifiedClaimIds: postTurnArc.unverifiedClaimIds,
980
+ });
981
+ await refreshContextLossSentinelNonFatal({ agentName: (0, identity_1.getAgentName)(), agentRoot, trigger: "post_turn" });
982
+ }
983
+ catch (checkpointError) {
984
+ (0, runtime_1.emitNervesEvent)({
985
+ level: "warn",
986
+ component: "senses",
987
+ event: "senses.flight_recorder_checkpoint_error",
988
+ message: "failed to record thrown-turn flight recorder checkpoint",
989
+ meta: { error: checkpointError instanceof Error ? checkpointError.message : String(checkpointError) },
990
+ });
991
+ }
923
992
  throw error;
924
993
  }
925
994
  const runLedgerLifecycle = result.outcome === "errored" ? "error" : "completed";
@@ -946,19 +1015,22 @@ async function handleInboundTurn(input) {
946
1015
  /* v8 ignore next -- defensive: error always set when errored @preserve */
947
1016
  result.error?.message ?? "unknown error", classification, currentProvider, currentBinding.model, agentName, inventory, {}, { currentLane });
948
1017
  input.failoverState.pending = failoverContext;
949
- await input.postTurn(sessionMessages, session.sessionPath, result.usage);
1018
+ const failoverNextState = selectPostTurnContinuityState({
1019
+ outcome: "errored",
1020
+ mustResolveBeforeHandoff,
1021
+ lastFriendActivityAt,
1022
+ });
1023
+ await input.postTurn(sessionMessages, session.sessionPath, result.usage, undefined, failoverNextState);
950
1024
  try {
951
1025
  const agentRoot = (0, identity_1.getAgentRoot)();
952
1026
  const postTurnArc = readPostTurnFlightRecorderArcSnapshot(agentRoot);
953
- recordPostTurnFlightRecorderCheckpoint({
1027
+ recordTurnFlightRecorderCheckpoint({
954
1028
  agentRoot,
955
1029
  currentSession,
956
- currentAsk: currentObligation
957
- ?? activeWorkFrame.primaryObligation?.content?.trim()
958
- ?? currentUserMessage
959
- ?? null,
1030
+ currentAsk: checkpointCurrentAsk,
960
1031
  nextSafeAction: failoverContext.userMessage,
961
1032
  outcome: "errored",
1033
+ sessionPersisted: true,
962
1034
  mustResolveBeforeHandoff,
963
1035
  activeObligationIds: postTurnArc.activeObligationIds,
964
1036
  activeReturnObligationIds: postTurnArc.activeReturnObligationIds,
@@ -1012,30 +1084,24 @@ async function handleInboundTurn(input) {
1012
1084
  // Episode emission is non-fatal
1013
1085
  }
1014
1086
  // Step 6: postTurn
1015
- const continuingState = {
1016
- ...(mustResolveBeforeHandoff ? { mustResolveBeforeHandoff: true } : {}),
1017
- ...(typeof lastFriendActivityAt === "string" ? { lastFriendActivityAt } : {}),
1018
- };
1019
- const nextState = result.outcome === "settled" || result.outcome === "blocked" || result.outcome === "superseded" || result.outcome === "observed"
1020
- ? (typeof lastFriendActivityAt === "string"
1021
- ? { lastFriendActivityAt }
1022
- : undefined)
1023
- : (Object.keys(continuingState).length > 0 ? continuingState : undefined);
1087
+ const nextState = selectPostTurnContinuityState({
1088
+ outcome: result.outcome,
1089
+ mustResolveBeforeHandoff,
1090
+ lastFriendActivityAt,
1091
+ });
1024
1092
  await input.postTurn(sessionMessages, session.sessionPath, result.usage, undefined, nextState);
1025
1093
  try {
1026
1094
  const agentRoot = (0, identity_1.getAgentRoot)();
1027
1095
  const postTurnArc = readPostTurnFlightRecorderArcSnapshot(agentRoot);
1028
- recordPostTurnFlightRecorderCheckpoint({
1096
+ recordTurnFlightRecorderCheckpoint({
1029
1097
  agentRoot,
1030
1098
  currentSession,
1031
- currentAsk: currentObligation
1032
- ?? activeWorkFrame.primaryObligation?.content?.trim()
1033
- ?? currentUserMessage
1034
- ?? null,
1099
+ currentAsk: checkpointCurrentAsk,
1035
1100
  nextSafeAction: activeWorkFrame.resumeHandle?.nextAction
1036
1101
  ?? activeWorkFrame.primaryObligation?.nextAction?.trim()
1037
1102
  ?? null,
1038
1103
  outcome: result.outcome ?? "observed",
1104
+ sessionPersisted: true,
1039
1105
  mustResolveBeforeHandoff,
1040
1106
  activeObligationIds: postTurnArc.activeObligationIds,
1041
1107
  activeReturnObligationIds: postTurnArc.activeReturnObligationIds,
@@ -55,6 +55,7 @@ const habit_policy_1 = require("../rsvp/habit-policy");
55
55
  const spend_ledger_1 = require("../rsvp/spend-ledger");
56
56
  const run_ledger_1 = require("../heart/run-ledger");
57
57
  const autonomy_budget_1 = require("../heart/autonomy-budget");
58
+ const habit_lifecycle_guard_1 = require("./habit-lifecycle-guard");
58
59
  function isUsageData(value) {
59
60
  const usage = value;
60
61
  return !!usage
@@ -275,21 +276,6 @@ function deriveHabitSummarySnapshot(habitRun) {
275
276
  nextLikelyStep: null,
276
277
  };
277
278
  }
278
- function fallbackHabitFile(habitName) {
279
- return {
280
- name: habitName,
281
- title: habitName,
282
- cadence: null,
283
- status: "active",
284
- lastRun: null,
285
- created: null,
286
- tools: [],
287
- origin: null,
288
- surface: { family: false, originator: false, extra: [] },
289
- continuity: { mode: "fresh" },
290
- body: "",
291
- };
292
- }
293
279
  function readHabitForRun(agentRoot, habitName, errors) {
294
280
  const habitPath = path.join(agentRoot, "habits", `${habitName}.md`);
295
281
  try {
@@ -305,16 +291,20 @@ function readHabitForRun(agentRoot, habitName, errors) {
305
291
  message: "habit file could not be read for habit session",
306
292
  meta: { habitName, habitPath, reason },
307
293
  });
308
- return fallbackHabitFile(habitName);
294
+ return (0, habit_parser_1.createDegradedHabitFile)(habitPath, "read_error");
309
295
  }
310
296
  }
311
297
  async function prepareHabitRun(habitName, trigger, startedAt) {
312
298
  const agentRoot = (0, identity_1.getAgentRoot)();
313
299
  const errors = [];
314
300
  const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, readHabitForRun(agentRoot, habitName, errors));
301
+ if (habit.status === "degraded" && habit.degradedDetail !== null) {
302
+ errors.push(habit.degradedDetail);
303
+ }
304
+ const lifecycleBlockedReason = (0, habit_lifecycle_guard_1.privateRuntimeHabitRejectionReason)(habit, "private-runtime-worker");
315
305
  const blockedReason = (0, habit_policy_1.isRsvpHabitName)(habitName) && !habit.rsvp
316
306
  ? "RSVP habit metadata is required before private runtime execution"
317
- : null;
307
+ : lifecycleBlockedReason;
318
308
  if (blockedReason && !errors.includes(blockedReason))
319
309
  errors.push(blockedReason);
320
310
  const runId = (0, flight_recorder_1.createHabitRunId)(habitName, new Date(startedAt));
@@ -422,6 +412,11 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
422
412
  async function reuseHeartbeatOkRest(habitName) {
423
413
  const nowIso = new Date(nowSource()).toISOString();
424
414
  const habitRun = await prepareHabitRun(habitName, "overdue", nowIso);
415
+ if (habitRun.blockedReason) {
416
+ clearHeartbeatRestShield();
417
+ recordHabitCompletion(habitRun, nowIso);
418
+ return;
419
+ }
425
420
  habitRun.results.push({ turnOutcome: "rested", restStatus: "HEARTBEAT_OK" });
426
421
  recordHabitCompletion(habitRun, nowIso);
427
422
  (0, runtime_1.emitNervesEvent)({
@@ -475,9 +470,9 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
475
470
  });
476
471
  }
477
472
  }
478
- async function run(reason, taskId, habitName, awaitName, trigger, privateTurnDecision) {
473
+ async function run(reason, taskId, habitName, awaitName, trigger, privateTurnDecision, noSend) {
479
474
  if (running) {
480
- queue.push({ reason, taskId, habitName, awaitName, trigger, privateTurnDecision });
475
+ queue.push({ reason, taskId, habitName, awaitName, trigger, privateTurnDecision, noSend });
481
476
  return;
482
477
  }
483
478
  running = true;
@@ -488,12 +483,14 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
488
483
  let nextAwaitName = awaitName;
489
484
  let nextTrigger = trigger;
490
485
  let nextPrivateTurnDecision = privateTurnDecision;
486
+ let nextNoSend = reason === "habit" ? noSend : undefined;
491
487
  let nextHabitRun = null;
492
488
  let consecutiveInstinctTurns = reason === "instinct" ? 1 : 0;
493
489
  runLoop: do {
494
490
  const currentReason = nextReason;
495
491
  const currentHabitName = nextHabitName;
496
492
  const currentTrigger = nextTrigger ?? "overdue";
493
+ const currentNoSend = nextNoSend;
497
494
  const currentHabitRun = currentReason === "habit" && currentHabitName
498
495
  ? nextHabitRun && nextHabitRun.habit.name === currentHabitName
499
496
  ? nextHabitRun
@@ -545,6 +542,7 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
545
542
  taskId: nextTaskId,
546
543
  habitName: nextHabitName,
547
544
  awaitName: nextAwaitName,
545
+ ...(currentNoSend ? { noSend: true } : {}),
548
546
  ...(nextPrivateTurnDecision ? { privateTurnDecision: nextPrivateTurnDecision } : {}),
549
547
  ...(currentHabitRun
550
548
  ? {
@@ -614,6 +612,7 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
614
612
  nextAwaitName = next.awaitName;
615
613
  nextTrigger = next.trigger;
616
614
  nextPrivateTurnDecision = next.privateTurnDecision;
615
+ nextNoSend = next.reason === "habit" ? next.noSend : undefined;
617
616
  consecutiveInstinctTurns = nextReason === "instinct" ? consecutiveInstinctTurns + 1 : 0;
618
617
  continue runLoop;
619
618
  }
@@ -650,6 +649,7 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
650
649
  nextAwaitName = undefined;
651
650
  nextTrigger = currentTrigger;
652
651
  nextPrivateTurnDecision = undefined;
652
+ nextNoSend = currentNoSend;
653
653
  nextHabitRun = currentHabitRun;
654
654
  }
655
655
  else {
@@ -661,6 +661,7 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
661
661
  nextAwaitName = undefined;
662
662
  nextTrigger = undefined;
663
663
  nextPrivateTurnDecision = undefined;
664
+ nextNoSend = undefined;
664
665
  }
665
666
  continue;
666
667
  }
@@ -684,7 +685,7 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
684
685
  return;
685
686
  }
686
687
  recordHabitFireForRecursion(habitName);
687
- await run("habit", undefined, maybeMessage.habitName, undefined, maybeMessage.trigger ?? "overdue", maybeMessage.privateTurnDecision);
688
+ await run("habit", undefined, maybeMessage.habitName, undefined, maybeMessage.trigger ?? "overdue", maybeMessage.privateTurnDecision, maybeMessage.noSend);
688
689
  return;
689
690
  }
690
691
  if (maybeMessage.type === "await") {
@@ -76,6 +76,7 @@ const await_parser_1 = require("../heart/awaiting/await-parser");
76
76
  const await_runtime_state_1 = require("../heart/awaiting/await-runtime-state");
77
77
  const habit_parser_1 = require("../heart/habits/habit-parser");
78
78
  const habit_runtime_state_1 = require("../heart/habits/habit-runtime-state");
79
+ const habit_lifecycle_guard_1 = require("./habit-lifecycle-guard");
79
80
  const cadence_1 = require("../heart/daemon/cadence");
80
81
  const habit_policy_1 = require("../rsvp/habit-policy");
81
82
  const daemon_health_1 = require("../heart/daemon/daemon-health");
@@ -344,6 +345,14 @@ function assertPayloadBinding(decision, kind, id) {
344
345
  return;
345
346
  throw new Error(`private-runtime decision payload mismatch: missing ${kind} origin ref ${id}`);
346
347
  }
348
+ function assertNoSendBinding(decision, noSend) {
349
+ const refs = decision.originRefs.filter((ref) => ref.kind === "capability" && ref.id === "no-send");
350
+ if (noSend === true && refs.length === 1)
351
+ return;
352
+ if (noSend === undefined && refs.length === 0)
353
+ return;
354
+ throw new Error("private-runtime decision payload mismatch: no-send capability binding does not match the turn");
355
+ }
347
356
  function ledgerDecisionFor(decision) {
348
357
  const ledgerPath = decision.ledgerLocator?.path;
349
358
  if (!ledgerPath) {
@@ -389,6 +398,7 @@ function assertPrivateTurnDecisionAllowed(input) {
389
398
  assertPayloadBinding(decision, "task", options?.taskId);
390
399
  assertPayloadBinding(decision, "habit", options?.habitName);
391
400
  assertPayloadBinding(decision, "await", options?.awaitName);
401
+ assertNoSendBinding(decision, options?.noSend);
392
402
  const ledgerRow = ledgerDecisionFor(decision);
393
403
  const decidedAtMs = Date.parse(ledgerRow.decidedAt);
394
404
  if (!Number.isFinite(decidedAtMs)) {
@@ -489,6 +499,7 @@ function extractToolCallNames(messages) {
489
499
  }
490
500
  function createPrivateRuntimeCallbacks() {
491
501
  return {
502
+ settleOutputMode: "final_only",
492
503
  onModelStart: () => { },
493
504
  onModelStreamStart: () => { },
494
505
  onTextChunk: () => { },
@@ -795,6 +806,32 @@ function buildHabitSurfacePolicy(origin, surface) {
795
806
  lines.push("- use send_message for intentional contact; use surface only for a held return tied to an existing return obligation.");
796
807
  return lines.join("\n");
797
808
  }
809
+ function reduceHabitSessionToNoSend(habitSession) {
810
+ const deniedTools = new Set([
811
+ ...(habitSession?.permissionEnvelope.deniedTools ?? []),
812
+ ...(habitSession?.toolPolicy.grantedTools ?? []),
813
+ ...(habitSession?.toolPolicy.deniedTools ?? []),
814
+ "send_message",
815
+ "surface",
816
+ ]);
817
+ return {
818
+ ...habitSession,
819
+ noSend: true,
820
+ permissionEnvelope: {
821
+ schemaVersion: 1,
822
+ canMessageOutward: false,
823
+ returnRoutes: [],
824
+ deniedTools: [...deniedTools],
825
+ warnings: [...(habitSession?.permissionEnvelope.warnings ?? [])],
826
+ },
827
+ toolPolicy: {
828
+ requestedTools: habitSession?.toolPolicy.requestedTools ?? null,
829
+ grantedTools: [],
830
+ deniedTools: [...deniedTools],
831
+ outwardMessagingAllowed: false,
832
+ },
833
+ };
834
+ }
798
835
  async function runPrivateRuntimeTurn(options) {
799
836
  const now = options?.now ?? (() => new Date());
800
837
  const reason = options?.reason ?? "instinct";
@@ -873,37 +910,43 @@ async function runPrivateRuntimeTurn(options) {
873
910
  let habitSurface = { family: true, originator: true, extra: [] };
874
911
  if (preparedHabit) {
875
912
  if (rsvpHabit && !preparedHabit.rsvp) {
876
- throw new Error(`RSVP habit metadata is required before private runtime execution: ${habitName}`);
913
+ const detail = preparedHabit.status === "degraded"
914
+ ? preparedHabit.degradedDetail
915
+ : null;
916
+ throw new Error(`RSVP habit metadata is required before private runtime execution: ${detail ?? habitName}`);
877
917
  }
878
- habitBody = preparedHabit.body || undefined;
879
- habitTitle = preparedHabit.title || habitName;
880
- habitLastRun = preparedHabit.lastRun;
881
- habitTools = preparedHabit.tools;
882
- habitOrigin = preparedHabit.origin;
883
- habitSurface = preparedHabit.surface;
918
+ const blockedReason = (0, habit_lifecycle_guard_1.privateRuntimeHabitRejectionReason)(preparedHabit, "private-runtime");
919
+ if (blockedReason)
920
+ throw new Error(blockedReason);
884
921
  }
885
- else {
886
- try {
887
- const habitContent = fs.readFileSync(habitFilePath, "utf-8");
888
- const parsed = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(habitContent, habitFilePath));
889
- if (rsvpHabit && !parsed.rsvp) {
890
- throw new Error(`RSVP habit metadata is required before private runtime execution: ${habitName}`);
891
- }
892
- habitBody = parsed.body || undefined;
893
- habitTitle = parsed.title || habitName;
894
- habitLastRun = parsed.lastRun;
895
- habitTools = parsed.tools;
896
- habitOrigin = parsed.origin;
897
- habitSurface = parsed.surface;
898
- }
899
- catch (error) {
900
- if (rsvpHabit) {
901
- const reason = error instanceof Error ? error.message : String(error);
902
- throw new Error(`RSVP habit metadata is required before private runtime execution: ${reason}`);
903
- }
904
- // Habit file missing or unreadable
922
+ let currentHabit;
923
+ try {
924
+ const habitContent = fs.readFileSync(habitFilePath, "utf-8");
925
+ currentHabit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(habitContent, habitFilePath));
926
+ }
927
+ catch (error) {
928
+ const readReason = error instanceof Error ? error.message : String(error);
929
+ if (rsvpHabit) {
930
+ throw new Error(`RSVP habit metadata is required before private runtime execution: ${readReason}`);
905
931
  }
932
+ const degraded = (0, habit_parser_1.createDegradedHabitFile)(habitFilePath, "read_error", "", readReason);
933
+ const blockedReason = (0, habit_lifecycle_guard_1.privateRuntimeHabitRejectionReason)(degraded, "private-runtime");
934
+ throw new Error(blockedReason);
935
+ }
936
+ if (rsvpHabit && !currentHabit.rsvp) {
937
+ const detail = currentHabit.status === "degraded" ? currentHabit.degradedDetail : null;
938
+ throw new Error(`RSVP habit metadata is required before private runtime execution: ${detail ?? habitName}`);
906
939
  }
940
+ const currentBlockedReason = (0, habit_lifecycle_guard_1.privateRuntimeHabitRejectionReason)(currentHabit, "private-runtime");
941
+ if (currentBlockedReason)
942
+ throw new Error(currentBlockedReason);
943
+ const executableHabit = preparedHabit ?? currentHabit;
944
+ habitBody = executableHabit.body || undefined;
945
+ habitTitle = executableHabit.title || habitName;
946
+ habitLastRun = executableHabit.lastRun;
947
+ habitTools = executableHabit.tools;
948
+ habitOrigin = executableHabit.origin;
949
+ habitSurface = executableHabit.surface;
907
950
  // If the habit file couldn't be read at all (no body, no title parsed), error message
908
951
  if (habitBody === undefined && habitTitle === habitName) {
909
952
  userContent = `habit "${habitName}" could not be read (file not found or unreadable). check habits/${habitName}.md exists.`;
@@ -1027,6 +1070,12 @@ async function runPrivateRuntimeTurn(options) {
1027
1070
  meta: { habitName: options.habitName },
1028
1071
  });
1029
1072
  }
1073
+ if (options?.noSend === true) {
1074
+ habitToolsResolved = [];
1075
+ }
1076
+ const effectiveHabitSession = options?.noSend === true
1077
+ ? reduceHabitSessionToNoSend(options.habitSession)
1078
+ : options?.habitSession;
1030
1079
  const sessionLoader = {
1031
1080
  loadOrCreate: async () => {
1032
1081
  if (existingMessages.length > 0) {
@@ -1111,9 +1160,10 @@ async function runPrivateRuntimeTurn(options) {
1111
1160
  toolContext: {
1112
1161
  signin: async () => undefined,
1113
1162
  delegatedOrigins: attentionQueue,
1114
- ...(options?.habitSession ? { habitSession: options.habitSession } : {}),
1163
+ ...(options?.noSend ? { noSend: true } : {}),
1164
+ ...(effectiveHabitSession ? { habitSession: effectiveHabitSession } : {}),
1115
1165
  },
1116
- ...(options?.habitSession ? { habitSession: options.habitSession } : {}),
1166
+ ...(effectiveHabitSession ? { habitSession: effectiveHabitSession } : {}),
1117
1167
  },
1118
1168
  });
1119
1169
  // Post-turn routeDelegatedCompletion removed: delivery is now inline via surface tool.
@@ -288,6 +288,7 @@ async function runSenseTurn(options) {
288
288
  };
289
289
  /* v8 ignore start — no-op callback stubs; only onTextChunk does real work (covered via mock) */
290
290
  const callbacks = {
291
+ settleOutputMode: "retractable_buffer",
291
292
  onModelStart: () => { },
292
293
  onModelStreamStart: () => { },
293
294
  onTextChunk: (chunk) => { pendingResponseText += chunk; },
@@ -409,6 +409,7 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
409
409
  }
410
410
  }
411
411
  return {
412
+ settleOutputMode: "final_only",
412
413
  onModelStart: () => {
413
414
  if (hadRealOutput)
414
415
  return; // real output already shown; don't overwrite with phrases
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.724",
3
+ "version": "0.1.0-alpha.725",
4
+ "engines": {
5
+ "node": ">=22"
6
+ },
4
7
  "main": "dist/heart/daemon/ouro-entry.js",
5
8
  "bin": {
6
9
  "cli": "dist/heart/daemon/ouro-bot-entry.js",
@@ -61,6 +64,7 @@
61
64
  "@ouro.bot/friends": "0.1.0-alpha.7",
62
65
  "@types/react": "^17.0.91",
63
66
  "@types/ws": "^8.18.1",
67
+ "better-sqlite3": "13.0.2",
64
68
  "fast-glob": "^3.3.3",
65
69
  "ink": "^3.2.0",
66
70
  "mailparser": "^3.9.8",
@@ -77,6 +81,7 @@
77
81
  },
78
82
  "devDependencies": {
79
83
  "@testing-library/react": "^16.3.2",
84
+ "@types/better-sqlite3": "7.6.13",
80
85
  "@types/mailparser": "^3.4.6",
81
86
  "@types/semver": "^7.7.1",
82
87
  "@types/smtp-server": "^3.5.13",