agenticros 0.1.18 → 0.2.0

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 (164) hide show
  1. package/dist/__tests__/robot-config.test.d.ts +2 -0
  2. package/dist/__tests__/robot-config.test.d.ts.map +1 -0
  3. package/dist/__tests__/robot-config.test.js +374 -0
  4. package/dist/__tests__/robot-config.test.js.map +1 -0
  5. package/dist/__tests__/transport-shorthand.test.d.ts +2 -0
  6. package/dist/__tests__/transport-shorthand.test.d.ts.map +1 -0
  7. package/dist/__tests__/transport-shorthand.test.js +88 -0
  8. package/dist/__tests__/transport-shorthand.test.js.map +1 -0
  9. package/dist/commands/robots.d.ts +57 -0
  10. package/dist/commands/robots.d.ts.map +1 -0
  11. package/dist/commands/robots.js +678 -0
  12. package/dist/commands/robots.js.map +1 -0
  13. package/dist/commands/skills.d.ts.map +1 -1
  14. package/dist/commands/skills.js +125 -2
  15. package/dist/commands/skills.js.map +1 -1
  16. package/dist/index.js +29 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/menu.js +22 -4
  19. package/dist/menu.js.map +1 -1
  20. package/dist/util/marketplace.d.ts +57 -0
  21. package/dist/util/marketplace.d.ts.map +1 -0
  22. package/dist/util/marketplace.js +144 -0
  23. package/dist/util/marketplace.js.map +1 -0
  24. package/dist/util/mcp-discovery.d.ts +57 -0
  25. package/dist/util/mcp-discovery.d.ts.map +1 -0
  26. package/dist/util/mcp-discovery.js +153 -0
  27. package/dist/util/mcp-discovery.js.map +1 -0
  28. package/dist/util/robot-config.d.ts +167 -0
  29. package/dist/util/robot-config.d.ts.map +1 -0
  30. package/dist/util/robot-config.js +375 -0
  31. package/dist/util/robot-config.js.map +1 -0
  32. package/dist/util/skills.d.ts +6 -6
  33. package/dist/util/skills.d.ts.map +1 -1
  34. package/dist/util/skills.js +14 -9
  35. package/dist/util/skills.js.map +1 -1
  36. package/dist/util/transport-shorthand.d.ts +66 -0
  37. package/dist/util/transport-shorthand.d.ts.map +1 -0
  38. package/dist/util/transport-shorthand.js +102 -0
  39. package/dist/util/transport-shorthand.js.map +1 -0
  40. package/package.json +12 -10
  41. package/runtime/BUNDLE.json +1 -1
  42. package/runtime/README.md +62 -1
  43. package/runtime/package.json +6 -1
  44. package/runtime/packages/agenticros/openclaw.plugin.json +7 -1
  45. package/runtime/packages/agenticros/package.json +2 -1
  46. package/runtime/packages/agenticros/src/__tests__/capabilities-plugin.test.ts +568 -0
  47. package/runtime/packages/agenticros/src/config-page.ts +1 -1
  48. package/runtime/packages/agenticros/src/mission-registry.ts +22 -0
  49. package/runtime/packages/agenticros/src/service.ts +120 -2
  50. package/runtime/packages/agenticros/src/skill-loader.ts +109 -30
  51. package/runtime/packages/agenticros/src/tools/_robot-helpers.ts +68 -0
  52. package/runtime/packages/agenticros/src/tools/index.ts +49 -9
  53. package/runtime/packages/agenticros/src/tools/mission-cancel.ts +70 -0
  54. package/runtime/packages/agenticros/src/tools/ros2-action.ts +11 -4
  55. package/runtime/packages/agenticros/src/tools/ros2-camera.ts +10 -4
  56. package/runtime/packages/agenticros/src/tools/ros2-capabilities.ts +63 -0
  57. package/runtime/packages/agenticros/src/tools/ros2-depth-distance.ts +11 -4
  58. package/runtime/packages/agenticros/src/tools/ros2-discover.ts +65 -0
  59. package/runtime/packages/agenticros/src/tools/ros2-find-robots-for.ts +128 -0
  60. package/runtime/packages/agenticros/src/tools/ros2-mission.ts +311 -0
  61. package/runtime/packages/agenticros/src/tools/ros2-param.ts +20 -7
  62. package/runtime/packages/agenticros/src/tools/ros2-publish.ts +15 -8
  63. package/runtime/packages/agenticros/src/tools/ros2-robots.ts +52 -0
  64. package/runtime/packages/agenticros/src/tools/ros2-service.ts +11 -4
  65. package/runtime/packages/agenticros/src/tools/ros2-subscribe.ts +11 -4
  66. package/runtime/packages/agenticros-claude-code/README.md +52 -9
  67. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.d.ts +2 -0
  68. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.d.ts.map +1 -0
  69. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.js +773 -0
  70. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.js.map +1 -0
  71. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.d.ts +2 -0
  72. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.d.ts.map +1 -0
  73. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.js +97 -0
  74. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.js.map +1 -0
  75. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.d.ts +2 -2
  76. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.d.ts.map +1 -1
  77. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.js +5 -5
  78. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.js.map +1 -1
  79. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.d.ts +109 -0
  80. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.d.ts.map +1 -0
  81. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.js +392 -0
  82. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.js.map +1 -0
  83. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.d.ts +8 -3
  84. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.d.ts.map +1 -1
  85. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.js +30 -12
  86. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.js.map +1 -1
  87. package/runtime/packages/agenticros-claude-code/dist/index.js +5 -5
  88. package/runtime/packages/agenticros-claude-code/dist/index.js.map +1 -1
  89. package/runtime/packages/agenticros-claude-code/dist/mission-registry.d.ts +16 -0
  90. package/runtime/packages/agenticros-claude-code/dist/mission-registry.d.ts.map +1 -0
  91. package/runtime/packages/agenticros-claude-code/dist/mission-registry.js +19 -0
  92. package/runtime/packages/agenticros-claude-code/dist/mission-registry.js.map +1 -0
  93. package/runtime/packages/agenticros-claude-code/dist/tools.d.ts +9 -0
  94. package/runtime/packages/agenticros-claude-code/dist/tools.d.ts.map +1 -1
  95. package/runtime/packages/agenticros-claude-code/dist/tools.js +700 -54
  96. package/runtime/packages/agenticros-claude-code/dist/tools.js.map +1 -1
  97. package/runtime/packages/agenticros-claude-code/dist/transport.d.ts +28 -8
  98. package/runtime/packages/agenticros-claude-code/dist/transport.d.ts.map +1 -1
  99. package/runtime/packages/agenticros-claude-code/dist/transport.js +37 -34
  100. package/runtime/packages/agenticros-claude-code/dist/transport.js.map +1 -1
  101. package/runtime/packages/agenticros-claude-code/package.json +2 -1
  102. package/runtime/packages/agenticros-claude-code/src/__tests__/capabilities-mcp.test.ts +965 -0
  103. package/runtime/packages/agenticros-claude-code/src/__tests__/follow-me-registry.test.ts +119 -0
  104. package/runtime/packages/agenticros-claude-code/src/find-object/find-object.ts +7 -6
  105. package/runtime/packages/agenticros-claude-code/src/follow-me/depth-loop.ts +41 -9
  106. package/runtime/packages/agenticros-claude-code/src/follow-me/loop.ts +40 -11
  107. package/runtime/packages/agenticros-claude-code/src/index.ts +5 -5
  108. package/runtime/packages/agenticros-claude-code/src/mission-registry.ts +21 -0
  109. package/runtime/packages/agenticros-claude-code/src/tools.ts +668 -56
  110. package/runtime/packages/agenticros-claude-code/src/transport.ts +43 -43
  111. package/runtime/packages/agenticros-gemini/package.json +1 -0
  112. package/runtime/packages/agenticros-gemini/src/__tests__/mission-cancel.test.ts +197 -0
  113. package/runtime/packages/agenticros-gemini/src/__tests__/mission-transcripts.test.ts +178 -0
  114. package/runtime/packages/agenticros-gemini/src/tools.ts +504 -21
  115. package/runtime/packages/agenticros-gemini/src/transport.ts +40 -43
  116. package/runtime/packages/core/LICENSE +192 -0
  117. package/runtime/packages/core/README.md +88 -0
  118. package/runtime/packages/core/package.json +36 -3
  119. package/runtime/packages/core/src/__tests__/capabilities.test.ts +266 -0
  120. package/runtime/packages/core/src/__tests__/discovery.test.ts +206 -0
  121. package/runtime/packages/core/src/__tests__/find-robots-for.test.ts +258 -0
  122. package/runtime/packages/core/src/__tests__/mission-cancel-transcript.test.ts +371 -0
  123. package/runtime/packages/core/src/__tests__/mission-transcript-sink.test.ts +189 -0
  124. package/runtime/packages/core/src/__tests__/mission.test.ts +501 -0
  125. package/runtime/packages/core/src/__tests__/planner.test.ts +279 -0
  126. package/runtime/packages/core/src/__tests__/robots.test.ts +406 -0
  127. package/runtime/packages/core/src/__tests__/topic-utils.test.ts +105 -0
  128. package/runtime/packages/core/src/__tests__/transport-pool.test.ts +295 -0
  129. package/runtime/packages/core/src/capabilities.ts +314 -0
  130. package/runtime/packages/core/src/config.ts +221 -39
  131. package/runtime/packages/core/src/discovery.ts +193 -0
  132. package/runtime/packages/core/src/find-robots-for.ts +164 -0
  133. package/runtime/packages/core/src/index.ts +67 -1
  134. package/runtime/packages/core/src/mission-registry.ts +142 -0
  135. package/runtime/packages/core/src/mission-transcript-sink.ts +81 -0
  136. package/runtime/packages/core/src/mission.ts +574 -0
  137. package/runtime/packages/core/src/planner/index.ts +564 -0
  138. package/runtime/packages/core/src/robots.ts +282 -0
  139. package/runtime/packages/core/src/topic-utils.ts +47 -9
  140. package/runtime/packages/core/src/transport-pool.ts +171 -0
  141. package/runtime/ros2_ws/src/agenticros_discovery/agenticros_discovery/discovery_node.py +108 -13
  142. package/runtime/ros2_ws/src/agenticros_msgs/CMakeLists.txt +1 -0
  143. package/runtime/ros2_ws/src/agenticros_msgs/msg/RobotInfo.msg +44 -0
  144. package/runtime/scripts/__tests__/refresh-skill-deps.test.mjs +222 -0
  145. package/runtime/scripts/configure_agenticros.sh +0 -0
  146. package/runtime/scripts/configure_for_sim.sh +0 -0
  147. package/runtime/scripts/fix-openclaw-control-ui-path.sh +0 -0
  148. package/runtime/scripts/install_cli.sh +0 -0
  149. package/runtime/scripts/install_rosbridge_from_source.sh +0 -0
  150. package/runtime/scripts/onboard_robot.sh +0 -0
  151. package/runtime/scripts/pack-runtime.mjs +0 -0
  152. package/runtime/scripts/refresh-skill-deps.mjs +381 -0
  153. package/runtime/scripts/run_demo_native.sh +0 -0
  154. package/runtime/scripts/run_nemoclaw_host_stack.sh +0 -0
  155. package/runtime/scripts/run_robot_rosbridge.sh +0 -0
  156. package/runtime/scripts/setup_gateway_plugin.sh +32 -5
  157. package/runtime/scripts/setup_robot.sh +0 -0
  158. package/runtime/scripts/setup_workspace.sh +0 -0
  159. package/runtime/scripts/sim/run_sim.sh +0 -0
  160. package/runtime/scripts/smoke_test_nemoclaw.sh +0 -0
  161. package/runtime/scripts/start_demo.sh +0 -0
  162. package/runtime/scripts/sync-skill-tools.mjs +69 -12
  163. package/runtime/scripts/use-openclaw-2026.2.26.sh +0 -0
  164. package/runtime/scripts/use-openclaw-2026.3.11.sh +0 -0
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Per-robot isolation tests for the follow-me + find-object skill registries.
3
+ *
4
+ * Phase 1.d-extend asserts that each robot keeps its own independent
5
+ * follow-me loop instance, keyed by `robot.id`. If two robots ever share a
6
+ * single loop, starting follow-me on one would silently steer the other
7
+ * and `ros2_follow_me_status` would conflate state — exactly the regression
8
+ * this suite is designed to catch.
9
+ *
10
+ * These tests run as pure registry checks — they construct a minimal mock
11
+ * `RosTransport` so the loop constructors don't reach for the network, and
12
+ * they reset the registry between tests so suites stay deterministic.
13
+ */
14
+ import { test } from "node:test";
15
+ import assert from "node:assert/strict";
16
+ import type { ResolvedRobot, RosTransport } from "@agenticros/core";
17
+ import {
18
+ getFollowMeDepth,
19
+ _resetFollowMeDepthRegistry,
20
+ } from "../follow-me/depth-loop.js";
21
+ import {
22
+ getFollowMeLocal,
23
+ _resetFollowMeLocalRegistry,
24
+ } from "../follow-me/loop.js";
25
+ import { parseConfig } from "@agenticros/core";
26
+
27
+ /**
28
+ * Minimal `RosTransport` stub. The follow-me loops only touch the transport
29
+ * inside `start()` (subscribe + publish) — the constructor + registry path
30
+ * never call into it. We provide a no-op surface so TypeScript is happy and
31
+ * any accidental call surfaces as an obvious "TypeError: ... not a function".
32
+ */
33
+ function stubTransport(): RosTransport {
34
+ // The follow-me loop constructors don't touch the transport — they only
35
+ // call into it from `start()`. We cast through `unknown` so the test
36
+ // doesn't have to track the full RosTransport surface area; any
37
+ // accidental method call surfaces as a clean "not a function" error.
38
+ return {} as unknown as RosTransport;
39
+ }
40
+
41
+ function makeRobot(id: string, namespace: string): ResolvedRobot {
42
+ return {
43
+ id,
44
+ name: id,
45
+ namespace,
46
+ cameraTopic: "",
47
+ kind: "amr",
48
+ sensors: { has_realsense: false, has_lidar: false, has_arm: false },
49
+ source: "config",
50
+ };
51
+ }
52
+
53
+ const CONFIG = parseConfig({ robot: { namespace: "robot-a" } });
54
+
55
+ test("follow-me-registry: getFollowMeDepth returns the same instance for the same robot.id", () => {
56
+ _resetFollowMeDepthRegistry();
57
+ const transport = stubTransport();
58
+ const robotA = makeRobot("robot-a", "robot-a");
59
+ const first = getFollowMeDepth(robotA, CONFIG, transport);
60
+ const second = getFollowMeDepth(robotA, CONFIG, transport);
61
+ assert.strictEqual(
62
+ first,
63
+ second,
64
+ "two calls with the same robot.id must return the identical loop instance",
65
+ );
66
+ assert.equal(first.robotId, "robot-a", "instance should remember its robot.id");
67
+ });
68
+
69
+ test("follow-me-registry: getFollowMeDepth returns DIFFERENT instances for DIFFERENT robot.ids", () => {
70
+ _resetFollowMeDepthRegistry();
71
+ const transport = stubTransport();
72
+ const robotA = makeRobot("robot-a", "robot-a");
73
+ const robotB = makeRobot("robot-b", "robot-b");
74
+ const loopA = getFollowMeDepth(robotA, CONFIG, transport);
75
+ const loopB = getFollowMeDepth(robotB, CONFIG, transport);
76
+ assert.notStrictEqual(loopA, loopB, "each robot must get its own independent loop");
77
+ assert.equal(loopA.robotId, "robot-a");
78
+ assert.equal(loopB.robotId, "robot-b");
79
+ // And a follow-up lookup must still return each robot's own loop, not
80
+ // the most-recently-created one.
81
+ assert.strictEqual(getFollowMeDepth(robotA, CONFIG, transport), loopA);
82
+ assert.strictEqual(getFollowMeDepth(robotB, CONFIG, transport), loopB);
83
+ });
84
+
85
+ test("follow-me-registry: getFollowMeLocal returns the same instance for the same robot.id", () => {
86
+ _resetFollowMeLocalRegistry();
87
+ const transport = stubTransport();
88
+ const robotA = makeRobot("robot-a", "robot-a");
89
+ const first = getFollowMeLocal(robotA, CONFIG, transport);
90
+ const second = getFollowMeLocal(robotA, CONFIG, transport);
91
+ assert.strictEqual(
92
+ first,
93
+ second,
94
+ "two calls with the same robot.id must return the identical loop instance",
95
+ );
96
+ assert.equal(first.robotId, "robot-a", "instance should remember its robot.id");
97
+ });
98
+
99
+ test("follow-me-registry: getFollowMeLocal returns DIFFERENT instances for DIFFERENT robot.ids", () => {
100
+ _resetFollowMeLocalRegistry();
101
+ const transport = stubTransport();
102
+ const robotA = makeRobot("robot-a", "robot-a");
103
+ const robotB = makeRobot("robot-b", "robot-b");
104
+ const loopA = getFollowMeLocal(robotA, CONFIG, transport);
105
+ const loopB = getFollowMeLocal(robotB, CONFIG, transport);
106
+ assert.notStrictEqual(loopA, loopB, "each robot must get its own independent loop");
107
+ assert.equal(loopA.robotId, "robot-a");
108
+ assert.equal(loopB.robotId, "robot-b");
109
+ });
110
+
111
+ test("follow-me-registry: status() defaults to disabled on a fresh per-robot instance", () => {
112
+ _resetFollowMeDepthRegistry();
113
+ const transport = stubTransport();
114
+ const robot = makeRobot("robot-a", "robot-a");
115
+ const loop = getFollowMeDepth(robot, CONFIG, transport);
116
+ const s = loop.status();
117
+ assert.equal(s.enabled, false, "a freshly-registered loop should not be enabled");
118
+ assert.equal(s.tracking, false, "a freshly-registered loop should not be tracking");
119
+ });
@@ -3,7 +3,7 @@
3
3
  * camera feed, then stop. Used by the ros2_find_object MCP tool.
4
4
  */
5
5
 
6
- import type { AgenticROSConfig, RosTransport } from "@agenticros/core";
6
+ import type { AgenticROSConfig, ResolvedRobot, RosTransport } from "@agenticros/core";
7
7
  import { resolveCameraSubscribeTopic, toNamespacedTopic } from "@agenticros/core";
8
8
  import {
9
9
  ROS_MSG_COMPRESSED_IMAGE,
@@ -48,6 +48,7 @@ export interface FindObjectResult {
48
48
  }
49
49
 
50
50
  export async function findObject(
51
+ robot: ResolvedRobot,
51
52
  config: AgenticROSConfig,
52
53
  transport: RosTransport,
53
54
  opts: FindObjectOptions,
@@ -78,10 +79,10 @@ export async function findObject(
78
79
  const detector = new PersonDetector({ scoreThreshold: minConfidence });
79
80
  await detector.load();
80
81
 
81
- const cmdVelTopic = resolveCmdVelTopic(config);
82
+ const cmdVelTopic = resolveCmdVelTopic(config, robot);
82
83
  const colorTopic = resolveCameraSubscribeTopic(
83
- config,
84
- (config.robot?.cameraTopic ?? "").trim() || DEFAULT_COLOR_TOPIC,
84
+ robot.namespace,
85
+ robot.cameraTopic.trim() || DEFAULT_COLOR_TOPIC,
85
86
  );
86
87
 
87
88
  const startedAt = Date.now();
@@ -178,9 +179,9 @@ function sleep(ms: number): Promise<void> {
178
179
  return new Promise((resolve) => setTimeout(resolve, ms));
179
180
  }
180
181
 
181
- function resolveCmdVelTopic(config: AgenticROSConfig): string {
182
+ function resolveCmdVelTopic(config: AgenticROSConfig, robot: ResolvedRobot): string {
182
183
  const raw = (config.teleop?.cmdVelTopic ?? "").trim() || "/cmd_vel";
183
- const namespaced = toNamespacedTopic(config, raw);
184
+ const namespaced = toNamespacedTopic(robot.namespace, raw);
184
185
  const match = namespaced.match(/^\/([^/]+)\/cmd_vel$/i);
185
186
  const segment = match?.[1] ?? "";
186
187
  if (match && !segment.toLowerCase().startsWith("robot")) {
@@ -20,7 +20,12 @@
20
20
  * person re-identification.
21
21
  */
22
22
 
23
- import type { AgenticROSConfig, RosTransport, Subscription } from "@agenticros/core";
23
+ import type {
24
+ AgenticROSConfig,
25
+ ResolvedRobot,
26
+ RosTransport,
27
+ Subscription,
28
+ } from "@agenticros/core";
24
29
  import { resolveCameraSubscribeTopic, toNamespacedTopic } from "@agenticros/core";
25
30
  import {
26
31
  ROS_MSG_IMAGE,
@@ -93,10 +98,16 @@ export class FollowMeDepth {
93
98
  private tracking = false;
94
99
 
95
100
  constructor(
101
+ private readonly robot: ResolvedRobot,
96
102
  private readonly config: AgenticROSConfig,
97
103
  private readonly transport: RosTransport,
98
104
  ) {}
99
105
 
106
+ /** Stable id of the robot this loop is bound to. Used by the registry below. */
107
+ get robotId(): string {
108
+ return this.robot.id;
109
+ }
110
+
100
111
  async start(opts: StartOptions = {}): Promise<void> {
101
112
  if (this.enabled) return;
102
113
  this.lastError = null;
@@ -145,7 +156,7 @@ export class FollowMeDepth {
145
156
  }
146
157
 
147
158
  private subscribeDepth(): void {
148
- const depthTopic = resolveCameraSubscribeTopic(this.config, DEFAULT_DEPTH_TOPIC);
159
+ const depthTopic = resolveCameraSubscribeTopic(this.robot.namespace, DEFAULT_DEPTH_TOPIC);
149
160
  this.depthSub = this.transport.subscribe(
150
161
  { topic: depthTopic, type: ROS_MSG_IMAGE },
151
162
  (msg) => {
@@ -228,7 +239,7 @@ export class FollowMeDepth {
228
239
  }
229
240
 
230
241
  private async publishTwist(twist: Twist): Promise<void> {
231
- const topic = resolveCmdVelTopic(this.config);
242
+ const topic = resolveCmdVelTopic(this.config, this.robot);
232
243
  const message = {
233
244
  linear: { x: twist.linearX, y: 0, z: 0 },
234
245
  angular: { x: 0, y: 0, z: twist.angularZ },
@@ -401,9 +412,9 @@ function sampleCellBand(
401
412
  return n;
402
413
  }
403
414
 
404
- function resolveCmdVelTopic(config: AgenticROSConfig): string {
415
+ function resolveCmdVelTopic(config: AgenticROSConfig, robot: ResolvedRobot): string {
405
416
  const raw = (config.teleop?.cmdVelTopic ?? "").trim() || "/cmd_vel";
406
- const namespaced = toNamespacedTopic(config, raw);
417
+ const namespaced = toNamespacedTopic(robot.namespace, raw);
407
418
  const match = namespaced.match(/^\/([^/]+)\/cmd_vel$/i);
408
419
  const segment = match?.[1] ?? "";
409
420
  if (match && !segment.toLowerCase().startsWith("robot")) {
@@ -412,9 +423,30 @@ function resolveCmdVelTopic(config: AgenticROSConfig): string {
412
423
  return namespaced;
413
424
  }
414
425
 
415
- let singleton: FollowMeDepth | null = null;
426
+ /**
427
+ * Per-robot registry. Each robot gets its own `FollowMeDepth` instance the
428
+ * first time anyone asks for one — multi-robot Phase 1.d-extend.
429
+ *
430
+ * Keying by `robot.id` (not the namespace string) so two configs that resolve
431
+ * to the same id share the same loop, and a config swap that *changes* the id
432
+ * yields a fresh loop with fresh subscriptions.
433
+ */
434
+ const instances = new Map<string, FollowMeDepth>();
435
+
436
+ export function getFollowMeDepth(
437
+ robot: ResolvedRobot,
438
+ config: AgenticROSConfig,
439
+ transport: RosTransport,
440
+ ): FollowMeDepth {
441
+ let entry = instances.get(robot.id);
442
+ if (!entry) {
443
+ entry = new FollowMeDepth(robot, config, transport);
444
+ instances.set(robot.id, entry);
445
+ }
446
+ return entry;
447
+ }
416
448
 
417
- export function getFollowMeDepth(config: AgenticROSConfig, transport: RosTransport): FollowMeDepth {
418
- if (!singleton) singleton = new FollowMeDepth(config, transport);
419
- return singleton;
449
+ /** Test-only: clear the registry so suites can run in isolation. */
450
+ export function _resetFollowMeDepthRegistry(): void {
451
+ instances.clear();
420
452
  }
@@ -5,7 +5,12 @@
5
5
  * This is the `mode: 'local'` alternative to the agenticros_follow_me ROS2 node.
6
6
  */
7
7
 
8
- import type { AgenticROSConfig, RosTransport, Subscription } from "@agenticros/core";
8
+ import type {
9
+ AgenticROSConfig,
10
+ ResolvedRobot,
11
+ RosTransport,
12
+ Subscription,
13
+ } from "@agenticros/core";
9
14
  import { resolveCameraSubscribeTopic, toNamespacedTopic } from "@agenticros/core";
10
15
  import {
11
16
  ROS_MSG_COMPRESSED_IMAGE,
@@ -75,10 +80,16 @@ export class FollowMeLocal {
75
80
  private tracking = false;
76
81
 
77
82
  constructor(
83
+ private readonly robot: ResolvedRobot,
78
84
  private readonly config: AgenticROSConfig,
79
85
  private readonly transport: RosTransport,
80
86
  ) {}
81
87
 
88
+ /** Stable id of the robot this loop is bound to. Used by the registry below. */
89
+ get robotId(): string {
90
+ return this.robot.id;
91
+ }
92
+
82
93
  async start(opts: StartOptions = {}): Promise<void> {
83
94
  if (this.enabled) return;
84
95
  this.lastError = null;
@@ -129,9 +140,9 @@ export class FollowMeLocal {
129
140
  }
130
141
 
131
142
  private subscribeFrames(): void {
132
- const colorTopicRaw = (this.config.robot?.cameraTopic ?? "").trim() || DEFAULT_COLOR_TOPIC;
133
- const colorTopic = resolveCameraSubscribeTopic(this.config, colorTopicRaw);
134
- const depthTopic = resolveCameraSubscribeTopic(this.config, DEFAULT_DEPTH_TOPIC);
143
+ const colorTopicRaw = this.robot.cameraTopic.trim() || DEFAULT_COLOR_TOPIC;
144
+ const colorTopic = resolveCameraSubscribeTopic(this.robot.namespace, colorTopicRaw);
145
+ const depthTopic = resolveCameraSubscribeTopic(this.robot.namespace, DEFAULT_DEPTH_TOPIC);
135
146
  const isCompressed = colorTopic.includes("compressed");
136
147
 
137
148
  this.colorSub = this.transport.subscribe(
@@ -261,7 +272,7 @@ export class FollowMeLocal {
261
272
  }
262
273
 
263
274
  private async publishTwist(twist: Twist): Promise<void> {
264
- const topic = resolveCmdVelTopic(this.config);
275
+ const topic = resolveCmdVelTopic(this.config, this.robot);
265
276
  const message = {
266
277
  linear: { x: twist.linearX, y: 0, z: 0 },
267
278
  angular: { x: 0, y: 0, z: twist.angularZ },
@@ -310,9 +321,9 @@ function sampleDepthRegion(d: LatestDepth, x0: number, y0: number, x1: number, y
310
321
  return out;
311
322
  }
312
323
 
313
- function resolveCmdVelTopic(config: AgenticROSConfig): string {
324
+ function resolveCmdVelTopic(config: AgenticROSConfig, robot: ResolvedRobot): string {
314
325
  const raw = (config.teleop?.cmdVelTopic ?? "").trim() || "/cmd_vel";
315
- const namespaced = toNamespacedTopic(config, raw);
326
+ const namespaced = toNamespacedTopic(robot.namespace, raw);
316
327
  // Apply same uuid → robot<uuid-no-dashes> rewrite as ros2_publish handler.
317
328
  const match = namespaced.match(/^\/([^/]+)\/cmd_vel$/i);
318
329
  const segment = match?.[1] ?? "";
@@ -322,9 +333,27 @@ function resolveCmdVelTopic(config: AgenticROSConfig): string {
322
333
  return namespaced;
323
334
  }
324
335
 
325
- let singleton: FollowMeLocal | null = null;
336
+ /**
337
+ * Per-robot registry. Each robot gets its own `FollowMeLocal` instance (with
338
+ * its own YOLO detector + camera subs) the first time anyone asks for one.
339
+ * Multi-robot Phase 1.d-extend.
340
+ */
341
+ const instances = new Map<string, FollowMeLocal>();
342
+
343
+ export function getFollowMeLocal(
344
+ robot: ResolvedRobot,
345
+ config: AgenticROSConfig,
346
+ transport: RosTransport,
347
+ ): FollowMeLocal {
348
+ let entry = instances.get(robot.id);
349
+ if (!entry) {
350
+ entry = new FollowMeLocal(robot, config, transport);
351
+ instances.set(robot.id, entry);
352
+ }
353
+ return entry;
354
+ }
326
355
 
327
- export function getFollowMeLocal(config: AgenticROSConfig, transport: RosTransport): FollowMeLocal {
328
- if (!singleton) singleton = new FollowMeLocal(config, transport);
329
- return singleton;
356
+ /** Test-only: clear the registry so suites can run in isolation. */
357
+ export function _resetFollowMeLocalRegistry(): void {
358
+ instances.clear();
330
359
  }
@@ -15,7 +15,7 @@ import type { AgenticROSConfig } from "@agenticros/core";
15
15
  import { renderAgenticROSBanner } from "@agenticros/core";
16
16
  import { loadConfig } from "./config.js";
17
17
  import { connect, disconnect } from "./transport.js";
18
- import { TOOLS, handleToolCall, MEMORY_TOOL_NAMES } from "./tools.js";
18
+ import { TOOLS, handleToolCall, MEMORY_TOOL_NAMES, NO_TRANSPORT_TOOL_NAMES } from "./tools.js";
19
19
  import { ensureMemory } from "./memory.js";
20
20
 
21
21
  let config: AgenticROSConfig | null = null;
@@ -81,10 +81,10 @@ function main(): void {
81
81
  process.stderr.write(
82
82
  `[AgenticROS] Config: robot.namespace=${ns ? `"${ns}"` : '""'} → ${ns ? `/${ns}/cmd_vel` : "/cmd_vel"}\n`,
83
83
  );
84
- // Memory tools are self-contained they don't talk to the robot, so
85
- // don't force a Zenoh/ROS transport connection (and don't fail if zenohd
86
- // isn't running). Every other tool needs the transport up.
87
- if (!MEMORY_TOOL_NAMES.has(name)) {
84
+ // Tools that don't need the ROS transport (memory + capabilities) are
85
+ // self-contained skip `ensureConnected()` so they work even when
86
+ // zenohd is down. Every other tool needs the transport up.
87
+ if (!NO_TRANSPORT_TOOL_NAMES.has(name)) {
88
88
  await ensureConnected();
89
89
  }
90
90
  const result = await handleToolCall(name, args ?? {}, config);
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Per-process singleton mission registry for the Claude Code MCP server.
3
+ *
4
+ * Each `run_mission` invocation registers a fresh `mission_id` here so
5
+ * a sibling `mission_cancel` MCP call can flip the cancellation token
6
+ * mid-run. The runner picks that up at the next step boundary and
7
+ * gracefully marks remaining steps as "cancelled".
8
+ *
9
+ * Lives at the module scope (not as a per-request object) because MCP
10
+ * tool calls are dispatched independently — the cancel tool has no
11
+ * other way to find the in-flight mission. See
12
+ * docs/strategy-ai-agents-plus-ros.md §4 Phase 1.f.
13
+ */
14
+
15
+ import { MissionRegistry } from "@agenticros/core";
16
+
17
+ const registry = new MissionRegistry();
18
+
19
+ export function getMissionRegistry(): MissionRegistry {
20
+ return registry;
21
+ }