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

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 (102) 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 +242 -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 +145 -50
  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/progress-story.js +42 -0
  46. package/dist/heart/providers/anthropic.js +16 -2
  47. package/dist/heart/sense-truth.js +61 -0
  48. package/dist/heart/session-activity.js +169 -0
  49. package/dist/heart/session-recall.js +116 -0
  50. package/dist/heart/streaming.js +96 -21
  51. package/dist/heart/turn-coordinator.js +28 -0
  52. package/dist/mind/associative-recall.js +14 -2
  53. package/dist/mind/bundle-manifest.js +70 -0
  54. package/dist/mind/context.js +27 -11
  55. package/dist/mind/first-impressions.js +16 -2
  56. package/dist/mind/friends/channel.js +43 -0
  57. package/dist/mind/friends/store-file.js +19 -0
  58. package/dist/mind/friends/types.js +9 -1
  59. package/dist/mind/memory.js +10 -3
  60. package/dist/mind/pending.js +72 -9
  61. package/dist/mind/phrases.js +1 -0
  62. package/dist/mind/prompt.js +266 -77
  63. package/dist/mind/token-estimate.js +8 -12
  64. package/dist/nerves/cli-logging.js +15 -2
  65. package/dist/repertoire/ado-client.js +4 -2
  66. package/dist/repertoire/coding/feedback.js +134 -0
  67. package/dist/repertoire/coding/index.js +4 -1
  68. package/dist/repertoire/coding/manager.js +62 -4
  69. package/dist/repertoire/coding/spawner.js +3 -3
  70. package/dist/repertoire/coding/tools.js +41 -2
  71. package/dist/repertoire/data/ado-endpoints.json +188 -0
  72. package/dist/repertoire/tasks/board.js +12 -0
  73. package/dist/repertoire/tasks/index.js +23 -9
  74. package/dist/repertoire/tasks/transitions.js +1 -2
  75. package/dist/repertoire/tools-base.js +462 -245
  76. package/dist/repertoire/tools-bluebubbles.js +93 -0
  77. package/dist/repertoire/tools-teams.js +58 -25
  78. package/dist/repertoire/tools.js +57 -35
  79. package/dist/senses/bluebubbles-client.js +484 -0
  80. package/dist/senses/bluebubbles-entry.js +13 -0
  81. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  82. package/dist/senses/bluebubbles-media.js +338 -0
  83. package/dist/senses/bluebubbles-model.js +261 -0
  84. package/dist/senses/bluebubbles-mutation-log.js +116 -0
  85. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  86. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  87. package/dist/senses/bluebubbles.js +1142 -0
  88. package/dist/senses/cli.js +340 -138
  89. package/dist/senses/continuity.js +94 -0
  90. package/dist/senses/debug-activity.js +148 -0
  91. package/dist/senses/inner-dialog-worker.js +47 -18
  92. package/dist/senses/inner-dialog.js +330 -84
  93. package/dist/senses/pipeline.js +256 -0
  94. package/dist/senses/teams.js +541 -129
  95. package/dist/senses/trust-gate.js +112 -2
  96. package/package.json +14 -3
  97. package/subagents/README.md +46 -33
  98. package/subagents/work-doer.md +28 -24
  99. package/subagents/work-merger.md +24 -30
  100. package/subagents/work-planner.md +44 -27
  101. package/dist/heart/daemon/specialist-session.js +0 -142
  102. package/dist/inner-worker-entry.js +0 -4
@@ -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))
@@ -33,24 +33,56 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.INNER_DIALOG_PENDING = void 0;
36
37
  exports.getPendingDir = getPendingDir;
38
+ exports.getDeferredReturnDir = getDeferredReturnDir;
39
+ exports.getInnerDialogPendingDir = getInnerDialogPendingDir;
40
+ exports.hasPendingMessages = hasPendingMessages;
41
+ exports.enqueueDeferredReturn = enqueueDeferredReturn;
42
+ exports.drainDeferredReturns = drainDeferredReturns;
37
43
  exports.drainPending = drainPending;
38
44
  const fs = __importStar(require("fs"));
39
45
  const path = __importStar(require("path"));
40
- const os = __importStar(require("os"));
46
+ const identity_1 = require("../heart/identity");
41
47
  const runtime_1 = require("../nerves/runtime");
42
48
  function getPendingDir(agentName, friendId, channel, key) {
43
- return path.join(os.homedir(), ".agentstate", agentName, "pending", friendId, channel, key);
49
+ return path.join((0, identity_1.getAgentRoot)(agentName), "state", "pending", friendId, channel, key);
44
50
  }
45
- function drainPending(pendingDir) {
51
+ function getDeferredReturnDir(agentName, friendId) {
52
+ return path.join((0, identity_1.getAgentRoot)(agentName), "state", "pending-returns", friendId);
53
+ }
54
+ /** Canonical inner-dialog pending path segments. */
55
+ exports.INNER_DIALOG_PENDING = { friendId: "self", channel: "inner", key: "dialog" };
56
+ /** Returns the pending dir for this agent's inner dialog. */
57
+ function getInnerDialogPendingDir(agentName) {
58
+ return getPendingDir(agentName, exports.INNER_DIALOG_PENDING.friendId, exports.INNER_DIALOG_PENDING.channel, exports.INNER_DIALOG_PENDING.key);
59
+ }
60
+ function hasPendingMessages(pendingDir) {
46
61
  if (!fs.existsSync(pendingDir))
47
- return [];
62
+ return false;
63
+ try {
64
+ return fs.readdirSync(pendingDir).some((entry) => entry.endsWith(".json") || entry.endsWith(".json.processing"));
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ function writeQueueFile(queueDir, message) {
71
+ fs.mkdirSync(queueDir, { recursive: true });
72
+ const fileName = `${message.timestamp}-${Math.random().toString(36).slice(2, 10)}.json`;
73
+ const filePath = path.join(queueDir, fileName);
74
+ fs.writeFileSync(filePath, JSON.stringify(message, null, 2));
75
+ return filePath;
76
+ }
77
+ function drainQueue(queueDir) {
78
+ if (!fs.existsSync(queueDir))
79
+ return { messages: [], recovered: 0 };
48
80
  let entries;
49
81
  try {
50
- entries = fs.readdirSync(pendingDir);
82
+ entries = fs.readdirSync(queueDir);
51
83
  }
52
84
  catch {
53
- return [];
85
+ return { messages: [], recovered: 0 };
54
86
  }
55
87
  // Collect both .json (new) and .processing (crash recovery)
56
88
  const jsonFiles = entries.filter(f => f.endsWith(".json") && !f.endsWith(".processing"));
@@ -62,9 +94,9 @@ function drainPending(pendingDir) {
62
94
  ].sort((a, b) => a.file.localeCompare(b.file));
63
95
  const messages = [];
64
96
  for (const { file, needsRename } of allFiles) {
65
- const srcPath = path.join(pendingDir, file);
97
+ const srcPath = path.join(queueDir, file);
66
98
  const processingPath = needsRename
67
- ? path.join(pendingDir, file + ".processing")
99
+ ? path.join(queueDir, file + ".processing")
68
100
  : srcPath;
69
101
  try {
70
102
  if (needsRename) {
@@ -83,11 +115,42 @@ function drainPending(pendingDir) {
83
115
  catch { /* ignore */ }
84
116
  }
85
117
  }
118
+ return {
119
+ messages,
120
+ recovered: processingFiles.length,
121
+ };
122
+ }
123
+ function enqueueDeferredReturn(agentName, friendId, message) {
124
+ const queueDir = getDeferredReturnDir(agentName, friendId);
125
+ const filePath = writeQueueFile(queueDir, message);
126
+ (0, runtime_1.emitNervesEvent)({
127
+ event: "mind.deferred_return_enqueued",
128
+ component: "mind",
129
+ message: "deferred return queued for later friend delivery",
130
+ meta: { friendId, queueDir },
131
+ });
132
+ return filePath;
133
+ }
134
+ function drainDeferredReturns(agentName, friendId) {
135
+ const queueDir = getDeferredReturnDir(agentName, friendId);
136
+ const { messages } = drainQueue(queueDir);
137
+ (0, runtime_1.emitNervesEvent)({
138
+ event: "mind.deferred_returns_drained",
139
+ component: "mind",
140
+ message: "deferred friend returns drained",
141
+ meta: { friendId, queueDir, count: messages.length },
142
+ });
143
+ return messages;
144
+ }
145
+ function drainPending(pendingDir) {
146
+ if (!fs.existsSync(pendingDir))
147
+ return [];
148
+ const { messages, recovered } = drainQueue(pendingDir);
86
149
  (0, runtime_1.emitNervesEvent)({
87
150
  event: "mind.pending_drained",
88
151
  component: "mind",
89
152
  message: "pending queue drained",
90
- meta: { pendingDir, count: messages.length, recovered: processingFiles.length },
153
+ meta: { pendingDir, count: messages.length, recovered },
91
154
  });
92
155
  return messages;
93
156
  }
@@ -16,6 +16,7 @@ function getPhrases() {
16
16
  message: "loading phrase pools",
17
17
  meta: {},
18
18
  });
19
+ (0, identity_1.resetAgentConfigCache)();
19
20
  const phrases = (0, identity_1.loadAgentConfig)().phrases;
20
21
  (0, runtime_1.emitNervesEvent)({
21
22
  event: "repertoire.load_end",