Stale-state cleanup + 7-day threshold + smoke tests (additive to #16) #19

Closed
opened 2026-09-06 06:22:09 +00:00 by Mark · 3 comments
Owner

Origin

Decomposed from kanban t_e5ac8155 (auto-decomposer, 2026-09-05). Sibling of t_9b77a683 (the --resume / --fresh core implementation), both children of t_d5c5d159 (issue #16 root). Issue #16 + the spec at docs/state-file.md (branch docs/state-file-schema, commit 217cb08) cover the core resume + atomic-write + soft-staleness story, but this issue adds four concrete user-facing items that #16 does not capture.

Scope (additions to #16)

These four items are additive to the #16 implementation and are intended to land in the SAME PR if possible -- they're not parallel work, they're part of the complete resumable-chunk story.

1. Change default --stale-hours from 24 to 168 (= 7 days)

Spec §5.4 / §6 say --stale-hours defaults to 24 hours. The user-facing product decision is 7 days -- long-running transcription jobs (overnight batch runs, multi-day podcasts) should not warn after 24h, and a state file that hasn't been touched in a week is genuinely stale.

Update the default in:

  • The spec doc §5.4 + §6 (so it stays the source of truth)
  • The implementation's --stale-hours default in the CLI
  • Any test that pins the default value

2. Source-audio mtime newer than state mtime -> WARN + REQUIRE --fresh (not hard-reject)

Spec §5.1/§5.2 currently treats source-audio mtime change as a hard reject (StaleStateError). This is too aggressive -- users who legitimately re-edited or upgraded the source want a confirmation step, not a crash. New behavior:

  • When state['source_audio_mtime'] < source_audio.mtime_at_startup, emit a clear warning to stderr:

    warning: source audio is newer than transcribe.state.json
    (state recorded mtime 2026-09-01 12:00; current mtime 2026-09-05 09:14)
    Pass --fresh to re-transcribe, or --resume to ignore the mtime change.

  • With --resume: continue anyway (with the warning).

  • Without --resume: refuse to start, error class StaleStateError, exit code 2.

  • The --fresh flag still deletes and restarts.

This requires a small refactor of validate_state() to distinguish "hard reject" from "warning + gate" outcomes. Either return a result object instead of raising, or raise a distinct SourceMtimeChangedWarning exception that the CLI catches.

3. New --clean-stale flag

A bulk-cleanup subcommand / flag: list (and, with --yes, delete) all transcribe.state.json files older than --stale-hours (default 7 days per #1) found in the search paths.

CLI surface:

  • transcribe --clean-stale [--stale-hours N] [--workdir PATH ...] [--yes] [--dry-run]
  • Default search paths: cwd, plus any --workdir values passed. Recurses one level into subdirs.
  • Output (no --yes): a table of (path, age_hours, chunks_done, chunks_total) for each stale state file, then exit 0 without deleting.
  • With --yes: deletes transcribe.state.json and the matching transcribe.chunk_*.json files in each stale workdir. Prompts via stdin if --yes is not passed and stdin is a TTY; in non-TTY contexts, refuses to delete.
  • --dry-run: print the table, never delete, exit 0.

Failure modes:

  • Stale state file but the source audio is missing -> still delete (the user is doing cleanup; the source being gone is not a blocker).
  • Stale state file but in_progress chunk -> emit an additional warning that the chunk was never completed; delete anyway (user explicitly asked for --clean-stale).
  • Path is read-only -> skip with a warning per path, exit 0 with a summary at the end (skipped: 3 read-only).

4. tests/test_resume.py -- four-scenario smoke test

New test file (no test file by this name exists today -- verified via tests/conftest.py import surface). Four scenarios, all unit-level (no GPU, no network, no real WhisperX -- use the tmp_path fixture and a fake transcribe.state.json written directly):

(a) Interrupted run leaves state file. Simulate a chunk loop that writes transcribe.state.json with chunks[0..2].status == 'done' and crashes (raise KeyboardInterrupt) before chunks[3]. Assert the state file exists on disk, parses, and reflects the partial progress.

(b) Resume picks up correctly. Spawn a fresh chunk-loop run with the state file from (a) as input and assert: chunks[0..2] are NOT re-processed (mock the chunk processor and assert it was called only for chunks 3..N), the final state file has all chunks done, and no duplicate segments appear in the merged output.

(c) Modified source audio triggers stale warning. Write a state file with source_audio_mtime = T-1h, then bump the source audio's mtime to T. Run the resume entry point and assert: a warning matching source audio .* newer than transcribe.state.json appears on stderr, and the function exits non-zero (or, depending on the chosen implementation shape, returns a distinct error code) without --resume.

(d) --fresh clears state. Pre-populate a workdir with transcribe.state.json + three transcribe.chunk_*.json files. Run transcribe --fresh against it. Assert: all four files are deleted; the chunk processor is called for chunks 0..N from scratch; the new state file is fresh.

Acceptance: all four scenarios pass; warning messages are grep-friendly (exact regex in test) so future message rewording is intentional, not accidental.

Tied to

Acceptance (this issue)

  • Spec doc §5.4 + §6 updated: --stale-hours default is 168 (= 7 days).
  • Spec doc §5.1/§5.2 updated: source-mtime-newer-than-state is a soft warning, not a hard reject.
  • --clean-stale flag implemented per #3, with --dry-run default behavior and --yes opt-in for delete.
  • tests/test_resume.py exists and covers all four scenarios; pytest tests/test_resume.py -v is green.
  • The full pytest tests/ --ignore=tests/test_benchmark_gpu_sizing.py baseline still passes.
  • ruff check src tests is clean.
  • PR description cross-references both this issue and #16.

Routing

  • Label: feature-request + triage-approved (per kanban-worker discipline -- daemon webhook will pick up triage-approved and dispatch gsd-issue-fixer.py).
  • Suggested branch name: feat/stale-cleanup-and-tests (or whatever the in-flight #16 branch is named, with these additions appended).
  • This issue is intended to land in the SAME PR as #16 if the active kilo-gsd.py Plan phase (tmux kilo-gsd-whisper-transcribe-16, Phase 2/9 at filing time) can absorb the additions. If the Plan phase has already progressed past the spec-update step, file as a follow-up PR.

Kanban tracking

  • Source task: t_e5ac8155 (this work)
  • Sibling: t_9b77a683 (--resume/--fresh core; delegated to kilo-gsd.py)
  • Parent: t_d5c5d159 (issue #16 root)
## Origin Decomposed from kanban t_e5ac8155 (auto-decomposer, 2026-09-05). Sibling of t_9b77a683 (the --resume / --fresh core implementation), both children of t_d5c5d159 (issue #16 root). Issue #16 + the spec at docs/state-file.md (branch docs/state-file-schema, commit 217cb08) cover the core resume + atomic-write + soft-staleness story, but **this issue adds four concrete user-facing items that #16 does not capture.** ## Scope (additions to #16) These four items are additive to the #16 implementation and are intended to land in the SAME PR if possible -- they're not parallel work, they're part of the complete resumable-chunk story. ### 1. Change default `--stale-hours` from 24 to 168 (= 7 days) Spec §5.4 / §6 say `--stale-hours` defaults to 24 hours. The user-facing product decision is **7 days** -- long-running transcription jobs (overnight batch runs, multi-day podcasts) should not warn after 24h, and a state file that hasn't been touched in a week is genuinely stale. Update the default in: - The spec doc §5.4 + §6 (so it stays the source of truth) - The implementation's `--stale-hours` default in the CLI - Any test that pins the default value ### 2. Source-audio mtime newer than state mtime -> WARN + REQUIRE `--fresh` (not hard-reject) Spec §5.1/§5.2 currently treats source-audio mtime change as a hard reject (StaleStateError). This is too aggressive -- users who legitimately re-edited or upgraded the source want a confirmation step, not a crash. New behavior: - When `state['source_audio_mtime'] < source_audio.mtime_at_startup`, emit a clear warning to stderr: warning: source audio <path> is newer than transcribe.state.json (state recorded mtime 2026-09-01 12:00; current mtime 2026-09-05 09:14) Pass --fresh to re-transcribe, or --resume to ignore the mtime change. - With `--resume`: continue anyway (with the warning). - Without `--resume`: refuse to start, error class `StaleStateError`, exit code 2. - The `--fresh` flag still deletes and restarts. This requires a small refactor of `validate_state()` to distinguish "hard reject" from "warning + gate" outcomes. Either return a result object instead of raising, or raise a distinct `SourceMtimeChangedWarning` exception that the CLI catches. ### 3. New `--clean-stale` flag A bulk-cleanup subcommand / flag: list (and, with `--yes`, delete) all `transcribe.state.json` files older than `--stale-hours` (default 7 days per #1) found in the search paths. CLI surface: - `transcribe --clean-stale [--stale-hours N] [--workdir PATH ...] [--yes] [--dry-run]` - Default search paths: cwd, plus any `--workdir` values passed. Recurses one level into subdirs. - Output (no `--yes`): a table of `(path, age_hours, chunks_done, chunks_total)` for each stale state file, then exit 0 without deleting. - With `--yes`: deletes `transcribe.state.json` and the matching `transcribe.chunk_*.json` files in each stale workdir. Prompts via stdin if `--yes` is not passed and stdin is a TTY; in non-TTY contexts, refuses to delete. - `--dry-run`: print the table, never delete, exit 0. Failure modes: - Stale state file but the source audio is missing -> still delete (the user is doing cleanup; the source being gone is not a blocker). - Stale state file but `in_progress` chunk -> emit an additional warning that the chunk was never completed; delete anyway (user explicitly asked for --clean-stale). - Path is read-only -> skip with a warning per path, exit 0 with a summary at the end (`skipped: 3 read-only`). ### 4. tests/test_resume.py -- four-scenario smoke test New test file (no test file by this name exists today -- verified via tests/conftest.py import surface). Four scenarios, all unit-level (no GPU, no network, no real WhisperX -- use the `tmp_path` fixture and a fake `transcribe.state.json` written directly): (a) **Interrupted run leaves state file.** Simulate a chunk loop that writes `transcribe.state.json` with `chunks[0..2].status == 'done'` and crashes (raise `KeyboardInterrupt`) before `chunks[3]`. Assert the state file exists on disk, parses, and reflects the partial progress. (b) **Resume picks up correctly.** Spawn a fresh chunk-loop run with the state file from (a) as input and assert: `chunks[0..2]` are NOT re-processed (mock the chunk processor and assert it was called only for chunks 3..N), the final state file has all chunks `done`, and no duplicate segments appear in the merged output. (c) **Modified source audio triggers stale warning.** Write a state file with `source_audio_mtime = T-1h`, then bump the source audio's mtime to `T`. Run the resume entry point and assert: a warning matching `source audio .* newer than transcribe.state.json` appears on stderr, and the function exits non-zero (or, depending on the chosen implementation shape, returns a distinct error code) without `--resume`. (d) **`--fresh` clears state.** Pre-populate a workdir with `transcribe.state.json` + three `transcribe.chunk_*.json` files. Run `transcribe --fresh` against it. Assert: all four files are deleted; the chunk processor is called for chunks 0..N from scratch; the new state file is fresh. Acceptance: all four scenarios pass; warning messages are grep-friendly (exact regex in test) so future message rewording is intentional, not accidental. ## Tied to - Mark/whisper-transcribe#16 (parent feature -- Resume from checkpoint on long runs) - Mark/whisper-transcribe#8 (long-file streaming; integration coverage) - Spec doc: docs/state-file.md on branch docs/state-file-schema (commit 217cb08) -- items #1 and #2 require spec updates before the implementation lands. ## Acceptance (this issue) - [ ] Spec doc §5.4 + §6 updated: `--stale-hours` default is 168 (= 7 days). - [ ] Spec doc §5.1/§5.2 updated: source-mtime-newer-than-state is a soft warning, not a hard reject. - [ ] `--clean-stale` flag implemented per #3, with `--dry-run` default behavior and `--yes` opt-in for delete. - [ ] `tests/test_resume.py` exists and covers all four scenarios; `pytest tests/test_resume.py -v` is green. - [ ] The full `pytest tests/ --ignore=tests/test_benchmark_gpu_sizing.py` baseline still passes. - [ ] `ruff check src tests` is clean. - [ ] PR description cross-references both this issue and #16. ## Routing - Label: `feature-request` + `triage-approved` (per kanban-worker discipline -- daemon webhook will pick up `triage-approved` and dispatch `gsd-issue-fixer.py`). - Suggested branch name: `feat/stale-cleanup-and-tests` (or whatever the in-flight #16 branch is named, with these additions appended). - This issue is intended to land in the SAME PR as #16 if the active kilo-gsd.py Plan phase (tmux `kilo-gsd-whisper-transcribe-16`, Phase 2/9 at filing time) can absorb the additions. If the Plan phase has already progressed past the spec-update step, file as a follow-up PR. ## Kanban tracking - Source task: `t_e5ac8155` (this work) - Sibling: `t_9b77a683` (--resume/--fresh core; delegated to kilo-gsd.py) - Parent: `t_d5c5d159` (issue #16 root)
Author
Owner

Cross-linked from #16. Sibling issue (and the kanban subtask t_e5ac8155) intends for these additions to land in the same PR as #16 if the Plan phase of tmux kilo-gsd-whisper-transcribe-16 has not yet finalized (Phase 2/9 at filing time, 2026-09-06 06:22 PDT). Watcher profile on t_d5c5d159 will own the terminal transition.

Cross-linked from #16. Sibling issue (and the kanban subtask t_e5ac8155) intends for these additions to land in the same PR as #16 if the Plan phase of tmux kilo-gsd-whisper-transcribe-16 has not yet finalized (Phase 2/9 at filing time, 2026-09-06 06:22 PDT). Watcher profile on t_d5c5d159 will own the terminal transition.
Author
Owner

Cross-link note (kanban t_583cec8a, 2026-09-06 06:24 PDT): kilo-gsd-runner attempted to spawn kilo-gsd.py against this issue at 23:24 PDT, but the per-repo concurrency gate rejected the spawn because Mark/whisper-transcribe#16 still holds the lock (Phase 4/9 Implement, tmux kilo-gsd-whisper-transcribe-16, pid 134253).

Resolution path: file as a follow-up PR after #16 lands. The card body already names this as the expected outcome when the Plan phase has progressed past the spec-update step. Once the #16 PR merges (or its branch is pushed), a re-dispatch of t_583cec8a will spawn a fresh kilo-gsd.py against this issue and the four items (stale-hours default 168, source-mtime soft warn, --clean-stale, tests/test_resume.py) will land on a new branch atop the #16 commit.

No code work attempted from this run. kanban t_583cec8a blocked transient.

Cross-link note (kanban t_583cec8a, 2026-09-06 06:24 PDT): kilo-gsd-runner attempted to spawn kilo-gsd.py against this issue at 23:24 PDT, but the per-repo concurrency gate rejected the spawn because Mark/whisper-transcribe#16 still holds the lock (Phase 4/9 Implement, tmux kilo-gsd-whisper-transcribe-16, pid 134253). Resolution path: file as a follow-up PR after #16 lands. The card body already names this as the expected outcome when the Plan phase has progressed past the spec-update step. Once the #16 PR merges (or its branch is pushed), a re-dispatch of t_583cec8a will spawn a fresh kilo-gsd.py against this issue and the four items (stale-hours default 168, source-mtime soft warn, --clean-stale, tests/test_resume.py) will land on a new branch atop the #16 commit. No code work attempted from this run. kanban t_583cec8a blocked transient.
Author
Owner

Re-dispatched: kilo-gsd.py Mark/whisper-transcribe#19 launched from kanban t_583cec8a (re-run after #16 cleared the concurrency cap on origin/main, b95dfe3). Monitor session: kilo-monitor-whisper-transcribe-19; log at ~/.hermes/logs/kilo-monitor/whisper-transcribe-19.log. Follow-up PR track per card body: items #1 (stale-hours=168), #2 (source-mtime soft-warn), #3 (--clean-stale flag), #4 (tests/test_resume.py four-scenario smoke test). Watcher card: t_edbd396b.

Re-dispatched: kilo-gsd.py Mark/whisper-transcribe#19 launched from kanban t_583cec8a (re-run after #16 cleared the concurrency cap on origin/main, b95dfe3). Monitor session: `kilo-monitor-whisper-transcribe-19`; log at `~/.hermes/logs/kilo-monitor/whisper-transcribe-19.log`. Follow-up PR track per card body: items #1 (stale-hours=168), #2 (source-mtime soft-warn), #3 (--clean-stale flag), #4 (tests/test_resume.py four-scenario smoke test). Watcher card: t_edbd396b.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Mark/whisper-transcribe#19
No description provided.