carlos / platform Public

zdd plan 3 task 10: fix round 1: a member's wake event reads as the claims refusal's cause, not its admission wrapper

startAdmitted wraps a refusal before the exec hook in errClaimsAdmission
whatever the flag, so a claims-on wake on the closed launch reached
members as "wake failed: the claims admission refused before the exec:
...". memberSafeWakeReason now removes that wrapper's text (alone or
inside doClaimWake's join), so the event is byte-identical to main's.
Every other mapping is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev f089cff93d8bdbd121aa10d5877cfe74f8692f09 parent 86e3070
3 files changed, +40 −0
  • internal/activator/logsink.go +6 −0
  • internal/activator/logsink_test.go +30 −0
  • internal/activator/swap.go +4 −0
diff --git a/internal/activator/logsink.go b/internal/activator/logsink.go
index 08ebaed60..e025bb6df 100644
--- a/internal/activator/logsink.go
+++ b/internal/activator/logsink.go
@@ -57,6 +57,12 @@ func memberSafeWakeReason(err error) string {
err = ErrFenced
}
s := err.Error()
+ if errors.Is(err, errClaimsAdmission) {
+ // claimAdmission's wrapper is the classifier's, not the member's
+ // (zero-downtime plan 3, Task 10): the event reads exactly as its
+ // cause did, alone or joined. Every other mapping is unchanged.
+ s = strings.ReplaceAll(s, claimAdmissionPrefix, "")
+ }
switch {
case strings.Contains(s, "address already in use"):
// The stale-socket wake trap (#129): every later wake fails until
diff --git a/internal/activator/logsink_test.go b/internal/activator/logsink_test.go
index e892e6f68..f1649cae0 100644
--- a/internal/activator/logsink_test.go
+++ b/internal/activator/logsink_test.go
@@ -147,3 +147,33 @@ func TestWakeOutcomesReachTheSink(t *testing.T) {
t.Fatalf("no woke event: %+v", sink2.events)
}
}
+
+// A claims-on wake on the closed launch fails before its exec hook, and
+// startAdmitted wraps that in errClaimsAdmission so the one classifier
+// reads it as the admission's (Task 10). The wrapper is internal: the
+// member's event renders exactly as the bare cause did on main, whatever
+// the flag, alone or joined with FailedAdmission's error as doClaimWake
+// returns it. No "claims admission" text reaches a member.
+func TestMemberSafeWakeReasonHidesTheClaimsAdmissionWrapper(t *testing.T) {
+ recovery := errors.New("recovery: release the admission")
+ for _, tc := range []struct {
+ name string
+ wrapped, bare error
+ }{
+ {"the closed launch", claimAdmission(ErrClaimIsolationUnavailable, false), ErrClaimIsolationUnavailable},
+ {"the closed launch, wrapped by the launcher", claimAdmission(fmt.Errorf("start main: %w", ErrClaimIsolationUnavailable), false),
+ fmt.Errorf("start main: %w", ErrClaimIsolationUnavailable)},
+ {"joined as doClaimWake returns it", errors.Join(claimAdmission(ErrClaimIsolationUnavailable, false), nil),
+ errors.Join(ErrClaimIsolationUnavailable, nil)},
+ {"joined with a recovery error", errors.Join(claimAdmission(ErrClaimIsolationUnavailable, false), recovery),
+ errors.Join(ErrClaimIsolationUnavailable, recovery)},
+ } {
+ got, want := memberSafeWakeReason(tc.wrapped), memberSafeWakeReason(tc.bare)
+ if got != want {
+ t.Errorf("%s: %q, want main's %q", tc.name, got, want)
+ }
+ if strings.Contains(got, "claims admission") {
+ t.Errorf("%s: the admission wrapper reached a member: %q", tc.name, got)
+ }
+ }
+}
diff --git a/internal/activator/swap.go b/internal/activator/swap.go
index 5dba4d72f..6431aa318 100644
--- a/internal/activator/swap.go
+++ b/internal/activator/swap.go
@@ -191,6 +191,10 @@ func verdictOf(err error) gateVerdict {
// failure of the build (spec §7.4).
var errClaimsAdmission = errors.New("the claims admission refused before the exec")
+// claimAdmissionPrefix is the text claimAdmission puts before its cause's.
+// memberSafeWakeReason removes it, so a member's event reads as the cause.
+var claimAdmissionPrefix = errClaimsAdmission.Error() + ": "
+
// claimAdmission wraps err in errClaimsAdmission when the spawn never
// reached its exec hook (reached false). An error after the hook ran is the
// spawn's own, and is returned as it is.