feat(M3): re-run engine — replay + rebuild as chained-cron jobs under CPO guardrails

Two operator primitives, both queued (never synchronous — CPO hard
constraint), all mutations in the cron handler after re-validating
eligibility UNDER the generation lock:

- Replay (Stage_Replay + on_replay_job): one governed chat call from the
  latest edited input fork. options_from_body() is the exact inverse of
  build_body()+apply_reasoning_budget() (headroom subtracted back out;
  reasoning pinned explicitly so global-setting drift can't reshape a
  replay). Goes through send_chat_completion -> cost governor reserves
  against the SAME run row (guardrail 4), retry/empty-guard machinery,
  B1 recorder. Fresh output lands in the stage snapshot; downstream
  stale-marked (guardrail 3); human_modified set at execution entry
  (guardrail 2).
- Rebuild (on_rebuild_job): demote target+downstream to pending, re-enter
  Article_Worker with the idea from the stored seed; upstream done stages
  reuse (never re-charged), demoted stages rebuild prompts from CURRENT
  snapshots — how an upstream edit propagates. Decisions recorded while
  rebuilding a human-modified item are flagged.
- Rerun_Eligibility: published/future/private = frozen (guardrail 5,
  server-side); executing runs blocked; editable stages = writer chat
  stages (outline/draft/polish); chain-order downstream sets.
- Run_State::reopen(): atomic terminal->running gate, ceiling
  re-snapshotted from the current setting, accumulated spend NEVER reset.
- Publisher: re-run output refreshes the existing UNPUBLISHED post
  (status preserved — re-runs never publish); published posts untouched.
- Article_Worker persists the exact idea payload as a seed (INSERT-once)
  so rebuilds can't mis-derive item_key and duplicate a post.
- Rerun_Job_Support: queue/lock/budget/status-transient plumbing; jobs
  ride the M1 status transient so the board shows queued->pickup->result.

Agent-Session: engineer-prab-m3-20260612
This commit is contained in:
peptiderepo
2026-06-12 13:15:50 +00:00
parent aaf1b4274e
commit 38500b2781
14 changed files with 1705 additions and 5 deletions

View File

@@ -252,6 +252,41 @@ class PRAutoBlogger_Run_State {
return is_array( $rows ) ? $rows : array();
}
/**
* Reopen a terminal run for operator-deliberate new spend (M3
* re-runs). done/failed/halted -> running; finished_at cleared; the
* cost ceiling is RE-SNAPSHOTTED from the current setting — the
* deliberate re-run action adopts the operator's current per-run
* policy (can lower as well as raise), exactly as a new run would.
* Accumulated reserved/settled spend is kept: the governor treats
* re-runs as new spend on the SAME run (CPO guardrail 4).
*
* The conditional UPDATE is the atomic gate: an actively executing
* run (pending/running) can never be reopened.
*
* @param string $run_id Run UUID.
* @return bool Whether the run transitioned to running.
*/
public static function reopen( string $run_id ): bool {
if ( '' === $run_id || ! self::is_available() ) {
return false;
}
global $wpdb;
$table = self::table_name();
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$updated = $wpdb->query(
$wpdb->prepare(
"UPDATE {$table}
SET status = 'running', ceiling_usd = %f, updated_at = %s, finished_at = NULL
WHERE run_id = %s AND status IN ('done','failed','halted')",
self::ceiling_setting(),
current_time( 'mysql' ),
$run_id
)
);
return 1 === (int) $updated;
}
/** Reset per-request caches (tests). */
public static function flush_cache(): void {
self::$table_ok = null;