monitor: widen stamp eligibility to watchable = named ∪ exported (soak sig 5)

The terminal record existed for watches that raced their target's death, but
e43c673 scoped its stamp to named tenancies — and the pid-identity watch
surface (§4 Slice 3) targets arbitrary actors, including anonymous ones whose
pids cross the boundary in contract replies. The first wild pid-face hit
(width-20 soak, pid_watch_test.exs:47, 1/600 full-suite: a monitor installed
while the child was alive delivered :noproc instead of {:smarm_exit, :panic})
is exactly the residual a0ba9be's commit body deferred.

ever_named becomes `watchable`, with a second set-site: mark_watchable(pid),
which the bridge calls wherever a smarm pid is encoded across the boundary —
BEAM can only watch pids it holds, and can only hold pids that crossed.
Anonymous never-exported churn (holder threads, egress tasks) stays
ineligible, preserving e43c673's LIFO-eviction protection unchanged.

mark_watchable takes the cold lock before the liveness screen: finalize
publishes Done and reads the bit under the same lock, so the mark either
lands before the death stamps or observes the tenancy dead and no-ops —
no lost-stamp window, and marking a corpse cannot invent history (pinned
in the test alongside the mark-while-alive stamp).
This commit is contained in:
smarm-agent
2026-08-13 05:56:19 +00:00
parent 461fe4b768
commit 1262cc30e3
5 changed files with 87 additions and 22 deletions
+32 -1
View File
@@ -17,7 +17,7 @@
//! caller's deliberate act, not a semantics change.
use smarm::{
init, request_stop, resolve_name, terminal_reason, CallError, Config, DownReason, GenServer,
init, mark_watchable, request_stop, resolve_name, terminal_reason, CallError, Config, DownReason, GenServer,
GenServerBuilder, GenServerName, NameResolution,
};
use std::sync::{Arc, Mutex};
@@ -59,6 +59,10 @@ impl GenServer for Filler {
struct Observed {
exit_reason: Option<DownReason>,
anon_reason: Option<DownReason>,
/// Anonymous but export-marked while alive — must stamp (sig 5).
marked_reason: Option<DownReason>,
/// Marked only after death — must remain unknowable.
marked_late_reason: Option<DownReason>,
panic_reason: Option<DownReason>,
stopped_reason: Option<DownReason>,
live_reason: Option<DownReason>,
@@ -126,6 +130,22 @@ fn terminal_record_recovers_the_reason_a_raced_watch_lost() {
let _ = h.join();
let anon_reason = terminal_reason(pid_anon);
// --- mark_watchable: the bridge's export-seam eligibility (sig 5).
// An anonymous actor marked while alive stamps like a named one ...
let h = smarm::spawn(|| loop {
smarm::sleep(Duration::from_millis(2));
});
let pid_marked = h.pid();
mark_watchable(pid_marked);
request_stop(pid_marked);
let _ = h.join();
let marked_reason = terminal_reason(pid_marked);
// ... while marking a pid whose tenancy already ended is a no-op:
// the history is honestly unknowable, not retroactively invented.
mark_watchable(pid_anon);
let marked_late_reason = terminal_reason(pid_anon);
// --- The named target: live readings first. -----------------------
let target = GenServerBuilder::new(Target)
.named(TARGET)
@@ -197,6 +217,8 @@ fn terminal_record_recovers_the_reason_a_raced_watch_lost() {
unknown_resolution,
corpse_resolution_matches,
resolution_after_prune,
marked_reason,
marked_late_reason,
corpse_reason_after_prune,
corpse_reason_after_reuse,
corpse_reason_after_tenant_death,
@@ -211,6 +233,15 @@ fn terminal_record_recovers_the_reason_a_raced_watch_lost() {
"anonymous deaths must not stamp: {o:?}"
);
assert_eq!(o.panic_reason, Some(DownReason::Panic), "{o:?}");
assert_eq!(
o.marked_reason,
Some(DownReason::Stopped),
"mark_watchable while alive must make the death stamp: {o:?}"
);
assert_eq!(
o.marked_late_reason, None,
"marking a dead tenancy must not invent history: {o:?}"
);
assert_eq!(o.stopped_reason, Some(DownReason::Stopped), "{o:?}");
assert_eq!(
o.live_reason, None,