v1.0.0 - 2026-09-15
A rewrite around the control point, designed from the problem rather than
from 0.1. There is no compatibility layer; docs/upgrading.md
lists every 0.1 surface and its replacement.
Added - the control point
- Two forms.
Monitor::control('payment.charge', $this)declares a point
inline; a class extendingControlPointwith a#[Point]attribute declares
one that the inventory can read in full, the stub can generate, and an agent
can be sent to. Both produce the sameOutcome. - Two terminals.
run()returns the value or throws what escaped;
attempt()returns theOutcomeand never throws for what the operation did. - Risks and corrections.
recover(Class, fn)declares an expected failure;
the handler's return value is the result,null,falseand0included.
A handler that throws escalates. First declared match wins. - Escalation.
escalate()with a closure or anEscalationclass, called
for every failure no correction covers; an escalation that throws is recorded
asEscalationFailedand the original exception still propagates. - Limits.
within()records a duration breach and never fails a completed
run;attempts()caps retries whatever the policies ask;ensure()fails a
run whose result is wrong, after every policy, so it is never retried. - Policies.
Retrywith backoff, multiplier and jitter;Transaction
retrying deadlocks as whole transactions;Breakeron a named circuit. A
fixed pipeline order (breaker, retry, transaction) so a breaker sees one
failure per run. Custom policies implement one interface. - Profiles.
external,database,messagingandinternalbundle
policies and limits from config; a point starts from one and overrides what
it declares. - Nesting. Points nest to any depth with a parent run id and the stack on
every record. A child's escalation reaches the parent's corrections, a
child's refusal surfaces asBreakerOpen, and retries never compose across
the stack. - Outcome. One readonly object per run: status, value, exception, the risk
recovered from, attempts, duration, limits breached, policies, context,
timeline and stack.
Added - records
- One schema for every transition (
monitor/1, published at
resources/schema/record-1.json):point,run_id,parent_run_id,
trace_id,domain,origin,status,attempts,duration_msand the
rest as log context fields, with a readable message. Levels per event are
configurable. - Domain as a field. Derived from the origin's namespace through a
configurable map, so "escalations per domain" is a query rather than a regex. - Redaction through Redactor. Context and exception messages pass through
theobservabilityprofile; identifiers are left alone so records stay
joinable. - Events first.
PointStarted,PointRetried,PointLimitBreached,
PointRecovered,PointEscalated,PointRefused,PointEnded,
EscalationFailed,BreakerOpened,BreakerHalfOpen,BreakerClosed.
Log lines and the store are listeners. - NDJSON.
JsonTapandRecordFormatterwrite one JSON object per line
with the record fields at the top level, and never lose a line to bad bytes. Monitor::log($origin)is a PSR-3 logger bound to an origin, adding
originanddomainto every record it writes.
Added - tracing
- Laravel Context. The trace id and the current point live in Context, so
they reach queued jobs and every log line the application writes. Monitor
keeps no request state of its own. - W3C traceparent read and written by
StartTrace, alongside a legacy
header; incoming values are validated and an invalid one is replaced. Http::traced()adds both headers to an outgoing request.- Jobs and console pick up or start a trace on their own.
Added - breakers
- A real state machine: closed, open, half-open with a single probe, kept
in the cache so every process sees the same circuit. "after" failures
"within" seconds open it "for" seconds. - Standalone API through
Monitor::breaker()and theCheckBreakers
route middleware with a realRetry-After.
Added - store
monitor_outcomes, off by default. Rows are written after the response
or the job, never on the request path; a failing write is logged once and
never reaches a control point.monitor:outcomesandmonitor:prune.
Added - verification
Monitor::fake()records every outcome while still running the point,
cans values withreturning()and failures withfailing(), and asserts
withassertRan,assertSucceeded,assertRecovered,assertEscalated,
assertRefused,assertRetried,assertLimitBreached,
assertNothingEscalatedand friends.monitor:pointsinventories every point without running it, class-form
points in full and inline points by name, with table, JSON and SARIF
output.--checkfails the build on duplicate names, names off the pattern,
a point with no escalation and no catch-all,control()that cannot be read
statically, a class in a critical namespace that is not a control point,
and warns on catch-alls without escalation and computed names.monitor:explaindescribes one point's contract in prose.make:control-pointgenerates the class form and a test that already
uses the fake.- A Pest expectation (
toBeControlled(),toHaveCompleteControlPoints())
and a PHPStan rule (monitor.uncontrolled) with the same semantics. - The fake follows the framework's.
MonitorFakeimplements theFake
contract,Monitor::fake()is idempotent,outcomes()is aCollection,
and every positive assertion has its negative:assertNotRan,
assertNotRecovered,assertNotEscalated,assertNotRefused,
assertNotRetried, plusassertRanOnce.assertRecoveredand
assertEscalatedtake a class or a closure.
Added - agents
- Guidelines shipped at
.ai/guidelines/core.blade.php, which Laravel
Boost composes into every consuming application's agent guidelines. - An MCP server on
laravel/mcp:list_points,explain_point,
outcomesandescalationstools, the record schema and guidelines as
resources, and awrap_operationprompt. Read-only, off by default, every
response redacted JSON-aware. Registered from config, or by hand in
routes/ai.php. - A Boost skill at
resources/boost/skills/monitor-development/SKILL.md,
the path Laravel's own packages use, alongside the guideline.
Added - conventions, following Laravel's first-party packages
- Contracts live in
Contracts\:Runner,Escalation,Limit,
PolicyandRule. Commands live inConsole\Commands\. - Services are open, value objects are
final readonly.Outcome,
RunInfo,BreakerState,BreakerConfig,Decision,PointDescription,
Finding,ScannedFile, the limits and#[Point]are immutable data;
everything else can be extended.Outcome,BreakerState,
PointDescriptionandFindingimplementArrayableandJsonSerializable. ControlisConditionableandMacroable, andStructuredLoggeris
Conditionable, so a declaration can branch withwhen()andunless()and
an application can add methods of its own.- Point names accept a backed enum wherever a name is passed, so an
application can centralise them. MonitorExceptionis an interface. Each exception keeps its natural SPL
parent: the fourInvalid*exceptions extendInvalidArgumentException,
the risks extendRuntimeException. Messages quote values in brackets.CircuitBreaker::attempt()ispermit(), so it no longer shares a name
withControl::attempt()on the same facade. The policies drop their
setTimes()andsetRetries()setters in favour of constructor arguments.- The provider does nothing at boot beyond registering: commands and
publishing sit behindrunningInConsole(), the migration is published with
publishesMigrations()and never auto-loaded, middleware aliases are
registered when the router resolves, the console trace starts on
CommandStarting, andphp artisan aboutshows the package's settings. - The store flushes where first-party packages flush: after each request
and command through the kernels, after each job, onQueue::loopingand
worker stop, and when its buffer reaches 100, so it is safe under Octane and
in long-running commands. - Redaction degrades: a Redactor failure inside the recorder or the MCP
server never reaches a control point.
Added - after the first review
- Idempotency.
once('invoice:'.$id, ttl: 3600)runs a point at most once
per key inside a window; a second run is refused before anything executes
with aDuplicaterisk. The key is released when the run fails inside the
policies and kept when it completed, because the side effect has happened.
Outermost in the pipeline, so a refused run never claims a key. - A typed
run().Control::run()carries a template, so PHPStan knows
the value a callback returns. Http::breaker('stripe')on the HTTP client, and on anyPendingRequest:
the same circuit a control point uses, refusing without sending while open
and counting connection failures and 5xx responses.- Dispatchable points.
ChargeCard::dispatch($invoice)runs a control
point class as a queued job throughQueue\RunControlPoint, named and
tagged after the point for Horizon; a run refused by an open breaker
releases the job for the breaker's retry-after instead of failing it. - Corrections as classes.
recover(Class, Handler::class)with a class
implementingContracts\Correction, resolved from the container and named
in the inventory. escalateLimits()hands a run that completed but breached a limit to
the escalation, andthrottleEscalation(seconds)lets at most one
escalation through per point per window, announcing the rest with
EscalationThrottled.Monitor::log()with no origin binds to the running control point, so
ad hoc lines inside a point carry its origin and domain.Outcome::$startedAtand$endedAt, written to the store's new
started_atcolumn and toended_atin place of the flush time.- Jobs start clean. A job hands off the control stack it inherited through
Context and keeps the dispatching run asdispatched_from_run, so a point
inside a job has no parent from another process. The
Queue\Middleware\WaitForBreakerjob middleware releases any job for a
breaker's retry-after while the circuit is open.
Changed
- Requires PHP 8.3 to 8.5 and Laravel 12 or 13. Laravel 11 is no longer
supported. - Environment variables are
MONITOR_STORE_ENABLEDandMONITOR_MCP_ENABLED. kirschbaum-development/redactor1.x is a hard dependency.
Removed
Monitor::controlled(),catching(),onUncaughtException(),
withCircuitBreaker(),withDatabaseTransaction(),overrideContext(),
addContext(),overrideTraceId()andNestedControlledBlockException.Monitor::time(),LogTimer,Monitor::redactor(), the internal
LogRedactor,OriginWrapperand the origin prefix, separator and wrapper
settings.- The
enabled,exception_trace,console_auto_trace,trace_header,
circuit_breakerandredactorconfig keys.