@amd-gaia/agent-ui 0.21.2 → 0.22.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.
- package/dist/assets/{browser-B1LxEoD3.js → browser-CmSowSA-.js} +1 -1
- package/dist/assets/index-B31DMoxr.css +1 -0
- package/dist/assets/index-lScW7mUY.js +72 -0
- package/dist/index.html +2 -2
- package/main-safety-net.cjs +13 -3
- package/main.cjs +3 -0
- package/package.json +3 -3
- package/services/agent-seeder.cjs +256 -14
- package/services/backend-installer.cjs +141 -63
- package/dist/assets/index-BaBSHxtq.js +0 -72
- package/dist/assets/index-CGT9a3El.css +0 -1
package/dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/png" href="./favicon.png" />
|
|
7
7
|
<title>GAIA Agent UI</title>
|
|
8
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
8
|
+
<script type="module" crossorigin src="./assets/index-lScW7mUY.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="./assets/index-B31DMoxr.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/main-safety-net.cjs
CHANGED
|
@@ -134,12 +134,22 @@ function installSafetyNet({ logPath, dialogModule, appModule, homedirFn }) {
|
|
|
134
134
|
|
|
135
135
|
appModule.on("child-process-gone", (_event, details) => {
|
|
136
136
|
const reason = details && details.reason;
|
|
137
|
+
const type = details && details.type;
|
|
137
138
|
// Ignore expected terminations during shutdown so the crash dialog
|
|
138
139
|
// doesn't flash on a clean quit.
|
|
139
140
|
if (reason === "clean-exit" || reason === "killed") return;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
141
|
+
// A GPU-process crash is recoverable — Chromium relaunches the GPU process
|
|
142
|
+
// and falls back to software rendering. It fires routinely in GPU-less
|
|
143
|
+
// environments (Windows Sandbox, VMs, RDP), so log it and keep running
|
|
144
|
+
// instead of killing an otherwise-healthy app.
|
|
145
|
+
if (type === "GPU") {
|
|
146
|
+
appendLog(
|
|
147
|
+
logPath,
|
|
148
|
+
`[${new Date().toISOString()}] GPU_PROCESS_GONE reason=${reason}`
|
|
149
|
+
);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
fatal(new Error(`child-process-gone: type=${type} reason=${reason}`));
|
|
143
153
|
});
|
|
144
154
|
|
|
145
155
|
return { fatal };
|
package/main.cjs
CHANGED
|
@@ -783,6 +783,9 @@ app.whenReady().then(async () => {
|
|
|
783
783
|
if (seedResult.seeded.length > 0) {
|
|
784
784
|
console.log("[main] Seeded agents:", seedResult.seeded);
|
|
785
785
|
}
|
|
786
|
+
if (seedResult.cleaned.length > 0) {
|
|
787
|
+
console.log("[main] Cleaned legacy agents:", seedResult.cleaned);
|
|
788
|
+
}
|
|
786
789
|
if (seedResult.errors.length > 0) {
|
|
787
790
|
console.warn(
|
|
788
791
|
"[main] Agent seeding errors:",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amd-gaia/agent-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"productName": "GAIA Agent UI",
|
|
6
6
|
"description": "Privacy-first agentic AI interface with document Q&A - runs 100% locally on AMD Ryzen AI",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"@types/react": "^19.2.17",
|
|
71
71
|
"@types/react-dom": "^19.2.3",
|
|
72
72
|
"@vitejs/plugin-react": "^6.0.2",
|
|
73
|
-
"electron": "^
|
|
73
|
+
"electron": "^43.0.0",
|
|
74
74
|
"electron-builder": "^26.8.1",
|
|
75
75
|
"jsdom": "^29.1.1",
|
|
76
76
|
"lucide-react": "^1.17.0",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"react-markdown": "^10.1.0",
|
|
82
82
|
"rehype-sanitize": "^6.0.0",
|
|
83
83
|
"remark-gfm": "^4.0.1",
|
|
84
|
-
"typescript": "^
|
|
84
|
+
"typescript": "^7.0.2",
|
|
85
85
|
"vite": "^8.0.16",
|
|
86
86
|
"vitest": "^4.1.8",
|
|
87
87
|
"zustand": "^5.0.14"
|
|
@@ -16,20 +16,40 @@
|
|
|
16
16
|
* - macOS: <Bundle>.app/Contents/Resources/agents/<id>/
|
|
17
17
|
* - Linux: /opt/<AppName>/resources/agents/<id>/
|
|
18
18
|
* - Target: path.join(os.homedir(), ".gaia", "agents", "<id>")
|
|
19
|
-
* - Sentinel: <target>/.seeded (
|
|
19
|
+
* - Sentinel: <target>/.seeded (this copy completed)
|
|
20
|
+
* - Marker: ~/.gaia/seeder/<id>.seeded ("this machine seeded <id> once";
|
|
21
|
+
* EXISTENCE is the signal — content is informational, never parsed for
|
|
22
|
+
* decisions. Lives outside ~/.gaia/agents/ so deleting an agent, or the
|
|
23
|
+
* whole agents dir, never resurrects it.)
|
|
20
24
|
*
|
|
21
25
|
* Write protocol (atomic-ish, crash-safe):
|
|
22
26
|
* 1. Remove any stale `<id>.partial/` sibling from a prior failed run.
|
|
23
27
|
* 2. Copy source → `<id>.partial/`.
|
|
24
28
|
* 3. `fs.renameSync(<id>.partial, <id>)` — atomic on the same filesystem.
|
|
25
|
-
* 4. Write `<id>/.seeded
|
|
29
|
+
* 4. Write `<id>/.seeded`, so a partial seed never looks complete.
|
|
30
|
+
* 5. Write the marker LAST — a crash between 4 and 5 self-heals next
|
|
31
|
+
* launch via the marker back-fill (rule 2 below).
|
|
32
|
+
*
|
|
33
|
+
* Per-agent decision order (markers read fresh on every call, no caching):
|
|
34
|
+
* 1. Marker exists → skip, full stop. A deleted agent is never re-seeded;
|
|
35
|
+
* a dir the user re-created under the same id is never touched.
|
|
36
|
+
* 2. No marker, target has `.seeded` → back-fill the marker, skip.
|
|
37
|
+
* 3. No marker, target exists WITHOUT `.seeded` → user-owned data,
|
|
38
|
+
* log a warning, skip, write no marker.
|
|
39
|
+
* 4. No marker, no target → seed via the write protocol above.
|
|
40
|
+
*
|
|
41
|
+
* Recovery: to get a deleted bundled agent back, remove
|
|
42
|
+
* `~/.gaia/seeder/<id>.seeded` and relaunch.
|
|
43
|
+
*
|
|
44
|
+
* Legacy cleanup: the retired `zoo-agent` demo is removed from
|
|
45
|
+
* ~/.gaia/agents/ when it is provably an unmodified seeded copy — sentinel
|
|
46
|
+
* present, no file newer than its seededAt (+5s slack), never through a
|
|
47
|
+
* symlink. Removed ids are reported via the additive `cleaned` array.
|
|
26
48
|
*
|
|
27
49
|
* Behaviour:
|
|
28
|
-
* -
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* - `process.resourcesPath` unset (dev / Jest) or source dir missing →
|
|
32
|
-
* empty result, no error.
|
|
50
|
+
* - `process.resourcesPath` unset (dev / Jest) → empty result, no error,
|
|
51
|
+
* no cleanup (never touch the real HOME from dev/test contexts).
|
|
52
|
+
* - Source dir missing → legacy cleanup still runs, then empty result.
|
|
33
53
|
* - Per-agent failures are isolated: they go into `errors[]` but do not
|
|
34
54
|
* stop the next agent from being seeded.
|
|
35
55
|
*
|
|
@@ -53,6 +73,10 @@ function agentsTargetRoot() {
|
|
|
53
73
|
return path.join(gaiaHome(), "agents");
|
|
54
74
|
}
|
|
55
75
|
|
|
76
|
+
function seederMarkersDir() {
|
|
77
|
+
return path.join(gaiaHome(), "seeder");
|
|
78
|
+
}
|
|
79
|
+
|
|
56
80
|
function logsDir() {
|
|
57
81
|
return path.join(gaiaHome(), "logs");
|
|
58
82
|
}
|
|
@@ -124,6 +148,48 @@ function isDirectory(p) {
|
|
|
124
148
|
}
|
|
125
149
|
}
|
|
126
150
|
|
|
151
|
+
// ── Marker helpers ───────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Defense-in-depth: an agent id must be a single plain path segment before
|
|
155
|
+
* it is used in any path.join or marker filename.
|
|
156
|
+
*/
|
|
157
|
+
function isSinglePathSegment(id) {
|
|
158
|
+
return (
|
|
159
|
+
typeof id === "string" &&
|
|
160
|
+
id.length > 0 &&
|
|
161
|
+
id !== "." &&
|
|
162
|
+
id !== ".." &&
|
|
163
|
+
!id.includes("/") &&
|
|
164
|
+
!id.includes("\\") &&
|
|
165
|
+
!id.includes(path.sep)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function markerPath(id) {
|
|
170
|
+
if (!isSinglePathSegment(id)) {
|
|
171
|
+
throw new Error(`Invalid agent id for marker path: ${JSON.stringify(id)}`);
|
|
172
|
+
}
|
|
173
|
+
return path.join(seederMarkersDir(), `${id}.seeded`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function hasMarker(id) {
|
|
177
|
+
return fs.existsSync(markerPath(id));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function writeMarker(id, source) {
|
|
181
|
+
fs.mkdirSync(seederMarkersDir(), { recursive: true });
|
|
182
|
+
fs.writeFileSync(
|
|
183
|
+
markerPath(id),
|
|
184
|
+
JSON.stringify(
|
|
185
|
+
{ seededAt: new Date().toISOString(), source },
|
|
186
|
+
null,
|
|
187
|
+
2
|
|
188
|
+
),
|
|
189
|
+
{ encoding: "utf8" }
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
127
193
|
// ── Seeding core ─────────────────────────────────────────────────────────
|
|
128
194
|
|
|
129
195
|
/**
|
|
@@ -135,18 +201,42 @@ function isDirectory(p) {
|
|
|
135
201
|
* Throws only on programmer error. All IO errors are caught and logged.
|
|
136
202
|
*/
|
|
137
203
|
function seedOneAgent(sourceDir, targetRoot, id) {
|
|
204
|
+
if (!isSinglePathSegment(id)) {
|
|
205
|
+
log("WARN", `Skipping invalid agent id ${JSON.stringify(id)}`);
|
|
206
|
+
return { status: "skipped" };
|
|
207
|
+
}
|
|
208
|
+
|
|
138
209
|
const src = path.join(sourceDir, id);
|
|
139
210
|
const target = path.join(targetRoot, id);
|
|
140
211
|
const partial = path.join(targetRoot, `${id}.partial`);
|
|
141
212
|
const sentinel = path.join(target, ".seeded");
|
|
142
213
|
|
|
143
|
-
//
|
|
214
|
+
// Rule 1: marker wins, full stop — honors deletion and leaves any
|
|
215
|
+
// user-recreated dir under the same id untouched.
|
|
216
|
+
if (hasMarker(id)) {
|
|
217
|
+
log("INFO", `Skipping "${id}" — already seeded once on this machine (marker present)`);
|
|
218
|
+
return { status: "skipped" };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Rule 2: seeded before markers existed → back-fill the marker.
|
|
222
|
+
// Runs every launch, so it also heals a crash between sentinel and marker.
|
|
144
223
|
if (fs.existsSync(sentinel)) {
|
|
145
|
-
|
|
224
|
+
try {
|
|
225
|
+
writeMarker(id, src);
|
|
226
|
+
log("INFO", `Skipping "${id}" — already seeded (sentinel present); marker back-filled`);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
log(
|
|
229
|
+
"ERROR",
|
|
230
|
+
`Failed to back-fill marker for "${id}": ${
|
|
231
|
+
err && err.message ? err.message : err
|
|
232
|
+
} — will retry next launch`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
146
235
|
return { status: "skipped" };
|
|
147
236
|
}
|
|
148
237
|
|
|
149
|
-
//
|
|
238
|
+
// Rule 3: target exists but no sentinel → user-owned data. Do not touch,
|
|
239
|
+
// write no marker.
|
|
150
240
|
if (fs.existsSync(target)) {
|
|
151
241
|
log(
|
|
152
242
|
"WARN",
|
|
@@ -176,7 +266,7 @@ function seedOneAgent(sourceDir, targetRoot, id) {
|
|
|
176
266
|
copyDirRecursive(src, partial);
|
|
177
267
|
fs.renameSync(partial, target);
|
|
178
268
|
|
|
179
|
-
// Write sentinel
|
|
269
|
+
// Write sentinel — its presence means "copy completed".
|
|
180
270
|
fs.writeFileSync(
|
|
181
271
|
sentinel,
|
|
182
272
|
JSON.stringify(
|
|
@@ -190,6 +280,9 @@ function seedOneAgent(sourceDir, targetRoot, id) {
|
|
|
190
280
|
{ encoding: "utf8" }
|
|
191
281
|
);
|
|
192
282
|
|
|
283
|
+
// Marker LAST — a crash here self-heals via the rule-2 back-fill.
|
|
284
|
+
writeMarker(id, src);
|
|
285
|
+
|
|
193
286
|
log("INFO", `Seeded "${id}" from ${src} to ${target}`);
|
|
194
287
|
return { status: "seeded" };
|
|
195
288
|
} catch (err) {
|
|
@@ -214,15 +307,156 @@ function seedOneAgent(sourceDir, targetRoot, id) {
|
|
|
214
307
|
}
|
|
215
308
|
}
|
|
216
309
|
|
|
310
|
+
// ── Legacy cleanup ───────────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
// Retired bundled ids removed from user machines when provably unmodified.
|
|
313
|
+
const LEGACY_CLEANUP_IDS = ["zoo-agent"];
|
|
314
|
+
|
|
315
|
+
// Filesystem mtime granularity + copy latency headroom for the
|
|
316
|
+
// modified-since-seed check.
|
|
317
|
+
const MODIFIED_SLACK_MS = 5000;
|
|
318
|
+
|
|
319
|
+
function listFilesRecursive(dir) {
|
|
320
|
+
const out = [];
|
|
321
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
322
|
+
// __pycache__/*.pyc is regenerated by the registry on every load — it
|
|
323
|
+
// postdates seededAt but is not a user modification.
|
|
324
|
+
if (entry.isDirectory() && entry.name === "__pycache__") continue;
|
|
325
|
+
if (!entry.isDirectory() && entry.name.endsWith(".pyc")) continue;
|
|
326
|
+
const p = path.join(dir, entry.name);
|
|
327
|
+
if (entry.isDirectory()) {
|
|
328
|
+
out.push(...listFilesRecursive(p));
|
|
329
|
+
} else {
|
|
330
|
+
out.push(p);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Remove retired seeded demo agents (currently `zoo-agent`) from
|
|
338
|
+
* ~/.gaia/agents/. Deletes ONLY when the dir is provably an unmodified
|
|
339
|
+
* seeded copy; anything ambiguous is left in place. Failures are logged
|
|
340
|
+
* and retried on the next launch (the sentinel keys the retry — no marker
|
|
341
|
+
* is involved, since a retired id can never be re-seeded anyway).
|
|
342
|
+
*
|
|
343
|
+
* @returns {string[]} ids actually removed (verified gone from disk).
|
|
344
|
+
*/
|
|
345
|
+
function cleanupLegacyAgents() {
|
|
346
|
+
const cleaned = [];
|
|
347
|
+
|
|
348
|
+
for (const id of LEGACY_CLEANUP_IDS) {
|
|
349
|
+
const target = path.join(agentsTargetRoot(), id);
|
|
350
|
+
|
|
351
|
+
let st;
|
|
352
|
+
try {
|
|
353
|
+
st = fs.lstatSync(target);
|
|
354
|
+
} catch {
|
|
355
|
+
continue; // not present — nothing to clean
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Never rm through a symlink — it would delete the link target.
|
|
359
|
+
if (st.isSymbolicLink()) {
|
|
360
|
+
log(
|
|
361
|
+
"WARN",
|
|
362
|
+
`Legacy "${id}" at ${target} is a symlink — leaving it untouched`
|
|
363
|
+
);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!st.isDirectory()) {
|
|
368
|
+
log("WARN", `Legacy "${id}" at ${target} is not a directory — leaving it untouched`);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// No sentinel → user-authored, not ours to remove.
|
|
373
|
+
const sentinel = path.join(target, ".seeded");
|
|
374
|
+
if (!fs.existsSync(sentinel)) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let seededAtMs;
|
|
379
|
+
try {
|
|
380
|
+
seededAtMs = Date.parse(
|
|
381
|
+
JSON.parse(fs.readFileSync(sentinel, "utf8")).seededAt
|
|
382
|
+
);
|
|
383
|
+
if (!Number.isFinite(seededAtMs)) {
|
|
384
|
+
throw new Error("seededAt missing or not a valid timestamp");
|
|
385
|
+
}
|
|
386
|
+
} catch (err) {
|
|
387
|
+
log(
|
|
388
|
+
"WARN",
|
|
389
|
+
`Cannot determine seededAt for legacy "${id}" (${
|
|
390
|
+
err && err.message ? err.message : err
|
|
391
|
+
}) — treating as user-modified, leaving it untouched`
|
|
392
|
+
);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Any file newer than the seed time means the user customized it —
|
|
397
|
+
// it is their work now, never delete it.
|
|
398
|
+
let modified = false;
|
|
399
|
+
try {
|
|
400
|
+
for (const file of listFilesRecursive(target)) {
|
|
401
|
+
if (fs.lstatSync(file).mtimeMs > seededAtMs + MODIFIED_SLACK_MS) {
|
|
402
|
+
log(
|
|
403
|
+
"WARN",
|
|
404
|
+
`Legacy "${id}" was modified after seeding (${file}) — leaving it untouched`
|
|
405
|
+
);
|
|
406
|
+
modified = true;
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
} catch (err) {
|
|
411
|
+
log(
|
|
412
|
+
"WARN",
|
|
413
|
+
`Cannot inspect legacy "${id}" for modifications (${
|
|
414
|
+
err && err.message ? err.message : err
|
|
415
|
+
}) — leaving it untouched`
|
|
416
|
+
);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (modified) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
425
|
+
} catch (err) {
|
|
426
|
+
log(
|
|
427
|
+
"ERROR",
|
|
428
|
+
`Failed to remove legacy "${id}" at ${target}: ${
|
|
429
|
+
err && err.message ? err.message : err
|
|
430
|
+
} — will retry next launch`
|
|
431
|
+
);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Report success only after verifying it is actually gone.
|
|
436
|
+
if (fs.existsSync(target)) {
|
|
437
|
+
log(
|
|
438
|
+
"ERROR",
|
|
439
|
+
`Legacy "${id}" still present after removal attempt: ${target} — will retry next launch`
|
|
440
|
+
);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
log("INFO", `Removed legacy seeded agent "${id}" from ${target}`);
|
|
445
|
+
cleaned.push(id);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return cleaned;
|
|
449
|
+
}
|
|
450
|
+
|
|
217
451
|
/**
|
|
218
452
|
* Seed all bundled agents found under `<resourcesPath>/agents/`.
|
|
219
453
|
*
|
|
220
454
|
* Idempotent — safe to call on every app launch.
|
|
221
455
|
*
|
|
222
|
-
* @returns {Promise<{seeded: string[], skipped: string[], errors: {id: string, error: Error}[]}>}
|
|
456
|
+
* @returns {Promise<{seeded: string[], skipped: string[], errors: {id: string, error: Error}[], cleaned: string[]}>}
|
|
223
457
|
*/
|
|
224
458
|
async function seedBundledAgents() {
|
|
225
|
-
const result = { seeded: [], skipped: [], errors: [] };
|
|
459
|
+
const result = { seeded: [], skipped: [], errors: [], cleaned: [] };
|
|
226
460
|
|
|
227
461
|
// Guard against dev / test environments where resourcesPath is unset.
|
|
228
462
|
if (!process.resourcesPath) {
|
|
@@ -233,6 +467,11 @@ async function seedBundledAgents() {
|
|
|
233
467
|
return result;
|
|
234
468
|
}
|
|
235
469
|
|
|
470
|
+
// Legacy cleanup runs before the missing-source guard: an upgraded
|
|
471
|
+
// install must shed the retired zoo-agent even if this build ships no
|
|
472
|
+
// bundled agents.
|
|
473
|
+
result.cleaned = cleanupLegacyAgents();
|
|
474
|
+
|
|
236
475
|
const sourceDir = path.join(process.resourcesPath, "agents");
|
|
237
476
|
|
|
238
477
|
if (!fs.existsSync(sourceDir) || !isDirectory(sourceDir)) {
|
|
@@ -284,7 +523,8 @@ async function seedBundledAgents() {
|
|
|
284
523
|
log(
|
|
285
524
|
"INFO",
|
|
286
525
|
`Seeding complete — seeded=${result.seeded.length} ` +
|
|
287
|
-
`skipped=${result.skipped.length} errors=${result.errors.length}`
|
|
526
|
+
`skipped=${result.skipped.length} errors=${result.errors.length} ` +
|
|
527
|
+
`cleaned=${result.cleaned.length}`
|
|
288
528
|
);
|
|
289
529
|
|
|
290
530
|
return result;
|
|
@@ -295,7 +535,9 @@ module.exports = {
|
|
|
295
535
|
// Exposed for tests — do not rely on these from production code.
|
|
296
536
|
_internals: {
|
|
297
537
|
seedOneAgent,
|
|
538
|
+
cleanupLegacyAgents,
|
|
298
539
|
agentsTargetRoot,
|
|
540
|
+
seederMarkersDir,
|
|
299
541
|
logFilePath,
|
|
300
542
|
},
|
|
301
543
|
};
|