{"record":{"id":"45c349b569d09f7b","repo":"conductor-oss/conductor","slug":"pruneexecutions-olderthandays-must-be-1-got","errorCode":null,"errorMessage":"pruneExecutions: olderThanDays must be >= 1, got ${olderThanDays}","messagePattern":"pruneExecutions: olderThanDays must be >= 1, got (.+?)","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":400,"severity":"error","filePath":"agentspan/src/main/java/org/conductoross/conductor/ai/agentspan/runtime/service/AgentService.java","lineNumber":605,"sourceCode":"        workflowService.deleteWorkflow(executionId, archiveTasks);\n    }\n\n    /**\n     * Computes the prune cutoff, guarding the two ways an unchecked {@code olderThanDays} turned\n     * the prune into a data-loss operation (issue #1331): non-positive values put the cutoff in the\n     * future (matching every terminal execution), and very large values push the computed epoch\n     * negative, which the search backend matched against recent executions. A cutoff clamped to\n     * epoch start matches nothing, which is the correct meaning of \"older than anything that\n     * exists\".\n     *\n     * @param olderThanDays minimum age in days, must be >= 1\n     * @param now the current instant\n     * @return cutoff in epoch milliseconds, never negative\n     */\n    @VisibleForTesting\n    static long computePruneCutoffEpochMs(int olderThanDays, Instant now) {\n        if (olderThanDays < 1) {\n            throw new IllegalArgumentException(\n                    \"pruneExecutions: olderThanDays must be >= 1, got \" + olderThanDays);\n        }\n        return Math.max(0L, now.minus(olderThanDays, ChronoUnit.DAYS).toEpochMilli());\n    }\n\n    /**\n     * Bulk-delete completed execution records older than {@code olderThanDays} days.\n     *\n     * <p>Searches for COMPLETED, FAILED, TERMINATED, and TIMED_OUT executions whose end time is\n     * before the cutoff, then removes them from the DB in batches.\n     *\n     * @param olderThanDays minimum age in days for executions to be pruned\n     * @param archiveTasks if true, archive task records instead of deleting\n     * @return number of executions deleted\n     */\n    public int pruneExecutions(int olderThanDays, boolean archiveTasks) {\n        long cutoffEpochMs = computePruneCutoffEpochMs(olderThanDays, Instant.now());\n        String[] terminalStatuses = {\"COMPLETED\", \"FAILED\", \"TERMINATED\", \"TIMED_OUT\"};","sourceCodeStart":587,"sourceCodeEnd":623,"githubUrl":"https://github.com/conductor-oss/conductor/blob/cf7c3e4a8adfb158be778ab1ec525323c363cd3a/agentspan/src/main/java/org/conductoross/conductor/ai/agentspan/runtime/service/AgentService.java#L587-L623","documentation":"Thrown by AgentService.computePruneCutoffEpochMs when olderThanDays is less than 1. This guard exists because non-positive values would place the cutoff in the future, matching every terminal execution and causing mass data deletion (documented in issue #1331). The method computes now.minus(olderThanDays, DAYS) and clamps to >= 0. IllegalArgumentException maps to HTTP 400.","triggerScenarios":"Calling pruneExecutions(0, ...) or pruneExecutions(-1, ...); a CLI/API caller passes olderThanDays=0 intending 'delete everything now' not realizing the guard rejects it; a default value of 0 is used when the parameter is omitted.","commonSituations":"Operator misinterprets 0 as 'no minimum age'; configuration template has an unset default that resolves to 0; automated cleanup job receives a computed days value that underflows to 0 or negative.","solutions":["Pass olderThanDays >= 1 — use a sensible retention like 30 or 90.","If the intent is aggressive cleanup, use 1 (the minimum) rather than 0.","Validate the input at the CLI/API boundary before forwarding to pruneExecutions."],"exampleFix":"// before\nagentService.pruneExecutions(0, false); // -> error\n\n// after\nint days = Math.max(1, configuredRetentionDays);\nagentService.pruneExecutions(days, false);","handlingStrategy":"validation","validationCode":"if (olderThanDays < 1) {\n    throw new IllegalArgumentException(\n        \"Retention must be at least 1 day, got \" + olderThanDays);\n}\nagentService.pruneExecutions(olderThanDays, archiveTasks);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never pass 0 or negative days to pruneExecutions — this guard prevents accidental mass deletion.","Set a sensible default retention (30/60/90 days) in configuration.","Validate the value at the CLI/API boundary before forwarding."],"tags":["validation","prune","data-safety","bad-request"],"backgroundTag":null,"analyzedSha":"cf7c3e4a8adfb158be778ab1ec525323c363cd3a","analyzedAt":"2026-08-14T03:33:19.897Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}