@ouro.bot/cli 0.1.0-alpha.723 → 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 (55) hide show
  1. package/changelog.json +295 -275
  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 +132 -15
  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 +82 -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/mail.js +129 -0
  50. package/dist/senses/pipeline.js +90 -24
  51. package/dist/senses/private-runtime-worker.js +21 -20
  52. package/dist/senses/private-runtime.js +79 -29
  53. package/dist/senses/shared-turn.js +1 -0
  54. package/dist/senses/teams.js +1 -0
  55. 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 });
@@ -50,6 +50,125 @@ const smtp_ingress_1 = require("../mailroom/smtp-ingress");
50
50
  function readRegistry(registryPath) {
51
51
  return JSON.parse(fs.readFileSync(registryPath, "utf-8"));
52
52
  }
53
+ function isRecord(value) {
54
+ return !!value && typeof value === "object" && !Array.isArray(value);
55
+ }
56
+ function unverifiableCoverage(checkedAt, reason) {
57
+ return { status: "could-not-verify", declaredKeyIds: [], absentKeyIds: [], reason, checkedAt };
58
+ }
59
+ /**
60
+ * Key ids the registry says mail can currently be encrypted to for this agent.
61
+ *
62
+ * Filtered to this agent because the hosted registry is domain-wide: counting
63
+ * every agent's records would report another agent's keys as absent on every
64
+ * cycle, and a check that always fails is a check operators learn to ignore.
65
+ *
66
+ * The match is case-insensitive. The harness sends `agentId: agent` verbatim to
67
+ * hosted Mail Control while this repo's own hosted ensure-response fixture
68
+ * returns the lowercased form for that same request, so a registry record's
69
+ * agent id can differ from the sense's agent name by case alone. Case-sensitive
70
+ * matching would then declare nothing and report `could-not-verify` forever —
71
+ * the same silence this probe exists to end. Widening the match cannot produce
72
+ * a false `absent` unless two agents differ only by case, which the store and
73
+ * address resolvers could not tell apart either.
74
+ *
75
+ * Disabled source grants are excluded because `resolveMailAddress`
76
+ * (`mailroom/core.ts`) refuses their alias outright, so no new mail can be
77
+ * encrypted to their key and a missing private half is not a live gap.
78
+ */
79
+ function registryDeclaredKeyIds(registry, agentName) {
80
+ const wanted = agentName.toLowerCase();
81
+ const isThisAgent = (agentId) => agentId.toLowerCase() === wanted;
82
+ const declared = [
83
+ ...registry.mailboxes.filter((mailbox) => isThisAgent(mailbox.agentId)).map((mailbox) => mailbox.keyId),
84
+ ...registry.sourceGrants.filter((grant) => isThisAgent(grant.agentId) && grant.enabled).map((grant) => grant.keyId),
85
+ ];
86
+ return [...new Set(declared.filter((keyId) => keyId.trim().length > 0))];
87
+ }
88
+ /**
89
+ * Mail key ids the vault can actually decrypt with, read fresh rather than
90
+ * taken from the reader resolution captured at sense startup — a sense that has
91
+ * been up for days would otherwise measure a key set the vault has since
92
+ * changed, and report a rotation as a loss.
93
+ *
94
+ * Throws rather than returning empty when the read is untrustworthy, so the
95
+ * caller classifies it `could-not-verify` instead of `absent`. An empty-but-
96
+ * present `privateKeys` record is a real observation and is not a throw.
97
+ */
98
+ function vaultMailKeyIds(agentName) {
99
+ const runtime = (0, runtime_credentials_1.readRuntimeCredentialConfig)(agentName);
100
+ if (!runtime.ok)
101
+ throw new Error(`vault ${runtime.itemPath} read is ${runtime.reason}`);
102
+ const mailroom = runtime.config.mailroom;
103
+ const privateKeys = isRecord(mailroom) ? mailroom.privateKeys : undefined;
104
+ if (!isRecord(privateKeys))
105
+ throw new Error(`vault ${runtime.itemPath} has no mailroom.privateKeys record`);
106
+ return Object.entries(privateKeys)
107
+ .filter((entry) => typeof entry[1] === "string" && entry[1].trim().length > 0)
108
+ .map((entry) => entry[0]);
109
+ }
110
+ /**
111
+ * Never throws: every failure degrades to `could-not-verify`. `scan()` is
112
+ * awaited during sense startup, so a throw here would stop the mail sense from
113
+ * starting — an integrity probe must not be able to take mail down.
114
+ */
115
+ async function computeMailKeyCoverage(input) {
116
+ try {
117
+ const registry = await input.readRegistry(input.config);
118
+ const declaredKeyIds = registryDeclaredKeyIds(registry, input.agentName);
119
+ if (declaredKeyIds.length === 0) {
120
+ // Nothing to check against is not the same as nothing wrong: reporting
121
+ // `covered` for an empty declaration would turn an unprovisioned or
122
+ // wrong registry into a green light.
123
+ return unverifiableCoverage(input.checkedAt, `mailroom registry declares no mail keys for ${input.agentName}`);
124
+ }
125
+ const present = new Set(vaultMailKeyIds(input.agentName));
126
+ const absentKeyIds = declaredKeyIds.filter((keyId) => !present.has(keyId));
127
+ return {
128
+ status: absentKeyIds.length === 0 ? "covered" : "absent",
129
+ declaredKeyIds,
130
+ absentKeyIds,
131
+ reason: null,
132
+ checkedAt: input.checkedAt,
133
+ };
134
+ }
135
+ catch (error) {
136
+ return unverifiableCoverage(input.checkedAt, error instanceof Error ? error.message : String(error));
137
+ }
138
+ }
139
+ /**
140
+ * Coverage has to push, not wait to be asked. `ouro doctor` is on-demand — it
141
+ * has no scheduled invocation — so a doctor line alone leaves a gap invisible
142
+ * until someone happens to run it, which is how `mail_slugger-hey_6e7b4bfa34c0b826`
143
+ * stayed unnoticed for 23 hours while mail encrypted to it piled up unreadable.
144
+ */
145
+ function emitMailKeyCoverageEvents(agentName, coverage) {
146
+ (0, runtime_1.emitNervesEvent)({
147
+ component: "senses",
148
+ event: "senses.mail_key_coverage_recorded",
149
+ message: "mail key coverage recorded",
150
+ meta: {
151
+ agentName,
152
+ status: coverage.status,
153
+ declaredCount: coverage.declaredKeyIds.length,
154
+ absentCount: coverage.absentKeyIds.length,
155
+ reason: coverage.reason,
156
+ },
157
+ });
158
+ if (coverage.status !== "absent")
159
+ return;
160
+ (0, runtime_1.emitNervesEvent)({
161
+ level: "error",
162
+ component: "senses",
163
+ event: "senses.mail_key_coverage_absent",
164
+ message: `mailroom registry declares mail keys with no private half in ${agentName}'s vault: ${coverage.absentKeyIds.join(", ")}`,
165
+ meta: {
166
+ agentName,
167
+ absentKeyIds: coverage.absentKeyIds,
168
+ declaredKeyIds: coverage.declaredKeyIds,
169
+ },
170
+ });
171
+ }
53
172
  function validPort(value) {
54
173
  return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 65535 ? value : 0;
55
174
  }
@@ -280,6 +399,7 @@ async function startMailSenseApp(options) {
280
399
  const importDiscoveryPath = importDiscoveryStatePath(options.agentName);
281
400
  let lastScanAt = null;
282
401
  let lastQueuedCount = 0;
402
+ let lastKeyCoverage = unverifiableCoverage(new Date(now()).toISOString(), "mail key coverage not probed yet");
283
403
  const scan = async () => {
284
404
  const scanStartedAt = new Date(now()).toISOString();
285
405
  let queuedCount = 0;
@@ -322,6 +442,13 @@ async function startMailSenseApp(options) {
322
442
  meta: { agentName: options.agentName, error: error instanceof Error ? error.message : String(error) },
323
443
  });
324
444
  }
445
+ lastKeyCoverage = await computeMailKeyCoverage({
446
+ agentName: options.agentName,
447
+ config: resolved.config,
448
+ readRegistry: options.readRegistry ?? reader_1.readMailroomRegistry,
449
+ checkedAt: scanStartedAt,
450
+ });
451
+ emitMailKeyCoverageEvents(options.agentName, lastKeyCoverage);
325
452
  lastScanAt = scanStartedAt;
326
453
  lastQueuedCount = queuedCount;
327
454
  writeRuntimeState(runtimePath, {
@@ -336,6 +463,7 @@ async function startMailSenseApp(options) {
336
463
  storeLabel: resolved.storeLabel,
337
464
  lastScanAt,
338
465
  lastQueuedCount,
466
+ keyCoverage: lastKeyCoverage,
339
467
  updatedAt: new Date(now()).toISOString(),
340
468
  });
341
469
  };
@@ -382,6 +510,7 @@ async function startMailSenseApp(options) {
382
510
  storeLabel: resolved.storeLabel,
383
511
  lastScanAt,
384
512
  lastQueuedCount,
513
+ keyCoverage: lastKeyCoverage,
385
514
  updatedAt: new Date(now()).toISOString(),
386
515
  });
387
516
  (0, runtime_1.emitNervesEvent)({
@@ -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") {