Skip to content

Clean up terminal jobs and successful one-shot reminders - #1821

Merged
Aaronontheweb merged 10 commits into
devfrom
fix/background-job-cleanup
Aug 9, 2026
Merged

Clean up terminal jobs and successful one-shot reminders#1821
Aaronontheweb merged 10 commits into
devfrom
fix/background-job-cleanup

Conversation

@Aaronontheweb

@Aaronontheweb Aaronontheweb commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Delete terminal background job definitions and output logs after a 24-hour grace period.
  • Run the terminal job sweep during startup reconciliation and once each hour.
  • Delete a successful one-shot reminder definition and history after its acknowledgement succeeds.
  • Remove completed one-shot definitions and histories from prior versions during startup reconciliation.
  • Keep failed or ambiguous one-shot reminders visible for retry and diagnosis.

Scope

This change does not alter reminder storage, scheduler payloads, session ownership, delivery routes, or retry settlement.

Existing active reminders and background jobs keep their current paths.

The failed one-shot retention window remains separate work in #1820.

Validation

  • Netclaw.Actors.Tests: 2,921 passed.
  • The focused cleanup and failure-retention tests passed.
  • Slopwatch found zero issues.
  • The file header check passed.
  • git diff --check passed.

The eval suite did not run.

Addresses #1820.

Background job definitions and output logs were never deleted: the
BackgroundJobDefinitionStore.Delete method was dead code, so every
submitted job (shell_execute + _background:true) left a {jobId}.json and
output.log behind forever. This is the background-job half of #1820.

Add a terminal-job cleanup sweep:
- Terminal states (Completed/Failed/Cancelled/TimedOut/Lost/Reaped) whose
  CompletedAtMs is older than TerminalJobRetentionWindow (24h) have their
  definition AND output-log directory deleted.
- The window doubles as the delivery grace period: the completed-result
  message references the output-log path, and a slow polling session can
  still check_background_job within the window.
- Sweep runs once at startup reconciliation and on a self-scheduled
  hourly timer (canceled on PostStop), so the store stops growing during
  long-lived daemon runs.
- Active jobs are never touched (status guard + in-memory active set).

Store: add BackgroundJobDefinitionStore.DeleteJobArtifacts (definition
json + output directory, idempotent).

Tests: DeleteJobArtifacts removes both artifacts and is idempotent;
manager sweep deletes past-window jobs (definition + log), keeps
within-window jobs, and never touches Running/Pending jobs.
@Aaronontheweb Aaronontheweb added cleanup Code quality improvements and tech debt reduction reliability Retries, resilience, graceful degradation tools Issues related to agent tools: file_read, web_search, shell_execute, image processing, etc. labels Aug 8, 2026
@Aaronontheweb

Copy link
Copy Markdown
Collaborator Author

Fixes #1820 (background-job half).

Replace the manual Context.System.Scheduler.ScheduleTellRepeatedlyCancelable
+ ICancelable bookkeeping with the actor's injected ITimerScheduler. Timers
cancel automatically on stop, so the PostStop override goes away entirely.
Comment on lines +380 to +401
foreach (var def in _store.List())
{
if (!IsTerminalStatus(def.Status))
continue;

// Belt-and-braces: never sweep a job this actor still considers
// active, even if the on-disk snapshot is stale.
if (_activeJobIds.Contains(def.Id.Value))
continue;

if (def.CompletedAtMs is null || def.CompletedAtMs.Value > cutoffMs)
continue;

if (_store.DeleteJobArtifacts(def.Id))
{
_definitions.Remove(def.Id.Value);
swept++;
_log.Info(
"Swept terminal background job {JobId} (status={Status}, completed_at={CompletedAtMs}) past retention window",
def.Id, def.Status, def.CompletedAtMs);
}
}
Comment thread src/Netclaw.Actors/Jobs/BackgroundJobManagerActor.cs Fixed
Comment thread src/Netclaw.Actors/Jobs/BackgroundJobManagerActor.cs Fixed
Comment thread src/Netclaw.Actors/Jobs/BackgroundJobDefinitionStore.cs Fixed
- Mark TerminalJobRetentionWindow / TerminalSweepInterval static readonly
  (review: missed readonly opportunity).
- DeleteJobArtifacts: derive the output directory from the canonical
  GetOutputLogPathOnly path instead of a second Path.Combine, so the
  artifact directory always matches what the execution actor wrote
  (review: Path.Combine may drop earlier arguments).
Adversarial review findings on PR #1821:

HIGH-1 (directory traversal): Uri.EscapeDataString does not escape dots,
so a persisted definition with id "." or ".." resolved DeleteJobArtifacts
to the jobs directory or its parent and Directory.Delete(recursive) would
wipe it. Two layers of defense:
- Deserialize rejects unsafe ids (dot-only, path separators) so they never
  load into List()/Get().
- DeleteJobArtifacts canonicalizes the output dir and refuses to touch
  anything outside GetFullPath(jobs directory).

MED-1 (permanent leak): a terminal-status definition with null
CompletedAtMs was never swept. Fall back to StartedAtMs (always set at
submission) so that class still gets cleaned up; a recent StartedAtMs
keeps it in the retention window.

Tests: dot-only ids rejected at load (Theory . / ..); DeleteJobArtifacts
with id ".." leaves the parent untouched; null-CompletedAtMs terminal
swept via StartedAt fallback; null-CompletedAtMs with recent StartedAt
kept. 44 background-job tests green; slopwatch 0; headers clean.
var store = new BackgroundJobDefinitionStore(_paths);
var jobsDir = _paths.JobsDirectory;
var parentDir = Path.GetDirectoryName(jobsDir.TrimEnd(Path.DirectorySeparatorChar))!;
var sentinel = Path.Combine(parentDir, "sentinel-file.txt");

// File name mirrors what GetPath would produce for this id:
// Uri.EscapeDataString leaves dots unescaped, so the file is "..json".
var filePath = Path.Combine(_paths.JobsDirectory, $"{Uri.EscapeDataString(unsafeId)}.json");
Comment on lines +415 to +423
catch (Exception ex)
{
// Keep the definition so the next periodic sweep can retry the
// cleanup. One filesystem failure must not stop the manager or
// prevent cleanup of other terminal jobs.
_log.Warning(
"Failed to sweep terminal background job {JobId}; cleanup will retry: {Error}",
def.Id, ex.Message);
}
OriginChannelType = Netclaw.Actors.Channels.ChannelType.Slack
});

var victimPath = Path.Combine(_paths.JobsDirectory, $"{Uri.EscapeDataString(victimId.Value)}.json");
});

var victimPath = Path.Combine(_paths.JobsDirectory, $"{Uri.EscapeDataString(victimId.Value)}.json");
var aliasPath = Path.Combine(_paths.JobsDirectory, "stale-alias.json");
@Aaronontheweb

Copy link
Copy Markdown
Collaborator Author

Going to go with a different approach and have session-scoped reminders and background jobs store their state in the session directory and get cleaned up alongside sessions.

@Aaronontheweb
Aaronontheweb deleted the fix/background-job-cleanup branch August 8, 2026 23:15
@Aaronontheweb
Aaronontheweb restored the fix/background-job-cleanup branch August 9, 2026 01:59
@Aaronontheweb Aaronontheweb reopened this Aug 9, 2026
@Aaronontheweb Aaronontheweb changed the title fix(jobs): sweep terminal background jobs past retention window (#1820) Clean up terminal jobs and successful one-shot reminders Aug 9, 2026
@Aaronontheweb Aaronontheweb added the reminders Reminder scheduling, execution, and history label Aug 9, 2026
@Aaronontheweb
Aaronontheweb enabled auto-merge (squash) August 9, 2026 02:21
@Aaronontheweb
Aaronontheweb disabled auto-merge August 9, 2026 02:48
@Aaronontheweb
Aaronontheweb merged commit 1c5a8fc into dev Aug 9, 2026
21 checks passed
@Aaronontheweb
Aaronontheweb deleted the fix/background-job-cleanup branch August 9, 2026 02:48
Aaronontheweb added a commit that referenced this pull request Aug 20, 2026
* Sync delta specs for completed OpenSpec changes

Apply the delta specs of 16 completed changes to the main specs. Create four
new capability specs: daemon-shell-path, shell-policy-evaluator-architecture,
skillserver-native-sidecar-sync, and named-model-definitions.

Correct three reminder requirements against the merged code:

- One-shot success removes the definition and its history. Only a poisoned
  one-shot is soft-deleted (PR #1821).
- No execution capacity cap exists (PR #1839). The nack and ack-skip policy
  now covers a duplicate active occurrence and a short acknowledgement lease.
- Every delivery kind now holds its envelope. ReminderDeliveryResult replaces
  ReminderDeliveryObserved.

* Archive 16 completed OpenSpec changes

Move each completed change to openspec/changes/archive/2026-08-19-<name>/.
Their code is merged on dev.

Also tick task 5.3 of consolidate-binding-actor-engines. PR #2005 merged that
work.

Leave eval-run checkboxes unticked. An evals-only gap does not block the
archive.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cleanup Code quality improvements and tech debt reduction reliability Retries, resilience, graceful degradation reminders Reminder scheduling, execution, and history tools Issues related to agent tools: file_read, web_search, shell_execute, image processing, etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant