@ouro.bot/cli 0.1.0-alpha.1 → 0.1.0-alpha.100

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 (132) 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 +596 -0
  7. package/dist/heart/active-work.js +251 -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/commitments.js +109 -0
  12. package/dist/heart/config.js +102 -23
  13. package/dist/heart/core.js +512 -94
  14. package/dist/heart/cross-chat-delivery.js +146 -0
  15. package/dist/heart/daemon/agent-discovery.js +81 -0
  16. package/dist/heart/daemon/auth-flow.js +430 -0
  17. package/dist/heart/daemon/daemon-cli.js +1935 -185
  18. package/dist/heart/daemon/daemon-entry.js +55 -6
  19. package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
  20. package/dist/heart/daemon/daemon.js +218 -9
  21. package/dist/heart/daemon/hatch-animation.js +35 -0
  22. package/dist/heart/daemon/hatch-flow.js +10 -83
  23. package/dist/heart/daemon/hatch-specialist.js +6 -1
  24. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  25. package/dist/heart/daemon/launchd.js +159 -0
  26. package/dist/heart/daemon/log-tailer.js +147 -0
  27. package/dist/heart/daemon/message-router.js +17 -8
  28. package/dist/heart/daemon/os-cron.js +260 -0
  29. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  30. package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
  31. package/dist/heart/daemon/ouro-path-installer.js +260 -0
  32. package/dist/heart/daemon/ouro-uti.js +11 -2
  33. package/dist/heart/daemon/ouro-version-manager.js +171 -0
  34. package/dist/heart/daemon/process-manager.js +32 -2
  35. package/dist/heart/daemon/run-hooks.js +37 -0
  36. package/dist/heart/daemon/runtime-logging.js +61 -14
  37. package/dist/heart/daemon/runtime-metadata.js +219 -0
  38. package/dist/heart/daemon/runtime-mode.js +67 -0
  39. package/dist/heart/daemon/sense-manager.js +307 -0
  40. package/dist/heart/daemon/skill-management-installer.js +94 -0
  41. package/dist/heart/daemon/socket-client.js +202 -0
  42. package/dist/heart/daemon/specialist-orchestrator.js +129 -0
  43. package/dist/heart/daemon/specialist-prompt.js +99 -0
  44. package/dist/heart/daemon/specialist-tools.js +283 -0
  45. package/dist/heart/daemon/staged-restart.js +114 -0
  46. package/dist/heart/daemon/task-scheduler.js +4 -1
  47. package/dist/heart/daemon/thoughts.js +507 -0
  48. package/dist/heart/daemon/update-checker.js +111 -0
  49. package/dist/heart/daemon/update-hooks.js +138 -0
  50. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  51. package/dist/heart/delegation.js +62 -0
  52. package/dist/heart/identity.js +153 -23
  53. package/dist/heart/kicks.js +1 -19
  54. package/dist/heart/model-capabilities.js +48 -0
  55. package/dist/heart/obligations.js +191 -0
  56. package/dist/heart/progress-story.js +42 -0
  57. package/dist/heart/providers/anthropic.js +77 -9
  58. package/dist/heart/providers/azure.js +86 -7
  59. package/dist/heart/providers/github-copilot.js +149 -0
  60. package/dist/heart/providers/minimax.js +4 -0
  61. package/dist/heart/providers/openai-codex.js +12 -3
  62. package/dist/heart/safe-workspace.js +381 -0
  63. package/dist/heart/sense-truth.js +61 -0
  64. package/dist/heart/session-activity.js +169 -0
  65. package/dist/heart/session-recall.js +116 -0
  66. package/dist/heart/streaming.js +103 -22
  67. package/dist/heart/target-resolution.js +123 -0
  68. package/dist/heart/turn-coordinator.js +28 -0
  69. package/dist/mind/associative-recall.js +37 -4
  70. package/dist/mind/bundle-manifest.js +70 -0
  71. package/dist/mind/context.js +141 -11
  72. package/dist/mind/first-impressions.js +16 -2
  73. package/dist/mind/friends/channel.js +43 -0
  74. package/dist/mind/friends/group-context.js +144 -0
  75. package/dist/mind/friends/store-file.js +19 -0
  76. package/dist/mind/friends/trust-explanation.js +74 -0
  77. package/dist/mind/friends/types.js +9 -1
  78. package/dist/mind/memory.js +89 -26
  79. package/dist/mind/obligation-steering.js +31 -0
  80. package/dist/mind/pending.js +160 -0
  81. package/dist/mind/phrases.js +1 -0
  82. package/dist/mind/prompt-refresh.js +20 -0
  83. package/dist/mind/prompt.js +499 -8
  84. package/dist/mind/token-estimate.js +8 -12
  85. package/dist/nerves/cli-logging.js +15 -2
  86. package/dist/nerves/coverage/file-completeness.js +14 -4
  87. package/dist/nerves/coverage/run-artifacts.js +1 -1
  88. package/dist/nerves/index.js +12 -0
  89. package/dist/repertoire/ado-client.js +4 -2
  90. package/dist/repertoire/coding/feedback.js +210 -0
  91. package/dist/repertoire/coding/index.js +4 -1
  92. package/dist/repertoire/coding/manager.js +69 -4
  93. package/dist/repertoire/coding/spawner.js +21 -3
  94. package/dist/repertoire/coding/tools.js +105 -2
  95. package/dist/repertoire/data/ado-endpoints.json +188 -0
  96. package/dist/repertoire/guardrails.js +290 -0
  97. package/dist/repertoire/mcp-client.js +254 -0
  98. package/dist/repertoire/mcp-manager.js +195 -0
  99. package/dist/repertoire/skills.js +3 -26
  100. package/dist/repertoire/tasks/board.js +12 -0
  101. package/dist/repertoire/tasks/index.js +23 -9
  102. package/dist/repertoire/tasks/transitions.js +1 -2
  103. package/dist/repertoire/tools-base.js +770 -213
  104. package/dist/repertoire/tools-bluebubbles.js +93 -0
  105. package/dist/repertoire/tools-teams.js +58 -25
  106. package/dist/repertoire/tools.js +106 -53
  107. package/dist/senses/bluebubbles-client.js +484 -0
  108. package/dist/senses/bluebubbles-entry.js +13 -0
  109. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  110. package/dist/senses/bluebubbles-media.js +339 -0
  111. package/dist/senses/bluebubbles-model.js +261 -0
  112. package/dist/senses/bluebubbles-mutation-log.js +116 -0
  113. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  114. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  115. package/dist/senses/bluebubbles.js +1181 -0
  116. package/dist/senses/cli-layout.js +187 -0
  117. package/dist/senses/cli.js +452 -99
  118. package/dist/senses/continuity.js +94 -0
  119. package/dist/senses/debug-activity.js +154 -0
  120. package/dist/senses/inner-dialog-worker.js +47 -18
  121. package/dist/senses/inner-dialog.js +387 -70
  122. package/dist/senses/pipeline.js +307 -0
  123. package/dist/senses/session-lock.js +119 -0
  124. package/dist/senses/teams.js +574 -129
  125. package/dist/senses/trust-gate.js +112 -2
  126. package/package.json +16 -4
  127. package/subagents/README.md +4 -68
  128. package/dist/heart/daemon/subagent-installer.js +0 -125
  129. package/dist/inner-worker-entry.js +0 -4
  130. package/subagents/work-doer.md +0 -233
  131. package/subagents/work-merger.md +0 -593
  132. package/subagents/work-planner.md +0 -373
@@ -87,7 +87,19 @@ function readFacts(memoryRoot) {
87
87
  const raw = fs.readFileSync(factsPath, "utf8").trim();
88
88
  if (!raw)
89
89
  return [];
90
- return raw.split("\n").map((line) => JSON.parse(line));
90
+ const facts = [];
91
+ for (const line of raw.split("\n")) {
92
+ const trimmed = line.trim();
93
+ if (!trimmed)
94
+ continue;
95
+ try {
96
+ facts.push(JSON.parse(trimmed));
97
+ }
98
+ catch {
99
+ // Skip corrupt lines (e.g. partial write from a crash).
100
+ }
101
+ }
102
+ return facts;
91
103
  }
92
104
  function getLatestUserText(messages) {
93
105
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -144,8 +156,29 @@ async function injectAssociativeRecall(messages, options) {
144
156
  const facts = readFacts(memoryRoot);
145
157
  if (facts.length === 0)
146
158
  return;
147
- const provider = options?.provider ?? createDefaultProvider();
148
- const recalled = await recallFactsForQuery(query, facts, provider, options);
159
+ let recalled;
160
+ try {
161
+ const provider = options?.provider ?? createDefaultProvider();
162
+ recalled = await recallFactsForQuery(query, facts, provider, options);
163
+ }
164
+ catch {
165
+ // Embeddings unavailable — fall back to substring matching
166
+ const lowerQuery = query.toLowerCase();
167
+ const topK = options?.topK ?? DEFAULT_TOP_K;
168
+ recalled = facts
169
+ .filter((fact) => fact.text.toLowerCase().includes(lowerQuery))
170
+ .slice(0, topK)
171
+ .map((fact) => ({ ...fact, score: 1 }));
172
+ if (recalled.length > 0) {
173
+ (0, runtime_1.emitNervesEvent)({
174
+ level: "warn",
175
+ component: "mind",
176
+ event: "mind.associative_recall_fallback",
177
+ message: "embeddings unavailable, used substring fallback",
178
+ meta: { matchCount: recalled.length },
179
+ });
180
+ }
181
+ }
149
182
  if (recalled.length === 0)
150
183
  return;
151
184
  const recallSection = recalled
@@ -169,7 +202,7 @@ async function injectAssociativeRecall(messages, options) {
169
202
  event: "mind.associative_recall_error",
170
203
  message: "associative recall failed",
171
204
  meta: {
172
- reason: error instanceof Error ? error.message : String(error),
205
+ reason: error instanceof Error ? error.message : /* v8 ignore start -- defensive: non-Error catch branch @preserve */ String(error) /* v8 ignore stop */,
173
206
  },
174
207
  });
175
208
  }
@@ -34,6 +34,11 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.CANONICAL_BUNDLE_MANIFEST = void 0;
37
+ exports.getChangelogPath = getChangelogPath;
38
+ exports.getPackageVersion = getPackageVersion;
39
+ exports.createBundleMeta = createBundleMeta;
40
+ exports.backfillBundleMeta = backfillBundleMeta;
41
+ exports.resetBackfillTracking = resetBackfillTracking;
37
42
  exports.isCanonicalBundlePath = isCanonicalBundlePath;
38
43
  exports.findNonCanonicalBundlePaths = findNonCanonicalBundlePaths;
39
44
  const fs = __importStar(require("fs"));
@@ -41,6 +46,7 @@ const path = __importStar(require("path"));
41
46
  const runtime_1 = require("../nerves/runtime");
42
47
  exports.CANONICAL_BUNDLE_MANIFEST = [
43
48
  { path: "agent.json", kind: "file" },
49
+ { path: "bundle-meta.json", kind: "file" },
44
50
  { path: "psyche/SOUL.md", kind: "file" },
45
51
  { path: "psyche/IDENTITY.md", kind: "file" },
46
52
  { path: "psyche/LORE.md", kind: "file" },
@@ -48,11 +54,75 @@ exports.CANONICAL_BUNDLE_MANIFEST = [
48
54
  { path: "psyche/ASPIRATIONS.md", kind: "file" },
49
55
  { path: "psyche/memory", kind: "dir" },
50
56
  { path: "friends", kind: "dir" },
57
+ { path: "state", kind: "dir" },
51
58
  { path: "tasks", kind: "dir" },
52
59
  { path: "skills", kind: "dir" },
53
60
  { path: "senses", kind: "dir" },
54
61
  { path: "senses/teams", kind: "dir" },
55
62
  ];
63
+ function getChangelogPath() {
64
+ const changelogPath = path.resolve(__dirname, "../../changelog.json");
65
+ (0, runtime_1.emitNervesEvent)({
66
+ component: "mind",
67
+ event: "mind.changelog_path_resolved",
68
+ message: "resolved changelog path",
69
+ meta: { path: changelogPath },
70
+ });
71
+ return changelogPath;
72
+ }
73
+ function getPackageVersion() {
74
+ const packageJsonPath = path.resolve(__dirname, "../../package.json");
75
+ const raw = fs.readFileSync(packageJsonPath, "utf-8");
76
+ const parsed = JSON.parse(raw);
77
+ (0, runtime_1.emitNervesEvent)({
78
+ component: "mind",
79
+ event: "mind.package_version_read",
80
+ message: "read package version",
81
+ meta: { version: parsed.version },
82
+ });
83
+ return parsed.version;
84
+ }
85
+ function createBundleMeta() {
86
+ return {
87
+ runtimeVersion: getPackageVersion(),
88
+ bundleSchemaVersion: 1,
89
+ lastUpdated: new Date().toISOString(),
90
+ };
91
+ }
92
+ const _backfilledRoots = new Set();
93
+ /**
94
+ * If bundle-meta.json is missing from the agent root, create it with current runtime version.
95
+ * This backfills existing agent bundles that were created before bundle-meta.json was introduced.
96
+ * Only attempts once per bundleRoot per process.
97
+ */
98
+ function backfillBundleMeta(bundleRoot) {
99
+ if (_backfilledRoots.has(bundleRoot))
100
+ return;
101
+ _backfilledRoots.add(bundleRoot);
102
+ const metaPath = path.join(bundleRoot, "bundle-meta.json");
103
+ try {
104
+ if (fs.existsSync(metaPath)) {
105
+ return;
106
+ }
107
+ const meta = createBundleMeta();
108
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
109
+ (0, runtime_1.emitNervesEvent)({
110
+ component: "mind",
111
+ event: "mind.bundle_meta_backfill",
112
+ message: "backfilled missing bundle-meta.json",
113
+ meta: { bundleRoot },
114
+ });
115
+ }
116
+ catch {
117
+ // Non-blocking: if we can't write, that's okay
118
+ }
119
+ }
120
+ /**
121
+ * Reset the backfill tracking set. Used in tests.
122
+ */
123
+ function resetBackfillTracking() {
124
+ _backfilledRoots.clear();
125
+ }
56
126
  const CANONICAL_FILE_PATHS = new Set(exports.CANONICAL_BUNDLE_MANIFEST
57
127
  .filter((entry) => entry.kind === "file")
58
128
  .map((entry) => entry.path));
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.trimMessages = trimMessages;
37
+ exports.validateSessionMessages = validateSessionMessages;
38
+ exports.repairSessionMessages = repairSessionMessages;
37
39
  exports.saveSession = saveSession;
38
40
  exports.loadSession = loadSession;
39
41
  exports.postTurn = postTurn;
@@ -48,17 +50,17 @@ function buildTrimmableBlocks(messages) {
48
50
  let i = 0;
49
51
  while (i < messages.length) {
50
52
  const msg = messages[i];
51
- if (msg?.role === "system") {
53
+ if (msg.role === "system") {
52
54
  i++;
53
55
  continue;
54
56
  }
55
57
  // Tool coherence block: assistant message with tool_calls + immediately following tool results
56
- if (msg?.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
58
+ if (msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
57
59
  const indices = [i];
58
60
  i++;
59
61
  while (i < messages.length) {
60
62
  const next = messages[i];
61
- if (next?.role !== "tool")
63
+ if (next.role !== "tool")
62
64
  break;
63
65
  indices.push(i);
64
66
  i++;
@@ -76,13 +78,13 @@ function buildTrimmableBlocks(messages) {
76
78
  function getSystemMessageIndices(messages) {
77
79
  const indices = [];
78
80
  for (let i = 0; i < messages.length; i++) {
79
- if (messages[i]?.role === "system")
81
+ if (messages[i].role === "system")
80
82
  indices.push(i);
81
83
  }
82
84
  return indices;
83
85
  }
84
86
  function buildTrimmedMessages(messages, kept) {
85
- return messages.filter((m, idx) => m?.role === "system" || kept.has(idx));
87
+ return messages.filter((m, idx) => m.role === "system" || kept.has(idx));
86
88
  }
87
89
  function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
88
90
  const targetTokens = Math.floor(maxTokens * (1 - contextMargin / 100));
@@ -130,7 +132,7 @@ function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
130
132
  let remaining = actualTokenCount;
131
133
  const kept = new Set();
132
134
  for (let i = 0; i < messages.length; i++) {
133
- if (messages[i]?.role !== "system")
135
+ if (messages[i].role !== "system")
134
136
  kept.add(i);
135
137
  }
136
138
  // Drop oldest blocks until we fall under target.
@@ -144,7 +146,7 @@ function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
144
146
  let trimmed = buildTrimmedMessages(messages, kept);
145
147
  // If we're still above budget after dropping everything trimmable, preserve system only.
146
148
  if (remaining > targetTokens) {
147
- trimmed = messages.filter((m) => m?.role === "system");
149
+ trimmed = messages.filter((m) => m.role === "system");
148
150
  }
149
151
  const estimatedAfter = (0, token_estimate_1.estimateTokensForMessages)(trimmed);
150
152
  (0, runtime_1.emitNervesEvent)({
@@ -166,11 +168,116 @@ function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
166
168
  });
167
169
  return trimmed;
168
170
  }
169
- function saveSession(filePath, messages, lastUsage) {
171
+ /**
172
+ * Checks session invariant: after system messages, sequence must be
173
+ * user → assistant (with optional tool calls/results) → user → assistant...
174
+ * Never assistant → assistant without a user in between.
175
+ */
176
+ function validateSessionMessages(messages) {
177
+ const violations = [];
178
+ let prevNonToolRole = null;
179
+ let prevAssistantHadToolCalls = false;
180
+ let sawToolResultSincePrevAssistant = false;
181
+ for (let i = 0; i < messages.length; i++) {
182
+ const msg = messages[i];
183
+ if (msg.role === "system")
184
+ continue;
185
+ if (msg.role === "tool") {
186
+ sawToolResultSincePrevAssistant = true;
187
+ continue;
188
+ }
189
+ if (msg.role === "assistant" && prevNonToolRole === "assistant") {
190
+ // assistant → tool(s) → assistant is valid (tool call flow)
191
+ if (!(prevAssistantHadToolCalls && sawToolResultSincePrevAssistant)) {
192
+ violations.push(`back-to-back assistant at index ${i}`);
193
+ }
194
+ }
195
+ prevAssistantHadToolCalls = msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0;
196
+ sawToolResultSincePrevAssistant = false;
197
+ prevNonToolRole = msg.role;
198
+ }
199
+ return violations;
200
+ }
201
+ /**
202
+ * Repairs session invariant violations by merging consecutive assistant messages.
203
+ */
204
+ function repairSessionMessages(messages) {
205
+ const violations = validateSessionMessages(messages);
206
+ if (violations.length === 0)
207
+ return messages;
208
+ const result = [];
209
+ for (const msg of messages) {
210
+ if (msg.role === "assistant" && result.length > 0) {
211
+ const prev = result[result.length - 1];
212
+ if (prev.role === "assistant" && !("tool_calls" in prev)) {
213
+ const prevContent = typeof prev.content === "string" ? prev.content : "";
214
+ const curContent = typeof msg.content === "string" ? msg.content : "";
215
+ prev.content = `${prevContent}\n\n${curContent}`;
216
+ continue;
217
+ }
218
+ }
219
+ result.push(msg);
220
+ }
221
+ (0, runtime_1.emitNervesEvent)({
222
+ level: "info",
223
+ event: "mind.session_invariant_repair",
224
+ component: "mind",
225
+ message: "repaired session invariant violations",
226
+ meta: { violations },
227
+ });
228
+ return result;
229
+ }
230
+ function stripOrphanedToolResults(messages) {
231
+ const validCallIds = new Set();
232
+ for (const msg of messages) {
233
+ if (msg.role !== "assistant" || !Array.isArray(msg.tool_calls))
234
+ continue;
235
+ for (const toolCall of msg.tool_calls)
236
+ validCallIds.add(toolCall.id);
237
+ }
238
+ let removed = 0;
239
+ const repaired = messages.filter((msg) => {
240
+ if (msg.role !== "tool")
241
+ return true;
242
+ const keep = validCallIds.has(msg.tool_call_id);
243
+ if (!keep)
244
+ removed++;
245
+ return keep;
246
+ });
247
+ if (removed > 0) {
248
+ (0, runtime_1.emitNervesEvent)({
249
+ level: "info",
250
+ event: "mind.session_orphan_tool_result_repair",
251
+ component: "mind",
252
+ message: "removed orphaned tool results from session history",
253
+ meta: { removed },
254
+ });
255
+ }
256
+ return repaired;
257
+ }
258
+ function saveSession(filePath, messages, lastUsage, state) {
259
+ const violations = validateSessionMessages(messages);
260
+ if (violations.length > 0) {
261
+ (0, runtime_1.emitNervesEvent)({
262
+ level: "info",
263
+ event: "mind.session_invariant_violation",
264
+ component: "mind",
265
+ message: "session invariant violated on save",
266
+ meta: { path: filePath, violations },
267
+ });
268
+ messages = repairSessionMessages(messages);
269
+ }
270
+ messages = stripOrphanedToolResults(messages);
170
271
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
171
272
  const envelope = { version: 1, messages };
172
273
  if (lastUsage)
173
274
  envelope.lastUsage = lastUsage;
275
+ if (state?.mustResolveBeforeHandoff === true || typeof state?.lastFriendActivityAt === "string") {
276
+ envelope.state = {
277
+ ...(state?.mustResolveBeforeHandoff === true ? { mustResolveBeforeHandoff: true } : {}),
278
+ ...(typeof state?.lastFriendActivityAt === "string" ? { lastFriendActivityAt: state.lastFriendActivityAt } : {}),
279
+ };
280
+ }
174
281
  fs.writeFileSync(filePath, JSON.stringify(envelope, null, 2));
175
282
  }
176
283
  function loadSession(filePath) {
@@ -179,13 +286,36 @@ function loadSession(filePath) {
179
286
  const data = JSON.parse(raw);
180
287
  if (data.version !== 1)
181
288
  return null;
182
- return { messages: data.messages, lastUsage: data.lastUsage };
289
+ let messages = data.messages;
290
+ const violations = validateSessionMessages(messages);
291
+ if (violations.length > 0) {
292
+ (0, runtime_1.emitNervesEvent)({
293
+ level: "info",
294
+ event: "mind.session_invariant_violation",
295
+ component: "mind",
296
+ message: "session invariant violated on load",
297
+ meta: { path: filePath, violations },
298
+ });
299
+ messages = repairSessionMessages(messages);
300
+ }
301
+ messages = stripOrphanedToolResults(messages);
302
+ const rawState = data?.state && typeof data.state === "object" && data.state !== null
303
+ ? data.state
304
+ : undefined;
305
+ const state = rawState && (rawState.mustResolveBeforeHandoff === true
306
+ || typeof rawState.lastFriendActivityAt === "string")
307
+ ? {
308
+ ...(rawState.mustResolveBeforeHandoff === true ? { mustResolveBeforeHandoff: true } : {}),
309
+ ...(typeof rawState.lastFriendActivityAt === "string" ? { lastFriendActivityAt: rawState.lastFriendActivityAt } : {}),
310
+ }
311
+ : undefined;
312
+ return { messages, lastUsage: data.lastUsage, state };
183
313
  }
184
314
  catch {
185
315
  return null;
186
316
  }
187
317
  }
188
- function postTurn(messages, sessPath, usage, hooks) {
318
+ function postTurn(messages, sessPath, usage, hooks, state) {
189
319
  if (hooks?.beforeTrim) {
190
320
  try {
191
321
  hooks.beforeTrim([...messages]);
@@ -205,7 +335,7 @@ function postTurn(messages, sessPath, usage, hooks) {
205
335
  const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
206
336
  const trimmed = trimMessages(messages, maxTokens, contextMargin, usage?.input_tokens);
207
337
  messages.splice(0, messages.length, ...trimmed);
208
- saveSession(sessPath, messages, usage);
338
+ saveSession(sessPath, messages, usage, state);
209
339
  }
210
340
  function deleteSession(filePath) {
211
341
  try {
@@ -11,9 +11,22 @@ exports.ONBOARDING_TOKEN_THRESHOLD = 100_000;
11
11
  function isOnboarding(friend) {
12
12
  return (friend.totalTokens ?? 0) < exports.ONBOARDING_TOKEN_THRESHOLD;
13
13
  }
14
- function getFirstImpressions(friend) {
14
+ function hasLiveContinuityPressure(state) {
15
+ if (!state)
16
+ return false;
17
+ if (typeof state.currentObligation === "string" && state.currentObligation.trim().length > 0)
18
+ return true;
19
+ if (state.mustResolveBeforeHandoff === true)
20
+ return true;
21
+ if (state.hasQueuedFollowUp === true)
22
+ return true;
23
+ return false;
24
+ }
25
+ function getFirstImpressions(friend, state) {
15
26
  if (!isOnboarding(friend))
16
27
  return "";
28
+ if (hasLiveContinuityPressure(state))
29
+ return "";
17
30
  (0, runtime_1.emitNervesEvent)({
18
31
  component: "mind",
19
32
  event: "mind.first_impressions",
@@ -37,7 +50,8 @@ function getFirstImpressions(friend) {
37
50
  lines.push("- what do they do outside of work that they care about?");
38
51
  lines.push("i don't ask all of these at once -- i weave them into conversation naturally, one or two at a time, and i genuinely follow up on what they share.");
39
52
  lines.push("i introduce what i can do -- i have tools, integrations, and skills that can help them. i mention these naturally as they become relevant.");
40
- lines.push("if my friend hasn't asked me to do something specific, or i've already finished what they asked for, that's my cue to turn the tables -- i ask them questions about themselves, what they're into, what they need. no idle small talk; i'm on a mission to get to know them.");
53
+ lines.push("if we're already in motion on a task, thread, or follow-up, i do not reset with a generic opener like 'hiya' or 'what do ya need help with?'. i continue directly or ask the specific next question.");
54
+ lines.push("only when the conversation is genuinely fresh and idle, with no active ask or thread in flight, a light opener is okay.");
41
55
  lines.push("i save everything i learn immediately with save_friend_note -- names, roles, preferences, projects, anything. the bar is low: if i learned it, i save it.");
42
56
  return lines.join("\n");
43
57
  }
@@ -3,10 +3,13 @@
3
3
  // Pure lookup, no I/O, cannot fail. Unknown channel gets minimal defaults.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.getChannelCapabilities = getChannelCapabilities;
6
+ exports.isRemoteChannel = isRemoteChannel;
7
+ exports.getAlwaysOnSenseNames = getAlwaysOnSenseNames;
6
8
  const runtime_1 = require("../../nerves/runtime");
7
9
  const CHANNEL_CAPABILITIES = {
8
10
  cli: {
9
11
  channel: "cli",
12
+ senseType: "local",
10
13
  availableIntegrations: [],
11
14
  supportsMarkdown: false,
12
15
  supportsStreaming: true,
@@ -15,15 +18,35 @@ const CHANNEL_CAPABILITIES = {
15
18
  },
16
19
  teams: {
17
20
  channel: "teams",
21
+ senseType: "closed",
18
22
  availableIntegrations: ["ado", "graph", "github"],
19
23
  supportsMarkdown: true,
20
24
  supportsStreaming: true,
21
25
  supportsRichCards: true,
22
26
  maxMessageLength: Infinity,
23
27
  },
28
+ bluebubbles: {
29
+ channel: "bluebubbles",
30
+ senseType: "open",
31
+ availableIntegrations: [],
32
+ supportsMarkdown: false,
33
+ supportsStreaming: false,
34
+ supportsRichCards: false,
35
+ maxMessageLength: Infinity,
36
+ },
37
+ inner: {
38
+ channel: "inner",
39
+ senseType: "internal",
40
+ availableIntegrations: [],
41
+ supportsMarkdown: false,
42
+ supportsStreaming: true,
43
+ supportsRichCards: false,
44
+ maxMessageLength: Infinity,
45
+ },
24
46
  };
25
47
  const DEFAULT_CAPABILITIES = {
26
48
  channel: "cli",
49
+ senseType: "local",
27
50
  availableIntegrations: [],
28
51
  supportsMarkdown: false,
29
52
  supportsStreaming: false,
@@ -39,3 +62,23 @@ function getChannelCapabilities(channel) {
39
62
  });
40
63
  return CHANNEL_CAPABILITIES[channel] ?? DEFAULT_CAPABILITIES;
41
64
  }
65
+ /** Whether the channel is remote (open or closed) vs local/internal. */
66
+ function isRemoteChannel(capabilities) {
67
+ const senseType = capabilities?.senseType;
68
+ return senseType !== undefined && senseType !== "local" && senseType !== "internal";
69
+ }
70
+ /**
71
+ * Returns channel names whose senseType is "open" or "closed" -- i.e. channels
72
+ * that are always-on (daemon-managed) rather than interactive or internal.
73
+ */
74
+ function getAlwaysOnSenseNames() {
75
+ (0, runtime_1.emitNervesEvent)({
76
+ component: "channels",
77
+ event: "channel.always_on_lookup",
78
+ message: "always-on sense names lookup",
79
+ meta: {},
80
+ });
81
+ return Object.entries(CHANNEL_CAPABILITIES)
82
+ .filter(([, cap]) => cap.senseType === "open" || cap.senseType === "closed")
83
+ .map(([channel]) => channel);
84
+ }
@@ -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
+ }
@@ -100,6 +100,25 @@ class FileFriendStore {
100
100
  }
101
101
  return entries.some((entry) => entry.endsWith(".json"));
102
102
  }
103
+ async listAll() {
104
+ let entries;
105
+ try {
106
+ entries = await fsPromises.readdir(this.friendsPath);
107
+ }
108
+ catch {
109
+ return [];
110
+ }
111
+ const records = [];
112
+ for (const entry of entries) {
113
+ if (!entry.endsWith(".json"))
114
+ continue;
115
+ const raw = await this.readJson(path.join(this.friendsPath, entry));
116
+ if (!raw)
117
+ continue;
118
+ records.push(this.normalize(raw));
119
+ }
120
+ return records;
121
+ }
103
122
  normalize(raw) {
104
123
  const trustLevel = raw.trustLevel;
105
124
  const normalizedTrustLevel = trustLevel === "family" ||