The 0.6.3 fix replaced 'first-non-empty-pattern-wins' with category
iteration but used a HARDCODED list of 'well-known V3.5 GUIDs' that was
wrong. None of those GUIDs matched the actual Build category, so
compile errors stayed invisible (compile_project still reported '0
errors' even when the IDE-side download path saw them).
Diagnosed via a one-shot probe injected into compile_project on
2026-04-29: dumped attrs of script_engine.system, then called
script_engine.system.get_message_categories() (the METHOD) directly.
That returned the actual 7 category GUIDs in this CODESYS V3.5 SP22
Patch 1 install:
05581bd1-66d3-4251-aff2-047cc8e9adf7 Offline Help
936e1a33-3af8-47fa-b40b-903f0ae0b6cc Application Composer
a9b26e07-6ae1-4c06-9cd1-9ddddd397a2d SVN
0a6fcb64-7f24-43c6-a6d3-f70cb5d31114 (no parameterless ctor; Git)
194b48a9-ab51-43ae-b9a9-51d3edaaddf3 Script Messages
97f48d64-a2a3-4856-b640-75c046e37ea9 Build <-- the one we needed
220493a1-f49b-4416-9a3f-a545db707cbe Additional code checks
Real fix: replace the hardcoded list in _enumerate_categories() in both
compile_project.py and get_compile_messages.py with a call to
system.get_message_categories(); label each one via
get_message_category_description(guid). Iterate per category as before.
GUIDs are now discovered at runtime so the same code works on any SP
and any locale.
Verification: injected `THIS_IS_NOT_VALID_IEC_KEYWORD;` into
PLC_PRG.implementation, ran compile_project; output now reads
"1 error(s), 2 warning(s). ERROR: Identifier
'THIS_IS_NOT_VALID_IEC_KEYWORD' not defined". Restored PLC_PRG;
0 error(s) again. End-to-end fix confirmed.
Bumped 0.6.3 -> 0.6.4.
Two real bugs surfaced today:
1. compile_project / get_compile_messages reported 0 errors even when
the IDE's download path saw real compile errors. Root cause: both
scripts queried get_message_objects() with no category arg, which
returns only the IDE's last-active message tab (typically "Other"
for the WATCHER startup messages). Build/Code-Generation errors
live in a different category and were never queried.
Fix: enumerate script_engine.system.message_categories AND probe a
set of well-known V3.5 category GUIDs (Compile, Build, Online,
LibMan); query target_app.get_message_objects(cat) and
system.get_message_objects(cat) per category; aggregate dedup'd
entries (severity, text, object, line). Each entry now carries its
originating category label.
2. add_library's post-add _is_resolved() check trusted is_placeholder
== False as proof of resolution. CODESYS lets you call
add_placeholder(name_str) for a name that is NOT in the installed
Library Repository -- the resulting reference reports
is_placeholder=False yet has empty effective_version and the IDE
shows it with a yellow-warning triangle in Library Manager (no
Effective Version column populated). Karstein hit this with
"OPC UA PubSub SL": list_project_libraries reported it as
[managed] but compile failed because the IDE couldn't resolve it.
Fix: for the non-placeholder branch, probe effective_version /
resolved_version / version / resolved_library / managed_library /
library. ALL must be empty/None for the ref to be considered
hollow. Logs a DEBUG with the ref's attribute list so the next
such bug is diagnosable without source spelunking.
Bumped 0.6.2 -> 0.6.3.
Empirical failure: TypeError 281474976710655L is not JSON serializable from get_message_objects().
Root cause: previous per-attribute coercion only flattened known fields (severity/text/line); nested dicts/lists carrying CLR longs slipped through.
Fix: add a recursive _coerce_for_json helper that walks dicts/lists/tuples and downcasts long->int (or str if >Int64), keeps bool, then call it before every json.dumps in both compile_project.py and get_compile_messages.py.
Docs: https://content.helpme-codesys.com/en/ScriptingEngine/ScriptSystem.html (get_message_objects), https://content.helpme-codesys.com/en/ScriptingEngine/ScriptObject.html
Note: previous commit 418f678 added the per-field coercers; this commit
strengthens that with a recursive walker per the bug doc's proposal --
defensive against any future API change that nests longs deeper.
Manual smoke test only -- requires CODESYS-in-the-loop with a project
that produces compile messages whose severity bitmask is 0xFFFFFFFFFFFF.
### Manual smoke test
1. Open a project with at least one warning or error.
2. Call mcp__codesys__compile_project on it.
3. Expect SCRIPT_SUCCESS and a populated COMPILE_MESSAGES_START block;
no TypeError 'is not JSON serializable' anywhere in the output.
4. Repeat with mcp__codesys__get_compile_messages.
create_folder SP21+ fallback, ensure_project_open cross-project switch)
+ bench results
compile_project.py + get_compile_messages.py:
- IronPython 2.7's json.dumps cannot serialize System.Int64-backed `long`
values, which is what CODESYS's compile-message objects expose as
line_number / position. Added _coerce_int + _coerce_str helpers and a
shared _build_message_entry function. Three duplicated message-building
blocks collapsed into single helper calls.
- Defensive `try: json.dumps(...) except TypeError: json.dumps(default=str)`
so a stray field that slips past the helpers doesn't kill the emit.
connect_to_device.py:
- SP21+ may expose the login enum as LoginMode rather than
OnlineChangeOption. Extended the candidate sweep to probe both
script_engine.LoginMode and script_engine.OnlineChangeOption AND
online_app.LoginMode/OnlineChangeOption (some builds attach it to the
app object). Added "OnlineChange" + "Login" + "Download" to the
preferred-priority list. Added a 3-arg call shape variant for SPs
that take (mode, secondary-mode, force-bool).
create_folder.py:
- parent_object.create_folder() is no longer exposed on every parent type
in SP21+. Added two fallback factories tried in order:
1. parent.create_object(typeUuid='85d1215e-6520-4983-9a55-2d39d1f24cb4', name=...)
2. parent.add(script_engine.types.IecFolder, name=...)
with detailed warnings when each path fails. Final TypeError now lists
every factory tried so a future SP rotation surfaces clearly.
ensure_project_open.py:
- Uncommented the close-prior-project branch (was a TODO since the
initial fork). Cross-project switches in a persistent CODESYS now do
save() -> close() -> 500ms pump -> open(target). Without this,
projects.open against a different already-primary project fails
intermittently on file lock contention or pops a "project in use"
modal that freezes the IDE thread.
- save() is best-effort: if it raises (transient lock, save-as required)
we still proceed with close + open rather than getting stuck in a
half-switched state forever.
tests/bench-results.json:
- Captured timings from a clean run on MCPTest2 (PLCWinNT, 5 lib refs,
~12 POUs). 9 tools x 2 modes x iterations.
Headers (mean ms): persistent vs headless --
open_project 7700 vs 40041 (~5x; first call cold)
mirror_export 1547 vs 23723 (~15x)
list_project_libraries 1565 vs 23322 (~15x)
get_all_pou_code 1607 vs 23376 (~15x)
save_project 2095 vs 23321 (~11x)
create_pou (FB) 1540 vs 23903 (~16x)
delete_object 1544 vs 27420 (~18x)
bump_project_version 1540 vs 30678 (~20x)
bump_project_version #2 1556 vs 37769 (~24x)
- set_pou_code FAILED in both modes -- bench harness param-shape issue
(multi-line code passed verbatim to triple-quoted-string interpolation
doesn't survive the round-trip). Tool itself works fine through the
MCP tool call path; bench needs to escape newlines / use the same
prepareScriptWithHelpers shape the server uses. Filed for follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.3.0 returning-watcher architecture — background thread polls for commands
and marshals execution onto the CODESYS UI thread, keeping the IDE fully
responsive between operations. File-based IPC with atomic writes, async mutex
command serialization, headless fallback, and 35 passing tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>