mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 05:29:45 +00:00
871d94f53d
Three gaps remained in how Pulse projects a Patrol investigation's action and closes its runs, after rcourtman/Pulse#3147. - **Projections are serialized per investigation.** The lifecycle's transition callback, an investigation read and a run's close each read the action audit and apply it under the investigation's lock. A run's close also finalizes before releasing it. A projection read from an older state can no longer land after a newer one, and an approval can no longer land between a run's reconciliation and its finalization. - **The projection writes only what changed.** It records the changed action and outcome through `SetAction` and `SetOutcome` when the store offers `aicontracts.InvestigationActionStore` (a new optional interface; a store without it keeps the old `Update`). It therefore cannot undo run progress the orchestrator saved in between. The reference it writes carries the broker's canonical result and verification status (`dispositionFromRecord`) and keeps the model's causal attribution, which the audit does not hold. - **Runs that hang past their limit are finalized.** The stuck-run sweep finalizes such a run with a copy of its session marked failed, leaving the orchestrator's own session as it is. Once the orchestrator closes the run itself, its stored session is finalized instead of the copy, and a later refresh no longer reopens a record that finalization closed. - **A recovered run keeps its own status** (second commit). The sweep used to mark a run that a restart or its limit cut short after it planned a fix as `completed`, because the finding already held the action's outcome, while its session and record said `failed`. The orchestrator records the same run as `failed` when it closes itself, and every gate against a second plan keys on the `fix_queued` outcome, so the sweep now keeps the outcome and the run's own status. The matching pulse-enterprise change makes the orchestrator's saves keep the action Pulse recorded, and gives its store `SetAction`. Each side works without the other. Testing - New tests cover the per-investigation lock, field-only writes, hydration returning the stored action after a refresh replaced the plan, a close preferring the orchestrator's own close over the sweep's copy and finalizing under the lock, a run past its limit finalized as given up, a refresh not reopening a finalized record, the full action reference, and a recovered run keeping the status it closed with. Removing any one of the fixes fails its test. - On pulse-dev: gofmt, vet, race runs of the targeted tests, and full runs of `internal/ai/...`, `internal/api`, `internal/actionlifecycle`, `internal/unifiedresources/...` and `pkg/aicontracts/...`. The registry, contract, completion and browser guards pass on both commits. - Live, on a private Pro stack built from this branch and pulse-enterprise main, with a real model (Claude Sonnet through the subscription CLI) investigating a Docker container whose web server had died. In each run the model planned a container restart through `patrol_propose_action`, and the next model turn was made to fail: - **Provider error after the plan.** The run closed failed with the error, `fix_queued` and the pending restart on the session, the finding and its record. Re-investigation returned 409 `action_pending`, the restart waited in Actions, and the finding's review showed the queued fix with its plan. When the plan expired, the sweep published it, and the session, finding and record moved to `needs_attention` with the action expired. - **Deadline after the plan.** With a 300-second limit and the next turn hung, the run failed at the deadline with `fix_queued`, not `timed_out`, and the next Patrol pass did not retry it. - **Restart after the plan.** Pulse was killed mid-run. On boot the stored session closed as interrupted with the plan kept, re-investigation still returned 409, and once the grace passed a Patrol pass recovered and finalized the run. With the second commit the finding reads `failed`, matching its session and record; before it, it read `completed`. - Not observable on a local stack: the approval push, which needs a relay client, and a run hanging past its limit without closing, since the provider call is cancelled at the deadline. Unit tests cover both.