Change detection
A change is a new state of something the client depends on: a target's schema, a document's running version, the provider options, or the values a compiled mapping receives. When the client finds one, it records it, runs the handlers you registered, checks it at the Environment checkpoint and reports what happened.
| Kind | Source | A change when |
|---|---|---|
TargetSchema |
a target | its probe token changed, or, for a target with no probe, its description changed |
Document |
a document in the client's document store | a new version runs, or the running version's content changed |
ProviderOptions |
the provider file or its options source | the options changed, while the process ran or while it was stopped |
MappingShape |
a compiled mapping | the values it receives drift from the shape it was compiled for, or a lookup misses; no shipped handler acts on it |
A change carries its kind, its source, the fingerprint now and the one it replaced, the entities and fields added,
removed or changed where there are some, and AcrossRestart, which is true for a change found at startup.
When the client looks
A target operation, PlanAsync, CheckAsync, ExecuteAsync or RunAsync of a TargetRuntime, checks its target
before it runs. It first waits for the changes the client is handling in the background, then runs the handlers of any
change it finds, and reports them in its outcome, so it never runs against a description or a rule set the client
already knows is stale.
Every other call does not wait: TransformAsync and the calls built on it, catalog intents, compiled mappings and
embeddings. A change found in the background, such as a document's new version or new provider options, is handled on a
thread pool thread, and a call that starts while its handlers run uses what is running then. Rule sources read the
running version at each check, so such a call never runs with no rules.
CommandOutcome outcome = await runtime.RunAsync("Customers in California");
if (outcome.Maintenance is { } report)
{
foreach (EnvironmentChange change in report.Changes)
Console.WriteLine($"{change.Kind} of {change.Source}, added fields: {string.Join(", ", change.Delta?.AddedFields ?? Array.Empty<string>())}");
foreach (string problem in report.Problems)
Console.WriteLine(problem); // a handler that failed, ran out of time or left a rule set pending
}
if (!outcome.Executed && outcome.Check.Decision?.Checkpoint == GuardrailCheckpoint.Environment)
foreach (string refusal in outcome.Check.Refusals)
Console.WriteLine(refusal); // the Environment check refused the operation
CommandOutcome.Maintenance is the MaintenanceReport of the changes handled since an operation last reported, the
ones found in the background included: the changes, the problems, the number of rule sets left waiting, and the model
calls and tokens the handlers spent. It is null when nothing changed.
PlanAsync and CheckAsync return no outcome, so the next operation reports what they handled. Each change is
reported once.
Handlers and listeners
An IEnvironmentChangeHandler receives each change and an EnvironmentChangeContext, and returns when it is done. The
context gives it the client, a logger, a target's new description, and ReportPending for a rule set it could not
bring up to date. This handler leaves a rule set waiting when a target's schema gains fields:
/// <summary>
/// Leaves a rule set pending when a target's schema gains fields, so every operation that names the set waits until a
/// later change of the target is handled without it.
/// </summary>
public sealed class HoldOnNewFields(string set) : IEnvironmentChangeHandler
{
public ValueTask HandleAsync(EnvironmentChange change, EnvironmentChangeContext context, CancellationToken ct = default)
{
// A target seen for the first time has no previous fingerprint, so there is nothing to compare it with.
if (change.Kind == EnvironmentChangeKind.TargetSchema && change.PreviousFingerprint is not null
&& change.Delta is { AddedFields.Count: > 0 } delta)
context.ReportPending(set, "new fields wait for review: " + string.Join(", ", delta.AddedFields));
return ValueTask.CompletedTask;
}
}
// builder: an IntentClientBuilder with your providers and rule files.
var client = builder
.WithDocumentStore(store) // the client records what it has seen in the store's state, so a restart finds what changed
.AddEnvironmentChangeHandler(new HoldOnNewFields("customers-read"))
.Build();
// Told about each change after its handlers ran. A listener decides nothing.
client.EnvironmentChanged += (_, change) => Console.WriteLine($"{change.Kind} of {change.Source}");
- Handlers run one after another, each within
environment.handlerTimeout, one minute unless a setting says otherwise. A handler that runs out of time is cancelled, and one that throws is treated the same way: the change is recorded, the previous version of what the handler maintains keeps running, and a problem is reported. The change is not raised again. EnvironmentChangedtells your code about each change after its handlers ran. A listener does not decide what IntentSharp does.- The client keeps its record of what it has seen in the document store's state, so a change made while the application was stopped is found at the next start.
The Environment checkpoint
After its handlers run, a change is checked at the Environment checkpoint with three subjects: environment.kind
(targetSchema, document, providerOptions, mappingShape or other), environment.source, and
environment.pendingRecompile, the number of rule sets left waiting.
- A change a target runtime finds is checked with that runtime's sets, else the client's default sets, and a refusal refuses the operation.
- A change found in the background, which no runtime found, is checked with the client's default sets and the sets of every target runtime created over the client. A set named only on a runtime therefore applies to a document's new version too. A refusal refuses the next target operation, once.
- With no set named anywhere, the check is skipped.
- The handlers of a change run under the settings of the same sets, so
environment.handlerTimeoutandenvironment.maxRecompilesPerHourin a set named only on a runtime bound them too.
Rule sets that wait
A rule set waits when a handler could not bring it up to date: the compile of a
rule file written as intent failed, reached its cap or was held by an edit
writer, or a handler of your own reported it with ReportPending, as the handler above does. The previous version of
the set keeps running.
While a set waits, every operation of a target runtime that names it is checked at the Environment checkpoint again,
after it senses the target, with environment.pendingRecompile counting its waiting sets. A rule like this one
holds operations until the rules catch up:
intentsharp: rules/1
set: customers-changes
checkpoint: environment
rules:
- id: hold-while-pending
subject: environment.pendingRecompile
count: { max: 0 }
message: The rules for this database wait to be brought up to date, so no command runs.
settings:
environment.recompileRetry: nextChange
environment.maxRecompilesPerHour: 3
Below, two clients over one database and one document store stand for two processes, each with the handler above and a
runtime that names this set and the targets guide's customers-read. After a column is added, client A's
next command finds the change, and its handler leaves customers-read waiting:
The first command of client A:
Executed: true
Change: TargetSchema of sqlite, seen for the first time
Added entities: Customers, PaymentCards
After ALTER TABLE Customers ADD COLUMN Phone TEXT, client A's next command:
Executed: false
Change: TargetSchema of sqlite
Changed entities: Customers
Added fields: Customers.Phone
Pending rule sets: 1
Problem: the rule set "customers-read" was not brought up to date: new fields wait for review: Customers.Phone
- customers-changes/hold-while-pending: The rules for this database wait to be brought up to date, so no command runs.
- the Environment check refused the change of sqlite (targetSchema): The rules for this database wait to be brought up to date, so no command runs.
Client A's command after that:
Executed: false
- customers-changes/hold-while-pending: The rules for this database wait to be brought up to date, so no command runs.
- the Environment check refused the operation while rule sets it names wait to be compiled again (customers-read): The rules for this database wait to be brought up to date, so no command runs.
A command of client B, on the same document store:
Executed: false
- customers-changes/hold-while-pending: The rules for this database wait to be brought up to date, so no command runs.
- the Environment check refused the operation while rule sets it names wait to be compiled again (customers-read): The rules for this database wait to be brought up to date, so no command runs.
- The hold applies in every process that shares the document store. The waiting sets are recorded in the store's state and every target operation reads them, so client B, which found the change already recorded and ran no handler, is held too. When the state cannot be read, an operation goes by the sets its own process holds.
environment.pendingHoldchooses which operations are held:allOperations, the default, orchangedOperation, only the operation that found the change. It is a security setting: unset, it fails closed toallOperations, and an edit that setschangedOperationis flagged as widening.environment.recompileRetrysays when a waiting set is tried again. WithlaterOperations, the default, a later operation of its target tries it again, withinenvironment.maxRecompilesPerHour, as do the next change of its target or intent and the next start. WithnextChange, as in this file, only the next change or start does. Withoff, the library never does.- A set stops waiting when a handler takes a recompile of it for that target and does not report it again, which a compile that applied does, or when a later change of the target is handled without the set being reported.
One change, handled once
Several processes can share one document store. Each fingerprint is recorded in the store's state with a version tag,
so two processes that see the same change compete for one record, and only the winner runs the handlers. A store of
your own that several clients or processes share overrides TryCreateStateAsync, so that the first record of a key is
created once; both shipped stores do.
Restarts
Across a restart the client compares what it finds with the record in the store's state:
- A target whose schema changed while the process was stopped raises one
TargetSchemachange, withAcrossRestarttrue, and handles it once. - A provider file edited while the process was stopped raises one
ProviderOptionschange, withAcrossRestarttrue. - A rule file edited on disk while the process was stopped is checked by its validator at the next start, and runs only when it passes.
- A target whose record holds waiting sets raises its first description as a change, so the handlers try again or
report the sets again. With
environment.recompileRetryset tooff, the sets stay held and nothing is raised.
Recompile caps
environment.maxRecompilesPerHour caps the recompiles of a rule set, 3 an hour unless set. At the cap the previous
version of the rule file runs, and a problem is reported. 0 never recompiles, so a rule file that must not change
without a person is set to 0. The cap is shared through the store, so processes that retry together take no more
recompiles than it allows. Raising it or environment.handlerTimeout in a file, or setting either to unlimited, is
allowed and flagged as widening:
An edit that raises environment.maxRecompilesPerHour from 3 to 10:
Status: Applied
WidensSecurity: true (environment.maxRecompilesPerHour)
Compiled mappings have a cap of their own, MappingOptions.MaxRecompilesPerHour (Compiled mappings).
Limits
- Only target operations wait for changes. Every other call sees a change once its handlers finish.
- A runtime's sets join the background check when the runtime is created and stay for the life of the client, so a set that a runtime names and the rule source does not load refuses every change found in the background.
- A process that reported a set waiting keeps holding its operations after another process brought the set up to date, until it tries the set again, handles a change of the target itself, or restarts. It fails closed: operations are held, never run under stale rules.
- How a fingerprint is computed, and how the folder store notices that a file changed, are not part of the contract and can change.