@ouro.bot/cli 0.1.0-alpha.696 → 0.1.0-alpha.698

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 (46) hide show
  1. package/changelog.json +18 -0
  2. package/dist/heart/agent-entry.js +32 -2
  3. package/dist/heart/autonomy-budget.js +434 -0
  4. package/dist/heart/core.js +11 -0
  5. package/dist/heart/daemon/cadence.js +110 -0
  6. package/dist/heart/daemon/cli-exec.js +20 -0
  7. package/dist/heart/daemon/cli-help.js +74 -0
  8. package/dist/heart/daemon/cli-parse.js +466 -0
  9. package/dist/heart/daemon/daemon-entry.js +104 -3
  10. package/dist/heart/daemon/daemon.js +24 -3
  11. package/dist/heart/daemon/doctor.js +151 -0
  12. package/dist/heart/daemon/stale-bundle-prune.js +23 -6
  13. package/dist/heart/habits/habit-parser.js +4 -1
  14. package/dist/heart/habits/habit-scheduler.js +46 -38
  15. package/dist/heart/habits/habit-session.js +2 -3
  16. package/dist/heart/provider-credentials.js +16 -0
  17. package/dist/heart/run-ledger.js +247 -0
  18. package/dist/heart/runtime-credentials.js +12 -3
  19. package/dist/mind/prompt-budget.js +380 -0
  20. package/dist/repertoire/tools-base.js +2 -0
  21. package/dist/repertoire/tools-rsvp.js +139 -0
  22. package/dist/rsvp/aisleplanner-client.js +354 -0
  23. package/dist/rsvp/cli.js +704 -0
  24. package/dist/rsvp/config.js +327 -0
  25. package/dist/rsvp/cutover.js +431 -0
  26. package/dist/rsvp/diagnostics.js +222 -0
  27. package/dist/rsvp/diff-renderer.js +161 -0
  28. package/dist/rsvp/habit-policy.js +85 -0
  29. package/dist/rsvp/habit-stage.js +93 -0
  30. package/dist/rsvp/incident-bundle.js +97 -0
  31. package/dist/rsvp/migration.js +204 -0
  32. package/dist/rsvp/outbound-state.js +254 -0
  33. package/dist/rsvp/query.js +118 -0
  34. package/dist/rsvp/replay.js +233 -0
  35. package/dist/rsvp/snapshot.js +232 -0
  36. package/dist/senses/bluebubbles/client.js +13 -7
  37. package/dist/senses/bluebubbles/index.js +227 -22
  38. package/dist/senses/bluebubbles/outbound-state.js +304 -0
  39. package/dist/senses/bluebubbles/replay.js +147 -0
  40. package/dist/senses/context-packet-ledger.js +245 -0
  41. package/dist/senses/context-packets.js +154 -0
  42. package/dist/senses/pipeline.js +91 -1
  43. package/dist/senses/private-runtime-worker.js +170 -31
  44. package/dist/senses/private-runtime.js +13 -1
  45. package/dist/senses/voice/twilio-phone.js +9 -5
  46. package/package.json +1 -1
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RSVP_SNAPSHOT_POLICY_VERSION = void 0;
4
+ exports.rsvpHash = rsvpHash;
5
+ exports.rsvpSnapshotContentHash = rsvpSnapshotContentHash;
6
+ exports.buildRsvpSnapshot = buildRsvpSnapshot;
7
+ exports.parseRsvpSnapshot = parseRsvpSnapshot;
8
+ exports.serializeRsvpSnapshotMetadata = serializeRsvpSnapshotMetadata;
9
+ const node_crypto_1 = require("node:crypto");
10
+ const runtime_1 = require("../nerves/runtime");
11
+ exports.RSVP_SNAPSHOT_POLICY_VERSION = "rsvp-snapshot/v1";
12
+ function sha256Hex(input) {
13
+ return (0, node_crypto_1.createHash)("sha256").update(input).digest("hex");
14
+ }
15
+ function rsvpHash(input) {
16
+ return `sha256:${sha256Hex(canonicalJson(input))}`;
17
+ }
18
+ function canonicalJson(value) {
19
+ if (Array.isArray(value))
20
+ return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
21
+ if (value && typeof value === "object") {
22
+ const record = value;
23
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
24
+ }
25
+ return JSON.stringify(value);
26
+ }
27
+ function assertIsoTimestamp(value, label) {
28
+ if (!Number.isFinite(Date.parse(value))) {
29
+ throw new Error(`${label} must be an ISO timestamp`);
30
+ }
31
+ }
32
+ function stringValue(value) {
33
+ return typeof value === "string" ? value : "";
34
+ }
35
+ function groupIdValue(value) {
36
+ if (typeof value === "string" || typeof value === "number")
37
+ return value;
38
+ return null;
39
+ }
40
+ function normalizeStatus(value) {
41
+ if (value === null || value === undefined)
42
+ return { status: "pending", sourceStatus: null };
43
+ const raw = String(value);
44
+ if (raw === "attending")
45
+ return { status: "attending", sourceStatus: raw };
46
+ if (raw === "declined")
47
+ return { status: "declined", sourceStatus: raw };
48
+ return { status: "unknown", sourceStatus: raw };
49
+ }
50
+ function displayName(firstName, lastName) {
51
+ const name = `${firstName} ${lastName}`.trim();
52
+ return name || "Unnamed guest";
53
+ }
54
+ function normalizeGuest(id, row) {
55
+ const firstName = stringValue(row.first_name).trim();
56
+ const lastName = stringValue(row.last_name).trim();
57
+ const normalizedStatus = normalizeStatus(row.attending_status);
58
+ return {
59
+ id,
60
+ firstName,
61
+ lastName,
62
+ displayName: displayName(firstName, lastName),
63
+ groupId: groupIdValue(row.group_id),
64
+ status: normalizedStatus.status,
65
+ sourceStatus: normalizedStatus.sourceStatus,
66
+ };
67
+ }
68
+ function summaryFor(guests) {
69
+ const summary = {
70
+ attending: 0,
71
+ declined: 0,
72
+ pending: 0,
73
+ unknown: 0,
74
+ total: guests.length,
75
+ };
76
+ for (const guest of guests) {
77
+ summary[guest.status] += 1;
78
+ }
79
+ return summary;
80
+ }
81
+ function hashBasis(snapshot) {
82
+ return {
83
+ schemaVersion: snapshot.schemaVersion,
84
+ policyVersion: snapshot.policyVersion,
85
+ agent: snapshot.agent,
86
+ fetchedAt: snapshot.fetchedAt,
87
+ source: snapshot.source,
88
+ provenance: snapshot.provenance,
89
+ guests: snapshot.guests,
90
+ allGuestIdsHash: snapshot.allGuestIdsHash,
91
+ summary: snapshot.summary,
92
+ privacy: snapshot.privacy,
93
+ };
94
+ }
95
+ function rsvpSnapshotContentHash(snapshot) {
96
+ return rsvpHash(hashBasis(snapshot));
97
+ }
98
+ function snapshotIdFor(contentHash) {
99
+ return `rsvp_${contentHash.slice("sha256:".length, "sha256:".length + 32)}`;
100
+ }
101
+ function allGuestIdsHash(allGuests) {
102
+ return rsvpHash(Object.keys(allGuests).sort());
103
+ }
104
+ function buildRsvpSnapshot(input) {
105
+ assertIsoTimestamp(input.fetchedAt, "fetchedAt");
106
+ if (input.provenance.kind === "legacy-import")
107
+ assertIsoTimestamp(input.provenance.importedAt, "importedAt");
108
+ const guests = Object.entries(input.guests)
109
+ .map(([id, row]) => normalizeGuest(id, row))
110
+ .sort((left, right) => left.id.localeCompare(right.id));
111
+ const base = {
112
+ schemaVersion: 1,
113
+ policyVersion: exports.RSVP_SNAPSHOT_POLICY_VERSION,
114
+ agent: input.agent,
115
+ fetchedAt: input.fetchedAt,
116
+ source: input.source,
117
+ provenance: input.provenance,
118
+ guests,
119
+ allGuestIdsHash: allGuestIdsHash(input.allGuests),
120
+ summary: summaryFor(guests),
121
+ privacy: {
122
+ rawCredentialsStored: false,
123
+ indexPolicy: { search: false, vector: false },
124
+ },
125
+ };
126
+ const contentHash = rsvpHash(hashBasis(base));
127
+ const snapshot = {
128
+ ...base,
129
+ snapshotId: snapshotIdFor(contentHash),
130
+ contentHash,
131
+ };
132
+ (0, runtime_1.emitNervesEvent)({
133
+ component: "rsvp",
134
+ event: "rsvp.snapshot_built",
135
+ message: "built RSVP snapshot",
136
+ meta: {
137
+ agent: snapshot.agent,
138
+ snapshotId: snapshot.snapshotId,
139
+ total: snapshot.summary.total,
140
+ },
141
+ });
142
+ return snapshot;
143
+ }
144
+ function isRecord(value) {
145
+ return !!value && typeof value === "object" && !Array.isArray(value);
146
+ }
147
+ function parseGuest(value) {
148
+ if (!isRecord(value))
149
+ return null;
150
+ if (typeof value.id !== "string")
151
+ return null;
152
+ if (typeof value.firstName !== "string")
153
+ return null;
154
+ if (typeof value.lastName !== "string")
155
+ return null;
156
+ if (typeof value.displayName !== "string")
157
+ return null;
158
+ if (!(typeof value.groupId === "string" || typeof value.groupId === "number" || value.groupId === null))
159
+ return null;
160
+ if (value.status !== "attending" && value.status !== "declined" && value.status !== "pending" && value.status !== "unknown")
161
+ return null;
162
+ if (!(typeof value.sourceStatus === "string" || value.sourceStatus === null))
163
+ return null;
164
+ return value;
165
+ }
166
+ function parseSummary(value) {
167
+ if (!isRecord(value))
168
+ return null;
169
+ const summary = value;
170
+ return typeof summary.attending === "number"
171
+ && typeof summary.declined === "number"
172
+ && typeof summary.pending === "number"
173
+ && typeof summary.unknown === "number"
174
+ && typeof summary.total === "number"
175
+ ? summary
176
+ : null;
177
+ }
178
+ function parseRsvpSnapshot(value) {
179
+ if (!isRecord(value))
180
+ return { ok: false, reason: "snapshot must be an object" };
181
+ if (value.schemaVersion !== 1)
182
+ return { ok: false, reason: "unsupported schemaVersion" };
183
+ if (value.policyVersion !== exports.RSVP_SNAPSHOT_POLICY_VERSION)
184
+ return { ok: false, reason: "unsupported policyVersion" };
185
+ if (typeof value.snapshotId !== "string")
186
+ return { ok: false, reason: "missing snapshotId" };
187
+ if (typeof value.agent !== "string")
188
+ return { ok: false, reason: "missing agent" };
189
+ if (typeof value.fetchedAt !== "string" || !Number.isFinite(Date.parse(value.fetchedAt)))
190
+ return { ok: false, reason: "invalid fetchedAt" };
191
+ if (!isRecord(value.source))
192
+ return { ok: false, reason: "invalid source" };
193
+ if (!isRecord(value.provenance))
194
+ return { ok: false, reason: "invalid provenance" };
195
+ if (!Array.isArray(value.guests))
196
+ return { ok: false, reason: "guests must be an array" };
197
+ const guests = value.guests.map(parseGuest);
198
+ if (guests.some((guest) => guest === null))
199
+ return { ok: false, reason: "invalid guest row" };
200
+ const ids = guests.map((guest) => guest.id);
201
+ if (new Set(ids).size !== ids.length)
202
+ return { ok: false, reason: "duplicate guest id" };
203
+ if (typeof value.allGuestIdsHash !== "string")
204
+ return { ok: false, reason: "missing allGuestIdsHash" };
205
+ const summary = parseSummary(value.summary);
206
+ if (!summary)
207
+ return { ok: false, reason: "invalid summary" };
208
+ if (!isRecord(value.privacy) || value.privacy.rawCredentialsStored !== false)
209
+ return { ok: false, reason: "invalid privacy" };
210
+ const snapshot = value;
211
+ if (typeof value.contentHash !== "string" || rsvpSnapshotContentHash(snapshot) !== value.contentHash) {
212
+ return { ok: false, reason: "contentHash mismatch" };
213
+ }
214
+ if (snapshotIdFor(value.contentHash) !== value.snapshotId)
215
+ return { ok: false, reason: "snapshotId mismatch" };
216
+ return { ok: true, snapshot };
217
+ }
218
+ function serializeRsvpSnapshotMetadata(snapshot) {
219
+ return {
220
+ schemaVersion: 1,
221
+ policyVersion: exports.RSVP_SNAPSHOT_POLICY_VERSION,
222
+ snapshotId: snapshot.snapshotId,
223
+ contentHash: snapshot.contentHash,
224
+ agent: snapshot.agent,
225
+ fetchedAt: snapshot.fetchedAt,
226
+ source: snapshot.source,
227
+ summary: snapshot.summary,
228
+ guestIdsHash: rsvpHash(snapshot.guests.map((guest) => guest.id).sort()),
229
+ rawGuestNamesStored: false,
230
+ indexPolicy: { search: false, vector: false },
231
+ };
232
+ }
@@ -273,7 +273,7 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
273
273
  const url = buildBlueBubblesApiUrl(config.serverUrl, "/api/v1/message/text", config.password);
274
274
  const body = {
275
275
  chatGuid: resolvedChatGuid,
276
- tempGuid: (0, node_crypto_1.randomUUID)(),
276
+ tempGuid: params.tempGuid?.trim() || (0, node_crypto_1.randomUUID)(),
277
277
  message: trimmedText,
278
278
  };
279
279
  if (params.replyToMessageGuid?.trim()) {
@@ -425,15 +425,21 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
425
425
  message: "querying recent bluebubbles messages",
426
426
  meta: { limit, offset },
427
427
  });
428
+ const body = {
429
+ limit,
430
+ offset,
431
+ sort: "DESC",
432
+ ...(params.chatGuid?.trim() ? { chatGuid: params.chatGuid.trim() } : {}),
433
+ ...(params.chatIdentifier?.trim() ? { chatIdentifier: params.chatIdentifier.trim() } : {}),
434
+ ...(typeof params.beforeTimestamp === "number" && Number.isFinite(params.beforeTimestamp)
435
+ ? { beforeTimestamp: params.beforeTimestamp }
436
+ : {}),
437
+ with: ["chats", "attachments", "payloadData", "messageSummaryInfo"],
438
+ };
428
439
  const response = await fetch(url, {
429
440
  method: "POST",
430
441
  headers: { "Content-Type": "application/json" },
431
- body: JSON.stringify({
432
- limit,
433
- offset,
434
- sort: "DESC",
435
- with: ["chats", "attachments", "payloadData", "messageSummaryInfo"],
436
- }),
442
+ body: JSON.stringify(body),
437
443
  signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
438
444
  });
439
445
  if (!response.ok) {
@@ -59,6 +59,7 @@ exports.startBlueBubblesApp = startBlueBubblesApp;
59
59
  const fs = __importStar(require("node:fs"));
60
60
  const http = __importStar(require("node:http"));
61
61
  const path = __importStar(require("node:path"));
62
+ const node_crypto_1 = require("node:crypto");
62
63
  const core_1 = require("../../heart/core");
63
64
  const config_1 = require("../../heart/config");
64
65
  const identity_1 = require("../../heart/identity");
@@ -83,6 +84,9 @@ const session_cleanup_1 = require("./session-cleanup");
83
84
  const active_turns_1 = require("./active-turns");
84
85
  const trust_gate_1 = require("../trust-gate");
85
86
  const pipeline_1 = require("../pipeline");
87
+ const context_packets_1 = require("../context-packets");
88
+ const context_packet_ledger_1 = require("../context-packet-ledger");
89
+ const autonomy_budget_1 = require("../../heart/autonomy-budget");
86
90
  const bbFailoverStates = new Map();
87
91
  /**
88
92
  * In-flight message tracker.
@@ -259,6 +263,8 @@ const BLUEBUBBLES_LIVE_TURN_STALLED_MS = 90_000;
259
263
  const BLUEBUBBLES_SILENCE_WATCHDOG_MS = 75_000;
260
264
  const BLUEBUBBLES_CALLBACK_CLEANUP_TIMEOUT_MS = 2_000;
261
265
  const BLUEBUBBLES_ACTIVITY_OPERATION_TIMEOUT_MS = 20_000;
266
+ const BLUEBUBBLES_CONTEXT_PACKET_LIMIT = 40;
267
+ const BLUEBUBBLES_CONTEXT_PACKET_MAX_AGE_MS = 48 * 60 * 60 * 1000;
262
268
  const BLUEBUBBLES_CATCHUP_PAGE_SIZE = 50;
263
269
  const BLUEBUBBLES_CATCHUP_MAX_PAGES = 20;
264
270
  const BLUEBUBBLES_HEALTHY_CATCHUP_OVERLAP_MS = 90_000;
@@ -558,6 +564,174 @@ function getBlueBubblesContinuityIngressTexts(event) {
558
564
  .join("\n");
559
565
  return fallbackText ? [fallbackText] : [];
560
566
  }
567
+ function sha256Hex(input) {
568
+ return (0, node_crypto_1.createHash)("sha256").update(input).digest("hex");
569
+ }
570
+ function timestampIsoForAutonomy(event) {
571
+ return Number.isFinite(event.timestamp)
572
+ ? new Date(event.timestamp).toISOString()
573
+ : new Date().toISOString();
574
+ }
575
+ function blueBubblesAutonomyTarget(event) {
576
+ return {
577
+ messageGuid: event.messageGuid,
578
+ sessionKeyHash: sha256Hex(event.chat.sessionKey),
579
+ chatGuidHash: event.chat.chatGuid ? sha256Hex(event.chat.chatGuid) : undefined,
580
+ chatIdentifierHash: event.chat.chatIdentifier ? sha256Hex(event.chat.chatIdentifier) : undefined,
581
+ };
582
+ }
583
+ function blueBubblesRecoveryStormInput(agentName, event, source) {
584
+ return {
585
+ agent: agentName,
586
+ triggerType: "recovery",
587
+ sourceKind: "sense",
588
+ senseOrHabit: "bluebubbles",
589
+ provider: "bluebubbles-recovery",
590
+ target: blueBubblesAutonomyTarget(event),
591
+ normalizedErrorName: "BlueBubblesRecoveryError",
592
+ normalizedErrorCode: "RECOVERY_FAILED",
593
+ codeLocation: `senses/bluebubbles/${source}`,
594
+ idempotencyBucket: `bluebubbles-recovery:${source}`,
595
+ };
596
+ }
597
+ function shouldUseBlueBubblesRecoveryBudget(source, options) {
598
+ return options.autonomyBudgetTrigger === "recovery"
599
+ || source === "mutation-recovery"
600
+ || source === "upstream-catchup"
601
+ || source === "recovery-bootstrap";
602
+ }
603
+ function shouldCountBlueBubblesRecoveryResultAsSkipped(reason) {
604
+ return reason === "already_processed" || reason === "autonomy_budget_blocked";
605
+ }
606
+ function recordBlueBubblesRecoveryFailureForBudget(agentName, event, source) {
607
+ try {
608
+ (0, autonomy_budget_1.recordAutonomyFailure)((0, identity_1.getAgentRoot)(agentName), {
609
+ ...blueBubblesRecoveryStormInput(agentName, event, source),
610
+ occurredAt: timestampIsoForAutonomy(event),
611
+ });
612
+ }
613
+ catch {
614
+ // Autonomy failure accounting must not turn a recovery error into a broader sense outage.
615
+ }
616
+ }
617
+ function chatKeyForContext(event) {
618
+ return event.chat.chatGuid ?? event.chat.chatIdentifier ?? event.chat.sessionKey;
619
+ }
620
+ function isSameBlueBubblesChat(candidate, anchor) {
621
+ return candidate.chat.sessionKey === anchor.chat.sessionKey
622
+ || (!!candidate.chat.chatGuid && candidate.chat.chatGuid === anchor.chat.chatGuid)
623
+ || (!!candidate.chat.chatIdentifier && candidate.chat.chatIdentifier === anchor.chat.chatIdentifier);
624
+ }
625
+ function contextAuthorLabel(event) {
626
+ if (event.fromMe)
627
+ return "Slugger";
628
+ return event.sender.displayName || event.sender.externalId || "Unknown";
629
+ }
630
+ function messageToContextInput(event, chatGuidHash) {
631
+ return {
632
+ timestamp: new Date(event.timestamp).toISOString(),
633
+ authorLabel: contextAuthorLabel(event),
634
+ body: event.textForAgent || event.text,
635
+ sourceRef: {
636
+ sense: "bluebubbles",
637
+ adapter: "bluebubbles-api-v1",
638
+ service: "imessage",
639
+ chatGuid: event.chat.chatGuid,
640
+ chatGuidHash,
641
+ messageGuid: event.messageGuid,
642
+ senderExternalIdHash: sha256Hex(event.sender.externalId || event.sender.rawId || "unknown"),
643
+ observedAt: new Date(event.timestamp).toISOString(),
644
+ },
645
+ };
646
+ }
647
+ function insertEphemeralContextMessages(messages, contextMessages) {
648
+ if (contextMessages.length === 0)
649
+ return messages;
650
+ const insertionIndex = Math.max(0, messages.length - 1);
651
+ return [
652
+ ...messages.slice(0, insertionIndex),
653
+ ...contextMessages,
654
+ ...messages.slice(insertionIndex),
655
+ ];
656
+ }
657
+ async function buildBlueBubblesContextMessages(input) {
658
+ if (input.event.kind !== "message")
659
+ return [];
660
+ const event = input.event;
661
+ if (!Number.isFinite(event.timestamp))
662
+ return [];
663
+ const chatKey = chatKeyForContext(event);
664
+ const chatGuidHash = sha256Hex(chatKey);
665
+ const anchorTimestamp = new Date(event.timestamp).toISOString();
666
+ try {
667
+ const query = {
668
+ beforeTimestamp: event.timestamp,
669
+ limit: BLUEBUBBLES_CONTEXT_PACKET_LIMIT,
670
+ offset: 0,
671
+ ...(event.chat.chatGuid ? { chatGuid: event.chat.chatGuid } : {}),
672
+ ...(!event.chat.chatGuid && event.chat.chatIdentifier ? { chatIdentifier: event.chat.chatIdentifier } : {}),
673
+ };
674
+ const candidates = input.client.listRecentMessages
675
+ ? await input.client.listRecentMessages(query)
676
+ : [];
677
+ const history = candidates
678
+ .filter((candidate) => candidate.kind === "message")
679
+ .filter((candidate) => isSameBlueBubblesChat(candidate, event))
680
+ .filter((candidate) => candidate.messageGuid !== event.messageGuid)
681
+ .filter((candidate) => candidate.timestamp <= event.timestamp)
682
+ .filter((candidate) => event.timestamp - candidate.timestamp <= BLUEBUBBLES_CONTEXT_PACKET_MAX_AGE_MS);
683
+ if (history.length === 0)
684
+ return [];
685
+ const packet = (0, context_packets_1.buildSenseContextPacket)({
686
+ agent: input.agentName,
687
+ sense: "bluebubbles",
688
+ sessionKey: event.chat.sessionKey,
689
+ chatKeyHash: chatGuidHash,
690
+ anchorMessageGuid: event.messageGuid,
691
+ anchorTimestamp,
692
+ windowBeforeMessages: BLUEBUBBLES_CONTEXT_PACKET_LIMIT,
693
+ windowBeforeMs: BLUEBUBBLES_CONTEXT_PACKET_MAX_AGE_MS,
694
+ messages: history.map((candidate) => messageToContextInput(candidate, chatGuidHash)),
695
+ });
696
+ (0, context_packet_ledger_1.writeSenseContextPacket)(input.agentRoot, packet);
697
+ const rendered = (0, context_packets_1.renderSenseContextPacketForPrompt)(packet, {
698
+ redactionPatterns: [/password=[^\s]+/gi],
699
+ });
700
+ (0, runtime_1.emitNervesEvent)({
701
+ component: "senses",
702
+ event: "senses.bluebubbles_context_packet_injected",
703
+ message: "injected bluebubbles same-chat context packet",
704
+ meta: {
705
+ packetId: packet.packetId,
706
+ messageGuid: event.messageGuid,
707
+ contextMessages: history.length,
708
+ },
709
+ });
710
+ return [{ role: "system", content: rendered.text }];
711
+ }
712
+ catch (error) {
713
+ (0, runtime_1.emitNervesEvent)({
714
+ level: "warn",
715
+ component: "senses",
716
+ event: "senses.bluebubbles_context_packet_error",
717
+ message: "failed to build live bluebubbles context packet",
718
+ meta: {
719
+ messageGuid: event.messageGuid,
720
+ reason: error instanceof Error ? error.message : String(error),
721
+ },
722
+ });
723
+ const fallback = (0, context_packet_ledger_1.readLatestVisibleSenseContextPacket)(input.agentRoot, {
724
+ sense: "bluebubbles",
725
+ chatKeyHash: chatGuidHash,
726
+ beforeAnchorTimestamp: anchorTimestamp,
727
+ maxAgeMs: 24 * 60 * 60 * 1000,
728
+ });
729
+ if (!fallback)
730
+ return [];
731
+ const rendered = (0, context_packets_1.renderSenseContextPacketForPrompt)(fallback);
732
+ return [{ role: "system", content: rendered.text }];
733
+ }
734
+ }
561
735
  function createReplyTargetController(event) {
562
736
  const defaultTargetLabel = event.kind === "message" && event.threadOriginatorGuid?.trim() ? "current_lane" : "top_level";
563
737
  let selection = event.kind === "message" && event.threadOriginatorGuid?.trim()
@@ -1164,6 +1338,23 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1164
1338
  }
1165
1339
  ownsInFlightMessage = true;
1166
1340
  }
1341
+ if (event.kind === "message" && shouldUseBlueBubblesRecoveryBudget(source, options)) {
1342
+ const decision = (0, autonomy_budget_1.reserveAutonomyBudget)((0, identity_1.getAgentRoot)(agentName), {
1343
+ agent: agentName,
1344
+ triggerType: "recovery",
1345
+ sourceKind: "sense",
1346
+ senseOrHabit: "bluebubbles",
1347
+ target: blueBubblesAutonomyTarget(event),
1348
+ idempotencyKey: `bluebubbles-recovery:${source}:${event.messageGuid}`,
1349
+ now: timestampIsoForAutonomy(event),
1350
+ storm: blueBubblesRecoveryStormInput(agentName, event, source),
1351
+ });
1352
+ if (!decision.allowed) {
1353
+ endBlueBubblesMessageInFlight(inFlightKey.sessionKey, inFlightKey.messageGuid);
1354
+ ownsInFlightMessage = false;
1355
+ return { handled: true, notifiedAgent: false, kind: event.kind, reason: "autonomy_budget_blocked" };
1356
+ }
1357
+ }
1167
1358
  try {
1168
1359
  // ── Adapter setup: friend, session, content, callbacks ──────────
1169
1360
  const store = resolvedDeps.createFriendStore();
@@ -1359,27 +1550,35 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1359
1550
  enforceTrustGate: trust_gate_1.enforceTrustGate,
1360
1551
  drainPending: pending_1.drainPending,
1361
1552
  drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(resolvedDeps.getAgentName(), deferredFriendId),
1362
- runAgent: (msgs, cb, channel, sig, opts) => resolvedDeps.runAgent(msgs, cb, channel, sig, {
1363
- ...opts,
1364
- toolContext: {
1365
- /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
1366
- signin: async () => undefined,
1367
- ...opts?.toolContext,
1368
- summarize,
1369
- bluebubblesReplyTarget: {
1370
- setSelection: (selection) => replyTarget.setSelection(selection),
1371
- },
1372
- codingFeedback: {
1373
- send: async (message) => {
1374
- await client.sendText({
1375
- chat: event.chat,
1376
- text: message,
1377
- replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
1378
- });
1553
+ runAgent: async (msgs, cb, channel, sig, opts) => {
1554
+ const contextMessages = await buildBlueBubblesContextMessages({
1555
+ agentName,
1556
+ agentRoot: (0, identity_1.getAgentRoot)(),
1557
+ client,
1558
+ event,
1559
+ });
1560
+ return resolvedDeps.runAgent(insertEphemeralContextMessages(msgs, contextMessages), cb, channel, sig, {
1561
+ ...opts,
1562
+ toolContext: {
1563
+ /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
1564
+ signin: async () => undefined,
1565
+ ...opts?.toolContext,
1566
+ summarize,
1567
+ bluebubblesReplyTarget: {
1568
+ setSelection: (selection) => replyTarget.setSelection(selection),
1569
+ },
1570
+ codingFeedback: {
1571
+ send: async (message) => {
1572
+ await client.sendText({
1573
+ chat: event.chat,
1574
+ text: message,
1575
+ replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
1576
+ });
1577
+ },
1379
1578
  },
1380
1579
  },
1381
- },
1382
- }),
1580
+ });
1581
+ },
1383
1582
  postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
1384
1583
  const prepared = resolvedDeps.postTurnTrim(turnMessages, usage, hooks);
1385
1584
  resolvedDeps.deferPostTurnPersist(sessionPathArg, prepared, usage, state);
@@ -1970,8 +2169,9 @@ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, option
1970
2169
  }
1971
2170
  const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, "upstream-catchup", {
1972
2171
  timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
2172
+ autonomyBudgetTrigger: "recovery",
1973
2173
  });
1974
- if (handled.reason === "already_processed") {
2174
+ if (shouldCountBlueBubblesRecoveryResultAsSkipped(handled.reason)) {
1975
2175
  result.skipped++;
1976
2176
  }
1977
2177
  else {
@@ -1980,6 +2180,7 @@ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, option
1980
2180
  }
1981
2181
  }
1982
2182
  catch (error) {
2183
+ recordBlueBubblesRecoveryFailureForBudget(agentName, event, "upstream-catchup");
1983
2184
  result.failed++;
1984
2185
  (0, runtime_1.emitNervesEvent)({
1985
2186
  level: "warn",
@@ -2066,8 +2267,9 @@ async function recoverCapturedBlueBubblesInboundMessages(deps = {}) {
2066
2267
  }
2067
2268
  const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, entry.source, {
2068
2269
  timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
2270
+ autonomyBudgetTrigger: "recovery",
2069
2271
  });
2070
- if (handled.reason === "already_processed") {
2272
+ if (shouldCountBlueBubblesRecoveryResultAsSkipped(handled.reason)) {
2071
2273
  result.skipped++;
2072
2274
  }
2073
2275
  else {
@@ -2075,6 +2277,7 @@ async function recoverCapturedBlueBubblesInboundMessages(deps = {}) {
2075
2277
  }
2076
2278
  }
2077
2279
  catch (error) {
2280
+ recordBlueBubblesRecoveryFailureForBudget(agentName, inboundEntryToRecoveryEvent(entry), entry.source);
2078
2281
  result.failed++;
2079
2282
  (0, runtime_1.emitNervesEvent)({
2080
2283
  level: "warn",
@@ -2111,8 +2314,9 @@ async function recoverMissedBlueBubblesMessages(deps = {}) {
2111
2314
  }
2112
2315
  const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, "mutation-recovery", {
2113
2316
  timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
2317
+ autonomyBudgetTrigger: "recovery",
2114
2318
  });
2115
- if (handled.reason === "already_processed") {
2319
+ if (shouldCountBlueBubblesRecoveryResultAsSkipped(handled.reason)) {
2116
2320
  result.skipped++;
2117
2321
  }
2118
2322
  else {
@@ -2120,6 +2324,7 @@ async function recoverMissedBlueBubblesMessages(deps = {}) {
2120
2324
  }
2121
2325
  }
2122
2326
  catch (error) {
2327
+ recordBlueBubblesRecoveryFailureForBudget(agentName, mutationEntryToEvent(candidate), "mutation-recovery");
2123
2328
  result.failed++;
2124
2329
  (0, runtime_1.emitNervesEvent)({
2125
2330
  level: "warn",