@ouro.bot/cli 0.1.0-alpha.51 → 0.1.0-alpha.52

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,14 @@
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.52",
6
+ "changes": [
7
+ "Trusted 1:1 chats can now act live across active group threads: the harness resolves candidate target chats, carries explicit trust context into the model, and delivers messages into the right live BlueBubbles or Teams session instead of only queueing for later.",
8
+ "People discovered through a relevant live group are now bootstrapped as acquaintances with shared-group context, so the agent gets a socially truthful model of who is merely unknown versus who is known through the current group.",
9
+ "Cross-chat work now returns a truthful outcome back to the asking chat, and bridge suggestions can span different outward relationships when one live piece of work is clearly happening across them."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.51",
6
14
  "changes": [
@@ -5,6 +5,7 @@ exports.buildActiveWorkFrame = buildActiveWorkFrame;
5
5
  exports.formatActiveWorkFrame = formatActiveWorkFrame;
6
6
  const runtime_1 = require("../nerves/runtime");
7
7
  const state_machine_1 = require("./bridges/state-machine");
8
+ const target_resolution_1 = require("./target-resolution");
8
9
  function activityPriority(source) {
9
10
  return source === "friend-facing" ? 0 : 1;
10
11
  }
@@ -31,16 +32,28 @@ function hasSharedObligationPressure(input) {
31
32
  || summarizeLiveTasks(input.taskBoard).length > 0;
32
33
  }
33
34
  function suggestBridgeForActiveWork(input) {
34
- const candidateSessions = input.friendSessions
35
- .filter((session) => !input.currentSession
36
- || session.friendId !== input.currentSession.friendId
37
- || session.channel !== input.currentSession.channel
38
- || session.key !== input.currentSession.key)
39
- .sort(compareActivity);
40
- const targetSession = candidateSessions[0] ?? null;
41
- if (!targetSession || !hasSharedObligationPressure(input)) {
35
+ const targetCandidates = (input.targetCandidates ?? [])
36
+ .filter((candidate) => {
37
+ if (candidate.delivery.mode === "blocked") {
38
+ return false;
39
+ }
40
+ if (candidate.activitySource !== "friend-facing" || candidate.channel === "inner") {
41
+ return false;
42
+ }
43
+ if (!input.currentSession) {
44
+ return true;
45
+ }
46
+ return !(candidate.friendId === input.currentSession.friendId
47
+ && candidate.channel === input.currentSession.channel
48
+ && candidate.key === input.currentSession.key);
49
+ })
50
+ .sort((a, b) => {
51
+ return b.lastActivityMs - a.lastActivityMs;
52
+ });
53
+ if (!hasSharedObligationPressure(input) || targetCandidates.length !== 1) {
42
54
  return null;
43
55
  }
56
+ const targetSession = targetCandidates[0];
44
57
  const activeBridge = input.bridges.find(isActiveBridge) ?? null;
45
58
  if (activeBridge) {
46
59
  const alreadyAttached = activeBridge.attachedSessions.some((session) => session.friendId === targetSession.friendId
@@ -53,14 +66,14 @@ function suggestBridgeForActiveWork(input) {
53
66
  kind: "attach-existing",
54
67
  bridgeId: activeBridge.id,
55
68
  targetSession,
56
- reason: "same-friend-shared-work",
69
+ reason: "shared-work-candidate",
57
70
  };
58
71
  }
59
72
  return {
60
73
  kind: "begin-new",
61
74
  targetSession,
62
75
  objectiveHint: input.currentObligation?.trim() || "keep this shared work aligned",
63
- reason: "same-friend-shared-work",
76
+ reason: "shared-work-candidate",
64
77
  };
65
78
  }
66
79
  function formatSessionLabel(session) {
@@ -95,13 +108,14 @@ function buildActiveWorkFrame(input) {
95
108
  freshestForCurrentFriend: friendSessions[0] ?? null,
96
109
  otherLiveSessionsForCurrentFriend: friendSessions,
97
110
  },
111
+ targetCandidates: input.targetCandidates ?? [],
98
112
  bridgeSuggestion: suggestBridgeForActiveWork({
99
113
  currentSession: input.currentSession,
100
114
  currentObligation: input.currentObligation,
101
115
  mustResolveBeforeHandoff: input.mustResolveBeforeHandoff,
102
116
  bridges: input.bridges,
103
117
  taskBoard: input.taskBoard,
104
- friendSessions,
118
+ targetCandidates: input.targetCandidates,
105
119
  }),
106
120
  };
107
121
  (0, runtime_1.emitNervesEvent)({
@@ -144,6 +158,13 @@ function formatActiveWorkFrame(frame) {
144
158
  if (frame.friendActivity?.freshestForCurrentFriend) {
145
159
  lines.push(`freshest friend-facing session: ${formatSessionLabel(frame.friendActivity.freshestForCurrentFriend)}`);
146
160
  }
161
+ const targetCandidatesBlock = frame.targetCandidates && frame.targetCandidates.length > 0
162
+ ? (0, target_resolution_1.formatTargetSessionCandidates)(frame.targetCandidates)
163
+ : "";
164
+ if (targetCandidatesBlock) {
165
+ lines.push("");
166
+ lines.push(targetCandidatesBlock);
167
+ }
147
168
  if (frame.bridgeSuggestion) {
148
169
  if (frame.bridgeSuggestion.kind === "attach-existing") {
149
170
  lines.push(`suggested bridge: attach ${frame.bridgeSuggestion.bridgeId} -> ${formatSessionLabel(frame.bridgeSuggestion.targetSession)}`);
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deliverCrossChatMessage = deliverCrossChatMessage;
4
+ const types_1 = require("../mind/friends/types");
5
+ const runtime_1 = require("../nerves/runtime");
6
+ function buildPendingEnvelope(request, agentName, now) {
7
+ return {
8
+ from: agentName,
9
+ friendId: request.friendId,
10
+ channel: request.channel,
11
+ key: request.key,
12
+ content: request.content,
13
+ timestamp: now,
14
+ };
15
+ }
16
+ function queueForLater(request, deps, detail) {
17
+ deps.queuePending(buildPendingEnvelope(request, deps.agentName, (deps.now ?? Date.now)()));
18
+ return {
19
+ status: "queued_for_later",
20
+ detail,
21
+ };
22
+ }
23
+ function isExplicitlyAuthorized(request) {
24
+ return request.intent === "explicit_cross_chat"
25
+ && Boolean(request.authorizingSession)
26
+ && (0, types_1.isTrustedLevel)(request.authorizingSession?.trustLevel);
27
+ }
28
+ async function deliverCrossChatMessage(request, deps) {
29
+ (0, runtime_1.emitNervesEvent)({
30
+ component: "engine",
31
+ event: "engine.cross_chat_delivery_start",
32
+ message: "resolving cross-chat delivery",
33
+ meta: {
34
+ friendId: request.friendId,
35
+ channel: request.channel,
36
+ key: request.key,
37
+ intent: request.intent,
38
+ authorizingTrustLevel: request.authorizingSession?.trustLevel ?? null,
39
+ },
40
+ });
41
+ if (request.intent === "generic_outreach") {
42
+ const result = queueForLater(request, deps, "generic outreach stays queued until the target session is next active");
43
+ (0, runtime_1.emitNervesEvent)({
44
+ component: "engine",
45
+ event: "engine.cross_chat_delivery_end",
46
+ message: "queued generic outreach for later delivery",
47
+ meta: {
48
+ friendId: request.friendId,
49
+ channel: request.channel,
50
+ key: request.key,
51
+ status: result.status,
52
+ },
53
+ });
54
+ return result;
55
+ }
56
+ if (!isExplicitlyAuthorized(request)) {
57
+ const result = {
58
+ status: "blocked",
59
+ detail: "explicit cross-chat delivery requires a trusted asking session",
60
+ };
61
+ (0, runtime_1.emitNervesEvent)({
62
+ level: "warn",
63
+ component: "engine",
64
+ event: "engine.cross_chat_delivery_end",
65
+ message: "blocked explicit cross-chat delivery",
66
+ meta: {
67
+ friendId: request.friendId,
68
+ channel: request.channel,
69
+ key: request.key,
70
+ status: result.status,
71
+ },
72
+ });
73
+ return result;
74
+ }
75
+ const deliverer = deps.deliverers?.[request.channel];
76
+ if (!deliverer) {
77
+ const result = queueForLater(request, deps, "live delivery unavailable right now; queued for the next active turn");
78
+ (0, runtime_1.emitNervesEvent)({
79
+ component: "engine",
80
+ event: "engine.cross_chat_delivery_end",
81
+ message: "queued explicit cross-chat delivery because no live deliverer was available",
82
+ meta: {
83
+ friendId: request.friendId,
84
+ channel: request.channel,
85
+ key: request.key,
86
+ status: result.status,
87
+ },
88
+ });
89
+ return result;
90
+ }
91
+ try {
92
+ const direct = await deliverer(request);
93
+ if (direct.status === "delivered_now" || direct.status === "blocked" || direct.status === "failed") {
94
+ const result = {
95
+ status: direct.status,
96
+ detail: direct.detail,
97
+ };
98
+ (0, runtime_1.emitNervesEvent)({
99
+ level: result.status === "failed" ? "error" : result.status === "blocked" ? "warn" : "info",
100
+ component: "engine",
101
+ event: "engine.cross_chat_delivery_end",
102
+ message: "completed direct cross-chat delivery resolution",
103
+ meta: {
104
+ friendId: request.friendId,
105
+ channel: request.channel,
106
+ key: request.key,
107
+ status: result.status,
108
+ },
109
+ });
110
+ return result;
111
+ }
112
+ const result = queueForLater(request, deps, direct.detail.trim() || "live delivery unavailable right now; queued for the next active turn");
113
+ (0, runtime_1.emitNervesEvent)({
114
+ component: "engine",
115
+ event: "engine.cross_chat_delivery_end",
116
+ message: "queued explicit cross-chat delivery after adapter reported unavailability",
117
+ meta: {
118
+ friendId: request.friendId,
119
+ channel: request.channel,
120
+ key: request.key,
121
+ status: result.status,
122
+ },
123
+ });
124
+ return result;
125
+ }
126
+ catch (error) {
127
+ const result = {
128
+ status: "failed",
129
+ detail: error instanceof Error ? error.message : String(error),
130
+ };
131
+ (0, runtime_1.emitNervesEvent)({
132
+ level: "error",
133
+ component: "engine",
134
+ event: "engine.cross_chat_delivery_end",
135
+ message: "cross-chat delivery threw unexpectedly",
136
+ meta: {
137
+ friendId: request.friendId,
138
+ channel: request.channel,
139
+ key: request.key,
140
+ status: result.status,
141
+ reason: result.detail,
142
+ },
143
+ });
144
+ return result;
145
+ }
146
+ }
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listTargetSessionCandidates = listTargetSessionCandidates;
4
+ exports.formatTargetSessionCandidates = formatTargetSessionCandidates;
5
+ const session_recall_1 = require("./session-recall");
6
+ const session_activity_1 = require("./session-activity");
7
+ const trust_explanation_1 = require("../mind/friends/trust-explanation");
8
+ const runtime_1 = require("../nerves/runtime");
9
+ function synthesizeFriendRecord(candidate) {
10
+ return {
11
+ id: candidate.friendId,
12
+ name: candidate.friendName,
13
+ role: "stranger",
14
+ trustLevel: "stranger",
15
+ connections: [],
16
+ externalIds: [],
17
+ tenantMemberships: [],
18
+ toolPreferences: {},
19
+ notes: {},
20
+ totalTokens: 0,
21
+ createdAt: new Date(0).toISOString(),
22
+ updatedAt: new Date(0).toISOString(),
23
+ schemaVersion: 1,
24
+ };
25
+ }
26
+ function deliveryPriority(mode) {
27
+ if (mode === "deliver_now")
28
+ return 0;
29
+ if (mode === "queue_only")
30
+ return 1;
31
+ return 2;
32
+ }
33
+ function activityPriority(source) {
34
+ return source === "friend-facing" ? 0 : 1;
35
+ }
36
+ function describeDelivery(candidate) {
37
+ if (candidate.channel !== "bluebubbles" && candidate.channel !== "teams") {
38
+ return { mode: "blocked", reason: "this channel does not support proactive outward delivery yet" };
39
+ }
40
+ if (candidate.trust.level === "family" || candidate.trust.level === "friend") {
41
+ return { mode: "deliver_now", reason: "directly trusted target on a proactive-delivery channel" };
42
+ }
43
+ return { mode: "queue_only", reason: "visible as a live chat, but immediate delivery still needs explicit cross-chat authorization" };
44
+ }
45
+ async function listTargetSessionCandidates(input) {
46
+ (0, runtime_1.emitNervesEvent)({
47
+ component: "engine",
48
+ event: "engine.target_resolution_start",
49
+ message: "listing live target session candidates",
50
+ meta: {
51
+ sessionsDir: input.sessionsDir,
52
+ currentSession: input.currentSession
53
+ ? `${input.currentSession.friendId}/${input.currentSession.channel}/${input.currentSession.key}`
54
+ : null,
55
+ },
56
+ });
57
+ const activity = (0, session_activity_1.listSessionActivity)({
58
+ sessionsDir: input.sessionsDir,
59
+ friendsDir: input.friendsDir,
60
+ agentName: input.agentName,
61
+ currentSession: input.currentSession ?? null,
62
+ }).filter((entry) => entry.channel !== "inner");
63
+ const candidates = [];
64
+ for (const entry of activity) {
65
+ const friend = await input.friendStore.get(entry.friendId) ?? synthesizeFriendRecord(entry);
66
+ const trust = (0, trust_explanation_1.describeTrustContext)({
67
+ friend,
68
+ channel: entry.channel,
69
+ });
70
+ const recall = await (0, session_recall_1.recallSession)({
71
+ sessionPath: entry.sessionPath,
72
+ friendId: entry.friendId,
73
+ channel: entry.channel,
74
+ key: entry.key,
75
+ messageCount: 6,
76
+ summarize: input.summarize,
77
+ trustLevel: trust.level,
78
+ });
79
+ const snapshot = recall.kind === "ok"
80
+ ? recall.snapshot
81
+ : recall.kind === "empty"
82
+ ? "recent focus: no recent visible messages"
83
+ : "recent focus: session transcript unavailable";
84
+ const delivery = describeDelivery({
85
+ channel: entry.channel,
86
+ trust,
87
+ });
88
+ candidates.push({
89
+ friendId: entry.friendId,
90
+ friendName: entry.friendName,
91
+ channel: entry.channel,
92
+ key: entry.key,
93
+ sessionPath: entry.sessionPath,
94
+ snapshot,
95
+ trust,
96
+ delivery,
97
+ lastActivityAt: entry.lastActivityAt,
98
+ lastActivityMs: entry.lastActivityMs,
99
+ activitySource: entry.activitySource,
100
+ });
101
+ }
102
+ return candidates.sort((a, b) => {
103
+ const deliveryDiff = deliveryPriority(a.delivery.mode) - deliveryPriority(b.delivery.mode);
104
+ if (deliveryDiff !== 0)
105
+ return deliveryDiff;
106
+ const sourceDiff = activityPriority(a.activitySource) - activityPriority(b.activitySource);
107
+ if (sourceDiff !== 0)
108
+ return sourceDiff;
109
+ return b.lastActivityMs - a.lastActivityMs;
110
+ });
111
+ }
112
+ function formatTargetSessionCandidates(candidates) {
113
+ if (candidates.length === 0)
114
+ return "";
115
+ const lines = ["## candidate target chats"];
116
+ for (const candidate of candidates) {
117
+ lines.push(`- ${candidate.friendName} [${candidate.friendId}] via ${candidate.channel}/${candidate.key}`);
118
+ lines.push(` trust: ${candidate.trust.level} (${candidate.trust.basis}) — ${candidate.trust.summary}`);
119
+ lines.push(` delivery: ${candidate.delivery.mode} — ${candidate.delivery.reason}`);
120
+ lines.push(` snapshot: ${candidate.snapshot}`);
121
+ }
122
+ return lines.join("\n");
123
+ }
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.upsertGroupContextParticipants = upsertGroupContextParticipants;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const runtime_1 = require("../../nerves/runtime");
6
+ const CURRENT_SCHEMA_VERSION = 1;
7
+ function normalizeDisplayName(externalId, displayName) {
8
+ const trimmed = displayName?.trim();
9
+ return trimmed && trimmed.length > 0 ? trimmed : externalId;
10
+ }
11
+ function buildNameNotes(name, now) {
12
+ return name !== "Unknown"
13
+ ? { name: { value: name, savedAt: now } }
14
+ : {};
15
+ }
16
+ function dedupeParticipants(participants) {
17
+ const deduped = new Map();
18
+ for (const participant of participants) {
19
+ const externalId = participant.externalId.trim();
20
+ if (!externalId)
21
+ continue;
22
+ const key = `${participant.provider}:${externalId}`;
23
+ if (!deduped.has(key)) {
24
+ deduped.set(key, {
25
+ ...participant,
26
+ externalId,
27
+ displayName: participant.displayName?.trim() || undefined,
28
+ });
29
+ }
30
+ }
31
+ return Array.from(deduped.values());
32
+ }
33
+ function createGroupExternalId(provider, groupExternalId, linkedAt) {
34
+ return {
35
+ provider,
36
+ externalId: groupExternalId,
37
+ linkedAt,
38
+ };
39
+ }
40
+ function shouldPromoteToAcquaintance(friend) {
41
+ return (friend.trustLevel ?? "stranger") === "stranger";
42
+ }
43
+ function createAcquaintanceRecord(participant, groupExternalId, linkedAt) {
44
+ const name = normalizeDisplayName(participant.externalId, participant.displayName);
45
+ return {
46
+ id: (0, node_crypto_1.randomUUID)(),
47
+ name,
48
+ role: "acquaintance",
49
+ trustLevel: "acquaintance",
50
+ connections: [],
51
+ externalIds: [
52
+ {
53
+ provider: participant.provider,
54
+ externalId: participant.externalId,
55
+ linkedAt,
56
+ },
57
+ createGroupExternalId(participant.provider, groupExternalId, linkedAt),
58
+ ],
59
+ tenantMemberships: [],
60
+ toolPreferences: {},
61
+ notes: buildNameNotes(name, linkedAt),
62
+ totalTokens: 0,
63
+ createdAt: linkedAt,
64
+ updatedAt: linkedAt,
65
+ schemaVersion: CURRENT_SCHEMA_VERSION,
66
+ };
67
+ }
68
+ async function upsertGroupContextParticipants(input) {
69
+ (0, runtime_1.emitNervesEvent)({
70
+ component: "friends",
71
+ event: "friends.group_context_upsert_start",
72
+ message: "upserting shared-group participant context",
73
+ meta: {
74
+ participantCount: input.participants.length,
75
+ hasGroupExternalId: input.groupExternalId.trim().length > 0,
76
+ },
77
+ });
78
+ const groupExternalId = input.groupExternalId.trim();
79
+ if (!groupExternalId) {
80
+ return [];
81
+ }
82
+ const now = input.now ?? (() => new Date().toISOString());
83
+ const participants = dedupeParticipants(input.participants);
84
+ const results = [];
85
+ for (const participant of participants) {
86
+ const linkedAt = now();
87
+ const existing = await input.store.findByExternalId(participant.provider, participant.externalId);
88
+ if (!existing) {
89
+ const created = createAcquaintanceRecord(participant, groupExternalId, linkedAt);
90
+ await input.store.put(created.id, created);
91
+ results.push({
92
+ friendId: created.id,
93
+ name: created.name,
94
+ trustLevel: "acquaintance",
95
+ created: true,
96
+ updated: false,
97
+ addedGroupExternalId: true,
98
+ });
99
+ continue;
100
+ }
101
+ const hasGroupExternalId = existing.externalIds.some((externalId) => externalId.externalId === groupExternalId);
102
+ const promoteToAcquaintance = shouldPromoteToAcquaintance(existing);
103
+ const trustLevel = promoteToAcquaintance
104
+ ? "acquaintance"
105
+ : existing.trustLevel;
106
+ const role = promoteToAcquaintance
107
+ ? "acquaintance"
108
+ : existing.role;
109
+ const updatedExternalIds = hasGroupExternalId
110
+ ? existing.externalIds
111
+ : [...existing.externalIds, createGroupExternalId(participant.provider, groupExternalId, linkedAt)];
112
+ const updated = promoteToAcquaintance || !hasGroupExternalId;
113
+ const record = updated
114
+ ? {
115
+ ...existing,
116
+ role,
117
+ trustLevel,
118
+ externalIds: updatedExternalIds,
119
+ updatedAt: linkedAt,
120
+ }
121
+ : existing;
122
+ if (updated) {
123
+ await input.store.put(record.id, record);
124
+ }
125
+ results.push({
126
+ friendId: record.id,
127
+ name: record.name,
128
+ trustLevel,
129
+ created: false,
130
+ updated,
131
+ addedGroupExternalId: !hasGroupExternalId,
132
+ });
133
+ }
134
+ (0, runtime_1.emitNervesEvent)({
135
+ component: "friends",
136
+ event: "friends.group_context_upsert_end",
137
+ message: "upserted shared-group participant context",
138
+ meta: {
139
+ participantCount: participants.length,
140
+ updatedCount: results.filter((result) => result.created || result.updated).length,
141
+ },
142
+ });
143
+ return results;
144
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.describeTrustContext = describeTrustContext;
4
+ const runtime_1 = require("../../nerves/runtime");
5
+ function findRelatedGroupId(friend) {
6
+ return friend.externalIds.find((externalId) => externalId.externalId.startsWith("group:"))?.externalId;
7
+ }
8
+ function resolveLevel(friend) {
9
+ return friend.trustLevel ?? "stranger";
10
+ }
11
+ function describeTrustContext(input) {
12
+ const level = resolveLevel(input.friend);
13
+ const relatedGroupId = findRelatedGroupId(input.friend);
14
+ const explanation = level === "family" || level === "friend"
15
+ ? {
16
+ level,
17
+ basis: "direct",
18
+ summary: level === "family"
19
+ ? "direct family trust"
20
+ : "direct trusted relationship",
21
+ why: "this relationship is directly trusted rather than inferred through a shared group or cold first contact.",
22
+ permits: [
23
+ "local operations when appropriate",
24
+ "proactive follow-through",
25
+ "full collaborative problem solving",
26
+ ],
27
+ constraints: [],
28
+ }
29
+ : level === "acquaintance"
30
+ ? {
31
+ level,
32
+ basis: "shared_group",
33
+ summary: relatedGroupId
34
+ ? "known through the shared project group"
35
+ : "known through a shared group context",
36
+ why: relatedGroupId
37
+ ? `this trust comes from the shared group context ${relatedGroupId}, not from direct endorsement.`
38
+ : "this trust comes from shared group context rather than direct endorsement.",
39
+ permits: [
40
+ "group-safe coordination",
41
+ "normal conversation inside the shared context",
42
+ ],
43
+ constraints: [
44
+ "guarded local actions",
45
+ "do not assume broad private authority",
46
+ ],
47
+ relatedGroupId,
48
+ }
49
+ : {
50
+ level,
51
+ basis: "unknown",
52
+ summary: "truly unknown first-contact context",
53
+ why: "this person is not known through direct trust or a shared group context.",
54
+ permits: [
55
+ "safe first-contact orientation only",
56
+ ],
57
+ constraints: [
58
+ "first contact does not reach the full model on open channels",
59
+ "no local or privileged actions",
60
+ ],
61
+ };
62
+ (0, runtime_1.emitNervesEvent)({
63
+ component: "friends",
64
+ event: "friends.trust_explained",
65
+ message: "built explicit trust explanation",
66
+ meta: {
67
+ channel: input.channel,
68
+ level: explanation.level,
69
+ basis: explanation.basis,
70
+ hasRelatedGroup: Boolean(explanation.relatedGroupId),
71
+ },
72
+ });
73
+ return explanation;
74
+ }
@@ -51,6 +51,7 @@ const tools_1 = require("../repertoire/tools");
51
51
  const skills_1 = require("../repertoire/skills");
52
52
  const identity_1 = require("../heart/identity");
53
53
  const types_1 = require("./friends/types");
54
+ const trust_explanation_1 = require("./friends/trust-explanation");
54
55
  const channel_1 = require("./friends/channel");
55
56
  const runtime_1 = require("../nerves/runtime");
56
57
  const bundle_manifest_1 = require("./bundle-manifest");
@@ -329,6 +330,31 @@ some of my tools are unavailable right now: ${toolList}
329
330
 
330
331
  i don't know this person well enough yet to run local operations on their behalf. i can suggest remote-safe alternatives or ask them to run it from CLI.`;
331
332
  }
333
+ function trustContextSection(context) {
334
+ if (!context?.friend)
335
+ return "";
336
+ const channelName = context.channel.channel;
337
+ if (channelName === "cli" || channelName === "inner")
338
+ return "";
339
+ const explanation = (0, trust_explanation_1.describeTrustContext)({
340
+ friend: context.friend,
341
+ channel: channelName,
342
+ isGroupChat: context.isGroupChat,
343
+ });
344
+ const lines = [
345
+ "## trust context",
346
+ `level: ${explanation.level}`,
347
+ `basis: ${explanation.basis}`,
348
+ `summary: ${explanation.summary}`,
349
+ `why: ${explanation.why}`,
350
+ `permits: ${explanation.permits.join(", ")}`,
351
+ `constraints: ${explanation.constraints.join(", ") || "none"}`,
352
+ ];
353
+ if (explanation.relatedGroupId) {
354
+ lines.push(`related group: ${explanation.relatedGroupId}`);
355
+ }
356
+ return lines.join("\n");
357
+ }
332
358
  function skillsSection() {
333
359
  const names = (0, skills_1.listSkills)() || [];
334
360
  if (!names.length)
@@ -520,6 +546,7 @@ async function buildSystem(channel = "cli", options, context) {
520
546
  toolsSection(channel, options, context),
521
547
  reasoningEffortSection(options),
522
548
  toolRestrictionSection(context),
549
+ trustContextSection(context),
523
550
  mixedTrustGroupSection(context),
524
551
  skillsSection(),
525
552
  taskBoardSection(),
@@ -50,6 +50,7 @@ const tools_1 = require("./coding/tools");
50
50
  const memory_1 = require("../mind/memory");
51
51
  const pending_1 = require("../mind/pending");
52
52
  const progress_story_1 = require("../heart/progress-story");
53
+ const cross_chat_delivery_1 = require("../heart/cross-chat-delivery");
53
54
  // Tracks which file paths have been read via read_file in this session.
54
55
  // edit_file requires a file to be read first (must-read-first guard).
55
56
  exports.editFileReadTracker = new Set();
@@ -115,6 +116,34 @@ function normalizeProgressOutcome(text) {
115
116
  }
116
117
  return trimmed;
117
118
  }
119
+ function writePendingEnvelope(queueDir, message) {
120
+ fs.mkdirSync(queueDir, { recursive: true });
121
+ const fileName = `${message.timestamp}-${Math.random().toString(36).slice(2, 10)}.json`;
122
+ const filePath = path.join(queueDir, fileName);
123
+ fs.writeFileSync(filePath, JSON.stringify(message, null, 2));
124
+ }
125
+ function renderCrossChatDeliveryStatus(target, result) {
126
+ const phase = result.status === "delivered_now"
127
+ ? "completed"
128
+ : result.status === "queued_for_later"
129
+ ? "queued"
130
+ : result.status === "blocked"
131
+ ? "blocked"
132
+ : "errored";
133
+ const lead = result.status === "delivered_now"
134
+ ? "delivered now"
135
+ : result.status === "queued_for_later"
136
+ ? "queued for later"
137
+ : result.status === "blocked"
138
+ ? "blocked"
139
+ : "failed";
140
+ return (0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
141
+ scope: "shared-work",
142
+ phase,
143
+ objective: `message to ${target}`,
144
+ outcomeText: `${lead}\n${result.detail}`,
145
+ }));
146
+ }
118
147
  function renderInnerProgressStatus(status) {
119
148
  if (status.processing === "pending") {
120
149
  return (0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
@@ -817,7 +846,7 @@ exports.baseToolDefinitions = [
817
846
  type: "function",
818
847
  function: {
819
848
  name: "send_message",
820
- description: "send a message to a friend's session. the message is queued as a pending file and delivered when the target session drains its queue.",
849
+ description: "send a message to a friend's session. when the request is explicitly authorized from a trusted live chat, the harness will try to deliver immediately; otherwise it reports truthful queued/block/failure state.",
821
850
  parameters: {
822
851
  type: "object",
823
852
  properties: {
@@ -843,9 +872,6 @@ exports.baseToolDefinitions = [
843
872
  const pendingDir = isSelf
844
873
  ? (0, pending_1.getInnerDialogPendingDir)(agentName)
845
874
  : (0, pending_1.getPendingDir)(agentName, friendId, channel, key);
846
- fs.mkdirSync(pendingDir, { recursive: true });
847
- const fileName = `${now}-${Math.random().toString(36).slice(2, 10)}.json`;
848
- const filePath = path.join(pendingDir, fileName);
849
875
  const delegatingBridgeId = findDelegatingBridgeId(ctx);
850
876
  const delegatedFrom = isSelf
851
877
  && ctx?.currentSession
@@ -866,8 +892,8 @@ exports.baseToolDefinitions = [
866
892
  timestamp: now,
867
893
  ...(delegatedFrom ? { delegatedFrom } : {}),
868
894
  };
869
- fs.writeFileSync(filePath, JSON.stringify(envelope, null, 2));
870
895
  if (isSelf) {
896
+ writePendingEnvelope(pendingDir, envelope);
871
897
  let wakeResponse = null;
872
898
  try {
873
899
  wakeResponse = await (0, socket_client_1.requestInnerWake)(agentName);
@@ -906,9 +932,103 @@ exports.baseToolDefinitions = [
906
932
  surfaced: "nothing yet",
907
933
  });
908
934
  }
909
- const preview = content.length > 80 ? content.slice(0, 80) + "…" : content;
910
- const target = `${channel}/${key}`;
911
- return `message queued for delivery to ${friendId} on ${target}. preview: "${preview}". it will be delivered when their session is next active.`;
935
+ const deliveryResult = await (0, cross_chat_delivery_1.deliverCrossChatMessage)({
936
+ friendId,
937
+ channel,
938
+ key,
939
+ content,
940
+ intent: ctx?.currentSession && ctx.currentSession.friendId !== "self"
941
+ ? "explicit_cross_chat"
942
+ : "generic_outreach",
943
+ ...(ctx?.currentSession && ctx.currentSession.friendId !== "self"
944
+ ? {
945
+ authorizingSession: {
946
+ friendId: ctx.currentSession.friendId,
947
+ channel: ctx.currentSession.channel,
948
+ key: ctx.currentSession.key,
949
+ trustLevel: ctx?.context?.friend?.trustLevel,
950
+ },
951
+ }
952
+ : {}),
953
+ }, {
954
+ agentName,
955
+ queuePending: (message) => writePendingEnvelope(pendingDir, message),
956
+ deliverers: {
957
+ bluebubbles: async (request) => {
958
+ const { sendProactiveBlueBubblesMessageToSession } = await Promise.resolve().then(() => __importStar(require("../senses/bluebubbles")));
959
+ const result = await sendProactiveBlueBubblesMessageToSession({
960
+ friendId: request.friendId,
961
+ sessionKey: request.key,
962
+ text: request.content,
963
+ intent: request.intent,
964
+ authorizingSession: request.authorizingSession,
965
+ });
966
+ if (result.delivered) {
967
+ return {
968
+ status: "delivered_now",
969
+ detail: "sent to the active bluebubbles chat now",
970
+ };
971
+ }
972
+ if (result.reason === "missing_target") {
973
+ return {
974
+ status: "blocked",
975
+ detail: "bluebubbles could not resolve a routable target for that session",
976
+ };
977
+ }
978
+ if (result.reason === "send_error") {
979
+ return {
980
+ status: "failed",
981
+ detail: "bluebubbles send failed",
982
+ };
983
+ }
984
+ return {
985
+ status: "unavailable",
986
+ detail: "live delivery unavailable right now; queued for the next active turn",
987
+ };
988
+ },
989
+ teams: async (request) => {
990
+ if (!ctx?.botApi) {
991
+ return {
992
+ status: "unavailable",
993
+ detail: "live delivery unavailable right now; queued for the next active turn",
994
+ };
995
+ }
996
+ const { sendProactiveTeamsMessageToSession } = await Promise.resolve().then(() => __importStar(require("../senses/teams")));
997
+ const result = await sendProactiveTeamsMessageToSession({
998
+ friendId: request.friendId,
999
+ sessionKey: request.key,
1000
+ text: request.content,
1001
+ intent: request.intent,
1002
+ authorizingSession: request.authorizingSession,
1003
+ }, {
1004
+ botApi: ctx.botApi,
1005
+ });
1006
+ if (result.delivered) {
1007
+ return {
1008
+ status: "delivered_now",
1009
+ detail: "sent to the active teams chat now",
1010
+ };
1011
+ }
1012
+ if (result.reason === "missing_target") {
1013
+ return {
1014
+ status: "blocked",
1015
+ detail: "teams could not resolve a routable target for that session",
1016
+ };
1017
+ }
1018
+ if (result.reason === "send_error") {
1019
+ return {
1020
+ status: "failed",
1021
+ detail: "teams send failed",
1022
+ };
1023
+ }
1024
+ return {
1025
+ status: "unavailable",
1026
+ detail: "live delivery unavailable right now; queued for the next active turn",
1027
+ };
1028
+ },
1029
+ },
1030
+ });
1031
+ return renderCrossChatDeliveryStatus(`${friendId} on ${channel}/${key}`, deliveryResult);
912
1032
  },
913
1033
  },
914
1034
  {
@@ -48,6 +48,7 @@ const identity_1 = require("../heart/identity");
48
48
  const turn_coordinator_1 = require("../heart/turn-coordinator");
49
49
  const context_1 = require("../mind/context");
50
50
  const tokens_1 = require("../mind/friends/tokens");
51
+ const group_context_1 = require("../mind/friends/group-context");
51
52
  const resolver_1 = require("../mind/friends/resolver");
52
53
  const store_file_1 = require("../mind/friends/store-file");
53
54
  const types_1 = require("../mind/friends/types");
@@ -97,6 +98,10 @@ function resolveFriendParams(event) {
97
98
  channel: "bluebubbles",
98
99
  };
99
100
  }
101
+ function resolveGroupExternalId(event) {
102
+ const groupKey = event.chat.chatGuid ?? event.chat.chatIdentifier ?? event.sender.externalId;
103
+ return `group:${groupKey}`;
104
+ }
100
105
  /**
101
106
  * Check if any participant in a group chat is a known family member.
102
107
  * Looks up each participant handle in the friend store.
@@ -585,6 +590,16 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
585
590
  return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
586
591
  }
587
592
  }
593
+ if (event.kind === "message" && event.chat.isGroup) {
594
+ await (0, group_context_1.upsertGroupContextParticipants)({
595
+ store,
596
+ participants: (event.chat.participantHandles ?? []).map((externalId) => ({
597
+ provider: "imessage-handle",
598
+ externalId,
599
+ })),
600
+ groupExternalId: resolveGroupExternalId(event),
601
+ });
602
+ }
588
603
  // Build inbound user message (adapter concern: BB-specific content formatting)
589
604
  const userMessage = {
590
605
  role: "user",
@@ -842,21 +857,33 @@ function findImessageHandle(friend) {
842
857
  }
843
858
  return undefined;
844
859
  }
860
+ function normalizeBlueBubblesSessionKey(sessionKey) {
861
+ const trimmed = sessionKey.trim();
862
+ if (trimmed.startsWith("chat_identifier_")) {
863
+ return `chat_identifier:${trimmed.slice("chat_identifier_".length)}`;
864
+ }
865
+ if (trimmed.startsWith("chat_")) {
866
+ return `chat:${trimmed.slice("chat_".length)}`;
867
+ }
868
+ return trimmed;
869
+ }
845
870
  function extractChatIdentifierFromSessionKey(sessionKey) {
846
- if (sessionKey.startsWith("chat:")) {
847
- const chatGuid = sessionKey.slice("chat:".length).trim();
871
+ const normalizedKey = normalizeBlueBubblesSessionKey(sessionKey);
872
+ if (normalizedKey.startsWith("chat:")) {
873
+ const chatGuid = normalizedKey.slice("chat:".length).trim();
848
874
  const parts = chatGuid.split(";");
849
875
  return parts.length >= 3 ? parts[2]?.trim() || undefined : undefined;
850
876
  }
851
- if (sessionKey.startsWith("chat_identifier:")) {
852
- const identifier = sessionKey.slice("chat_identifier:".length).trim();
877
+ if (normalizedKey.startsWith("chat_identifier:")) {
878
+ const identifier = normalizedKey.slice("chat_identifier:".length).trim();
853
879
  return identifier || undefined;
854
880
  }
855
881
  return undefined;
856
882
  }
857
883
  function buildChatRefForSessionKey(friend, sessionKey) {
858
- if (sessionKey.startsWith("chat:")) {
859
- const chatGuid = sessionKey.slice("chat:".length).trim();
884
+ const normalizedKey = normalizeBlueBubblesSessionKey(sessionKey);
885
+ if (normalizedKey.startsWith("chat:")) {
886
+ const chatGuid = normalizedKey.slice("chat:".length).trim();
860
887
  if (!chatGuid)
861
888
  return null;
862
889
  return {
@@ -900,12 +927,20 @@ async function sendProactiveBlueBubblesMessageToSession(params, deps = {}) {
900
927
  });
901
928
  return { delivered: false, reason: "friend_not_found" };
902
929
  }
903
- if (!types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
930
+ const explicitCrossChatAuthorized = params.intent === "explicit_cross_chat"
931
+ && types_1.TRUSTED_LEVELS.has(params.authorizingSession?.trustLevel ?? "stranger");
932
+ if (!explicitCrossChatAuthorized && !types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
904
933
  (0, runtime_1.emitNervesEvent)({
905
934
  component: "senses",
906
935
  event: "senses.bluebubbles_proactive_trust_skip",
907
936
  message: "proactive send skipped: trust level not allowed",
908
- meta: { friendId: params.friendId, sessionKey: params.sessionKey, trustLevel: friend.trustLevel ?? "unknown" },
937
+ meta: {
938
+ friendId: params.friendId,
939
+ sessionKey: params.sessionKey,
940
+ trustLevel: friend.trustLevel ?? "unknown",
941
+ intent: params.intent ?? "generic_outreach",
942
+ authorizingTrustLevel: params.authorizingSession?.trustLevel ?? null,
943
+ },
909
944
  });
910
945
  return { delivered: false, reason: "trust_skip" };
911
946
  }
@@ -14,6 +14,7 @@ const tasks_1 = require("../repertoire/tasks");
14
14
  const session_activity_1 = require("../heart/session-activity");
15
15
  const active_work_1 = require("../heart/active-work");
16
16
  const delegation_1 = require("../heart/delegation");
17
+ const target_resolution_1 = require("../heart/target-resolution");
17
18
  const thoughts_1 = require("../heart/daemon/thoughts");
18
19
  const pending_1 = require("../mind/pending");
19
20
  function emptyTaskBoard() {
@@ -137,6 +138,26 @@ async function handleInboundTurn(input) {
137
138
  catch {
138
139
  sessionActivity = [];
139
140
  }
141
+ let targetCandidates = [];
142
+ try {
143
+ if (input.channel !== "inner") {
144
+ const agentRoot = (0, identity_1.getAgentRoot)();
145
+ targetCandidates = await (0, target_resolution_1.listTargetSessionCandidates)({
146
+ sessionsDir: `${agentRoot}/state/sessions`,
147
+ friendsDir: `${agentRoot}/friends`,
148
+ agentName: (0, identity_1.getAgentName)(),
149
+ currentSession: {
150
+ friendId: currentSession.friendId,
151
+ channel: currentSession.channel,
152
+ key: currentSession.key,
153
+ },
154
+ friendStore: input.friendStore,
155
+ });
156
+ }
157
+ }
158
+ catch {
159
+ targetCandidates = [];
160
+ }
140
161
  const activeWorkFrame = (0, active_work_1.buildActiveWorkFrame)({
141
162
  currentSession,
142
163
  currentObligation,
@@ -152,6 +173,7 @@ async function handleInboundTurn(input) {
152
173
  }
153
174
  })(),
154
175
  friendActivity: sessionActivity,
176
+ targetCandidates,
155
177
  });
156
178
  const delegationDecision = (0, delegation_1.decideDelegation)({
157
179
  channel: input.channel,
@@ -40,6 +40,7 @@ exports.createTeamsCallbacks = createTeamsCallbacks;
40
40
  exports.resolvePendingConfirmation = resolvePendingConfirmation;
41
41
  exports.withConversationLock = withConversationLock;
42
42
  exports.handleTeamsMessage = handleTeamsMessage;
43
+ exports.sendProactiveTeamsMessageToSession = sendProactiveTeamsMessageToSession;
43
44
  exports.drainAndSendPendingTeams = drainAndSendPendingTeams;
44
45
  exports.startTeamsApp = startTeamsApp;
45
46
  const fs = __importStar(require("fs"));
@@ -867,6 +868,98 @@ function findAadObjectId(friend) {
867
868
  }
868
869
  return undefined;
869
870
  }
871
+ function resolveTeamsFriendStore(deps) {
872
+ return deps.store
873
+ ?? deps.createFriendStore?.()
874
+ ?? new store_file_1.FileFriendStore(path.join((0, identity_1.getAgentRoot)(), "friends"));
875
+ }
876
+ function getTeamsConversations(botApi) {
877
+ return botApi.conversations;
878
+ }
879
+ function hasExplicitCrossChatAuthorization(params) {
880
+ return params.intent === "explicit_cross_chat"
881
+ && types_1.TRUSTED_LEVELS.has(params.authorizingSession?.trustLevel ?? "stranger");
882
+ }
883
+ async function sendProactiveTeamsMessageToSession(params, deps) {
884
+ const store = resolveTeamsFriendStore(deps);
885
+ const conversations = getTeamsConversations(deps.botApi);
886
+ let friend;
887
+ try {
888
+ friend = await store.get(params.friendId);
889
+ }
890
+ catch {
891
+ friend = null;
892
+ }
893
+ if (!friend) {
894
+ (0, runtime_1.emitNervesEvent)({
895
+ level: "warn",
896
+ component: "senses",
897
+ event: "senses.teams_proactive_no_friend",
898
+ message: "proactive send skipped: friend not found",
899
+ meta: { friendId: params.friendId, sessionKey: params.sessionKey },
900
+ });
901
+ return { delivered: false, reason: "friend_not_found" };
902
+ }
903
+ if (!hasExplicitCrossChatAuthorization(params) && !types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
904
+ (0, runtime_1.emitNervesEvent)({
905
+ component: "senses",
906
+ event: "senses.teams_proactive_trust_skip",
907
+ message: "proactive send skipped: trust level not allowed",
908
+ meta: {
909
+ friendId: params.friendId,
910
+ trustLevel: friend.trustLevel ?? "unknown",
911
+ intent: params.intent ?? "generic_outreach",
912
+ authorizingTrustLevel: params.authorizingSession?.trustLevel ?? null,
913
+ },
914
+ });
915
+ return { delivered: false, reason: "trust_skip" };
916
+ }
917
+ const aadInfo = findAadObjectId(friend);
918
+ if (!aadInfo) {
919
+ (0, runtime_1.emitNervesEvent)({
920
+ level: "warn",
921
+ component: "senses",
922
+ event: "senses.teams_proactive_no_aad_id",
923
+ message: "proactive send skipped: no AAD object ID found",
924
+ meta: { friendId: params.friendId, sessionKey: params.sessionKey },
925
+ });
926
+ return { delivered: false, reason: "missing_target" };
927
+ }
928
+ try {
929
+ const conversation = await conversations.create({
930
+ bot: { id: deps.botApi.id },
931
+ members: [{ id: aadInfo.aadObjectId, role: "user", name: friend.name || aadInfo.aadObjectId }],
932
+ tenantId: aadInfo.tenantId,
933
+ isGroup: false,
934
+ });
935
+ await conversations.activities(conversation.id).create({
936
+ type: "message",
937
+ text: params.text,
938
+ });
939
+ (0, runtime_1.emitNervesEvent)({
940
+ component: "senses",
941
+ event: "senses.teams_proactive_sent",
942
+ message: "proactive teams message sent",
943
+ meta: { friendId: params.friendId, aadObjectId: aadInfo.aadObjectId, sessionKey: params.sessionKey },
944
+ });
945
+ return { delivered: true };
946
+ }
947
+ catch (error) {
948
+ (0, runtime_1.emitNervesEvent)({
949
+ level: "error",
950
+ component: "senses",
951
+ event: "senses.teams_proactive_send_error",
952
+ message: "proactive teams send failed",
953
+ meta: {
954
+ friendId: params.friendId,
955
+ aadObjectId: aadInfo.aadObjectId,
956
+ sessionKey: params.sessionKey,
957
+ reason: error instanceof Error ? error.message : String(error),
958
+ },
959
+ });
960
+ return { delivered: false, reason: "send_error" };
961
+ }
962
+ }
870
963
  function scanPendingTeamsFiles(pendingRoot) {
871
964
  const results = [];
872
965
  let friendIds;
@@ -912,8 +1005,7 @@ async function drainAndSendPendingTeams(store, botApi, pendingRoot) {
912
1005
  const root = pendingRoot ?? path.join((0, identity_1.getAgentRoot)(), "state", "pending");
913
1006
  const pendingFiles = scanPendingTeamsFiles(root);
914
1007
  const result = { sent: 0, skipped: 0, failed: 0 };
915
- const conversations = botApi.conversations;
916
- for (const { friendId, filePath, content } of pendingFiles) {
1008
+ for (const { friendId, key, filePath, content } of pendingFiles) {
917
1009
  let parsed;
918
1010
  try {
919
1011
  parsed = JSON.parse(content);
@@ -935,95 +1027,32 @@ async function drainAndSendPendingTeams(store, botApi, pendingRoot) {
935
1027
  catch { /* ignore */ }
936
1028
  continue;
937
1029
  }
938
- let friend;
939
- try {
940
- friend = await store.get(friendId);
941
- }
942
- catch {
943
- friend = null;
944
- }
945
- if (!friend) {
946
- result.skipped++;
947
- try {
948
- fs.unlinkSync(filePath);
949
- }
950
- catch { /* ignore */ }
951
- (0, runtime_1.emitNervesEvent)({
952
- level: "warn",
953
- component: "senses",
954
- event: "senses.teams_proactive_no_friend",
955
- message: "proactive send skipped: friend not found",
956
- meta: { friendId },
957
- });
958
- continue;
959
- }
960
- if (!types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
961
- result.skipped++;
1030
+ const sendResult = await sendProactiveTeamsMessageToSession({
1031
+ friendId,
1032
+ sessionKey: key,
1033
+ text: messageText,
1034
+ intent: "generic_outreach",
1035
+ }, {
1036
+ botApi,
1037
+ store,
1038
+ });
1039
+ if (sendResult.delivered) {
1040
+ result.sent++;
962
1041
  try {
963
1042
  fs.unlinkSync(filePath);
964
1043
  }
965
1044
  catch { /* ignore */ }
966
- (0, runtime_1.emitNervesEvent)({
967
- component: "senses",
968
- event: "senses.teams_proactive_trust_skip",
969
- message: "proactive send skipped: trust level not allowed",
970
- meta: { friendId, trustLevel: friend.trustLevel ?? "unknown" },
971
- });
972
1045
  continue;
973
1046
  }
974
- const aadInfo = findAadObjectId(friend);
975
- if (!aadInfo) {
1047
+ if (sendResult.reason === "friend_not_found" || sendResult.reason === "trust_skip" || sendResult.reason === "missing_target") {
976
1048
  result.skipped++;
977
1049
  try {
978
1050
  fs.unlinkSync(filePath);
979
1051
  }
980
1052
  catch { /* ignore */ }
981
- (0, runtime_1.emitNervesEvent)({
982
- level: "warn",
983
- component: "senses",
984
- event: "senses.teams_proactive_no_aad_id",
985
- message: "proactive send skipped: no AAD object ID found",
986
- meta: { friendId },
987
- });
988
1053
  continue;
989
1054
  }
990
- try {
991
- const conversation = await conversations.create({
992
- bot: { id: botApi.id },
993
- members: [{ id: aadInfo.aadObjectId, role: "user", name: friend.name || aadInfo.aadObjectId }],
994
- tenantId: aadInfo.tenantId,
995
- isGroup: false,
996
- });
997
- await conversations.activities(conversation.id).create({
998
- type: "message",
999
- text: messageText,
1000
- });
1001
- result.sent++;
1002
- try {
1003
- fs.unlinkSync(filePath);
1004
- }
1005
- catch { /* ignore */ }
1006
- (0, runtime_1.emitNervesEvent)({
1007
- component: "senses",
1008
- event: "senses.teams_proactive_sent",
1009
- message: "proactive teams message sent",
1010
- meta: { friendId, aadObjectId: aadInfo.aadObjectId },
1011
- });
1012
- }
1013
- catch (error) {
1014
- result.failed++;
1015
- (0, runtime_1.emitNervesEvent)({
1016
- level: "error",
1017
- component: "senses",
1018
- event: "senses.teams_proactive_send_error",
1019
- message: "proactive teams send failed",
1020
- meta: {
1021
- friendId,
1022
- aadObjectId: aadInfo.aadObjectId,
1023
- reason: error instanceof Error ? error.message : String(error),
1024
- },
1025
- });
1026
- }
1055
+ result.failed++;
1027
1056
  }
1028
1057
  if (result.sent > 0 || result.skipped > 0 || result.failed > 0) {
1029
1058
  (0, runtime_1.emitNervesEvent)({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.51",
3
+ "version": "0.1.0-alpha.52",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",