Documentation
Table of Contents

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.
  • EnvironmentChanged tells 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.handlerTimeout and environment.maxRecompilesPerHour in 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.pendingHold chooses which operations are held: allOperations, the default, or changedOperation, only the operation that found the change. It is a security setting: unset, it fails closed to allOperations, and an edit that sets changedOperation is flagged as widening.
  • environment.recompileRetry says when a waiting set is tried again. With laterOperations, the default, a later operation of its target tries it again, within environment.maxRecompilesPerHour, as do the next change of its target or intent and the next start. With nextChange, as in this file, only the next change or start does. With off, 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 TargetSchema change, with AcrossRestart true, and handles it once.
  • A provider file edited while the process was stopped raises one ProviderOptions change, with AcrossRestart true.
  • 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.recompileRetry set to off, 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.