R-717: after_setup open_command — the family window reopens an app's DB-held sign-up switch
The command form ran only when the lock was SET, so a database switch closed by
`command` stayed closed through the household's 15-minute window. New twin
fields `open_command` + `open_success` (same service/user/args_env and the same
argv-safe expansion and success-marker rules as `command`/`success`).
- liftNativeLock (the window, via OpenSignupWindow → goNativeLock(false)): marks
the gate record native_lock "opening" BEFORE anything opens, lifts the env,
runs open_command; only full success records "lifted". A failed open closes
the switch again at once and records after_setup {ok: false, step: open}; the
app page shows its own line (app_info.signup_native_open_failed).
- The close: reconcileSignupBlocks (every 20 s and at controller start) runs
`command` for any non-"applied" state once no window runs. A failed close
after a window is logged ERROR ("may still be OPEN past the household's
window") and retried every nativeLockOpenRetry (2 min) instead of 30.
- After a successful app update, verifyAndConclude → markNativeLockForReapply
sets native_lock "" so the loop closes the switch again.
- A template with `command` but no `open_command` keeps today's window (env
only) and logs once per app that its own switch cannot be reopened.
Tests: internal/stacks/after_setup_r717_test.go (7), web render + parity case.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -134,7 +134,9 @@ func (m *Manager) OpenSignupWindow(name string) (string, error) {
|
||||
if err := m.removeSignupBlockFile(name); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if st.Meta.AfterSetup != nil && len(st.Meta.AfterSetup.Env) > 0 { // v0.282.0: the app's own switch opens too (a restart)
|
||||
// v0.282.0: the app's own switch opens too (env: a restart). R-717: and the template's open_command; a template
|
||||
// that closes by command without one is logged once by liftNativeLock.
|
||||
if as := st.Meta.AfterSetup; as != nil && (len(as.Env) > 0 || len(as.OpenCommand) > 0 || len(as.Command) > 0) {
|
||||
m.goNativeLock(name, false, "the household's window")
|
||||
}
|
||||
m.logger.Printf("[INFO] [stacks] %s: the household opened sign-up until %s — the loop closes it again", name, until)
|
||||
@@ -188,7 +190,12 @@ func (m *Manager) reconcileSignupBlocks() {
|
||||
last := st.AppConfig.AfterSetup
|
||||
due := last == nil || last.OK
|
||||
if !due {
|
||||
if t, err := time.Parse(time.RFC3339, last.At); err != nil || m.now().Sub(t) > nativeLockRetry {
|
||||
// R-717: a switch the window opened (or may have) is retried sooner — sign-up may be open in the app.
|
||||
gap := nativeLockRetry
|
||||
if s := st.AppConfig.SetupGate.NativeLock; s == NativeLockLifted || s == NativeLockOpening {
|
||||
gap = nativeLockOpenRetry
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339, last.At); err != nil || m.now().Sub(t) > gap {
|
||||
due = true
|
||||
}
|
||||
}
|
||||
@@ -202,6 +209,9 @@ func (m *Manager) reconcileSignupBlocks() {
|
||||
// nativeLockRetry: how often the loop retries an app's own switch that could not be set.
|
||||
var nativeLockRetry = 30 * time.Minute
|
||||
|
||||
// nativeLockOpenRetry (R-717): how often the loop retries closing a switch the household's window opened.
|
||||
var nativeLockOpenRetry = 2 * time.Minute
|
||||
|
||||
// SetupGateProbe asks the app's own "setup done" status once (the household's button asks it first, Part A of the
|
||||
// 2026-09-29 afternoon brief). has=false: the template declares no probe.
|
||||
func (m *Manager) SetupGateProbe(name string) (has bool, done bool, got string, err error) {
|
||||
|
||||
Reference in New Issue
Block a user