feat(events): a failed step may say when to retry (MODULE_API 1.15.0)
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 48s
PR Checks / client-build (pull_request) Successful in 59s
PR Checks / server-tests (pull_request) Successful in 6m6s

A module's perform() may answer { ok: false, retry: true, retryAfterMs }
and the runner waits that long instead of the flat EVENT_STEP_RETRY_MS
(Rust PLAN_REDESIGNS §14.3.13, D371). Dangerous Treasures' warm-up is the
first user.

- dispatch.classify reads it on the retry shape only, clamped to 60 000 to
  1 800 000 ms; a value that is not a number is dropped and logged, and the
  step retries on the flat delay.
- It is one attempt of EVENT_STEP_MAX_ATTEMPTS and degrades the run like any
  retry. A run paused or cancelled in the wait is not picked up.
- Every step.retry log line now carries retryAt (and retryAfterMs when
  hinted); the run console reads "retrying at 20:10".
- MODULE_API 1.15.0, additions only; the engagement manifest is regenerated.
  Module-uo (^1.10.0) answers none of it and is unaffected.
- Test harness: resourcesDb.finishedRunsWithNothingLeft was never stubbed in
  eventRunner.test.js, so a tick that reached the cleanup sweep with a
  terminal run waited on the dead pool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-15 02:39:43 -05:00
parent a981b2b854
commit 769794d183
9 changed files with 229 additions and 17 deletions

View File

@@ -966,6 +966,15 @@ function detailValue(value) {
export const logKindWord = (kind) => KIND_WORDS[kind] || kind
// "20:10", in the viewer's own clock. Time only: the log line it sits in carries
// its own timestamp, and a retry is at most thirty minutes after it.
function clockTime(value) {
if (!value) return ''
const t = new Date(value)
if (Number.isNaN(t.getTime())) return ''
return `${String(t.getHours()).padStart(2, '0')}:${String(t.getMinutes()).padStart(2, '0')}`
}
/**
* One log line as a sentence.
*
@@ -992,8 +1001,12 @@ export function describeLogLine(line) {
return `${line.phase} finished`
case 'step.parked':
return `${d.action} is waiting on a human`
case 'step.retry':
return `${d.action} failed, attempt ${d.attempt} of ${d.of}${d.error ? `: ${d.error}` : ''}`
case 'step.retry': {
// `retryAt` is on every retry line since MODULE_API 1.15.0; a line written
// before it says what it always said.
const at = clockTime(d.retryAt)
return `${d.action} failed, attempt ${d.attempt} of ${d.of}${at ? `, retrying at ${at}` : ''}${d.error ? `: ${d.error}` : ''}`
}
case 'step.status':
return d.control
? `${d.action} → ${d.to}${by} — ${d.control}${d.note || d.reason ? `: ${d.note || d.reason}` : ''}`

View File

@@ -11,7 +11,10 @@
// that the two files can drift, so a test asserts they agree
// (client/test/moduleRegistry.test.js) rather than trusting a bump to remember
// both.
// 1.14.0 — an action param's `multiple` (Rust PLAN_REDESIGNS §12.3): a param
// 1.15.0 — a failed step's `retryAfterMs` (Rust PLAN_REDESIGNS §14.3.13): when
// to try again. Server-side; the run console's retry line now says when. This
// file bumps for the reason at the top.
// 1.14.0 —an action param's `multiple` (Rust PLAN_REDESIGNS §12.3): a param
// that holds several values, drawn by core's step editor as a multi-select.
// Server-side declaration; this file bumps for the reason at the top.
// 1.13.0 — the event step editor (Rust PLAN_REDESIGNS §2): event templates, a
@@ -88,4 +91,4 @@
// but the two halves state ONE version: a module declares a single coreApi range
// and is served one chunk, so a client that claimed 1.0.0 while the server
// answered 1.1.0 would be two answers to one question.
export const MODULE_API_VERSION = '1.14.0'
export const MODULE_API_VERSION = '1.15.0'

View File

@@ -347,6 +347,21 @@ test('the log lines a run produces all render as something', () => {
}
})
test('a retry line says when it will run (MODULE_API 1.15.0), and an older line still reads', () => {
// Built from local parts so the assertion holds in any timezone the suite runs in.
const at = new Date(2026, 9, 15, 20, 10, 0)
const text = describeLogLine({
kind: 'step.retry',
detail: { action: 'rust.dt.start', attempt: 1, of: 3, error: 'not ready yet', retryAt: at.toISOString() },
})
assert.equal(text, 'rust.dt.start failed, attempt 1 of 3, retrying at 20:10: not ready yet')
const before = describeLogLine({ kind: 'step.retry', detail: { action: 'core.announce', attempt: 2, of: 3 } })
assert.equal(before, 'core.announce failed, attempt 2 of 3')
const junk = describeLogLine({ kind: 'step.retry', detail: { action: 'a', attempt: 1, of: 3, retryAt: 'soon' } })
assert.equal(junk, 'a failed, attempt 1 of 3', 'an unreadable time is left out, not shown as NaN')
})
// ── The one log line core did not compose (Phase 15) ──────────────────────
test('a module detail line renders the module keys, not the kind id', () => {