macro-agent 0.1.5 → 0.1.7

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 (45) hide show
  1. package/.claude/settings.json +128 -1
  2. package/.sessionlog/settings.json +4 -0
  3. package/CLAUDE.md +125 -10
  4. package/README.md +93 -31
  5. package/dist/acp/macro-agent.d.ts.map +1 -1
  6. package/dist/acp/macro-agent.js +1 -3
  7. package/dist/acp/macro-agent.js.map +1 -1
  8. package/dist/boot-v2.d.ts +1 -0
  9. package/dist/boot-v2.d.ts.map +1 -1
  10. package/dist/boot-v2.js +1 -0
  11. package/dist/boot-v2.js.map +1 -1
  12. package/dist/cognitive/workspace-handler.d.ts +17 -9
  13. package/dist/cognitive/workspace-handler.d.ts.map +1 -1
  14. package/dist/cognitive/workspace-handler.js +10 -11
  15. package/dist/cognitive/workspace-handler.js.map +1 -1
  16. package/dist/map/coordination-handler.d.ts +7 -23
  17. package/dist/map/coordination-handler.d.ts.map +1 -1
  18. package/dist/map/coordination-handler.js +124 -100
  19. package/dist/map/coordination-handler.js.map +1 -1
  20. package/dist/map/server.d.ts.map +1 -1
  21. package/dist/map/server.js +13 -3
  22. package/dist/map/server.js.map +1 -1
  23. package/dist/map/sidecar.d.ts.map +1 -1
  24. package/dist/map/sidecar.js +13 -15
  25. package/dist/map/sidecar.js.map +1 -1
  26. package/dist/map/trajectory-reporter.d.ts +4 -9
  27. package/dist/map/trajectory-reporter.d.ts.map +1 -1
  28. package/dist/map/trajectory-reporter.js +15 -129
  29. package/dist/map/trajectory-reporter.js.map +1 -1
  30. package/dist/map/types.d.ts +39 -0
  31. package/dist/map/types.d.ts.map +1 -1
  32. package/package.json +2 -3
  33. package/src/__tests__/e2e/cognitive-workspace.e2e.test.ts +1 -1
  34. package/src/acp/macro-agent.ts +1 -4
  35. package/src/boot-v2.ts +2 -0
  36. package/src/cognitive/__tests__/workspace-handler.test.ts +2 -10
  37. package/src/cognitive/workspace-handler.ts +18 -15
  38. package/src/map/__tests__/trajectory-reporter.test.ts +2 -254
  39. package/src/map/coordination-handler.ts +137 -120
  40. package/src/map/server.ts +14 -2
  41. package/src/map/sidecar.ts +13 -20
  42. package/src/map/trajectory-reporter.ts +16 -154
  43. package/src/map/types.ts +43 -2
  44. package/src/__tests__/e2e/trajectory-content.e2e.test.ts +0 -708
  45. package/src/map/__tests__/coordination-handler.test.ts +0 -598
@@ -1,11 +1,8 @@
1
1
  /**
2
- * Tests for Trajectory Reporter — checkpoint reporting & content serving.
2
+ * Tests for Trajectory Reporter — checkpoint building & reporting.
3
3
  */
4
4
 
5
- import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
6
- import * as fs from "node:fs";
7
- import * as path from "node:path";
8
- import * as os from "node:os";
5
+ import { describe, it, expect, beforeEach, vi } from "vitest";
9
6
  import {
10
7
  createTrajectoryReporter,
11
8
  type TrajectoryConnection,
@@ -174,252 +171,3 @@ describe("TrajectoryReporter", () => {
174
171
  );
175
172
  });
176
173
  });
177
-
178
- // =============================================================================
179
- // Tests — Content Serving via sessionlog
180
- // =============================================================================
181
-
182
- describe("TrajectoryReporter — content serving", () => {
183
- let conn: ReturnType<typeof mockConnection>;
184
- let tmpDir: string;
185
-
186
- beforeEach(() => {
187
- conn = mockConnection();
188
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "trajectory-content-test-"));
189
- });
190
-
191
- afterEach(() => {
192
- fs.rmSync(tmpDir, { recursive: true, force: true });
193
- });
194
-
195
- /** Write a sessionlog-compatible flat state file: <sessionsDir>/<sessionId>.json */
196
- function writeSessionState(
197
- sessionsDir: string,
198
- sessionId: string,
199
- state: Record<string, unknown>,
200
- transcript?: string,
201
- ): string {
202
- fs.mkdirSync(sessionsDir, { recursive: true });
203
-
204
- const transcriptPath = path.join(sessionsDir, `${sessionId}.jsonl`);
205
- if (transcript) {
206
- fs.writeFileSync(transcriptPath, transcript);
207
- }
208
-
209
- fs.writeFileSync(
210
- path.join(sessionsDir, `${sessionId}.json`),
211
- JSON.stringify({
212
- sessionID: sessionId,
213
- phase: "active",
214
- baseCommit: "abc123",
215
- startedAt: new Date().toISOString(),
216
- agentType: "claude",
217
- transcriptPath: transcript ? transcriptPath : undefined,
218
- ...state,
219
- }),
220
- );
221
-
222
- return transcriptPath;
223
- }
224
-
225
- it("serves transcript from live session matching session ID", async () => {
226
- const sessionsDir = path.join(tmpDir, "sessions");
227
- const transcript = [
228
- JSON.stringify({ type: "user", message: "Fix the bug" }),
229
- JSON.stringify({ type: "assistant", message: "I'll look into it" }),
230
- ].join("\n");
231
-
232
- writeSessionState(sessionsDir, "sess-abc", {
233
- stepCount: 3,
234
- filesTouched: ["src/main.ts"],
235
- firstPrompt: "Fix the bug",
236
- }, transcript);
237
-
238
- createTrajectoryReporter(conn, {
239
- trajectorySyncLevel: "full",
240
- sessionDirs: [sessionsDir],
241
- });
242
-
243
- const handler = conn.onNotification.mock.calls[0][1];
244
- await handler({ request_id: "req-1", checkpoint_id: "sess-abc-step2" });
245
-
246
- expect(conn.sendNotification).toHaveBeenCalledWith(
247
- "trajectory/content.response",
248
- expect.objectContaining({
249
- request_id: "req-1",
250
- transcript: expect.stringContaining("Fix the bug"),
251
- prompts: "Fix the bug",
252
- metadata: expect.objectContaining({
253
- sessionID: "sess-abc",
254
- source: "live",
255
- }),
256
- }),
257
- );
258
- });
259
-
260
- it("serves transcript matching checkpoint ID in turnCheckpointIDs", async () => {
261
- const sessionsDir = path.join(tmpDir, "sessions");
262
- const transcript = JSON.stringify({ type: "user", message: "Deploy it" }) + "\n";
263
-
264
- writeSessionState(sessionsDir, "sess-xyz", {
265
- turnCheckpointIDs: ["sess-xyz-step1", "sess-xyz-step2"],
266
- firstPrompt: "Deploy it",
267
- }, transcript);
268
-
269
- createTrajectoryReporter(conn, {
270
- trajectorySyncLevel: "full",
271
- sessionDirs: [sessionsDir],
272
- });
273
-
274
- const handler = conn.onNotification.mock.calls[0][1];
275
- await handler({ request_id: "req-2", checkpoint_id: "sess-xyz-step2" });
276
-
277
- expect(conn.sendNotification).toHaveBeenCalledWith(
278
- "trajectory/content.response",
279
- expect.objectContaining({
280
- request_id: "req-2",
281
- transcript: expect.stringContaining("Deploy it"),
282
- }),
283
- );
284
- });
285
-
286
- it("uses promptAttributions for multi-prompt sessions", async () => {
287
- const sessionsDir = path.join(tmpDir, "sessions");
288
- const transcript = JSON.stringify({ type: "user", message: "First" }) + "\n"
289
- + JSON.stringify({ type: "user", message: "Second" }) + "\n";
290
-
291
- writeSessionState(sessionsDir, "sess-multi", {
292
- firstPrompt: "First",
293
- promptAttributions: [
294
- { prompt: "First", timestamp: "2026-01-01T00:00:00Z", agentLines: 10 },
295
- { prompt: "Second", timestamp: "2026-01-01T00:01:00Z", agentLines: 5 },
296
- ],
297
- }, transcript);
298
-
299
- createTrajectoryReporter(conn, {
300
- trajectorySyncLevel: "full",
301
- sessionDirs: [sessionsDir],
302
- });
303
-
304
- const handler = conn.onNotification.mock.calls[0][1];
305
- await handler({ request_id: "req-3", checkpoint_id: "sess-multi-step1" });
306
-
307
- expect(conn.sendNotification).toHaveBeenCalledWith(
308
- "trajectory/content.response",
309
- expect.objectContaining({
310
- prompts: "First\n---\nSecond",
311
- }),
312
- );
313
- });
314
-
315
- it("returns empty response when no session found", async () => {
316
- createTrajectoryReporter(conn, {
317
- trajectorySyncLevel: "full",
318
- sessionDirs: [path.join(tmpDir, "nonexistent")],
319
- });
320
-
321
- const handler = conn.onNotification.mock.calls[0][1];
322
- await handler({ request_id: "req-4", checkpoint_id: "unknown-session-step1" });
323
-
324
- expect(conn.sendNotification).toHaveBeenCalledWith(
325
- "trajectory/content.response",
326
- expect.objectContaining({
327
- request_id: "req-4",
328
- transcript: "",
329
- metadata: expect.objectContaining({ source: "macro-agent" }),
330
- }),
331
- );
332
- });
333
-
334
- it("serves transcripts from ended sessions (content is still valid)", async () => {
335
- const sessionsDir = path.join(tmpDir, "sessions");
336
- const transcript = JSON.stringify({ type: "user", message: "Old session" }) + "\n";
337
-
338
- writeSessionState(sessionsDir, "sess-ended", {
339
- phase: "ended",
340
- }, transcript);
341
-
342
- createTrajectoryReporter(conn, {
343
- trajectorySyncLevel: "full",
344
- sessionDirs: [sessionsDir],
345
- });
346
-
347
- const handler = conn.onNotification.mock.calls[0][1];
348
- await handler({ request_id: "req-5", checkpoint_id: "sess-ended-step1" });
349
-
350
- expect(conn.sendNotification).toHaveBeenCalledWith(
351
- "trajectory/content.response",
352
- expect.objectContaining({
353
- request_id: "req-5",
354
- transcript: expect.stringContaining("Old session"),
355
- }),
356
- );
357
- });
358
-
359
- it("skips sessions with missing transcript path", async () => {
360
- const sessionsDir = path.join(tmpDir, "sessions");
361
-
362
- // Write state without transcript file
363
- writeSessionState(sessionsDir, "sess-no-file", {});
364
-
365
- createTrajectoryReporter(conn, {
366
- trajectorySyncLevel: "full",
367
- sessionDirs: [sessionsDir],
368
- });
369
-
370
- const handler = conn.onNotification.mock.calls[0][1];
371
- await handler({ request_id: "req-6", checkpoint_id: "sess-no-file-step1" });
372
-
373
- const call = conn.sendNotification.mock.calls[0];
374
- expect(call[1]).toHaveProperty("transcript", "");
375
- });
376
-
377
- it("sends error response when content handler throws", async () => {
378
- conn.sendNotification
379
- .mockRejectedValueOnce(new Error("network"))
380
- .mockResolvedValueOnce(undefined);
381
-
382
- createTrajectoryReporter(conn, {
383
- trajectorySyncLevel: "full",
384
- sessionDirs: [path.join(tmpDir, "nonexistent")],
385
- });
386
-
387
- const handler = conn.onNotification.mock.calls[0][1];
388
- await handler({ request_id: "req-7", checkpoint_id: "any" });
389
-
390
- // First call fails, second call sends error response
391
- expect(conn.sendNotification).toHaveBeenCalledTimes(2);
392
- expect(conn.sendNotification).toHaveBeenLastCalledWith(
393
- "trajectory/content.response",
394
- expect.objectContaining({
395
- request_id: "req-7",
396
- error: "Content serving failed",
397
- }),
398
- );
399
- });
400
-
401
- it("searches multiple session directories", async () => {
402
- const dir1 = path.join(tmpDir, "dir1");
403
- const dir2 = path.join(tmpDir, "dir2");
404
- const transcript = JSON.stringify({ type: "user", message: "Found in dir2" }) + "\n";
405
-
406
- // Only dir2 has the session
407
- fs.mkdirSync(dir1, { recursive: true });
408
- writeSessionState(dir2, "sess-multi-dir", {}, transcript);
409
-
410
- createTrajectoryReporter(conn, {
411
- trajectorySyncLevel: "full",
412
- sessionDirs: [dir1, dir2],
413
- });
414
-
415
- const handler = conn.onNotification.mock.calls[0][1];
416
- await handler({ request_id: "req-8", checkpoint_id: "sess-multi-dir-step1" });
417
-
418
- expect(conn.sendNotification).toHaveBeenCalledWith(
419
- "trajectory/content.response",
420
- expect.objectContaining({
421
- transcript: expect.stringContaining("Found in dir2"),
422
- }),
423
- );
424
- });
425
- });
@@ -1,26 +1,21 @@
1
1
  /**
2
2
  * Coordination Handler — dispatches inbound coordination messages from the MAP hub.
3
3
  *
4
- * Task operations use generic MAP scope messages (task.created, task.assigned,
5
- * task.status) matching the wire format used by cc-swarm and opentasks.
6
- * Context sharing and messaging use agent-inbox (not MAP).
7
- * Workspace execution uses x-workspace/* notifications.
4
+ * Handles x-openhive/* JSON-RPC notifications for task assignment, status updates,
5
+ * context sharing, messaging, and workspace execution.
8
6
  *
9
7
  * @module map/coordination-handler
10
8
  */
11
9
 
12
- import { WORKSPACE_METHODS, WORKSPACE_METHODS_LEGACY } from "agent-workspace";
10
+ import type { AgentManager } from "../agent/agent-manager.js";
13
11
  import type { InboxAdapter, TasksAdapter } from "../adapters/types.js";
14
-
15
- /** MAP Message shape (subset of @multi-agent-protocol/sdk Message) */
16
- export interface MAPMessage {
17
- id: string;
18
- from: string;
19
- to: string | { scope: string };
20
- timestamp: string;
21
- payload?: Record<string, unknown>;
22
- meta?: Record<string, unknown>;
23
- }
12
+ import type {
13
+ CoordinationTaskAssign,
14
+ CoordinationTaskStatus,
15
+ CoordinationContextShare,
16
+ CoordinationMessage,
17
+ TrajectoryReporter,
18
+ } from "./types.js";
24
19
 
25
20
  export interface CoordinationConnection {
26
21
  onNotification(
@@ -32,15 +27,14 @@ export interface CoordinationConnection {
32
27
  handler: (params: unknown) => void | Promise<void>,
33
28
  ): void;
34
29
  sendNotification(method: string, params: unknown): Promise<void>;
35
- onMessage(handler: (message: MAPMessage) => void | Promise<void>): void;
36
- offMessage(handler: (message: MAPMessage) => void | Promise<void>): void;
37
30
  }
38
31
 
39
32
  export interface CoordinationDeps {
40
33
  connection: CoordinationConnection;
41
- /** Used by task handlers to notify assignees */
34
+ agentManager: AgentManager;
42
35
  inboxAdapter: InboxAdapter;
43
36
  tasksAdapter: TasksAdapter;
37
+ trajectoryReporter?: TrajectoryReporter;
44
38
  /** Workspace handler from cognitive module (if available) */
45
39
  workspaceHandler?: {
46
40
  handleWorkspaceExecute(params: unknown): Promise<void>;
@@ -48,138 +42,164 @@ export interface CoordinationDeps {
48
42
  };
49
43
  }
50
44
 
51
- /** Notification method constants (workspace only — task/context/message use MAP messages) */
45
+ /** Coordination method constants */
52
46
  const METHODS = {
53
- WORKSPACE_EXECUTE: WORKSPACE_METHODS.EXECUTE,
54
- WORKSPACE_EXECUTE_LEGACY: WORKSPACE_METHODS_LEGACY.EXECUTE,
47
+ TASK_ASSIGN: "x-openhive/task.assign",
48
+ TASK_STATUS: "x-openhive/task.status",
49
+ CONTEXT_SHARE: "x-openhive/context.share",
50
+ MESSAGE_SEND: "x-openhive/message.send",
51
+ WORKSPACE_EXECUTE: "x-openhive/learning.workspace.execute",
55
52
  } as const;
56
53
 
57
54
  /**
58
- * Register coordination handlers on the MAP connection.
59
- *
60
- * Task operations are handled via MAP scope messages (onMessage).
61
- * Context sharing and messaging are handled by agent-inbox (not here).
62
- * Workspace execution uses x-workspace/* notifications.
55
+ * Register coordination notification handlers on the MAP connection.
63
56
  * Returns a cleanup function that removes all handlers.
64
57
  */
65
58
  export function setupCoordinationHandlers(
66
59
  deps: CoordinationDeps,
67
60
  ): () => void {
68
- const { connection, inboxAdapter, tasksAdapter } = deps;
69
- const notificationHandlers: Array<{ method: string; handler: (params: unknown) => void | Promise<void> }> = [];
61
+ const { connection, agentManager, inboxAdapter, tasksAdapter } = deps;
62
+ const handlers: Array<{ method: string; handler: (params: unknown) => void | Promise<void> }> = [];
70
63
 
71
64
  const register = (
72
65
  method: string,
73
66
  handler: (params: unknown) => void | Promise<void>,
74
67
  ): void => {
75
68
  connection.onNotification(method, handler);
76
- notificationHandlers.push({ method, handler });
69
+ handlers.push({ method, handler });
77
70
  };
78
71
 
79
- // =========================================================================
80
- // Task operations generic MAP scope messages
81
- // Wire format matches cc-swarm / opentasks MAP Event Bridge:
82
- // { type: "task.created", task: { id, title, status, assignee } }
83
- // { type: "task.assigned", taskId, assignee }
84
- // { type: "task.status", taskId, previous, current }
85
- // =========================================================================
86
-
87
- const messageHandler = async (message: MAPMessage): Promise<void> => {
88
- const payload = message.payload;
89
- if (!payload || typeof payload.type !== "string") return;
90
-
91
- // Skip messages we originated (echo prevention)
92
- const origin = payload._origin as string | undefined;
93
- if (origin === "macro-agent") return;
72
+ // --- Task Assignment ---
73
+ register(METHODS.TASK_ASSIGN, async (params: unknown) => {
74
+ const p = params as CoordinationTaskAssign;
75
+ if (!p?.title) return;
94
76
 
95
77
  try {
96
- switch (payload.type) {
97
- case "task.created": {
98
- const task = payload.task as
99
- | { id?: string; title?: string; status?: string; assignee?: string }
100
- | undefined;
101
- if (!task?.title) return;
102
-
103
- const taskId = await tasksAdapter.createTask({
104
- title: task.title,
105
- content: (task as Record<string, unknown>).description as string | undefined,
106
- assignee: task.assignee,
78
+ // Create task in opentasks
79
+ const taskId = await tasksAdapter.createTask({
80
+ title: p.title,
81
+ content: p.description,
82
+ assignee: p.assigned_to,
83
+ priority: p.priority === "critical" ? 1 : p.priority === "high" ? 2 : p.priority === "low" ? 4 : 3,
84
+ });
85
+
86
+ // Optionally spawn an agent to work on the task
87
+ if (p.assigned_to) {
88
+ try {
89
+ await inboxAdapter.send("system", p.assigned_to, {
90
+ type: "event",
91
+ event: "TASK_ASSIGNED",
92
+ data: { taskId, title: p.title, description: p.description },
107
93
  });
108
-
109
- if (task.assignee) {
110
- await inboxAdapter
111
- .send("system", task.assignee, {
112
- type: "event",
113
- event: "TASK_ASSIGNED",
114
- data: { taskId, title: task.title },
115
- })
116
- .catch(() => {});
117
- }
118
- break;
94
+ } catch {
95
+ // Agent may not exist locally — that's fine
119
96
  }
97
+ }
98
+ } catch (err) {
99
+ console.warn(
100
+ `[map-sidecar] Failed to handle task.assign: ${(err as Error).message}`,
101
+ );
102
+ }
103
+ });
120
104
 
121
- case "task.assigned": {
122
- const taskId = payload.taskId as string | undefined;
123
- const assignee = payload.assignee as string | undefined;
124
- if (!taskId || !assignee) return;
125
-
126
- await tasksAdapter.assignTask(taskId, assignee);
127
-
128
- await inboxAdapter
129
- .send("system", assignee, {
130
- type: "event",
131
- event: "TASK_ASSIGNED",
132
- data: { taskId },
133
- })
134
- .catch(() => {});
135
- break;
136
- }
105
+ // --- Task Status ---
106
+ register(METHODS.TASK_STATUS, async (params: unknown) => {
107
+ const p = params as CoordinationTaskStatus;
108
+ if (!p?.task_id || !p?.status) return;
137
109
 
138
- case "task.status": {
139
- const taskId = payload.taskId as string | undefined;
140
- const current = payload.current as string | undefined;
141
- if (!taskId || !current) return;
142
-
143
- const actionMap: Record<string, string> = {
144
- in_progress: "start",
145
- completed: "complete",
146
- closed: "complete",
147
- failed: "fail",
148
- blocked: "block",
149
- open: "reopen",
150
- };
151
- const action = actionMap[current];
152
- if (action) {
153
- await tasksAdapter.transitionTask(taskId, action as any);
154
- }
155
- break;
156
- }
110
+ try {
111
+ const actionMap: Record<string, string> = {
112
+ in_progress: "start",
113
+ completed: "complete",
114
+ closed: "complete",
115
+ failed: "fail",
116
+ blocked: "block",
117
+ open: "reopen",
118
+ };
119
+ const action = actionMap[p.status];
120
+ if (action) {
121
+ await tasksAdapter.transitionTask(p.task_id, action as any);
122
+ }
123
+ } catch (err) {
124
+ console.warn(
125
+ `[map-sidecar] Failed to handle task.status: ${(err as Error).message}`,
126
+ );
127
+ }
128
+ });
157
129
 
158
- // Context sharing and messaging are handled by agent-inbox directly
159
- // (not through MAP scope messages). See InboxAdapter for broadcast
160
- // scope delivery and agent-to-agent messaging.
130
+ // --- Context Share ---
131
+ register(METHODS.CONTEXT_SHARE, async (params: unknown) => {
132
+ const p = params as CoordinationContextShare;
133
+ if (!p?.context_type || !p?.data) return;
161
134
 
162
- // Ignore other message types (e.g., task.completed is informational)
163
- default:
164
- break;
135
+ try {
136
+ // Deliver context to all running agents via inbox
137
+ const agents = agentManager.list()
138
+ .filter((a: any) => a.state === "running");
139
+ for (const agent of agents) {
140
+ await inboxAdapter
141
+ .send("system", agent.id, {
142
+ type: "event",
143
+ event: "CONTEXT_SHARED",
144
+ data: {
145
+ context_type: p.context_type,
146
+ data: p.data,
147
+ source: p.source_swarm_id,
148
+ },
149
+ })
150
+ .catch(() => {});
165
151
  }
166
152
  } catch (err) {
167
153
  console.warn(
168
- `[map-sidecar] Failed to handle ${payload.type}: ${(err as Error).message}`,
154
+ `[map-sidecar] Failed to handle context.share: ${(err as Error).message}`,
169
155
  );
170
156
  }
171
- };
157
+ });
172
158
 
173
- connection.onMessage(messageHandler);
159
+ // --- Message Send ---
160
+ register(METHODS.MESSAGE_SEND, async (params: unknown) => {
161
+ const p = params as CoordinationMessage;
162
+ if (!p?.content) return;
174
163
 
175
- // =========================================================================
176
- // Workspace JSON-RPC notifications (x-workspace protocol)
177
- // =========================================================================
164
+ try {
165
+ const agents = agentManager.list()
166
+ .filter((a: any) => a.state === "running");
167
+ if (agents.length === 0) return;
168
+
169
+ // Route to the best target:
170
+ // 1. If to_swarm_id matches a local agent ID, send directly
171
+ // 2. If metadata has a target_agent hint, use it
172
+ // 3. Otherwise, send to the coordinator/head manager (parentless agent)
173
+ // 4. Fallback: first running agent
174
+ const targetId = p.to_swarm_id;
175
+ const directTarget = targetId
176
+ ? agents.find((a: any) => a.id === targetId)
177
+ : undefined;
178
+ const coordinator = agents.find((a: any) => !a.parent);
179
+ const target = directTarget ?? coordinator ?? agents[0];
180
+
181
+ await inboxAdapter.send("system", target.id, {
182
+ type: "event",
183
+ event: "EXTERNAL_MESSAGE",
184
+ data: {
185
+ from: p.from_swarm_id,
186
+ content_type: p.content_type,
187
+ content: p.content,
188
+ reply_to: p.reply_to,
189
+ metadata: p.metadata,
190
+ },
191
+ });
192
+ } catch (err) {
193
+ console.warn(
194
+ `[map-sidecar] Failed to handle message.send: ${(err as Error).message}`,
195
+ );
196
+ }
197
+ });
178
198
 
179
199
  // --- Workspace Execute (delegate to cognitive module) ---
180
200
  if (deps.workspaceHandler) {
181
201
  const wh = deps.workspaceHandler;
182
- const workspaceHandler = async (params: unknown) => {
202
+ register(METHODS.WORKSPACE_EXECUTE, async (params: unknown) => {
183
203
  try {
184
204
  await wh.handleWorkspaceExecute(params);
185
205
  } catch (err) {
@@ -187,17 +207,14 @@ export function setupCoordinationHandlers(
187
207
  `[map-sidecar] Failed to handle workspace.execute: ${(err as Error).message}`,
188
208
  );
189
209
  }
190
- };
191
- register(METHODS.WORKSPACE_EXECUTE, workspaceHandler);
192
- register(METHODS.WORKSPACE_EXECUTE_LEGACY, workspaceHandler);
210
+ });
193
211
  }
194
212
 
195
213
  // Return cleanup function
196
214
  return () => {
197
- connection.offMessage(messageHandler);
198
- for (const { method, handler } of notificationHandlers) {
215
+ for (const { method, handler } of handlers) {
199
216
  connection.offNotification(method, handler);
200
217
  }
201
- notificationHandlers.length = 0;
218
+ handlers.length = 0;
202
219
  };
203
220
  }
package/src/map/server.ts CHANGED
@@ -290,14 +290,26 @@ export function createMAPServerInstance(
290
290
  const message = data?.message;
291
291
  if (!message) return;
292
292
 
293
+ // Check if this is an ACP envelope — these should always be handled
294
+ // by the bridge, even if the target agent can't be resolved to a
295
+ // specific local agent (the bridge creates a head manager on demand).
296
+ const payload = message?.payload;
297
+ const isAcp = payload && typeof payload === 'object' &&
298
+ 'acp' in payload && 'acpContext' in payload;
299
+
293
300
  const toField = message.to;
294
301
  const mapTargetId = data?.agentId ??
295
302
  (typeof toField === "string" ? toField : toField?.agent ?? toField?.id);
296
303
  if (!mapTargetId) return;
297
304
 
298
305
  const localAgentId = mapIdToLocalId.get(mapTargetId) ?? mapTargetId;
299
- const localAgent = deps.agentManager.get(localAgentId);
300
- if (!localAgent) return;
306
+
307
+ // For ACP envelopes, always forward to bridge (it creates sessions on demand).
308
+ // For non-ACP messages, require a local agent to exist.
309
+ if (!isAcp) {
310
+ const localAgent = deps.agentManager.get(localAgentId);
311
+ if (!localAgent) return;
312
+ }
301
313
 
302
314
  // Defer ACP processing to next tick so map/send response goes out first
303
315
  setImmediate(() => {