unity-open-mcp 1.0.0 → 1.2.3
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/batch-spawn.d.ts +26 -0
- package/dist/batch-spawn.js +152 -35
- 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/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 +17 -7
- package/dist/constants.js +17 -7
- 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 +6 -3
- package/dist/event-stream.js +62 -5
- 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 +23 -5
- package/dist/live-client.js +132 -31
- package/dist/live-client.js.map +1 -1
- package/dist/process-diagnostics.d.ts +51 -23
- package/dist/process-diagnostics.js +67 -22
- package/dist/process-diagnostics.js.map +1 -1
- 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/server.js +8 -2
- package/dist/server.js.map +1 -1
- 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 +8 -1
- package/dist/tool-router.js +346 -64
- 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/execute-csharp.js +11 -1
- 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.js +10 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/invoke-method.js +15 -1
- package/dist/tools/invoke-method.js.map +1 -1
- package/dist/tools/read-compile-errors.js +30 -9
- 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 +27 -17
- 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/unity-log.d.ts +16 -0
- package/dist/unity-log.js +60 -9
- package/dist/unity-log.js.map +1 -1
- package/package.json +1 -1
package/dist/tool-router.js
CHANGED
|
@@ -1,6 +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, parseLogAuthorship, compareLogAuthorship } 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";
|
|
5
7
|
import { countProjectAsmdefs, countAssembliesWithErrors } from "./asmdef-discovery.js";
|
|
6
8
|
import { buildCapabilities } from "./capabilities/build-capabilities.js";
|
|
@@ -8,11 +10,12 @@ import { RULE_CATALOG, FIX_CATALOG } from "./capabilities/rule-catalog.js";
|
|
|
8
10
|
import { GROUP_IDS, TOOL_GROUPS, AUTO_ACTIVATE_GROUPS, groupToTools, getGroup, } from "./capabilities/tool-groups.js";
|
|
9
11
|
import { recommendGroups, } from "./capabilities/intent-groups.js";
|
|
10
12
|
import { listRules } from "./capabilities/list-rules.js";
|
|
13
|
+
import { viewCapabilities } from "./capabilities/capabilities-view.js";
|
|
11
14
|
import { generateSkill } from "./skill/generate-skill.js";
|
|
12
15
|
import { getKnownClientKeys } from "./skill/client-paths.js";
|
|
13
16
|
import { ALL_TOOLS } from "./tools/index.js";
|
|
14
17
|
import { lockPath, readInstanceLock, classifyInstance, isPidAlive } from "./instance-discovery.js";
|
|
15
|
-
import { PORT_ENV_VAR,
|
|
18
|
+
import { PORT_ENV_VAR, EXPECTED_BRIDGE_WIRE_CONTRACT } from "./constants.js";
|
|
16
19
|
import { findUnityForProject, readProcessCommandLine } from "./running-unity.js";
|
|
17
20
|
// M31-optimizations Plan 1 / L14 — opt-in route-logging gate. Resolved once at
|
|
18
21
|
// module load into a boolean; the per-dispatch log sites read the boolean
|
|
@@ -51,7 +54,7 @@ const defaultHubBackend = {
|
|
|
51
54
|
setInstallPath: (path) => setInstallPath(path),
|
|
52
55
|
};
|
|
53
56
|
import { BATCH_TOOL_NAMES, ALWAYS_BATCH_TOOLS } from "./batch-spawn.js";
|
|
54
|
-
import { readProfileAndDetail, applyPaging, attachPagination, foldVerifyResult, parseResultBody, withResultBody, } from "./output-profile.js";
|
|
57
|
+
import { readProfileAndDetail, applyPaging, attachPagination, foldVerifyResult, parseResultBody, withResultBody, isOutputProfile, } from "./output-profile.js";
|
|
55
58
|
/**
|
|
56
59
|
* Single source of truth for the `_source` tag. Every route-tagging site goes
|
|
57
60
|
* through one of the two helpers below so live / offline / local stay
|
|
@@ -339,7 +342,25 @@ function groupLiveRefsByKind(paths) {
|
|
|
339
342
|
}
|
|
340
343
|
// Operator-facing "what to do next" hint for each coarse status. Kept short
|
|
341
344
|
// and action-oriented so the Validation Suite can render it inline.
|
|
342
|
-
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
|
+
}
|
|
343
364
|
switch (status) {
|
|
344
365
|
case "running":
|
|
345
366
|
return "Bridge is ready. Proceed with live-only MCP tools.";
|
|
@@ -371,7 +392,66 @@ function bridgeStatusNextStep(status) {
|
|
|
371
392
|
"Bridge) and confirm it is started.";
|
|
372
393
|
}
|
|
373
394
|
}
|
|
374
|
-
|
|
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
|
+
}
|
|
375
455
|
if (status === "dead_bridge") {
|
|
376
456
|
return {
|
|
377
457
|
tool: "unity_open_mcp_read_compile_errors",
|
|
@@ -490,10 +570,21 @@ export class ToolRouter {
|
|
|
490
570
|
}
|
|
491
571
|
// Compact drill-down reads: offline-first for text-serialized assets, fall
|
|
492
572
|
// back to live bridge for binary formats. The compressible-router handles
|
|
493
|
-
// 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.
|
|
494
581
|
if (isCompressible(toolName)) {
|
|
495
582
|
const result = await routeCompressible(toolName, args, live, this.modelCache, this.projectPath);
|
|
496
|
-
|
|
583
|
+
const source = parseResultBody(result)?._source;
|
|
584
|
+
const meta = source === "offline" || source === "local"
|
|
585
|
+
? { route: source }
|
|
586
|
+
: { route: "live" };
|
|
587
|
+
return injectRouteMeta(result, meta);
|
|
497
588
|
}
|
|
498
589
|
const canBatch = this.batch.isBatchTool(toolName);
|
|
499
590
|
const isPing = toolName === "unity_open_mcp_ping";
|
|
@@ -561,6 +652,11 @@ export class ToolRouter {
|
|
|
561
652
|
mode: "batch",
|
|
562
653
|
compiling: false,
|
|
563
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",
|
|
564
660
|
_route: {
|
|
565
661
|
route: "batch",
|
|
566
662
|
fallbackReason: "live_unavailable",
|
|
@@ -700,6 +796,16 @@ export class ToolRouter {
|
|
|
700
796
|
// newest Library/ScriptAssemblies/*.dll mtime against Assets/**/*.cs source
|
|
701
797
|
// mtimes and surface staleAssembly when any source is newer than the DLL.
|
|
702
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;
|
|
703
809
|
// feedback-04-08-opus §2 — log-authorship cross-check. A batch run of a
|
|
704
810
|
// NEWER Unity writes a log whose errors (e.g. an API deprecation that is
|
|
705
811
|
// only an error on that version) CANNOT occur in the live editor. Parse the
|
|
@@ -729,7 +835,17 @@ export class ToolRouter {
|
|
|
729
835
|
: "no_errors_found";
|
|
730
836
|
const errorsMayNotApply = health.unhealthy &&
|
|
731
837
|
(!!staleLog || authorshipCheck.versionMismatch);
|
|
732
|
-
|
|
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;
|
|
733
849
|
// specs/feedback.md 2026-08-12 — partial-compile signal. Unity compiles
|
|
734
850
|
// assemblies in dependency order; when an early assembly fails the pipeline
|
|
735
851
|
// may not reach dependent ones, so the reported errors can be a SUBSET of
|
|
@@ -749,6 +865,54 @@ export class ToolRouter {
|
|
|
749
865
|
const assembliesWithErrors = partialCompileLikely
|
|
750
866
|
? countAssembliesWithErrors(errors.map((e) => e.file), this.projectPath)
|
|
751
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;
|
|
752
916
|
return sourceResult({
|
|
753
917
|
status,
|
|
754
918
|
// When the errors may not apply (stale log / version mismatch), the
|
|
@@ -758,14 +922,11 @@ export class ToolRouter {
|
|
|
758
922
|
// log verdict is preserved in `logVerdict` for callers that want the
|
|
759
923
|
// literal signal regardless of freshness.
|
|
760
924
|
unhealthy: errorsMayNotApply ? false : health.unhealthy,
|
|
761
|
-
headline
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
`group — ${TYPED_EDITOR_ACTIVATE_INSTRUCTION} first) confirms them.`
|
|
767
|
-
: health.headline,
|
|
768
|
-
...(errorsMayNotApply ? { logVerdict } : {}),
|
|
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 } : {}),
|
|
769
930
|
errorCount: errors.length,
|
|
770
931
|
errors,
|
|
771
932
|
// specs/feedback.md 2026-08-12 — present only when there are errors AND
|
|
@@ -816,6 +977,26 @@ export class ToolRouter {
|
|
|
816
977
|
staleAssemblyNewerSources: staleAsm.newerSources,
|
|
817
978
|
}
|
|
818
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
|
+
: {}),
|
|
819
1000
|
// feedback-04-08-opus §2 — log authorship. Emitted whenever the header
|
|
820
1001
|
// was parseable (the live editor's long tail usually has no header, so
|
|
821
1002
|
// these stay absent in the common case). On a version mismatch the hint
|
|
@@ -945,6 +1126,12 @@ export class ToolRouter {
|
|
|
945
1126
|
? args.kind
|
|
946
1127
|
: undefined;
|
|
947
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";
|
|
948
1135
|
// M18 Plan 2 / T18.2.3 — when the bridge is live, probe its compiled-
|
|
949
1136
|
// state tool inventory so per-group `available` reflects whether each
|
|
950
1137
|
// domain dependency compiled in (e.g. UNITY_OPEN_MCP_EXT_NAVIGATION).
|
|
@@ -970,7 +1157,11 @@ export class ToolRouter {
|
|
|
970
1157
|
fixes: FIX_CATALOG,
|
|
971
1158
|
availableBridgeTools,
|
|
972
1159
|
}, { kind, includePlanned });
|
|
973
|
-
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");
|
|
974
1165
|
}
|
|
975
1166
|
async routeListRules(args) {
|
|
976
1167
|
const assetKind = typeof args.asset_kind === "string" ? args.asset_kind : undefined;
|
|
@@ -1474,8 +1665,52 @@ export class ToolRouter {
|
|
|
1474
1665
|
if (proc)
|
|
1475
1666
|
unityProcessPid = proc.pid;
|
|
1476
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
|
+
}
|
|
1477
1709
|
let status;
|
|
1478
|
-
if (
|
|
1710
|
+
if (wedge !== null) {
|
|
1711
|
+
status = "wedged";
|
|
1712
|
+
}
|
|
1713
|
+
else if (classification === "dead_bridge") {
|
|
1479
1714
|
status = "dead_bridge";
|
|
1480
1715
|
}
|
|
1481
1716
|
else if (compiling) {
|
|
@@ -1517,7 +1752,13 @@ export class ToolRouter {
|
|
|
1517
1752
|
// Safe Mode / compile failure", not the generic "stopped" the bare
|
|
1518
1753
|
// status used to suggest.
|
|
1519
1754
|
classification,
|
|
1520
|
-
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 } : {}),
|
|
1521
1762
|
instance: {
|
|
1522
1763
|
lockPath: lockOnDisk,
|
|
1523
1764
|
classification,
|
|
@@ -1535,10 +1776,15 @@ export class ToolRouter {
|
|
|
1535
1776
|
isPlaying: pingBody?.isPlaying ?? null,
|
|
1536
1777
|
unityVersion: pingBody?.unityVersion ?? null,
|
|
1537
1778
|
bridgeVersion: pingBody?.bridgeVersion ?? null,
|
|
1779
|
+
wireContract: pingBody?.wireContract ?? null,
|
|
1538
1780
|
mode: pingBody?.mode ?? null,
|
|
1539
1781
|
}
|
|
1540
1782
|
: { reachable: false },
|
|
1541
|
-
|
|
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),
|
|
1542
1788
|
};
|
|
1543
1789
|
// bridge_status never reports an error — even a stopped bridge is a
|
|
1544
1790
|
// successful status read. _source=local because the synthesis happens in
|
|
@@ -1894,74 +2140,110 @@ export class ToolRouter {
|
|
|
1894
2140
|
const probe = countFileDescriptors(pid);
|
|
1895
2141
|
const count = probe.count;
|
|
1896
2142
|
const approximate = "approximate" in probe ? probe.approximate : false;
|
|
1897
|
-
const
|
|
2143
|
+
const partial = "partial" in probe ? probe.partial === true : false;
|
|
2144
|
+
// The threshold softening is about WHAT the metric counts, not about how
|
|
2145
|
+
// precise the number is. Only Windows HandleCount counts more than fds; a
|
|
2146
|
+
// timed-out lsof is imprecise but still counts real fds (a lower bound),
|
|
2147
|
+
// so it takes the normal warn/critical thresholds — softening it made a
|
|
2148
|
+
// partial 900/1024 report `ok` on the very platform this tool targets.
|
|
2149
|
+
const broaderThanFds = probe.method === "handle_count";
|
|
2150
|
+
const headroom = computeFdHeadroom(count, broaderThanFds, ceiling);
|
|
1898
2151
|
const ts = Date.now();
|
|
1899
2152
|
this.sessionState.recordFdSample({ ts, pid, count });
|
|
1900
2153
|
const samples = this.sessionState.fdSamplesSnapshot();
|
|
1901
2154
|
const trend = analyzeFdTrend(samples, ceiling);
|
|
1902
|
-
//
|
|
1903
|
-
//
|
|
1904
|
-
//
|
|
1905
|
-
|
|
1906
|
-
//
|
|
2155
|
+
// Samples are per-PID (as the tool description states): after an Editor
|
|
2156
|
+
// restart the ring still holds the previous process's entries — report
|
|
2157
|
+
// only this PID's series. The trend already analyzes the latest PID only.
|
|
2158
|
+
const pidSamples = samples.filter((s) => s.pid === pid);
|
|
2159
|
+
// 4. Decide whether to raise an operator warning. Alarm on:
|
|
2160
|
+
// - a real fd count at/past the ceiling (`over_ceiling` from lsof//proc):
|
|
2161
|
+
// POSIX allocates the lowest free descriptor number, so every NEW
|
|
2162
|
+
// descriptor now lands above the ceiling and the next IOSelector
|
|
2163
|
+
// registration (any async pipe/socket — e.g. the Bee build driver)
|
|
2164
|
+
// can hang the Editor. A fresh healthy Unity 6 editor sits at ~160
|
|
2165
|
+
// fds; real counts past 1024 are never legitimate now that
|
|
2166
|
+
// countLsofFdRows excludes the cwd/txt/mmap rows that once inflated
|
|
2167
|
+
// the count.
|
|
1907
2168
|
// - a monotonic leak (the actionable trend signal), or
|
|
1908
2169
|
// - approaching the ceiling from below (warn/critical).
|
|
1909
|
-
//
|
|
1910
|
-
//
|
|
1911
|
-
//
|
|
1912
|
-
//
|
|
1913
|
-
//
|
|
2170
|
+
// The one downgraded case is a Windows HandleCount probe over the
|
|
2171
|
+
// ceiling: handle counts cover kernel/GDI/user objects and routinely
|
|
2172
|
+
// exceed 1024 on a healthy process, so it gets a `pressureNote` and
|
|
2173
|
+
// alarms only on the trend. `rising` is any net-positive delta that is
|
|
2174
|
+
// NOT a qualifying leak — it fires on ordinary drift, so it never
|
|
2175
|
+
// alarms on its own.
|
|
1914
2176
|
const fdMethod = probe.method;
|
|
1915
2177
|
const probeReason = "reason" in probe ? probe.reason : null;
|
|
1916
2178
|
const probeMessage = "message" in probe ? probe.message : null;
|
|
1917
|
-
|
|
2179
|
+
// A partial (timed-out) lsof listing is a LOWER BOUND on real fds, so an
|
|
2180
|
+
// over-ceiling verdict from it still stands; only the Windows HandleCount
|
|
2181
|
+
// metric measures something broader than fds. Same predicate that picked
|
|
2182
|
+
// the headroom thresholds above, so the two can never disagree.
|
|
2183
|
+
const overCeilingRealFds = headroom.state === "over_ceiling" && !broaderThanFds;
|
|
2184
|
+
const shouldWarn = overCeilingRealFds ||
|
|
2185
|
+
trend.state === "leaking" ||
|
|
1918
2186
|
headroom.state === "warn" ||
|
|
1919
2187
|
headroom.state === "critical";
|
|
1920
2188
|
const warningState = shouldWarn
|
|
1921
2189
|
? {
|
|
1922
|
-
level: headroom.state === "critical"
|
|
2190
|
+
level: headroom.state === "critical" || overCeilingRealFds
|
|
1923
2191
|
? "critical"
|
|
1924
2192
|
: trend.state === "leaking"
|
|
1925
2193
|
? "leaking"
|
|
1926
2194
|
: "warn",
|
|
1927
|
-
message:
|
|
1928
|
-
? `Editor fd
|
|
1929
|
-
`
|
|
1930
|
-
`
|
|
1931
|
-
`
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
`
|
|
1941
|
-
`
|
|
1942
|
-
`
|
|
1943
|
-
|
|
1944
|
-
`
|
|
1945
|
-
|
|
1946
|
-
|
|
2195
|
+
message: overCeilingRealFds
|
|
2196
|
+
? `Editor fd count (${count}) is at or above the ${ceiling}-descriptor ` +
|
|
2197
|
+
`Mono IOSelector ceiling. Every newly allocated descriptor number now ` +
|
|
2198
|
+
`lands above the ceiling, so the next async pipe/socket registration ` +
|
|
2199
|
+
`(e.g. the Bee build driver's IPC) can hang the Editor mid-build` +
|
|
2200
|
+
(trend.state === "leaking"
|
|
2201
|
+
? ` — and the count is still climbing (trend delta ${trend.delta} ` +
|
|
2202
|
+
`over ${trend.sampleCount} sample(s))`
|
|
2203
|
+
: "") +
|
|
2204
|
+
`. Save scene work and restart Unity via the Hub now.`
|
|
2205
|
+
: headroom.state === "critical"
|
|
2206
|
+
? broaderThanFds
|
|
2207
|
+
? `Windows handle count is at ${Math.floor(headroom.pressureRatio * 100)}% ` +
|
|
2208
|
+
`of the ${ceiling}-descriptor ceiling proxy. HandleCount covers ` +
|
|
2209
|
+
`kernel/GDI/user objects — far broader than fds — so this is a soft ` +
|
|
2210
|
+
`signal, not fd risk: watch the trend, and treat a monotonic climb ` +
|
|
2211
|
+
`(trend.state "leaking") as the real alarm.`
|
|
2212
|
+
: `Editor fd usage is at ${Math.floor(headroom.pressureRatio * 100)}% of ` +
|
|
2213
|
+
`the ${ceiling}-descriptor ceiling — the next domain reload is ` +
|
|
2214
|
+
`likely to trip the Bee build-driver hang. Save scene work and restart ` +
|
|
2215
|
+
`Unity via the Hub now, before the Editor hangs.`
|
|
2216
|
+
: trend.state === "leaking"
|
|
2217
|
+
? headroom.state === "over_ceiling"
|
|
2218
|
+
? `Editor handle count (${count}) is ABOVE the ${ceiling} ceiling ` +
|
|
2219
|
+
`proxy and climbing monotonically (leak in progress): trend ` +
|
|
2220
|
+
`delta ${trend.delta} over ${trend.sampleCount} sample(s). ` +
|
|
2221
|
+
`Save scene work and restart Unity via the Hub now.`
|
|
2222
|
+
: `Editor fd usage is climbing monotonically across samples (leak in ` +
|
|
2223
|
+
`progress): trend delta ${trend.delta} over ${trend.sampleCount} ` +
|
|
2224
|
+
`sample(s). Save scene work and plan a restart before the count ` +
|
|
2225
|
+
`crosses the ${ceiling}-descriptor ceiling.`
|
|
2226
|
+
: `Editor fd usage is at ${Math.floor(headroom.pressureRatio * 100)}% of ` +
|
|
2227
|
+
`the ${ceiling}-descriptor ceiling. Monitor the trend; if it keeps ` +
|
|
2228
|
+
`climbing across domain reloads, save scene work and restart Unity ` +
|
|
2229
|
+
`via the Hub before the Editor hangs.`,
|
|
1947
2230
|
}
|
|
1948
2231
|
: null;
|
|
1949
|
-
// Informational
|
|
2232
|
+
// Informational note for the remaining soft case: a Windows HandleCount
|
|
2233
|
+
// over the ceiling proxy with no qualifying leak trend.
|
|
1950
2234
|
const pressureNote = !shouldWarn && headroom.state === "over_ceiling"
|
|
1951
|
-
? `
|
|
1952
|
-
`proxy, but
|
|
1953
|
-
`
|
|
1954
|
-
`
|
|
1955
|
-
`
|
|
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.`
|
|
2235
|
+
? `Windows handle count (${count}) is above the ${ceiling} fd-ceiling ` +
|
|
2236
|
+
`proxy, but HandleCount covers kernel/GDI/user objects — far broader ` +
|
|
2237
|
+
`than Unix fds — so this is expected on a healthy process. Treat it as ` +
|
|
2238
|
+
`a concern ONLY if the trend becomes a monotonic leak (leaking) — ` +
|
|
2239
|
+
`re-sample after the next domain reload to establish one.`
|
|
1959
2240
|
: null;
|
|
1960
2241
|
return sourceResult({
|
|
1961
2242
|
pid,
|
|
1962
2243
|
fdCount: count,
|
|
1963
2244
|
fdMethod,
|
|
1964
2245
|
approximate,
|
|
2246
|
+
...(partial ? { partial } : {}),
|
|
1965
2247
|
ceiling,
|
|
1966
2248
|
ceilingSource,
|
|
1967
2249
|
headroom: headroom.headroom,
|
|
@@ -1969,12 +2251,12 @@ export class ToolRouter {
|
|
|
1969
2251
|
state: headroom.state,
|
|
1970
2252
|
reliable: headroom.reliable,
|
|
1971
2253
|
trend,
|
|
1972
|
-
samples:
|
|
2254
|
+
samples: pidSamples.map((s) => ({
|
|
1973
2255
|
ts: s.ts,
|
|
1974
2256
|
pid: s.pid,
|
|
1975
2257
|
count: s.count,
|
|
1976
2258
|
})),
|
|
1977
|
-
sampleCount:
|
|
2259
|
+
sampleCount: pidSamples.length,
|
|
1978
2260
|
launchContextCaveat: LAUNCH_CONTEXT_CAVEAT,
|
|
1979
2261
|
...(probeReason !== null ? { probeReason } : {}),
|
|
1980
2262
|
...(probeMessage !== null ? { probeMessage } : {}),
|