unity-open-mcp 0.8.4 → 1.0.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/asmdef-discovery.d.ts +35 -0
- package/dist/asmdef-discovery.js +171 -0
- package/dist/asmdef-discovery.js.map +1 -0
- package/dist/batch-spawn.d.ts +1 -0
- package/dist/batch-spawn.js +118 -2
- package/dist/batch-spawn.js.map +1 -1
- package/dist/capabilities/intent-groups.js +42 -0
- package/dist/capabilities/intent-groups.js.map +1 -1
- package/dist/capabilities/lifecycle.js +12 -0
- package/dist/capabilities/lifecycle.js.map +1 -1
- package/dist/capabilities/rule-catalog.js +7 -1
- package/dist/capabilities/rule-catalog.js.map +1 -1
- package/dist/capabilities/tool-groups.js +39 -2
- package/dist/capabilities/tool-groups.js.map +1 -1
- package/dist/constants.d.ts +21 -1
- package/dist/constants.js +21 -1
- package/dist/constants.js.map +1 -1
- package/dist/event-stream.d.ts +13 -3
- package/dist/event-stream.js +77 -12
- package/dist/event-stream.js.map +1 -1
- package/dist/live-client.d.ts +17 -0
- package/dist/live-client.js +130 -9
- package/dist/live-client.js.map +1 -1
- package/dist/offline/api.js +82 -30
- package/dist/offline/api.js.map +1 -1
- package/dist/offline/index-builders.d.ts +9 -0
- package/dist/offline/index-builders.js +58 -8
- package/dist/offline/index-builders.js.map +1 -1
- package/dist/offline/parse.js +5 -1
- package/dist/offline/parse.js.map +1 -1
- package/dist/process-diagnostics.d.ts +100 -19
- package/dist/process-diagnostics.js +167 -59
- package/dist/process-diagnostics.js.map +1 -1
- package/dist/project-settings.d.ts +36 -0
- package/dist/project-settings.js +96 -0
- package/dist/project-settings.js.map +1 -0
- package/dist/running-unity.d.ts +23 -0
- package/dist/running-unity.js +60 -0
- package/dist/running-unity.js.map +1 -1
- package/dist/stale-assembly-cache.d.ts +40 -0
- package/dist/stale-assembly-cache.js +76 -0
- package/dist/stale-assembly-cache.js.map +1 -0
- package/dist/tool-router.d.ts +40 -1
- package/dist/tool-router.js +479 -68
- package/dist/tool-router.js.map +1 -1
- package/dist/tool-session-state.d.ts +12 -6
- package/dist/tool-session-state.js +16 -9
- package/dist/tool-session-state.js.map +1 -1
- package/dist/tools/capture-inline.js +4 -3
- package/dist/tools/capture-inline.js.map +1 -1
- package/dist/tools/component-get.js +9 -0
- package/dist/tools/component-get.js.map +1 -1
- package/dist/tools/editor-status.js +12 -3
- package/dist/tools/editor-status.js.map +1 -1
- package/dist/tools/execute-csharp.js +41 -4
- package/dist/tools/execute-csharp.js.map +1 -1
- package/dist/tools/gameobject-create.js +19 -6
- package/dist/tools/gameobject-create.js.map +1 -1
- package/dist/tools/gameobject-duplicate.js +7 -2
- package/dist/tools/gameobject-duplicate.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +37 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inputsim-key.d.ts +40 -0
- package/dist/tools/inputsim-key.js +72 -0
- package/dist/tools/inputsim-key.js.map +1 -0
- package/dist/tools/inputsim-pointer.d.ts +40 -0
- package/dist/tools/inputsim-pointer.js +121 -0
- package/dist/tools/inputsim-pointer.js.map +1 -0
- package/dist/tools/inputsim-pointer3d.d.ts +40 -0
- package/dist/tools/inputsim-pointer3d.js +55 -0
- package/dist/tools/inputsim-pointer3d.js.map +1 -0
- package/dist/tools/inputsim-probe.d.ts +40 -0
- package/dist/tools/inputsim-probe.js +43 -0
- package/dist/tools/inputsim-probe.js.map +1 -0
- package/dist/tools/inputsim-step.d.ts +40 -0
- package/dist/tools/inputsim-step.js +50 -0
- package/dist/tools/inputsim-step.js.map +1 -0
- package/dist/tools/inputsim-touch.d.ts +40 -0
- package/dist/tools/inputsim-touch.js +83 -0
- package/dist/tools/inputsim-touch.js.map +1 -0
- package/dist/tools/manage-tools.js +8 -1
- package/dist/tools/manage-tools.js.map +1 -1
- package/dist/tools/memory-snapshot-capture.js +11 -5
- package/dist/tools/memory-snapshot-capture.js.map +1 -1
- package/dist/tools/prefab-instantiate.js +11 -0
- package/dist/tools/prefab-instantiate.js.map +1 -1
- package/dist/tools/pull-events.js +18 -10
- package/dist/tools/pull-events.js.map +1 -1
- package/dist/tools/read-compile-errors.js +15 -1
- package/dist/tools/read-compile-errors.js.map +1 -1
- package/dist/tools/recompile-scripts.d.ts +40 -0
- package/dist/tools/recompile-scripts.js +35 -0
- package/dist/tools/recompile-scripts.js.map +1 -0
- package/dist/tools/resource-pressure.js +21 -10
- package/dist/tools/resource-pressure.js.map +1 -1
- package/dist/tools/run-tests.js +21 -4
- package/dist/tools/run-tests.js.map +1 -1
- package/dist/tools/screenshot-window.js +18 -12
- package/dist/tools/screenshot-window.js.map +1 -1
- package/dist/tools/screenshot.js +13 -6
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/ui-canvas-add.js +19 -6
- package/dist/tools/ui-canvas-add.js.map +1 -1
- package/dist/tools/visual-compare.d.ts +40 -0
- package/dist/tools/visual-compare.js +98 -0
- package/dist/tools/visual-compare.js.map +1 -0
- package/dist/unity-log.d.ts +111 -1
- package/dist/unity-log.js +334 -11
- package/dist/unity-log.js.map +1 -1
- package/package.json +1 -1
package/dist/tool-router.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AssetModelCache, isCompressible, routeCompressible } from "./compressible-router.js";
|
|
2
2
|
import { listAssetsOffline, findReferencesOffline, dependenciesOffline } from "./offline.js";
|
|
3
|
-
import { resolveEditorLogPath, readLogTail, DEFAULT_LOG_TAIL_BYTES, detectStaleLog } from "./unity-log.js";
|
|
3
|
+
import { resolveEditorLogPath, readLogTail, DEFAULT_LOG_TAIL_BYTES, detectStaleLog, detectStaleAssembly, parseLogAuthorship, compareLogAuthorship } from "./unity-log.js";
|
|
4
4
|
import { summarizeProjectHealth, extractProjectHealthIssues } from "./project-health.js";
|
|
5
|
+
import { countProjectAsmdefs, countAssembliesWithErrors } from "./asmdef-discovery.js";
|
|
5
6
|
import { buildCapabilities } from "./capabilities/build-capabilities.js";
|
|
6
7
|
import { RULE_CATALOG, FIX_CATALOG } from "./capabilities/rule-catalog.js";
|
|
7
8
|
import { GROUP_IDS, TOOL_GROUPS, AUTO_ACTIVATE_GROUPS, groupToTools, getGroup, } from "./capabilities/tool-groups.js";
|
|
@@ -11,8 +12,8 @@ import { generateSkill } from "./skill/generate-skill.js";
|
|
|
11
12
|
import { getKnownClientKeys } from "./skill/client-paths.js";
|
|
12
13
|
import { ALL_TOOLS } from "./tools/index.js";
|
|
13
14
|
import { lockPath, readInstanceLock, classifyInstance, isPidAlive } from "./instance-discovery.js";
|
|
14
|
-
import { PORT_ENV_VAR } from "./constants.js";
|
|
15
|
-
import { findUnityForProject } from "./running-unity.js";
|
|
15
|
+
import { PORT_ENV_VAR, TYPED_EDITOR_ACTIVATE_INSTRUCTION } from "./constants.js";
|
|
16
|
+
import { findUnityForProject, readProcessCommandLine } from "./running-unity.js";
|
|
16
17
|
// M31-optimizations Plan 1 / L14 — opt-in route-logging gate. Resolved once at
|
|
17
18
|
// module load into a boolean; the per-dispatch log sites read the boolean
|
|
18
19
|
// (not process.env on every call) so the hot path stays a single branch.
|
|
@@ -38,7 +39,8 @@ export function logRoute(message) {
|
|
|
38
39
|
// (resource_pressure). Both act on the OS process, not the bridge, so they
|
|
39
40
|
// share the local route classification with bridge_status.
|
|
40
41
|
import { killEditorProcess, DEFAULT_KILL_GRACE_MS, MAX_KILL_WAIT_MS, } from "./editor-process-control.js";
|
|
41
|
-
import { countFileDescriptors, computeFdHeadroom, analyzeFdTrend, LAUNCH_CONTEXT_CAVEAT,
|
|
42
|
+
import { countFileDescriptors, computeFdHeadroom, analyzeFdTrend, LAUNCH_CONTEXT_CAVEAT, } from "./process-diagnostics.js";
|
|
43
|
+
import { readFdCeiling } from "./project-settings.js";
|
|
42
44
|
import { listInstalledEditors, fetchAvailableReleases, openInstallDeepLink, getInstallPath, setInstallPath, } from "./hub-control.js";
|
|
43
45
|
/** Default Hub-control backend — calls the real hub-control functions. */
|
|
44
46
|
const defaultHubBackend = {
|
|
@@ -276,33 +278,59 @@ function pageFindReferencesResult(result, pageSize, cursor) {
|
|
|
276
278
|
function foldFindReferencesLiveBody(body, profile, wantPaging, args) {
|
|
277
279
|
if (body.error)
|
|
278
280
|
return null;
|
|
279
|
-
// Bridge shape: { referencedBy:
|
|
281
|
+
// Bridge shape: { referencedBy: Array<{assetPath, guid}>, totalCount, ... } —
|
|
282
|
+
// each entry is an object (FindReferencesTool emits {assetPath, guid}). Older
|
|
283
|
+
// comments assumed a flat string[]; the live bridge has never emitted that.
|
|
284
|
+
// `extractRefPath` normalizes both the object form and a legacy/defensive
|
|
285
|
+
// bare-string form so a future shape change does not crash the compact
|
|
286
|
+
// grouping (feedback-01-08-glm §3: a non-string element reached
|
|
287
|
+
// `.lastIndexOf` and hard-crashed the tool).
|
|
280
288
|
const rawList = Array.isArray(body.referencedBy) ? body.referencedBy : [];
|
|
289
|
+
const pathList = rawList.map((entry) => extractRefPath(entry));
|
|
281
290
|
const pageSize = typeof args.page_size === "number" && args.page_size > 0 ? args.page_size : 0;
|
|
282
291
|
const cursor = typeof args.cursor === "string" ? args.cursor : undefined;
|
|
283
292
|
if (profile === "compact") {
|
|
284
293
|
// compact: counts + byKind grouping only — drop the per-asset list.
|
|
285
294
|
const { referencedBy: _dropped, ...rest } = body;
|
|
286
295
|
void _dropped;
|
|
287
|
-
const byKind = groupLiveRefsByKind(
|
|
296
|
+
const byKind = groupLiveRefsByKind(pathList);
|
|
288
297
|
return {
|
|
289
298
|
...rest,
|
|
290
|
-
totalCount:
|
|
299
|
+
totalCount: pathList.length,
|
|
291
300
|
byKind,
|
|
292
301
|
referencedBy: [],
|
|
293
302
|
};
|
|
294
303
|
}
|
|
295
304
|
// balanced / full: keep the path list (paged when requested).
|
|
296
305
|
if (pageSize > 0) {
|
|
297
|
-
const { page, block } = applyPaging(
|
|
298
|
-
return attachPagination({ ...body, referencedBy: page, totalCount:
|
|
306
|
+
const { page, block } = applyPaging(pathList, "find_references", { page_size: pageSize, cursor });
|
|
307
|
+
return attachPagination({ ...body, referencedBy: page, totalCount: pathList.length }, block);
|
|
299
308
|
}
|
|
300
|
-
return { ...body, totalCount:
|
|
309
|
+
return { ...body, referencedBy: pathList, totalCount: pathList.length };
|
|
310
|
+
}
|
|
311
|
+
/** Coerce a `referencedBy` entry to its asset path. The live bridge emits
|
|
312
|
+
* `{assetPath, guid}` objects; accept a bare string defensively so a shape
|
|
313
|
+
* change never reaches the string-only `.lastIndexOf` in the grouping. */
|
|
314
|
+
function extractRefPath(entry) {
|
|
315
|
+
if (typeof entry === "string")
|
|
316
|
+
return entry;
|
|
317
|
+
if (entry && typeof entry === "object") {
|
|
318
|
+
const obj = entry;
|
|
319
|
+
if (typeof obj.assetPath === "string")
|
|
320
|
+
return obj.assetPath;
|
|
321
|
+
if (typeof obj.path === "string")
|
|
322
|
+
return obj.path;
|
|
323
|
+
}
|
|
324
|
+
return "";
|
|
301
325
|
}
|
|
302
326
|
/** Derive a byKind grouping from flat referenced paths for the live compact view. */
|
|
303
327
|
function groupLiveRefsByKind(paths) {
|
|
304
328
|
const counts = {};
|
|
305
329
|
for (const path of paths) {
|
|
330
|
+
if (!path) {
|
|
331
|
+
counts.other = (counts.other ?? 0) + 1;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
306
334
|
const dot = path.lastIndexOf(".");
|
|
307
335
|
const kind = dot > 0 ? path.slice(dot + 1).toLowerCase() : "other";
|
|
308
336
|
counts[kind] = (counts[kind] ?? 0) + 1;
|
|
@@ -362,6 +390,7 @@ export class ToolRouter {
|
|
|
362
390
|
sessionState;
|
|
363
391
|
onToolListChanged;
|
|
364
392
|
hubBackend;
|
|
393
|
+
globalLogPathOverride;
|
|
365
394
|
modelCache = new AssetModelCache();
|
|
366
395
|
/**
|
|
367
396
|
* M28-refactoring Plan 3 (T3.2) — named-tool dispatch map. Replaces the
|
|
@@ -418,7 +447,15 @@ export class ToolRouter {
|
|
|
418
447
|
// M26 Plan 2 — injectable Hub-control backend. Omit in production (the
|
|
419
448
|
// default calls the real hub-control functions); tests pass a fake to
|
|
420
449
|
// avoid real subprocess + network side effects.
|
|
421
|
-
hubBackend = defaultHubBackend
|
|
450
|
+
hubBackend = defaultHubBackend,
|
|
451
|
+
// feedback-fable-04-08 §2b — injectable global Editor.log path for tests.
|
|
452
|
+
// Production leaves this undefined so resolveEditorLogPath computes the
|
|
453
|
+
// real platform global path; tests pass a path under their tmp sandbox so
|
|
454
|
+
// the freshness comparison does not consult the host machine's real
|
|
455
|
+
// ~/Library/Logs/Unity/Editor.log (which is newer than the artificial
|
|
456
|
+
// 1970-era mtimes the tests plant and would win the comparison, reading
|
|
457
|
+
// the wrong file).
|
|
458
|
+
globalLogPathOverride) {
|
|
422
459
|
this.live = live;
|
|
423
460
|
this.batch = batch;
|
|
424
461
|
this.projectPath = projectPath;
|
|
@@ -426,6 +463,7 @@ export class ToolRouter {
|
|
|
426
463
|
this.sessionState = sessionState;
|
|
427
464
|
this.onToolListChanged = onToolListChanged;
|
|
428
465
|
this.hubBackend = hubBackend;
|
|
466
|
+
this.globalLogPathOverride = globalLogPathOverride;
|
|
429
467
|
}
|
|
430
468
|
async route(toolName, args) {
|
|
431
469
|
return this.routeCore(this.live, toolName, args);
|
|
@@ -502,10 +540,13 @@ export class ToolRouter {
|
|
|
502
540
|
return this.batchPingResult();
|
|
503
541
|
}
|
|
504
542
|
const result = await this.batch.route(toolName, args);
|
|
505
|
-
|
|
543
|
+
// feedback-fable-04-08 §7 — when the batch fallback hit a live editor's
|
|
544
|
+
// project lock, the actionable signal is editor_busy, not live_unavailable.
|
|
545
|
+
const meta = this.rewriteEditorBusy(result, {
|
|
506
546
|
route: "batch",
|
|
507
547
|
fallbackReason: "live_unavailable",
|
|
508
548
|
});
|
|
549
|
+
return injectRouteMeta(result, meta);
|
|
509
550
|
}
|
|
510
551
|
batchPingResult() {
|
|
511
552
|
return {
|
|
@@ -530,6 +571,52 @@ export class ToolRouter {
|
|
|
530
571
|
isError: false,
|
|
531
572
|
};
|
|
532
573
|
}
|
|
574
|
+
/**
|
|
575
|
+
* feedback-fable-04-08 §7 — when a `live_unavailable` batch fallback lands
|
|
576
|
+
* on `editor_instance_locked`, the message describes the BATCH SPAWNER's
|
|
577
|
+
* problem ("a live editor holds the project lock, so no headless spawn"),
|
|
578
|
+
* but the agent's actionable truth is "the editor is busy (TestRunner run /
|
|
579
|
+
* long import); retry later", not a lock conflict. Detect this case by
|
|
580
|
+
* inspecting the batch result for `error.code === "editor_instance_locked"`
|
|
581
|
+
* AND confirming a live PID still holds the project (a stale lock with no
|
|
582
|
+
* live PID is a genuine lock conflict — leave it as `live_unavailable`).
|
|
583
|
+
*
|
|
584
|
+
* Returns the rewritten meta (and the result unchanged — the lock error body
|
|
585
|
+
* is real and must stay intact) so the caller stamps it via injectRouteMeta.
|
|
586
|
+
* The live PID is surfaced as `editorPid` for agent correlation. Falls
|
|
587
|
+
* through unchanged when the code differs, no live PID exists, or the body
|
|
588
|
+
* is unparseable.
|
|
589
|
+
*/
|
|
590
|
+
rewriteEditorBusy(result, meta) {
|
|
591
|
+
if (meta.fallbackReason !== "live_unavailable")
|
|
592
|
+
return meta;
|
|
593
|
+
const body = parseResultBody(result);
|
|
594
|
+
if (body === null)
|
|
595
|
+
return meta;
|
|
596
|
+
const err = body.error;
|
|
597
|
+
const code = typeof err === "object" && err !== null
|
|
598
|
+
? err.code
|
|
599
|
+
: undefined;
|
|
600
|
+
if (code !== "editor_instance_locked")
|
|
601
|
+
return meta;
|
|
602
|
+
// Confirm a live editor actually holds the project. The batch pre-spawn
|
|
603
|
+
// lock check already read the lock, but a race (the editor quit between
|
|
604
|
+
// the batch check and here) or a corrupt lock must NOT be misreported as
|
|
605
|
+
// "busy" — fall through to the generic live_unavailable in that case.
|
|
606
|
+
try {
|
|
607
|
+
const lock = readInstanceLock(this.projectPath);
|
|
608
|
+
if (!lock?.pid || !isPidAlive(lock.pid))
|
|
609
|
+
return meta;
|
|
610
|
+
return {
|
|
611
|
+
route: "batch",
|
|
612
|
+
fallbackReason: "editor_busy",
|
|
613
|
+
editorPid: lock.pid,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
return meta;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
533
620
|
async routeReadCompileErrors(args) {
|
|
534
621
|
const tailBytes = typeof args.tail_bytes === "number" && args.tail_bytes >= 4096
|
|
535
622
|
? Math.min(Math.floor(args.tail_bytes), 1048576)
|
|
@@ -538,7 +625,15 @@ export class ToolRouter {
|
|
|
538
625
|
// project-relative log (<project>/Logs/Editor.log); the global per-user
|
|
539
626
|
// log is stale there. resolveEditorLogPath prefers the project-relative
|
|
540
627
|
// log when it exists and falls back to the global log for older Unity.
|
|
541
|
-
|
|
628
|
+
//
|
|
629
|
+
// feedback-fable-31-07 §5 — when a live editor holds the project AND the
|
|
630
|
+
// resolved Editor.log looks frozen (rotated by a batch spawn that failed
|
|
631
|
+
// on the project lock), fall back to Editor-prev.log, where the live
|
|
632
|
+
// editor keeps writing. Pass the live PID from the instance lock so the
|
|
633
|
+
// resolver can make the strong-signal decision.
|
|
634
|
+
const livePid = this.resolveLiveEditorPid();
|
|
635
|
+
const resolvedLog = resolveEditorLogPath(this.projectPath, undefined, livePid, this.globalLogPathOverride);
|
|
636
|
+
const logPath = resolvedLog.path;
|
|
542
637
|
const tail = readLogTail(logPath, tailBytes);
|
|
543
638
|
if (tail.error) {
|
|
544
639
|
return {
|
|
@@ -583,11 +678,6 @@ export class ToolRouter {
|
|
|
583
678
|
// backward compatibility) so existing callers do not break.
|
|
584
679
|
const health = summarizeProjectHealth(tail.content);
|
|
585
680
|
const errors = health.compilerErrors;
|
|
586
|
-
const status = health.unhealthy
|
|
587
|
-
? errors.length > 0
|
|
588
|
-
? "compile_failed"
|
|
589
|
-
: "project_unhealthy"
|
|
590
|
-
: "no_errors_found";
|
|
591
681
|
// specs/feedback.md 2026-07-05 — stale-log detection. When an assembly is
|
|
592
682
|
// stuck in a failed-compile state, AssetDatabase.Refresh no-ops and the
|
|
593
683
|
// log's most-recent CSxxxx block can reference on-disk source that has
|
|
@@ -602,17 +692,108 @@ export class ToolRouter {
|
|
|
602
692
|
if (stale.staleLogSuspected)
|
|
603
693
|
staleLog = stale;
|
|
604
694
|
}
|
|
695
|
+
// feedback-01-08-glm §5b — stale-ASSEMBLY detection. Runs UNCONDITIONALLY
|
|
696
|
+
// (even on no_errors_found): after a C# edit, AssetDatabase.Refresh often
|
|
697
|
+
// no-ops, the DLL is never rebuilt, and the log carries no fresh CSxxxx —
|
|
698
|
+
// so a healthy signal reported against a stale assembly misleads the agent
|
|
699
|
+
// into trusting code that has not actually been rebuilt. Cross-check the
|
|
700
|
+
// newest Library/ScriptAssemblies/*.dll mtime against Assets/**/*.cs source
|
|
701
|
+
// mtimes and surface staleAssembly when any source is newer than the DLL.
|
|
702
|
+
const staleAsm = detectStaleAssembly(this.projectPath);
|
|
703
|
+
// feedback-04-08-opus §2 — log-authorship cross-check. A batch run of a
|
|
704
|
+
// NEWER Unity writes a log whose errors (e.g. an API deprecation that is
|
|
705
|
+
// only an error on that version) CANNOT occur in the live editor. Parse the
|
|
706
|
+
// log header's version + batch-mode + date, compare against the live bridge
|
|
707
|
+
// version, and surface a mismatch so the agent does not "fix" 31 unrelated
|
|
708
|
+
// files or refuse to continue on errors that cannot occur here. The fields
|
|
709
|
+
// are always emitted when the header was parseable (so an agent can see the
|
|
710
|
+
// log was a batch run even when versions agree); the warning hint is added
|
|
711
|
+
// only on a real mismatch or a batch-mode log.
|
|
712
|
+
const authorship = parseLogAuthorship(tail.content);
|
|
713
|
+
const liveUnityVersion = this.resolveLiveUnityVersion();
|
|
714
|
+
const authorshipCheck = compareLogAuthorship(authorship, liveUnityVersion);
|
|
715
|
+
// feedback-fable-04-08 §4 — downgrade the top-level status when the errors
|
|
716
|
+
// may not apply to the running editor. An agent that trusts a
|
|
717
|
+
// `compile_failed` + `unhealthy: true` headline will start "fixing" 31
|
|
718
|
+
// phantom errors from a stale / batch-authored log. When EITHER the log is
|
|
719
|
+
// stale (a cited source is newer than the log) OR a version-mismatch is
|
|
720
|
+
// detected (a different Unity wrote the log), the verdict becomes
|
|
721
|
+
// `stale_log` — the errors[] are still attached as evidence, but the
|
|
722
|
+
// headline + status make clear they MUST NOT be acted on until a genuine
|
|
723
|
+
// recompile confirms them. The raw verdict (what the log itself says) is
|
|
724
|
+
// preserved in `logVerdict` for agents that want the literal signal.
|
|
725
|
+
const logVerdict = health.unhealthy
|
|
726
|
+
? errors.length > 0
|
|
727
|
+
? "compile_failed"
|
|
728
|
+
: "project_unhealthy"
|
|
729
|
+
: "no_errors_found";
|
|
730
|
+
const errorsMayNotApply = health.unhealthy &&
|
|
731
|
+
(!!staleLog || authorshipCheck.versionMismatch);
|
|
732
|
+
const status = errorsMayNotApply ? "stale_log" : logVerdict;
|
|
733
|
+
// specs/feedback.md 2026-08-12 — partial-compile signal. Unity compiles
|
|
734
|
+
// assemblies in dependency order; when an early assembly fails the pipeline
|
|
735
|
+
// may not reach dependent ones, so the reported errors can be a SUBSET of
|
|
736
|
+
// the true error set. When there are errors AND the project has multiple
|
|
737
|
+
// compile assemblies, surface asmdefCount + assembliesWithErrors +
|
|
738
|
+
// partialCompileLikely so an agent does not treat `errorCount` as final
|
|
739
|
+
// (this is exactly the trap that masked 6 errors behind 1 during the
|
|
740
|
+
// input-simulation review).
|
|
741
|
+
// Only walk for asmdefs when there are errors — the signal is only
|
|
742
|
+
// meaningful with errors, and countProjectAsmdefs stats the whole Assets/
|
|
743
|
+
// tree synchronously (memoized per project, but still skip it when unused).
|
|
744
|
+
const hasErrors = errors.length > 0;
|
|
745
|
+
const asmdefs = hasErrors
|
|
746
|
+
? countProjectAsmdefs(this.projectPath)
|
|
747
|
+
: { count: 0, sample: [] };
|
|
748
|
+
const partialCompileLikely = hasErrors && asmdefs.count > 1;
|
|
749
|
+
const assembliesWithErrors = partialCompileLikely
|
|
750
|
+
? countAssembliesWithErrors(errors.map((e) => e.file), this.projectPath)
|
|
751
|
+
: 0;
|
|
605
752
|
return sourceResult({
|
|
606
753
|
status,
|
|
607
|
-
|
|
608
|
-
|
|
754
|
+
// When the errors may not apply (stale log / version mismatch), the
|
|
755
|
+
// top-level verdict is downgraded to "stale_log" and unhealthy is
|
|
756
|
+
// suppressed so an agent branching on `unhealthy` does not treat
|
|
757
|
+
// phantom errors as a hard failure (feedback-fable-04-08 §4). The raw
|
|
758
|
+
// log verdict is preserved in `logVerdict` for callers that want the
|
|
759
|
+
// literal signal regardless of freshness.
|
|
760
|
+
unhealthy: errorsMayNotApply ? false : health.unhealthy,
|
|
761
|
+
headline: errorsMayNotApply
|
|
762
|
+
? `${errors.length} error(s) in Editor.log, but the log appears ` +
|
|
763
|
+
"STALE or authored by a different Unity — these errors may NOT " +
|
|
764
|
+
"apply to the running editor. Do NOT act on them until a genuine " +
|
|
765
|
+
"recompile (unity_open_mcp_recompile_scripts, in the typed-editor " +
|
|
766
|
+
`group — ${TYPED_EDITOR_ACTIVATE_INSTRUCTION} first) confirms them.`
|
|
767
|
+
: health.headline,
|
|
768
|
+
...(errorsMayNotApply ? { logVerdict } : {}),
|
|
609
769
|
errorCount: errors.length,
|
|
610
770
|
errors,
|
|
771
|
+
// specs/feedback.md 2026-08-12 — present only when there are errors AND
|
|
772
|
+
// the project has multiple compile assemblies. Unity's dependency-ordered
|
|
773
|
+
// pipeline means these errors may be a subset; fixing them lets dependent
|
|
774
|
+
// assemblies compile and may surface more. Re-check until errorCount is 0.
|
|
775
|
+
...(partialCompileLikely
|
|
776
|
+
? {
|
|
777
|
+
partialCompileLikely: true,
|
|
778
|
+
asmdefCount: asmdefs.count,
|
|
779
|
+
// Distinct assemblies the reported errors span (null when none of
|
|
780
|
+
// the cited files mapped to an asmdef — e.g. global/package code).
|
|
781
|
+
assembliesWithErrors: assembliesWithErrors > 0 ? assembliesWithErrors : null,
|
|
782
|
+
partialCompileHint: "Unity compiles assemblies in dependency order; the reported errors " +
|
|
783
|
+
"may be a SUBSET — once these are fixed, dependent assemblies that " +
|
|
784
|
+
"were not reached compile next and may surface additional errors. " +
|
|
785
|
+
"Re-check read_compile_errors after each fix until errorCount is 0.",
|
|
786
|
+
}
|
|
787
|
+
: {}),
|
|
611
788
|
// Package / assembly issues from the same log tail. Empty when the
|
|
612
789
|
// only red flags are compiler errors (the common case).
|
|
613
790
|
issues: health.issues,
|
|
614
791
|
issueCount: health.issues.length,
|
|
615
792
|
logPath,
|
|
793
|
+
// Why this log file was chosen (feedback-fable-31-07 §5). Non-default
|
|
794
|
+
// values signal a log-rotation fallback: the live editor is writing to
|
|
795
|
+
// Editor-prev.log after a batch spawn rotated Editor.log.
|
|
796
|
+
logSource: resolvedLog.reason,
|
|
616
797
|
tailBytes: tail.bytes,
|
|
617
798
|
// Present ONLY when at least one cited source file is newer than
|
|
618
799
|
// Editor.log — the log's most-recent error block may be stale and
|
|
@@ -624,8 +805,78 @@ export class ToolRouter {
|
|
|
624
805
|
staleLogNewerFiles: staleLog.newerFiles,
|
|
625
806
|
}
|
|
626
807
|
: {}),
|
|
808
|
+
// Present ONLY when at least one Assets/**/*.cs source is newer than
|
|
809
|
+
// the newest built DLL — the running assembly predates the latest
|
|
810
|
+
// source, so a no_errors_found signal cannot be trusted until the
|
|
811
|
+
// assembly is rebuilt (feedback-01-08-glm §5b).
|
|
812
|
+
...(staleAsm.staleAssembly
|
|
813
|
+
? {
|
|
814
|
+
staleAssembly: true,
|
|
815
|
+
staleAssemblyHint: staleAsm.hint,
|
|
816
|
+
staleAssemblyNewerSources: staleAsm.newerSources,
|
|
817
|
+
}
|
|
818
|
+
: {}),
|
|
819
|
+
// feedback-04-08-opus §2 — log authorship. Emitted whenever the header
|
|
820
|
+
// was parseable (the live editor's long tail usually has no header, so
|
|
821
|
+
// these stay absent in the common case). On a version mismatch the hint
|
|
822
|
+
// says the errors CANNOT all occur in the live editor; on a batch-mode
|
|
823
|
+
// log it reminds the agent the errors are from a headless run.
|
|
824
|
+
...(authorship.logUnityVersion !== null || authorship.logBatchMode !== null
|
|
825
|
+
? {
|
|
826
|
+
logUnityVersion: authorship.logUnityVersion,
|
|
827
|
+
liveUnityVersion: authorshipCheck.liveUnityVersion,
|
|
828
|
+
logBatchMode: authorship.logBatchMode,
|
|
829
|
+
logDate: authorship.logDate,
|
|
830
|
+
...(authorshipCheck.hint
|
|
831
|
+
? {
|
|
832
|
+
logAuthorshipMismatch: authorshipCheck.versionMismatch,
|
|
833
|
+
logAuthorshipHint: authorshipCheck.hint,
|
|
834
|
+
}
|
|
835
|
+
: {}),
|
|
836
|
+
}
|
|
837
|
+
: {}),
|
|
627
838
|
}, "offline");
|
|
628
839
|
}
|
|
840
|
+
// Resolve the live Unity Editor PID for this project from the instance lock,
|
|
841
|
+
// or undefined when no live editor is known. Used by routeReadCompileErrors
|
|
842
|
+
// to feed the log-rotation fallback in resolveEditorLogPath
|
|
843
|
+
// (feedback-fable-31-07 §5): a frozen Editor.log + a live PID is the
|
|
844
|
+
// signature of a batch spawn that rotated the log while the live editor
|
|
845
|
+
// keeps writing to Editor-prev.log.
|
|
846
|
+
resolveLiveEditorPid() {
|
|
847
|
+
if (!this.projectPath)
|
|
848
|
+
return undefined;
|
|
849
|
+
try {
|
|
850
|
+
const lock = readInstanceLock(this.projectPath);
|
|
851
|
+
if (lock && lock.pid && isPidAlive(lock.pid))
|
|
852
|
+
return lock.pid;
|
|
853
|
+
}
|
|
854
|
+
catch {
|
|
855
|
+
// Unreadable/missing lock → no live PID known.
|
|
856
|
+
}
|
|
857
|
+
return undefined;
|
|
858
|
+
}
|
|
859
|
+
// feedback-04-08-opus §2 — resolve the live bridge's Unity version from the
|
|
860
|
+
// instance lock, so read_compile_errors can cross-check the LOG's version
|
|
861
|
+
// header against the version of the editor that is actually running. A batch
|
|
862
|
+
// run of a NEWER Unity leaves a log whose errors cannot occur in the live
|
|
863
|
+
// editor; without this check the tool would surface them as current. Returns
|
|
864
|
+
// null when no live editor / no version recorded (the benign case — no
|
|
865
|
+
// version to compare against, so no false mismatch fires).
|
|
866
|
+
resolveLiveUnityVersion() {
|
|
867
|
+
if (!this.projectPath)
|
|
868
|
+
return null;
|
|
869
|
+
try {
|
|
870
|
+
const lock = readInstanceLock(this.projectPath);
|
|
871
|
+
if (lock && lock.pid && isPidAlive(lock.pid) && lock.unityVersion) {
|
|
872
|
+
return lock.unityVersion;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
// Unreadable/missing lock → no live version known.
|
|
877
|
+
}
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
629
880
|
async routeListAssets(args) {
|
|
630
881
|
try {
|
|
631
882
|
const result = await listAssetsOffline({
|
|
@@ -677,7 +928,13 @@ export class ToolRouter {
|
|
|
677
928
|
const maxEvents = typeof args.max_events === "number" && args.max_events > 0
|
|
678
929
|
? Math.min(args.max_events, 1000)
|
|
679
930
|
: 50;
|
|
680
|
-
|
|
931
|
+
// feedback #8 — forward the caller-supplied subscriber id so each agent
|
|
932
|
+
// session gets its own read cursor (a fresh subscriber starts at the
|
|
933
|
+
// current high-water mark and does not replay the shared queue's backlog).
|
|
934
|
+
const subscriber = typeof args.subscriber === "string" && args.subscriber.length > 0
|
|
935
|
+
? args.subscriber
|
|
936
|
+
: undefined;
|
|
937
|
+
const result = this.eventStream.pull(maxEvents, subscriber);
|
|
681
938
|
return {
|
|
682
939
|
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
683
940
|
isError: false,
|
|
@@ -1329,12 +1586,14 @@ export class ToolRouter {
|
|
|
1329
1586
|
// fixable compile failure. We reuse the same offline log read +
|
|
1330
1587
|
// health-issue extractor as read_compile_errors so the diagnosis is
|
|
1331
1588
|
// identical to what the agent already saw.
|
|
1332
|
-
const logPath = resolveEditorLogPath(this.projectPath);
|
|
1589
|
+
const logPath = resolveEditorLogPath(this.projectPath, undefined, this.resolveLiveEditorPid(), this.globalLogPathOverride).path;
|
|
1333
1590
|
const tail = readLogTail(logPath, DEFAULT_LOG_TAIL_BYTES);
|
|
1334
1591
|
let signaturePresent = false;
|
|
1592
|
+
let signatureSource = null;
|
|
1335
1593
|
let logWarning = null;
|
|
1336
1594
|
if (tail.error || !tail.exists) {
|
|
1337
|
-
// Without the log we cannot confirm the signature
|
|
1595
|
+
// Without the log we cannot confirm the signature from the file. The
|
|
1596
|
+
// live-console fallback below may still recover it.
|
|
1338
1597
|
logWarning = tail.error
|
|
1339
1598
|
? `Editor.log unreadable at ${logPath}: ${tail.error}`
|
|
1340
1599
|
: `No Editor.log found at ${logPath}.`;
|
|
@@ -1342,7 +1601,10 @@ export class ToolRouter {
|
|
|
1342
1601
|
else {
|
|
1343
1602
|
const issues = extractProjectHealthIssues(tail.content);
|
|
1344
1603
|
signaturePresent = issues.some((i) => i.kind === "editor_fd_exhaustion");
|
|
1345
|
-
if (
|
|
1604
|
+
if (signaturePresent) {
|
|
1605
|
+
signatureSource = "editor_log";
|
|
1606
|
+
}
|
|
1607
|
+
else {
|
|
1346
1608
|
logWarning =
|
|
1347
1609
|
"The editor_fd_exhaustion signature is NOT present in the recent " +
|
|
1348
1610
|
"Editor.log tail. The Editor may be slow/stuck for a different " +
|
|
@@ -1351,34 +1613,87 @@ export class ToolRouter {
|
|
|
1351
1613
|
"the diagnosis before killing the Editor.";
|
|
1352
1614
|
}
|
|
1353
1615
|
}
|
|
1354
|
-
//
|
|
1355
|
-
//
|
|
1356
|
-
//
|
|
1357
|
-
//
|
|
1616
|
+
// feedback-fable-04-08 §2b — live-console signature fallback. When the log
|
|
1617
|
+
// file did not carry the signature (it may be the wrong/stale log for the
|
|
1618
|
+
// running session, or the bridge is still up and the Bee exception is only
|
|
1619
|
+
// in the live console via LogEntries), probe the bridge's console read as a
|
|
1620
|
+
// secondary signature source. The fd-exhaustion exception is raised into
|
|
1621
|
+
// the live Unity console independently of whichever Editor.log file the
|
|
1622
|
+
// resolver picked, so this catches the case the file read missed. The
|
|
1623
|
+
// bridge is usually down by full fd-exhaustion, so this is opportunistic —
|
|
1624
|
+
// a failure here leaves the file-based verdict standing.
|
|
1625
|
+
if (!signaturePresent) {
|
|
1626
|
+
try {
|
|
1627
|
+
const liveAvailable = await live.isLiveAvailable();
|
|
1628
|
+
if (liveAvailable) {
|
|
1629
|
+
const consoleResult = await live.route("unity_senses_read_console", { max_entries: 200, type: "error", detail: "verbose" });
|
|
1630
|
+
const consoleBody = parseResultBody(consoleResult);
|
|
1631
|
+
const text = this.serializeConsoleEntries(consoleBody);
|
|
1632
|
+
if (text) {
|
|
1633
|
+
const liveIssues = extractProjectHealthIssues(text);
|
|
1634
|
+
if (liveIssues.some((i) => i.kind === "editor_fd_exhaustion")) {
|
|
1635
|
+
signaturePresent = true;
|
|
1636
|
+
signatureSource = "live_console";
|
|
1637
|
+
logWarning = null;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
catch {
|
|
1643
|
+
// Bridge unreachable (expected on fd-exhaustion). The file-based
|
|
1644
|
+
// verdict above is the authoritative fallback.
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
// 2. Resolve the live Unity PID. feedback #2 (2026-08-06): the instance
|
|
1648
|
+
// lock's `pid` is authoritative — the bridge only writes it from the
|
|
1649
|
+
// main interactive editor (it early-returns in worker/batch processes),
|
|
1650
|
+
// while the process scan could return an AssetImportWorker child that
|
|
1651
|
+
// shares the project path. Prefer the lock PID when it is live; fall
|
|
1652
|
+
// back to the scan (which now also excludes AssetImportWorker children)
|
|
1653
|
+
// only when no live lock PID exists.
|
|
1358
1654
|
let pid = null;
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1655
|
+
let pidSource = null;
|
|
1656
|
+
const lockPid = this.resolveLiveEditorPid();
|
|
1657
|
+
if (lockPid !== undefined) {
|
|
1658
|
+
pid = lockPid;
|
|
1659
|
+
pidSource = "instance_lock";
|
|
1362
1660
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1661
|
+
else {
|
|
1662
|
+
try {
|
|
1663
|
+
const proc = findUnityForProject(this.projectPath);
|
|
1664
|
+
if (proc) {
|
|
1665
|
+
pid = proc.pid;
|
|
1666
|
+
pidSource = "process_scan";
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
catch {
|
|
1670
|
+
pid = null;
|
|
1671
|
+
}
|
|
1365
1672
|
}
|
|
1366
1673
|
// 3. Dry-run branch: confirm was false/absent. Return the diagnosis +
|
|
1367
1674
|
// the PID the tool WOULD kill, with no side effect. Lets an agent
|
|
1368
|
-
// preview before committing.
|
|
1675
|
+
// preview before committing. feedback #2: include the PID source and
|
|
1676
|
+
// the resolved process's command line so a misattribution (e.g. an
|
|
1677
|
+
// AssetImportWorker child) is visible before confirm.
|
|
1369
1678
|
if (!confirm) {
|
|
1679
|
+
const processCommandLine = pid !== null ? readProcessCommandLine(pid) : null;
|
|
1370
1680
|
return sourceResult({
|
|
1371
1681
|
action: "restart_editor",
|
|
1372
1682
|
confirm: false,
|
|
1373
1683
|
dryRun: true,
|
|
1374
1684
|
signaturePresent,
|
|
1685
|
+
...(signatureSource !== null ? { signatureSource } : {}),
|
|
1375
1686
|
...(pid !== null ? { wouldKillPid: pid } : {}),
|
|
1687
|
+
...(pidSource !== null ? { pidSource } : {}),
|
|
1688
|
+
...(processCommandLine !== null ? { processCommandLine } : {}),
|
|
1376
1689
|
...(logWarning !== null ? { warning: logWarning } : {}),
|
|
1377
1690
|
projectPath: this.projectPath,
|
|
1378
1691
|
message: "Dry-run preview. Pass confirm: true to actually terminate the " +
|
|
1379
1692
|
"hung Unity Editor. Only do so after read_compile_errors " +
|
|
1380
1693
|
"confirms an editor_fd_exhaustion issue — killing the Editor " +
|
|
1381
|
-
"can destroy unsaved scene work and in-flight asset imports."
|
|
1694
|
+
"can destroy unsaved scene work and in-flight asset imports. " +
|
|
1695
|
+
"Verify processCommandLine targets the main editor, not an " +
|
|
1696
|
+
"AssetImportWorker child.",
|
|
1382
1697
|
}, "local");
|
|
1383
1698
|
}
|
|
1384
1699
|
// 4. Confirmed-path refusals. Each guard surfaces a structured code so
|
|
@@ -1434,10 +1749,10 @@ export class ToolRouter {
|
|
|
1434
1749
|
}
|
|
1435
1750
|
// 6. Kill the Editor.
|
|
1436
1751
|
const kill = await killEditorProcess(pid, graceMs);
|
|
1437
|
-
return this.restartEditorKillResponse(pid, graceMs, kill, dirtyScenesWarning);
|
|
1752
|
+
return this.restartEditorKillResponse(pid, graceMs, kill, dirtyScenesWarning, signatureSource);
|
|
1438
1753
|
}
|
|
1439
1754
|
/** Build the confirmed-kill response from a {@link KillResult}. */
|
|
1440
|
-
restartEditorKillResponse(pid, graceMs, kill, dirtyScenesWarning) {
|
|
1755
|
+
restartEditorKillResponse(pid, graceMs, kill, dirtyScenesWarning, signatureSource) {
|
|
1441
1756
|
if (kill.terminated) {
|
|
1442
1757
|
return sourceResult({
|
|
1443
1758
|
action: "restart_editor",
|
|
@@ -1447,6 +1762,7 @@ export class ToolRouter {
|
|
|
1447
1762
|
method: kill.method,
|
|
1448
1763
|
elapsedMs: kill.elapsedMs,
|
|
1449
1764
|
graceMs,
|
|
1765
|
+
...(signatureSource !== null ? { signatureSource } : {}),
|
|
1450
1766
|
...(dirtyScenesWarning !== null
|
|
1451
1767
|
? {
|
|
1452
1768
|
dirtyScenesWarning,
|
|
@@ -1491,6 +1807,33 @@ export class ToolRouter {
|
|
|
1491
1807
|
],
|
|
1492
1808
|
}, "local", true);
|
|
1493
1809
|
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Flatten a unity_senses_read_console result body into a single string the
|
|
1812
|
+
* health-issue extractor can scan for the fd-exhaustion signature. The
|
|
1813
|
+
* console result is a JSON object with an `entries[]` array of
|
|
1814
|
+
* `{ message, stack, ... }`; we join message + stack so the Bee driver's
|
|
1815
|
+
* `Could not register to wait for file descriptor N` frame (which lives in
|
|
1816
|
+
* the stack trace) is reachable by {@link extractProjectHealthIssues}.
|
|
1817
|
+
* Returns "" when the body has no entries.
|
|
1818
|
+
*/
|
|
1819
|
+
serializeConsoleEntries(body) {
|
|
1820
|
+
if (!body || typeof body !== "object")
|
|
1821
|
+
return "";
|
|
1822
|
+
const entries = body.entries;
|
|
1823
|
+
if (!Array.isArray(entries))
|
|
1824
|
+
return "";
|
|
1825
|
+
const parts = [];
|
|
1826
|
+
for (const ent of entries) {
|
|
1827
|
+
if (!ent || typeof ent !== "object")
|
|
1828
|
+
continue;
|
|
1829
|
+
const e = ent;
|
|
1830
|
+
if (typeof e.message === "string")
|
|
1831
|
+
parts.push(e.message);
|
|
1832
|
+
if (typeof e.stack === "string")
|
|
1833
|
+
parts.push(e.stack);
|
|
1834
|
+
}
|
|
1835
|
+
return parts.join("\n");
|
|
1836
|
+
}
|
|
1494
1837
|
/**
|
|
1495
1838
|
* T31.4 — sample the live Unity process's file-descriptor usage and report
|
|
1496
1839
|
* headroom against Mono's ~1024 fd ceiling (the real trip point for the Bee
|
|
@@ -1504,20 +1847,27 @@ export class ToolRouter {
|
|
|
1504
1847
|
* calls build a trend (monotonic climb = leak in progress). No disk cache.
|
|
1505
1848
|
*/
|
|
1506
1849
|
async routeResourcePressure(args) {
|
|
1507
|
-
// 1. Resolve the PID. An explicit pid arg wins; otherwise
|
|
1508
|
-
//
|
|
1509
|
-
//
|
|
1850
|
+
// 1. Resolve the PID. An explicit pid arg wins; otherwise prefer the
|
|
1851
|
+
// instance-lock PID (authoritative main editor — feedback #2) and fall
|
|
1852
|
+
// back to the process scan (which excludes AssetImportWorker children).
|
|
1853
|
+
// The probe must NOT depend on the bridge.
|
|
1510
1854
|
let pid = null;
|
|
1511
1855
|
if (typeof args.pid === "number" && Number.isInteger(args.pid) && args.pid > 0) {
|
|
1512
1856
|
pid = args.pid;
|
|
1513
1857
|
}
|
|
1514
1858
|
else {
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
pid =
|
|
1859
|
+
const lockPid = this.resolveLiveEditorPid();
|
|
1860
|
+
if (lockPid !== undefined) {
|
|
1861
|
+
pid = lockPid;
|
|
1518
1862
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1863
|
+
else {
|
|
1864
|
+
try {
|
|
1865
|
+
const proc = findUnityForProject(this.projectPath);
|
|
1866
|
+
pid = proc ? proc.pid : null;
|
|
1867
|
+
}
|
|
1868
|
+
catch {
|
|
1869
|
+
pid = null;
|
|
1870
|
+
}
|
|
1521
1871
|
}
|
|
1522
1872
|
}
|
|
1523
1873
|
if (pid === null) {
|
|
@@ -1532,25 +1882,42 @@ export class ToolRouter {
|
|
|
1532
1882
|
},
|
|
1533
1883
|
}, "local", true);
|
|
1534
1884
|
}
|
|
1535
|
-
// 2.
|
|
1885
|
+
// 2. Resolve the configured fd ceiling (default 1024, overridable per
|
|
1886
|
+
// project via `.unity-open-mcp/settings.json` for runtimes whose
|
|
1887
|
+
// internal limit differs from Mono's classic 1024). Read fresh —
|
|
1888
|
+
// resource_pressure is called rarely, and this lets an operator change
|
|
1889
|
+
// the setting without restarting the MCP server.
|
|
1890
|
+
const { ceiling, source: ceilingSource } = readFdCeiling(this.projectPath);
|
|
1891
|
+
// 3. Probe + record the sample. A failed probe still records a sample
|
|
1536
1892
|
// (count: null) so the trend detector sees the gap and does not
|
|
1537
1893
|
// interpolate across it.
|
|
1538
1894
|
const probe = countFileDescriptors(pid);
|
|
1539
1895
|
const count = probe.count;
|
|
1540
|
-
const
|
|
1896
|
+
const approximate = "approximate" in probe ? probe.approximate : false;
|
|
1897
|
+
const headroom = computeFdHeadroom(count, approximate, ceiling);
|
|
1541
1898
|
const ts = Date.now();
|
|
1542
1899
|
this.sessionState.recordFdSample({ ts, pid, count });
|
|
1543
1900
|
const samples = this.sessionState.fdSamplesSnapshot();
|
|
1544
|
-
const trend = analyzeFdTrend(samples);
|
|
1545
|
-
//
|
|
1546
|
-
//
|
|
1547
|
-
//
|
|
1548
|
-
//
|
|
1901
|
+
const trend = analyzeFdTrend(samples, ceiling);
|
|
1902
|
+
// 4. Decide whether to raise an operator warning. For an fd-heavy project
|
|
1903
|
+
// the OS-level count legitimately sits OVER the Mono-ceiling proxy on a
|
|
1904
|
+
// fresh healthy process (lsof counts mmap'd assets / file watchers /
|
|
1905
|
+
// FileStream handles that Mono's IOSelector never registers), so an
|
|
1906
|
+
// absolute `over_ceiling` state is NOT an alarm by itself. Alarm only on:
|
|
1907
|
+
// - a monotonic leak (the actionable trend signal), or
|
|
1908
|
+
// - approaching the ceiling from below (warn/critical).
|
|
1909
|
+
// A merely `rising` or stable/no-history over-ceiling process gets a
|
|
1910
|
+
// `pressureNote` instead. `rising` is any net-positive delta that is NOT a
|
|
1911
|
+
// qualifying leak — it fires on ordinary drift, so it must NOT alarm on its
|
|
1912
|
+
// own (the prior over_ceiling+rising alarm re-cried wolf on every sample
|
|
1913
|
+
// after the first).
|
|
1549
1914
|
const fdMethod = probe.method;
|
|
1550
|
-
const approximate = "approximate" in probe ? probe.approximate : false;
|
|
1551
1915
|
const probeReason = "reason" in probe ? probe.reason : null;
|
|
1552
1916
|
const probeMessage = "message" in probe ? probe.message : null;
|
|
1553
|
-
const
|
|
1917
|
+
const shouldWarn = trend.state === "leaking" ||
|
|
1918
|
+
headroom.state === "warn" ||
|
|
1919
|
+
headroom.state === "critical";
|
|
1920
|
+
const warningState = shouldWarn
|
|
1554
1921
|
? {
|
|
1555
1922
|
level: headroom.state === "critical"
|
|
1556
1923
|
? "critical"
|
|
@@ -1559,26 +1926,44 @@ export class ToolRouter {
|
|
|
1559
1926
|
: "warn",
|
|
1560
1927
|
message: headroom.state === "critical"
|
|
1561
1928
|
? `Editor fd usage is at ${Math.round(headroom.pressureRatio * 100)}% of ` +
|
|
1562
|
-
`
|
|
1929
|
+
`the ${ceiling}-descriptor ceiling — the next domain reload is ` +
|
|
1563
1930
|
`likely to trip the Bee build-driver hang. Save scene work and restart ` +
|
|
1564
1931
|
`Unity via the Hub now, before the Editor hangs.`
|
|
1565
1932
|
: trend.state === "leaking"
|
|
1566
|
-
?
|
|
1567
|
-
`
|
|
1568
|
-
|
|
1569
|
-
|
|
1933
|
+
? headroom.state === "over_ceiling"
|
|
1934
|
+
? `Editor fd usage (${count}) is ABOVE the ${ceiling}-descriptor ` +
|
|
1935
|
+
`ceiling and climbing monotonically (leak in progress): trend ` +
|
|
1936
|
+
`delta ${trend.delta} over ${trend.sampleCount} sample(s). The ` +
|
|
1937
|
+
`count has already crossed the ceiling proxy — save scene work ` +
|
|
1938
|
+
`and restart Unity via the Hub now.`
|
|
1939
|
+
: `Editor fd usage is climbing monotonically across samples (leak in ` +
|
|
1940
|
+
`progress): trend delta ${trend.delta} over ${trend.sampleCount} ` +
|
|
1941
|
+
`sample(s). Save scene work and plan a restart before the count ` +
|
|
1942
|
+
`crosses the ${ceiling}-descriptor ceiling.`
|
|
1570
1943
|
: `Editor fd usage is at ${Math.round(headroom.pressureRatio * 100)}% of ` +
|
|
1571
|
-
`
|
|
1572
|
-
`
|
|
1573
|
-
`
|
|
1944
|
+
`the ${ceiling}-descriptor ceiling. Monitor the trend; if it keeps ` +
|
|
1945
|
+
`climbing across domain reloads, save scene work and restart Unity ` +
|
|
1946
|
+
`via the Hub before the Editor hangs.`,
|
|
1574
1947
|
}
|
|
1575
1948
|
: null;
|
|
1949
|
+
// Informational (non-alarming) note for the over-ceiling-but-stable case.
|
|
1950
|
+
const pressureNote = !shouldWarn && headroom.state === "over_ceiling"
|
|
1951
|
+
? `OS-level fd count (${count}) is above the ${ceiling}-descriptor ceiling ` +
|
|
1952
|
+
`proxy, but lsof / HandleCount count ALL OS file descriptors (mmap'd ` +
|
|
1953
|
+
`assets, file watchers, regular FileStream handles) while only ` +
|
|
1954
|
+
`Mono-IOSelector-registered descriptors (sockets/pipes under async IO) ` +
|
|
1955
|
+
`count toward the real trip point. For an asset-heavy project this is ` +
|
|
1956
|
+
`expected on a healthy process. Treat this as a concern ONLY if the trend ` +
|
|
1957
|
+
`becomes a monotonic leak (leaking) — re-sample after the next domain ` +
|
|
1958
|
+
`reload to establish one.`
|
|
1959
|
+
: null;
|
|
1576
1960
|
return sourceResult({
|
|
1577
1961
|
pid,
|
|
1578
1962
|
fdCount: count,
|
|
1579
1963
|
fdMethod,
|
|
1580
1964
|
approximate,
|
|
1581
|
-
ceiling
|
|
1965
|
+
ceiling,
|
|
1966
|
+
ceilingSource,
|
|
1582
1967
|
headroom: headroom.headroom,
|
|
1583
1968
|
pressureRatio: headroom.pressureRatio,
|
|
1584
1969
|
state: headroom.state,
|
|
@@ -1609,6 +1994,7 @@ export class ToolRouter {
|
|
|
1609
1994
|
],
|
|
1610
1995
|
}
|
|
1611
1996
|
: {}),
|
|
1997
|
+
...(pressureNote !== null ? { pressureNote } : {}),
|
|
1612
1998
|
}, "local");
|
|
1613
1999
|
}
|
|
1614
2000
|
// ── M26 Plan 2 — Unity Hub control (local-routed) ────────────────
|
|
@@ -1899,6 +2285,9 @@ export class ToolRouter {
|
|
|
1899
2285
|
routeMeta = { route: "batch", fallbackReason: "live_unavailable" };
|
|
1900
2286
|
}
|
|
1901
2287
|
}
|
|
2288
|
+
// feedback-fable-04-08 §7 — rewrite the generic live_unavailable to
|
|
2289
|
+
// editor_busy when the batch hit a live editor's project lock.
|
|
2290
|
+
routeMeta = this.rewriteEditorBusy(raw, routeMeta);
|
|
1902
2291
|
const body = parseResultBody(raw);
|
|
1903
2292
|
if (body === null) {
|
|
1904
2293
|
// Body not parseable — fall back to the legacy injectRouteMeta path so
|
|
@@ -1954,7 +2343,14 @@ export class ToolRouter {
|
|
|
1954
2343
|
}
|
|
1955
2344
|
else {
|
|
1956
2345
|
raw = await this.batch.route("unity_open_mcp_scene_get_data", bridgeArgs);
|
|
1957
|
-
|
|
2346
|
+
// feedback-fable-04-08 §7 — rewrite to editor_busy when the batch hit
|
|
2347
|
+
// a live editor's project lock. Compute the meta from the raw result
|
|
2348
|
+
// (which carries the editor_instance_locked error) before stamping.
|
|
2349
|
+
const meta = this.rewriteEditorBusy(raw, {
|
|
2350
|
+
route: "batch",
|
|
2351
|
+
fallbackReason: "live_unavailable",
|
|
2352
|
+
});
|
|
2353
|
+
raw = injectRouteMeta(raw, meta);
|
|
1958
2354
|
}
|
|
1959
2355
|
}
|
|
1960
2356
|
if (!wantPaging)
|
|
@@ -2005,12 +2401,25 @@ function pageSceneNodes(body, pageSize, cursor) {
|
|
|
2005
2401
|
void roots;
|
|
2006
2402
|
void rootGameObjects;
|
|
2007
2403
|
void nodes;
|
|
2008
|
-
|
|
2404
|
+
// `truncated` is read at the source-truncated carry-through below, so it is
|
|
2405
|
+
// not voided with the other intentionally-discarded scene fields.
|
|
2009
2406
|
void moreHidden;
|
|
2010
2407
|
void _existing;
|
|
2011
2408
|
// Pull the top-level meta (everything except `scene`, which we rebuild).
|
|
2012
|
-
|
|
2409
|
+
// B-N21 — strip the structural fields from the TOP level too. For a
|
|
2410
|
+
// pre-unwrapped body (roots/rootGameObjects at the top level, no `scene`
|
|
2411
|
+
// wrapper — the fallback flattenSceneBody handles), `scene` above resolves
|
|
2412
|
+
// to {} and strips nothing, so spreading the raw remainder of `body` used
|
|
2413
|
+
// to carry the ENTIRE unpaged roots array into the response alongside the
|
|
2414
|
+
// page — paging never bounded the payload for that shape. flattenSceneBody
|
|
2415
|
+
// already consumed those fields; drop them wherever they live.
|
|
2416
|
+
const { scene: _s, roots: topRoots, rootGameObjects: topRootGameObjects, nodes: topNodes, truncated: topTruncated, moreHidden: topMoreHidden, pagination: _topExisting, ...topMeta } = body;
|
|
2013
2417
|
void _s;
|
|
2418
|
+
void topRoots;
|
|
2419
|
+
void topRootGameObjects;
|
|
2420
|
+
void topNodes;
|
|
2421
|
+
void topMoreHidden;
|
|
2422
|
+
void _topExisting;
|
|
2014
2423
|
const result = {
|
|
2015
2424
|
...topMeta,
|
|
2016
2425
|
status: body.status ?? "ok",
|
|
@@ -2023,8 +2432,10 @@ function pageSceneNodes(body, pageSize, cursor) {
|
|
|
2023
2432
|
// Carry the bridge's source-side truncation count through to the page so an
|
|
2024
2433
|
// agent can distinguish "more pages" (pagination.truncated) from "the bridge
|
|
2025
2434
|
// capped the hierarchy" (scene.truncated). Both can be non-zero at once.
|
|
2026
|
-
|
|
2027
|
-
|
|
2435
|
+
// For a pre-unwrapped body the count lives at the top level (B-N21).
|
|
2436
|
+
const sourceTruncated = typeof truncated === "number" ? truncated : topTruncated;
|
|
2437
|
+
if (typeof sourceTruncated === "number" && sourceTruncated > 0) {
|
|
2438
|
+
result.scene.truncated = sourceTruncated;
|
|
2028
2439
|
}
|
|
2029
2440
|
return attachPagination(result, block);
|
|
2030
2441
|
}
|