Skip to content

Commit 73ff1ce

Browse files
committed
Stabilize DING delivery and remove root shepherd
1 parent 6166008 commit 73ff1ce

16 files changed

Lines changed: 259 additions & 1275 deletions

File tree

INVARIANTS.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ materialization, messaging, DING, or presence must preserve them.
99
| **Transport-decoupled lifecycle** | Each task is isolated from a supervisor/transport process-group or cgroup cascade. | `tests/transport_isolation.rs`; `tests/transport_isolation_macos.rs` |
1010
| **Clean exec teardown** | Killing an exec task reaps its whole process group. | `tests/exec_backend.rs::exec_kill_reaps_the_whole_process_group_not_just_the_leader` |
1111
| **Bounded restart diagnostics** | Relaunching an exec task preserves the just-finished log as one prior generation while bounding retained diagnostics to current plus prior. Final retirement removes the PID and both logs. | `tests/exec_backend.rs::exec_restart_reap_keeps_bounded_diagnostics_and_final_remove_cleans_them`; `tests/run.rs::up_once_finally_removes_dead_retired_tasks_without_restarting_them` |
12-
| **Exactly-once-safe native bus** | Messages use stable `<unix-ms>-<rand6>.md` files. An archive filename is a durable receipt that suppresses restored inbox replicas and makes repeated archive cleanup idempotent. | `src/message.rs::filename_grammar`; `src/message.rs::archive_receipt_suppresses_and_idempotently_cleans_a_restored_inbox_copy`; `tests/message.rs` |
13-
| **Generic delayed-submit native DING** | Each post-start unread message becomes one normalized `[DING]` frame sent as one exact PTY sequence: literal notice bytes, a fixed 500 ms pause, then Return. DING does not inspect renderer or pane state and never sends Escape. Startup backlog is silent; new arrivals remain FIFO; archive receipts suppress re-DING; `busy` and `dnd` defer delivery without loss. | `src/ding.rs::poke_text_normalizes_and_bounds_untrusted_fields`; `src/ding.rs::malicious_controls_cannot_escape_the_single_paste_frame`; `src/ding.rs::pty_send_is_one_exact_delayed_sequence_without_inspection_or_escape`; `src/ding.rs::session_watch_has_startup_grace_debounce_and_live_reset`; `src/ding.rs::new_arrivals_is_fifo_and_archive_receipts_prevent_reding`; `src/ding.rs::pending_delivery_respects_status_fifo_archive_and_transport_retry`; `src/ding.rs::startup_backlog_is_silent_and_only_post_start_arrivals_poke` |
14-
| **Agent-declared delivery gate** | The shipped bus contract requires agents to declare `busy` before executing work, use `available` only while yielding or ready, and reserve `dnd` for an explicit hold. Both native harnesses materialize that contract; DING relies on those declarations rather than renderer inspection. | `tests/compile_agent.rs::compile_agent_generates_claude_then_materializes_verbatim_persona`; `tests/compile_agent.rs::compile_agent_generates_codex_then_materializes_composed_agents_md`; `src/ding.rs::pending_delivery_respects_status_fifo_archive_and_transport_retry` |
12+
| **Exactly-once-safe native bus** | Messages use stable `<unix-ms>-<rand6>.md` files. An archive filename is a durable receipt that shadows and cleans restored inbox replicas and makes repeated archive cleanup idempotent. | `src/message.rs::filename_grammar`; `src/message.rs::archive_receipt_suppresses_and_idempotently_cleans_a_restored_inbox_copy`; `tests/message.rs` |
13+
| **Generic delayed-submit native DING** | Each post-start unread message becomes one normalized `[DING]` frame sent as one exact PTY sequence: literal notice bytes, a fixed 500 ms pause, then Return. DING does not inspect renderer or pane state and never sends Escape. Startup backlog becomes one generic recovery DING; new arrivals remain FIFO; archive receipts suppress re-DING; `busy` delivers immediately; only fresh `dnd` defers; failed sends retry in memory. | `src/ding.rs::poke_text_normalizes_and_bounds_untrusted_fields`; `src/ding.rs::malicious_controls_cannot_escape_the_single_paste_frame`; `src/ding.rs::pty_send_is_one_exact_delayed_sequence_without_inspection_or_escape`; `src/ding.rs::session_watch_has_startup_grace_debounce_and_live_reset`; `src/ding.rs::new_arrivals_is_fifo_and_archive_receipts_prevent_reding`; `src/ding.rs::pending_delivery_ignores_busy_but_respects_fresh_dnd_archive_and_retry`; `src/ding.rs::startup_recovery_notice_retries_in_memory`; `src/ding.rs::startup_backlog_gets_one_generic_recovery_then_new_arrivals_poke` |
14+
| **Agent-declared presence discipline** | The shipped bus contract requires agents to declare `busy` before executing work, use `available` only while yielding or ready, and reserve `dnd` for an explicit hold. Both native harnesses materialize that contract. Busy remains observable but does not suppress DING; fresh `dnd` is the only delivery gate. | `tests/compile_agent.rs::compile_agent_generates_claude_then_materializes_verbatim_persona`; `tests/compile_agent.rs::compile_agent_generates_codex_then_materializes_composed_agents_md`; `src/ding.rs::pending_delivery_ignores_busy_but_respects_fresh_dnd_archive_and_retry` |
1515
| **Stable roster JSON** | `st2 agents --json [--enrich]` preserves field names, order, null handling, presence, explicit retirement state, activity, and inbox counts. Human output marks retired declarations without changing active rows. | `src/agents.rs::agents_json_has_stable_wire_shape`; `tests/status_agents.rs::roster_json_and_human_output_distinguish_retirement_from_presence` |
16-
| **Agent-declared presence** | Refresh preserves the agent's declared status and only advances liveness; a missing status starts as `available`, while an unrefreshed declaration ages to `unknown`. | `src/status.rs::refresh_preserves_value_and_bumps_mtime`; `src/status.rs::refresh_missing_writes_available_default`; `src/status.rs::stale_mtime_reads_as_unknown_regardless_of_contents` |
16+
| **Agent-declared presence** | Refresh preserves non-DND declared status and only advances liveness; a missing status starts as `available`, while `dnd` is never refreshed and an unrefreshed declaration ages to `unknown`. | `src/status.rs::refresh_preserves_value_and_bumps_mtime`; `src/status.rs::refresh_leaves_dnd_to_age_out`; `src/status.rs::refresh_missing_writes_available_default`; `src/status.rs::stale_mtime_reads_as_unknown_regardless_of_contents` |
1717
| **Retirement health** | A retired declaration is healthy only after every declared task ID is absent. Any live or dead declared task record reports incomplete retirement; retired declarations do not require presence. Live declarations retain their existing task and presence checks. | `tests/doctor.rs::retired_declaration_is_healthy_when_tasks_and_presence_are_absent`; `tests/doctor.rs::retired_declaration_is_unhealthy_while_a_declared_task_is_alive`; `tests/doctor.rs::retired_declaration_is_unhealthy_while_a_dead_task_record_remains` |
1818
| **Crash loops surface** | A task parked by a fail-mode restart policy notifies its supervisor once over the bus. | `tests/run.rs::surface_crash_loop_notifies_the_supervisor_over_the_bus` |
1919
| **Tracked workspaces fail closed** | Materialization simulates content operations before writing and refuses a real change to any Git-tracked target. Byte-identical tracked, untracked, and non-Git targets retain useful behavior. | `tests/materialize.rs::every_content_directive_refuses_to_change_a_tracked_target_before_any_write`; `tests/materialize.rs::byte_identical_tracked_target_is_allowed_without_modification`; `tests/materialize.rs::untracked_and_non_git_targets_remain_materializable` |

README.md

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ During materialization, hook commands such as `$ST_HOOKS/codex-stop.sh` resolve
4747
immutable set, so rendered settings are versioned without embedding a machine-specific root in the
4848
declaration.
4949

50+
The hooks have a small operational purpose: session-start restores durable context and exposes the
51+
current inbox; pre-compact preserves a recovery breadcrumb when no context was written; stop and
52+
failure hooks surface newly arrived work or a harness failure. They fail open so hook trouble does
53+
not prevent the harness from starting or stopping.
54+
5055
## Author a native agent
5156

5257
Start from the maintained [Codex](examples/native/agent-codex.kdl) or
@@ -85,6 +90,24 @@ agent "<identity>" {
8590
}
8691
```
8792

93+
### Scheduled work is coming soon, not implemented
94+
95+
st2 does not parse or run scheduled entries today. The intended direction starts with a declarative
96+
DING targeted at the containing agent; scheduled PTY or exec work may be considered later. This is a
97+
non-functional preview, and `st2 validate` correctly rejects it today:
98+
99+
```kdl
100+
agent "<identity>" {
101+
// Current implemented fields and tasks go here.
102+
103+
// FUTURE ONLY — not valid current agent.kdl syntax.
104+
schedule "local-health" {
105+
every "2h"
106+
ding "Run the local health check."
107+
}
108+
}
109+
```
110+
88111
st2 provides `CATALOG`, flat native `ST_ROOT`, local `PTY_ROOT`, `ST_AGENT`, and `ST_HOOKS` to the
89112
task. Declarations should not contain machine-specific install paths.
90113

@@ -172,11 +195,13 @@ text, send one bracketed-paste sequence, wait 500 ms, then send Return in that s
172195
command. The fixed delay addresses observed paste settling, but st2 does not inspect the terminal
173196
and cannot guarantee modal safety; a modal that opens during the gap could receive Return. Agents
174197
must therefore declare `busy` before actively executing work and return to `available` only when
175-
yielding or ready for new work. `dnd` is an explicit hold. `busy` and `dnd` defer new arrivals in
176-
FIFO order, archive receipts suppress restored duplicates, and transport failures retain the head
177-
for retry. A restarted sidecar seeds but never replays the existing inbox; the boot ritual owns
178-
backlog draining. The generic path is covered for both maintained harness declarations, while live
179-
Claude delivery proof remains pending.
198+
yielding or ready for new work, but `busy` never suppresses DING. Fresh `dnd` is the only delivery
199+
hold. The sidecar does not refresh `dnd`, so an abandoned hold becomes stale after 15 minutes and
200+
delivery resumes. New arrivals remain FIFO, same-filename archive receipts shadow and clean restored
201+
inbox duplicates, and a failed PTY send retains the notice in memory for retry. On start or restart,
202+
the sidecar sends one generic check-inbox recovery DING if unread work remains; it does not replay a
203+
notice per backlog message. The generic path is covered for both maintained harness declarations,
204+
while live Claude delivery proof remains pending.
180205

181206
## Cleanup
182207

examples/native/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ selected immutable hook set; they never refresh shared scripts. Managed settings
3939

4040
Both maintained declarations load the shipped bus contract. Agents must declare `busy` before
4141
actively executing a unit of work and return to `available` only when yielding or ready for new
42-
work. `dnd` is an explicit hold, not an inferred terminal state. This declaration is the DING safety
43-
gate: st2 intentionally does not inspect either harness's terminal pixels.
42+
work. Busy agents still receive DING. `dnd` is the only delivery hold and the sidecar does not renew
43+
it, so an abandoned hold becomes stale after 15 minutes. st2 intentionally does not inspect either
44+
harness's terminal pixels.
4445

4546
The `render { ... }` block is ordered. `copy`, `file`, `json-upsert`, and `ensure-line` are
4647
boot-gating operations; a failure prevents that agent from starting. Materialization refuses any

src/agents.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ fn read_name(agent_dir: &Path) -> Option<String> {
110110
}
111111

112112
/// Count logically unread messages in the agent's `resources/inbox`. A same-filename archive receipt
113-
/// suppresses a raw inbox duplicate restored by eventually-consistent sync.
113+
/// suppresses and cleans a raw inbox duplicate restored by eventually-consistent sync.
114114
fn inbox_count(agent_dir: &Path) -> usize {
115115
message::list_inbox(&message::inbox_dir(agent_dir))
116116
.map(|msgs| msgs.len())

0 commit comments

Comments
 (0)