---
id: 10673
title: 'checkSunsetted detector contract: emit sunset vs idle_out_candidate signals'
state: CLOSED
labels:
  - enhancement
  - ai
  - architecture
assignees:
  - neo-opus-ada
createdAt: '2026-05-04T08:49:31Z'
updatedAt: '2026-06-07T17:23:46Z'
githubUrl: 'https://github.com/neomjs/neo/issues/10673'
author: neo-opus-ada
commentsCount: 0
parentIssue: 10671
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-05-04T13:07:23Z'
---
# checkSunsetted detector contract: emit sunset vs idle_out_candidate signals

## Context

Sub-issue of #10671. Today `checkSunsetted.mjs:124-127` emits `sunsetted: boolean` only on missing `WAKE_SUBSCRIPTION` (per #10641 fix). The two-mode recovery design needs a richer detector that distinguishes:

- **sunset** = no active subscription (terminal — needs full harness restart)
- **idle_out_candidate** = active subscription + stale memory (recoverable via in-place A2A nudge)

## The Problem

Current binary signal forces `resumeHarness` into one path (Cmd+N spawn). Two-mode design needs the detector to disambiguate so the right recovery primitive fires.

## The Architectural Reality

`checkSunsetted.mjs:36-44` queries `WAKE_SUBSCRIPTION`; lines 90-99 query last `AGENT_MEMORY` for staleness. Both signals are available; today only the `WAKE_SUBSCRIPTION` presence is consumed downstream.

**Critical discipline preserved from #10641:** staleness is NOT an authoritative sunset signal. Reintroduction here is as a separate, lower-authority `idle_out_candidate` signal. MUST NOT regress to `staleness=sunset`.

## The Fix

Detector emits structured payload:

```json
{
  "identity": "@neo-opus-ada",
  "sunset": true|false,
  "idle_out_candidate": true|false,
  "evidence": {
    "subscription_active": true|false,
    "subscription_status": "active|degraded|disabled|missing",
    "last_memory_age_min": N,
    "last_sessionId": "..."
  },
  "recommended_action": "sunset_restart" | "idle_out_nudge" | "no_action"
}
```

`resumeHarness.mjs` consumes `recommended_action` to fork into the right path.

## Acceptance Criteria

- [ ] Detector returns structured JSON with explicit evidence fields, not implicit boolean
- [ ] `sunset` signal preserves #10641's discipline: only fires on missing/disabled `WAKE_SUBSCRIPTION`, never on staleness alone
- [ ] `idle_out_candidate` signal fires only when `subscription_active: true` AND `last_memory_age_min` > threshold
- [ ] Unit test coverage for all 4 quadrants of (`sunset`, `idle_out_candidate`)
- [ ] Backward-compatible legacy field (`sunsetted: boolean`) emitted alongside new structured payload until callers migrate

## Out of Scope

- `resumeHarness` consumption logic — covered by sibling sub-issue (sunset-mode restart substrate)
- In-flight lock state in detector output — covered by separate lock-primitive sub-issue

## Related

- **Parent:** #10671
- **Builds on:** #10641 (staleness false-positive lesson preserved)

Origin Session ID: cce1fea5-32ff-410c-b820-2e9a27b3cd51

## Timeline

- 2026-05-04T08:49:33Z @neo-opus-ada added the `enhancement` label
- 2026-05-04T08:49:34Z @neo-opus-ada added the `ai` label
- 2026-05-04T08:49:34Z @neo-opus-ada added the `architecture` label
- 2026-05-04T08:52:01Z @neo-opus-ada added parent issue #10671
- 2026-05-04T08:53:24Z @neo-opus-ada cross-referenced by #10627
- 2026-05-04T08:57:22Z @neo-gpt cross-referenced by #10671
- 2026-05-04T11:50:23Z @neo-opus-ada assigned to @neo-opus-ada
- 2026-05-04T11:57:08Z @neo-opus-ada cross-referenced by PR #10689
- 2026-05-04T13:07:23Z @tobiu referenced in commit `23b2d79` - "feat(ai): two-mode detector contract for checkSunsetted (#10673) (#10689)

Evolves checkSunsetted.mjs to emit BOTH sunset AND idle_out_candidate
signals with explicit evidence fields, supporting the two-mode recovery
substrate from Epic #10671.

Output shape:
  {
    identity, sunset, idle_out_candidate,
    evidence: {
      subscription_active, subscription_status,
      last_memory_age_min, last_sessionId
    },
    recommended_action: 'sunset_restart' | 'idle_out_nudge' | 'no_action',

    // Backward-compat (AC5):
    sunsetted, reason, originSessionId, abandonedCount
  }

Signals are mutually exclusive by construction:
- sunset=true requires subscription_active=false
- idle_out_candidate=true requires subscription_active=true

In-flight lock integration: when a sunset_restart or idle_out_nudge
action is in flight (per inflightLock.mjs from #10683), the corresponding
signal downgrades to false — substrate is mid-recovery, no second
dispatch needed.

#10641 discipline preserved: subscription presence is the authoritative
sunset signal; staleness only surfaces as the lower-authority
idle_out_candidate "candidate in-place nudge."

IDLE_THRESHOLD_MS env var matches checkAllAgentIdle.mjs convention
(default 10 min) so downstream consumers see consistent idle semantics
across detectors.

Backward-compat: all 4 legacy fields (sunsetted/reason/originSessionId/
abandonedCount) preserved exactly as swarm-heartbeat.sh jq-parsing
requires. Verified via grep on consumer scripts.

Tests: 11 passed (5 existing + 6 new). New tests cover all 4 quadrants
of (sunset, idle_out_candidate), the (T,T)-impossible invariant via
8-scenario sweep, subscription_status disambiguation, in-flight lock
downgrade, and backward-compat field shape. Downstream regression check
on resumeHarness.spec.mjs / swarm-heartbeat.spec.mjs / inflightLock.spec.mjs:
21 passed / 2 skipped (live-host opt-in).

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-04T13:07:23Z @tobiu closed this issue
- 2026-05-04T13:33:08Z @neo-opus-ada cross-referenced by PR #10688
- 2026-05-05T18:05:03Z @neo-opus-ada cross-referenced by #10766
- 2026-05-05T19:52:01Z @neo-opus-ada cross-referenced by #10781
- 2026-05-07T13:23:18Z @neo-opus-ada cross-referenced by #10903

