@ouro.bot/cli 0.1.0-alpha.609 → 0.1.0-alpha.611

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.
package/changelog.json CHANGED
@@ -1,6 +1,18 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.611",
6
+ "changes": [
7
+ "BlueBubbles recovery: stale captured inbound sidecars are now marked superseded when the chat session has already advanced, preventing old iMessage recovery entries from keeping runtime health in warn or replaying obsolete chat turns."
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.1.0-alpha.610",
12
+ "changes": [
13
+ "Fix the inner-dialog rest loop incident: skip empty rest-only assistant turns when deriving checkpoints, surface meaningful tool-only actions in thoughts/status output, compact idle heartbeat rest triplets, and avoid stale session repair churn during active-session scans."
14
+ ]
15
+ },
4
16
  {
5
17
  "version": "0.1.0-alpha.609",
6
18
  "changes": [
@@ -344,12 +344,79 @@ function extractSettleAnswer(messages) {
344
344
  }
345
345
  return "";
346
346
  }
347
+ function parseToolArguments(argumentsValue) {
348
+ if (!argumentsValue)
349
+ return {};
350
+ try {
351
+ const parsed = JSON.parse(argumentsValue);
352
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
353
+ ? parsed
354
+ : {};
355
+ }
356
+ catch {
357
+ return {};
358
+ }
359
+ }
360
+ function toolArgumentText(args, keys) {
361
+ for (const key of keys) {
362
+ const value = args[key];
363
+ if (typeof value === "string" && value.trim()) {
364
+ return value.trim();
365
+ }
366
+ }
367
+ return "";
368
+ }
369
+ function truncateToolSummary(summary) {
370
+ if (summary.length <= 220)
371
+ return summary;
372
+ return `${summary.slice(0, 217)}...`;
373
+ }
374
+ function summarizeToolAction(name, argumentsValue) {
375
+ if (!name)
376
+ return null;
377
+ const args = parseToolArguments(argumentsValue);
378
+ if (name === "surface") {
379
+ const message = toolArgumentText(args, ["message", "text", "content"]);
380
+ return message ? `surfaced: ${message}` : null;
381
+ }
382
+ if (name === "ponder") {
383
+ const thought = toolArgumentText(args, ["summary", "question", "topic", "prompt"]);
384
+ return thought ? `pondered: ${thought}` : null;
385
+ }
386
+ if (name === "diary_write") {
387
+ const note = toolArgumentText(args, ["text", "content", "note", "entry"]);
388
+ return note ? `diary: ${note}` : null;
389
+ }
390
+ if (name === "let_go") {
391
+ const reason = toolArgumentText(args, ["reason", "note", "status"]);
392
+ return reason ? `let go: ${reason}` : null;
393
+ }
394
+ if (name === "rest") {
395
+ const note = toolArgumentText(args, ["note", "status"]);
396
+ return note ? `rested: ${note}` : null;
397
+ }
398
+ return null;
399
+ }
400
+ function extractToolActionSummary(messages) {
401
+ for (let k = messages.length - 1; k >= 0; k--) {
402
+ const msg = messages[k];
403
+ if (msg.role !== "assistant" || !Array.isArray(msg.tool_calls))
404
+ continue;
405
+ for (let i = msg.tool_calls.length - 1; i >= 0; i--) {
406
+ const toolFunction = extractToolFunction(msg.tool_calls[i]);
407
+ const summary = summarizeToolAction(toolFunction?.name, toolFunction?.arguments);
408
+ if (summary)
409
+ return truncateToolSummary(summary);
410
+ }
411
+ }
412
+ return "";
413
+ }
347
414
  function extractThoughtResponseFromMessages(messages) {
348
415
  const assistantMsgs = messages.filter((message) => message.role === "assistant");
349
416
  const lastAssistant = assistantMsgs.reverse().find((message) => contentToText(message.content).trim().length > 0);
350
417
  return lastAssistant
351
418
  ? contentToText(lastAssistant.content).trim()
352
- : extractSettleAnswer(messages);
419
+ : extractSettleAnswer(messages) || extractToolActionSummary(messages);
353
420
  }
354
421
  function parseInnerDialogSession(sessionPath) {
355
422
  (0, runtime_1.emitNervesEvent)({
@@ -56,7 +56,7 @@ function resolveFriendName(friendId, friendsDir, agentName) {
56
56
  return friendId;
57
57
  }
58
58
  }
59
- function parseFriendActivity(sessionPath) {
59
+ function parseFriendActivity(sessionPath, activeThresholdMs, nowMs) {
60
60
  let mtimeMs;
61
61
  try {
62
62
  mtimeMs = fs.statSync(sessionPath).mtimeMs;
@@ -64,6 +64,9 @@ function parseFriendActivity(sessionPath) {
64
64
  catch {
65
65
  return null;
66
66
  }
67
+ if (Number.isFinite(activeThresholdMs) && nowMs - mtimeMs > activeThresholdMs) {
68
+ return null;
69
+ }
67
70
  const envelope = (0, session_events_1.loadSessionEnvelopeFile)(sessionPath);
68
71
  const chronology = envelope ? (0, session_events_1.deriveSessionChronology)(envelope.events) : null;
69
72
  const explicit = envelope?.state.lastFriendActivityAt;
@@ -152,7 +155,7 @@ function listSessionActivity(query) {
152
155
  continue;
153
156
  }
154
157
  const sessionPath = path.join(channelPath, keyFile);
155
- const activity = parseFriendActivity(sessionPath);
158
+ const activity = parseFriendActivity(sessionPath, activeThresholdMs, nowMs);
156
159
  if (!activity)
157
160
  continue;
158
161
  if (nowMs - activity.lastActivityMs > activeThresholdMs)
@@ -54,6 +54,7 @@ Object.defineProperty(exports, "detectDuplicateToolCallIds", { enumerable: true,
54
54
  Object.defineProperty(exports, "migrateToolNames", { enumerable: true, get: function () { return session_events_2.migrateToolNames; } });
55
55
  Object.defineProperty(exports, "repairSessionMessages", { enumerable: true, get: function () { return session_events_2.repairSessionMessages; } });
56
56
  Object.defineProperty(exports, "validateSessionMessages", { enumerable: true, get: function () { return session_events_2.validateSessionMessages; } });
57
+ const IDLE_REST_ONLY_KEEP_TURNS = 20;
57
58
  function buildTrimmableBlocks(messages) {
58
59
  const blocks = [];
59
60
  let i = 0;
@@ -95,6 +96,89 @@ function getSystemMessageIndices(messages) {
95
96
  function buildTrimmedMessages(messages, kept) {
96
97
  return messages.filter((m, idx) => m.role === "system" || kept.has(idx));
97
98
  }
99
+ function messageContentToText(content) {
100
+ if (typeof content === "string")
101
+ return content;
102
+ if (!Array.isArray(content))
103
+ return "";
104
+ return content
105
+ .map((part) => {
106
+ if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
107
+ return part.text;
108
+ }
109
+ return "";
110
+ })
111
+ .join("\n");
112
+ }
113
+ function toolCallName(toolCall) {
114
+ return toolCall.function.name;
115
+ }
116
+ function toolCallId(toolCall) {
117
+ return toolCall.id;
118
+ }
119
+ function isIdleHeartbeatRestUserMessage(message) {
120
+ if (message.role !== "user")
121
+ return false;
122
+ const text = messageContentToText(message.content);
123
+ return text.includes("...time passing. anything stirring?")
124
+ && !text.includes("[pending from ")
125
+ && !text.includes("## task:");
126
+ }
127
+ function isEmptyRestOnlyAssistantMessage(message) {
128
+ if (message.role !== "assistant")
129
+ return null;
130
+ if (messageContentToText(message.content).trim())
131
+ return null;
132
+ if (!Array.isArray(message.tool_calls) || message.tool_calls.length !== 1)
133
+ return null;
134
+ const onlyToolCall = message.tool_calls[0];
135
+ if (toolCallName(onlyToolCall) !== "rest")
136
+ return null;
137
+ return toolCallId(onlyToolCall);
138
+ }
139
+ function isMatchingToolResult(message, toolId) {
140
+ return message.tool_call_id === toolId;
141
+ }
142
+ function compactIdleRestOnlyTurns(messages, keepTurns = IDLE_REST_ONLY_KEEP_TURNS) {
143
+ const blocks = [];
144
+ let i = 0;
145
+ while (i < messages.length - 2) {
146
+ const userMessage = messages[i];
147
+ const assistantMessage = messages[i + 1];
148
+ const toolMessage = messages[i + 2];
149
+ const restToolId = isIdleHeartbeatRestUserMessage(userMessage)
150
+ ? isEmptyRestOnlyAssistantMessage(assistantMessage)
151
+ : null;
152
+ if (restToolId && isMatchingToolResult(toolMessage, restToolId)) {
153
+ blocks.push({ start: i, end: i + 2 });
154
+ i += 3;
155
+ continue;
156
+ }
157
+ i++;
158
+ }
159
+ if (blocks.length <= keepTurns)
160
+ return messages;
161
+ const drop = new Set();
162
+ for (const block of blocks.slice(0, blocks.length - keepTurns)) {
163
+ for (let index = block.start; index <= block.end; index++) {
164
+ drop.add(index);
165
+ }
166
+ }
167
+ const compacted = messages.filter((_message, index) => !drop.has(index));
168
+ (0, runtime_1.emitNervesEvent)({
169
+ component: "mind",
170
+ event: "mind.session_idle_rest_compaction",
171
+ message: "compacted old idle rest-only inner turns",
172
+ meta: {
173
+ removedTurns: blocks.length - keepTurns,
174
+ keptTurns: keepTurns,
175
+ removedMessages: drop.size,
176
+ originalCount: messages.length,
177
+ finalCount: compacted.length,
178
+ },
179
+ });
180
+ return compacted;
181
+ }
98
182
  function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
99
183
  const targetTokens = Math.floor(maxTokens * (1 - contextMargin / 100));
100
184
  const estimatedBefore = (0, token_estimate_1.estimateTokensForMessages)(messages);
@@ -284,9 +368,10 @@ function postTurnTrim(messages, usage, hooks) {
284
368
  }
285
369
  }
286
370
  const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
287
- const currentIngressTimes = messages.map(session_events_1.getIngressTime);
288
371
  const currentMessages = (0, session_events_1.sanitizeProviderMessages)(messages);
289
- const trimmedMessages = trimMessages(currentMessages, maxTokens, contextMargin, usage?.input_tokens);
372
+ const currentIngressTimes = currentMessages.map(session_events_1.getIngressTime);
373
+ const tokenTrimmedMessages = trimMessages(currentMessages, maxTokens, contextMargin, usage?.input_tokens);
374
+ const trimmedMessages = compactIdleRestOnlyTurns(tokenTrimmedMessages);
290
375
  messages.splice(0, messages.length, ...trimmedMessages);
291
376
  return { currentMessages, trimmedMessages, currentIngressTimes, maxTokens, contextMargin };
292
377
  }
@@ -1514,7 +1514,11 @@ function listPendingCapturedInboundMessages(agentName) {
1514
1514
  seenMessageGuids.add(entry.messageGuid);
1515
1515
  return true;
1516
1516
  })
1517
- .filter((entry) => !(0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, entry.messageGuid));
1517
+ .filter((entry) => {
1518
+ if ((0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, entry.messageGuid))
1519
+ return false;
1520
+ return !markCapturedInboundSuperseded(agentName, entry);
1521
+ });
1518
1522
  }
1519
1523
  function listPendingRecoveryEntries(agentName) {
1520
1524
  const pendingByGuid = new Map();
@@ -1546,6 +1550,67 @@ function parseTimestampMs(value) {
1546
1550
  const parsed = Date.parse(value);
1547
1551
  return Number.isFinite(parsed) ? parsed : null;
1548
1552
  }
1553
+ function eventRecordedAtMs(event) {
1554
+ return parseTimestampMs(event.time?.recordedAt ?? undefined);
1555
+ }
1556
+ function blueBubblesSessionPathsForKey(agentName, sessionKey) {
1557
+ const sessionsRoot = path.join((0, identity_1.getAgentRoot)(agentName), "state", "sessions");
1558
+ const fileName = `${(0, config_1.sanitizeKey)(sessionKey)}.json`;
1559
+ let friendDirs;
1560
+ try {
1561
+ friendDirs = fs.readdirSync(sessionsRoot, { withFileTypes: true });
1562
+ }
1563
+ catch {
1564
+ return [];
1565
+ }
1566
+ return friendDirs
1567
+ .filter((entry) => entry.isDirectory())
1568
+ .map((entry) => path.join(sessionsRoot, entry.name, "bluebubbles", fileName))
1569
+ .filter((filePath) => {
1570
+ try {
1571
+ return fs.statSync(filePath).isFile();
1572
+ }
1573
+ catch {
1574
+ return false;
1575
+ }
1576
+ });
1577
+ }
1578
+ function hasNewerBlueBubblesSessionActivity(agentName, sessionKey, recordedAt) {
1579
+ const recordedAtMs = parseTimestampMs(recordedAt);
1580
+ if (recordedAtMs === null)
1581
+ return false;
1582
+ for (const sessionFilePath of blueBubblesSessionPathsForKey(agentName, sessionKey)) {
1583
+ const session = (0, context_1.loadSession)(sessionFilePath);
1584
+ for (const event of session?.events ?? []) {
1585
+ if (event.role === "system")
1586
+ continue;
1587
+ const nextRecordedAtMs = eventRecordedAtMs(event);
1588
+ if (nextRecordedAtMs !== null && nextRecordedAtMs > recordedAtMs) {
1589
+ return true;
1590
+ }
1591
+ }
1592
+ }
1593
+ return false;
1594
+ }
1595
+ function markCapturedInboundSuperseded(agentName, entry) {
1596
+ if (!entry.messageGuid.trim())
1597
+ return false;
1598
+ if (!hasNewerBlueBubblesSessionActivity(agentName, entry.sessionKey, entry.recordedAt))
1599
+ return false;
1600
+ (0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, inboundEntryToRecoveryEvent(entry), entry.source, "recovery-superseded");
1601
+ (0, runtime_1.emitNervesEvent)({
1602
+ component: "senses",
1603
+ event: "senses.bluebubbles_recovery_skip",
1604
+ message: "skipped stale captured bluebubbles recovery because the session already advanced",
1605
+ meta: {
1606
+ messageGuid: entry.messageGuid,
1607
+ sessionKey: entry.sessionKey,
1608
+ source: entry.source,
1609
+ dedupeReason: "session_superseded",
1610
+ },
1611
+ });
1612
+ return true;
1613
+ }
1549
1614
  function resolveBlueBubblesCatchUpSince(previousState, nowMs = Date.now()) {
1550
1615
  if (previousState.upstreamStatus === "error") {
1551
1616
  return nowMs - BLUEBUBBLES_RECOVERY_CATCHUP_LOOKBACK_MS;
@@ -183,13 +183,10 @@ function contentToText(content) {
183
183
  .join("\n");
184
184
  return text.trim();
185
185
  }
186
- function deriveResumeCheckpoint(messages) {
187
- const lastAssistant = [...messages].reverse().find((message) => message.role === "assistant");
188
- if (!lastAssistant)
189
- return "no prior checkpoint recorded";
190
- const assistantText = contentToText(lastAssistant.content);
186
+ function checkpointTextFromAssistantContent(content) {
187
+ const assistantText = contentToText(content);
191
188
  if (!assistantText)
192
- return "no prior checkpoint recorded";
189
+ return null;
193
190
  const cleanedLines = assistantText
194
191
  .split("\n")
195
192
  .map((line) => line.replace(/<\/?think>/gi, "").trim())
@@ -198,14 +195,102 @@ function deriveResumeCheckpoint(messages) {
198
195
  .find((line) => /^checkpoint\s*:/i.test(line));
199
196
  if (explicitCheckpoint) {
200
197
  const parsed = explicitCheckpoint.replace(/^checkpoint\s*:\s*/i, "").trim();
201
- return parsed || "no prior checkpoint recorded";
198
+ return parsed || null;
202
199
  }
203
200
  const firstLine = cleanedLines[0];
204
- if (!firstLine)
205
- return "no prior checkpoint recorded";
206
- if (firstLine.length <= 220)
207
- return firstLine;
208
- return `${firstLine.slice(0, 217)}...`;
201
+ return firstLine ?? null;
202
+ }
203
+ function truncateCheckpointText(text) {
204
+ if (text.length <= 220)
205
+ return text;
206
+ return `${text.slice(0, 217)}...`;
207
+ }
208
+ function parseToolArguments(argumentsValue) {
209
+ if (!argumentsValue)
210
+ return {};
211
+ try {
212
+ const parsed = JSON.parse(argumentsValue);
213
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
214
+ ? parsed
215
+ : {};
216
+ }
217
+ catch {
218
+ return {};
219
+ }
220
+ }
221
+ function toolArgumentText(args, keys) {
222
+ for (const key of keys) {
223
+ const value = args[key];
224
+ if (typeof value === "string" && value.trim()) {
225
+ return value.trim();
226
+ }
227
+ }
228
+ return "";
229
+ }
230
+ function summarizeToolAction(name, argumentsValue) {
231
+ if (!name)
232
+ return null;
233
+ const args = parseToolArguments(argumentsValue);
234
+ if (name === "surface") {
235
+ const message = toolArgumentText(args, ["message", "text", "content"]);
236
+ return message ? `surfaced: ${message}` : null;
237
+ }
238
+ if (name === "ponder") {
239
+ const thought = toolArgumentText(args, ["summary", "question", "topic", "prompt"]);
240
+ return thought ? `pondered: ${thought}` : null;
241
+ }
242
+ if (name === "diary_write") {
243
+ const note = toolArgumentText(args, ["text", "content", "note", "entry"]);
244
+ return note ? `diary: ${note}` : null;
245
+ }
246
+ if (name === "let_go") {
247
+ const reason = toolArgumentText(args, ["reason", "note", "status"]);
248
+ return reason ? `let go: ${reason}` : null;
249
+ }
250
+ if (name === "rest") {
251
+ const note = toolArgumentText(args, ["note", "status"]);
252
+ return note ? `rested: ${note}` : null;
253
+ }
254
+ return null;
255
+ }
256
+ function extractToolFunction(toolCall) {
257
+ if (!toolCall || typeof toolCall !== "object" || !("function" in toolCall))
258
+ return null;
259
+ const maybeFunction = toolCall.function;
260
+ if (!maybeFunction || typeof maybeFunction !== "object")
261
+ return null;
262
+ const name = "name" in maybeFunction && typeof maybeFunction.name === "string"
263
+ ? maybeFunction.name
264
+ : undefined;
265
+ const argumentsValue = "arguments" in maybeFunction && typeof maybeFunction.arguments === "string"
266
+ ? maybeFunction.arguments
267
+ : undefined;
268
+ return { name, arguments: argumentsValue };
269
+ }
270
+ function checkpointTextFromAssistantToolCalls(message) {
271
+ if (message.role !== "assistant" || !Array.isArray(message.tool_calls))
272
+ return null;
273
+ for (let i = message.tool_calls.length - 1; i >= 0; i--) {
274
+ const toolFunction = extractToolFunction(message.tool_calls[i]);
275
+ const summary = summarizeToolAction(toolFunction?.name, toolFunction?.arguments);
276
+ if (summary)
277
+ return summary;
278
+ }
279
+ return null;
280
+ }
281
+ function deriveResumeCheckpoint(messages) {
282
+ for (let i = messages.length - 1; i >= 0; i--) {
283
+ const message = messages[i];
284
+ if (message.role !== "assistant")
285
+ continue;
286
+ const textCheckpoint = checkpointTextFromAssistantContent(message.content);
287
+ if (textCheckpoint)
288
+ return truncateCheckpointText(textCheckpoint);
289
+ const toolCheckpoint = checkpointTextFromAssistantToolCalls(message);
290
+ if (toolCheckpoint)
291
+ return truncateCheckpointText(toolCheckpoint);
292
+ }
293
+ return "no prior checkpoint recorded";
209
294
  }
210
295
  function extractAssistantPreview(messages, maxLength = 120) {
211
296
  const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.609",
3
+ "version": "0.1.0-alpha.611",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",