unity-open-mcp 0.9.0 → 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 +78 -1
- 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/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/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 +122 -5
- 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 +39 -1
- package/dist/tool-router.js +397 -62
- package/dist/tool-router.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 +25 -2
- package/dist/tools/execute-csharp.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +34 -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/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 +10 -2
- package/dist/tools/read-compile-errors.js.map +1 -1
- package/dist/tools/resource-pressure.js +21 -10
- package/dist/tools/resource-pressure.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 +51 -2
- package/dist/unity-log.js +152 -5
- 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, detectStaleAssembly } 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 = {
|
|
@@ -388,6 +390,7 @@ export class ToolRouter {
|
|
|
388
390
|
sessionState;
|
|
389
391
|
onToolListChanged;
|
|
390
392
|
hubBackend;
|
|
393
|
+
globalLogPathOverride;
|
|
391
394
|
modelCache = new AssetModelCache();
|
|
392
395
|
/**
|
|
393
396
|
* M28-refactoring Plan 3 (T3.2) — named-tool dispatch map. Replaces the
|
|
@@ -444,7 +447,15 @@ export class ToolRouter {
|
|
|
444
447
|
// M26 Plan 2 — injectable Hub-control backend. Omit in production (the
|
|
445
448
|
// default calls the real hub-control functions); tests pass a fake to
|
|
446
449
|
// avoid real subprocess + network side effects.
|
|
447
|
-
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) {
|
|
448
459
|
this.live = live;
|
|
449
460
|
this.batch = batch;
|
|
450
461
|
this.projectPath = projectPath;
|
|
@@ -452,6 +463,7 @@ export class ToolRouter {
|
|
|
452
463
|
this.sessionState = sessionState;
|
|
453
464
|
this.onToolListChanged = onToolListChanged;
|
|
454
465
|
this.hubBackend = hubBackend;
|
|
466
|
+
this.globalLogPathOverride = globalLogPathOverride;
|
|
455
467
|
}
|
|
456
468
|
async route(toolName, args) {
|
|
457
469
|
return this.routeCore(this.live, toolName, args);
|
|
@@ -528,10 +540,13 @@ export class ToolRouter {
|
|
|
528
540
|
return this.batchPingResult();
|
|
529
541
|
}
|
|
530
542
|
const result = await this.batch.route(toolName, args);
|
|
531
|
-
|
|
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, {
|
|
532
546
|
route: "batch",
|
|
533
547
|
fallbackReason: "live_unavailable",
|
|
534
548
|
});
|
|
549
|
+
return injectRouteMeta(result, meta);
|
|
535
550
|
}
|
|
536
551
|
batchPingResult() {
|
|
537
552
|
return {
|
|
@@ -556,6 +571,52 @@ export class ToolRouter {
|
|
|
556
571
|
isError: false,
|
|
557
572
|
};
|
|
558
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
|
+
}
|
|
559
620
|
async routeReadCompileErrors(args) {
|
|
560
621
|
const tailBytes = typeof args.tail_bytes === "number" && args.tail_bytes >= 4096
|
|
561
622
|
? Math.min(Math.floor(args.tail_bytes), 1048576)
|
|
@@ -571,7 +632,7 @@ export class ToolRouter {
|
|
|
571
632
|
// editor keeps writing. Pass the live PID from the instance lock so the
|
|
572
633
|
// resolver can make the strong-signal decision.
|
|
573
634
|
const livePid = this.resolveLiveEditorPid();
|
|
574
|
-
const resolvedLog = resolveEditorLogPath(this.projectPath, undefined, livePid);
|
|
635
|
+
const resolvedLog = resolveEditorLogPath(this.projectPath, undefined, livePid, this.globalLogPathOverride);
|
|
575
636
|
const logPath = resolvedLog.path;
|
|
576
637
|
const tail = readLogTail(logPath, tailBytes);
|
|
577
638
|
if (tail.error) {
|
|
@@ -617,11 +678,6 @@ export class ToolRouter {
|
|
|
617
678
|
// backward compatibility) so existing callers do not break.
|
|
618
679
|
const health = summarizeProjectHealth(tail.content);
|
|
619
680
|
const errors = health.compilerErrors;
|
|
620
|
-
const status = health.unhealthy
|
|
621
|
-
? errors.length > 0
|
|
622
|
-
? "compile_failed"
|
|
623
|
-
: "project_unhealthy"
|
|
624
|
-
: "no_errors_found";
|
|
625
681
|
// specs/feedback.md 2026-07-05 — stale-log detection. When an assembly is
|
|
626
682
|
// stuck in a failed-compile state, AssetDatabase.Refresh no-ops and the
|
|
627
683
|
// log's most-recent CSxxxx block can reference on-disk source that has
|
|
@@ -644,12 +700,91 @@ export class ToolRouter {
|
|
|
644
700
|
// newest Library/ScriptAssemblies/*.dll mtime against Assets/**/*.cs source
|
|
645
701
|
// mtimes and surface staleAssembly when any source is newer than the DLL.
|
|
646
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;
|
|
647
752
|
return sourceResult({
|
|
648
753
|
status,
|
|
649
|
-
|
|
650
|
-
|
|
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 } : {}),
|
|
651
769
|
errorCount: errors.length,
|
|
652
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
|
+
: {}),
|
|
653
788
|
// Package / assembly issues from the same log tail. Empty when the
|
|
654
789
|
// only red flags are compiler errors (the common case).
|
|
655
790
|
issues: health.issues,
|
|
@@ -681,6 +816,25 @@ export class ToolRouter {
|
|
|
681
816
|
staleAssemblyNewerSources: staleAsm.newerSources,
|
|
682
817
|
}
|
|
683
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
|
+
: {}),
|
|
684
838
|
}, "offline");
|
|
685
839
|
}
|
|
686
840
|
// Resolve the live Unity Editor PID for this project from the instance lock,
|
|
@@ -702,6 +856,27 @@ export class ToolRouter {
|
|
|
702
856
|
}
|
|
703
857
|
return undefined;
|
|
704
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
|
+
}
|
|
705
880
|
async routeListAssets(args) {
|
|
706
881
|
try {
|
|
707
882
|
const result = await listAssetsOffline({
|
|
@@ -753,7 +928,13 @@ export class ToolRouter {
|
|
|
753
928
|
const maxEvents = typeof args.max_events === "number" && args.max_events > 0
|
|
754
929
|
? Math.min(args.max_events, 1000)
|
|
755
930
|
: 50;
|
|
756
|
-
|
|
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);
|
|
757
938
|
return {
|
|
758
939
|
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
759
940
|
isError: false,
|
|
@@ -1405,12 +1586,14 @@ export class ToolRouter {
|
|
|
1405
1586
|
// fixable compile failure. We reuse the same offline log read +
|
|
1406
1587
|
// health-issue extractor as read_compile_errors so the diagnosis is
|
|
1407
1588
|
// identical to what the agent already saw.
|
|
1408
|
-
const logPath = resolveEditorLogPath(this.projectPath, undefined, this.resolveLiveEditorPid()).path;
|
|
1589
|
+
const logPath = resolveEditorLogPath(this.projectPath, undefined, this.resolveLiveEditorPid(), this.globalLogPathOverride).path;
|
|
1409
1590
|
const tail = readLogTail(logPath, DEFAULT_LOG_TAIL_BYTES);
|
|
1410
1591
|
let signaturePresent = false;
|
|
1592
|
+
let signatureSource = null;
|
|
1411
1593
|
let logWarning = null;
|
|
1412
1594
|
if (tail.error || !tail.exists) {
|
|
1413
|
-
// 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.
|
|
1414
1597
|
logWarning = tail.error
|
|
1415
1598
|
? `Editor.log unreadable at ${logPath}: ${tail.error}`
|
|
1416
1599
|
: `No Editor.log found at ${logPath}.`;
|
|
@@ -1418,7 +1601,10 @@ export class ToolRouter {
|
|
|
1418
1601
|
else {
|
|
1419
1602
|
const issues = extractProjectHealthIssues(tail.content);
|
|
1420
1603
|
signaturePresent = issues.some((i) => i.kind === "editor_fd_exhaustion");
|
|
1421
|
-
if (
|
|
1604
|
+
if (signaturePresent) {
|
|
1605
|
+
signatureSource = "editor_log";
|
|
1606
|
+
}
|
|
1607
|
+
else {
|
|
1422
1608
|
logWarning =
|
|
1423
1609
|
"The editor_fd_exhaustion signature is NOT present in the recent " +
|
|
1424
1610
|
"Editor.log tail. The Editor may be slow/stuck for a different " +
|
|
@@ -1427,34 +1613,87 @@ export class ToolRouter {
|
|
|
1427
1613
|
"the diagnosis before killing the Editor.";
|
|
1428
1614
|
}
|
|
1429
1615
|
}
|
|
1430
|
-
//
|
|
1431
|
-
//
|
|
1432
|
-
//
|
|
1433
|
-
//
|
|
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.
|
|
1434
1654
|
let pid = null;
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1655
|
+
let pidSource = null;
|
|
1656
|
+
const lockPid = this.resolveLiveEditorPid();
|
|
1657
|
+
if (lockPid !== undefined) {
|
|
1658
|
+
pid = lockPid;
|
|
1659
|
+
pidSource = "instance_lock";
|
|
1438
1660
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
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
|
+
}
|
|
1441
1672
|
}
|
|
1442
1673
|
// 3. Dry-run branch: confirm was false/absent. Return the diagnosis +
|
|
1443
1674
|
// the PID the tool WOULD kill, with no side effect. Lets an agent
|
|
1444
|
-
// 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.
|
|
1445
1678
|
if (!confirm) {
|
|
1679
|
+
const processCommandLine = pid !== null ? readProcessCommandLine(pid) : null;
|
|
1446
1680
|
return sourceResult({
|
|
1447
1681
|
action: "restart_editor",
|
|
1448
1682
|
confirm: false,
|
|
1449
1683
|
dryRun: true,
|
|
1450
1684
|
signaturePresent,
|
|
1685
|
+
...(signatureSource !== null ? { signatureSource } : {}),
|
|
1451
1686
|
...(pid !== null ? { wouldKillPid: pid } : {}),
|
|
1687
|
+
...(pidSource !== null ? { pidSource } : {}),
|
|
1688
|
+
...(processCommandLine !== null ? { processCommandLine } : {}),
|
|
1452
1689
|
...(logWarning !== null ? { warning: logWarning } : {}),
|
|
1453
1690
|
projectPath: this.projectPath,
|
|
1454
1691
|
message: "Dry-run preview. Pass confirm: true to actually terminate the " +
|
|
1455
1692
|
"hung Unity Editor. Only do so after read_compile_errors " +
|
|
1456
1693
|
"confirms an editor_fd_exhaustion issue — killing the Editor " +
|
|
1457
|
-
"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.",
|
|
1458
1697
|
}, "local");
|
|
1459
1698
|
}
|
|
1460
1699
|
// 4. Confirmed-path refusals. Each guard surfaces a structured code so
|
|
@@ -1510,10 +1749,10 @@ export class ToolRouter {
|
|
|
1510
1749
|
}
|
|
1511
1750
|
// 6. Kill the Editor.
|
|
1512
1751
|
const kill = await killEditorProcess(pid, graceMs);
|
|
1513
|
-
return this.restartEditorKillResponse(pid, graceMs, kill, dirtyScenesWarning);
|
|
1752
|
+
return this.restartEditorKillResponse(pid, graceMs, kill, dirtyScenesWarning, signatureSource);
|
|
1514
1753
|
}
|
|
1515
1754
|
/** Build the confirmed-kill response from a {@link KillResult}. */
|
|
1516
|
-
restartEditorKillResponse(pid, graceMs, kill, dirtyScenesWarning) {
|
|
1755
|
+
restartEditorKillResponse(pid, graceMs, kill, dirtyScenesWarning, signatureSource) {
|
|
1517
1756
|
if (kill.terminated) {
|
|
1518
1757
|
return sourceResult({
|
|
1519
1758
|
action: "restart_editor",
|
|
@@ -1523,6 +1762,7 @@ export class ToolRouter {
|
|
|
1523
1762
|
method: kill.method,
|
|
1524
1763
|
elapsedMs: kill.elapsedMs,
|
|
1525
1764
|
graceMs,
|
|
1765
|
+
...(signatureSource !== null ? { signatureSource } : {}),
|
|
1526
1766
|
...(dirtyScenesWarning !== null
|
|
1527
1767
|
? {
|
|
1528
1768
|
dirtyScenesWarning,
|
|
@@ -1567,6 +1807,33 @@ export class ToolRouter {
|
|
|
1567
1807
|
],
|
|
1568
1808
|
}, "local", true);
|
|
1569
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
|
+
}
|
|
1570
1837
|
/**
|
|
1571
1838
|
* T31.4 — sample the live Unity process's file-descriptor usage and report
|
|
1572
1839
|
* headroom against Mono's ~1024 fd ceiling (the real trip point for the Bee
|
|
@@ -1580,20 +1847,27 @@ export class ToolRouter {
|
|
|
1580
1847
|
* calls build a trend (monotonic climb = leak in progress). No disk cache.
|
|
1581
1848
|
*/
|
|
1582
1849
|
async routeResourcePressure(args) {
|
|
1583
|
-
// 1. Resolve the PID. An explicit pid arg wins; otherwise
|
|
1584
|
-
//
|
|
1585
|
-
//
|
|
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.
|
|
1586
1854
|
let pid = null;
|
|
1587
1855
|
if (typeof args.pid === "number" && Number.isInteger(args.pid) && args.pid > 0) {
|
|
1588
1856
|
pid = args.pid;
|
|
1589
1857
|
}
|
|
1590
1858
|
else {
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
pid =
|
|
1859
|
+
const lockPid = this.resolveLiveEditorPid();
|
|
1860
|
+
if (lockPid !== undefined) {
|
|
1861
|
+
pid = lockPid;
|
|
1594
1862
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1863
|
+
else {
|
|
1864
|
+
try {
|
|
1865
|
+
const proc = findUnityForProject(this.projectPath);
|
|
1866
|
+
pid = proc ? proc.pid : null;
|
|
1867
|
+
}
|
|
1868
|
+
catch {
|
|
1869
|
+
pid = null;
|
|
1870
|
+
}
|
|
1597
1871
|
}
|
|
1598
1872
|
}
|
|
1599
1873
|
if (pid === null) {
|
|
@@ -1608,25 +1882,42 @@ export class ToolRouter {
|
|
|
1608
1882
|
},
|
|
1609
1883
|
}, "local", true);
|
|
1610
1884
|
}
|
|
1611
|
-
// 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
|
|
1612
1892
|
// (count: null) so the trend detector sees the gap and does not
|
|
1613
1893
|
// interpolate across it.
|
|
1614
1894
|
const probe = countFileDescriptors(pid);
|
|
1615
1895
|
const count = probe.count;
|
|
1616
|
-
const
|
|
1896
|
+
const approximate = "approximate" in probe ? probe.approximate : false;
|
|
1897
|
+
const headroom = computeFdHeadroom(count, approximate, ceiling);
|
|
1617
1898
|
const ts = Date.now();
|
|
1618
1899
|
this.sessionState.recordFdSample({ ts, pid, count });
|
|
1619
1900
|
const samples = this.sessionState.fdSamplesSnapshot();
|
|
1620
|
-
const trend = analyzeFdTrend(samples);
|
|
1621
|
-
//
|
|
1622
|
-
//
|
|
1623
|
-
//
|
|
1624
|
-
//
|
|
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).
|
|
1625
1914
|
const fdMethod = probe.method;
|
|
1626
|
-
const approximate = "approximate" in probe ? probe.approximate : false;
|
|
1627
1915
|
const probeReason = "reason" in probe ? probe.reason : null;
|
|
1628
1916
|
const probeMessage = "message" in probe ? probe.message : null;
|
|
1629
|
-
const
|
|
1917
|
+
const shouldWarn = trend.state === "leaking" ||
|
|
1918
|
+
headroom.state === "warn" ||
|
|
1919
|
+
headroom.state === "critical";
|
|
1920
|
+
const warningState = shouldWarn
|
|
1630
1921
|
? {
|
|
1631
1922
|
level: headroom.state === "critical"
|
|
1632
1923
|
? "critical"
|
|
@@ -1635,26 +1926,44 @@ export class ToolRouter {
|
|
|
1635
1926
|
: "warn",
|
|
1636
1927
|
message: headroom.state === "critical"
|
|
1637
1928
|
? `Editor fd usage is at ${Math.round(headroom.pressureRatio * 100)}% of ` +
|
|
1638
|
-
`
|
|
1929
|
+
`the ${ceiling}-descriptor ceiling — the next domain reload is ` +
|
|
1639
1930
|
`likely to trip the Bee build-driver hang. Save scene work and restart ` +
|
|
1640
1931
|
`Unity via the Hub now, before the Editor hangs.`
|
|
1641
1932
|
: trend.state === "leaking"
|
|
1642
|
-
?
|
|
1643
|
-
`
|
|
1644
|
-
|
|
1645
|
-
|
|
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.`
|
|
1646
1943
|
: `Editor fd usage is at ${Math.round(headroom.pressureRatio * 100)}% of ` +
|
|
1647
|
-
`
|
|
1648
|
-
`
|
|
1649
|
-
`
|
|
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.`,
|
|
1650
1947
|
}
|
|
1651
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;
|
|
1652
1960
|
return sourceResult({
|
|
1653
1961
|
pid,
|
|
1654
1962
|
fdCount: count,
|
|
1655
1963
|
fdMethod,
|
|
1656
1964
|
approximate,
|
|
1657
|
-
ceiling
|
|
1965
|
+
ceiling,
|
|
1966
|
+
ceilingSource,
|
|
1658
1967
|
headroom: headroom.headroom,
|
|
1659
1968
|
pressureRatio: headroom.pressureRatio,
|
|
1660
1969
|
state: headroom.state,
|
|
@@ -1685,6 +1994,7 @@ export class ToolRouter {
|
|
|
1685
1994
|
],
|
|
1686
1995
|
}
|
|
1687
1996
|
: {}),
|
|
1997
|
+
...(pressureNote !== null ? { pressureNote } : {}),
|
|
1688
1998
|
}, "local");
|
|
1689
1999
|
}
|
|
1690
2000
|
// ── M26 Plan 2 — Unity Hub control (local-routed) ────────────────
|
|
@@ -1975,6 +2285,9 @@ export class ToolRouter {
|
|
|
1975
2285
|
routeMeta = { route: "batch", fallbackReason: "live_unavailable" };
|
|
1976
2286
|
}
|
|
1977
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);
|
|
1978
2291
|
const body = parseResultBody(raw);
|
|
1979
2292
|
if (body === null) {
|
|
1980
2293
|
// Body not parseable — fall back to the legacy injectRouteMeta path so
|
|
@@ -2030,7 +2343,14 @@ export class ToolRouter {
|
|
|
2030
2343
|
}
|
|
2031
2344
|
else {
|
|
2032
2345
|
raw = await this.batch.route("unity_open_mcp_scene_get_data", bridgeArgs);
|
|
2033
|
-
|
|
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);
|
|
2034
2354
|
}
|
|
2035
2355
|
}
|
|
2036
2356
|
if (!wantPaging)
|
|
@@ -2081,12 +2401,25 @@ function pageSceneNodes(body, pageSize, cursor) {
|
|
|
2081
2401
|
void roots;
|
|
2082
2402
|
void rootGameObjects;
|
|
2083
2403
|
void nodes;
|
|
2084
|
-
|
|
2404
|
+
// `truncated` is read at the source-truncated carry-through below, so it is
|
|
2405
|
+
// not voided with the other intentionally-discarded scene fields.
|
|
2085
2406
|
void moreHidden;
|
|
2086
2407
|
void _existing;
|
|
2087
2408
|
// Pull the top-level meta (everything except `scene`, which we rebuild).
|
|
2088
|
-
|
|
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;
|
|
2089
2417
|
void _s;
|
|
2418
|
+
void topRoots;
|
|
2419
|
+
void topRootGameObjects;
|
|
2420
|
+
void topNodes;
|
|
2421
|
+
void topMoreHidden;
|
|
2422
|
+
void _topExisting;
|
|
2090
2423
|
const result = {
|
|
2091
2424
|
...topMeta,
|
|
2092
2425
|
status: body.status ?? "ok",
|
|
@@ -2099,8 +2432,10 @@ function pageSceneNodes(body, pageSize, cursor) {
|
|
|
2099
2432
|
// Carry the bridge's source-side truncation count through to the page so an
|
|
2100
2433
|
// agent can distinguish "more pages" (pagination.truncated) from "the bridge
|
|
2101
2434
|
// capped the hierarchy" (scene.truncated). Both can be non-zero at once.
|
|
2102
|
-
|
|
2103
|
-
|
|
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;
|
|
2104
2439
|
}
|
|
2105
2440
|
return attachPagination(result, block);
|
|
2106
2441
|
}
|