@ouro.bot/cli 0.1.0-alpha.7 → 0.1.0-alpha.71

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 (123) hide show
  1. package/AdoptionSpecialist.ouro/agent.json +70 -9
  2. package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
  3. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  4. package/README.md +147 -205
  5. package/assets/ouroboros.png +0 -0
  6. package/changelog.json +395 -0
  7. package/dist/heart/active-work.js +178 -0
  8. package/dist/heart/bridges/manager.js +358 -0
  9. package/dist/heart/bridges/state-machine.js +135 -0
  10. package/dist/heart/bridges/store.js +123 -0
  11. package/dist/heart/config.js +68 -23
  12. package/dist/heart/core.js +282 -92
  13. package/dist/heart/cross-chat-delivery.js +146 -0
  14. package/dist/heart/daemon/agent-discovery.js +81 -0
  15. package/dist/heart/daemon/auth-flow.js +409 -0
  16. package/dist/heart/daemon/daemon-cli.js +1408 -248
  17. package/dist/heart/daemon/daemon-entry.js +55 -6
  18. package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
  19. package/dist/heart/daemon/daemon.js +216 -10
  20. package/dist/heart/daemon/hatch-animation.js +10 -3
  21. package/dist/heart/daemon/hatch-flow.js +7 -82
  22. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  23. package/dist/heart/daemon/launchd.js +159 -0
  24. package/dist/heart/daemon/log-tailer.js +4 -3
  25. package/dist/heart/daemon/message-router.js +17 -8
  26. package/dist/heart/daemon/ouro-bot-entry.js +0 -0
  27. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  28. package/dist/heart/daemon/ouro-entry.js +0 -0
  29. package/dist/heart/daemon/ouro-path-installer.js +178 -0
  30. package/dist/heart/daemon/ouro-uti.js +11 -2
  31. package/dist/heart/daemon/process-manager.js +14 -1
  32. package/dist/heart/daemon/run-hooks.js +37 -0
  33. package/dist/heart/daemon/runtime-logging.js +58 -15
  34. package/dist/heart/daemon/runtime-metadata.js +219 -0
  35. package/dist/heart/daemon/runtime-mode.js +67 -0
  36. package/dist/heart/daemon/sense-manager.js +307 -0
  37. package/dist/heart/daemon/skill-management-installer.js +94 -0
  38. package/dist/heart/daemon/socket-client.js +202 -0
  39. package/dist/heart/daemon/specialist-orchestrator.js +53 -84
  40. package/dist/heart/daemon/specialist-prompt.js +64 -5
  41. package/dist/heart/daemon/specialist-tools.js +213 -58
  42. package/dist/heart/daemon/staged-restart.js +114 -0
  43. package/dist/heart/daemon/thoughts.js +379 -0
  44. package/dist/heart/daemon/update-checker.js +111 -0
  45. package/dist/heart/daemon/update-hooks.js +138 -0
  46. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  47. package/dist/heart/delegation.js +62 -0
  48. package/dist/heart/identity.js +126 -21
  49. package/dist/heart/kicks.js +1 -19
  50. package/dist/heart/model-capabilities.js +48 -0
  51. package/dist/heart/progress-story.js +42 -0
  52. package/dist/heart/providers/anthropic.js +74 -9
  53. package/dist/heart/providers/azure.js +86 -7
  54. package/dist/heart/providers/github-copilot.js +149 -0
  55. package/dist/heart/providers/minimax.js +4 -0
  56. package/dist/heart/providers/openai-codex.js +12 -3
  57. package/dist/heart/safe-workspace.js +228 -0
  58. package/dist/heart/sense-truth.js +61 -0
  59. package/dist/heart/session-activity.js +169 -0
  60. package/dist/heart/session-recall.js +116 -0
  61. package/dist/heart/streaming.js +100 -22
  62. package/dist/heart/target-resolution.js +123 -0
  63. package/dist/heart/turn-coordinator.js +28 -0
  64. package/dist/mind/associative-recall.js +14 -2
  65. package/dist/mind/bundle-manifest.js +70 -0
  66. package/dist/mind/context.js +27 -11
  67. package/dist/mind/first-impressions.js +16 -2
  68. package/dist/mind/friends/channel.js +35 -0
  69. package/dist/mind/friends/group-context.js +144 -0
  70. package/dist/mind/friends/store-file.js +19 -0
  71. package/dist/mind/friends/trust-explanation.js +74 -0
  72. package/dist/mind/friends/types.js +8 -0
  73. package/dist/mind/memory.js +27 -26
  74. package/dist/mind/pending.js +72 -9
  75. package/dist/mind/phrases.js +1 -0
  76. package/dist/mind/prompt.js +358 -77
  77. package/dist/mind/token-estimate.js +8 -12
  78. package/dist/nerves/cli-logging.js +15 -2
  79. package/dist/nerves/coverage/run-artifacts.js +1 -1
  80. package/dist/repertoire/ado-client.js +4 -2
  81. package/dist/repertoire/coding/feedback.js +134 -0
  82. package/dist/repertoire/coding/index.js +4 -1
  83. package/dist/repertoire/coding/manager.js +62 -4
  84. package/dist/repertoire/coding/spawner.js +3 -3
  85. package/dist/repertoire/coding/tools.js +41 -2
  86. package/dist/repertoire/data/ado-endpoints.json +188 -0
  87. package/dist/repertoire/guardrails.js +279 -0
  88. package/dist/repertoire/mcp-client.js +254 -0
  89. package/dist/repertoire/mcp-manager.js +195 -0
  90. package/dist/repertoire/skills.js +3 -26
  91. package/dist/repertoire/tasks/board.js +12 -0
  92. package/dist/repertoire/tasks/index.js +23 -9
  93. package/dist/repertoire/tasks/transitions.js +1 -2
  94. package/dist/repertoire/tools-base.js +642 -251
  95. package/dist/repertoire/tools-bluebubbles.js +93 -0
  96. package/dist/repertoire/tools-teams.js +58 -25
  97. package/dist/repertoire/tools.js +93 -52
  98. package/dist/senses/bluebubbles-client.js +210 -5
  99. package/dist/senses/bluebubbles-entry.js +2 -0
  100. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  101. package/dist/senses/bluebubbles-media.js +339 -0
  102. package/dist/senses/bluebubbles-model.js +12 -4
  103. package/dist/senses/bluebubbles-mutation-log.js +45 -5
  104. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  105. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  106. package/dist/senses/bluebubbles.js +893 -45
  107. package/dist/senses/cli-layout.js +87 -0
  108. package/dist/senses/cli.js +348 -144
  109. package/dist/senses/continuity.js +94 -0
  110. package/dist/senses/debug-activity.js +148 -0
  111. package/dist/senses/inner-dialog-worker.js +47 -18
  112. package/dist/senses/inner-dialog.js +333 -84
  113. package/dist/senses/pipeline.js +278 -0
  114. package/dist/senses/teams.js +573 -129
  115. package/dist/senses/trust-gate.js +112 -2
  116. package/package.json +14 -3
  117. package/subagents/README.md +4 -70
  118. package/dist/heart/daemon/specialist-session.js +0 -142
  119. package/dist/heart/daemon/subagent-installer.js +0 -125
  120. package/dist/inner-worker-entry.js +0 -4
  121. package/subagents/work-doer.md +0 -233
  122. package/subagents/work-merger.md +0 -624
  123. package/subagents/work-planner.md +0 -373
@@ -0,0 +1,339 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.hydrateBlueBubblesAttachments = hydrateBlueBubblesAttachments;
37
+ const node_child_process_1 = require("node:child_process");
38
+ const fs = __importStar(require("node:fs/promises"));
39
+ const os = __importStar(require("node:os"));
40
+ const path = __importStar(require("node:path"));
41
+ const runtime_1 = require("../nerves/runtime");
42
+ const identity_1 = require("../heart/identity");
43
+ const MAX_ATTACHMENT_BYTES = 8 * 1024 * 1024;
44
+ const AUDIO_EXTENSIONS = new Set([".mp3", ".wav", ".m4a", ".caf", ".ogg"]);
45
+ const IMAGE_EXTENSIONS = new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".heic", ".heif"]);
46
+ const AUDIO_EXTENSION_BY_CONTENT_TYPE = {
47
+ "audio/wav": ".wav",
48
+ "audio/x-wav": ".wav",
49
+ "audio/mp3": ".mp3",
50
+ "audio/mpeg": ".mp3",
51
+ "audio/x-caf": ".caf",
52
+ "audio/caf": ".caf",
53
+ "audio/mp4": ".m4a",
54
+ "audio/x-m4a": ".m4a",
55
+ };
56
+ const AUDIO_INPUT_FORMAT_BY_CONTENT_TYPE = {
57
+ "audio/wav": "wav",
58
+ "audio/x-wav": "wav",
59
+ "audio/mp3": "mp3",
60
+ "audio/mpeg": "mp3",
61
+ };
62
+ const AUDIO_INPUT_FORMAT_BY_EXTENSION = {
63
+ ".wav": "wav",
64
+ ".mp3": "mp3",
65
+ };
66
+ const WHISPER_CPP_FORMULA = "whisper-cpp";
67
+ const WHISPER_CPP_MODEL_NAME = "ggml-base.en.bin";
68
+ const WHISPER_CPP_MODEL_URL = `https://huggingface.co/ggerganov/whisper.cpp/resolve/main/${WHISPER_CPP_MODEL_NAME}`;
69
+ const WHISPER_CPP_TOOLS_DIR = path.join((0, identity_1.getAgentToolsRoot)(), "whisper-cpp");
70
+ const WHISPER_CPP_MODELS_DIR = path.join(WHISPER_CPP_TOOLS_DIR, "models");
71
+ const WHISPER_CPP_MODEL_PATH = path.join(WHISPER_CPP_MODELS_DIR, WHISPER_CPP_MODEL_NAME);
72
+ function buildBlueBubblesApiUrl(baseUrl, endpoint, password) {
73
+ const root = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
74
+ const url = new URL(endpoint.replace(/^\//, ""), root);
75
+ url.searchParams.set("password", password);
76
+ return url.toString();
77
+ }
78
+ function describeAttachment(attachment) {
79
+ return attachment.transferName?.trim() || attachment.guid?.trim() || "attachment";
80
+ }
81
+ function inferContentType(attachment, responseType) {
82
+ const normalizedResponseType = responseType?.split(";")[0]?.trim().toLowerCase();
83
+ if (normalizedResponseType) {
84
+ return normalizedResponseType;
85
+ }
86
+ return attachment.mimeType?.trim().toLowerCase() || undefined;
87
+ }
88
+ function isImageAttachment(attachment, contentType) {
89
+ if (contentType?.startsWith("image/"))
90
+ return true;
91
+ const extension = path.extname(attachment.transferName ?? "").toLowerCase();
92
+ return IMAGE_EXTENSIONS.has(extension);
93
+ }
94
+ function isAudioAttachment(attachment, contentType) {
95
+ if (contentType?.startsWith("audio/"))
96
+ return true;
97
+ const extension = path.extname(attachment.transferName ?? "").toLowerCase();
98
+ return AUDIO_EXTENSIONS.has(extension);
99
+ }
100
+ function sanitizeFilename(name) {
101
+ return path.basename(name).replace(/[\r\n"\\]/g, "_");
102
+ }
103
+ function fileExtensionForAudio(attachment, contentType) {
104
+ const transferExt = path.extname(attachment.transferName ?? "").toLowerCase();
105
+ if (transferExt) {
106
+ return transferExt;
107
+ }
108
+ if (contentType && AUDIO_EXTENSION_BY_CONTENT_TYPE[contentType]) {
109
+ return AUDIO_EXTENSION_BY_CONTENT_TYPE[contentType];
110
+ }
111
+ return ".audio";
112
+ }
113
+ function audioFormatForInput(contentType, attachment) {
114
+ const extension = path.extname(attachment?.transferName ?? "").toLowerCase();
115
+ return AUDIO_INPUT_FORMAT_BY_CONTENT_TYPE[contentType ?? ""] ?? AUDIO_INPUT_FORMAT_BY_EXTENSION[extension];
116
+ }
117
+ async function execFileText(file, args, timeout) {
118
+ return await new Promise((resolve, reject) => {
119
+ (0, node_child_process_1.execFile)(file, args, { timeout }, (error, stdout = "", stderr = "") => {
120
+ if (error) {
121
+ const detail = stderr.trim() || stdout.trim() || error.message;
122
+ reject(new Error(detail));
123
+ return;
124
+ }
125
+ resolve(stdout);
126
+ });
127
+ });
128
+ }
129
+ async function pathExists(targetPath) {
130
+ try {
131
+ await fs.access(targetPath);
132
+ return true;
133
+ }
134
+ catch {
135
+ return false;
136
+ }
137
+ }
138
+ async function resolveWhisperCppBinary(timeoutMs) {
139
+ try {
140
+ const existing = (await execFileText("which", ["whisper-cli"], timeoutMs)).trim();
141
+ if (existing) {
142
+ return existing;
143
+ }
144
+ }
145
+ catch {
146
+ // fall through to managed install
147
+ }
148
+ let prefix = "";
149
+ try {
150
+ prefix = (await execFileText("brew", ["--prefix", WHISPER_CPP_FORMULA], timeoutMs)).trim();
151
+ if (prefix) {
152
+ const candidate = path.join(prefix, "bin", "whisper-cli");
153
+ if (await pathExists(candidate)) {
154
+ return candidate;
155
+ }
156
+ }
157
+ }
158
+ catch {
159
+ // fall through to managed install
160
+ }
161
+ await execFileText("brew", ["install", WHISPER_CPP_FORMULA], Math.max(timeoutMs, 300_000));
162
+ prefix = (await execFileText("brew", ["--prefix", WHISPER_CPP_FORMULA], timeoutMs)).trim();
163
+ if (!prefix) {
164
+ throw new Error("whisper.cpp installed but brew did not return a usable prefix");
165
+ }
166
+ const candidate = path.join(prefix, "bin", "whisper-cli");
167
+ if (!await pathExists(candidate)) {
168
+ throw new Error("whisper.cpp installed but whisper-cli binary is missing");
169
+ }
170
+ return candidate;
171
+ }
172
+ async function ensureWhisperCppModel(timeoutMs, fetchImpl) {
173
+ try {
174
+ await fs.access(WHISPER_CPP_MODEL_PATH);
175
+ return WHISPER_CPP_MODEL_PATH;
176
+ }
177
+ catch {
178
+ await fs.mkdir(WHISPER_CPP_MODELS_DIR, { recursive: true });
179
+ const response = await fetchImpl(WHISPER_CPP_MODEL_URL, {
180
+ method: "GET",
181
+ signal: AbortSignal.timeout(Math.max(timeoutMs, 300_000)),
182
+ });
183
+ if (!response.ok) {
184
+ throw new Error(`failed to download whisper.cpp model: HTTP ${response.status}`);
185
+ }
186
+ await fs.writeFile(WHISPER_CPP_MODEL_PATH, Buffer.from(await response.arrayBuffer()));
187
+ return WHISPER_CPP_MODEL_PATH;
188
+ }
189
+ }
190
+ async function convertAudioForWhisperCpp(sourcePath, outputPath, timeoutMs) {
191
+ try {
192
+ await execFileText("ffmpeg", ["-y", "-i", sourcePath, "-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le", outputPath], Math.max(timeoutMs, 120_000));
193
+ return;
194
+ }
195
+ catch (ffmpegError) {
196
+ try {
197
+ await execFileText("afconvert", ["-f", "WAVE", "-d", "LEI16@16000", "-c", "1", sourcePath, outputPath], Math.max(timeoutMs, 120_000));
198
+ return;
199
+ }
200
+ catch (afconvertError) {
201
+ const ffmpegReason = ffmpegError.message;
202
+ const afconvertReason = afconvertError.message;
203
+ throw new Error(`failed to prepare audio for whisper.cpp (ffmpeg: ${ffmpegReason}; afconvert: ${afconvertReason})`);
204
+ }
205
+ }
206
+ }
207
+ async function transcribeAudioWithWhisperCpp(params, modelFetchImpl = fetch) {
208
+ const workDir = await fs.mkdtemp(path.join(os.tmpdir(), "ouro-bb-audio-"));
209
+ const filename = sanitizeFilename(describeAttachment(params.attachment));
210
+ const extension = fileExtensionForAudio(params.attachment, params.contentType);
211
+ const audioPath = path.join(workDir, `${path.parse(filename).name}${extension}`);
212
+ const wavPath = path.join(workDir, `${path.parse(audioPath).name}.wav`);
213
+ const outputBase = path.join(workDir, path.parse(audioPath).name);
214
+ try {
215
+ await fs.writeFile(audioPath, params.buffer);
216
+ const whisperCliPath = await resolveWhisperCppBinary(params.timeoutMs);
217
+ const modelPath = await ensureWhisperCppModel(params.timeoutMs, modelFetchImpl);
218
+ await convertAudioForWhisperCpp(audioPath, wavPath, params.timeoutMs);
219
+ await execFileText(whisperCliPath, ["-m", modelPath, "-f", wavPath, "-oj", "-of", outputBase], Math.max(params.timeoutMs, 120_000));
220
+ const transcriptPath = `${outputBase}.json`;
221
+ const raw = await fs.readFile(transcriptPath, "utf8");
222
+ const parsed = JSON.parse(raw);
223
+ if (typeof parsed.text === "string") {
224
+ return parsed.text.trim();
225
+ }
226
+ if (Array.isArray(parsed.transcription)) {
227
+ return parsed.transcription
228
+ .map((entry) => (typeof entry?.text === "string" ? entry.text.trim() : ""))
229
+ .filter(Boolean)
230
+ .join(" ")
231
+ .trim();
232
+ }
233
+ return "";
234
+ }
235
+ finally {
236
+ await fs.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
237
+ }
238
+ }
239
+ async function downloadAttachment(attachment, config, channelConfig, fetchImpl) {
240
+ const guid = attachment.guid?.trim();
241
+ if (!guid) {
242
+ throw new Error("attachment guid missing");
243
+ }
244
+ if (typeof attachment.totalBytes === "number" && attachment.totalBytes > MAX_ATTACHMENT_BYTES) {
245
+ throw new Error(`attachment exceeds ${MAX_ATTACHMENT_BYTES} byte limit`);
246
+ }
247
+ const url = buildBlueBubblesApiUrl(config.serverUrl, `/api/v1/attachment/${encodeURIComponent(guid)}/download`, config.password);
248
+ const response = await fetchImpl(url, {
249
+ method: "GET",
250
+ signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
251
+ });
252
+ if (!response.ok) {
253
+ throw new Error(`HTTP ${response.status}`);
254
+ }
255
+ const buffer = Buffer.from(await response.arrayBuffer());
256
+ if (buffer.length > MAX_ATTACHMENT_BYTES) {
257
+ throw new Error(`attachment exceeds ${MAX_ATTACHMENT_BYTES} byte limit`);
258
+ }
259
+ return {
260
+ buffer,
261
+ contentType: inferContentType(attachment, response.headers.get("content-type")),
262
+ };
263
+ }
264
+ async function hydrateBlueBubblesAttachments(attachments, config, channelConfig, deps = {}) {
265
+ (0, runtime_1.emitNervesEvent)({
266
+ component: "senses",
267
+ event: "senses.bluebubbles_media_hydrate",
268
+ message: "hydrating bluebubbles attachments",
269
+ meta: {
270
+ attachmentCount: attachments.length,
271
+ preferAudioInput: deps.preferAudioInput ?? false,
272
+ },
273
+ });
274
+ const fetchImpl = deps.fetchImpl ?? fetch;
275
+ const modelFetchImpl = deps.modelFetchImpl ?? fetch;
276
+ const transcribeAudio = deps.transcribeAudio ?? ((params) => transcribeAudioWithWhisperCpp(params, modelFetchImpl));
277
+ const preferAudioInput = deps.preferAudioInput ?? false;
278
+ const inputParts = [];
279
+ const transcriptAdditions = [];
280
+ const notices = [];
281
+ for (const attachment of attachments) {
282
+ const name = describeAttachment(attachment);
283
+ try {
284
+ const downloaded = await downloadAttachment(attachment, config, channelConfig, fetchImpl);
285
+ const base64 = downloaded.buffer.toString("base64");
286
+ if (isImageAttachment(attachment, downloaded.contentType)) {
287
+ inputParts.push({
288
+ type: "image_url",
289
+ image_url: {
290
+ url: `data:${downloaded.contentType ?? "application/octet-stream"};base64,${base64}`,
291
+ detail: "auto",
292
+ },
293
+ });
294
+ continue;
295
+ }
296
+ if (isAudioAttachment(attachment, downloaded.contentType)) {
297
+ const audioFormat = audioFormatForInput(downloaded.contentType, attachment);
298
+ if (preferAudioInput && audioFormat) {
299
+ inputParts.push({
300
+ type: "input_audio",
301
+ input_audio: {
302
+ data: base64,
303
+ format: audioFormat,
304
+ },
305
+ });
306
+ continue;
307
+ }
308
+ const transcript = (await transcribeAudio({
309
+ attachment,
310
+ buffer: downloaded.buffer,
311
+ contentType: downloaded.contentType,
312
+ timeoutMs: channelConfig.requestTimeoutMs,
313
+ })).trim();
314
+ if (!transcript) {
315
+ notices.push(`attachment hydration failed for ${name}: empty audio transcript`);
316
+ continue;
317
+ }
318
+ transcriptAdditions.push(`voice note transcript: ${transcript}`);
319
+ continue;
320
+ }
321
+ inputParts.push({
322
+ type: "file",
323
+ file: {
324
+ file_data: base64,
325
+ filename: sanitizeFilename(name),
326
+ },
327
+ });
328
+ }
329
+ catch (error) {
330
+ const reason = error instanceof Error ? error.message : String(error);
331
+ notices.push(`attachment hydration failed for ${name}: ${reason}`);
332
+ }
333
+ }
334
+ return {
335
+ inputParts,
336
+ transcriptAdditions,
337
+ notices,
338
+ };
339
+ }
@@ -39,6 +39,7 @@ function extractChatIdentifierFromGuid(chatGuid) {
39
39
  return parts.length >= 3 ? parts[2]?.trim() || undefined : undefined;
40
40
  }
41
41
  function buildChatRef(data, threadOriginatorGuid) {
42
+ void threadOriginatorGuid;
42
43
  const chats = Array.isArray(data.chats) ? data.chats : [];
43
44
  const chat = asRecord(chats[0]) ?? null;
44
45
  const chatGuid = readString(chat, "guid");
@@ -48,12 +49,18 @@ function buildChatRef(data, threadOriginatorGuid) {
48
49
  const displayName = readString(chat, "displayName")?.trim() || undefined;
49
50
  const style = readNumber(chat, "style");
50
51
  const isGroup = style === 43 || (chatGuid?.includes(";+;") ?? false) || Boolean(displayName);
51
- const baseKey = chatGuid?.trim()
52
+ const sessionKey = chatGuid?.trim()
52
53
  ? `chat:${chatGuid.trim()}`
53
54
  : `chat_identifier:${(chatIdentifier ?? "unknown").trim()}`;
54
- const sessionKey = threadOriginatorGuid?.trim()
55
- ? `${baseKey}:thread:${threadOriginatorGuid.trim()}`
56
- : baseKey;
55
+ // Extract participant handles from chat.participants (when available from BB API)
56
+ const rawParticipants = Array.isArray(chat?.participants) ? chat.participants : [];
57
+ const participantHandles = rawParticipants
58
+ .map((p) => {
59
+ const rec = asRecord(p);
60
+ const addr = readString(rec, "address") ?? readString(rec, "id");
61
+ return addr ? normalizeHandle(addr) : "";
62
+ })
63
+ .filter(Boolean);
57
64
  return {
58
65
  chatGuid: chatGuid?.trim() || undefined,
59
66
  chatIdentifier: chatIdentifier?.trim() || undefined,
@@ -63,6 +70,7 @@ function buildChatRef(data, threadOriginatorGuid) {
63
70
  sendTarget: chatGuid?.trim()
64
71
  ? { kind: "chat_guid", value: chatGuid.trim() }
65
72
  : { kind: "chat_identifier", value: (chatIdentifier ?? "unknown").trim() },
73
+ participantHandles,
66
74
  };
67
75
  }
68
76
  function extractSender(data, chat) {
@@ -35,15 +35,14 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getBlueBubblesMutationLogPath = getBlueBubblesMutationLogPath;
37
37
  exports.recordBlueBubblesMutation = recordBlueBubblesMutation;
38
+ exports.listBlueBubblesRecoveryCandidates = listBlueBubblesRecoveryCandidates;
38
39
  const fs = __importStar(require("node:fs"));
39
- const os = __importStar(require("node:os"));
40
40
  const path = __importStar(require("node:path"));
41
41
  const runtime_1 = require("../nerves/runtime");
42
- function sanitizeKey(key) {
43
- return key.replace(/[/:]/g, "_");
44
- }
42
+ const identity_1 = require("../heart/identity");
43
+ const config_1 = require("../heart/config");
45
44
  function getBlueBubblesMutationLogPath(agentName, sessionKey) {
46
- return path.join(os.homedir(), ".agentstate", agentName, "senses", "bluebubbles", "mutations", `${sanitizeKey(sessionKey)}.ndjson`);
45
+ return path.join((0, identity_1.getAgentRoot)(agentName), "state", "senses", "bluebubbles", "mutations", `${(0, config_1.sanitizeKey)(sessionKey)}.ndjson`);
47
46
  }
48
47
  function recordBlueBubblesMutation(agentName, event) {
49
48
  const filePath = getBlueBubblesMutationLogPath(agentName, event.chat.sessionKey);
@@ -74,3 +73,44 @@ function recordBlueBubblesMutation(agentName, event) {
74
73
  });
75
74
  return filePath;
76
75
  }
76
+ function listBlueBubblesRecoveryCandidates(agentName) {
77
+ const rootDir = path.join((0, identity_1.getAgentRoot)(agentName), "state", "senses", "bluebubbles", "mutations");
78
+ let files;
79
+ try {
80
+ files = fs.readdirSync(rootDir);
81
+ }
82
+ catch {
83
+ return [];
84
+ }
85
+ const deduped = new Map();
86
+ for (const file of files.filter((entry) => entry.endsWith(".ndjson")).sort()) {
87
+ const filePath = path.join(rootDir, file);
88
+ let raw = "";
89
+ try {
90
+ raw = fs.readFileSync(filePath, "utf-8");
91
+ }
92
+ catch {
93
+ continue;
94
+ }
95
+ for (const line of raw.split("\n")) {
96
+ const trimmed = line.trim();
97
+ if (!trimmed)
98
+ continue;
99
+ try {
100
+ const entry = JSON.parse(trimmed);
101
+ if (typeof entry.messageGuid !== "string"
102
+ || !entry.messageGuid.trim()
103
+ || entry.fromMe
104
+ || entry.shouldNotifyAgent
105
+ || (entry.mutationType !== "read" && entry.mutationType !== "delivery")) {
106
+ continue;
107
+ }
108
+ deduped.set(entry.messageGuid, entry);
109
+ }
110
+ catch {
111
+ // ignore malformed recovery candidates
112
+ }
113
+ }
114
+ }
115
+ return [...deduped.values()].sort((left, right) => left.recordedAt.localeCompare(right.recordedAt));
116
+ }
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getBlueBubblesRuntimeStatePath = getBlueBubblesRuntimeStatePath;
37
+ exports.readBlueBubblesRuntimeState = readBlueBubblesRuntimeState;
38
+ exports.writeBlueBubblesRuntimeState = writeBlueBubblesRuntimeState;
39
+ const fs = __importStar(require("node:fs"));
40
+ const path = __importStar(require("node:path"));
41
+ const identity_1 = require("../heart/identity");
42
+ const runtime_1 = require("../nerves/runtime");
43
+ const DEFAULT_RUNTIME_STATE = {
44
+ upstreamStatus: "unknown",
45
+ detail: "startup health probe pending",
46
+ pendingRecoveryCount: 0,
47
+ };
48
+ function getBlueBubblesRuntimeStatePath(agentName, agentRoot = (0, identity_1.getAgentRoot)(agentName)) {
49
+ return path.join(agentRoot, "state", "senses", "bluebubbles", "runtime.json");
50
+ }
51
+ function readBlueBubblesRuntimeState(agentName, agentRoot) {
52
+ const filePath = getBlueBubblesRuntimeStatePath(agentName, agentRoot);
53
+ try {
54
+ const raw = fs.readFileSync(filePath, "utf-8");
55
+ const parsed = JSON.parse(raw);
56
+ return {
57
+ upstreamStatus: parsed.upstreamStatus === "ok" || parsed.upstreamStatus === "error"
58
+ ? parsed.upstreamStatus
59
+ : "unknown",
60
+ detail: typeof parsed.detail === "string" && parsed.detail.trim()
61
+ ? parsed.detail
62
+ : DEFAULT_RUNTIME_STATE.detail,
63
+ lastCheckedAt: typeof parsed.lastCheckedAt === "string" ? parsed.lastCheckedAt : undefined,
64
+ pendingRecoveryCount: typeof parsed.pendingRecoveryCount === "number" && Number.isFinite(parsed.pendingRecoveryCount)
65
+ ? parsed.pendingRecoveryCount
66
+ : 0,
67
+ lastRecoveredAt: typeof parsed.lastRecoveredAt === "string" ? parsed.lastRecoveredAt : undefined,
68
+ lastRecoveredMessageGuid: typeof parsed.lastRecoveredMessageGuid === "string"
69
+ ? parsed.lastRecoveredMessageGuid
70
+ : undefined,
71
+ };
72
+ }
73
+ catch {
74
+ return { ...DEFAULT_RUNTIME_STATE };
75
+ }
76
+ }
77
+ function writeBlueBubblesRuntimeState(agentName, state, agentRoot) {
78
+ const filePath = getBlueBubblesRuntimeStatePath(agentName, agentRoot);
79
+ try {
80
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
81
+ fs.writeFileSync(filePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
82
+ }
83
+ catch (error) {
84
+ (0, runtime_1.emitNervesEvent)({
85
+ level: "warn",
86
+ component: "senses",
87
+ event: "senses.bluebubbles_runtime_state_error",
88
+ message: "failed to write bluebubbles runtime state",
89
+ meta: {
90
+ agentName,
91
+ upstreamStatus: state.upstreamStatus,
92
+ reason: error instanceof Error ? error.message : String(error),
93
+ },
94
+ });
95
+ return filePath;
96
+ }
97
+ (0, runtime_1.emitNervesEvent)({
98
+ component: "senses",
99
+ event: "senses.bluebubbles_runtime_state_written",
100
+ message: "wrote bluebubbles runtime state",
101
+ meta: {
102
+ agentName,
103
+ upstreamStatus: state.upstreamStatus,
104
+ pendingRecoveryCount: state.pendingRecoveryCount,
105
+ path: filePath,
106
+ },
107
+ });
108
+ return filePath;
109
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.findObsoleteBlueBubblesThreadSessions = findObsoleteBlueBubblesThreadSessions;
37
+ const fs = __importStar(require("node:fs"));
38
+ const path = __importStar(require("node:path"));
39
+ const runtime_1 = require("../nerves/runtime");
40
+ function findObsoleteBlueBubblesThreadSessions(trunkSessionPath) {
41
+ const normalized = trunkSessionPath.trim();
42
+ if (!normalized.endsWith(".json"))
43
+ return [];
44
+ const trunkName = path.basename(normalized);
45
+ if (trunkName.includes("_thread_"))
46
+ return [];
47
+ if (!fs.existsSync(normalized))
48
+ return [];
49
+ const dir = path.dirname(normalized);
50
+ const prefix = trunkName.slice(0, -".json".length);
51
+ const threadLaneFiles = [];
52
+ for (const entry of fs.readdirSync(dir)) {
53
+ if (!entry.endsWith(".json"))
54
+ continue;
55
+ if (!entry.startsWith(`${prefix}_thread_`))
56
+ continue;
57
+ threadLaneFiles.push(path.join(dir, entry));
58
+ }
59
+ if (threadLaneFiles.length > 0) {
60
+ (0, runtime_1.emitNervesEvent)({
61
+ level: "warn",
62
+ component: "senses",
63
+ event: "senses.bluebubbles_thread_lane_artifacts_detected",
64
+ message: "detected obsolete bluebubbles thread-lane sessions",
65
+ meta: {
66
+ sessionPath: normalized,
67
+ artifactCount: threadLaneFiles.length,
68
+ },
69
+ });
70
+ }
71
+ return threadLaneFiles;
72
+ }