hacifootprint
Actions

When verify conditions already held

alreadyTrue records declared verify conditions that held before firing. The handler still runs, and its result remains separate.

The failure this prevents

A consumer integration recorded this one from a real agent run.

The agent pressed a control while the thing that control does was already the case: it asked to open a domain view while it was already inside that domain. The app's store publishes on change — which is what a store does — so writing the value it already held notified nobody, updateState() never ran, and the fire sat waiting for a state report that was never coming.

The Session does not turn elapsed time into a settlement. So the settlement stayed open for the life of the session, did_it_work could only ever answer still-pending, and the model read that as the app is still working. It waited, re-checked, waited — and spent fifteen of its thirty steps on an outcome that could never arrive, then gave up with no answer.

The app is not at fault and neither is the netting. The library is the only party standing where both halves are visible, and this is it saying so.

The rule

Matching verify conditions are fire-time evidence, not an action result. When an action's declarative verify contract covers every key it declares it writes and already holds at fire time, the fire settles on its handler path rather than waiting for a state report and answers alreadyTrue.

const  = ('desk', {
  : {
    : {
      : {
        'open-billing': {
          : 'Open the billing domain view',
          : ['view.domain'],
          // The declaration that carries a VALUE — and the one this rule reads.
          : { 'view.domain': { : 'billing' } },
        },
      },
    },
  },
});
const  = .({ : 'workspace', : { 'view.domain': 'billing' } });
.('workspace', { : { 'open-billing': () =>  } });
const  = .('workspace.open-billing', { : 'agent' });
if (.) {
  .; // conditions that held before firing, not the handler's result
}

The immediate tool result includes this fire-time evidence, even while its handler is pending:

{ "ok": true, "did": "workspace.open-billing", "effectStatus": "pending",
  "transitionId": "workspace.open-billing#1",
  "alreadyTrue": [{ "key": "view.domain", "op": "eq", "threshold": "billing",
                    "actualSummary": "\"billing\"", "result": true, "redacted": false }],
  "why": "These declared verify conditions held before this action fired. alreadyTrue records that earlier check, not whether the action succeeded or changed anything. Read effectStatus, outcome, outcomeNow when present, and any error for the actual result." }

The same marker and sentence ride settledAnswer and the settled did_it_work answer; the official MCP adapter also folds them into a result when settlement arrives within its wait ceiling. The conditions remain evidence about fire time, even if the handler later writes or fails. Use the separate outcome and error fields to decide what happened.

Why the verify contract, and not writes

Effect.writes is key names only, by the law stated on it: this library never learns the value an action would set, and it does not read your handler to find out. So "the write nets out" is a fact writes alone can never establish — a rule built on it would have to invent the value it compared against, which is the one thing this library refuses to do.

verify in its declarative filter form is the one declaration that carries values: your own statement of what must hold once the action has run. So it is what this asks. That does not make it a description of the requested input. For example, quantity >= 1 may hold before a request to set quantity to 0; the handler can still reject that input. A range that already holds also does not prove a later write left the value unchanged.

An action that declares writes and no declarative verify behaves exactly as it always did. Nothing is guessed on its behalf. The correction is one line of declaration next to the action, and you get the verify contract's own settlement verdict with it.

The five conditions

Every one of them is a refusal to over-claim.

#the conditionwhat it refuses
1the action declares at least one writes keyan action declaring none never joined the state rail, so it cannot hang — marking it would be a word about a fate it does not have
2the verify contract is the declarative forma predicate is opaque by construction; asking it before the action ran asks a postcondition a question it was not written to answer
3the contract names every declared writethe partial boundary — an action writing view.domain and view.tab under a contract constraining only the first has a declared effect the contract says nothing about
4the contract holds at fire time, evaluablya failed or unevaluable check supplies no matching fire-time evidence
5any declared goTo is the page you are on at fire timea matching state check does not establish a navigation that has not happened

Your handler still runs

Read alreadyTrue as these verify conditions held before firing. The library does not skip your handler. It may write, finish without a change, or fail; none of those results follows from the marker.

What changed is only what the fire waits on: the handler path instead of the state-report queue. A handler that throws is still refused, and an unresolved handler is still pending. The marker does not establish current conditions after execution or suppress a later rejection or rollback.

The fire also never joins the state-report queue, and that is the second half of the fix. A phantom sitting in that queue would let the next real report settle it by FIFO — certifying an agent's no-op as the verified cause of motion somebody else performed.

What the record shows

A record retains the fire-time condition evidence alongside its execution result. In the concrete idempotent example, an open-billing handler finishes successfully without changing the billing view:

  • that example's row commits with an empty commit bundle;
  • its effect is 'unobservable' because no write report establishes a change;
  • after successful settlement, a journey can count the step done and any held single flight is released.

Those are results of that example, not guarantees from alreadyTrue. A failed action keeps its failure, and a later rollback remains visible through the live outcome (outcomeNow when it differs from the receipt). The condition evidence is not refreshed to describe the later world.

Run it

npm run example:already-true prints one real run of exactly this: the already-true press, the ordinary press beside it for contrast, and the same press on an action that declares writes and no verify — the honest limit, shown rather than described. Nothing in the transcript is hand-written; it drives the port a remote model actually holds. Source: examples/already-true/.

Honest limits

  • It checks declarations against Session state. No DOM is watched, no handler code is read, and no intended write value is inferred. An action that declares nothing here serves byte-identical results.
  • An allowed unmaterialized fire keeps its own word. materialized: false already says nothing in the app was wired to execute it; a second marker beside it would be two words for one press, so alreadyTrue is never stamped there.
  • It is not an outcome. A minted transitionId identifies the fire, while settlement and error fields describe its result. The marker alone proves neither success nor failure.

On this page