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

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 (106) 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 +117 -188
  5. package/assets/ouroboros.png +0 -0
  6. package/changelog.json +252 -0
  7. package/dist/heart/active-work.js +157 -0
  8. package/dist/heart/bridges/manager.js +358 -0
  9. package/dist/heart/bridges/state-machine.js +135 -0
  10. package/dist/heart/bridges/store.js +123 -0
  11. package/dist/heart/config.js +81 -8
  12. package/dist/heart/core.js +172 -52
  13. package/dist/heart/daemon/agent-discovery.js +81 -0
  14. package/dist/heart/daemon/daemon-cli.js +1099 -164
  15. package/dist/heart/daemon/daemon-entry.js +14 -5
  16. package/dist/heart/daemon/daemon-runtime-sync.js +90 -0
  17. package/dist/heart/daemon/daemon.js +184 -9
  18. package/dist/heart/daemon/hatch-animation.js +10 -3
  19. package/dist/heart/daemon/hatch-flow.js +3 -20
  20. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  21. package/dist/heart/daemon/launchd.js +151 -0
  22. package/dist/heart/daemon/message-router.js +15 -6
  23. package/dist/heart/daemon/ouro-bot-entry.js +0 -0
  24. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  25. package/dist/heart/daemon/ouro-entry.js +0 -0
  26. package/dist/heart/daemon/ouro-path-installer.js +178 -0
  27. package/dist/heart/daemon/ouro-uti.js +11 -2
  28. package/dist/heart/daemon/process-manager.js +1 -1
  29. package/dist/heart/daemon/run-hooks.js +37 -0
  30. package/dist/heart/daemon/runtime-metadata.js +118 -0
  31. package/dist/heart/daemon/sense-manager.js +290 -0
  32. package/dist/heart/daemon/socket-client.js +202 -0
  33. package/dist/heart/daemon/specialist-orchestrator.js +53 -84
  34. package/dist/heart/daemon/specialist-prompt.js +64 -5
  35. package/dist/heart/daemon/specialist-tools.js +213 -58
  36. package/dist/heart/daemon/staged-restart.js +114 -0
  37. package/dist/heart/daemon/subagent-installer.js +48 -7
  38. package/dist/heart/daemon/thoughts.js +379 -0
  39. package/dist/heart/daemon/update-checker.js +111 -0
  40. package/dist/heart/daemon/update-hooks.js +138 -0
  41. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  42. package/dist/heart/delegation.js +62 -0
  43. package/dist/heart/identity.js +82 -4
  44. package/dist/heart/kicks.js +1 -19
  45. package/dist/heart/model-capabilities.js +40 -0
  46. package/dist/heart/progress-story.js +42 -0
  47. package/dist/heart/providers/anthropic.js +72 -7
  48. package/dist/heart/providers/azure.js +8 -1
  49. package/dist/heart/providers/minimax.js +4 -0
  50. package/dist/heart/providers/openai-codex.js +10 -1
  51. package/dist/heart/sense-truth.js +61 -0
  52. package/dist/heart/session-activity.js +169 -0
  53. package/dist/heart/session-recall.js +116 -0
  54. package/dist/heart/streaming.js +100 -22
  55. package/dist/heart/turn-coordinator.js +28 -0
  56. package/dist/mind/associative-recall.js +14 -2
  57. package/dist/mind/bundle-manifest.js +70 -0
  58. package/dist/mind/context.js +27 -11
  59. package/dist/mind/first-impressions.js +16 -2
  60. package/dist/mind/friends/channel.js +43 -0
  61. package/dist/mind/friends/store-file.js +19 -0
  62. package/dist/mind/friends/types.js +9 -1
  63. package/dist/mind/memory.js +10 -3
  64. package/dist/mind/pending.js +72 -9
  65. package/dist/mind/phrases.js +1 -0
  66. package/dist/mind/prompt.js +275 -77
  67. package/dist/mind/token-estimate.js +8 -12
  68. package/dist/nerves/cli-logging.js +15 -2
  69. package/dist/repertoire/ado-client.js +4 -2
  70. package/dist/repertoire/coding/feedback.js +134 -0
  71. package/dist/repertoire/coding/index.js +4 -1
  72. package/dist/repertoire/coding/manager.js +62 -4
  73. package/dist/repertoire/coding/spawner.js +3 -3
  74. package/dist/repertoire/coding/tools.js +41 -2
  75. package/dist/repertoire/data/ado-endpoints.json +188 -0
  76. package/dist/repertoire/tasks/board.js +12 -0
  77. package/dist/repertoire/tasks/index.js +23 -9
  78. package/dist/repertoire/tasks/transitions.js +1 -2
  79. package/dist/repertoire/tools-base.js +496 -245
  80. package/dist/repertoire/tools-bluebubbles.js +93 -0
  81. package/dist/repertoire/tools-teams.js +58 -25
  82. package/dist/repertoire/tools.js +93 -49
  83. package/dist/senses/bluebubbles-client.js +484 -0
  84. package/dist/senses/bluebubbles-entry.js +13 -0
  85. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  86. package/dist/senses/bluebubbles-media.js +338 -0
  87. package/dist/senses/bluebubbles-model.js +261 -0
  88. package/dist/senses/bluebubbles-mutation-log.js +116 -0
  89. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  90. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  91. package/dist/senses/bluebubbles.js +1142 -0
  92. package/dist/senses/cli.js +340 -138
  93. package/dist/senses/continuity.js +94 -0
  94. package/dist/senses/debug-activity.js +148 -0
  95. package/dist/senses/inner-dialog-worker.js +47 -18
  96. package/dist/senses/inner-dialog.js +330 -84
  97. package/dist/senses/pipeline.js +256 -0
  98. package/dist/senses/teams.js +541 -129
  99. package/dist/senses/trust-gate.js +112 -2
  100. package/package.json +14 -3
  101. package/subagents/README.md +46 -33
  102. package/subagents/work-doer.md +28 -24
  103. package/subagents/work-merger.md +24 -30
  104. package/subagents/work-planner.md +44 -27
  105. package/dist/heart/daemon/specialist-session.js +0 -142
  106. package/dist/inner-worker-entry.js +0 -4
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FinalAnswerParser = void 0;
3
+ exports.FinalAnswerStreamer = exports.FinalAnswerParser = void 0;
4
4
  exports.toResponsesInput = toResponsesInput;
5
5
  exports.toResponsesTools = toResponsesTools;
6
6
  exports.streamChatCompletion = streamChatCompletion;
@@ -77,6 +77,89 @@ class FinalAnswerParser {
77
77
  }
78
78
  }
79
79
  exports.FinalAnswerParser = FinalAnswerParser;
80
+ // Shared helper: wraps FinalAnswerParser with onClearText + onTextChunk wiring.
81
+ // Used by all streaming providers (Chat Completions, Responses API, Anthropic)
82
+ // so the eager-match streaming pattern lives in one place.
83
+ class FinalAnswerStreamer {
84
+ parser = new FinalAnswerParser();
85
+ _detected = false;
86
+ callbacks;
87
+ constructor(callbacks) {
88
+ this.callbacks = callbacks;
89
+ }
90
+ get detected() { return this._detected; }
91
+ get streamed() { return this.parser.active; }
92
+ /** Mark final_answer as detected. Calls onClearText on the callbacks. */
93
+ activate() {
94
+ if (this._detected)
95
+ return;
96
+ this._detected = true;
97
+ this.callbacks.onClearText?.();
98
+ }
99
+ /** Feed an argument delta through the parser. Emits text via onTextChunk. */
100
+ processDelta(delta) {
101
+ if (!this._detected)
102
+ return;
103
+ const text = this.parser.process(delta);
104
+ if (text)
105
+ this.callbacks.onTextChunk(text);
106
+ }
107
+ }
108
+ exports.FinalAnswerStreamer = FinalAnswerStreamer;
109
+ function toResponsesUserContent(content) {
110
+ if (typeof content === "string") {
111
+ return content;
112
+ }
113
+ if (!Array.isArray(content)) {
114
+ return "";
115
+ }
116
+ const parts = [];
117
+ for (const part of content) {
118
+ if (!part || typeof part !== "object") {
119
+ continue;
120
+ }
121
+ if (part.type === "text" && typeof part.text === "string") {
122
+ parts.push({ type: "input_text", text: part.text });
123
+ continue;
124
+ }
125
+ if (part.type === "image_url") {
126
+ const imageUrl = typeof part.image_url?.url === "string" ? part.image_url.url : "";
127
+ if (!imageUrl)
128
+ continue;
129
+ parts.push({
130
+ type: "input_image",
131
+ image_url: imageUrl,
132
+ detail: part.image_url?.detail ?? "auto",
133
+ });
134
+ continue;
135
+ }
136
+ if (part.type === "input_audio" &&
137
+ typeof part.input_audio?.data === "string" &&
138
+ (part.input_audio.format === "mp3" || part.input_audio.format === "wav")) {
139
+ parts.push({
140
+ type: "input_audio",
141
+ input_audio: {
142
+ data: part.input_audio.data,
143
+ format: part.input_audio.format,
144
+ },
145
+ });
146
+ continue;
147
+ }
148
+ if (part.type === "file") {
149
+ const fileRecord = { type: "input_file" };
150
+ if (typeof part.file?.file_data === "string")
151
+ fileRecord.file_data = part.file.file_data;
152
+ if (typeof part.file?.file_id === "string")
153
+ fileRecord.file_id = part.file.file_id;
154
+ if (typeof part.file?.filename === "string")
155
+ fileRecord.filename = part.file.filename;
156
+ if (typeof part.file?.file_data === "string" || typeof part.file?.file_id === "string") {
157
+ parts.push(fileRecord);
158
+ }
159
+ }
160
+ }
161
+ return parts.length > 0 ? parts : "";
162
+ }
80
163
  function toResponsesInput(messages) {
81
164
  let instructions = "";
82
165
  const input = [];
@@ -90,7 +173,7 @@ function toResponsesInput(messages) {
90
173
  }
91
174
  if (msg.role === "user") {
92
175
  const u = msg;
93
- input.push({ role: "user", content: typeof u.content === "string" ? u.content : "" });
176
+ input.push({ role: "user", content: toResponsesUserContent(u.content) });
94
177
  continue;
95
178
  }
96
179
  if (msg.role === "assistant") {
@@ -102,7 +185,10 @@ function toResponsesInput(messages) {
102
185
  }
103
186
  }
104
187
  if (a.content) {
105
- input.push({ role: "assistant", content: typeof a.content === "string" ? a.content : "" });
188
+ const assistantItem = { role: "assistant", content: typeof a.content === "string" ? a.content : "" };
189
+ if (a.phase)
190
+ assistantItem.phase = a.phase;
191
+ input.push(assistantItem);
106
192
  }
107
193
  if (a.tool_calls) {
108
194
  for (const tc of a.tool_calls) {
@@ -155,8 +241,7 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
155
241
  let toolCalls = {};
156
242
  let streamStarted = false;
157
243
  let usage;
158
- const answerParser = new FinalAnswerParser();
159
- let finalAnswerDetected = false;
244
+ const answerStreamer = new FinalAnswerStreamer(callbacks);
160
245
  // State machine for parsing inline <think> tags (MiniMax pattern)
161
246
  let contentBuf = "";
162
247
  let inThinkTag = false;
@@ -274,21 +359,18 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
274
359
  // Detect final_answer tool call on first name delta.
275
360
  // Only activate streaming if this is the sole tool call (index 0
276
361
  // and no other indices seen). Mixed calls are rejected by core.ts.
277
- if (tc.function.name === "final_answer" && !finalAnswerDetected
362
+ if (tc.function.name === "final_answer" && !answerStreamer.detected
278
363
  && tc.index === 0 && Object.keys(toolCalls).length === 1) {
279
- finalAnswerDetected = true;
280
- callbacks.onClearText?.();
364
+ answerStreamer.activate();
281
365
  }
282
366
  }
283
367
  if (tc.function?.arguments) {
284
368
  toolCalls[tc.index].arguments += tc.function.arguments;
285
369
  // Feed final_answer argument deltas to the parser for progressive
286
370
  // streaming, but only when it appears to be the sole tool call.
287
- if (finalAnswerDetected && toolCalls[tc.index].name === "final_answer"
371
+ if (answerStreamer.detected && toolCalls[tc.index].name === "final_answer"
288
372
  && Object.keys(toolCalls).length === 1) {
289
- const text = answerParser.process(tc.function.arguments);
290
- if (text)
291
- callbacks.onTextChunk(text);
373
+ answerStreamer.processDelta(tc.function.arguments);
292
374
  }
293
375
  }
294
376
  }
@@ -302,7 +384,7 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
302
384
  toolCalls: Object.values(toolCalls),
303
385
  outputItems: [],
304
386
  usage,
305
- finalAnswerStreamed: answerParser.active,
387
+ finalAnswerStreamed: answerStreamer.streamed,
306
388
  };
307
389
  }
308
390
  async function streamResponsesApi(client, createParams, callbacks, signal) {
@@ -320,9 +402,8 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
320
402
  const outputItems = [];
321
403
  let currentToolCall = null;
322
404
  let usage;
323
- const answerParser = new FinalAnswerParser();
405
+ const answerStreamer = new FinalAnswerStreamer(callbacks);
324
406
  let functionCallCount = 0;
325
- let finalAnswerDetected = false;
326
407
  for await (const event of response) {
327
408
  if (signal?.aborted)
328
409
  break;
@@ -355,8 +436,7 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
355
436
  // Only activate when this is the first (and so far only) function call.
356
437
  // Mixed calls are rejected by core.ts; no need to stream their args.
357
438
  if (String(event.item.name) === "final_answer" && functionCallCount === 1) {
358
- finalAnswerDetected = true;
359
- callbacks.onClearText?.();
439
+ answerStreamer.activate();
360
440
  }
361
441
  }
362
442
  break;
@@ -366,11 +446,9 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
366
446
  currentToolCall.arguments += event.delta;
367
447
  // Feed final_answer argument deltas to the parser for progressive
368
448
  // streaming, but only when it appears to be the sole function call.
369
- if (finalAnswerDetected && currentToolCall.name === "final_answer"
449
+ if (answerStreamer.detected && currentToolCall.name === "final_answer"
370
450
  && functionCallCount === 1) {
371
- const text = answerParser.process(String(event.delta));
372
- if (text)
373
- callbacks.onTextChunk(text);
451
+ answerStreamer.processDelta(String(event.delta));
374
452
  }
375
453
  }
376
454
  break;
@@ -410,6 +488,6 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
410
488
  toolCalls,
411
489
  outputItems,
412
490
  usage,
413
- finalAnswerStreamed: answerParser.active,
491
+ finalAnswerStreamed: answerStreamer.streamed,
414
492
  };
415
493
  }
@@ -1,7 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createTurnCoordinator = createTurnCoordinator;
4
+ exports.withSharedTurnLock = withSharedTurnLock;
5
+ exports.tryBeginSharedTurn = tryBeginSharedTurn;
6
+ exports.endSharedTurn = endSharedTurn;
7
+ exports.isSharedTurnActive = isSharedTurnActive;
8
+ exports.enqueueSharedFollowUp = enqueueSharedFollowUp;
9
+ exports.drainSharedFollowUps = drainSharedFollowUps;
4
10
  const runtime_1 = require("../nerves/runtime");
11
+ function scopedKey(scope, key) {
12
+ return `${scope}:${key}`;
13
+ }
5
14
  function createTurnCoordinator() {
6
15
  const turnLocks = new Map();
7
16
  const activeTurns = new Set();
@@ -60,3 +69,22 @@ function createTurnCoordinator() {
60
69
  },
61
70
  };
62
71
  }
72
+ const _sharedTurnCoordinator = createTurnCoordinator();
73
+ function withSharedTurnLock(scope, key, fn) {
74
+ return _sharedTurnCoordinator.withTurnLock(scopedKey(scope, key), fn);
75
+ }
76
+ function tryBeginSharedTurn(scope, key) {
77
+ return _sharedTurnCoordinator.tryBeginTurn(scopedKey(scope, key));
78
+ }
79
+ function endSharedTurn(scope, key) {
80
+ _sharedTurnCoordinator.endTurn(scopedKey(scope, key));
81
+ }
82
+ function isSharedTurnActive(scope, key) {
83
+ return _sharedTurnCoordinator.isTurnActive(scopedKey(scope, key));
84
+ }
85
+ function enqueueSharedFollowUp(scope, key, followUp) {
86
+ _sharedTurnCoordinator.enqueueFollowUp(scopedKey(scope, key), followUp);
87
+ }
88
+ function drainSharedFollowUps(scope, key) {
89
+ return _sharedTurnCoordinator.drainFollowUps(scopedKey(scope, key));
90
+ }
@@ -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--) {
@@ -190,7 +202,7 @@ async function injectAssociativeRecall(messages, options) {
190
202
  event: "mind.associative_recall_error",
191
203
  message: "associative recall failed",
192
204
  meta: {
193
- 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 */,
194
206
  },
195
207
  });
196
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));
@@ -50,17 +50,17 @@ function buildTrimmableBlocks(messages) {
50
50
  let i = 0;
51
51
  while (i < messages.length) {
52
52
  const msg = messages[i];
53
- if (msg?.role === "system") {
53
+ if (msg.role === "system") {
54
54
  i++;
55
55
  continue;
56
56
  }
57
57
  // Tool coherence block: assistant message with tool_calls + immediately following tool results
58
- 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) {
59
59
  const indices = [i];
60
60
  i++;
61
61
  while (i < messages.length) {
62
62
  const next = messages[i];
63
- if (next?.role !== "tool")
63
+ if (next.role !== "tool")
64
64
  break;
65
65
  indices.push(i);
66
66
  i++;
@@ -78,13 +78,13 @@ function buildTrimmableBlocks(messages) {
78
78
  function getSystemMessageIndices(messages) {
79
79
  const indices = [];
80
80
  for (let i = 0; i < messages.length; i++) {
81
- if (messages[i]?.role === "system")
81
+ if (messages[i].role === "system")
82
82
  indices.push(i);
83
83
  }
84
84
  return indices;
85
85
  }
86
86
  function buildTrimmedMessages(messages, kept) {
87
- return messages.filter((m, idx) => m?.role === "system" || kept.has(idx));
87
+ return messages.filter((m, idx) => m.role === "system" || kept.has(idx));
88
88
  }
89
89
  function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
90
90
  const targetTokens = Math.floor(maxTokens * (1 - contextMargin / 100));
@@ -132,7 +132,7 @@ function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
132
132
  let remaining = actualTokenCount;
133
133
  const kept = new Set();
134
134
  for (let i = 0; i < messages.length; i++) {
135
- if (messages[i]?.role !== "system")
135
+ if (messages[i].role !== "system")
136
136
  kept.add(i);
137
137
  }
138
138
  // Drop oldest blocks until we fall under target.
@@ -146,7 +146,7 @@ function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
146
146
  let trimmed = buildTrimmedMessages(messages, kept);
147
147
  // If we're still above budget after dropping everything trimmable, preserve system only.
148
148
  if (remaining > targetTokens) {
149
- trimmed = messages.filter((m) => m?.role === "system");
149
+ trimmed = messages.filter((m) => m.role === "system");
150
150
  }
151
151
  const estimatedAfter = (0, token_estimate_1.estimateTokensForMessages)(trimmed);
152
152
  (0, runtime_1.emitNervesEvent)({
@@ -227,7 +227,7 @@ function repairSessionMessages(messages) {
227
227
  });
228
228
  return result;
229
229
  }
230
- function saveSession(filePath, messages, lastUsage) {
230
+ function saveSession(filePath, messages, lastUsage, state) {
231
231
  const violations = validateSessionMessages(messages);
232
232
  if (violations.length > 0) {
233
233
  (0, runtime_1.emitNervesEvent)({
@@ -243,6 +243,12 @@ function saveSession(filePath, messages, lastUsage) {
243
243
  const envelope = { version: 1, messages };
244
244
  if (lastUsage)
245
245
  envelope.lastUsage = lastUsage;
246
+ if (state?.mustResolveBeforeHandoff === true || typeof state?.lastFriendActivityAt === "string") {
247
+ envelope.state = {
248
+ ...(state?.mustResolveBeforeHandoff === true ? { mustResolveBeforeHandoff: true } : {}),
249
+ ...(typeof state?.lastFriendActivityAt === "string" ? { lastFriendActivityAt: state.lastFriendActivityAt } : {}),
250
+ };
251
+ }
246
252
  fs.writeFileSync(filePath, JSON.stringify(envelope, null, 2));
247
253
  }
248
254
  function loadSession(filePath) {
@@ -263,13 +269,23 @@ function loadSession(filePath) {
263
269
  });
264
270
  messages = repairSessionMessages(messages);
265
271
  }
266
- return { messages, lastUsage: data.lastUsage };
272
+ const rawState = data?.state && typeof data.state === "object" && data.state !== null
273
+ ? data.state
274
+ : undefined;
275
+ const state = rawState && (rawState.mustResolveBeforeHandoff === true
276
+ || typeof rawState.lastFriendActivityAt === "string")
277
+ ? {
278
+ ...(rawState.mustResolveBeforeHandoff === true ? { mustResolveBeforeHandoff: true } : {}),
279
+ ...(typeof rawState.lastFriendActivityAt === "string" ? { lastFriendActivityAt: rawState.lastFriendActivityAt } : {}),
280
+ }
281
+ : undefined;
282
+ return { messages, lastUsage: data.lastUsage, state };
267
283
  }
268
284
  catch {
269
285
  return null;
270
286
  }
271
287
  }
272
- function postTurn(messages, sessPath, usage, hooks) {
288
+ function postTurn(messages, sessPath, usage, hooks, state) {
273
289
  if (hooks?.beforeTrim) {
274
290
  try {
275
291
  hooks.beforeTrim([...messages]);
@@ -289,7 +305,7 @@ function postTurn(messages, sessPath, usage, hooks) {
289
305
  const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
290
306
  const trimmed = trimMessages(messages, maxTokens, contextMargin, usage?.input_tokens);
291
307
  messages.splice(0, messages.length, ...trimmed);
292
- saveSession(sessPath, messages, usage);
308
+ saveSession(sessPath, messages, usage, state);
293
309
  }
294
310
  function deleteSession(filePath) {
295
311
  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
+ }
@@ -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" ||
@@ -2,10 +2,12 @@
2
2
  // Context kernel type definitions.
3
3
  // FriendRecord (merged identity + memory), channel capabilities, and resolved context.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.TRUSTED_LEVELS = void 0;
5
6
  exports.isIdentityProvider = isIdentityProvider;
6
7
  exports.isIntegration = isIntegration;
8
+ exports.isTrustedLevel = isTrustedLevel;
7
9
  const runtime_1 = require("../../nerves/runtime");
8
- const IDENTITY_PROVIDERS = new Set(["aad", "local", "teams-conversation"]);
10
+ const IDENTITY_PROVIDERS = new Set(["aad", "local", "teams-conversation", "imessage-handle"]);
9
11
  function isIdentityProvider(value) {
10
12
  (0, runtime_1.emitNervesEvent)({
11
13
  component: "friends",
@@ -19,3 +21,9 @@ const INTEGRATIONS = new Set(["ado", "github", "graph"]);
19
21
  function isIntegration(value) {
20
22
  return typeof value === "string" && INTEGRATIONS.has(value);
21
23
  }
24
+ /** Trust levels that grant full tool access and proactive send capability. */
25
+ exports.TRUSTED_LEVELS = new Set(["family", "friend"]);
26
+ /** Whether a trust level grants full access (family or friend). Defaults to "friend" for legacy records. */
27
+ function isTrustedLevel(trustLevel) {
28
+ return exports.TRUSTED_LEVELS.has(trustLevel ?? "friend");
29
+ }
@@ -120,9 +120,16 @@ function readExistingFacts(factsPath) {
120
120
  const raw = fs.readFileSync(factsPath, "utf8").trim();
121
121
  if (!raw)
122
122
  return [];
123
- return raw
124
- .split("\n")
125
- .map((line) => JSON.parse(line));
123
+ const facts = [];
124
+ for (const line of raw.split("\n")) {
125
+ try {
126
+ facts.push(JSON.parse(line));
127
+ }
128
+ catch {
129
+ // Skip corrupt lines (e.g. partial write from a crash).
130
+ }
131
+ }
132
+ return facts;
126
133
  }
127
134
  function readEntityIndex(entitiesPath) {
128
135
  if (!fs.existsSync(entitiesPath))