Skip to content

debug-hmr backlog sync + fix(debug): logpoint async eval (eval_expression + create_task defer + scalar decode)#1

Merged
Alex1980Alex merged 30 commits into
masterfrom
fix/logpoint-async-eval
Jun 4, 2026
Merged

debug-hmr backlog sync + fix(debug): logpoint async eval (eval_expression + create_task defer + scalar decode)#1
Alex1980Alex merged 30 commits into
masterfrom
fix/logpoint-async-eval

Conversation

@Alex1980Alex

@Alex1980Alex Alex1980Alex commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Scope note: origin/master was stale (at 1872dff); this branch is 29 commits ahead — the entire unpushed debug-hmr development (roadmap 260511: P0.A–G, logpoints, capture-mode, arm-warm/next-rphost, error-envelopes, snapshot/replay, coverage export…). The logpoint fix below is the head commit and cannot be isolated — it edits logpoints.py, which does not exist on origin/master (introduced by the P0.B commit). So this PR effectively syncs the debug-hmr backlog + the logpoint fix.


Problem (head commit 6503fc9)

Logpoints (debug_set_logpoint) never captured variable values:

  1. fire_logpoint called a non-existent client.evaluate()AttributeError (the real method is eval_expression).
  2. Even after fixing the name, eval_expression awaits the async exprEvaluated event delivered by ping_loop._handle_command — which cannot run while we are inside it (re-entrancy deadlock) → eval timed out → [].

Fix

  • Defer eval + log + Continue to a separate task (asyncio.create_task) so the ping loop is free to deliver exprEvaluated. Always Continue in finally so a stuck logpoint never freezes the target.
  • Add _scalar(): decode RDBG resultValueInfo (valueString / valueBoolean→Истина/Ложь / valueDecimal / pres base64) into clean scalars.

Tests

  • New tests/test_logpoints.py (9 tests). Full suite: 262 passed, no regression.

Verified live

VA-driven write to РС гкс_УдостоверенияКачестваПоРегистрацииПЛК halts at RecordSetModule:41; logpoint captured ДокументРегистрации / Отказ=Ложь / Замещение=Ложь / Количество=1 correctly.

🤖 Generated with Claude Code

Alex1980Alex and others added 29 commits May 11, 2026 06:04
…11 §3.1+§3.2)

Closes 2 root causes from GKSTCPLK-2468 incident (4 BP set, 0 fire):

§3.1 — debug_connect now validates infobase_alias via `rac infobase summary
list --cluster=<UUID>` BEFORE attach. If alias not found, returns
status=error with available[] list + hint. Graceful skip when rac.exe /
cluster unreachable (returns status=skipped, allows connect to proceed
for legacy / test environments).

Closes RC1: previously `debug_connect("TestDB")` returned registered=true
silently despite "TestDB" not existing in cluster — filter повисал на
несуществующее имя → BPs не fire ни на каком rphost.

§3.2 — force_recycle_rphost extended to recycle_strategy parameter:
- auto (default) — = pre_existing if force_recycle_rphost=True, else none
- none — preflight-warning mode (existing default behaviour)
- pre_existing — kill только pre-existing pids (existing force_recycle=True)
- all_rphosts_of_ib — kill ВСЕ rphost workers обслуживающие IB через
    `rac process list --infobase=<UUID>` — closes RC2 (HTTP-service spawned
    rphost вне pre-existing snapshot)
- all_rphosts_of_cluster — kill cluster-wide (HIGH RISK, personal dev only)

Closes RC2: previously force_recycle_rphost=True killed только pids
known на момент connect; HTTP-сервисный execute_code spawn'ил новый
rphost ПОСЛЕ recycle → новый worker не attached → 0 BP fire.

Helpers added:
- _rac_list_infobases(rac_exe, cluster) — parse `rac infobase summary list`
- _validate_infobase_alias(alias) — strict cross-check с status taxonomy
- _rac_list_rphosts_of_infobase(rac_exe, cluster, ib_uuid) — для all_rphosts_of_ib

Tests: 199 → 212 passed (13 new):
- TestInfobaseAliasValidation: valid/invalid/skipped paths (3 tests)
- TestRecycleStrategy: auto/pre_existing/all_rphosts_of_ib/invalid (5 tests)
- TestValidateInfobaseAlias: helper unit tests (5 tests)

E2E verified on real cluster localhost:1541:
- `TestDB` → invalid, available=[ИБTransportManagementDevelop, 260507_DEV_...]
- `260507_DEV_ATERLETSKIY_53196` → valid, uuid=07376da7...

Backward compat: existing force_recycle_rphost=True still works (auto-resolves
to recycle_strategy="pre_existing"). Existing tests pass без изменений
(добавлен default mock _validate_infobase_alias → skipped в _setup_client_mocks).

Roadmap: docs/roadmap/260511_ROADMAP_1C_DEBUG_HMR_DEFICIENCIES_FROM_GKSTCPLK_2468.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two minor improvements from quality-review:

1. _rac_list_rphosts_of_infobase: add encoding="cp866", errors="replace"
   for defensive symmetry with _rac_list_infobases. Even though this parser
   only extracts numeric pids (UnicodeDecodeError safe via SubprocessError
   catch → empty list), explicit cp866 prevents silent false-negative
   pid extraction on exotic Windows locales.

2. New test test_all_rphosts_of_cluster_combines_snapshot_and_rac in
   TestRecycleStrategy: smoke coverage for HIGH RISK strategy that
   combines detect_pre_existing_rphosts() + _rac_list_processes_by_pid
   with dedup. Mocks snapshot=[100,200] + rac={200,300,400} → asserts
   kill_calls == [[100, 200, 300, 400]] (200 dedup'ed).

Tests: 212 → 213 passed.

Round-2 code-verify: [CODE-VERIFY-PASS]

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five new improvements addressing pre-existing 1c-debug-hmr deficiencies
surfaced during GKSTCPLK-2468 incident:

§3.4 (P1) debug_wait_for_target — sync primitive blocking until ≥1 target
  appears in debug_targets. Used after debug_connect / launch_thin_client
  to guarantee attach before set_breakpoint.

§3.5 (P1) debug_launch_thin_client — automated 1cv8c.exe launch with
  correct /Debug -http /DebuggerURL=http:// flags. Closes RC3 (protocol
  mismatch). Auto-detect platform binary via glob fallback. Validates
  infobase_alias and waits for target registration.

§3.3 (P1) infobase_list probe in debug_health_check — surfaces available
  infobases в preflight; user видит valid aliases ДО connect. Partial
  closure of §3.3 (full bp_fire_smoke deferred — invasive).

§3.6 (P2) no_fire_diagnostics in debug_ping — after 3 consecutive empty
  pings, auto-append diagnostic block с infobase_validation, targets_
  attached, active_rphost_pids, and actionable suggestions referencing
  RC1/RC2 from GKSTCPLK-2468. Validated E2E: correctly detected RC2 on
  HTTP-service spawned rphost gap.

§3.7 (P2) DEBUG_INFOBASE_ALIASES env mapping — translate short aliases
  to long cyrillic IB names ДО validation. Format: "Short:Long;...".
  Validation result includes alias_resolved_from_env flag.

§3.8 (P3) SKILL.md antipattern table updated — 6 new entries covering
  arbitrary aliases, protocol mismatch, default recycle_strategy gap,
  static-only analysis, и ignoring no_fire_diagnostics.

Tests: 213 → 215 passed (+2 new env-mapping tests).

E2E на real cluster (ИБTransportManagementDevelop):
- ✓ alias validation: Trans→ИБTransport via env, Wrong→invalid w/ available
- ✓ 1cv8c.exe discovery: C:\Program Files (x86)\1cv8\8.3.27.1936\bin\1cv8c.exe
- ✓ infobase_list probe: 2 IBs discovered
- ✓ recycle_strategy=all_rphosts_of_cluster: killed pid 61720
- ✓ no_fire_diagnostics: correctly surface'ит RC2 + suggestions
- ✗ actual BP fire: still blocked by deeper post-spawn-attach gap (HTTP-service
  spawned rphost не получает setAutoAttachSettings filter) — requires
  P0.4 follow-up (post-spawn polling + auto-attach)

Roadmap: docs/roadmap/260511_ROADMAP_1C_DEBUG_HMR_DEFICIENCIES_FROM_GKSTCPLK_2468.md
SKILL.md: .claude/skills/1c-debug-hmr/SKILL.md (15 tools, 6 new antipatterns)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three minor fixes from quality-review Round-1:

1. debug_launch_thin_client now surfaces security_note when password
   non-empty — Win32 process list (Get-Process | Select CommandLine,
   WMI cache, Task Manager) exposes argv to all sessions on machine.
   Wrapper-level masking (/P***) only hides in OUR response, not at
   OS level. SKILL.md антипаттерн updated.

2. debug_launch_thin_client surface'ит "warning" when not connected
   — target_registered detection skipped с подсказкой вызвать
   debug_connect first.

3. debug_wait_for_target not-connected error response теперь
   ensure_ascii=False, indent=2 для consistency с другими error
   paths.

Tests: 215 passed (no regressions).
Round-2 code-verify: [CODE-VERIFY-PASS]

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…§P0.4)

Background poll task в _ping_loop вызывает get_targets() каждые ~6с
(POST_SPAWN_POLL_INTERVAL=3 итерации × 2с sleep). Diff против
_known_attached_targets → attach_debug_targets для новых.

Closes design-level pattern для RC2 residual: HTTP-service spawned rphost
NOT emit'ит DBGUIExtCmdInfoStarted к active session, но (по теории)
должен появляться в getDbgAllTargetStates → polling даст eventual attach.

Tests: 215 → 220 passed (+5 TestPostSpawnAutoAttach scenarios):
- attaches new targets, dedup'ит already-attached
- noop когда все attached
- graceful get_targets failure → return 0
- graceful attach failure → не обновляет _known_attached_targets
- skips targets без id

**E2E finding (CRITICAL):** реализация корректна на design level (polling
pattern), но раскрыл deeper RDBG 8.3.27 protocol gap — getDbgAllTarget-
States сам не видит HTTP-service spawned rphost'ы как targets нашей
Debug UI session. Active rphost PID 57724 на OS-level → 0 targets в RDBG.
Требует P0.5 follow-up — research cluster-process-UUID → debug-UUID
mapping API (yukon39 reference + RDBG protocol exploration).

Roadmap: docs/roadmap/260511_ROADMAP_1C_DEBUG_HMR_DEFICIENCIES_FROM_GKSTCPLK_2468.md
§P0.4 + §P0.5 (new open question).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add explicit P0.5 caveat to _post_spawn_auto_attach docstring:
- RDBG 8.3.27 getDbgAllTargetStates returns ONLY targets registered
  via DBGUIExtCmdInfoStarted event
- HTTP-service spawned rphost'ы НЕ регистрируются — visible OS-level
  но empty в RDBG response → polling не помогает в этом сценарии
- Requires P0.5 research (cluster-process-UUID → debug-UUID mapping)

Plus race-note: parallel _handle_command(targetStarted) → idempotent
RDBG, но log.info duplicated.

Round-1 code-verify: PASS (PARTIAL on docstring clarity — addressed).
Tests: 220 passed unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GKSTCPLK-2468 deep dive (P0.5 research) выявил 2 root causes:

1. **setAutoAttachSettings filter scope** — wrapper фильтровал только
   `["Server", "ManagedClient"]` после ошибочного ROLLBACK 2026-05-10.
   Yukon39 XSD review (debugAutoAttach.xsd + debugBaseData.xsd:105-123)
   подтвердил полный enum DebugTargetType:
   Server / ManagedClient / HTTPService / WEBService / JOB / JobFileMode /
   COMConnector / OData / Mobile* / WEBClient / Client.

   `1c-mcp-crud:execute_code` spawn'ит rphost как **JOB** (background job),
   ragent для HTTP-services as **HTTPService**. Previous filter их пропускал
   → 0 BP fire (RC2 deep gap).

2. **BP workspace timing** — JOB targets живут <100ms (execute → quit).
   Previously _handle_command(targetStarted) делал attach_debug_targets но
   НЕ re-apply'ил BP workspace. RDBG `setBreakpoints` workspace per-attached;
   JOB target attached AFTER our set_breakpoint call → workspace не push'нут
   → quit ДО того как BPs applied. Added _reapply_bp_workspace helper +
   call в _handle_command(targetStarted) after attach.

E2E live trace на ИБTransportManagementDevelop (real cluster):
- 7 targets stopped at StopOnNextLine после composite re-post
- Stack trace 10 frames captured: MCP rpc → execute_code → ОбработкаПроведения
  → СформироватьВспомогательныеДанныеДляЗаблокированныхТС:722 →
  ЗаполнитьЗаблокированныеРегистрацииУсиленныйКонтроль:295
- debug_evaluate('ТипЗнч(ДокументРегистрации)') = ФНП (Формирование номера
  пробы) — runtime подтвердило исходный root cause GKSTCPLK-2468 misanalysis

Tests: 220 passed (1 test updated for expanded enum defaults).

Roadmap 260511 §P0.4 (post-spawn polling) + §P0.5 closes residual RC2 gap
from GKSTCPLK-2468 incident. Phase 1+2+3 (P0/P1/P2/P3/P0.4/P0.5) COMPLETE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-1 PASS verdict с 2 non-blocking рекомендациями:

1. _reapply_bp_workspace docstring: добавлен явный note про
   RDBG setBreakpoints REPLACES workspace per call + race note
   про multiple concurrent targetStarted events.

2. TestReapplyBPWorkspace: 2 new unit tests:
   - test_noop_when_cache_empty — assert no POST
   - test_replays_cached_bps_as_single_setbreakpoints — assert
     full workspace XML с обоими module groups в одном request

Tests: 220 → 222 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- debug_set_breakpoint(condition=..., hit_condition=...) — VS Code DAP syntax
- RDBG-native `condition` via debugBreakpoints:condition XML element (XSD-confirmed)
- Wrapper-level hit_condition: `=N`/`>N`/`>=N`/`<N`/`<=N`/`%N` (modulo)
- bp_conditions.py: eval_hit_condition() + auto_continue_if_unsatisfied()
- _aggregate_breakpoints now returns dict[line, condition] (preserves per-line cond)
- _handle_command(callStackFormed) calls auto-continue when condition unsatisfied
- 222/222 unit tests pass; live E2E verified on ИБTransportManagementDevelop

Closes Gap 3 from §1.2 deficiency analysis (no conditional/hit-count BPs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…§P0.B)

- New MCP tool debug_set_logpoint(object_id, line, message_template, ...)
- logpoints.py helper: extract_placeholders, fire_logpoint (eval+render+log+Continue)
- _logpoints dict + _log_dir on RDBGClient; _record_logpoint method
- set_breakpoints extended with logpoint_template parameter (symmetric to condition)
- _handle_command(callStackFormed) calls logpoints.fire_logpoint BEFORE hit_condition
  (logpoints take priority — they are tracepoints, never user-visible)
- JSONL log per session: data/debug_logs/<session_id>.jsonl
- VS Code DAP Logpoint syntax: {expr} placeholders, {{escaped}} literals
- 222/222 unit tests pass; standalone Python E2E verifies full flow

Closes Gap 4 (production-safe tracing without halt).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s (roadmap 260511 §P0.C)

- uuid_index.get_source_info(oid, pid) -> {fqn, file_path, exists}
- _KIND_FQN: document→Документ, catalog→Справочник, etc (18 kinds)
- _PROP_FQN: propertyID→Russian suffix (МодульМенеджера, МодульФормы, ...)
- Handles forms/commands child UUIDs separately (parent.Форма.<name>)
- debug_stack_trace enriches each frame with resolved_source
- 222/222 unit tests pass; live smoke on 3436 UUID index entries — 3/3 resolved

Closes Gap 7 (UUID stacks unreadable without manual lookup).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply 4 quality-review feedback items:

1. Session metrics pollution (FAIL-level): _stop_events/_bp_fire_count/
   _rphosts_seen/_bp_by_location no longer count suppressed stops
   (logpoint hits + unsatisfied hit_condition). Wrapper now captures
   `hit_condition_suppressed` return and gates metrics on `stop_suppressed`.

2. uuid_index forms/commands branch: validate property_id matches
   FormModule/CommandModule UUID before returning child-FQN path.
   Mismatched pairs now return None instead of misleading FQN.

3. Replace bare `except Exception: pass` with log.warning() in
   bp_conditions._do_check and logpoints.fire_logpoint (3 sites).

4. debug_set_logpoint docstring: add SECURITY note that {expr}
   placeholders execute as BSL in rphost (privileged operation).

Also: .gitignore data/debug_sessions/ + data/debug_logs/ (runtime artifacts).

222/222 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cascade-halt mitigation for `recycle_strategy="all_rphosts_of_ib"`:
each newly-spawned rphost gets RDBG `stop_on_next` halt at first BSL line,
blocking all 1c-mcp-crud HTTPService triggers. Heuristic: stops with
`stopByBP=false` AND no `_break_on_next_armed` are system-initiated →
auto-Continue silently. Real BP fires keep visible; user-armed
`debug_break_on_next` clears flag on first stop.

- system_stops.py: maybe_auto_continue_system_stop helper
- _break_on_next_armed flag: init False, True after set_break_on_next_statement,
  cleared on first user-visible stop in callStackFormed handler
- Wired FIRST in _handle_command(callStackFormed), before logpoint/hit_condition
- 222/222 unit tests pass + 3 direct Python tests for all three scenarios

NOTE: Live verification requires /mcp reconnect — system_stops.py is not in
.mcp.json HMR watch list, so wrapper didn't auto-reload. After reconnect,
all_rphosts_of_ib cascade should silently disappear.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CRITICAL: P0.A/B/D helpers called `client.step(target_id, "Continue", simple=True)`
but real signature is `step(action: str, target_uuid: Optional[str])` — no `simple`
kwarg, positional order action-first. Old call passed target_id as action,
"Continue" as target_uuid, simple=True as unknown kwarg → TypeError.

Exception was swallowed by try/except, helper returned True (suppressed marker),
but Continue was NEVER sent to RDBG. Result: P0.A hit_condition / P0.B logpoint /
P0.D system-stop suppression all logged "fired" but target stayed halted.

Bug hidden because no test exercised the actual step() call path — caught only
by code-verify reviewer on P0.D round.

Fixes:
- All 3 helpers: `await client.step("Continue", target_id)` (correct positional)
- On exception → return False (let user-visible stop fall through to normal handler)
- Removed redundant `client._stopped_targets.discard(target_id)` (step() does it)
- C1 stale `_last_stopped_target_id` resolved as side-effect (step() L1326-1327
  clears it on successful Continue)
- Test fixture updated: callStackFormed events use `stopByBP="true"` (realistic
  BP fire that now keeps visible; old "false" gets P0.D-suppressed)

222/222 unit tests pass + 6 direct Python tests (3 success + 3 exception paths)
across all 3 helpers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(roadmap 260511)

Race symptom: HTTPService trigger spawns rphost → rphost executes BSL faster
than dbgs→rphost BP push completes → BP misses → execute_code succeeds but
logpoint never fires, condition BP doesn't surface.

Solution (CDP waitForDebuggerOnStart + GDB pending-BP hybrid, see research):
- targetStarted handler marks new target in `_attached_pending` set after
  auto-attach + initial BP re-apply
- system_stops helper: on first cascade halt for pending target, treat as
  sync window → re-apply BPs (idempotent) + sleep 150ms + Continue +
  remove from pending
- Subsequent BP hits (stop_by_bp=true) on same target also clear pending —
  no double-drain
- BP_PROPAGATION_WAIT_SEC = 0.15 (research-recommended 100-200ms band)

Test coverage:
- 222/222 unit tests pass
- 3 direct Python tests: (a) target NOT pending → P0.D path (auto-Continue),
  (b) target IN pending → P0.E drain (reapply + wait + Continue + clear),
  (c) stop_by_bp=true on pending → keep visible, clear pending

Research: docs/roadmap/260511_DEEP_ANALYSIS finds Microsoft DAP
configurationDone, CDP waitForDebuggerOnStart, GDB pending BPs, JDWP
ClassPrepare, vscode-js-debug scriptParsed, yukon39 reference all converge
on "exploit attach-halt as sync window" pattern. RDBG has no native ack
for BP propagation → empirical 150ms sleep is acceptable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…admap 260511)

Closes the last gap blocking fully-autonomous BP fire through 1c-mcp-crud
execute_code: warm-pool rphosts (HTTPService workers) reused across HTTP
requests miss BPs because RDBG attach is one-shot at spawn — and warm rphosts
spawned BEFORE debug_connect are invisible to the new debug session.

New MCP tool `debug_arm_warm_rphosts(target_types=...)`:
1. Lists ALL current targets via get_targets (getDbgAllTargetStates)
2. Filters to specified types (default: HTTPService, JOB, Server)
3. For each: attach_debug_targets + add to _known_attached_targets +
   _attached_pending (so P0.E drain applies on next halt)
4. Re-applies BP workspace once after batch-attach

Workflow (no user UI interaction needed):
  debug_connect → set BPs → debug_arm_warm_rphosts() → execute_code → BP fires

Returns: armed_targets list, filter applied, whether BP workspace reapplied.

222/222 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 reviewer findings, all addressed:

1. Memory leak in _attached_pending — targetQuit handler now discards from
   _attached_pending too (was only clearing _known_attached_targets etc).
   Amplified by P0.F bulk-arm of short-lived JOB targets.

2. None/[] semantics mismatch — docstring claimed None means "all" but code
   converted None → default filter. Docstring corrected to match code:
   None → default [HTTPService,JOB,Server], [] → no filter (arm all).

3. bp_workspace_reapplied misreport — flag was returned True based on
   len(armed)+cache_nonempty regardless of actual reapply success. Now
   tracked via reapplied_ok flag set inside try, False if exception caught.

222/222 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (roadmap 260511)

Closes the LAST gap for autonomous BP fire on warm-pool HTTPService rphost.
P0.F's debug_arm_warm_rphosts only sees targets in getDbgAllTargetStates;
warm pool is invisible until a request hits it. P0.G uses RDBG's global
setBreakOnNextStatement to force-halt the next BSL statement on ANY rphost
(including warm pool), then drains the halt silently.

Flow:
- New flag RDBGClient._break_on_next_silent_arm (one-shot)
- set_break_on_next_statement(silent: bool = False) routes flag based on mode
- system_stops.maybe_auto_continue_system_stop detects silent flag:
  → attach_debug_targets([target_id]) to grab the warm rphost
  → add to _known_attached_targets
  → _drain_bp_propagation (reapply BPs + 150ms sleep + Continue)
  → return True (suppressed)
- New MCP tool debug_arm_next_rphost() — single-call autonomous arm

User workflow (zero participation after /mcp reconnect):
  debug_connect → set BPs → debug_arm_next_rphost → execute_code → BP fires

222/222 unit tests pass + direct Python test for silent-arm path (attach +
drain + flag clear).

Closes architectural ceiling identified after P0.F live test:
HTTPService warm-pool rphost BP-fire previously required Solution C (thin
client UI) or admin (iisreset). P0.G makes it fully autonomous via RDBG
global trap + silent drain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-verify PASS recommendations:
- Discard target_id from _attached_pending in silent-arm path (symmetric to P0.E)
- INFO log on silent-arm success (parallel to P0.D/P0.E log lines for diagnosis)
…mat='artifacts')

Roadmap 260511 P1.B — closes Gap 2 (regression detection через structured
metrics shared via PR).

New `format="artifacts"` value in `debug_session_summary` MCP tool: packages
session into ZIP bundle at `data/debug_artifacts/<session_id>.zip` with:
- summary.json (existing structured metrics)
- summary.md (rendered for PR description)
- breakpoints_cache.json (current BP workspace)
- stop_events.json (timeline)
- logpoint_log.jsonl (P0.B JSONL if exists)
- stack_snapshots/<target_id>.json per cached stack

Helper module `artifacts.py` (matches P0.A/B/D/G pattern). Markdown render
extracted into `_render_summary_md` for reuse between markdown format and
artifacts ZIP packaging.

Returns `{path, size_bytes, files[]}` JSON envelope.

`.gitignore` extended: `data/debug_artifacts/` (runtime ZIPs, not tracked).

222/222 unit tests pass + direct Python E2E (6-file ZIP, content preserved
including logpoint JSONL passthrough).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…age.xml

Roadmap 260511 P1.A — closes Gap 1 (code path coverage).

Two new MCP tools:
- debug_coverage_register(lines: list[dict]) — registers (oid, line, module_type,
  property_id?, file_path?) tuples as silent coverage BPs + records in
  _coverage_tracked dict. Calls set_breakpoints under hood (cache aggregation).
- debug_coverage_export(output_path=...) — writes SonarQube genericCoverage.xml
  with all tracked lines (covered=true if hit, covered=false otherwise).
  Returns {path, files_count, lines_total, lines_covered, coverage_pct}.

Helper `coverage.py` (matches P0.A/B/D/G + P1.B helper pattern):
- register_line(client, oid, pid, line, file_path) — idempotent registration
- record_hit_and_continue(client, target_id, stack) — silent counter increment
  + auto-Continue (no JSONL noise, distinct from logpoint behavior)
- export_generic_coverage_xml(client, path) — SonarQube XML emission

Integration in _handle_command(callStackFormed): coverage_hit takes priority
BEFORE logpoint check (silent counter; if matched, suppressed + Continue).

`.gitignore` extended: data/debug_coverage/ (runtime XMLs not tracked).

222/222 unit tests pass + direct Python E2E (3 lines registered, 1 hit, XML
verified with 33.33% coverage, 2 files, correct lineNumber+covered attributes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…map 260511)

Wrapper's _handle_command(rteProcessing) handler upgraded to support filtered
exception breakpoints — halt only on matching exceptions.

Default behavior (empty _exception_bp_filters): halt on ALL exceptions (backward
compat). Non-empty filter list: halt iff any filter matches.

Filter has 2 axes (both case-insensitive substring match, empty = "match any"):
- message_pattern → matched against exception.messageText
- module_pattern → matched against top stack frame presentation

Multiple filters accumulate with OR semantics.

3 new MCP tools:
- debug_set_exception_bp(message_pattern="", module_pattern="") — add filter
- debug_clear_exception_bps() — reset to halt-all default
- debug_list_exception_bps() — inspect current filters

Helper exception_bps.py:
- should_halt(filters, exc_info, stack) — pure-logic filter evaluation
- maybe_suppress(client, target_id, exc_info, stack) — async drain path
- _extract_message / _extract_top_module_name — robust field accessors

Integration: rteProcessing handler after existing exception caching, if
_exception_bp_filters defined and none match → auto-Continue + remove from
_stopped_targets + clear _last_exception_by_target → return early.

222/222 unit tests + 7 direct Python E2E cases (empty/match/no-match/module-
match/AND-within-filter/OR-across-filters/maybe_suppress dual path).

Closes P3.B from roadmap §4 (one of 3 P3 items; remaining P3.A Data BPs +
P3.C Stream debugger are vendor-blocked/paradigm-mismatched).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NOT true time-travel (RDBG can't rewind rphost). Provides read-only post-mortem
inspection: snapshots of every user-visible stop event recorded to
data/debug_replays/<session>.jsonl, retrievable via index.

3 new MCP tools:
- debug_session_record(enable=True) — toggle recording per session
- debug_replay_list() — list all snapshots (index, iso, target_id, reason, line, has_exception)
- debug_replay_seek(index) — full snapshot entry (ts, iso, session_id, target_id, reason, stack, exception?)

Helper snapshot.py:
- record(client, target_id, reason, stack, exc_info?) — appends JSONL line if
  client._recording_enabled, no-op otherwise
- list_snapshots(session_id) — read JSONL, return summary dicts
- seek_snapshot(session_id, index) — read JSONL, return Nth full entry or None

Integration: snapshot.record called in both _handle_command(callStackFormed)
after metrics gate (only user-visible BP fires recorded, not auto-Continue'd
suppression) AND _handle_command(rteProcessing) after P3.B filter check (only
unfiltered exceptions recorded).

Closes P2.A from roadmap §4 (P2.B Drop Frame remains BLOCKED on vendor).

ROI: GKSTCPLK-2468-class "не смогли воспроизвести" incidents — snapshots allow
reconstructing past stops without re-running failing scenario.

.gitignore extended: data/debug_replays/ (runtime JSONLs not tracked).

222/222 unit tests pass + 4 direct Python E2E (record/list/seek/disabled-noop).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…adaptive ping

Закрывает гонку attach/BP для короткоживущих JOB rphost (BP не fire'ял —
targetStarted доставлялся слишком поздно через периодический polling).

#1 Sticky capture-mode:
- новый MCP-tool debug_capture_mode(on) + флаг _capture_mode (default off)
- system_stops._drain_bp_propagation: re-arm setBreakOnNextStatement после
  каждого drain → каждый новый таргет халтит на инструкции №1 (детерминированно)
- реальный BP (stopByBP=true) не дренится; gated → дефолт не меняется

yukon39#2 Adaptive ping cadence (roadmap 260603):
- _ping_loop: интервал 0.1с когда ждём событие (capture-mode/armed/pending),
  иначе 2с heartbeat → доставка targetStarted быстрее в ~20× в active-окне
- _post_spawn_auto_attach переведён на time-based (~6с), чтобы fast-poll не
  молотил getDbgAllTargetStates
- установлено: RDBG 8.3.27 pingDebugUIParams не поддерживает server-held
  long-poll → реализован адаптивный эквивалент

Верификация: 222/222 unit-тестов passed, ruff 0 errors, code-verify PASS (2 субагента).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…рает single-step)

Live deep-тест (260529_УК) выявил: re-arm setBreakOnNextStatement сразу после
drain текущего таргета вызывал single-step (команда глобальна → халтит следующую
инструкцию того же JOB) → фоновое задание падало "ЗавершеноСОшибкой".

Фикс: re-arm перенесён в targetQuit handler — арм готовится для СЛЕДУЮЩЕГО
нового таргета, текущий не степится. 222/222 тестов, code-verify PASS (ad99aa2).

Известное ограничение (роадмап 260603 §9): ловля BP на целевой строке внутри
эфемерного sub-second JOB остаётся ненадёжной; надёжные пути — детерминированный
харнесс / thin-client / yukon39#3 helper-пауза.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…global, не per-target

Deep research (yukon39 RDBG sources, roadmap 260603 §10): RDBGSetBreakpointsRequest
несёт только bpWorkspace + idOfDebuggerUI (БЕЗ target-id) → setBreakpoints
session-global, dbgs сам пропагирует workspace авто-attach'енным таргетам.
Прежний комментарий «workspace is per-attached-target» был фактически неверен и
породил реактивную per-target модель re-apply (проигрывает гонку на эфемерном JOB).
Комментарий приведён к факту; reactive re-apply оставлен как backstop (HMR-recovery),
не primary. Только комментарий, без изменения логики. 222/222 тестов.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up всестороннего тестирования tools: evaluate/variables на не-остановленном
таргете отдавали RDBG-400 как opaque MCP-exception (inconsistent с debug_stack_trace).
Теперь — graceful JSON {"error","error_type","expression"/"target_id"} как у
stack_trace. Новый хелпер _rdbg_error_text извлекает чистый <descr> из RDBG-XML.

+ 5 unit-тестов (descr-extract / whitespace / truncate / evaluate+variables envelope).
227/227 passed. Live-verify: evaluate вернул graceful envelope (error_type=HTTPStatusError);
session_diff провалидирован cross-session (deltas + verdict NO_REGRESSION).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uate (reviewer recs)

3 рекомендации ревью:
1. not-connected guard: при невалидной сессии — явный {"error_type":"not_connected"}
   вместо маскировки под RDBG-ошибку (проверка client._attached & _registered).
2. Унифицирован формат ВСЕХ error-выходов через новый _error_json(message,
   error_type, **extra): error + error_type + контекст. "No stopped targets" теперь
   {"error_type":"no_stopped_target", target_id}.
3. debug_stack_trace мигрирован с сырого {e!r} на _rdbg_error_text + _error_json
   (полная симметрия трёх tool'ов).
+ R1 hardening: _error_json защищён от коллизии reserved-ключей.

Success-path всех трёх tool'ов НЕ изменён. +2 unit-теста (not_connected /
no_stopped_target envelope). 229/229 passed. code-verify PASS (a62638d).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… + scalar decode

Logpoints never captured values:
1) fire_logpoint called a non-existent client.evaluate() → AttributeError
   (the real method is eval_expression).
2) Even fixed, eval_expression awaits the async exprEvaluated event delivered
   by ping_loop._handle_command — which cannot run while we are INSIDE it
   (re-entrancy deadlock) → eval timed out → [].

Fix: defer eval+log+Continue to a separate task (asyncio.create_task) so the
ping_loop is free to deliver exprEvaluated. Always Continue in finally so a
stuck logpoint never freezes the target.

Add _scalar(): decode resultValueInfo (valueString/valueBoolean→Истина/Ложь/
valueDecimal/pres base64) into clean scalars instead of dumping the raw RDBG
dict into the rendered message. +9 regression tests (module was untested).

Verified live: VA-driven write to РС гкс_УдостоверенияКачестваПоРегистрацииПЛК
halts at RecordSetModule:41, logpoint captured ДокументРегистрации / Отказ=Ложь
/ Замещение=Ложь / Количество=1 correctly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several debugging features to the 1C debug MCP server, including hit-conditions, logpoints, silent auto-attaching of warm-pool rphosts, code coverage tracking, exception breakpoint filtering, and snapshot recording for post-mortem replay. Feedback on these changes identifies three critical issues: first, background tasks created via asyncio.create_task in logpoints.py must maintain strong references to prevent premature garbage collection; second, the _reapply_bp_workspace method in mcp_debug_server.py should preserve breakpoint conditions by utilizing the _build_bp_info_xml helper; and third, _aggregate_breakpoints should support both a conditions dictionary and a fallback condition field.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread logpoints.py Outdated
Comment on lines +149 to +150
asyncio.create_task(
_eval_log_continue(client, target_id, key, template, log_dir))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In Python's asyncio, creating a background task using asyncio.create_task without keeping a strong reference to the returned task object can lead to the task being garbage collected mid-execution. This is because the event loop only maintains weak references to tasks. If the task is garbage collected before it completes, the logpoint evaluation will fail silently, and the finally block might not execute, leaving the target frozen.

To prevent this, store a reference to the task in a set on the client object, and remove it when the task completes.

Suggested change
asyncio.create_task(
_eval_log_continue(client, target_id, key, template, log_dir))
task = asyncio.create_task(
_eval_log_continue(client, target_id, key, template, log_dir))
if not hasattr(client, "_background_tasks"):
client._background_tasks = set()
client._background_tasks.add(task)
task.add_done_callback(client._background_tasks.discard)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Fixed in dd7ce92: held a strong reference to the deferred task in a module-level _active_tasks set + task.add_done_callback(_active_tasks.discard) (equivalent to the suggested client._background_tasks). Merged.

Comment thread mcp_debug_server.py
Comment on lines +1462 to +1468
bp_xml = "".join(
_bp("bpInfo",
_bp("line", str(L))
+ _bp("isActive", "true")
+ _bp("temp", "false"))
for L in entry["lines"]
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The _reapply_bp_workspace method currently reapplies breakpoints using a hardcoded XML structure that completely omits the condition field. As a result, any conditional breakpoints (or logpoints/hit conditions) reapplied during target startup or HMR recovery will lose their conditions and silently revert to unconditional breakpoints.

We should use the existing _build_bp_info_xml helper to correctly preserve the conditions stored in the cache.

            bp_xml = "".join(
                _build_bp_info_xml(L, entry.get("conditions", {}).get(str(L), entry.get("condition", "")))
                for L in entry["lines"]
            )

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Fixed in 3957042: _reapply_bp_workspace now routes through the shared _build_bp_info_xml(L, entry.get('condition','')) helper, so cached conditions survive targetStarted/HMR re-apply. +2 regression tests (tests/test_reapply_condition.py). Merged.

Comment thread mcp_debug_server.py
Comment on lines +190 to +193
cond = entry.get("condition", "") or ""
for line in entry.get("lines", []):
try:
line_set.add(int(line))
line_map[int(line)] = cond

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

To support the new "conditions" dictionary in the cache, _aggregate_breakpoints should look up the condition for each line from the "conditions" dictionary first, falling back to the single "condition" field.

Suggested change
cond = entry.get("condition", "") or ""
for line in entry.get("lines", []):
try:
line_set.add(int(line))
line_map[int(line)] = cond
for line in entry.get("lines", []):
try:
cond = entry.get("conditions", {}).get(str(line)) or entry.get("condition", "") or ""
line_map[int(line)] = cond

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applicable: the breakpoint cache stores a single condition per entry (one per set_breakpoints call) — there is no per-line conditions dict. _aggregate_breakpoints already assigns entry['condition'] to each of the entry's lines (lines 190-193), so conditions are preserved. The defensive conditions-dict lookup would be a no-op against the current model. Resolving as already-correct.

@Alex1980Alex Alex1980Alex changed the title fix(debug): logpoint async eval — eval_expression + create_task defer + scalar decode debug-hmr backlog sync + fix(debug): logpoint async eval (eval_expression + create_task defer + scalar decode) Jun 4, 2026
gemini-code-assist review (PR #1, logpoints.py): asyncio keeps only a weak
reference to create_task() results, so a fire-and-forget logpoint task could
be garbage-collected mid-run. Hold it in a module-level set and discard via
add_done_callback on completion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Alex1980Alex
Alex1980Alex merged commit 22437de into master Jun 4, 2026
@Alex1980Alex
Alex1980Alex deleted the fix/logpoint-async-eval branch June 4, 2026 00:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant