Skip to content

feat(awol): accountable-day count + days-AWOL severity report (#159)#162

Merged
SyniRon merged 6 commits into
developfrom
agent/issue-159
Jun 4, 2026
Merged

feat(awol): accountable-day count + days-AWOL severity report (#159)#162
SyniRon merged 6 commits into
developfrom
agent/issue-159

Conversation

@SyniRon

@SyniRon SyniRon commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Closes #159. Parent #157. Builds on #158 (LOA history store).

What changed

/awol now counts accountable days — raw inactivity minus valid LOA-covered dates — instead of flagging on raw time since last forum post, and renders the agreed days-AWOL severity report.

Pure calc (highest seam, exhaustively table-tested): utils.AccountableDaysAWOL(lastPost, now, windows) int — candidate UTC dates (lastPost, today] minus the union of inclusive [StartDate, EndDate] LOA ranges, overage past 7, clamped at 0. Backwards/zero ranges covered nothing and are DEBUG-logged. Companions: RawDaysAWOL, DaysSinceLastPost, ActiveWindow(windows, now).

Display: flag when days AWOL > 0, sorted days-AWOL desc (tie-break username). Glyphs: 🔴 >14 · 🟠 >7 · 🟡 >0 · ⚪ active-LOA override (with thread link, incl. the "on LOA, still AWOL" row). Summary N total · M LOA, footer AWOL days subtracts valid LOA days.

Degraded mode (IsHealthy false): raw whole-date inactivity, NO LOA subtraction, still flags >7, footer ⚠️ LOA cache unavailable — accountable-day adjustment SKIPPED; figures are raw inactivity (LOA NOT subtracted)., summary N total · LOA unknown. Applies to both embed and file-output paths; never a silent non-LOA all-clear.

Folded-in PR #161 re-review deferrals

Regression guard

No-LOA troopers render exactly as before (raw == accountable). /loa byte-identical (zero diff).

Gate

lint 0 · go mod tidy clean · go test ./... -race pass · floors OK (utils 89.8% ≥ 87, commands 83.0% ≥ 81) · build OK. Independently re-verified by orchestrator.

⚠️ Manual gate before merge

User-facing /awol rendering change — needs a test-guild smoke test (CI can't exercise the live Discord gateway).

🤖 Generated with Claude Code

SyniRon and others added 6 commits June 4, 2026 15:41
Implements #159 (parent PRD #157, ADR 0008). /awol now flags on accountable
days — UTC calendar dates in (lastPost, today] not covered by a valid LOA
window — instead of raw time since last forum post, and renders the agreed
days-AWOL severity report.

- New pure calc utils.AccountableDaysAWOL(lastPost, now, []LOAEntry) int:
  candidate set minus the union of inclusive [Start,End] LOA ranges, overage
  past the 7-day requirement. Overlap/adjacent/multiple/future windows fall out
  of set coverage; backwards ranges are ignored and DEBUG-logged. Exhaustively
  table-tested. Companion utils.RawDaysAWOL / utils.DaysSinceLastPost and a
  clock-injected utils.ActiveWindow selector.
- loaCacheReader gains GetEntries (drops GetEntry); diverges from /loa's
  loaCacheView, which stays {GetEntry, IsHealthy} and single-window (#161 #6).
- AwolUser models the LOA backing as *utils.LOAEntry (nil = none) instead of the
  zero-value LOAEntry + HasLOAEntry pair (#161 #4).
- Active-window selection and the accountable-day verdict both run against the
  handler's single injected now off one GetEntries read per user, so selection
  and verdict can't disagree at a boundary (#161 #2).
- Render: worst-first by days AWOL (tie-break username), severity glyphs
  (🔴 >14 · 🟠 >7 · 🟡 >0 · ⚪ active LOA), milpac + [LOA] thread links,
  "N total · M LOA" summary, "AWOL days subtracts valid LOA days." footer.
- Degraded mode (unhealthy cache): raw inactivity, NO LOA subtraction, loud
  "accountable-day adjustment SKIPPED; LOA NOT subtracted" warning on both embed
  and file paths; summary reads "LOA unknown", never a silent non-LOA all-clear.

/loa is unchanged. No-LOA troopers render raw == accountable as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… degraded empty result

Reserve the summary-line prefix + separator length when sizing the per-chunk
budget so the final embed description (summary + "\n\n" + chunk) can never exceed
Discord's 4096-char cap (was: raw chunk capped at 4096, then ~18 bytes of prefix
prepended → API 400).

Also surface the degraded/SKIPPED warning on the empty-result reply when the LOA
cache is unhealthy, so an unhealthy cache is never reported as a silent all-clear
(every other terminal degraded path already warns).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
validLOAWindows previously only dropped end-before-start ranges; a zero-value
LOAEntry (both dates zero) slipped through and would "cover" the epoch-zero UTC
date. Unreachable from production today (parseLOAPost only emits when both dates
parse) but cheap to harden. Also reword the coveredByLOA/validLOAWindows comments:
a zero-width range (StartDate == EndDate) is VALID and covers exactly one day —
only end-before-start covers nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… comments

IsActive had zero callers after this PR switched /awol to ActiveWindow/isActiveAt
(`rg IsActive` shows only its own def). Delete it and scrub its stale doc comment.
Correct loaCacheReader's doc (it is {GetEntries,IsHealthy}, a different surface
from /loa's {GetEntry,IsHealthy}, not a superset) and /loa's loaCacheView comment
that still claimed /awol's reader had the same {GetEntry,IsHealthy} shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…age gaps

New behavioral coverage:
- embed description stays <= 4096 with a near-limit accumulated chunk
- unhealthy cache + empty list surfaces the SKIPPED warning; healthy stays all-clear

New boundary/contract coverage:
- DaysSinceLastPost no-clamp contract vs RawDaysAWOL; rawAccountableDates early-return guard
- severityGlyph EXACT tier edges (14->orange, 7->yellow) + active-LOA override
- ActiveWindow now==StartDate inclusive edge and the no-active (_, false) return
- validLOAWindows drops a zero-value entry; mixed slice still subtracts the valid window

Also fix test fixture comment drift (expired-LOA EndDate, scratch glyph notes) and
the IsActive reference in the isActiveAt doc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework TestRunAwol_EmbedDescriptionWithinDiscordLimit so a chunk provably
lands in the danger band (4077, 4096]: 51 no-LOA rows render at exactly 80
bytes each, so the pre-fix budget (4096) packs all 51 into one chunk
(19-byte summary prefix + 4080 = 4099 > 4096) while the prefix-aware budget
(4096 - 19 = 4077) flushes after 50 rows (4019 <= 4096). The old ~139-byte
lines stepped over the band, so the test passed even against the un-fixed
budget. Assert on bytes (the conservative production measure) across every
emitted embed, and reference discordEmbedDescriptionLimit. Verified: PASS on
current code, FAIL (4099 bytes) against a reverted budget, PASS after restore.

Relabel TestAccountableDaysAWOL_MixedSliceValidStillSubtracts as a calc-level
smoke test: it does not pin the validLOAWindows end-before-start filter (that
branch is guarded by TestValidLOAWindows_MixedSliceKeepsValid); coveredByLOA's
range check already covers nothing for a backwards window.

No production logic changed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@SyniRon SyniRon merged commit 9b7fba8 into develop Jun 4, 2026
2 checks passed
@SyniRon SyniRon deleted the agent/issue-159 branch June 4, 2026 20:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

/awol: accountable-day count (subtract LOA) + days-AWOL severity report

1 participant