Skip to content

fix: Fail fast without clearing the marker when a pause-point trigger rejects its arguments - #2014

Merged
hatayama merged 2 commits into
feature/cli-discoverability-integrationfrom
fix/pause-point-trigger-fail-fast
Jul 26, 2026
Merged

fix: Fail fast without clearing the marker when a pause-point trigger rejects its arguments#2014
hatayama merged 2 commits into
feature/cli-discoverability-integrationfrom
fix/pause-point-trigger-fail-fast

Conversation

@hatayama

@hatayama hatayama commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Summary

  • A pause-point wait whose --trigger command is rejected before it runs — a bad argument value or a command name that does not exist — now fails immediately instead of waiting out the full --timeout-seconds.
  • The marker stays armed, so the corrected trigger can be retried without re-enabling, and the failure explains what to fix.

User Impact

Before, enable-pause-point --await --timeout-seconds 60 --trigger "simulate-keyboard --action Hold --key A" (an action that does not exist) blocked for the whole 60 seconds and then reported PAUSE_POINT_WAIT_TIMEOUT with the PlayMode diagnosis for a missed code path — a diagnosis describing neither what happened nor what to fix. The trigger had already been rejected in the first milliseconds and never performed any action, so the marker could not possibly be hit by it. Worse, the timeout path clears the marker, so fixing the typo also required re-enabling it.

A mistyped command name (--trigger "simulate-keybord ...") cost exactly the same: the command never reached Unity at all, yet the wait ran to completion, cleared the marker, and reported the failure as retryable.

The advice to pass a generous --timeout-seconds when arming while paused is correct, which means following it doubled the time lost to this failure.

After this change both cases return in under a second with a dedicated failure that carries:

  • the trigger's own rejection, verbatim, under Details.TriggerResult.Error
  • Details.RemainingMilliseconds, since the marker is deliberately left armed and its lifetime keeps counting down while the trigger is being fixed
  • recovery guidance that asks for one changed value in the command just run, plus a fully spelled-out alternative carrying the real marker id — no argument left to guess

A wait that resumed PlayMode itself puts PlayMode back into pause when it abandons the wait, and reports that as ResumePlayResult.Repaused. Without this the game keeps running while the trigger is fixed, unrelated gameplay can reach the marker's line, and a single-shot marker's hit gets consumed — the retry would then return someone else's hit as if it were the corrected trigger's result. That is a quieter and worse failure than waiting 60 seconds.

Only a pre-execution rejection aborts the wait. A connection drop, a disabled tool, or unparseable trigger output keeps waiting, because the game itself may still hit the marker and abandoning that wait would turn a recoverable situation into a lost hit.

Changes

  • Add an internal wait state for an aborted wait rather than reusing the timeout state, which unconditionally clears the marker and carries a message and hint aimed at a missed code path. The state never appears on the wire.
  • Abort on a trigger rejected before execution: an INVALID_ARGUMENT or UNKNOWN_COMMAND envelope on the dispatched command's stderr. Only stderr is inspected, since that is where every error envelope is written.
  • Observe the trigger's completion inside the status poll loop via a third select case, and reuse the received result instead of joining afterwards — the handle's channel holds a single buffered value, so a second receive would block for the join grace window and then report Completed:false over a real result.
  • Take one last status reading before abandoning the wait, so a trigger rejection racing a genuine hit still reports the hit. The failure message states that a zero RemainingMilliseconds with an empty Details.Status means that re-read failed, rather than asserting a lifetime the response may not carry.
  • Add PAUSE_POINT_TRIGGER_FAILED to the shared error-code list, with Retryable/SafeToRetry false: retrying the identical command reproduces the same rejection. Export the existing unknown-command code so the trigger check can reference it.
  • Report the re-pause additively on ResumePlayResult (Repaused, RepauseError), including when the re-pause itself fails. A resume that was a no-op because PlayMode was already running sends no Pause.
  • Extract the pre-wait resume/trigger work out of waitForPausePoint so the added re-pause and result handling stay under the repository complexity limit.
  • Rename the internal control-play-mode send helper, which previously named only the resume it served and now also sends Pause.
  • Document the new failure in the pause-point skill and its fast-progressing-games reference, and regenerate the tracked skill copies.

Verification

Tests were written first and confirmed failing before the implementation (build failure on the not-yet-existing state, error code, and predicate), then re-run after it.

  • scripts/check-go-cli.sh — exit status 0, lint 0 issues. for all four modules, all module test suites ok
  • The 10 new tests run and pass: go test ./internal/projectrunner/ -v -run '...' printed --- PASS for each of TestWaitForPausePointAbortsWhenTriggerRejectsArguments, TestRunWaitForPausePointKeepsMarkerWhenTriggerRejectsArguments, TestRunWaitForPausePointKeepsMarkerWhenTriggerCommandIsUnknown, TestWaitForPausePointDoesNotAbortWhenTriggerSucceeds, TestWaitForPausePointReportsHitRacingATriggerRejection, TestRunWaitForPausePointRepausesPlayModeWhenTriggerRejectsArguments, TestRunWaitForPausePointReportsRepauseFailure, TestWaitForPausePointDoesNotRepauseWhenResumeWasANoOp, TestWaitForPausePointDoesNotRepauseWhenItDidNotResume, and TestPausePointTriggerRejectedBeforeExecution (6 subcases)
  • scripts/stamp-release-inputs.sh — run in this pull request, since it changes a non-test cli/common/ source

Manual verification against a running Editor, with a marker armed on a real line (enable-pause-point --file .../SpaceHoldPoller.cs --line 19 --timeout-seconds 30) and ULOOP_PROJECT_RUNNER_PATH pointed at the locally built runner:

  • --trigger "simulate-keyboard --action Hold --key A" returned in 0s (previously 30s) with PAUSE_POINT_TRIGGER_FAILED, Retryable:false, RemainingMilliseconds:29966, the verbatim INVALID_ARGUMENT rejection under TriggerResult.Error, and a recovery command carrying the real marker id.
  • --trigger "simulate-keybord --action Press --key Space" (mistyped command name) returned in 0s with the same error code, Retryable:false, RemainingMilliseconds:29963, and the UNKNOWN_COMMAND rejection under TriggerResult.Error.
  • pause-point-status after each run reported Status:Enabled, IsEnabled:true, HitCount:0 — the marker was preserved.
  • With PlayMode paused first and --resume-play added, the abort reported ResumePlayResult {WasPaused:true, Resumed:true, Repaused:true}, and control-play-mode --action Status afterwards reported IsPlaying:true, IsPaused:true — PlayMode was genuinely put back into pause.

Note on CI: repository workflows are scoped to pull_request.branches [main, v3-beta], so none of them run on a pull request targeting this integration branch. The results above are local runs; CI validation happens on the final integration pull request.

A --trigger command rejected by its own argument parsing performed no
action, so the marker it was meant to hit can never fire from it. The
wait still ran the full --timeout-seconds window, then reported a
timeout with a PlayMode diagnosis describing neither what happened nor
what to fix.

Abort on that one failure only, in a dedicated internal wait state so
the timeout path's unconditional marker clear is not inherited: the
marker stays armed and a corrected trigger can be retried without
re-enabling. A wait that resumed PlayMode itself puts it back into
pause, otherwise unrelated gameplay could consume the preserved
marker's single shot and the retry would return someone else's hit.

Any other trigger failure (connection drop, disabled tool, unparseable
output) keeps waiting, since the game itself may still hit the marker.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Pause-point waits now detect rejected triggers before execution, return a dedicated structured error, preserve the marker, report trigger results, and re-pause PlayMode after abandoned resumed waits. Documentation and tests describe and validate these behaviors.

Changes

Pause-point trigger flow

Layer / File(s) Summary
Trigger rejection contracts
cli/common/errors/*, cli/project-runner/internal/projectrunner/pause_point_{trigger,errors}.go
Exports trigger-related error codes, classifies invalid-argument and unknown-command rejections, adds a trigger_failed wait state, and generates recovery actions.
Wait polling and PlayMode recovery
cli/project-runner/internal/projectrunner/pause_point_{wait_poll,resume_play}.go
Tracks trigger completion during polling, preserves raced hits and diagnostics, and re-pauses PlayMode when a resumed wait is abandoned.
Behavior validation and usage guidance
cli/project-runner/internal/projectrunner/*_test.go, .agents/skills/*, .claude/skills/*, Packages/src/Editor/CliOnlyTools~/PausePoint/*, cli/*/shared-inputs-stamp.json
Tests rejection, marker preservation, race handling, and repause outcomes; documentation records the updated trigger and resume semantics.structured error and recovery actions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant PausePointWait
  participant Trigger
  participant Unity
  CLI->>PausePointWait: arm marker and start wait
  PausePointWait->>Trigger: dispatch --trigger
  Trigger-->>PausePointWait: completion or pre-execution rejection
  PausePointWait->>Unity: query final marker status
  PausePointWait->>Unity: re-pause after abandoned resumed wait
  Unity-->>CLI: wait and recovery results
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the fast-fail pause-point trigger change and marker preservation, though it only mentions argument rejection.
Description check ✅ Passed The description is directly about the pause-point trigger fast-fail behavior and matches the changeset.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/pause-point-trigger-fail-fast

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

A --trigger whose command name does not exist is rejected before it
reaches Unity, exactly like a rejected argument value: it performs no
action, so the marker can never be hit by it. It was still costing a
full timeout window, a cleared marker, and a Retryable:true verdict on
a permanent failure — the same incident this wait state was added to
prevent, reachable by one typo.

Widen the abort condition to that class of pre-execution rejection and
rename the predicate accordingly, since the criterion is no longer
about argument parsing alone.

Drop the response-side detection: no CLI path writes an error envelope
to stdout and no Unity tool emits INVALID_ARGUMENT, so that branch was
unreachable. A Unity-side rejection arriving on stdout carries no error
code to match on.

Also restate the failure message so it no longer asserts a marker
lifetime the response may not carry when the final status re-read
fails, and document the new behavior in the pause-point skill.
@hatayama
hatayama merged commit e0022fa into feature/cli-discoverability-integration Jul 26, 2026
1 of 2 checks passed
@hatayama
hatayama deleted the fix/pause-point-trigger-fail-fast branch July 26, 2026 15:26
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