unity-open-mcp 0.9.0 → 1.1.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 +27 -0
- package/dist/batch-spawn.js +218 -24
- package/dist/batch-spawn.js.map +1 -1
- package/dist/capabilities/build-capabilities.d.ts +10 -0
- package/dist/capabilities/build-capabilities.js +4 -0
- package/dist/capabilities/build-capabilities.js.map +1 -1
- package/dist/capabilities/capabilities-view.d.ts +48 -0
- package/dist/capabilities/capabilities-view.js +93 -0
- package/dist/capabilities/capabilities-view.js.map +1 -0
- package/dist/capabilities/intent-groups.js +42 -0
- package/dist/capabilities/intent-groups.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 +37 -2
- package/dist/capabilities/tool-groups.js.map +1 -1
- package/dist/child-supervision.d.ts +34 -0
- package/dist/child-supervision.js +110 -0
- package/dist/child-supervision.js.map +1 -0
- package/dist/cli/cli.js +9 -0
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/commands.js +37 -4
- package/dist/cli/commands.js.map +1 -1
- package/dist/compressible-router.js +12 -1
- package/dist/compressible-router.js.map +1 -1
- package/dist/constants.d.ts +31 -1
- package/dist/constants.js +31 -1
- package/dist/constants.js.map +1 -1
- package/dist/dialog-dismiss.d.ts +11 -0
- package/dist/dialog-dismiss.js +59 -4
- package/dist/dialog-dismiss.js.map +1 -1
- package/dist/editor-wedge.d.ts +45 -0
- package/dist/editor-wedge.js +111 -0
- package/dist/editor-wedge.js.map +1 -0
- package/dist/event-stream.d.ts +19 -6
- package/dist/event-stream.js +139 -17
- package/dist/event-stream.js.map +1 -1
- package/dist/gate-error.d.ts +2 -0
- package/dist/gate-error.js +9 -0
- package/dist/gate-error.js.map +1 -1
- package/dist/instance-discovery.d.ts +23 -0
- package/dist/instance-discovery.js +47 -1
- package/dist/instance-discovery.js.map +1 -1
- package/dist/live-client.d.ts +40 -5
- package/dist/live-client.js +254 -36
- 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 +101 -19
- package/dist/process-diagnostics.js +197 -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/results.d.ts +8 -0
- package/dist/results.js +4 -1
- package/dist/results.js.map +1 -1
- package/dist/routers.js +4 -1
- package/dist/routers.js.map +1 -1
- 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/server.js +8 -2
- package/dist/server.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-hint.d.ts +36 -0
- package/dist/tool-hint.js +98 -0
- package/dist/tool-hint.js.map +1 -0
- package/dist/tool-router.d.ts +47 -2
- package/dist/tool-router.js +652 -71
- package/dist/tool-router.js.map +1 -1
- package/dist/tools/agent-capabilities.js +27 -1
- package/dist/tools/agent-capabilities.js.map +1 -1
- package/dist/tools/baseline-create.js +1 -1
- package/dist/tools/baseline-create.js.map +1 -1
- package/dist/tools/batch-execute.js +12 -1
- package/dist/tools/batch-execute.js.map +1 -1
- package/dist/tools/bridge-status.js +21 -1
- package/dist/tools/bridge-status.js.map +1 -1
- package/dist/tools/capture-inline.js +4 -3
- package/dist/tools/capture-inline.js.map +1 -1
- package/dist/tools/editor-status.js +7 -2
- package/dist/tools/editor-status.js.map +1 -1
- package/dist/tools/execute-csharp.js +36 -3
- package/dist/tools/execute-csharp.js.map +1 -1
- package/dist/tools/execute-menu.js +33 -2
- package/dist/tools/execute-menu.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +44 -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/invoke-method.js +15 -1
- package/dist/tools/invoke-method.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/pull-events.js +18 -10
- package/dist/tools/pull-events.js.map +1 -1
- package/dist/tools/read-compile-errors.js +36 -7
- package/dist/tools/read-compile-errors.js.map +1 -1
- package/dist/tools/regression-check.js +2 -1
- package/dist/tools/regression-check.js.map +1 -1
- package/dist/tools/resource-pressure.js +21 -10
- package/dist/tools/resource-pressure.js.map +1 -1
- package/dist/tools/run-tests.js +11 -2
- package/dist/tools/run-tests.js.map +1 -1
- package/dist/tools/scan-all.js +2 -2
- package/dist/tools/scan-all.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/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 +67 -2
- package/dist/unity-log.js +206 -8
- package/dist/unity-log.js.map +1 -1
- package/package.json +1 -1
package/dist/tool-router.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
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, detectStaleAssembly } from "./unity-log.js";
|
|
3
|
+
import { resolveEditorLogPath, readLogTail, DEFAULT_LOG_TAIL_BYTES, detectStaleLog, detectStaleAssembly, detectErrorsPredatingEdits, parseLogAuthorship, compareLogAuthorship } from "./unity-log.js";
|
|
4
|
+
import { toolHintReference } from "./tool-hint.js";
|
|
5
|
+
import { scanForFdExhaustion } from "./editor-wedge.js";
|
|
4
6
|
import { summarizeProjectHealth, extractProjectHealthIssues } from "./project-health.js";
|
|
7
|
+
import { countProjectAsmdefs, countAssembliesWithErrors } from "./asmdef-discovery.js";
|
|
5
8
|
import { buildCapabilities } from "./capabilities/build-capabilities.js";
|
|
6
9
|
import { RULE_CATALOG, FIX_CATALOG } from "./capabilities/rule-catalog.js";
|
|
7
10
|
import { GROUP_IDS, TOOL_GROUPS, AUTO_ACTIVATE_GROUPS, groupToTools, getGroup, } from "./capabilities/tool-groups.js";
|
|
8
11
|
import { recommendGroups, } from "./capabilities/intent-groups.js";
|
|
9
12
|
import { listRules } from "./capabilities/list-rules.js";
|
|
13
|
+
import { viewCapabilities } from "./capabilities/capabilities-view.js";
|
|
10
14
|
import { generateSkill } from "./skill/generate-skill.js";
|
|
11
15
|
import { getKnownClientKeys } from "./skill/client-paths.js";
|
|
12
16
|
import { ALL_TOOLS } from "./tools/index.js";
|
|
13
17
|
import { lockPath, readInstanceLock, classifyInstance, isPidAlive } from "./instance-discovery.js";
|
|
14
|
-
import { PORT_ENV_VAR } from "./constants.js";
|
|
15
|
-
import { findUnityForProject } from "./running-unity.js";
|
|
18
|
+
import { PORT_ENV_VAR, EXPECTED_BRIDGE_WIRE_CONTRACT } from "./constants.js";
|
|
19
|
+
import { findUnityForProject, readProcessCommandLine } from "./running-unity.js";
|
|
16
20
|
// M31-optimizations Plan 1 / L14 — opt-in route-logging gate. Resolved once at
|
|
17
21
|
// module load into a boolean; the per-dispatch log sites read the boolean
|
|
18
22
|
// (not process.env on every call) so the hot path stays a single branch.
|
|
@@ -38,7 +42,8 @@ export function logRoute(message) {
|
|
|
38
42
|
// (resource_pressure). Both act on the OS process, not the bridge, so they
|
|
39
43
|
// share the local route classification with bridge_status.
|
|
40
44
|
import { killEditorProcess, DEFAULT_KILL_GRACE_MS, MAX_KILL_WAIT_MS, } from "./editor-process-control.js";
|
|
41
|
-
import { countFileDescriptors, computeFdHeadroom, analyzeFdTrend, LAUNCH_CONTEXT_CAVEAT,
|
|
45
|
+
import { countFileDescriptors, computeFdHeadroom, analyzeFdTrend, LAUNCH_CONTEXT_CAVEAT, } from "./process-diagnostics.js";
|
|
46
|
+
import { readFdCeiling } from "./project-settings.js";
|
|
42
47
|
import { listInstalledEditors, fetchAvailableReleases, openInstallDeepLink, getInstallPath, setInstallPath, } from "./hub-control.js";
|
|
43
48
|
/** Default Hub-control backend — calls the real hub-control functions. */
|
|
44
49
|
const defaultHubBackend = {
|
|
@@ -49,7 +54,7 @@ const defaultHubBackend = {
|
|
|
49
54
|
setInstallPath: (path) => setInstallPath(path),
|
|
50
55
|
};
|
|
51
56
|
import { BATCH_TOOL_NAMES, ALWAYS_BATCH_TOOLS } from "./batch-spawn.js";
|
|
52
|
-
import { readProfileAndDetail, applyPaging, attachPagination, foldVerifyResult, parseResultBody, withResultBody, } from "./output-profile.js";
|
|
57
|
+
import { readProfileAndDetail, applyPaging, attachPagination, foldVerifyResult, parseResultBody, withResultBody, isOutputProfile, } from "./output-profile.js";
|
|
53
58
|
/**
|
|
54
59
|
* Single source of truth for the `_source` tag. Every route-tagging site goes
|
|
55
60
|
* through one of the two helpers below so live / offline / local stay
|
|
@@ -337,7 +342,25 @@ function groupLiveRefsByKind(paths) {
|
|
|
337
342
|
}
|
|
338
343
|
// Operator-facing "what to do next" hint for each coarse status. Kept short
|
|
339
344
|
// and action-oriented so the Validation Suite can render it inline.
|
|
340
|
-
function bridgeStatusNextStep(status) {
|
|
345
|
+
function bridgeStatusNextStep(status, wedge) {
|
|
346
|
+
if (status === "wedged") {
|
|
347
|
+
return wedge?.reason === "main_thread_wedged"
|
|
348
|
+
? "Unity's main thread is wedged — the HTTP listener still answers /ping, but the " +
|
|
349
|
+
"editor heartbeat has stopped, which is the signature of a MODAL DIALOG blocking " +
|
|
350
|
+
"the message pump (material/scene converters and several ExecuteMenuItem paths " +
|
|
351
|
+
"open one). This is NOT a compile failure and NOT a dead bridge assembly — a dead " +
|
|
352
|
+
"assembly has no listener at all. It cannot self-heal and no tool can dismiss the " +
|
|
353
|
+
"dialog: an operator must dismiss it in the Unity UI, after which queued calls " +
|
|
354
|
+
"resume. Prefer non-interactive APIs over unity_open_mcp_execute_menu for " +
|
|
355
|
+
"converter-style menus."
|
|
356
|
+
: "The Unity Editor hit the Bee build driver's file-descriptor exhaustion — the " +
|
|
357
|
+
"process is alive and the listener answers, but it can no longer BUILD: " +
|
|
358
|
+
"Library/ScriptAssemblies stops updating, so C# edits never take effect and " +
|
|
359
|
+
"unity_open_mcp_execute_csharp silently runs the previous assembly. Do not trust " +
|
|
360
|
+
"any result computed since. Call unity_open_mcp_read_compile_errors to confirm " +
|
|
361
|
+
"the editor_fd_exhaustion issue, save scene work, then restart the Editor " +
|
|
362
|
+
"(unity_open_mcp_restart_editor with confirm: true, or manually).";
|
|
363
|
+
}
|
|
341
364
|
switch (status) {
|
|
342
365
|
case "running":
|
|
343
366
|
return "Bridge is ready. Proceed with live-only MCP tools.";
|
|
@@ -369,7 +392,66 @@ function bridgeStatusNextStep(status) {
|
|
|
369
392
|
"Bridge) and confirm it is started.";
|
|
370
393
|
}
|
|
371
394
|
}
|
|
372
|
-
|
|
395
|
+
/**
|
|
396
|
+
* specs/feedback.md 2026-08-24 — wire-contract staleness block.
|
|
397
|
+
*
|
|
398
|
+
* The field report: a blocker fixed and shipped on 2026-08-17 (registry tools
|
|
399
|
+
* rejecting the transport envelope keys, which made `editor_status` uncallable
|
|
400
|
+
* with default args) recurred against an installed bridge that still reported
|
|
401
|
+
* `bridgeVersion "1.0.0"` — the same string the FIXED source reports, because
|
|
402
|
+
* the package semver does not move for a wire-contract fix. "Your install is
|
|
403
|
+
* stale" and "this regressed" were therefore indistinguishable at the call
|
|
404
|
+
* site, and the agent had no way to pick between reinstalling and filing a bug.
|
|
405
|
+
*
|
|
406
|
+
* `BridgeSession.WireContract` is bumped for every observable change to the
|
|
407
|
+
* request/response contract, so comparing it against
|
|
408
|
+
* {@link EXPECTED_BRIDGE_WIRE_CONTRACT} answers that question directly. A
|
|
409
|
+
* bridge that predates the field reports no `wireContract` at all — `bridge`
|
|
410
|
+
* is then null and `stale` is true, which is the correct reading (it is older
|
|
411
|
+
* than revision 1 by definition).
|
|
412
|
+
*
|
|
413
|
+
* Returns null when /ping was unreachable: there is no bridge to compare, and a
|
|
414
|
+
* bogus "stale" on an offline bridge would be worse than silence.
|
|
415
|
+
*/
|
|
416
|
+
function wireContractBlock(reachable, reported) {
|
|
417
|
+
if (!reachable)
|
|
418
|
+
return null;
|
|
419
|
+
const bridge = typeof reported === "number" ? reported : null;
|
|
420
|
+
const stale = bridge === null || bridge < EXPECTED_BRIDGE_WIRE_CONTRACT;
|
|
421
|
+
return {
|
|
422
|
+
bridge,
|
|
423
|
+
expected: EXPECTED_BRIDGE_WIRE_CONTRACT,
|
|
424
|
+
stale,
|
|
425
|
+
note: stale
|
|
426
|
+
? "The installed Unity bridge package is OLDER than this MCP server's wire " +
|
|
427
|
+
"contract" +
|
|
428
|
+
(bridge === null
|
|
429
|
+
? " (it does not report `wireContract` at all, so it predates revision 1)"
|
|
430
|
+
: ` (revision ${bridge} vs ${EXPECTED_BRIDGE_WIRE_CONTRACT})`) +
|
|
431
|
+
". A tool failing in a way the docs say is fixed is almost certainly " +
|
|
432
|
+
"this staleness, not a regression: reinstall/update the bridge package " +
|
|
433
|
+
"in Unity (Package Manager > the unity-open-mcp-bridge git URL) and " +
|
|
434
|
+
"retry before filing anything. bridgeVersion alone cannot tell you " +
|
|
435
|
+
"this — it does not move for a wire-contract fix."
|
|
436
|
+
: null,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function bridgeStatusRecoveryHint(status, wedge) {
|
|
440
|
+
if (status === "wedged") {
|
|
441
|
+
return wedge?.reason === "main_thread_wedged"
|
|
442
|
+
? {
|
|
443
|
+
tool: "unity_open_mcp_editor_status",
|
|
444
|
+
reason: "Main thread wedged (likely a modal dialog): /ping answers but the " +
|
|
445
|
+
"editor heartbeat stopped. No tool can dismiss a modal — an operator " +
|
|
446
|
+
"must close it in the Unity UI, then editor_status confirms recovery.",
|
|
447
|
+
}
|
|
448
|
+
: {
|
|
449
|
+
tool: "unity_open_mcp_read_compile_errors",
|
|
450
|
+
reason: "editor_fd_exhaustion in the freshest Editor.log — the Editor answers " +
|
|
451
|
+
"HTTP but can no longer build, so C# edits never take effect. Confirm " +
|
|
452
|
+
"with read_compile_errors, then restart the Editor.",
|
|
453
|
+
};
|
|
454
|
+
}
|
|
373
455
|
if (status === "dead_bridge") {
|
|
374
456
|
return {
|
|
375
457
|
tool: "unity_open_mcp_read_compile_errors",
|
|
@@ -388,6 +470,7 @@ export class ToolRouter {
|
|
|
388
470
|
sessionState;
|
|
389
471
|
onToolListChanged;
|
|
390
472
|
hubBackend;
|
|
473
|
+
globalLogPathOverride;
|
|
391
474
|
modelCache = new AssetModelCache();
|
|
392
475
|
/**
|
|
393
476
|
* M28-refactoring Plan 3 (T3.2) — named-tool dispatch map. Replaces the
|
|
@@ -444,7 +527,15 @@ export class ToolRouter {
|
|
|
444
527
|
// M26 Plan 2 — injectable Hub-control backend. Omit in production (the
|
|
445
528
|
// default calls the real hub-control functions); tests pass a fake to
|
|
446
529
|
// avoid real subprocess + network side effects.
|
|
447
|
-
hubBackend = defaultHubBackend
|
|
530
|
+
hubBackend = defaultHubBackend,
|
|
531
|
+
// feedback-fable-04-08 §2b — injectable global Editor.log path for tests.
|
|
532
|
+
// Production leaves this undefined so resolveEditorLogPath computes the
|
|
533
|
+
// real platform global path; tests pass a path under their tmp sandbox so
|
|
534
|
+
// the freshness comparison does not consult the host machine's real
|
|
535
|
+
// ~/Library/Logs/Unity/Editor.log (which is newer than the artificial
|
|
536
|
+
// 1970-era mtimes the tests plant and would win the comparison, reading
|
|
537
|
+
// the wrong file).
|
|
538
|
+
globalLogPathOverride) {
|
|
448
539
|
this.live = live;
|
|
449
540
|
this.batch = batch;
|
|
450
541
|
this.projectPath = projectPath;
|
|
@@ -452,6 +543,7 @@ export class ToolRouter {
|
|
|
452
543
|
this.sessionState = sessionState;
|
|
453
544
|
this.onToolListChanged = onToolListChanged;
|
|
454
545
|
this.hubBackend = hubBackend;
|
|
546
|
+
this.globalLogPathOverride = globalLogPathOverride;
|
|
455
547
|
}
|
|
456
548
|
async route(toolName, args) {
|
|
457
549
|
return this.routeCore(this.live, toolName, args);
|
|
@@ -478,10 +570,21 @@ export class ToolRouter {
|
|
|
478
570
|
}
|
|
479
571
|
// Compact drill-down reads: offline-first for text-serialized assets, fall
|
|
480
572
|
// back to live bridge for binary formats. The compressible-router handles
|
|
481
|
-
// the source selection internally
|
|
573
|
+
// the source selection internally and tags the payload `_source`
|
|
574
|
+
// ("offline" when it was served from the disk parser without touching the
|
|
575
|
+
// bridge; error results tag their origin too — "local" for caller-side
|
|
576
|
+
// validation, "offline" for a failed offline-first attempt with the
|
|
577
|
+
// bridge down). Derive `_route` from that tag: stamping route:"live"
|
|
578
|
+
// unconditionally mislabeled offline-served reads as bridge responses —
|
|
579
|
+
// contradictory metadata that pointed an agent's diagnostics at the
|
|
580
|
+
// bridge for data it never produced.
|
|
482
581
|
if (isCompressible(toolName)) {
|
|
483
582
|
const result = await routeCompressible(toolName, args, live, this.modelCache, this.projectPath);
|
|
484
|
-
|
|
583
|
+
const source = parseResultBody(result)?._source;
|
|
584
|
+
const meta = source === "offline" || source === "local"
|
|
585
|
+
? { route: source }
|
|
586
|
+
: { route: "live" };
|
|
587
|
+
return injectRouteMeta(result, meta);
|
|
485
588
|
}
|
|
486
589
|
const canBatch = this.batch.isBatchTool(toolName);
|
|
487
590
|
const isPing = toolName === "unity_open_mcp_ping";
|
|
@@ -528,10 +631,13 @@ export class ToolRouter {
|
|
|
528
631
|
return this.batchPingResult();
|
|
529
632
|
}
|
|
530
633
|
const result = await this.batch.route(toolName, args);
|
|
531
|
-
|
|
634
|
+
// feedback-fable-04-08 §7 — when the batch fallback hit a live editor's
|
|
635
|
+
// project lock, the actionable signal is editor_busy, not live_unavailable.
|
|
636
|
+
const meta = this.rewriteEditorBusy(result, {
|
|
532
637
|
route: "batch",
|
|
533
638
|
fallbackReason: "live_unavailable",
|
|
534
639
|
});
|
|
640
|
+
return injectRouteMeta(result, meta);
|
|
535
641
|
}
|
|
536
642
|
batchPingResult() {
|
|
537
643
|
return {
|
|
@@ -546,6 +652,11 @@ export class ToolRouter {
|
|
|
546
652
|
mode: "batch",
|
|
547
653
|
compiling: false,
|
|
548
654
|
isPlaying: false,
|
|
655
|
+
// feedback 2026-08-17 — provenance marker distinguishing this
|
|
656
|
+
// synthesized fallback (no probe was possible; the live route was
|
|
657
|
+
// unavailable) from the live route's verified probe answer
|
|
658
|
+
// (source:"probe" + asOf).
|
|
659
|
+
source: "unprobed",
|
|
549
660
|
_route: {
|
|
550
661
|
route: "batch",
|
|
551
662
|
fallbackReason: "live_unavailable",
|
|
@@ -556,6 +667,52 @@ export class ToolRouter {
|
|
|
556
667
|
isError: false,
|
|
557
668
|
};
|
|
558
669
|
}
|
|
670
|
+
/**
|
|
671
|
+
* feedback-fable-04-08 §7 — when a `live_unavailable` batch fallback lands
|
|
672
|
+
* on `editor_instance_locked`, the message describes the BATCH SPAWNER's
|
|
673
|
+
* problem ("a live editor holds the project lock, so no headless spawn"),
|
|
674
|
+
* but the agent's actionable truth is "the editor is busy (TestRunner run /
|
|
675
|
+
* long import); retry later", not a lock conflict. Detect this case by
|
|
676
|
+
* inspecting the batch result for `error.code === "editor_instance_locked"`
|
|
677
|
+
* AND confirming a live PID still holds the project (a stale lock with no
|
|
678
|
+
* live PID is a genuine lock conflict — leave it as `live_unavailable`).
|
|
679
|
+
*
|
|
680
|
+
* Returns the rewritten meta (and the result unchanged — the lock error body
|
|
681
|
+
* is real and must stay intact) so the caller stamps it via injectRouteMeta.
|
|
682
|
+
* The live PID is surfaced as `editorPid` for agent correlation. Falls
|
|
683
|
+
* through unchanged when the code differs, no live PID exists, or the body
|
|
684
|
+
* is unparseable.
|
|
685
|
+
*/
|
|
686
|
+
rewriteEditorBusy(result, meta) {
|
|
687
|
+
if (meta.fallbackReason !== "live_unavailable")
|
|
688
|
+
return meta;
|
|
689
|
+
const body = parseResultBody(result);
|
|
690
|
+
if (body === null)
|
|
691
|
+
return meta;
|
|
692
|
+
const err = body.error;
|
|
693
|
+
const code = typeof err === "object" && err !== null
|
|
694
|
+
? err.code
|
|
695
|
+
: undefined;
|
|
696
|
+
if (code !== "editor_instance_locked")
|
|
697
|
+
return meta;
|
|
698
|
+
// Confirm a live editor actually holds the project. The batch pre-spawn
|
|
699
|
+
// lock check already read the lock, but a race (the editor quit between
|
|
700
|
+
// the batch check and here) or a corrupt lock must NOT be misreported as
|
|
701
|
+
// "busy" — fall through to the generic live_unavailable in that case.
|
|
702
|
+
try {
|
|
703
|
+
const lock = readInstanceLock(this.projectPath);
|
|
704
|
+
if (!lock?.pid || !isPidAlive(lock.pid))
|
|
705
|
+
return meta;
|
|
706
|
+
return {
|
|
707
|
+
route: "batch",
|
|
708
|
+
fallbackReason: "editor_busy",
|
|
709
|
+
editorPid: lock.pid,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
return meta;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
559
716
|
async routeReadCompileErrors(args) {
|
|
560
717
|
const tailBytes = typeof args.tail_bytes === "number" && args.tail_bytes >= 4096
|
|
561
718
|
? Math.min(Math.floor(args.tail_bytes), 1048576)
|
|
@@ -571,7 +728,7 @@ export class ToolRouter {
|
|
|
571
728
|
// editor keeps writing. Pass the live PID from the instance lock so the
|
|
572
729
|
// resolver can make the strong-signal decision.
|
|
573
730
|
const livePid = this.resolveLiveEditorPid();
|
|
574
|
-
const resolvedLog = resolveEditorLogPath(this.projectPath, undefined, livePid);
|
|
731
|
+
const resolvedLog = resolveEditorLogPath(this.projectPath, undefined, livePid, this.globalLogPathOverride);
|
|
575
732
|
const logPath = resolvedLog.path;
|
|
576
733
|
const tail = readLogTail(logPath, tailBytes);
|
|
577
734
|
if (tail.error) {
|
|
@@ -617,11 +774,6 @@ export class ToolRouter {
|
|
|
617
774
|
// backward compatibility) so existing callers do not break.
|
|
618
775
|
const health = summarizeProjectHealth(tail.content);
|
|
619
776
|
const errors = health.compilerErrors;
|
|
620
|
-
const status = health.unhealthy
|
|
621
|
-
? errors.length > 0
|
|
622
|
-
? "compile_failed"
|
|
623
|
-
: "project_unhealthy"
|
|
624
|
-
: "no_errors_found";
|
|
625
777
|
// specs/feedback.md 2026-07-05 — stale-log detection. When an assembly is
|
|
626
778
|
// stuck in a failed-compile state, AssetDatabase.Refresh no-ops and the
|
|
627
779
|
// log's most-recent CSxxxx block can reference on-disk source that has
|
|
@@ -644,12 +796,156 @@ export class ToolRouter {
|
|
|
644
796
|
// newest Library/ScriptAssemblies/*.dll mtime against Assets/**/*.cs source
|
|
645
797
|
// mtimes and surface staleAssembly when any source is newer than the DLL.
|
|
646
798
|
const staleAsm = detectStaleAssembly(this.projectPath);
|
|
799
|
+
// specs/feedback.md 2026-08-14 — "these errors may predate your edits".
|
|
800
|
+
// Three consecutive reads returned the same error block for code that had
|
|
801
|
+
// already been fixed, with no staleness signal: Editor.log's FILE mtime
|
|
802
|
+
// was fresh (asset imports keep appending) while the error BLOCK inside it
|
|
803
|
+
// was old, so the staleLog heuristic could not fire. The built-assembly
|
|
804
|
+
// mtime is the anchor that works — a cited source newer than the newest
|
|
805
|
+
// built DLL proves no compile has COMPLETED since the edit.
|
|
806
|
+
const errorsPredateEdits = errors.length > 0
|
|
807
|
+
? detectErrorsPredatingEdits(errors.map((e) => e.file), this.projectPath, staleAsm.dllMtimeMs)
|
|
808
|
+
: null;
|
|
809
|
+
// feedback-04-08-opus §2 — log-authorship cross-check. A batch run of a
|
|
810
|
+
// NEWER Unity writes a log whose errors (e.g. an API deprecation that is
|
|
811
|
+
// only an error on that version) CANNOT occur in the live editor. Parse the
|
|
812
|
+
// log header's version + batch-mode + date, compare against the live bridge
|
|
813
|
+
// version, and surface a mismatch so the agent does not "fix" 31 unrelated
|
|
814
|
+
// files or refuse to continue on errors that cannot occur here. The fields
|
|
815
|
+
// are always emitted when the header was parseable (so an agent can see the
|
|
816
|
+
// log was a batch run even when versions agree); the warning hint is added
|
|
817
|
+
// only on a real mismatch or a batch-mode log.
|
|
818
|
+
const authorship = parseLogAuthorship(tail.content);
|
|
819
|
+
const liveUnityVersion = this.resolveLiveUnityVersion();
|
|
820
|
+
const authorshipCheck = compareLogAuthorship(authorship, liveUnityVersion);
|
|
821
|
+
// feedback-fable-04-08 §4 — downgrade the top-level status when the errors
|
|
822
|
+
// may not apply to the running editor. An agent that trusts a
|
|
823
|
+
// `compile_failed` + `unhealthy: true` headline will start "fixing" 31
|
|
824
|
+
// phantom errors from a stale / batch-authored log. When EITHER the log is
|
|
825
|
+
// stale (a cited source is newer than the log) OR a version-mismatch is
|
|
826
|
+
// detected (a different Unity wrote the log), the verdict becomes
|
|
827
|
+
// `stale_log` — the errors[] are still attached as evidence, but the
|
|
828
|
+
// headline + status make clear they MUST NOT be acted on until a genuine
|
|
829
|
+
// recompile confirms them. The raw verdict (what the log itself says) is
|
|
830
|
+
// preserved in `logVerdict` for agents that want the literal signal.
|
|
831
|
+
const logVerdict = health.unhealthy
|
|
832
|
+
? errors.length > 0
|
|
833
|
+
? "compile_failed"
|
|
834
|
+
: "project_unhealthy"
|
|
835
|
+
: "no_errors_found";
|
|
836
|
+
const errorsMayNotApply = health.unhealthy &&
|
|
837
|
+
(!!staleLog || authorshipCheck.versionMismatch);
|
|
838
|
+
// feedback 2026-08-17 — a clean bill of health read from the ROTATED log
|
|
839
|
+
// is not a clean bill of health. When logSource is a prev_log_* value the
|
|
840
|
+
// resolver fell back to Editor-prev.log while a live Editor writes a
|
|
841
|
+
// different file; an agent reading only `status` got a false green. A
|
|
842
|
+
// no-errors read from a rotated log is explicitly `indeterminate`.
|
|
843
|
+
const prevLogRead = resolvedLog.reason.startsWith("prev_log_");
|
|
844
|
+
const status = errorsMayNotApply
|
|
845
|
+
? "stale_log"
|
|
846
|
+
: logVerdict === "no_errors_found" && prevLogRead
|
|
847
|
+
? "indeterminate"
|
|
848
|
+
: logVerdict;
|
|
849
|
+
// specs/feedback.md 2026-08-12 — partial-compile signal. Unity compiles
|
|
850
|
+
// assemblies in dependency order; when an early assembly fails the pipeline
|
|
851
|
+
// may not reach dependent ones, so the reported errors can be a SUBSET of
|
|
852
|
+
// the true error set. When there are errors AND the project has multiple
|
|
853
|
+
// compile assemblies, surface asmdefCount + assembliesWithErrors +
|
|
854
|
+
// partialCompileLikely so an agent does not treat `errorCount` as final
|
|
855
|
+
// (this is exactly the trap that masked 6 errors behind 1 during the
|
|
856
|
+
// input-simulation review).
|
|
857
|
+
// Only walk for asmdefs when there are errors — the signal is only
|
|
858
|
+
// meaningful with errors, and countProjectAsmdefs stats the whole Assets/
|
|
859
|
+
// tree synchronously (memoized per project, but still skip it when unused).
|
|
860
|
+
const hasErrors = errors.length > 0;
|
|
861
|
+
const asmdefs = hasErrors
|
|
862
|
+
? countProjectAsmdefs(this.projectPath)
|
|
863
|
+
: { count: 0, sample: [] };
|
|
864
|
+
const partialCompileLikely = hasErrors && asmdefs.count > 1;
|
|
865
|
+
const assembliesWithErrors = partialCompileLikely
|
|
866
|
+
? countAssembliesWithErrors(errors.map((e) => e.file), this.projectPath)
|
|
867
|
+
: 0;
|
|
868
|
+
// Headline assembly. Precedence (most severe first):
|
|
869
|
+
// 1. errorsMayNotApply — stale log / version mismatch: the errors may
|
|
870
|
+
// not apply at all (feedback-fable-04-08 §4).
|
|
871
|
+
// 2. errorsPredateEdits — errors + a stale assembly: the error block
|
|
872
|
+
// was written by a compile that predates the on-disk edits (no
|
|
873
|
+
// compile has COMPLETED since: every built DLL is older than a cited
|
|
874
|
+
// source). The log-mtime staleness heuristic cannot see this — asset
|
|
875
|
+
// imports keep appending to Editor.log, so the FILE is fresh while
|
|
876
|
+
// the error BLOCK inside it is old. Say so in the headline rather
|
|
877
|
+
// than letting the agent re-read code it has already fixed
|
|
878
|
+
// (specs/feedback.md 2026-08-14).
|
|
879
|
+
// 3. feedback 2026-08-17 — errors + staleAssembly WITHOUT the
|
|
880
|
+
// cited-files proof: the whole-Assets scan still shows a source
|
|
881
|
+
// newer than the newest built DLL, so the error block may come from
|
|
882
|
+
// an earlier compile. The 2026-08-14 fix covered only the
|
|
883
|
+
// errorsPredateEdits shape; this extends the headline caveat to the
|
|
884
|
+
// staleAssembly shape (the field report's 12 stale CS errors had
|
|
885
|
+
// staleAssembly:true but the headline stated them as fact).
|
|
886
|
+
// 4. indeterminate (feedback 2026-08-17) — no errors, but read from the
|
|
887
|
+
// ROTATED log: not a verified clean bill of health.
|
|
888
|
+
// 5. the plain health headline.
|
|
889
|
+
const headline = errorsMayNotApply
|
|
890
|
+
? `${errors.length} error(s) in Editor.log, but the log appears ` +
|
|
891
|
+
"STALE or authored by a different Unity — these errors may NOT " +
|
|
892
|
+
"apply to the running editor. Do NOT act on them until a genuine " +
|
|
893
|
+
`recompile (${toolHintReference("unity_open_mcp_recompile_scripts")}) ` +
|
|
894
|
+
"confirms them."
|
|
895
|
+
: errorsPredateEdits
|
|
896
|
+
? `${health.headline} NOTE: these errors may PREDATE your edits — ` +
|
|
897
|
+
`${errorsPredateEdits.files.length} cited source file(s) are newer ` +
|
|
898
|
+
"than the newest built assembly, so no compile has completed since " +
|
|
899
|
+
"you changed them. Force a recompile " +
|
|
900
|
+
`(${toolHintReference("unity_open_mcp_recompile_scripts")}) and ` +
|
|
901
|
+
"re-read before acting on them."
|
|
902
|
+
: hasErrors && staleAsm.staleAssembly
|
|
903
|
+
? `${health.headline} NOTE: the error block may PREDATE the current ` +
|
|
904
|
+
`sources — ${staleAsm.newerSources.length} Assets/**/*.cs source(s) ` +
|
|
905
|
+
"are newer than the newest built assembly, so these errors may come " +
|
|
906
|
+
"from an earlier compile. Force a recompile " +
|
|
907
|
+
`(${toolHintReference("unity_open_mcp_recompile_scripts")}) and ` +
|
|
908
|
+
"re-read before acting on them."
|
|
909
|
+
: status === "indeterminate"
|
|
910
|
+
? "No errors found, but this is NOT a verified clean bill of " +
|
|
911
|
+
`health: the read came from the ROTATED log (${resolvedLog.reason === "prev_log_live_editor" ? "Editor-prev.log chosen because a live editor holds the project" : "Editor-prev.log fallback"}) ` +
|
|
912
|
+
"while a live Editor may be writing a different log. Confirm via " +
|
|
913
|
+
"the live bridge (unity_open_mcp_editor_status / a live tool " +
|
|
914
|
+
"probe) or re-read after the Editor settles."
|
|
915
|
+
: health.headline;
|
|
647
916
|
return sourceResult({
|
|
648
917
|
status,
|
|
649
|
-
|
|
650
|
-
|
|
918
|
+
// When the errors may not apply (stale log / version mismatch), the
|
|
919
|
+
// top-level verdict is downgraded to "stale_log" and unhealthy is
|
|
920
|
+
// suppressed so an agent branching on `unhealthy` does not treat
|
|
921
|
+
// phantom errors as a hard failure (feedback-fable-04-08 §4). The raw
|
|
922
|
+
// log verdict is preserved in `logVerdict` for callers that want the
|
|
923
|
+
// literal signal regardless of freshness.
|
|
924
|
+
unhealthy: errorsMayNotApply ? false : health.unhealthy,
|
|
925
|
+
headline,
|
|
926
|
+
// Raw verdict preserved whenever the top-level status was rewritten
|
|
927
|
+
// (stale_log downgrade OR the indeterminate prev-log case), so callers
|
|
928
|
+
// can still read what the log literally said.
|
|
929
|
+
...(errorsMayNotApply || status === "indeterminate" ? { logVerdict } : {}),
|
|
651
930
|
errorCount: errors.length,
|
|
652
931
|
errors,
|
|
932
|
+
// specs/feedback.md 2026-08-12 — present only when there are errors AND
|
|
933
|
+
// the project has multiple compile assemblies. Unity's dependency-ordered
|
|
934
|
+
// pipeline means these errors may be a subset; fixing them lets dependent
|
|
935
|
+
// assemblies compile and may surface more. Re-check until errorCount is 0.
|
|
936
|
+
...(partialCompileLikely
|
|
937
|
+
? {
|
|
938
|
+
partialCompileLikely: true,
|
|
939
|
+
asmdefCount: asmdefs.count,
|
|
940
|
+
// Distinct assemblies the reported errors span (null when none of
|
|
941
|
+
// the cited files mapped to an asmdef — e.g. global/package code).
|
|
942
|
+
assembliesWithErrors: assembliesWithErrors > 0 ? assembliesWithErrors : null,
|
|
943
|
+
partialCompileHint: "Unity compiles assemblies in dependency order; the reported errors " +
|
|
944
|
+
"may be a SUBSET — once these are fixed, dependent assemblies that " +
|
|
945
|
+
"were not reached compile next and may surface additional errors. " +
|
|
946
|
+
"Re-check read_compile_errors after each fix until errorCount is 0.",
|
|
947
|
+
}
|
|
948
|
+
: {}),
|
|
653
949
|
// Package / assembly issues from the same log tail. Empty when the
|
|
654
950
|
// only red flags are compiler errors (the common case).
|
|
655
951
|
issues: health.issues,
|
|
@@ -681,6 +977,45 @@ export class ToolRouter {
|
|
|
681
977
|
staleAssemblyNewerSources: staleAsm.newerSources,
|
|
682
978
|
}
|
|
683
979
|
: {}),
|
|
980
|
+
// specs/feedback.md 2026-08-14 — machine-readable sibling of the
|
|
981
|
+
// headline caveat above. Present ONLY when errors were returned AND at
|
|
982
|
+
// least one file they cite is newer than the newest built assembly, so
|
|
983
|
+
// the error block cannot reflect the current source. Distinct from
|
|
984
|
+
// `staleLogSuspected` (log-file mtime) and `staleAssembly` (whole
|
|
985
|
+
// Assets/ scan): this names the cited files the agent is about to
|
|
986
|
+
// re-read.
|
|
987
|
+
...(errorsPredateEdits
|
|
988
|
+
? {
|
|
989
|
+
errorsMayPredateEdits: true,
|
|
990
|
+
errorsMayPredateEditsFiles: errorsPredateEdits.files,
|
|
991
|
+
errorsMayPredateEditsHint: "These errors were parsed from a compile that ran BEFORE the " +
|
|
992
|
+
"listed files were last edited (each is newer than the newest " +
|
|
993
|
+
"Library/ScriptAssemblies/*.dll, so no compile has completed " +
|
|
994
|
+
"since). Do not re-read or re-fix that code on the strength of " +
|
|
995
|
+
"this block — force a recompile " +
|
|
996
|
+
`(${toolHintReference("unity_open_mcp_recompile_scripts")}) ` +
|
|
997
|
+
"and read again.",
|
|
998
|
+
}
|
|
999
|
+
: {}),
|
|
1000
|
+
// feedback-04-08-opus §2 — log authorship. Emitted whenever the header
|
|
1001
|
+
// was parseable (the live editor's long tail usually has no header, so
|
|
1002
|
+
// these stay absent in the common case). On a version mismatch the hint
|
|
1003
|
+
// says the errors CANNOT all occur in the live editor; on a batch-mode
|
|
1004
|
+
// log it reminds the agent the errors are from a headless run.
|
|
1005
|
+
...(authorship.logUnityVersion !== null || authorship.logBatchMode !== null
|
|
1006
|
+
? {
|
|
1007
|
+
logUnityVersion: authorship.logUnityVersion,
|
|
1008
|
+
liveUnityVersion: authorshipCheck.liveUnityVersion,
|
|
1009
|
+
logBatchMode: authorship.logBatchMode,
|
|
1010
|
+
logDate: authorship.logDate,
|
|
1011
|
+
...(authorshipCheck.hint
|
|
1012
|
+
? {
|
|
1013
|
+
logAuthorshipMismatch: authorshipCheck.versionMismatch,
|
|
1014
|
+
logAuthorshipHint: authorshipCheck.hint,
|
|
1015
|
+
}
|
|
1016
|
+
: {}),
|
|
1017
|
+
}
|
|
1018
|
+
: {}),
|
|
684
1019
|
}, "offline");
|
|
685
1020
|
}
|
|
686
1021
|
// Resolve the live Unity Editor PID for this project from the instance lock,
|
|
@@ -702,6 +1037,27 @@ export class ToolRouter {
|
|
|
702
1037
|
}
|
|
703
1038
|
return undefined;
|
|
704
1039
|
}
|
|
1040
|
+
// feedback-04-08-opus §2 — resolve the live bridge's Unity version from the
|
|
1041
|
+
// instance lock, so read_compile_errors can cross-check the LOG's version
|
|
1042
|
+
// header against the version of the editor that is actually running. A batch
|
|
1043
|
+
// run of a NEWER Unity leaves a log whose errors cannot occur in the live
|
|
1044
|
+
// editor; without this check the tool would surface them as current. Returns
|
|
1045
|
+
// null when no live editor / no version recorded (the benign case — no
|
|
1046
|
+
// version to compare against, so no false mismatch fires).
|
|
1047
|
+
resolveLiveUnityVersion() {
|
|
1048
|
+
if (!this.projectPath)
|
|
1049
|
+
return null;
|
|
1050
|
+
try {
|
|
1051
|
+
const lock = readInstanceLock(this.projectPath);
|
|
1052
|
+
if (lock && lock.pid && isPidAlive(lock.pid) && lock.unityVersion) {
|
|
1053
|
+
return lock.unityVersion;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
catch {
|
|
1057
|
+
// Unreadable/missing lock → no live version known.
|
|
1058
|
+
}
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
705
1061
|
async routeListAssets(args) {
|
|
706
1062
|
try {
|
|
707
1063
|
const result = await listAssetsOffline({
|
|
@@ -753,7 +1109,13 @@ export class ToolRouter {
|
|
|
753
1109
|
const maxEvents = typeof args.max_events === "number" && args.max_events > 0
|
|
754
1110
|
? Math.min(args.max_events, 1000)
|
|
755
1111
|
: 50;
|
|
756
|
-
|
|
1112
|
+
// feedback #8 — forward the caller-supplied subscriber id so each agent
|
|
1113
|
+
// session gets its own read cursor (a fresh subscriber starts at the
|
|
1114
|
+
// current high-water mark and does not replay the shared queue's backlog).
|
|
1115
|
+
const subscriber = typeof args.subscriber === "string" && args.subscriber.length > 0
|
|
1116
|
+
? args.subscriber
|
|
1117
|
+
: undefined;
|
|
1118
|
+
const result = this.eventStream.pull(maxEvents, subscriber);
|
|
757
1119
|
return {
|
|
758
1120
|
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
759
1121
|
isError: false,
|
|
@@ -764,6 +1126,12 @@ export class ToolRouter {
|
|
|
764
1126
|
? args.kind
|
|
765
1127
|
: undefined;
|
|
766
1128
|
const includePlanned = args.include_planned !== false;
|
|
1129
|
+
// specs/feedback.md 2026-08-24 — the tool surface defaults to `compact`
|
|
1130
|
+
// (the unfolded catalog is ~513 KB, which overflows a typical harness
|
|
1131
|
+
// per-result ceiling). `buildCapabilities` itself keeps returning the full
|
|
1132
|
+
// shape so its other caller (generate_skill) is unaffected; the fold is a
|
|
1133
|
+
// view applied here.
|
|
1134
|
+
const profile = isOutputProfile(args.profile) ? args.profile : "compact";
|
|
767
1135
|
// M18 Plan 2 / T18.2.3 — when the bridge is live, probe its compiled-
|
|
768
1136
|
// state tool inventory so per-group `available` reflects whether each
|
|
769
1137
|
// domain dependency compiled in (e.g. UNITY_OPEN_MCP_EXT_NAVIGATION).
|
|
@@ -789,7 +1157,11 @@ export class ToolRouter {
|
|
|
789
1157
|
fixes: FIX_CATALOG,
|
|
790
1158
|
availableBridgeTools,
|
|
791
1159
|
}, { kind, includePlanned });
|
|
792
|
-
return sourceResult(result,
|
|
1160
|
+
return sourceResult(viewCapabilities(result, {
|
|
1161
|
+
profile,
|
|
1162
|
+
page_size: typeof args.page_size === "number" ? args.page_size : undefined,
|
|
1163
|
+
cursor: typeof args.cursor === "string" ? args.cursor : undefined,
|
|
1164
|
+
}), "local");
|
|
793
1165
|
}
|
|
794
1166
|
async routeListRules(args) {
|
|
795
1167
|
const assetKind = typeof args.asset_kind === "string" ? args.asset_kind : undefined;
|
|
@@ -1293,8 +1665,52 @@ export class ToolRouter {
|
|
|
1293
1665
|
if (proc)
|
|
1294
1666
|
unityProcessPid = proc.pid;
|
|
1295
1667
|
}
|
|
1668
|
+
// specs/feedback.md 2026-08-14 — wedge detection, evaluated BEFORE the
|
|
1669
|
+
// existing token ladder because both wedges masquerade as one of its
|
|
1670
|
+
// healthy-looking outcomes.
|
|
1671
|
+
//
|
|
1672
|
+
// (a) main_thread_wedged. `dead_bridge` means "live PID + stale
|
|
1673
|
+
// heartbeat". A modal dialog produces exactly that (the heartbeat is
|
|
1674
|
+
// written from EditorApplication.update, which a blocked pump stops
|
|
1675
|
+
// running) — but a dead bridge ASSEMBLY cannot answer /ping at all,
|
|
1676
|
+
// because the listener lives in the assembly that failed to compile.
|
|
1677
|
+
// So a reachable /ping alongside a stale heartbeat rules out the
|
|
1678
|
+
// safe-mode diagnosis and identifies a stalled main thread instead.
|
|
1679
|
+
// (b) editor_fd_exhaustion. Process, heartbeat and /ping all survive a
|
|
1680
|
+
// dead build driver, so nothing in the existing ladder can see it.
|
|
1681
|
+
// One regex over the freshest log tail can (the same scan
|
|
1682
|
+
// read_compile_errors runs), and it only runs when the ladder would
|
|
1683
|
+
// otherwise report a healthy editor — the offline/stopped paths have
|
|
1684
|
+
// no editor to be wedged.
|
|
1685
|
+
let wedge = null;
|
|
1686
|
+
if (classification === "dead_bridge" && pingReachable) {
|
|
1687
|
+
wedge = {
|
|
1688
|
+
reason: "main_thread_wedged",
|
|
1689
|
+
detail: "The instance lock's heartbeat is stale (the editor main thread stopped " +
|
|
1690
|
+
"ticking) but the bridge listener still answers /ping. A failed bridge " +
|
|
1691
|
+
"assembly would take the listener down with it, so this is a blocked main " +
|
|
1692
|
+
"thread — almost always a modal dialog — not Safe Mode.",
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
else if (pingReachable && connected) {
|
|
1696
|
+
const fd = scanForFdExhaustion(this.projectPath, this.resolveLiveEditorPid(), this.globalLogPathOverride);
|
|
1697
|
+
if (fd.present) {
|
|
1698
|
+
wedge = {
|
|
1699
|
+
reason: "editor_fd_exhaustion",
|
|
1700
|
+
detail: "The freshest Editor.log carries the Bee build-driver file-descriptor " +
|
|
1701
|
+
"exhaustion signature. The Editor answers HTTP but cannot build: " +
|
|
1702
|
+
"Library/ScriptAssemblies stops updating, so C# edits never take effect " +
|
|
1703
|
+
"and execute_csharp keeps running the previous assembly.",
|
|
1704
|
+
...(fd.logPath ? { logPath: fd.logPath } : {}),
|
|
1705
|
+
...(fd.raw ? { raw: fd.raw } : {}),
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1296
1709
|
let status;
|
|
1297
|
-
if (
|
|
1710
|
+
if (wedge !== null) {
|
|
1711
|
+
status = "wedged";
|
|
1712
|
+
}
|
|
1713
|
+
else if (classification === "dead_bridge") {
|
|
1298
1714
|
status = "dead_bridge";
|
|
1299
1715
|
}
|
|
1300
1716
|
else if (compiling) {
|
|
@@ -1336,7 +1752,13 @@ export class ToolRouter {
|
|
|
1336
1752
|
// Safe Mode / compile failure", not the generic "stopped" the bare
|
|
1337
1753
|
// status used to suggest.
|
|
1338
1754
|
classification,
|
|
1339
|
-
recoveryHint: bridgeStatusRecoveryHint(status),
|
|
1755
|
+
recoveryHint: bridgeStatusRecoveryHint(status, wedge),
|
|
1756
|
+
// specs/feedback.md 2026-08-14 — present ONLY when the editor is wedged.
|
|
1757
|
+
// `classification` keeps mirroring the instance lock verbatim (healthy |
|
|
1758
|
+
// reloading | dead_bridge | gone) so that contract is unchanged; the
|
|
1759
|
+
// wedge is carried by the tool's own `status` token plus this block, and
|
|
1760
|
+
// `ready` is false either way.
|
|
1761
|
+
...(wedge !== null ? { wedged: wedge } : {}),
|
|
1340
1762
|
instance: {
|
|
1341
1763
|
lockPath: lockOnDisk,
|
|
1342
1764
|
classification,
|
|
@@ -1354,10 +1776,15 @@ export class ToolRouter {
|
|
|
1354
1776
|
isPlaying: pingBody?.isPlaying ?? null,
|
|
1355
1777
|
unityVersion: pingBody?.unityVersion ?? null,
|
|
1356
1778
|
bridgeVersion: pingBody?.bridgeVersion ?? null,
|
|
1779
|
+
wireContract: pingBody?.wireContract ?? null,
|
|
1357
1780
|
mode: pingBody?.mode ?? null,
|
|
1358
1781
|
}
|
|
1359
1782
|
: { reachable: false },
|
|
1360
|
-
|
|
1783
|
+
// specs/feedback.md 2026-08-24 — null when /ping was unreachable; a
|
|
1784
|
+
// `stale: true` block is the signal to reinstall the Unity package
|
|
1785
|
+
// rather than file a regression.
|
|
1786
|
+
wireContract: wireContractBlock(pingReachable, pingBody?.wireContract),
|
|
1787
|
+
nextStep: bridgeStatusNextStep(status, wedge),
|
|
1361
1788
|
};
|
|
1362
1789
|
// bridge_status never reports an error — even a stopped bridge is a
|
|
1363
1790
|
// successful status read. _source=local because the synthesis happens in
|
|
@@ -1405,12 +1832,14 @@ export class ToolRouter {
|
|
|
1405
1832
|
// fixable compile failure. We reuse the same offline log read +
|
|
1406
1833
|
// health-issue extractor as read_compile_errors so the diagnosis is
|
|
1407
1834
|
// identical to what the agent already saw.
|
|
1408
|
-
const logPath = resolveEditorLogPath(this.projectPath, undefined, this.resolveLiveEditorPid()).path;
|
|
1835
|
+
const logPath = resolveEditorLogPath(this.projectPath, undefined, this.resolveLiveEditorPid(), this.globalLogPathOverride).path;
|
|
1409
1836
|
const tail = readLogTail(logPath, DEFAULT_LOG_TAIL_BYTES);
|
|
1410
1837
|
let signaturePresent = false;
|
|
1838
|
+
let signatureSource = null;
|
|
1411
1839
|
let logWarning = null;
|
|
1412
1840
|
if (tail.error || !tail.exists) {
|
|
1413
|
-
// Without the log we cannot confirm the signature
|
|
1841
|
+
// Without the log we cannot confirm the signature from the file. The
|
|
1842
|
+
// live-console fallback below may still recover it.
|
|
1414
1843
|
logWarning = tail.error
|
|
1415
1844
|
? `Editor.log unreadable at ${logPath}: ${tail.error}`
|
|
1416
1845
|
: `No Editor.log found at ${logPath}.`;
|
|
@@ -1418,7 +1847,10 @@ export class ToolRouter {
|
|
|
1418
1847
|
else {
|
|
1419
1848
|
const issues = extractProjectHealthIssues(tail.content);
|
|
1420
1849
|
signaturePresent = issues.some((i) => i.kind === "editor_fd_exhaustion");
|
|
1421
|
-
if (
|
|
1850
|
+
if (signaturePresent) {
|
|
1851
|
+
signatureSource = "editor_log";
|
|
1852
|
+
}
|
|
1853
|
+
else {
|
|
1422
1854
|
logWarning =
|
|
1423
1855
|
"The editor_fd_exhaustion signature is NOT present in the recent " +
|
|
1424
1856
|
"Editor.log tail. The Editor may be slow/stuck for a different " +
|
|
@@ -1427,34 +1859,87 @@ export class ToolRouter {
|
|
|
1427
1859
|
"the diagnosis before killing the Editor.";
|
|
1428
1860
|
}
|
|
1429
1861
|
}
|
|
1430
|
-
//
|
|
1431
|
-
//
|
|
1432
|
-
//
|
|
1433
|
-
//
|
|
1862
|
+
// feedback-fable-04-08 §2b — live-console signature fallback. When the log
|
|
1863
|
+
// file did not carry the signature (it may be the wrong/stale log for the
|
|
1864
|
+
// running session, or the bridge is still up and the Bee exception is only
|
|
1865
|
+
// in the live console via LogEntries), probe the bridge's console read as a
|
|
1866
|
+
// secondary signature source. The fd-exhaustion exception is raised into
|
|
1867
|
+
// the live Unity console independently of whichever Editor.log file the
|
|
1868
|
+
// resolver picked, so this catches the case the file read missed. The
|
|
1869
|
+
// bridge is usually down by full fd-exhaustion, so this is opportunistic —
|
|
1870
|
+
// a failure here leaves the file-based verdict standing.
|
|
1871
|
+
if (!signaturePresent) {
|
|
1872
|
+
try {
|
|
1873
|
+
const liveAvailable = await live.isLiveAvailable();
|
|
1874
|
+
if (liveAvailable) {
|
|
1875
|
+
const consoleResult = await live.route("unity_senses_read_console", { max_entries: 200, type: "error", detail: "verbose" });
|
|
1876
|
+
const consoleBody = parseResultBody(consoleResult);
|
|
1877
|
+
const text = this.serializeConsoleEntries(consoleBody);
|
|
1878
|
+
if (text) {
|
|
1879
|
+
const liveIssues = extractProjectHealthIssues(text);
|
|
1880
|
+
if (liveIssues.some((i) => i.kind === "editor_fd_exhaustion")) {
|
|
1881
|
+
signaturePresent = true;
|
|
1882
|
+
signatureSource = "live_console";
|
|
1883
|
+
logWarning = null;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
catch {
|
|
1889
|
+
// Bridge unreachable (expected on fd-exhaustion). The file-based
|
|
1890
|
+
// verdict above is the authoritative fallback.
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
// 2. Resolve the live Unity PID. feedback #2 (2026-08-06): the instance
|
|
1894
|
+
// lock's `pid` is authoritative — the bridge only writes it from the
|
|
1895
|
+
// main interactive editor (it early-returns in worker/batch processes),
|
|
1896
|
+
// while the process scan could return an AssetImportWorker child that
|
|
1897
|
+
// shares the project path. Prefer the lock PID when it is live; fall
|
|
1898
|
+
// back to the scan (which now also excludes AssetImportWorker children)
|
|
1899
|
+
// only when no live lock PID exists.
|
|
1434
1900
|
let pid = null;
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1901
|
+
let pidSource = null;
|
|
1902
|
+
const lockPid = this.resolveLiveEditorPid();
|
|
1903
|
+
if (lockPid !== undefined) {
|
|
1904
|
+
pid = lockPid;
|
|
1905
|
+
pidSource = "instance_lock";
|
|
1438
1906
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1907
|
+
else {
|
|
1908
|
+
try {
|
|
1909
|
+
const proc = findUnityForProject(this.projectPath);
|
|
1910
|
+
if (proc) {
|
|
1911
|
+
pid = proc.pid;
|
|
1912
|
+
pidSource = "process_scan";
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
catch {
|
|
1916
|
+
pid = null;
|
|
1917
|
+
}
|
|
1441
1918
|
}
|
|
1442
1919
|
// 3. Dry-run branch: confirm was false/absent. Return the diagnosis +
|
|
1443
1920
|
// the PID the tool WOULD kill, with no side effect. Lets an agent
|
|
1444
|
-
// preview before committing.
|
|
1921
|
+
// preview before committing. feedback #2: include the PID source and
|
|
1922
|
+
// the resolved process's command line so a misattribution (e.g. an
|
|
1923
|
+
// AssetImportWorker child) is visible before confirm.
|
|
1445
1924
|
if (!confirm) {
|
|
1925
|
+
const processCommandLine = pid !== null ? readProcessCommandLine(pid) : null;
|
|
1446
1926
|
return sourceResult({
|
|
1447
1927
|
action: "restart_editor",
|
|
1448
1928
|
confirm: false,
|
|
1449
1929
|
dryRun: true,
|
|
1450
1930
|
signaturePresent,
|
|
1931
|
+
...(signatureSource !== null ? { signatureSource } : {}),
|
|
1451
1932
|
...(pid !== null ? { wouldKillPid: pid } : {}),
|
|
1933
|
+
...(pidSource !== null ? { pidSource } : {}),
|
|
1934
|
+
...(processCommandLine !== null ? { processCommandLine } : {}),
|
|
1452
1935
|
...(logWarning !== null ? { warning: logWarning } : {}),
|
|
1453
1936
|
projectPath: this.projectPath,
|
|
1454
1937
|
message: "Dry-run preview. Pass confirm: true to actually terminate the " +
|
|
1455
1938
|
"hung Unity Editor. Only do so after read_compile_errors " +
|
|
1456
1939
|
"confirms an editor_fd_exhaustion issue — killing the Editor " +
|
|
1457
|
-
"can destroy unsaved scene work and in-flight asset imports."
|
|
1940
|
+
"can destroy unsaved scene work and in-flight asset imports. " +
|
|
1941
|
+
"Verify processCommandLine targets the main editor, not an " +
|
|
1942
|
+
"AssetImportWorker child.",
|
|
1458
1943
|
}, "local");
|
|
1459
1944
|
}
|
|
1460
1945
|
// 4. Confirmed-path refusals. Each guard surfaces a structured code so
|
|
@@ -1510,10 +1995,10 @@ export class ToolRouter {
|
|
|
1510
1995
|
}
|
|
1511
1996
|
// 6. Kill the Editor.
|
|
1512
1997
|
const kill = await killEditorProcess(pid, graceMs);
|
|
1513
|
-
return this.restartEditorKillResponse(pid, graceMs, kill, dirtyScenesWarning);
|
|
1998
|
+
return this.restartEditorKillResponse(pid, graceMs, kill, dirtyScenesWarning, signatureSource);
|
|
1514
1999
|
}
|
|
1515
2000
|
/** Build the confirmed-kill response from a {@link KillResult}. */
|
|
1516
|
-
restartEditorKillResponse(pid, graceMs, kill, dirtyScenesWarning) {
|
|
2001
|
+
restartEditorKillResponse(pid, graceMs, kill, dirtyScenesWarning, signatureSource) {
|
|
1517
2002
|
if (kill.terminated) {
|
|
1518
2003
|
return sourceResult({
|
|
1519
2004
|
action: "restart_editor",
|
|
@@ -1523,6 +2008,7 @@ export class ToolRouter {
|
|
|
1523
2008
|
method: kill.method,
|
|
1524
2009
|
elapsedMs: kill.elapsedMs,
|
|
1525
2010
|
graceMs,
|
|
2011
|
+
...(signatureSource !== null ? { signatureSource } : {}),
|
|
1526
2012
|
...(dirtyScenesWarning !== null
|
|
1527
2013
|
? {
|
|
1528
2014
|
dirtyScenesWarning,
|
|
@@ -1567,6 +2053,33 @@ export class ToolRouter {
|
|
|
1567
2053
|
],
|
|
1568
2054
|
}, "local", true);
|
|
1569
2055
|
}
|
|
2056
|
+
/**
|
|
2057
|
+
* Flatten a unity_senses_read_console result body into a single string the
|
|
2058
|
+
* health-issue extractor can scan for the fd-exhaustion signature. The
|
|
2059
|
+
* console result is a JSON object with an `entries[]` array of
|
|
2060
|
+
* `{ message, stack, ... }`; we join message + stack so the Bee driver's
|
|
2061
|
+
* `Could not register to wait for file descriptor N` frame (which lives in
|
|
2062
|
+
* the stack trace) is reachable by {@link extractProjectHealthIssues}.
|
|
2063
|
+
* Returns "" when the body has no entries.
|
|
2064
|
+
*/
|
|
2065
|
+
serializeConsoleEntries(body) {
|
|
2066
|
+
if (!body || typeof body !== "object")
|
|
2067
|
+
return "";
|
|
2068
|
+
const entries = body.entries;
|
|
2069
|
+
if (!Array.isArray(entries))
|
|
2070
|
+
return "";
|
|
2071
|
+
const parts = [];
|
|
2072
|
+
for (const ent of entries) {
|
|
2073
|
+
if (!ent || typeof ent !== "object")
|
|
2074
|
+
continue;
|
|
2075
|
+
const e = ent;
|
|
2076
|
+
if (typeof e.message === "string")
|
|
2077
|
+
parts.push(e.message);
|
|
2078
|
+
if (typeof e.stack === "string")
|
|
2079
|
+
parts.push(e.stack);
|
|
2080
|
+
}
|
|
2081
|
+
return parts.join("\n");
|
|
2082
|
+
}
|
|
1570
2083
|
/**
|
|
1571
2084
|
* T31.4 — sample the live Unity process's file-descriptor usage and report
|
|
1572
2085
|
* headroom against Mono's ~1024 fd ceiling (the real trip point for the Bee
|
|
@@ -1580,20 +2093,27 @@ export class ToolRouter {
|
|
|
1580
2093
|
* calls build a trend (monotonic climb = leak in progress). No disk cache.
|
|
1581
2094
|
*/
|
|
1582
2095
|
async routeResourcePressure(args) {
|
|
1583
|
-
// 1. Resolve the PID. An explicit pid arg wins; otherwise
|
|
1584
|
-
//
|
|
1585
|
-
//
|
|
2096
|
+
// 1. Resolve the PID. An explicit pid arg wins; otherwise prefer the
|
|
2097
|
+
// instance-lock PID (authoritative main editor — feedback #2) and fall
|
|
2098
|
+
// back to the process scan (which excludes AssetImportWorker children).
|
|
2099
|
+
// The probe must NOT depend on the bridge.
|
|
1586
2100
|
let pid = null;
|
|
1587
2101
|
if (typeof args.pid === "number" && Number.isInteger(args.pid) && args.pid > 0) {
|
|
1588
2102
|
pid = args.pid;
|
|
1589
2103
|
}
|
|
1590
2104
|
else {
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
pid =
|
|
2105
|
+
const lockPid = this.resolveLiveEditorPid();
|
|
2106
|
+
if (lockPid !== undefined) {
|
|
2107
|
+
pid = lockPid;
|
|
1594
2108
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
2109
|
+
else {
|
|
2110
|
+
try {
|
|
2111
|
+
const proc = findUnityForProject(this.projectPath);
|
|
2112
|
+
pid = proc ? proc.pid : null;
|
|
2113
|
+
}
|
|
2114
|
+
catch {
|
|
2115
|
+
pid = null;
|
|
2116
|
+
}
|
|
1597
2117
|
}
|
|
1598
2118
|
}
|
|
1599
2119
|
if (pid === null) {
|
|
@@ -1608,25 +2128,42 @@ export class ToolRouter {
|
|
|
1608
2128
|
},
|
|
1609
2129
|
}, "local", true);
|
|
1610
2130
|
}
|
|
1611
|
-
// 2.
|
|
2131
|
+
// 2. Resolve the configured fd ceiling (default 1024, overridable per
|
|
2132
|
+
// project via `.unity-open-mcp/settings.json` for runtimes whose
|
|
2133
|
+
// internal limit differs from Mono's classic 1024). Read fresh —
|
|
2134
|
+
// resource_pressure is called rarely, and this lets an operator change
|
|
2135
|
+
// the setting without restarting the MCP server.
|
|
2136
|
+
const { ceiling, source: ceilingSource } = readFdCeiling(this.projectPath);
|
|
2137
|
+
// 3. Probe + record the sample. A failed probe still records a sample
|
|
1612
2138
|
// (count: null) so the trend detector sees the gap and does not
|
|
1613
2139
|
// interpolate across it.
|
|
1614
2140
|
const probe = countFileDescriptors(pid);
|
|
1615
2141
|
const count = probe.count;
|
|
1616
|
-
const
|
|
2142
|
+
const approximate = "approximate" in probe ? probe.approximate : false;
|
|
2143
|
+
const headroom = computeFdHeadroom(count, approximate, ceiling);
|
|
1617
2144
|
const ts = Date.now();
|
|
1618
2145
|
this.sessionState.recordFdSample({ ts, pid, count });
|
|
1619
2146
|
const samples = this.sessionState.fdSamplesSnapshot();
|
|
1620
|
-
const trend = analyzeFdTrend(samples);
|
|
1621
|
-
//
|
|
1622
|
-
//
|
|
1623
|
-
//
|
|
1624
|
-
//
|
|
2147
|
+
const trend = analyzeFdTrend(samples, ceiling);
|
|
2148
|
+
// 4. Decide whether to raise an operator warning. For an fd-heavy project
|
|
2149
|
+
// the OS-level count legitimately sits OVER the Mono-ceiling proxy on a
|
|
2150
|
+
// fresh healthy process (lsof counts mmap'd assets / file watchers /
|
|
2151
|
+
// FileStream handles that Mono's IOSelector never registers), so an
|
|
2152
|
+
// absolute `over_ceiling` state is NOT an alarm by itself. Alarm only on:
|
|
2153
|
+
// - a monotonic leak (the actionable trend signal), or
|
|
2154
|
+
// - approaching the ceiling from below (warn/critical).
|
|
2155
|
+
// A merely `rising` or stable/no-history over-ceiling process gets a
|
|
2156
|
+
// `pressureNote` instead. `rising` is any net-positive delta that is NOT a
|
|
2157
|
+
// qualifying leak — it fires on ordinary drift, so it must NOT alarm on its
|
|
2158
|
+
// own (the prior over_ceiling+rising alarm re-cried wolf on every sample
|
|
2159
|
+
// after the first).
|
|
1625
2160
|
const fdMethod = probe.method;
|
|
1626
|
-
const approximate = "approximate" in probe ? probe.approximate : false;
|
|
1627
2161
|
const probeReason = "reason" in probe ? probe.reason : null;
|
|
1628
2162
|
const probeMessage = "message" in probe ? probe.message : null;
|
|
1629
|
-
const
|
|
2163
|
+
const shouldWarn = trend.state === "leaking" ||
|
|
2164
|
+
headroom.state === "warn" ||
|
|
2165
|
+
headroom.state === "critical";
|
|
2166
|
+
const warningState = shouldWarn
|
|
1630
2167
|
? {
|
|
1631
2168
|
level: headroom.state === "critical"
|
|
1632
2169
|
? "critical"
|
|
@@ -1635,26 +2172,44 @@ export class ToolRouter {
|
|
|
1635
2172
|
: "warn",
|
|
1636
2173
|
message: headroom.state === "critical"
|
|
1637
2174
|
? `Editor fd usage is at ${Math.round(headroom.pressureRatio * 100)}% of ` +
|
|
1638
|
-
`
|
|
2175
|
+
`the ${ceiling}-descriptor ceiling — the next domain reload is ` +
|
|
1639
2176
|
`likely to trip the Bee build-driver hang. Save scene work and restart ` +
|
|
1640
2177
|
`Unity via the Hub now, before the Editor hangs.`
|
|
1641
2178
|
: trend.state === "leaking"
|
|
1642
|
-
?
|
|
1643
|
-
`
|
|
1644
|
-
|
|
1645
|
-
|
|
2179
|
+
? headroom.state === "over_ceiling"
|
|
2180
|
+
? `Editor fd usage (${count}) is ABOVE the ${ceiling}-descriptor ` +
|
|
2181
|
+
`ceiling and climbing monotonically (leak in progress): trend ` +
|
|
2182
|
+
`delta ${trend.delta} over ${trend.sampleCount} sample(s). The ` +
|
|
2183
|
+
`count has already crossed the ceiling proxy — save scene work ` +
|
|
2184
|
+
`and restart Unity via the Hub now.`
|
|
2185
|
+
: `Editor fd usage is climbing monotonically across samples (leak in ` +
|
|
2186
|
+
`progress): trend delta ${trend.delta} over ${trend.sampleCount} ` +
|
|
2187
|
+
`sample(s). Save scene work and plan a restart before the count ` +
|
|
2188
|
+
`crosses the ${ceiling}-descriptor ceiling.`
|
|
1646
2189
|
: `Editor fd usage is at ${Math.round(headroom.pressureRatio * 100)}% of ` +
|
|
1647
|
-
`
|
|
1648
|
-
`
|
|
1649
|
-
`
|
|
2190
|
+
`the ${ceiling}-descriptor ceiling. Monitor the trend; if it keeps ` +
|
|
2191
|
+
`climbing across domain reloads, save scene work and restart Unity ` +
|
|
2192
|
+
`via the Hub before the Editor hangs.`,
|
|
1650
2193
|
}
|
|
1651
2194
|
: null;
|
|
2195
|
+
// Informational (non-alarming) note for the over-ceiling-but-stable case.
|
|
2196
|
+
const pressureNote = !shouldWarn && headroom.state === "over_ceiling"
|
|
2197
|
+
? `OS-level fd count (${count}) is above the ${ceiling}-descriptor ceiling ` +
|
|
2198
|
+
`proxy, but lsof / HandleCount count ALL OS file descriptors (mmap'd ` +
|
|
2199
|
+
`assets, file watchers, regular FileStream handles) while only ` +
|
|
2200
|
+
`Mono-IOSelector-registered descriptors (sockets/pipes under async IO) ` +
|
|
2201
|
+
`count toward the real trip point. For an asset-heavy project this is ` +
|
|
2202
|
+
`expected on a healthy process. Treat this as a concern ONLY if the trend ` +
|
|
2203
|
+
`becomes a monotonic leak (leaking) — re-sample after the next domain ` +
|
|
2204
|
+
`reload to establish one.`
|
|
2205
|
+
: null;
|
|
1652
2206
|
return sourceResult({
|
|
1653
2207
|
pid,
|
|
1654
2208
|
fdCount: count,
|
|
1655
2209
|
fdMethod,
|
|
1656
2210
|
approximate,
|
|
1657
|
-
ceiling
|
|
2211
|
+
ceiling,
|
|
2212
|
+
ceilingSource,
|
|
1658
2213
|
headroom: headroom.headroom,
|
|
1659
2214
|
pressureRatio: headroom.pressureRatio,
|
|
1660
2215
|
state: headroom.state,
|
|
@@ -1685,6 +2240,7 @@ export class ToolRouter {
|
|
|
1685
2240
|
],
|
|
1686
2241
|
}
|
|
1687
2242
|
: {}),
|
|
2243
|
+
...(pressureNote !== null ? { pressureNote } : {}),
|
|
1688
2244
|
}, "local");
|
|
1689
2245
|
}
|
|
1690
2246
|
// ── M26 Plan 2 — Unity Hub control (local-routed) ────────────────
|
|
@@ -1975,6 +2531,9 @@ export class ToolRouter {
|
|
|
1975
2531
|
routeMeta = { route: "batch", fallbackReason: "live_unavailable" };
|
|
1976
2532
|
}
|
|
1977
2533
|
}
|
|
2534
|
+
// feedback-fable-04-08 §7 — rewrite the generic live_unavailable to
|
|
2535
|
+
// editor_busy when the batch hit a live editor's project lock.
|
|
2536
|
+
routeMeta = this.rewriteEditorBusy(raw, routeMeta);
|
|
1978
2537
|
const body = parseResultBody(raw);
|
|
1979
2538
|
if (body === null) {
|
|
1980
2539
|
// Body not parseable — fall back to the legacy injectRouteMeta path so
|
|
@@ -2030,7 +2589,14 @@ export class ToolRouter {
|
|
|
2030
2589
|
}
|
|
2031
2590
|
else {
|
|
2032
2591
|
raw = await this.batch.route("unity_open_mcp_scene_get_data", bridgeArgs);
|
|
2033
|
-
|
|
2592
|
+
// feedback-fable-04-08 §7 — rewrite to editor_busy when the batch hit
|
|
2593
|
+
// a live editor's project lock. Compute the meta from the raw result
|
|
2594
|
+
// (which carries the editor_instance_locked error) before stamping.
|
|
2595
|
+
const meta = this.rewriteEditorBusy(raw, {
|
|
2596
|
+
route: "batch",
|
|
2597
|
+
fallbackReason: "live_unavailable",
|
|
2598
|
+
});
|
|
2599
|
+
raw = injectRouteMeta(raw, meta);
|
|
2034
2600
|
}
|
|
2035
2601
|
}
|
|
2036
2602
|
if (!wantPaging)
|
|
@@ -2081,12 +2647,25 @@ function pageSceneNodes(body, pageSize, cursor) {
|
|
|
2081
2647
|
void roots;
|
|
2082
2648
|
void rootGameObjects;
|
|
2083
2649
|
void nodes;
|
|
2084
|
-
|
|
2650
|
+
// `truncated` is read at the source-truncated carry-through below, so it is
|
|
2651
|
+
// not voided with the other intentionally-discarded scene fields.
|
|
2085
2652
|
void moreHidden;
|
|
2086
2653
|
void _existing;
|
|
2087
2654
|
// Pull the top-level meta (everything except `scene`, which we rebuild).
|
|
2088
|
-
|
|
2655
|
+
// B-N21 — strip the structural fields from the TOP level too. For a
|
|
2656
|
+
// pre-unwrapped body (roots/rootGameObjects at the top level, no `scene`
|
|
2657
|
+
// wrapper — the fallback flattenSceneBody handles), `scene` above resolves
|
|
2658
|
+
// to {} and strips nothing, so spreading the raw remainder of `body` used
|
|
2659
|
+
// to carry the ENTIRE unpaged roots array into the response alongside the
|
|
2660
|
+
// page — paging never bounded the payload for that shape. flattenSceneBody
|
|
2661
|
+
// already consumed those fields; drop them wherever they live.
|
|
2662
|
+
const { scene: _s, roots: topRoots, rootGameObjects: topRootGameObjects, nodes: topNodes, truncated: topTruncated, moreHidden: topMoreHidden, pagination: _topExisting, ...topMeta } = body;
|
|
2089
2663
|
void _s;
|
|
2664
|
+
void topRoots;
|
|
2665
|
+
void topRootGameObjects;
|
|
2666
|
+
void topNodes;
|
|
2667
|
+
void topMoreHidden;
|
|
2668
|
+
void _topExisting;
|
|
2090
2669
|
const result = {
|
|
2091
2670
|
...topMeta,
|
|
2092
2671
|
status: body.status ?? "ok",
|
|
@@ -2099,8 +2678,10 @@ function pageSceneNodes(body, pageSize, cursor) {
|
|
|
2099
2678
|
// Carry the bridge's source-side truncation count through to the page so an
|
|
2100
2679
|
// agent can distinguish "more pages" (pagination.truncated) from "the bridge
|
|
2101
2680
|
// capped the hierarchy" (scene.truncated). Both can be non-zero at once.
|
|
2102
|
-
|
|
2103
|
-
|
|
2681
|
+
// For a pre-unwrapped body the count lives at the top level (B-N21).
|
|
2682
|
+
const sourceTruncated = typeof truncated === "number" ? truncated : topTruncated;
|
|
2683
|
+
if (typeof sourceTruncated === "number" && sourceTruncated > 0) {
|
|
2684
|
+
result.scene.truncated = sourceTruncated;
|
|
2104
2685
|
}
|
|
2105
2686
|
return attachPagination(result, block);
|
|
2106
2687
|
}
|