(
label: string,
operation: () => Promise<void>,
options: { rethrow?: boolean } = {},
)
| 1636 | } |
| 1637 | |
| 1638 | private enqueueRuntimeEventStore( |
| 1639 | label: string, |
| 1640 | operation: () => Promise<void>, |
| 1641 | options: { rethrow?: boolean } = {}, |
| 1642 | ): Promise<void> { |
| 1643 | if (!this.input.runtimeEventStore || !this.runtimeEventStoreAvailable) return Promise.resolve(); |
| 1644 | const next = this.runtimeEventQueue.then(operation, operation).catch(async (error) => { |
| 1645 | // A rejection is the ledger refusing one malformed candidate, not the |
| 1646 | // store going away: it stays healthy and readable, so the latch would |
| 1647 | // only cost this run the writes it still owes — above all its own |
| 1648 | // terminal event, which `recordRuntimeEvents` refuses once the store |
| 1649 | // reads unavailable. That is how a single refused append left a run at |
| 1650 | // `running` with no terminal event and no visible failure (#2234). The |
| 1651 | // append still fails the caller (a producer bug must not pass quietly), |
| 1652 | // but the ledger stays open so the turn can end the way every other |
| 1653 | // failure ends. |
| 1654 | // |
| 1655 | // Only that one class is exempt. A store that went away keeps latching: |
| 1656 | // nothing this run emits next can land. |
| 1657 | // |
| 1658 | // `ToolLedgerCorruptionError` also keeps latching, and that is the |
| 1659 | // right economy for THIS path: stream writes stay fail-closed against |
| 1660 | // a damaged ledger. What the latch must not cost is the terminal fact |
| 1661 | // (#2313): the health scan gates only tool-bearing appends, so the |
| 1662 | // terminal event is a write the damaged ledger would have taken. |
| 1663 | // `commitTerminalRun` therefore carries the one exception: under a |
| 1664 | // corruption latch it still attempts the terminal durability barrier |
| 1665 | // (the barrier is its own scoped probe), so the run says it ended |
| 1666 | // while everything routed through here keeps failing closed. |
| 1667 | if (error instanceof RunSealedError) { |
| 1668 | // A refusal that is correct in itself (#2311): the run already owns |
| 1669 | // its terminal fact, and a straggler from the still-draining stream |
| 1670 | // is by definition not part of it. Neither the store nor this run's |
| 1671 | // durable history is at fault, so no latch and no trace-write |
| 1672 | // failure; a caller that asked for the rejection still receives it. |
| 1673 | if (options.rethrow) throw error; |
| 1674 | return; |
| 1675 | } |
| 1676 | if (!(error instanceof ToolLedgerRejectionError)) { |
| 1677 | this.runtimeEventStoreAvailable = false; |
| 1678 | this.runtimeEventStoreFailure = error; |
| 1679 | } |
| 1680 | await this.enqueueTraceWriteFailure(error, label); |
| 1681 | if (options.rethrow) throw error; |
| 1682 | }); |
| 1683 | this.runtimeEventQueue = next.catch(() => {}); |
| 1684 | return next; |
| 1685 | } |
| 1686 | |
| 1687 | private async recordRuntimePartial(event: RuntimeEvent, streamKey: string): Promise<void> { |
| 1688 | const store = this.input.runtimeEventStore; |
no test coverage detected