Skip to content

Commit aec1ace

Browse files
committed
docs(vrs): notify agents after material changes
1 parent a1c5fc2 commit aec1ace

2 files changed

Lines changed: 83 additions & 9 deletions

File tree

docs/vrs/02-agent-spec/requirements.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,15 @@ wrong.
7171
restarts, tears down, or rewrites an unrelated task or agent.
7272
- **SPEC-R04 Keep metadata nondisruptive:** Role and Resource binding changes
7373
update observable declaration metadata. They do not stop, replace, or
74-
relaunch a healthy task.
74+
relaunch a healthy task. A committed Resource change visible to a preserved
75+
live incarnation follows SPEC-R12; a role-only edit is not by itself a
76+
material file or Resource change.
7577
- **SPEC-R05 Materialize without process churn:** A render delta is preflighted
7678
against the complete active local fleet, then applied idempotently. A
7779
conflict fails every affected owner before the first write. Successful
78-
materialization does not itself restart a healthy task.
80+
materialization does not itself restart a healthy task. When committed bytes
81+
or Resources become visible to an already-running agent whose incarnation is
82+
preserved, reconciliation follows SPEC-R12.
7983
- **SPEC-R06 Reconcile task-set deltas narrowly:** Adding one uniquely
8084
identified PTY or exec task launches only that missing child. Removing one
8185
from a complete valid current owner explicitly tears down only an old child
@@ -127,10 +131,29 @@ wrong.
127131
is proven, without synchronous all-host availability, CAS, or an external
128132
registry. Quiet status and true dry-run report `pending`, `refused`, or
129133
`completed`; removing the mapping before completion fails closed.
134+
- **SPEC-R12 Notify a surviving agent after a material change:** When one
135+
successful reconcile transaction materially changes files or Resources
136+
visible to an already-running agent without replacing or retiring that
137+
incarnation, st2 persists one targeted event in that agent's inbox after the
138+
write transaction commits, then attempts DING. The event has a stable
139+
idempotency identifier and includes the agent identity, host, a host-local
140+
desired-state or reconcile identifier available without CAS, affected target
141+
paths but never contents or secrets, the change class, and whether follow-up
142+
is required. One transaction is coalesced to one event per affected agent;
143+
replay or duplicate delivery is harmless. A semantic no-op, unchanged render
144+
bytes, a failed or rolled-back write, a periodic check, or replacement or
145+
retirement of the old incarnation emits no such event. The inbox record is
146+
the durable fact: DING failure neither removes it nor rolls back a successful
147+
write. The running agent decides how to incorporate the change; notification
148+
alone never forces restart. Notification paths are excluded from
149+
materialization-triggered reconciliation so delivery cannot recursively
150+
trigger itself. This is a quiet event boundary, not routine status narration,
151+
and requires no harness, CAS, or external registry.
130152

131153
## Evidence boundary
132154

133155
This draft is normative documentation only. After approval, a paired external
134156
field matrix must prove every row in [spec.md](./spec.md) against the public CLI
135-
and isolated PTY/exec state. Unit tests remain necessary source evidence but are
136-
not sufficient acceptance for this contract.
157+
and isolated PTY/exec state, including exact notification inclusion, exclusion,
158+
deduplication, DING-failure, and non-recursion cases. Unit tests remain
159+
necessary source evidence but are not sufficient acceptance for this contract.

docs/vrs/02-agent-spec/spec.md

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,10 @@ normalized effects also remain equal.
4444
| Formatting, comments, KDL node order, map order | [KDL lowering](../../../crates/agent-spec/src/kdl_format.rs), [shared lowering](../../../crates/agent-spec/src/spec.rs) | If the complete normalized model and render plan are unchanged, the desired delta is `no-op` and authorizes no materialization or process-lifecycle action. Ordinary actual-state reconciliation may still discover and heal a separately absent/dead task. | Parsing is order-insensitive for agent env and maps; the resident watcher still treats a write as evidence and runs an ordinary pass. It does not yet publish a semantic-delta classification. |
4545
| Declaration path with explicit identity and host | [path/default resolution](../../../crates/agent-spec/src/discovery.rs), [cwd resolution and bus environment](../../../src/run.rs) | A move is a source-only no-op only when task IDs, resolved cwd/workspace, and state/resource anchors are unchanged. Otherwise classify the affected path or placement boundary explicitly. | Explicit identity plus host are path-independent. A moved file can still change fallback cwd or the declaration-relative state anchor, so unconditional path no-op is not implemented. |
4646
| Agent `type` | [job type model and lowering](../../../crates/agent-spec/src/spec.rs), [validation](../../../src/validate.rs) | Omitted and explicit `service` are equivalent. Any other value makes that owner scope invalid and authorizes no launch or destructive action in the unproven scope. | The normalized model supports only `service`, and `validate` reports other values. Lowering currently maps every raw value to `service`, so a caller that reconciles without a clean validation gate can still treat an invalid type as runnable. |
47-
| `role`, `resource` | [Agent Spec fields](../../../crates/agent-spec/src/spec.rs), [catalog inspection](../../../src/agents.rs) | Update observable declaration metadata. Do not change the launch fingerprint or churn a healthy task. | These fields do not drive the current ID-only adoption decision. Resource inspection exists. No general field-delta event/status surface exists. |
47+
| `role`, `resource` | [Agent Spec fields](../../../crates/agent-spec/src/spec.rs), [catalog inspection](../../../src/agents.rs) | Update observable declaration metadata. Do not change the launch fingerprint or churn a healthy task. If a committed Resource projection materially changes what a preserved live incarnation can observe, enqueue the targeted post-commit event below; a role-only edit is not itself a material file or Resource change. | These fields do not drive the current ID-only adoption decision. Resource inspection exists. No general field-delta event/status surface exists. |
4848
| Unknown `harness`, `model`, `persona`, `permissions`, `transport`, `strategy`, `meta`, or provider extension | [ignored KDL fields](../../../crates/agent-spec/src/kdl_format.rs), [runner model boundary](../../../crates/agent-spec/src/spec.rs) | Exclude ignored source from core semantic equality: core cannot compare it or wake a specialized consumer for it. A compiler/provider may observe its own input and lower a change into render, task-set, launch, or metadata fields; core acts only on that lowered delta. | Core parsing ignores these fields. Their effects become visible only after a provider lowers them into runner-normative fields or render operations. |
49-
| `render {}` operation, template bytes, resolved destination | [render parsing and execution](../../../src/materialize.rs), [full-pass gate](../../../src/run.rs) | Preflight all resolved claims against the complete active local fleet; reject incompatible shared ownership before any write; otherwise apply idempotently. Never restart a healthy task merely because workspace bytes changed. | Implemented for full and selected materialization, including complete-fleet conflict analysis and tracked-file safety. The watcher does not yet compute the smallest template dependency set described by root R13. |
49+
| `render {}` operation, template bytes, resolved destination | [render parsing and execution](../../../src/materialize.rs), [full-pass gate](../../../src/run.rs) | Preflight all resolved claims against the complete active local fleet; reject incompatible shared ownership before any write; otherwise apply idempotently. Never restart a healthy task merely because workspace bytes changed. After changed bytes commit for an already-running unchanged incarnation, persist the targeted event specified below and then attempt DING. | Implemented for full and selected materialization, including complete-fleet conflict analysis and tracked-file safety. The watcher does not yet compute the smallest template dependency set described by root R13, and successful changed writes do not yet enqueue a targeted reconciliation event. |
50+
| Successful in-place file or Resource transaction visible to a live agent | [materialization report](../../../src/materialize.rs), [message storage](../../../src/message.rs), [DING consumer](../../../src/ding/mod.rs) | After commit, coalesce one targeted durable inbox event per affected surviving agent, then attempt DING. Do not notify for no-op/unchanged bytes, failed or rolled-back writes, periodic checks, or an incarnation being replaced or retired. | Gap: materialization reports changed operations and inbox/DING transport exists independently, but reconciliation does not yet connect a committed material change to a targeted agent event. |
5051
| Add one explicit `pty`/`exec`, or add compact `ding` | [compact/explicit task lowering](../../../crates/agent-spec/src/spec.rs), [task-level reconcile](../../../src/reconcile.rs) | Launch only the uniquely missing child from desired bytes. Preserve every existing sibling PID/incarnation. | Implemented by task-ID reconciliation; generated compact DING is a normal derived exec task. |
5152
| Remove one explicit `pty`/`exec`, or remove compact `ding` | [discovery](../../../crates/agent-spec/src/discovery.rs), [task-level reconcile](../../../src/reconcile.rs), [PTY/exec runtime state](../../../src/run.rs) | A complete valid current owner plus ordinary host-local runtime metadata must attribute the old child to the exact catalog, host, owner, task ID, and current incarnation. Then tear down only that child. Legacy/unattributed records hold/refuse explicit recovery; no synced prior snapshot, tombstone, or CAS is required. | Gap: current reconciliation sees only current declarations, while runtime records do not yet provide the full ownership/incarnation proof, so a removed task becomes invisible and may remain running. |
5253
| Task `kind`, `command`/`argv`, resolved effective `cwd`, agent/task `env`, task `tags`, synthesized managed environment including supervisor routing | [normalized task fields](../../../crates/agent-spec/src/spec.rs), [effective task target](../../../src/reconcile.rs), [spawn construction](../../../src/run.rs) | Change the desired launch/backend fingerprint. Dead/absent launches use the latest desired definition. A healthy older or unproven incarnation remains alive and reports `drifted` or `unknown` until an explicit task-scoped replacement. A source-path edit that changes effective cwd also belongs here. | Gap tracked by [#40](https://github.com/compoundingtech/st2/issues/40): current reconciliation adopts any healthy matching task ID without a desired/observed fingerprint or incarnation binding. `run.rs` currently passes task tags as backend spawn arguments, so they cannot be classified as pure metadata. |
@@ -68,7 +69,8 @@ incarnation; a stale or absent binding is `unknown`, never converged.
6869

6970
| Desired task | Actual task | Delta class | Normative action |
7071
| --- | --- | --- | --- |
71-
| unchanged active | healthy | source/metadata/render/policy no-op | adopt; materialize or update declaration state only when that class requires it |
72+
| unchanged active | healthy | source/metadata/policy no-op or unchanged render bytes | adopt; perform no write and emit no material-change event |
73+
| unchanged active | healthy | committed material in-place change | adopt; persist one coalesced targeted event after commit, then attempt DING without forcing restart |
7274
| active | absent | any launch definition | launch latest desired bytes once |
7375
| active | dead | `service` | retain diagnostics, reap eligible dead state, launch latest desired bytes once |
7476
| active | dead or absent | `adopt-only` | hold; do not reap or launch |
@@ -120,6 +122,49 @@ Core st2 does not currently parse or execute this mapping. The syntax above
120122
must not be treated as supported catalog input until a later runtime change and
121123
paired acceptance make it normative.
122124

125+
## Targeted material-change notification
126+
127+
This boundary applies only when a reconcile transaction commits a material
128+
change visible to an agent whose exact live incarnation remains in place. A
129+
successful changed `render {}` upsert is the primary case. After the write
130+
transaction commits, st2 must first persist one event in the affected agent's
131+
inbox and then attempt DING:
132+
133+
```text
134+
commit changed targets -> persist targeted inbox event -> attempt DING
135+
```
136+
137+
One transaction produces at most one event per affected agent, even when it
138+
changes multiple targets. The event carries:
139+
140+
- a stable event/idempotency identifier for the logical committed transaction;
141+
- the affected agent identity and pinned host;
142+
- the available host-local desired-state or reconcile identifier, without a
143+
CAS dependency;
144+
- affected render or Resource target paths, never file contents, rendered
145+
bytes, environment values, credentials, or other secrets;
146+
- the normalized change class; and
147+
- whether the agent must perform follow-up.
148+
149+
Retries and duplicate delivery reuse the stable event identity and are
150+
harmless. The inbox record is the durable delivery fact; DING is only the wake
151+
attempt. A failed DING leaves the event queued and never rolls back the already
152+
committed write. The live agent chooses when and how to incorporate the update,
153+
and st2 does not restart it merely to deliver the event.
154+
155+
A semantic no-op, unchanged destination bytes, a failed or rolled-back write,
156+
a periodic audit with no material delta, and replacement or retirement of the
157+
old incarnation produce no event under this contract. Inbox creation and the
158+
delivery sidecar's own state remain supervisor no-ops, so an inbox beneath
159+
watched or materialized state cannot recursively start another reconciliation
160+
or notification transaction. This targeted post-commit edge replaces polling;
161+
it does not add routine narration to quiet status.
162+
163+
Core st2 does not currently emit this reconciliation event. Runtime support and
164+
paired acceptance must prove changed-render inclusion, every exclusion above,
165+
one-event coalescing, duplicate idempotency, durable retention after DING
166+
failure, and absence of watcher recursion.
167+
123168
## Reporting and dry-run
124169

125170
Before mutation, quiet status and true dry-run must expose, per affected task or
@@ -128,7 +173,8 @@ refused proof scope:
128173
- normalized desired task ID and pinned host;
129174
- changed field category, without leaking secret environment values;
130175
- action class: `no-op`, `materialize`, `launch`, `hold`, `adopt`,
131-
`drifted`, `replace-required`, `teardown`, `migrate`, or `refuse`;
176+
`notify-after-commit`, `drifted`, `replace-required`, `teardown`, `migrate`,
177+
or `refuse`;
132178
- desired and observed launch-fingerprint identities and exact-incarnation
133179
binding state when available;
134180
- actionable refusal reasons.
@@ -168,7 +214,12 @@ machine-readable fingerprint state. A true no-write plan is tracked by
168214
for catalog root, PTY root, or another explicitly selected sensitive root.
169215
A future optional `moved` mapping may explicitly guide rename/re-address
170216
intent under SPEC-R11, but ordinary edits never infer it.
217+
6. **Post-commit notification:** A material transaction affecting a surviving
218+
live incarnation produces one durable targeted inbox event after commit,
219+
followed by a best-effort DING. No-op, failure, polling, replacement, and
220+
retirement paths remain silent; notification never forces process churn.
171221

172222
After these decisions and merge, external acceptance should execute a
173223
table-driven field matrix against both PTY and exec tasks, including compact
174-
DING, partitions, invalid snapshots, and unrelated-sibling identity proofs.
224+
DING, targeted material-change notification, partitions, invalid snapshots,
225+
and unrelated-sibling identity proofs.

0 commit comments

Comments
 (0)