@ouro.bot/cli 0.1.0-alpha.484 → 0.1.0-alpha.486

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.
@@ -0,0 +1,111 @@
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.getBlueBubblesProcessedLogPath = getBlueBubblesProcessedLogPath;
37
+ exports.hasProcessedBlueBubblesMessage = hasProcessedBlueBubblesMessage;
38
+ exports.recordProcessedBlueBubblesMessage = recordProcessedBlueBubblesMessage;
39
+ const fs = __importStar(require("node:fs"));
40
+ const path = __importStar(require("node:path"));
41
+ const config_1 = require("../../heart/config");
42
+ const identity_1 = require("../../heart/identity");
43
+ const runtime_1 = require("../../nerves/runtime");
44
+ function getBlueBubblesProcessedLogPath(agentName, sessionKey) {
45
+ return path.join((0, identity_1.getAgentRoot)(agentName), "state", "senses", "bluebubbles", "processed", `${(0, config_1.sanitizeKey)(sessionKey)}.ndjson`);
46
+ }
47
+ function readEntries(filePath) {
48
+ try {
49
+ const raw = fs.readFileSync(filePath, "utf-8");
50
+ return raw
51
+ .split("\n")
52
+ .map((line) => line.trim())
53
+ .filter(Boolean)
54
+ .map((line) => JSON.parse(line))
55
+ .filter((entry) => typeof entry.messageGuid === "string" && typeof entry.sessionKey === "string");
56
+ }
57
+ catch {
58
+ return [];
59
+ }
60
+ }
61
+ function hasProcessedBlueBubblesMessage(agentName, sessionKey, messageGuid) {
62
+ if (!messageGuid.trim())
63
+ return false;
64
+ const filePath = getBlueBubblesProcessedLogPath(agentName, sessionKey);
65
+ return readEntries(filePath).some((entry) => entry.messageGuid === messageGuid);
66
+ }
67
+ function recordProcessedBlueBubblesMessage(agentName, event, source, outcome) {
68
+ const filePath = getBlueBubblesProcessedLogPath(agentName, event.chat.sessionKey);
69
+ try {
70
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
71
+ if (event.messageGuid.trim() && readEntries(filePath).some((entry) => entry.messageGuid === event.messageGuid)) {
72
+ return filePath;
73
+ }
74
+ fs.appendFileSync(filePath, JSON.stringify({
75
+ recordedAt: new Date().toISOString(),
76
+ messageGuid: event.messageGuid,
77
+ sessionKey: event.chat.sessionKey,
78
+ source,
79
+ outcome,
80
+ }) + "\n", "utf-8");
81
+ }
82
+ catch (error) {
83
+ (0, runtime_1.emitNervesEvent)({
84
+ level: "warn",
85
+ component: "senses",
86
+ event: "senses.bluebubbles_processed_log_error",
87
+ message: "failed to record bluebubbles processed sidecar log",
88
+ meta: {
89
+ agentName,
90
+ messageGuid: event.messageGuid,
91
+ sessionKey: event.chat.sessionKey,
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_processed_logged",
100
+ message: "recorded handled bluebubbles message to processed sidecar log",
101
+ meta: {
102
+ agentName,
103
+ messageGuid: event.messageGuid,
104
+ sessionKey: event.chat.sessionKey,
105
+ source,
106
+ outcome,
107
+ path: filePath,
108
+ },
109
+ });
110
+ return filePath;
111
+ }
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.MAX_CONSECUTIVE_INSTINCT_TURNS = void 0;
36
37
  exports.createInnerDialogWorker = createInnerDialogWorker;
37
38
  exports.startInnerDialogWorker = startInnerDialogWorker;
38
39
  const path = __importStar(require("path"));
@@ -41,6 +42,20 @@ const runtime_1 = require("../nerves/runtime");
41
42
  const identity_1 = require("../heart/identity");
42
43
  const pending_1 = require("../mind/pending");
43
44
  const habit_runtime_state_1 = require("../heart/habits/habit-runtime-state");
45
+ /**
46
+ * Cap on consecutive `instinct` follow-on turns triggered by `hasPendingWork()`
47
+ * with no externally-queued work in between. Without this cap, a turn that
48
+ * writes anything back into the inner-dialog pending dir as a side effect of
49
+ * processing (e.g. a surface tool routing a response) puts the worker into
50
+ * a self-sustaining loop where the next turn's drain produces another write,
51
+ * and so on. Real workflows rarely chain more than 2–3 instinct turns; an
52
+ * external trigger (habit, poke, chat) resets the counter so legitimate
53
+ * follow-on work is unaffected.
54
+ *
55
+ * Three feels right: legitimate cascading follow-ups (e.g. processing a
56
+ * batch of delegated returns) get through; a true self-loop caps fast.
57
+ */
58
+ exports.MAX_CONSECUTIVE_INSTINCT_TURNS = 3;
44
59
  function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runInnerDialogTurn)(options), hasPendingWork = () => (0, pending_1.hasPendingMessages)((0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)()))) {
45
60
  let running = false;
46
61
  const queue = [];
@@ -54,6 +69,7 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
54
69
  let nextReason = reason;
55
70
  let nextTaskId = taskId;
56
71
  let nextHabitName = habitName;
72
+ let consecutiveInstinctTurns = reason === "instinct" ? 1 : 0;
57
73
  do {
58
74
  try {
59
75
  await runTurn({ reason: nextReason, taskId: nextTaskId, habitName: nextHabitName });
@@ -82,16 +98,36 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
82
98
  // Habit file/state may be unavailable during the turn — skip gracefully
83
99
  }
84
100
  }
85
- // Drain queue first
101
+ // Drain queue first. Externally-queued work resets the instinct cap
102
+ // because a real outside trigger arrived between turns.
86
103
  if (queue.length > 0) {
87
104
  const next = queue.shift();
88
105
  nextReason = next.reason;
89
106
  nextTaskId = next.taskId;
90
107
  nextHabitName = next.habitName;
108
+ consecutiveInstinctTurns = nextReason === "instinct" ? consecutiveInstinctTurns + 1 : 0;
91
109
  continue;
92
110
  }
93
- // Then check hasPendingWork fallback
111
+ // Then check hasPendingWork fallback. This is the loop site: any
112
+ // tool that writes to the inner-dialog pending dir during a turn
113
+ // would cause hasPendingWork() to be true here, producing a
114
+ // self-sustaining "instinct" loop with no external input. Cap it.
94
115
  if (hasPendingWork()) {
116
+ if (consecutiveInstinctTurns >= exports.MAX_CONSECUTIVE_INSTINCT_TURNS) {
117
+ (0, runtime_1.emitNervesEvent)({
118
+ level: "warn",
119
+ component: "senses",
120
+ event: "senses.inner_dialog_worker_instinct_loop_capped",
121
+ message: "inner dialog worker stopped chaining instinct turns; pending work remains for next external trigger",
122
+ meta: {
123
+ consecutiveInstinctTurns,
124
+ cap: exports.MAX_CONSECUTIVE_INSTINCT_TURNS,
125
+ lastReason: nextReason,
126
+ },
127
+ });
128
+ break;
129
+ }
130
+ consecutiveInstinctTurns += 1;
95
131
  nextReason = "instinct";
96
132
  nextTaskId = undefined;
97
133
  nextHabitName = undefined;
@@ -135,14 +135,29 @@ function stringArray(value) {
135
135
  return [];
136
136
  return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
137
137
  }
138
- function renderImportDiscoveryContent(candidatePaths) {
138
+ function candidateDescriptors(value) {
139
+ if (!Array.isArray(value))
140
+ return [];
141
+ return value
142
+ .filter((entry) => !!entry && typeof entry === "object" && !Array.isArray(entry))
143
+ .map((entry) => ({
144
+ path: typeof entry.path === "string" ? entry.path : "",
145
+ originKind: typeof entry.originKind === "string" ? entry.originKind : undefined,
146
+ originLabel: typeof entry.originLabel === "string" ? entry.originLabel : undefined,
147
+ }))
148
+ .filter((entry) => entry.path.trim().length > 0);
149
+ }
150
+ function renderImportDiscoveryContent(candidatePaths, descriptors) {
151
+ const renderedCandidates = descriptors.length > 0
152
+ ? descriptors.map((descriptor) => `- [${descriptor.originLabel ?? descriptor.originKind ?? "filesystem"}] ${descriptor.path}`)
153
+ : candidatePaths.map((candidatePath) => `- ${candidatePath}`);
139
154
  return [
140
155
  "[Mail Import Ready]",
141
156
  "A local MBOX archive is ready for delegated-mail backfill.",
142
157
  "This may live in a worktree-local Playwright sandbox rather than ~/Downloads.",
143
158
  "",
144
159
  "recent candidates:",
145
- ...candidatePaths.map((candidatePath) => `- ${candidatePath}`),
160
+ ...renderedCandidates,
146
161
  "",
147
162
  "If this matches an expected mailbox backfill, run `ouro mail import-mbox --discover --owner-email <email> --source hey --agent <agent>` first so Ouro can pick the matching archive or report ambiguity.",
148
163
  ].join("\n");
@@ -170,6 +185,7 @@ async function scanMailImportDiscoveryAttention(input) {
170
185
  ? discovered.spec.fingerprint
171
186
  : null;
172
187
  const candidatePaths = stringArray(discovered?.spec?.candidatePaths);
188
+ const descriptors = candidateDescriptors(discovered?.spec?.candidateDescriptors);
173
189
  const shouldQueue = Boolean(discovered && fingerprint && fingerprint !== state.lastNotifiedFingerprint);
174
190
  if (shouldQueue) {
175
191
  (0, pending_1.queuePendingMessage)(pendingDir, {
@@ -177,7 +193,7 @@ async function scanMailImportDiscoveryAttention(input) {
177
193
  friendId: "self",
178
194
  channel: "mail",
179
195
  key: "import-ready",
180
- content: renderImportDiscoveryContent(candidatePaths),
196
+ content: renderImportDiscoveryContent(candidatePaths, descriptors),
181
197
  timestamp: nowMs,
182
198
  mode: "reflect",
183
199
  });
@@ -93,6 +93,91 @@ function writeInnerPendingNotice(bundleRoot, noticeContent, nowIso) {
93
93
  fs.mkdirSync(innerPendingDir, { recursive: true });
94
94
  fs.writeFileSync(filePath, JSON.stringify(payload), "utf-8");
95
95
  }
96
+ const ACKNOWLEDGED_GROUPS_FILENAME = "acknowledged-auto-groups.json";
97
+ function acknowledgedGroupsPath(bundleRoot) {
98
+ return path.join(bundleRoot, "state", ACKNOWLEDGED_GROUPS_FILENAME);
99
+ }
100
+ function loadAcknowledgedGroupsState(bundleRoot) {
101
+ try {
102
+ const raw = fs.readFileSync(acknowledgedGroupsPath(bundleRoot), "utf-8");
103
+ if (!raw.trim())
104
+ return {};
105
+ const parsed = JSON.parse(raw);
106
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
107
+ return {};
108
+ return parsed;
109
+ }
110
+ catch {
111
+ return {};
112
+ }
113
+ }
114
+ function persistAcknowledgedGroupsState(bundleRoot, state) {
115
+ const target = acknowledgedGroupsPath(bundleRoot);
116
+ fs.mkdirSync(path.dirname(target), { recursive: true });
117
+ fs.writeFileSync(target, `${JSON.stringify(state, null, 2)}\n`, "utf-8");
118
+ }
119
+ /**
120
+ * For BlueBubbles group chats that were auto-created at stranger trust (no
121
+ * explicit operator/agent action ever bound the harness to this group), the
122
+ * gate's family-member bypass would otherwise let messages flow through
123
+ * silently and the agent would accumulate a session it has no mental model
124
+ * for. Surface the relationship as an inner-pending notice exactly once so
125
+ * the agent can categorize / rename / dismiss the group on its next turn.
126
+ *
127
+ * Returns true if a notice was written so callers can emit a telemetry event.
128
+ */
129
+ function maybeSurfaceAutoCreatedGroup(input, bundleRoot, nowIso) {
130
+ // Caller guarantees isGroupChat = true (only invoked from the family-member
131
+ // bypass branch); skip a redundant guard here.
132
+ if (input.friend.trustLevel !== "stranger")
133
+ return false;
134
+ if (!input.friend.notes?.["autoCreatedGroup"])
135
+ return false;
136
+ // loadAcknowledgedGroupsState is defensive (its own try/catch returns {})
137
+ // so we don't wrap it in another try here.
138
+ const state = loadAcknowledgedGroupsState(bundleRoot);
139
+ if (state[input.friend.id])
140
+ return false;
141
+ const noticeContent = `New BlueBubbles group "${input.friend.name}" became active without explicit acknowledgment. ` +
142
+ `It was auto-created at stranger trust the first time a message routed through it. ` +
143
+ `If you recognize the group, label or rename it (and consider promoting trust); if not, you can leave it as a stranger group or rename it for clarity. ` +
144
+ `external id: ${input.externalId}; friend id: ${input.friend.id}.`;
145
+ try {
146
+ writeInnerPendingNotice(bundleRoot, noticeContent, nowIso);
147
+ persistAcknowledgedGroupsState(bundleRoot, {
148
+ ...state,
149
+ [input.friend.id]: { surfacedAt: nowIso },
150
+ });
151
+ (0, runtime_1.emitNervesEvent)({
152
+ level: "info",
153
+ component: "senses",
154
+ event: "senses.trust_gate_group_acknowledgment_surfaced",
155
+ message: "auto-created group surfaced for agent acknowledgment",
156
+ meta: {
157
+ friendId: input.friend.id,
158
+ friendName: input.friend.name,
159
+ externalId: input.externalId,
160
+ provider: input.provider,
161
+ },
162
+ });
163
+ return true;
164
+ /* v8 ignore start -- defensive: surfacing failure must not block the gate decision @preserve */
165
+ }
166
+ catch (error) {
167
+ (0, runtime_1.emitNervesEvent)({
168
+ level: "error",
169
+ component: "senses",
170
+ event: "senses.trust_gate_error",
171
+ message: "failed to surface auto-created group for acknowledgment",
172
+ meta: {
173
+ friendId: input.friend.id,
174
+ reason: error instanceof Error ? error.message : String(error),
175
+ },
176
+ });
177
+ return false;
178
+ }
179
+ /* v8 ignore stop */
180
+ }
96
181
  function enforceTrustGate(input) {
97
182
  const { senseType } = input;
98
183
  // Local (CLI) and internal (inner dialog) — always allow
@@ -104,8 +189,18 @@ function enforceTrustGate(input) {
104
189
  return { allowed: true };
105
190
  }
106
191
  // Open senses (BlueBubbles/iMessage) — enforce trust rules
107
- // Group chat with a family member present — allow regardless of trust level
192
+ // Group chat with a family member present — allow regardless of trust level.
193
+ // BUT if this is an auto-created stranger group (the harness picked it up
194
+ // silently via the family-member shortcut and the agent never explicitly
195
+ // acknowledged it), surface a one-time inner-pending notice so the agent
196
+ // gets a chance to categorize / rename / dismiss the relationship instead
197
+ // of accumulating activity invisibly.
108
198
  if (input.isGroupChat && input.groupHasFamilyMember) {
199
+ /* v8 ignore start -- defaults shared with the rest of the gate; tested via the stranger-trust path */
200
+ const bundleRoot = input.bundleRoot ?? (0, identity_1.getAgentRoot)();
201
+ const nowIso = (input.now ?? (() => new Date()))().toISOString();
202
+ /* v8 ignore stop */
203
+ maybeSurfaceAutoCreatedGroup(input, bundleRoot, nowIso);
109
204
  return { allowed: true };
110
205
  }
111
206
  const trustLevel = input.friend.trustLevel ?? "friend";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.484",
3
+ "version": "0.1.0-alpha.486",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",