openclaw-node-harness 2.1.1 → 2.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 (32) hide show
  1. package/bin/lane-watchdog.js +31 -21
  2. package/bin/mesh-agent.js +11 -2
  3. package/bin/mesh-deploy.js +4 -0
  4. package/bin/mesh-task-daemon.js +9 -4
  5. package/bin/mesh.js +9 -24
  6. package/lib/exec-safety.js +60 -2
  7. package/lib/kanban-io.js +8 -5
  8. package/lib/llm-providers.js +12 -1
  9. package/lib/mesh-collab.js +8 -2
  10. package/lib/mesh-harness.js +6 -0
  11. package/lib/mesh-plans.js +20 -2
  12. package/lib/mesh-tasks.js +26 -10
  13. package/mission-control/package-lock.json +4188 -3698
  14. package/mission-control/package.json +2 -2
  15. package/mission-control/src/app/api/diagnostics/route.ts +8 -0
  16. package/mission-control/src/app/api/diagnostics/test-runner/route.ts +8 -0
  17. package/mission-control/src/app/api/memory/graph/route.ts +34 -18
  18. package/mission-control/src/app/api/memory/search/route.ts +3 -2
  19. package/mission-control/src/app/api/mesh/identity/route.ts +13 -5
  20. package/mission-control/src/app/api/mesh/nodes/route.ts +8 -0
  21. package/mission-control/src/app/api/settings/gateway/route.ts +62 -0
  22. package/mission-control/src/app/api/souls/[id]/evolution/route.ts +28 -7
  23. package/mission-control/src/app/api/souls/[id]/propagate/route.ts +10 -3
  24. package/mission-control/src/app/api/souls/route.ts +6 -4
  25. package/mission-control/src/app/api/tasks/[id]/route.ts +20 -4
  26. package/mission-control/src/app/api/tasks/route.ts +68 -9
  27. package/mission-control/src/lib/config.ts +2 -2
  28. package/mission-control/src/lib/sync/tasks.ts +4 -1
  29. package/package.json +1 -1
  30. package/services/launchd/ai.openclaw.lane-watchdog.plist +1 -1
  31. package/services/launchd/ai.openclaw.mesh-agent.plist +4 -0
  32. package/services/launchd/ai.openclaw.mission-control.plist +4 -3
@@ -26,7 +26,7 @@
26
26
  "gray-matter": "^4.0.3",
27
27
  "lucide-react": "^0.575.0",
28
28
  "nats": "^2.29.3",
29
- "next": "16.1.6",
29
+ "next": "^16.2.1",
30
30
  "react": "19.2.3",
31
31
  "react-dom": "19.2.3",
32
32
  "react-force-graph-2d": "^1.29.1",
@@ -43,7 +43,7 @@
43
43
  "@types/node": "^20",
44
44
  "@types/react": "^19",
45
45
  "@types/react-dom": "^19",
46
- "drizzle-kit": "^0.31.9",
46
+ "drizzle-kit": "^0.31.10",
47
47
  "eslint": "^9",
48
48
  "eslint-config-next": "16.1.6",
49
49
  "tailwindcss": "^4.2.1",
@@ -8,6 +8,7 @@ import { parseTasksMarkdown, serializeTasksMarkdown } from "@/lib/parsers/task-m
8
8
  export const dynamic = "force-dynamic";
9
9
 
10
10
  export async function GET() {
11
+ try {
11
12
  const raw = getRawDb();
12
13
 
13
14
  // Task stats
@@ -94,4 +95,11 @@ export async function GET() {
94
95
  nats: natsStatus,
95
96
  workspace: workspaceExists,
96
97
  });
98
+ } catch (err) {
99
+ console.error("[diagnostics] error:", err);
100
+ return NextResponse.json(
101
+ { error: err instanceof Error ? err.message : "Internal server error" },
102
+ { status: err instanceof SyntaxError ? 400 : 500 }
103
+ );
104
+ }
97
105
  }
@@ -41,6 +41,7 @@ async function runTest(suite: string, name: string, fn: TestFn): Promise<TestRes
41
41
  * All test tasks use the prefix __TEST__ so they can be safely cleaned up.
42
42
  */
43
43
  export async function POST() {
44
+ try {
44
45
  const results: TestResult[] = [];
45
46
  const db = getDb();
46
47
  const raw = getRawDb();
@@ -987,4 +988,11 @@ export async function POST() {
987
988
  results,
988
989
  timestamp: new Date().toISOString(),
989
990
  });
991
+ } catch (err) {
992
+ console.error("[diagnostics/test-runner] error:", err);
993
+ return NextResponse.json(
994
+ { error: err instanceof Error ? err.message : "Internal server error" },
995
+ { status: err instanceof SyntaxError ? 400 : 500 }
996
+ );
997
+ }
990
998
  }
@@ -18,31 +18,47 @@ import {
18
18
  import { getRawDb } from "@/lib/db";
19
19
 
20
20
  export async function GET(request: Request) {
21
- const url = new URL(request.url);
22
- const boot = url.searchParams.get("boot");
23
- const format = url.searchParams.get("format");
21
+ try {
22
+ const url = new URL(request.url);
23
+ const boot = url.searchParams.get("boot");
24
+ const format = url.searchParams.get("format");
24
25
 
25
- if (boot === "true") {
26
- const block = formatEntityContextBlock();
27
- return NextResponse.json({ block });
28
- }
26
+ if (boot === "true") {
27
+ const block = formatEntityContextBlock();
28
+ return NextResponse.json({ block });
29
+ }
29
30
 
30
- if (format === "viz") {
31
- return getVizData();
32
- }
31
+ if (format === "viz") {
32
+ return getVizData();
33
+ }
33
34
 
34
- const stats = getGraphStats();
35
- const topEntities = getTopEntities(10).map((entity) => {
36
- const relations = getEntityRelations(entity.id, 3);
37
- return { ...entity, relations };
38
- });
35
+ const stats = getGraphStats();
36
+ const topEntities = getTopEntities(10).map((entity) => {
37
+ const relations = getEntityRelations(entity.id, 3);
38
+ return { ...entity, relations };
39
+ });
39
40
 
40
- return NextResponse.json({ stats, topEntities });
41
+ return NextResponse.json({ stats, topEntities });
42
+ } catch (err) {
43
+ console.error("[memory/graph] GET error:", err);
44
+ return NextResponse.json(
45
+ { error: err instanceof Error ? err.message : "Internal server error" },
46
+ { status: err instanceof SyntaxError ? 400 : 500 }
47
+ );
48
+ }
41
49
  }
42
50
 
43
51
  export async function POST() {
44
- const seeded = seedKnownEntities();
45
- return NextResponse.json({ seeded, message: `${seeded} entities seeded` });
52
+ try {
53
+ const seeded = seedKnownEntities();
54
+ return NextResponse.json({ seeded, message: `${seeded} entities seeded` });
55
+ } catch (err) {
56
+ console.error("[memory/graph] POST error:", err);
57
+ return NextResponse.json(
58
+ { error: err instanceof Error ? err.message : "Internal server error" },
59
+ { status: err instanceof SyntaxError ? 400 : 500 }
60
+ );
61
+ }
46
62
  }
47
63
 
48
64
  /**
@@ -57,7 +57,7 @@ export async function GET(request: NextRequest) {
57
57
  const source = searchParams.get("source");
58
58
  const category = searchParams.get("category");
59
59
  const limit = Math.min(parseInt(searchParams.get("limit") || "20", 10), 100);
60
- const offset = parseInt(searchParams.get("offset") || "0", 10);
60
+ const offset = Math.max(0, Math.min(parseInt(searchParams.get("offset") || "0", 10) || 0, 10000));
61
61
  const useDecay = searchParams.get("decay") !== "false"; // default: true
62
62
  const decayRate = parseFloat(searchParams.get("decay_rate") || "0.01");
63
63
 
@@ -141,6 +141,7 @@ export async function GET(request: NextRequest) {
141
141
  console.error("GET /api/memory/search error:", err);
142
142
  const message =
143
143
  err instanceof Error ? err.message : "Failed to search memory";
144
- return NextResponse.json({ error: message }, { status: 500 });
144
+ const status = err instanceof SyntaxError || err instanceof RangeError ? 400 : 500;
145
+ return NextResponse.json({ error: message }, { status });
145
146
  }
146
147
  }
@@ -3,9 +3,17 @@ import { NODE_ID, NODE_ROLE, NODE_PLATFORM } from "@/lib/config";
3
3
  export const dynamic = "force-dynamic";
4
4
 
5
5
  export async function GET() {
6
- return Response.json({
7
- nodeId: NODE_ID,
8
- role: NODE_ROLE,
9
- platform: NODE_PLATFORM,
10
- });
6
+ try {
7
+ return Response.json({
8
+ nodeId: NODE_ID,
9
+ role: NODE_ROLE,
10
+ platform: NODE_PLATFORM,
11
+ });
12
+ } catch (err) {
13
+ console.error("[mesh/identity] error:", err);
14
+ return Response.json(
15
+ { error: err instanceof Error ? err.message : "Internal server error" },
16
+ { status: 500 }
17
+ );
18
+ }
11
19
  }
@@ -85,6 +85,7 @@ const KNOWN_NODES = [
85
85
  * No synchronous round-trips to nodes. No timeout races. No flickering.
86
86
  */
87
87
  export async function GET() {
88
+ try {
88
89
  const kv = await getHealthKv();
89
90
  const db = getDb();
90
91
  const now = Date.now();
@@ -218,4 +219,11 @@ export async function GET() {
218
219
  }
219
220
 
220
221
  return NextResponse.json({ nodes, tokenStats });
222
+ } catch (err) {
223
+ console.error("[mesh/nodes] error:", err);
224
+ return NextResponse.json(
225
+ { error: err instanceof Error ? err.message : "Internal server error" },
226
+ { status: err instanceof SyntaxError ? 400 : 500 }
227
+ );
228
+ }
221
229
  }
@@ -52,9 +52,63 @@ export async function GET() {
52
52
  * Update gateway-related settings. Accepts partial updates.
53
53
  * Body: { heartbeat?: { target, every? }, maxConcurrent?: number }
54
54
  */
55
+ const ALLOWED_TOP_LEVEL_KEYS = new Set([
56
+ "heartbeat", "maxConcurrent", "compaction", "gateway",
57
+ ]);
58
+ const ALLOWED_GATEWAY_KEYS = new Set(["port", "mode", "bind"]);
59
+ const VALID_MODES = new Set(["local", "remote", "hybrid"]);
60
+ const VALID_BINDS = new Set(["loopback", "tailscale", "any"]);
61
+
55
62
  export async function PATCH(request: NextRequest) {
56
63
  try {
57
64
  const body = await request.json();
65
+
66
+ // Reject unknown top-level keys
67
+ const unknownKeys = Object.keys(body).filter((k) => !ALLOWED_TOP_LEVEL_KEYS.has(k));
68
+ if (unknownKeys.length > 0) {
69
+ return NextResponse.json(
70
+ { error: `Unknown keys: ${unknownKeys.join(", ")}` },
71
+ { status: 400 }
72
+ );
73
+ }
74
+
75
+ // Validate gateway sub-object if present
76
+ if (body.gateway !== undefined) {
77
+ if (typeof body.gateway !== "object" || body.gateway === null || Array.isArray(body.gateway)) {
78
+ return NextResponse.json(
79
+ { error: "gateway must be an object" },
80
+ { status: 400 }
81
+ );
82
+ }
83
+ const unknownGw = Object.keys(body.gateway).filter((k) => !ALLOWED_GATEWAY_KEYS.has(k));
84
+ if (unknownGw.length > 0) {
85
+ return NextResponse.json(
86
+ { error: `Unknown gateway keys: ${unknownGw.join(", ")}` },
87
+ { status: 400 }
88
+ );
89
+ }
90
+ if (body.gateway.port !== undefined) {
91
+ if (!Number.isInteger(body.gateway.port) || body.gateway.port < 1 || body.gateway.port > 65535) {
92
+ return NextResponse.json(
93
+ { error: "gateway.port must be an integer between 1 and 65535" },
94
+ { status: 400 }
95
+ );
96
+ }
97
+ }
98
+ if (body.gateway.mode !== undefined && !VALID_MODES.has(body.gateway.mode)) {
99
+ return NextResponse.json(
100
+ { error: `gateway.mode must be one of: ${[...VALID_MODES].join(", ")}` },
101
+ { status: 400 }
102
+ );
103
+ }
104
+ if (body.gateway.bind !== undefined && !VALID_BINDS.has(body.gateway.bind)) {
105
+ return NextResponse.json(
106
+ { error: `gateway.bind must be one of: ${[...VALID_BINDS].join(", ")}` },
107
+ { status: 400 }
108
+ );
109
+ }
110
+ }
111
+
58
112
  const config = await loadConfig();
59
113
 
60
114
  if (!config.agents) config.agents = {};
@@ -79,6 +133,14 @@ export async function PATCH(request: NextRequest) {
79
133
  config.agents.defaults.compaction = body.compaction;
80
134
  }
81
135
 
136
+ // Gateway settings
137
+ if (body.gateway !== undefined) {
138
+ if (!config.gateway) config.gateway = {};
139
+ if (body.gateway.port !== undefined) config.gateway.port = body.gateway.port;
140
+ if (body.gateway.mode !== undefined) config.gateway.mode = body.gateway.mode;
141
+ if (body.gateway.bind !== undefined) config.gateway.bind = body.gateway.bind;
142
+ }
143
+
82
144
  await saveConfig(config);
83
145
 
84
146
  return NextResponse.json({ ok: true });
@@ -69,7 +69,10 @@ export async function GET(
69
69
  const lines = content.trim().split("\n").filter(Boolean);
70
70
  fullEvents = lines.map((line) => JSON.parse(line));
71
71
  } catch (error) {
72
- // events.jsonl might be empty or not exist yet
72
+ // events.jsonl might be empty, not exist yet, or contain malformed lines
73
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
74
+ console.warn("Failed to parse events.jsonl:", error);
75
+ }
73
76
  }
74
77
 
75
78
  // Merge DB records with full event details
@@ -174,11 +177,20 @@ export async function PATCH(
174
177
  "events.jsonl"
175
178
  );
176
179
  const content = await fs.readFile(eventsPath, "utf-8");
177
- const events = content
178
- .trim()
179
- .split("\n")
180
- .filter(Boolean)
181
- .map((line) => JSON.parse(line));
180
+ let events: EvolutionEvent[];
181
+ try {
182
+ events = content
183
+ .trim()
184
+ .split("\n")
185
+ .filter(Boolean)
186
+ .map((line) => JSON.parse(line));
187
+ } catch (parseErr) {
188
+ console.error("Malformed JSONL in events.jsonl:", parseErr);
189
+ return NextResponse.json(
190
+ { error: "Failed to parse evolution events file (malformed JSONL)" },
191
+ { status: 500 }
192
+ );
193
+ }
182
194
  const event = events.find((e: EvolutionEvent) => e.eventId === eventId);
183
195
 
184
196
  if (!event) {
@@ -195,7 +207,16 @@ export async function PATCH(
195
207
  );
196
208
 
197
209
  if (event.proposedChange.action === "add") {
198
- const existing = JSON.parse(await fs.readFile(targetPath, "utf-8"));
210
+ let existing;
211
+ try {
212
+ existing = JSON.parse(await fs.readFile(targetPath, "utf-8"));
213
+ } catch (parseErr) {
214
+ console.error(`Malformed JSON in ${targetPath}:`, parseErr);
215
+ return NextResponse.json(
216
+ { error: `Failed to parse ${event.proposedChange.target} (malformed JSON)` },
217
+ { status: 500 }
218
+ );
219
+ }
199
220
  if (event.proposedChange.target === "genes.json") {
200
221
  existing.genes.push(event.proposedChange.content);
201
222
  }
@@ -75,10 +75,17 @@ export async function POST(
75
75
  const lines = content.trim().split("\n").filter(Boolean);
76
76
  const events: EvolutionEventEntry[] = lines.map((line) => JSON.parse(line));
77
77
  sourceEvent = events.find((e) => e.eventId === sourceEventId) ?? null;
78
- } catch {
78
+ } catch (error) {
79
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
80
+ return NextResponse.json(
81
+ { error: "Source events file not found" },
82
+ { status: 404 }
83
+ );
84
+ }
85
+ console.error("Failed to parse source events.jsonl:", error);
79
86
  return NextResponse.json(
80
- { error: "Source events file not found" },
81
- { status: 404 }
87
+ { error: "Failed to parse source events file (malformed JSONL)" },
88
+ { status: 500 }
82
89
  );
83
90
  }
84
91
 
@@ -94,9 +94,10 @@ export async function POST(request: NextRequest) {
94
94
  return NextResponse.json(newSoul, { status: 201 });
95
95
  } catch (error) {
96
96
  console.error("Failed to register soul:", error);
97
+ const status = error instanceof SyntaxError ? 400 : 500;
97
98
  return NextResponse.json(
98
- { error: "Failed to register soul" },
99
- { status: 500 }
99
+ { error: status === 400 ? String(error) : "Failed to register soul" },
100
+ { status }
100
101
  );
101
102
  }
102
103
  }
@@ -133,9 +134,10 @@ export async function PATCH(request: NextRequest) {
133
134
  return NextResponse.json(registry.souls[soulIndex]);
134
135
  } catch (error) {
135
136
  console.error("Failed to update soul:", error);
137
+ const status = error instanceof SyntaxError ? 400 : 500;
136
138
  return NextResponse.json(
137
- { error: "Failed to update soul" },
138
- { status: 500 }
139
+ { error: status === 400 ? String(error) : "Failed to update soul" },
140
+ { status }
139
141
  );
140
142
  }
141
143
  }
@@ -9,6 +9,12 @@ import { gatewayNotify } from "@/lib/gateway-notify";
9
9
  import { AGENT_NAME, HUMAN_NAME } from "@/lib/config";
10
10
  import { getNats, sc } from "@/lib/nats";
11
11
 
12
+ /** Safely parse a JSON string from a DB field, returning fallback on failure. */
13
+ function safeParse(json: string | null, fallback: unknown = []): unknown {
14
+ if (!json) return fallback;
15
+ try { return JSON.parse(json); } catch { return fallback; }
16
+ }
17
+
12
18
  /**
13
19
  * Push a notification message to the OpenClaw TUI via gateway chat.send + abort.
14
20
  * Fire-and-forget — does not block the API response.
@@ -130,6 +136,18 @@ export async function PATCH(
130
136
  const body = await request.json();
131
137
  const now = new Date().toISOString();
132
138
 
139
+ // Status validation
140
+ const VALID_STATUSES = new Set([
141
+ "not started", "queued", "ready", "submitted", "running",
142
+ "blocked", "waiting-user", "done", "cancelled", "archived",
143
+ ]);
144
+ if (body.status && !VALID_STATUSES.has(body.status)) {
145
+ return NextResponse.json(
146
+ { error: `Invalid status: ${body.status}` },
147
+ { status: 400 }
148
+ );
149
+ }
150
+
133
151
  // Transition guard: block execution mode changes on in-flight mesh tasks
134
152
  if (body.execution !== undefined && body.execution !== existing.execution) {
135
153
  if (existing.meshTaskId) {
@@ -337,10 +355,8 @@ export async function PATCH(
337
355
 
338
356
  return NextResponse.json({
339
357
  ...updated,
340
- successCriteria: updated?.successCriteria
341
- ? JSON.parse(updated.successCriteria)
342
- : [],
343
- artifacts: updated?.artifacts ? JSON.parse(updated.artifacts) : [],
358
+ successCriteria: safeParse(updated?.successCriteria ?? null),
359
+ artifacts: safeParse(updated?.artifacts ?? null),
344
360
  });
345
361
  } catch (err) {
346
362
  console.error("PATCH /api/tasks/[id] error:", err);
@@ -12,6 +12,12 @@ import { getNats, sc } from "@/lib/nats";
12
12
  import { WORKSPACE_ROOT, AGENT_NAME } from "@/lib/config";
13
13
  import path from "path";
14
14
 
15
+ /** Safely parse a JSON string from a DB field, returning fallback on failure. */
16
+ function safeParse(json: string | null, fallback: unknown = []): unknown {
17
+ if (!json) return fallback;
18
+ try { return JSON.parse(json); } catch { return fallback; }
19
+ }
20
+
15
21
  /**
16
22
  * Parse .companion-state.md to extract current active task.
17
23
  * Returns a synthetic task object if there's active work, null otherwise.
@@ -121,11 +127,11 @@ export async function GET(request: NextRequest) {
121
127
  .all();
122
128
  }
123
129
 
124
- // Parse JSON fields for the response
130
+ // Parse JSON fields for the response (per-row guard against corrupt data)
125
131
  const result = rows.map((t) => ({
126
132
  ...t,
127
- successCriteria: t.successCriteria ? JSON.parse(t.successCriteria) : [],
128
- artifacts: t.artifacts ? JSON.parse(t.artifacts) : [],
133
+ successCriteria: safeParse(t.successCriteria),
134
+ artifacts: safeParse(t.artifacts),
129
135
  }));
130
136
 
131
137
  return NextResponse.json(result);
@@ -155,6 +161,60 @@ export async function POST(request: NextRequest) {
155
161
  );
156
162
  }
157
163
 
164
+ if (body.title.length > 500) {
165
+ return NextResponse.json(
166
+ { error: "title must be 500 characters or fewer" },
167
+ { status: 400 }
168
+ );
169
+ }
170
+
171
+ if (body.success_criteria !== undefined) {
172
+ if (!Array.isArray(body.success_criteria)) {
173
+ if (typeof body.success_criteria === "string") {
174
+ try {
175
+ const parsed = JSON.parse(body.success_criteria);
176
+ if (!Array.isArray(parsed)) {
177
+ return NextResponse.json(
178
+ { error: "success_criteria must be a JSON array" },
179
+ { status: 400 }
180
+ );
181
+ }
182
+ } catch {
183
+ return NextResponse.json(
184
+ { error: "success_criteria must be a valid JSON array string" },
185
+ { status: 400 }
186
+ );
187
+ }
188
+ } else {
189
+ return NextResponse.json(
190
+ { error: "success_criteria must be an array or JSON array string" },
191
+ { status: 400 }
192
+ );
193
+ }
194
+ }
195
+ }
196
+
197
+ const VALID_STATUSES = [
198
+ "not started", "queued", "ready", "submitted", "running",
199
+ "blocked", "waiting-user", "done", "cancelled", "archived",
200
+ ];
201
+ if (body.status && !VALID_STATUSES.includes(body.status)) {
202
+ return NextResponse.json(
203
+ { error: `status must be one of: ${VALID_STATUSES.join(", ")}` },
204
+ { status: 400 }
205
+ );
206
+ }
207
+
208
+ if (body.scheduled_date) {
209
+ const d = new Date(body.scheduled_date);
210
+ if (isNaN(d.getTime())) {
211
+ return NextResponse.json(
212
+ { error: "scheduled_date must be a valid ISO date string" },
213
+ { status: 400 }
214
+ );
215
+ }
216
+ }
217
+
158
218
  const status = body.status || "not started";
159
219
  const now = new Date();
160
220
  // Allow custom IDs for projects/pipelines/phases; auto-generate for tasks
@@ -227,18 +287,17 @@ export async function POST(request: NextRequest) {
227
287
  return NextResponse.json(
228
288
  {
229
289
  ...created,
230
- successCriteria: created?.successCriteria
231
- ? JSON.parse(created.successCriteria)
232
- : [],
233
- artifacts: created?.artifacts ? JSON.parse(created.artifacts) : [],
290
+ successCriteria: safeParse(created?.successCriteria ?? null),
291
+ artifacts: safeParse(created?.artifacts ?? null),
234
292
  },
235
293
  { status: 201 }
236
294
  );
237
295
  } catch (err) {
238
296
  console.error("POST /api/tasks error:", err);
297
+ const status = err instanceof SyntaxError ? 400 : 500;
239
298
  return NextResponse.json(
240
- { error: "Failed to create task" },
241
- { status: 500 }
299
+ { error: status === 400 ? String(err) : "Failed to create task" },
300
+ { status }
242
301
  );
243
302
  }
244
303
  }
@@ -1,7 +1,8 @@
1
1
  import path from "path";
2
+ import { hostname, homedir } from "os";
2
3
 
3
4
  export const WORKSPACE_ROOT =
4
- process.env.WORKSPACE_ROOT || "/Users/moltymac/.openclaw/workspace";
5
+ process.env.WORKSPACE_ROOT || path.join(homedir(), ".openclaw", "workspace");
5
6
 
6
7
  export const DB_PATH =
7
8
  process.env.DB_PATH ||
@@ -75,7 +76,6 @@ export function validatePathParam(param: string): string {
75
76
  }
76
77
 
77
78
  // ── Node Identity (Distributed MC) ──
78
- import { hostname } from "os";
79
79
 
80
80
  /** This node's unique identifier in the mesh */
81
81
  export const NODE_ID = process.env.OPENCLAW_NODE_ID || hostname();
@@ -283,7 +283,10 @@ export function syncTasksToMarkdown(db: DrizzleDb): void {
283
283
  if (!fs.existsSync(dir)) {
284
284
  fs.mkdirSync(dir, { recursive: true });
285
285
  }
286
- fs.writeFileSync(tmpPath, markdown, "utf-8");
286
+ const fd = fs.openSync(tmpPath, "w");
287
+ fs.writeSync(fd, markdown, 0, "utf-8");
288
+ fs.fsyncSync(fd);
289
+ fs.closeSync(fd);
287
290
  fs.renameSync(tmpPath, ACTIVE_TASKS_MD);
288
291
 
289
292
  // Record mtime of our own write so we don't re-import it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-node-harness",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "One-command installer for the OpenClaw node layer — identity, skills, souls, daemon, and Mission Control.",
5
5
  "bin": {
6
6
  "openclaw-node": "./cli.js"
@@ -26,7 +26,7 @@
26
26
  <key>HOME</key>
27
27
  <string>${HOME}</string>
28
28
  <key>PATH</key>
29
- <string>/usr/local/bin:/usr/bin:/bin</string>
29
+ <string>${HOME}/.bun/bin:${HOME}/.local/bin:${HOME}/.npm-global/bin:${HOME}/bin:${HOME}/.volta/bin:${HOME}/.asdf/shims:${HOME}/Library/Application Support/fnm/aliases/default/bin:${HOME}/.fnm/aliases/default/bin:${HOME}/Library/pnpm:${HOME}/.local/share/pnpm:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
30
30
  </dict>
31
31
  </dict>
32
32
  </plist>
@@ -31,6 +31,10 @@
31
31
  <string>${OPENCLAW_NATS}</string>
32
32
  <key>MESH_WORKSPACE</key>
33
33
  <string>${OPENCLAW_WORKSPACE}</string>
34
+ <key>OPENCLAW_NODE_ID</key>
35
+ <string>${OPENCLAW_NODE_ID}</string>
36
+ <key>OPENCLAW_NODE_ROLE</key>
37
+ <string>${OPENCLAW_NODE_ROLE}</string>
34
38
  </dict>
35
39
  <key>ThrottleInterval</key>
36
40
  <integer>30</integer>
@@ -11,14 +11,15 @@
11
11
  <key>ProgramArguments</key>
12
12
  <array>
13
13
  <string>${HOME}/.openclaw/bin/npm</string>
14
- <string>run</string>
15
- <string>dev</string>
14
+ <string>start</string>
16
15
  </array>
17
16
 
18
17
  <key>EnvironmentVariables</key>
19
18
  <dict>
19
+ <key>HOME</key>
20
+ <string>${HOME}</string>
20
21
  <key>PATH</key>
21
- <string>/usr/local/bin:/usr/bin:/bin</string>
22
+ <string>${HOME}/.bun/bin:${HOME}/.local/bin:${HOME}/.npm-global/bin:${HOME}/bin:${HOME}/.volta/bin:${HOME}/.asdf/shims:${HOME}/Library/Application Support/fnm/aliases/default/bin:${HOME}/.fnm/aliases/default/bin:${HOME}/Library/pnpm:${HOME}/.local/share/pnpm:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
22
23
  <key>NODE_ENV</key>
23
24
  <string>production</string>
24
25
  </dict>