Skip to content

feat: make every software interlock operator-optional, warning when suppressed - #113

Draft
test1card wants to merge 18 commits into
masterfrom
feat/interlock-operator-optionality
Draft

test1card wants to merge 18 commits into
masterfrom
feat/interlock-operator-optionality

Conversation

@test1card

Copy link
Copy Markdown
Owner

Every software interlock becomes operator-optional: on by default, turnable off
by the operator, and warning instead of going quiet when it is off.

Why

The owner ruled this on 2026-08-28. His reason is that this is a science
laboratory with a new prerequisite every time, and that the operator is a
trained professional who must not be blocked from operating.

The standing rule behind it is a safety argument, not a convenience one: when
CryoDAQ refuses a legitimate action, the operators wire the hardware outside
CryoDAQ. The refusal does not prevent the action — it only removes this
system's ability to see, control and record it.

What it does

A disabled row suppresses the action and never the observation:

  • it keeps evaluating, and a violation it would have acted on is raised as an
    operator-facing warning naming the row, the value and the threshold;
  • the bottom bar lists what is currently off, because a warning nobody sees is
    the same as deleting the check;
  • every transition writes a durable operator-log receipt — identity, UTC time,
    the exact notice shown, request id — and the receipt commits before the
    enabled state changes
    , so a crash cannot leave a guard off with no record
    of who turned it off;
  • the state survives a restart, and re-enabling re-evaluates immediately
    against the latest observation rather than waiting for the next one;
  • disable intervals are written into the experiment metadata, so a week-long
    dataset can say which guards were off while its points were taken.

emergency_off is included, by explicit owner decision.

set_disabled_interlocks distinguishes "no interlock is disabled" from "nobody
told me which interlocks are disabled". The second renders as , never as an
empty list, because only the first is safe to read as all-guards-armed.

Verification

The control is reversion, not a passing test. All eight production paths were
reverted to their base with the new regression kept: 7 tests failed.
Restoring production and re-running: 7 passed. Per-property results are in
the lane's report; the properties covered are optionality of every row
including emergency_off, evaluation-while-disabled, warning content, disable
and re-enable receipts, immediate firing on re-enable, restart persistence, the
bottom-bar listing through real telemetry routing, and the run-provenance
interval.

The regression adds 46 assertions and deletes none. No existing test was
weakened, skipped or restated.

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

Not determined

  • Worker-thread filesystem persistence across a real process restart. The
    sandbox stalls filesystem work inside asyncio.to_thread, so the behavioural
    tests call the same synchronous persistence function inline. Production keeps
    the off-event-loop boundary.
  • The full repository suite. Broader runs stalled on pre-existing threaded
    filesystem tests and one transport test that cannot open an AF_INET socket
    under the sandbox. The focused partitions ran: 92, 35 and 1 passed, plus ruff
    check and format.
  • Physical hardware behaviour and laboratory acceptance. No instrument was
    touched.

Disclosure

Written by an AI lane (gpt-5.6-sol) under an agent brief, verified and landed
by the coordinating agent. The reversion control above was run independently of
the lane's own report.

soak measurement added 4 commits August 28, 2026 02:54
…uppressed

The owner ruled on 2026-08-28 that every interlock, emergency_off included, is
optional: on by default and turnable off by the operator. His reason is that
this is a science laboratory with a new prerequisite every time, and that the
operator is a trained professional who must not be blocked from operating.

The standing safety argument is why suppression never means silence: when
CryoDAQ refuses a legitimate action, the operators wire the hardware outside
CryoDAQ, which does not prevent the action but removes this system's ability to
see, control and record it.

So a disabled row suppresses the ACTION and never the OBSERVATION. It keeps
evaluating, and a violation it would have acted on is raised as an
operator-facing warning naming the row, the value and the threshold, not a
debug line. The bottom bar lists what is currently off, because a warning
nobody sees is the same as deleting the check.

Every transition writes a durable operator-log receipt -- identity, UTC time,
the exact notice shown, request id -- and the receipt commits BEFORE the
enabled state changes, so a crash cannot leave a guard off with no record of
who turned it off. State survives a restart, and re-enabling re-evaluates
immediately against the latest observation rather than waiting for the next
one. Disable intervals are written into the experiment metadata, so a
week-long dataset can say which guards were off while its points were taken.
@test1card

Copy link
Copy Markdown
Owner Author

@codex review

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

This is a direction-reversing safety-adjacent change, so the things worth your
attention are not the usual ones:

  1. The receipt-before-state ordering. A disable must not be able to take
    effect without a durable record of who did it. Check that no path changes
    the enabled state before its receipt commits, including the failure paths.
  2. Suppression must never become silence. A disabled row is required to keep
    evaluating and to raise an operator-facing warning it would otherwise have
    acted on. Look for any path where disabling a row stops the evaluation
    itself, or downgrades the event to something the operator will not see.
  3. emergency_off is deliberately included in optionality by explicit owner
    decision. Please do not report that as a defect; do report it if disabling it
    has consequences beyond suppressing its action.
  4. The unknown/empty distinction in set_disabled_interlocks: None means
    "not confirmed" and must never render as an empty list, because only an empty
    list may be read as all-guards-armed.
  5. Restart persistence and re-enable. Re-enabling re-evaluates against the
    latest observation and fires immediately if the condition still holds. Check
    the reconstruction path after a restart, and whether a partially written
    state file can be read as "everything enabled".

Two limits stated openly rather than discovered: worker-thread persistence
across a real process restart is NOT determined (the sandbox stalls filesystem
work inside asyncio.to_thread, so tests call the synchronous function inline),
and the full suite did not run. If either matters to your verdict, say so and I
will get it measured rather than argue the point.

@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: 83fcc65200

ℹ️ 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/core/interlock.py Outdated
if record.condition.matches_reading(reading, descriptor_envelope)
]
matching = [record for record in identity_matching if record.enabled and record.state == InterlockState.ARMED]
protected_matching = identity_matching

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 Separate dead-channel shutdown from disabled rows

When a disabled row receives persistent non-usable readings while a source is active, protected_matching still includes it, so _handle_nonusable eventually invokes on_interlock_dead_channel, which latches a fault and performs the full OFF path. Thus disabling an emergency_off row suppresses threshold crossings but can still shut down the experiment when that row's sensor fails. Either make this escalation respect the row's disabled state or represent the dead-channel protection as a separate, explicitly non-optional guard so the operator-visible contract is truthful.

Useful? React with 👍 / 👎.

Comment thread src/cryodaq/gui/shell/main_window_v2.py Outdated
Comment on lines +826 to +827
self._last_disabled_interlocks = tuple(disabled_raw)
self._bottom_bar.set_disabled_interlocks(self._last_disabled_interlocks)

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 Validate safety ordering before replacing the disabled list

The disabled list is installed before the packet's timestamp and ordering checks. If an older or duplicate READY safety packet arrives after a newer one, the branch at lines 846–849 returns without invalidating the presentation, so an old disabled_interlocks: [] can replace a current non-empty list and be rendered as “none” rather than stale or unavailable. Defer this assignment until the safety packet has passed the same freshness/order cut as the rest of its metadata.

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

Useful? React with 👍 / 👎.

Comment thread src/cryodaq/core/interlock.py Outdated
) -> None:
"""Publish one loud observation while deliberately suppressing action."""
condition = record.condition
record.last_suppressed_warning = now

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 Retry a suppressed warning after publication failure

If the suppressed-warning handler fails to establish any operator-facing surface—for example, both alarm-event dispatch and operator-log persistence fail—this timestamp is already advanced, the exception is swallowed below, and condition_active remains true. Subsequent hazardous readings therefore do not retry until the configured cooldown (or the 60-second fallback), turning a transient publication failure into a silent interval even though the disabled condition remains active. Only mark the warning delivered after a confirmed surface succeeds, or retain immediate retry authority on total failure.

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

Useful? React with 👍 / 👎.

Comment thread src/cryodaq/core/interlock.py Outdated
next_state[interlock_name] = {"enabled": enabled, "receipt": receipt}
next_state.update(self._retired_operator_state)
await asyncio.to_thread(self._write_operator_state, next_state)
record.enabled = enabled

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 Re-arm or reject a tripped row before enabling it

If an interlock trips, is disabled while still TRIPPED, then is re-enabled after its latest reading becomes safe, this assignment reports the row as enabled but leaves its latch state TRIPPED. Future hazardous readings are excluded from matching because they are not ARMED, and they also receive no suppressed warning because the row is enabled, so the interlock becomes completely silent until a separate acknowledgement happens. Re-enable should either require/perform the explicit re-arm transition or refuse to report success while the row remains tripped.

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

Useful? React with 👍 / 👎.

Comment thread src/cryodaq/core/interlock.py Outdated
Comment on lines +1112 to +1116
not previous
and record.latest_reading is not None
and record.condition.is_triggered(record.latest_reading.value)
):
await self._trip(record, record.latest_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 Re-evaluate every bound channel when re-enabling

Production interlocks bind multiple channels, but latest_reading stores only the last reading seen for the entire row. If T1 remains above threshold and a later T2–T8 reading is safe, re-enabling checks only that safe final reading and does not fire immediately, even though the interlock condition still holds on T1; the action is delayed until T1's next poll. Retain and evaluate the latest observation for every bound channel rather than a single row-wide value.

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

Useful? React with 👍 / 👎.

Comment thread src/cryodaq/core/interlock.py Outdated
for name, item in self._interlocks.items()
if name != interlock_name and item.last_transition_receipt is not None
}
next_state[interlock_name] = {"enabled": enabled, "receipt": receipt}

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 Retain both receipts until experiment provenance commits

Each toggle replaces the state file's only receipt for the row. If disabling succeeds but record_interlock_operator_state fails, and the operator later re-enables before restart, this write overwrites the disable receipt with the re-enable receipt; sync_interlock_operator_provenance then sees an enabled row with no open interval and cannot reconstruct the missed disable interval. The operator log still contains the events, but the experiment metadata permanently omits the period during which its guard was disabled, so retain transition history or reconcile from the durable log before discarding the prior receipt.

Useful? React with 👍 / 👎.

Comment on lines +2924 to +2926
async def publish_interlock_operator_state(self) -> None:
"""Refresh the existing safety-state carrier after an operator toggle."""
await self._publish_state("interlock_operator_state_changed")

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 Make disabled-state publication converge after delivery loss

This is the only update sent to MainWindowV2 after a toggle, but _publish_state catches broker failures and returns success, and the GUI never polls the new interlock_status endpoint. If the publication fails—or the GUI reconnects after the one-shot packet—the durable state can be disabled while the bottom bar indefinitely retains the prior empty list as current and appears to say all guards are armed. Require a publication receipt/retry or have the GUI periodically reconcile from the read endpoint instead of treating this best-effort packet as convergent state.

AGENTS.md reference: AGENTS.md:L490-L491

Useful? React with 👍 / 👎.

soak measurement added 2 commits August 28, 2026 04:22
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: 08d4e48067. Please bind your verdict to that exact SHA.

This round answers your findings, plus one the coordinator measured that was not
among them: the regression wrote its persistence files INSIDE the repository,
under .pytest_cache/interlock-operator-optionality/, which is why twelve CI
checks were red on both platforms — one defect wearing twelve faces. It was NOT
fixed by widening the evidence gate or by a teardown that deletes the files; the
code under test is now given a temporary directory outside the tree.

Verified by the coordinator: the regression passes 13 nodes and the working tree
is byte-unchanged by the run, with none of the paths CI named recreated. The
lane's central claim was checked by reverting production and watching 8 nodes
redden. Docs gate green at this head, derived pair regenerated last.

If a finding is unaddressed rather than fixed, name it rather than treating
silence as agreement.

@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: 08d4e48067

ℹ️ 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/engine.py
Comment on lines +2564 to +2567
receipt_result = await _submit_operator_log_entry(log_cmd, context)
entry = receipt_result.get("entry") if type(receipt_result) is dict else None
commit_receipt = receipt_result.get("commit_receipt") if type(receipt_result) is dict else None
if receipt_result.get("committed") is not True or type(entry) is not dict or type(commit_receipt) is not dict:

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 Make replayed toggles idempotent

When an exact request is replayed after a later opposite toggle, _submit_operator_log_entry returns the original committed receipt, but this path unconditionally calls set_enabled again. For example, disable request A, enable request B, then a delayed replay of A leaves the interlock disabled even though B is the newest operator decision. Persist or recognize toggle settlement by request ID and return the original result without reapplying stale state.

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

Useful? React with 👍 / 👎.

Comment thread src/cryodaq/engine.py Outdated
Comment on lines +2580 to +2582
provenance_recorded = await asyncio.to_thread(
context.experiment_manager.record_interlock_operator_state,
interlock_name=name,

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 toggle provenance bound to its experiment

If an experiment stop/start runs after the operator-log append releases its experiment CAS but before this call, record_interlock_operator_state writes through whichever experiment is currently active rather than the experiment_id captured above. The toggle can therefore be logged against experiment A while its disable interval is written to experiment B; the pending-receipt recovery path also lacks an experiment identity with which to repair this. Serialize the whole toggle with experiment lifecycle changes or pass and enforce the captured experiment ID.

AGENTS.md reference: AGENTS.md:L490-L491

Useful? React with 👍 / 👎.

Comment thread src/cryodaq/core/interlock.py Outdated
target_receipt=receipt,
target_pending=pending,
)
await asyncio.to_thread(self._write_operator_state, next_state)

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 state writes before honoring cancellation

If the command task is cancelled while this to_thread write is in progress, the worker thread continues and can durably write the new enabled value, but the coroutine exits before updating record.enabled, its receipt journal, or the publication surface. This leaves disk and the running interlock authority disagreeing until restart; a cancelled enable can therefore leave the live guard disabled even though the durable file says it is enabled. Shield and settle the write, then complete the in-memory transition before propagating cancellation.

AGENTS.md reference: AGENTS.md:L470-L471

Useful? React with 👍 / 👎.

Comment on lines +1241 to +1248
or type(state.get("enabled")) is not bool
or type(state.get("receipt")) is not dict
or (
schema_version == 2
and (
type(state.get("pending_receipts")) is not list
or len(state["pending_receipts"]) > _MAX_PENDING_OPERATOR_TRANSITIONS
or any(type(receipt) is not dict for receipt in state["pending_receipts"])

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 Reject unproven disabled-state receipts

When the state file is corrupted or hand-edited but retains the expected outer keys, this validation accepts any dictionaries as receipts; for example, {"enabled": false, "receipt": {}, "pending_receipts": []} is loaded successfully and disables the interlock without an operator, timestamp, request ID, notice, or commit proof. Validate the complete receipt schema and timestamp before applying a persisted disabled value so malformed persistence fails closed instead of silently removing guard authority.

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

Useful? React with 👍 / 👎.

Comment thread src/cryodaq/engine.py
Comment on lines +5600 to +5601
if action == "interlock_set_enabled":
return await _set_interlock_operator_state(cmd, context=context)

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 Gate interlock disabling behind the required hazard approval

At the reviewed head, the ordinary ZMQ mutation endpoint accepts interlock_set_enabled without checking verified-OFF state or routing the disable through a dedicated safety adapter, so any admitted mutation client can suppress an emergency_off guard while a source is active. A repository-wide search found no separately approved hazard analysis or the required host-death and physical-bench evidence for this new control path. Keep disabling unavailable until that approval stack exists, or enforce the approved adapter and operating-state gate here.

AGENTS.md reference: AGENTS.md:L33-L35

Useful? React with 👍 / 👎.

Comment on lines +1262 to +1264
if state["enabled"] is False and not record.condition.operator_disableable:
raise InterlockConfigError(f"persisted disabled interlock {name!r} is no longer operator-disableable")
record.enabled = state["enabled"]

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 persisted disables to the reviewed interlock policy

When interlocks.yaml changes between restarts, this loader applies a prior disabled decision solely by row name. If that name now has a different threshold, action, or physical channel binding, the old receipt—whose notice described the previous condition—silently disables the newly reviewed guard without a fresh operator decision. Persist and verify a fingerprint of the exact interlock policy before restoring enabled=False, and refuse stale disabled receipts on mismatch.

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

Useful? React with 👍 / 👎.

soak measurement added 2 commits August 28, 2026 07:50
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: a412247640a33c069e864a2c665df0ed3ff14518. Please bind your verdict to that exact SHA.

This round answers your findings AND fixes the defect behind ten red checks,
which was one defect wearing ten faces: the branch had added two required
positional arguments to _InterlockHandlerContext and left an existing test
constructing it the old way. Measured before the fix — the node passes on master
and raised TypeError here, so it was this branch's, not a flake.

The coordinator verified the round by reverting the production change and
watching seven nodes redden, and confirmed tests/core now runs 2219 passed.

One node fails here that also fails identically on master
(test_engine_b3_structure.py::test_engine_wiring_submodules_import_without_engine_reverse_cycle).
It is not attributable to this branch — I mention it so you do not spend time on
it, and because master carrying a red node is worth someone knowing.

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

@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: a412247640

ℹ️ 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/core/experiment.py Outdated
Comment on lines +2279 to +2281
if enabled:
for interval in reversed(intervals):
if interval.get("interlock_name") == interlock_name and interval.get("reenabled_at") is 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 Preserve disable provenance across experiment boundaries

When an interlock is disabled during experiment A, A is finalized, experiment B starts, and the interlock is re-enabled, this branch searches only B's metadata for an open interval. Consequently A's interval remains permanently open and B contains no record that it began while the guard was disabled. Fresh evidence at this head is this experiment-spanning lifecycle sequence, rather than the previously reported concurrent experiment-binding race; close or split the interval explicitly when experiment ownership changes.

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

Useful? React with 👍 / 👎.

Comment on lines +3434 to +3436
disabled_interlocks = self._disabled_interlocks()
if disabled_interlocks:
return False, "Software interlock(s) disabled: " + ", ".join(disabled_interlocks)

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 Include disabled interlocks in the authoritative readiness cut

If an interlock is disabled while SafetyManager is already READY, this new precondition rejects the eventual RUN request but neither demotes the FSM nor adds a blocker to _operator_safety_snapshot. The typed snapshot therefore continues to publish READY, and MainWindowV2 continues treating its source-control gate as ready even though the backend will refuse the action; project the provider state into the authoritative snapshot so operator readiness agrees with the enforced precondition.

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

Useful? React with 👍 / 👎.

Comment thread src/cryodaq/engine.py
Comment on lines +2516 to +2517
or type(operator) is not str
or not operator.strip()

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 Validate operator size before committing the toggle intent

For an operator value between 513 and 4096 UTF-8 bytes, this admission accepts the command and the operator-log layer durably commits it, but InterlockEngine.set_enabled() subsequently rejects the receipt because _validated_operator_transition_receipt() caps the operator at 512 bytes. The state change therefore never occurs and no pending transition is persisted for restart reconciliation, leaving a committed enable/disable decision permanently detached from interlock state; apply the receipt's 512-byte bound before _submit_operator_log_entry().

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

Useful? React with 👍 / 👎.

Comment thread src/cryodaq/core/interlock.py Outdated
Comment on lines +824 to +825
if not triggered:
record.condition_active = 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.

P2 Badge Preserve the active warning across safe sibling channels

For a multi-channel row such as overheat_cryostat, if T1 remains above threshold while a later T2–T8 reading is safe, this row-wide assignment clears condition_active even though the interlock condition is still active on T1. The next T1 sample then satisfies not record.condition_active and emits another alarm/operator-log warning regardless of the configured cooldown, so one persistently hot channel can generate a loud warning on every poll; derive the aggregate active state from all latest bound-channel readings or track it per channel.

Useful? React with 👍 / 👎.

soak measurement added 2 commits August 28, 2026 09:34
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: c63cac159f. I verified that SHA is the branch head on the
remote before writing this.

This round answers your four findings. Coordinator's control: 5 nodes red
without the production change, 24 green with it.
Patch shape: 623 insertions,
30 deletions across 7 files — no assertion was deleted to make anything pass.

Three of the five reddening nodes are worth your eye because they are about
losing or mis-attributing operator intent, which is what this branch exists to
record:

  • test_oversized_interlock_operator_is_rejected_before_operator_log_commit
    an oversized operator identity must be refused BEFORE anything is written, not
    after. Please check nothing can commit a partial receipt on that path.
  • test_disabled_interval_is_split_when_experiment_ownership_changes — a guard
    disabled across an experiment boundary must appear in BOTH runs' provenance,
    not just the one it started in. A week-long dataset has to be able to say which
    guards were off while its points were taken.
  • test_experiment_boundary_split_recovers_from_pending_transition_journal
    the split must survive a crash mid-transition.

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

If a finding is unaddressed rather than fixed, please name it rather than
treating silence as agreement.

@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: c63cac159f

ℹ️ 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 +2478 to +2482
if (transitions or state.get("enabled") is False) and not any(
_interlock_receipt_request_id(receipt) == _interlock_receipt_request_id(last_receipt)
for receipt in transitions
):
transitions.append(dict(last_receipt))

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 Split disabled intervals at every experiment boundary

When experiment B starts while the row remains disabled, this branch re-adds the last receipt but later resolves it to the receipt's original experiment A and skips it as already recorded; because the new split logic runs only on a later re-enable, B receives no interval if the guard stays disabled through finalization, and if re-enabling occurs after experiment C begins, B is omitted entirely. Fresh evidence at this head is that the new test always re-enables during B, so it does not cover a disabled guard spanning multiple boundaries or never being re-enabled; seed and split the interval as part of each lifecycle boundary instead.

AGENTS.md reference: AGENTS.md:L468-L469

Useful? React with 👍 / 👎.

Comment thread src/cryodaq/core/interlock.py Outdated
f"interlocks.yaml at {config_path}: 'interlocks' must be a list, got {type(entries).__name__}"
)

operator_disableable_default = raw.get("operator_disableable", True)

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 Default omitted disable authority to false

When an existing deployment selects a pre-upgrade interlocks.local.yaml that lacks the new root key, this fallback silently makes every configured row operator-disableable. _engine_config_path() prefers that complete machine-local override, and the new mutation endpoint then accepts toggles for those rows, so merely upgrading grants authority to suppress safety guards without an explicit reviewed policy change; require the key or default omission to False.

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

Useful? React with 👍 / 👎.

Comment on lines +1281 to +1282
if len(record.transition_receipts) >= _MAX_OPERATOR_TRANSITION_RECEIPTS:
raise RuntimeError("interlock operator idempotency journal is full")

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 Cap the serialized journals below the loader limit

The 1,024-entry bounds apply independently to every row and to both the pending and retained arrays, while _write_operator_state() imposes no aggregate byte limit and _load_operator_state() rejects any resulting file over 4 MiB. With the three tracked rows, legally bounded receipts using the accepted 512-byte operator identity already produce a retained state file above 4 MiB (and pending receipts duplicate that history), so a file written successfully by this code can make the next engine restart fail before provenance recovery runs; prune or impose a serialized aggregate budget that cannot exceed the loader's limit.

Useful? React with 👍 / 👎.

@test1card

Copy link
Copy Markdown
Owner Author

Correction to my earlier comments on this pull request.

I said that tests/core/test_engine_b3_structure.py::test_engine_wiring_submodules_import_without_engine_reverse_cycle
"also fails on master" and implied master carries a red test. That was wrong.
Master is fine.

The failure was in MY environment. That test spawns a subprocess to prove there
is no import cycle, and the subprocess inherited my shell's library search path
rather than the conda environment's — loading a system libstdc++ older than
the environment's ICU library requires:

ImportError: /lib/x86_64-linux-gnu/libstdc++.so.6: version `CXXABI_1.3.15' not found

With LD_LIBRARY_PATH pointed at the environment's lib directory it passes.
Hosted CI never saw it because its interpreter is the system one.

The operational advice was still right — that node is not this branch's problem
and there is nothing here to chase. But the reason I gave for it was not, and it
was a claim about the trunk, so it is worth correcting on the record rather than
leaving to stand.

soak measurement added 2 commits August 28, 2026 11:02
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: 35441a643f670bd990f784ddc8af00e5fb2807a9 (verified equal to
origin/feat/interlock-operator-optionality at the moment of writing).

Reversion control, run this turn on Ubuntu 22.04. Production reverted, tests kept:

PHASE 1  tests only, no production change  -> 3 failed, 24 passed
  test_legacy_policy_without_disable_authority_remains_non_disableable
  test_disabled_interval_is_seeded_at_each_boundary_without_reenable
  test_serialized_operator_journals_cannot_outgrow_the_restart_loader
PHASE 2  with production                   -> 27 passed

Documentation gate: tests/docs 68 passed. No test weakened.

Context you should hold while reviewing. The owner's standing ruling is that nothing may
disable the Start button.
A source may be turned off — if the operator forgot it, or if it
misbehaves — but the control itself stays available and the operator is told what is wrong.
A change that makes the interlock refuse rather than warn is a defect here even if every
test passes, because a refusal makes the laboratory wire the hardware outside this program
entirely.

So please check specifically that test_legacy_policy_without_disable_authority_remains_non_disableable
means what its name claims: a legacy policy that never granted disable authority cannot acquire
it by omission.

The third node is a memory-shape guard — serialized operator journals must not outgrow what the
restart loader can read back. This program has to run for a week without unbounded growth, so if
that bound is enforced by trimming, I want to know what is discarded and whether anything tells
the operator it was.

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: 35441a643f

ℹ️ 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 +2723 to +2728
blockers.append(
SafetyBlocker(
"software_interlock_disabled",
OperatorPresentationState.WARNING,
f"Software interlocks are disabled: {disabled_names}",
"Re-enable every named software interlock before requesting RUN",

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 Start available for disabled interlocks

When any interlock is disabled, this SafetyBlocker makes the authoritative snapshot report BLOCKED; MainWindowV2._apply_operator_snapshot() then clears safety readiness and KeithleyPanel._update_control_enablement() disables the Start button. Fresh evidence at this head is the owner's explicit requirement in this review that a disabled interlock must warn without refusing RUN, so project this as warning-only rather than a readiness blocker.

Useful? React with 👍 / 👎.

Comment on lines +1507 to +1509
serialized = json.dumps(payload, ensure_ascii=False, indent=2, allow_nan=False)
if len(serialized.encode("utf-8")) > _MAX_OPERATOR_STATE_BYTES:
raise RuntimeError("interlock operator state exceeds its size bound")

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 re-enable authority when the journal reaches its bound

Once the append-only transition_receipts arrays approach this aggregate limit, every subsequent toggle is rejected instead of old settled idempotency entries being retired. This includes an attempt to re-enable a currently disabled guard; because the operator-log intent is committed before this write, the operator receives only a reconciliation failure while the guard remains disabled, and retries cannot recover because no path reduces the retained journal. Bound the retained history without discarding pending provenance, and warn before capacity exhaustion.

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

Useful? React with 👍 / 👎.

Comment on lines +2770 to +2773
for experiment_id in sorted(changed_experiments):
atomic_write_text(
self._metadata_path(experiment_id),
json.dumps(payloads[experiment_id], ensure_ascii=False, indent=2),

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 Make boundary provenance writes recoverable after partial failure

When a disabled guard crosses an experiment boundary, the new seeding path queues both the prior experiment's closure and the current experiment's continuation, but this loop persists those files independently. If the second write fails, retry cannot converge: a persisted forward pointer with no target interval raises active interlock disable interval is unavailable, while a persisted target with an unclosed source hits the early matching-target return and never repairs the source. Use a recoverable transaction/journal or make reconciliation repair either one-sided state.

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

Useful? React with 👍 / 👎.

Comment on lines +2428 to +2431
elif not any(
interval.get("interlock_name") == interlock_name and _interlock_interval_is_open(interval)
for interval in intervals
):

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 interval identity across repeated disable requests

If an operator submits a new disable request while the row is already disabled, this condition leaves the existing open interval bound to the first request even though set_enabled() replaces the engine's disable_receipt with the second request. At the next experiment boundary, _seed_disabled_interlock_boundary() searches the prior metadata using that second request ID, raises prior interlock disable interval is unavailable, and the new experiment loses its disabled-guard provenance. Treat same-state disables as no-ops or update the open interval and its receipt identity consistently.

AGENTS.md reference: AGENTS.md:L468-L469

Useful? React with 👍 / 👎.

Comment thread src/cryodaq/engine.py
Comment on lines +5635 to +5636
if action == "interlock_set_enabled":
return await _set_interlock_operator_state(cmd, context=context)

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 Expose interlock toggles through the operator GUI

At the inspected head, a repository-wide search for interlock_set_enabled, interlock_status, and operator_disableable under src/cryodaq/gui finds no caller or control; the only GUI addition is the explicitly passive bottom-status display. Consequently an operator using the shipped application can see that a guard is disabled but cannot disable or re-enable it, leaving this advertised operator option available only to bespoke ZMQ clients and encouraging control outside the supported UI. Add a reviewed, accessible GUI route that invokes this mutation and presents its receipt/outcome.

Useful? React with 👍 / 👎.

@test1card

Copy link
Copy Markdown
Owner Author

Hold the review of 35441a643 — one node is red and it is this branch's own.

Measured this turn by running the node on Windows directly, which is where it fails:

tests/gui/shell/test_bottom_status_bar_fault_beep.py::test_protocol_maxima_fit_1280_with_full_evidence_in_accessible_detail

  on origin/master (d5a0f315e)   1 passed
  on this head (35441a643)       1 failed -- assert 1334 <= 1280

bar.minimumSizeHint().width() is 1334 on this branch and within 1280 on master, so this
branch causes it.

Why this is a real defect and not a layout detail. 1280 is the width of the screen the
instrument runs on in the laboratory. A bottom status bar whose minimum width is 1334 cannot
be drawn whole there — something is clipped. What this branch adds to that bar is the interlock
warning text: the message that tells the operator a software interlock has been switched off.
The owner's governing rule for this product is to warn the operator, never block him, and make
sure he knows what is up. A warning that does not fit on his screen is exactly the failure that
rule exists to prevent.

The threshold will not be raised. Lowering a threshold to make a test pass is prohibited here,
and in this case it would amount to deciding on the owner's behalf that his screen is wide
enough. The node's own name states the acceptable shape: fit 1280 with full evidence in
accessible detail
— the visible bar fits, the complete text stays reachable through the
accessible-description channel the widget already uses.

A lane is working it now, instructed to reuse the existing mechanism rather than truncate the
warning into nothing, and to report the measured width before and after.

The rest of the round stands: the three interlock-optionality nodes pass, and the reversion
control I ran (production reverted, tests kept, three nodes red; restored, all 27 green) is
unaffected by this. I will post a fresh review request against a head that has actually passed.

soak measurement added 2 commits August 28, 2026 13:50
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: 30e8dd79e704a0a5fc8dce6dc9ff60ca1be4813b (verified equal to
origin/feat/interlock-operator-optionality at the moment of writing). This replaces my
withdrawn request on 35441a643.

The status bar now fits, and the threshold was not touched. Measured this turn on Ubuntu
22.04:

minimum width with a 1000-character disabled-interlock string:  659 px   (it was 1334)
assert bar.minimumSizeHint().width() <= 1280                    unchanged, still present once

The lane made the node stricter rather than passing it: it now feeds a 1000-character
interlock string, adds the interlock label to the elision check, and asserts the full text
appears in the accessible description AND the tooltip while asserting it does NOT appear in the
visible label. That is the "full evidence in accessible detail" contract extended to the label
this branch adds.

A regression this round caused, and closed. The first attempt fixed the width and broke
test_main_window_v2_safety_staleness.py::test_reordered_safety_packet_cannot_replace_current_disabled_interlocks,
the guard that stops an out-of-order safety packet from replacing the current disabled-interlock
list. I found it by running every test file naming the changed modules, not only the files the
task named. It is fixed and the guard passes.

Suites, measured this turn:

module gate: every test file naming `bottom_status_bar` or `interlock` -- 44 files
  -> 1525 passed, 5 skipped, 0 failed     (it was 1524 passed / 1 failed before the correction)
production+docs reverted, tests kept
  -> 2 failed  (the two this round fixes), the reordered-packet guard passing
documentation gate -> 68 passed

What I would like you to attack. The visible label is now elided while the full text lives
in the accessible description. Two ways that can go wrong for the operator:

  1. If the elision ever drops the identity of which interlock is disabled rather than only its
    detail, the bar tells him something is off without telling him what — and he cannot act on
    it. Check what survives into the visible text when several interlocks are disabled at once.
  2. The accessible description is only reachable through a screen reader or a tooltip hover.
    If that is the only place the identity exists, a sighted operator who never hovers has not
    been told. Is the visible remainder sufficient on its own?

The owner's rule here is that an interlock may be switched off and the source may be turned
off, but the operator must never lose the ability to act and must always be told what is up.

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: 30e8dd79e7

ℹ️ 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".

visible_name = _bounded_visible(canonical[0], _MAX_VISIBLE_INTERLOCK_CHARS)
visible = f"⚠ Блок.: {visible_name}{suffix}"
else:
visible = f"⚠ Блок.: {len(canonical)}{suffix}"

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 every disabled interlock identity visible

When two or more interlocks are disabled, this branch renders only ⚠ Блок.: N; a repository-wide GUI search shows that the identities then exist only in this label's tooltip and accessible description. A sighted operator who does not hover can therefore see that protection is reduced but cannot determine which guards are unavailable or act on that information. Keep the identities—or an immediately visible, non-hover detail surface—in the persistent presentation while still bounding the bar width.

AGENTS.md reference: AGENTS.md:L529-L532

Useful? React with 👍 / 👎.

Comment on lines +889 to +891
result = self._suppressed_handler(condition, reading, message)
if asyncio.iscoroutine(result):
await result

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 await warning persistence in the interlock consumer

When a disabled row crosses its threshold, the sole _check_loop consumer waits here for the complete production warning handler before reading another broker item. That handler awaits SQLiteWriter.append_operator_log(), which queues work on the shared write executor without a timeout, so a slow or stuck persistence write for this observational warning also delays processing a subsequent hazardous reading for an enabled interlock. Preserve warning retries and settlement under a separately owned task rather than blocking the serial protection loop.

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

Useful? React with 👍 / 👎.

Comment on lines +683 to +685
self._interlocks[condition.name] = _InterlockRecord(
condition=condition,
enabled=condition.enabled_by_default,

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 Record disabled-by-default intervals

When a valid configuration sets enabled_by_default: false, this assignment creates a disabled record without a last_transition_receipt or disable_receipt. ExperimentManager.sync_interlock_operator_provenance() consequently skips the row, and a later re-enable has neither a prior receipt nor an open interval to close, so every experiment spanning this supported default-disabled state permanently omits the fact that its guard was unavailable. Either reject disabled initialization or seed durable, policy-bound provenance for it.

Useful? React with 👍 / 👎.

@test1card

Copy link
Copy Markdown
Owner Author

One red on Windows at 30e8dd79e, and it is not a Windows quirk — it is a real measurement defect.

tests/core/test_interlock_operator_optionality.py::test_serialized_operator_journals_cannot_outgrow_the_restart_loader

  Ubuntu 22.04  -> passes
  Windows       -> AssertionError: assert 32781 <= 32768

Over the bound by exactly 13 bytes, which is the number of newlines in the file.

Why. The bound is checked on the JSON string, and then a different thing is written to disk:

# src/cryodaq/core/interlock.py::_write_operator_state
serialized = json.dumps(payload, ensure_ascii=False, indent=2, allow_nan=False)
if len(serialized.encode("utf-8")) > _MAX_OPERATOR_STATE_BYTES:   # LF newlines
    raise RuntimeError("interlock operator state exceeds its size bound")
atomic_write_text(path, serialized)

# src/cryodaq/core/atomic_write.py::atomic_write_text
with os.fdopen(fd, "w", encoding=encoding) as f:                  # text mode, no newline=""

Text mode turns every \n into \r\n on Windows, so the file on disk is larger than the
string that passed the check. The guard and the filesystem are measuring different objects.

This is wider than this pull request. atomic_write_text is used by nine modules,
including core/experiment.py, which writes the durable run records. Any size bound, digest or
byte-comparison over a file written through that helper means one thing on Ubuntu and another
on Windows. The laboratory machine is Ubuntu 22.04, so the instrument itself writes LF and the
run is unaffected — but Windows continuous integration is part of the merge gate, and a receipt
that binds a file by digest would not agree across platforms.

The fix is one argument — newline="" in atomic_write_text — and the care is that nine
callers depend on that helper, so it needs a guard that writes through it and compares the
file's bytes against the string that was checked, on both platforms. The 32 KiB bound will
not be raised
; the bound is right and the accounting is wrong.

I have not changed anything on this branch for it yet. The rest of the round stands: the status
bar fits 1280 with the full text kept in the accessible detail, and the reordered-safety-packet
guard passes. This is the only red.

soak measurement added 4 commits August 28, 2026 15:16
Landed by the batch lander. The lane's own report and the coordinator's
verification are recorded on the pull request.
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: 47b5020856979fe4ed5b54a1772570d452acdc79 (verified equal to
origin/feat/interlock-operator-optionality at the moment of writing).

The Windows red is closed, and it was a real cross-platform defect rather than a platform
quirk.
The size bound was checked against the JSON string, then the file was written through
atomic_write_text, which opened in text mode — so Windows turned every newline into two bytes
and the file on disk exceeded the bytes that had passed the check. Over the 32 KiB bound by
exactly 13, the number of newlines. The fix is one argument:

-  with os.fdopen(fd, "w", encoding=encoding) as f:
+  with os.fdopen(fd, "w", encoding=encoding, newline="") as f:

Verified on Windows, because that is the only platform where this change alters behaviour:

with the fix      -> 7 passed
production reverted -> 2 failed
   test_serialized_operator_journals_cannot_outgrow_the_restart_loader
   test_atomic_write_text_preserves_checked_utf8_bytes   (the new guard)

The new guard writes a known string through the helper and asserts the file's bytes equal the
bytes that were checked. On Ubuntu it passes either way — I state that plainly rather than
claiming a red I did not observe there.

The 32 KiB bound was not raised. The bound is right; the accounting was wrong.

The blast radius, checked rather than assumed. atomic_write_text is used by nine modules
including core/experiment.py, which writes the durable run records. On Windows I ran every
test file naming atomic_write, calibration, cooldown_fingerprint, first_run_config or
_theme_loader — 51 files — and got 1732 passed, 17 skipped, 0 failed.

docs/CLAIM_CORRECTIONS.md moves its moving candidate count from 717 to 719, read out of
the freshness guard, naming the two paths that entered.

What I would like you to attack: this changes bytes on disk for nine callers on Windows. If any
of them stores or compares a digest of a file written through this helper, that digest changes
once — and a receipt bound to the old bytes would refuse. I found nothing of the sort, but I
would rather you looked than take my search for proof.

The rest of the round stands: the status bar fits 1280 with the full interlock text in the
accessible detail, and a reordered safety packet cannot replace the current disabled-interlock
list.

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: 47b5020856

ℹ️ 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 +807 to +813
elif identity_matching:
self._nonusable_windows.pop(reading.channel, None)
if not (
math.isinf(reading.value)
and any(record.condition.is_triggered(reading.value) for record in identity_matching)
):
return

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 alarms for unusable disabled channels

When every interlock matching a channel is disabled and the sensor reports NaN or a finite error-status value, this branch clears the non-usable window and returns instead of calling _handle_nonusable. The disabled row therefore loses both the immediate diagnostic alarm and persistent dead-channel observation; the general sensor-diagnostics alarm is delayed and configurable, so disabling an action can make this safety-relevant sensor failure substantially quieter. Keep the observational warning/debounce path active while suppressing only the eventual control action or fault latch.

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

Useful? React with 👍 / 👎.

@test1card

Copy link
Copy Markdown
Owner Author

One Windows red remains at 47b502085, and the measurement says the interlock label is not
the culprit.

tests/gui/shell/test_bottom_status_bar_fault_beep.py::test_protocol_maxima_fit_1280_with_full_evidence_in_accessible_detail

  Ubuntu 22.04 -> passes
  Windows      -> AssertionError: assert 1306 <= 1280

Measured on Windows this turn, both ways:

  • with only the 1000-character interlock string set, the bar is 960 px — comfortably
    inside the limit;
  • with every field at maximum together (safety label, a 1000-character connection string,
    disk at 1e300, uptime, interlock), it is 1306 px.

So the label this branch added is fine on its own. The aggregate is over, and only on Windows.
The elision budget is tuned to one platform's font metrics — what fits in Ubuntu's default
font does not fit in Windows'.

That is a real defect rather than a platform quirk: a width budget that is correct only by luck
of the font is not a budget. The 1280 will not be raised — it is the width of the screen in the
laboratory — and it will not be special-cased per platform either. A lane is making the elision
compute from the width actually available through the font in use, keeping the full text in the
accessible description and the tooltip as this branch already does.

The laboratory machine is Ubuntu 22.04, so the instrument itself is not affected today; Windows
continuous integration is part of the merge gate, which is why it blocks.

The rest of this head stands and is verified: atomic_write_text now writes with newline=""
so a size bound checked on a string is enforced against the same bytes on disk (proved on
Windows: 7 passed with the fix, 2 failed without), and the 51-file caller sweep is 1732 passed.

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