Skip to content

fix: an interlock may stop a running source, never disable the Start control - #114

Draft
test1card wants to merge 14 commits into
masterfrom
fix/interlock-warns-never-disables-start
Draft

test1card wants to merge 14 commits into
masterfrom
fix/interlock-warns-never-disables-start

Conversation

@test1card

Copy link
Copy Markdown
Owner

An interlock may stop a running source. It may never grey out the Start control.

The ruling

"no, it should not stop the source. nothing should disable start button. source
may be turned off if operator forgot, or if there is misbehaving, but not
disable the button!!!" — owner, 2026-08-28

What was actually wrong

The end-to-end thermal run timed out because Start never became available. The
cause was not the panel wiring: detector_warmup held stop_source at
77.5 K against a 10 K threshold, Safety went not-ready, and that greyed out
Start, the P/V/I spin boxes, and showed "Управление заблокировано".

The defect was never that the guard fired. It is that a guard could reach the
button at all. Earlier attempts at this went after the guard — retiring the row,
then adjusting its threshold — which is treating the symptom.

The change

_safety_ready carried two meanings a boolean cannot separate:

  • Safety authority is present and reports a current condition — a guard
    judging the operator;
  • no authoritative Safety state exists at all — the software is blind.

Only the first is what the ruling severs. The second is a different question and
is still open with the owner, so it is deliberately left alone.

The cause is now explicit as SafetyGateCause:

Cause Start and parameters Presentation
AUTHORITATIVE_NOT_READY stay available WARNING naming the condition and saying it may stop a running source
AUTHORITY_UNAVAILABLE stay disabled, as before "Управление заблокировано"

emergency_off is unaffected and always available.

Nothing suppresses the guard. A stop_source interlock still stops a source
that is running. The operator may start it, be told why that is questionable,
and be stopped by the guard if the condition holds — that loop is the intent,
not a bug. His remedy is to disable the interlock, which is #113.

No code branches on the wording of a Safety reason; that text is operator-facing
prose and will be reworded.

Verification

The lane's own control was a collection error, which proves a symbol is new and
little else, so the coordinator ran a behavioural mutation instead, in a
throwaway worktree:

  • Reverting the enablement predicate to the pre-ruling form
    (and self._safety_ready) reddens 6 of 12 requested nodes; restoring makes
    all 12 pass.
  • Reverting the authorization_reason term separately reddens the receipt node.
  • Tests only, without any production change: collection fails outright.

test_missing_safety_authority_still_disables_controls_except_emergency is the
control on this change — it must stay green, and it does, under both
mutations. Without it, over-severing would look like success.

Two assertions were changed, both in that one test: they pinned the old
fault_latched reason string. One isHidden() check was lost there; visibility
of the warning path is asserted by
test_authoritative_safety_condition_shows_visible_warning. Reviewer: please
confirm you are content with that trade.

Docs gate green at this head; the derived pair was regenerated as the last commit.

Measured end-to-end, stated exactly

Running the source-gate probe against this branch alone:

before: start_enabled=false  safety_ready=false  gate_reason="Interlock stop_source: detector_warmup"
after:  start_enabled=false  channel_state="unknown"  gate_reason -> "Backend readiness authority"

The interlock term is severed — it is no longer what holds the button. Start
is still disabled, now by channel_state == "unknown", which is a different
condition: the panel has not yet been told the source's authoritative state.
That is what #111 addresses and it is not merged here. This branch does not,
on its own, make the end-to-end run pass, and it is not claimed to.

Not determined

  • The combined behaviour with fix: replay authoritative source state into a lazily opened panel #111 and the interlock config change. A combined
    probe worktree is the next measurement.
  • Physical Keithley and dummy-load behaviour. No instrument was touched.
  • The full repository suite: an unrelated node
    (test_cancelled_reply_does_not_cancel_late_commit_or_completion_side_effects)
    hung during a broad run.

Disclosure

Written by an AI lane (gpt-5.6-sol) under an agent brief; verified and landed
by the coordinating agent. The mutation controls above were run by the
coordinator, independently of the lane's report, and contradicted it.

soak measurement added 2 commits August 28, 2026 03:17
…control

The owner ruled on 2026-08-28: "nothing should disable start button. source may
be turned off if operator forgot, or if there is misbehaving, but not disable
the button".

Measured before this change: the end-to-end thermal run timed out because the
Source panel's Start action never became available. The cause was not the panel
wiring but `detector_warmup` holding `stop_source` at 77.5 K against a 10 K
threshold, which made Safety not-ready, which greyed out Start and the P/V/I
spin boxes and showed "Управление заблокировано".

The defect was never that the guard fired. It is that a guard could reach the
button at all.

`_safety_ready` carried two different meanings that a boolean cannot separate:
Safety authority is present and reports a current condition, and no
authoritative Safety state exists at all. Only the first is a guard judging the
operator. The second is the software being blind, which is a different question
and is still open with the owner.

So the cause is now explicit, as `SafetyGateCause`, and the two are treated
differently:

  AUTHORITATIVE_NOT_READY  - a condition. Start and the parameter controls stay
                             available, and the panel shows a WARNING naming the
                             condition and saying it may stop a running source.
  AUTHORITY_UNAVAILABLE    - blindness. Everything except emergency-off stays
                             disabled, exactly as before.

Nothing suppresses the guard itself. A `stop_source` interlock still stops a
source that is running; the operator may start it, be told why that is
questionable, and be stopped by the guard if the condition holds. His remedy is
to disable that interlock, which is the sibling change.

The decision is structural, not textual: no code branches on the wording of a
Safety reason, because that text is operator-facing prose that will be reworded.
@test1card

Copy link
Copy Markdown
Owner Author

@codex review

Head under review: aade65d80db29856a572eec665b78bd76f7380fc. Please bind your
verdict to that exact SHA.

This reverses a safety-adjacent direction by owner ruling, so the useful review
is not "should a guard be able to disable Start" — that is decided. It is
whether the severing is exactly as wide as intended and no wider.

  1. Over-severing is the failure mode. AUTHORITY_UNAVAILABLE must still
    disable Start and the parameter controls. Please look for any path where a
    missing, stale, or wrongly-bound Safety authority now reaches the operator as
    a mere warning. The binding to bridge instance and experiment in
    _current_keithley_safety_gate matters here.
  2. The default. SafetyGateCause defaults to AUTHORITY_UNAVAILABLE at
    every entry point. Confirm that is genuinely the conservative default on all
    of them, including the block-level setter, and that no caller can reach the
    permissive cause by omission.
  3. Two enablement paths. authorization_reason drives the combined buttons
    and _update_control_enablement drives the per-channel ones. They must agree.
    A mutation of one reddened only one node, which is how the coordinator found
    they are separate — please check they cannot diverge.
  4. The warning must be seen. A silent non-block is as wrong as a block. The
    label must name the condition and say it may stop a running source.
  5. One deliberate test change, in
    test_missing_safety_authority_still_disables_controls_except_emergency: two
    assertions that pinned the old fault_latched string were replaced, and an
    isHidden() assertion was dropped there. Visibility is covered for the
    warning path by a new node. Say if you consider that a weakening.

Stated openly rather than left to be discovered: on this branch alone the probe
still reports start_enabled=false, now because channel_state == "unknown"
rather than because of the interlock. That is #111's subject and is not fixed
here. The PR body says so; I am not claiming this unblocks the run.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aade65d80d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/cryodaq/gui/shell/main_window_v2.py Outdated
Comment on lines +440 to +444
SafetyGateCause.AUTHORITATIVE_NOT_READY
if (
not ready
and bridge_instance_id is not None
and not transport_stale

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep UNKNOWN typed readiness fail-closed

At aade65d80db29856a572eec665b78bd76f7380fc, a current typed snapshot whose Safety receipt is unavailable legitimately carries ReadinessTruth.UNKNOWN/SafetyLifecycle.UNKNOWN, a non-transport caution state, and no transport reason codes; this condition nevertheless selects AUTHORITATIVE_NOT_READY. The panel treats that cause permissively, enabling Start for an OFF channel and enabling all parameter controls, so missing Safety authority reaches the operator as a warning. Restrict the permissive cause to the explicitly authoritative blocked/interlock shape and classify UNKNOWN readiness/lifecycle as AUTHORITY_UNAVAILABLE.

AGENTS.md reference: AGENTS.md:L20-L22

Useful? React with 👍 / 👎.

Comment thread src/cryodaq/gui/shell/main_window_v2.py Outdated
Comment on lines +526 to +529
cause = (
SafetyGateCause.AUTHORITATIVE_NOT_READY
if not ready and current_authority
else SafetyGateCause.AUTHORITY_UNAVAILABLE

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve stale Safety as unavailable on lazy open

When a STALE or DISCONNECTED typed snapshot arrives before the source overlay is constructed, _apply_operator_snapshot_safety retains its bridge/experiment bindings while setting readiness false; this lazy-open branch then checks only those bindings and labels the stale snapshot AUTHORITATIVE_NOT_READY. Opening the panel therefore enables Start for an OFF channel and all parameter controls, whereas the already-open-panel path explicitly maps the same transport-stale snapshot to AUTHORITY_UNAVAILABLE. Carry the transport/readiness classification into lazy construction rather than reconstructing authority from identity alone.

AGENTS.md reference: AGENTS.md:L20-L22

Useful? React with 👍 / 👎.

Comment on lines +1439 to +1441
condition = self._safety_reason or "Safety сообщает о действующем условии"
text = (
f"ПРЕДУПРЕЖДЕНИЕ Safety: {condition}. "

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Show the actual readiness blocker in the warning

The production snapshot composer sets readiness.status.operator_text to the generic text Backend readiness authority; the actual condition names and operator text are in readiness.blockers. Because this branch presents _safety_reason as the condition, a real typed interlock displays only ПРЕДУПРЕЖДЕНИЕ Safety: Backend readiness authority and persists that generic text in the Start receipt, unlike the test fixture that injects Interlock stop_source: detector_warmup directly into the summary status. Build the warning from the authoritative blocker evidence so the operator can see what may stop the source.

AGENTS.md reference: AGENTS.md:L533-L536

Useful? React with 👍 / 👎.

interactive_ok = (
self._connected
and self._safety_ready
and (self._safety_ready or self._safety_gate_cause is SafetyGateCause.AUTHORITATIVE_NOT_READY)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Update the canonical Keithley gate contract

This new predicate reverses a reusable Keithley state semantic, but docs/design-system/cryodaq-primitives/keithley-panel.md still says every Safety-gated state disables Start/Stop/spins and still documents only the boolean set_safety_ready(ok, reason) API. Leaving the strict canonical component specification, examples, version, and changelog unchanged makes the owner-approved behavior look like a regression to later maintainers and design-system tests; update those governed artifacts in this slice.

AGENTS.md reference: AGENTS.md:L537-L539

Useful? React with 👍 / 👎.

@test1card

Copy link
Copy Markdown
Owner Author

@codex review

Head under review: 433a060134. Please bind your verdict to that exact SHA.

This round answers your findings and reconciles the design-system documentation
that the severing changed — the panel's contract no longer says every
Safety-gated state disables controls, because that is no longer true.

The coordinator's control: with the two production files reverted, 5 nodes
redden; with code and documentation both applied, 36 pass. A new guard,
tests/docs/test_keithley_panel_contract.py, binds the written contract to the
live code so the two cannot drift apart again — please check that guard actually
reads the code rather than restating the prose.

Still worth your hardest look: over-severing. AUTHORITY_UNAVAILABLE must keep
disabling Start; only a current, authoritative blocker is permitted through.

Docs gate green at this head, derived pair regenerated last.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 433a060134

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +895 to +899
if current_safe_off:
# The negative observation revokes readiness, not the
# still-current bridge/experiment identity binding.
self._typed_safety_ready = False
self._last_safety_gate_cause = SafetyGateCause.AUTHORITATIVE_NOT_READY

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep SAFE_OFF telemetry fail-closed

When a typed snapshot has been accepted and its bridge/experiment binding still matches, any fresh analytics/safety_state packet with state="safe_off" reaches this branch and assigns AUTHORITATIVE_NOT_READY, which enables Start and parameter controls. This feed is explicitly legacy observational telemetry, and SafetyManager._publish_state supplies neither a typed ReadinessBlocker nor bridge-binding evidence, so a routine SAFE_OFF event after a READY cut can create the permissive cause without the current authoritative blocker required by this change. Treat this packet only as revocation/AUTHORITY_UNAVAILABLE and wait for a newer typed BLOCKED cut before enabling controls.

AGENTS.md reference: AGENTS.md:L20-L22

Useful? React with 👍 / 👎.

Comment on lines +13 to +15
spec = (DESIGN_ROOT / "cryodaq-primitives" / "keithley-panel.md").read_text(encoding="utf-8")
version = (DESIGN_ROOT / "VERSION").read_text(encoding="utf-8").strip()
changelog = (DESIGN_ROOT / "CHANGELOG.md").read_text(encoding="utf-8")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bind the contract guard to production code

This guard never reads either production file: it searches only the Markdown specification, version, and changelog. If the enum names, default cause, or permissive enablement predicate are removed or changed while the prose remains intact, every assertion still passes, so the test cannot prevent the code/documentation drift it claims to bind. Read or exercise the production implementation as part of this guard rather than restating only the documentation.

AGENTS.md reference: AGENTS.md:L380-L386

Useful? React with 👍 / 👎.

soak measurement added 4 commits August 28, 2026 07:12
…ranch

#111 and #114 both edit the shell's Keithley safety wiring, for different
reasons, and conflicted in main_window_v2.py. Resolved by intent rather than by
hunk, keeping both properties:

  #111 - the panel must LEARN the source's confirmed state when it is opened
         lazily, after the state was already published.
  #114 - a current, authoritative Safety condition WARNS and leaves Start
         available; a missing Safety authority still disables it.

They are not in tension: one is about what the panel knows, the other about what
it permits. But they touch adjacent lines, and taking either side wholesale
would have compiled, passed most tests, and silently deleted a property.

Each conflicted hunk kept both sides. Where #114 sets a typed gate cause on
invalidation, #111's typed snapshot reset is kept alongside it: taking #114
alone would have retained invalid typed replay evidence, and taking #111 alone
would have lost the explicit fail-closed cause reset.

The three properties are pinned by named nodes, all passing: a current
authoritative condition leaves Start enabled; missing Safety authority leaves
Start disabled; a lazily opened panel reaches confirmed OFF.
@test1card

Copy link
Copy Markdown
Owner Author

@codex review

Head under review: 6e3912fd37d9ca3b0cab973f0b37e818394263ad. Please bind your verdict to that exact SHA.

#111 has been taken into this branch by merge. The two conflicted in
main_window_v2.py because both edit the Keithley safety wiring, and the
resolution is the thing worth reviewing here — a wholesale take of either side
would have compiled, passed most tests, and silently deleted a property.

Both properties are kept:

The hunk most worth your eye is safety-authority invalidation: #114's typed
gate-cause reset and #111's typed snapshot reset are both kept. Taking #114 alone
would have retained invalid typed replay evidence; taking #111 alone would have
lost the explicit fail-closed cause reset.

Three named nodes pin the properties and all pass:
test_typed_interlock_only_warns_for_current_experiment_binding,
test_unknown_typed_safety_authority_disables_start_and_parameters,
test_keithley_overlay_channel_state_replay_on_lazy_open.

Measured by the coordinator on a tree carrying this integration plus #110, #115,
#116 and #117, in a shippable configuration with nothing hand-edited: the gate
probe is fully green — channel_state: off, start_enabled: True,
safety_ready: True — and the end-to-end run energises the source and reaches
the auto-sweep.

Docs gate green at this head; derived pair regenerated as the last commit; the
changed-Python count re-derived at the merged index and read out of the guard.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6e3912fd37

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


def test_keithley_overlay_channel_state_replay_cleared_on_lifecycle_reset() -> None:
_app()
w = MainWindowV2()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Exercise lifecycle reset with an admitted source state

Constructing MainWindowV2 without a live bridge leaves _current_bridge_instance_id() as None, so the OFF packet sent later is rejected by the production cache-admission condition and the cache is already empty before the experiment changes. This test therefore stays green even if lifecycle invalidation is absent; inject live_zmq_bridge, assert the OFF state was cached, and trigger the actual producer-lifecycle reset before checking that replay was cleared.

AGENTS.md reference: AGENTS.md:L380-L386

Useful? React with 👍 / 👎.

Comment thread src/cryodaq/engine.py
Comment on lines +5317 to +5320
warning_receipt = None
raw_warning_choice = cmd.get("operator_warning_choice")
try:
warning_choice = _keithley_warning_choice(raw_warning_choice)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Require the warning receipt for warning-permissive Start

When a compatibility-qualified caller sends keithley_start while Safety is in the warning-permissive SAFE_OFF/interlock condition, omitting operator_warning_choice makes _keithley_warning_choice(None) return None and this handler proceeds directly to request_run. The GUI currently supplies the field, but the authoritative command boundary therefore permits another or regressed client to energize the source without first persisting the operator-choice receipt that this change introduces; determine the warning requirement server-side and reject an unreceipted Start before dispatch.

AGENTS.md reference: AGENTS.md:L20-L22

Useful? React with 👍 / 👎.

Comment thread src/cryodaq/gui/shell/main_window_v2.py Outdated
Comment on lines +1001 to +1003
if self._current_bridge_instance_id() is not None:
source_state_key = channel.rsplit("/", maxsplit=1)[-1]
self._keithley_channel_state_snapshot[source_state_key] = reading

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bind cached OFF states to the SafetyManager producer

This admission check verifies only that some live bridge ID exists; DataBroker.publish() does not reserve channels by publisher, so any passive in-process producer can emit analytics/keithley_channel_state/smua with metadata["state"] = "off". If that packet arrives before the panel is opened, the new cache retains and replays it, and a warning-permissive Safety condition then enables Start from evidence that was never issued by SafetyManager; previously the unopened panel discarded the packet. Require producer-bound source-state evidence rather than granting actuator-state authority from the channel name and metadata shape.

AGENTS.md reference: AGENTS.md:L466-L469

Useful? React with 👍 / 👎.

soak measurement added 2 commits August 28, 2026 09:45
Landed by the batch lander. The lane's own report and the coordinator's
verification are recorded on the pull request.
@test1card

Copy link
Copy Markdown
Owner Author

@codex review

Head under review: 5da4aa32fd41b2dc9c3bfbd5fba8fd33e3454f25. Verified as the branch head on the remote before
writing this.

This round answers your findings and removes a new identity-inference site the
repo-wide C2 sweep flagged: the shell was DECLARING the publisher authority
string safety_manager_source_state_v1 as its own module-level constant. It is
now defined once in src/cryodaq/core/safety_manager.py as
SAFETY_MANAGER_SOURCE_STATE_PUBLISHER and imported by the shell.

That is not cosmetic. A shell that spells the producer's name itself can drift
from the producer silently — rename it in one place and the authority check stops
matching, so authoritative readings would be treated as unauthorised, or the
reverse. One definition, one importer.

Coordinator's measurements: the C2 sweep passes (14 nodes) with no unregistered
site; the control shows collection failing without the production change and 180
passing with it.

Worth your scepticism: the check must still REJECT a reading that lacks the
broker's authority stamp, not merely accept one that has it. Please confirm the
negative case is covered.

Docs gate green at this head; derived pair regenerated as the last commit.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 5da4aa32fd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

soak measurement added 2 commits August 28, 2026 15:36
Landed by the batch lander. The lane's own report and the coordinator's
verification are recorded on the pull request.
@test1card

Copy link
Copy Markdown
Owner Author

@codex review

Head under review: 42db3be8d95a049155fc5973690aa53087a97697 (verified equal to
origin/fix/interlock-warns-never-disables-start at the moment of writing).

What this closes: a safety decision could wait forever on a disk write.

The branch's own node hung indefinitely — test_post_publication_authority_cut_revokes_run_receipt_and_start_audit[dead_channel_fault]
passes on origin/master in 0.63 s and never finished here. Two halves:

  1. The test double had drifted from production (_RunPublicationGate did not accept
    publisher_authority, and the command context passed writer=None), so the durable commit
    the production path awaits could never arrive.
  2. More importantly, production waited without a bound. A missing writer in a test is a
    slow or stalled writer on the bench, and the same lane observed
    _read_executor.shutdown(wait=True) stalling in sqlite_writer.py.

Reversion control, run this turn on Ubuntu 22.04 — production reverted, tests kept:

production reverted -> 4 failed, 252 passed
  test_keithley_start_persists_warning_choice_before_request_run
  test_stalled_warning_persistence_does_not_block_operator_requested_start
  test_missing_warning_persistence_is_receipted_without_refusing_start
  test_successful_start_surfaces_unconfirmed_warning_persistence
production restored -> 256 passed

module gate: every test file naming safety_manager, experiment_commands or keithley_panel
  -- 61 files -> 1588 passed, 3 skipped, 0 failed
documentation gate -> 70 passed

Note the shape of those node names: a stalled persistence does not block an operator-requested
start
, and a missing persistence is receipted without refusing the start. That is the
owner's standing rule made executable — the software warns and records, and never refuses him,
because a refusal sends him to drive the hardware by hand where nothing is recorded at all.

What I want you to attack. The timeout outcome is a judgement, not a mechanism: when the
bound is reached the source must end in the state the operator asked for, and the receipt must
say persistence was not confirmed rather than leave a receipt that cannot say. If any path
reaches a bound and then reports success, or reports a confirmed receipt it did not get, that is
worse than the original hang.

Related and deliberately not duplicated here: you raised the same shape on #117
(emergency_off overtaken by the journal write). That one is fixed on its own branch; this
lane checked and did not build the guard twice.

Disclosure: this change was produced and verified with AI assistance.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 42db3be8d9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1281 to +1285
if error.strip():
self._set_banner(
f"{description}: Engine подтвердил выполнение. {error.strip()}",
theme.STATUS_CAUTION,
auto_clear=False,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Retain unconfirmed-persistence warnings across A+B replies

When Start A+B is in flight and the first channel returns ok with an unconfirmed-persistence warning, this branch displays the caution while the other command remains pending but does not retain it. If the second channel then returns an ordinary success, that callback reaches show_info() and overwrites the caution with a generic confirmation, so reply ordering can hide the only operator-visible indication that one Start choice was not durably recorded. Preserve the warning until all commands in the A+B batch settle and render the aggregate outcome.

AGENTS.md reference: AGENTS.md:L529-L535

Useful? React with 👍 / 👎.

Comment thread src/cryodaq/engine.py
Comment on lines +543 to +546
if not done:
commit_task.cancel()
commit_task.add_done_callback(
lambda task: task.exception() if not task.cancelled() else None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Settle or isolate the timed-out writer operation

When the real SQLiteWriter executor call is the operation that stalls, cancelling commit_task here only cancels the wrapper: append_operator_log_idempotent() has already created a retained owner whose _run_owned_executor() deliberately suppresses cancellation until the thread future finishes. The command can therefore return after one second, but the stalled owner remains in _owned_write_tasks, and SQLiteWriter.stop() later waits for that owner and calls executor.shutdown(wait=True), moving the same unbounded disk wait into engine shutdown (and accumulating more retained owners after repeated Starts). The timed-out write needs a bounded/isolated settlement that cannot leave shutdown waiting forever.

AGENTS.md reference: AGENTS.md:L20-L22

Useful? React with 👍 / 👎.

if (
type(command_warning_requirement) is not RunWarningRequirement
or command_warning_requirement._authority is not self._run_warning_authority
or command_warning_requirement.revision != self._operator_safety_snapshot.revision

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not invalidate choices on routine snapshot refreshes

When warning persistence takes long enough for any normal Safety reading or monitor tick to occur, this exact revision comparison rejects the subsequent Start even if the authoritative warning and every relevant Safety fact are unchanged. _collect_loop() advances the snapshot revision for every reading, and _run_checks() advances it every second; because the production persistence bound is also one second, the stalled-write path is especially likely to return keithley_warning_requirement_changed instead of ending in the operator-requested RUN state, while the 10 ms test timeout avoids the collision. Bind the choice to a generation that changes only when the warning decision changes, rather than the routinely refreshed presentation revision.

Useful? React with 👍 / 👎.

test1card added a commit that referenced this pull request Aug 28, 2026
…he Start control

Integrates #114 onto the candidate, and fixes one defect that only appears once #114 and #116
are in the same tree.

The severing: main_window_v2 now distinguishes an authoritative blocked state from an
unavailable one. A current interlock condition warns and may stop a running source; it does not
grey out Start. Missing or stale Safety authority still disables ordinary controls, and
emergency OFF stays available throughout. AUTHORITATIVE_NOT_READY and AUTHORITY_UNAVAILABLE stay
distinct -- collapsing them to one boolean is what took the operator's button away.

Conflicts were resolved per file rather than per side, because the two branches do not contest
the same behaviour -- they have different bases. safety_manager and keithley_panel keep this
candidate's A2 content (evidence staleness, the producer revision, replay-is-not-observation);
main_window_v2 and its wiring tests take #114's severing. Taking either side wholesale would
have silently reverted the other.

THE INTEGRATION DEFECT, found by #114's test once both were present: the panel read the
producer revision with `reading.metadata.get("source_observation_revision")`, which returns None
when a producer stamps no revision. #116's stricter handling accepts the sentinel or an int, so
None fell through both branches, the revision never advanced, and a genuine new observation
could never reconcile an unknown command outcome -- leaving the operator's controls latched
indefinitely. Absence now means unspecified, not invalid. Neither pull request could have found
this alone: #116's tests always stamp a revision and #114's path does not.

Verified on the candidate: 298 passed across the wiring, channel-state, panel, safety-manager,
driver and sensor suites -- all three blocks' properties holding at once.

Block A3. Semantic delta only; documentation and generated artifacts reconcile at freeze.
test1card pushed a commit that referenced this pull request Aug 28, 2026
…s him

Two merged-in-parallel branches rewrote the operator warning-choice
persistence path in mutually exclusive ways, and neither referenced the
other: #114 carried command_warning_requirement (x8 in engine.py, x0 of the
committer), #117 carried warning_choice_committer (x6, x0 of the requirement).
The candidate held #114's design only because #114 was integrated first, which
is not a reason.

Both keep the receipt write outside _cmd_lock, so that does not separate them.
What separates them is what can refuse the operator.  #114 validates a
prepared RunWarningRequirement token inside _cmd_lock against the live
snapshot, so a token that goes stale is a refusal path by construction - and it
had already produced one: a routine one-second snapshot tick landing inside the
one-second persistence bound returned keithley_warning_requirement_changed
instead of starting the source.  #117's committer has no token; its failure
mode is a receipt recording "not confirmed".

A refusal sends the operator to drive the source by hand outside this program,
where nothing is recorded at all.  So this takes #117's committer shape and
ports #114's properties onto it, keeping #114's Start severing untouched.

Four properties, each with a guard, each red before the change and green after:

  1. a stalled persistence does not block an operator-requested start;
  2. a missing or unconfirmed persistence is receipted, and the start is not
     refused - the operator can see his choice was not durably recorded;
  3. an emergency OFF is not overtaken by an in-flight receipt write: no driver
     ON may occur after the OFF epoch;
  4. a RUN already QUEUED when emergency OFF was pressed does not energize.  It
     captures its abort epoch at ADMISSION, before it can wait behind another
     RUN at _run_request_lock, so an OFF while it is queued always changes the
     epoch it must present at both command-lock cuts.

A malformed GUI correlation id no longer refuses the command either: it is
reused when well-formed and otherwise replaced by a fresh token, so the receipt
always carries an identity without that identity being authority.

The operator-log entry for a confirmed start intent is now attributed to
author="system"/source="auto" rather than to the operator, because the system
composes the text; it keeps the operator_warning_choice tag and embeds the
operator's warning verbatim.  That is a deliberate change to what the run
record asserts about a safety acknowledgment and is called out for the owner
rather than settled here.

Verified on this candidate, not inherited from the branch:
  - reversion control: production reverted to HEAD, the four guards go red
    (by interface absence - the previous design cannot express the committer),
    restored, green;
  - mutation control P4: moving the epoch capture inside _run_request_lock
    reddens exactly guard 4;
  - mutation control P3: disabling the post-receipt cut alone changes nothing
    because the check exists at both cuts; disabling both reddens guards 3
    and 4.  The property is defended in depth;
  - tests/core/test_experiment_commands.py: 72 passed, matching the candidate
    before the change - the stall the branch reported as pre-existing does not
    reproduce here and was a sandbox artifact;
  - test nodes in the two rewritten test files: 179 before, 179 after, none
    removed, none renamed, no skip or xfail introduced.
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