@intent-systems/nexus 2026.1.5-3 → 2026.1.5-5

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 (144) hide show
  1. package/dist/agents/agent-id.js +41 -0
  2. package/dist/agents/auth-profiles.js +114 -25
  3. package/dist/agents/identity-state.js +79 -0
  4. package/dist/agents/model-auth.js +1 -0
  5. package/dist/agents/model-fallback.js +15 -9
  6. package/dist/agents/model-selection.js +1 -1
  7. package/dist/agents/models-config.js +17 -11
  8. package/dist/agents/pi-embedded-runner.js +101 -9
  9. package/dist/agents/sandbox.js +12 -3
  10. package/dist/agents/skill-runner.js +29 -4
  11. package/dist/agents/skill-usage.js +114 -11
  12. package/dist/agents/skills-status.js +4 -4
  13. package/dist/agents/skills.js +18 -7
  14. package/dist/agents/subagent-registry.js +25 -11
  15. package/dist/agents/system-prompt.js +16 -0
  16. package/dist/agents/tool-policy.js +19 -3
  17. package/dist/agents/tools/browser-tool.js +5 -2
  18. package/dist/agents/tools/image-tool.js +93 -8
  19. package/dist/agents/tools/sessions-announce-target.js +5 -1
  20. package/dist/agents/workspace.js +55 -46
  21. package/dist/auto-reply/command-detection.js +2 -1
  22. package/dist/auto-reply/reply/directive-handling.js +153 -28
  23. package/dist/auto-reply/reply/directives.js +17 -2
  24. package/dist/auto-reply/reply/model-selection.js +8 -3
  25. package/dist/auto-reply/reply/queue.js +2 -2
  26. package/dist/auto-reply/reply.js +1 -1
  27. package/dist/auto-reply/thinking.js +15 -0
  28. package/dist/browser/chrome.js +1 -1
  29. package/dist/browser/client.js +2 -0
  30. package/dist/browser/config.js +6 -2
  31. package/dist/browser/pw-tools-core.js +3 -0
  32. package/dist/browser/routes/agent.js +14 -0
  33. package/dist/canvas-host/server.js +1 -1
  34. package/dist/capabilities/detector.js +245 -0
  35. package/dist/capabilities/registry.js +99 -0
  36. package/dist/channels/location.js +44 -0
  37. package/dist/channels/web/index.js +2 -0
  38. package/dist/cli/cloud-cli.js +12 -7
  39. package/dist/cli/credential-cli.js +139 -17
  40. package/dist/cli/gateway-cli.js +1 -1
  41. package/dist/cli/log-cli.js +25 -0
  42. package/dist/cli/pairing-cli.js +1 -1
  43. package/dist/cli/program.js +58 -6
  44. package/dist/cli/run-main.js +1 -1
  45. package/dist/cli/skills-cli.js +144 -21
  46. package/dist/cli/skills-hub-cli.js +59 -29
  47. package/dist/cli/tool-connector-cli.js +99 -24
  48. package/dist/cli/upstream-sync-cli.js +253 -96
  49. package/dist/cli/usage-cli.js +14 -0
  50. package/dist/commands/auth-choice-options.js +6 -1
  51. package/dist/commands/auth-choice.js +157 -5
  52. package/dist/commands/bootstrap-preset.js +10 -6
  53. package/dist/commands/capabilities.js +33 -6
  54. package/dist/commands/claude-md.js +3 -2
  55. package/dist/commands/config-view.js +1 -1
  56. package/dist/commands/configure.js +4 -4
  57. package/dist/commands/credential.js +497 -36
  58. package/dist/commands/cursor-rules.js +39 -19
  59. package/dist/commands/doctor.js +5 -4
  60. package/dist/commands/identity.js +28 -31
  61. package/dist/commands/init.js +15 -18
  62. package/dist/commands/log.js +134 -0
  63. package/dist/commands/models/fallbacks.js +1 -1
  64. package/dist/commands/models/image-fallbacks.js +1 -1
  65. package/dist/commands/models/list.js +1 -1
  66. package/dist/commands/models/scan.js +1 -1
  67. package/dist/commands/onboard-auth.js +27 -2
  68. package/dist/commands/onboard-eve-identity.js +7 -8
  69. package/dist/commands/onboard-non-interactive.js +4 -2
  70. package/dist/commands/onboard-quickstart.js +18 -11
  71. package/dist/commands/quest-state.js +271 -0
  72. package/dist/commands/quest.js +53 -13
  73. package/dist/commands/reset.js +1 -1
  74. package/dist/commands/sessions-ingest.js +5 -4
  75. package/dist/commands/setup.js +4 -2
  76. package/dist/commands/skills-manifest.js +2 -2
  77. package/dist/commands/status.js +179 -61
  78. package/dist/commands/suggestions.js +1 -1
  79. package/dist/commands/usage-tracking.js +32 -0
  80. package/dist/commands/usage-upload.js +6 -1
  81. package/dist/config/defaults.js +1 -3
  82. package/dist/config/includes.js +5 -7
  83. package/dist/config/io.js +88 -16
  84. package/dist/config/legacy.js +4 -2
  85. package/dist/config/paths.js +16 -0
  86. package/dist/config/sessions.js +9 -5
  87. package/dist/config/zod-schema.js +4 -3
  88. package/dist/control-plane/broker/broker.js +1022 -0
  89. package/dist/control-plane/compaction.js +282 -0
  90. package/dist/control-plane/factory.js +31 -0
  91. package/dist/control-plane/index.js +10 -0
  92. package/dist/control-plane/odu/agents.js +192 -0
  93. package/dist/control-plane/odu/interaction-tools.js +208 -0
  94. package/dist/control-plane/odu/prompt-loader.js +95 -0
  95. package/dist/control-plane/odu/runtime.js +479 -0
  96. package/dist/control-plane/odu/types.js +6 -0
  97. package/dist/control-plane/odu-control-plane.js +316 -0
  98. package/dist/control-plane/single-agent.js +249 -0
  99. package/dist/control-plane/types.js +11 -0
  100. package/dist/credentials/store.js +449 -0
  101. package/dist/gateway/server-browser.js +5 -4
  102. package/dist/gateway/server-methods/cron.js +11 -1
  103. package/dist/gateway/server.js +14 -7
  104. package/dist/infra/bonjour.js +1 -1
  105. package/dist/infra/event-log.js +8 -2
  106. package/dist/infra/path-env.js +1 -2
  107. package/dist/infra/provider-usage.auth.js +5 -3
  108. package/dist/infra/provider-usage.fetch.claude.js +16 -6
  109. package/dist/infra/provider-usage.fetch.minimax.js +8 -3
  110. package/dist/infra/provider-usage.js +9 -5
  111. package/dist/infra/restart.js +2 -2
  112. package/dist/infra/usage-settings.js +78 -0
  113. package/dist/infra/usage-suggestions.js +17 -5
  114. package/dist/infra/usage-upload.js +38 -1
  115. package/dist/infra/voicewake.js +2 -2
  116. package/dist/logging/redact.js +109 -0
  117. package/dist/markdown/fences.js +58 -0
  118. package/dist/media/image-ops.js +3 -1
  119. package/dist/memory/embeddings.js +146 -0
  120. package/dist/memory/index.js +3 -0
  121. package/dist/memory/internal.js +163 -0
  122. package/dist/pairing/pairing-store.js +218 -0
  123. package/dist/plugins/cli.js +42 -0
  124. package/dist/plugins/discovery.js +253 -0
  125. package/dist/plugins/install.js +181 -0
  126. package/dist/plugins/loader.js +290 -0
  127. package/dist/plugins/registry.js +105 -0
  128. package/dist/plugins/status.js +29 -0
  129. package/dist/plugins/tools.js +39 -0
  130. package/dist/plugins/types.js +1 -0
  131. package/dist/providers/github-copilot-auth.js +1 -1
  132. package/dist/routing/resolve-route.js +144 -0
  133. package/dist/routing/session-key.js +65 -0
  134. package/dist/sessions/send-policy.js +5 -5
  135. package/dist/slack/monitor.js +22 -1
  136. package/dist/telegram/reaction-level.js +2 -1
  137. package/dist/utils/provider-utils.js +28 -0
  138. package/dist/utils.js +4 -3
  139. package/dist/wizard/onboarding.js +29 -7
  140. package/package.json +4 -29
  141. package/patches/@mariozechner__pi-ai.patch +215 -0
  142. package/patches/playwright-core@1.57.0.patch +13 -0
  143. package/patches/qrcode-terminal.patch +12 -0
  144. package/scripts/postinstall.js +202 -0
@@ -0,0 +1,1022 @@
1
+ /**
2
+ * Active Message Broker - Agent Lifecycle Manager
3
+ *
4
+ * The broker is responsible for:
5
+ * - Receiving messages for agents
6
+ * - Starting agents when they have work
7
+ * - Restarting agents when new work arrives
8
+ * - Interrupting agents if needed
9
+ * - Managing agent lifecycle
10
+ *
11
+ * Agents are pure workers - they don't check queues or manage themselves.
12
+ *
13
+ * Ported from magic-toolbox and adapted for Nexus:
14
+ * - Uses Nexus session storage (sessions.json + transcript JSONL) instead of SQLite
15
+ * - Uses Nexus logging system (createSubsystemLogger)
16
+ * - Removed instance manager (sharding not yet supported)
17
+ * - Simplified for single ODU initially
18
+ */
19
+ import crypto from "node:crypto";
20
+ import { loadSession, writeSessionMetadata, } from "../../config/sessions.js";
21
+ import { createSubsystemLogger } from "../../logging.js";
22
+ import { DEFAULT_AGENT_ID, normalizeAgentId, } from "../../routing/session-key.js";
23
+ /**
24
+ * Active Message Broker - manages agent lifecycle
25
+ */
26
+ export class ActiveMessageBroker {
27
+ // Message queues per agent
28
+ queues = new Map();
29
+ // Currently running agents
30
+ runningAgents = new Map();
31
+ // Agents currently being started (to prevent race conditions)
32
+ startingAgents = new Set();
33
+ // Session status tracking (in-memory only, resets on server restart)
34
+ agentStatus = new Map();
35
+ // Registered IAs (always-on singletons)
36
+ registeredIAs = new Map();
37
+ // Track external callers per agent (who has sent messages to this agent)
38
+ // Format: agentId -> Set<senderAgentId>
39
+ externalCallers = new Map();
40
+ // Delivery mode per agent (programmatic override)
41
+ deliveryModes = new Map();
42
+ // Agent factories per ODU (registered by each ODU)
43
+ agentFactories = new Map();
44
+ // Session store paths per ODU
45
+ sessionStorePaths = new Map();
46
+ // Completion callbacks for async waiting
47
+ completionCallbacks = new Map();
48
+ // Collection support for 'collect' mode
49
+ collectionTimers = new Map();
50
+ collectionBuffers = new Map();
51
+ collectDebounceMs = 500; // Default debounce delay
52
+ collectMaxMessages = 10; // Default max messages before auto-flush
53
+ // Logger
54
+ logger;
55
+ config;
56
+ constructor(config) {
57
+ this.logger = createSubsystemLogger("broker");
58
+ this.config = config;
59
+ }
60
+ /**
61
+ * Register an ODU with the broker
62
+ * Each ODU registers its agent factory and session store path
63
+ */
64
+ registerODU(oduName, sessionStorePath, agentFactory) {
65
+ this.sessionStorePaths.set(oduName, sessionStorePath);
66
+ this.agentFactories.set(oduName, agentFactory);
67
+ this.logger.info(`ODU registered: ${oduName}`, {
68
+ oduName,
69
+ sessionStorePath,
70
+ });
71
+ }
72
+ /**
73
+ * Register an IA with the broker
74
+ * IAs are singleton, always-on agents that can receive messages
75
+ */
76
+ registerIA(iaId, instance) {
77
+ this.registeredIAs.set(iaId, instance);
78
+ this.logger.info(`IA registered: ${iaId}`, { iaId });
79
+ }
80
+ /**
81
+ * Set delivery mode for an agent (programmatic only, for tests)
82
+ */
83
+ setDeliveryMode(agentId, mode) {
84
+ this.deliveryModes.set(agentId, mode);
85
+ this.logger.debug(`Delivery mode set for ${agentId}: ${mode}`, {
86
+ agentId,
87
+ mode,
88
+ });
89
+ }
90
+ /**
91
+ * Set collection parameters (for 'collect' mode)
92
+ */
93
+ setCollectionParams(debounceMs, maxMessages) {
94
+ if (debounceMs !== undefined)
95
+ this.collectDebounceMs = debounceMs;
96
+ if (maxMessages !== undefined)
97
+ this.collectMaxMessages = maxMessages;
98
+ this.logger.debug("Collection params updated", {
99
+ debounceMs: this.collectDebounceMs,
100
+ maxMessages: this.collectMaxMessages,
101
+ });
102
+ }
103
+ /**
104
+ * Handle collect mode: buffer messages and debounce delivery
105
+ */
106
+ handleCollectMode(message) {
107
+ const agentId = message.to;
108
+ // Add to collection buffer
109
+ if (!this.collectionBuffers.has(agentId)) {
110
+ this.collectionBuffers.set(agentId, []);
111
+ }
112
+ this.collectionBuffers.get(agentId)?.push(message);
113
+ // Clear existing timer
114
+ const existingTimer = this.collectionTimers.get(agentId);
115
+ if (existingTimer) {
116
+ clearTimeout(existingTimer);
117
+ }
118
+ // Check if we've hit max messages
119
+ const buffer = this.collectionBuffers.get(agentId);
120
+ if (!buffer)
121
+ return;
122
+ if (buffer.length >= this.collectMaxMessages) {
123
+ // Flush immediately
124
+ this.flushCollectionBuffer(agentId);
125
+ return;
126
+ }
127
+ // Set new debounce timer
128
+ const timer = setTimeout(() => {
129
+ this.flushCollectionBuffer(agentId);
130
+ }, this.collectDebounceMs);
131
+ this.collectionTimers.set(agentId, timer);
132
+ }
133
+ /**
134
+ * Flush collection buffer to queue
135
+ */
136
+ flushCollectionBuffer(agentId) {
137
+ const buffer = this.collectionBuffers.get(agentId);
138
+ if (!buffer || buffer.length === 0)
139
+ return;
140
+ this.logger.debug(`Flushing collection buffer for ${agentId}`, {
141
+ agentId,
142
+ messageCount: buffer.length,
143
+ });
144
+ // Clear timer and buffer
145
+ const timer = this.collectionTimers.get(agentId);
146
+ if (timer) {
147
+ clearTimeout(timer);
148
+ this.collectionTimers.delete(agentId);
149
+ }
150
+ this.collectionBuffers.delete(agentId);
151
+ // Enqueue all buffered messages
152
+ for (const message of buffer) {
153
+ this.enqueue(message);
154
+ }
155
+ // Trigger agent execution if not running
156
+ if (!this.runningAgents.has(agentId) && !this.registeredIAs.has(agentId)) {
157
+ const queue = this.queues.get(agentId) || [];
158
+ if (queue.length > 0) {
159
+ void this.startAgentWithBatch(agentId, queue);
160
+ }
161
+ }
162
+ }
163
+ /**
164
+ * Send message and wait for synchronous acknowledgment from IA
165
+ * Used for cross-ODU calls where caller needs immediate confirmation
166
+ *
167
+ * @param message - The message to send
168
+ * @returns Promise that resolves with acknowledgment string
169
+ */
170
+ async sendAndWaitForAck(message) {
171
+ this.logger.debug(`Sending with ack wait: ${message.from} → ${message.to}`, { message });
172
+ // Route message (resolve short names to full IDs)
173
+ const resolvedTo = this.routeMessage(message.from, message.to);
174
+ message.to = resolvedTo;
175
+ // Validate target is an IA
176
+ const ia = this.registeredIAs.get(message.to);
177
+ if (!ia) {
178
+ throw new Error(`Cannot wait for ack: ${message.to} is not a registered IA`);
179
+ }
180
+ // Track external caller if needed
181
+ if (message.from !== message.to &&
182
+ message.from !== "user" &&
183
+ message.from !== "system") {
184
+ if (!this.externalCallers.has(message.to)) {
185
+ this.externalCallers.set(message.to, new Set());
186
+ }
187
+ this.externalCallers.get(message.to)?.add(message.from);
188
+ }
189
+ // Add to queue for tracking
190
+ this.enqueue(message);
191
+ // Queue message at IA
192
+ if (ia.queueMessage) {
193
+ ia.queueMessage(message.content, message.priority || "normal", message.from);
194
+ }
195
+ this.logger.debug(`Waiting for ack from ${message.to}...`, {
196
+ to: message.to,
197
+ });
198
+ // Kick off processing and wait for acknowledgment
199
+ let ack = "";
200
+ if (ia.processQueue) {
201
+ ack = await ia.processQueue();
202
+ }
203
+ this.logger.debug(`Received ack from ${message.to}`, {
204
+ to: message.to,
205
+ ackLength: ack.length,
206
+ });
207
+ return ack;
208
+ }
209
+ /**
210
+ * Send a message to an agent
211
+ *
212
+ * The broker decides everything:
213
+ * - Should we interrupt?
214
+ * - Should we batch?
215
+ * - Should we start the agent?
216
+ */
217
+ async send(message) {
218
+ this.logger.debug(`Message from ${message.from} to ${message.to}`, {
219
+ from: message.from,
220
+ to: message.to,
221
+ priority: message.priority,
222
+ contentPreview: message.content.substring(0, 100),
223
+ });
224
+ // Route message (resolve short names to full IDs)
225
+ const resolvedTo = this.routeMessage(message.from, message.to);
226
+ message.to = resolvedTo;
227
+ this.logger.debug(`Routed to ${resolvedTo}`, { resolvedTo });
228
+ // 1. Track external caller (if not self-message)
229
+ if (message.from !== message.to &&
230
+ message.from !== "user" &&
231
+ message.from !== "system") {
232
+ if (!this.externalCallers.has(message.to)) {
233
+ this.externalCallers.set(message.to, new Set());
234
+ }
235
+ this.externalCallers.get(message.to)?.add(message.from);
236
+ }
237
+ // 2. Handle collect mode (buffer and debounce)
238
+ if (message.deliveryMode === "collect") {
239
+ this.handleCollectMode(message);
240
+ return; // Don't enqueue immediately
241
+ }
242
+ // 3. Handle steer mode (interrupt like urgent)
243
+ if (message.deliveryMode === "steer") {
244
+ // Steer mode interrupts current work
245
+ message.priority = "urgent";
246
+ message.deliveryMode = "interrupt";
247
+ }
248
+ // 4. Handle followup mode (queue without interrupting)
249
+ if (message.deliveryMode === "followup") {
250
+ // Followup doesn't interrupt - just queues normally
251
+ // No special handling needed, just enqueue
252
+ }
253
+ // 5. Add to queue
254
+ this.enqueue(message);
255
+ // 3. Decide what to do
256
+ // Check if target is a registered IA
257
+ const ia = this.registeredIAs.get(message.to);
258
+ if (ia) {
259
+ // IA is always running - queue message and trigger processing
260
+ this.logger.debug(`Delivering to IA: ${message.to}`, {
261
+ to: message.to,
262
+ contentPreview: message.content.substring(0, 100),
263
+ });
264
+ // Queue the message with sender information
265
+ if (ia.queueMessage) {
266
+ ia.queueMessage(message.content, message.priority || "normal", message.from);
267
+ }
268
+ // Trigger processing by calling processQueue() if it exists, otherwise use chatSync
269
+ if (ia.processQueue) {
270
+ this.logger.debug(`Calling processQueue() for ${message.to}`, {
271
+ to: message.to,
272
+ });
273
+ ia.processQueue().catch((error) => {
274
+ this.logger.error(`IA ${message.to} processQueue error: ${error.message}`, { to: message.to, error: error.message });
275
+ });
276
+ }
277
+ else if (ia.chatSync) {
278
+ // Fallback: Call chatSync with empty string
279
+ this.logger.debug(`Calling chatSync('') for ${message.to} (fallback)`, {
280
+ to: message.to,
281
+ });
282
+ setImmediate(() => {
283
+ ia.chatSync?.("").catch((error) => {
284
+ this.logger.error(`IA ${message.to} chatSync error: ${error.message}`, { to: message.to, error: error.message });
285
+ });
286
+ });
287
+ }
288
+ return;
289
+ }
290
+ // Target is an EA - handle normally
291
+ const shouldInterrupt = this.shouldInterrupt(message);
292
+ const running = this.runningAgents.get(message.to);
293
+ if (shouldInterrupt && running) {
294
+ // Interrupt and restart immediately
295
+ this.logger.info(`Interrupting ${message.to} for urgent message`, {
296
+ agentId: message.to,
297
+ });
298
+ await this.interruptAndRestart(message.to);
299
+ }
300
+ else if (!running && !this.startingAgents.has(message.to)) {
301
+ // Agent not running and not being started - mark as starting and process
302
+ this.startingAgents.add(message.to);
303
+ this.logger.info(`Processing batch for ${message.to}`, {
304
+ agentId: message.to,
305
+ });
306
+ try {
307
+ await this.processNextBatch(message.to);
308
+ }
309
+ finally {
310
+ this.startingAgents.delete(message.to);
311
+ }
312
+ }
313
+ else {
314
+ // Agent running or being started - let it finish
315
+ this.logger.debug(`Message queued for ${message.to}, will process after current session`, { agentId: message.to });
316
+ // When it completes, broker will process next batch
317
+ }
318
+ }
319
+ /**
320
+ * Route message to correct agent
321
+ * Resolves short names to fully-qualified agent IDs
322
+ *
323
+ * Rules:
324
+ * 1. Fully-qualified name (e.g., "toolbox-ea-worktrees") → Route directly
325
+ * 2. Short name (e.g., "worktrees") → Expand to caller's ODU EA
326
+ */
327
+ routeMessage(from, to) {
328
+ // Rule 1: If fully-qualified, route directly
329
+ if (this.isFullyQualified(to)) {
330
+ // Validate agent exists or can be created
331
+ if (!this.validateAgentExists(to)) {
332
+ throw new Error(`Unknown agent: ${to}. Agent does not exist in session store or running agents.`);
333
+ }
334
+ return to;
335
+ }
336
+ // Rule 2: Short name - expand to caller's ODU EA
337
+ // Special case: 'user' and 'system' default to primary ODU
338
+ let callerODU;
339
+ if (from === "user" || from === "system") {
340
+ // Get first registered ODU (primary)
341
+ const odus = Array.from(this.agentFactories.keys());
342
+ callerODU = odus[0] || "nexus";
343
+ }
344
+ else {
345
+ callerODU = this.getODUName(from);
346
+ }
347
+ const expandedId = this.expandAgentName(callerODU, to);
348
+ this.logger.debug(`Expanded "${to}" to "${expandedId}" for caller ${from}`, { from, to, expandedId });
349
+ return expandedId;
350
+ }
351
+ /**
352
+ * Check if agent name is fully-qualified
353
+ * Fully-qualified format: {oduName}-{ia|ea}-{identifier} OR {oduName}-ia
354
+ * Examples: "toolbox-ea-worktrees", "meta-ia"
355
+ */
356
+ isFullyQualified(name) {
357
+ const parts = name.split("-");
358
+ // Must have at least 2 parts (oduName-ia) or 3+ parts (oduName-ea-identifier)
359
+ if (parts.length < 2) {
360
+ return false;
361
+ }
362
+ // Second part must be 'ia' or 'ea'
363
+ const agentType = parts[1];
364
+ return agentType === "ia" || agentType === "ea";
365
+ }
366
+ /**
367
+ * Expand short agent name to fully-qualified ID
368
+ * Short name "worktrees" → "toolbox-ea-worktrees"
369
+ */
370
+ expandAgentName(callerODU, shortName) {
371
+ return `${callerODU}-ea-${shortName}`;
372
+ }
373
+ /**
374
+ * Validate that agent exists or can be created
375
+ * Checks: registered IAs, running agents, or if ODU is registered
376
+ */
377
+ validateAgentExists(agentId) {
378
+ // Check if it's a registered IA
379
+ if (this.registeredIAs.has(agentId)) {
380
+ return true;
381
+ }
382
+ // Check if agent is currently running
383
+ if (this.runningAgents.has(agentId)) {
384
+ return true;
385
+ }
386
+ // Check if ODU is registered (can create agent)
387
+ try {
388
+ const oduName = this.getODUName(agentId);
389
+ const storePath = this.sessionStorePaths.get(oduName);
390
+ if (!storePath) {
391
+ // ODU not registered - can't create agent
392
+ return false;
393
+ }
394
+ // ODU is registered, so we can create the agent if needed
395
+ return true;
396
+ }
397
+ catch (error) {
398
+ this.logger.error(`Error validating agent ${agentId}`, {
399
+ agentId,
400
+ error: error instanceof Error ? error.message : String(error),
401
+ });
402
+ return false;
403
+ }
404
+ }
405
+ /**
406
+ * Check if agent has pending messages
407
+ */
408
+ hasPending(agentId) {
409
+ const queue = this.queues.get(agentId);
410
+ return queue ? queue.length > 0 : false;
411
+ }
412
+ /**
413
+ * Get current session status for an agent
414
+ * Returns 'idle' if agent never started or completed
415
+ */
416
+ getAgentStatus(agentId) {
417
+ return this.agentStatus.get(agentId) || "idle";
418
+ }
419
+ /**
420
+ * Set session status for an agent
421
+ * Called internally during agent lifecycle
422
+ */
423
+ setAgentStatus(agentId, status) {
424
+ const oldStatus = this.agentStatus.get(agentId);
425
+ this.agentStatus.set(agentId, status);
426
+ this.logger.debug(`Agent ${agentId} status: ${status}`, {
427
+ agentId,
428
+ status,
429
+ });
430
+ // Emit status change event
431
+ if (oldStatus !== status) {
432
+ this.emit("agent_status_changed", {
433
+ agentId,
434
+ oldStatus,
435
+ newStatus: status,
436
+ timestamp: Date.now(),
437
+ });
438
+ }
439
+ }
440
+ /**
441
+ * Check if agent is currently active (processing messages)
442
+ */
443
+ isAgentActive(agentId) {
444
+ return this.getAgentStatus(agentId) === "active";
445
+ }
446
+ /**
447
+ * Get list of external agents that have sent messages to this agent
448
+ * Returns fully-qualified agent IDs
449
+ */
450
+ getExternalCallers(agentId) {
451
+ const callers = this.externalCallers.get(agentId);
452
+ return callers ? Array.from(callers) : [];
453
+ }
454
+ /**
455
+ * Get queue size for agent (for debugging/monitoring)
456
+ */
457
+ getQueueSize(agentId) {
458
+ return this.queues.get(agentId)?.length || 0;
459
+ }
460
+ /**
461
+ * Wait for specific agent to complete
462
+ * Returns Promise that resolves when agent finishes (success or error)
463
+ *
464
+ * Usage:
465
+ * const result = await broker.onceAgentCompletes('toolbox-ea-worktrees');
466
+ * if (result.success) { ... }
467
+ */
468
+ onceAgentCompletes(agentId) {
469
+ return new Promise((resolve) => {
470
+ if (!this.completionCallbacks.has(agentId)) {
471
+ this.completionCallbacks.set(agentId, []);
472
+ }
473
+ this.completionCallbacks.get(agentId)?.push(resolve);
474
+ });
475
+ }
476
+ /**
477
+ * Decide if we should interrupt based on context-aware rules
478
+ */
479
+ shouldInterrupt(message) {
480
+ // Rule 1: External → IA always interrupts
481
+ if (message.to.endsWith("-ia") && message.metadata?.source === "user") {
482
+ return true;
483
+ }
484
+ // Rule 2: EA → parent IA never interrupts
485
+ if (message.to.endsWith("-ia") && message.metadata?.source === "ea") {
486
+ return false;
487
+ }
488
+ // Rule 3: Explicit interrupt mode
489
+ if (message.deliveryMode === "interrupt") {
490
+ return true;
491
+ }
492
+ // Rule 4: Priority-based
493
+ if (message.priority === "urgent") {
494
+ return true;
495
+ }
496
+ if (message.priority === "high") {
497
+ const running = this.runningAgents.get(message.to);
498
+ if (running && Date.now() - running.startedAt > 30000) {
499
+ return true; // Running > 30s, interrupt
500
+ }
501
+ }
502
+ return false; // Default: don't interrupt
503
+ }
504
+ /**
505
+ * Process next batch of messages for an agent
506
+ * Batches consecutive messages from the same sender
507
+ *
508
+ * NOTE: Caller should add agent to startingAgents before calling this
509
+ */
510
+ async processNextBatch(agentId) {
511
+ const queue = this.queues.get(agentId);
512
+ if (!queue || queue.length === 0) {
513
+ this.logger.debug(`No messages for ${agentId}`, { agentId });
514
+ return;
515
+ }
516
+ // Get consecutive messages from same sender
517
+ const firstSender = queue[0].from;
518
+ const batch = [];
519
+ // Take all consecutive messages from the same sender
520
+ while (queue.length > 0 && queue[0].from === firstSender) {
521
+ const nextMessage = queue.shift();
522
+ if (!nextMessage)
523
+ break;
524
+ batch.push(nextMessage);
525
+ }
526
+ this.logger.info(`Batched ${batch.length} messages from ${firstSender} for ${agentId}`, {
527
+ agentId,
528
+ batchSize: batch.length,
529
+ sender: firstSender,
530
+ });
531
+ // Start agent with this batch
532
+ await this.startAgentWithBatch(agentId, batch);
533
+ }
534
+ /**
535
+ * Start an agent with a specific batch of messages
536
+ *
537
+ * Steps:
538
+ * 1. Parse agent ID to find ODU
539
+ * 2. Load session history from store
540
+ * 3. Format batch messages
541
+ * 4. Create agent via factory
542
+ * 5. Start agent.execute()
543
+ * 6. Monitor completion
544
+ */
545
+ async startAgentWithBatch(agentId, batch) {
546
+ try {
547
+ // 1. Parse agent ID to find ODU
548
+ const oduName = this.getODUName(agentId);
549
+ const factory = this.agentFactories.get(oduName);
550
+ const storePath = this.sessionStorePaths.get(oduName);
551
+ if (!factory || !storePath) {
552
+ throw new Error(`ODU not registered: ${oduName} (for agent ${agentId})`);
553
+ }
554
+ // 2. Register EA (creates if new, updates if exists)
555
+ const displayName = this.getDisplayName(agentId);
556
+ await this.registerEA(storePath, agentId, displayName);
557
+ // 3. Load session history from store
558
+ const session = await this.loadSessionFromStore(storePath, agentId);
559
+ const history = session?.history || [];
560
+ this.logger.debug(`Loaded session for ${agentId}`, {
561
+ agentId,
562
+ historyLength: history.length,
563
+ displayName,
564
+ });
565
+ // 4. Format batch messages
566
+ let taskDescription;
567
+ const deliveryMode = this.deliveryModes.get(agentId) || "batch";
568
+ if (deliveryMode === "single" || batch.length === 1) {
569
+ // Single message
570
+ taskDescription = batch[0].content;
571
+ this.logger.debug(`Processing single message from ${this.getDisplayName(batch[0].from)}`, {
572
+ agentId,
573
+ from: batch[0].from,
574
+ });
575
+ }
576
+ else {
577
+ // Multiple messages from same sender - batch them
578
+ if (batch.length === 1) {
579
+ taskDescription = batch[0].content;
580
+ }
581
+ else {
582
+ taskDescription = batch
583
+ .map((m, i) => `Message ${i + 1}:\n${m.content}`)
584
+ .join("\n\n---\n\n");
585
+ }
586
+ this.logger.debug(`Batched ${batch.length} messages into prompt`, {
587
+ agentId,
588
+ batchSize: batch.length,
589
+ });
590
+ }
591
+ // 5. Create agent via factory
592
+ const agent = factory(agentId, taskDescription, history);
593
+ // 6. Start and track
594
+ const promise = agent.execute();
595
+ // Mark agent as active
596
+ this.setAgentStatus(agentId, "active");
597
+ // Emit agent started event
598
+ this.emit("agent_started", {
599
+ agentId,
600
+ oduName: this.getODUName(agentId),
601
+ timestamp: Date.now(),
602
+ queueSize: batch.length,
603
+ });
604
+ this.runningAgents.set(agentId, {
605
+ agentId,
606
+ instance: agent,
607
+ promise,
608
+ startedAt: Date.now(),
609
+ status: "active",
610
+ });
611
+ this.logger.info(`Agent ${agentId} started (status: active)`, {
612
+ agentId,
613
+ historyLength: history.length,
614
+ queuedMessages: batch.length,
615
+ sender: batch[0].from,
616
+ });
617
+ // 7. Monitor completion
618
+ promise
619
+ .then(() => this.onAgentComplete(agentId))
620
+ .catch((error) => this.onAgentError(agentId, error));
621
+ }
622
+ catch (error) {
623
+ this.logger.error(`Failed to start agent ${agentId}`, {
624
+ agentId,
625
+ error: error instanceof Error ? error.message : String(error),
626
+ });
627
+ throw error;
628
+ }
629
+ }
630
+ /**
631
+ * Get display name from fully-qualified agent ID
632
+ * toolbox-ea-worktrees → worktrees
633
+ * meta-ia → meta-ia (keep IAs fully-qualified)
634
+ */
635
+ getDisplayName(agentId) {
636
+ const parts = agentId.split("-");
637
+ if (parts.length >= 3 && parts[1] === "ea") {
638
+ // EA: return task name part
639
+ return parts.slice(2).join("-");
640
+ }
641
+ // IA or other: return full name
642
+ return agentId;
643
+ }
644
+ /**
645
+ * When agent completes, check for more work
646
+ */
647
+ onAgentComplete(agentId) {
648
+ this.logger.info(`Agent ${agentId} completed`, { agentId });
649
+ // Mark agent as idle
650
+ this.setAgentStatus(agentId, "idle");
651
+ // Remove from running agents
652
+ this.runningAgents.delete(agentId);
653
+ // Emit agent completed event
654
+ this.emit("agent_completed", {
655
+ agentId,
656
+ oduName: this.getODUName(agentId),
657
+ timestamp: Date.now(),
658
+ success: true,
659
+ });
660
+ // Notify completion listeners
661
+ this.notifyCompletion(agentId, { success: true });
662
+ // Are there more messages queued?
663
+ if (this.hasPending(agentId) && !this.startingAgents.has(agentId)) {
664
+ const queueSize = this.getQueueSize(agentId);
665
+ this.logger.info(`Processing next batch for ${agentId} (${queueSize} messages queued)`, {
666
+ agentId,
667
+ queueSize,
668
+ });
669
+ // Mark as starting and process next batch
670
+ this.startingAgents.add(agentId);
671
+ this.processNextBatch(agentId)
672
+ .then(() => this.startingAgents.delete(agentId))
673
+ .catch((error) => {
674
+ this.logger.error(`Error processing next batch for ${agentId}`, {
675
+ agentId,
676
+ error: error instanceof Error ? error.message : String(error),
677
+ });
678
+ this.startingAgents.delete(agentId);
679
+ });
680
+ }
681
+ else {
682
+ this.logger.debug(`Agent ${agentId} idle (no more messages)`, {
683
+ agentId,
684
+ });
685
+ }
686
+ }
687
+ /**
688
+ * Handle agent errors
689
+ */
690
+ onAgentError(agentId, error) {
691
+ this.logger.error(`Agent ${agentId} failed`, {
692
+ agentId,
693
+ error: error.message,
694
+ });
695
+ // Mark agent as idle (failed, but can receive new messages)
696
+ this.setAgentStatus(agentId, "idle");
697
+ // Remove from running
698
+ this.runningAgents.delete(agentId);
699
+ // Notify completion listeners with error
700
+ this.notifyCompletion(agentId, { success: false, error: error.message });
701
+ // Don't restart on error - let user handle it
702
+ // Messages stay in queue for manual intervention
703
+ }
704
+ /**
705
+ * Notify all completion listeners for an agent
706
+ * Called when agent completes (success or error)
707
+ */
708
+ notifyCompletion(agentId, result) {
709
+ const callbacks = this.completionCallbacks.get(agentId) || [];
710
+ callbacks.forEach((cb) => {
711
+ cb(result);
712
+ });
713
+ this.completionCallbacks.delete(agentId); // One-time callbacks
714
+ if (callbacks.length > 0) {
715
+ this.logger.debug(`Notified ${callbacks.length} listener(s) for ${agentId}`, {
716
+ agentId,
717
+ listenersCount: callbacks.length,
718
+ success: result.success,
719
+ });
720
+ }
721
+ }
722
+ /**
723
+ * Interrupt a running agent and restart with new messages
724
+ */
725
+ async interruptAndRestart(agentId) {
726
+ const running = this.runningAgents.get(agentId);
727
+ if (!running)
728
+ return;
729
+ this.logger.info(`Sending interrupt to ${agentId}`, { agentId });
730
+ // Send interrupt signal
731
+ const instance = running.instance;
732
+ if (instance.interrupt) {
733
+ instance.interrupt();
734
+ }
735
+ // Wait for graceful stop (agent saves state)
736
+ try {
737
+ await running.promise;
738
+ }
739
+ catch {
740
+ // Interrupt causes early exit, that's expected
741
+ this.logger.debug(`Agent ${agentId} interrupted successfully`, {
742
+ agentId,
743
+ });
744
+ }
745
+ // Remove from running
746
+ this.runningAgents.delete(agentId);
747
+ // Mark as starting and process next batch with new messages
748
+ if (!this.startingAgents.has(agentId)) {
749
+ this.startingAgents.add(agentId);
750
+ try {
751
+ await this.processNextBatch(agentId);
752
+ }
753
+ finally {
754
+ this.startingAgents.delete(agentId);
755
+ }
756
+ }
757
+ }
758
+ /**
759
+ * Add message to queue with priority + FIFO sorting
760
+ */
761
+ enqueue(message) {
762
+ if (!this.queues.has(message.to)) {
763
+ this.queues.set(message.to, []);
764
+ }
765
+ this.queues.get(message.to)?.push(message);
766
+ this.sortQueue(message.to);
767
+ // Emit message queued event
768
+ this.emit("message_queued", {
769
+ messageId: message.id,
770
+ from: message.from,
771
+ to: message.to,
772
+ priority: message.priority,
773
+ timestamp: message.timestamp,
774
+ queueSize: this.queues.get(message.to)?.length,
775
+ });
776
+ }
777
+ /**
778
+ * Sort queue: priority first, then FIFO within priority
779
+ */
780
+ sortQueue(agentId) {
781
+ const queue = this.queues.get(agentId);
782
+ if (!queue)
783
+ return;
784
+ queue.sort((a, b) => {
785
+ const priorityOrder = { urgent: 0, high: 1, normal: 2, low: 3 };
786
+ const aPriority = priorityOrder[a.priority];
787
+ const bPriority = priorityOrder[b.priority];
788
+ if (aPriority !== bPriority) {
789
+ return aPriority - bPriority;
790
+ }
791
+ return a.timestamp - b.timestamp; // FIFO within same priority
792
+ });
793
+ }
794
+ /**
795
+ * Parse agent ID to determine ODU name
796
+ * Examples:
797
+ * - 'toolbox-ia' → 'toolbox'
798
+ * - 'toolbox-ea-abc123' → 'toolbox'
799
+ * - 'meta-ia' → 'meta'
800
+ */
801
+ getODUName(agentId) {
802
+ const parts = agentId.split("-");
803
+ if (parts.length < 2) {
804
+ throw new Error(`Invalid agent ID format: ${agentId}`);
805
+ }
806
+ // Regular instance: first part is ODU name
807
+ return parts[0];
808
+ }
809
+ /**
810
+ * Load session from Nexus session store (new format)
811
+ */
812
+ async loadSessionFromStore(_storePath, agentId) {
813
+ try {
814
+ const sessionKey = `agent:${agentId}`;
815
+ const agentIdNormalized = normalizeAgentId(DEFAULT_AGENT_ID);
816
+ const session = await loadSession(agentIdNormalized, sessionKey);
817
+ if (!session) {
818
+ return null;
819
+ }
820
+ // Load history from new format
821
+ const history = session.history.map((turn) => ({
822
+ role: turn.role,
823
+ content: turn.content,
824
+ timestamp: new Date(turn.timestamp).getTime(),
825
+ }));
826
+ return {
827
+ history,
828
+ };
829
+ }
830
+ catch (error) {
831
+ this.logger.error(`Failed to load session for ${agentId}`, {
832
+ agentId,
833
+ error: error instanceof Error ? error.message : String(error),
834
+ });
835
+ return null;
836
+ }
837
+ }
838
+ /**
839
+ * Register or update EA in session store (new format)
840
+ * EAs persist forever once created
841
+ */
842
+ async registerEA(_storePath, agentId, taskName) {
843
+ try {
844
+ const sessionKey = `agent:${agentId}`;
845
+ const agentIdNormalized = normalizeAgentId(DEFAULT_AGENT_ID);
846
+ const now = Date.now();
847
+ // Check if EA already exists
848
+ const existing = await loadSession(agentIdNormalized, sessionKey);
849
+ if (existing) {
850
+ // Update last updated timestamp
851
+ await writeSessionMetadata(agentIdNormalized, sessionKey, {
852
+ ...existing.metadata,
853
+ updatedAt: now,
854
+ });
855
+ this.logger.debug(`Updated session for ${agentId}`, { agentId });
856
+ }
857
+ else {
858
+ // Create new EA registration
859
+ const displayName = this.getDisplayName(agentId);
860
+ const oduName = this.getODUName(agentId);
861
+ const newEntry = {
862
+ sessionId: crypto.randomUUID(),
863
+ updatedAt: now,
864
+ displayName: taskName || displayName,
865
+ chatType: "direct",
866
+ };
867
+ await writeSessionMetadata(agentIdNormalized, sessionKey, {
868
+ ...newEntry,
869
+ created: new Date().toISOString(),
870
+ });
871
+ this.logger.info(`Registered new EA: ${agentId}`, {
872
+ agentId,
873
+ oduName,
874
+ displayName,
875
+ });
876
+ }
877
+ }
878
+ catch (error) {
879
+ this.logger.error(`Failed to register EA ${agentId}`, {
880
+ agentId,
881
+ error: error instanceof Error ? error.message : String(error),
882
+ });
883
+ // Don't throw - registration failure shouldn't block agent execution
884
+ }
885
+ }
886
+ // ============================================================
887
+ // PUBLIC OBSERVABILITY METHODS (for GUI and monitoring)
888
+ // ============================================================
889
+ /**
890
+ * Get all registered IAs
891
+ * Returns array of IA metadata for GUI display
892
+ */
893
+ getRegisteredIAs() {
894
+ const ias = [];
895
+ for (const [id] of this.registeredIAs.entries()) {
896
+ ias.push({
897
+ id,
898
+ oduName: this.getODUName(id),
899
+ });
900
+ }
901
+ return ias;
902
+ }
903
+ /**
904
+ * Get all running EAs with their status
905
+ * Returns array of EA metadata for GUI display
906
+ */
907
+ getRunningAgents() {
908
+ const agents = [];
909
+ for (const [agentId, runningAgent] of this.runningAgents.entries()) {
910
+ agents.push({
911
+ agentId,
912
+ status: runningAgent.status,
913
+ startedAt: runningAgent.startedAt,
914
+ oduName: this.getODUName(agentId),
915
+ queueSize: this.getQueueSize(agentId),
916
+ });
917
+ }
918
+ return agents;
919
+ }
920
+ /**
921
+ * Get all queues with their sizes
922
+ * Returns map of agentId -> queue size
923
+ */
924
+ getAllQueues() {
925
+ const queueSizes = new Map();
926
+ for (const [agentId, queue] of this.queues.entries()) {
927
+ queueSizes.set(agentId, queue.length);
928
+ }
929
+ return queueSizes;
930
+ }
931
+ /**
932
+ * Get all agent IDs (both IAs and EAs) that the broker knows about
933
+ * Includes running, queued, and registered agents
934
+ */
935
+ getAllKnownAgents() {
936
+ const agents = [];
937
+ // Add all registered IAs
938
+ for (const [id] of this.registeredIAs.entries()) {
939
+ agents.push({
940
+ agentId: id,
941
+ type: "ia",
942
+ status: this.getAgentStatus(id),
943
+ oduName: this.getODUName(id),
944
+ queueSize: this.getQueueSize(id),
945
+ isRunning: false, // IAs are always available, not "running" in the same sense
946
+ });
947
+ }
948
+ // Add all running EAs
949
+ for (const [id, runningAgent] of this.runningAgents.entries()) {
950
+ agents.push({
951
+ agentId: id,
952
+ type: "ea",
953
+ status: runningAgent.status,
954
+ oduName: this.getODUName(id),
955
+ queueSize: this.getQueueSize(id),
956
+ isRunning: true,
957
+ });
958
+ }
959
+ // Add queued agents that aren't running
960
+ for (const [id, queue] of this.queues.entries()) {
961
+ if (!this.runningAgents.has(id) &&
962
+ !this.registeredIAs.has(id) &&
963
+ queue.length > 0) {
964
+ agents.push({
965
+ agentId: id,
966
+ type: "ea",
967
+ status: "idle",
968
+ oduName: this.getODUName(id),
969
+ queueSize: queue.length,
970
+ isRunning: false,
971
+ });
972
+ }
973
+ }
974
+ return agents;
975
+ }
976
+ /**
977
+ * Event emitter support for real-time updates
978
+ * Listeners can subscribe to broker events
979
+ */
980
+ eventListeners = new Map();
981
+ /**
982
+ * Subscribe to broker events
983
+ * Events: 'agent_started', 'agent_completed', 'agent_status_changed', 'message_queued'
984
+ */
985
+ on(event, callback) {
986
+ if (!this.eventListeners.has(event)) {
987
+ this.eventListeners.set(event, []);
988
+ }
989
+ this.eventListeners.get(event)?.push(callback);
990
+ }
991
+ /**
992
+ * Unsubscribe from broker events
993
+ */
994
+ off(event, callback) {
995
+ const listeners = this.eventListeners.get(event);
996
+ if (!listeners)
997
+ return;
998
+ const index = listeners.indexOf(callback);
999
+ if (index > -1) {
1000
+ listeners.splice(index, 1);
1001
+ }
1002
+ }
1003
+ /**
1004
+ * Emit event to all subscribers
1005
+ */
1006
+ emit(event, data) {
1007
+ const listeners = this.eventListeners.get(event);
1008
+ if (!listeners)
1009
+ return;
1010
+ for (const callback of listeners) {
1011
+ try {
1012
+ callback(data);
1013
+ }
1014
+ catch (error) {
1015
+ this.logger.error(`Error in event listener for ${event}`, {
1016
+ event,
1017
+ error: error instanceof Error ? error.message : String(error),
1018
+ });
1019
+ }
1020
+ }
1021
+ }
1022
+ }