meshy-node 0.0.5 → 0.0.6

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.
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Meshy Dashboard</title>
7
7
  <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>&#x1F578;</text></svg>" />
8
- <script type="module" crossorigin src="/assets/index-Cf2Ckzj8.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-Br1FoGY0.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-CS0F09J4.css">
10
10
  </head>
11
11
  <body>
package/main.cjs CHANGED
@@ -23672,6 +23672,10 @@ var NodeRegistry = class {
23672
23672
  endpoint: info.endpoint
23673
23673
  });
23674
23674
  }
23675
+ upsertNode(info) {
23676
+ this.nodes.set(info.id, { ...info });
23677
+ this.store.upsertNode(info);
23678
+ }
23675
23679
  removeNode(id) {
23676
23680
  this.nodes.delete(id);
23677
23681
  this.store.removeNode(id);
@@ -24092,6 +24096,44 @@ var TaskEngine = class {
24092
24096
  const tasks = this.store.getAllTasks(filter);
24093
24097
  return { tasks, total: tasks.length };
24094
24098
  }
24099
+ importTasks(tasks) {
24100
+ let created = 0;
24101
+ let updated = 0;
24102
+ for (const task of tasks) {
24103
+ const existing = this.store.getTask(task.id);
24104
+ if (!existing) {
24105
+ this.store.createTask(task);
24106
+ created++;
24107
+ this.eventBus.emit("task.created", {
24108
+ taskId: task.id,
24109
+ title: task.title,
24110
+ priority: task.priority
24111
+ });
24112
+ this.eventBus.emit("task.status", {
24113
+ taskId: task.id,
24114
+ status: task.status,
24115
+ ...task.result ? { result: task.result } : {},
24116
+ ...task.error ? { error: task.error } : {}
24117
+ });
24118
+ continue;
24119
+ }
24120
+ if (existing.updatedAt > task.updatedAt) {
24121
+ continue;
24122
+ }
24123
+ const previousStatus = existing.status;
24124
+ this.store.updateTask(task.id, task);
24125
+ updated++;
24126
+ if (task.status !== previousStatus) {
24127
+ this.eventBus.emit("task.status", {
24128
+ taskId: task.id,
24129
+ status: task.status,
24130
+ ...task.result ? { result: task.result } : {},
24131
+ ...task.error ? { error: task.error } : {}
24132
+ });
24133
+ }
24134
+ }
24135
+ return { created, updated };
24136
+ }
24095
24137
  updateTask(id, updates) {
24096
24138
  const existing = this.store.getTask(id);
24097
24139
  if (!existing) return null;
@@ -24789,11 +24831,67 @@ var HeartbeatMonitor = class {
24789
24831
  }
24790
24832
  // ── Leader: handle keepalive from follower ──────────────────────────
24791
24833
  handleKeepalive(req) {
24792
- const { nodeId, status, load, devtunnelEnabled, devtunnelEndpoint, previewOrigin, workDirFolders } = req;
24834
+ const {
24835
+ nodeId,
24836
+ name,
24837
+ endpoint,
24838
+ status,
24839
+ load,
24840
+ capabilities,
24841
+ transportType,
24842
+ workDir,
24843
+ devtunnelEnabled,
24844
+ devtunnelEndpoint,
24845
+ previewOrigin,
24846
+ workDirFolders
24847
+ } = req;
24848
+ const existingNode = this.nodeRegistry.getNode(nodeId);
24849
+ const normalizedName = name ?? existingNode?.name ?? nodeId;
24850
+ const normalizedEndpoint = endpoint ?? existingNode?.endpoint ?? devtunnelEndpoint ?? "";
24851
+ const normalizedCapabilities = capabilities ?? existingNode?.capabilities ?? [];
24852
+ const currentCapabilities = existingNode?.capabilities ?? [];
24793
24853
  this.log.debug("keepalive received", { nodeId, status });
24794
24854
  this.followerMissedMap.set(nodeId, 0);
24795
- this.nodeRegistry.updateHeartbeat(nodeId, Date.now());
24796
- const node = this.nodeRegistry.getNode(nodeId);
24855
+ let node = existingNode;
24856
+ const now = Date.now();
24857
+ if (!node) {
24858
+ this.nodeRegistry.addNode({
24859
+ id: nodeId,
24860
+ name: normalizedName,
24861
+ endpoint: normalizedEndpoint,
24862
+ role: "follower",
24863
+ status,
24864
+ capabilities: normalizedCapabilities,
24865
+ joinedAt: now,
24866
+ lastHeartbeat: now,
24867
+ ...transportType ? { transportType } : {},
24868
+ ...workDir ? { workDir } : {},
24869
+ ...devtunnelEnabled && devtunnelEndpoint ? { devtunnelEndpoint } : {},
24870
+ ...previewOrigin ? { previewOrigin } : {},
24871
+ ...workDirFolders ? { workDirFolders } : {}
24872
+ });
24873
+ this.log.info("keepalive restored missing follower", { nodeId, endpoint: normalizedEndpoint });
24874
+ node = this.nodeRegistry.getNode(nodeId);
24875
+ }
24876
+ this.nodeRegistry.updateHeartbeat(nodeId, now);
24877
+ node = this.nodeRegistry.getNode(nodeId);
24878
+ if (node && normalizedEndpoint && node.endpoint !== normalizedEndpoint) {
24879
+ this.nodeRegistry.updateEndpoint(nodeId, normalizedEndpoint);
24880
+ node = this.nodeRegistry.getNode(nodeId);
24881
+ }
24882
+ if (node && (node.name !== normalizedName || node.transportType !== transportType || node.workDir !== workDir || currentCapabilities.length !== normalizedCapabilities.length || currentCapabilities.some((capability, index) => capability !== normalizedCapabilities[index]))) {
24883
+ this.nodeRegistry.upsertNode({
24884
+ ...node,
24885
+ name: normalizedName,
24886
+ endpoint: normalizedEndpoint || node.endpoint,
24887
+ status,
24888
+ lastHeartbeat: now,
24889
+ capabilities: normalizedCapabilities,
24890
+ ...transportType ? { transportType } : {},
24891
+ ...workDir ? { workDir } : {}
24892
+ });
24893
+ node = this.nodeRegistry.getNode(nodeId);
24894
+ }
24797
24895
  if (node && node.status === "offline") {
24798
24896
  this.nodeRegistry.updateStatus(nodeId, status);
24799
24897
  this.log.info("keepalive restored node", { nodeId, status });
@@ -24840,8 +24938,13 @@ var HeartbeatMonitor = class {
24840
24938
  const self = this.nodeRegistry.getSelf();
24841
24939
  const request = {
24842
24940
  nodeId: self.id,
24941
+ name: self.name,
24942
+ endpoint: self.endpoint,
24843
24943
  status: self.status,
24844
24944
  load: 0,
24945
+ capabilities: self.capabilities,
24946
+ transportType: self.transportType,
24947
+ workDir: self.workDir,
24845
24948
  devtunnelEnabled: self.devtunnelEndpoint !== void 0,
24846
24949
  devtunnelEndpoint: self.devtunnelEndpoint,
24847
24950
  previewOrigin: self.previewOrigin,
@@ -26191,7 +26294,8 @@ ${joinErrors.map((e) => ` - ${e}`).join("\n")}`
26191
26294
  id: this.selfInfo.id,
26192
26295
  endpoint: this.selfInfo.endpoint,
26193
26296
  name: this.config.node.name,
26194
- capabilities: []
26297
+ capabilities: [],
26298
+ tasks: this.taskEngine.listTasks().tasks
26195
26299
  };
26196
26300
  if (this.selfInfo.devtunnelEndpoint) {
26197
26301
  joinBody.devtunnelEndpoint = this.selfInfo.devtunnelEndpoint;
@@ -26212,13 +26316,36 @@ ${joinErrors.map((e) => ` - ${e}`).join("\n")}`
26212
26316
  throw new Error(`Failed to join cluster via ${seedUrl}: ${response.status}`);
26213
26317
  }
26214
26318
  const data = await response.json();
26215
- for (const node of data.nodes) {
26216
- const existing = this.nodeRegistry.getNode(node.id);
26217
- if (!existing && node.id !== this.selfInfo.id) {
26218
- this.nodeRegistry.addNode(node);
26219
- }
26319
+ this.applyClusterState(data.nodes, data.leaderId, data.term);
26320
+ }
26321
+ async leaveCluster() {
26322
+ const leader = this.nodeRegistry.getLeader();
26323
+ const leaderEndpoint = this.nodeRegistry.getLeaderEndpoint();
26324
+ const self = this.nodeRegistry.getSelf();
26325
+ if (!leader || !leaderEndpoint || leader.id === self.id) {
26326
+ return;
26220
26327
  }
26221
- this.nodeRegistry.setLeader(data.leaderId, data.term);
26328
+ try {
26329
+ await fetch(`${leaderEndpoint}/api/cluster/leave`, {
26330
+ method: "POST",
26331
+ headers: { "Content-Type": "application/json" },
26332
+ body: JSON.stringify({ nodeId: self.id })
26333
+ });
26334
+ } catch {
26335
+ }
26336
+ const refreshedSelf = {
26337
+ ...self,
26338
+ role: "follower",
26339
+ status: "online",
26340
+ lastHeartbeat: Date.now(),
26341
+ workDirFolders: listWorkDirFolders(this.getWorkDir())
26342
+ };
26343
+ this.selfInfo = refreshedSelf;
26344
+ this.nodeRegistry.setSelf(refreshedSelf);
26345
+ this.nodeRegistry.syncFromHeartbeat([refreshedSelf]);
26346
+ this.nodeRegistry.clearLeader();
26347
+ this.election.stepDown();
26348
+ await this.election.startElection();
26222
26349
  }
26223
26350
  async stop() {
26224
26351
  if (!this.started) return;
@@ -26381,6 +26508,22 @@ ${joinErrors.map((e) => ` - ${e}`).join("\n")}`
26381
26508
  this.selfInfo.role = current.role;
26382
26509
  this.selfInfo.status = current.status;
26383
26510
  }
26511
+ applyClusterState(nodes, leaderId, term) {
26512
+ const remoteSelf = nodes.find((node) => node.id === this.selfInfo.id);
26513
+ const nextSelf = {
26514
+ ...this.selfInfo,
26515
+ ...remoteSelf ?? {},
26516
+ transportType: this.config.transport.type,
26517
+ workDir: this.selfInfo.workDir,
26518
+ workDirFolders: listWorkDirFolders(this.getWorkDir()),
26519
+ ...this.selfInfo.devtunnelEndpoint ? { devtunnelEndpoint: this.selfInfo.devtunnelEndpoint } : {},
26520
+ ...this.selfInfo.previewOrigin ? { previewOrigin: this.selfInfo.previewOrigin } : {}
26521
+ };
26522
+ this.selfInfo = nextSelf;
26523
+ this.nodeRegistry.setSelf(nextSelf);
26524
+ this.nodeRegistry.syncFromHeartbeat(nodes);
26525
+ this.election.handleLeaderAnnounce({ leaderId, term });
26526
+ }
26384
26527
  cloneTransportConfig(config) {
26385
26528
  return {
26386
26529
  type: config.type,
@@ -30611,12 +30754,33 @@ var NodeInfoSchema = external_exports.object({
30611
30754
  devtunnelEndpoint: external_exports.string().optional(),
30612
30755
  previewOrigin: external_exports.string().optional()
30613
30756
  });
30757
+ var JoinTaskSchema = external_exports.object({
30758
+ id: external_exports.string(),
30759
+ title: external_exports.string(),
30760
+ description: external_exports.string(),
30761
+ agent: external_exports.string(),
30762
+ project: external_exports.string().nullable(),
30763
+ effectiveProjectPath: external_exports.string().nullable(),
30764
+ payload: external_exports.record(external_exports.unknown()),
30765
+ status: external_exports.enum(["pending", "assigned", "running", "completed", "failed", "cancelled", "archived"]),
30766
+ priority: external_exports.enum(["low", "normal", "high", "critical"]),
30767
+ assignedTo: external_exports.string().nullable(),
30768
+ assignedNodeName: external_exports.string().nullable().optional(),
30769
+ createdBy: external_exports.string(),
30770
+ result: external_exports.record(external_exports.unknown()).nullable(),
30771
+ error: external_exports.string().nullable(),
30772
+ retryCount: external_exports.number(),
30773
+ maxRetries: external_exports.number(),
30774
+ createdAt: external_exports.number(),
30775
+ updatedAt: external_exports.number()
30776
+ });
30614
30777
  var JoinClusterBody = external_exports.object({
30615
30778
  id: external_exports.string().min(1),
30616
30779
  endpoint: external_exports.string().url(),
30617
30780
  name: external_exports.string().min(1),
30618
30781
  capabilities: external_exports.array(external_exports.string()).default([]),
30619
- devtunnelEndpoint: external_exports.string().url().optional()
30782
+ devtunnelEndpoint: external_exports.string().url().optional(),
30783
+ tasks: external_exports.array(JoinTaskSchema).default([])
30620
30784
  });
30621
30785
  var JoinClusterResponse = external_exports.object({
30622
30786
  clusterId: external_exports.string(),
@@ -30640,6 +30804,10 @@ var ElectionBody = external_exports.object({
30640
30804
  candidateId: external_exports.string(),
30641
30805
  term: external_exports.number().int().min(1)
30642
30806
  });
30807
+ var ConnectClusterBody = external_exports.object({
30808
+ leaderEndpoint: external_exports.string().url(),
30809
+ transport: external_exports.literal("devtunnel")
30810
+ });
30643
30811
 
30644
30812
  // ../../packages/api/src/schemas/nodes.ts
30645
30813
  var NodeInfoSchema2 = external_exports.object({
@@ -30707,6 +30875,7 @@ var TaskListResponse = external_exports.object({
30707
30875
  priority: external_exports.enum(["low", "normal", "high", "critical"]),
30708
30876
  assignedTo: external_exports.string().nullable(),
30709
30877
  assignedNodeName: external_exports.string().nullable().optional(),
30878
+ assignedNodeAvailable: external_exports.boolean().optional(),
30710
30879
  createdBy: external_exports.string(),
30711
30880
  result: external_exports.record(external_exports.unknown()).nullable(),
30712
30881
  error: external_exports.string().nullable(),
@@ -30885,13 +31054,32 @@ function createErrorMiddleware() {
30885
31054
 
30886
31055
  // ../../packages/api/src/routes/cluster.ts
30887
31056
  var import_express = __toESM(require_express2(), 1);
31057
+ var CONNECTIVITY_TIMEOUT_MS = 8e3;
31058
+ async function verifyHealth(endpoint) {
31059
+ const controller = new AbortController();
31060
+ const timer = setTimeout(() => controller.abort(), CONNECTIVITY_TIMEOUT_MS);
31061
+ try {
31062
+ const response = await fetch(`${endpoint}/api/system/health`, {
31063
+ signal: controller.signal
31064
+ });
31065
+ if (!response.ok) {
31066
+ throw new Error(`health check returned ${response.status}`);
31067
+ }
31068
+ } catch (err) {
31069
+ throw new Error(
31070
+ `Failed to verify connectivity for ${endpoint}: ${err instanceof Error ? err.message : String(err)}`
31071
+ );
31072
+ } finally {
31073
+ clearTimeout(timer);
31074
+ }
31075
+ }
30888
31076
  function asyncHandler(fn) {
30889
31077
  return (req, res, next) => fn(req, res, next).catch(next);
30890
31078
  }
30891
31079
  function createClusterRoutes() {
30892
31080
  const router = (0, import_express.Router)();
30893
31081
  router.post("/join", asyncHandler(async (req, res) => {
30894
- const { nodeRegistry, election } = req.app.locals.deps;
31082
+ const { nodeRegistry, election, taskEngine } = req.app.locals.deps;
30895
31083
  if (!election.isLeader()) {
30896
31084
  const leaderEndpoint = nodeRegistry.getLeaderEndpoint();
30897
31085
  res.status(421).json({
@@ -30922,6 +31110,7 @@ function createClusterRoutes() {
30922
31110
  ...body.devtunnelEndpoint && { devtunnelEndpoint: body.devtunnelEndpoint }
30923
31111
  };
30924
31112
  nodeRegistry.addNode(node);
31113
+ taskEngine.importTasks(body.tasks ?? []);
30925
31114
  const clusterState = nodeRegistry.getClusterState();
30926
31115
  res.status(201).json({
30927
31116
  clusterId: clusterState.clusterId ?? nanoid(),
@@ -30930,13 +31119,50 @@ function createClusterRoutes() {
30930
31119
  nodes: clusterState.nodes
30931
31120
  });
30932
31121
  }));
31122
+ router.post("/connect", asyncHandler(async (req, res) => {
31123
+ const {
31124
+ nodeRegistry,
31125
+ joinCurrentNodeToCluster
31126
+ } = req.app.locals.deps;
31127
+ const body = ConnectClusterBody.parse(req.body);
31128
+ if (!joinCurrentNodeToCluster) {
31129
+ throw new MeshyError("VALIDATION_ERROR", "Join flow is not available on this node", 501);
31130
+ }
31131
+ await verifyHealth(body.leaderEndpoint);
31132
+ await joinCurrentNodeToCluster(body.leaderEndpoint);
31133
+ const self = nodeRegistry.getSelf();
31134
+ const clusterState = nodeRegistry.getClusterState();
31135
+ res.json({
31136
+ ok: true,
31137
+ transport: body.transport,
31138
+ self,
31139
+ leaderId: clusterState.leaderId,
31140
+ term: clusterState.term,
31141
+ nodes: clusterState.nodes
31142
+ });
31143
+ }));
30933
31144
  router.post("/leave", asyncHandler(async (req, res) => {
30934
- const { nodeRegistry, eventBus } = req.app.locals.deps;
31145
+ const { nodeRegistry } = req.app.locals.deps;
30935
31146
  const { nodeId } = req.body;
30936
31147
  nodeRegistry.removeNode(nodeId);
30937
- eventBus.emit("node.left", { nodeId, reason: "left" });
30938
31148
  res.json({ ok: true });
30939
31149
  }));
31150
+ router.post("/disconnect", asyncHandler(async (req, res) => {
31151
+ const { leaveCurrentCluster, nodeRegistry } = req.app.locals.deps;
31152
+ if (!leaveCurrentCluster) {
31153
+ throw new MeshyError("VALIDATION_ERROR", "Leave flow is not available on this node", 501);
31154
+ }
31155
+ await leaveCurrentCluster();
31156
+ const self = nodeRegistry.getSelf();
31157
+ const clusterState = nodeRegistry.getClusterState();
31158
+ res.json({
31159
+ ok: true,
31160
+ self,
31161
+ leaderId: clusterState.leaderId,
31162
+ term: clusterState.term,
31163
+ nodes: clusterState.nodes
31164
+ });
31165
+ }));
30940
31166
  router.get("/state", asyncHandler(async (req, res) => {
30941
31167
  const { nodeRegistry, taskEngine } = req.app.locals.deps;
30942
31168
  const clusterState = nodeRegistry.getClusterState();
@@ -31826,7 +32052,7 @@ function createNodeRoutes() {
31826
32052
  res.json(node);
31827
32053
  }));
31828
32054
  router.delete("/:id", asyncHandler2(async (req, res) => {
31829
- const { nodeRegistry, eventBus, election } = req.app.locals.deps;
32055
+ const { nodeRegistry, election } = req.app.locals.deps;
31830
32056
  if (!election.isLeader()) {
31831
32057
  throw new MeshyError("NOT_LEADER", "Only the leader node can delete nodes", 403);
31832
32058
  }
@@ -31835,7 +32061,6 @@ function createNodeRoutes() {
31835
32061
  throw new MeshyError("NODE_NOT_FOUND", `Node ${req.params.id} not found`, 404);
31836
32062
  }
31837
32063
  nodeRegistry.removeNode(req.params.id);
31838
- eventBus.emit("node.left", { nodeId: req.params.id, reason: "deleted" });
31839
32064
  res.json({ ok: true });
31840
32065
  }));
31841
32066
  router.post("/:id/devtunnel", asyncHandler2(async (req, res) => {
@@ -32042,17 +32267,15 @@ var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["pending", "assigned", "running"]
32042
32267
  function asyncHandler3(fn) {
32043
32268
  return (req, res, next) => fn(req, res, next).catch(next);
32044
32269
  }
32045
- function withAssignedNodeName(task, nodeRegistry) {
32046
- if (task.assignedNodeName || !task.assignedTo) {
32270
+ function withAssignedNodeMetadata(task, nodeRegistry) {
32271
+ if (!task.assignedTo) {
32047
32272
  return task;
32048
32273
  }
32049
32274
  const node = nodeRegistry.getNode(task.assignedTo);
32050
- if (!node?.name) {
32051
- return task;
32052
- }
32053
32275
  return {
32054
32276
  ...task,
32055
- assignedNodeName: node.name
32277
+ assignedNodeName: task.assignedNodeName ?? node?.name ?? null,
32278
+ assignedNodeAvailable: node?.status !== "offline" && Boolean(node)
32056
32279
  };
32057
32280
  }
32058
32281
  function createTaskRoutes() {
@@ -32071,10 +32294,10 @@ function createTaskRoutes() {
32071
32294
  });
32072
32295
  if (body.assignTo) {
32073
32296
  const assigned = taskEngine.assignTask(task.id, body.assignTo);
32074
- res.status(201).json(withAssignedNodeName(assigned, nodeRegistry));
32297
+ res.status(201).json(withAssignedNodeMetadata(assigned, nodeRegistry));
32075
32298
  return;
32076
32299
  }
32077
- res.status(201).json(withAssignedNodeName(task, nodeRegistry));
32300
+ res.status(201).json(withAssignedNodeMetadata(task, nodeRegistry));
32078
32301
  }));
32079
32302
  router.get("/", asyncHandler3(async (req, res) => {
32080
32303
  const { taskEngine, nodeRegistry } = req.app.locals.deps;
@@ -32087,7 +32310,7 @@ function createTaskRoutes() {
32087
32310
  if (query.offset) filter.offset = query.offset;
32088
32311
  const result = taskEngine.listTasks(filter);
32089
32312
  res.json({
32090
- tasks: result.tasks.map((task) => withAssignedNodeName(task, nodeRegistry)),
32313
+ tasks: result.tasks.map((task) => withAssignedNodeMetadata(task, nodeRegistry)),
32091
32314
  total: result.total
32092
32315
  });
32093
32316
  }));
@@ -32128,7 +32351,7 @@ function createTaskRoutes() {
32128
32351
  if (!task) {
32129
32352
  throw new MeshyError("TASK_NOT_FOUND", `Task ${req.params.id} not found`, 404);
32130
32353
  }
32131
- res.json(withAssignedNodeName(task, nodeRegistry));
32354
+ res.json(withAssignedNodeMetadata(task, nodeRegistry));
32132
32355
  }));
32133
32356
  router.patch("/:id", asyncHandler3(async (req, res) => {
32134
32357
  const { taskEngine, nodeRegistry, logger: rootLogger } = req.app.locals.deps;
@@ -32144,7 +32367,7 @@ function createTaskRoutes() {
32144
32367
  currentStatus: existing.status,
32145
32368
  incomingStatus: updates.status
32146
32369
  });
32147
- res.json(withAssignedNodeName(existing, nodeRegistry));
32370
+ res.json(withAssignedNodeMetadata(existing, nodeRegistry));
32148
32371
  return;
32149
32372
  }
32150
32373
  if (updates.status === "archived" && !ARCHIVABLE_STATUSES.has(existing.status)) {
@@ -32160,14 +32383,14 @@ function createTaskRoutes() {
32160
32383
  currentStatus: existing.status,
32161
32384
  incomingStatus: updates.status
32162
32385
  });
32163
- res.json(withAssignedNodeName(existing, nodeRegistry));
32386
+ res.json(withAssignedNodeMetadata(existing, nodeRegistry));
32164
32387
  return;
32165
32388
  }
32166
32389
  const task = taskEngine.updateTask(req.params.id, updates);
32167
32390
  if (!task) {
32168
32391
  throw new MeshyError("TASK_NOT_FOUND", `Task ${req.params.id} not found`, 404);
32169
32392
  }
32170
- res.json(withAssignedNodeName(task, nodeRegistry));
32393
+ res.json(withAssignedNodeMetadata(task, nodeRegistry));
32171
32394
  }));
32172
32395
  router.delete("/:id", asyncHandler3(async (req, res) => {
32173
32396
  const { taskEngine } = req.app.locals.deps;
@@ -32218,12 +32441,12 @@ function createTaskRoutes() {
32218
32441
  const { taskEngine, nodeRegistry } = req.app.locals.deps;
32219
32442
  const body = AssignTaskBody.parse(req.body);
32220
32443
  const task = taskEngine.assignTask(req.params.id, body.nodeId);
32221
- res.json(withAssignedNodeName(task, nodeRegistry));
32444
+ res.json(withAssignedNodeMetadata(task, nodeRegistry));
32222
32445
  }));
32223
32446
  router.post("/:id/retry", asyncHandler3(async (req, res) => {
32224
32447
  const { taskEngine, nodeRegistry } = req.app.locals.deps;
32225
32448
  const task = taskEngine.retryTask(req.params.id);
32226
- res.json(withAssignedNodeName(task, nodeRegistry));
32449
+ res.json(withAssignedNodeMetadata(task, nodeRegistry));
32227
32450
  }));
32228
32451
  router.get("/:id/logs", asyncHandler3(async (req, res) => {
32229
32452
  const { engineRegistry, taskEngine, nodeRegistry, logger: rootLogger } = req.app.locals.deps;
@@ -32308,26 +32531,18 @@ function createTaskRoutes() {
32308
32531
  }
32309
32532
  if (task.assignedTo && task.assignedTo !== nodeRegistry.getSelf()?.id) {
32310
32533
  const node = nodeRegistry.getNode(task.assignedTo);
32311
- const selfId = nodeRegistry.getSelf()?.id ?? null;
32312
32534
  if (!node || node.status === "offline") {
32313
- log.warn("assigned worker unavailable, resuming follow-up locally", {
32535
+ log.warn("assigned worker unavailable for follow-up", {
32314
32536
  taskId: task.id,
32315
- assignedTo: task.assignedTo,
32316
- nodeFound: Boolean(node),
32317
- nodeStatus: node?.status,
32318
- reassignedTo: selfId
32319
- });
32320
- startLocalTaskFollowUp({
32321
- taskEngine,
32322
- engineRegistry,
32323
- logger: log
32324
- }, task, body.content, selfId, {
32325
32537
  assignedTo: task.assignedTo,
32326
32538
  nodeFound: Boolean(node),
32327
32539
  nodeStatus: node?.status
32328
32540
  });
32329
- res.json({ ok: true, reassigned: true });
32330
- return;
32541
+ throw new MeshyError(
32542
+ "NODE_OFFLINE",
32543
+ `Task ${task.id} is unavailable because ${task.assignedNodeName ?? task.assignedTo} is no longer reachable`,
32544
+ 409
32545
+ );
32331
32546
  }
32332
32547
  taskEngine.updateTask(task.id, { status: "running" });
32333
32548
  let workerEndpoint = node.endpoint;
@@ -33637,6 +33852,12 @@ async function main() {
33637
33852
  config: { apiKey: config.cluster.apiKey },
33638
33853
  workDir: meshyNode.getWorkDir(),
33639
33854
  persistNodeNamePreference: (name) => persistDefaultNodeName(config.storage.path, name),
33855
+ joinCurrentNodeToCluster: async (leaderEndpoint) => {
33856
+ await meshyNode.joinCluster(leaderEndpoint);
33857
+ },
33858
+ leaveCurrentCluster: async () => {
33859
+ await meshyNode.leaveCluster();
33860
+ },
33640
33861
  switchTransport: async (type) => {
33641
33862
  return meshyNode.switchTransport(type);
33642
33863
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meshy-node",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "private": false,
5
5
  "description": "Standalone Meshy node package with bundled runtime and dashboard assets.",
6
6
  "type": "commonjs",